diff --git a/CODEOWNERS b/CODEOWNERS index 5a99306835..1dc93b8b65 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -17,7 +17,7 @@ server/chat-plugins/responder.ts @mia-pi-git server/chat-plugins/rock-paper-scissors.ts @mia-pi-git server/chat-plugins/scavenger*.ts @xfix @sparkychildcharlie server/chat-plugins/the-studio.ts @KrisXV -server/chat-plugins/trivia*.ts @AnnikaCodes +server/chat-plugins/trivia/ @AnnikaCodes server/friends.ts @mia-pi-git server/chat-plugins/username-prefixes.ts @AnnikaCodes server/modlog/* @monsanto @AnnikaCodes diff --git a/server/chat-plugins/trivia-database.ts b/server/chat-plugins/trivia/database.ts similarity index 92% rename from server/chat-plugins/trivia-database.ts rename to server/chat-plugins/trivia/database.ts index b7abe6cde5..33516083cf 100644 --- a/server/chat-plugins/trivia-database.ts +++ b/server/chat-plugins/trivia/database.ts @@ -4,10 +4,10 @@ * @author Annika */ -import type {TriviaGame, TriviaLeaderboard, TriviaLeaderboardScore, TriviaQuestion} from "./trivia"; -import {FS} from "../../lib"; -import {formatSQLArray} from "../../lib/utils"; -import type {Statement} from "../../lib/sql"; +import type {TriviaGame, TriviaHistory, TriviaLeaderboard, TriviaLeaderboardScore, TriviaQuestion} from "./trivia"; +import {FS} from "../../../lib"; +import {formatSQLArray} from "../../../lib/utils"; +import type {Statement} from "../../../lib/sql"; export class TriviaSQLiteDatabase { readyPromise: Promise | null; @@ -121,48 +121,48 @@ export class TriviaSQLiteDatabase { }); } - async addHistory(history: TriviaGame & {scores: {[k: string]: number}}) { + async addHistory(history: Iterable) { if (this.readyPromise) await this.readyPromise; if (!Chat.database) { throw new Chat.ErrorMessage(`Can't add a Trivia game to the history because there is no SQL database open.`); } - const {lastInsertRowid} = await this.gameHistoryInsertion!.run( - [history.mode, history.length, history.category, history.startTime, history.creator, Number(history.givesPoints)] - ); - for (const userid in history.scores) { - await this.scoreHistoryInsertion!.run([lastInsertRowid, userid, history.scores[userid]]); - } + const res = await Chat.database.transaction('addHistory', { + history, + gameHistoryInsertion: this.gameHistoryInsertion!.toString(), + scoreHistoryInsertion: this.scoreHistoryInsertion!.toString(), + }); + if (!res) throw new Error(`Error updating Trivia history.`); } - async addQuestion(question: TriviaQuestion) { + async addQuestions(questions: Iterable) { if (this.readyPromise) await this.readyPromise; if (!Chat.database) { throw new Chat.ErrorMessage(`Can't add a Trivia question because there is no SQL database open.`); } - if (!question.addedAt) question.addedAt = Date.now(); - const {lastInsertRowid} = await this.questionInsertion!.run( - [question.question, question.category, question.addedAt, question.user, 0] // 0 for false - not a submission - ); - for (const answer of question.answers) { - await this.answerInsertion!.run([lastInsertRowid, answer]); - } + const res = await Chat.database.transaction('addQuestions', { + questions, + questionInsertion: this.questionInsertion!.toString(), + answerInsertion: this.answerInsertion!.toString(), + isSubmission: false, + }); + if (!res) throw new Chat.ErrorMessage(`Error adding Trivia questions.`); } - async addQuestionSubmission(question: TriviaQuestion) { + async addQuestionSubmissions(questions: Iterable) { if (this.readyPromise) await this.readyPromise; if (!Chat.database) { - throw new Chat.ErrorMessage(`Can't add a Trivia question because there is no SQL database open.`); + throw new Chat.ErrorMessage(`Can't submit a Trivia question for review because there is no SQL database open.`); } - const {lastInsertRowid} = await Chat.database.run( - this.questionInsertion!, - [question.question, question.category, question.addedAt, question.user, 1] // 1 for true - is a submission - ); - for (const answer of question.answers) { - await this.answerInsertion!.run([lastInsertRowid, answer]); - } + const res = await Chat.database.transaction('addQuestions', { + questions, + questionInsertion: this.questionInsertion!.toString(), + answerInsertion: this.answerInsertion!.toString(), + isSubmission: true, + }); + if (!res) throw new Chat.ErrorMessage(`Error adding Trivia questions for review.`); } async setShouldMoveEventQuestions(shouldMove: boolean) { @@ -355,13 +355,13 @@ export class TriviaSQLiteDatabase { async ensureQuestionExists(questionText: string) { if (!(await this.checkIfQuestionExists(questionText))) { - throw new Chat.ErrorMessage(`Question "${questionText}" is already awaiting review or in the question database.`); + throw new Chat.ErrorMessage(`Question "${questionText}" is not in the question database.`); } } async ensureQuestionDoesNotExist(questionText: string) { if (await this.checkIfQuestionExists(questionText)) { - throw new Chat.ErrorMessage(`Question "${questionText}" is not in the question database.`); + throw new Chat.ErrorMessage(`Question "${questionText}" is already in the question database.`); } } @@ -375,37 +375,6 @@ export class TriviaSQLiteDatabase { return Promise.all(rows.map((row: AnyObject) => this.rowToQuestion(row))); } - /***************************** - * Methods for deleting data * - * ***************************/ - async clearSubmissions() { - if (this.readyPromise) await this.readyPromise; - if (!Chat.database) { - throw new Chat.ErrorMessage(`Can't clear the Trivia question submissions because there is no SQL database open.`); - } - - await this.clearAllSubmissionsQuery!.run([]); - } - - async clearCategory(category: string) { - if (this.readyPromise) await this.readyPromise; - if (!Chat.database) { - throw new Chat.ErrorMessage(`Can't clear the Trivia questions in category "${category}" because there is no SQL database open.`); - } - - await this.clearCategoryQuery!.run([category]); - } - - async deleteQuestion(questionText: string) { - if (this.readyPromise) await this.readyPromise; - if (!Chat.database) { - throw new Chat.ErrorMessage(`Can't delete the Trivia question because there is no SQL database open.`); - } - - await this.deleteQuestionQuery!.run([questionText]); - } - - async getQuestionCounts(): Promise<{[k: string]: number, total: number}> { if (this.readyPromise) await this.readyPromise; if (!Chat.database) { @@ -438,6 +407,37 @@ export class TriviaSQLiteDatabase { return Promise.all(rows.map((row: AnyObject) => this.rowToQuestion(row))); } + + /***************************** + * Methods for deleting data * + * ***************************/ + async clearSubmissions() { + if (this.readyPromise) await this.readyPromise; + if (!Chat.database) { + throw new Chat.ErrorMessage(`Can't clear the Trivia question submissions because there is no SQL database open.`); + } + + await Chat.database.run(this.clearAllSubmissionsQuery!, []); + } + + async clearCategory(category: string) { + if (this.readyPromise) await this.readyPromise; + if (!Chat.database) { + throw new Chat.ErrorMessage(`Can't clear the Trivia questions in category "${category}" because there is no SQL database open.`); + } + + await Chat.database.run(this.clearCategoryQuery!, [category]); + } + + async deleteQuestion(questionText: string) { + if (this.readyPromise) await this.readyPromise; + if (!Chat.database) { + throw new Chat.ErrorMessage(`Can't delete the Trivia question because there is no SQL database open.`); + } + + await Chat.database.run(this.deleteQuestionQuery!, [questionText]); + } + async deleteLeaderboardEntry(userid: ID, isAllTime: boolean) { if (this.readyPromise) await this.readyPromise; if (!Chat.database) { @@ -560,6 +560,7 @@ export class TriviaSQLiteDatabase { ); await Chat.database.exec("PRAGMA foreign_keys = ON;"); + await Chat.database.loadExtension('server/chat-plugins/trivia/transactions.ts'); } private async convertLegacyJSON() { @@ -621,7 +622,7 @@ export class TriviaSQLiteDatabase { if (!question.addedAt) question.addedAt = addedAt; if (!question.user) question.user = 'unknown user'; question.question = question.question.trim(); - await this.addQuestion(question); + await this.addQuestions([question]); } } } @@ -632,18 +633,16 @@ export class TriviaSQLiteDatabase { if (!question.addedAt) question.addedAt = addedAt; if (!question.user) question.user = 'unknown user'; question.question = question.question.trim(); - await this.addQuestionSubmission(question); + await this.addQuestionSubmissions([question]); } } } if (Array.isArray(triviaData.history)) { - const startTime = Date.now(); + const now = Date.now(); for (const game of triviaData.history) { - await this.addHistory({ - ...game, - startTime, - }); + if (!game.startTime) game.startTime = now; + await this.addHistory([game]); } } diff --git a/server/chat-plugins/trivia/transactions.ts b/server/chat-plugins/trivia/transactions.ts new file mode 100644 index 0000000000..9fca87326d --- /dev/null +++ b/server/chat-plugins/trivia/transactions.ts @@ -0,0 +1,54 @@ +/** + * SQL transactions for the Trivia plugin. + */ + +import type {TransactionEnvironment} from '../../../lib/sql'; +import type {TriviaHistory, TriviaQuestion} from './trivia'; + +export const transactions = { + addHistory: ( + args: {history: Iterable, gameHistoryInsertion: string, scoreHistoryInsertion: string}, + env: TransactionEnvironment + ) => { + const gameHistoryInsertion = env.statements.get(args.gameHistoryInsertion); + const scoreHistoryInsertion = env.statements.get(args.scoreHistoryInsertion); + if (!gameHistoryInsertion || !scoreHistoryInsertion) throw new Error('Statements not found'); + + for (const game of args.history) { + const {lastInsertRowid} = gameHistoryInsertion.run( + game.mode, game.length, game.category, game.startTime, game.creator, Number(game.givesPoints) + ); + for (const userid in game.scores) { + scoreHistoryInsertion.run(lastInsertRowid, userid, game.scores[userid]); + } + } + + return true; + }, + + addQuestions: ( + args: { + questions: Iterable, + questionInsertion: string, + answerInsertion: string, + isSubmission: boolean, + }, + env: TransactionEnvironment + ) => { + const questionInsertion = env.statements.get(args.questionInsertion); + const answerInsertion = env.statements.get(args.answerInsertion); + if (!questionInsertion || !answerInsertion) throw new Error('Statements not found'); + + const isSubmissionForSQLite = Number(args.isSubmission); + for (const question of args.questions) { + const {lastInsertRowid} = questionInsertion.run( + question.question, question.category, question.addedAt, question.user, isSubmissionForSQLite + ); + for (const answer of question.answers) { + answerInsertion.run(lastInsertRowid, answer); + } + } + + return true; + }, +}; diff --git a/server/chat-plugins/trivia.ts b/server/chat-plugins/trivia/trivia.ts similarity index 98% rename from server/chat-plugins/trivia.ts rename to server/chat-plugins/trivia/trivia.ts index 59854347e7..981557516e 100644 --- a/server/chat-plugins/trivia.ts +++ b/server/chat-plugins/trivia/trivia.ts @@ -3,8 +3,8 @@ * Written by Morfent */ -import {Utils} from '../../lib'; -import {TriviaSQLiteDatabase} from './trivia-database'; +import {Utils} from '../../../lib'; +import {TriviaSQLiteDatabase} from './database'; const MAIN_CATEGORIES: {[k: string]: string} = { ae: 'Arts and Entertainment', @@ -113,6 +113,7 @@ export interface TriviaGame { } type TriviaLadder = ID[][]; +export type TriviaHistory = TriviaGame & {scores: {[k: string]: number}}; export interface TriviaData { /** category:questions */ @@ -121,7 +122,7 @@ export interface TriviaData { leaderboard?: TriviaLeaderboard; altLeaderboard?: TriviaLeaderboard; /* `scores` key is a user ID */ - history?: (TriviaGame & {scores?: {[k: string]: number}})[]; + history?: TriviaHistory[]; moveEventQuestions?: boolean; } @@ -787,11 +788,11 @@ export class Trivia extends Rooms.RoomGame { const scores = Object.fromEntries(this.getTopPlayers({max: null}) .map(player => [player.player.id, player.player.points])); - await database.addHistory({ + await database.addHistory([{ ...this.game, length: typeof this.game.length === 'number' ? `${this.game.length} questions` : this.game.length, scores, - }); + }]); this.destroy(); } @@ -1710,6 +1711,7 @@ const triviaCommands: Chat.ChatCommands = { if (!target) return false; this.checkChat(); + const questions: TriviaQuestion[] = []; const params = target.split('\n').map(str => str.split('|')); for (const param of params) { if (param.length !== 3) { @@ -1757,24 +1759,25 @@ const triviaCommands: Chat.ChatCommands = { continue; } - const entry = { + questions.push({ category: category, question: question, answers: answers, user: user.id, addedAt: Date.now(), - }; + }); + } - if (cmd === 'add') { - await database.addQuestion(entry); - this.modlog('TRIVIAQUESTION', null, `added '${param[1]}'`); - this.privateModAction(`Question '${param[1]}' was added to the question database by ${user.name}.`); - } else { - await database.addQuestionSubmission(entry); - if (!user.can('mute', null, room)) this.sendReply(`Question '${param[1]}' was submitted for review.`); - this.modlog('TRIVIAQUESTION', null, `submitted '${param[1]}'`); - this.privateModAction(`Question '${param[1]}' was submitted to the submission database by ${user.name} for review.`); - } + const formattedQuestions = questions.map(q => q.question).join("', '"); + if (cmd === 'add') { + await database.addQuestions(questions); + this.modlog('TRIVIAQUESTION', null, `added '${formattedQuestions}'`); + this.privateModAction(`Questions '${formattedQuestions}' were added to the question database by ${user.name}.`); + } else { + await database.addQuestionSubmissions(questions); + if (!user.can('mute', null, room)) this.sendReply(`Questions '${formattedQuestions}' were submitted for review.`); + this.modlog('TRIVIAQUESTION', null, `submitted '${formattedQuestions}'`); + this.privateModAction(`Questions '${formattedQuestions}' were submitted to the submission database by ${user.name} for review.`); } }, submithelp: [`/trivia submit [category] | [question] | [answer1], [answer2] ... [answern] - Adds question(s) to the submission database for staff to review. Requires: + % @ # &`], @@ -1815,10 +1818,8 @@ const triviaCommands: Chat.ChatCommands = { const submissions = await database.getSubmissions(); if (toID(target) === 'all') { - if (isAccepting) { - await Promise.all(submissions.map(sub => database.addQuestion(sub))); - } - + if (isAccepting) await database.addQuestions(submissions); + await database.clearSubmissions(); this.modlog(`TRIVIAQUESTION`, null, `${(isAccepting ? "added" : "removed")} all from submission database.`); return this.privateModAction(`${user.name} ${(isAccepting ? " added " : " removed ")} all questions from the submission database.`); } diff --git a/test/server/chat-plugins/trivia.js b/test/server/chat-plugins/trivia.js index 36cd11a0f9..b525690892 100644 --- a/test/server/chat-plugins/trivia.js +++ b/test/server/chat-plugins/trivia.js @@ -3,7 +3,7 @@ const assert = require('assert').strict; const {makeUser} = require('../../users-utils'); -const trivia = require('../../../server/chat-plugins/trivia'); +const trivia = require('../../../server/chat-plugins/trivia/trivia'); const Trivia = trivia.Trivia; const FirstModeTrivia = trivia.FirstModeTrivia; const TimerModeTrivia = trivia.TimerModeTrivia;