mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 23:21:30 -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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user