mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 22:51:30 -07:00
152 lines
5.0 KiB
TypeScript
152 lines
5.0 KiB
TypeScript
import { Hono } from 'hono'
|
||
import { describeRoute } from 'hono-openapi'
|
||
|
||
import { getRoomById } from '@repo/domain'
|
||
|
||
import { authedId } from '../http'
|
||
import {
|
||
AUTHED,
|
||
BareBoolean,
|
||
form,
|
||
json,
|
||
JsonArray,
|
||
QuickPlayResponse,
|
||
TagFilters,
|
||
VerifyRoleRequest,
|
||
} from '../openapi'
|
||
|
||
import type { App } from '../context'
|
||
|
||
// ---- Room keys / quick play / rooms ----------------------------------------
|
||
export const roomRoutes = new Hono<App>({ strict: false })
|
||
.get(
|
||
'/api/roomkeys/v1/mine',
|
||
describeRoute({
|
||
tags: ['Rooms'],
|
||
summary: 'The caller’s room keys',
|
||
description: 'Nothing issues room keys yet, so this is an empty list.',
|
||
responses: { 200: json(JsonArray, 'An empty list') },
|
||
}),
|
||
(c) => c.json([])
|
||
)
|
||
.get(
|
||
'/api/roomkeys/v1/room',
|
||
describeRoute({
|
||
tags: ['Rooms'],
|
||
summary: 'A room’s keys',
|
||
description: 'Nothing issues room keys yet, so this is an empty list.',
|
||
responses: { 200: json(JsonArray, 'An empty list') },
|
||
}),
|
||
(c) => c.json([])
|
||
)
|
||
.get(
|
||
'/api/quickPlay/v1/getandclear',
|
||
describeRoute({
|
||
tags: ['Rooms'],
|
||
summary: 'Take the pending quick-play action',
|
||
description:
|
||
'A read-and-clear of whatever quick-play action is queued for the caller (joining ' +
|
||
'a friend, an invite deep link). Nothing queues one yet, so all three fields are ' +
|
||
'null — which the client reads as “nothing to do”.',
|
||
responses: { 200: json(QuickPlayResponse, 'All null — no pending action') },
|
||
}),
|
||
(c) => c.json({ RoomName: null, ActionCode: null, TargetPlayerId: null })
|
||
)
|
||
|
||
// Room search filters. The client deserializes this into an object (not an
|
||
// array) — shape from the 2025 reference.
|
||
.get(
|
||
'/api/rooms/v1/filters',
|
||
describeRoute({
|
||
tags: ['Rooms'],
|
||
summary: 'Room browse filter chips',
|
||
description:
|
||
'The filter chips on the room browse screen. Static, taken from the 2025 ' +
|
||
'reference. The client deserializes this as an OBJECT, not an array — and unlike ' +
|
||
'the invention/event filters, `TrendingFilters` here is a real list.',
|
||
responses: { 200: json(TagFilters, 'The filter chips') },
|
||
}),
|
||
(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',
|
||
],
|
||
})
|
||
)
|
||
|
||
// Verify the caller holds at least `role` in a room. Params come from the form
|
||
// body (falling back to the query string). Returns a bare `true`/`false`: the
|
||
// room creator always passes; otherwise the caller needs a Roles entry with
|
||
// `Role >= role`. Any failure (no token, unknown room, insufficient role) is
|
||
// `false`. The `context` field (e.g. MakerPen) is accepted and ignored.
|
||
.post(
|
||
'/api/rooms/v1/verifyRole',
|
||
describeRoute({
|
||
tags: ['Rooms'],
|
||
summary: 'Verify the caller’s role in a room',
|
||
description:
|
||
'Whether the caller holds at least `role` in the room — the gate the client checks ' +
|
||
'before letting someone into the Maker Pen. The room’s creator always passes; ' +
|
||
'anyone else needs a `Roles` entry at that level or higher.\n\n' +
|
||
'Answers a bare `true`/`false`, and every failure is `false` rather than an error ' +
|
||
'status: no token, an unknown room, and an insufficient role are indistinguishable ' +
|
||
'to the client. Params are read from the form body, falling back to the query ' +
|
||
'string. Room data is read from the shared rooms database (owned by the `rooms` ' +
|
||
'worker).',
|
||
security: AUTHED,
|
||
requestBody: form(VerifyRoleRequest, 'The room and the role level to check'),
|
||
responses: { 200: json(BareBoolean, 'Whether the caller holds the role') },
|
||
}),
|
||
async (c) => {
|
||
const body = (await c.req.parseBody().catch(() => ({}))) as Record<string, unknown>
|
||
const param = (name: string): string => {
|
||
// (named `fromBody` rather than `form` — the openapi helper owns that name here)
|
||
const fromBody = body[name]
|
||
if (typeof fromBody === 'string' && fromBody !== '') return fromBody
|
||
return c.req.query(name) ?? ''
|
||
}
|
||
const roomId = Number.parseInt(param('roomId'), 10)
|
||
const role = Number.parseInt(param('role'), 10)
|
||
|
||
const accountId = await authedId(c)
|
||
if (accountId === null || Number.isNaN(roomId)) return c.json(false)
|
||
|
||
const room = await getRoomById(c.env.DB, roomId)
|
||
if (!room) return c.json(false)
|
||
|
||
// The creator always passes.
|
||
if (room.CreatorAccountId === accountId) return c.json(true)
|
||
|
||
// Otherwise the caller needs a room role at least as high as requested.
|
||
const roles = Array.isArray(room.Roles) ? (room.Roles as Array<Record<string, unknown>>) : []
|
||
const hasRole = roles.some(
|
||
(r) => r.AccountId === accountId && typeof r.Role === 'number' && r.Role >= (role || 0)
|
||
)
|
||
return c.json(hasRole)
|
||
}
|
||
)
|