[rooms,match] add a couple missing endpoints

This commit is contained in:
Devin Zuczek
2026-08-25 14:02:39 -04:00
parent 599730379c
commit 5cb2cbf967
8 changed files with 336 additions and 4 deletions
+9
View File
@@ -815,6 +815,15 @@ export const RoomExperiencePlayer = z
.array(z.unknown())
.describe('Always empty — no per-room experience is tracked')
/**
* `GET /rooms/curated_playlists` — the curated room playlists the discovery pages'
* playlist sections draw from. Nothing curates one on this server, so the list is always
* empty and the element shape is unknown until something fills it.
*/
export const CuratedPlaylists = z
.array(z.unknown())
.describe('Always empty — nothing curates a room playlist yet')
/**
* `GET /publishState/configs` — the limits the client enforces on republishing a room:
* how many updates are allowed in the rolling window, and the cooldown/expiry around
+54
View File
@@ -34,6 +34,7 @@ import {
getSubRoomPermissions,
getSubRoomSaveById,
getSubRoomSaves,
getTrendingRooms,
getVisitedRooms,
isPlayerBannedFromRoom,
modifySubRoom,
@@ -76,6 +77,7 @@ import {
CloneRoomRequest,
CloningRequest,
CreateSubRoomRequest,
CuratedPlaylists,
DescriptionRequest,
DormRoomId,
FeaturedRoomGroupDto,
@@ -864,6 +866,58 @@ const app = new Hono<App>()
}
)
// Curated room playlists — the editorially grouped room lists the discovery pages'
// `PlaylistById` sections draw from. Nothing curates one yet, so this is an empty array:
// the client reads that as "no playlists" and simply draws no playlist rows, where a 404
// leaves it retrying a feed that isn't coming.
.get(
'/rooms/curated_playlists',
describeRoute({
tags: ['Discovery'],
summary: 'Curated room playlists',
description: [
'The curated room playlists the discovery pages playlist sections draw from. There',
'is no editorial curation on this server yet, so this is always an empty array —',
'which the client reads as “no playlists” and draws nothing, rather than the 404 it',
'would keep retrying.',
].join(' '),
responses: { 200: json(CuratedPlaylists, 'Always an empty list') },
}),
(c) => c.json([])
)
// The `rising` carousel — the discovery pages fill a `CarouselEndpoint` section by
// slug, and this is the one the client asks for by name. Trending means someone is IN
// the room right now: unlike the hot feed, which ranks by head-count but still lists
// the empty rooms underneath, this one FILTERS on live presence, so a quiet server
// serves an empty carousel rather than a stale one.
//
// Paged like the hot feed (`skip`/`take`, take defaults to 100) and answers the same
// `{ Results, TotalResults }` envelope its sibling feeds do. Only `rising` is served —
// the other slugs in the discovery catalogue (`foryou`, `staffpicks`, the
// `*_algoendpoint` rows) keep 404ing until each is given a feed of its own.
.get(
'/rooms/carousel/rising',
describeRoute({
tags: ['Discovery'],
summary: 'The “rising” rooms carousel',
description: [
'The rooms players are in RIGHT NOW, busiest first — the trending carousel. Live',
'presence is a filter here, not just a sort: a room nobody is standing in is absent',
'entirely, so this is empty when the server is quiet rather than falling back to',
'stored engagement the way `/rooms/hot` does. Ties break on engagement and then',
'RoomId, so equally busy rooms page stably. Public, non-dorm, listable rooms only.',
].join(' '),
parameters: pageParams(100),
responses: { 200: json(PagedRooms, 'The carousel page') },
}),
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 getTrendingRooms(c.env.DB, skip, take))
}
)
// "Base" rooms — template rooms (tagged `base`) the client offers when creating
// a room. Returned regardless of accessibility. Paginated via skip/take (take
// defaults to 100). Returns a bare array.
@@ -890,6 +890,68 @@ describe('rooms endpoints', () => {
).run()
})
it('GET /rooms/curated_playlists is an empty list, not a 404', async () => {
const res = await SELF.fetch(`${ORIGIN}/rooms/curated_playlists`)
expect(res.status).toBe(200)
expect(await res.json()).toEqual([])
})
it('GET /rooms/carousel/rising serves only rooms players are in, busiest first', async () => {
const rising = async (qs = '?skip=0&take=100') =>
(await (await SELF.fetch(`${ORIGIN}/rooms/carousel/rising${qs}`)).json()) as {
Results: Array<{ RoomId: number; IsDorm?: boolean }>
TotalResults: number
}
// Nobody is anywhere in the fixture, and an empty carousel is the honest answer —
// this feed does NOT fall back to engagement the way /rooms/hot does.
expect(await rising()).toEqual({ Results: [], TotalResults: 0 })
// Pick from the tail of the hot feed so the order below can only come from presence.
const hot = (
(await (await SELF.fetch(`${ORIGIN}/rooms/hot?skip=0&take=100`)).json()) as {
Results: Array<{ RoomId: number }>
}
).Results.map((r) => r.RoomId)
const busiest = hot[hot.length - 1]
const quieter = hot[hot.length - 2]
const expiresAt = Math.floor(Date.now() / 1000) + 900
const seed = env.DB.prepare('INSERT OR REPLACE INTO presence (data) VALUES (?1)')
await env.DB.batch(
[
{ accountId: 90101, roomInstance: { roomInstanceId: 1001001, roomId: busiest } },
{ accountId: 90102, roomInstance: { roomInstanceId: 1001002, roomId: busiest } },
{ accountId: 90103, roomInstance: { roomInstanceId: 1001003, roomId: quieter } },
// The dorm nobody may list, and a lobby presence in no room at all: neither
// puts a room in the carousel.
{ accountId: 90104, roomInstance: { roomInstanceId: 1001004, roomId: 1 } },
{ accountId: 90105, roomInstance: null },
].map((p) => seed.bind(JSON.stringify({ ...p, expiresAt })))
)
const busy = await rising()
expect(busy.Results.map((r) => r.RoomId)).toEqual([busiest, quieter])
expect(busy.TotalResults).toBe(2)
expect(busy.Results.some((r) => r.RoomId === 1 || r.IsDorm === true)).toBe(false)
// Paged like its sibling feeds: TotalResults stays the full count.
expect(await rising('?skip=0&take=1')).toMatchObject({ TotalResults: 2 })
expect((await rising('?skip=0&take=1')).Results.map((r) => r.RoomId)).toEqual([busiest])
expect((await rising('?skip=1&take=100')).Results.map((r) => r.RoomId)).toEqual([quieter])
// Presence that has expired is nobody standing there.
await env.DB.prepare(
`UPDATE presence SET data = json_set(data, '$.expiresAt', ?1)
WHERE account_id BETWEEN 90101 AND 90105`
)
.bind(Math.floor(Date.now() / 1000) - 1)
.run()
expect(await rising()).toEqual({ Results: [], TotalResults: 0 })
await env.DB.prepare('DELETE FROM presence WHERE account_id BETWEEN 90101 AND 90105').run()
})
it('GET /rooms/hot aliases #recroomoriginal to the rro tag', async () => {
const aliased = (await (
await SELF.fetch(`${ORIGIN}/rooms/hot?tag=recroomoriginal`)
@@ -3746,8 +3808,10 @@ describe('rooms endpoints', () => {
'GET /rooms/autocomplete_search',
'GET /rooms/base',
'GET /rooms/bulk',
'GET /rooms/carousel/rising',
'GET /rooms/contributedby/me',
'GET /rooms/createdby/me',
'GET /rooms/curated_playlists',
'GET /rooms/favoritedby/me',
'GET /rooms/hot',
'GET /rooms/ownedby/me',