mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 07:01:27 -07:00
167 lines
5.9 KiB
TypeScript
167 lines
5.9 KiB
TypeScript
import { Hono } from 'hono'
|
||
import { describeRoute } from 'hono-openapi'
|
||
|
||
import charadesWords from '../../static/charades.json'
|
||
import communityBoard from '../../static/community-board.json'
|
||
import {
|
||
BareString,
|
||
idParam,
|
||
IsPureResponse,
|
||
json,
|
||
JsonArray,
|
||
jsonBody,
|
||
JsonObject,
|
||
KeepsakeConfig,
|
||
SanitizeRequest,
|
||
stringParam,
|
||
SubscriptionResponse,
|
||
} from '../openapi'
|
||
|
||
import type { App } from '../context'
|
||
|
||
// Text sanitization, keepsakes, objectives/events/rewards, and the misc
|
||
// analytics/subscription sinks the client hits during load.
|
||
export const gameplayRoutes = new Hono<App>({ strict: false })
|
||
// Text sanitization (display names, room names, chat). `v1` echoes the input
|
||
// value back; `isPure` reports the text is clean.
|
||
.post(
|
||
'/api/sanitize/v1',
|
||
describeRoute({
|
||
tags: ['Gameplay'],
|
||
summary: 'Sanitize a string',
|
||
description:
|
||
'Runs display names, room names and chat through the profanity filter. There is ' +
|
||
'no filter here — the input `Value` is echoed back verbatim as a bare JSON string ' +
|
||
'(an empty string if the body has no `Value`).',
|
||
requestBody: jsonBody(SanitizeRequest, 'The text to clean'),
|
||
responses: { 200: json(BareString, 'The input text, unchanged (a bare JSON string)') },
|
||
}),
|
||
async (c) => {
|
||
const body = await c.req.json<{ Value?: unknown }>().catch(() => ({}) as { Value?: unknown })
|
||
return c.json(typeof body.Value === 'string' ? body.Value : '')
|
||
}
|
||
)
|
||
.post(
|
||
'/api/sanitize/v1/isPure',
|
||
describeRoute({
|
||
tags: ['Gameplay'],
|
||
summary: 'Whether a string is clean',
|
||
description: 'The yes/no form of the filter. Always `true` — nothing is filtered here.',
|
||
requestBody: jsonBody(SanitizeRequest, 'The text to check'),
|
||
responses: { 200: json(IsPureResponse, 'Always pure') },
|
||
}),
|
||
(c) => c.json({ IsPure: true })
|
||
)
|
||
|
||
// ---- Activities -----------------------------------------------------------
|
||
// Word bank for the Charades activity. The client requests the list by
|
||
// activity name (`.../words/Charades`); other activities have no data yet.
|
||
.get(
|
||
'/api/activities/charades/v1/words/:activity',
|
||
describeRoute({
|
||
tags: ['Gameplay'],
|
||
summary: 'An activity’s word bank',
|
||
description:
|
||
'The words the Charades activity draws from. The client asks by activity name ' +
|
||
'(`.../words/Charades`); the name is not matched on, so every activity gets the ' +
|
||
'charades list — no other activity has data yet.',
|
||
parameters: [stringParam('activity', 'Activity name, e.g. `Charades`. Not matched on.')],
|
||
responses: { 200: json(JsonArray, 'The word list') },
|
||
}),
|
||
(c) => c.json(charadesWords)
|
||
)
|
||
|
||
// Keepsakes (room mementos). Stubbed empty.
|
||
.get(
|
||
'/api/keepsakes/globalconfig',
|
||
describeRoute({
|
||
tags: ['Gameplay'],
|
||
summary: 'Keepsake feature switches',
|
||
description:
|
||
'Whether keepsakes (room mementos) are on and how many a room may hold. The ' +
|
||
'feature reports as enabled, but nothing stores keepsakes yet.',
|
||
responses: { 200: json(KeepsakeConfig, 'The keepsake config') },
|
||
}),
|
||
(c) =>
|
||
c.json({ KeepsakeFeatureEnabled: true, KeepsakeRoomLimit: 10, SocialXpBoostEnabled: false })
|
||
)
|
||
.get(
|
||
'/api/keepsakes/rooms/:roomId',
|
||
describeRoute({
|
||
tags: ['Gameplay'],
|
||
summary: 'A room’s keepsakes',
|
||
description:
|
||
'No keepsake storage yet. Answers 204 with no body rather than an empty list — ' +
|
||
'that is what the reference does, and the client treats a body here as data.',
|
||
parameters: [idParam('roomId', 'Room id')],
|
||
responses: { 204: { description: 'No keepsakes (empty body)' } },
|
||
}),
|
||
(c) => c.body(null, 204)
|
||
)
|
||
.get(
|
||
'/api/keepsakes/categories',
|
||
describeRoute({
|
||
tags: ['Gameplay'],
|
||
summary: 'Keepsake categories',
|
||
description: 'No keepsake catalog yet, so this is an empty list.',
|
||
responses: { 200: json(JsonArray, 'An empty list') },
|
||
}),
|
||
(c) => c.json([])
|
||
)
|
||
|
||
// ---- Objectives / events / rewards ---------------------------------------
|
||
// Objectives live on the `econ` host (`updateobjective` / `myprogress`), which is
|
||
// where the client calls them — they are not served here.
|
||
.get(
|
||
'/api/communityboard/v2/current',
|
||
describeRoute({
|
||
tags: ['Gameplay'],
|
||
summary: 'The current community board',
|
||
description:
|
||
'The rotating community board on the home screen — featured player, featured room ' +
|
||
'group, announcement and image strips. Served verbatim from a static blob.',
|
||
responses: { 200: json(JsonObject, 'The community board') },
|
||
}),
|
||
(c) => c.json(communityBoard)
|
||
)
|
||
// Player events live in their own controller (routes/events.ts) — they're D1-backed
|
||
// now, unlike the stubs around them here.
|
||
.get(
|
||
'/api/announcement/v1/get',
|
||
describeRoute({
|
||
tags: ['Gameplay'],
|
||
summary: 'Announcements',
|
||
description: 'The announcement banners on the home screen. Not hydrated yet.',
|
||
responses: { 200: json(JsonArray, 'An empty list') },
|
||
}),
|
||
(c) => c.json([])
|
||
) // TODO: hydrate from JSON/announcements.json
|
||
|
||
// GameSight attribution/analytics event sink. Accept and ack without persisting.
|
||
.post(
|
||
'/api/gamesight/event',
|
||
describeRoute({
|
||
tags: ['Gameplay'],
|
||
summary: 'Analytics event sink',
|
||
description:
|
||
'The client’s GameSight attribution/analytics events. Accepted and dropped — ' +
|
||
'nothing is persisted. Answers 200 with an empty body.',
|
||
responses: { 200: { description: 'Accepted (empty body)' } },
|
||
}),
|
||
(c) => c.body(null, 200)
|
||
)
|
||
|
||
// ---- Subscription ---------------------------------------------------------
|
||
.post(
|
||
'/api/CampusCard/v1/UpdateAndGetSubscription',
|
||
describeRoute({
|
||
tags: ['Gameplay'],
|
||
summary: 'The caller’s subscription',
|
||
description:
|
||
'Rec Room Plus subscription state. There are no subscriptions on this server, so ' +
|
||
'both fields are null. Also served by the `econ` worker on its own host.',
|
||
responses: { 200: json(SubscriptionResponse, 'No subscription') },
|
||
}),
|
||
(c) => c.json({ subscription: null, platformAccountSubscribedPlayerId: null })
|
||
)
|