more image endpoints, rooms

This commit is contained in:
Devin Zuczek
2026-07-05 16:29:56 -04:00
parent 0e8bf114e0
commit bc7e4d718c
6 changed files with 298 additions and 1 deletions
+21
View File
@@ -366,6 +366,27 @@ export async function getHotRooms(
return { Results: rooms.slice(skip, skip + take), TotalResults: rooms.length }
}
/**
* Recommended rooms feed: public, non-dorm rooms not excluded from lists, ranked
* by engagement (same score as the hot feed). Unlike the hot feed this returns a
* bare array — the client's recommendation room-source loader expects a plain
* list, like the other `*by/me`/base sources. The `splitTest*` A/B params the
* client passes don't change the result. Paginated via skip/take; the dataset is
* small, so this filters/sorts in memory rather than in SQL.
*/
export async function getRecommendedRooms(
db: D1Database,
skip: number,
take: number
): Promise<Room[]> {
const { results } = await db.prepare('SELECT data FROM rooms').all<RoomRow>()
const roomId = (r: Room): number => (typeof r.RoomId === 'number' ? r.RoomId : 0)
return parseAll(results)
.filter((r) => r.IsDorm !== true && r.Accessibility === 1 && r.ExcludeFromLists !== true)
.sort((a, b) => hotScore(b) - hotScore(a) || roomId(a) - roomId(b))
.slice(skip, skip + take)
}
/**
* Rooms similar to a target room: public, non-dorm rooms (excluding the target)
* that share at least one tag with it, ranked by shared-tag count then
+11
View File
@@ -11,6 +11,7 @@ import {
getHotRooms,
getInteraction,
getPublicRoomsByCreator,
getRecommendedRooms,
getRoomById,
getRoomByName,
getRoomsByCreator,
@@ -185,6 +186,16 @@ const app = new Hono<App>()
return c.json(await getBaseRooms(c.env.DB, skip, take))
})
// Recommended rooms feed — public, non-dorm rooms ranked by engagement, returned
// as a bare array (the client's recommendation room-source expects a plain list).
// The `splitTestId`/`splitTestValue` A/B params are accepted and ignored.
// Paginated via skip/take (take defaults to 100).
.get('/rooms/recommendations', async (c) => {
const skip = Number.parseInt(c.req.query('skip') ?? '0', 10) || 0
const take = Number.parseInt(c.req.query('take') ?? '100', 10) || 100
return c.json(await getRecommendedRooms(c.env.DB, skip, take))
})
// Bulk room lookup by `id` or `name` — returns an array of matched rooms (the
// client calls this bare on the rooms host). Rooms not in D1 are simply absent
// from the result; the client treats an empty result as NoSuchRoom.
@@ -316,6 +316,30 @@ describe('rooms endpoints', () => {
expect(body.length).toBeLessThanOrEqual(5)
})
it('GET /rooms/recommendations returns a bare array of public rooms (split-test params ignored)', async () => {
const res = await SELF.fetch(
`${ORIGIN}/rooms/recommendations?splitTestId=1&splitTestValue=5`
)
expect(res.status).toBe(200)
const body = (await res.json()) as Array<{ RoomId: number; IsDorm?: boolean }>
expect(Array.isArray(body)).toBe(true)
expect(body.length).toBeGreaterThan(0)
// The dorm (RoomId 1) is non-public, so it's never recommended.
expect(body.some((r) => r.RoomId === 1 || r.IsDorm === true)).toBe(false)
// The split-test params don't change the result.
const plain = (await (
await SELF.fetch(`${ORIGIN}/rooms/recommendations`)
).json()) as Array<{ RoomId: number }>
expect(plain.map((r) => r.RoomId)).toEqual(body.map((r) => r.RoomId))
})
it('GET /rooms/recommendations respects take pagination', async () => {
const res = await SELF.fetch(`${ORIGIN}/rooms/recommendations?skip=0&take=3`)
const body = (await res.json()) as unknown[]
expect(body.length).toBeLessThanOrEqual(3)
})
it('GET /rooms/:id/similar returns { Results, TotalResults } of tag-sharing rooms (excluding self)', async () => {
const res = await SELF.fetch(`${ORIGIN}/rooms/2/similar`)
expect(res.status).toBe(200)