mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-03-21 17:25:10 -05:00
Modlog: Refactor to use ModlogEntry objects (#7403)
This commit is contained in:
parent
1a9293e2cf
commit
f1b4e3d43a
|
|
@ -13,6 +13,7 @@
|
|||
".*-dist/",
|
||||
"tools/set-import/importer.js",
|
||||
"tools/set-import/sets",
|
||||
"tools/modlog/converter.js",
|
||||
"server/global-variables.d.ts",
|
||||
"sim/global-variables.d.ts"
|
||||
],
|
||||
|
|
@ -189,7 +190,10 @@
|
|||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["./config/*.ts", "./data/**/*.ts", "./lib/*.ts", "./server/**/*.ts", "./sim/**/*.ts", "./tools/set-import/*.ts"],
|
||||
"files": [
|
||||
"./config/*.ts", "./data/**/*.ts", "./lib/*.ts", "./server/**/*.ts", "./sim/**/*.ts",
|
||||
"./tools/set-import/*.ts", "./tools/modlog/*.ts"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -17,6 +17,7 @@ databases/*.db*
|
|||
# Typescript build artifacts
|
||||
.*-dist/
|
||||
tools/set-import/importer.js
|
||||
tools/modlog/converter.js
|
||||
|
||||
# visual studio live share
|
||||
.vs
|
||||
|
|
|
|||
8
build
8
build
|
|
@ -112,7 +112,7 @@ function sucrase(src, out, opts) {
|
|||
if (sucrase('./config', './.config-dist')) {
|
||||
replace('.config-dist', [
|
||||
{regex: /(require\(.*?)(lib|sim)/g, replace: `$1.$2-dist`},
|
||||
]);
|
||||
]);
|
||||
}
|
||||
|
||||
if (sucrase('./data', './.data-dist')) {
|
||||
|
|
@ -141,6 +141,12 @@ if (sucrase('./tools/set-import', './tools/set-import', '--exclude-dirs=sets'))
|
|||
]);
|
||||
}
|
||||
|
||||
if (sucrase('./tools/modlog', './tools/modlog')) {
|
||||
replace('./tools/modlog/converter.js', [
|
||||
{regex: /(require\(.*?)(server|lib)/g, replace: `$1.$2-dist`},
|
||||
]);
|
||||
}
|
||||
|
||||
if (!fs.existsSync('./.data-dist/README.md')) {
|
||||
const text = '**NOTE**: This folder contains the compiled output of the `data/` directory.\n' +
|
||||
'You should be editing the `.ts` files there and then running `npm run build` or\n' +
|
||||
|
|
|
|||
|
|
@ -130,7 +130,8 @@ export class HelpTicket extends Rooms.RoomGame {
|
|||
}
|
||||
tickets[this.ticket.userid] = this.ticket;
|
||||
writeTickets();
|
||||
this.modnote(`${user.name} claimed this ticket.`, user);
|
||||
this.room.modlog({action: 'TICKETCLAIM', isGlobal: true, loggedBy: user.id});
|
||||
this.addText(`${user.name} claimed this ticket.`, user);
|
||||
notifyStaff();
|
||||
} else {
|
||||
this.claimQueue.push(user.name);
|
||||
|
|
@ -147,11 +148,14 @@ export class HelpTicket extends Rooms.RoomGame {
|
|||
if (toID(this.ticket.claimed) === user.id) {
|
||||
if (this.claimQueue.length) {
|
||||
this.ticket.claimed = this.claimQueue.shift() || null;
|
||||
this.modnote(`This ticket is now claimed by ${this.ticket.claimed}.`, user);
|
||||
this.room.modlog({action: 'TICKETCLAIM', isGlobal: true, loggedBy: toID(this.ticket.claimed)});
|
||||
this.addText(`This ticket is now claimed by ${this.ticket.claimed}.`, user);
|
||||
} else {
|
||||
const oldClaimed = this.ticket.claimed;
|
||||
this.ticket.claimed = null;
|
||||
this.lastUnclaimedStart = Date.now();
|
||||
this.modnote(`This ticket is no longer claimed.`, user);
|
||||
this.room.modlog({action: 'TICKETUNCLAIM', isGlobal: true, loggedBy: toID(oldClaimed)});
|
||||
this.addText(`This ticket is no longer claimed.`, user);
|
||||
notifyStaff();
|
||||
}
|
||||
tickets[this.ticket.userid] = this.ticket;
|
||||
|
|
@ -190,20 +194,20 @@ export class HelpTicket extends Rooms.RoomGame {
|
|||
if (!(user.id in this.playerTable)) return;
|
||||
this.removePlayer(user);
|
||||
if (!this.ticket.open) return;
|
||||
this.modnote(`${user.name} is no longer interested in this ticket.`, user);
|
||||
this.room.modlog({action: 'TICKETABANDON', isGlobal: true, loggedBy: user.id});
|
||||
this.addText(`${user.name} is no longer interested in this ticket.`, user);
|
||||
if (this.playerCount - 1 > 0) return; // There are still users in the ticket room, dont close the ticket
|
||||
this.close(!!(this.firstClaimTime), user);
|
||||
return true;
|
||||
}
|
||||
|
||||
modnote(text: string, user?: User) {
|
||||
addText(text: string, user?: User) {
|
||||
if (user) {
|
||||
this.room.addByUser(user, text);
|
||||
} else {
|
||||
this.room.add(text);
|
||||
}
|
||||
this.room.update();
|
||||
this.room.modlog(text);
|
||||
}
|
||||
|
||||
getButton() {
|
||||
|
|
@ -211,12 +215,12 @@ export class HelpTicket extends Rooms.RoomGame {
|
|||
const creator = (
|
||||
this.ticket.claimed ? Utils.html`${this.ticket.creator}` : Utils.html`<strong>${this.ticket.creator}</strong>`
|
||||
);
|
||||
const ticketUser = Users.get(this.ticket.userid);
|
||||
return (
|
||||
`<a class="button ${notifying}" href="/help-${this.ticket.userid}"` +
|
||||
` ${this.getPreview()}>${creator}${ticketUser?.language ? ` <small>(${ticketUser.language})</small>` : ``}: ${this.ticket.type}</a> `
|
||||
` ${this.getPreview()}>Help ${creator}: ${this.ticket.type}</a> `
|
||||
);
|
||||
}
|
||||
|
||||
getPreview() {
|
||||
if (!this.ticket.active) return `title="The ticket creator has not spoken yet."`;
|
||||
const hoverText = [];
|
||||
|
|
@ -241,7 +245,8 @@ export class HelpTicket extends Rooms.RoomGame {
|
|||
this.ticket.open = false;
|
||||
tickets[this.ticket.userid] = this.ticket;
|
||||
writeTickets();
|
||||
this.modnote(staff ? `${staff.name} closed this ticket.` : `This ticket was closed.`, staff);
|
||||
this.room.modlog({action: 'TICKETCLOSE', isGlobal: true, loggedBy: staff?.id || 'unknown' as ID});
|
||||
this.addText(staff ? `${staff.name} closed this ticket.` : `This ticket was closed.`, staff);
|
||||
notifyStaff();
|
||||
this.room.pokeExpireTimer();
|
||||
for (const ticketGameUser of Object.values(this.playerTable)) {
|
||||
|
|
@ -306,7 +311,8 @@ export class HelpTicket extends Rooms.RoomGame {
|
|||
|
||||
deleteTicket(staff: User) {
|
||||
this.close('deleted', staff);
|
||||
this.modnote(`${staff.name} deleted this ticket.`, staff);
|
||||
this.room.modlog({action: 'TICKETDELETE', isGlobal: true, loggedBy: staff.id});
|
||||
this.addText(`${staff.name} deleted this ticket.`, staff);
|
||||
delete tickets[this.ticket.userid];
|
||||
writeTickets();
|
||||
notifyStaff();
|
||||
|
|
@ -1169,7 +1175,8 @@ export const commands: ChatCommands = {
|
|||
helpRoom.game = new HelpTicket(helpRoom, ticket);
|
||||
}
|
||||
const ticketGame = helpRoom.getGame(HelpTicket)!;
|
||||
ticketGame.modnote(`${user.name} opened a new ticket. Issue: ${ticket.type}`, user);
|
||||
helpRoom.modlog({action: 'TICKETOPEN', isGlobal: true, loggedBy: user.id, note: ticket.type});
|
||||
ticketGame.addText(`${user.name} opened a new ticket. Issue: ${ticket.type}`, user);
|
||||
this.parse(`/join help-${user.id}`);
|
||||
if (!(user.id in ticketGame.playerTable)) {
|
||||
// User was already in the room, manually add them to the "game" so they get a popup if they try to leave
|
||||
|
|
@ -1275,7 +1282,7 @@ export const commands: ChatCommands = {
|
|||
this.privateModAction(displayMessage);
|
||||
}
|
||||
|
||||
this.globalModlog(`TICKETBAN`, targetUser || userid, ` by ${user.name}${(target ? `: ${target}` : ``)}`);
|
||||
this.globalModlog(`TICKETBAN`, targetUser || userid, target);
|
||||
for (const userObj of affected) {
|
||||
const userObjID = (typeof userObj !== 'string' ? userObj.getLastId() : toID(userObj));
|
||||
const targetTicket = tickets[userObjID];
|
||||
|
|
@ -1306,7 +1313,7 @@ export const commands: ChatCommands = {
|
|||
|
||||
const affected = HelpTicket.unban(target);
|
||||
this.addModAction(`${affected} was ticket unbanned by ${user.name}.`);
|
||||
this.globalModlog("UNTICKETBAN", toID(target), ` by ${user.id}`);
|
||||
this.globalModlog("UNTICKETBAN", toID(target));
|
||||
if (targetUser) targetUser.popup(`${user.name} has ticket unbanned you.`);
|
||||
},
|
||||
unbanhelp: [`/helpticket unban [user] - Ticket unbans a user. Requires: % @ &`],
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ export const commands: ChatCommands = {
|
|||
IPTools.addRange(range);
|
||||
}
|
||||
|
||||
this.globalModlog('IPRANGE ADD', null, `by ${user.id}: added ${successes} IP ranges`);
|
||||
this.globalModlog('IPRANGE ADD', null, `added ${successes} IP ranges`);
|
||||
return this.sendReply(`Successfully added ${successes} IP ranges!`);
|
||||
},
|
||||
addhelp: [
|
||||
|
|
@ -221,7 +221,7 @@ export const commands: ChatCommands = {
|
|||
void IPTools.removeRange(range.minIP, range.maxIP);
|
||||
removed++;
|
||||
}
|
||||
this.globalModlog('IPRANGE REMOVE', null, `by ${user.id}: ${removed} IP ranges`);
|
||||
this.globalModlog('IPRANGE REMOVE', null, `${removed} IP ranges`);
|
||||
return this.sendReply(`Removed ${removed} IP ranges!`);
|
||||
},
|
||||
removehelp: [
|
||||
|
|
@ -248,7 +248,7 @@ export const commands: ChatCommands = {
|
|||
};
|
||||
void IPTools.addRange(range);
|
||||
const renameInfo = `IP range at '${rangeString}' to ${range.host}`;
|
||||
this.globalModlog('DATACENTER RENAME', null, `by ${user.id}: ${renameInfo}`);
|
||||
this.globalModlog('DATACENTER RENAME', null, renameInfo);
|
||||
return this.sendReply(`Renamed the ${renameInfo}.`);
|
||||
},
|
||||
renamehelp: [
|
||||
|
|
@ -394,7 +394,7 @@ export const commands: ChatCommands = {
|
|||
note = ` (${note})`;
|
||||
|
||||
this.privateGlobalModAction(`The IP '${ip}' was marked as shared by ${user.name}.${note}`);
|
||||
this.globalModlog('SHAREDIP', ip, ` by ${user.name}${note}`);
|
||||
this.globalModlog('SHAREDIP', ip, note);
|
||||
},
|
||||
marksharedhelp: [
|
||||
`/markshared [IP], [owner/organization of IP] - Marks an IP address as shared.`,
|
||||
|
|
@ -411,7 +411,7 @@ export const commands: ChatCommands = {
|
|||
Punishments.removeSharedIp(target);
|
||||
|
||||
this.privateGlobalModAction(`The IP '${target}' was unmarked as shared by ${user.name}.`);
|
||||
this.globalModlog('UNSHAREDIP', target, ` by ${user.name}`);
|
||||
this.globalModlog('UNSHAREDIP', target);
|
||||
},
|
||||
unmarksharedhelp: [`/unmarkshared [IP] - Unmarks a shared IP address. Requires @ &`],
|
||||
|
||||
|
|
@ -437,7 +437,7 @@ export const commands: ChatCommands = {
|
|||
Punishments.addBlacklistedSharedIp(ip, reason);
|
||||
|
||||
this.privateGlobalModAction(`The IP '${ip}' was blacklisted from being marked as shared by ${user.name}.`);
|
||||
this.globalModlog('SHAREDIP BLACKLIST', ip, ` by ${user.name}: ${reason.trim()}`);
|
||||
this.globalModlog('SHAREDIP BLACKLIST', ip, reason.trim());
|
||||
},
|
||||
remove(target, room, user) {
|
||||
if (!target) return this.parse(`/help nomarkshared`);
|
||||
|
|
@ -450,7 +450,7 @@ export const commands: ChatCommands = {
|
|||
Punishments.removeBlacklistedSharedIp(target);
|
||||
|
||||
this.privateGlobalModAction(`The IP '${target}' was unblacklisted from being marked as shared by ${user.name}.`);
|
||||
this.globalModlog('SHAREDIP UNBLACKLIST', target, ` by ${user.name}`);
|
||||
this.globalModlog('SHAREDIP UNBLACKLIST', target);
|
||||
},
|
||||
view() {
|
||||
return this.parse(`/join view-sharedipblacklist`);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {QueryProcessManager} from '../../lib/process-manager';
|
|||
import {Repl} from '../../lib/repl';
|
||||
import {Dex} from '../../sim/dex';
|
||||
import {Config} from '../config-loader';
|
||||
import {checkRipgrepAvailability, ModlogID} from '../modlog';
|
||||
import {ModlogID, ModlogSearch, ModlogEntry, checkRipgrepAvailability} from '../modlog';
|
||||
|
||||
interface BattleOutcome {
|
||||
lost: string;
|
||||
|
|
@ -57,7 +57,7 @@ const ALIASES: {[k: string]: string} = {
|
|||
*********************************************************/
|
||||
|
||||
function getMoreButton(
|
||||
roomid: ModlogID, search: string, useExactSearch: boolean,
|
||||
roomid: ModlogID, searchCmd: string,
|
||||
lines: number, maxLines: number, onlyPunishments: boolean
|
||||
) {
|
||||
let newLines = 0;
|
||||
|
|
@ -70,8 +70,7 @@ function getMoreButton(
|
|||
if (!newLines || lines < maxLines) {
|
||||
return ''; // don't show a button if no more pre-set increments are valid or if the amount of results is already below the max
|
||||
} else {
|
||||
if (useExactSearch) search = Utils.escapeHTML(`"${search}"`);
|
||||
return `<br /><div style="text-align:center"><button class="button" name="send" value="/${onlyPunishments ? 'punish' : 'mod'}log ${roomid}, ${search} ${LINES_SEPARATOR}${newLines}" title="View more results">Older results<br />▼</button></div>`;
|
||||
return `<br /><div style="text-align:center"><button class="button" name="send" value="/${onlyPunishments ? 'punish' : 'mod'}log room=${roomid}, ${searchCmd}, ${LINES_SEPARATOR}${newLines}" title="View more results">Older results<br />▼</button></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,15 +79,8 @@ function getRoomID(id: string) {
|
|||
return id as ModlogID;
|
||||
}
|
||||
|
||||
function getAlias(id: string) {
|
||||
for (const [alias, value] of Object.entries(ALIASES)) {
|
||||
if (id === value) return alias as ModlogID;
|
||||
}
|
||||
return id as ModlogID;
|
||||
}
|
||||
|
||||
function prettifyResults(
|
||||
resultArray: string[], roomid: ModlogID, searchString: string, exactSearch: boolean,
|
||||
resultArray: ModlogEntry[], roomid: ModlogID, search: ModlogSearch, searchCmd: string,
|
||||
addModlogLinks: boolean, hideIps: boolean, maxLines: number, onlyPunishments: boolean
|
||||
) {
|
||||
if (resultArray === null) {
|
||||
|
|
@ -106,69 +98,66 @@ function prettifyResults(
|
|||
roomName = `room ${roomid}`;
|
||||
}
|
||||
const scope = onlyPunishments ? 'punishment-related ' : '';
|
||||
let searchString = ``;
|
||||
if (search.note) searchString += `with a note including any of: ${search.note.searches.join(', ')} `;
|
||||
if (search.user) searchString += `taken against ${search.user} `;
|
||||
if (search.ip) searchString += `taken against a user on the IP ${search.ip} `;
|
||||
if (search.action) searchString += `of the type ${search.action} `;
|
||||
if (search.actionTaker) searchString += `taken by ${search.actionTaker} `;
|
||||
if (!resultArray.length) {
|
||||
return `|popup|No ${scope}moderator actions containing ${searchString} found on ${roomName}.` +
|
||||
(exactSearch ? "" : " Add quotes to the search parameter to search for a phrase, rather than a user.");
|
||||
return `|popup|No ${scope}moderator actions ${searchString}found on ${roomName}.`;
|
||||
}
|
||||
const title = `[${roomid}]` + (searchString ? ` ${searchString}` : ``);
|
||||
const title = `[${roomid}] ${searchCmd}`;
|
||||
const lines = resultArray.length;
|
||||
let curDate = '';
|
||||
resultArray.unshift('');
|
||||
const resultString = resultArray.map(line => {
|
||||
let time;
|
||||
let bracketIndex;
|
||||
if (line) {
|
||||
if (hideIps) line = line.replace(IPS_REGEX, '');
|
||||
bracketIndex = line.indexOf(']');
|
||||
if (bracketIndex < 0) return Utils.escapeHTML(line);
|
||||
time = new Date(line.slice(1, bracketIndex));
|
||||
let resultString = resultArray.map(result => {
|
||||
const date = new Date(result.time || Date.now());
|
||||
const entryRoom = result.visualRoomID || result.roomID || 'global';
|
||||
let [dateString, timestamp] = Chat.toTimestamp(date, {human: true}).split(' ');
|
||||
let line = `<small>[${timestamp}] (${entryRoom})</small> ${result.action}`;
|
||||
if (result.userid) {
|
||||
line += `: [${result.userid}]`;
|
||||
if (result.autoconfirmedID) line += ` ac: [${result.autoconfirmedID}]`;
|
||||
if (result.alts) line += ` alts: [${result.alts.join('], [')}]`;
|
||||
if (!hideIps && result.ip) line += ` [${result.ip}]`;
|
||||
}
|
||||
|
||||
if (result.loggedBy) line += `: by ${result.loggedBy}`;
|
||||
if (result.note) line += `: ${result.note}`;
|
||||
|
||||
if (dateString !== curDate) {
|
||||
curDate = dateString;
|
||||
dateString = `</p><p>[${dateString}]<br />`;
|
||||
} else {
|
||||
time = new Date();
|
||||
dateString = ``;
|
||||
}
|
||||
let [date, timestamp] = Chat.toTimestamp(time, {human: true}).split(' ');
|
||||
if (date !== curDate) {
|
||||
curDate = date;
|
||||
date = `</p><p>[${date}]<br />`;
|
||||
} else {
|
||||
date = ``;
|
||||
}
|
||||
if (!line) {
|
||||
return `${date}<small>[${timestamp}] \u2190 current server time</small>`;
|
||||
}
|
||||
const parenIndex = line.indexOf(')');
|
||||
const thisRoomID = line.slice((bracketIndex as number) + 3, parenIndex)
|
||||
.replace(
|
||||
/tournament: ([a-zA-z0-9]+)/,
|
||||
(match, room) => `tournament: «<a href="/${room}" target="_blank">${room}</a>»`
|
||||
);
|
||||
const thisRoomID = entryRoom?.split(' ')[0];
|
||||
if (addModlogLinks) {
|
||||
const url = Config.modloglink(time, thisRoomID);
|
||||
const url = Config.modloglink(date, thisRoomID);
|
||||
if (url) timestamp = `<a href="${url}">${timestamp}</a>`;
|
||||
}
|
||||
line = Utils.escapeHTML(line.slice(parenIndex + 1));
|
||||
line = Utils.escapeHTML(line.slice(line.indexOf(')') + ` </small>`.length));
|
||||
if (!hideIps) line = line.replace(IPS_REGEX, `[<a href="https://whatismyipaddress.com/ip/$1" target="_blank">$1</a>]`);
|
||||
return `${date}<small>[${timestamp}] (${thisRoomID})</small>${line}`;
|
||||
return `${dateString}<small>[${timestamp}] (${thisRoomID})</small>${line}`;
|
||||
}).join(`<br />`);
|
||||
const [dateString, timestamp] = Chat.toTimestamp(new Date(), {human: true}).split(' ');
|
||||
resultString += `${dateString !== curDate ? `</p><p>[${dateString}]` : ``}<br /><small>[${timestamp}] \u2190 current server time</small>`;
|
||||
let preamble;
|
||||
const modlogid = roomid + (searchString ? '-' + Dashycode.encode(searchString) : '');
|
||||
if (searchString) {
|
||||
const searchStringDescription = exactSearch ?
|
||||
Utils.html`containing the string "${searchString}"` : Utils.html`matching the username "${searchString}"`;
|
||||
preamble = `>view-modlog-${modlogid}\n|init|html\n|title|[Modlog]${title}\n` +
|
||||
`|pagehtml|<div class="pad"><p>The last ${scope}${Chat.count(lines, "logged actions")} ${searchStringDescription} on ${roomName}.` +
|
||||
(exactSearch ? "" : " Add quotes to the search parameter to search for a phrase, rather than a user.");
|
||||
`|pagehtml|<div class="pad"><p>The last ${scope}${Chat.count(lines, "logged actions")} ${Utils.escapeHTML(searchString)} on ${roomName}.`;
|
||||
} else {
|
||||
preamble = `>view-modlog-${modlogid}\n|init|html\n|title|[Modlog]${title}\n` +
|
||||
`|pagehtml|<div class="pad"><p>The last ${Chat.count(lines, `${scope}lines`)} of the Moderator Log of ${roomName}.`;
|
||||
}
|
||||
|
||||
const moreButton = getMoreButton(getAlias(roomid), searchString, exactSearch, lines, maxLines, onlyPunishments);
|
||||
const moreButton = getMoreButton(roomid, searchCmd, lines, maxLines, onlyPunishments);
|
||||
return `${preamble}${resultString}${moreButton}</div>`;
|
||||
}
|
||||
|
||||
async function getModlog(
|
||||
connection: Connection, roomid: ModlogID = 'global', searchString = '',
|
||||
maxLines = 20, onlyPunishments = false, timed = false
|
||||
connection: Connection, roomid: ModlogID = 'global', search: ModlogSearch = {},
|
||||
searchCmd: string, maxLines = 20, onlyPunishments = false, timed = false
|
||||
) {
|
||||
const targetRoom = Rooms.search(roomid);
|
||||
const user = connection.user;
|
||||
|
|
@ -189,29 +178,32 @@ async function getModlog(
|
|||
const addModlogLinks = !!(
|
||||
Config.modloglink && (user.tempGroup !== ' ' || (targetRoom && targetRoom.settings.isPrivate !== true))
|
||||
);
|
||||
if (hideIps && /^\["']?[?[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\]?["']?$/.test(searchString)) {
|
||||
if (hideIps && search.ip) {
|
||||
connection.popup(`You cannot search for IPs.`);
|
||||
return;
|
||||
}
|
||||
if (searchString.length > MAX_QUERY_LENGTH) {
|
||||
connection.popup(`Your search query must be shorter than ${MAX_QUERY_LENGTH} characters.`);
|
||||
if (Object.values(search).join('').length > MAX_QUERY_LENGTH) {
|
||||
connection.popup(`Your search query is too long.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let exactSearch = false;
|
||||
if (/^["'].+["']$/.test(searchString)) {
|
||||
exactSearch = true;
|
||||
searchString = searchString.substring(1, searchString.length - 1);
|
||||
if (search.note?.searches) {
|
||||
for (const [i, noteSearch] of search.note.searches.entries()) {
|
||||
if (/^["'].+["']$/.test(noteSearch)) {
|
||||
search.note.searches[i] = noteSearch.substring(1, noteSearch.length - 1);
|
||||
search.note.isExact = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await Rooms.Modlog.search(roomid, searchString, maxLines, exactSearch, onlyPunishments);
|
||||
const response = await Rooms.Modlog.search(roomid, search, maxLines, onlyPunishments);
|
||||
|
||||
connection.send(
|
||||
prettifyResults(
|
||||
response.results,
|
||||
roomid,
|
||||
searchString,
|
||||
exactSearch,
|
||||
search,
|
||||
searchCmd,
|
||||
addModlogLinks,
|
||||
hideIps,
|
||||
maxLines,
|
||||
|
|
@ -444,13 +436,6 @@ async function getBattleSearch(
|
|||
}
|
||||
|
||||
export const pages: PageTable = {
|
||||
modlog(args, user, connection) {
|
||||
if (!user.named) return Rooms.RETRY_AFTER_LOGIN;
|
||||
const roomid = args[0];
|
||||
const target = Dashycode.decode(args.slice(1).join('-'));
|
||||
|
||||
void getModlog(connection, roomid as RoomID, target);
|
||||
},
|
||||
async battlesearch(args, user, connection) {
|
||||
if (!user.named) return Rooms.RETRY_AFTER_LOGIN;
|
||||
this.checkCan('forcewin');
|
||||
|
|
@ -550,39 +535,67 @@ export const commands: ChatCommands = {
|
|||
timedmodlog: 'modlog',
|
||||
modlog(target, room, user, connection, cmd) {
|
||||
let roomid: ModlogID = (!room || room.roomid === 'staff' ? 'global' : room.roomid);
|
||||
|
||||
if (target.includes(',')) {
|
||||
const targets = target.split(',');
|
||||
target = targets[1].trim();
|
||||
const newid = toID(targets[0]) as RoomID;
|
||||
if (newid) roomid = newid;
|
||||
let lines;
|
||||
const search: ModlogSearch = {};
|
||||
for (const option of target.split(',')) {
|
||||
let [param, value] = option.split('=').map(part => part.trim());
|
||||
if (!value) {
|
||||
// We should guess what parameter they meant
|
||||
value = param.trim();
|
||||
if (/^[0-9]{1,3}\.[0-9]{1,3}/.test(value)) {
|
||||
param = 'ip';
|
||||
} else if (value === value.toUpperCase()) {
|
||||
param = 'action';
|
||||
} else if (toID(param).length < 19) {
|
||||
param = 'user';
|
||||
} else {
|
||||
param = 'note';
|
||||
}
|
||||
}
|
||||
param = toID(param);
|
||||
switch (param) {
|
||||
case 'note': case 'text':
|
||||
if (!search.note?.searches) search.note = {searches: []};
|
||||
search.note.searches.push(value);
|
||||
break;
|
||||
case 'user': case 'name': case 'username': case 'userid':
|
||||
search.user = toID(value);
|
||||
break;
|
||||
case 'ip': case 'ipaddress': case 'ipaddr':
|
||||
search.ip = value;
|
||||
break;
|
||||
case 'action': case 'punishment':
|
||||
search.action = value.toUpperCase();
|
||||
break;
|
||||
case 'actiontaker': case 'moderator': case 'staff': case 'mod':
|
||||
search.actionTaker = toID(value);
|
||||
break;
|
||||
case 'room': case 'roomid':
|
||||
roomid = value.toLowerCase().replace(/[^a-z0-9-]+/g, '') as ModlogID;
|
||||
break;
|
||||
case 'lines': case 'maxlines':
|
||||
lines = parseInt(value);
|
||||
if (isNaN(lines) || lines < 1) return this.errorReply(`Invalid linecount: '${value}'.`);
|
||||
break;
|
||||
default:
|
||||
this.errorReply(`Invalid modlog parameter: '${param}'.`);
|
||||
return this.errorReply(`Please specify 'room', 'note', 'user', 'ip', 'action', 'staff', or 'lines'.`);
|
||||
}
|
||||
}
|
||||
|
||||
const targetRoom = Rooms.search(roomid);
|
||||
// if a room alias was used, replace alias with actual id
|
||||
if (targetRoom) roomid = targetRoom.roomid;
|
||||
|
||||
if (Rooms.Modlog.getSharedID(roomid)) {
|
||||
if (roomid.includes('-')) {
|
||||
if (user.can('modlog')) {
|
||||
// default to global modlog for staff convenience
|
||||
roomid = 'global';
|
||||
} else {
|
||||
return this.errorReply(`Access to global modlog denied. Battles and groupchats (and other rooms with - in their ID) don't have individual modlogs.`);
|
||||
return this.errorReply(`Only global staff may view battle and groupchat modlogs.`);
|
||||
}
|
||||
}
|
||||
|
||||
let lines;
|
||||
if (target.includes(LINES_SEPARATOR)) { // undocumented line specification
|
||||
const reqIndex = target.indexOf(LINES_SEPARATOR);
|
||||
const requestedLines = parseInt(target.substr(reqIndex + LINES_SEPARATOR.length, target.length));
|
||||
if (isNaN(requestedLines) || requestedLines < 1) {
|
||||
this.errorReply(`${LINES_SEPARATOR}${requestedLines} is not a valid line count.`);
|
||||
return;
|
||||
}
|
||||
lines = requestedLines;
|
||||
target = target.substr(0, reqIndex).trim(); // strip search out
|
||||
}
|
||||
|
||||
if (!target && !lines) {
|
||||
lines = 20;
|
||||
}
|
||||
|
|
@ -592,17 +605,30 @@ export const commands: ChatCommands = {
|
|||
void getModlog(
|
||||
connection,
|
||||
roomid,
|
||||
target,
|
||||
search,
|
||||
target.replace(/,?\s*(room|lines)\s*=[^,]*,?/g, ''),
|
||||
lines,
|
||||
(cmd === 'punishlog' || cmd === 'pl'),
|
||||
cmd === 'timedmodlog'
|
||||
);
|
||||
},
|
||||
modloghelp: [
|
||||
`/modlog OR /ml [roomid], [search] - Searches the moderator log - defaults to the current room unless specified otherwise.`,
|
||||
`If you set [roomid] as [all], it searches for [search] on all rooms' moderator logs.`,
|
||||
`If you set [roomid] as [public], it searches for [search] in all public rooms' moderator logs, excluding battles. Requires: % @ # &`,
|
||||
],
|
||||
modloghelp() {
|
||||
this.sendReplyBox(
|
||||
`<code>/modlog [comma-separated list of parameters]</code>: searches the moderator log, defaulting to the current room unless specified otherwise.<br />` +
|
||||
`If an unnamed parameter is specified, <code>/modlog</code> will make its best guess as to which search you meant.<br />` +
|
||||
`<details><summary>Parameters:</summary>` +
|
||||
`<ul>` +
|
||||
`<li><code>room=[room]</code> - searches a room's modlog</li>` +
|
||||
`<li><code>userid=[user]</code> - searches for a username (or fragment of one)</li>` +
|
||||
`<li><code>note=[text]</code> - searches the contents of notes/reasons</li>` +
|
||||
`<li><code>ip=[IP address]</code> - searches for an IP address (or fragment of one)</li>` +
|
||||
`<li><code>staff=[user]</code> - searches for actions taken by a particular staff member</li>` +
|
||||
`<li><code>action=[type]</code> - searches for a particular type of action</li>` +
|
||||
`<li><code>lines=[number]</code> - displays the given number of lines</li>` +
|
||||
`</ul>` +
|
||||
`</details>`
|
||||
);
|
||||
},
|
||||
|
||||
battlesearch(target, room, user, connection) {
|
||||
if (!target.trim()) return this.parse('/help battlesearch');
|
||||
|
|
|
|||
|
|
@ -375,7 +375,11 @@ export const commands: ChatCommands = {
|
|||
curRoom.minorActivityQueue!.splice(slot - 1, 1);
|
||||
if (!curRoom.minorActivityQueue?.length) curRoom.minorActivityQueue = null;
|
||||
|
||||
curRoom.modlog(`DELETEQUEUE: by ${user}: ${slot}`);
|
||||
curRoom.modlog({
|
||||
action: 'DELETEQUEUE',
|
||||
loggedBy: user.id,
|
||||
note: slot.toString(),
|
||||
});
|
||||
curRoom.sendMods(this.tr`(${user.name} deleted the queued poll in slot ${slot}.)`);
|
||||
curRoom.update();
|
||||
if (update) this.parse(`/j view-pollqueue-${curRoom}`);
|
||||
|
|
|
|||
|
|
@ -881,10 +881,13 @@ export class ScavengerHunt extends Rooms.RoomGame {
|
|||
|
||||
// notify staff
|
||||
const staffMsg = `(${player.name} has been caught trying to do their own hunt.)`;
|
||||
const logMsg = `([${player.id}] has been caught trying to do their own hunt.)`;
|
||||
this.room.sendMods(staffMsg);
|
||||
this.room.roomlog(staffMsg);
|
||||
this.room.modlog(logMsg);
|
||||
this.room.modlog({
|
||||
action: 'SCAV CHEATER',
|
||||
userid: player.id,
|
||||
note: 'caught trying to do their own hunt',
|
||||
});
|
||||
|
||||
PlayerLeaderboard.addPoints(player.name, 'infraction', 1);
|
||||
player.infracted = true;
|
||||
|
|
@ -897,11 +900,14 @@ export class ScavengerHunt extends Rooms.RoomGame {
|
|||
|
||||
// notify staff
|
||||
const staffMsg = `(${player.name} has been caught attempting a hunt with ${uniqueConnections} connections on the account. The user has also been given 1 infraction point on the player leaderboard.)`;
|
||||
const logMsg = `([${player.id}] has been caught attempting a hunt with ${uniqueConnections} connections on the account. The user has also been given 1 infraction point on the player leaderboard.)`;
|
||||
|
||||
this.room.sendMods(staffMsg);
|
||||
this.room.roomlog(staffMsg);
|
||||
this.room.modlog(logMsg);
|
||||
this.room.modlog({
|
||||
action: 'SCAV CHEATER',
|
||||
userid: player.id,
|
||||
note: `caught attempting a hunt with ${uniqueConnections} connections on the account; has also been given 1 infraction point on the player leaderboard`,
|
||||
});
|
||||
|
||||
PlayerLeaderboard.addPoints(player.name, 'infraction', 1);
|
||||
player.infracted = true;
|
||||
|
|
@ -1992,7 +1998,11 @@ const ScavengerCommands: ChatCommands = {
|
|||
|
||||
// double modnote in scavs room if it is a subroomgroupchat
|
||||
if (room.parent && !room.persist && scavsRoom) {
|
||||
scavsRoom.modlog(`SCAV BLITZ: by ${user.id}: ${gameType}: ${blitzPoints}`);
|
||||
scavsRoom.modlog({
|
||||
action: 'SCAV BLITZ',
|
||||
loggedBy: user.id,
|
||||
note: `${gameType}: ${blitzPoints}`,
|
||||
});
|
||||
scavsRoom.sendMods(`(${user.name} has set the points awarded for blitz for ${gameType} hunts to ${blitzPoints} in <<${room.roomid}>>.)`);
|
||||
scavsRoom.roomlog(`(${user.name} has set the points awarded for blitz for ${gameType} hunts to ${blitzPoints} in <<${room.roomid}>>.)`);
|
||||
}
|
||||
|
|
@ -2023,7 +2033,11 @@ const ScavengerCommands: ChatCommands = {
|
|||
|
||||
// double modnote in scavs room if it is a subroomgroupchat
|
||||
if (room.parent && !room.persist) {
|
||||
scavsRoom.modlog(`SCAV SETHOSTPOINTS: [room: ${room.roomid}] by ${user.id}: ${points}`);
|
||||
scavsRoom.modlog({
|
||||
action: 'SCAV SETHOSTPOINTS',
|
||||
loggedBy: user.id,
|
||||
note: `${points} [room: ${room.roomid}]`,
|
||||
});
|
||||
scavsRoom.sendMods(`(${user.name} has set the points awarded for hosting regular scavenger hunts to - ${points} in <<${room.roomid}>>)`);
|
||||
scavsRoom.roomlog(`(${user.name} has set the points awarded for hosting regular scavenger hunts to - ${points} in <<${room.roomid}>>)`);
|
||||
}
|
||||
|
|
@ -2069,7 +2083,11 @@ const ScavengerCommands: ChatCommands = {
|
|||
|
||||
// double modnote in scavs room if it is a subroomgroupchat
|
||||
if (room.parent && !room.persist) {
|
||||
scavsRoom.modlog(`SCAV SETPOINTS: [room: ${room.roomid}] by ${user.id}: ${type}: ${pointsDisplay}`);
|
||||
scavsRoom.modlog({
|
||||
action: 'SCAV SETPOINTS',
|
||||
loggedBy: user.id,
|
||||
note: `${pointsDisplay} [room: ${room.roomid}]`,
|
||||
});
|
||||
scavsRoom.sendMods(`(${user.name} has set the points awarded for winning ${type} scavenger hunts to - ${pointsDisplay} in <<${room.roomid}>>)`);
|
||||
scavsRoom.roomlog(`(${user.name} has set the points awarded for winning ${type} scavenger hunts to - ${pointsDisplay} in <<${room.roomid}>>)`);
|
||||
}
|
||||
|
|
@ -2110,7 +2128,11 @@ const ScavengerCommands: ChatCommands = {
|
|||
// double modnote in scavs room if it is a subroomgroupchat
|
||||
if (room.parent && !room.persist) {
|
||||
if (room.settings.scavSettings.officialtwist) {
|
||||
scavsRoom.modlog(`SCAV TWIST: [room: ${room.roomid}] by ${user.id}: ${room.settings.scavSettings.officialtwist}`);
|
||||
scavsRoom.modlog({
|
||||
action: 'SCAV TWIST',
|
||||
loggedBy: user.id,
|
||||
note: `${room.settings.scavSettings.officialtwist} [room: ${room.roomid}]`,
|
||||
});
|
||||
scavsRoom.sendMods(`(${user.name} has set the official twist to - ${room.settings.scavSettings.officialtwist} in <<${room.roomid}>>)`);
|
||||
scavsRoom.roomlog(`(${user.name} has set the official twist to - ${room.settings.scavSettings.officialtwist} in <<${room.roomid}>>)`);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -823,7 +823,11 @@ export class Trivia extends Rooms.RoomGame {
|
|||
const logbuf = this.getStaffEndMessage(winners, winner => winner.id);
|
||||
this.room.sendMods(`(${buf}!)`);
|
||||
this.room.roomlog(buf);
|
||||
this.room.modlog(`TRIVIAGAME: by ${toID(this.game.creator)}: ${logbuf}`);
|
||||
this.room.modlog({
|
||||
action: 'TRIVIAGAME',
|
||||
loggedBy: toID(this.game.creator),
|
||||
note: logbuf,
|
||||
});
|
||||
|
||||
if (!triviaData.history) triviaData.history = [];
|
||||
triviaData.history.push(this.game);
|
||||
|
|
|
|||
|
|
@ -312,7 +312,11 @@ export class QuestionGiveaway extends Giveaway {
|
|||
this.changeUhtml('<p style="text-align:center;font-size:13pt;font-weight:bold;">The giveaway has ended! Scroll down to see the answer.</p>');
|
||||
this.phase = 'ended';
|
||||
this.clearTimer();
|
||||
this.room.modlog(`GIVEAWAY WIN: ${this.winner.name} won ${this.giver.name}'s giveaway for a "${this.prize}" (OT: ${this.ot} TID: ${this.tid})`);
|
||||
this.room.modlog({
|
||||
action: 'GIVEAWAY WIN',
|
||||
userid: this.winner.id,
|
||||
note: `${this.giver.name}'s giveaway for a "${this.prize}" (OT: ${this.ot} TID: ${this.tid})`,
|
||||
});
|
||||
this.send(this.generateWindow(
|
||||
`<p style="text-align:center;font-size:12pt;"><b>${Utils.escapeHTML(this.winner.name)}</b> won the giveaway! Congratulations!</p>` +
|
||||
`<p style="text-align:center;">${this.question}<br />Correct answer${Chat.plural(this.answers)}: ${this.answers.join(', ')}</p>`
|
||||
|
|
@ -459,7 +463,10 @@ export class LotteryGiveaway extends Giveaway {
|
|||
this.changeUhtml(`<p style="text-align:center;font-size:13pt;font-weight:bold;">The giveaway has ended! Scroll down to see the winner${Chat.plural(this.winners)}.</p>`);
|
||||
this.phase = 'ended';
|
||||
const winnerNames = this.winners.map(winner => winner.name).join(', ');
|
||||
this.room.modlog(`GIVEAWAY WIN: ${winnerNames} won ${this.giver.name}'s giveaway for "${this.prize}" (OT: ${this.ot} TID: ${this.tid})`);
|
||||
this.room.modlog({
|
||||
action: 'GIVEAWAY WIN',
|
||||
note: `${winnerNames} won ${this.giver.name}'s giveaway for "${this.prize}" (OT: ${this.ot} TID: ${this.tid})`,
|
||||
});
|
||||
this.send(this.generateWindow(
|
||||
`<p style="text-align:center;font-size:10pt;font-weight:bold;">Lottery Draw</p>` +
|
||||
`<p style="text-align:center;">${Object.keys(this.joined).length} users joined the giveaway.<br />` +
|
||||
|
|
@ -584,7 +591,11 @@ export class GTSGiveaway {
|
|||
} else {
|
||||
this.clearTimer();
|
||||
this.changeUhtml(`<p style="text-align:center;font-size:13pt;font-weight:bold;">The GTS giveaway has finished.</p>`);
|
||||
this.room.modlog(`GTS FINISHED: ${this.giver.name} has finished their GTS giveaway for "${this.summary}"`);
|
||||
this.room.modlog({
|
||||
action: 'GTS FINISHED',
|
||||
userid: this.giver.id,
|
||||
note: `their GTS giveaway for "${this.summary}"`,
|
||||
});
|
||||
this.send(`<p style="text-align:center;font-size:11pt">The GTS giveaway for a "<strong>${Utils.escapeHTML(this.lookfor)}</strong>" has finished.</p>`);
|
||||
Giveaway.updateStats(this.monIDs);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ const TRANSLATION_DIRECTORY = 'translations/';
|
|||
import {FS} from '../lib/fs';
|
||||
import {Utils} from '../lib/utils';
|
||||
import {formatText, linkRegex, stripFormatting} from './chat-formatter';
|
||||
import {ModlogEntry} from './modlog';
|
||||
|
||||
// @ts-ignore no typedef available
|
||||
import ProbeModule = require('probe-image-size');
|
||||
|
|
@ -810,24 +811,20 @@ export class CommandContext extends MessageContext {
|
|||
this.roomlog(`(${msg})`);
|
||||
}
|
||||
globalModlog(action: string, user: string | User | null, note?: string | null) {
|
||||
let buf = `${action}: `;
|
||||
const entry: ModlogEntry = {action, isGlobal: true, loggedBy: this.user.id, note: note?.replace(/\n/gm, ' ')};
|
||||
if (user) {
|
||||
if (typeof user === 'string') {
|
||||
buf += `[${user}]`;
|
||||
entry.userid = toID(user);
|
||||
} else {
|
||||
entry.ip = user.latestIp;
|
||||
const userid = user.getLastId();
|
||||
buf += `[${userid}]`;
|
||||
if (user.autoconfirmed && user.autoconfirmed !== userid) buf += ` ac:[${user.autoconfirmed}]`;
|
||||
const alts = user.getAltUsers(false, true).slice(1).map(alt => alt.getLastId()).join('], [');
|
||||
if (alts.length) buf += ` alts:[${alts}]`;
|
||||
buf += ` [${user.latestIp}]`;
|
||||
entry.userid = userid;
|
||||
if (user.autoconfirmed && user.autoconfirmed !== userid) entry.autoconfirmedID = user.autoconfirmed;
|
||||
const alts = user.getAltUsers(false, true).slice(1).map(alt => alt.getLastId());
|
||||
if (alts.length) entry.alts = alts;
|
||||
}
|
||||
}
|
||||
if (!note) note = ` by ${this.user.id}`;
|
||||
buf += note.replace(/\n/gm, ' ');
|
||||
|
||||
Rooms.global.modlog(buf, this.room?.roomid);
|
||||
if (this.room) this.room.modlog(buf);
|
||||
(this.room || Rooms.global).modlog(entry, this.room?.roomid);
|
||||
}
|
||||
modlog(
|
||||
action: string,
|
||||
|
|
@ -835,25 +832,22 @@ export class CommandContext extends MessageContext {
|
|||
note: string | null = null,
|
||||
options: Partial<{noalts: any, noip: any}> = {}
|
||||
) {
|
||||
let buf = `${action}: `;
|
||||
const entry: ModlogEntry = {action, loggedBy: this.user.id, note: note?.replace(/\n/gm, ' ')};
|
||||
if (user) {
|
||||
if (typeof user === 'string') {
|
||||
buf += `[${toID(user)}]`;
|
||||
entry.userid = toID(user);
|
||||
} else {
|
||||
const userid = user.getLastId();
|
||||
buf += `[${userid}]`;
|
||||
entry.userid = userid;
|
||||
if (!options.noalts) {
|
||||
if (user.autoconfirmed && user.autoconfirmed !== userid) buf += ` ac:[${user.autoconfirmed}]`;
|
||||
const alts = user.getAltUsers(false, true).slice(1).map(alt => alt.getLastId()).join('], [');
|
||||
if (alts.length) buf += ` alts:[${alts}]`;
|
||||
if (user.autoconfirmed && user.autoconfirmed !== userid) entry.autoconfirmedID = user.autoconfirmed;
|
||||
const alts = user.getAltUsers(false, true).slice(1).map(alt => alt.getLastId());
|
||||
if (alts.length) entry.alts = alts;
|
||||
}
|
||||
if (!options.noip) buf += ` [${user.latestIp}]`;
|
||||
if (!options.noip) entry.ip = user.latestIp;
|
||||
}
|
||||
}
|
||||
buf += ` by ${this.user.id}`;
|
||||
if (note) buf += `: ${note.replace(/\n/gm, ' ')}`;
|
||||
|
||||
(this.room || Rooms.global).modlog(buf);
|
||||
(this.room || Rooms.global).modlog(entry);
|
||||
}
|
||||
roomlog(data: string) {
|
||||
if (this.room) this.room.roomlog(data);
|
||||
|
|
|
|||
116
server/modlog.ts
116
server/modlog.ts
|
|
@ -16,12 +16,13 @@ import {FS} from '../lib/fs';
|
|||
import {QueryProcessManager} from '../lib/process-manager';
|
||||
import {Repl} from '../lib/repl';
|
||||
|
||||
import {parseModlog} from '../tools/modlog/converter';
|
||||
|
||||
const MAX_PROCESSES = 1;
|
||||
// If a modlog query takes longer than this, it will be logged.
|
||||
const LONG_QUERY_DURATION = 2000;
|
||||
const MODLOG_PATH = 'logs/modlog';
|
||||
|
||||
|
||||
const GLOBAL_PUNISHMENTS = [
|
||||
'WEEKLOCK', 'LOCK', 'BAN', 'RANGEBAN', 'RANGELOCK', 'FORCERENAME',
|
||||
'TICKETBAN', 'AUTOLOCK', 'AUTONAMELOCK', 'NAMELOCK', 'AUTOBAN', 'MONTHLOCK',
|
||||
|
|
@ -43,18 +44,40 @@ const execFile = util.promisify(child_process.execFile);
|
|||
export type ModlogID = RoomID | 'global';
|
||||
|
||||
interface ModlogResults {
|
||||
results: string[];
|
||||
results: ModlogEntry[];
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
interface ModlogQuery {
|
||||
rooms: ModlogID[];
|
||||
search: string;
|
||||
isExact: boolean;
|
||||
regexString: string;
|
||||
maxLines: number;
|
||||
onlyPunishments: boolean | string;
|
||||
}
|
||||
|
||||
export interface ModlogSearch {
|
||||
note?: {searches: string[], isExact?: boolean};
|
||||
user?: string;
|
||||
ip?: string;
|
||||
action?: string;
|
||||
actionTaker?: string;
|
||||
}
|
||||
|
||||
export interface ModlogEntry {
|
||||
action: string;
|
||||
roomID?: string;
|
||||
visualRoomID?: string;
|
||||
userid?: ID;
|
||||
autoconfirmedID?: ID;
|
||||
alts?: ID[];
|
||||
ip?: string;
|
||||
isGlobal?: boolean;
|
||||
loggedBy?: ID;
|
||||
note?: string;
|
||||
/** Milliseconds since the epoch */
|
||||
time?: number;
|
||||
}
|
||||
|
||||
class SortedLimitedLengthList {
|
||||
maxSize: number;
|
||||
list: string[];
|
||||
|
|
@ -141,14 +164,21 @@ export class Modlog {
|
|||
|
||||
/**
|
||||
* Writes to the modlog
|
||||
* @param overrideID Specify this parameter for when the room ID to be displayed
|
||||
* is different from the ID for the modlog stream
|
||||
* (The primary use case of this is tournament battles.)
|
||||
*/
|
||||
write(roomid: ModlogID, message: string, overrideID?: string) {
|
||||
const stream = this.streams.get(roomid);
|
||||
write(roomid: string, entry: ModlogEntry, overrideID?: string) {
|
||||
roomid = entry.roomID || roomid;
|
||||
const stream = this.streams.get(roomid as ModlogID);
|
||||
if (!stream) throw new Error(`Attempted to write to an uninitialized modlog stream for the room '${roomid}'`);
|
||||
void stream.write(`[${new Date().toJSON()}] (${overrideID || roomid}) ${message}\n`);
|
||||
|
||||
let buf = `[${new Date(entry.time || Date.now()).toJSON()}] (${overrideID || entry.visualRoomID || roomid}) ${entry.action}:`;
|
||||
if (entry.userid) buf += ` [${entry.userid}]`;
|
||||
if (entry.autoconfirmedID) buf += ` ac:[${entry.autoconfirmedID}]`;
|
||||
if (entry.alts) buf += ` alts:[${entry.alts.join('], [')}]`;
|
||||
if (entry.ip) buf += ` [${entry.ip}]`;
|
||||
if (entry.loggedBy) buf += ` by ${entry.loggedBy}`;
|
||||
if (entry.note) buf += `: ${entry.note}`;
|
||||
|
||||
void stream.write(`${buf}\n`);
|
||||
}
|
||||
|
||||
async destroy(roomid: ModlogID) {
|
||||
|
|
@ -185,7 +215,7 @@ export class Modlog {
|
|||
* Methods for reading (searching) modlog *
|
||||
******************************************/
|
||||
async runSearch(
|
||||
rooms: ModlogID[], search: string, isExact: boolean, maxLines: number, onlyPunishments: boolean | string
|
||||
rooms: ModlogID[], regexString: string, maxLines: number, onlyPunishments: boolean | string
|
||||
) {
|
||||
const useRipgrep = await checkRipgrepAvailability();
|
||||
let fileNameList: string[] = [];
|
||||
|
|
@ -203,20 +233,6 @@ export class Modlog {
|
|||
}
|
||||
fileNameList = fileNameList.map(filename => `${this.logPath}/${filename}`);
|
||||
|
||||
// Ensure regexString can never be greater than or equal to the value of
|
||||
// RegExpMacroAssembler::kMaxRegister in v8 (currently 1 << 16 - 1) given a
|
||||
// searchString with max length MAX_QUERY_LENGTH. Otherwise, the modlog
|
||||
// child process will crash when attempting to execute any RegExp
|
||||
// constructed with it (i.e. when not configured to use ripgrep).
|
||||
let regexString;
|
||||
if (!search) {
|
||||
regexString = '.';
|
||||
} else if (isExact) {
|
||||
regexString = search.replace(/[\\.+*?()|[\]{}^$]/g, '\\$&');
|
||||
} else {
|
||||
search = toID(search);
|
||||
regexString = `[^a-zA-Z0-9]${[...search].join('[^a-zA-Z0-9]*')}([^a-zA-Z0-9]|\\z)`;
|
||||
}
|
||||
if (onlyPunishments) {
|
||||
regexString = `${onlyPunishments === 'global' ? GLOBAL_PUNISHMENTS_REGEX_STRING : PUNISHMENTS_REGEX_STRING}${regexString}`;
|
||||
}
|
||||
|
|
@ -226,7 +242,7 @@ export class Modlog {
|
|||
if (checkAllRooms) fileNameList = [this.logPath];
|
||||
await this.runRipgrepSearch(fileNameList, regexString, results, maxLines);
|
||||
} else {
|
||||
const searchStringRegex = (search || onlyPunishments) ? new RegExp(regexString, 'i') : undefined;
|
||||
const searchStringRegex = new RegExp(regexString, 'i');
|
||||
for (const fileName of fileNameList) {
|
||||
await this.readRoomModlog(fileName, results, searchStringRegex);
|
||||
}
|
||||
|
|
@ -260,16 +276,31 @@ export class Modlog {
|
|||
async getGlobalPunishments(user: User | string, days = 30) {
|
||||
const response = await PM.query({
|
||||
rooms: ['global' as ModlogID],
|
||||
search: toID(user),
|
||||
isExact: true,
|
||||
regexString: `[${this.escapeRegex(toID(user))}]`,
|
||||
maxLines: days * 10,
|
||||
onlyPunishments: 'global',
|
||||
});
|
||||
return response.length;
|
||||
}
|
||||
|
||||
generateRegex(search: string) {
|
||||
// Ensure the generated regex can never be greater than or equal to the value of
|
||||
// RegExpMacroAssembler::kMaxRegister in v8 (currently 1 << 16 - 1) given a
|
||||
// search with max length MAX_QUERY_LENGTH. Otherwise, the modlog
|
||||
// child process will crash when attempting to execute any RegExp
|
||||
// constructed with it (i.e. when not configured to use ripgrep).
|
||||
return `[^a-zA-Z0-9]?${[...search].join('[^a-zA-Z0-9]*')}([^a-zA-Z0-9]|\\z)`;
|
||||
}
|
||||
|
||||
escapeRegex(search: string) {
|
||||
return search.replace(/[\\.+*?()|[\]{}^$]/g, '\\$&');
|
||||
}
|
||||
|
||||
async search(
|
||||
roomid: ModlogID = 'global', search = '', maxLines = 20, exactSearch = false, onlyPunishments = false
|
||||
roomid: ModlogID = 'global',
|
||||
search: ModlogSearch = {},
|
||||
maxLines = 20,
|
||||
onlyPunishments = false
|
||||
): Promise<ModlogResults> {
|
||||
const rooms = (roomid === 'public' ?
|
||||
[...Rooms.rooms.values()]
|
||||
|
|
@ -277,14 +308,31 @@ export class Modlog {
|
|||
.map(room => room.roomid) :
|
||||
[roomid]);
|
||||
|
||||
// Ensure regexString can never be greater than or equal to the value of
|
||||
// RegExpMacroAssembler::kMaxRegister in v8 (currently 1 << 16 - 1) given a
|
||||
// searchString with max length MAX_QUERY_LENGTH. Otherwise, the modlog
|
||||
// child process will crash when attempting to execute any RegExp
|
||||
// constructed with it (i.e. when not configured to use ripgrep).
|
||||
let regexString = '.*?';
|
||||
if (search.action) regexString += `${this.escapeRegex(`) ${search.action}: `)}.*?`;
|
||||
if (search.user) regexString += `.*?\\[.*?${this.escapeRegex(search.user)}.*?\\].*?`;
|
||||
if (search.ip) regexString += `${this.escapeRegex(`[${search.ip}`)}.*?\\].*?`;
|
||||
if (search.actionTaker) regexString += `${this.escapeRegex(`by ${search.actionTaker}: `)}.*?`;
|
||||
if (search.note) {
|
||||
const regexGenerator = search.note.isExact ? this.generateRegex : this.escapeRegex;
|
||||
for (const noteSearch of search.note.searches) {
|
||||
regexString += `${regexGenerator(noteSearch)}.*?`;
|
||||
}
|
||||
}
|
||||
|
||||
const query = {
|
||||
rooms: rooms,
|
||||
search: search,
|
||||
isExact: exactSearch,
|
||||
regexString,
|
||||
maxLines: maxLines,
|
||||
onlyPunishments: onlyPunishments,
|
||||
};
|
||||
const response = await PM.query(query);
|
||||
const rawResponse = await PM.query(query);
|
||||
const response = rawResponse.map((line: string, index: number) => parseModlog(line, rawResponse[index + 1]));
|
||||
|
||||
if (response.duration > LONG_QUERY_DURATION) {
|
||||
Monitor.log(`Long modlog query took ${response.duration} ms to complete: ${query}`);
|
||||
|
|
@ -305,9 +353,9 @@ export class Modlog {
|
|||
}
|
||||
|
||||
export const PM = new QueryProcessManager<ModlogQuery, string[] | undefined>(module, async data => {
|
||||
const {rooms, search, isExact, maxLines, onlyPunishments} = data;
|
||||
const {rooms, regexString, maxLines, onlyPunishments} = data;
|
||||
try {
|
||||
return await modlog.runSearch(rooms, search, isExact, maxLines, onlyPunishments);
|
||||
return await modlog.runSearch(rooms, regexString, maxLines, onlyPunishments);
|
||||
} catch (err) {
|
||||
Monitor.crashlog(err, 'A modlog query', data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -784,11 +784,16 @@ export const Punishments = new class {
|
|||
Monitor.log(`[CrisisMonitor] Autolocked user ${name} has public roomauth (${roomauth.join(', ')}), and should probably be demoted.`);
|
||||
}
|
||||
|
||||
const ipStr = typeof user !== 'string' ? ` [${(user as User).latestIp}]` : '';
|
||||
const roomid = typeof room !== 'string' ? (room as Room).roomid : room;
|
||||
const modlogEntry = `AUTO${punishment}: [${userid}]${ipStr}: ${reason}`;
|
||||
Rooms.global.modlog(modlogEntry, roomid);
|
||||
Rooms.get(room)?.modlog(modlogEntry);
|
||||
const logEntry = {
|
||||
action: `AUTO${punishment}`,
|
||||
visualRoomID: typeof room !== 'string' ? (room as Room).roomid : room,
|
||||
ip: typeof user !== 'string' ? (user as User).latestIp : undefined,
|
||||
userid: userid,
|
||||
note: reason,
|
||||
};
|
||||
if (typeof user !== 'string') logEntry.ip = (user as User).latestIp;
|
||||
Rooms.global.modlog(logEntry);
|
||||
Rooms.get(room)?.modlog(logEntry);
|
||||
|
||||
const roomObject = Rooms.get(room);
|
||||
const userObject = Users.get(user);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import {FS} from '../lib/fs';
|
||||
import {Utils} from '../lib/utils';
|
||||
import type {ModlogEntry} from './modlog';
|
||||
|
||||
interface RoomlogOptions {
|
||||
isMultichannel?: boolean;
|
||||
|
|
@ -223,8 +224,8 @@ export class Roomlog {
|
|||
message = message.replace(/<img[^>]* src="data:image\/png;base64,[^">]+"[^>]*>/g, '');
|
||||
void this.roomlogStream.write(timestamp + message + '\n');
|
||||
}
|
||||
modlog(message: string, overrideID?: string) {
|
||||
void Rooms.Modlog.write(this.roomid, message, overrideID);
|
||||
modlog(entry: ModlogEntry, overrideID?: string) {
|
||||
void Rooms.Modlog.write(this.roomid, entry, overrideID);
|
||||
}
|
||||
async rename(newID: RoomID): Promise<true> {
|
||||
const roomlogPath = `logs/chat`;
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ import type {Poll} from './chat-plugins/poll';
|
|||
import type {Announcement} from './chat-plugins/announcements';
|
||||
import type {RoomEvent, RoomEventAlias, RoomEventCategory} from './chat-plugins/room-events';
|
||||
import type {Tournament} from './tournaments/index';
|
||||
import type {ModlogEntry} from './modlog';
|
||||
|
||||
export abstract class BasicRoom {
|
||||
roomid: RoomID;
|
||||
|
|
@ -339,9 +340,9 @@ export abstract class BasicRoom {
|
|||
this.log.roomlog(message);
|
||||
return this;
|
||||
}
|
||||
modlog(message: string) {
|
||||
modlog(entry: ModlogEntry) {
|
||||
const override = this.tour ? `${this.roomid} tournament: ${this.tour.roomid}` : undefined;
|
||||
this.log.modlog(message, override);
|
||||
this.log.modlog(entry, override);
|
||||
return this;
|
||||
}
|
||||
uhtmlchange(name: string, message: string) {
|
||||
|
|
@ -1036,8 +1037,8 @@ export class GlobalRoomState {
|
|||
this.lastWrittenBattle = this.lastBattle;
|
||||
}
|
||||
|
||||
modlog(message: string, overrideID?: string) {
|
||||
void Rooms.Modlog.write('global', message, overrideID);
|
||||
modlog(entry: ModlogEntry, overrideID?: string) {
|
||||
void Rooms.Modlog.write('global', entry, overrideID);
|
||||
}
|
||||
|
||||
writeChatRoomData() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
test/main.js test/lib/**/*.js test/server/**/*.js test/sim/**/*.js
|
||||
test/main.js test/lib/**/*.js test/server/**/*.js test/sim/**/*.js test/tools/**/*.js
|
||||
-g "^((?!\(slow\)).)*$"
|
||||
-R dot
|
||||
-u bdd
|
||||
|
|
|
|||
661
test/tools/modlog/converter.js
Normal file
661
test/tools/modlog/converter.js
Normal file
|
|
@ -0,0 +1,661 @@
|
|||
/**
|
||||
* Tests for modlog conversion tools
|
||||
* @author Annika
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const converter = require('../../../tools/modlog/converter');
|
||||
const ml = require('../../../.server-dist/modlog');
|
||||
|
||||
const garfieldCopypasta = [
|
||||
`[2020-08-24T03:52:00.917Z] (staff) AUTOLOCK: [guest903036] [127.0.0.1]: "Now where could my pipe be?" This... I always come to this, because I was a young man, I'm older now, and I still don't have the secrets, the answers, so this question still rings true, Jon looks up and he thinks: "Now where could my pipe be?", and then it happens, you see it, you see... it's almost like divine intervention, suddenly, it is there, and it overpowers you, a cat is smoking a pipe. It is the mans pipe, it's Jon's pipe, but the cat, this cat, Garfield, is smoking the pipe, and from afar, and from someplace near, but not clear... near but not clear, the man calls out, Jon calls out, he is shocked. "Garfield!" he shouts. Garfield, the cats name. But let's take a step back. Let us examine this from all sides, all perspectives, and when I first came across this comic strip, I was at my fathers house. The newspaper had arrived, and I picked it up for him, and brought it`,
|
||||
`inside. I organized his sections for him and then, yes, the comic strip section fell out from somewhere in the middle, landed on the kitchen floor. I picked up the picture pages and saw up somewhere near the top of this strip, just like Jon, I too was wearing an aquamarine shirt, so I thought, "Hah! Interesting, I'll have to see this later." I snipped out the little comic and held onto it, and 5 days later, I re-examined, and it gripped me, I needed to find out more about this. The information I had was minimal, but enough. An orange cat named Garfield. Okay, that seemed to be the linchpin of this whole operation. Yes, another clue, a signature on the bottom right corner, a mans name, Jim Davis. Yes, I'm onto it for sure, so. 1. Garfield, orange cat, and 2. Jim Davis, the creator of this cat, and that curiously plain man. I did not know at the time that his name was Jon. The strip, you see, had no mention of this mans name, and, I've never seen it before.`,
|
||||
` But I had these clues. Jim Davis, Garfield. And then I saw more, I spotted the tiny copyright at the upper left corner, copyright 1978, to... what is this? Copyright belongs to a "PAWS Incorporated"? I used the local library and mail services to track down the information I was looking for. Jim Davis, a cartoonist, who created a comic strip about a cat, Garfield, and a man, Jon Arbuckle. Well from that point on I made sure I read the Garfield comic strips, but as I read each one, as each day passed, the strips seemed to resonate with me less and less. I sent letters to PAWS Incorporated, long letters, pages upon pages, asking if Mr. Jim Davis could somehow publish just the one comic, over and over again, it would be meditative, I wrote, the strength of that, could you imagine? But, no response. The strips lost their power, and eventually I stopped reading, but... I did not want my perceptions deluded so I vowed to read the pipe strip over and over again.`,
|
||||
` That is what I called it, "The Pipe Strip", The Pipe Strip. Everything about it is perfect, I can only describe it as a miracle creation, something came together, the elements aligned. It is like the comets, the cosmic orchestra that is up there over your head. The immense, enormous void is working all for one thing, to tell you one thing. Gas, and rock and purity and... Nothing! I will say this, when I see the pipe strip, and I mean every single time I look at the lines, the colors, the shapes, that make up the three panel comic, I see perfection. Do I find perfection in many things? Some things I would say, some things are perfect. And this is one of them. I can look at the little tuft of hair on Jon Arbuckle's head, it is the perfect shade, the purple pipe in Garfield's mouth, how could a mere mortal even make this? I have a theory about Jim Davis, after copious research, and yes of course now we have the internet, and all this information is now readily`,
|
||||
` available but... Jim Davis, he used his life experiences to influence his comic. Like I mentioned before, none of them seemed to have the weight of The Pipe Strip, but you have to wonder about the man who is able to even, just once, create the perfect form, a literally flawless execution of art, brilliance! Just as an award, I think there is a spiritual element at work. I've seen my share of bad times, and when you have something, well, it's just, emotions and neurons in your brain, but something tells you it's the truth, truth's radiant light. Garfield the cat? Neurons in my brain, it's, it's harmony you see, Jon and Garfield, it's truly harmony, like a continuous looping everlasting harmony. The lavender chair, the brown end table, the salmon colored wall, the forest green carpet, and Garfield is hunched, perched perhaps, with the pipe stuck firmly between his jowls, his tail curls around. It's more then shapes too because... I... Okay, stay with me, I've done`,
|
||||
` this experiment several times. You take the strip, you trace only the basic elements. You can do anything, you can simplify the shapes down to just blobs, just outlines, but it still makes sense. You can replace the blobs with magazine cutouts of other things, replace Jon Arbuckle with a car parked in a driveway sideways, cut that out of a magazine, stick it in, replace it there in the second panel with a, a food processor, okay. And then we put a picture of the planet in the third panel over Garfield. It still works. These are universal proportions, I don't know how best to explain why it works, I have studied The Pipe Strip, and analyzed Jon and Garfield's proportions against several universal mathematical constants: e, pi, the Golden Ratio, the Feigenbaum constants and so on, and it's surprising, scary, how things align. You can take just tiny pieces of the pipe strip for instance, take Jon's elbow from the second panel, and take that and project it over Jon's entire shape in the second panel,`,
|
||||
` and you'll see a near perfect Fibonacci sequence emerge. It's eerie to me, and it makes you wonder if you were in the presence of a deity, if there is some larger hand at work. There is no doubt in my mind that Jim Davis is a smart man. Jim Davis is capable of anything, to me, he is remarkable, but this is so far beyond that. I think we might see that this work of art is revered and respected in years to come. Jim Davis is possibly a new master of the craft, a genius of the eye, they very well may say the same things about Jim Davis in 500 years that we say about the great philosophical and artistic masters from centuries ago. Jim Davis is a modern day Socrates, or Da Vinci. Mixing both striking visual beauty with classical, daring, unheard of intellect. Look, he combines these things to make profoundly simple expressions. This strip is his masterpiece, the pipe strip, is his masterpiece, and it is a masterpiece and a marvel. I often look at Garfield's... particular pose in this strip, he is poised`,
|
||||
` and statuesque. And this cat stares reminiscent of the fiery gaze often found in religious iconography. But still his eyes are playful, lying somewhere between the solemn father's expression, and Rembrandt's Return of the Prodigal Son, and the coy smirk of Da Vinci's St. John the Baptist, his ears stick up, signifying a peak readiness. It's as if he could at any moment pounce. He is after all a close relative and descendant of the mighty jungle cats of Africa that could leap after prey. You could see the power drawn into Garfield's hindquarters, powerful haunches indeed. The third panel. And I'm just saying this now, this, this is just coming to me now, the third panel of The Pipe Strip is essentially a microcosm for the entire strip itself. All the power dynamics, the struggle for superiority, right? Who has the pipe? Where is the pipe? All of that is drawn, built, layered into Garfield's iconic pose here, you can see it in the curl of his tail, Garfield's ear whiskers stick up on end, the smoke billows`,
|
||||
` upwards drawing the eye upward, the increasing scope, I'm just... amazed, really, that after 33 years of reading and analyzing the same comic strip, I'm able to find new dimensions. It's a testament the work. For six years I delved into tobacco research, because... can a cat smoke? This is a metaphysical question. Yes, can any cat smoke? Do we know? Can just Garfield smoke? The research says no, nicotine poisoning can kill animals, especially household pets. All it takes is the nicotine found in as little as a single cigarette. Surely Jon's pipe holds a substantial amount of tobacco, and it is true that pets living in the homes of smokers are nearly 25% more likely to develop some form of cancer... most likely due to second hand smoke. But these are facts of smoking, and its tolls on our world. But after visiting two tobacco processing plants in Virginia, and the Philip Morris cigarette manufacturing facility, I came no closer to cracking the meaning. I was looking for any insight, a detective of a homicide`,
|
||||
` case has to look at every angle. So I'm always taking apart the pipe strip. I have focused on every minutiae, every detail of this strip. Jon Arbuckle's clothing. I have replicas, I'm an expert in textiles, so you see the smoking thing was a hangup for me What was the statement here? Until... and this is key... this is the breakthrough, the pipe is not a pipe really. Obviously there is symbolism at work here. I saw that from the beginning and I looked at the literal aspect of the strip to gain insight into the metaphors at play, I worked at a newspaper printing press for 18 months in the late 1980's, I was learning the literal to form the gestural, the sub-literal, the in-between. Jon reading the newspaper means so much more then just... Jon reading the newspaper. But how can you ever hope to decipher the puzzle without knowing everything there is to know about newspapers? Okay, for example, Jon holds his paper up with his left hand, thumb gripping the interior. I learned that this particular grip here is the`,
|
||||
` newspaper grip of 19th century aristocrats. And this aristocrat grip was a point of contention that influenced the decision to move forward Prohibition in the United States in the early 20th century. So Jon's hand position is much more then that, it is a comment on class war, and the resulting reactionary culture. But I didn't know about the aristocratic newspaper grip until I came across some microfiche archives at the printing press, it's about information. You have to take it apart... and the breakthrough on a smoking cat came late. Just 8 years ago actually. A smoking cat, is an industry term, it's what the smoking industry calls a tattletale teenager who tells on his friends after they've all tried smoking for the first time, and it is actually a foreign translation, bastardization of the term smoking rat. But the phrase was confused when secret documents when back and forth between China and America These documents are still secret, and the only reason I know about the term is because I know a man, my friend...`,
|
||||
` let's call him Timothy, yes, it's a fake name for his protection. Timothy worked for Philip Morris for 16 years and he had seen the documents. When he told me, it was an "Aha!" moment. And he said "But how? How could this cartoonist Jim Davis know about this obscure term from the mid 70's used exclusively "Yes, a cat in this room would have a hard time differentiating the wall from the floor." Add to that cats' known spatial confusion and you have the makings of a cat rage room. Now she informed me this isn't exactly common knowledge among cat owners. But a seasoned cat owner, or someone particularly perceptive would've picked up on it. So what's incredible here is not only is Garfield's behavior symbolic of the devil and all the evil constructs in the world, but, but, but, but also... it is rooted in science and scientific fact, look at that. You cannot spell fact without cat. Heh. Just a little joke there. Just some wordplay but, getting back on track, and you can't spell track without cat, okay, okay okay. I digress.`,
|
||||
` I gotcha, I gotcha. Enough, kidding around. It is established here that Garfield is in a rage, an ultimate rage of fury and hatred caused by colorblindness. We know the what, we know the why, but let us examine the how. The how of his rage is particularly interesting here, we've looked at his posture and called it powerful, in control... statuesque, et cetera, et cetera. Composed rage. It's peculiar, and I've talked to a number of psychologists and psychiatrists and even a couple of anger management therapists about this concept. Could we see the same kind of behavior in a human? Is Garfield representative of something more specific then just chaos and rage? Deciphering is going to take some perseverance for sure. The psychologists pointed to a phenomenon in humans and yes, I believe one of the anger management counselors brought it up as well. The idea that people often times will bottle their rage. Garfield the cat here, well, he could be bottling his anger inside shoving it deep into his cat gut to ignore and deal with`,
|
||||
` at a later time. Uhh, well, no, that's not exactly right. Garfield has already acted out, he's already stolen the pipe. He's smoking the pipe, he's already dealt with his anger. He's already lashed out, so, psychologically, what is going on here? What is this cat doing and how does it impact his owner Jon Arbuckle? Psychologically. Well Garfield is angry, he is acting on his anger, but is this passive anger or aggressive anger? Passive. It is passive because if Garfield has a problem with Jon specifically, he's choosing a passive way of dealing with that problem. He has not confronted Jon and said "Jon, I have a problem with the way you've decorated this room, as a cat I am colorblind and this sends me into a rage. You've created a rage room for me here and I don't like it, I want you to change it." Instead of that confrontational approach, though, Garfield has chosen to steal Jon's pipe. And that in turn angers Jon. But Jon decides to be aggressively angry and yell at Garfield, so now instead of a calm conversation between`,
|
||||
` two respectful parties, you have two heated, angry individuals, each with a problem and no direct line to solving it. The layered emotions here tell a story with tight, focused brevity that would make Hemingway weep. This is an entire drama in just 3 panels, people. But let's not be remiss and miss the humor of the situation, the... absurdity of it all. For certainly there is a reason the visual shorthand for drama includes both a crying mask and a laughing mask. Comedy and tragedy compliment each other and meld together to create drama. The tension, the height of humanity, the peak of art that reflects back to us our own condition. And here, in its basist form we can laugh at this comic, yes, comic! In which a cat smokes a pipe, heh! When was the last time you've seen such a thing in your life? Never, I presume. I certainly never have. The great Muse Thalia's presence is strong in this world of art here, comedy, it is comedy! And if you look at the structure again you'll see this perfect form of thirds works magically for`,
|
||||
` the transmission of yes, yes, a joke! The joke is as old as time. Even cavemen told jokes. And the joke here is that Jon... has lost his pipe, well he thinks he has. But lo and behold, it is the cat Garfield who has the pipe, surprise, surprise, the cat is smoking! Again the transition from setup to punchline takes place between the second and third panels. But make no mistake, the comic is more than just a comic. Yes it is funny, of course it is. It is operating at the hight of sophisticated humor on par with any of Shakespeare's piercing wit. On the one hand, Garfield the comic with Jon the man, humor as art. The other hand, Garfield comic with Jon the man, stirring, no, riveting drama! As with everything it is tension and release, tension, and release, a cycle. I keep returning to this idea because it is, it is so omnipresent, yes. You could and yes... I have done this on more then one occasion, you could print this comic strip on a giant piece of paper. The dimensions would be something like, 34 inches by 11 inches. Now`,
|
||||
` tape the ends together with the comic facing inward, stick your head in the middle of this Garfield comic loop and read, start at the first panel, Jon is reading the newspaper, he feels for something on the end table. Second panel, he sets the newspaper down, something is not right. "Now where could my pipe be," he thinks. And then the payoff, the third panel. Garfield has Jon's pipe and is smoking it. But ahaha! The paper is in a loop around your head so you can see that once again Jon is in his seat reading the paper, and so on and so on, you could literally read the comic strip for eternity! And spend many a relaxing Sunday afternoon reading this strip over and over. I'm reminded of the Portuguese death carvings with always begin and end with the same scrawled image. So this idea of repetition, of the beginning being the end, and the end being the beginning, it's not new. It is an ageless tradition among the best story tellers humanity has ever offered. And I'm not wrong to include cartoonist Jim Davis in that exalted set for this particular strip alone. I'm not foolish enough to deny that great art is subjective, divisive, even.`,
|
||||
` And that some people see this Garfield and shrug with no real reaction. But I will say that I believe everyone in the world should see it, at the very least, see it. You should all see it, read it. Spend some time with it. Spend an hour reading it. What's an hour? Yes, you could watch some television program, you could play some fast paced video games or computer games, yes, you could do all those things. But it's just an hour. And if you give this strip a chance, if you look into Jon Arbuckle's eyes, if you look into Jon Arbuckle's soul, you might find that you'll really be looking into your own soul. It's self discovery, that is what I'm talking about here. You have the opportunity, the possibility, it could change you. Don't be afraid. You know, just last week, I was eating lunch near the municipal court, like I do every Thursday and, there was a plumbing van, a plumbing van parked out in front, and a man, a plumber, would step out from the court and retrieve something from his van every so often. A few times this happened, I thought nothing of it, just a plumber doing some work at the municipal court. But then he came out and looked`,
|
||||
` through his van and it was clear, he couldn't find something. I noticed and thought, well, that's sort of similar to the Garfield comic in a way. Someone looks for something, can't find it. But yes, that probably happens billions of times a day around the world. But then this plumber put his hands on his hips, then he scratched his head and he said aloud "Now where could my pipe wrench be?" Huh! Well at this I leaped off the bench, sandwich still in hand and I rushed over, I shouted, "What was that you said?!" He looked at me and said "What? I can't find my pipe wrench." and I said "No, no no, say it like how you just said it." He scratched his head and repeated, "Now where could my pipe wrench be?" I slapped him on the back and said "Garfield!" He looked so confused so I said it again, then I said, "Your orange cat took it." Hehe, I laughed and laughed. He smiled and went back into the court room. I walked away knowing that the plumber and I, two complete strangers, bonded over this Garfield comic. You see life imitates art, and becomes a common ground. I have a feeling that if I see this plumber again we'll be sharing stories like two old friends.`,
|
||||
` Because we've been united by art, we have a common love for Jim Davis and his characters, his writings, the humor, the drama, the... that rascal Garfield the cat... Oh, and by the way, if you're wondering I was having for lunch that day it was a ham sandwich with an apple and potato chips, in a bag, I had a soda as well. I think it's important to view the pipe strip in philosophical terms. We've touched briefly on the notion of existentialism. That theme is very prevalent in this strip. Garfield is in fact a modern existential antihero. But if Garfield embodies the bewilderment in a meaningless life, what is Jon? What are the telltale signs that informs Jon's philosophical standpoint, his approach, what style of thinking he represents? Jon is depicted as being grounded in the material world, a world of things, he is surrounded by objects, and he touches these objects, he interacts with them. The newspaper, the end table, the chair, his clothes, all these physical things make up Jon's world.`,
|
||||
` In some sense, even his cat Garfield is an object to him, a thing. The first ideology that comes to mind that comes to mind when thinking of objects in the tangible world... is pragmatism. slur => slur)`,
|
||||
].join('');
|
||||
|
||||
describe('Modlog conversion script', () => {
|
||||
describe('bracket parser', () => {
|
||||
it('should correctly parse parentheses', () => {
|
||||
assert.strictEqual(converter.parseBrackets('(id)', '('), 'id');
|
||||
});
|
||||
|
||||
it('should correctly parse square brackets', () => {
|
||||
assert.strictEqual(converter.parseBrackets('[id]', '['), 'id');
|
||||
});
|
||||
|
||||
it('should correctly parse the wrong type of bracket coming before', () => {
|
||||
assert.strictEqual(converter.parseBrackets('(something) [id]', '['), 'id');
|
||||
assert.strictEqual(converter.parseBrackets('[something] (id)', '('), 'id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('log modernizer', () => {
|
||||
it('should ignore logs that are already modernized', () => {
|
||||
const modernLogs = [
|
||||
'[2020-08-23T19:50:49.944Z] (development) ROOMMODERATOR: [annika] by annika',
|
||||
'[2020-08-23T19:45:24.326Z] (help-uwu) NOTE: by annika: j',
|
||||
'[2020-08-23T19:45:32.346Z] (battle-gen8randombattle-5348538495) NOTE: by annika: k',
|
||||
'[2020-08-23T19:48:14.823Z] (help-uwu) TICKETCLOSE: by annika',
|
||||
'[2020-08-23T19:48:14.823Z] (development) ROOMBAN: [sometroll] alts:[alt1], [alt2] ac:[autoconfirmed] [127.0.0.1] by annika: never uses the room for development',
|
||||
'[2018-01-18T14:30:02.564Z] (tournaments) TOUR CREATE: by ladymonita: gen7randombattle',
|
||||
`[2014-11-24T11:10:34.798Z] (lobby) NOTE: by joimnotesyakcity: lled by his friends`,
|
||||
`[2015-03-18T20:56:19.462Z] (lobby) WARN: [peterpablo] by xfix (Frost was banned for a reason - don't talk about Frost.)`,
|
||||
`[2015-10-23T19:13:58.190Z] (lobby) NOTE: by imas234: [2015-07-31 01:54pm] (lobby) Tru identity was locked from talking by Trickster. (bad Chingu) uh....`,
|
||||
`[2015-11-27T12:26:15.741Z] (lobby) NOTE: by theraven: Arik Ex was banned under Eastglo`,
|
||||
`[2018-01-07T07:13:10.279Z] (lobby) NOTE: by gentlejellicent: Ah, you changed the staffintro to have bloodtext in it`,
|
||||
garfieldCopypasta,
|
||||
];
|
||||
for (const log of modernLogs) {
|
||||
assert.strictEqual(converter.modernizeLog(log), log);
|
||||
}
|
||||
});
|
||||
|
||||
it('should correctly parse old-format promotions and demotions', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) [annika] was promoted to Voice by [heartofetheria].'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) GLOBAL VOICE: [annika] by heartofetheria'
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) ([annika] was demoted to Room regular user by [heartofetheria].)'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) ROOMREGULAR USER: [annika] by heartofetheria: (demote)'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog(`[2017-05-31T22:00:33.159Z] (espanol) vodsrtrainer MAR cos was demoted to Room regular user by [blazask].`),
|
||||
`[2017-05-31T22:00:33.159Z] (espanol) ROOMREGULAR USER: [vodsrtrainermarcos] by blazask: (demote)`
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) ([annika] was demoted to Room Moderator by [heartofetheria].)'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) ROOMMODERATOR: [annika] by heartofetheria: (demote)'
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) [annika] was appointed Room Owner by [heartofetheria].'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) ROOMOWNER: [annika] by heartofetheria'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse entries about modchat and modjoin', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) ([annika] set modchat to autoconfirmed)'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) MODCHAT: by annika: to autoconfirmed'
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) Annika set modjoin to +.'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) MODJOIN: by annika: +'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) Annika turned off modjoin.'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) MODJOIN: by annika: OFF'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) Annika set modjoin to sync.'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) MODJOIN SYNC: by annika'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse modnotes', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog(`[2020-08-23T19:50:49.944Z] (development) ([annika] notes: I'm making a modnote)`),
|
||||
`[2020-08-23T19:50:49.944Z] (development) NOTE: by annika: I'm making a modnote`
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog(`[2017-10-04T20:48:14.592Z] (bigbang) (Lionyx notes: test was banned by lionyx`),
|
||||
`[2017-10-04T20:48:14.592Z] (bigbang) NOTE: by lionyx: test was banned by lionyx`
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse userids containing `notes`', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog(`[2014-11-24T11:10:34.798Z] (lobby) ([joimnotesyakcity] was trolled by his friends)`),
|
||||
`[2014-11-24T11:10:34.798Z] (lobby) [joimnotesyakcity] was trolled by his friends`
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse roomintro and staffintro entries', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) (Annika changed the roomintro.)'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) ROOMINTRO: by annika'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) (Annika changed the staffintro.)'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) STAFFINTRO: by annika'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) (Annika deleted the roomintro.)'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) DELETEROOMINTRO: by annika'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) (Annika deleted the staffintro.)'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) DELETESTAFFINTRO: by annika'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse room description changes', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) ([annika] changed the roomdesc to: "a description".)'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) ROOMDESC: by annika: to "a description"'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse declarations', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) Annika declared I am declaring something'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) DECLARE: by annika: I am declaring something'
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) Annika declared: I am declaring something'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) DECLARE: by annika: I am declaring something'
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) Annika globally declared (chat level) I am chat declaring something'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) CHATDECLARE: by annika: I am chat declaring something'
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) Annika globally declared I am globally declaring something'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) GLOBALDECLARE: by annika: I am globally declaring something'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse entries about roomevents', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) (Annika edited the roomevent titled "Writing Unit Tests".)'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) ROOMEVENT: by annika: edited "Writing Unit Tests"'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) (Annika removed a roomevent titled "Writing Unit Tests".)'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) ROOMEVENT: by annika: removed "Writing Unit Tests"'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) (Annika added a roomevent titled "Writing Unit Tests".)'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) ROOMEVENT: by annika: added "Writing Unit Tests"'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse old-format tournament modlogs', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (tournaments) ([annika] created a tournament in randombattle format.)'),
|
||||
'[2020-08-23T19:50:49.944Z] (tournaments) TOUR CREATE: by annika: randombattle'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (tournaments) ([heartofetheria] was disqualified from the tournament by Annika)'),
|
||||
'[2020-08-23T19:50:49.944Z] (tournaments) TOUR DQ: [heartofetheria] by annika'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (tournaments) (The tournament auto disqualify timeout was set to 2 by Annika)'),
|
||||
'[2020-08-23T19:50:49.944Z] (tournaments) TOUR AUTODQ: by annika: 2'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse old-format roombans', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) [heartofetheria] was banned from room development by annika'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) ROOMBAN: [heartofetheria] by annika'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) [heartofetheria] was banned from room development by annika (reason)'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) ROOMBAN: [heartofetheria] by annika: reason'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog(`[2015-06-07T13:44:30.057Z] (shituusers) ROOMBAN: [eyan] (You have been kicked by +Cynd(~'e')~quil. Reason: Undefined) by shituubot`),
|
||||
`[2015-06-07T13:44:30.057Z] (shituusers) ROOMBAN: [eyan] by shituubot: You have been kicked by +Cynd(~'e')~quil. Reason: Undefined`
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse old-format mutes', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) [heartofetheria] was muted by annikafor1hour (reason)'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) HOURMUTE: [heartofetheria] by annika: reason'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) heartofetheria was muted by Annika for 1 hour (reason)'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) HOURMUTE: [heartofetheria] by annika: reason'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2016-09-27T18:25:55.574Z] (swag) harembe⚠ was muted by rubyfor1hour (harembe was promoted to ! by ruby.)'),
|
||||
'[2016-09-27T18:25:55.574Z] (swag) HOURMUTE: [harembe] by ruby: harembe was promoted to ! by ruby.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse old-format weeklocks', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) heartofetheria was locked from talking for a week by annika (reason) [IP]'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) WEEKLOCK: [heartofetheria] [IP] by annika: reason'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse old-format global bans', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) [heartofetheria] was banned by annika (reason) [IP]'),
|
||||
'[2020-08-23T19:50:49.944Z] (development) BAN: [heartofetheria] [IP] by annika: reason'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse alts using nextLine', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog(
|
||||
'[2020-08-23T19:50:49.944Z] (development) heartofetheria was locked from talking for a week by annika (reason)',
|
||||
`[2020-08-23T19:50:49.944Z] (development) ([heartofetheria]'s locked alts: [annika0], [hordeprime])`
|
||||
),
|
||||
'[2020-08-23T19:50:49.944Z] (development) WEEKLOCK: [heartofetheria] alts: [annika0], [hordeprime] by annika: reason'
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog(
|
||||
'[2020-08-23T19:50:49.944Z] (development) [heartofetheria] was banned from room development by annika',
|
||||
`[2020-08-23T19:50:49.944Z] (development) ([heartofetheria]'s banned alts: [annika0], [hordeprime])`
|
||||
),
|
||||
'[2020-08-23T19:50:49.944Z] (development) ROOMBAN: [heartofetheria] alts: [annika0], [hordeprime] by annika'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse poll modlogs', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) ([apoll] was started by [annika].)',),
|
||||
'[2020-08-23T19:50:49.944Z] (development) POLL: by annika'
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (development) ([thepoll] was ended by [annika].)',),
|
||||
'[2020-08-23T19:50:49.944Z] (development) POLL END: by annika'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse Trivia modlogs', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (trivia) (User annika won the game of Triumvirate mode trivia under the All category with a cap of 50 points, with 50 points and 10 correct answers! Second place: heartofetheria (10 points), third place: hordeprime (5 points))'),
|
||||
'[2020-08-23T19:50:49.944Z] (trivia) TRIVIAGAME: by unknown: User annika won the game of Triumvirate mode trivia under the All category with a cap of 50 points, with 50 points and 10 correct answers! Second place: heartofetheria (10 points), third place: hordeprime (5 points)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle claiming helptickets', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (help-heartofetheria) Annika claimed this ticket.'),
|
||||
'[2020-08-23T19:50:49.944Z] (help-heartofetheria) TICKETCLAIM: by annika'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (help-heartofetheria) This ticket is now claimed by Annika.'),
|
||||
'[2020-08-23T19:50:49.944Z] (help-heartofetheria) TICKETCLAIM: by annika'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (help-heartofetheria) This ticket is now claimed by [annika]'),
|
||||
'[2020-08-23T19:50:49.944Z] (help-heartofetheria) TICKETCLAIM: by annika'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle closing helptickets', () => {
|
||||
// Abandonment
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (help-heartofetheria) This ticket is no longer claimed.'),
|
||||
'[2020-08-23T19:50:49.944Z] (help-heartofetheria) TICKETUNCLAIM'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (help-heartofetheria) Heart of Etheria is no longer interested in this ticket.'),
|
||||
'[2020-08-23T19:50:49.944Z] (help-heartofetheria) TICKETABANDON: by heartofetheria'
|
||||
);
|
||||
|
||||
// Closing
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (help-heartofetheria) Annika closed this ticket.'),
|
||||
'[2020-08-23T19:50:49.944Z] (help-heartofetheria) TICKETCLOSE: by annika'
|
||||
);
|
||||
|
||||
// Deletion
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (help-heartofetheria) Annika deleted this ticket.'),
|
||||
'[2020-08-23T19:50:49.944Z] (help-heartofetheria) TICKETDELETE: by annika'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle opening helptickets', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (help-heartofetheria) Heart of Etheria opened a new ticket. Issue: Being trapped in a unit test factory'),
|
||||
'[2020-08-23T19:50:49.944Z] (help-heartofetheria) TICKETOPEN: by heartofetheria: Being trapped in a unit test factory'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Scavengers modlogs', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (scavengers) SCAV SETHOSTPOINTS: [room: subroom] by annika: 42'),
|
||||
'[2020-08-23T19:50:49.944Z] (scavengers) SCAV SETHOSTPOINTS: by annika: 42 [room: subroom]'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (scavengers) SCAV TWIST: [room: subroom] by annika: your mom'),
|
||||
'[2020-08-23T19:50:49.944Z] (scavengers) SCAV TWIST: by annika: your mom [room: subroom]'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (scavengers) SCAV SETPOINTS: [room: subroom] by annika: ååååååå'),
|
||||
'[2020-08-23T19:50:49.944Z] (scavengers) SCAV SETPOINTS: by annika: ååååååå [room: subroom]'
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog('[2020-08-23T19:50:49.944Z] (scavengers) ([annika] has been caught attempting a hunt with 2 connections on the account. The user has also been given 1 infraction point on the player leaderboard.)'),
|
||||
'[2020-08-23T19:50:49.944Z] (scavengers) SCAV CHEATER: [annika]: caught attempting a hunt with 2 connections on the account; has also been given 1 infraction point on the player leaderboard'
|
||||
);
|
||||
// No moderator actions containing has been caught trying to do their own hunt found on room scavengers.
|
||||
// Apparently this never got written to main's modlog, so I am not going to write a special test case
|
||||
// and converter logic for it.
|
||||
});
|
||||
|
||||
it('should handle Wi-Fi modlogs', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog(`[2020-08-23T19:50:49.944Z] (wifi) GIVEAWAY WIN: Annika won Heart of Etheria's giveaway for a "deluxe shitposter 1000" (OT: Entrapta TID: 1337)`),
|
||||
`[2020-08-23T19:50:49.944Z] (wifi) GIVEAWAY WIN: [annika]: Heart of Etheria's giveaway for a "deluxe shitposter 1000" (OT: Entrapta TID: 1337)`
|
||||
);
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog(`[2020-08-23T19:50:49.944Z] (wifi) GTS FINISHED: Annika has finished their GTS giveaway for "deluxe shitposter 2000"`),
|
||||
`[2020-08-23T19:50:49.944Z] (wifi) GTS FINISHED: [annika]: their GTS giveaway for "deluxe shitposter 2000"`
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle global declarations mentioning promotions correctly', () => {
|
||||
assert.strictEqual(
|
||||
converter.modernizeLog(`[2015-07-21T06:04:54.369Z] (lobby) xfix declared GrumpyGungan was promoted to a global voice, feel free to congratulate him :-).`),
|
||||
`[2015-07-21T06:04:54.369Z] (lobby) DECLARE: by xfix: GrumpyGungan was promoted to a global voice, feel free to congratulate him :-).`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('text entry to ModlogEntry converter', () => {
|
||||
it('should correctly parse modernized promotions and demotions', () => {
|
||||
assert.deepStrictEqual(
|
||||
converter.parseModlog(`[2020-08-23T19:50:49.944Z] (development) ROOMMODERATOR: [annika] by heartofetheria`),
|
||||
{
|
||||
action: 'ROOMMODERATOR', roomID: 'development', userid: 'annika',
|
||||
isGlobal: false, loggedBy: 'heartofetheria', time: 1598212249944,
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
converter.parseModlog(`[2020-08-23T19:50:49.944Z] (development) ROOMVOICE: [annika] by heartofetheria: (demote)`),
|
||||
{
|
||||
action: 'ROOMVOICE', roomID: 'development', userid: 'annika',
|
||||
isGlobal: false, loggedBy: 'heartofetheria', note: '(demote)', time: 1598212249944,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should not mess up HIDEALTSTEXT', () => {
|
||||
// HIDEALTSTEXT apparently was causing bugs
|
||||
assert.deepStrictEqual(
|
||||
converter.parseModlog(`[2020-08-23T19:50:49.944Z] (development) HIDEALTSTEXT: [auser] alts:[alt1] by annika: hnr`),
|
||||
{
|
||||
action: 'HIDEALTSTEXT', roomID: 'development', userid: 'auser', alts: ['alt1'],
|
||||
note: 'hnr', isGlobal: false, loggedBy: 'annika', time: 1598212249944,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse modernized punishments, including alts/IP/autoconfirmed', () => {
|
||||
assert.deepStrictEqual(
|
||||
converter.parseModlog(`[2020-08-23T19:50:49.944Z] (development) WEEKLOCK: [gejg] ac: [annika] alts: [annalytically], [heartofetheria] [127.0.0.1] by somemod: terrible user`),
|
||||
{
|
||||
action: 'WEEKLOCK', roomID: 'development', userid: 'gejg', autoconfirmedID: 'annika', alts: ['annalytically', 'heartofetheria'],
|
||||
ip: '127.0.0.1', isGlobal: false, loggedBy: 'somemod', note: 'terrible user', time: 1598212249944,
|
||||
}
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
converter.parseModlog(`[2020-08-23T19:50:49.944Z] (development) WEEKLOCK: [gejg] ac:[annika] alts:[annalytically], [heartofetheria] [127.0.0.1] by somemod: terrible user`),
|
||||
{
|
||||
action: 'WEEKLOCK', roomID: 'development', userid: 'gejg', autoconfirmedID: 'annika', alts: ['annalytically', 'heartofetheria'],
|
||||
ip: '127.0.0.1', isGlobal: false, loggedBy: 'somemod', note: 'terrible user', time: 1598212249944,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
assert.deepStrictEqual(
|
||||
converter.parseModlog(`[2020-08-23T19:50:49.944Z] (development) WEEKLOCK: [gejg] alts:[annalytically] [127.0.0.1] by somemod: terrible user`),
|
||||
{
|
||||
action: 'WEEKLOCK', roomID: 'development', userid: 'gejg', alts: ['annalytically'],
|
||||
ip: '127.0.0.1', isGlobal: false, loggedBy: 'somemod', note: 'terrible user', time: 1598212249944,
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
converter.parseModlog(`[2020-08-23T19:50:49.944Z] (development) WEEKLOCK: [gejg] [127.0.0.1] by somemod: terrible user`),
|
||||
{
|
||||
action: 'WEEKLOCK', roomID: 'development', userid: 'gejg',
|
||||
ip: '127.0.0.1', isGlobal: false, loggedBy: 'somemod', note: 'terrible user', time: 1598212249944,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse modnotes', () => {
|
||||
assert.deepStrictEqual(
|
||||
converter.parseModlog(`[2020-08-23T19:50:49.944Z] (development) NOTE: by annika: HELP! I'm trapped in a unit test factory...`),
|
||||
{
|
||||
action: 'NOTE', roomID: 'development', isGlobal: false, loggedBy: 'annika',
|
||||
note: `HELP! I'm trapped in a unit test factory...`, time: 1598212249944,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly parse visual roomids', () => {
|
||||
const withVisualID = converter.parseModlog(`[time] (battle-gen7randombattle-1 tournament: development) SOMETHINGBORING: by annika`);
|
||||
assert.strictEqual(withVisualID.visualRoomID, 'battle-gen7randombattle-1 tournament: development');
|
||||
assert.strictEqual(withVisualID.roomID, 'battle-gen7randombattle-1');
|
||||
|
||||
const noVisualID = converter.parseModlog(`[time] (battle-gen7randombattle-1) SOMETHINGBORING: by annika`);
|
||||
assert.strictEqual(noVisualID.visualRoomID, undefined);
|
||||
});
|
||||
|
||||
it('should properly handle OLD MODLOG', () => {
|
||||
assert.deepStrictEqual(
|
||||
converter.parseModlog(`[2014-11-20T13:46:00.288Z] (lobby) OLD MODLOG: by unknown: [punchoface] would be muted by [thecaptain] but was already muted.)`),
|
||||
{
|
||||
action: 'OLD MODLOG', roomID: 'lobby', isGlobal: false, loggedBy: 'unknown',
|
||||
note: `[punchoface] would be muted by [thecaptain] but was already muted.)`, time: 1416491160288,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly handle hangman', () => {
|
||||
assert.deepStrictEqual(
|
||||
converter.parseModlog(`[2020-09-19T23:25:24.908Z] (lobby) HANGMAN: by archastl`),
|
||||
{action: 'HANGMAN', roomID: 'lobby', isGlobal: false, loggedBy: 'archastl', time: 1600557924908}
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly handle nonstandard alt formats', () => {
|
||||
assert.deepStrictEqual(
|
||||
converter.parseModlog(
|
||||
`[2018-01-18T19:47:11.404Z] (battle-gen7randombattle-690788015) AUTOLOCK: [trreckko] alts:[MasterOP13, [luckyfella], Derp11223, [askul], vfffgcfvgvfghj, trreckko, MrShnugglebear] [127.0.0.1]: Pornhub__.__com/killyourself`
|
||||
).alts,
|
||||
['masterop13', 'luckyfella', 'derp11223', 'askul', 'vfffgcfvgvfghj', 'trreckko', 'mrshnugglebear']
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
converter.parseModlog(
|
||||
`[2018-01-20T10:19:19.763Z] (battle-gen7randombattle-691544312) AUTOLOCK: [zilgo] alts:[[ghjkjguygjbjb], zilgo] [127.0.0.1]: www__.__pornhub__.__com`
|
||||
).alts,
|
||||
['ghjkjguygjbjb', 'zilgo']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ModlogEntry to text converter', () => {
|
||||
it('should handle all fields of the ModlogEntry object', () => {
|
||||
const entry = {
|
||||
action: 'UNITTEST',
|
||||
roomID: 'development',
|
||||
userid: 'annika',
|
||||
autoconfirmedID: 'heartofetheria',
|
||||
alts: ['googlegoddess', 'princessentrapta'],
|
||||
ip: '127.0.0.1',
|
||||
isGlobal: false,
|
||||
loggedBy: 'yourmom',
|
||||
note: 'Hey Adora~',
|
||||
time: 1598212249944,
|
||||
};
|
||||
assert.strictEqual(
|
||||
converter.rawifyLog(entry),
|
||||
`[2020-08-23T19:50:49.944Z] (development) UNITTEST: [annika] ac: [heartofetheria] alts: [googlegoddess], [princessentrapta] [127.0.0.1] by yourmom: Hey Adora~\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle OLD MODLOG', () => {
|
||||
assert.deepStrictEqual(
|
||||
converter.rawifyLog({
|
||||
action: 'OLD MODLOG', roomID: 'development', isGlobal: false, loggedBy: 'unknown',
|
||||
note: `hello hi test`, time: 1598212249944,
|
||||
}),
|
||||
`[2020-08-23T19:50:49.944Z] (development) OLD MODLOG: by unknown: hello hi test\n`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle hangman', () => {
|
||||
assert.deepStrictEqual(
|
||||
converter.rawifyLog({action: 'HANGMAN', roomID: 'lobby', isGlobal: false, loggedBy: 'archastl', time: 1600557924908}),
|
||||
`[2020-09-19T23:25:24.908Z] (lobby) HANGMAN: by archastl\n`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reversability', () => {
|
||||
it('should be reversible', () => {
|
||||
const tests = [
|
||||
`[2020-08-23T19:50:49.944Z] (development) OLD MODLOG: by unknown: hello hi test`,
|
||||
`[2014-11-20T13:46:00.288Z] (lobby) OLD MODLOG: by unknown: [punchoface] would be muted by [thecaptain] but was already muted.)`,
|
||||
`[2017-04-20T18:20:42.408Z] (1v1) OLD MODLOG: by unknown: The tournament auto disqualify timer was set to 2 by Scrappie`,
|
||||
`[2020-09-19T23:28:49.309Z] (lobby) HANGMAN: by archastl`,
|
||||
`[2020-09-20T22:57:27.263Z] (lobby) NOTIFYRANK: by officerjenny: %, You are the last staff left in lobby.`,
|
||||
`[2020-08-12T00:23:49.183Z] (lobby) MUTE: [hypergigahax] [127.0.0.1]: please only speak english. Checking your modlog you should have understood the rules`,
|
||||
`[2020-09-07T02:17:22.132Z] (lobby) ROOMBAN: [gamingisawesome] [127.0.0.1] by tenshi: wow this is the most written by a 10 year old sentence I've read in a long time`,
|
||||
`[2017-10-04T20:48:14.592Z] (bigbang) NOTE: by lionyx: test was banned by lionyx`,
|
||||
`[2018-01-18T18:09:49.765Z] (global) NAMELOCK: [guest130921] alts: [guest130921]: [127.0.0.1] NameMonitor: vvvv.xxx`,
|
||||
`[2018-01-19T01:12:30.660Z] (wifi) GIVEAWAY WIN: [ikehylt]: rareassassin90's giveaway for "(Previously won from another giveaway) Paradise the Shiny VC Ho-Oh - Poké Ball - Adamant - Regenerator - (31/31/31/30/31/31) Proof: https://imgur.com/a/5enjr, move list: nightmare, curse zap cannon, dragon breath" (OT: Kaushik TID: 00001 FC: 0276-3231-6110)`,
|
||||
`[2018-02-12T23:44:16.498Z] (wifi) GTS FINISHED: [throwingstar]: their GTS giveaway for "Heracross - Adamant - Guts - '' Battle ready '' - 31/31/31/x/31/31 - 248Hp/252atk/8SpDef - [[item: beast ball]] - It's holding a [[item: flame orb]] because [[item: heracronite]] won't pass through GTS - OT TSK, 479870"`,
|
||||
`[2016-03-09T02:39:04.064Z] (groupchat-arandomduck-becausememes) ROOMBAN: [theeaglesarecoming] by arandomduck: (A game of hangman was started WORST FUCKIN MOD/DRIVER ON THIS SERVER SLKDJFS)`,
|
||||
garfieldCopypasta,
|
||||
];
|
||||
for (const test of tests) {
|
||||
assert.strictEqual(test, converter.rawifyLog(converter.parseModlog(test)).replace('\n', ''));
|
||||
}
|
||||
});
|
||||
|
||||
it('multiline entries should be reversible', () => {
|
||||
const originalConvert = converter.rawifyLog(converter.parseModlog(
|
||||
`[2014-11-20T16:30:17.661Z] (lobby) LOCK: [violight] (spamming) by joim`,
|
||||
`[2014-11-20T16:30:17.673Z] (lobby) (violight's ac account: violight)`
|
||||
)).replace('\n', '');
|
||||
assert.strictEqual(originalConvert, converter.rawifyLog(converter.parseModlog(originalConvert)).replace('\n', ''));
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('integration tests', () => {
|
||||
it('should convert from SQLite to text', async () => {
|
||||
const modlog = new ml.Modlog(':memory:', true);
|
||||
const mlConverter = new converter.ModlogConverterSQLite('', '', modlog.database);
|
||||
|
||||
const entry = {
|
||||
action: 'UNITTEST',
|
||||
roomID: 'development',
|
||||
userid: 'annika',
|
||||
autoconfirmedID: 'heartofetheria',
|
||||
alts: ['googlegoddess', 'princessentrapta'],
|
||||
ip: '127.0.0.1',
|
||||
isGlobal: false,
|
||||
loggedBy: 'yourmom',
|
||||
note: 'Write 1',
|
||||
time: 1598212249944,
|
||||
};
|
||||
modlog.write('development', entry);
|
||||
entry.time++;
|
||||
entry.note = 'Write 2';
|
||||
modlog.write('development', entry);
|
||||
entry.time++;
|
||||
entry.note = 'Write 3';
|
||||
modlog.write('development', entry);
|
||||
modlog.write('development', {
|
||||
action: 'GLOBAL UNITTEST',
|
||||
roomID: 'development',
|
||||
userid: 'annika',
|
||||
autoconfirmedID: 'heartofetheria',
|
||||
alts: ['googlegoddess', 'princessentrapta'],
|
||||
ip: '127.0.0.1',
|
||||
isGlobal: true,
|
||||
loggedBy: 'yourmom',
|
||||
note: 'Global test',
|
||||
time: 1598212249947,
|
||||
});
|
||||
|
||||
await mlConverter.toTxt();
|
||||
assert.strictEqual(
|
||||
mlConverter.isTesting.files.get('/modlog_development.txt'),
|
||||
`[2020-08-23T19:50:49.944Z] (development) UNITTEST: [annika] ac: [heartofetheria] alts: [googlegoddess], [princessentrapta] [127.0.0.1] by yourmom: Write 1\n` +
|
||||
`[2020-08-23T19:50:49.945Z] (development) UNITTEST: [annika] ac: [heartofetheria] alts: [googlegoddess], [princessentrapta] [127.0.0.1] by yourmom: Write 2\n` +
|
||||
`[2020-08-23T19:50:49.946Z] (development) UNITTEST: [annika] ac: [heartofetheria] alts: [googlegoddess], [princessentrapta] [127.0.0.1] by yourmom: Write 3\n` +
|
||||
`[2020-08-23T19:50:49.947Z] (development) GLOBAL UNITTEST: [annika] ac: [heartofetheria] alts: [googlegoddess], [princessentrapta] [127.0.0.1] by yourmom: Global test\n`
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
mlConverter.isTesting.files.get('/modlog_global.txt'),
|
||||
`[2020-08-23T19:50:49.947Z] (development) GLOBAL UNITTEST: [annika] ac: [heartofetheria] alts: [googlegoddess], [princessentrapta] [127.0.0.1] by yourmom: Global test\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('should convert from text to SQLite, including old-format lines and nonsense lines', async () => {
|
||||
const lines = [
|
||||
`[2020-08-23T19:50:49.944Z] (development) UNITTEST: [annika] ac: [heartofetheria] alts: [googlegoddess], [princessentrapta] [127.0.0.1] by yourmom: Write 1`,
|
||||
`[2020-08-23T19:50:49.945Z] (development) GLOBAL UNITTEST: [annika] ac: [heartofetheria] alts: [googlegoddess], [princessentrapta] [127.0.0.1] by yourmom: Global Write`,
|
||||
`[2020-08-23T19:50:49.946Z] (development tournament: lobby) UNITTEST: [annika] ac: [heartofetheria] alts: [googlegoddess], [princessentrapta] [127.0.0.1] by yourmom: Write 3`,
|
||||
];
|
||||
const globalLines = [
|
||||
`[2020-08-23T19:50:49.945Z] (development) GLOBAL UNITTEST: [annika] ac: [heartofetheria] alts: [googlegoddess], [princessentrapta] [127.0.0.1] by yourmom: Global Write`,
|
||||
];
|
||||
const mlConverter = new converter.ModlogConverterTxt('', '', new Map([
|
||||
['modlog_development.txt', lines.join('\n')],
|
||||
['modlog_global.txt', globalLines.join('\n')],
|
||||
]));
|
||||
|
||||
const database = await mlConverter.toSQLite();
|
||||
const globalEntries = database
|
||||
.prepare(`SELECT *, (SELECT group_concat(userid, ',') FROM alts WHERE alts.modlog_id = modlog.modlog_id) as alts FROM modlog WHERE roomid LIKE 'global-%'`)
|
||||
.all();
|
||||
const entries = database
|
||||
.prepare(`SELECT *, (SELECT group_concat(userid, ',') FROM alts WHERE alts.modlog_id = modlog.modlog_id) as alts FROM modlog WHERE roomid IN (?, ?) ORDER BY timestamp ASC`)
|
||||
.all('development', 'trivia');
|
||||
|
||||
assert.strictEqual(globalEntries.length, globalLines.length);
|
||||
assert.strictEqual(entries.length, lines.length);
|
||||
|
||||
const visualIDEntry = entries[entries.length - 1];
|
||||
|
||||
assert.strictEqual(visualIDEntry.note, 'Write 3');
|
||||
assert.strictEqual(visualIDEntry.visual_roomid, 'development tournament: lobby');
|
||||
assert.ok(!globalEntries[0].visual_roomid);
|
||||
|
||||
assert.strictEqual(globalEntries[0].timestamp, 1598212249945);
|
||||
assert.strictEqual(globalEntries[0].roomid.replace(/^global-/, ''), 'development');
|
||||
assert.strictEqual(globalEntries[0].action, 'GLOBAL UNITTEST');
|
||||
assert.strictEqual(globalEntries[0].action_taker_userid, 'yourmom');
|
||||
assert.strictEqual(globalEntries[0].userid, 'annika');
|
||||
assert.strictEqual(globalEntries[0].autoconfirmed_userid, 'heartofetheria');
|
||||
assert.strictEqual(globalEntries[0].ip, '127.0.0.1');
|
||||
assert.strictEqual(globalEntries[0].note, 'Global Write');
|
||||
assert.strictEqual(globalEntries[0].alts, 'googlegoddess,princessentrapta');
|
||||
});
|
||||
});
|
||||
});
|
||||
45
tools/modlog/convert
Executable file
45
tools/modlog/convert
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* This script can be used to convert databases from one format to another
|
||||
* Format:
|
||||
* `node tools/modlog/convert --db path/to/database --logdir path/to/modlogs --from <database format> --to <database format>`
|
||||
* Usage example:
|
||||
* `node tools/modlog/convert --db databases/modlog.db --logdir logs/modlog --from txt --to sqlite`
|
||||
* @author jetou
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const ModlogConverter = require(`${__dirname}/converter.js`).ModlogConverter;
|
||||
const path = require('path');
|
||||
|
||||
function parseFlags() {
|
||||
const args = process.argv.slice(2);
|
||||
const flags = Object.create(null);
|
||||
for (const [idx, arg] of args.entries()) {
|
||||
if (idx % 2 === 0) {
|
||||
flags[arg] = args[idx + 1];
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
const flags = parseFlags();
|
||||
|
||||
const databasePath = flags['--db'];
|
||||
const textLogPath = flags['--logdir'];
|
||||
const outputLogPath = flags['--outputlogdir'];
|
||||
const from = flags['--from'];
|
||||
const to = flags['--to'];
|
||||
|
||||
if (
|
||||
!((databasePath || outputLogPath) && textLogPath && from && to) ||
|
||||
!['txt', 'sqlite'].includes(from) ||
|
||||
!['txt', 'sqlite'].includes(to)
|
||||
) {
|
||||
throw new Error(`Invalid arguments specified.\nUsage: node tools/modlog/convert --db path/to/database --logdir path/to/modlogs --from <database format> --to <database format>.`);
|
||||
}
|
||||
|
||||
console.log(`Converting ${from === 'txt' ? `the text modlog files in ${textLogPath}` : `${databasePath}`} from ${from} to ${to}...`);
|
||||
ModlogConverter.convert(from, to, path.resolve(databasePath || ""), path.resolve(textLogPath), path.resolve(outputLogPath));
|
||||
496
tools/modlog/converter.ts
Normal file
496
tools/modlog/converter.ts
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
|
||||
/**
|
||||
* Converts modlogs between text and SQLite; also modernizes old-format modlogs
|
||||
* @author Annika
|
||||
* @author jetou
|
||||
*/
|
||||
|
||||
// @ts-ignore Needed for FS
|
||||
if (!global.Config) global.Config = {nofswriting: false};
|
||||
|
||||
import {FS} from '../../lib/fs';
|
||||
import {ModlogEntry} from '../../server/modlog';
|
||||
import {IPTools} from '../../server/ip-tools';
|
||||
|
||||
type ModlogFormat = 'txt';
|
||||
|
||||
/** The number of modlog entries to write to the database on each transaction */
|
||||
const ENTRIES_TO_BUFFER = 100;
|
||||
|
||||
export function parseBrackets(line: string, openingBracket: '(' | '[', greedy?: boolean) {
|
||||
const brackets = {
|
||||
'(': ')',
|
||||
'[': ']',
|
||||
};
|
||||
const bracketOpenIndex = line.indexOf(openingBracket);
|
||||
const bracketCloseIndex = greedy ? line.lastIndexOf(brackets[openingBracket]) : line.indexOf(brackets[openingBracket]);
|
||||
if (bracketCloseIndex < 0 || bracketOpenIndex < 0) return '';
|
||||
return line.slice(bracketOpenIndex + 1, bracketCloseIndex);
|
||||
}
|
||||
|
||||
function toID(text: any): ID {
|
||||
return (text && typeof text === "string" ? text : "").toLowerCase().replace(/[^a-z0-9]+/g, "") as ID;
|
||||
}
|
||||
|
||||
export function modernizeLog(line: string, nextLine?: string): string | undefined {
|
||||
// first we save and remove the timestamp and the roomname
|
||||
const prefix = line.match(/\[.+?\] \(.+?\) /i)?.[0];
|
||||
if (!prefix) return;
|
||||
if (/\]'s\s.*\salts: \[/.test(line)) return;
|
||||
line = line.replace(prefix, '');
|
||||
|
||||
if (line.startsWith('(') && line.endsWith(')')) {
|
||||
line = line.slice(1, -1);
|
||||
}
|
||||
const getAlts = () => {
|
||||
let alts;
|
||||
const regex = new RegExp(`\\(\\[.*\\]'s (locked|muted|banned) alts: (\\[.*\\])\\)`);
|
||||
nextLine?.replace(regex, (a, b, rawAlts) => {
|
||||
alts = rawAlts;
|
||||
return '';
|
||||
});
|
||||
return alts ? `alts: ${alts} ` : ``;
|
||||
};
|
||||
|
||||
// Special cases
|
||||
|
||||
if (line.startsWith('SCAV ')) {
|
||||
line = line.replace(/: (\[room: .*?\]) by (.*)/, (match, roominfo, rest) => `: by ${rest} ${roominfo}`);
|
||||
}
|
||||
line = line.replace(/(GIVEAWAY WIN|GTS FINISHED): ([A-Za-z0-9].*?)(won|has finished)/, (match, action, user) => {
|
||||
return `${action}: [${toID(user)}]:`;
|
||||
});
|
||||
|
||||
if (line.includes(':')) {
|
||||
const possibleModernAction = line.slice(0, line.indexOf(':')).trim();
|
||||
if (possibleModernAction === possibleModernAction.toUpperCase()) {
|
||||
if (possibleModernAction.includes('[')) {
|
||||
// for corrupted lines
|
||||
const [drop, ...keep] = line.split('[');
|
||||
process.stderr.write(`Ignoring malformed line: ${drop}\n`);
|
||||
return modernizeLog(keep.join(''));
|
||||
}
|
||||
if (/\(.+\) by [a-z0-9]{1,19}$/.test(line) && !['OLD MODLOG', 'NOTE'].includes(possibleModernAction)) {
|
||||
// weird reason formatting
|
||||
const reason = parseBrackets(line, '(', true);
|
||||
return `${prefix}${line.replace(` (${reason})`, '')}: ${reason}`;
|
||||
}
|
||||
// Log is already modernized
|
||||
return `${prefix}${line}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (/\[(the|a)poll\] was (started|ended) by/.test(line)) {
|
||||
const actionTaker = toID(line.slice(line.indexOf(' by ') + ' by '.length));
|
||||
const isEnding = line.includes('was ended by');
|
||||
return `${prefix}POLL${isEnding ? ' END' : ''}: by ${actionTaker}`;
|
||||
}
|
||||
if (/User (.*?) won the game of (.*?) mode trivia/.test(line)) {
|
||||
return `${prefix}TRIVIAGAME: by unknown: ${line}`;
|
||||
}
|
||||
|
||||
const modernizerTransformations: {[k: string]: (log: string) => string} = {
|
||||
'notes: ': (log) => {
|
||||
const [actionTaker, ...rest] = line.split(' notes: ');
|
||||
return `NOTE: by ${toID(actionTaker)}: ${rest.join('')}`;
|
||||
},
|
||||
|
||||
' declared': (log) => {
|
||||
let newAction = 'DECLARE';
|
||||
let oldAction = ' declared';
|
||||
if (log.includes(' globally declared')) {
|
||||
oldAction = ' globally declared';
|
||||
newAction = 'GLOBALDECLARE';
|
||||
}
|
||||
if (log.includes('(chat level)')) {
|
||||
oldAction += ' (chat level)';
|
||||
newAction = `CHATDECLARE`;
|
||||
}
|
||||
|
||||
const actionTakerName = toID(log.slice(0, log.lastIndexOf(oldAction)));
|
||||
|
||||
log = log.slice(actionTakerName.length);
|
||||
log = log.slice(oldAction.length);
|
||||
log = log.replace(/^\s?:/, '').trim();
|
||||
return `${newAction}: by ${actionTakerName}: ${log}`;
|
||||
},
|
||||
|
||||
'changed the roomdesc to: ': (log) => {
|
||||
const actionTaker = parseBrackets(log, '[');
|
||||
log = log.slice(actionTaker.length + 3);
|
||||
log = log.slice('changed the roomdesc to: '.length + 1, -2);
|
||||
return `ROOMDESC: by ${actionTaker}: to "${log}"`;
|
||||
},
|
||||
|
||||
'roomevent titled "': (log) => {
|
||||
let action;
|
||||
if (log.includes(' added a roomevent titled "')) {
|
||||
action = 'added a';
|
||||
} else if (log.includes(' removed a roomevent titled "')) {
|
||||
action = 'removed a';
|
||||
} else {
|
||||
action = 'edited the';
|
||||
}
|
||||
const actionTakerName = log.slice(0, log.lastIndexOf(` ${action} roomevent titled "`));
|
||||
log = log.slice(actionTakerName.length + 1);
|
||||
const eventName = log.slice(` ${action} roomevent titled `.length, -2);
|
||||
return `ROOMEVENT: by ${toID(actionTakerName)}: ${action.split(' ')[0]} "${eventName}"`;
|
||||
},
|
||||
|
||||
'set modchat to ': (log) => {
|
||||
const actionTaker = parseBrackets(log, '[');
|
||||
log = log.slice(actionTaker.length + 3);
|
||||
log = log.slice('set modchat to '.length);
|
||||
return `MODCHAT: by ${actionTaker}: to ${log}`;
|
||||
},
|
||||
'set modjoin to ': (log) => {
|
||||
const actionTakerName = log.slice(0, log.lastIndexOf(' set'));
|
||||
log = log.slice(actionTakerName.length + 1);
|
||||
log = log.slice('set modjoin to '.length);
|
||||
const rank = log.startsWith('sync') ? 'sync' : log.replace('.', '');
|
||||
return `MODJOIN${rank === 'sync' ? ' SYNC' : ''}: by ${toID(actionTakerName)}${rank !== 'sync' ? `: ${rank}` : ``}`;
|
||||
},
|
||||
'turned off modjoin': (log) => {
|
||||
const actionTakerName = log.slice(0, log.lastIndexOf(' turned off modjoin'));
|
||||
return `MODJOIN: by ${toID(actionTakerName)}: OFF`;
|
||||
},
|
||||
|
||||
'changed the roomintro': (log) => {
|
||||
const isDeletion = /deleted the (staff|room)intro/.test(log);
|
||||
const isRoomintro = log.includes('roomintro');
|
||||
const actionTaker = toID(log.slice(0, log.indexOf(isDeletion ? 'deleted' : 'changed')));
|
||||
return `${isDeletion ? 'DELETE' : ''}${isRoomintro ? 'ROOM' : 'STAFF'}INTRO: by ${actionTaker}`;
|
||||
},
|
||||
'deleted the roomintro': (log) => modernizerTransformations['changed the roomintro'](log),
|
||||
'changed the staffintro': (log) => modernizerTransformations['changed the roomintro'](log),
|
||||
'deleted the staffintro': (log) => modernizerTransformations['changed the roomintro'](log),
|
||||
|
||||
'created a tournament in': (log) => {
|
||||
const actionTaker = parseBrackets(log, '[');
|
||||
log = log.slice(actionTaker.length + 3);
|
||||
log = log.slice(24, -8);
|
||||
return `TOUR CREATE: by ${actionTaker}: ${log}`;
|
||||
},
|
||||
'was disqualified from the tournament by': (log) => {
|
||||
const disqualified = parseBrackets(log, '[');
|
||||
log = log.slice(disqualified.length + 3);
|
||||
log = log.slice('was disqualified from the tournament by'.length);
|
||||
return `TOUR DQ: [${toID(disqualified)}] by ${toID(log)}`;
|
||||
},
|
||||
'The tournament auto disqualify timeout was set to': (log) => {
|
||||
const byIndex = log.indexOf(' by ');
|
||||
const actionTaker = log.slice(byIndex + ' by '.length);
|
||||
const length = log.slice('The tournament auto disqualify timeout was set to'.length, byIndex);
|
||||
return `TOUR AUTODQ: by ${toID(actionTaker)}: ${length.trim()}`;
|
||||
},
|
||||
|
||||
' was banned from room ': (log) => {
|
||||
const banned = toID(log.slice(0, log.indexOf(' was banned from room ')));
|
||||
log = log.slice(log.indexOf(' by ') + ' by '.length);
|
||||
let reason, ip;
|
||||
if (/\(.*\)/.test(log)) {
|
||||
reason = parseBrackets(log, '(');
|
||||
if (/\[.*\]/.test(log)) ip = parseBrackets(log, '[');
|
||||
log = log.slice(0, log.indexOf('('));
|
||||
}
|
||||
const actionTaker = toID(log);
|
||||
return `ROOMBAN: [${banned}] ${getAlts()}${ip ? `[${ip}] ` : ``}by ${actionTaker}${reason ? `: ${reason}` : ``}`;
|
||||
},
|
||||
' was muted by ': (log) => {
|
||||
let muted = '';
|
||||
let isHour = false;
|
||||
[muted, log] = log.split(' was muted by ');
|
||||
muted = toID(muted);
|
||||
let reason, ip;
|
||||
if (/\(.*\)/.test(log)) {
|
||||
reason = parseBrackets(log, '(');
|
||||
if (/\[.*\]/.test(log)) ip = parseBrackets(log, '[');
|
||||
log = log.slice(0, log.indexOf('('));
|
||||
}
|
||||
let actionTaker = toID(log);
|
||||
if (actionTaker.endsWith('for1hour')) {
|
||||
isHour = true;
|
||||
actionTaker = actionTaker.replace(/^(.*)(for1hour)$/, (match, staff) => staff) as ID;
|
||||
}
|
||||
return `${isHour ? 'HOUR' : ''}MUTE: [${muted}] ${getAlts()}${ip ? `[${ip}] ` : ``}by ${actionTaker}${reason ? `: ${reason}` : ``}`;
|
||||
},
|
||||
' was locked from talking ': (log) => {
|
||||
const isWeek = log.includes(' was locked from talking for a week ');
|
||||
const locked = toID(log.slice(0, log.indexOf(' was locked from talking ')));
|
||||
log = log.slice(log.indexOf(' by ') + ' by '.length);
|
||||
let reason, ip;
|
||||
if (/\(.*\)/.test(log)) {
|
||||
reason = parseBrackets(log, '(');
|
||||
if (/\[.*\]/.test(log)) ip = parseBrackets(log, '[');
|
||||
log = log.slice(0, log.indexOf('('));
|
||||
}
|
||||
const actionTaker = toID(log);
|
||||
return `${isWeek ? 'WEEK' : ''}LOCK: [${locked}] ${getAlts()}${ip ? `[${ip}] ` : ``}by ${actionTaker}${reason ? `: ${reason}` : ``}`;
|
||||
},
|
||||
' was banned ': (log) => {
|
||||
if (log.includes(' was banned from room ')) return modernizerTransformations[' was banned from room '](log);
|
||||
const banned = toID(log.slice(0, log.indexOf(' was banned ')));
|
||||
log = log.slice(log.indexOf(' by ') + ' by '.length);
|
||||
let reason, ip;
|
||||
if (/\(.*\)/.test(log)) {
|
||||
reason = parseBrackets(log, '(');
|
||||
if (/\[.*\]/.test(log)) ip = parseBrackets(log, '[');
|
||||
log = log.slice(0, log.indexOf('('));
|
||||
}
|
||||
const actionTaker = toID(log);
|
||||
return `BAN: [${banned}] ${getAlts()}${ip ? `[${ip}] ` : ``}by ${actionTaker}${reason ? `: ${reason}` : ``}`;
|
||||
},
|
||||
|
||||
'was promoted to ': (log) => {
|
||||
const isDemotion = log.includes('was demoted to ');
|
||||
const userid = toID(log.split(' was ')[0]);
|
||||
if (!userid) {
|
||||
throw new Error(`Ignoring malformed line: ${prefix}${log}`);
|
||||
}
|
||||
log = log.slice(userid.length + 3);
|
||||
log = log.slice(`was ${isDemotion ? 'demoted' : 'promoted'} to `.length);
|
||||
let rank = log.slice(0, log.indexOf(' by')).replace(/ /, '').toUpperCase();
|
||||
|
||||
log = log.slice(`${rank} by `.length);
|
||||
if (!rank.startsWith('ROOM')) rank = `GLOBAL ${rank}`;
|
||||
const actionTaker = parseBrackets(log, '[');
|
||||
return `${rank}: [${userid}] by ${actionTaker}${isDemotion ? ': (demote)' : ''}`;
|
||||
},
|
||||
'was demoted to ': (log) => modernizerTransformations['was promoted to '](log),
|
||||
'was appointed Room Owner by ': (log) => {
|
||||
const userid = parseBrackets(log, '[');
|
||||
log = log.slice(userid.length + 3);
|
||||
log = log.slice('was appointed Room Owner by '.length);
|
||||
const actionTaker = parseBrackets(log, '[');
|
||||
return `ROOMOWNER: [${userid}] by ${actionTaker}`;
|
||||
},
|
||||
|
||||
' claimed this ticket': (log) => {
|
||||
const actions: {[k: string]: string} = {
|
||||
' claimed this ticket': 'TICKETCLAIM',
|
||||
' closed this ticket': 'TICKETCLOSE',
|
||||
' deleted this ticket': 'TICKETDELETE',
|
||||
};
|
||||
for (const oldAction in actions) {
|
||||
if (log.includes(oldAction)) {
|
||||
const actionTaker = toID(log.slice(0, log.indexOf(oldAction)));
|
||||
return `${actions[oldAction]}: by ${actionTaker}`;
|
||||
}
|
||||
}
|
||||
return log;
|
||||
},
|
||||
'This ticket is now claimed by ': (log) => {
|
||||
const claimer = toID(log.slice(log.indexOf(' by ') + ' by '.length));
|
||||
return `TICKETCLAIM: by ${claimer}`;
|
||||
},
|
||||
' is no longer interested in this ticket': (log) => {
|
||||
const abandoner = toID(log.slice(0, log.indexOf(' is no longer interested in this ticket')));
|
||||
return `TICKETABANDON: by ${abandoner}`;
|
||||
},
|
||||
' opened a new ticket': (log) => {
|
||||
const opener = toID(log.slice(0, log.indexOf(' opened a new ticket')));
|
||||
const problem = log.slice(log.indexOf(' Issue: ') + ' Issue: '.length).trim();
|
||||
return `TICKETOPEN: by ${opener}: ${problem}`;
|
||||
},
|
||||
' closed this ticket': (log) => modernizerTransformations[' claimed this ticket'](log),
|
||||
' deleted this ticket': (log) => modernizerTransformations[' claimed this ticket'](log),
|
||||
'This ticket is no longer claimed': () => 'TICKETUNCLAIM',
|
||||
|
||||
' has been caught attempting a hunt with ': (log) => {
|
||||
const index = log.indexOf(' has been caught attempting a hunt with ');
|
||||
const user = toID(log.slice(0, index));
|
||||
log = log.slice(index + ' has been caught attempting a hunt with '.length);
|
||||
log = log.replace('. The user has also', '; has also').replace('.', '');
|
||||
return `SCAV CHEATER: [${user}]: caught attempting a hunt with ${log}`;
|
||||
},
|
||||
};
|
||||
|
||||
for (const oldAction in modernizerTransformations) {
|
||||
if (line.includes(oldAction)) {
|
||||
try {
|
||||
return prefix + modernizerTransformations[oldAction](line);
|
||||
} catch (err) {
|
||||
if (Config.nofswriting) throw err;
|
||||
process.stderr.write(`${err.message}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `${prefix}${line}`;
|
||||
}
|
||||
|
||||
export function parseModlog(raw: string, nextLine?: string, isGlobal = false): ModlogEntry | undefined {
|
||||
let line = modernizeLog(raw);
|
||||
if (!line) return;
|
||||
|
||||
const log: ModlogEntry = {action: 'NULL', isGlobal};
|
||||
const timestamp = parseBrackets(line, '[');
|
||||
log.time = Math.floor(new Date(timestamp).getTime());
|
||||
line = line.slice(timestamp.length + 3);
|
||||
|
||||
const [roomid, ...bonus] = parseBrackets(line, '(').split(' ');
|
||||
log.roomID = roomid;
|
||||
if (bonus.length) log.visualRoomID = `${roomid} ${bonus.join(' ')}`;
|
||||
line = line.slice((log.visualRoomID || log.roomID).length + 3);
|
||||
const actionColonIndex = line.indexOf(':');
|
||||
const action = line.slice(0, actionColonIndex);
|
||||
if (action !== action.toUpperCase()) {
|
||||
// no action (probably an old-format log that slipped past the modernizer)
|
||||
log.action = 'OLD MODLOG';
|
||||
log.loggedBy = 'unknown' as ID;
|
||||
log.note = line.trim();
|
||||
return log;
|
||||
} else {
|
||||
log.action = action;
|
||||
if (log.action === 'OLD MODLOG') {
|
||||
log.loggedBy = 'unknown' as ID;
|
||||
log.note = line.slice(line.indexOf('by unknown: ') + 'by unknown :'.length).trim();
|
||||
return log;
|
||||
}
|
||||
line = line.slice(actionColonIndex + 2);
|
||||
}
|
||||
|
||||
if (line[0] === '[') {
|
||||
const userid = toID(parseBrackets(line, '['));
|
||||
log.userid = userid;
|
||||
line = line.slice(userid.length + 3).trim();
|
||||
if (line.startsWith('ac:')) {
|
||||
line = line.slice(3).trim();
|
||||
const ac = parseBrackets(line, '[');
|
||||
log.autoconfirmedID = toID(ac);
|
||||
line = line.slice(ac.length + 3).trim();
|
||||
}
|
||||
if (line.startsWith('alts:')) {
|
||||
line = line.slice(5).trim();
|
||||
const alts = new Set<ID>(); // we need to weed out duplicate alts
|
||||
|
||||
let alt = parseBrackets(line, '[');
|
||||
do {
|
||||
if (alt.includes(', ')) {
|
||||
// old alt format
|
||||
for (const trueAlt of alt.split(', ')) {
|
||||
alts.add(toID(trueAlt));
|
||||
}
|
||||
line = line.slice(line.indexOf(`[${alt}],`) + `[${alt}],`.length).trim();
|
||||
if (!line.startsWith('[')) line = `[${line}`;
|
||||
} else {
|
||||
if (IPTools.ipRegex.test(alt)) break;
|
||||
alts.add(toID(alt));
|
||||
line = line.slice(line.indexOf(`[${alt}],`) + `[${alt}],`.length).trim();
|
||||
if (alt.includes('[') && !line.startsWith('[')) line = `[${line}`;
|
||||
}
|
||||
alt = parseBrackets(line, '[');
|
||||
} while (alt);
|
||||
log.alts = [...alts];
|
||||
}
|
||||
if (line[0] === '[') {
|
||||
log.ip = parseBrackets(line, '[');
|
||||
line = line.slice(log.ip.length + 3).trim();
|
||||
}
|
||||
}
|
||||
|
||||
let regex = /by .*:/;
|
||||
let actionTakerIndex = regex.exec(line)?.index;
|
||||
if (actionTakerIndex === undefined) {
|
||||
actionTakerIndex = line.indexOf('by ');
|
||||
regex = /by .*/;
|
||||
}
|
||||
if (actionTakerIndex !== -1) {
|
||||
const colonIndex = line.indexOf(': ');
|
||||
const actionTaker = line.slice(actionTakerIndex + 3, colonIndex > actionTakerIndex ? colonIndex : undefined);
|
||||
if (toID(actionTaker).length < 19) {
|
||||
log.loggedBy = toID(actionTaker) || undefined;
|
||||
if (colonIndex > actionTakerIndex) line = line.slice(colonIndex);
|
||||
line = line.replace(regex, '');
|
||||
}
|
||||
}
|
||||
if (line) log.note = line.replace(/^\s?:\s?/, '').trim();
|
||||
return log;
|
||||
}
|
||||
|
||||
export function rawifyLog(log: ModlogEntry) {
|
||||
let result = `[${new Date(log.time || Date.now()).toJSON()}] (${(log.visualRoomID || log.roomID || 'global').replace(/^global-/, '')}) ${log.action}`;
|
||||
if (log.userid) result += `: [${log.userid}]`;
|
||||
if (log.autoconfirmedID) result += ` ac: [${log.autoconfirmedID}]`;
|
||||
if (log.alts) result += ` alts: [${log.alts.join('], [')}]`;
|
||||
if (log.ip) result += ` [${log.ip}]`;
|
||||
if (log.loggedBy) result += `${result.endsWith(']') ? '' : ':'} by ${log.loggedBy}`;
|
||||
if (log.note) result += `: ${log.note}`;
|
||||
return result + `\n`;
|
||||
}
|
||||
|
||||
export class ModlogConverterTest {
|
||||
readonly inputDir: string;
|
||||
readonly outputDir: string;
|
||||
|
||||
constructor(inputDir: string, outputDir: string) {
|
||||
this.inputDir = inputDir;
|
||||
this.outputDir = outputDir;
|
||||
}
|
||||
|
||||
async toTxt() {
|
||||
const files = await FS(this.inputDir).readdir();
|
||||
// Read global modlog last to avoid inserting duplicate data to database
|
||||
if (files.includes('modlog_global.txt')) {
|
||||
files.splice(files.indexOf('modlog_global.txt'), 1);
|
||||
files.push('modlog_global.txt');
|
||||
}
|
||||
|
||||
const globalEntries = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (file === 'README.md') continue;
|
||||
const roomid = file.slice(7, -4);
|
||||
|
||||
let entriesLogged = 0;
|
||||
let lastLine = undefined;
|
||||
let entries: string[] = [];
|
||||
|
||||
const insertEntries = async () => {
|
||||
if (roomid === 'global') return;
|
||||
entriesLogged += entries.length;
|
||||
if (!Config.nofswriting && (entriesLogged % ENTRIES_TO_BUFFER === 0 || entriesLogged < ENTRIES_TO_BUFFER)) {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(`Wrote ${entriesLogged} entries from '${roomid}'`);
|
||||
}
|
||||
await FS(`${this.outputDir}/modlog_${roomid}.txt`).append(entries.join(''));
|
||||
entries = [];
|
||||
};
|
||||
|
||||
const readStream = FS(`${this.inputDir}/${file}`).createReadStream();
|
||||
for await (const line of readStream.byLine()) {
|
||||
const entry = parseModlog(line, lastLine, roomid === 'global');
|
||||
lastLine = line;
|
||||
if (!entry) continue;
|
||||
const rawLog = rawifyLog(entry);
|
||||
if (roomid !== 'global') entries.push(rawLog);
|
||||
if (entry.isGlobal) {
|
||||
globalEntries.push(rawLog);
|
||||
}
|
||||
if (entries.length === ENTRIES_TO_BUFFER) await insertEntries();
|
||||
}
|
||||
await insertEntries();
|
||||
if (entriesLogged) process.stdout.write('\n');
|
||||
}
|
||||
|
||||
if (!Config.nofswriting) console.log(`Writing the global modlog...`);
|
||||
await FS(`${this.outputDir}/modlog_global.txt`).append(globalEntries.join(''));
|
||||
}
|
||||
}
|
||||
|
||||
export class ModlogConverter {
|
||||
static async convert(
|
||||
from: ModlogFormat, to: ModlogFormat, databasePath: string,
|
||||
textLogDirectoryPath: string, outputLogPath?: string
|
||||
) {
|
||||
if (from === 'txt' && to === 'txt' && outputLogPath) {
|
||||
const converter = new ModlogConverterTest(textLogDirectoryPath, outputLogPath);
|
||||
return converter.toTxt().then(() => {
|
||||
console.log("\nDone!");
|
||||
process.exit();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user