diff --git a/src/actions.ts b/src/actions.ts index 6ac7c0c76..f7ea66388 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -1182,40 +1182,23 @@ export const actions: { [k: string]: QueryHandler } = { async 'replays/edit'(params) { const user = await this.getUser(); if (!user.isLeader()) throw new ActionError(`Access denied.`); - const id = toID(params.id); + const id = (params.id || '').toLowerCase(); + if (id && !/^[a-z0-9-]+$/.test(id)) throw new ActionError(`Invalid replay ID.`); if (!id) throw new ActionError(`No replay ID was provided.`); - const replay = await tables.replays.get(id); + const replay = await Replays.get(id); if (!replay) throw new ActionError(`Replay ${id} not found.`); - let pw; switch (Number(params.private)) { case 3: - await tables.replays.update(id, { - password: null, - private: 3, - }); - break; - case 2: // private [1], no pass - await tables.replays.update(id, { - private: 1, - password: null, - }); - break; + case 2: case 1: - if (!replay.password) replay.password = Replays.generatePassword(); - pw = replay.password; - await tables.replays.update(id, { - private: 1, - password: replay.password, - }); + case 0: + replay.private = Number(params.private) as 0; break; default: - await tables.replays.update(id, { - password: null, - private: 0, - }); - break; + throw new ActionError(`Invalid private value: ${params.private!}`); } - return { password: pw }; + const editedReplay = await Replays.edit(replay); + return { password: editedReplay.password || undefined }; }, async 'replays/batch'(params) { if (!params.ids) { diff --git a/src/replays.ts b/src/replays.ts index 28f482a33..ec724e54d 100644 --- a/src/replays.ts +++ b/src/replays.ts @@ -6,32 +6,10 @@ * Ported to Postgres by Zarel. */ import { toID, time } from './utils.ts'; -import { replayPlayers, replays } from './tables.ts'; +import { replayPlayers, replays, type ReplayRow } from './tables.ts'; import { SQL } from './database.ts'; import * as crypto from 'node:crypto'; -// must be a type and not an interface to qualify as an SQLRow -export type ReplayRow = { - id: string, - format: string, - /** player names delimited by `,`; starting with `!` denotes that player wants the replay private */ - players: string, - log: string, - inputlog: string | null, - uploadtime: number, - views: number, - formatid: string, - rating: number | null, - /** - * 0 = public - * 1 = private (with or without password) - * 2 = NOT USED; ONLY USED IN PREPREPLAY - * 3 = deleted - * 10 = autosaved - */ - private: 0 | 1 | 2 | 3 | 10, - password: string | null, -}; type Replay = Omit & { players: string[], views?: number, @@ -154,9 +132,13 @@ export const Replays = new class { ); } + /** can currently only edit privacy level */ async edit(replay: Replay) { const replayData = this.toReplayRow(replay); - await replays.update(replay.id, { private: replayData.private, password: replayData.password }); + const update = { private: replayData.private, password: replayData.password }; + await replays.update(replay.id, update); + await replayPlayers.updateAll(update)`WHERE id = ${replay.id}`; + return replayData; } generatePassword(length = 31) { diff --git a/src/tables.ts b/src/tables.ts index 08bf4f435..41a1dbd7b 100644 --- a/src/tables.ts +++ b/src/tables.ts @@ -5,7 +5,6 @@ import { MockDatabase, MySQLDatabase, PGDatabase, SQLiteDatabase } from './datab import { Config } from './config-loader.ts'; import type { LadderEntry } from './ladder.ts'; -import type { ReplayRow } from './replays.ts'; import type { Suspect } from './actions.ts'; type DatabaseDriver = 'mysql' | 'postgres' | 'sqlite' | 'mock'; @@ -85,6 +84,28 @@ export const replayPrep = replaysDB.getTable<{ uploadtime: number, }>('replayprep', 'id'); +// must be a type and not an interface to qualify as an SQLRow +export type ReplayRow = { + id: string, + format: string, + /** player names delimited by `,`; starting with `!` denotes that player wants the replay private */ + players: string, + log: string, + inputlog: string | null, + uploadtime: number, + views: number, + formatid: string, + rating: number | null, + /** + * 0 = public + * 1 = private (with or without password) + * 2 = NOT USED; ONLY USED IN PREPREPLAY + * 3 = deleted + * 10 = autosaved + */ + private: 0 | 1 | 2 | 3 | 10, + password: string | null, +}; export const replays = replaysDB.getTable< ReplayRow >('replays', 'id'); diff --git a/src/test/replays.test.ts b/src/test/replays.test.ts index 6064dfe32..467f158d6 100644 --- a/src/test/replays.test.ts +++ b/src/test/replays.test.ts @@ -5,7 +5,7 @@ import { strict as assert } from 'node:assert'; import { suite, test } from 'node:test'; import { Replays } from '../replays.ts'; -import { replays } from '../tables.ts'; +import { replayPlayers, replays } from '../tables.ts'; void suite('Replay database manipulation', () => { // prepreplay no longer exists @@ -48,11 +48,15 @@ void suite('Replay database manipulation', () => { }); void test('should support editing replays', async () => { - await replays.insert({ + await Replays.add({ id: 'edittest', - views: 1, - players: 'annika,annikatesting', + players: ['annika', 'annikatesting'], format: 'gen8ou', + log: '', + inputlog: null, + uploadtime: 1, + private: 0, + rating: null, }); const original = await Replays.get('edittest'); @@ -62,8 +66,14 @@ void suite('Replay database manipulation', () => { original.private = 2; await Replays.edit(original); - await Replays.get('edittest'); - assert.equal(original.private, 2); + const edited = await Replays.get('edittest'); + assert.equal(edited?.private, 2); + const playerRows = await replayPlayers.selectAll(['private', 'password'])`WHERE id = ${'edittest'}`; + assert.equal(playerRows.length, 2); + for (const row of playerRows) { + assert.equal(row.private, 1); + assert.equal(row.password, null); + } }); void test('should properly upload replays', async () => {