From cee8f5a12ccf0e6dccf9bbe5579bcbada49dca86 Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Thu, 23 Dec 2021 17:56:25 -0600 Subject: [PATCH] Abuse-monitor: Support tracking stats (#8575) --- databases/migrations/chat-plugins/v5.sql | 17 +++++ server/chat-plugins/abuse-monitor.ts | 88 ++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 databases/migrations/chat-plugins/v5.sql diff --git a/databases/migrations/chat-plugins/v5.sql b/databases/migrations/chat-plugins/v5.sql new file mode 100644 index 0000000000..b1aa733ab5 --- /dev/null +++ b/databases/migrations/chat-plugins/v5.sql @@ -0,0 +1,17 @@ +-- Creates stats tables for the Perspective moderation tool. +BEGIN TRANSACTION; + +CREATE TABLE perspective_stats ( + staff TEXT NOT NULL, + roomid TEXT NOT NULL PRIMARY KEY, + result TINYINT(1) NOT NULL, + timestamp INTEGER NOT NULL +); + +CREATE INDEX date_idx ON perspective_stats(date); + +-- update database version +UPDATE db_info SET value = '5' WHERE key = 'version'; +COMMIT; + + diff --git a/server/chat-plugins/abuse-monitor.ts b/server/chat-plugins/abuse-monitor.ts index 776ae2255a..2cb5418e09 100644 --- a/server/chat-plugins/abuse-monitor.ts +++ b/server/chat-plugins/abuse-monitor.ts @@ -98,6 +98,12 @@ interface BattleInfo { log: string[]; } +// stolen from chatlog. necessary here, but importing chatlog sucks. +function nextMonth(month: string) { + const next = new Date(new Date(`${month}-15`).getTime() + 30 * 24 * 60 * 60 * 1000); + return next.toISOString().slice(0, 7); +} + // Mostly stolen from my code in helptickets. // Necessary because we can't require this in without also requiring in a LOT of other // modules, most of which crash the child process. Lot messier to fix that than it is to do this. @@ -442,20 +448,39 @@ export const commands: Chat.ChatCommands = { `|html|Remember to use /am respawn to deploy the settings to the child process.` ); }, - resolve(target) { + async resolve(target) { this.checkCan('lock'); target = target.toLowerCase().trim().replace(/ +/g, ''); - if (!target) return this.parse(`/help abusemonitor`); - if (!cache[target]?.staffNotified) { + let [roomid, rawResult] = Utils.splitFirst(target, ',').map(f => f.trim()); + if (!cache[roomid]?.staffNotified) { return this.popupReply(`That room has not been flagged by the abuse monitor.`); } + if (roomid.includes('-') && roomid.endsWith('pw')) { + // cut off passwords + roomid = roomid.split('-').slice(0, -1).join('-'); + } + let result = toID(rawResult) === 'success' ? 1 : toID(rawResult) === 'failure' ? 0 : null; + if (!result) return this.popupReply(`Invalid result - must be 'success' or 'failure'.`); + const inserted = await Chat.database.get(`SELECT result FROM perspective_stats WHERE roomid = ?`, [roomid]); + if (inserted?.result) { + // has already been logged as accurate - ensure if one success is logged it's still a success if it's hit again + // (even if it's a failure now, it was a success before - that's what's relevant.) + result = inserted.result; + } // we delete the cache because if more stuff happens in it // post punishment, we want to know about it - delete cache[target]; + delete cache[roomid]; notifyStaff(); - this.closePage(`abusemonitor-view-${target}`); + this.closePage(`abusemonitor-view-${roomid}`); // bring the listing page to the front - need to close and reopen this.closePage(`abusemonitor-flagged`); + await Chat.database.run( + `INSERT INTO perspective_stats (staff, roomid, result, timestamp) VALUES ($staff, $roomid, $result, $timestamp) ` + + // on conflict in case it's re-triggered later. + // (we want it to be updated to success if it is now a success where it was previously inaccurate) + `ON CONFLICT (roomid) DO UPDATE SET result = $result, timestamp = $timestamp`, + {staff: this.user.id, roomid, result, timestamp: Date.now()} + ); return this.parse(`/j view-abusemonitor-flagged`); }, async nojoinpunish(target, room, user) { @@ -491,6 +516,10 @@ export const commands: Chat.ChatCommands = { const [count, userid] = Utils.splitFirst(target, ',').map(toID); this.parse(`/join view-abusemonitor-logs-${count || '200'}${userid ? `-${userid}` : ""}`); }, + stats(target) { + checkAccess(this); + return this.parse(`/join view-abusemonitor-stats${target ? `-${target}` : ''}`); + }, async respawn(target, room, user) { checkAccess(this); this.sendReply(`Respawning...`); @@ -760,7 +789,9 @@ export const pages: Chat.PageTable = { } buf += `
`; } - buf += ``; + buf += `
Mark resolved:
`; + buf += ` | `; + buf += ``; return buf; }, async logs(query, user) { @@ -825,5 +856,50 @@ export const pages: Chat.PageTable = { } return buf; }, + async stats(query, user) { + checkAccess(this); + const date = new Date(query.join('-') || Chat.toTimestamp(new Date()).split(' ')[0]); + if (isNaN(date.getTime())) { + return this.errorReply(`Invalid date: ${date}`); + } + const month = Chat.toTimestamp(date).split(' ')[0].slice(0, -3); + let buf = `
`; + buf += ``; + buf += `

Abuse Monitor stats for ${month}


`; + const logs = await Chat.database.all( + `SELECT * FROM perspective_stats WHERE timestamp > ? AND timestamp < ?`, + [new Date(month).getTime(), new Date(nextMonth(month)).getTime()] + ); + this.title = '[Abuse Monitor] Stats'; + if (!logs.length) { + buf += `

No logs found for the month ${month}.

`; + return buf; + } + this.title += ` ${month}`; + buf += `

${Chat.count(logs.length, 'logs')} found.

`; + let successes = 0; + let failures = 0; + const staffStats: Record = {}; + for (const log of logs) { + if (log.result) { + successes++; + } else { + failures++; + } + if (!staffStats[log.staff]) staffStats[log.staff] = 0; + staffStats[log.staff]++; + } + buf += `

Success rate: ${(successes / logs.length) * 100}%

`; + buf += `

Failure rate: ${(failures / logs.length) * 100}%

`; + buf += `

Staff stats:

`; + buf += `
`; + buf += ``; + for (const id of Utils.sortBy(Object.keys(staffStats), k => -staffStats[k])) { + buf += ``; + } + buf += `
UserTotalPercent total
${id}${staffStats[id]}${(staffStats[id] / logs.length) * 100}%
`; + return buf; + }, }, };