mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 15:11:29 -07:00
misc fixes to clubs/moderation/room schema
This commit is contained in:
@@ -24,6 +24,18 @@ export const gameplayRoutes = new Hono<App>({ strict: false })
|
|||||||
.post('/api/objectives/v1/updateobjective', (c) => c.body(null, 200))
|
.post('/api/objectives/v1/updateobjective', (c) => c.body(null, 200))
|
||||||
.get('/api/communityboard/v2/current', (c) => c.json({})) // TODO: hydrate from JSON/communityboard.json
|
.get('/api/communityboard/v2/current', (c) => c.json({})) // TODO: hydrate from JSON/communityboard.json
|
||||||
.get('/api/playerevents/v1/all', (c) => c.json({ Created: [], Responses: [] }))
|
.get('/api/playerevents/v1/all', (c) => c.json({ Created: [], Responses: [] }))
|
||||||
|
|
||||||
|
// Player events for a set of clubs (`?id=1&id=2`) — the events shelf on a club's
|
||||||
|
// page. A bare array: the client deserializes this one as a list, and chokes on the
|
||||||
|
// `{ ContinuationToken, Events }` envelope the single-club form uses. No
|
||||||
|
// player-event storage yet, so the feed is empty.
|
||||||
|
.get('/api/playerevents/v1/clubs', (c) => c.json([]))
|
||||||
|
|
||||||
|
// The same feed for a single club (`/club/1`) — the form the reference serves,
|
||||||
|
// which *does* wrap the events with a paging cursor (empty = no next page).
|
||||||
|
.get('/api/playerevents/v1/club/:clubId{[0-9]+}', (c) =>
|
||||||
|
c.json({ ContinuationToken: '', Events: [] })
|
||||||
|
)
|
||||||
.get('/api/announcement/v1/get', (c) => c.json([])) // TODO: hydrate from JSON/announcements.json
|
.get('/api/announcement/v1/get', (c) => c.json([])) // TODO: hydrate from JSON/announcements.json
|
||||||
|
|
||||||
// GameSight attribution/analytics event sink. Accept and ack without persisting.
|
// GameSight attribution/analytics event sink. Accept and ack without persisting.
|
||||||
|
|||||||
@@ -4,15 +4,21 @@ import type { App } from '../context'
|
|||||||
|
|
||||||
// ---- Player reporting ------------------------------------------------------
|
// ---- Player reporting ------------------------------------------------------
|
||||||
export const moderationRoutes = new Hono<App>({ strict: false })
|
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. `ReportCategory` is
|
||||||
|
// -1 (no category) rather than 0, which is a real category; `Message` is null, not
|
||||||
|
// an empty string — the client distinguishes "no message" from a blank one.
|
||||||
.get('/api/PlayerReporting/v1/moderationBlockDetails', (c) =>
|
.get('/api/PlayerReporting/v1/moderationBlockDetails', (c) =>
|
||||||
c.json({
|
c.json({
|
||||||
ReportCategory: 0,
|
ReportCategory: -1,
|
||||||
Duration: 0,
|
Duration: 0,
|
||||||
GameSessionId: 0,
|
GameSessionId: 0,
|
||||||
IsHostKick: false,
|
|
||||||
Message: '',
|
|
||||||
PlayerIdReporter: null,
|
|
||||||
IsBan: false,
|
IsBan: false,
|
||||||
|
IsHostKick: false,
|
||||||
|
IsVoiceModAutoban: false,
|
||||||
|
Message: null,
|
||||||
|
PlayerIdReporter: null,
|
||||||
|
TimeoutStartedAt: null,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.get('/api/PlayerReporting/v1/voteToKickReasons', (c) => c.json([])) // TODO: hydrate from JSON/vtkreasons.json
|
.get('/api/PlayerReporting/v1/voteToKickReasons', (c) => c.json([])) // TODO: hydrate from JSON/vtkreasons.json
|
||||||
|
|||||||
@@ -4,18 +4,26 @@ import { parseFormIds, queryIds } from '../http'
|
|||||||
|
|
||||||
import type { App } from '../context'
|
import type { App } from '../context'
|
||||||
|
|
||||||
/** Default reputation for an account — the fallback used with no DB. */
|
/**
|
||||||
|
* Default reputation for an account — the fallback used with no DB. Nobody has
|
||||||
|
* earned cheers yet, so every counter is 0 and everyone has their full cheer credit.
|
||||||
|
* `SelectedCheer` is an int (0 = none selected), not null, and `IsCheerful` is true:
|
||||||
|
* the client reads it to decide whether the player may hand out cheers at all.
|
||||||
|
*/
|
||||||
function defaultReputation(id: number) {
|
function defaultReputation(id: number) {
|
||||||
return {
|
return {
|
||||||
AccountId: id,
|
AccountId: id,
|
||||||
|
IsCheerful: true,
|
||||||
Noteriety: 0,
|
Noteriety: 0,
|
||||||
|
SelectedCheer: 0,
|
||||||
|
CheerCredit: 20,
|
||||||
CheerGeneral: 0,
|
CheerGeneral: 0,
|
||||||
CheerHelpful: 0,
|
CheerHelpful: 0,
|
||||||
CheerCreative: 0,
|
CheerCreative: 0,
|
||||||
CheerGreatHost: 0,
|
CheerGreatHost: 0,
|
||||||
CheerSportsman: 0,
|
CheerSportsman: 0,
|
||||||
CheerCredit: 20,
|
SubscriberCount: 0,
|
||||||
SelectedCheer: null,
|
SubscribedCount: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,11 +154,61 @@ describe('public endpoints', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('GET /api/playerReputation/v2/bulk?id= returns a reputation per id', async () => {
|
test('GET /api/playerReputation/v2/bulk?id= returns a reputation per id', async () => {
|
||||||
const res = await exports.default.fetch(`${ORIGIN}/api/playerReputation/v2/bulk?id=1&id=2`)
|
const res = await exports.default.fetch(`${ORIGIN}/api/playerReputation/v2/bulk?id=1380`)
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
const reps = (await res.json()) as Array<{ AccountId: number; CheerCredit: number }>
|
// The full reputation shape the client expects, field for field.
|
||||||
|
expect(await res.json()).toEqual([
|
||||||
|
{
|
||||||
|
AccountId: 1380,
|
||||||
|
IsCheerful: true,
|
||||||
|
Noteriety: 0,
|
||||||
|
SelectedCheer: 0,
|
||||||
|
CheerCredit: 20,
|
||||||
|
CheerGeneral: 0,
|
||||||
|
CheerHelpful: 0,
|
||||||
|
CheerCreative: 0,
|
||||||
|
CheerGreatHost: 0,
|
||||||
|
CheerSportsman: 0,
|
||||||
|
SubscriberCount: 0,
|
||||||
|
SubscribedCount: 0,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const many = await exports.default.fetch(`${ORIGIN}/api/playerReputation/v2/bulk?id=1&id=2`)
|
||||||
|
const reps = (await many.json()) as Array<{ AccountId: number }>
|
||||||
expect(reps.map((r) => r.AccountId)).toEqual([1, 2])
|
expect(reps.map((r) => r.AccountId)).toEqual([1, 2])
|
||||||
expect(reps[0]).toMatchObject({ CheerCredit: 20 })
|
})
|
||||||
|
|
||||||
|
test('GET /api/playerevents/v1/clubs returns an empty event list', async () => {
|
||||||
|
// The client deserializes this as a bare array — an envelope here fails with
|
||||||
|
// "expected:'[', actual:'{'". No player-event storage yet → empty.
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/api/playerevents/v1/clubs?id=1&id=2`)
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toEqual([])
|
||||||
|
|
||||||
|
// The single-club form does wrap its events with a paging cursor.
|
||||||
|
const one = await exports.default.fetch(`${ORIGIN}/api/playerevents/v1/club/1`)
|
||||||
|
expect(one.status).toBe(200)
|
||||||
|
expect(await one.json()).toEqual({ ContinuationToken: '', Events: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('GET /api/PlayerReporting/v1/moderationBlockDetails reports "not blocked"', async () => {
|
||||||
|
const res = await exports.default.fetch(
|
||||||
|
`${ORIGIN}/api/PlayerReporting/v1/moderationBlockDetails`
|
||||||
|
)
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
// ReportCategory -1 = no category (0 is a real one), and Message is null.
|
||||||
|
expect(await res.json()).toEqual({
|
||||||
|
ReportCategory: -1,
|
||||||
|
Duration: 0,
|
||||||
|
GameSessionId: 0,
|
||||||
|
IsBan: false,
|
||||||
|
IsHostKick: false,
|
||||||
|
IsVoiceModAutoban: false,
|
||||||
|
Message: null,
|
||||||
|
PlayerIdReporter: null,
|
||||||
|
TimeoutStartedAt: null,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('POST /api/playerReputation/v2/bulk returns a reputation per id', async () => {
|
test('POST /api/playerReputation/v2/bulk returns a reputation per id', async () => {
|
||||||
|
|||||||
@@ -166,7 +166,10 @@ export async function createRoomInstance(
|
|||||||
joinDisabled: input.joinDisabled ?? false,
|
joinDisabled: input.joinDisabled ?? false,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
await db.prepare('INSERT INTO room_instance (data) VALUES (?1)').bind(JSON.stringify(stored)).run()
|
await db
|
||||||
|
.prepare('INSERT INTO room_instance (data) VALUES (?1)')
|
||||||
|
.bind(JSON.stringify(stored))
|
||||||
|
.run()
|
||||||
return toDto(stored)
|
return toDto(stored)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,19 +241,25 @@ export async function refreshInstanceFullness(
|
|||||||
* The oldest joinable public instance of a room (not private, not full, joins
|
* The oldest joinable public instance of a room (not private, not full, joins
|
||||||
* enabled, not already in progress), or null when there's none to join. Used by
|
* enabled, not already in progress), or null when there's none to join. Used by
|
||||||
* matchmaking to reuse an existing instance before creating a new one.
|
* matchmaking to reuse an existing instance before creating a new one.
|
||||||
|
*
|
||||||
|
* A room's subrooms are separate places, so `subRoomId` scopes the search: joining
|
||||||
|
* subroom 35 must never drop you into a live instance of subroom 1. Omitting it
|
||||||
|
* matches any subroom.
|
||||||
*/
|
*/
|
||||||
export async function getJoinableInstance(
|
export async function getJoinableInstance(
|
||||||
db: D1Database,
|
db: D1Database,
|
||||||
roomId: number
|
roomId: number,
|
||||||
|
subRoomId?: number
|
||||||
): Promise<RoomInstanceDto | null> {
|
): Promise<RoomInstanceDto | null> {
|
||||||
|
const bySubRoom = subRoomId === undefined ? '' : 'AND sub_room_id = ?2'
|
||||||
const row = await db
|
const row = await db
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT data FROM room_instance
|
`SELECT data FROM room_instance
|
||||||
WHERE room_id = ?1 AND is_private = 0 AND is_full = 0 AND join_disabled = 0
|
WHERE room_id = ?1 AND is_private = 0 AND is_full = 0 AND join_disabled = 0
|
||||||
AND is_in_progress = 0
|
AND is_in_progress = 0 ${bySubRoom}
|
||||||
ORDER BY id LIMIT 1`
|
ORDER BY id LIMIT 1`
|
||||||
)
|
)
|
||||||
.bind(roomId)
|
.bind(...(subRoomId === undefined ? [roomId] : [roomId, subRoomId]))
|
||||||
.first<{ data: string }>()
|
.first<{ data: string }>()
|
||||||
return row ? toDto(parse(row.data)) : null
|
return row ? toDto(parse(row.data)) : null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user