mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
more image endpoints, rooms
This commit is contained in:
+37
-1
@@ -9,7 +9,13 @@ import storefrontGiftDrop2 from '../static/storefronts-v3-giftdropstore-2.json'
|
||||
import storefrontGiftDrop3 from '../static/storefronts-v3-giftdropstore-3.json'
|
||||
import storefrontGiftDrop300 from '../static/storefronts-v3-giftdropstore-300.json'
|
||||
import { defaultSettings } from './default-settings'
|
||||
import { createImage, getImageByName } from './images-db'
|
||||
import {
|
||||
createImage,
|
||||
getImageByName,
|
||||
getImagesByPlayer,
|
||||
getImagesByRoom,
|
||||
getPlayerFeed,
|
||||
} from './images-db'
|
||||
import { validateAndGetAccountId } from './jwt'
|
||||
import { getRoomById, getRoomByName, getRoomsByCreator, getRoomsByIds } from './rooms-db'
|
||||
|
||||
@@ -482,6 +488,36 @@ const app = new Hono<App>({ strict: false })
|
||||
return c.json({ ImageName: name })
|
||||
})
|
||||
|
||||
// A room's photo feed — the public images taken in that room. `sort` orders the
|
||||
// feed (1 = most cheered, else newest) and `filter` narrows by SavedImageType
|
||||
// (0 = all). Paginated via skip/take (take defaults to 100). Returns a bare array.
|
||||
.get('/api/images/v4/room/:roomId{[0-9]+}', async (c) => {
|
||||
const roomId = Number.parseInt(c.req.param('roomId'), 10)
|
||||
const sort = Number.parseInt(c.req.query('sort') ?? '0', 10) || 0
|
||||
const filter = Number.parseInt(c.req.query('filter') ?? '0', 10) || 0
|
||||
const skip = Number.parseInt(c.req.query('skip') ?? '0', 10) || 0
|
||||
const take = Number.parseInt(c.req.query('take') ?? '100', 10) || 100
|
||||
return c.json(await getImagesByRoom(c.env.DB, roomId, sort, filter, skip, take))
|
||||
})
|
||||
|
||||
// A player's photos — the public images that player has taken, newest first.
|
||||
// Paginated via skip/take (take defaults to 100). Returns a bare array.
|
||||
.get('/api/images/v4/player/:playerId{[0-9]+}', async (c) => {
|
||||
const playerId = Number.parseInt(c.req.param('playerId'), 10)
|
||||
const skip = Number.parseInt(c.req.query('skip') ?? '0', 10) || 0
|
||||
const take = Number.parseInt(c.req.query('take') ?? '100', 10) || 100
|
||||
return c.json(await getImagesByPlayer(c.env.DB, playerId, skip, take))
|
||||
})
|
||||
|
||||
// A player's photo feed — the public images they took plus ones they're tagged
|
||||
// in, newest first. Paginated via skip/take (take defaults to 100). Bare array.
|
||||
.get('/api/images/v3/feed/player/:playerId{[0-9]+}', async (c) => {
|
||||
const playerId = Number.parseInt(c.req.param('playerId'), 10)
|
||||
const skip = Number.parseInt(c.req.query('skip') ?? '0', 10) || 0
|
||||
const take = Number.parseInt(c.req.query('take') ?? '100', 10) || 100
|
||||
return c.json(await getPlayerFeed(c.env.DB, playerId, skip, take))
|
||||
})
|
||||
|
||||
// Image metadata by filename. Returns the stored SavedImage record, or 404 when
|
||||
// there's no metadata row for that name.
|
||||
.get('/api/images/v6', async (c) => {
|
||||
|
||||
@@ -89,3 +89,92 @@ export async function getImageByName(db: D1Database, name: string): Promise<Save
|
||||
.first<ImageRow>()
|
||||
return row ? (JSON.parse(row.data) as SavedImage) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* The public images taken in a room, for the room's photo feed. Only publicly
|
||||
* accessible images (Accessibility === 1) are returned. `filter` narrows by
|
||||
* `SavedImageType` (0 = all types); `sort` orders the feed — `1` puts the most
|
||||
* cheered first (ties broken by newest), anything else is newest-first. Paginated
|
||||
* via skip/take; returns a bare array of SavedImage. The per-room set is small, so
|
||||
* the room_id index does the lookup and filtering/sorting happens in memory.
|
||||
*
|
||||
* NOTE: the exact `sort`/`filter` enum values are best guesses — the client sends
|
||||
* `sort=1&filter=1`, and this treats them as most-cheered / ShareCamera.
|
||||
*/
|
||||
export async function getImagesByRoom(
|
||||
db: D1Database,
|
||||
roomId: number,
|
||||
sort: number,
|
||||
filter: number,
|
||||
skip: number,
|
||||
take: number
|
||||
): Promise<SavedImage[]> {
|
||||
const { results } = await db
|
||||
.prepare('SELECT data FROM image WHERE room_id = ?1')
|
||||
.bind(roomId)
|
||||
.all<ImageRow>()
|
||||
let images = results
|
||||
.map((r) => JSON.parse(r.data) as SavedImage)
|
||||
.filter((img) => img.Accessibility === 1)
|
||||
|
||||
if (filter > 0) images = images.filter((img) => img.Type === filter)
|
||||
|
||||
images.sort(
|
||||
sort === 1 ? (a, b) => b.CheerCount - a.CheerCount || newestFirst(a, b) : newestFirst
|
||||
)
|
||||
|
||||
return images.slice(skip, skip + take)
|
||||
}
|
||||
|
||||
/** Newest-first order: most recent CreatedAt, ties broken by higher Id. */
|
||||
const newestFirst = (a: SavedImage, b: SavedImage) =>
|
||||
b.CreatedAt.localeCompare(a.CreatedAt) || b.Id - a.Id
|
||||
|
||||
/**
|
||||
* The public images a player has taken — their photo list, newest first.
|
||||
* Paginated via skip/take; returns a bare array of SavedImage. Uses the
|
||||
* player_id index; the per-player set is small, so filtering/sorting is in memory.
|
||||
*/
|
||||
export async function getImagesByPlayer(
|
||||
db: D1Database,
|
||||
playerId: number,
|
||||
skip: number,
|
||||
take: number
|
||||
): Promise<SavedImage[]> {
|
||||
const { results } = await db
|
||||
.prepare('SELECT data FROM image WHERE player_id = ?1')
|
||||
.bind(playerId)
|
||||
.all<ImageRow>()
|
||||
return results
|
||||
.map((r) => JSON.parse(r.data) as SavedImage)
|
||||
.filter((img) => img.Accessibility === 1)
|
||||
.sort(newestFirst)
|
||||
.slice(skip, skip + take)
|
||||
}
|
||||
|
||||
/**
|
||||
* A player's photo feed — the public images they took plus the ones they're
|
||||
* tagged in (TaggedPlayerIds). Newest first, paginated via skip/take; returns a
|
||||
* bare array of SavedImage. The tagged-in match uses json_each over the stored
|
||||
* TaggedPlayerIds array (there's no index for it).
|
||||
*/
|
||||
export async function getPlayerFeed(
|
||||
db: D1Database,
|
||||
playerId: number,
|
||||
skip: number,
|
||||
take: number
|
||||
): Promise<SavedImage[]> {
|
||||
const { results } = await db
|
||||
.prepare(
|
||||
`SELECT data FROM image
|
||||
WHERE player_id = ?1
|
||||
OR EXISTS (SELECT 1 FROM json_each(image.data, '$.TaggedPlayerIds') WHERE value = ?1)`
|
||||
)
|
||||
.bind(playerId)
|
||||
.all<ImageRow>()
|
||||
return results
|
||||
.map((r) => JSON.parse(r.data) as SavedImage)
|
||||
.filter((img) => img.Accessibility === 1)
|
||||
.sort(newestFirst)
|
||||
.slice(skip, skip + take)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../api.app'
|
||||
import { SCHEMA_DDL as IMAGES_SCHEMA_DDL } from '../../images-db'
|
||||
|
||||
import type { Env } from '../../context'
|
||||
import type { SavedImage } from '../../images-db'
|
||||
|
||||
declare module 'cloudflare:test' {
|
||||
interface ProvidedEnv extends Env {}
|
||||
@@ -493,4 +494,119 @@ describe('images', () => {
|
||||
})
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
test('GET /api/images/v4/room/:id returns a public room feed, filtered/sorted/paginated', async () => {
|
||||
// Seed images in room 54: two public (one with more cheers, of different
|
||||
// types), one private (hidden), and one in another room (excluded).
|
||||
const seed = (img: Partial<SavedImage> & { Id: number }) =>
|
||||
env.DB.prepare('INSERT INTO image (data) VALUES (?1)').bind(
|
||||
JSON.stringify({
|
||||
Type: 1,
|
||||
Accessibility: 1,
|
||||
AccessibilityLocked: false,
|
||||
ImageName: `img${img.Id}.jpg`,
|
||||
Description: null,
|
||||
PlayerId: 42,
|
||||
TaggedPlayerIds: [],
|
||||
RoomId: 54,
|
||||
PlayerEventId: null,
|
||||
CreatedAt: '2026-01-01T00:00:00.000Z',
|
||||
CheerCount: 0,
|
||||
CommentCount: 0,
|
||||
...img,
|
||||
})
|
||||
)
|
||||
await env.DB.batch([
|
||||
seed({ Id: 101, CheerCount: 5, CreatedAt: '2026-02-01T00:00:00.000Z' }),
|
||||
seed({ Id: 102, CheerCount: 9, CreatedAt: '2026-01-15T00:00:00.000Z', Type: 3 }),
|
||||
seed({ Id: 103, Accessibility: 0 }), // private → hidden from the public feed
|
||||
seed({ Id: 104, RoomId: 99 }), // different room → excluded
|
||||
])
|
||||
|
||||
// sort=1 → most cheered first (102 has 9, 101 has 5).
|
||||
const top = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/api/images/v4/room/54?sort=1&filter=0&take=100&skip=0`)
|
||||
).json()) as SavedImage[]
|
||||
expect(top.map((i) => i.Id)).toEqual([102, 101])
|
||||
|
||||
// sort=0 → newest first (101 is more recent than 102).
|
||||
const newest = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/api/images/v4/room/54?sort=0`)
|
||||
).json()) as SavedImage[]
|
||||
expect(newest.map((i) => i.Id)).toEqual([101, 102])
|
||||
|
||||
// filter=1 (ShareCamera) drops the Type-3 image (102).
|
||||
const filtered = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/api/images/v4/room/54?filter=1`)
|
||||
).json()) as SavedImage[]
|
||||
expect(filtered.map((i) => i.Id)).toEqual([101])
|
||||
|
||||
// take/skip paginate.
|
||||
const page = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/api/images/v4/room/54?sort=1&take=1&skip=1`)
|
||||
).json()) as SavedImage[]
|
||||
expect(page.map((i) => i.Id)).toEqual([101])
|
||||
|
||||
// A room with no images → empty array.
|
||||
expect(
|
||||
await (await exports.default.fetch(`${ORIGIN}/api/images/v4/room/12345`)).json()
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
test('GET /api/images/v4/player/:id and v3/feed/player/:id return the player photos + feed', async () => {
|
||||
const seed = (img: Partial<SavedImage> & { Id: number }) =>
|
||||
env.DB.prepare('INSERT INTO image (data) VALUES (?1)').bind(
|
||||
JSON.stringify({
|
||||
Type: 1,
|
||||
Accessibility: 1,
|
||||
AccessibilityLocked: false,
|
||||
ImageName: `p${img.Id}.jpg`,
|
||||
Description: null,
|
||||
PlayerId: 700,
|
||||
TaggedPlayerIds: [],
|
||||
RoomId: null,
|
||||
PlayerEventId: null,
|
||||
CreatedAt: '2026-01-01T00:00:00.000Z',
|
||||
CheerCount: 0,
|
||||
CommentCount: 0,
|
||||
...img,
|
||||
})
|
||||
)
|
||||
await env.DB.batch([
|
||||
// Player 700's own photos (newest last so ordering is exercised).
|
||||
seed({ Id: 201, PlayerId: 700, CreatedAt: '2026-03-01T00:00:00.000Z' }),
|
||||
seed({ Id: 202, PlayerId: 700, CreatedAt: '2026-04-01T00:00:00.000Z' }),
|
||||
seed({ Id: 203, PlayerId: 700, Accessibility: 0 }), // private → hidden
|
||||
// Taken by someone else, but player 700 is tagged in it → feed only.
|
||||
seed({ Id: 204, PlayerId: 999, TaggedPlayerIds: [700], CreatedAt: '2026-05-01T00:00:00.000Z' }),
|
||||
// Unrelated to 700 → in neither.
|
||||
seed({ Id: 205, PlayerId: 999, TaggedPlayerIds: [111] }),
|
||||
])
|
||||
|
||||
// v4/player → only photos 700 *took*, public, newest first.
|
||||
const mine = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/api/images/v4/player/700`)
|
||||
).json()) as SavedImage[]
|
||||
expect(mine.map((i) => i.Id)).toEqual([202, 201])
|
||||
|
||||
// take paginates.
|
||||
const one = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/api/images/v4/player/700?take=1`)
|
||||
).json()) as SavedImage[]
|
||||
expect(one.map((i) => i.Id)).toEqual([202])
|
||||
|
||||
// v3/feed/player → photos taken *or* tagged in, newest first (204 is newest).
|
||||
const feed = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/api/images/v3/feed/player/700?take=100`)
|
||||
).json()) as SavedImage[]
|
||||
expect(feed.map((i) => i.Id)).toEqual([204, 202, 201])
|
||||
|
||||
// A player with no photos → empty array on both.
|
||||
expect(await (await exports.default.fetch(`${ORIGIN}/api/images/v4/player/424242`)).json()).toEqual(
|
||||
[]
|
||||
)
|
||||
expect(
|
||||
await (await exports.default.fetch(`${ORIGIN}/api/images/v3/feed/player/424242`)).json()
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user