`;
}
buf += ``;
return this.linkify(buf);
}
async battle(tier, number, context) {
if (number > Rooms.global.lastBattle) {
throw new import_chat.Chat.ErrorMessage(`That battle cannot exist, as the number has not been used.`);
}
const roomid = `battle-${tier}-${number}`;
context.setHTML(`
Locating battle logs for the battle ${tier}-${number}...
`);
const log = await PM.query({
queryType: "battlesearch",
roomid: toID(tier),
search: number
});
if (!log)
return context.setHTML(this.error("Logs not found."));
const { connection } = context;
context.close();
connection.sendTo(
roomid,
`|init|battle
|title|[Battle Log] ${tier}-${number}
${log.join("\n")}`
);
connection.sendTo(roomid, `|expire|This is a battle log.`);
}
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`
${fullLine}
`;
const cmd2 = line.slice(0, line.indexOf("|"));
if (opts?.includes("onlychat")) {
if (cmd2 !== "c")
return "";
if (opts.includes("txt"))
return `
Searching linecounts on room ${roomid}${user ? ` for the user ${user}` : ""}.
`
);
const results = await PM.query({ roomid, date: month, search: user, queryType: "linecount" });
context.setHTML(results);
}
async sharedBattles(userids) {
let buf = `Logged shared battles between the users ${userids.join(", ")}`;
const results = await PM.query({
queryType: "sharedsearch",
search: userids
});
if (!results.length) {
buf += `: None found.`;
return buf;
}
buf += ` (${results.length}): `;
buf += results.map((id) => `${id}`).join(", ");
return buf;
}
// 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 (!(0, import_lib.FS)(`logs/chat/${room}`).existsSync()) {
return LogViewer.error(import_lib.Utils.html`Room ${room} not found.`);
}
if (!(0, import_lib.FS)(`logs/chat/${room}/${month}`).existsSync()) {
return LogViewer.error(import_lib.Utils.html`Room ${room} does not have logs for the month ${month}.`);
}
const stats = await PM.query({
queryType: "roomstats",
search: month,
roomid: room
});
let buf = `
Room stats for ${room} [${month}]
`;
buf += `Total days with logs: ${stats.average.days} `;
const next = LogReader.nextMonth(month);
const prev = LogReader.prevMonth(month);
const prevExists = (0, import_lib.FS)(`logs/chat/${room}/${prev}`).existsSync();
const nextExists = (0, import_lib.FS)(`logs/chat/${room}/${next}`).existsSync();
if (prevExists) {
buf += ` Previous month`;
buf += nextExists ? ` | ` : ` `;
}
if (nextExists) {
buf += `${prevExists ? `` : ` `}Next month `;
}
buf += this.visualizeStats(stats.average);
buf += ``;
buf += `Stats by day`;
for (const day of stats.days) {
buf += `
`;
}
buf += "";
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 = `
`;
buf += Object.values(titles).join("
");
buf += `
`;
for (const k in titles) {
buf += `
`;
switch (k) {
case "deadTime":
buf += import_chat.Chat.toDurationString(stats.deadTime, { precision: 2 });
break;
case "linesPerUser":
case "totalLines":
case "averagePresent":
case "deadPercent":
buf += (stats[k] || 0).toFixed(2);
break;
}
buf += `
`;
}
buf += `
`;
return buf;
}
async activityStats(room, month) {
const days = (await (0, import_lib.FS)(`logs/chat/${room}/${month}`).readdir()).map((f) => f.slice(0, -4));
const stats = [];
for (const day of days) {
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 };
}
async dayStats(room, day) {
const cached = this.roomstatsCache.get(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 = (0, import_lib.FS)(`logs/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 = results.averagePresent / userstatCount;
if (day !== LogReader.today()) {
this.roomstatsCache.set(day, results);
}
return results;
}
calculateDead(waitIncrements) {
let num = 0;
for (const k of waitIncrements) {
num += k;
}
return num / waitIncrements.length;
}
}
class FSLogSearcher extends Searcher {
constructor() {
super();
this.results = 0;
}
async searchLinecounts(roomid, month, user) {
const directory = (0, import_lib.FS)(`logs/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 = (0, import_lib.FS)(`logs/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);
}
searchLogs(roomid, search, limit, date) {
if (!date)
date = import_chat.Chat.toTimestamp(new Date()).split(" ")[0].slice(0, -3);
const isAll = date === "all";
const isYear = date.length === 4;
const isMonth = date.length === 7;
if (!limit || limit > MAX_RESULTS)
limit = MAX_RESULTS;
if (isAll) {
return this.runYearSearch(roomid, null, search, limit);
} else if (isYear) {
date = date.substr(0, 4);
return this.runYearSearch(roomid, date, search, limit);
} else if (isMonth) {
date = date.substr(0, 7);
return this.runMonthSearch(roomid, date, search, limit);
} else {
return Promise.resolve(LogViewer.error("Invalid date."));
}
}
async fsSearchDay(roomid, day, search, limit) {
if (!limit || limit > MAX_RESULTS)
limit = MAX_RESULTS;
const text = await LogReader.read(roomid, day, limit);
if (!text)
return [];
const lines = text.split("\n");
const matches = [];
const searchTerms = search.split("+").filter(Boolean);
const searchTermRegexes = [];
for (const searchTerm of searchTerms) {
if (searchTerm.startsWith("user-")) {
const id = toID(searchTerm.slice(5));
searchTermRegexes.push(new RegExp(`\\|c\\|${this.constructUserRegex(id)}\\|`, "i"));
continue;
}
searchTermRegexes.push(new RegExp(searchTerm, "i"));
}
function matchLine(line) {
return searchTermRegexes.every((term) => term.test(line));
}
for (const [i, line] of lines.entries()) {
if (matchLine(line)) {
matches.push([
lines[i - 2],
lines[i - 1],
line,
lines[i + 1],
lines[i + 2]
]);
if (matches.length > limit)
break;
}
}
return matches;
}
renderDayResults(results, roomid) {
const renderResult = (match) => {
this.results++;
return LogViewer.renderLine(match[0]) + LogViewer.renderLine(match[1]) + `
${LogViewer.renderLine(match[2])}
` + LogViewer.renderLine(match[3]) + LogViewer.renderLine(match[4]);
};
let buf = ``;
for (const day in results) {
const dayResults = results[day];
const plural = dayResults.length !== 1 ? "es" : "";
buf += `${dayResults.length} match${plural} on `;
buf += `${day} `;
buf += `
`);
return LogSearcher.roomStats(roomid, date);
}
};
const commands = {
chatlogs: "chatlog",
cl: "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.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:`,
`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(" "));
},
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 = `/searchlogs [arguments]: searches logs in the current room using the [arguments].A room can be specified using the argument room=[roomid]. Defaults to the room it is used in. A limit can be specified using the argument limit=[number less than or equal to 3000]. Defaults to 500. A date can be specified in ISO (YYYY-MM-DD) format using the argument date=[month] (for example, date: 2020-05). Defaults to searching all logs. If you provide a user argument in the form user=username, it will search for messages (that match the other arguments) only from that user. All other arguments will be considered part of the search (if more than one argument is specified, it searches for lines containing all terms). Requires: % @ # &
`;
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(
`/linecount OR /roomstats OR /topusers [key=value formatted parameters] - Searches linecounts with the given parameters. Parameters:- room (aliases: roomid) - Select a room to search. If no room is given, defaults to current room. - date (aliases: month, time) - Select a month to search linecounts on (requires YYYY-MM format). Defaults to current month. - user (aliases: id, userid) - Searches for linecounts only from a given user. If this is not provided, /linecount instead shows line counts for all users from that month.Parameters may also be specified without a [key]. When using this, arguments are provided in the format /linecount [room], [month], [user].. This does not use any defaults. `
);
},
slb: "sharedloggedbattles",
async sharedloggedbattles(target, room, user) {
this.checkCan("lock");
if (import_config_loader.Config.nobattlesearch)
return this.errorReply(`/${this.cmd} has been temporarily disabled due to load issues.`);
const targets = target.split(",").map(toID).filter(Boolean);
if (targets.length < 2 || targets.length > 2) {
return this.errorReply(`Specify two users.`);
}
const results = await LogSearcher.sharedBattles(targets);
if (room?.settings.staffRoom || this.pmTarget?.isStaff) {
this.runBroadcast();
}
return this.sendReplyBox(results);
},
sharedloggedbattleshelp: [
`/sharedloggedbattles OR /slb [user1, user2] - View shared battle logs between user1 and user2`
],
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(`${import_config_loader.Config.routes.client}/`))
target = target.slice(import_config_loader.Config.routes.client.length + 1);
if (target.startsWith(`${import_config_loader.Config.routes.replays}/`))
target = `battle-${target.slice(import_config_loader.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(`${import_config_loader.Config.routes.client}/`)) {
roomName = roomName.slice(import_config_loader.Config.routes.client.length + 1);
}
if (roomName.startsWith(`${import_config_loader.Config.routes.replays}/`)) {
roomName = `battle-${roomName.slice(import_config_loader.Config.routes.replays.length + 1)}`;
}
if (roomName.startsWith("psim.us/"))
roomName = roomName.slice(8);
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 {
try {
const raw = await (0, import_lib.Net)(`https://${import_config_loader.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`
${username}: ${message}
`;
atLeastOne = true;
}
if (i > 20)
buf = `${buf}`;
if (!atLeastOne)
buf = ` None found.`;
this.runBroadcast();
return this.sendReplyBox(
import_lib.Utils.html`Chat messages in the battle '${roomid}'` + (userid ? `from the user '${userid}'` : "") + `` + 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 (0, import_lib.FS)(`logs/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) => `${id}`).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: [
`/roomactibity [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