mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
fix custom loading screens, add private endpoint for match into instance
This commit is contained in:
@@ -70,6 +70,11 @@ inconsistency here without checking the client first.
|
|||||||
- A club's `AdditionalImages` (`clubs`) is an array of whole `SavedImage` records, not
|
- A club's `AdditionalImages` (`clubs`) is an array of whole `SavedImage` records, not
|
||||||
image names — a bare string array fails the client's parser ("expected '{'"). The list
|
image names — a bare string array fails the client's parser ("expected '{'"). The list
|
||||||
is packed: removing an image shifts the rest up, never leaving a blank slot.
|
is packed: removing an image shifts the rest up, never leaving a blank slot.
|
||||||
|
- A room's `LoadScreens` (`rooms`: `PUT /rooms/:id/loadscreen`) is an array — the
|
||||||
|
client's parser wants one — but the client renders only the FIRST entry and only ever
|
||||||
|
posts one. So the endpoint REPLACES the list rather than appending: an appended screen
|
||||||
|
sits unreachable behind the old one and setting a load screen looks like it did
|
||||||
|
nothing. Keep the array shape for eventual multi-screen support.
|
||||||
- Endpoints the client re-renders from must return the updated entity, not
|
- Endpoints the client re-renders from must return the updated entity, not
|
||||||
`{ error, success, value: null }` — e.g. `clubs` `PUT /club/:id/clubhouse` left the old
|
`{ error, success, value: null }` — e.g. `clubs` `PUT /club/:id/clubhouse` left the old
|
||||||
clubhouse on screen until it answered the full details envelope.
|
clubhouse on screen until it answered the full details envelope.
|
||||||
|
|||||||
@@ -505,7 +505,7 @@ export const RestrictionsRequest = z.object({
|
|||||||
supportsJuniors: z.string().optional().describe('`True` / `False`'),
|
supportsJuniors: z.string().optional().describe('`True` / `False`'),
|
||||||
})
|
})
|
||||||
|
|
||||||
/** `PUT /rooms/{roomId}/loadscreen` — appends one screen to the list. */
|
/** `PUT /rooms/{roomId}/loadscreen` — the posted screen replaces the whole list. */
|
||||||
export const LoadScreenRequest = z.object({
|
export const LoadScreenRequest = z.object({
|
||||||
imageName: z.string().describe('A key from the storage upload'),
|
imageName: z.string().describe('A key from the storage upload'),
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
|
|||||||
+19
-12
@@ -1752,24 +1752,31 @@ const app = new Hono<App>()
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add a load screen to a room (`LoadScreens[]` — the images shown while the room
|
// Set a room's load screen (`LoadScreens[]` — the image shown while the room loads).
|
||||||
// loads). Auth-gated (401) and owner/co-owner-only (403). Body is the `imageName`
|
// Auth-gated (401) and owner/co-owner-only (403). Body is the `imageName` form field
|
||||||
// form field plus optional `title`/`subtitle`. Appends one
|
// plus optional `title`/`subtitle`. REPLACES the list with the single posted
|
||||||
// `{ ImageName, Title, Subtitle }` to the existing list and returns the updated
|
// `{ ImageName, Title, Subtitle }` and returns the updated room in the
|
||||||
// room in the `{ success, error, value }` envelope.
|
// `{ success, error, value }` envelope.
|
||||||
|
//
|
||||||
|
// The field is an array because the client's parser wants one, but the client only
|
||||||
|
// ever renders (and only ever posts) a single screen — appending left the old screen
|
||||||
|
// in slot 0 and the new one unreachable behind it, so setting a load screen appeared
|
||||||
|
// to do nothing. Kept as an array so multi-screen support can land without a
|
||||||
|
// migration.
|
||||||
.put(
|
.put(
|
||||||
'/rooms/:roomId{[0-9]+}/loadscreen',
|
'/rooms/:roomId{[0-9]+}/loadscreen',
|
||||||
describeRoute({
|
describeRoute({
|
||||||
tags: ['Room settings'],
|
tags: ['Room settings'],
|
||||||
summary: 'Add a load screen to a room',
|
summary: 'Set a room’s load screen',
|
||||||
description: [
|
description: [
|
||||||
'APPENDS one `{ ImageName, Title, Subtitle }` to the room’s `LoadScreens` — the images',
|
'REPLACES the room’s `LoadScreens` with the single posted `{ ImageName, Title,',
|
||||||
'shown while the room loads. There is no remove or replace counterpart. Owner or',
|
'Subtitle }` — the image shown while the room loads. The field is an array (the',
|
||||||
'co-owner only (403 otherwise).',
|
'client’s parser expects one) but the client only supports a single screen, so this',
|
||||||
|
'never appends. Owner or co-owner only (403 otherwise).',
|
||||||
].join(' '),
|
].join(' '),
|
||||||
security: AUTHED,
|
security: AUTHED,
|
||||||
parameters: [roomIdParam],
|
parameters: [roomIdParam],
|
||||||
requestBody: form(LoadScreenRequest, 'The load screen to append'),
|
requestBody: form(LoadScreenRequest, 'The load screen to set'),
|
||||||
responses: {
|
responses: {
|
||||||
200: json(RoomEnvelope, 'The updated room, or a rejection with `success: false`'),
|
200: json(RoomEnvelope, 'The updated room, or a rejection with `success: false`'),
|
||||||
401: UNAUTHORIZED_RESPONSE,
|
401: UNAUTHORIZED_RESPONSE,
|
||||||
@@ -1793,8 +1800,8 @@ const app = new Hono<App>()
|
|||||||
const title = typeof body.title === 'string' ? body.title : ''
|
const title = typeof body.title === 'string' ? body.title : ''
|
||||||
const subtitle = typeof body.subtitle === 'string' ? body.subtitle : ''
|
const subtitle = typeof body.subtitle === 'string' ? body.subtitle : ''
|
||||||
|
|
||||||
const existing = Array.isArray(room.LoadScreens) ? (room.LoadScreens as unknown[]) : []
|
// The posted screen becomes the whole list — the client shows one load screen.
|
||||||
const loadScreens = [...existing, { ImageName: imageName, Title: title, Subtitle: subtitle }]
|
const loadScreens = [{ ImageName: imageName, Title: title, Subtitle: subtitle }]
|
||||||
const updated = await updateRoomFields(c.env.DB, roomId, room, { LoadScreens: loadScreens })
|
const updated = await updateRoomFields(c.env.DB, roomId, room, { LoadScreens: loadScreens })
|
||||||
await pushRoomUpdate(c, accountId, updated)
|
await pushRoomUpdate(c, accountId, updated)
|
||||||
return roomEnvelope(c, updated)
|
return roomEnvelope(c, updated)
|
||||||
|
|||||||
@@ -1258,7 +1258,7 @@ describe('rooms endpoints', () => {
|
|||||||
expect(typeof room.SupportsMobile).toBe('boolean')
|
expect(typeof room.SupportsMobile).toBe('boolean')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('PUT /rooms/:id/loadscreen appends a load screen (auth-gated, owner/co-owner-only)', async () => {
|
it('PUT /rooms/:id/loadscreen replaces the load screen (auth-gated, owner/co-owner-only)', async () => {
|
||||||
const screensOf = async (): Promise<Array<Record<string, unknown>>> => {
|
const screensOf = async (): Promise<Array<Record<string, unknown>>> => {
|
||||||
const room = (await (await SELF.fetch(`${ORIGIN}/rooms/2`)).json()) as {
|
const room = (await (await SELF.fetch(`${ORIGIN}/rooms/2`)).json()) as {
|
||||||
LoadScreens?: Array<Record<string, unknown>>
|
LoadScreens?: Array<Record<string, unknown>>
|
||||||
@@ -1278,10 +1278,8 @@ describe('rooms endpoints', () => {
|
|||||||
success: false,
|
success: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const before = (await screensOf()).length
|
// Owner sets one (imageName + title + subtitle) — the success envelope carries the
|
||||||
|
// updated room, and the posted screen is the ONLY entry.
|
||||||
// Owner adds one (imageName + title + subtitle) — appended, and the success
|
|
||||||
// envelope carries the updated room.
|
|
||||||
const added = await envOf(
|
const added = await envOf(
|
||||||
await putForm(
|
await putForm(
|
||||||
'/rooms/2/loadscreen',
|
'/rooms/2/loadscreen',
|
||||||
@@ -1290,18 +1288,17 @@ describe('rooms endpoints', () => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
expect(added).toMatchObject({ success: true })
|
expect(added).toMatchObject({ success: true })
|
||||||
expect(added.value?.LoadScreens as unknown[]).toContainEqual({
|
expect(added.value?.LoadScreens).toEqual([
|
||||||
ImageName: 'sharecamera/2026-07-15/abc.jpg',
|
{ ImageName: 'sharecamera/2026-07-15/abc.jpg', Title: 'asdf', Subtitle: 'sdf' },
|
||||||
Title: 'asdf',
|
])
|
||||||
Subtitle: 'sdf',
|
expect(await screensOf()).toHaveLength(1)
|
||||||
})
|
|
||||||
expect(await screensOf()).toHaveLength(before + 1)
|
|
||||||
|
|
||||||
// A second call appends rather than replacing; title/subtitle default to empty.
|
// A second call REPLACES rather than appending (the client renders one screen, so
|
||||||
|
// an appended one would sit unreachable behind the old); title/subtitle default to
|
||||||
|
// empty when omitted.
|
||||||
const co = await envOf(await putForm('/rooms/2/loadscreen', { imageName: 'second.jpg' }, '2'))
|
const co = await envOf(await putForm('/rooms/2/loadscreen', { imageName: 'second.jpg' }, '2'))
|
||||||
expect(co).toMatchObject({ success: true })
|
expect(co).toMatchObject({ success: true })
|
||||||
expect(await screensOf()).toHaveLength(before + 2)
|
expect(await screensOf()).toEqual([{ ImageName: 'second.jpg', Title: '', Subtitle: '' }])
|
||||||
expect(await screensOf()).toContainEqual({ ImageName: 'second.jpg', Title: '', Subtitle: '' })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('PUT /rooms/:id/accessibility sets the room-level Accessibility (auth-gated, owner/co-owner-only)', async () => {
|
it('PUT /rooms/:id/accessibility sets the room-level Accessibility (auth-gated, owner/co-owner-only)', async () => {
|
||||||
|
|||||||
@@ -222,6 +222,35 @@ export async function setRoomInstanceInProgress(
|
|||||||
return toDto(stored)
|
return toDto(stored)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flip an instance's `isPrivate` flag, rewriting the JSON blob (the generated
|
||||||
|
* `is_private` column follows it). Returns the updated DTO, or null when the
|
||||||
|
* instance doesn't exist.
|
||||||
|
*
|
||||||
|
* Marking an instance private is what closes it to strangers: {@link
|
||||||
|
* getJoinableInstance} only ever reuses instances with `is_private = 0`, so a public
|
||||||
|
* matchmake stops landing new players here the moment this is set. Everyone already
|
||||||
|
* inside stays — this shuts the door, it doesn't clear the room.
|
||||||
|
*/
|
||||||
|
export async function setRoomInstancePrivate(
|
||||||
|
db: D1Database,
|
||||||
|
id: number,
|
||||||
|
isPrivate: boolean
|
||||||
|
): Promise<RoomInstanceDto | null> {
|
||||||
|
const row = await db
|
||||||
|
.prepare('SELECT data FROM room_instance WHERE id = ?1')
|
||||||
|
.bind(id)
|
||||||
|
.first<{ data: string }>()
|
||||||
|
if (!row) return null
|
||||||
|
const stored = parse(row.data)
|
||||||
|
stored.isPrivate = isPrivate
|
||||||
|
await db
|
||||||
|
.prepare('UPDATE room_instance SET data = ?1 WHERE id = ?2')
|
||||||
|
.bind(JSON.stringify(stored), id)
|
||||||
|
.run()
|
||||||
|
return toDto(stored)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recompute an instance's `isFull` flag from live match presence: full once the
|
* Recompute an instance's `isFull` flag from live match presence: full once the
|
||||||
* number of players currently present in the instance reaches its `maxCapacity`
|
* number of players currently present in the instance reaches its `maxCapacity`
|
||||||
|
|||||||
Reference in New Issue
Block a user