Files
recflare/apps/api/src/routes/gameplay.ts
T
Devin Zuczek 10eb89ac12 (wip) events
2026-08-04 18:44:11 -04:00

167 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 activitys 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 rooms 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 clients 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 callers 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 })
)