const express = require("express"); const { config } = require("dotenv"); const path = require("path"); config({ path: path.join(__dirname, ".env") }); const bodyParser = require("body-parser"); const mongoose = require("mongoose"); const { MongoClient } = require("mongodb"); const cors = require("cors"); const admin = require("firebase-admin"); const helmet = require("helmet"); const { User } = require("./models/user"); const { Leaderboard } = require("./models/leaderboard"); const { BotCommand } = require("./models/bot-command"); const { Stats } = require("./models/stats"); // Firebase admin setup //currently uses account key in functions to prevent repetition const serviceAccount = require("./credentials/serviceAccountKey.json"); const { connectDB } = require("./init/mongodb"); admin.initializeApp({ credential: admin.credential.cert(serviceAccount), }); // MIDDLEWARE & SETUP const app = express(); app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use(cors()); app.use(helmet()); const port = process.env.PORT || "5005"; connectDB() .then(() => { app.listen(process.env.PORT, () => { console.log(`listening on port ${process.env.PORT}`); }); }) .catch((e) => { console.log(e); }); const authRouter = require("./api/routes/auth"); app.use("/auth", authRouter); // Daily leaderboard clear function function clearDailyLeaderboards() { var nextClear = new Date(); nextClear.setHours(24, 0, 0, 0); //next occurrence of 12am let currentTime = new Date(); Leaderboard.find({ type: "daily" }, (err, lbs) => { lbs.forEach((lb) => { lb.resetTime = nextClear; lb.save(); }); }); setTimeout(() => { Leaderboard.find({ type: "daily" }, (err, lbs) => { lbs.forEach((lb) => { User.findOne({ name: lb.board[0].name }, (err, user) => { if (user) { if (user.dailyLbWins === undefined) { user.dailyLbWins = { [lb.mode + lb.mode2]: 1, }; } else if (user.dailyLbWins[lb.mode + lb.mode2] === undefined) { user.dailyLbWins[lb.mode + lb.mode2] = 1; } else { user.dailyLbWins[lb.mode + lb.mode2]++; } user.save(); } }).then(() => { announceDailyLbResult(lb); lb.board = []; lb.save(); }); }); }); clearDailyLeaderboards(); }, nextClear.getTime() - currentTime.getTime()); } // Initialize database leaderboards if no leaderboards exist and start clearDailyLeaderboards Leaderboard.findOne((err, lb) => { if (lb === null) { let lb = { size: 999, board: [], mode: "time", mode2: 15, type: "global", }; Leaderboard.create(lb); lb.mode2 = 60; Leaderboard.create(lb); lb.type = "daily"; lb.size = 100; Leaderboard.create(lb); lb.mode2 = 15; Leaderboard.create(lb); } }).then(() => { clearDailyLeaderboards(); }); // Initialize stats database if none exists Stats.findOne((err, stats) => { if (!stats) { let newStats = new Stats({ completedTests: 0, startedTests: 0, timeTyping: 0, }); newStats.save(); } }); async function authenticateToken(req, res, next) { const authHeader = req.headers["authorization"]; const token = await admin .auth() .verifyIdToken(req.headers.authorization.split(" ")[1]); if (token == null) { return res.sendStatus(401); } else { req.name = token.name; req.uid = token.user_id; next(); } } // NON-ROUTE FUNCTIONS function updateDiscordRole(discordId, wpm) { newBotCommand = new BotCommand({ command: "updateRole", arguments: [discordId, wpm], executed: false, requestTimestamp: Date.now(), }); newBotCommand.save(); } async function announceLbUpdate(discordId, pos, lb, wpm, raw, acc, con) { newBotCommand = new BotCommand({ command: "sayLbUpdate", arguments: [discordId, pos, lb, wpm, raw, acc, con], executed: false, requestTimestamp: Date.now(), }); newBotCommand.save(); } async function announceDailyLbResult(lbdata) { newBotCommand = new BotCommand({ command: "announceDailyLbResult", arguments: [lbdata], executed: false, requestTimestamp: Date.now(), }); newBotCommand.save(); } async function checkIfPB(obj, userdata) { let pbs = null; if (obj.mode == "quote") { return false; } if (obj.funbox !== "none") { return false; } try { pbs = userdata.personalBests; if (pbs === undefined) { throw new Error("pb is undefined"); } } catch (e) { User.findOne({ uid: userdata.uid }, (err, user) => { user.personalBests = { [obj.mode]: { [obj.mode2]: [ { language: obj.language, difficulty: obj.difficulty, punctuation: obj.punctuation, wpm: obj.wpm, acc: obj.acc, raw: obj.rawWpm, timestamp: Date.now(), consistency: obj.consistency, }, ], }, }; }).then(() => { return true; }); } // //check mode, mode2, punctuation, language and difficulty let toUpdate = false; let found = false; try { if (pbs[obj.mode][obj.mode2] === undefined) { pbs[obj.mode][obj.mode2] = []; } pbs[obj.mode][obj.mode2].forEach((pb) => { if ( pb.punctuation === obj.punctuation && pb.difficulty === obj.difficulty && pb.language === obj.language ) { //entry like this already exists, compare wpm found = true; if (pb.wpm < obj.wpm) { //new pb pb.wpm = obj.wpm; pb.acc = obj.acc; pb.raw = obj.rawWpm; pb.timestamp = Date.now(); pb.consistency = obj.consistency; toUpdate = true; } else { //no pb return false; } } }); //checked all pbs, nothing found - meaning this is a new pb if (!found) { pbs[obj.mode][obj.mode2] = [ { language: obj.language, difficulty: obj.difficulty, punctuation: obj.punctuation, wpm: obj.wpm, acc: obj.acc, raw: obj.rawWpm, timestamp: Date.now(), consistency: obj.consistency, }, ]; toUpdate = true; } } catch (e) { // console.log(e); pbs[obj.mode] = {}; pbs[obj.mode][obj.mode2] = [ { language: obj.language, difficulty: obj.difficulty, punctuation: obj.punctuation, wpm: obj.wpm, acc: obj.acc, raw: obj.rawWpm, timestamp: Date.now(), consistency: obj.consistency, }, ]; toUpdate = true; } if (toUpdate) { User.findOne({ uid: userdata.uid }, (err, user) => { user.personalBests = pbs; user.save(); }); return true; } else { return false; } } async function checkIfTagPB(obj, userdata) { //function returns a list of tag ids where a pb was set //i think if (obj.tags.length === 0) { return []; } if (obj.mode === "quote") { return []; } let dbtags = []; //tags from database: include entire document: name, id, pbs let restags = obj.tags; //result tags try { let snap; await User.findOne({ uid: userdata.uid }, (err, user) => { snap = user.tags; }); snap.forEach((doc) => { //if (restags.includes(doc._id)) { //if (restags.indexOf((doc._id).toString()) > -1) { if (restags.includes(doc._id.toString())) { //not sure what this is supposed to do /* let data = doc.data(); data.id = doc.id; dbtags.push(data); */ dbtags.push(doc); } }); } catch { return []; } let ret = []; for (let i = 0; i < dbtags.length; i++) { let pbs = null; try { pbs = dbtags[i].personalBests; if (pbs === undefined || pbs === {}) { throw new Error("pb is undefined"); } } catch (e) { //if pb is undefined, create a new personalBests field with only specified value await User.findOne({ uid: userdata.uid }, (err, user) => { //it might be more convenient if tags was an object with ids as the keys //find tag index in tags list // save that tags personal bests as object let j = user.tags.findIndex((tag) => { return tag._id.toString() == dbtags[i]._id.toString(); }); user.tags[j].personalBests = { [obj.mode]: { [obj.mode2]: [ { language: obj.language, difficulty: obj.difficulty, punctuation: obj.punctuation, wpm: obj.wpm, acc: obj.acc, raw: obj.rawWpm, timestamp: Date.now(), consistency: obj.consistency, }, ], }, }; pbs = user.tags[j].personalBests; user.save(); }).then((updatedUser) => { ret.push(dbtags[i]._id.toString()); }); continue; } let toUpdate = false; let found = false; try { if (pbs[obj.mode] === undefined) { pbs[obj.mode] = { [obj.mode2]: [] }; } else if (pbs[obj.mode][obj.mode2] === undefined) { pbs[obj.mode][obj.mode2] = []; } pbs[obj.mode][obj.mode2].forEach((pb) => { if ( pb.punctuation === obj.punctuation && pb.difficulty === obj.difficulty && pb.language === obj.language ) { //entry like this already exists, compare wpm found = true; if (pb.wpm < obj.wpm) { //replace old pb with new obj pb.wpm = obj.wpm; pb.acc = obj.acc; pb.raw = obj.rawWpm; pb.timestamp = Date.now(); pb.consistency = obj.consistency; toUpdate = true; } else { //no pb return false; } } }); //checked all pbs, nothing found - meaning this is a new pb if (!found) { console.log("Semi-new pb"); //push this pb to array pbs[obj.mode][obj.mode2].push({ language: obj.language, difficulty: obj.difficulty, punctuation: obj.punctuation, wpm: obj.wpm, acc: obj.acc, raw: obj.rawWpm, timestamp: Date.now(), consistency: obj.consistency, }); toUpdate = true; } } catch (e) { // console.log(e); console.log("Catch pb"); console.log(e); pbs[obj.mode] = {}; pbs[obj.mode][obj.mode2] = [ { language: obj.language, difficulty: obj.difficulty, punctuation: obj.punctuation, wpm: obj.wpm, acc: obj.acc, raw: obj.rawWpm, timestamp: Date.now(), consistency: obj.consistency, }, ]; toUpdate = true; } if (toUpdate) { //push working pb array to user tags pbs await User.findOne({ uid: userdata.uid }, (err, user) => { for (let j = 0; j < user.tags.length; j++) { if (user.tags[j]._id.toString() === dbtags[i]._id.toString()) { user.tags[j].personalBests = pbs; } } user.save(); }); ret.push(dbtags[i]._id.toString()); } } console.log(ret); return ret; } // // async function stripAndSave(uid, obj) { // if (obj.bailedOut === false) delete obj.bailedOut; // if (obj.blindMode === false) delete obj.blindMode; // if (obj.difficulty === "normal") delete obj.difficulty; // if (obj.funbox === "none") delete obj.funbox; // //stripping english causes issues in result filtering; this line: // //let langFilter = ResultFilters.getFilter("language", result.language); // //returns false if language isn't defined in result // //if (obj.language === "english") delete obj.language; // if (obj.numbers === false) delete obj.numbers; // if (obj.punctuation === false) delete obj.punctuation; // await User.findOne({ uid: uid }, (err, user) => { // user.results.push(obj); // user.save(); // }); // } function incrementT60Bananas(uid, result, userData) { try { let best60; try { best60 = Math.max( ...userData.personalBests.time[60].map((best) => best.wpm) ); if (!Number.isFinite(best60)) { throw "Not finite"; } } catch (e) { best60 = undefined; } if (best60 != undefined && result.wpm < best60 - best60 * 0.25) { // console.log("returning"); } else { //increment // console.log("checking"); User.findOne({ uid: uid }, (err, user) => { if (user.bananas === undefined) { user.bananas.t60bananas = 1; } else { user.bananas.t60bananas += 1; } user.save(); }); } } catch (e) { console.log( "something went wrong when trying to increment bananas " + e.message ); } } async function incrementUserGlobalTypingStats(userData, resultObj) { let userGlobalStats = userData.globalStats; try { let newStarted; let newCompleted; let newTime; let tt = 0; let afk = resultObj.afkDuration; if (afk == undefined) { afk = 0; } tt = resultObj.testDuration + resultObj.incompleteTestSeconds - afk; if (tt > 500) console.log( `FUCK, INCREASING BY A LOT ${resultObj.uid}: ${JSON.stringify( resultObj )}` ); if (userGlobalStats.started === undefined) { newStarted = resultObj.restartCount + 1; } else { newStarted = userGlobalStats.started + resultObj.restartCount + 1; } if (userGlobalStats.completed === undefined) { newCompleted = 1; } else { newCompleted = userGlobalStats.completed + 1; } if (userGlobalStats.time === undefined) { newTime = tt; } else { newTime = userGlobalStats.time + tt; } incrementPublicTypingStats(resultObj.restartCount + 1, 1, tt); User.findOne({ uid: userData.uid }, (err, user) => { user.globalStats = { started: newStarted, completed: newCompleted, time: roundTo2(newTime), }; user.save(); }); } catch (e) { console.error(`Error while incrementing stats for user ${uid}: ${e}`); } } async function incrementPublicTypingStats(started, completed, time) { try { time = roundTo2(time); Stats.findOne({}, (err, stats) => { stats.completedTests += completed; stats.startedTests += started; stats.timeTyping += time; stats.save(); }); } catch (e) { console.error(`Error while incrementing public stats: ${e}`); } } // API app.post("/testCompleted", authenticateToken, (req, res) => { User.findOne({ uid: req.uid }, (err, user) => { if (err) res.status(500).send({ error: err }); request = req.body; if (request === undefined) { res.status(200).send({ resultCode: -999 }); return; } try { if (req.uid === undefined || request.obj === undefined) { console.error(`error saving result for - missing input`); res.status(200).send({ resultCode: -999 }); return; } let obj = request.obj; if (obj.incompleteTestSeconds > 500) console.log( `FUCK, HIGH INCOMPLETE TEST SECONDS ${req.uid}: ${JSON.stringify( obj )}` ); function verifyValue(val) { let errCount = 0; if (val === null || val === undefined) { } else if (Array.isArray(val)) { //array val.forEach((val2) => { errCount += verifyValue(val2); }); } else if (typeof val === "object" && !Array.isArray(val)) { //object Object.keys(val).forEach((valkey) => { errCount += verifyValue(val[valkey]); }); } else { if (!/^[0-9a-zA-Z._\-\+]+$/.test(val)) errCount++; } return errCount; } let errCount = verifyValue(obj); if (errCount > 0) { console.error( `error saving result for ${ req.uid } error count ${errCount} - bad input - ${JSON.stringify( request.obj )}` ); res.status(200).send({ resultCode: -1 }); return; } if ( obj.wpm <= 0 || obj.wpm > 350 || obj.acc < 50 || obj.acc > 100 || obj.consistency > 100 ) { res.status(200).send({ resultCode: -1 }); return; } if ( (obj.mode === "time" && obj.mode2 < 15 && obj.mode2 > 0) || (obj.mode === "time" && obj.mode2 == 0 && obj.testDuration < 15) || (obj.mode === "words" && obj.mode2 < 10 && obj.mode2 > 0) || (obj.mode === "words" && obj.mode2 == 0 && obj.testDuration < 15) || (obj.mode === "custom" && obj.customText !== undefined && !obj.customText.isWordRandom && !obj.customText.isTimeRandom && obj.customText.textLen < 10) || (obj.mode === "custom" && obj.customText !== undefined && obj.customText.isWordRandom && !obj.customText.isTimeRandom && obj.customText.word < 10) || (obj.mode === "custom" && obj.customText !== undefined && !obj.customText.isWordRandom && obj.customText.isTimeRandom && obj.customText.time < 15) ) { res.status(200).send({ resultCode: -5, message: "Test too short" }); return; } if (!validateResult(obj)) { if ( obj.bailedOut && ((obj.mode === "time" && obj.mode2 >= 3600) || (obj.mode === "words" && obj.mode2 >= 5000) || obj.mode === "custom") ) { //dont give an error } else { res.status(200).send({ resultCode: -4 }); return; } } let keySpacing = null; let keyDuration = null; try { keySpacing = { average: obj.keySpacing.reduce( (previous, current) => (current += previous) ) / obj.keySpacing.length, sd: stdDev(obj.keySpacing), }; keyDuration = { average: obj.keyDuration.reduce( (previous, current) => (current += previous) ) / obj.keyDuration.length, sd: stdDev(obj.keyDuration), }; } catch (e) { console.error( `cant verify key spacing or duration for user ${req.uid}! - ${e} - ${obj.keySpacing} ${obj.keyDuration}` ); } obj.keySpacingStats = keySpacing; obj.keyDurationStats = keyDuration; if (obj.mode == "time" && (obj.mode2 == 15 || obj.mode2 == 60)) { } else { obj.keySpacing = "removed"; obj.keyDuration = "removed"; } // emailVerified = await admin // .auth() // .getUser(req.uid) // .then((user) => { // return user.emailVerified; // }); // emailVerified = true; // if (obj.funbox === "nospace") { // res.status(200).send({ data: { resultCode: -1 } }); // return; // } //user.results.push() let userdata = user; let name = userdata.name === undefined ? false : userdata.name; let banned = userdata.banned === undefined ? false : userdata.banned; let verified = userdata.verified; request.obj.name = name; //check keyspacing and duration here if (obj.mode === "time" && obj.wpm > 130 && obj.testDuration < 122) { if (verified === false || verified === undefined) { if (keySpacing !== null && keyDuration !== null) { if ( keySpacing.sd <= 15 || keyDuration.sd <= 10 || keyDuration.average < 15 || (obj.wpm > 200 && obj.consistency < 70) ) { console.error( `possible bot detected by user (${obj.wpm} ${obj.rawWpm} ${ obj.acc }) ${req.name} ${name} - spacing ${JSON.stringify( keySpacing )} duration ${JSON.stringify(keyDuration)}` ); res.status(200).send({ resultCode: -2 }); return; } if ( (keySpacing.sd > 15 && keySpacing.sd <= 25) || (keyDuration.sd > 10 && keyDuration.sd <= 15) || (keyDuration.average > 15 && keyDuration.average <= 20) ) { console.error( `very close to bot detected threshold by user (${obj.wpm} ${ obj.rawWpm } ${obj.acc}) ${req.uid} ${name} - spacing ${JSON.stringify( keySpacing )} duration ${JSON.stringify(keyDuration)}` ); } } else { res.status(200).send({ resultCode: -3 }); return; } } } //yeet the key data obj.keySpacing = null; obj.keyDuration = null; try { obj.keyDurationStats.average = roundTo2(obj.keyDurationStats.average); obj.keyDurationStats.sd = roundTo2(obj.keyDurationStats.sd); obj.keySpacingStats.average = roundTo2(obj.keySpacingStats.average); obj.keySpacingStats.sd = roundTo2(obj.keySpacingStats.sd); } catch (e) {} // return db // .collection(`users/${req.uid}/results`) // .add(obj) // .then((e) => { // let createdDocId = e.id; return Promise.all([ // checkLeaderboards( // request.obj, // "global", // banned, // name, // verified, // emailVerified // ), // checkLeaderboards( // request.obj, // "daily", // banned, // name, // verified, // emailVerified // ), checkIfPB(request.obj, userdata), checkIfTagPB(request.obj, userdata), ]) .then(async (values) => { // let globallb = values[0].insertedAt; // let dailylb = values[1].insertedAt; let ispb = values[0]; let tagPbs = values[1]; // console.log(values); if (obj.mode === "time" && String(obj.mode2) === "60") { incrementT60Bananas(req.uid, obj, userdata); } await incrementUserGlobalTypingStats(userdata, obj); //equivalent to getIncrementedTypingStats let returnobj = { resultCode: null, // globalLeaderboard: globallb, // dailyLeaderboard: dailylb, // lbBanned: banned, name: name, needsToVerify: values[0].needsToVerify, needsToVerifyEmail: values[0].needsToVerifyEmail, tagPbs: tagPbs, }; if (ispb) { let logobj = request.obj; logobj.keySpacing = "removed"; logobj.keyDuration = "removed"; console.log( `saved result for ${req.uid} (new PB) - ${JSON.stringify(logobj)}` ); /* User.findOne({ name: userdata.name }, (err, user2) => { console.log(user2.results[user2.results.length-1]) console.log(user2.results[user2.results.length-1]).isPb user2.results[user2.results.length-1].isPb = true; user2.save(); }) */ request.obj.isPb = true; if ( obj.mode === "time" && String(obj.mode2) === "60" && userdata.discordId !== null && userdata.discordId !== undefined ) { if (verified !== false) { console.log( `sending command to the bot to update the role for user ${req.uid} with wpm ${obj.wpm}` ); updateDiscordRole(userdata.discordId, Math.round(obj.wpm)); } } returnobj.resultCode = 2; } else { let logobj = request.obj; logobj.keySpacing = "removed"; logobj.keyDuration = "removed"; request.obj.isPb = false; console.log( `saved result for ${req.uid} - ${JSON.stringify(logobj)}` ); returnobj.resultCode = 1; } stripAndSave(req.uid, request.obj); res.status(200).send(returnobj); }) .catch((e) => { console.error( `error saving result when checking for PB / checking leaderboards for ${req.uid} - ${e.message}` ); res .status(200) .send({ data: { resultCode: -999, message: e.message } }); }); } catch (e) { console.error( `error saving result for ${req.uid} - ${JSON.stringify( request.obj )} - ${e}` ); res.status(200).send({ resultCode: -999, message: e.message }); } }); }); app.get("/userResults", authenticateToken, (req, res) => { User.findOne({ uid: req.uid }, (err, user) => { if (err) res.status(500).send({ error: err }); res.status(200).send({ results: user.results }); }); res.sendStatus(200); }); app.post("/clearTagPb", authenticateToken, (req, res) => { User.findOne({ uid: req.uid }, (err, user) => { for (let i = 0; i < user.tags.length; i++) { if (user.tags[i]._id.toString() === req.body.tagid.toString()) { user.tags[i].personalBests = {}; user.save(); res.send({ resultCode: 1 }); return; } } }).catch((e) => { console.error(`error deleting tag pb for user ${req.uid}: ${e.message}`); res.send({ resultCode: -999, message: e.message, }); }); res.sendStatus(200); }); app.post("/unlinkDiscord", authenticateToken, (req, res) => { request = req.body.data; try { if (request === null || req.uid === undefined) { res.status(200).send({ status: -999, message: "Empty request" }); return; } User.findOne({ uid: req.uid }, (err, user) => { user.discordId = null; user.save(); }) .then((f) => { res.status(200).send({ status: 1, message: "Unlinked", }); }) .catch((e) => { res.status(200).send({ status: -999, message: e.message, }); }); } catch (e) { res.status(200).send({ status: -999, message: e, }); } }); app.post("/removeSmallTestsAndQPB", authenticateToken, (req, res) => { User.findOne({ uid: req.uid }, (err, user) => { user.results.forEach((result, index) => { if ( (result.mode == "time" && result.mode2 < 15) || (result.mode == "words" && result.mode2 < 10) || (result.mode == "custom" && result.testDuration < 10) ) { user.results.splice(index, 1); } }); try { delete user.personalBests.quote; } catch {} user.refactored = true; user.save(); console.log("removed small tests for " + req.uid); res.status(200); }).catch((e) => { console.log(`something went wrong for ${req.uid}: ${e.message}`); res.status(200); }); }); app.post("/updateResultTags", authenticateToken, (req, res) => { try { let validTags = true; req.body.tags.forEach((tag) => { if (!/^[0-9a-zA-Z]+$/.test(tag)) validTags = false; }); if (validTags) { User.findOne({ uid: req.uid }, (err, user) => { for (let i = 0; i < user.results.length; i++) { if (user.results[i]._id.toString() === req.body.resultid.toString()) { user.results[i].tags = req.body.tags; user.save(); console.log( `user ${request.uid} updated tags for result ${request.resultid}` ); res.send({ resultCode: 1 }); return; } } console.error( `error while updating tags for result by user ${req.uid}: ${e.message}` ); res.send({ resultCode: -999 }); }); } else { console.error(`invalid tags for user ${req.uid}: ${req.body.tags}`); res.send({ resultCode: -1 }); } } catch (e) { console.error(`error updating tags by ${req.uid} - ${e}`); res.send({ resultCode: -999, message: e }); } }); app.post("/updateEmail", authenticateToken, (req, res) => { try { admin .auth() .getUser(req.uid) .then((previous) => { if (previous.email !== req.body.previousEmail) { res.send({ resultCode: -1 }); } else { User.findOne({ uid: req.uid }, (err, user) => { user.email = req.body.newEmail; user.emailVerified = false; user.save(); res.send({ resultCode: 1 }); }); } }); } catch (e) { console.error(`error updating email for ${req.uid} - ${e}`); res.send({ resultCode: -999, message: e.message, }); } }); app.post("/saveConfig", authenticateToken, (req, res) => { try { if (req.uid === undefined || req.body.obj === undefined) { console.error(`error saving config for ${req.uid} - missing input`); res.send({ resultCode: -1, message: "Missing input", }); } let obj = req.body.obj; let errorMessage = ""; let err = false; Object.keys(obj).forEach((key) => { if (err) return; if (!isConfigKeyValid(key)) { err = true; console.error(`${key} failed regex check`); errorMessage = `${key} failed regex check`; } if (err) return; if (key === "resultFilters") return; if (key === "customBackground") return; let val = obj[key]; if (Array.isArray(val)) { val.forEach((valarr) => { if (!isConfigKeyValid(valarr)) { err = true; console.error(`${key}: ${valarr} failed regex check`); errorMessage = `${key}: ${valarr} failed regex check`; } }); } else { if (!isConfigKeyValid(val)) { err = true; console.error(`${key}: ${val} failed regex check`); errorMessage = `${key}: ${val} failed regex check`; } } }); if (err) { console.error( `error saving config for ${req.uid} - bad input - ${JSON.stringify( request.obj )}` ); res.send({ resultCode: -1, message: "Bad input. " + errorMessage, }); } User.findOne({ uid: req.uid }, (err, user) => { if (err) res.status(500).send({ error: err }); user.config = obj; user.save(); }) .then(() => { res.send({ resultCode: 1, message: "Saved", }); }) .catch((e) => { console.error( `error saving config to DB for ${req.uid} - ${e.message}` ); res.send({ resultCode: -1, message: e.message, }); }); } catch (e) { console.error(`error saving config for ${req.uid} - ${e}`); res.send({ resultCode: -999, message: e, }); } }); app.post("/addPreset", authenticateToken, (req, res) => { try { if (!isTagPresetNameValid(req.body.obj.name)) { return { resultCode: -1 }; } else if (req.uid === undefined || req.body.obj === undefined) { console.error(`error saving config for ${req.uid} - missing input`); res.json({ resultCode: -1, message: "Missing input", }); } else { let config = req.body.obj.config; let errorMessage = ""; let err = false; Object.keys(config).forEach((key) => { if (err) return; if (!isConfigKeyValid(key)) { err = true; console.error(`${key} failed regex check`); errorMessage = `${key} failed regex check`; } if (err) return; if (key === "resultFilters") return; if (key === "customBackground") return; let val = config[key]; if (Array.isArray(val)) { val.forEach((valarr) => { if (!isConfigKeyValid(valarr)) { err = true; console.error(`${key}: ${valarr} failed regex check`); errorMessage = `${key}: ${valarr} failed regex check`; } }); } else { if (!isConfigKeyValid(val)) { err = true; console.error(`${key}: ${val} failed regex check`); errorMessage = `${key}: ${val} failed regex check`; } } }); if (err) { console.error( `error adding preset for ${req.uid} - bad input - ${JSON.stringify( req.body.obj )}` ); res.json({ resultCode: -1, message: "Bad input. " + errorMessage, }); } User.findOne({ uid: req.uid }, (err, user) => { if (user.presets.length >= 10) { res.json({ resultCode: -2, message: "Preset limit", }); } else { user.presets.push(req.body.obj); user.save(); } }) .then((updatedUser) => { User.findOne({ uid: req.uid }, (err, user) => { res.json({ resultCode: 1, message: "Saved", id: user.presets[user.presets.length - 1]._id, }); }); }) .catch((e) => { console.error( `error adding preset to DB for ${req.uid} - ${e.message}` ); res.json({ resultCode: -1, message: e.message, }); }); } } catch (e) { console.error(`error adding preset for ${req.uid} - ${e}`); res.json({ resultCode: -999, message: e, }); } }); app.post("/editPreset", authenticateToken, (req, res) => { try { if (!isTagPresetNameValid(req.body.presetName)) { res.json({ resultCode: -1 }); } else { User.findOne({ uid: req.uid }, (err, user) => { for (i = 0; i < user.presets.length; i++) { if (user.presets[i]._id.toString() == req.body.presetid.toString()) { user.presets[i] = { config: req.body.config, name: req.body.presetName, }; break; } } user.save(); }) .then((e) => { console.log( `user ${req.uid} updated a preset: ${req.body.presetName}` ); res.json({ resultCode: 1, }); }) .catch((e) => { console.error( `error while updating preset for user ${req.uid}: ${e.message}` ); res.json({ resultCode: -999, message: e.message }); }); } } catch (e) { console.error(`error updating preset for ${req.uid} - ${e}`); res.json({ resultCode: -999, message: e.message }); } }); app.post("/removePreset", authenticateToken, (req, res) => { try { User.findOne({ uid: req.uid }, (err, user) => { for (i = 0; i < user.presets.length; i++) { if (user.presets[i]._id.toString() == req.body.presetid.toString()) { user.presets.splice(i, 1); break; } } user.save(); }) .then((e) => { console.log(`user ${req.uid} deleted a preset`); res.send({ resultCode: 1 }); }) .catch((e) => { console.error( `error deleting preset for user ${req.uid}: ${e.message}` ); res.send({ resultCode: -999 }); }); } catch (e) { console.error(`error deleting preset for ${req.uid} - ${e}`); res.send({ resultCode: -999 }); } }); //could use /tags/add instead app.post("/addTag", authenticateToken, (req, res) => { try { if (!isTagPresetNameValid(req.body.tagName)) return { resultCode: -1 }; User.findOne({ uid: req.uid }, (err, user) => { if (err) res.status(500).send({ error: err }); if (user.tags.includes(req.body.tagName)) { return { resultCode: -999, message: "Duplicate tag" }; } const tagObj = { name: req.body.tagName }; user.tags.push(tagObj); user.save(); }) .then(() => { console.log(`user ${req.uid} created a tag: ${req.body.tagName}`); let newTagId; User.findOne({ uid: req.uid }, (err, user) => { newTagId = user.tags[user.tags.length - 1]._id; }).then(() => { res.json({ resultCode: 1, id: newTagId, }); }); }) .catch((e) => { console.error( `error while creating tag for user ${req.uid}: ${e.message}` ); res.json({ resultCode: -999, message: e.message }); }); } catch (e) { console.error(`error adding tag for ${req.uid} - ${e}`); res.json({ resultCode: -999, message: e.message }); } }); app.post("/editTag", authenticateToken, (req, res) => { try { if (!isTagPresetNameValid(req.body.tagName)) return { resultCode: -1 }; User.findOne({ uid: req.uid }, (err, user) => { if (err) res.status(500).send({ error: err }); for (var i = 0; i < user.tags.length; i++) { if (user.tags[i]._id == req.body.tagId) { user.tags[i].name = req.body.tagName; } } user.save(); }) .then((updatedUser) => { console.log(`user ${req.uid} updated a tag: ${req.body.tagName}`); res.json({ resultCode: 1 }); }) .catch((e) => { console.error( `error while updating tag for user ${req.uid}: ${e.message}` ); res.json({ resultCode: -999, message: e.message }); }); } catch (e) { console.error(`error updating tag for ${req.uid} - ${e}`); res.json({ resultCode: -999, message: e.message }); } }); app.post("/removeTag", authenticateToken, (req, res) => { try { User.findOne({ uid: req.uid }, (err, user) => { if (err) res.status(500).send({ error: err }); for (var i = 0; i < user.tags.length; i++) { if (user.tags[i]._id == req.body.tagId) { user.tags.splice(i, 1); } } user.save(); }) .then((updatedUser) => { console.log(`user ${req.uid} deleted a tag`); res.json({ resultCode: 1 }); }) .catch((e) => { console.error(`error deleting tag for user ${req.uid}: ${e.message}`); res.json({ resultCode: -999 }); }); } catch (e) { console.error(`error deleting tag for ${req.uid} - ${e}`); res.json({ resultCode: -999 }); } }); app.post("/verifyDiscord", authenticateToken, (req, res) => { /* Not tested yet */ response.set("Access-Control-Allow-Origin", origin); response.set("Access-Control-Allow-Headers", "*"); response.set("Access-Control-Allow-Credentials", "true"); if (request.method === "OPTIONS") { // Send response to OPTIONS requests response.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); response.set("Access-Control-Allow-Headers", "Authorization,Content-Type"); response.set("Access-Control-Max-Age", "3600"); response.status(204).send(""); return; } request = req.body.data; if (request.uid == undefined) { response.status(200).send({ status: -1, message: "Need to provide uid" }); return; } try { return fetch("https://discord.com/api/users/@me", { headers: { authorization: `${request.tokenType} ${request.accessToken}`, }, }) .then((res) => res.json()) .then(async (res2) => { let did = res2.id; User.findOne({ discordId: did }, (err, user) => { if (user) { res.status(200).send({ status: -1, message: "This Discord account is already paired to a different Monkeytype account", }); } else { User.findOne({ uid: req.uid }, (err, user2) => { user2.discordId = did; user2.save(); newCommand = new BotCommand({ command: "verify", arguments: [did, req.uid], executed: false, requestTimestamp: Date.now(), }); newCommand.save(); res .status(200) .send({ status: 1, message: "Verified", did: did }); }); } }); }) .catch((e) => { console.error( "Something went wrong when trying to verify discord of user " + e.message ); response.status(200).send({ status: -1, message: e.message }); }); } catch (e) { response.status(200).send({ status: -1, message: e }); } }); app.post("/resetPersonalBests", authenticateToken, (req, res) => { try { User.findOne({ uid: req.uid }, (err, user) => { if (err) res.status(500).send({ error: err }); user.personalBests = {}; user.save(); }); res.status(200).send({ status: "Reset Pbs successfully" }); } catch (e) { console.log( `something went wrong when deleting personal bests for ${uid}: ${e.message}` ); res.status(500).send({ status: "Reset Pbs successfully" }); } }); function addToLeaderboard(lb, result, username) { //insertedAt is index of array inserted position, 1 is added after retData = { insertedAt: -1 }; //check for duplicate user for (i = 0; i < lb.board.length; i++) { if (lb.board[i].name == username) { if (lb.board[i].wpm <= result.wpm) { //delete old entry if speed is faster this time lb.board.splice(i, 1); retData.foundAt = i + 1; retData.newBest = true; } else { //don't add new entry if slower than last time return lb, { insertedAt: -1, foundAt: i + 1 }; } } } //when is newBest not true? retData.newBest = true; if (!retData.foundAt) retData.foundAt = -1; //determine if the entry should be hidden //add item to leaderboard const lbitem = { name: username, wpm: result.wpm, raw: result.rawWpm, acc: result.acc, consistency: result.consistency, mode: result.mode, mode2: result.mode2, timestamp: Date.now(), hidden: false, }; if (lb.board.length == 0) { console.log("adding to first position"); lb.board.push(lbitem); retData.insertedAt = 0; } else if (lbitem.wpm < lb.board.slice(-1)[0].wpm) { console.log("adding to the end"); console.log(lb.board.slice(-1)[0].wpm); lb.board.push(lbitem); retData.insertedAt = lb.board.length - 1; } else { console.log("searching for addition spot"); for (i = 0; i < lb.board.length; i++) { //start from top, if item wpm > lb item wpm, insert before it if (lbitem.wpm >= lb.board[i].wpm) { console.log("adding to daily lb position " + i); lb.board.splice(i, 0, lbitem); retData.insertedAt = i; break; } } if (lb.board.length > lb.size) { lb.pop(); } } return lb, retData; } app.post("/attemptAddToLeaderboards", authenticateToken, (req, res) => { const result = req.body.result; let retData = {}; User.findOne({ uid: req.uid }, (err, user) => { admin .auth() .getUser(req.uid) .then((fbUser) => { return fbUser.emailVerified; }) .then((emailVerified) => { if (user.emailVerified === false) { if (emailVerified === true) { user.emailVerified = true; } else { res.status(200).send({ needsToVerifyEmail: true }); return; } } if (user.name === undefined) { //cannot occur since name is required, why is this here? res.status(200).send({ noName: true }); return; } if (user.banned) { res.status(200).send({ banned: true }); return; } /* if (user.verified === false) { res.status(200).send({ needsToVerify: true }); return; }*/ Leaderboard.find( { mode: result.mode, mode2: result.mode2, }, (err, lbs) => { //for all leaderboards queried, determine if it qualifies, and add if it does lbs.forEach((lb) => { if ( lb.board.length == 0 || lb.board.length < lb.size || result.wpm > lb.board.slice(-1)[0].wpm ) { lb, (lbPosData = addToLeaderboard(lb, result, user.name)); //should uid be added instead of name? //or together console.log(user.lbMemory[lb.mode + lb.mode2][lb.type]); //lbPosData.foundAt = user.lbMemory[lb.mode+lb.mode2][lb.type]; retData[lb.type] = lbPosData; lb.save(); user.lbMemory[lb.mode + lb.mode2][lb.type] = retData[lb.type].insertedAt; //check if made global top 10 and send to discord if it did if (lb.type == "global") { let usr = user.discordId != undefined ? user.discordId : user.name; if ( retData.global !== null && retData.global.insertedAt >= 0 && retData.global.insertedAt <= 9 && retData.global.newBest ) { let lbstring = `${result.mode} ${result.mode2} global`; console.log( `sending command to the bot to announce lb update ${usr} ${ retData.global.insertedAt + 1 } ${lbstring} ${result.wpm}` ); announceLbUpdate( usr, retData.global.insertedAt + 1, lbstring, result.wpm, result.rawWpm, result.acc, result.consistency ); } } } }); } ).then((e) => { retData.status = 2; user.save(); res.json(retData); }); }); }); res.status(200); }); app.get("/getLeaderboard/:type/:mode/:mode2", (req, res) => { Leaderboard.findOne( { mode: req.params.mode, mode2: req.params.mode2, type: req.params.type }, (err, lb) => { res.send(lb); } ); }); // BOT API // Might want to move this to a seperate file and add some sort of middleware that can send error if the user is not found app.use(function (e, req, res, next) { console.log("Error", e); return res.status(e.status || 500).json(e || {}); });