Jofthomas's picture
Jofthomas HF staff
Upload 4781 files
5c2ed06 verified
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var chatlog_exports = {};
__export(chatlog_exports, {
DatabaseLogSearcher: () => DatabaseLogSearcher,
FSLogSearcher: () => FSLogSearcher,
LogReader: () => LogReader,
LogReaderRoom: () => LogReaderRoom,
LogSearcher: () => LogSearcher,
LogViewer: () => LogViewer,
RipgrepLogSearcher: () => RipgrepLogSearcher,
Searcher: () => Searcher,
commands: () => commands,
pages: () => pages
});
module.exports = __toCommonJS(chatlog_exports);
var import_lib = require("../../lib");
var import_database = require("../../lib/database");
var import_roomlogs = require("../roomlogs");
/**
* Pokemon Showdown log viewer
*
* by Zarel
* @license MIT
*/
const DAY = 24 * 60 * 60 * 1e3;
const MAX_MEMORY = 67108864;
const MAX_TOPUSERS = 100;
const UPPER_STAFF_ROOMS = ["upperstaff", "adminlog", "slowlog"];
class LogReaderRoom {
constructor(roomid) {
this.roomid = roomid;
}
async listMonths() {
if (import_roomlogs.roomlogTable) {
const dates = await import_roomlogs.roomlogTable.query()`SELECT DISTINCT month FROM roomlog_dates WHERE roomid = ${this.roomid}`;
return dates.map((x) => x.month);
}
try {
const listing = await Monitor.logPath(`chat/${this.roomid}`).readdir();
return listing.filter((file) => /^[0-9][0-9][0-9][0-9]-[0-9][0-9]$/.test(file));
} catch {
return [];
}
}
async listDays(month) {
if (import_roomlogs.roomlogTable) {
const dates = await import_roomlogs.roomlogTable.query()`SELECT DISTINCT date FROM roomlog_dates WHERE roomid = ${this.roomid} AND month = ${month}`;
return dates.map((x) => x.date);
}
try {
const listing = await Monitor.logPath(`chat/${this.roomid}/${month}`).readdir();
return listing.filter((file) => file.endsWith(".txt")).map((file) => file.slice(0, -4));
} catch {
return [];
}
}
async getLog(day) {
if (import_roomlogs.roomlogTable) {
const [dayStart, dayEnd] = LogReader.dayToRange(day);
const logs = await import_roomlogs.roomlogTable.selectAll(
["log", "time"]
)`WHERE roomid = ${this.roomid} AND time BETWEEN ${dayStart}::int::timestamp AND ${dayEnd}::int::timestamp`;
return new import_lib.Streams.ObjectReadStream({
read() {
for (const { log: log2, time } of logs) {
this.buf.push(`${Chat.toTimestamp(time).split(" ")[1]} ${log2}`);
}
this.pushEnd();
}
});
}
const month = LogReader.getMonth(day);
const log = Monitor.logPath(`chat/${this.roomid}/${month}/${day}.txt`);
if (!await log.exists())
return null;
return log.createReadStream().byLine();
}
}
const LogReader = new class {
async get(roomid) {
if (import_roomlogs.roomlogTable) {
if (!await import_roomlogs.roomlogTable.selectOne()`WHERE roomid = ${roomid}`)
return null;
} else {
if (!await Monitor.logPath(`chat/${roomid}`).exists())
return null;
}
return new LogReaderRoom(roomid);
}
async list() {
if (import_roomlogs.roomlogTable) {
const roomids = await import_roomlogs.roomlogTable.query()`SELECT DISTINCT roomid FROM roomlogs`;
return roomids.map((x) => x.roomid);
}
const listing = await Monitor.logPath(`chat`).readdir();
return listing.filter((file) => /^[a-z0-9-]+$/.test(file));
}
async listCategorized(user, opts) {
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 = [];
const deletedPersonal = [];
let atLeastOne = false;
for (const roomid of list) {
const room = Rooms.get(roomid);
const forceShow = room && (room.auth.has(user.id) && user.can("mute", null, 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.section === "official") {
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 };
}
/** @returns [dayStart, dayEnd] as seconds (NOT milliseconds) since Unix epoch */
dayToRange(day) {
const nextDay = LogReader.nextDay(day);
return [
Math.trunc(new Date(day).getTime() / 1e3),
Math.trunc(new Date(nextDay).getTime() / 1e3)
];
}
/** @returns [monthStart, monthEnd] as seconds (NOT milliseconds) since Unix epoch */
monthToRange(month) {
const nextMonth = LogReader.nextMonth(month);
return [
Math.trunc(new Date(`${month}-01`).getTime() / 1e3),
Math.trunc(new Date(`${nextMonth}-01`).getTime() / 1e3)
];
}
getMonth(day) {
if (!day)
day = Chat.toTimestamp(new Date()).split(" ")[0];
return day.slice(0, 7);
}
nextDay(day) {
const nextDay = new Date(new Date(day).getTime() + DAY);
return nextDay.toISOString().slice(0, 10);
}
prevDay(day) {
const prevDay = new Date(new Date(day).getTime() - DAY);
return prevDay.toISOString().slice(0, 10);
}
nextMonth(month) {
const nextMonth = new Date(new Date(`${month}-15`).getTime() + 30 * DAY);
return nextMonth.toISOString().slice(0, 7);
}
prevMonth(month) {
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);
}
isMonth(text) {
return /^[0-9]{4}-(?:0[0-9]|1[0-2])$/.test(text);
}
isDay(text) {
return /^[0-9]{4}-(?:0[0-9]|1[0-2])-(?:[0-2][0-9]|3[0-1])$/.test(text);
}
}();
const LogViewer = new class {
async day(roomid, day, opts) {
const month = LogReader.getMonth(day);
let buf = `<div class="pad"><p><a roomid="view-chatlog">\u25C2 All logs</a> / <a roomid="view-chatlog-${roomid}">${roomid}</a> / <a roomid="view-chatlog-${roomid}--${month}">${month}</a> / <strong>${day}</strong></p><small>${opts ? `Options in use: ${opts}` : ""}</small> <hr />`;
const roomLog = await LogReader.get(roomid);
if (!roomLog) {
buf += `<p class="message-error">Room "${roomid}" doesn't exist</p></div>`;
return this.linkify(buf);
}
const prevDay = LogReader.prevDay(day);
const prevRoomid = `view-chatlog-${roomid}--${prevDay}${opts ? `--${opts}` : ""}`;
buf += `<p><a roomid="${prevRoomid}" class="blocklink" style="text-align:center">\u25B2<br />${prevDay}</a></p><div class="message-log" style="overflow-wrap: break-word">`;
const stream = await roomLog.getLog(day);
if (!stream) {
buf += `<p class="message-error">Room "${roomid}" doesn't have logs for ${day}</p>`;
} else {
for await (const line of stream) {
for (const part of line.split("\n")) {
buf += this.renderLine(part, opts, { roomid, date: day });
}
}
}
buf += `</div>`;
if (day !== LogReader.today()) {
const nextDay = LogReader.nextDay(day);
const nextRoomid = `view-chatlog-${roomid}--${nextDay}${opts ? `--${opts}` : ""}`;
buf += `<p><a roomid="${nextRoomid}" class="blocklink" style="text-align:center">${nextDay}<br />\u25BC</a></p>`;
}
buf += `</div>`;
return this.linkify(buf);
}
parseChatLine(line, day) {
const [timestamp, type, ...rest] = line.split("|");
if (type === "c:") {
const [time, username, ...message] = rest;
return { time: new Date(time), username, message: message.join("|") };
}
return { time: new Date(timestamp + day), username: rest[0], message: rest.join("|") };
}
renderLine(fullLine, opts, data) {
if (!fullLine)
return ``;
let timestamp = fullLine.slice(0, 8);
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 getClass = (name) => {
const stampNums = toID(timestamp);
if (toID(opts) === stampNums)
name += ` highlighted`;
return `class="${name}" data-server="${stampNums}"`;
};
if (opts === "txt")
return import_lib.Utils.html`<div ${getClass("chat")}>${fullLine}</div>`;
const cmd = line.slice(0, line.indexOf("|"));
if (opts?.includes("onlychat")) {
if (cmd !== "c")
return "";
if (opts.includes("txt"))
return `<div ${getClass("chat")}>${import_lib.Utils.escapeHTML(fullLine)}</div>`;
}
const timeLink = data ? `<a class="subtle" href="/view-chatlog-${data.roomid}--${data.date}--time-${timestamp}">${timestamp}</a>` : timestamp;
switch (cmd) {
case "c": {
const [, name, message] = import_lib.Utils.splitFirst(line, "|", 2);
if (name.length <= 1) {
return `<div ${getClass("chat")}><small>[${timeLink}] </small><q>${Chat.formatText(message)}</q></div>`;
}
if (message.startsWith(`/log `)) {
return `<div ${getClass("chat")}><small>[${timeLink}] </small><q>${Chat.formatText(message.slice(5))}</q></div>`;
}
if (message.startsWith(`/raw `)) {
return `<div ${getClass("notice")}>${message.slice(5)}</div>`;
}
if (message.startsWith(`/uhtml `) || message.startsWith(`/uhtmlchange `)) {
if (message.startsWith(`/uhtmlchange `))
return ``;
if (opts !== "all")
return `<div ${getClass("notice")}>[uhtml box hidden]</div>`;
return `<div ${getClass("notice")}>${message.slice(message.indexOf(",") + 1)}</div>`;
}
const group = !name.startsWith(" ") ? name.charAt(0) : ``;
return `<div ${getClass("chat")}><small>[${timeLink}]` + import_lib.Utils.html` ${group}</small><username>${name.slice(1)}:</username> ` + `<q>${Chat.formatText(message)}</q></div>`;
}
case "html":
case "raw": {
const [, html] = import_lib.Utils.splitFirst(line, "|", 1);
return `<div ${getClass("notice")}>${html}</div>`;
}
case "uhtml":
case "uhtmlchange": {
if (cmd !== "uhtml")
return ``;
const [, , html] = import_lib.Utils.splitFirst(line, "|", 2);
return `<div ${getClass("notice")}>${html}</div>`;
}
case "!NT":
return `<div ${getClass("chat")}>${import_lib.Utils.escapeHTML(fullLine)}</div>`;
case "":
return `<div ${getClass("chat")}><small>[${timeLink}] </small>${import_lib.Utils.escapeHTML(line.slice(1))}</div>`;
default:
return `<div ${getClass("chat")}><small>[${timeLink}] </small><code>${"|" + import_lib.Utils.escapeHTML(line)}</code></div>`;
}
}
async month(roomid, month) {
let buf = `<div class="pad"><p><a roomid="view-chatlog">\u25C2 All logs</a> / <a roomid="view-chatlog-${roomid}">${roomid}</a> / <strong>${month}</strong></p><hr />`;
const roomLog = await LogReader.get(roomid);
if (!roomLog) {
buf += `<p class="message-error">Room "${roomid}" doesn't exist</p></div>`;
return this.linkify(buf);
}
const prevMonth = LogReader.prevMonth(month);
buf += `<p><a roomid="view-chatlog-${roomid}--${prevMonth}" class="blocklink" style="text-align:center">\u25B2<br />${prevMonth}</a></p><div>`;
const days = await roomLog.listDays(month);
if (!days.length) {
buf += `<p class="message-error">Room "${roomid}" doesn't have logs in ${month}</p></div>`;
return this.linkify(buf);
} else {
for (const day of days) {
buf += `<p>- <a roomid="view-chatlog-${roomid}--${day}">${day}</a> <small>`;
for (const opt of ["txt", "onlychat", "all", "txt-onlychat"]) {
buf += ` (<a roomid="view-chatlog-${roomid}--${day}--${opt}">${opt}</a>) `;
}
buf += `</small></p>`;
}
}
if (!LogReader.today().startsWith(month)) {
const nextMonth = LogReader.nextMonth(month);
buf += `<p><a roomid="view-chatlog-${roomid}--${nextMonth}" class="blocklink" style="text-align:center">${nextMonth}<br />\u25BC</a></p>`;
}
buf += `</div>`;
return this.linkify(buf);
}
async room(roomid) {
let buf = `<div class="pad"><p><a roomid="view-chatlog">\u25C2 All logs</a> / <strong>${roomid}</strong></p><hr />`;
const roomLog = await LogReader.get(roomid);
if (!roomLog) {
buf += `<p class="message-error">Room "${roomid}" doesn't exist</p></div>`;
return this.linkify(buf);
}
const months = await roomLog.listMonths();
if (!months.length) {
buf += `<p class="message-error">Room "${roomid}" doesn't have logs</p></div>`;
return this.linkify(buf);
}
for (const month of months) {
buf += `<p>- <a roomid="view-chatlog-${roomid}--${month}">${month}</a></p>`;
}
buf += `</div>`;
return this.linkify(buf);
}
async list(user, opts) {
let buf = `<div class="pad"><p><strong>All logs</strong></p><hr />`;
const categories = {
"official": "Official",
"normal": "Public",
"hidden": "Hidden",
"secret": "Secret",
"deleted": "Deleted",
"personal": "Personal",
"deletedPersonal": "Deleted Personal"
};
const list = await LogReader.listCategorized(user, opts);
if (!list) {
buf += `<p class="message-error">You must be a staff member of a room to view its logs</p></div>`;
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 += `<p>${categories[k]}</p>`;
if (k === "personal" && showPersonalLink) {
if (opts !== "help")
buf += `<p>- <a roomid="view-chatlog--help">(show all help)</a></p>`;
if (opts !== "groupchat")
buf += `<p>- <a roomid="view-chatlog--groupchat">(show all groupchat)</a></p>`;
}
if (k === "deleted" && showPersonalLink) {
if (opts !== "deleted")
buf += `<p>- <a roomid="view-chatlog--deleted">(show deleted)</a></p>`;
}
for (const roomid of list[k]) {
buf += `<p>- <a roomid="view-chatlog-${roomid}">${roomid}</a></p>`;
}
}
buf += `</div>`;
return this.linkify(buf);
}
error(message) {
return `<div class="pad"><p class="message-error">${message}</p></div>`;
}
linkify(buf) {
return buf.replace(/<a roomid="/g, `<a target="replace" href="/`);
}
}();
class Searcher {
constructor() {
this.roomstatsCache = /* @__PURE__ */ new Map();
}
static checkEnabled() {
if (global.Config.disableripgrep) {
throw new Chat.ErrorMessage("Log searching functionality is currently disabled.");
}
}
constructUserRegex(user) {
const id = toID(user);
return `.${[...id].join("[^a-zA-Z0-9]*")}[^a-zA-Z0-9]*`;
}
renderLinecountResults(results, roomid, month, user) {
let buf = import_lib.Utils.html`<div class="pad"><h2>Linecounts on `;
buf += `${roomid}${user ? ` for the user ${user}` : ` (top ${MAX_TOPUSERS})`}</h2>`;
buf += `<strong>Total lines: {total}</strong><br />`;
buf += `<strong>Month: ${month}</strong><br />`;
const nextMonth = LogReader.nextMonth(month);
const prevMonth = LogReader.prevMonth(month);
if (Monitor.logPath(`chat/${roomid}/${prevMonth}`).existsSync()) {
buf += `<small><a roomid="view-roomstats-${roomid}--${prevMonth}${user ? `--${user}` : ""}">Previous month</a></small>`;
}
if (Monitor.logPath(`chat/${roomid}/${nextMonth}`).existsSync()) {
buf += ` <small><a roomid="view-roomstats-${roomid}--${nextMonth}${user ? `--${user}` : ""}">Next month</a></small>`;
}
if (!results) {
buf += "<hr />";
buf += LogViewer.error(`Logs for month '${month}' do not exist on room ${roomid}.`);
return buf;
} else if (user) {
buf += "<hr /><ol>";
const sortedDays = import_lib.Utils.sortBy(Object.keys(results));
let total = 0;
for (const day of sortedDays) {
const dayResults = results[day][user];
if (isNaN(dayResults))
continue;
total += dayResults;
buf += `<li>[<a roomid="view-chatlog-${roomid}--${day}">${day}</a>]: `;
buf += `${Chat.count(dayResults, "lines")}</li>`;
}
buf = buf.replace("{total}", `${total}`);
} else {
buf += "<hr /><ol>";
const totalResults = {};
for (const date of import_lib.Utils.sortBy(Object.keys(results))) {
for (const userid in results[date]) {
if (!totalResults[userid])
totalResults[userid] = 0;
totalResults[userid] += results[date][userid];
}
}
const resultKeys = Object.keys(totalResults);
const sortedResults = import_lib.Utils.sortBy(resultKeys, (userid) => -totalResults[userid]).slice(0, MAX_TOPUSERS);
let total = 0;
for (const userid of sortedResults) {
total += totalResults[userid];
buf += `<li><span class="username"><username>${userid}</username></span>: `;
buf += `${Chat.count(totalResults[userid], "lines")}</li>`;
}
buf = buf.replace("{total}", `${total}`);
}
buf += `</div>`;
return LogViewer.linkify(buf);
}
async runLinecountSearch(context, roomid, month, user) {
context.setHTML(
`<div class="pad"><h2>Searching linecounts on room ${roomid}${user ? ` for the user ${user}` : ""}.</h2></div>`
);
context.setHTML(await LogSearcher.searchLinecounts(roomid, month, user));
}
runSearch() {
throw new Chat.ErrorMessage(`This functionality is currently disabled.`);
}
// this would normally be abstract, but it's very difficult with ripgrep
// so it's easier to just do it the same way for both.
async roomStats(room, month) {
if (!Monitor.logPath(`chat/${room}`).existsSync()) {
return LogViewer.error(import_lib.Utils.html`Room ${room} not found.`);
}
if (!Monitor.logPath(`chat/${room}/${month}`).existsSync()) {
return LogViewer.error(import_lib.Utils.html`Room ${room} does not have logs for the month ${month}.`);
}
const stats = await LogSearcher.activityStats(room, month);
let buf = `<div class="pad"><h2>Room stats for ${room} [${month}]</h2><hr />`;
buf += `<strong>Total days with logs: ${stats.average.days}</strong><br />`;
buf += this.visualizeStats(stats.average);
buf += `<hr />`;
buf += `<details class="readmore"><summary><strong>Stats by day</strong></summary>`;
for (const day of stats.days) {
buf += `<div class="infobox"><strong><a roomid="view-chatlog-${room}--${day.day}">${day.day}</a></strong><br />`;
buf += this.visualizeStats(day);
buf += `</div>`;
}
buf += "</details>";
return LogViewer.linkify(buf);
}
visualizeStats(stats) {
const titles = {
deadTime: "Average time between lines",
deadPercent: "Average % of the day spent more than 5 minutes inactive",
linesPerUser: "Average lines per user",
averagePresent: "Average users present",
totalLines: "Average lines per day"
};
let buf = `<div class="ladder pad"><table><tr><th>`;
buf += Object.values(titles).join("</th><th>");
buf += `</th></tr><tr>`;
for (const k in titles) {
buf += `<td>`;
switch (k) {
case "deadTime":
buf += Chat.toDurationString(stats.deadTime, { precision: 2 });
break;
case "linesPerUser":
case "totalLines":
case "averagePresent":
case "deadPercent":
buf += (stats[k] || 0).toFixed(2);
break;
}
buf += `</td>`;
}
buf += `</tr></table></div>`;
return buf;
}
}
class FSLogSearcher extends Searcher {
constructor() {
super();
this.results = 0;
}
async searchLinecounts(roomid, month, user) {
const directory = Monitor.logPath(`chat/${roomid}/${month}`);
if (!directory.existsSync()) {
return this.renderLinecountResults(null, roomid, month, user);
}
const files = await directory.readdir();
const results = {};
for (const file of files) {
const day = file.slice(0, -4);
const stream = Monitor.logPath(`chat/${roomid}/${month}/${file}`).createReadStream();
for await (const line of stream.byLine()) {
const parts = line.split("|").map(toID);
const id = parts[2];
if (!id)
continue;
if (parts[1] === "c") {
if (user && id !== user)
continue;
if (!results[day])
results[day] = {};
if (!results[day][id])
results[day][id] = 0;
results[day][id]++;
}
}
}
return this.renderLinecountResults(results, roomid, month, user);
}
async dayStats(room, day) {
const cached = this.roomstatsCache.get(room + "-" + day);
if (cached)
return cached;
const results = {
deadTime: 0,
deadPercent: 0,
lines: {},
users: {},
days: 1,
// irrelevant
linesPerUser: 0,
totalLines: 0,
averagePresent: 0,
day
};
const path = Monitor.logPath(`chat/${room}/${LogReader.getMonth(day)}/${day}.txt`);
if (!path.existsSync())
return false;
const stream = path.createReadStream();
let lastTime = new Date(day).getTime();
let userstatCount = 0;
const waitIncrements = [];
for await (const line of stream.byLine()) {
const [, type, ...rest] = line.split("|");
switch (type) {
case "J":
case "j": {
if (rest[0]?.startsWith("*"))
continue;
const userid = toID(rest[0]);
if (!results.users[userid]) {
results.users[userid] = 0;
}
results.users[userid]++;
break;
}
case "c:":
case "c": {
const { time, username } = LogViewer.parseChatLine(line, day);
const curTime = time.getTime();
if (curTime - lastTime > 5 * 60 * 1e3) {
waitIncrements.push(curTime - lastTime);
lastTime = curTime;
}
const userid = toID(username);
if (!results.lines[userid])
results.lines[userid] = 0;
results.lines[userid]++;
results.totalLines++;
break;
}
case "userstats": {
const [rawTotal] = rest;
const total = parseInt(rawTotal.split(":")[1]);
results.averagePresent += total;
userstatCount++;
break;
}
}
}
results.deadTime = waitIncrements.length ? this.calculateDead(waitIncrements) : 0;
results.deadPercent = !results.totalLines ? 100 : waitIncrements.length / results.totalLines * 100;
results.linesPerUser = results.totalLines / Object.keys(results.users).length || 0;
results.averagePresent /= userstatCount;
if (day !== LogReader.today()) {
this.roomstatsCache.set(room + "-" + day, results);
}
return results;
}
calculateDead(waitIncrements) {
let num = 0;
for (const k of waitIncrements) {
num += k;
}
return num / waitIncrements.length;
}
async activityStats(room, month) {
const days = (await Monitor.logPath(`chat/${room}/${month}`).readdir()).map((f) => f.slice(0, -4));
const stats = [];
const today = Chat.toTimestamp(new Date()).split(" ")[0];
for (const day of days) {
if (day === today) {
continue;
}
const curStats = await this.dayStats(room, day);
if (!curStats)
continue;
stats.push(curStats);
}
const collected = {
deadTime: 0,
deadPercent: 0,
lines: {},
users: {},
days: days.length,
linesPerUser: 0,
totalLines: 0,
averagePresent: 0
};
for (const entry of stats) {
for (const k of ["deadTime", "deadPercent", "linesPerUser", "totalLines", "averagePresent"]) {
collected[k] += entry[k];
}
for (const type of ["lines"]) {
for (const k in entry[type]) {
if (!collected[type][k])
collected[type][k] = 0;
collected[type][k] += entry[type][k];
}
}
}
for (const k of ["deadTime", "deadPercent", "linesPerUser", "totalLines", "averagePresent"]) {
collected[k] /= stats.length;
}
return { average: collected, days: stats };
}
}
class RipgrepLogSearcher extends FSLogSearcher {
async ripgrepSearchMonth(opts) {
const { search, room: roomid, date: month, args } = opts;
let results;
let lineCount = 0;
if (Config.disableripgrep) {
return { lineCount: 0, results: [] };
}
const resultSep = args?.includes("-m") ? "--" : "\n";
try {
const options = [
"-e",
search,
Monitor.logPath(`chat/${roomid}/${month}`).path,
"-i"
];
if (args) {
options.push(...args);
}
const { stdout } = await import_lib.ProcessManager.exec(["rg", ...options], {
maxBuffer: MAX_MEMORY,
cwd: import_lib.FS.ROOT_PATH
});
results = stdout.split(resultSep);
} catch (e) {
if (e.code !== 1 && !e.message.includes("stdout maxBuffer") && !e.message.includes("No such file or directory")) {
throw e;
}
if (e.stdout) {
results = e.stdout.split(resultSep);
} else {
results = [];
}
}
lineCount += results.length;
return { results, lineCount };
}
async searchLinecounts(room, month, user) {
const regexString = (user ? `\\|c\\|${this.constructUserRegex(user)}\\|` : `\\|c\\|([^|]+)\\|`) + `(?!\\/uhtml(change)?)`;
const args = user ? ["--count"] : [];
args.push(`--pcre2`);
const { results: rawResults } = await this.ripgrepSearchMonth({
search: regexString,
raw: true,
date: month,
room,
args
});
const results = {};
for (const fullLine of rawResults) {
const [data, line] = fullLine.split(".txt:");
const date = data.split("/").pop();
if (!results[date])
results[date] = {};
if (!toID(date))
continue;
if (user) {
if (!results[date][user])
results[date][user] = 0;
const parsed = parseInt(line);
results[date][user] += isNaN(parsed) ? 0 : parsed;
} else {
const parts = line?.split("|").map(toID);
if (!parts || parts[1] !== "c")
continue;
const id = parts[2];
if (!id)
continue;
if (!results[date][id])
results[date][id] = 0;
results[date][id]++;
}
}
return this.renderLinecountResults(results, room, month, user);
}
}
class DatabaseLogSearcher extends Searcher {
async searchLinecounts(roomid, month, user) {
user = toID(user);
if (!Rooms.Roomlogs.table)
throw new Error(`Database search made while database is disabled.`);
const results = {};
const [monthStart, monthEnd] = LogReader.monthToRange(month);
const rows = await Rooms.Roomlogs.table.selectAll()`
WHERE ${user ? import_database.SQL`userid = ${user} AND ` : import_database.SQL``}roomid = ${roomid} AND
time BETWEEN ${monthStart}::int::timestamp AND ${monthEnd}::int::timestamp AND
type = ${"c"}
`;
for (const row of rows) {
if (!row.userid)
continue;
const day = Chat.toTimestamp(row.time).split(" ")[0];
if (!results[day])
results[day] = {};
if (!results[day][row.userid])
results[day][row.userid] = 0;
results[day][row.userid]++;
}
return this.renderLinecountResults(results, roomid, month, user);
}
activityStats(room, month) {
throw new Chat.ErrorMessage("This is not yet implemented for the new logs database.");
}
}
const LogSearcher = new (Rooms.Roomlogs.table ? DatabaseLogSearcher : (
// no db, determine fs reader type.
Config.chatlogreader === "ripgrep" ? RipgrepLogSearcher : FSLogSearcher
))();
const accessLog = Monitor.logPath(`chatlog-access.txt`).createAppendStream();
const pages = {
async chatlog(args, user, connection) {
if (!user.named)
return Rooms.RETRY_AFTER_LOGIN;
let [roomid, date, opts] = import_lib.Utils.splitFirst(args.join("-"), "--", 2);
if (!roomid || roomid.startsWith("-")) {
this.title = "[Logs]";
return LogViewer.list(user, roomid?.slice(1));
}
this.title = "[Logs] " + roomid;
const room = Rooms.get(roomid);
if (!user.trusted) {
if (room) {
this.checkCan("declare", null, room);
} else {
return this.errorReply(`Access denied.`);
}
}
if (!user.can("rangeban")) {
if (roomid.startsWith("spl") && roomid !== "splatoon") {
return this.errorReply("SPL team discussions are super secret.");
}
if (roomid.startsWith("wcop")) {
return this.errorReply("WCOP team discussions are super secret.");
}
if (UPPER_STAFF_ROOMS.includes(roomid) && !user.inRooms.has(roomid)) {
return this.errorReply("Upper staff rooms are super secret.");
}
}
if (room) {
if (!user.can("lock") || room.settings.isPrivate === "hidden" && !room.checkModjoin(user)) {
if (!room.persist)
return this.errorReply(`Access denied.`);
this.checkCan("mute", null, room);
}
} else {
this.checkCan("lock");
}
void accessLog.writeLine(`${user.id}: <${roomid}> ${date}`);
if (!date) {
return LogViewer.room(roomid);
}
date = date.trim();
let search;
const parsedDate = new Date(date);
const validDateStrings = ["all", "alltime"];
const validNonDateTerm = search ? validDateStrings.includes(date) : date === "today";
if (isNaN(parsedDate.getTime()) && !validNonDateTerm) {
return this.errorReply(`Invalid date.`);
}
const isTime = opts?.startsWith("time-");
if (isTime && opts)
opts = toID(opts.slice(5));
if (search) {
Searcher.checkEnabled();
this.checkCan("bypassall");
return LogSearcher.runSearch();
} else {
if (date === "today") {
this.setHTML(await LogViewer.day(roomid, LogReader.today(), opts));
if (isTime)
this.send(`|scroll|div[data-server="${opts}"]`);
} else if (date.split("-").length === 3) {
this.setHTML(await LogViewer.day(roomid, parsedDate.toISOString().slice(0, 10), opts));
if (isTime)
this.send(`|scroll|div[data-server="${opts}"]`);
} else {
return LogViewer.month(roomid, parsedDate.toISOString().slice(0, 7));
}
}
},
roomstats(args, user) {
Searcher.checkEnabled();
const room = this.extractRoom();
if (room) {
this.checkCan("mute", null, room);
} else {
if (!user.can("bypassall")) {
return this.errorReply(`You cannot view logs for rooms that no longer exist.`);
}
}
const [, date, target] = import_lib.Utils.splitFirst(args.join("-"), "--", 3).map((item) => item.trim());
if (isNaN(new Date(date).getTime())) {
return this.errorReply(`Invalid date.`);
}
if (!LogReader.isMonth(date)) {
return this.errorReply(`You must specify an exact month - both a year and a month.`);
}
this.title = `[Log Stats] ${date}`;
return LogSearcher.runLinecountSearch(this, room ? room.roomid : args[2], date, toID(target));
},
async logsaccess(query) {
this.checkCan("rangeban");
const type = toID(query.shift());
if (type && !["chat", "battle", "all", "battles"].includes(type)) {
return this.errorReply(`Invalid log type.`);
}
let title = "";
switch (type) {
case "battle":
case "battles":
title = "Battlelog access log";
break;
case "chat":
title = "Chatlog access log";
break;
default:
title = "Logs access log";
break;
}
const userid = toID(query.shift());
let buf = `<div class="pad"><h2>${title}`;
if (userid)
buf += ` for ${userid}`;
buf += `</h2><hr /><ol>`;
const accessStream = Monitor.logPath(`chatlog-access.txt`).createReadStream();
for await (const line of accessStream.byLine()) {
const [id, rest] = import_lib.Utils.splitFirst(line, ": ");
if (userid && id !== userid)
continue;
if (type === "battle" && !line.includes("battle-"))
continue;
if (userid) {
buf += `<li>${rest}</li>`;
} else {
buf += `<li><username>${id}</username>: ${rest}</li>`;
}
}
buf += `</ol>`;
return buf;
},
roominfo(query, user) {
this.checkCan("rangeban");
const args = import_lib.Utils.splitFirst(query.join("-"), "--", 2);
const roomid = toID(args.shift());
if (!roomid) {
return this.errorReply(`Specify a room.`);
}
const date = args.shift() || LogReader.getMonth();
this.title = `[${roomid}] Activity Stats (${date})`;
this.setHTML(`<div class="pad">Collecting stats for ${roomid} in ${date}...</div>`);
return LogSearcher.roomStats(roomid, date);
}
};
const commands = {
chatlogs: "chatlog",
cl: "chatlog",
roomlog: "chatlog",
rl: "chatlog",
roomlogs: "chatlog",
chatlog(target, room, user) {
const [tarRoom, ...opts] = target.split(",");
const targetRoom = tarRoom ? Rooms.search(tarRoom) : room;
const roomid = targetRoom ? targetRoom.roomid : target;
return this.parse(`/join view-chatlog-${roomid}--today${opts ? `--${opts.map(toID).join("--")}` : ""}`);
},
chatloghelp() {
const strings = [
`/chatlog [optional room], [opts] - View chatlogs from the given room. `,
`If none is specified, shows logs from the room you're in. Requires: % @ * # ~`,
`Supported options:`,
`<code>txt</code> - Do not render logs.`,
`<code>txt-onlychat</code> - Show only chat lines, untransformed.`,
`<code>onlychat</code> - Show only chat lines.`,
`<code>all</code> - Show all lines, including userstats and join/leave messages.`
];
this.runBroadcast();
return this.sendReplyBox(strings.join("<br />"));
},
sl: "searchlogs",
logsearch: "searchlogs",
searchlog: "searchlogs",
searchlogs(target, room) {
target = target.trim();
const args = target.split(",").map((item) => item.trim());
if (!target)
return this.parse("/help searchlogs");
let date = "all";
const searches = [];
let limit = "500";
let targetRoom = room?.roomid;
for (const arg of args) {
if (arg.startsWith("room=")) {
targetRoom = arg.slice(5).trim().toLowerCase();
} else if (arg.startsWith("limit=")) {
limit = arg.slice(6);
} else if (arg.startsWith("date=")) {
date = arg.slice(5);
} else if (arg.startsWith("user=")) {
args.push(`user-${toID(arg.slice(5))}`);
} else {
searches.push(arg);
}
}
if (!targetRoom) {
return this.parse(`/help searchlogs`);
}
return this.parse(
`/join view-chatlog-${targetRoom}--${date}--search-${import_lib.Dashycode.encode(searches.join("+"))}--limit-${limit}`
);
},
searchlogshelp() {
const buffer = `<details class="readmore"><summary><code>/searchlogs [arguments]</code>: searches logs in the current room using the <code>[arguments]</code>.</summary>A room can be specified using the argument <code>room=[roomid]</code>. Defaults to the room it is used in.<br />A limit can be specified using the argument <code>limit=[number less than or equal to 3000]</code>. Defaults to 500.<br />A date can be specified in ISO (YYYY-MM-DD) format using the argument <code>date=[month]</code> (for example, <code>date: 2020-05</code>). Defaults to searching all logs.<br />If you provide a user argument in the form <code>user=username</code>, it will search for messages (that match the other arguments) only from that user.<br />All other arguments will be considered part of the search (if more than one argument is specified, it searches for lines containing all terms).<br />Requires: ~</div>`;
return this.sendReplyBox(buffer);
},
topusers: "linecount",
roomstats: "linecount",
linecount(target, room, user) {
const params = target.split(",").map((f) => f.trim());
const search = {};
for (const [i, param] of params.entries()) {
let [key, val] = param.split("=");
if (!val) {
switch (i) {
case 0:
val = key;
key = "room";
break;
case 1:
val = key;
key = "date";
break;
case 2:
val = key;
key = "user";
break;
default:
return this.parse(`/help linecount`);
}
}
if (!toID(val))
continue;
key = key.toLowerCase().replace(/ /g, "");
switch (key) {
case "room":
case "roomid":
const tarRoom = Rooms.search(val);
if (!tarRoom) {
return this.errorReply(`Room '${val}' not found.`);
}
search.roomid = tarRoom.roomid;
break;
case "user":
case "id":
case "userid":
search.user = toID(val);
break;
case "date":
case "month":
case "time":
if (!LogReader.isMonth(val)) {
return this.errorReply(`Invalid date.`);
}
search.date = val;
}
}
if (!search.roomid) {
if (!room) {
return this.errorReply(`If you're not specifying a room, you must use this command in a room.`);
}
search.roomid = room.roomid;
}
if (!search.date) {
search.date = LogReader.getMonth();
}
return this.parse(`/join view-roomstats-${search.roomid}--${search.date}${search.user ? `--${search.user}` : ""}`);
},
linecounthelp() {
return this.sendReplyBox(
`<code>/linecount OR /roomstats OR /topusers</code> [<code>key=value</code> formatted parameters] - Searches linecounts with the given parameters.<br /><details class="readmore"><summary><strong>Parameters:</strong></summary>- <code>room</code> (aliases: <code>roomid</code>) - Select a room to search. If no room is given, defaults to current room.<br />- <code>date</code> (aliases: <code>month</code>, <code>time</code>) - Select a month to search linecounts on (requires YYYY-MM format). Defaults to current month.<br />- <code>user</code> (aliases: <code>id</code>, <code>userid</code>) - Searches for linecounts only from a given user. If this is not provided, /linecount instead shows line counts for all users from that month.</details>Parameters may also be specified without a [key]. When using this, arguments are provided in the format <code>/linecount [room], [month], [user].</code>. This does not use any defaults.<br />`
);
},
battlelog(target, room, user) {
this.checkCan("lock");
target = target.trim();
if (!target)
return this.errorReply(`Specify a battle.`);
if (target.startsWith("http://"))
target = target.slice(7);
if (target.startsWith("https://"))
target = target.slice(8);
if (target.startsWith(`${Config.routes.client}/`))
target = target.slice(Config.routes.client.length + 1);
if (target.startsWith(`${Config.routes.replays}/`))
target = `battle-${target.slice(Config.routes.replays.length + 1)}`;
if (target.startsWith("psim.us/"))
target = target.slice(8);
return this.parse(`/join view-battlelog-${target}`);
},
battleloghelp: [
`/battlelog [battle link] - View the log of the given [battle link], even if the replay was not saved.`,
`Requires: % @ ~`
],
gbc: "getbattlechat",
async getbattlechat(target, room, user) {
this.checkCan("lock");
let [roomName, userName] = import_lib.Utils.splitFirst(target, ",").map((f) => f.trim());
if (!roomName) {
if (!room) {
return this.errorReply(`If you are not specifying a room, use this command in a room.`);
}
roomName = room.roomid;
}
if (roomName.startsWith("http://"))
roomName = roomName.slice(7);
if (roomName.startsWith("https://"))
roomName = roomName.slice(8);
if (roomName.startsWith(`${Config.routes.client}/`)) {
roomName = roomName.slice(Config.routes.client.length + 1);
}
if (roomName.startsWith(`${Config.routes.replays}/`)) {
roomName = `battle-${roomName.slice(Config.routes.replays.length + 1)}`;
}
if (roomName.startsWith("psim.us/"))
roomName = roomName.slice(8);
const queryStringStart = roomName.indexOf("?");
if (queryStringStart > -1) {
roomName = roomName.slice(0, queryStringStart);
}
const roomid = roomName.toLowerCase().replace(/[^a-z0-9-]+/g, "");
if (!roomid)
return this.parse("/help getbattlechat");
const userid = toID(userName);
if (userName && !userid)
return this.errorReply(`Invalid username.`);
if (!roomid.startsWith("battle-"))
return this.errorReply(`You must specify a battle.`);
const tarRoom = Rooms.get(roomid);
let log;
if (tarRoom) {
log = tarRoom.log.log;
} else if (Rooms.Replays.db) {
let battleId = roomid.replace("battle-", "");
if (battleId.endsWith("pw")) {
battleId = battleId.slice(0, battleId.lastIndexOf("-", battleId.length - 2));
}
const replayData = await Rooms.Replays.get(battleId);
if (!replayData) {
return this.errorReply(`No room or replay found for that battle.`);
}
log = replayData.log.split("\n");
} else {
try {
const raw = await (0, import_lib.Net)(`https://${Config.routes.replays}/${roomid.slice("battle-".length)}.json`).get();
const data = JSON.parse(raw);
log = data.log ? data.log.split("\n") : [];
} catch {
return this.errorReply(`No room or replay found for that battle.`);
}
}
log = log.filter((l) => l.startsWith("|c|"));
let buf = "";
let atLeastOne = false;
let i = 0;
for (const line of log) {
const [, , username, message] = import_lib.Utils.splitFirst(line, "|", 3);
if (userid && toID(username) !== userid)
continue;
i++;
buf += import_lib.Utils.html`<div class="chat"><span class="username"><username>${username}:</username></span> ${message}</div>`;
atLeastOne = true;
}
if (i > 20)
buf = `<details class="readmore">${buf}</details>`;
if (!atLeastOne)
buf = `<br />None found.`;
this.runBroadcast();
return this.sendReplyBox(
import_lib.Utils.html`<strong>Chat messages in the battle '${roomid}'` + (userid ? `from the user '${userid}'` : "") + `</strong>` + buf
);
},
getbattlechathelp: [
`/getbattlechat [battle link][, username] - Gets all battle chat logs from the given [battle link].`,
`If a [username] is given, searches only chat messages from the given username.`,
`Requires: % @ ~`
],
logsaccess(target, room, user) {
this.checkCan("rangeban");
const [type, userid] = target.split(",").map(toID);
return this.parse(`/j view-logsaccess-${type || "all"}${userid ? `-${userid}` : ""}`);
},
logsaccesshelp: [
`/logsaccess [type], [user] - View chatlog access logs for the given [type] and [user].`,
`If no arguments are given, shows the entire access log.`,
`Requires: ~`
],
gcsearch: "groupchatsearch",
async groupchatsearch(target, room, user) {
this.checkCan("lock");
target = target.toLowerCase().replace(/[^a-z0-9-]+/g, "");
if (!target)
return this.parse(`/help groupchatsearch`);
if (target.length < 3) {
return this.errorReply(`Too short of a search term.`);
}
const files = await Monitor.logPath(`chat`).readdir();
const buffer = [];
for (const roomid of files) {
if (roomid.startsWith("groupchat-") && roomid.includes(target)) {
buffer.push(roomid);
}
}
import_lib.Utils.sortBy(buffer, (roomid) => !!Rooms.get(roomid));
return this.sendReplyBox(
`Groupchats with a roomid matching '${target}': ` + (buffer.length ? buffer.map((id) => `<a href="/view-chatlog-${id}">${id}</a>`).join("; ") : "None found.")
);
},
groupchatsearchhelp: [
`/groupchatsearch [target] - Searches for logs of groupchats with names containing the [target]. Requires: % @ ~`
],
roomact: "roomactivity",
roomactivity(target, room, user) {
this.checkCan("bypassall");
const [id, date] = target.split(",").map((i) => i.trim());
if (id)
room = Rooms.search(toID(id));
if (!room)
return this.errorReply(`Either use this command in the target room or specify a room.`);
return this.parse(`/join view-roominfo-${room}${date ? `--${date}` : ""}`);
},
roomactivityhelp: [
`/roomactivity [room][, date] - View room activity logs for the given room.`,
`If a date is provided, it searches for logs from that date. Otherwise, it searches the current month.`,
`Requires: ~`
]
};
//# sourceMappingURL=chatlog.js.map