leaderboard working but with bugs

This commit is contained in:
lukew3 2021-05-26 14:55:42 -04:00
parent faf10edc2d
commit 82ca581f6d
6 changed files with 192 additions and 363 deletions

View file

@ -0,0 +1,32 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const leaderboardEntrySchema = new Schema({
name: String,
wpm: Number,
raw: Number,
acc: Number,
consistency: Number, //can be null
mode: String, //not sure why mode and mode2 are needed
mode2: Number,
timestamp: Date, //Is this the right type?
hidden: Boolean,
});
const leaderboardSchema = new Schema(
{
resetTime: Date, //or Number, only on daily lb
size: Number,
board: [{ type: leaderboardEntrySchema }], //contents of leaderbaord
mode: String, //only time for now
mode2: Number, //only 15 and 60 for now
type: String, //global or local
},
{
timestamps: true,
}
);
const Leaderboard = mongoose.model("Leaderboard", leaderboardSchema);
module.exports = { Leaderboard };

View file

@ -21,10 +21,14 @@
- Create configSchema
- Figure out if filteredResults.reverse(); in account.js is going to cause efficiency issues
- Could reverse processing of results, but that would add more complexity to code
- In order to transfer users over, users should be able to be validated through firebase until they login again, when they will use their password to login. If firebase confirms that the password and email are valid, the new password will be hashed and saved to the new database
- All data is moved and retrieved via the mongo server, just authentication uses firebase
- Could force users to sign in again immediately in order to transfer users' passwords faster
- Is it worth the inconvenience though.
### leaderboard
- Add boardcleartime
- How will boards be cleared
- Can there be a function that runs outside of requests
- Wait until desired time with setTimeout and then set next timeout
- Identify bugs
## After beta is ready
@ -34,6 +38,19 @@
- Get somebody else to check over security due to my lack of expertise
- Work on transfering data from firebase to mongo
- Make sure that development can be done on mac and windows computers as well
- directories in server.js might cause issues
## User transfer
- Create a script to pull all data from monkeytype and move it to the new mongo server
- In order to transfer users over, users should be able to be validated through firebase until they login again, when they will use their password to login. If firebase confirms that the password and email are valid, the new password will be hashed and saved to the new database
- All data is moved and retrieved via the mongo server, just authentication uses firebase
- Could force users to sign in again immediately in order to transfer users' passwords faster
- Is it worth the inconvenience though.
- Probably the best option would be to have a notification that asks users to log out and log back in again
- Could have a set date that firebase usage will expire and users must log out and back in again before they are forcibly logged out
- Still can't completely remove firebase dependency unless ALL users are transferred
## After release
@ -42,3 +59,6 @@
- Users who have been requested in the last hour will be stored in the redis database so that their data can be sent again without having to search a large database
- After an hour without a new request they can be removed from memory
- User data should not be requested from the server every time a test is submitted, result should just be appended to results
- Create a backup system to prevent loss of data
- Users should be able to export their data themselves
- It's convenient because they would just have to download their user document, only one query for the server

View file

