/** * Pokemon Showdown log viewer * * by Zarel * @license MIT */ import {FS} from "../../lib/fs"; 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 => /\.txt$/.test(file)).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(); } } const DAY = 24 * 60 * 60 * 1000; 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 && user.id in room.auth && 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.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.isOfficial) { official.push(roomid); } else if (!room.isPrivate) { normal.push(roomid); } else if (room.isPrivate === 'hidden') { hidden.push(roomid); } else { secret.push(roomid); } } if (!atLeastOne) return null; return {official, normal, hidden, secret, deleted, personal, deletedPersonal}; } getMonth(day: string) { 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); } }; const LogViewer = new class { async day(roomid: RoomID, day: string, opts?: string) { const month = LogReader.getMonth(day); let buf = `

` + `◂ All logs / ` + `${roomid} / ` + `${month} / ` + `${day}


`; const roomLog = await LogReader.get(roomid); if (!roomLog) { buf += `

Room "${roomid}" doesn't exist

`; return this.linkify(buf); } const prevDay = LogReader.prevDay(day); buf += `


${prevDay}

` + `
`; const stream = await roomLog.getLog(day); if (!stream) { buf += `

Room "${roomid}" doesn't have logs for ${day}

`; } else { let line; while ((line = await stream.readLine()) !== null) { buf += this.renderLine(line, opts); } } buf += `
`; if (day !== LogReader.today()) { const nextDay = LogReader.nextDay(day); buf += `

${nextDay}

`; } buf += ``; return this.linkify(buf); } renderLine(fullLine: string, opts?: string) { let timestamp = fullLine.slice(0, opts ? 8 : 5); let line; if (/^[0-9:]+$/.test(timestamp)) { line = fullLine.charAt(9) === '|' ? fullLine.slice(10) : '|' + fullLine.slice(9); } else { timestamp = ''; line = '!NT|'; } if (opts !== 'all' && ( line.startsWith(`userstats|`) || line.startsWith('J|') || line.startsWith('L|') || line.startsWith('N|') )) return ``; const cmd = line.slice(0, line.indexOf('|')); switch (cmd) { case 'c': { const [, name, message] = Chat.splitFirst(line, '|', 2); if (name.length <= 1) { return `
[${timestamp}] ${Chat.formatText(message)}
`; } if (message.startsWith(`/log `)) { return `
[${timestamp}] ${Chat.formatText(message.slice(5))}
`; } if (message.startsWith(`/raw `)) { return `
${message.slice(5)}
`; } if (message.startsWith(`/uhtml `) || message.startsWith(`/uhtmlchange `)) { return `
${message.slice(message.indexOf(',') + 1)}
`; } const group = name.charAt(0) !== ' ' ? `${name.charAt(0)}` : ``; return `
[${timestamp}] ${group}${name.slice(1)}: ${Chat.formatText(message)}
`; } case 'html': case 'raw': { const [, html] = Chat.splitFirst(line, '|', 1); return `
${html}
`; } case 'uhtml': case 'uhtmlchange': { const [, , html] = Chat.splitFirst(line, '|', 2); return `
${html}
`; } case '!NT': return `
${Chat.escapeHTML(fullLine)}
`; case '': return `
[${timestamp}] ${Chat.escapeHTML(line.slice(1))}
`; default: return `
[${timestamp}] ${'|' + Chat.escapeHTML(line)}
`; } } async month(roomid: RoomID, month: string) { let buf = `

` + `◂ All logs / ` + `${roomid} / ` + `${month}


`; const roomLog = await LogReader.get(roomid); if (!roomLog) { buf += `

Room "${roomid}" doesn't exist

`; return this.linkify(buf); } const prevMonth = LogReader.prevMonth(month); buf += `


${prevMonth}

`; const days = await roomLog.listDays(month); if (!days.length) { buf += `

Room "${roomid}" doesn't have logs in ${month}

`; return this.linkify(buf); } else { for (const day of days) { buf += `

- ${day}

`; } } if (!LogReader.today().startsWith(month)) { const nextMonth = LogReader.nextMonth(month); buf += `

${nextMonth}

`; } buf += ``; return this.linkify(buf); } async room(roomid: RoomID) { let buf = `

` + `◂ All logs / ` + `${roomid}


`; const roomLog = await LogReader.get(roomid); if (!roomLog) { buf += `

Room "${roomid}" doesn't exist

`; return this.linkify(buf); } const months = await roomLog.listMonths(); if (!months.length) { buf += `

Room "${roomid}" doesn't have logs

`; return this.linkify(buf); } for (const month of months) { buf += `

- ${month}

`; } buf += ``; return this.linkify(buf); } async list(user: User, opts?: string) { let buf = `

` + `All logs


`; const categories: {[k: string]: string} = { 'official': "Official", 'normal': "Public", 'hidden': "Hidden", 'secret': "Secret", 'deleted': "Deleted", 'personal': "Personal", 'deletedPersonal': "Deleted Personal", }; const list = await LogReader.listCategorized(user, opts) as {[k: string]: RoomID[]}; if (!list) { buf += `

You must be a staff member of a room, to view logs

`; return buf; } const showPersonalLink = opts !== 'all' && user.can('rangeban'); for (const k in categories) { if (!list[k].length && !(['personal', 'deleted'].includes(k) && showPersonalLink)) { continue; } buf += `

${categories[k]}

`; if (k === 'personal' && showPersonalLink) { if (opts !== 'help') buf += `

- (show all help)

`; if (opts !== 'groupchat') buf += `

- (show all groupchat)

`; } if (k === 'deleted' && showPersonalLink) { if (opts !== 'deleted') buf += `

- (show deleted)

`; } for (const roomid of list[k]) { buf += `

- ${roomid}

`; } } buf += ``; return this.linkify(buf); } error(message: string) { return `

${message}

`; } linkify(buf: string) { return buf.replace(/ ${date}`); this.title = '[Logs] ' + roomid; if (date) { if (date === 'today') { return LogViewer.day(roomid, LogReader.today(), opts); } const parsedDate = new Date(date); // this is apparently the best way to tell if a date is invalid if (isNaN(parsedDate.getTime())) return LogViewer.error(`Invalid date.`); if (date.split('-').length === 3) { return LogViewer.day(roomid, parsedDate.toISOString().slice(0, 10), opts); } else { return LogViewer.month(roomid, parsedDate.toISOString().slice(0, 7)); } } return LogViewer.room(roomid); }, }; export const commands: ChatCommands = { chatlog(target, room, user) { const targetRoom = target ? Rooms.search(target) : room; const roomid = targetRoom ? targetRoom.roomid : target; this.parse(`/join view-chatlog-${roomid}--today`); }, };