mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 07:01:27 -07:00
trying to get rooms up
This commit is contained in:
+18
-70
@@ -12,6 +12,7 @@ import storefrontGiftDrop300 from '../static/storefronts-v3-giftdropstore-300.js
|
||||
import { DEFAULT_AVATAR_ITEMS } from './default-avatar-items'
|
||||
import { defaultSettings } from './default-settings'
|
||||
import { validateAndGetAccountId } from './jwt'
|
||||
import { getRoomById, getRoomByName, getRoomsByCreator, getRoomsByIds } from './rooms-db'
|
||||
|
||||
import type { Context } from 'hono'
|
||||
import type { App } from './context'
|
||||
@@ -69,64 +70,6 @@ function queryIds(c: Context<App>): number[] {
|
||||
)
|
||||
}
|
||||
|
||||
/** Unity scene id for the dorm (matches the match worker's instance location). */
|
||||
const DORM_SCENE_ID = '76d98498-60a1-430c-ab76-b54a29b7a163'
|
||||
|
||||
/**
|
||||
* Full room payload (PascalCase), mirroring the C#'s `BuildRoomResponse` /
|
||||
* `RoomserverRoomsBulk`. With no Rooms DB, room 1 is the dorm and other ids get
|
||||
* a generic published room. The SubRoom carries the UnitySceneId/DataBlob the
|
||||
* client needs to load the scene.
|
||||
*/
|
||||
function buildRoomResponse(roomId: number) {
|
||||
const isDorm = roomId === 1
|
||||
return {
|
||||
RoomId: roomId,
|
||||
Name: isDorm ? 'DormRoom' : `Room${roomId}`,
|
||||
Description: isDorm ? 'Your private room' : '',
|
||||
CreatorAccountId: 1,
|
||||
ImageName: 'DefaultRoomImage.jpg',
|
||||
State: 0,
|
||||
Accessibility: 0,
|
||||
SupportsLevelVoting: false,
|
||||
IsRRO: false,
|
||||
IsDorm: isDorm,
|
||||
CloningAllowed: false,
|
||||
SupportsVRLow: true,
|
||||
SupportsQuest2: true,
|
||||
SupportsMobile: true,
|
||||
SupportsScreens: true,
|
||||
SupportsWalkVR: true,
|
||||
SupportsTeleportVR: true,
|
||||
SupportsJuniors: true,
|
||||
MinLevel: 0,
|
||||
WarningMask: 0,
|
||||
CustomWarning: null,
|
||||
DisableMicAutoMute: false,
|
||||
DisableRoomComments: false,
|
||||
EncryptVoiceChat: false,
|
||||
CreatedAt: '2026-01-18T02:31:37.6171131',
|
||||
Stats: { CheerCount: 0, FavoriteCount: 0, VisitorCount: 1, VisitCount: 1 },
|
||||
SubRooms: [
|
||||
{
|
||||
SubRoomId: 1,
|
||||
Name: '',
|
||||
DataBlob: '',
|
||||
IsSandbox: false,
|
||||
MaxPlayers: 4,
|
||||
Accessibility: 0,
|
||||
UnitySceneId: isDorm ? DORM_SCENE_ID : '',
|
||||
DataSavedAt: '2026-01-18T02:31:37.6171131',
|
||||
},
|
||||
],
|
||||
Roles: [],
|
||||
LoadScreens: [],
|
||||
PromoImages: [],
|
||||
PromoExternalContent: [],
|
||||
Tags: [],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Photon access-token response (`/roomserver/photon_access_token`). The 2023
|
||||
* client calls this to get its room permissions + the instance id it's spawning
|
||||
@@ -524,34 +467,39 @@ const app = new Hono<App>({ strict: false })
|
||||
)
|
||||
|
||||
// ---- Room server ----------------------------------------------------------
|
||||
// Room data is read from the shared `rec-rooms` D1 (owned by the rooms worker).
|
||||
// Register specific paths before the `/:id` param route.
|
||||
.get('/roomserver/rooms/bulk', (c) => {
|
||||
.get('/roomserver/rooms/bulk', async (c) => {
|
||||
const idParam = c.req.query('id')
|
||||
const nameParam = c.req.query('name')
|
||||
if (!idParam && !nameParam) {
|
||||
return c.text("Either 'id' or 'name' query parameter is required", 400)
|
||||
}
|
||||
// Synthesize a room per requested id (the client needs SubRooms to load).
|
||||
// TODO: query Rooms + related tables once a DB binding exists.
|
||||
const ids = (idParam ?? '')
|
||||
.split(',')
|
||||
.map((s) => Number.parseInt(s.trim(), 10))
|
||||
.filter((n) => !Number.isNaN(n))
|
||||
return c.json(ids.map(buildRoomResponse))
|
||||
if (idParam) {
|
||||
const ids = idParam
|
||||
.split(',')
|
||||
.map((s) => Number.parseInt(s.trim(), 10))
|
||||
.filter((n) => !Number.isNaN(n))
|
||||
return c.json(await getRoomsByIds(c.env.DB, ids))
|
||||
}
|
||||
const room = await getRoomByName(c.env.DB, nameParam ?? '')
|
||||
return c.json(room ? [room] : [])
|
||||
})
|
||||
// Photon access token + room permissions the client needs to spawn into a room.
|
||||
.get('/roomserver/photon_access_token', (c) => c.json(photonAccessToken()))
|
||||
.get('/roomserver/rooms/hot', (c) => c.json({ Results: [], TotalResults: 0 }))
|
||||
.get('/roomserver/roomsandplaylists/hot', (c) => c.json({ Results: [], TotalResults: 0 }))
|
||||
.get('/roomserver/rooms/createdby/me', (c) => c.json([buildRoomResponse(1)]))
|
||||
.get('/roomserver/rooms/createdby/me', async (c) =>
|
||||
c.json(await getRoomsByCreator(c.env.DB, (await authedId(c)) ?? 1))
|
||||
)
|
||||
.get('/roomserver/rooms/:id/interactionby/me', (c) =>
|
||||
c.json({ Cheered: false, Favorited: false })
|
||||
)
|
||||
.get('/roomserver/rooms/:id', (c) => {
|
||||
.get('/roomserver/rooms/:id', async (c) => {
|
||||
const roomId = Number.parseInt(c.req.param('id'), 10)
|
||||
if (Number.isNaN(roomId)) return c.notFound()
|
||||
// No Rooms binding → synthesize the room so the client can load it.
|
||||
return c.json(buildRoomResponse(roomId))
|
||||
const room = await getRoomById(c.env.DB, roomId)
|
||||
return room ? c.json(room) : c.notFound()
|
||||
})
|
||||
|
||||
export default app
|
||||
|
||||
@@ -2,7 +2,9 @@ import type { HonoApp } from '@repo/hono-helpers'
|
||||
import type { SharedHonoEnv, SharedHonoVariables } from '@repo/hono-helpers/src/types'
|
||||
|
||||
export type Env = SharedHonoEnv & {
|
||||
// add additional Bindings here
|
||||
// Shared rooms database (schema/migrations owned by the `rooms` worker). Used
|
||||
// read-only here for the /roomserver/rooms/* endpoints.
|
||||
DB: D1Database
|
||||
}
|
||||
|
||||
/** Variables can be extended */
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Read helpers for the shared `rec-rooms` D1 database. The schema, migrations,
|
||||
* and seed are owned by the `rooms` worker (apps/rooms/src/rooms-db.ts +
|
||||
* migrations); this worker binds the same database read-only for its
|
||||
* `/roomserver/rooms/*` endpoints. Keep these queries in sync with the rooms
|
||||
* worker's.
|
||||
*/
|
||||
|
||||
/** A stored room — the parsed JSON blob (full client-facing room response). */
|
||||
export type Room = Record<string, unknown>
|
||||
|
||||
interface RoomRow {
|
||||
data: string
|
||||
}
|
||||
|
||||
const parseOne = (row: RoomRow | null): Room | null => (row ? (JSON.parse(row.data) as Room) : null)
|
||||
const parseAll = (rows: RoomRow[]): Room[] => rows.map((r) => JSON.parse(r.data) as Room)
|
||||
|
||||
export async function getRoomById(db: D1Database, roomId: number): Promise<Room | null> {
|
||||
return parseOne(
|
||||
await db.prepare('SELECT data FROM rooms WHERE room_id = ?1').bind(roomId).first<RoomRow>()
|
||||
)
|
||||
}
|
||||
|
||||
export async function getRoomByName(db: D1Database, name: string): Promise<Room | null> {
|
||||
return parseOne(
|
||||
await db
|
||||
.prepare('SELECT data FROM rooms WHERE name_lower = ?1')
|
||||
.bind(name.toLowerCase())
|
||||
.first<RoomRow>()
|
||||
)
|
||||
}
|
||||
|
||||
export async function getRoomsByIds(db: D1Database, ids: number[]): Promise<Room[]> {
|
||||
if (ids.length === 0) return []
|
||||
const placeholders = ids.map((_, i) => `?${i + 1}`).join(',')
|
||||
const { results } = await db
|
||||
.prepare(`SELECT data FROM rooms WHERE room_id IN (${placeholders})`)
|
||||
.bind(...ids)
|
||||
.all<RoomRow>()
|
||||
return parseAll(results)
|
||||
}
|
||||
|
||||
export async function getRoomsByCreator(db: D1Database, accountId: number): Promise<Room[]> {
|
||||
const { results } = await db
|
||||
.prepare('SELECT data FROM rooms WHERE creator_account_id = ?1')
|
||||
.bind(accountId)
|
||||
.all<RoomRow>()
|
||||
return parseAll(results)
|
||||
}
|
||||
@@ -1,12 +1,45 @@
|
||||
import { env } from 'cloudflare:test'
|
||||
import { exports } from 'cloudflare:workers'
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { beforeAll, describe, expect, test } from 'vitest'
|
||||
|
||||
import '../../api.app'
|
||||
|
||||
import { DEFAULT_AVATAR_ITEMS } from '../../default-avatar-items'
|
||||
|
||||
import type { Env } from '../../context'
|
||||
|
||||
declare module 'cloudflare:test' {
|
||||
interface ProvidedEnv extends Env {}
|
||||
}
|
||||
|
||||
const ORIGIN = 'https://api.rec.djdevin.net'
|
||||
|
||||
// The /roomserver/rooms/* routes read from the shared rec-rooms D1. Set up the
|
||||
// schema (matching the rooms worker's migration) + a couple of rooms for tests.
|
||||
const TEST_ROOMS = [
|
||||
{
|
||||
RoomId: 1,
|
||||
Name: 'DormRoom',
|
||||
IsDorm: true,
|
||||
CreatorAccountId: 1,
|
||||
SubRooms: [{ SubRoomId: 1, UnitySceneId: '76d98498-60a1-430c-ab76-b54a29b7a163' }],
|
||||
},
|
||||
{ RoomId: 2, Name: 'RecCenter', IsDorm: false, CreatorAccountId: 1, SubRooms: [{ SubRoomId: 2 }] },
|
||||
]
|
||||
|
||||
beforeAll(async () => {
|
||||
await env.DB.prepare(
|
||||
`CREATE TABLE IF NOT EXISTS rooms (
|
||||
data TEXT NOT NULL,
|
||||
room_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.RoomId')) VIRTUAL,
|
||||
name_lower TEXT GENERATED ALWAYS AS (lower(json_extract(data, '$.Name'))) VIRTUAL,
|
||||
creator_account_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.CreatorAccountId')) VIRTUAL
|
||||
)`
|
||||
).run()
|
||||
const insert = env.DB.prepare('INSERT OR IGNORE INTO rooms (data) VALUES (?1)')
|
||||
await env.DB.batch(TEST_ROOMS.map((r) => insert.bind(JSON.stringify(r))))
|
||||
})
|
||||
|
||||
// Mint a token the way the `auth` worker does, using the same dev secret, so the
|
||||
// api worker's validation accepts it. Kept inline to avoid a cross-package import.
|
||||
const DEV_SECRET = 'dev-insecure-signing-key-change-me'
|
||||
@@ -251,12 +284,17 @@ describe('room server', () => {
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
test('GET /roomserver/rooms/bulk with id returns rooms with SubRooms', async () => {
|
||||
test('GET /roomserver/rooms/bulk with id returns rooms from D1', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/roomserver/rooms/bulk?id=1,2`)
|
||||
expect(res.status).toBe(200)
|
||||
const rooms = (await res.json()) as Array<{ RoomId: number; SubRooms: unknown[] }>
|
||||
expect(rooms.map((r) => r.RoomId)).toEqual([1, 2])
|
||||
expect(rooms[0].SubRooms).toHaveLength(1)
|
||||
const rooms = (await res.json()) as Array<{ RoomId: number; Name: string }>
|
||||
expect(rooms.map((r) => r.RoomId).sort((a, b) => a - b)).toEqual([1, 2])
|
||||
})
|
||||
|
||||
test('GET /roomserver/rooms/bulk?name= resolves from D1', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/roomserver/rooms/bulk?name=reccenter`)
|
||||
const rooms = (await res.json()) as Array<{ Name: string }>
|
||||
expect(rooms.map((r) => r.Name)).toEqual(['RecCenter'])
|
||||
})
|
||||
|
||||
test('GET /roomserver/photon_access_token returns permissions + instance id', async () => {
|
||||
@@ -277,7 +315,7 @@ describe('room server', () => {
|
||||
expect(await res.json()).toEqual({ Results: [], TotalResults: 0 })
|
||||
})
|
||||
|
||||
test('GET /roomserver/rooms/:id synthesizes a room with a SubRoom', async () => {
|
||||
test('GET /roomserver/rooms/:id returns the room from D1', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/roomserver/rooms/1`)
|
||||
expect(res.status).toBe(200)
|
||||
const room = (await res.json()) as {
|
||||
@@ -289,6 +327,11 @@ describe('room server', () => {
|
||||
expect(room.SubRooms[0].UnitySceneId).toBe('76d98498-60a1-430c-ab76-b54a29b7a163')
|
||||
})
|
||||
|
||||
test('GET /roomserver/rooms/:id 404s for an unknown room', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/roomserver/rooms/99999`)
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
|
||||
test('GET /roomserver/rooms/:id/interactionby/me', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/roomserver/rooms/5/interactionby/me`)
|
||||
expect(await res.json()).toEqual({ Cheered: false, Favorited: false })
|
||||
|
||||
@@ -10,6 +10,14 @@
|
||||
"custom_domain": true
|
||||
}
|
||||
],
|
||||
// Shared rooms DB (created + migrated by the `rooms` worker; bound read-only here).
|
||||
"d1_databases": [
|
||||
{
|
||||
"binding": "DB",
|
||||
"database_name": "rec-rooms",
|
||||
"database_id": "d44083e1-5bfe-4467-aa9a-f13c5c2496d5"
|
||||
}
|
||||
],
|
||||
"logpush": false,
|
||||
"upload_source_maps": true,
|
||||
"observability": {
|
||||
|
||||
Reference in New Issue
Block a user