Skip to content

Commit

Permalink
Support side server replay upload
Browse files Browse the repository at this point in the history
The new API is `/api/addreplay`.
  • Loading branch information
Zarel committed Dec 16, 2023
1 parent e49d209 commit 256fe20
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 148 deletions.
95 changes: 58 additions & 37 deletions src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@
* By Mia
* @author mia-pi-git
*/
import {Config} from './config-loader';
import {promises as fs, readFileSync} from 'fs';
import * as pathModule from 'path';
import * as crypto from 'crypto';
import * as url from 'url';
import {Config} from './config-loader';
import {Ladder} from './ladder';
import {Replays} from './replays';
import {ActionError, QueryHandler, Server} from './server';
import {Session} from './user';
import {toID, updateserver, bash, time, escapeHTML} from './utils';
import * as tables from './tables';
import {SQL} from './database';
import * as pathModule from 'path';
import IPTools from './ip-tools';
import * as crypto from 'crypto';
import * as url from 'url';

const OAUTH_TOKEN_TIME = 2 * 7 * 24 * 60 * 60 * 1000;

Expand Down Expand Up @@ -171,44 +172,64 @@ export const actions: {[k: string]: QueryHandler} = {
throw new ActionError("Malformed request", 400);
},

async prepreplay(params) {
return 'currently unavailable';
// const server = await this.getServer(true);
// if (!server) {
// // legacy error
// return {errorip: this.getIp()};
// }
async addreplay(params) {
// required params:
// id, format, log, players
// optional params:
// inputlog, hidden, password

// const extractedFormatId = /^([a-z0-9]+)-[0-9]+$/.exec(`${params.id}`)?.[1];
// const formatId = /^([a-z0-9]+)$/.exec(`${params.format}`)?.[1];
// if (
// // the server must send all the required values
// !params.id || !params.format || !params.loghash || !params.p1 || !params.p2 ||
// // player usernames cannot be longer than 18 characters
// params.p1.length > 18 || params.p2.length > 18 ||
// // the battle ID must be valid
// !extractedFormatId ||
// // the format from the battle ID must match the format ID
// formatId !== extractedFormatId
// ) {
// return 0;
// }
const server = await this.getServer(true);
if (!server) {
// legacy error
return {errorip: this.getIp()};
}

// if (server.id !== Config.mainserver) {
// params.id = server.id + '-' + params.id;
// }
// params.serverid = server.id;
// the server must send all the required values
if (!params.id || !params.format || !params.log || !params.players) {
throw new ActionError("Required params: id, format, log, players", 400);
}
// player usernames cannot be longer than 18 characters
if (!params.players.split(',').some(p => p.length > 18)) {
throw new ActionError("Player names much be 18 chars or shorter", 400);
}
// the battle ID must be valid
// the format from the battle ID must match the format ID
const extractedFormatId = /^([a-z0-9]+)-[0-9]+$/.exec(`${params.id}`)?.[1];
const formatId = toID(params.format);
if (!extractedFormatId || formatId !== extractedFormatId) {
throw new ActionError("Format ID must match the one in the replay ID", 400);
}

if (server.id !== Config.mainserver) {
params.id = server.id + '-' + params.id;
}

const id = ('' + params.id).toLowerCase().replace(/[^a-z0-9-]+/g, '');
let isPrivate: 0 | 1 | 2 = params.hidden ? 1 : 0;
if (params.hidden === '2') isPrivate = 2;
const players = params.players.split(',').map(p => Session.wordfilter(p));
const out = await Replays.add({
id,
log: params.log,
players,
format: params.format,
uploadtime: time(),
rating: null,
inputlog: params.inputlog || null,
private: isPrivate,
password: params.password || null,
});

// const result = await Replays.prep(params);
this.setPrefix(''); // No need for prefix since only usable by server.
return {replayid: out};
},

// this.setPrefix(''); // No need for prefix since only usable by server.
// return result;
prepreplay() {
throw new ActionError("No longer exists; use addreplay.", 410);
},

uploadreplay(params) {
return 'currently unavailable';
// this.setHeader('Content-Type', 'text/plain; charset=utf-8');
// return Replays.upload(params, this);
uploadreplay() {
throw new ActionError("No longer exists; use addreplay.", 410);
},

async invalidatecss() {
Expand Down Expand Up @@ -708,7 +729,7 @@ export const actions: {[k: string]: QueryHandler} = {
throw new ActionError("Failed to fetch team. Please try again later.");
}
},
async 'replays/recent'() {
'replays/recent'() {
this.allowCORS();
return Replays.recent();
},
Expand Down
121 changes: 13 additions & 108 deletions src/replays.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
/**
* Code for uploading and managing replays.
*
* By Zarel.
* Ported to TypeScript by Annika and Mia.
* Ported to Postgres by Zarel.
*/
import {Session} from './user';
import {ActionError, ActionContext} from './server';
import {toID, time, stripNonAscii, md5} from './utils';
import {replayPrep, replayPlayers, replays} from './tables';
import {Config} from './config-loader';
import {toID, time} from './utils';
import {replayPlayers, replays} from './tables';
import {SQL} from './database';

// must be a type and not an interface to qualify as an SQLRow
Expand Down Expand Up @@ -41,104 +40,6 @@ type Replay = Omit<ReplayRow, 'formatid' | 'players' | 'password' | 'views'> & {

export const Replays = new class {
readonly passwordCharacters = '0123456789abcdefghijklmnopqrstuvwxyz';
// async prep(params: {[k: string]: unknown}) {
// const id = ('' + params.id).toLowerCase().replace(/[^a-z0-9-]+/g, '');
// let isPrivate: 0 | 1 | 2 = params.hidden ? 1 : 0;
// if (params.hidden === 2) isPrivate = 2;
// let p1 = Session.wordfilter(`${params.p1}`);
// let p2 = Session.wordfilter(`${params.p2}`);
// if (isPrivate) {
// p1 = `!${p1}`;
// p2 = `!${p2}`;
// }
// const {loghash, format} = params as Record<string, string>;
// let rating = Number(params.rating);
// if (params.serverid !== Config.mainserver) rating = 0;
// const inputlog = params.inputlog || null;
// const out = await replayPrep.replace({
// id, loghash,
// players: `${p1},${p2}`,
// format,
// uploadtime: time(),
// rating,
// inputlog: Array.isArray(inputlog) ? inputlog.join('\n') : inputlog as string,
// private: isPrivate,
// });
// return !!out.affectedRows;
// }

// /**
// * Not a direct upload; you should call prep first.
// *
// * The intended use is that the sim server sends `prepreplay` directly
// * to here, and then the client sends `upload`. Convoluted mostly in
// * case of firewalls between the sim server and the loginserver.
// */
// async upload(params: {[k: string]: string | undefined}, context: ActionContext) {
// let id = ('' + params.id).toLowerCase().replace(/[^a-z0-9-]+/g, '');
// if (!id) throw new ActionError('Battle ID needed.');
// const preppedReplay = await replayPrep.get(id);
// const replay = await replays.get(id, ['id', 'private', 'password']);
// if (!preppedReplay) {
// if (replay) {
// if (replay.password) {
// id += '-' + replay.password + 'pw';
// }
// return 'success:' + id;
// }
// if (!/^[a-z0-9]+-[a-z0-9]+-[0-9]+$/.test(id)) {
// return 'invalid id';
// }
// return 'not found';
// }
// let password: string | null = null;
// if (preppedReplay.private && preppedReplay.private !== 2) {
// if (replay?.password) {
// password = replay.password;
// } else if (!replay?.private) {
// password = this.generatePassword();
// }
// }
// if (typeof params.password === 'string') password = params.password;

// let fullid = id;
// if (password) fullid += '-' + password + 'pw';

// let log = params.log as string;
// if (md5(stripNonAscii(log)) !== preppedReplay.loghash) {
// log = log.replace('\r', '');
// if (md5(stripNonAscii(log)) !== preppedReplay.loghash) {
// // Hashes don't match.

// // Someone else tried to upload a replay of the same battle,
// // while we were uploading this
// // ...pretend it was a success
// return 'success:' + fullid;
// }
// }

// if (password && password.length > 31) {
// context.setHeader('HTTP/1.1', '403 Forbidden');
// return 'password must be 31 or fewer chars long';
// }

// const formatid = toID(preppedReplay.format);

// const privacy = preppedReplay.private ? 1 : 0;
// const {players, format, uploadtime, rating, inputlog} = preppedReplay;
// await replays.insert({
// id, players, format,
// formatid, uploadtime,
// private: privacy, rating, log,
// inputlog, password,
// }, SQL`ON DUPLICATE KEY UPDATE log = ${params.log as string},
// inputlog = ${inputlog}, rating = ${rating},
// private = ${privacy}, \`password\` = ${password}`);

// await replayPrep.deleteOne()`WHERE id = ${id} AND loghash = ${preppedReplay.loghash}`;

// return 'success:' + fullid;
// }

toReplay(this: void, row: ReplayRow) {
const replay: Replay = {
Expand Down Expand Up @@ -175,6 +76,7 @@ export const Replays = new class {

// obviously upsert exists but this is the easiest way when multiple things need to be changed
const replayData = this.toReplayRow(replay);
replayData.uploadtime ||= time();
try {
await replays.insert(replayData);
for (const playerName of replay.players) {
Expand Down Expand Up @@ -270,11 +172,13 @@ export const Replays = new class {
}
} else {
if (format) {
return replays.query()`SELECT uploadtime, id, format, players, rating, private, password FROM replayplayers
return replays.query()`SELECT
uploadtime, id, format, players, rating, private, password FROM replayplayers
WHERE playerid = ${userid} AND formatid = ${format} AND "private" = ${isPrivate}
${order} ${paginate};`.then(this.toReplays);
} else {
return replays.query()`SELECT uploadtime, id, format, players, rating, private, password FROM replayplayers
return replays.query()`SELECT
uploadtime, id, format, players, rating, private, password FROM replayplayers
WHERE playerid = ${userid} AND private = ${isPrivate}
${order} ${paginate};`.then(this.toReplays);
}
Expand Down Expand Up @@ -305,10 +209,11 @@ export const Replays = new class {

const secondPattern = patterns.length >= 2 ? SQL`AND log LIKE ${patterns[1]} ` : undefined;

return replays.query()`SELECT /*+ MAX_EXECUTION_TIME(10000) */
const DAYS = 24 * 60 * 60;
return replays.query()`SELECT
uploadtime, id, format, players, rating FROM ps_replays
WHERE private = 0 AND log LIKE ${patterns[0]} ${secondPattern}
ORDER BY uploadtime DESC LIMIT 10;`.then(this.toReplays);
WHERE private = 0 AND uploadtime > ${time() - 3 * DAYS} AND log LIKE ${patterns[0]} ${secondPattern}
ORDER BY uploadtime DESC LIMIT 50;`.then(this.toReplays);
}

recent() {
Expand Down
5 changes: 2 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ export function toID(text: any): string {
}

export function time() {
// php has this with unix seconds. so we have to as well.
// for legacy reasons. Yes, I hate it too.
return Math.floor(Date.now() / 1000);
// Date.now() is in milliseconds but Unix timestamps are in seconds
return Math.trunc(Date.now() / 1000);
}

export function bash(command: string, cwd?: string): Promise<[number, string, string]> {
Expand Down

0 comments on commit 256fe20

Please sign in to comment.