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 { mergePlayerSettings as mergeStoredPlayerSettings, readPlayerSettings } from '@repo/domain'
|
||||||
import { logger, withCleanSpec, withNotFound, withOnError } from '@repo/hono-helpers'
|
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 { censorSwears } from '../../api/src/sanitize'
|
||||||
import { NotificationType } from '../../notify/src/notification-types'
|
import { NotificationType } from '../../notify/src/notification-types'
|
||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
json,
|
json,
|
||||||
messageCountParam,
|
messageCountParam,
|
||||||
NOT_A_MEMBER_RESPONSE,
|
NOT_A_MEMBER_RESPONSE,
|
||||||
PartyChatThread,
|
PartyChatReadResponse,
|
||||||
PartyInviteSettings,
|
PartyInviteSettings,
|
||||||
RenameThreadRequest,
|
RenameThreadRequest,
|
||||||
SendMessageRequest,
|
SendMessageRequest,
|
||||||
@@ -891,7 +891,10 @@ const app = new Hono<App>()
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
responses: {
|
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,
|
401: UNAUTHORIZED_RESPONSE,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -900,19 +903,22 @@ const app = new Hono<App>()
|
|||||||
if (id === null) return c.body(null, 401)
|
if (id === null) return c.body(null, 401)
|
||||||
|
|
||||||
// Path 1: already in a party. One query, no settings read.
|
// 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)
|
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.
|
// Path 2: not in one — the key is the only thing that can name a party to join.
|
||||||
if (thread === null) {
|
if (thread === null) {
|
||||||
const chatThreadId = await readLatestPartyChatId(c.env, id)
|
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
|
// 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 —
|
// too old to still be taking people isn't something to put anybody on —
|
||||||
// whoever wrote the key.
|
// whoever wrote the key.
|
||||||
const meta = await getThreadMeta(c.env.DB, chatThreadId)
|
const meta = await getThreadMeta(c.env.DB, chatThreadId)
|
||||||
if (meta === null || meta.chatThreadType !== ChatThreadType.Party) return c.json({})
|
if (meta === null || meta.chatThreadType !== ChatThreadType.Party) return noParty()
|
||||||
if (!isPartyJoinable(meta.createdAt)) return c.json({})
|
if (!isPartyJoinable(meta.createdAt)) return noParty()
|
||||||
|
|
||||||
await addThreadMember(c.env.DB, chatThreadId, id)
|
await addThreadMember(c.env.DB, chatThreadId, id)
|
||||||
// The same announcement the add-member route makes: the party learns someone
|
// 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, {
|
const messages = await getThreadMessages(c.env.DB, thread.chatThreadId, {
|
||||||
limit: DEFAULT_THREAD_MESSAGE_COUNT,
|
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'),
|
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
|
* `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.
|
* 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(/=+$/, '')
|
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 now = Math.floor(Date.now() / 1000)
|
||||||
const signingInput = `${b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))}.${b64url(
|
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(
|
const key = await crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
@@ -74,6 +74,18 @@ beforeAll(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('chat endpoints', () => {
|
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 () => {
|
it('GET / reports service status', async () => {
|
||||||
const res = await SELF.fetch(`${ORIGIN}/`)
|
const res = await SELF.fetch(`${ORIGIN}/`)
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
|
|||||||
Reference in New Issue
Block a user