mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-27 07:58:55 -07:00
support for 202507 endpoints (#37)
* [auth][api] accept the 20250424.01 client * [2025] unstable * 20250718.0 * correct one this time * stubs * more stubs * more stubs * [lists] add worker * [ai] route stubs * [api] player photo setting * [econ] add roomEconConfig route * [infra] update worker generators * [worker] add cards/moderation/platformnotification workers * [lists] updates to some endpoints * [clubs] stub out announcement endpoint, for now * [econ] stub out season endpoints for now * [chat] apps/chat stub out party endpoint not sure the shape yet * [api] stub out statsig and lockeditems * [doc] new services * [lists] stub the bulk endpoint * [datacollection] add placeholder service until we can kill it * [api] set gifting to lvl5 * update lock * [cdn] enable cache * [match] matchmake v2 * [lists] stub some lists * [ai] stubs * [rooms] new subroom save endpoint * [econ] add bulk purchase endpoint * [discovery] update featured creator to 1 for fun * [api] add photo settings flag * [chat] fixup chat permissions (sorta) * [auth] restrictions endpoint * [rooms] contributed endpoint * [api] fix outfit endpoint * [discovery] attempt to fix store * [chat] privacy endpoints * [api] cheered images * [rooms] add xp endpoint (disbaled) * [rooms] add xp endpoint (disabled) * update images-db for cheers * [rooms] add autocomplete endpoint * [cdn/img] increase cache ttl for statics * [api] bulk route for images * [accounts] add banner image * [api] add misc missing endpoints * [discovery] remove AI tab * [platformnotifications] stub some endpoints * [lists] add some more lists * [rooms] additional endpoints * [chat] stub a few privacy endpoints * [econ] stub some endpoints * misc db fixes * [api] tweak shape for images v6 * [rooms] dont show trending RROs
This commit is contained in:
+185
-2
@@ -10,6 +10,7 @@ import { getThreadMessages } from './message-db'
|
||||
import {
|
||||
AUTHED,
|
||||
ChatMessageDto,
|
||||
ChatPrivacySettings,
|
||||
ChatResult,
|
||||
ChatThreadDto,
|
||||
ChatThreadWithMessagesDto,
|
||||
@@ -20,6 +21,8 @@ import {
|
||||
json,
|
||||
messageCountParam,
|
||||
NOT_A_MEMBER_RESPONSE,
|
||||
PartyInviteSettings,
|
||||
PartyThread,
|
||||
RenameThreadRequest,
|
||||
SendMessageRequest,
|
||||
SendMessageResponse,
|
||||
@@ -85,14 +88,34 @@ async function formMessageCount(c: Context<App>, fallback: number): Promise<numb
|
||||
}
|
||||
|
||||
/**
|
||||
* What a chat action reports back to the client alongside its payload — the reference's
|
||||
* ChatResult. Only success and "bad arguments" are reachable here.
|
||||
* What a chat action reports back to the client — the reference's ChatResult, usually
|
||||
* alongside a payload but sometimes (the DM privacy check) as the whole body. Only these
|
||||
* four of the enum's twenty values are reachable here; `ChatResult` in openapi.ts records
|
||||
* the rest, including the 15/16 privacy refusals nothing on this server can answer.
|
||||
*/
|
||||
const CHAT_SUCCESS = 0
|
||||
const CHAT_INVALID_ARGUMENTS = 1
|
||||
const CHAT_MEMBERSHIP_NOT_FOUND = 3
|
||||
const CHAT_PLAYER_ALREADY_ON_THREAD = 4
|
||||
|
||||
/**
|
||||
* How long a party invite link stays usable, in minutes (`GET /settings/partyinvite`).
|
||||
* The reference's value. Nothing here stores invite links, so this is what the client
|
||||
* counts down with rather than a lifetime this server enforces.
|
||||
*/
|
||||
const PARTY_INVITE_LIFETIME_MINUTES = 60
|
||||
|
||||
/**
|
||||
* Who may start a chat with a player — the client's `ChatPrivacy` enum, served numerically
|
||||
* like every other enum on this build. `Friends` is what a fresh account reports, and what
|
||||
* every account reports here: nothing stores a per-player setting yet.
|
||||
*/
|
||||
const ChatPrivacy = {
|
||||
Friends: 0,
|
||||
Favorites: 1,
|
||||
NoOne: 2,
|
||||
} as const
|
||||
|
||||
/** The hub is a single global Durable Object instance, as every worker addresses it. */
|
||||
const HUB_INSTANCE = 'global'
|
||||
|
||||
@@ -426,6 +449,166 @@ const app = new Hono<App>()
|
||||
}
|
||||
)
|
||||
|
||||
// How long a party invite link lives. Server-side config the client reads to stamp its
|
||||
// own invite links, not per-player state — one hour, which is the reference's value.
|
||||
//
|
||||
// A bare single-key object: `{ InviteLinkLifetimeInMinutes }` and nothing else, no
|
||||
// `{ success, error, value }` envelope. Nothing here expires links (there is no invite
|
||||
// link store), so this is the number the client shows and counts down with rather than
|
||||
// a lifetime this server enforces.
|
||||
.get(
|
||||
'/settings/partyinvite',
|
||||
describeRoute({
|
||||
tags: ['Threads'],
|
||||
summary: 'Party invite settings',
|
||||
description: [
|
||||
'How long a party invite link stays usable, in minutes, as a bare single-key object —',
|
||||
'no envelope. 60 here, the reference’s value. Nothing on this server stores or expires',
|
||||
'invite links, so the client is the only thing that acts on it.',
|
||||
].join(' '),
|
||||
security: AUTHED,
|
||||
responses: {
|
||||
200: json(PartyInviteSettings, 'The invite-link lifetime'),
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const id = await authedId(c)
|
||||
if (id === null) return c.body(null, 401)
|
||||
return c.json({ InviteLinkLifetimeInMinutes: PARTY_INVITE_LIFETIME_MINUTES })
|
||||
}
|
||||
)
|
||||
|
||||
// The party thread (`/thread/party?maxCount=1&mode=0`). STUB: the response shape is
|
||||
// unknown — it hasn't been observed off a live client — so this answers an empty
|
||||
// object, which parses as "no party" rather than failing the client's deserializer the
|
||||
// way a 404 or a bare array would. `maxCount` and `mode` are accepted and ignored.
|
||||
// Replace the body once the real shape is captured.
|
||||
.get(
|
||||
'/thread/party',
|
||||
describeRoute({
|
||||
tags: ['Threads'],
|
||||
summary: 'The caller’s party thread (stub)',
|
||||
description: [
|
||||
'STUB — the response shape has not been observed off a live client, so this answers an',
|
||||
'empty object `{}`, which parses as "no party" rather than failing the client’s',
|
||||
'deserializer the way a 404 or a bare array would. `maxCount` and `mode` are accepted and',
|
||||
'ignored. Replace the body once the real shape is captured.',
|
||||
].join(' '),
|
||||
security: AUTHED,
|
||||
parameters: [
|
||||
{
|
||||
name: 'maxCount',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description: 'Page size the client sends (1). Ignored by the stub',
|
||||
schema: { type: 'integer' },
|
||||
},
|
||||
{
|
||||
name: 'mode',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description: 'Unknown mode selector the client sends (0). Ignored by the stub',
|
||||
schema: { type: 'integer' },
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: json(PartyThread, 'Always `{}` — the stub carries no party'),
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const id = await authedId(c)
|
||||
if (id === null) return c.body(null, 401)
|
||||
return c.json({})
|
||||
}
|
||||
)
|
||||
|
||||
// The caller's chat privacy settings — who may DM them, and who may pull them into a
|
||||
// group chat. Both report `Friends`, which is the reference's default and the safer of
|
||||
// the two directions to be wrong in: it describes a player as more private than the
|
||||
// server actually enforces, rather than less.
|
||||
//
|
||||
// REPORTED, NOT ENFORCED. Nothing here stores a per-player setting or checks one — the
|
||||
// DM check below allows every message regardless — so this is what the client renders on
|
||||
// its privacy screen. Wire the two together if this ever becomes real: a screen that says
|
||||
// "Friends" while anyone can message you is worse than one that says nothing.
|
||||
//
|
||||
// `playerId` comes off the TOKEN, not a query param: the answer is about the caller.
|
||||
.get(
|
||||
'/thread/chatPrivacySetting',
|
||||
describeRoute({
|
||||
tags: ['Threads'],
|
||||
summary: 'The caller’s chat privacy settings',
|
||||
description: [
|
||||
'Who may direct-message the caller and who may add them to a group chat, as the',
|
||||
'`ChatPrivacy` enum by NUMBER (0 Friends · 1 Favorites · 2 NoOne). Both are `Friends`',
|
||||
'here — nothing stores a per-player setting — and nothing enforces them either: the',
|
||||
'DM check allows every message. `playerId` is the caller, read from the token.',
|
||||
].join(' '),
|
||||
security: AUTHED,
|
||||
responses: {
|
||||
200: json(ChatPrivacySettings, 'The caller’s settings — always Friends/Friends'),
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const id = await authedId(c)
|
||||
if (id === null) return c.body(null, 401)
|
||||
return c.json({
|
||||
playerId: id,
|
||||
directMessagePrivacySetting: ChatPrivacy.Friends,
|
||||
groupChatPrivacySetting: ChatPrivacy.Friends,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// May the caller DM this player? Asked before the client opens a new direct message, so
|
||||
// it can grey the button out rather than let the send fail. Always 0 (Success): nothing
|
||||
// here stores the who-can-message-me privacy setting the name refers to, so there is no
|
||||
// setting to refuse on.
|
||||
//
|
||||
// The body is a bare ChatResult INTEGER — the client instantiates its response wrapper
|
||||
// with the ChatResult enum, not a bool, so `true` decodes as nothing. The refusals this
|
||||
// endpoint would otherwise answer are 15 (blocked by the caller's own privacy setting)
|
||||
// and 16 (blocked by the other player's); everything else in the enum belongs to the
|
||||
// thread actions. It is served numerically: this client build carries no by-name enum
|
||||
// formatter.
|
||||
.get(
|
||||
'/thread/checkCanSendDirectMessageWithPrivacySetting',
|
||||
describeRoute({
|
||||
tags: ['Threads'],
|
||||
summary: 'May the caller DM this player?',
|
||||
description: [
|
||||
'Whether the caller may open a direct message with `receivingPlayerId`, as a bare',
|
||||
'ChatResult integer — 0 (Success) means allowed; a real refusal would be 15 (the',
|
||||
'caller’s own privacy setting) or 16 (the other player’s). Always 0 here: this server',
|
||||
'stores no who-can-message-me privacy setting, so there is nothing to refuse on.',
|
||||
'`receivingPlayerId` is accepted and ignored; the answer is the same for every player,',
|
||||
'and the client asks again for the next one.',
|
||||
].join(' '),
|
||||
security: AUTHED,
|
||||
parameters: [
|
||||
{
|
||||
name: 'receivingPlayerId',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description: 'The player the caller wants to message. Accepted and ignored.',
|
||||
schema: { type: 'integer' },
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: json(ChatResult, 'Always 0 (Success) — the DM is allowed'),
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const id = await authedId(c)
|
||||
if (id === null) return c.body(null, 401)
|
||||
return c.json(CHAT_SUCCESS)
|
||||
}
|
||||
)
|
||||
|
||||
// "Open the chat with these people" — the client's GetChatBetweenPlayers. Fetch or
|
||||
// create: the thread whose membership is exactly `ids` plus the caller, opened only
|
||||
// if they don't already share one. Returning a fresh empty thread each call would
|
||||
|
||||
Reference in New Issue
Block a user