{ "version": 3, "sources": ["../../../server/replays.ts"], "sourcesContent": ["/**\n * Code for uploading and managing replays.\n *\n * Ported to TypeScript by Annika and Mia.\n */\nimport {SQL, PGDatabase} from '../lib/database';\n\nexport const replaysDB = Config.replaysdb ? new PGDatabase(Config.replaysdb) : null!;\n\nexport const replays = replaysDB?.getTable<\nReplayRow\n>('replays', 'id');\n\nexport const replayPlayers = replaysDB?.getTable<{\n\tplayerid: string,\n\tformatid: string,\n\tid: string,\n\trating: number | null,\n\tuploadtime: number,\n\tprivate: ReplayRow['private'],\n\tpassword: string | null,\n\tformat: string,\n\t/** comma-delimited player names */\n\tplayers: string,\n}>('replayplayers');\n\n// must be a type and not an interface to qualify as an SQLRow\n// eslint-disable-next-line\nexport type ReplayRow = {\n\tid: string,\n\tformat: string,\n\t/** player names delimited by `,`; starting with `!` denotes that player wants the replay private */\n\tplayers: string,\n\tlog: string,\n\tinputlog: string | null,\n\tuploadtime: number,\n\tviews: number,\n\tformatid: string,\n\trating: number | null,\n\t/**\n\t * 0 = public\n\t * 1 = private (with or without password)\n\t * 2 = NOT USED; ONLY USED IN PREPREPLAY\n\t * 3 = deleted\n\t * 10 = autosaved\n\t */\n\tprivate: 0 | 1 | 2 | 3 | 10,\n\tpassword: string | null,\n};\ntype Replay = Omit & {\n\tplayers: string[],\n\tviews?: number,\n\tpassword?: string | null,\n};\n\nexport const Replays = new class {\n\tdb = replaysDB as unknown;\n\treplaysTable = replays as unknown;\n\treplayPlayersTable = replayPlayers as unknown;\n\treadonly passwordCharacters = '0123456789abcdefghijklmnopqrstuvwxyz';\n\n\ttoReplay(this: void, row: ReplayRow) {\n\t\tconst replay: Replay = {\n\t\t\t...row,\n\t\t\tplayers: row.players.split(',').map(player => player.startsWith('!') ? player.slice(1) : player),\n\t\t};\n\t\tif (!replay.password && replay.private === 1) replay.private = 2;\n\t\treturn replay;\n\t}\n\ttoReplays(this: void, rows: ReplayRow[]) {\n\t\treturn rows.map(row => Replays.toReplay(row));\n\t}\n\n\ttoReplayRow(this: void, replay: Replay) {\n\t\tconst formatid = toID(replay.format);\n\t\tconst replayData: ReplayRow = {\n\t\t\tpassword: null,\n\t\t\tviews: 0,\n\t\t\t...replay,\n\t\t\tplayers: replay.players.join(','),\n\t\t\tformatid,\n\t\t};\n\t\tif (replayData.private === 1 && !replayData.password) {\n\t\t\treplayData.password = Replays.generatePassword();\n\t\t} else {\n\t\t\tif (replayData.private === 2) replayData.private = 1;\n\t\t\treplayData.password = null;\n\t\t}\n\t\treturn replayData;\n\t}\n\n\tasync add(replay: Replay) {\n\t\tconst fullid = replay.id + (replay.password ? `-${replay.password}pw` : '');\n\n\t\t// obviously upsert exists but this is the easiest way when multiple things need to be changed\n\t\tconst replayData = this.toReplayRow(replay);\n\t\ttry {\n\t\t\tawait replays.insert(replayData);\n\t\t\tfor (const playerName of replay.players) {\n\t\t\t\tawait replayPlayers.insert({\n\t\t\t\t\tplayerid: toID(playerName),\n\t\t\t\t\tformatid: replayData.formatid,\n\t\t\t\t\tid: replayData.id,\n\t\t\t\t\trating: replayData.rating,\n\t\t\t\t\tuploadtime: replayData.uploadtime,\n\t\t\t\t\tprivate: replayData.private,\n\t\t\t\t\tpassword: replayData.password,\n\t\t\t\t\tformat: replayData.format,\n\t\t\t\t\tplayers: replayData.players,\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (e: any) {\n\t\t\tif (e?.routine !== 'NewUniquenessConstraintViolationError') throw e;\n\t\t\tawait replays.update(replay.id, {\n\t\t\t\tlog: replayData.log,\n\t\t\t\tinputlog: replayData.inputlog,\n\t\t\t\trating: replayData.rating,\n\t\t\t\tprivate: replayData.private,\n\t\t\t\tpassword: replayData.password,\n\t\t\t});\n\t\t\tawait replayPlayers.updateAll({\n\t\t\t\trating: replayData.rating,\n\t\t\t\tprivate: replayData.private,\n\t\t\t\tpassword: replayData.password,\n\t\t\t})`WHERE id = ${replay.id}`;\n\t\t}\n\t\treturn fullid;\n\t}\n\n\tasync get(id: string): Promise {\n\t\tconst replayData = await replays.get(id);\n\t\tif (!replayData) return null;\n\n\t\tawait replays.update(replayData.id, {views: SQL`views + 1`});\n\n\t\treturn this.toReplay(replayData);\n\t}\n\n\tasync edit(replay: Replay) {\n\t\tconst replayData = this.toReplayRow(replay);\n\t\tawait replays.update(replay.id, {private: replayData.private, password: replayData.password});\n\t}\n\n\tgeneratePassword(length = 31) {\n\t\tlet password = '';\n\t\tfor (let i = 0; i < length; i++) {\n\t\t\tpassword += this.passwordCharacters[Math.floor(Math.random() * this.passwordCharacters.length)];\n\t\t}\n\n\t\treturn password;\n\t}\n\n\tsearch(args: {\n\t\tpage?: number, isPrivate?: boolean, byRating?: boolean,\n\t\tformat?: string, username?: string, username2?: string,\n\t}): Promise {\n\t\tconst page = args.page || 0;\n\t\tif (page > 100) return Promise.resolve([]);\n\n\t\tlet limit1 = 50 * (page - 1);\n\t\tif (limit1 < 0) limit1 = 0;\n\n\t\tconst isPrivate = args.isPrivate ? 1 : 0;\n\n\t\tconst format = args.format ? toID(args.format) : null;\n\n\t\tif (args.username) {\n\t\t\tconst order = args.byRating ? SQL`ORDER BY rating DESC` : SQL`ORDER BY uploadtime DESC`;\n\t\t\tconst userid = toID(args.username);\n\t\t\tif (args.username2) {\n\t\t\t\tconst userid2 = toID(args.username2);\n\t\t\t\tif (format) {\n\t\t\t\t\treturn replays.query()`SELECT \n\t\t\t\t\t\t\tp1.uploadtime AS uploadtime, p1.id AS id, p1.format AS format, p1.players AS players, \n\t\t\t\t\t\t\tp1.rating AS rating, p1.password AS password, p1.private AS private \n\t\t\t\t\t\tFROM replayplayers p1 INNER JOIN replayplayers p2 ON p2.id = p1.id \n\t\t\t\t\t\tWHERE p1.playerid = ${userid} AND p1.formatid = ${format} AND p1.private = ${isPrivate}\n\t\t\t\t\t\t\tAND p2.playerid = ${userid2} \n\t\t\t\t\t\t${order} LIMIT ${limit1}, 51;`.then(this.toReplays);\n\t\t\t\t} else {\n\t\t\t\t\treturn replays.query()`SELECT \n\t\t\t\t\t\t\tp1.uploadtime AS uploadtime, p1.id AS id, p1.format AS format, p1.players AS players, \n\t\t\t\t\t\t\tp1.rating AS rating, p1.password AS password, p1.private AS private \n\t\t\t\t\t\tFROM replayplayers p1 INNER JOIN replayplayers p2 ON p2.id = p1.id \n\t\t\t\t\t\tWHERE p1.playerid = ${userid} AND p1.private = ${isPrivate}\n\t\t\t\t\t\t\tAND p2.playerid = ${userid2} \n\t\t\t\t\t\t${order} LIMIT ${limit1}, 51;`.then(this.toReplays);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (format) {\n\t\t\t\t\treturn replays.query()`SELECT uploadtime, id, format, players, rating, password FROM replayplayers \n\t\t\t\t\t\tWHERE playerid = ${userid} AND formatid = ${format} AND private = ${isPrivate} \n\t\t\t\t\t\t${order} LIMIT ${limit1}, 51;`.then(this.toReplays);\n\t\t\t\t} else {\n\t\t\t\t\treturn replays.query()`SELECT uploadtime, id, format, players, rating, password FROM replayplayers \n\t\t\t\t\t\tWHERE playerid = ${userid} private = ${isPrivate} \n\t\t\t\t\t\t${order} LIMIT ${limit1}, 51;`.then(this.toReplays);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (args.byRating) {\n\t\t\treturn replays.query()`SELECT uploadtime, id, format, players, rating, password \n\t\t\t\tFROM replays \n\t\t\t\tWHERE private = ${isPrivate} AND formatid = ${format} ORDER BY rating DESC LIMIT ${limit1}, 51`\n\t\t\t\t.then(this.toReplays);\n\t\t} else {\n\t\t\treturn replays.query()`SELECT uploadtime, id, format, players, rating, password \n\t\t\t\tFROM replays \n\t\t\t\tWHERE private = ${isPrivate} AND formatid = ${format} ORDER BY uploadtime DESC LIMIT ${limit1}, 51`\n\t\t\t\t.then(this.toReplays);\n\t\t}\n\t}\n\n\tfullSearch(term: string, page = 0): Promise {\n\t\tif (page > 0) return Promise.resolve([]);\n\n\t\tconst patterns = term.split(',').map(subterm => {\n\t\t\tconst escaped = subterm.replace(/%/g, '\\\\%').replace(/_/g, '\\\\_');\n\t\t\treturn `%${escaped}%`;\n\t\t});\n\t\tif (patterns.length !== 1 && patterns.length !== 2) return Promise.resolve([]);\n\n\t\tconst secondPattern = patterns.length >= 2 ? SQL`AND log LIKE ${patterns[1]} ` : undefined;\n\n\t\treturn replays.query()`SELECT /*+ MAX_EXECUTION_TIME(10000) */ \n\t\t\tuploadtime, id, format, players, rating FROM ps_replays \n\t\t\tWHERE private = 0 AND log LIKE ${patterns[0]} ${secondPattern}\n\t\t\tORDER BY uploadtime DESC LIMIT 10;`.then(this.toReplays);\n\t}\n\n\trecent() {\n\t\treturn replays.selectAll(\n\t\t\tSQL`uploadtime, id, format, players, rating`\n\t\t)`WHERE private = 0 ORDER BY uploadtime DESC LIMIT 50`.then(this.toReplays);\n\t}\n};\n\nexport default Replays;\n"], "mappings": "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", "names": [] }