Emptied cloudfunctions and functions/index.js, added stats collection

This commit is contained in:
lukew3 2021-06-03 22:45:30 -04:00
parent 047436311a
commit d140ba2f63
17 changed files with 429 additions and 3147 deletions

View file

@ -2,6 +2,8 @@ const admin = require("firebase-admin");
const mongoose = require("mongoose");
const { User } = require("./models/user");
const { Leaderboard } = require("./models/leaderboard");
const { Stats } = require("./models/stats");
const { BotCommand } = require("./models/bot-command");
const serviceAccount = require("../functions/serviceAccountKey.json");
@ -18,6 +20,8 @@ mongoose.connect("mongodb://localhost:27017/monkeytype", {
useUnifiedTopology: true,
});
// Database should be completely clear before this is ran in order to prevent overlapping documents
// Migrate users
userCount = 1;
db.collection("users")
.get()
@ -74,8 +78,27 @@ db.collection("leaderboards")
.get()
.then((leaderboardsSnapshot) => {
leaderboardsSnapshot.forEach((lbDoc) => {
console.log(lbDoc);
let newLb = new Leaderboard(lbDoc);
newLb.save();
});
});
//migrate bot-commands
db.collection("bot-commands")
.get()
.then((botCommandsSnapshot) => {
botCommandsSnapshot.forEach((bcDoc) => {
let newBotCommand = new BotCommand(botCommandDoc);
newBotCommand.save();
});
});
//migrate public stats
db.collection("public")
.doc("stats")
.get()
.then((ret) => {
let stats = ret.data();
let newStats = new Stats(stats);
newStats.save();
});

17
backend/models/stats.js Normal file
View file

@ -0,0 +1,17 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const statsSchema = new Schema(
{
completedTests: { type: Number, default: 0 },
startedTests: { type: Number, default: 0 },
timeTyping: { type: Number, default: 0 },
},
{
timestamps: true,
}
);
const Stats = mongoose.model("Stats", statsSchema);
module.exports = { Stats };

View file

@ -2,16 +2,17 @@
## Todo
Make sure that the branch is ready for deployment
- Add deploy script(s) to package.json
- Create a plan for apache/nginx server
- Api should be accessible via api.monkeytype.com
- Make sure that the branch is ready for deployment
- Make sure that the bot can interact with the data on the express server
- Would be optimal if the bot were to run on the same server as the express server, so that the bot wouldn't have to access data through api routes
- Determine if generatePairingCode should be removed or migrated
- This function was commented out in index.js but is used in frontend
## Bugs
- Make sure that the bot is able to interact with the mongo database
- If bot is on same server, it could work with mongo directly, otherwise, more api routes are needed
- Do names have to be made lowercase before checking if a duplicate name is found?(that is when a new user is created or username is changed)
### Minor/efficiency bugs
@ -34,6 +35,17 @@ Make sure that the branch is ready for deployment
- Is this discord verified, if so, why do you need discord verified to be on leaderboard?
- Temporarily removed from leaderboard requirements
### Functions not found anywhere except for index.js
Might need to be migrated, might not. I'm not sure why these are in the file if they are not being used.
- getAllNames
- getAllUsers
- getPatreons
- requestTest
- incrementStartedTestCounter
- incrementTestCounter
### Possibilities
- Might be worthwhile to use redis to store userdata up to a certain point
@ -42,3 +54,4 @@ Make sure that the branch is ready for deployment
- Create a backup system to prevent loss of data
- Users should be able to export their data themselves
- Pretty much is just the user snap but without uid
- Could split server.js into multiple files for easier code management

View file

