From 796442e0cf94637f5cc102c3fc2248ccd6e9d067 Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Sun, 14 Jun 2026 22:37:06 -0400 Subject: [PATCH] addd a few more endpoints --- apps/api/src/api.app.ts | 34 ++++++++- apps/api/src/test/integration/api.test.ts | 22 ++++++ apps/econ/src/econ.app.ts | 2 + apps/econ/src/test/integration/api.test.ts | 6 ++ apps/match/src/match.app.ts | 4 ++ apps/match/src/test/integration/api.test.ts | 5 ++ apps/rooms/src/rooms.app.ts | 79 ++++++++++++++------- apps/rooms/src/test/integration/api.test.ts | 17 +++++ 8 files changed, 141 insertions(+), 28 deletions(-) diff --git a/apps/api/src/api.app.ts b/apps/api/src/api.app.ts index bcbfaa1..1efef41 100644 --- a/apps/api/src/api.app.ts +++ b/apps/api/src/api.app.ts @@ -386,6 +386,15 @@ const app = new Hono({ strict: false }) }) .post('/api/sanitize/v1/isPure', (c) => c.json({ IsPure: true })) + // Keepsakes (room mementos). Shapes from the 2025 reference; categories isn't + // in any reference, so it's stubbed empty. The client fetches these on room + // entry — a 404 stalls the load. + .get('/api/keepsakes/globalconfig', (c) => + c.json({ KeepsakeFeatureEnabled: true, KeepsakeRoomLimit: 10, SocialXpBoostEnabled: false }) + ) + .get('/api/keepsakes/rooms/:roomId', (c) => c.body(null, 204)) + .get('/api/keepsakes/categories', (c) => c.json([])) + // ---- Player reporting ----------------------------------------------------- .get('/api/PlayerReporting/v1/moderationBlockDetails', (c) => c.json({ @@ -489,7 +498,30 @@ const app = new Hono({ strict: false }) }) // ---- Rooms ---------------------------------------------------------------- - .get('/api/rooms/v1/filters', (c) => c.json([])) // TODO: hydrate from JSON/roomfilters.json + // Room search filters. The client deserializes this into an object (not an + // array) — shape from the 2025 reference. + .get('/api/rooms/v1/filters', (c) => + c.json({ + PinnedFilters: [ + 'recroomoriginal', + 'community', + 'featured', + 'quest', + 'pvp', + 'hangout', + 'game', + 'art', + 'store', + 'tutorial', + 'fandom', + 'performance', + 'action', + 'horror', + ], + PopularFilters: ['pvp', 'quest', 'game', 'hangout', 'art'], + TrendingFilters: ['roleplay', 'nomp', 'rp', 'casual', 'fun', 'action', 'military', 'sports'], + }) + ) // ---- Room server ---------------------------------------------------------- // Register specific paths before the `/:id` param route. diff --git a/apps/api/src/test/integration/api.test.ts b/apps/api/src/test/integration/api.test.ts index d36e1b3..e1aee73 100644 --- a/apps/api/src/test/integration/api.test.ts +++ b/apps/api/src/test/integration/api.test.ts @@ -148,6 +148,28 @@ describe('public endpoints', () => { expect(await res.json()).toBe(true) }) + test('GET /api/rooms/v1/filters returns an object with filter arrays', async () => { + const res = await exports.default.fetch(`${ORIGIN}/api/rooms/v1/filters`) + expect(res.status).toBe(200) + const body = (await res.json()) as { PinnedFilters: string[]; PopularFilters: string[] } + expect(Array.isArray(body.PinnedFilters)).toBe(true) + expect(Array.isArray(body.PopularFilters)).toBe(true) + }) + + test('GET /api/keepsakes/globalconfig returns the keepsake config', async () => { + const res = await exports.default.fetch(`${ORIGIN}/api/keepsakes/globalconfig`) + expect(res.status).toBe(200) + expect(await res.json()).toMatchObject({ KeepsakeFeatureEnabled: true }) + }) + + test('GET /api/keepsakes/rooms/:id returns 204; categories returns []', async () => { + const room = await exports.default.fetch(`${ORIGIN}/api/keepsakes/rooms/1`) + expect(room.status).toBe(204) + const cats = await exports.default.fetch(`${ORIGIN}/api/keepsakes/categories`) + expect(cats.status).toBe(200) + expect(await cats.json()).toEqual([]) + }) + test('GET /voice/config returns an object', async () => { const res = await exports.default.fetch(`${ORIGIN}/voice/config`) expect(res.status).toBe(200) diff --git a/apps/econ/src/econ.app.ts b/apps/econ/src/econ.app.ts index 115a37b..43fb568 100644 --- a/apps/econ/src/econ.app.ts +++ b/apps/econ/src/econ.app.ts @@ -165,6 +165,8 @@ const app = new Hono() // The player's room keys. The C# returns "[]". .get('/api/roomkeys/v1/mine', (c) => c.json([])) + // Room keys for a given room (client calls this on the econ host). [] with no DB. + .get('/api/roomkeys/v1/room', (c) => c.json([])) // Subscription lookup. The C# returns both fields null with no auth. .post('/api/CampusCard/v1/UpdateAndGetSubscription', (c) => diff --git a/apps/econ/src/test/integration/api.test.ts b/apps/econ/src/test/integration/api.test.ts index 0b465ee..9436e91 100644 --- a/apps/econ/src/test/integration/api.test.ts +++ b/apps/econ/src/test/integration/api.test.ts @@ -146,6 +146,12 @@ describe('econ endpoints', () => { expect(await res.json()).toEqual([]) }) + test('GET /api/roomkeys/v1/room returns []', async () => { + const res = await exports.default.fetch(`${ORIGIN}/api/roomkeys/v1/room?roomId=1`) + expect(res.status).toBe(200) + expect(await res.json()).toEqual([]) + }) + test('GET /api/roomconsumables/v1/roomConsumable/room/:id/me returns []', async () => { const res = await exports.default.fetch( `${ORIGIN}/api/roomconsumables/v1/roomConsumable/room/1/me` diff --git a/apps/match/src/match.app.ts b/apps/match/src/match.app.ts index 6d9e21d..521ee62 100644 --- a/apps/match/src/match.app.ts +++ b/apps/match/src/match.app.ts @@ -349,6 +349,10 @@ const app = new Hono() return c.json({ errorCode: 0, roomInstance: instance }) }) + // Region ping reports — accept-and-ack (the reference returns Ok()). + .put('/player/photonregionpings', (c) => c.body(null, 200)) + .put('/player/gameserverregionpings', (c) => c.body(null, 200)) + // ---- Room instance ------------------------------------------------------- .post('/roominstance/:id/reportjoinresult', (c) => c.body(null, 200)) diff --git a/apps/match/src/test/integration/api.test.ts b/apps/match/src/test/integration/api.test.ts index 3734109..f62fe46 100644 --- a/apps/match/src/test/integration/api.test.ts +++ b/apps/match/src/test/integration/api.test.ts @@ -118,6 +118,11 @@ describe('public endpoints', () => { expect(res.status).toBe(200) }) + test('PUT /player/photonregionpings returns 200', async () => { + const res = await exports.default.fetch(`${ORIGIN}/player/photonregionpings`, { method: 'PUT' }) + expect(res.status).toBe(200) + }) + test('POST /roominstance/:id/reportjoinresult returns 200', async () => { const res = await exports.default.fetch(`${ORIGIN}/roominstance/5/reportjoinresult`, { method: 'POST', diff --git a/apps/rooms/src/rooms.app.ts b/apps/rooms/src/rooms.app.ts index 9b36baf..984a0b3 100644 --- a/apps/rooms/src/rooms.app.ts +++ b/apps/rooms/src/rooms.app.ts @@ -24,6 +24,34 @@ import type { App } from './context' /** Unity scene id for the dorm (also the matchmake/heartbeat instance location). */ const DORM_SCENE_ID = '76d98498-60a1-430c-ab76-b54a29b7a163' +/** Room permissions + (empty) Photon token the client needs to spawn into a room. */ +function photonAccessToken() { + const perm = (Permission: string, Role: number, Override: boolean) => ({ + Override, + Permission, + Role, + Type: 0, + Value: 'True', + }) + return { + Permissions: [ + perm('CAN_USE_ROOM_RESET_BUTTON', 0, true), + perm('CAN_USE_DELETE_ALL_BUTTON', 0, true), + perm('CAN_SAVE_INVENTIONS', 0, true), + perm('CAN_SPAWN_INVENTIONS', 0, true), + perm('CAN_USE_PLAY_GIZMOS_TOGGLE', 0, true), + perm('CAN_USE_MAKER_PEN', 30, false), + perm('CAN_USE_ROOM_RESET_BUTTON', 30, true), + perm('CAN_USE_DELETE_ALL_BUTTON', 30, true), + perm('CAN_SAVE_INVENTIONS', 30, true), + perm('CAN_SPAWN_INVENTIONS', 30, true), + perm('CAN_USE_PLAY_GIZMOS_TOGGLE', 30, true), + ], + PhotonAccessToken: '', + RoomInstanceId: 1, + } +} + function buildRoomResponse(roomId: number) { const isDorm = roomId === 1 return { @@ -113,6 +141,26 @@ const app = new Hono() return c.json(buildRoomResponse(1)) }) + // Bulk room lookup by `id` (synthesized per id) or `name`. The client calls + // this bare on the rooms host. We have no named-room data, so a name lookup + // returns [] — the client treats that as NoSuchRoom (a non-fatal warning), + // which is the honest answer since we can't actually host that room. + .get('/rooms/bulk', (c) => { + const idParam = c.req.query('id') + const nameParam = c.req.query('name') + if (!idParam && !nameParam) { + return c.json("Either 'id' or 'name' query parameter is required", 400) + } + if (idParam) { + const ids = idParam + .split(',') + .map((s) => Number.parseInt(s.trim(), 10)) + .filter((n) => !Number.isNaN(n)) + return c.json(ids.map(buildRoomResponse)) + } + return c.json([]) + }) + // Single room by id. The C# 404s when the row is missing; with no DB we // synthesize the room so the client can load it (ignores the include/ // unityAsset* query params, same as the C#). @@ -127,32 +175,9 @@ const app = new Hono() // Photon access token + room permissions the client needs to spawn into a // room. Without it the player is stuck on a black screen. PhotonAccessToken is // empty (the client uses its baked-in Photon credentials); roomInstanceId is - // our constant 1. - .get('/roomserver/photon_access_token', (c) => { - const perm = (Permission: string, Role: number, Override: boolean) => ({ - Override, - Permission, - Role, - Type: 0, - Value: 'True', - }) - return c.json({ - Permissions: [ - perm('CAN_USE_ROOM_RESET_BUTTON', 0, true), - perm('CAN_USE_DELETE_ALL_BUTTON', 0, true), - perm('CAN_SAVE_INVENTIONS', 0, true), - perm('CAN_SPAWN_INVENTIONS', 0, true), - perm('CAN_USE_PLAY_GIZMOS_TOGGLE', 0, true), - perm('CAN_USE_MAKER_PEN', 30, false), - perm('CAN_USE_ROOM_RESET_BUTTON', 30, true), - perm('CAN_USE_DELETE_ALL_BUTTON', 30, true), - perm('CAN_SAVE_INVENTIONS', 30, true), - perm('CAN_SPAWN_INVENTIONS', 30, true), - perm('CAN_USE_PLAY_GIZMOS_TOGGLE', 30, true), - ], - PhotonAccessToken: '', - RoomInstanceId: 1, - }) - }) + // our constant 1. The client calls it on the rooms host both bare and under + // `/roomserver`, so both are registered. + .get('/photon_access_token', (c) => c.json(photonAccessToken())) + .get('/roomserver/photon_access_token', (c) => c.json(photonAccessToken())) export default app diff --git a/apps/rooms/src/test/integration/api.test.ts b/apps/rooms/src/test/integration/api.test.ts index fcef5da..a11bd22 100644 --- a/apps/rooms/src/test/integration/api.test.ts +++ b/apps/rooms/src/test/integration/api.test.ts @@ -55,6 +55,23 @@ describe('rooms endpoints', () => { expect(body.RoomInstanceId).toBe(1) }) + it('GET /rooms/bulk?id= returns an array; ?name= returns []', async () => { + const byId = await SELF.fetch(`${ORIGIN}/rooms/bulk?id=1,2`) + expect(byId.status).toBe(200) + expect(((await byId.json()) as unknown[]).length).toBe(2) + const byName = await SELF.fetch(`${ORIGIN}/rooms/bulk?name=RecCenter`) + expect(byName.status).toBe(200) + expect(await byName.json()).toEqual([]) + }) + + it('GET /photon_access_token (bare) also returns permissions', async () => { + const res = await SELF.fetch(`${ORIGIN}/photon_access_token`) + expect(res.status).toBe(200) + const body = (await res.json()) as { Permissions: unknown[]; RoomInstanceId: number } + expect(body.Permissions.length).toBeGreaterThan(0) + expect(body.RoomInstanceId).toBe(1) + }) + it('GET /roomserver/rooms/createdby/me returns the owned rooms array', async () => { const res = await SELF.fetch(`${ORIGIN}/roomserver/rooms/createdby/me`) expect(res.status).toBe(200)