mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-03-21 17:25:10 -05:00
Chatlog: Add support for linecounts on the database
This commit is contained in:
parent
0884e45f73
commit
bdc1ed2401
|
|
@ -14,6 +14,22 @@ export type BasicSQLValue = string | number | null;
|
|||
export type SQLRow = {[k: string]: BasicSQLValue};
|
||||
export type SQLValue = BasicSQLValue | SQLStatement | PartialOrSQL<SQLRow> | 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));
|
||||
|
|
|
|||
|
|
@ -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 += '<hr /><ol>';
|
||||
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 += '<hr /><ol>';
|
||||
// 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(
|
||||
`<div class="pad"><h2>Searching linecounts on room ${roomid}${user ? ` for the user ${user}` : ''}.</h2></div>`
|
||||
);
|
||||
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<AnyObject, any>(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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user