diff --git a/src/database/collections.ts b/src/database/collections.ts index 4ce29563..58a827e9 100644 --- a/src/database/collections.ts +++ b/src/database/collections.ts @@ -6,7 +6,6 @@ import type { GameModel } from './models/game.model' import type { KeyModel } from './models/key.model' import type { MapPoolEntry } from './models/map-pool-entry.model' import type { OnlinePlayerModel } from './models/online-player.model' -import type { PlayerBanModel } from './models/player-ban.model' import type { PlayerModel } from './models/player.model' import type { QueueFriendshipModel } from './models/queue-friendship.model' import type { QueueMapOptionModel } from './models/queue-map-option.model' @@ -27,7 +26,6 @@ export const collections = { maps: database.collection('maps'), onlinePlayers: database.collection('onlineplayers'), players: database.collection('players'), - playerBans: database.collection('playerbans'), queueFriends: database.collection('queue.friends'), queueSlots: database.collection('queue.slots'), queueState: database.collection('queue.state'), diff --git a/src/database/models/player-ban.model.ts b/src/database/models/player-ban.model.ts deleted file mode 100644 index d854e51b..00000000 --- a/src/database/models/player-ban.model.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ObjectId } from 'mongodb' - -export interface PlayerBanModel { - player: ObjectId - admin: ObjectId - start: Date - end: Date - reason: string -} diff --git a/src/database/models/player.model.ts b/src/database/models/player.model.ts index 8d923ce4..33d94852 100644 --- a/src/database/models/player.model.ts +++ b/src/database/models/player.model.ts @@ -1,3 +1,4 @@ +import type { Bot } from '../../shared/types/bot' import type { SteamId64 } from '../../shared/types/steam-id-64' import { Tf2ClassName } from '../../shared/types/tf2-class-name' import type { GameNumber } from './game.model' @@ -17,6 +18,13 @@ export interface PlayerPreferences { soundVolume?: number } +export interface PlayerBan { + actor: SteamId64 | Bot + start: Date + end: Date + reason: string +} + export interface PlayerModel { name: string steamId: SteamId64 @@ -30,4 +38,5 @@ export interface PlayerModel { skill?: Partial> preReadyUntil?: Date preferences: PlayerPreferences + bans?: PlayerBan[] } diff --git a/src/events.ts b/src/events.ts index 5be1e771..b4fa2e1e 100644 --- a/src/events.ts +++ b/src/events.ts @@ -5,12 +5,11 @@ import { logger } from './logger' import type { QueueSlotModel } from './database/models/queue-slot.model' import { QueueState } from './database/models/queue-state.model' import type { GameModel, GameNumber } from './database/models/game.model' -import type { PlayerModel } from './database/models/player.model' +import type { PlayerBan, PlayerModel } from './database/models/player.model' import type { MapPoolEntry } from './database/models/map-pool-entry.model' import type { StaticGameServerModel } from './database/models/static-game-server.model' import type { LogMessage } from './log-receiver/parse-log-message' import type { Tf2Team } from './shared/types/tf2-team' -import type { PlayerBanModel } from './database/models/player-ban.model' import type { StreamModel } from './database/models/stream.model' import type { Bot } from './shared/types/bot' import type { PlayerConnectionStatus } from './database/models/game-slot.model' @@ -114,10 +113,12 @@ export interface Events { after: PlayerModel } 'player/ban:added': { - ban: PlayerBanModel + player: SteamId64 + ban: PlayerBan } 'player/ban:revoked': { - ban: PlayerBanModel + player: SteamId64 + ban: PlayerBan admin: SteamId64 } diff --git a/src/migrations/004-remove-playerbans.ts b/src/migrations/004-remove-playerbans.ts new file mode 100644 index 00000000..b678303f --- /dev/null +++ b/src/migrations/004-remove-playerbans.ts @@ -0,0 +1,47 @@ +import type { ObjectId } from 'mongodb' +import { database } from '../database/database' +import { collections } from '../database/collections' +import { logger } from '../logger' + +interface PlayerBanModel { + player: ObjectId + admin: ObjectId + start: Date + end: Date + reason: string +} + +export async function up() { + const collection = database.collection('playerbans') + const bans = await collection.find().toArray() + for (const ban of bans) { + const player = await collections.players.findOne({ _id: ban.player }) + if (player === null) { + logger.warn(`actor ${ban.player.toString()} not found`) + continue + } + + const actor = await collections.players.findOne({ _id: ban.admin }) + if (actor === null) { + logger.warn(`actor ${ban.admin.toString()} not found; was this bot?`) + } + + await collections.players.updateOne( + { steamId: player.steamId }, + { + $push: { + bans: { + actor: actor?.steamId ?? 'bot', + start: ban.start, + end: ban.end, + reason: ban.reason, + }, + }, + }, + ) + + await collection.deleteOne({ _id: ban._id }) + } + + await collection.drop() +} diff --git a/src/players/add-ban.ts b/src/players/add-ban.ts index c327f0a0..d79ff349 100644 --- a/src/players/add-ban.ts +++ b/src/players/add-ban.ts @@ -1,33 +1,29 @@ import { collections } from '../database/collections' -import type { PlayerBanModel } from '../database/models/player-ban.model' +import type { PlayerBan } from '../database/models/player.model' import { events } from '../events' import type { SteamId64 } from '../shared/types/steam-id-64' import { PlayerNotFoundError } from './errors' +import { update } from './update' export async function addBan(props: { player: SteamId64 admin: SteamId64 end: Date reason: string -}): Promise { - const player = await collections.players.findOne({ steamId: props.player }) - if (!player) { - throw new PlayerNotFoundError(props.player) - } - +}): Promise { const admin = await collections.players.findOne({ steamId: props.admin }) if (!admin) { throw new PlayerNotFoundError(props.admin) } - const { insertedId } = await collections.playerBans.insertOne({ - player: player._id, - admin: admin._id, + const ban: PlayerBan = { + actor: admin.steamId, start: new Date(), end: props.end, reason: props.reason, - }) - const ban = (await collections.playerBans.findOne({ _id: insertedId }))! - events.emit('player/ban:added', { ban }) + } + + await update(props.player, { $push: { bans: ban } }) + events.emit('player/ban:added', { player: props.player, ban }) return ban } diff --git a/src/players/revoke-ban.ts b/src/players/revoke-ban.ts index 99aeaf7e..b9352157 100644 --- a/src/players/revoke-ban.ts +++ b/src/players/revoke-ban.ts @@ -1,25 +1,30 @@ -import type { ObjectId } from 'mongodb' -import { collections } from '../database/collections' import type { SteamId64 } from '../shared/types/steam-id-64' import { events } from '../events' +import { update } from './update' +import type { PlayerBan } from '../database/models/player.model' -export async function revokeBan(banId: ObjectId, adminId: SteamId64) { - let ban = await collections.playerBans.findOne({ _id: banId }) - if (!ban) { - throw new Error(`ban not found: ${banId}`) - } +export async function revokeBan(props: { + player: SteamId64 + banStart: Date + admin: SteamId64 +}): Promise { + const after = await update( + props.player, + { + $set: { + 'bans.$[ban].end': new Date(), + }, + }, + { + arrayFilters: [{ 'ban.start': { $eq: props.banStart } }], + }, + ) - if (ban.end < new Date()) { - throw new Error(`ban already expired: ${banId}`) + const ban = after.bans?.find(b => b.start.getTime() === props.banStart.getTime()) + if (!ban) { + throw new Error(`ban not found`) } - const after = (await collections.playerBans.findOneAndUpdate( - { _id: banId }, - { $set: { end: new Date() } }, - { - returnDocument: 'after', - }, - ))! - events.emit('player/ban:revoked', { ban: after, admin: adminId }) - return after + events.emit('player/ban:revoked', { player: after.steamId, ban, admin: props.admin }) + return ban } diff --git a/src/players/routes.ts b/src/players/routes.ts index 14fe8a7b..e9d04426 100644 --- a/src/players/routes.ts +++ b/src/players/routes.ts @@ -20,7 +20,6 @@ import { banExpiryFormSchema } from './schemas/ban-expiry-form.schema' import { format } from 'date-fns' import { getBanExpiryDate } from './get-ban-expiry-date' import { addBan } from './add-ban' -import { ObjectId } from 'mongodb' import { revokeBan } from './revoke-ban' export default fp( @@ -226,7 +225,7 @@ export default fp( }, ) .put( - '/players/:steamId/edit/bans/:banId/revoke', + '/players/:steamId/edit/bans/:banStart/revoke', { config: { authorize: [PlayerRole.admin], @@ -234,14 +233,23 @@ export default fp( schema: { params: z.object({ steamId: steamId64, - banId: z.string().transform(value => new ObjectId(value)), + banStart: z.coerce.number().transform(value => new Date(value)), }), }, }, async (request, reply) => { - const { banId } = request.params - const ban = await revokeBan(banId, request.user!.player.steamId) - reply.status(200).html(await BanDetails({ ban })) + const { steamId, banStart } = request.params + const player = await collections.players.findOne({ steamId }) + if (player === null) { + return reply.status(404).send() + } + + const ban = await revokeBan({ + player: steamId, + banStart, + admin: request.user!.player.steamId, + }) + reply.status(200).html(await BanDetails({ player, ban })) }, ) .get( diff --git a/src/players/update.ts b/src/players/update.ts index 8fbcba35..6c6b797a 100644 --- a/src/players/update.ts +++ b/src/players/update.ts @@ -1,4 +1,4 @@ -import type { StrictUpdateFilter } from 'mongodb' +import type { FindOneAndUpdateOptions, StrictUpdateFilter } from 'mongodb' import type { SteamId64 } from '../shared/types/steam-id-64' import type { PlayerModel } from '../database/models/player.model' import { mutex } from './mutex' @@ -9,6 +9,7 @@ import { events } from '../events' export async function update( steamId: SteamId64, update: StrictUpdateFilter, + options?: FindOneAndUpdateOptions, ): Promise { return await mutex.runExclusive(async () => { const before = await collections.players.findOne({ steamId }) @@ -18,6 +19,7 @@ export async function update( const after = (await collections.players.findOneAndUpdate({ steamId }, update, { returnDocument: 'after', + ...options, }))! events.emit('player:updated', { before, after }) diff --git a/src/players/views/html/edit-player.page.tsx b/src/players/views/html/edit-player.page.tsx index 5c1686b7..356e33cf 100644 --- a/src/players/views/html/edit-player.page.tsx +++ b/src/players/views/html/edit-player.page.tsx @@ -1,5 +1,5 @@ import { resolve } from 'node:path' -import type { PlayerModel } from '../../../database/models/player.model' +import type { PlayerBan, PlayerModel } from '../../../database/models/player.model' import { Layout } from '../../../html/layout' import { NavigationBar } from '../../../html/components/navigation-bar' import type { User } from '../../../auth/types/user' @@ -24,8 +24,8 @@ import { GameClassIcon } from '../../../html/components/game-class-icon' import { configuration } from '../../../configuration' import { collections } from '../../../database/collections' import type { WithId } from 'mongodb' -import type { PlayerBanModel } from '../../../database/models/player-ban.model' import { format } from 'date-fns' +import { isBot } from '../../../shared/types/bot' const editPlayerPages = { '/profile': 'Profile', @@ -100,11 +100,7 @@ export async function EditPlayerSkillPage(props: { player: PlayerModel; user: Us } export async function EditPlayerBansPage(props: { player: WithId; user: User }) { - const bans = await collections.playerBans - .find({ player: props.player._id }) - .sort({ start: -1 }) - .toArray() - + const bans = props.player.bans?.toSorted((a, b) => b.start.getTime() - a.start.getTime()) return ( ; u } >
- {bans.length === 0 ? ( - No bans - ) : ( + {bans?.length ? ( <>
{bans.map(ban => ( - + ))}
+ ) : ( + No bans )}
@@ -186,35 +182,41 @@ function EditPlayer(props: { ) } -export async function BanDetails(props: { ban: WithId }) { - const player = await collections.players.findOne({ _id: props.ban.player }) - if (!player) { - throw new Error(`player ${props.ban.player.toString()} not found`) +export async function BanDetails(props: { player: PlayerModel; ban: PlayerBan }) { + let actorDesc = <> + if (isBot(props.ban.actor)) { + actorDesc = <>bot + } else { + const actor = await collections.players.findOne({ steamId: props.ban.actor }) + if (actor === null) { + throw new Error(`actor not found: ${props.ban.actor}`) + } + + actorDesc = ( + + {' '} + {actor.name} + + ) } - const admin = await collections.players.findOne({ _id: props.ban.admin }) return ( -
+
{props.ban.reason} - - by{' '} - - {admin?.name ?? 'unknown admin'} - - + by {actorDesc}
{props.ban.end > new Date() ? (