Fix /api/replays/edit endpoint

This commit is contained in:
Guangcong Luo
2026-07-27 04:11:34 +00:00
parent 8002ff4319
commit 3720c0b4eb
4 changed files with 53 additions and 57 deletions

View File

@@ -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) {

View File

@@ -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<ReplayRow, 'players' | 'password' | 'views' | 'formatid' | 'inputlog'> & {
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) {

View File

@@ -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');

View File

@@ -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 () => {