From bdc1ed2401d38915e8523e4287f7ea590a8a690b Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Sat, 18 May 2024 03:14:57 -0500 Subject: [PATCH] Chatlog: Add support for linecounts on the database --- lib/database.ts | 18 ++++++++++++++- server/chat-plugins/chatlog.ts | 40 +++++++++++++++++++++++++++------- server/roomlogs.ts | 2 +- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/lib/database.ts b/lib/database.ts index d0601a2931..f14bb5b91b 100644 --- a/lib/database.ts +++ b/lib/database.ts @@ -14,6 +14,22 @@ export type BasicSQLValue = string | number | null; export type SQLRow = {[k: string]: BasicSQLValue}; export type SQLValue = BasicSQLValue | SQLStatement | PartialOrSQL | BasicSQLValue[] | undefined; +export function isSQL(value: any): value is SQLStatement { + /** + * This addresses a scenario where objects get out of sync due to hotpatching. + * Table A is instantiated, and retains SQLStatement at that specific point in time. Consumer A is also instantiated at + * the same time, and both can interact freely, since consumer A and table A share the same reference to SQLStatement. + * However, when consumer A is hotpatched, consumer A imports a new instance of SQLStatement. Thus, when consumer A + * provides that new SQLStatement, it does not pass the `instanceof SQLStatement` check in Table A, + * since table A is still referencing he old SQLStatement (checking that the new is an instance of the old). + * This does not work. Thus, we're forced to check constructor name instead. + */ + return value instanceof SQLStatement || ( + // assorted safety checks to be sure it'll actually work (theoretically preventing certain attacks) + value?.constructor.name === 'SQLStatement' && (Array.isArray(value.sql) && Array.isArray(value.values)) + ); +} + export class SQLStatement { sql: string[]; values: BasicSQLValue[]; @@ -25,7 +41,7 @@ export class SQLStatement { } } append(value: SQLValue, nextString = ''): this { - if (value instanceof SQLStatement) { + if (isSQL(value)) { if (!value.sql.length) return this; const oldLength = this.sql.length; this.sql = this.sql.concat(value.sql.slice(1)); diff --git a/server/chat-plugins/chatlog.ts b/server/chat-plugins/chatlog.ts index a774742d70..0bde4c2998 100644 --- a/server/chat-plugins/chatlog.ts +++ b/server/chat-plugins/chatlog.ts @@ -6,6 +6,7 @@ */ import {Utils, FS, Dashycode, ProcessManager, Repl, Net, Streams} from '../../lib'; +import {SQL} from '../../lib/database'; import {Config} from '../config-loader'; import {Dex} from '../../sim/dex'; import {Chat} from '../chat'; @@ -482,7 +483,7 @@ export abstract class Searcher { return buf; } else if (user) { buf += '
    '; - const sortedDays = Utils.sortBy(Object.keys(results), day => ({reverse: day})); + const sortedDays = Utils.sortBy(Object.keys(results)); let total = 0; for (const day of sortedDays) { const dayResults = results[day][user]; @@ -496,7 +497,7 @@ export abstract class Searcher { buf += '
      '; // squish the results together const totalResults: {[k: string]: number} = {}; - for (const date in results) { + for (const date of Utils.sortBy(Object.keys(results))) { for (const userid in results[date]) { if (!totalResults[userid]) totalResults[userid] = 0; totalResults[userid] += results[date][userid]; @@ -521,8 +522,7 @@ export abstract class Searcher { context.setHTML( `

      Searching linecounts on room ${roomid}${user ? ` for the user ${user}` : ''}.

      ` ); - const results = await PM.query({roomid, date: month, search: user, queryType: 'linecount'}); - context.setHTML(results); + context.setHTML(await LogSearcher.searchLinecounts(roomid, month, user)); } runSearch() { throw new Chat.ErrorMessage(`This functionality is currently disabled.`); @@ -810,7 +810,34 @@ export class RipgrepLogSearcher extends Searcher { } } +export class DatabaseLogSearcher extends Searcher { + async searchLinecounts(roomid: RoomID, monthString: string, user?: ID) { + user = toID(user); + if (!Rooms.Roomlogs.table) throw new Error(`Database search made while database is disabled.`); + const results: {[date: string]: {[user: string]: number}} = {}; + const [year, month] = monthString.split('-').map(Number); + const rows = await Rooms.Roomlogs.table.selectAll()` + WHERE EXTRACT("year" FROM time::DATE) = ${year} AND EXTRACT("month" FROM time::DATE) = ${month} AND + roomid = ${roomid} AND type = ${'c'}${user ? SQL` AND userid = ${user}` : SQL``} + `; + + for (const row of rows) { + // 'c' rows should always have userids, so this should never be an issue. + // this is just to appease TS. + if (!row.userid) continue; + const day = Chat.toTimestamp(row.time).split(' ')[0]; + if (!results[day]) results[day] = {}; + if (!results[day][row.userid]) results[day][row.userid] = 0; + results[day][row.userid]++; + } + + return this.renderLinecountResults(results, roomid, monthString, user); + } +} + export const LogSearcher: Searcher = new ( + Rooms.Roomlogs.table ? DatabaseLogSearcher : + // no db, determine fs reader type. Config.chatlogreader === 'ripgrep' ? RipgrepLogSearcher : FSLogSearcher )(); @@ -818,11 +845,8 @@ export const PM = new ProcessManager.QueryProcessManager(module, const start = Date.now(); try { let result: any; - const {date, search, roomid, queryType} = data; + const {search, roomid, queryType} = data; switch (queryType) { - case 'linecount': - result = await LogSearcher.searchLinecounts(roomid, date, search); - break; case 'roomstats': result = await LogSearcher.activityStats(roomid, search); break; diff --git a/server/roomlogs.ts b/server/roomlogs.ts index 2ea9887ba3..6b0d9b8d05 100644 --- a/server/roomlogs.ts +++ b/server/roomlogs.ts @@ -21,7 +21,7 @@ interface RoomlogOptions { interface RoomlogRow { type: string; roomid: string; - user: string | null; + userid: string | null; time: Date; log: string; // tsvector, really don't use