mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-26 07:28:53 -07:00
[api] add image moderation and v6 endpoint
This commit is contained in:
@@ -1596,29 +1596,6 @@ export const SavedImageDto = z.object({
|
||||
CommentCount: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* `GET /api/images/v6` — an image's metadata by bucket key. A third projection of the same
|
||||
* row: renamed like `ImagesPlayer` (`SavedImageId`/`SavedImageType`, no `TaggedPlayerIds`)
|
||||
* but carrying `ClubId`, and with no nullable fields — `RoomId`, `PlayerEventId` and
|
||||
* `ClubId` are 0 where the row holds null, `Description` is `""`. Don't unify it with the
|
||||
* other two.
|
||||
*/
|
||||
export const ImageMetadataDto = z.object({
|
||||
SavedImageId: z.int(),
|
||||
ImageName: z.string().describe('The bucket key the img worker serves it back by'),
|
||||
PlayerId: z.int(),
|
||||
RoomId: z.int().describe('0 when the photo was not taken in a room'),
|
||||
PlayerEventId: z.int().describe('0 when it belongs to no event'),
|
||||
ClubId: z.int().describe('Always 0 — nothing here associates an image with a club'),
|
||||
Description: z.string().describe('Empty string, never null'),
|
||||
Accessibility: z.int(),
|
||||
AccessibilityLocked: z.boolean(),
|
||||
SavedImageType: z.int().describe('1 = share camera, 3 = room, 4 = profile, …'),
|
||||
CreatedAt: z.string(),
|
||||
CheerCount: z.int(),
|
||||
CommentCount: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* The client's `ImagesPlayer` projection — the same record with `Id` → `SavedImageId`,
|
||||
* `Type` → `SavedImageType` and no `TaggedPlayerIds`. The player photo lists and feed
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
*
|
||||
* The `api` worker owns this schema/migration (migrations/0004_report.sql,
|
||||
* 0009_report_ban.sql, 0011_report_event.sql, 0016_report_invention.sql,
|
||||
* 0017_report_custom_avatar_item.sql, 0020_report_ban_audit.sql and
|
||||
* 0025_report_chat_message.sql, applied under its own `migrations_table` so it doesn't clash
|
||||
* 0017_report_custom_avatar_item.sql, 0020_report_ban_audit.sql,
|
||||
* 0025_report_chat_message.sql and 0034_report_image.sql, applied under its own `migrations_table` so it doesn't clash
|
||||
* with the other workers' migrations that share the database).
|
||||
*
|
||||
* The moderation-side READS here — `searchReports`, `getTopReported`, `getBansInForce` —
|
||||
@@ -24,7 +24,9 @@
|
||||
* CREATOR — see `POST /api/playerevents/v1/report`, `POST /api/inventions/v1/report` and
|
||||
* `POST /api/customAvatarItems/v1/{id}/report`. A reported CHAT MESSAGE is the fourth kind:
|
||||
* `chat_message_id`, with the message's SENDER as the reported player — see
|
||||
* `POST /api/chatreport/createChatReport`. The four id columns are mutually exclusive; a row
|
||||
* `POST /api/chatreport/createChatReport`. A reported IMAGE is the fifth: `image_id`, with
|
||||
* the player who took it as the reported player — see `POST /api/images/v1/{id}/report`.
|
||||
* The five id columns are mutually exclusive; a row
|
||||
* with none of them is an ordinary player report. They are separate columns rather than one
|
||||
* polymorphic id because the keys differ in TYPE (numbers and a guid) and in what they key.
|
||||
*
|
||||
@@ -42,9 +44,10 @@
|
||||
/**
|
||||
* Schema DDL (mirror of migrations/0004_report.sql + 0009_report_ban.sql +
|
||||
* 0011_report_event.sql + 0016_report_invention.sql + 0017_report_custom_avatar_item.sql +
|
||||
* 0020_report_ban_audit.sql + 0025_report_chat_message.sql + 0029_report_ban_lifted.sql).
|
||||
* 0020_report_ban_audit.sql + 0025_report_chat_message.sql + 0029_report_ban_lifted.sql +
|
||||
* 0034_report_image.sql).
|
||||
*
|
||||
* None of `event_id`, `invention_id`, `custom_avatar_item_id` or `chat_message_id` is
|
||||
* None of `event_id`, `invention_id`, `custom_avatar_item_id`, `chat_message_id` or `image_id` is
|
||||
* indexed: each is written on every report of its kind and read by nothing — no query here filters on any of them,
|
||||
* and the reads that do exist go by player or by the ban flag. 0011's partial index over
|
||||
* `event_id` was dropped in 0016 rather than mirrored. Add one back alongside the query that
|
||||
@@ -71,7 +74,8 @@ export const SCHEMA_DDL: string[] = [
|
||||
banned_at TEXT,
|
||||
chat_message_id INTEGER,
|
||||
unbanned_by_player_id INTEGER,
|
||||
unbanned_at TEXT
|
||||
unbanned_at TEXT,
|
||||
image_id INTEGER
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_report_reported ON report (reported_player_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_report_reporter ON report (reporter_player_id)`,
|
||||
@@ -150,6 +154,13 @@ export interface ReportRow {
|
||||
unbanned_by_player_id: number | null
|
||||
/** ISO-8601 UTC instant the ban was lifted; NULL with `unbanned_by_player_id`. */
|
||||
unbanned_at: string | null
|
||||
/**
|
||||
* The saved image (photo) this report is against, or NULL for any other kind — mutually
|
||||
* exclusive with the four id columns above. See `POST /api/images/v1/{id}/report`: the
|
||||
* request has no body, so `reported_player_id` is the image's `PlayerId` (who took it),
|
||||
* read from the image, and there is no category or description to store.
|
||||
*/
|
||||
image_id: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,6 +186,8 @@ export interface NewReport {
|
||||
customAvatarItemId?: string | null
|
||||
/** Set only when reporting a CHAT MESSAGE; never set alongside the three above. */
|
||||
chatMessageId?: number | null
|
||||
/** Set only when reporting an IMAGE; never set alongside the four above. */
|
||||
imageId?: number | null
|
||||
}
|
||||
|
||||
/** Record a submitted report, returning the stored row (with its assigned id). */
|
||||
@@ -184,8 +197,8 @@ export async function createReport(db: D1Database, input: NewReport): Promise<Re
|
||||
`INSERT INTO report (
|
||||
reporter_player_id, reported_player_id, report_category, details,
|
||||
height_reporter, height_reported, room_id, room_instance_type, created_at,
|
||||
event_id, invention_id, custom_avatar_item_id, chat_message_id
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
|
||||
event_id, invention_id, custom_avatar_item_id, chat_message_id, image_id
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)
|
||||
RETURNING *`
|
||||
)
|
||||
.bind(
|
||||
@@ -201,7 +214,8 @@ export async function createReport(db: D1Database, input: NewReport): Promise<Re
|
||||
input.eventId ?? null,
|
||||
input.inventionId ?? null,
|
||||
input.customAvatarItemId ?? null,
|
||||
input.chatMessageId ?? null
|
||||
input.chatMessageId ?? null,
|
||||
input.imageId ?? null
|
||||
)
|
||||
.first<ReportRow>()
|
||||
// RETURNING always yields the inserted row; the non-null assert keeps the caller
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
createImage,
|
||||
deleteImage,
|
||||
getCheeredImageIds,
|
||||
getImageById,
|
||||
getImageByName,
|
||||
getImagesByIds,
|
||||
getImagesByPlayer,
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
setImageDescription,
|
||||
SLIDESHOW_LIMIT,
|
||||
SLIDESHOW_MAX_LIMIT,
|
||||
toImageMetadata,
|
||||
toImagesPlayer,
|
||||
} from '@repo/domain'
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
ErrorResponse,
|
||||
form,
|
||||
idParam,
|
||||
ImageMetadataDto,
|
||||
ImagesPlayerDto,
|
||||
intQuery,
|
||||
json,
|
||||
@@ -47,11 +46,13 @@ import {
|
||||
SavedImageDto,
|
||||
SlideshowResponse,
|
||||
stringQuery,
|
||||
SuccessErrorEnvelope,
|
||||
SuccessResponse,
|
||||
UNAUTHORIZED_RESPONSE,
|
||||
UploadImageRequest,
|
||||
UploadImageResponse,
|
||||
} from '../openapi'
|
||||
import { createReport } from '../reports-db'
|
||||
import { exceedsApiUploadLimit, maxApiUploadBytes } from '../upload-limit'
|
||||
|
||||
import type { Context } from 'hono'
|
||||
@@ -642,23 +643,24 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
||||
}
|
||||
)
|
||||
|
||||
// Image metadata by filename. Returns the stored SavedImage record, or 404 when
|
||||
// there's no metadata row for that name.
|
||||
// Image metadata by filename, or 404 when there's no metadata row for that name.
|
||||
//
|
||||
// Serves the RAW 13-key `SavedImage`, the same type the client decodes `v6/:id` into.
|
||||
// This once answered a projection of its own (`SavedImageId`/`SavedImageType`, a
|
||||
// `ClubId`, no `TaggedPlayerIds`, nulls as 0/""): the client's DTO has `Id` and `Type`
|
||||
// and no `ClubId`, so it was silently dropping most of that.
|
||||
.get(
|
||||
'/api/images/v6',
|
||||
describeRoute({
|
||||
tags: ['Images'],
|
||||
summary: 'Image metadata by filename',
|
||||
description:
|
||||
'An image’s metadata for a bucket key. 404s when the object exists but has no ' +
|
||||
'metadata row.\n\n' +
|
||||
'Its own projection: renamed like the player lists (`SavedImageId`/`SavedImageType`, ' +
|
||||
'no `TaggedPlayerIds`) but carrying `ClubId`, and with nothing nullable — `RoomId`, ' +
|
||||
'`PlayerEventId` and `ClubId` read 0 where the row holds null, `Description` reads ' +
|
||||
'`""`. Three shapes of one row; keep them straight.',
|
||||
'An image’s stored `SavedImage` record for a bucket key — the raw record, as ' +
|
||||
'`v6/{id}` and `v5/bulk` serve, with `Description`, `RoomId` and `PlayerEventId` ' +
|
||||
'null where the row holds null. 404s when the object exists but has no metadata row.',
|
||||
parameters: [stringQuery('name', 'The image name (bucket key); required')],
|
||||
responses: {
|
||||
200: json(ImageMetadataDto, 'The image’s metadata'),
|
||||
200: json(SavedImageDto, 'The image record'),
|
||||
400: json(ErrorResponse, 'No name given'),
|
||||
404: { description: 'No metadata for that name' },
|
||||
},
|
||||
@@ -667,7 +669,85 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
||||
const name = c.req.query('name') ?? ''
|
||||
if (name === '') return c.json({ error: 'name is required' }, 400)
|
||||
const image = await getImageByName(c.env.DB, name)
|
||||
return image ? c.json(toImageMetadata(image)) : c.notFound()
|
||||
return image ? c.json(image) : c.notFound()
|
||||
}
|
||||
)
|
||||
|
||||
// One image by id — the client opening a single photo it holds only the id of.
|
||||
//
|
||||
// Serves the RAW 13-key `SavedImage` (`Id`/`Type`, `TaggedPlayerIds`, nulls left null),
|
||||
// which is the type the client decodes this route into — the same record `v6?name=`
|
||||
// serves, and `v5/bulk` a list of. NOT the `ImagesPlayer` projection of the player lists.
|
||||
//
|
||||
// Public, or the caller's own: ids are sequential, so anyone else's private photo is a
|
||||
// 404 here, the same as an id that doesn't exist.
|
||||
.get(
|
||||
'/api/images/v6/:id{[0-9]+}',
|
||||
describeRoute({
|
||||
tags: ['Images'],
|
||||
summary: 'One image by id',
|
||||
description:
|
||||
'The stored `SavedImage` record for an image id — the raw record, as `v5/bulk` ' +
|
||||
'serves, with `Description`, `RoomId` and `PlayerEventId` null where the row holds ' +
|
||||
'null. A private image is served only to the player who took it; to anyone else ' +
|
||||
'it is a 404, like an unknown id.',
|
||||
parameters: [idParam('id', 'Image id')],
|
||||
responses: {
|
||||
200: json(SavedImageDto, 'The image record'),
|
||||
404: { description: 'No such image, or not the caller’s to see' },
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const image = await getImageById(c.env.DB, Number.parseInt(c.req.param('id'), 10))
|
||||
if (image === null) return c.notFound()
|
||||
if (image.Accessibility !== 1 && image.PlayerId !== (await authedId(c))) return c.notFound()
|
||||
return c.json(image)
|
||||
}
|
||||
)
|
||||
|
||||
// Report a saved image. Stored in the `report` table the player, event, invention, item
|
||||
// and chat reports use — same moderation life — with `image_id` set. See
|
||||
// migrations/0034_report_image.sql.
|
||||
//
|
||||
// The request has NO body: the path names the image and the token names the reporter,
|
||||
// and that is the whole report. The reported player is whoever took the image, read from
|
||||
// the image. The `{ success, error: "" }` reply is the envelope the other reports use,
|
||||
// which is an ASSUMPTION here: what the client does with this response has not been
|
||||
// observed.
|
||||
.post(
|
||||
'/api/images/v1/:id{[0-9]+}/report',
|
||||
describeRoute({
|
||||
tags: ['Images', 'Moderation'],
|
||||
summary: 'Report a photo',
|
||||
description:
|
||||
'Files a report against a saved image, as a row in the same `report` table the ' +
|
||||
'player, event, invention, custom-avatar-item and chat reports go to. There is no ' +
|
||||
'request body: the row is the reporter (the caller), the image (`image_id`), and ' +
|
||||
'`reported_player_id` — the image’s `PlayerId`, who took it. No category or ' +
|
||||
'description is stored, since none is sent. Nothing dedupes the rows, and an ' +
|
||||
'unknown image is refused rather than filed against nobody.',
|
||||
security: AUTHED,
|
||||
parameters: [idParam('id', 'Image id')],
|
||||
responses: {
|
||||
200: json(SuccessErrorEnvelope, '`{ success: true, error: "" }`'),
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
404: json(SuccessErrorEnvelope, 'No such image'),
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const reporterId = await authedId(c)
|
||||
if (reporterId === null) return unauthorized(c)
|
||||
|
||||
const imageId = Number.parseInt(c.req.param('id'), 10)
|
||||
const image = await getImageById(c.env.DB, imageId)
|
||||
if (image === null) return c.json({ success: false, error: 'No such image' }, 404)
|
||||
|
||||
await createReport(c.env.DB, {
|
||||
reporterPlayerId: reporterId,
|
||||
reportedPlayerId: image.PlayerId,
|
||||
imageId,
|
||||
})
|
||||
return c.json({ success: true, error: '' })
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -6681,11 +6681,11 @@ describe('images', () => {
|
||||
// A metadata row was created, and it's readable by name via /api/images/v6.
|
||||
const meta = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/api/images/v6?name=${ImageName}`)
|
||||
).json()) as { ImageName: string; PlayerId: number; SavedImageId: number; CheerCount: number }
|
||||
).json()) as { ImageName: string; PlayerId: number; Id: number; CheerCount: number }
|
||||
expect(meta.ImageName).toBe(ImageName)
|
||||
expect(meta.PlayerId).toBe(42)
|
||||
// `SavedImageId`, not `Id` — v6 renames like the player lists do.
|
||||
expect(typeof meta.SavedImageId).toBe('number')
|
||||
// `Id`, not `SavedImageId` — v6 serves the raw record, unlike the player lists.
|
||||
expect(typeof meta.Id).toBe('number')
|
||||
expect(meta.CheerCount).toBe(0)
|
||||
})
|
||||
|
||||
@@ -7030,7 +7030,7 @@ describe('images', () => {
|
||||
expect(entries.filter((e) => e.IsCheered)).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('GET /api/images/v6 serves the metadata projection, nothing nullable', async () => {
|
||||
test('GET /api/images/v6 serves the raw SavedImage by name', async () => {
|
||||
const img = await createImage(env.DB, {
|
||||
imageName: 'v6shape.jpg',
|
||||
playerId: 7301,
|
||||
@@ -7038,24 +7038,98 @@ describe('images', () => {
|
||||
})
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/images/v6?name=v6shape.jpg`)
|
||||
expect(res.status).toBe(200)
|
||||
// The client's 13-key DTO: `Id` and `Type`, `TaggedPlayerIds`, nulls left null — the
|
||||
// same record `v6/:id` serves.
|
||||
expect(await res.json()).toEqual({
|
||||
SavedImageId: img.Id,
|
||||
Id: img.Id,
|
||||
ImageName: 'v6shape.jpg',
|
||||
Description: null,
|
||||
PlayerId: 7301,
|
||||
// Nulls on the row come out as 0 / "" — the client's DTO has no null to put there.
|
||||
RoomId: 0,
|
||||
PlayerEventId: 0,
|
||||
ClubId: 0,
|
||||
Description: '',
|
||||
RoomId: null,
|
||||
PlayerEventId: null,
|
||||
Accessibility: 1,
|
||||
AccessibilityLocked: false,
|
||||
SavedImageType: 1,
|
||||
Type: 1,
|
||||
CreatedAt: img.CreatedAt,
|
||||
TaggedPlayerIds: [],
|
||||
CheerCount: 0,
|
||||
CommentCount: 0,
|
||||
})
|
||||
})
|
||||
|
||||
test('POST /api/images/v1/:id/report files a bodiless report against the photo’s author', async () => {
|
||||
const img = await createImage(env.DB, { imageName: 'reported.jpg', playerId: 7310 })
|
||||
const report = async (id: number, sub?: string) =>
|
||||
exports.default.fetch(`${ORIGIN}/api/images/v1/${id}/report`, {
|
||||
method: 'POST',
|
||||
headers: sub ? await bearer(sub) : undefined,
|
||||
})
|
||||
|
||||
expect((await report(img.Id)).status).toBe(401)
|
||||
const unknown = await report(99999999, '7311')
|
||||
expect(unknown.status).toBe(404)
|
||||
expect(await unknown.json()).toEqual({ success: false, error: 'No such image' })
|
||||
|
||||
const res = await report(img.Id, '7311')
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({ success: true, error: '' })
|
||||
|
||||
const { results } = await env.DB.prepare('SELECT * FROM report WHERE image_id = ?1')
|
||||
.bind(img.Id)
|
||||
.all()
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0]).toMatchObject({
|
||||
reporter_player_id: 7311,
|
||||
reported_player_id: 7310,
|
||||
image_id: img.Id,
|
||||
report_category: 0,
|
||||
details: null,
|
||||
event_id: null,
|
||||
invention_id: null,
|
||||
custom_avatar_item_id: null,
|
||||
chat_message_id: null,
|
||||
})
|
||||
})
|
||||
|
||||
test('GET /api/images/v6/:id serves the raw SavedImage, private ones to their owner only', async () => {
|
||||
const img = await createImage(env.DB, { imageName: 'v6byid.jpg', playerId: 7302 })
|
||||
const get = async (id: number, sub?: string) =>
|
||||
exports.default.fetch(
|
||||
`${ORIGIN}/api/images/v6/${id}`,
|
||||
sub ? { headers: await bearer(sub) } : undefined
|
||||
)
|
||||
|
||||
// The 13-key record, nulls left null.
|
||||
const res = await get(img.Id)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
Id: img.Id,
|
||||
ImageName: 'v6byid.jpg',
|
||||
Description: null,
|
||||
PlayerId: 7302,
|
||||
RoomId: null,
|
||||
PlayerEventId: null,
|
||||
Accessibility: 1,
|
||||
AccessibilityLocked: false,
|
||||
Type: 1,
|
||||
CreatedAt: img.CreatedAt,
|
||||
TaggedPlayerIds: [],
|
||||
CheerCount: 0,
|
||||
CommentCount: 0,
|
||||
})
|
||||
expect((await get(99999999)).status).toBe(404)
|
||||
|
||||
// Made private, it is its owner's alone — ids are sequential.
|
||||
await env.DB.prepare(
|
||||
"UPDATE image SET data = json_set(data, '$.Accessibility', 0) WHERE id = ?1"
|
||||
)
|
||||
.bind(img.Id)
|
||||
.run()
|
||||
expect((await get(img.Id)).status).toBe(404)
|
||||
expect((await get(img.Id, '7303')).status).toBe(404)
|
||||
expect((await get(img.Id, '7302')).status).toBe(200)
|
||||
})
|
||||
|
||||
test('GET /api/images/v6 400s without a name and 404s for an unknown one', async () => {
|
||||
expect((await exports.default.fetch(`${ORIGIN}/api/images/v6`)).status).toBe(400)
|
||||
expect(
|
||||
@@ -7087,23 +7161,20 @@ describe('images', () => {
|
||||
const meta = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/api/images/v6?name=${ImageName}`)
|
||||
).json()) as {
|
||||
SavedImageType: number
|
||||
Type: number
|
||||
RoomId: number
|
||||
Accessibility: number
|
||||
PlayerEventId: number
|
||||
ClubId: number
|
||||
Description: string
|
||||
PlayerEventId: number | null
|
||||
TaggedPlayerIds: number[]
|
||||
}
|
||||
expect(meta.SavedImageType).toBe(1)
|
||||
expect(meta.Type).toBe(1)
|
||||
expect(meta.RoomId).toBe(777)
|
||||
expect(meta.Accessibility).toBe(2)
|
||||
// v6 carries no TaggedPlayerIds — the upload still records them, which the stored row
|
||||
// below proves. Nothing on this projection is nullable: a "none" event reads 0, not
|
||||
// null, and the club (which nothing here sets) reads 0 too.
|
||||
expect(meta).not.toHaveProperty('TaggedPlayerIds')
|
||||
expect(meta.PlayerEventId).toBe(0)
|
||||
expect(meta.ClubId).toBe(0)
|
||||
expect(meta.Description).toBe('')
|
||||
// v6 serves the stored record as it is: the tagged players ride along, a "none"
|
||||
// event is null rather than 0, and there is no `ClubId`.
|
||||
expect(meta.TaggedPlayerIds).toEqual([5, 6])
|
||||
expect(meta.PlayerEventId).toBeNull()
|
||||
expect(meta).not.toHaveProperty('ClubId')
|
||||
|
||||
// The tagged players and the null event id, as actually stored.
|
||||
const row = await env.DB.prepare('SELECT data FROM image WHERE image_name = ?1')
|
||||
@@ -7114,7 +7185,7 @@ describe('images', () => {
|
||||
PlayerEventId: number | null
|
||||
}
|
||||
expect(stored.TaggedPlayerIds).toEqual([5, 6])
|
||||
// playerEventId 0 means "none" → stored as null, and serialized back out as 0.
|
||||
// playerEventId 0 means "none" → stored as null.
|
||||
expect(stored.PlayerEventId).toBeNull()
|
||||
})
|
||||
|
||||
@@ -10011,6 +10082,7 @@ describe('openapi', () => {
|
||||
'GET /api/images/v5/cheered/bulk',
|
||||
'GET /api/images/v5/player/{playerId}',
|
||||
'GET /api/images/v6',
|
||||
'GET /api/images/v6/{id}',
|
||||
'GET /api/inventions/v1',
|
||||
'GET /api/inventions/v1/details',
|
||||
'GET /api/inventions/v1/featured',
|
||||
@@ -10095,6 +10167,7 @@ describe('openapi', () => {
|
||||
'POST /api/images/v1/cheer',
|
||||
'POST /api/images/v1/deletesaved',
|
||||
'POST /api/images/v1/modifydescription',
|
||||
'POST /api/images/v1/{id}/report',
|
||||
'POST /api/images/v2/modifyaccessibility',
|
||||
'POST /api/images/v4/uploadsaved',
|
||||
'POST /api/images/v5/cheered/bulk',
|
||||
|
||||
Reference in New Issue
Block a user