update moderationblockdetails to work on both clients for now

This commit is contained in:
Devin Zuczek
2026-07-29 12:47:17 -04:00
parent 5d70329067
commit a69f2a5dac
3 changed files with 43 additions and 32 deletions
+3 -2
View File
@@ -391,10 +391,11 @@ export const SubscriptionResponse = z.object({
// ---- Moderation ------------------------------------------------------------
/**
* `GET /api/PlayerReporting/v1/moderationBlockDetails` — always the "not blocked"
* `GET|POST /api/PlayerReporting/v1/moderationBlockDetails` — always the "not blocked"
* answer (no ban storage yet), mirroring the reference server's stub
* `ReturnModerationBlockDetails()`. `ReportCategory` is `Unknown` (-1) rather than 0,
* which is a real category, and `Message` is the empty string the reference sends.
* which is a real category, and `Message` is null — the client distinguishes "no
* message" from a blank one, so we send null where the reference sends an empty string.
* `IsVoiceModAutoban`/`TimeoutStartedAt` are on the DTO but unset by that stub, so
* they carry their C# defaults (false / null).
*/
+14 -9
View File
@@ -16,22 +16,27 @@ import type { App } from '../context'
export const moderationRoutes = new Hono<App>({ strict: false })
// Whether the caller is currently blocked (banned / timed out / host-kicked). No ban
// storage yet, so this is always the "not blocked" answer — the reference server's
// stub `ReturnModerationBlockDetails()` verbatim. `ReportCategory` is `Unknown` (-1),
// not 0, which is a real category, and `Message` is the empty string that stub sends.
// stub `ReturnModerationBlockDetails()`. `ReportCategory` is `Unknown` (-1), not 0,
// which is a real category. `Message` is null rather than the empty string that stub
// sends: the client distinguishes "no message" from a blank one.
// `IsVoiceModAutoban`/`TimeoutStartedAt` are on the DTO but left unset there, so they
// go out with their C# defaults.
// POST with no body the client's actual call, despite this being a pure read.
.post(
// POST with no body is the client's actual call, despite this being a pure read; it
// answers GET too, so the path is reachable either way.
.on(
['GET', 'POST'],
'/api/PlayerReporting/v1/moderationBlockDetails',
describeRoute({
tags: ['Moderation'],
summary: 'Whether the caller is blocked',
description:
'Ban / timeout / host-kick state for the caller. There is no ban storage yet, so ' +
'this is always the “not blocked” answer, matching the reference servers stub: ' +
'`ReportCategory` is `Unknown` (-1) rather than 0, which is a real category, and ' +
'`Message` is an empty string. `IsVoiceModAutoban` and `TimeoutStartedAt` are on ' +
'the DTO but unset by that stub, so they carry their defaults.',
'this is always the “not blocked” answer, following the reference servers stub: ' +
'`ReportCategory` is `Unknown` (-1) rather than 0, which is a real category. ' +
'`Message` is null rather than the empty string that stub sends — the client ' +
'distinguishes “no message” from a blank one. `IsVoiceModAutoban` and ' +
'`TimeoutStartedAt` are on the DTO but unset by that stub, so they carry their ' +
'defaults.',
responses: { 200: json(ModerationBlockDetails, 'Always “not blocked”') },
}),
(c) =>
@@ -42,7 +47,7 @@ export const moderationRoutes = new Hono<App>({ strict: false })
IsBan: false,
IsHostKick: false,
IsVoiceModAutoban: false,
Message: '',
Message: null,
PlayerIdReporter: null,
TimeoutStartedAt: null,
})
+26 -21
View File
@@ -233,27 +233,31 @@ describe('public endpoints', () => {
expect(await res.json()).toEqual([])
})
test('POST /api/PlayerReporting/v1/moderationBlockDetails reports "not blocked"', async () => {
// The client POSTs this with no body, despite it being a pure read.
const res = await exports.default.fetch(
`${ORIGIN}/api/PlayerReporting/v1/moderationBlockDetails`,
{ method: 'POST' }
)
expect(res.status).toBe(200)
// The reference server's stub verbatim: ReportCategory -1 = Unknown (0 is a real
// category) and an empty-string Message.
expect(await res.json()).toEqual({
ReportCategory: -1,
Duration: 0,
GameSessionId: 0,
IsBan: false,
IsHostKick: false,
IsVoiceModAutoban: false,
Message: '',
PlayerIdReporter: null,
TimeoutStartedAt: null,
})
})
// The client POSTs this with no body, despite it being a pure read; the route answers
// GET as well, and both methods serve the same body.
test.each(['GET', 'POST'])(
'%s /api/PlayerReporting/v1/moderationBlockDetails reports "not blocked"',
async (method) => {
const res = await exports.default.fetch(
`${ORIGIN}/api/PlayerReporting/v1/moderationBlockDetails`,
{ method }
)
expect(res.status).toBe(200)
// ReportCategory -1 = Unknown (0 is a real category). Message is null, not the
// reference stub's empty string — the client tells "no message" from a blank one.
expect(await res.json()).toEqual({
ReportCategory: -1,
Duration: 0,
GameSessionId: 0,
IsBan: false,
IsHostKick: false,
IsVoiceModAutoban: false,
Message: null,
PlayerIdReporter: null,
TimeoutStartedAt: null,
})
}
)
// Unauthenticated by design — the client posts this before it has an account, so
// there's no bearer token to check and nothing to attribute the id to.
@@ -1899,6 +1903,7 @@ describe('openapi', () => {
)
expect([...documented].sort()).toEqual([
'DELETE /api/images/v1/deletesaved',
'GET /api/PlayerReporting/v1/moderationBlockDetails',
'GET /api/PlayerReporting/v1/voteToKickReasons',
'GET /api/activities/charades/v1/words/{activity}',
'GET /api/announcement/v1/get',