mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
add a non working banlist
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
getRoomInstance,
|
||||
getRoomInstancesByRoom,
|
||||
isClubMember,
|
||||
isPlayerBannedFromRoom,
|
||||
MessageType,
|
||||
refreshInstanceFullness,
|
||||
RoomInstanceType,
|
||||
@@ -479,7 +480,8 @@ async function inviteParty(
|
||||
/**
|
||||
* Resolve a room by `:room` path segment (numeric id or name) from D1, then find a
|
||||
* joinable instance of it (public matchmakes reuse one via the `room_instance`
|
||||
* table) or create a new one. Returns null when the room isn't found.
|
||||
* table) or create a new one. Returns null when the room isn't found, or when the
|
||||
* caller is banned from it.
|
||||
*/
|
||||
async function resolveRoomInstance(
|
||||
c: Context<App>,
|
||||
@@ -495,6 +497,17 @@ async function resolveRoomInstance(
|
||||
if (!room) return null
|
||||
|
||||
const f = instanceFieldsFromRoom(room, subRoomId)
|
||||
|
||||
// A banned player never gets an instance. This is the whole enforcement of a room
|
||||
// ban: the Photon room id only ever reaches a player through a matchmake, so
|
||||
// refusing here means they have no coordinates to join or interact with. Handled
|
||||
// before any instance is created or reused so a ban can't spawn one. The caller sees
|
||||
// the same opaque NO_SUCH_ROOM every other refusal answers.
|
||||
if (await isPlayerBannedFromRoom(c.env.DB, f.roomId, ownerId)) {
|
||||
logger.info('matchmake refused: player banned from room', { roomId: f.roomId, ownerId })
|
||||
return null
|
||||
}
|
||||
|
||||
// Never place the player back into the instance they're already in: the client
|
||||
// keys the room transition off a changing `roomInstanceId`, so re-matchmaking into
|
||||
// your current instance (e.g. the only public instance of a room you're already in)
|
||||
@@ -961,6 +974,14 @@ const app = new Hono<App>()
|
||||
const instance = targetPresence?.roomInstance ?? null
|
||||
if (!instance) return c.json({ errorCode: NO_SUCH_ROOM, roomInstance: null })
|
||||
|
||||
// This path hands out a Photon room id without going through
|
||||
// resolveRoomInstance, so the room's bans have to be checked here too —
|
||||
// otherwise following a friend in is a way around a ban.
|
||||
if (await isPlayerBannedFromRoom(c.env.DB, instance.roomId, id)) {
|
||||
logger.info('follow refused: player banned from room', { roomId: instance.roomId, id })
|
||||
return c.json({ errorCode: NO_SUCH_ROOM, roomInstance: null })
|
||||
}
|
||||
|
||||
// Join that same instance (same id + Photon room) and store it as the caller's
|
||||
// presence, so the heartbeat replays it and their own friend fan-out fires.
|
||||
await enterRoom(c, id, instance)
|
||||
|
||||
@@ -1241,6 +1241,69 @@ describe('auth-gated endpoints', () => {
|
||||
|
||||
// No token → 401.
|
||||
expect((await follow(9801)).status).toBe(401)
|
||||
|
||||
// A ban on the room blocks the follow too: this path hands out a Photon room id
|
||||
// without going through resolveRoomInstance, so it carries its own ban check —
|
||||
// otherwise following a friend in would be a way around a ban.
|
||||
await env.DB.prepare(
|
||||
`INSERT INTO room_ban (room_id, banned_player_id, ban_mask, banned_by_account_id, created_at)
|
||||
VALUES (2, 9800, 0, 1, '2026-01-01T00:00:00.000Z')`
|
||||
).run()
|
||||
try {
|
||||
expect(await (await follow(9801, '9800')).json()).toEqual({
|
||||
errorCode: 20,
|
||||
roomInstance: null,
|
||||
})
|
||||
} finally {
|
||||
await env.DB.prepare('DELETE FROM room_ban WHERE room_id = 2 AND banned_player_id = 9800')
|
||||
.run()
|
||||
}
|
||||
})
|
||||
|
||||
test('POST /matchmake/room/:roomId refuses a player banned from the room', async () => {
|
||||
const matchmake = async (sub: string) =>
|
||||
(await (
|
||||
await exports.default.fetch(`${ORIGIN}/matchmake/room/2`, {
|
||||
method: 'POST',
|
||||
headers: await bearer(sub),
|
||||
})
|
||||
).json()) as { errorCode: number; roomInstance: { roomInstanceId: number } | null }
|
||||
|
||||
// Not banned yet → a normal join.
|
||||
expect((await matchmake('9700')).errorCode).toBe(0)
|
||||
|
||||
await env.DB.prepare(
|
||||
`INSERT INTO room_ban (room_id, banned_player_id, ban_mask, banned_by_account_id, created_at)
|
||||
VALUES (2, 9701, 0, 1, '2026-01-01T00:00:00.000Z')`
|
||||
).run()
|
||||
|
||||
// The ban is the whole enforcement: no instance means no Photon room id, so there
|
||||
// is nothing for the banned player to join. Same opaque NoSuchRoom as any other
|
||||
// refusal, and it applies to the subroom path as well.
|
||||
expect(await matchmake('9701')).toEqual({ errorCode: 20, roomInstance: null })
|
||||
const sub = await exports.default.fetch(`${ORIGIN}/matchmake/room/2/2`, {
|
||||
method: 'POST',
|
||||
headers: await bearer('9701'),
|
||||
})
|
||||
expect(await sub.json()).toEqual({ errorCode: 20, roomInstance: null })
|
||||
|
||||
// Refused before any instance is created, and no presence was recorded for them.
|
||||
expect(
|
||||
await env.DB.prepare('SELECT 1 AS hit FROM presence WHERE account_id = 9701').first()
|
||||
).toBeNull()
|
||||
|
||||
// The ban is per-room — another room is unaffected.
|
||||
const other = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/matchmake/room/77`, {
|
||||
method: 'POST',
|
||||
headers: await bearer('9701'),
|
||||
})
|
||||
).json()) as { errorCode: number }
|
||||
expect(other.errorCode).toBe(0)
|
||||
|
||||
// Lifting the ban lets them in again.
|
||||
await env.DB.prepare('DELETE FROM room_ban WHERE room_id = 2 AND banned_player_id = 9701').run()
|
||||
expect((await matchmake('9701')).errorCode).toBe(0)
|
||||
})
|
||||
|
||||
test('POST /matchmake/room/:id invites AdditionalPlayerIds (party) into the instance', async () => {
|
||||
|
||||
Reference in New Issue
Block a user