[api] add img delete and modify

This commit is contained in:
Devin Zuczek
2026-09-29 23:44:47 -04:00
parent ef5bfece59
commit a5ae46fac4
4 changed files with 264 additions and 7 deletions
+100 -2
View File
@@ -12,7 +12,9 @@ import {
getPlayerFeed,
getSlideshowImages,
SavedImageType,
setImageAccessibility,
setImageCheer,
setImageDescription,
SLIDESHOW_LIMIT,
SLIDESHOW_MAX_LIMIT,
toImageMetadata,
@@ -35,6 +37,8 @@ import {
json,
JsonArray,
jsonBody,
ModifyImageAccessibilityRequest,
ModifyImageDescriptionRequest,
pageParams,
PhotoTaggingSettingRequest,
PhotoTaggingSettingResponse,
@@ -308,8 +312,9 @@ export const imageRoutes = new Hono<App>({ strict: false })
// Delete one of the caller's saved images ({ ImageName }). Auth-gated. Looks the
// image up by name, refuses unless the caller took it (PlayerId), then removes the
// metadata row (and its cheers) and the object from R2. 404 for an unknown image,
// 403 for someone else's.
.delete(
// 403 for someone else's. A POST, not a DELETE: the client posts the JSON body to
// this path when a player deletes a photo from their gallery.
.post(
'/api/images/v1/deletesaved',
describeRoute({
tags: ['Images'],
@@ -349,6 +354,99 @@ export const imageRoutes = new Hono<App>({ strict: false })
}
)
// Make one of the caller's photos public or private ({ ImageName, Accessibility }).
// Auth-gated and owner-only, like deletesaved; additionally refuses an image whose
// accessibility moderation has locked. Answers the same `{ success: true }` ack.
.post(
'/api/images/v2/modifyaccessibility',
describeRoute({
tags: ['Images'],
summary: 'Make one of the caller’s photos public or private',
description:
'Looks the image up by name and refuses unless the caller took it (or its ' +
'accessibility is locked), then sets `Accessibility` on the stored record: 0 ' +
'private, 1 public.',
security: AUTHED,
requestBody: jsonBody(ModifyImageAccessibilityRequest, 'The image and its new accessibility'),
responses: {
200: json(SuccessResponse, 'Updated'),
400: json(ErrorResponse, 'No ImageName, or Accessibility not 0 or 1'),
401: UNAUTHORIZED_RESPONSE,
403: json(ErrorResponse, 'Not the caller’s image, or its accessibility is locked'),
404: { description: 'No image by that name' },
},
}),
async (c) => {
const id = await authedId(c)
if (id === null) return unauthorized(c)
const body = (await c.req.json().catch(() => null)) as {
ImageName?: unknown
Accessibility?: unknown
} | null
const imageName = typeof body?.ImageName === 'string' ? body.ImageName : ''
if (imageName === '') return c.json({ error: 'ImageName is required' }, 400)
const accessibility = body?.Accessibility
if (accessibility !== 0 && accessibility !== 1) {
return c.json({ error: 'Accessibility must be 0 or 1' }, 400)
}
const image = await getImageByName(c.env.DB, imageName)
if (!image) return c.notFound()
if (image.PlayerId !== id) return c.json({ error: 'Not your image' }, 403)
if (image.AccessibilityLocked) return c.json({ error: 'Accessibility is locked' }, 403)
await setImageAccessibility(c.env.DB, image, accessibility)
return c.json({ success: true })
}
)
// Set the caption on one of the caller's photos ({ ImageName, Description }).
// Auth-gated and owner-only, like deletesaved. An empty or null Description clears
// the caption (stored as null, the same as an upload without one).
.post(
'/api/images/v1/modifydescription',
describeRoute({
tags: ['Images'],
summary: 'Set the caption on one of the caller’s photos',
description:
'Looks the image up by name and refuses unless the caller took it, then sets ' +
'`Description` on the stored record. Null or empty clears it.',
security: AUTHED,
requestBody: jsonBody(ModifyImageDescriptionRequest, 'The image and its new caption'),
responses: {
200: json(SuccessResponse, 'Updated'),
400: json(ErrorResponse, 'No ImageName, or Description not a string'),
401: UNAUTHORIZED_RESPONSE,
403: json(ErrorResponse, 'Not the caller’s image'),
404: { description: 'No image by that name' },
},
}),
async (c) => {
const id = await authedId(c)
if (id === null) return unauthorized(c)
const body = (await c.req.json().catch(() => null)) as {
ImageName?: unknown
Description?: unknown
} | null
const imageName = typeof body?.ImageName === 'string' ? body.ImageName : ''
if (imageName === '') return c.json({ error: 'ImageName is required' }, 400)
const raw = body?.Description ?? null
if (raw !== null && typeof raw !== 'string') {
return c.json({ error: 'Description must be a string' }, 400)
}
const description = raw === null || raw.trim() === '' ? null : raw
const image = await getImageByName(c.env.DB, imageName)
if (!image) return c.notFound()
if (image.PlayerId !== id) return c.json({ error: 'Not your image' }, 403)
await setImageDescription(c.env.DB, image, description)
return c.json({ success: true })
}
)
// 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.