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:
devin
2026-08-18 23:07:24 -04:00
committed by Devin Zuczek
parent 66c09806f9
commit 178d3b5b0e
162 changed files with 114930 additions and 469 deletions
+102 -9
View File
@@ -163,6 +163,9 @@ export async function setImageCheer(
await syncImageCheerCount(db, savedImageId)
}
/** How many image ids one cheer lookup may bind: D1's 100-parameter cap, less the player id. */
const CHEER_ID_LIMIT = 99
/**
* Which of the given saved-image ids the player has cheered — the set of cheered
* ids (a subset of `ids`). Backs the bulk `cheered` lookup. Empty input → empty set.
@@ -172,16 +175,23 @@ export async function getCheeredImageIds(
playerId: number,
ids: number[]
): Promise<Set<number>> {
if (ids.length === 0) return new Set()
const inList = ids.map((_, i) => `?${i + 2}`).join(',')
const { results } = await db
.prepare(
`SELECT saved_image_id AS id FROM image_interaction
const cheered = new Set<number>()
// D1 caps a query at 100 bound parameters and the player id takes one of them, so a
// photo grid asking about more images than that is split across queries rather than
// failing the whole read. The client really does send a page of ~100 at a time.
for (let i = 0; i < ids.length; i += CHEER_ID_LIMIT) {
const page = ids.slice(i, i + CHEER_ID_LIMIT)
const inList = page.map((_, n) => `?${n + 2}`).join(',')
const { results } = await db
.prepare(
`SELECT saved_image_id AS id FROM image_interaction
WHERE player_id = ?1 AND cheered = 1 AND saved_image_id IN (${inList})`
)
.bind(playerId, ...ids)
.all<{ id: number }>()
return new Set(results.map((r) => r.id))
)
.bind(playerId, ...page)
.all<{ id: number }>()
for (const row of results) cheered.add(row.id)
}
return cheered
}
/** Look up an image record by its ImageName (the R2 key / filename), or null. */
@@ -214,6 +224,40 @@ export async function getSavedImagesByNames(
)
}
/** How many image ids one bulk lookup may bind: D1 caps a query at 100 parameters. */
const IMAGE_ID_LIMIT = 100
/**
* Look up image records by id — the bulk lookup behind `GET /api/images/v5/bulk`.
* Returned in REQUEST order, so the caller can line the answers up with what it asked
* for; an id with no record, or one that isn't public, is simply absent rather than a
* hole in the list.
*
* Public-only, like every other image read here ({@link getImagesByPlayer},
* {@link getImagesByRoom}). Image ids are sequential, so serving whatever an id names
* would make a private photo readable by anyone who counts.
*/
export async function getImagesByIds(db: D1Database, ids: number[]): Promise<SavedImage[]> {
if (ids.length === 0) return []
const found = new Map<number, SavedImage>()
// D1 caps a query at 100 bound parameters, and the client asks about a whole photo
// grid at once, so a large request is split rather than failing outright.
for (let i = 0; i < ids.length; i += IMAGE_ID_LIMIT) {
const page = ids.slice(i, i + IMAGE_ID_LIMIT)
const { results } = await db
.prepare(`SELECT data FROM image WHERE id IN (${placeholders(page.length)})`)
.bind(...page)
.all<ImageRow>()
for (const row of results) {
const image = JSON.parse(row.data) as SavedImage
if (image.Accessibility === 1) found.set(image.Id, image)
}
}
return ids.map((id) => found.get(id)).filter((image): image is SavedImage => image !== undefined)
}
/**
* A minimal SavedImage for an image name with no metadata row — enough for the client
* to render the picture. Uploads normally write a row first, so this only covers a
@@ -311,6 +355,55 @@ export async function getImagesByPlayer(
.slice(skip, skip + take)
}
/**
* An image's metadata as `GET /api/images/v6` serves it — the by-name lookup's shape.
*
* A THIRD projection of the same row, and deliberately not either of the other two: it
* renames like `ImagesPlayer` (`Id` → `SavedImageId`, `Type` → `SavedImageType`, no
* `TaggedPlayerIds`) but adds `ClubId`, and its numbers and strings are never null —
* `RoomId`, `PlayerEventId` and `ClubId` come out as 0 and `Description` as `""` where the
* row holds null. The reference's DTO declares them non-nullable, so a null is a decode
* failure rather than "none".
*
* `ClubId` is always 0: nothing here associates an image with a club.
*/
export interface ImageMetadata {
SavedImageId: number
ImageName: string
PlayerId: number
RoomId: number
PlayerEventId: number
ClubId: number
Description: string
Accessibility: number
AccessibilityLocked: boolean
SavedImageType: number
CreatedAt: string
CheerCount: number
CommentCount: number
}
/** Project a stored image into the {@link ImageMetadata} shape `/api/images/v6` answers. */
export function toImageMetadata(img: SavedImage): ImageMetadata {
return {
SavedImageId: img.Id,
ImageName: img.ImageName,
PlayerId: img.PlayerId,
// Null means "not taken in a room" / "no event"; the client's DTO has no null to put
// there, and 0 is the id it treats as none.
RoomId: img.RoomId ?? 0,
PlayerEventId: img.PlayerEventId ?? 0,
ClubId: 0,
Description: img.Description ?? '',
Accessibility: img.Accessibility,
AccessibilityLocked: img.AccessibilityLocked,
SavedImageType: img.Type,
CreatedAt: img.CreatedAt,
CheerCount: img.CheerCount,
CommentCount: img.CommentCount,
}
}
/**
* The client-facing projection of a saved image for the player photo lists (the
* reference's `ImagesPlayer`). Same data as the stored record, but the id and type