Trivia: Use transactions and split into a directory

This commit is contained in:
Annika 2021-08-14 17:32:42 -07:00
parent a7ddaa1b63
commit 0a33b52ad4
5 changed files with 145 additions and 91 deletions

View File

@ -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

View File

@ -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<void> | null;
@ -121,48 +121,48 @@ export class TriviaSQLiteDatabase {
});
}
async addHistory(history: TriviaGame & {scores: {[k: string]: number}}) {
async addHistory(history: Iterable<TriviaHistory>) {
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<TriviaQuestion>) {
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<TriviaQuestion>) {
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]);
}
}

View File

@ -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<TriviaHistory>, 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<TriviaQuestion>,
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;
},
};

View File

@ -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.`);
}

View File

@ -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;