@ -7,6 +7,7 @@ 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
@ -93,6 +94,18 @@ Leaderboard.findOne((err, lb) => {
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
@ -525,7 +538,7 @@ function incrementT60Bananas(uid, result, userData) {
}
}
async function incrementGlobalTypingStats(userData, resultObj) {
async function incrementUserGlobalTypingStats(userData, resultObj) {
let userGlobalStats = userData.globalStats;
try {
let newStarted;
@ -561,13 +574,6 @@ async function incrementGlobalTypingStats(userData, resultObj) {
} else {
newTime = userGlobalStats.time + tt;
}
// 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) => {
user.globalStats = {
@ -583,20 +589,17 @@ async function incrementGlobalTypingStats(userData, resultObj) {
}
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),
});
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}`);
}
*/
}
function isTagPresetNameValid(name) {
@ -660,11 +663,21 @@ app.post("/signUp", (req, res) => {
});
app.post("/updateName", authenticateToken, (req, res) => {
User.findOne({ uid: req.uid }, (err, user) => {
user.name = req.body.name;
user.save();
});
res.status(200);
if (isUsernameValid(name)) {
User.findOne({ uid: req.uid }, (err, user) => {
User.findOne({ name: req.body.name }, (err2, user2) => {
if (!user2) {
user.name = req.body.name;
user.save();
res.status(200).send({ status: 1 });
} else {
res.status(200).send({ status: -1, message: "Username taken" });
}
});
});
} else {
res.status(200).send({ status: -1, message: "Username invalid" });
}
});
app.get("/fetchSnapshot", authenticateToken, (req, res) => {
@ -927,7 +940,7 @@ app.post("/testCompleted", authenticateToken, (req, res) => {
incrementT60Bananas(req.uid, obj, userdata);
}
await incrementGlobalTypingStats(userdata, obj);
await incrementUserGlobalTypingStats(userdata, obj); //equivalent to getIncrementedTypingStats
let returnobj = {
resultCode: null,
@ -1007,11 +1020,150 @@ app.post("/testCompleted", authenticateToken, (req, res) => {
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 });
});
//return list of 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,
});
return;
});
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",
});
return;
})
.catch((e) => {
res.status(200).send({
status: -999,
message: e.message,
});
return;
});
} catch (e) {
res.status(200).send({
status: -999,
message: e,
});
return;
}
});
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,
});
}
});
function isConfigKeyValid(name) {
if (name === null || name === undefined || name === "") return false;
if (name.length > 30) return false;

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,6 @@ import * as Misc from "./misc";
import * as Settings from "./settings";
import * as ChallengeController from "./challenge-controller";
import Config from "./config";
import * as CloudFunctions from "./cloud-functions";
import * as AllTimeStats from "./all-time-stats";
import * as DB from "./db";
import * as TestLogic from "./test-logic";
@ -193,11 +192,15 @@ function signUp() {
});
if (TestLogic.notSignedInLastResult !== null) {
TestLogic.setNotSignedInUid(usr.uid);
CloudFunctions.testCompleted({
uid: usr.uid,
obj: TestLogic.notSignedInLastResult,
});
DB.getSnapshot().results.push(TestLogic.notSignedInLastResult);
axiosInstance
.post("/testCompleted", {
obj: TestLogic.notSignedInLastResult,
})
.then(() => {
DB.getSnapshot().results.push(
TestLogic.notSignedInLastResult
);
});
}
UI.changePage("account");
usr.sendEmailVerification();

View file

@ -1,6 +1,5 @@
import * as DB from "./db";
import * as Misc from "./misc";
import * as CloudFunctions from "./cloud-functions";
import * as Notifications from "./notifications";
import * as ResultFilters from "./result-filters";
import * as ThemeColors from "./theme-colors";
@ -18,6 +17,7 @@ import * as Settings from "./settings";
import * as ThemePicker from "./theme-picker";
import * as AllTimeStats from "./all-time-stats";
import * as PbTables from "./pb-tables";
import axiosInstance from "./axios-instance";
export function getDataAndInit() {
DB.initSnapshot()
@ -27,7 +27,7 @@ export function getDataAndInit() {
if (snap === null) {
throw "Missing db snapshot. Client likely could not connect to the backend.";
}
let user = firebase.auth().currentUser; // I think that this should be stored in cookie
let user = firebase.auth().currentUser;
if (snap.name === undefined) {
//verify username
if (Misc.isUsernameValid(user.name)) {
@ -49,21 +49,23 @@ export function getDataAndInit() {
promptVal = prompt(
"Your name is either invalid or unavailable (you also need to do this if you used Google Sign Up). Please provide a new display name (cannot be longer than 14 characters, can only contain letters, numbers, underscores, dots and dashes):"
);
cdnVal = await CloudFunctions.changename({
uid: user.uid,
name: promptVal,
});
if (cdnVal.data.status === 1) {
alert("Name updated", 1);
location.reload();
} else if (cdnVal.data.status < 0) {
alert(cdnVal.data.message, 0);
}
axiosInstance
.post("/updateName", {
name: promptVal,
})
.then((cdnVal) => {
if (cdnVal.data.status === 1) {
alert("Name updated", 1);
location.reload();
} else if (cdnVal.data.status < 0) {
alert(cdnVal.data.message, 0);
}
});
}
}
}
if (snap.refactored === false) {
CloudFunctions.removeSmallTests({ uid: user.uid });
axiosInstance.post("/removeSmallTestsAndQPB");
}
if (!UpdateConfig.changedBeforeDb) {
if (Config.localStorageConfig === null) {

View file

@ -1,4 +1,3 @@
import * as CloudFunctions from "./cloud-functions";
import * as Notifications from "./notifications";
import * as Settings from "./settings";
import * as DB from "./db";

View file

@ -1,73 +1,3 @@
// all functions here should be changed into api calls
//export function testCompleted = axios.post('/testCompleted', )firebase
// .functions()
// .httpsCallable("testCompleted");
import axiosInstance from "./axios-instance";
export function testCompleted(input) {
console.log("testCompleted");
}
export function addTag(input) {
console.log("request data here");
}
export function editTag(input) {
console.log("request data here");
}
export function removeTag(input) {
console.log("request data here");
}
export function updateResultTags(input) {
console.log("request data here");
}
export function saveConfig(input) {
console.log("request data here");
}
export function addPreset(input) {
console.log("request data here");
}
export function editPreset(input) {
console.log("request data here");
}
export function removePreset(input) {
console.log("request data here");
}
export function generatePairingCode(input) {
console.log("request data here");
}
export function saveLbMemory(input) {
console.log("request data here");
}
export function unlinkDiscord(input) {
console.log("request data here");
}
export function verifyUser(input) {
console.log("request data here");
}
export function reserveName(input) {
console.log("request data here");
}
export function updateEmail(input) {
console.log("request data here");
}
export function namecheck(input) {
console.log("request data here");
}
export function getLeaderboard(input) {
console.log("request data here");
}
export function clearTagPb(input) {
console.log("request data here");
}
export function changename(input) {
console.log("request data here");
}
export function removeSmallTests(input) {
console.log("request data here");
}
export function resetPersonalBests(input) {
console.log("request data here");
}
export function checkLeaderboards(input) {
console.log("request data here");
}

View file

@ -48,9 +48,7 @@ export async function getUserResults() {
return true;
} else {
axiosInstance
.get("/userResults", {
uid: user.uid,
})
.get("/userResults")
.then((response) => {
dbSnapshot.results = response.data.results;
})

View file

@ -1,6 +1,5 @@
import * as Loader from "./loader";
import * as DB from "./db";
import * as CloudFunctions from "./cloud-functions";
import * as Notifications from "./notifications";
import * as Settings from "./settings";
import * as Config from "./config";

View file

@ -1,7 +1,7 @@
import * as DB from "./db";
import * as Loader from "./loader";
import * as CloudFunctions from "./cloud-functions";
import * as Notifications from "./notifications";
import axiosInstance from "./axios-instance";
function show() {
if ($("#resultEditTagsPanelWrapper").hasClass("hidden")) {
@ -85,65 +85,66 @@ $("#resultEditTagsPanel .confirmButton").click((e) => {
});
Loader.show();
hide();
CloudFunctions.updateResultTags({
uid: firebase.auth().currentUser.uid,
tags: newtags,
resultid: resultid,
}).then((r) => {
Loader.hide();
if (r.data.resultCode === 1) {
Notifications.add("Tags updated.", 1, 2);
DB.getSnapshot().results.forEach((result) => {
if (result.id === resultid) {
result.tags = newtags;
}
});
let tagNames = "";
if (newtags.length > 0) {
newtags.forEach((tag) => {
DB.getSnapshot().tags.forEach((snaptag) => {
if (tag === snaptag._id) {
tagNames += snaptag.name + ", ";
}
});
axiosInstance
.post("/updateResultTags", {
tags: newtags,
resultid: resultid,
})
.then((r) => {
Loader.hide();
if (r.data.resultCode === 1) {
Notifications.add("Tags updated.", 1, 2);
DB.getSnapshot().results.forEach((result) => {
if (result.id === resultid) {
result.tags = newtags;
}
});
tagNames = tagNames.substring(0, tagNames.length - 2);
}
let restags;
if (newtags === undefined) {
restags = "[]";
} else {
restags = JSON.stringify(newtags);
}
let tagNames = "";
if (newtags.length > 0) {
newtags.forEach((tag) => {
DB.getSnapshot().tags.forEach((snaptag) => {
if (tag === snaptag._id) {
tagNames += snaptag.name + ", ";
}
});
});
tagNames = tagNames.substring(0, tagNames.length - 2);
}
let restags;
if (newtags === undefined) {
restags = "[]";
} else {
restags = JSON.stringify(newtags);
}
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).attr(
"tags",
restags
);
if (newtags.length > 0) {
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).css(
"opacity",
1
);
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).attr(
"aria-label",
tagNames
"tags",
restags
);
if (newtags.length > 0) {
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).css(
"opacity",
1
);
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).attr(
"aria-label",
tagNames
);
} else {
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).css(
"opacity",
0.25
);
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).attr(
"aria-label",
"no tags"
);
}
} else {
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).css(
"opacity",
0.25
);
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).attr(
"aria-label",
"no tags"
);
Notifications.add("Error updating tags: " + r.data.message, -1);
}
} else {
Notifications.add("Error updating tags: " + r.data.message, -1);
}
});
});
});

View file

@ -6,7 +6,6 @@ import * as Settings from "./settings";
import * as RouteController from "./route-controller";
import * as UI from "./ui";
import * as SignOutButton from "./sign-out-button";
import * as AccountController from "./account-controller";
ManualRestart.set();
Misc.migrateFromCookies();
@ -59,5 +58,4 @@ $(document).ready(() => {
}
});
Settings.settingsFillPromise.then(Settings.update);
let user = firebase.auth().currentUser;
});

View file

@ -8,6 +8,7 @@ import * as Notifications from "./notifications";
import * as DB from "./db";
import * as Loader from "./loader";
import * as CloudFunctions from "./cloud-functions";
import axiosInstance from "./axios-instance";
import * as Funbox from "./funbox";
import * as TagController from "./tag-controller";
import * as PresetController from "./preset-controller";
@ -674,9 +675,7 @@ $(".pageSettings .section.discordIntegration #unlinkDiscordButton").click(
(e) => {
if (confirm("Are you sure?")) {
Loader.show();
CloudFunctions.unlinkDiscord({
uid: firebase.auth().currentUser.uid,
}).then((ret) => {
axiosInstance.post("/unlinkDiscord").then((ret) => {
Loader.hide();
console.log(ret);
if (ret.data.status === 1) {

View file

@ -1,5 +1,4 @@
import * as Loader from "./loader";
import * as CloudFunctions from "./cloud-functions";
import * as Notifications from "./notifications";
import * as AccountController from "./account-controller";
import * as DB from "./db";
@ -152,26 +151,27 @@ list.updateEmail = new SimplePopup(
(previousEmail, newEmail) => {
try {
Loader.show();
CloudFunctions.updateEmail({
uid: firebase.auth().currentUser.uid,
previousEmail: previousEmail,
newEmail: newEmail,
}).then((data) => {
Loader.hide();
if (data.data.resultCode === 1) {
Notifications.add("Email updated", 0);
setTimeout(() => {
AccountController.signOut();
}, 1000);
} else if (data.data.resultCode === -1) {
Notifications.add("Current email doesn't match", 0);
} else {
Notifications.add(
"Something went wrong: " + JSON.stringify(data.data),
-1
);
}
});
axiosInstance
.post("/updateEmail", {
previousEmail: previousEmail,
newEmail: newEmail,
})
.then((data) => {
Loader.hide();
if (data.data.resultCode === 1) {
Notifications.add("Email updated", 0);
setTimeout(() => {
AccountController.signOut();
}, 1000);
} else if (data.data.resultCode === -1) {
Notifications.add("Current email doesn't match", 0);
} else {
Notifications.add(
"Something went wrong: " + JSON.stringify(data.data),
-1
);
}
});
} catch (e) {
Notifications.add("Something went wrong: " + e, -1);
}
@ -189,10 +189,10 @@ list.clearTagPb = new SimplePopup(
() => {
let tagid = eval("this.parameters[0]");
Loader.show();
CloudFunctions.clearTagPb({
uid: firebase.auth().currentUser.uid,
tagid: tagid,
})
axiosInstance
.post("/clearTagPb", {
tagid: tagid,
})
.then((res) => {
Loader.hide();
if (res.data.resultCode === 1) {

View file

@ -160,22 +160,8 @@ export async function check(completedEvent) {
delete lbRes.keySpacing;
delete lbRes.keyDuration;
delete lbRes.chartData;
/*
CloudFunctions.checkLeaderboards({
// uid: completedEvent.uid,
token: await firebase.auth().currentUser.getIdToken(),
// lbMemory: DB.getSnapshot().lbMemory,
// emailVerified: DB.getSnapshot().emailVerified,
// name: DB.getSnapshot().name,
// banned: DB.getSnapshot().banned,
// verified: DB.getSnapshot().verified,
// discordId: DB.getSnapshot().discordId,
result: lbRes,
})
*/
axiosInstance
.post("/attemptAddToLeaderboards", {
//user data can be retrieved from the database
result: lbRes,
})
.then((data) => {

View file

@ -5,7 +5,6 @@ import * as Notifications from "./notifications";
import Config from "./config";
import * as UI from "./ui";
import tinycolor from "tinycolor2";
import axiosInstance from "./axios-instance";
let isPreviewingTheme = false;
export let randomTheme = null;