/** * Pokemon Showdown log viewer * * by Zarel * @license MIT */ import {FS} from "../../lib/fs"; import {Utils} from '../../lib/utils'; import * as child_process from 'child_process'; import * as util from 'util'; import * as path from 'path'; import * as Dashycode from '../../lib/dashycode'; import {QueryProcessManager} from "../../lib/process-manager"; import {Repl} from '../../lib/repl'; import {Config} from '../config-loader'; import {Dex} from '../../sim/dex'; import {Chat} from '../chat'; const DAY = 24 * 60 * 60 * 1000; const MAX_RESULTS = 3000; const MAX_MEMORY = 67108864; // 64MB const MAX_PROCESSES = 1; const MAX_TOPUSERS = 100; const execFile = util.promisify(child_process.execFile); interface ChatlogSearch { raw?: boolean; search: string; room: RoomID; date: string; limit?: number | null; args?: string[]; } export class LogReaderRoom { roomid: RoomID; constructor(roomid: RoomID) { this.roomid = roomid; } async listMonths() { try { const listing = await FS(`logs/chat/${this.roomid}`).readdir(); return listing.filter(file => /^[0-9][0-9][0-9][0-9]-[0-9][0-9]$/.test(file)); } catch (err) { return []; } } async listDays(month: string) { try { const listing = await FS(`logs/chat/${this.roomid}/${month}`).readdir(); return listing.filter(file => file.endsWith(".txt")).map(file => file.slice(0, -4)); } catch (err) { return []; } } async getLog(day: string) { const month = LogReader.getMonth(day); const log = FS(`logs/chat/${this.roomid}/${month}/${day}.txt`); if (!await log.exists()) return null; return log.createReadStream(); } } export const LogReader = new class { async get(roomid: RoomID) { if (!await FS(`logs/chat/${roomid}`).exists()) return null; return new LogReaderRoom(roomid); } async list() { const listing = await FS(`logs/chat`).readdir(); return listing.filter(file => /^[a-z0-9-]+$/.test(file)) as RoomID[]; } async listCategorized(user: User, opts?: string) { const list = await this.list(); const isUpperStaff = user.can('rangeban'); const isStaff = user.can('lock'); const official = []; const normal = []; const hidden = []; const secret = []; const deleted = []; const personal: RoomID[] = []; const deletedPersonal: RoomID[] = []; let atLeastOne = false; for (const roomid of list) { const room = Rooms.get(roomid); const forceShow = room && ( // you are authed in the room (room.auth.has(user.id) && user.can('mute', null, room)) || // you are staff and currently in the room (isStaff && user.inRooms.has(room.roomid)) ); if (!isUpperStaff && !forceShow) { if (!isStaff) continue; if (!room) continue; if (!room.checkModjoin(user)) continue; if (room.settings.isPrivate === true) continue; } atLeastOne = true; if (roomid.includes('-')) { const matchesOpts = opts && roomid.startsWith(`${opts}-`); if (matchesOpts || opts === 'all' || forceShow) { (room ? personal : deletedPersonal).push(roomid); } } else if (!room) { if (opts === 'all' || opts === 'deleted') deleted.push(roomid); } else if (room.settings.isOfficial) { official.push(roomid); } else if (!room.settings.isPrivate) { normal.push(roomid); } else if (room.settings.isPrivate === 'hidden') { hidden.push(roomid); } else { secret.push(roomid); } } if (!atLeastOne) return null; return {official, normal, hidden, secret, deleted, personal, deletedPersonal}; } async read(roomid: RoomID, day: string, limit: number) { const roomLog = await LogReader.get(roomid); const stream = await roomLog!.getLog(day); let buf = ''; let i = LogViewer.results || 0; if (!stream) { buf += `
`; } else { for await (const line of stream.byLine()) { const rendered = LogViewer.renderLine(line); if (rendered) { buf += `${line}\n`; i++; if (i > limit) break; } } } return buf; } getMonth(day?: string) { if (!day) day = Chat.toTimestamp(new Date()).split(' ')[0]; return day.slice(0, 7); } nextDay(day: string) { const nextDay = new Date(new Date(day).getTime() + DAY); return nextDay.toISOString().slice(0, 10); } prevDay(day: string) { const prevDay = new Date(new Date(day).getTime() - DAY); return prevDay.toISOString().slice(0, 10); } nextMonth(month: string) { const nextMonth = new Date(new Date(`${month}-15`).getTime() + 30 * DAY); return nextMonth.toISOString().slice(0, 7); } prevMonth(month: string) { const prevMonth = new Date(new Date(`${month}-15`).getTime() - 30 * DAY); return prevMonth.toISOString().slice(0, 7); } today() { return Chat.toTimestamp(new Date()).slice(0, 10); } }; export const LogViewer = new class { results: number; constructor() { this.results = 0; } async day(roomid: RoomID, day: string, opts?: string) { const month = LogReader.getMonth(day); let buf = `` + `◂ All logs / ` + `${roomid} / ` + `${month} / ` + `${day}
${opts ? `Options in use: ${opts}` : ''}${dayResults.filter(Boolean).map(result => renderResult(result)).join(`
${Chat.formatText(message)}
${Chat.formatText(message.slice(5))}
${Chat.formatText(message)}
${'|' + Utils.escapeHTML(line)}` + `◂ All logs / ` + `${roomid} / ` + `${month}
- ${day} `; for (const opt of ['txt', 'onlychat', 'all', 'txt-onlychat']) { buf += ` (${opt}) `; } buf += `
`; } } if (!LogReader.today().startsWith(month)) { const nextMonth = LogReader.nextMonth(month); buf += ``; } buf += `` + `◂ All logs / ` + `${roomid}
- ${month}
`; } buf += ``; return this.linkify(buf); } async list(user: User, opts?: string) { let buf = `` + `All logs
${categories[k]}
`; if (k === 'personal' && showPersonalLink) { if (opts !== 'help') buf += ``; if (opts !== 'groupchat') buf += ``; } if (k === 'deleted' && showPersonalLink) { if (opts !== 'deleted') buf += ``; } for (const roomid of list[k]) { buf += ``; } } buf += ``; return this.linkify(buf); } error(message: string) { return ``; buf += sorted.join('
'); if (limit) { buf += `
txt - Do not render logs.`,
`txt-onlychat - Show only chat lines, untransformed.`,
`onlychat - Show only chat lines.`,
`all - Show all lines, including userstats and join/leave messages.`,
];
this.runBroadcast();
return this.sendReplyBox(strings.join('/searchlogs [arguments]: ` +
`searches logs in the current room using the [arguments].room: [roomid]. Defaults to the room it is used in.limit: [number less than or equal to 3000]. Defaults to 500.date: [month] (for example, date: 2020-05). Defaults to searching all logs.