monkeytype/backend/server.js

1436 lines
41 KiB
JavaScript
Raw Normal View History

const express = require("express");
const bodyParser = require("body-parser");
const mongoose = require("mongoose");
const cors = require("cors");
const admin = require("firebase-admin");
const { User } = require("./models/user");
2021-05-27 02:55:42 +08:00
const { Leaderboard } = require("./models/leaderboard");
// Firebase admin setup
//currently uses account key in functions to prevent repetition
const serviceAccount = require("../functions/serviceAccountKey.json");
2021-05-20 06:39:18 +08:00
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
// MIDDLEWARE & SETUP
const app = express();
app.use(cors());
const port = process.env.PORT || "5005";
mongoose.connect("mongodb://localhost:27017/monkeytype", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
2021-05-27 02:55:42 +08:00
const mtRootDir = __dirname.substring(0, __dirname.length - 8); //will this work for windows and mac computers?
app.use(express.static(mtRootDir + "/dist"));
app.use(bodyParser.json());
// 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) => {
lb.board = [];
lb.save();
});
});
clearDailyLeaderboards();
}, nextClear.getTime() - currentTime.getTime());
}
// Initialize database leaderboards if no leaderboards exist and start clearDailyLeaderboards
2021-05-27 02:55:42 +08:00
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();
2021-05-27 02:55:42 +08:00
});
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();
}
}
2021-05-20 06:39:18 +08:00
// NON-ROUTE FUNCTIONS
2021-05-15 10:45:55 +08:00
function validateResult(result) {
if (result.wpm > result.rawWpm) {
console.error(
`Could not validate result for ${result.uid}. ${result.wpm} > ${result.rawWpm}`
);
return false;
}
let wpm = roundTo2((result.correctChars * (60 / result.testDuration)) / 5);
if (
wpm < result.wpm - result.wpm * 0.01 ||
wpm > result.wpm + result.wpm * 0.01
) {
console.error(
`Could not validate result for ${result.uid}. wpm ${wpm} != ${result.wpm}`
);
return false;
}
// if (result.allChars != undefined) {
// let raw = roundTo2((result.allChars * (60 / result.testDuration)) / 5);
// if (
// raw < result.rawWpm - result.rawWpm * 0.01 ||
// raw > result.rawWpm + result.rawWpm * 0.01
// ) {
// console.error(
// `Could not validate result for ${result.uid}. raw ${raw} != ${result.rawWpm}`
// );
// return false;
// }
// }
if (result.mode === "time" && (result.mode2 === 15 || result.mode2 === 60)) {
let keyPressTimeSum =
result.keySpacing.reduce((total, val) => {
return total + val;
}) / 1000;
if (
keyPressTimeSum < result.testDuration - 1 ||
keyPressTimeSum > result.testDuration + 1
) {
console.error(
`Could not validate key spacing sum for ${result.uid}. ${keyPressTimeSum} !~ ${result.testDuration}`
);
return false;
}
if (
result.testDuration < result.mode2 - 1 ||
result.testDuration > result.mode2 + 1
) {
console.error(
`Could not validate test duration for ${result.uid}. ${result.testDuration} !~ ${result.mode2}`
);
return false;
}
}
if (result.chartData.raw !== undefined) {
if (result.chartData.raw.filter((w) => w > 350).length > 0) return false;
}
if (result.wpm > 100 && result.consistency < 10) return false;
return true;
}
function roundTo2(num) {
return Math.round((num + Number.EPSILON) * 100) / 100;
}
async function checkIfPB(obj, userdata) {
2021-05-15 10:45:55 +08:00
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) => {
2021-05-15 10:45:55 +08:00
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) {
2021-05-26 04:28:11 +08:00
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,
},
];
2021-05-15 10:45:55 +08:00
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) => {
2021-05-15 10:45:55 +08:00
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) {
2021-06-02 03:08:59 +08:00
//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
2021-06-02 03:08:59 +08:00
//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,
},
2021-06-02 03:08:59 +08:00
],
},
};
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) {
2021-06-02 03:08:59 +08:00
//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");
2021-06-02 03:08:59 +08:00
//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) {
2021-06-02 03:08:59 +08:00
//push working pb array to user tags pbs
await User.findOne({ uid: userdata.uid }, (err, user) => {
//it might be more convenient if tags was an object with ids as the keys
for (let j = 0; j < user.tags.length; j++) {
if (user.tags[j]._id.toString() === dbtags[i]._id.toString()) {
2021-06-02 03:08:59 +08:00
user.tags[j].personalBests = pbs;
}
}
user.save();
});
ret.push(dbtags[i]._id.toString());
}
}
console.log(ret);
return ret;
2021-05-15 10:45:55 +08:00
}
async function stripAndSave(uid, obj) {
2021-05-15 10:45:55 +08:00
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;
2021-05-20 04:22:18 +08:00
//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;
2021-05-15 10:45:55 +08:00
if (obj.numbers === false) delete obj.numbers;
if (obj.punctuation === false) delete obj.punctuation;
await User.findOne({ uid: uid }, (err, user) => {
2021-05-15 10:45:55 +08:00
user.results.push(obj);
user.save();
});
}
function incrementT60Bananas(uid, result, userData) {
2021-05-20 06:39:18 +08:00
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");
return;
} else {
//increment
// console.log("checking");
User.findOne({ uid: uid }, (err, user) => {
2021-05-20 06:39:18 +08:00
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
);
}
}
2021-05-20 06:25:29 +08:00
async function incrementGlobalTypingStats(userData, resultObj) {
2021-05-20 05:07:41 +08:00
let userGlobalStats = userData.globalStats;
2021-05-15 10:45:55 +08:00
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
)}`
);
2021-05-20 05:07:41 +08:00
if (userGlobalStats.started === undefined) {
2021-05-15 10:45:55 +08:00
newStarted = resultObj.restartCount + 1;
} else {
2021-05-20 05:07:41 +08:00
newStarted = userGlobalStats.started + resultObj.restartCount + 1;
2021-05-15 10:45:55 +08:00
}
2021-05-20 05:07:41 +08:00
if (userGlobalStats.completed === undefined) {
2021-05-15 10:45:55 +08:00
newCompleted = 1;
} else {
2021-05-20 05:07:41 +08:00
newCompleted = userGlobalStats.completed + 1;
2021-05-15 10:45:55 +08:00
}
2021-05-20 05:07:41 +08:00
if (userGlobalStats.time === undefined) {
2021-05-15 10:45:55 +08:00
newTime = tt;
} else {
2021-05-20 05:07:41 +08:00
newTime = userGlobalStats.time + tt;
2021-05-15 10:45:55 +08:00
}
// db.collection("users")
// .doc(uid)
// .update({
// startedTests: newStarted,
// completedTests: newCompleted,
// timeTyping: roundTo2(newTime),
// });
incrementPublicTypingStats(resultObj.restartCount + 1, 1, tt);
User.findOne({ uid: userData.uid }, (err, user) => {
2021-05-20 06:25:29 +08:00
user.globalStats = {
started: newStarted,
completed: newCompleted,
time: roundTo2(newTime),
};
user.save();
});
2021-05-15 10:45:55 +08:00
} catch (e) {
console.error(`Error while incrementing stats for user ${uid}: ${e}`);
}
}
async function incrementPublicTypingStats(started, completed, time) {
/*
try {
time = roundTo2(time);
db.collection("public")
.doc("stats")
.update({
completedTests: admin.firestore.FieldValue.increment(completed),
startedTests: admin.firestore.FieldValue.increment(started),
timeTyping: admin.firestore.FieldValue.increment(time),
});
} catch (e) {
console.error(`Error while incrementing public stats: ${e}`);
}
*/
}
2021-05-26 03:25:02 +08:00
function isTagPresetNameValid(name) {
if (name === null || name === undefined || name === "") return false;
if (name.length > 16) return false;
return /^[0-9a-zA-Z_.-]+$/.test(name);
}
function isUsernameValid(name) {
if (name === null || name === undefined || name === "") return false;
if (/miodec/.test(name.toLowerCase())) return false;
if (/bitly/.test(name.toLowerCase())) return false;
if (name.length > 14) return false;
if (/^\..*/.test(name.toLowerCase())) return false;
return /^[0-9a-zA-Z_.-]+$/.test(name);
2021-05-27 23:08:40 +08:00
}
// API
2021-05-20 06:39:18 +08:00
app.get("/api/nameCheck/:name", (req, res) => {
if (!isUsernameValid(req.params.name)) {
res.status(200).send({
resultCode: -2,
message: "Username is not valid",
});
return;
}
User.findOne({ name: req.params.name }, (err, user) => {
if (user) {
res.status(200).send({
resultCode: -1,
message: "Username is taken",
});
return;
} else {
res.status(200).send({
resultCode: 1,
message: "Username is available",
});
2021-05-27 23:08:40 +08:00
return;
2021-05-20 06:39:18 +08:00
}
});
});
app.post("/api/signUp", (req, res) => {
const newuser = new User({
name: req.body.name,
email: req.body.email,
uid: req.body.uid,
2021-05-20 06:39:18 +08:00
});
newuser.save();
res.status(200);
res.json({ user: newuser });
return;
2021-05-20 06:39:18 +08:00
});
app.post("/api/updateName", (req, res) => {
//this might be a put/patch request
//update the name of user with given uid
const uid = req.body.uid;
const name = req.body.name;
2021-05-20 06:39:18 +08:00
});
app.post("/api/passwordReset", (req, res) => {
const email = req.body.email;
//send email to the passed email requesting password reset
res.sendStatus(200);
});
app.get("/api/fetchSnapshot", authenticateToken, (req, res) => {
/* Takes token and returns snap */
console.log("UID: " + req.uid);
User.findOne({ uid: req.uid }, (err, user) => {
2021-05-20 06:39:18 +08:00
if (err) res.status(500).send({ error: err });
if (!user) res.status(200).send({ message: "No user found" }); //client doesn't do anything with this
2021-05-20 06:39:18 +08:00
//populate snap object with data from user document
let snap = user;
//return user data
res.send({ snap: snap });
return;
2021-05-20 06:39:18 +08:00
});
});
2021-05-22 23:15:58 +08:00
function stdDev(array) {
const n = array.length;
const mean = array.reduce((a, b) => a + b) / n;
return Math.sqrt(
array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n
);
}
app.post("/api/testCompleted", authenticateToken, (req, res) => {
User.findOne({ uid: req.uid }, (err, user) => {
if (err) res.status(500).send({ error: err });
2021-05-15 10:45:55 +08:00
request = req.body;
if (request === undefined) {
res.status(200).send({ resultCode: -999 });
2021-05-15 10:45:55 +08:00
return;
}
try {
if (req.uid === undefined || request.obj === undefined) {
2021-05-15 10:45:55 +08:00
console.error(`error saving result for - missing input`);
res.status(200).send({ resultCode: -999 });
2021-05-15 10:45:55 +08:00
return;
}
2021-05-15 10:45:55 +08:00
let obj = request.obj;
if (obj.incompleteTestSeconds > 500)
console.log(
`FUCK, HIGH INCOMPLETE TEST SECONDS ${req.uid}: ${JSON.stringify(
2021-05-15 10:45:55 +08:00
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
2021-05-15 10:45:55 +08:00
} error count ${errCount} - bad input - ${JSON.stringify(
request.obj
)}`
);
res.status(200).send({ resultCode: -1 });
2021-05-15 10:45:55 +08:00
return;
}
if (
obj.wpm <= 0 ||
obj.wpm > 350 ||
obj.acc < 50 ||
obj.acc > 100 ||
obj.consistency > 100
) {
res.status(200).send({ resultCode: -1 });
2021-05-15 10:45:55 +08:00
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" });
2021-05-15 10:45:55 +08:00
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 });
2021-05-15 10:45:55 +08:00
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}`
2021-05-15 10:45:55 +08:00
);
}
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)
2021-05-15 10:45:55 +08:00
// .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(
2021-05-15 10:45:55 +08:00
keySpacing
)} duration ${JSON.stringify(keyDuration)}`
);
res.status(200).send({ resultCode: -2 });
2021-05-15 10:45:55 +08:00
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(
2021-05-15 10:45:55 +08:00
keySpacing
)} duration ${JSON.stringify(keyDuration)}`
);
}
} else {
res.status(200).send({ resultCode: -3 });
2021-05-15 10:45:55 +08:00
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`)
2021-05-15 10:45:55 +08:00
// .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),
2021-05-15 10:45:55 +08:00
])
.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);
2021-05-15 10:45:55 +08:00
}
2021-05-20 06:25:29 +08:00
await incrementGlobalTypingStats(userdata, obj);
2021-05-15 10:45:55 +08:00
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)}`
2021-05-15 10:45:55 +08:00
);
2021-05-20 06:25:29 +08:00
/*
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;
2021-05-15 10:45:55 +08:00
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}`
2021-05-15 10:45:55 +08:00
);
updateDiscordRole(userdata.discordId, Math.round(obj.wpm));
}
}
returnobj.resultCode = 2;
} else {
let logobj = request.obj;
logobj.keySpacing = "removed";
logobj.keyDuration = "removed";
2021-05-20 06:25:29 +08:00
request.obj.isPb = false;
2021-05-15 10:45:55 +08:00
console.log(
`saved result for ${req.uid} - ${JSON.stringify(logobj)}`
2021-05-15 10:45:55 +08:00
);
returnobj.resultCode = 1;
}
stripAndSave(req.uid, request.obj);
res.status(200).send(returnobj);
2021-05-15 10:45:55 +08:00
return;
})
.catch((e) => {
console.error(
`error saving result when checking for PB / checking leaderboards for ${req.uid} - ${e.message}`
2021-05-15 10:45:55 +08:00
);
res
.status(200)
.send({ data: { resultCode: -999, message: e.message } });
return;
});
} catch (e) {
console.error(
`error saving result for ${req.uid} - ${JSON.stringify(
2021-05-15 10:45:55 +08:00
request.obj
)} - ${e}`
);
res.status(200).send({ resultCode: -999, message: e.message });
2021-05-15 10:45:55 +08:00
return;
}
});
});
2021-05-15 04:16:06 +08:00
app.get("/api/userResults", authenticateToken, (req, res) => {
User.findOne({ uid: req.uid }, (err, user) => {
2021-05-15 04:16:06 +08:00
if (err) res.status(500).send({ error: err });
});
//return list of results
res.sendStatus(200);
});
2021-05-22 21:56:51 +08:00
function isConfigKeyValid(name) {
if (name === null || name === undefined || name === "") return false;
if (name.length > 30) return false;
return /^[0-9a-zA-Z_.\-#+]+$/.test(name);
}
app.post("/api/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({
2021-05-22 21:56:51 +08:00
resultCode: -1,
message: "Missing input",
});
2021-05-22 21:56:51 +08:00
}
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(
2021-05-22 21:56:51 +08:00
request.obj
)}`
);
res.send({
2021-05-22 21:56:51 +08:00
resultCode: -1,
message: "Bad input. " + errorMessage,
});
2021-05-22 21:56:51 +08:00
}
User.findOne({ uid: req.uid }, (err, user) => {
2021-05-22 21:56:51 +08:00
if (err) res.status(500).send({ error: err });
user.config = obj;
//what does {merge: true} do in firebase
user.save();
})
.then(() => {
res.send({
2021-05-22 21:56:51 +08:00
resultCode: 1,
message: "Saved",
});
2021-05-22 21:56:51 +08:00
})
.catch((e) => {
console.error(
`error saving config to DB for ${req.uid} - ${e.message}`
2021-05-22 21:56:51 +08:00
);
res.send({
2021-05-22 21:56:51 +08:00
resultCode: -1,
message: e.message,
});
2021-05-22 21:56:51 +08:00
});
} catch (e) {
console.error(`error saving config for ${req.uid} - ${e}`);
res.send({
2021-05-22 21:56:51 +08:00
resultCode: -999,
message: e,
});
2021-05-22 21:56:51 +08:00
}
});
2021-05-26 03:25:02 +08:00
app.post("/api/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`);
2021-05-26 03:25:02 +08:00
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(
2021-05-26 03:25:02 +08:00
req.body.obj
)}`
);
res.json({
resultCode: -1,
message: "Bad input. " + errorMessage,
});
}
User.findOne({ uid: req.uid }, (err, user) => {
2021-05-26 03:25:02 +08:00
if (user.presets.length >= 10) {
res.json({
resultCode: -2,
message: "Preset limit",
});
} else {
user.presets.push(req.body.obj);
user.save();
}
})
.then((updatedUser) => {
2021-06-02 03:08:59 +08:00
User.findOne({ uid: req.uid }, (err, user) => {
res.json({
resultCode: 1,
message: "Saved",
id: user.presets[user.presets.length - 1]._id,
});
2021-05-26 03:25:02 +08:00
});
})
.catch((e) => {
console.error(
`error adding preset to DB for ${req.uid} - ${e.message}`
2021-05-26 03:25:02 +08:00
);
res.json({
resultCode: -1,
message: e.message,
});
});
}
} catch (e) {
console.error(`error adding preset for ${req.uid} - ${e}`);
2021-05-26 03:25:02 +08:00
res.json({
resultCode: -999,
message: e,
});
}
});
app.post("/api/editPreset", authenticateToken, (req, res) => {
try {
if (!isTagPresetNameValid(req.body.presetName)) {
2021-06-02 03:08:59 +08:00
res.json({ resultCode: -1 });
2021-05-26 03:25:02 +08:00
} else {
User.findOne({ uid: req.uid }, (err, user) => {
2021-05-26 03:25:02 +08:00
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}`
2021-05-26 03:25:02 +08:00
);
2021-06-02 03:08:59 +08:00
res.json({
2021-05-26 03:25:02 +08:00
resultCode: 1,
});
})
.catch((e) => {
console.error(
`error while updating preset for user ${req.uid}: ${e.message}`
2021-05-26 03:25:02 +08:00
);
2021-06-02 03:08:59 +08:00
res.json({ resultCode: -999, message: e.message });
2021-05-26 03:25:02 +08:00
});
}
} catch (e) {
console.error(`error updating preset for ${req.uid} - ${e}`);
2021-06-02 03:08:59 +08:00
res.json({ resultCode: -999, message: e.message });
2021-05-26 03:25:02 +08:00
}
});
app.post("/api/removePreset", authenticateToken, (req, res) => {
try {
User.findOne({ uid: req.uid }, (err, user) => {
2021-05-26 03:25:02 +08:00
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`);
2021-05-26 03:25:02 +08:00
res.send({ resultCode: 1 });
})
.catch((e) => {
console.error(
`error deleting preset for user ${req.uid}: ${e.message}`
2021-05-26 03:25:02 +08:00
);
res.send({ resultCode: -999 });
});
} catch (e) {
console.error(`error deleting preset for ${req.uid} - ${e}`);
2021-05-26 03:25:02 +08:00
res.send({ resultCode: -999 });
}
});
function isTagPresetNameValid(name) {
if (name === null || name === undefined || name === "") return false;
if (name.length > 16) return false;
return /^[0-9a-zA-Z_.-]+$/.test(name);
}
//could use /api/tags/add instead
app.post("/api/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("/api/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("/api/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("/api/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" });
}
});
2021-05-27 02:55:42 +08:00
function addToLeaderboard(lb, result, username) {
//insertedAt is index of array inserted position, 1 is added after
retData = { insertedAt: -1 };
2021-05-27 02:55:42 +08:00
//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;
2021-05-27 02:55:42 +08:00
} else {
//don't add new entry if slower than last time
return lb, { insertedAt: -1, foundAt: i + 1 };
2021-05-27 02:55:42 +08:00
}
}
}
//when is newBest not true?
retData.newBest = true;
if (!retData.foundAt) retData.foundAt = -1;
2021-05-27 02:55:42 +08:00
//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");
2021-05-27 02:55:42 +08:00
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;
2021-05-27 02:55:42 +08:00
} else {
console.log("searching for addition spot");
2021-05-27 02:55:42 +08:00
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;
2021-05-27 02:55:42 +08:00
break;
}
}
if (lb.board.length > lb.size) {
lb.pop();
}
}
return lb, retData;
}
app.post("/api/attemptAddToLeaderboards", authenticateToken, (req, res) => {
const result = req.body.result;
let retData = {};
User.findOne({ uid: req.uid }, (err, user) => {
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?
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;
}
});
}
).then((e) => {
retData.status = 2;
user.save();
res.json(retData);
});
2021-05-27 02:55:42 +08:00
});
res.status(200);
});
app.get("/api/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);
}
);
});
// LISTENER
app.listen(port, () => {
console.log(`Listening to requests on http://localhost:${port}`);
});