mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[chat] Return the 2026 party message contract (#122)
This commit is contained in:
@@ -4,7 +4,7 @@ import { useWorkersLogger } from 'workers-tagged-logger'
|
||||
|
||||
import { mergePlayerSettings as mergeStoredPlayerSettings, readPlayerSettings } from '@repo/domain'
|
||||
import { logger, withCleanSpec, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
import { validateAndGetAccountId } from '@repo/jwt'
|
||||
import { validateAndGetAccountId, validateAndGetVersion } from '@repo/jwt'
|
||||
|
||||
import { censorSwears } from '../../api/src/sanitize'
|
||||
import { NotificationType } from '../../notify/src/notification-types'
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
json,
|
||||
messageCountParam,
|
||||
NOT_A_MEMBER_RESPONSE,
|
||||
PartyChatThread,
|
||||
PartyChatReadResponse,
|
||||
PartyInviteSettings,
|
||||
RenameThreadRequest,
|
||||
SendMessageRequest,
|
||||
@@ -891,7 +891,10 @@ const app = new Hono<App>()
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: json(PartyChatThread, 'The caller’s party, or `{}` when they have none'),
|
||||
200: json(
|
||||
PartyChatReadResponse,
|
||||
'2026: a list of party threads; older builds: a thread or {}'
|
||||
),
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
},
|
||||
}),
|
||||
@@ -900,19 +903,22 @@ const app = new Hono<App>()
|
||||
if (id === null) return c.body(null, 401)
|
||||
|
||||
// Path 1: already in a party. One query, no settings read.
|
||||
const version = await validateAndGetVersion(c.req.raw, await c.env.JWT_SECRET.get())
|
||||
const listResponse = version !== null && version >= '20260323'
|
||||
const noParty = () => (listResponse ? c.json([]) : c.json({}))
|
||||
let thread = await getPartyThreadForPlayer(c.env.DB, id)
|
||||
|
||||
// Path 2: not in one — the key is the only thing that can name a party to join.
|
||||
if (thread === null) {
|
||||
const chatThreadId = await readLatestPartyChatId(c.env, id)
|
||||
if (chatThreadId === null) return c.json({})
|
||||
if (chatThreadId === null) return noParty()
|
||||
|
||||
// Checked BEFORE the join: a gone thread, one of some other type, or a party
|
||||
// too old to still be taking people isn't something to put anybody on —
|
||||
// whoever wrote the key.
|
||||
const meta = await getThreadMeta(c.env.DB, chatThreadId)
|
||||
if (meta === null || meta.chatThreadType !== ChatThreadType.Party) return c.json({})
|
||||
if (!isPartyJoinable(meta.createdAt)) return c.json({})
|
||||
if (meta === null || meta.chatThreadType !== ChatThreadType.Party) return noParty()
|
||||
if (!isPartyJoinable(meta.createdAt)) return noParty()
|
||||
|
||||
await addThreadMember(c.env.DB, chatThreadId, id)
|
||||
// The same announcement the add-member route makes: the party learns someone
|
||||
@@ -925,7 +931,8 @@ const app = new Hono<App>()
|
||||
const messages = await getThreadMessages(c.env.DB, thread.chatThreadId, {
|
||||
limit: DEFAULT_THREAD_MESSAGE_COUNT,
|
||||
})
|
||||
return c.json(toPartyChatThread(thread, messages))
|
||||
const party = toPartyChatThread(thread, messages)
|
||||
return listResponse ? c.json([party]) : c.json(party)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -274,6 +274,13 @@ export const PartyChatThread = z.object({
|
||||
ClubId: z.int().nullable().describe('Always null here — this worker serves no club threads'),
|
||||
})
|
||||
|
||||
/** 2026 reads a thread list; older clients read a thread or an empty object. */
|
||||
export const PartyChatReadResponse = z.union([
|
||||
PartyChatThread,
|
||||
z.array(PartyChatThread),
|
||||
z.object({}).strict(),
|
||||
])
|
||||
|
||||
/**
|
||||
* `POST /thread/party` — the client's CreatePartyChat. A bare two-key wrapper, PascalCase
|
||||
* like the thread inside it, with no `{ success, error, value }` envelope around it.
|
||||
|
||||
@@ -50,10 +50,10 @@ function b64url(input: ArrayBuffer | string): string {
|
||||
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
}
|
||||
|
||||
async function bearer(sub: number): Promise<Record<string, string>> {
|
||||
async function bearer(sub: number, version?: string): Promise<Record<string, string>> {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const signingInput = `${b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))}.${b64url(
|
||||
JSON.stringify({ sub: String(sub), exp: now + 3600 })
|
||||
JSON.stringify({ sub: String(sub), exp: now + 3600, 'rn.ver': version })
|
||||
)}`
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
@@ -74,6 +74,18 @@ beforeAll(async () => {
|
||||
})
|
||||
|
||||
describe('chat endpoints', () => {
|
||||
it('2026 party reads return a list, including when the caller has no party', async () => {
|
||||
const empty = await SELF.fetch(`${ORIGIN}/thread/party`, {
|
||||
headers: await bearer(42601, '20260323.14'),
|
||||
})
|
||||
expect(empty.status).toBe(200)
|
||||
expect(await empty.json()).toEqual([])
|
||||
const threadId = await createThread(env.DB, [42601], null, undefined, ChatThreadType.Party)
|
||||
const present = await SELF.fetch(`${ORIGIN}/thread/party`, {
|
||||
headers: await bearer(42601, '20260323.14'),
|
||||
})
|
||||
expect(await present.json()).toMatchObject([{ ChatThreadId: threadId }])
|
||||
})
|
||||
it('GET / reports service status', async () => {
|
||||
const res = await SELF.fetch(`${ORIGIN}/`)
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
Reference in New Issue
Block a user