@ -7,6 +7,7 @@ const bcrypt = require("bcrypt");
const saltRounds = 10;
const { User } = require("./models/user");
const { Analytics } = require("./models/analytics");
const { Leaderboard } = require("./models/leaderboard");
// MIDDLEWARE & SETUP
@ -14,16 +15,37 @@ const app = express();
const port = process.env.PORT || "5000";
//let dbConn = mongodb.MongoClient.connect('mongodb://localhost:27017/monkeytype');
mongoose.connect("mongodb://localhost:27017/monkeytype", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const mtRootDir = __dirname.substring(0, __dirname.length - 8);
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());
// Initialize database leaderboards if no leaderboards exist
function createBlankLeaderboards() {
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);
}
Leaderboard.findOne((err, lb) => {
if (lb === null) createBlankLeaderboards();
});
function authenticateToken(req, res, next) {
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
@ -1243,349 +1265,6 @@ app.post("/api/removePreset", authenticateToken, (req, res) => {
res.send({ resultCode: -999 });
}
});
/*
app.post("/api/checkLeaderboards", (req, res) => {
//not sure if the allow origin and options are necessary
res.set("Access-Control-Allow-Origin", origin);
if (req.method === "OPTIONS") {
// Send response to OPTIONS requests
res.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
res.set(
"Access-Control-Allow-Headers",
"Authorization,Content-Type"
);
res.set("Access-Control-Max-Age", "3600");
res.status(204).send("");
return;
}
const request = req.body.data;
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(request);
if (errCount > 0) {
console.error(
`error checking leaderboard for ${
request.uid
} error count ${errCount} - bad input - ${JSON.stringify(request.obj)}`
);
response.status(200).send({
data: {
status: -999,
message: "Bad input",
},
});
return;
}
let emailVerified = await admin
.auth()
.getUser(request.uid)
.then((user) => {
return user.emailVerified;
});
try {
if (emailVerified === false) {
response.status(200).send({
data: {
needsToVerifyEmail: true,
},
});
return;
}
if (request.name === undefined) {
response.status(200).send({
data: {
noName: true,
},
});
return;
}
if (request.banned) {
response.status(200).send({
data: {
banned: true,
},
});
return;
}
if (request.verified === false) {
response.status(200).send({
data: {
needsToVerify: true,
},
});
return;
}
request.result.name = request.name;
if (
request.result.mode === "time" &&
["15", "60"].includes(String(request.result.mode2)) &&
request.result.language === "english" &&
request.result.funbox === "none"
) {
let global = await db
.runTransaction(async (t) => {
const lbdoc = await t.get(
db
.collection("leaderboards")
.where("mode", "==", String(request.result.mode))
.where("mode2", "==", String(request.result.mode2))
.where("type", "==", "global")
);
// let lbData;
let docid = `${String(request.result.mode)}_${String(
request.result.mode2
)}_${"global"}`;
// if (lbdoc.docs.length === 0) {
// console.log(
// `no ${request.mode} ${request.mode2} ${type} leaderboard found - creating`
// );
// let toAdd = {
// size: 20,
// mode: String(request.mode),
// mode2: String(request.mode2),
// type: type,
// };
// t.set(
// db
// .collection("leaderboards")
// .doc(
// `${String(request.mode)}_${String(request.mode2)}_${type}`
// ),
// toAdd
// );
// lbData = toAdd;
// } else {
// lbData = lbdoc.docs[0].data();
// }
let boardInfo = lbdoc.docs[0].data();
if (
boardInfo.minWpm === undefined ||
boardInfo.board.length !== boardInfo.size ||
(boardInfo.minWpm !== undefined &&
request.result.wpm > boardInfo.minWpm &&
boardInfo.board.length === boardInfo.size)
) {
let boardData = boardInfo.board;
let lb = new Leaderboard(
boardInfo.size,
request.result.mode,
request.result.mode2,
boardInfo.type,
boardData
);
let insertResult = lb.insert(request.result);
if (insertResult.insertedAt >= 0) {
t.update(db.collection("leaderboards").doc(docid), {
size: lb.size,
type: lb.type,
board: lb.board,
minWpm: lb.getMinWpm(),
});
}
return insertResult;
} else {
//not above leaderboard minwpm
return {
insertedAt: -1,
};
}
})
.catch((error) => {
console.error(
`error in transaction checking leaderboards - ${error}`
);
response.status(200).send({
data: {
status: -999,
message: error,
},
});
});
let daily = await db
.runTransaction(async (t) => {
const lbdoc = await t.get(
db
.collection("leaderboards")
.where("mode", "==", String(request.result.mode))
.where("mode2", "==", String(request.result.mode2))
.where("type", "==", "daily")
);
// let lbData;
let docid = `${String(request.result.mode)}_${String(
request.result.mode2
)}_${"daily"}`;
// if (lbdoc.docs.length === 0) {
// console.log(
// `no ${request.mode} ${request.mode2} ${type} leaderboard found - creating`
// );
// let toAdd = {
// size: 20,
// mode: String(request.mode),
// mode2: String(request.mode2),
// type: type,
// };
// t.set(
// db
// .collection("leaderboards")
// .doc(
// `${String(request.mode)}_${String(request.mode2)}_${type}`
// ),
// toAdd
// );
// lbData = toAdd;
// } else {
// lbData = lbdoc.docs[0].data();
// }
let boardInfo = lbdoc.docs[0].data();
if (
boardInfo.minWpm === undefined ||
boardInfo.board.length !== boardInfo.size ||
(boardInfo.minWpm !== undefined &&
request.result.wpm > boardInfo.minWpm &&
boardInfo.board.length === boardInfo.size)
) {
let boardData = boardInfo.board;
let lb = new Leaderboard(
boardInfo.size,
request.result.mode,
request.result.mode2,
boardInfo.type,
boardData
);
let insertResult = lb.insert(request.result);
if (insertResult.insertedAt >= 0) {
t.update(db.collection("leaderboards").doc(docid), {
size: lb.size,
type: lb.type,
board: lb.board,
minWpm: lb.getMinWpm(),
});
}
return insertResult;
} else {
//not above leaderboard minwpm
return {
insertedAt: -1,
};
}
})
.catch((error) => {
console.error(
`error in transaction checking leaderboards - ${error}`
);
response.status(200).send({
data: {
status: -999,
message: error,
},
});
});
//send discord update
let usr =
request.discordId != undefined ? request.discordId : request.name;
if (
global !== null &&
global.insertedAt >= 0 &&
global.insertedAt <= 9 &&
global.newBest
) {
let lbstring = `${request.result.mode} ${request.result.mode2} global`;
console.log(
`sending command to the bot to announce lb update ${usr} ${
global.insertedAt + 1
} ${lbstring} ${request.result.wpm}`
);
announceLbUpdate(
usr,
global.insertedAt + 1,
lbstring,
request.result.wpm,
request.result.rawWpm,
request.result.acc,
request.result.consistency
);
}
//
if (
// obj.mode === "time" &&
// (obj.mode2 == "15" || obj.mode2 == "60") &&
// obj.language === "english"
global !== null ||
daily !== null
) {
let updatedLbMemory = await getUpdatedLbMemory(
request.lbMemory,
request.result.mode,
request.result.mode2,
global,
daily
);
db.collection("users").doc(request.uid).update({
lbMemory: updatedLbMemory,
});
}
response.status(200).send({
data: {
status: 2,
daily: daily,
global: global,
},
});
return;
} else {
response.status(200).send({
data: {
status: 1,
daily: {
insertedAt: null,
},
global: {
insertedAt: null,
},
},
});
return;
}
} catch (e) {
console.log(e);
response.status(200).send({
data: {
status: -999,
message: e,
},
});
return;
}
});
*/
function isTagPresetNameValid(name) {
if (name === null || name === undefined || name === "") return false;
@ -1693,6 +1372,99 @@ app.post("/api/resetPersonalBests", authenticateToken, (req, res) => {
res.status(500).send({ status: "Reset Pbs successfully" });
}
});
function addToLeaderboard(lb, result, username) {
retData = {};
//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;
} else {
//don't add new entry if slower than last time
return lb, { insertedAt: -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 || lbitem.wpm < lb.board.slice(-1)[0].wpm) {
lb.board.push(lbitem);
} else {
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 + 1;
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 = {};
//check daily first, if on daily, check global
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, req.name));
retData[lb.type] = lbPosData;
lb.save();
}
});
}
).then((e) => {
retData.status = 2;
res.json(retData);
});
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) => {
if (lb.type == "daily") {
date = new Date();
date.setDate(date.getDate() + 1);
lb.resetTime = date;
}
res.send(lb);
}
);
});
// ANALYTICS API
function newAnalyticsEvent(event, data) {

View file

@ -2,6 +2,7 @@ import * as CloudFunctions from "./cloud-functions";
import * as Loader from "./loader";
import * as Notifications from "./notifications";
import * as DB from "./db";
import axiosInstance from "./axios-instance";
let currentLeaderboard = "time_15";
@ -35,18 +36,12 @@ function update() {
Loader.show();
Promise.all([
CloudFunctions.getLeaderboard({
mode: boardinfo[0],
mode2: boardinfo[1],
type: "daily",
uid: uid,
}),
CloudFunctions.getLeaderboard({
mode: boardinfo[0],
mode2: boardinfo[1],
type: "global",
uid: uid,
}),
axiosInstance.get(
`/api/getLeaderboard/daily/${boardinfo[0]}/${boardinfo[1]}`
),
axiosInstance.get(
`/api/getLeaderboard/global/${boardinfo[0]}/${boardinfo[1]}`
),
])
.then((lbdata) => {
Loader.hide();
@ -92,7 +87,7 @@ function update() {
dailyData.board.forEach((entry) => {
if (entry.hidden) return;
let meClassString = "";
if (entry.currentUser) {
if (entry.name == DB.currentUser().displayName) {
meClassString = ' class="me"';
$("#leaderboardsWrapper table.daily tfoot").html(`
<tr>
@ -175,7 +170,7 @@ function update() {
globalData.board.forEach((entry) => {
if (entry.hidden) return;
let meClassString = "";
if (entry.currentUser) {
if (entry.name == DB.currentUser().displayName) {
meClassString = ' class="me"';
$("#leaderboardsWrapper table.global tfoot").html(`
<tr>

View file

@ -3,6 +3,7 @@ import * as DB from "./db";
import * as Notifications from "./notifications";
import Config from "./config";
import * as Misc from "./misc";
import axiosInstance from "./axios-instance";
let textTimeouts = [];
@ -131,6 +132,7 @@ export function show(data, mode2) {
}
export function check(completedEvent) {
console.log("starting lb checking");
try {
if (
completedEvent.funbox === "none" &&
@ -160,6 +162,7 @@ export function check(completedEvent) {
delete lbRes.keySpacing;
delete lbRes.keyDuration;
delete lbRes.chartData;
/*
CloudFunctions.checkLeaderboards({
uid: completedEvent.uid,
lbMemory: DB.getSnapshot().lbMemory,
@ -170,6 +173,12 @@ export function check(completedEvent) {
discordId: DB.getSnapshot().discordId,
result: lbRes,
})
*/
axiosInstance
.post("/api/attemptAddToLeaderboards", {
//user data can be retrieved from the database
result: lbRes,
})
.then((data) => {
Misc.clearTimeouts(textTimeouts);
show(data.data, completedEvent.mode2);

View file

@ -1545,6 +1545,7 @@ export function finish(difficultyFailed = false) {
obj: completedEvent,
})
.then((e) => {
e = e.data;
//return a result message that will be shown if there was an error
AccountButton.loading(false);
if (e.data == null) {