diff --git a/backend/api/controllers/quote.ts b/backend/api/controllers/quote.ts index 0f2b3eb19..f4a4ad9ab 100644 --- a/backend/api/controllers/quote.ts +++ b/backend/api/controllers/quote.ts @@ -2,7 +2,7 @@ import _ from "lodash"; import { v4 as uuidv4 } from "uuid"; import { getUser, updateQuoteRatings } from "../../dao/user"; import ReportDAO from "../../dao/report"; -import NewQuotesDao from "../../dao/new-quotes"; +import * as NewQuotesDAL from "../../dao/new-quotes"; import QuoteRatingsDAO from "../../dao/quote-ratings"; import MonkeyError from "../../utils/error"; import { verify } from "../../utils/captcha"; @@ -19,9 +19,16 @@ export async function getQuotes( req: MonkeyTypes.Request ): Promise { const { uid } = req.ctx.decodedToken; - let quoteMod: boolean | undefined | string = (await getUser(uid)).quoteMod; - if (quoteMod === true) quoteMod = "all"; - const data = await NewQuotesDao.get(quoteMod); + const quoteMod: boolean | undefined | string = (await getUser(uid)).quoteMod; + let quoteModString: string; + if (quoteMod === true) { + quoteModString = "all"; + } else if (quoteMod !== false && quoteMod !== undefined) { + quoteModString = quoteMod; + } else { + throw new MonkeyError(403, "You are not allowed to view submitted quotes"); + } + const data = await NewQuotesDAL.get(quoteModString); return new MonkeyResponse("Quote submissions retrieved", data); } @@ -33,7 +40,7 @@ export async function addQuote( await verifyCaptcha(captcha); - await NewQuotesDao.add(text, source, language, uid); + await NewQuotesDAL.add(text, source, language, uid); return new MonkeyResponse("Quote submission added"); } @@ -45,7 +52,11 @@ export async function approveQuote( const { name } = await getUser(uid); - const data = await NewQuotesDao.approve(quoteId, editText, editSource, name); + if (!name) { + throw new MonkeyError(500, "Missing name field"); + } + + const data = await NewQuotesDAL.approve(quoteId, editText, editSource, name); Logger.logToDb("system_quote_approved", data, uid); return new MonkeyResponse(data.message, data.quote); @@ -56,7 +67,7 @@ export async function refuseQuote( ): Promise { const { quoteId } = req.body; - await NewQuotesDao.refuse(quoteId); + await NewQuotesDAL.refuse(quoteId); return new MonkeyResponse("Quote refused"); } diff --git a/backend/dao/new-quotes.js b/backend/dao/new-quotes.js deleted file mode 100644 index bd9ad5bef..000000000 --- a/backend/dao/new-quotes.js +++ /dev/null @@ -1,148 +0,0 @@ -import simpleGit from "simple-git"; -import { ObjectId } from "mongodb"; -import stringSimilarity from "string-similarity"; -import path from "path"; -import fs from "fs"; -import db from "../init/db"; -import MonkeyError from "../utils/error"; - -const PATH_TO_REPO = "../../../../monkeytype-new-quotes"; - -let git; -try { - git = simpleGit(path.join(__dirname, PATH_TO_REPO)); -} catch (e) { - git = undefined; -} - -class NewQuotesDAO { - static async add(text, source, language, uid) { - if (!git) throw new MonkeyError(500, "Git not available."); - let quote = { - text: text, - source: source, - language: language.toLowerCase(), - submittedBy: uid, - timestamp: Date.now(), - approved: false, - }; - //check for duplicate first - const fileDir = path.join( - __dirname, - `${PATH_TO_REPO}/frontend/static/quotes/${language}.json` - ); - let duplicateId = -1; - let similarityScore = -1; - if (fs.existsSync(fileDir)) { - // let quoteFile = fs.readFileSync(fileDir); - // quoteFile = JSON.parse(quoteFile.toString()); - // quoteFile.quotes.every((old) => { - // if (stringSimilarity.compareTwoStrings(old.text, quote.text) > 0.9) { - // duplicateId = old.id; - // similarityScore = stringSimilarity.compareTwoStrings( - // old.text, - // quote.text - // ); - // return false; - // } - // return true; - // }); - } else { - return { languageError: 1 }; - } - if (duplicateId != -1) { - return { duplicateId, similarityScore }; - } - return await db.collection("new-quotes").insertOne(quote); - } - - static async get(language) { - if (!git) throw new MonkeyError(500, "Git not available."); - const where = { - approved: false, - }; - if (language !== "all") { - where.language = language; - } - return await db - .collection("new-quotes") - .find(where) - .sort({ timestamp: 1 }) - .limit(10) - .toArray(); - } - - static async approve(quoteId, editQuote, editSource, name) { - if (!git) throw new MonkeyError(500, "Git not available."); - //check mod status - const targetQuote = await db - .collection("new-quotes") - .findOne({ _id: new ObjectId(quoteId) }); - if (!targetQuote) { - throw new MonkeyError(404, "Quote not found"); - } - const language = targetQuote.language; - const quote = { - text: editQuote ? editQuote : targetQuote.text, - source: editSource ? editSource : targetQuote.source, - length: targetQuote.text.length, - name, - }; - let message = ""; - const fileDir = path.join( - __dirname, - `${PATH_TO_REPO}/frontend/static/quotes/${language}.json` - ); - await git.pull("upstream", "master"); - if (fs.existsSync(fileDir)) { - let quoteFile = fs.readFileSync(fileDir); - const quoteObject = JSON.parse(quoteFile.toString()); - quoteObject.quotes.every((old) => { - if (stringSimilarity.compareTwoStrings(old.text, quote.text) > 0.8) { - throw new MonkeyError(409, "Duplicate quote"); - } - }); - let maxid = 0; - quoteObject.quotes.map(function (q) { - if (q.id > maxid) { - maxid = q.id; - } - }); - quote.id = maxid + 1; - quoteObject.quotes.push(quote); - fs.writeFileSync(fileDir, JSON.stringify(quoteObject, null, 2)); - message = `Added quote to ${language}.json.`; - } else { - //file doesnt exist, create it - quote.id = 1; - fs.writeFileSync( - fileDir, - JSON.stringify({ - language: language, - groups: [ - [0, 100], - [101, 300], - [301, 600], - [601, 9999], - ], - quotes: [quote], - }) - ); - message = `Created file ${language}.json and added quote.`; - } - await git.add([`frontend/static/quotes/${language}.json`]); - await git.commit(`Added quote to ${language}.json`); - await git.push("origin", "master"); - await db.collection("new-quotes").deleteOne({ _id: new ObjectId(quoteId) }); - return { quote, message }; - } - - static async refuse(quoteId) { - if (!git) throw new MonkeyError(500, "Git not available."); - return await db - .collection("new-quotes") - .deleteOne({ _id: new ObjectId(quoteId) }); - } -} - -export default NewQuotesDAO; diff --git a/backend/dao/new-quotes.ts b/backend/dao/new-quotes.ts new file mode 100644 index 000000000..53b5de763 --- /dev/null +++ b/backend/dao/new-quotes.ts @@ -0,0 +1,174 @@ +import simpleGit from "simple-git"; +import { ObjectId } from "mongodb"; +import stringSimilarity from "string-similarity"; +import path from "path"; +import fs from "fs"; +import db from "../init/db"; +import MonkeyError from "../utils/error"; + +const PATH_TO_REPO = "../../../../monkeytype-new-quotes"; + +let git; +try { + git = simpleGit(path.join(__dirname, PATH_TO_REPO)); +} catch (e) { + git = undefined; +} + +type AddQuoteReturn = { + languageError?: number; + duplicateId?: number; + similarityScore?: number; +}; + +export async function add( + text: string, + source: string, + language: string, + uid: string +): Promise { + if (!git) throw new MonkeyError(500, "Git not available."); + const quote = { + text: text, + source: source, + language: language.toLowerCase(), + submittedBy: uid, + timestamp: Date.now(), + approved: false, + }; + //check for duplicate first + const fileDir = path.join( + __dirname, + `${PATH_TO_REPO}/frontend/static/quotes/${language}.json` + ); + let duplicateId = -1; + let similarityScore = -1; + if (fs.existsSync(fileDir)) { + const quoteFile = fs.readFileSync(fileDir); + const quoteFileJSON = JSON.parse(quoteFile.toString()); + quoteFileJSON.quotes.every((old) => { + if (stringSimilarity.compareTwoStrings(old.text, quote.text) > 0.9) { + duplicateId = old.id; + similarityScore = stringSimilarity.compareTwoStrings( + old.text, + quote.text + ); + return false; + } + return true; + }); + } else { + return { languageError: 1 }; + } + if (duplicateId != -1) { + return { duplicateId, similarityScore }; + } + await db.collection("new-quotes").insertOne(quote); +} + +export async function get(language: string): Promise { + if (!git) throw new MonkeyError(500, "Git not available."); + const where: { + approved: boolean; + language?: string; + } = { + approved: false, + }; + if (language !== "all") { + where.language = language; + } + return await db + .collection("new-quotes") + .find(where) + .sort({ timestamp: 1 }) + .limit(10) + .toArray(); +} + +type Quote = { + id?: number; + text: string; + source: string; + length: number; + name: string; +}; + +type ApproveReturn = { + quote: Quote; + message: string; +}; + +export async function approve( + quoteId: string, + editQuote: string, + editSource: string, + name: string +): Promise { + if (!git) throw new MonkeyError(500, "Git not available."); + //check mod status + const targetQuote = await db + .collection("new-quotes") + .findOne({ _id: new ObjectId(quoteId) }); + if (!targetQuote) { + throw new MonkeyError(404, "Quote not found"); + } + const language = targetQuote.language; + const quote: Quote = { + text: editQuote ? editQuote : targetQuote.text, + source: editSource ? editSource : targetQuote.source, + length: targetQuote.text.length, + name, + }; + let message = ""; + const fileDir = path.join( + __dirname, + `${PATH_TO_REPO}/frontend/static/quotes/${language}.json` + ); + await git.pull("upstream", "master"); + if (fs.existsSync(fileDir)) { + const quoteFile = fs.readFileSync(fileDir); + const quoteObject = JSON.parse(quoteFile.toString()); + quoteObject.quotes.every((old) => { + if (stringSimilarity.compareTwoStrings(old.text, quote.text) > 0.8) { + throw new MonkeyError(409, "Duplicate quote"); + } + }); + let maxid = 0; + quoteObject.quotes.map(function (q) { + if (q.id > maxid) { + maxid = q.id; + } + }); + quote.id = maxid + 1; + quoteObject.quotes.push(quote); + fs.writeFileSync(fileDir, JSON.stringify(quoteObject, null, 2)); + message = `Added quote to ${language}.json.`; + } else { + //file doesnt exist, create it + quote.id = 1; + fs.writeFileSync( + fileDir, + JSON.stringify({ + language: language, + groups: [ + [0, 100], + [101, 300], + [301, 600], + [601, 9999], + ], + quotes: [quote], + }) + ); + message = `Created file ${language}.json and added quote.`; + } + await git.add([`frontend/static/quotes/${language}.json`]); + await git.commit(`Added quote to ${language}.json`); + await git.push("origin", "master"); + await db.collection("new-quotes").deleteOne({ _id: new ObjectId(quoteId) }); + return { quote, message }; +} + +export async function refuse(quoteId: string): Promise { + if (!git) throw new MonkeyError(500, "Git not available."); + await db.collection("new-quotes").deleteOne({ _id: new ObjectId(quoteId) }); +} diff --git a/backend/types/types.d.ts b/backend/types/types.d.ts index 54ed5ee87..d05e3b290 100644 --- a/backend/types/types.d.ts +++ b/backend/types/types.d.ts @@ -126,6 +126,16 @@ declare namespace MonkeyTypes { enabled: boolean; } + interface NewQuote { + _id: ObjectId; + text: string; + source: string; + language: string; + submittedBy: string; + timestamp: number; + approved: boolean; + } + type Mode = "time" | "words" | "quote" | "zen" | "custom"; type Mode2 = keyof PersonalBests[M];