const functions = require("firebase-functions"); const admin = require("firebase-admin"); let key = "./serviceAccountKey.json"; if (process.env.GCLOUD_PROJECT === "monkey-type") { key = "./serviceAccountKey_live.json"; } var serviceAccount = require(key); admin.initializeApp({ credential: admin.credential.cert(serviceAccount), }); const db = admin.firestore(); const auth = admin.auth(); const fetch = require("node-fetch"); async function getAllNames() { // return admin // .auth() // .listUsers() // .then((data) => { // let names = []; // data.users.forEach((user) => { // names.push(user.displayName); // }); // return names; // }); let ret = []; async function getAll(nextPageToken) { // List batch of users, 1000 at a time. let listUsersResult = await admin.auth().listUsers(1000, nextPageToken); for (let i = 0; i < listUsersResult.users.length; i++) { ret.push(listUsersResult.users[i].displayName); } if (listUsersResult.pageToken) { // List next batch of users. await getAll(listUsersResult.pageToken); } } await getAll(); return ret; } async function getAllUsers() { // return admin // .auth() // .listUsers() // .then((data) => { // let names = []; // data.users.forEach((user) => { // names.push(user.displayName); // }); // return names; // }); let ret = []; async function getAll(nextPageToken) { // List batch of users, 1000 at a time. let listUsersResult = await auth.listUsers(1000, nextPageToken); for (let i = 0; i < listUsersResult.users.length; i++) { let loopuser = listUsersResult.users[i]; //if custom claim is undefined check, if its true then ignore // if (loopuser === undefined || loopuser.customClaims === undefined || loopuser.customClaims['nameChecked'] === undefined) { ret.push(listUsersResult.users[i]); // } // console.log(loopuser.customClaims['asd']); // let userdata = await db.collection('users').doc(listUsersResult.users[i].uid).get(); // let data = userdata.data(); // if (data === undefined || data.needsToChangeName === undefined) { // // console.log(data); // ret.push(listUsersResult.users[i]); // // console.log('user added'); // } else { // // console.log('user already added'); // } } if (listUsersResult.pageToken) { // List next batch of users. await getAll(listUsersResult.pageToken); } } await getAll(); return ret; } 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); } exports.reserveDisplayName = functions.https.onCall( async (request, response) => { let udata = await db.collection("users").doc(request.uid).get(); udata = udata.data(); if (request.name.toLowerCase() === udata.name.toLowerCase()) { db.collection("takenNames").doc(request.name.toLowerCase()).set( { taken: true, }, { merge: true } ); } } ); exports.clearName = functions.auth.user().onDelete((user) => { db.collection("takenNames").doc(user.displayName.toLowerCase()).delete(); db.collection("users").doc(user.uid).delete(); }); exports.checkNameAvailability = functions.https.onCall( async (request, response) => { // 1 - available // -1 - unavailable (taken) // -2 - not valid name // -999 - unknown error try { if (!isUsernameValid(request.name)) return -2; let takendata = await db .collection("takenNames") .doc(request.name.toLowerCase()) .get(); takendata = takendata.data(); if (takendata !== undefined && takendata.taken) { return -1; } else { return 1; } // return getAllNames().then((data) => { // let available = 1; // data.forEach((name) => { // try { // if (name.toLowerCase() === request.name.toLowerCase()) available = -1; // } catch (e) { // // // } // }); // return available; // }); } catch (e) { console.log(e.message); return -999; } } ); // exports.changeName = functions.https.onCall((request, response) => { // try { // if (!isUsernameValid(request.name)) { // console.warn( // `${request.uid} tried to change their name to ${request.name} - not valid` // ); // return 0; // } // return getAllNames().then((data) => { // let available = 1; // data.forEach((name) => { // try { // if (name.toLowerCase() === request.name.toLowerCase()) available = 0; // } catch (e) { // // // } // }); // if (available === 1) { // return admin // .auth() // .updateUser(request.uid, { // displayName: request.name, // }) // .then((d) => { // console.log( // `${request.uid} changed their name to ${request.name} - done` // ); // return 1; // }) // .catch((e) => { // console.error( // `${request.uid} tried to change their name to ${request.name} - ${e.message}` // ); // return -1; // }); // } else { // console.warn( // `${request.uid} tried to change their name to ${request.name} - already taken` // ); // return 0; // } // }); // } catch (e) { // console.error( // `${request.uid} tried to change their name to ${request.name} - ${e}` // ); // return -1; // } // }); // exports.checkIfNeedsToChangeName = functions.https.onCall( // (request, response) => { // try { // return db // .collection("users") // .doc(request.uid) // .get() // .then((doc) => { // if ( // doc.data().name === undefined || // doc.data().name === null || // doc.data().name === "" // ) { // return admin // .auth() // .getUser(request.uid) // .then((requestUser) => { // if (!isUsernameValid(requestUser.displayName)) { // //invalid name, needs to change // console.log( // `user ${requestUser.uid} ${requestUser.displayName} needs to change name` // ); // return 1; // } else { // //valid name, but need to change if not duplicate // return getAllUsers() // .then((users) => { // let sameName = []; // //look for name names // users.forEach((user) => { // if (user.uid !== requestUser.uid) { // try { // if ( // user.displayName.toLowerCase() === // requestUser.displayName.toLowerCase() // ) { // sameName.push(user); // } // } catch (e) { // // // } // } // }); // if (sameName.length === 0) { // db.collection("users") // .doc(request.uid) // .update({ name: requestUser.displayName }) // .then(() => { // return 0; // }); // } else { // //check when the request user made the account compared to others // let earliestTimestamp = 999999999999999; // sameName.forEach((sn) => { // let ts = // new Date(sn.metadata.creationTime).getTime() / 1000; // if (ts <= earliestTimestamp) { // earliestTimestamp = ts; // } // }); // if ( // new Date( // requestUser.metadata.creationTime // ).getTime() / // 1000 > // earliestTimestamp // ) { // console.log( // `user ${requestUser.uid} ${requestUser.displayName} needs to change name` // ); // return 2; // } else { // db.collection("users") // .doc(request.uid) // .update({ name: requestUser.displayName }) // .then(() => { // return 0; // }); // } // } // }) // .catch((e) => { // console.error(`error getting all users - ${e}`); // }); // } // }); // } else { // // console.log("name is good"); // return 0; // } // }); // } catch (e) { // return -1; // } // } // ); function checkIfPB(uid, obj, userdata) { let pbs = null; if (obj.funbox !== "none") { return false; } try { pbs = userdata.personalBests; if (pbs === undefined) { throw new Error("pb is undefined"); } } catch (e) { return db .collection("users") .doc(uid) .update({ 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((e) => { return true; }) .catch((e) => { return db .collection("users") .doc(uid) .set({ 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((e) => { 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].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); 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) { return db .collection("users") .doc(uid) .update({ personalBests: pbs }) .then((e) => { return true; }); } else { return false; } } 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 ); } function roundTo2(num) { return Math.round((num + Number.EPSILON) * 100) / 100; } 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 - 8 || 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; } } return true; } exports.requestTest = functions.https.onRequest((request, response) => { response.set("Access-Control-Allow-Origin", "*"); response.set("Access-Control-Allow-Headers", "*"); response.set("Access-Control-Allow-Credentials", "true"); response.status(200).send({ data: "test" }); }); exports.getPatreons = functions.https.onRequest(async (request, response) => { response.set("Access-Control-Allow-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 = request.body.data; try { let patreon = await db.collection("patreon").doc("patreons").get(); let data = patreon.data().list; data = data.sort((a, b) => { return b.value - a.value; }); let ret = []; data.forEach((pdoc) => { ret.push(pdoc.name); }); response.status(200).send({ data: ret }); return; } catch (e) { response.status(200).send({ e }); return; } }); exports.verifyUser = functions.https.onRequest(async (request, response) => { response.set("Access-Control-Allow-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 = request.body.data; if (request.uid == undefined) { response .status(200) .send({ data: { 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; await db.collection("users").doc(request.uid).update({ discordId: did, }); await db.collection("bot-commands").add({ command: "verify", arguments: [did, request.uid], executed: false, requestTimestamp: Date.now(), }); response .status(200) .send({ data: { status: 1, message: "Verified", did: did } }); return; }) .catch((e) => { console.error( "Something went wrong when trying to verify user " + e.message ); response.status(200).send({ data: { status: -1, message: e.message } }); return; }); } catch (e) { response.status(200).send({ data: { status: -1, message: e } }); return; } }); 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"); return; } else { //increment console.log("checking"); db.collection(`users/${uid}/bananas`) .doc("bananas") .get() .then((docRef) => { let data = docRef.data(); if (data === undefined) { //create doc db.collection(`users/${uid}/bananas`).doc("bananas").set( { t60bananas: 1, }, { merge: true } ); } else { //increment db.collection(`users/${uid}/bananas`) .doc("bananas") .set( { t60bananas: admin.firestore.FieldValue.increment(1), }, { merge: true } ); } }); } } catch (e) { console.log( "something went wrong when trying to increment bananas " + e.message ); } } async function incrementTestCounter(uid, userData) { try { if (userData.completedTests === undefined) { let results = await db.collection(`users/${uid}/results`).get(); let count = results.docs.length; db.collection("users") .doc(uid) .update({ completedTests: admin.firestore.FieldValue.increment(count), }); db.collection("public") .doc("stats") .update({ completedTests: admin.firestore.FieldValue.increment(count), }); } else { db.collection("users") .doc(uid) .update({ completedTests: admin.firestore.FieldValue.increment(1) }); db.collection("public") .doc("stats") .update({ completedTests: admin.firestore.FieldValue.increment(1) }); } } catch (e) { console.error( `Error while incrementing completed tests for user ${uid}: ${e}` ); } } async function incrementStartedTestCounter(uid, num, userData) { try { if (userData.startedTests === undefined) { let stepSize = 1000; let results = []; let query = await db .collection(`users/${uid}/results`) .orderBy("timestamp", "desc") .limit(stepSize) .get(); let lastDoc; while (query.docs.length > 0) { lastDoc = query.docs[query.docs.length - 1]; query.docs.forEach((doc) => { results.push({ restartCount: doc.data().restartCount }); }); query = await db .collection(`users/${uid}/results`) .orderBy("timestamp", "desc") .limit(stepSize) .startAfter(lastDoc) .get(); } let count = 0; results.forEach((result) => { try { let rc = result.restartCount; if (rc === undefined) { rc = 0; } count += parseInt(rc); } catch (e) {} }); count += results.length; db.collection("users") .doc(uid) .update({ startedTests: admin.firestore.FieldValue.increment(count), }); db.collection("public") .doc("stats") .update({ startedTests: admin.firestore.FieldValue.increment(count), }); } else { db.collection("users") .doc(uid) .update({ startedTests: admin.firestore.FieldValue.increment(num) }); db.collection("public") .doc("stats") .update({ startedTests: admin.firestore.FieldValue.increment(num) }); } } catch (e) { console.error( `Error while incrementing started tests for user ${uid}: ${e}` ); } } async function incrementTimeSpentTyping(uid, res, userData) { try { if (userData.timeTyping === undefined) { let stepSize = 1000; let results = []; let query = await db .collection(`users/${uid}/results`) .orderBy("timestamp", "desc") .limit(stepSize) .get(); let lastDoc; while (query.docs.length > 0) { lastDoc = query.docs[query.docs.length - 1]; query.docs.forEach((doc) => { let dd = doc.data(); results.push({ testDuration: dd.testDuration, incompleteTestSeconds: dd.incompleteTestSeconds, }); }); query = await db .collection(`users/${uid}/results`) .orderBy("timestamp", "desc") .limit(stepSize) .startAfter(lastDoc) .get(); } let timeSum = 0; results.forEach((result) => { try { let ts = result.testDuration; let its = result.incompleteTestSeconds; let s1 = ts == undefined ? 0 : ts; let s2 = its == undefined ? 0 : its; timeSum += parseFloat(s1) + parseFloat(s2); } catch (e) {} }); db.collection("users") .doc(uid) .update({ timeTyping: admin.firestore.FieldValue.increment(timeSum), }); db.collection("public") .doc("stats") .update({ timeTyping: admin.firestore.FieldValue.increment(timeSum), }); } else { let afk = res.afkDuration; if (afk == undefined) { afk = 0; } db.collection("users") .doc(uid) .update({ timeTyping: admin.firestore.FieldValue.increment( res.testDuration + res.incompleteTestSeconds - afk ), }); db.collection("public") .doc("stats") .update({ timeTyping: admin.firestore.FieldValue.increment( res.testDuration + res.incompleteTestSeconds - afk ), }); } } catch (e) { console.error(`Error while incrementing time typing for user ${uid}: ${e}`); } } exports.testCompleted = functions.https.onRequest(async (request, response) => { response.set("Access-Control-Allow-Origin", "*"); 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 = request.body.data; if (request === undefined) { response.status(200).send({ data: { resultCode: -999 } }); return; } try { if (request.uid === undefined || request.obj === undefined) { console.error(`error saving result for - missing input`); response.status(200).send({ data: { resultCode: -999 } }); return; } let obj = request.obj; function verifyValue(val) { let errCount = 0; 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); // console.log(errCount); if (errCount > 0) { console.error( `error saving result for ${ request.uid } error count ${errCount} - bad input - ${JSON.stringify(request.obj)}` ); response.status(200).send({ data: { resultCode: -1 } }); return; } if (obj.wpm <= 0 || obj.wpm > 350 || obj.acc < 50 || obj.acc > 100) { response.status(200).send({ data: { resultCode: -1 } }); 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 { response.status(200).send({ data: { 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 ${request.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(request.uid) .then((user) => { return user.emailVerified; }); // if (obj.funbox === "nospace") { // response.status(200).send({ data: { resultCode: -1 } }); // return; // } return db .collection("users") .doc(request.uid) .get() .then((ret) => { let userdata = ret.data(); 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 }) ${request.uid} ${name} - spacing ${JSON.stringify( keySpacing )} duration ${JSON.stringify(keyDuration)}` ); response.status(200).send({ data: { 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}) ${ request.uid } ${name} - spacing ${JSON.stringify( keySpacing )} duration ${JSON.stringify(keyDuration)}` ); } } else { response.status(200).send({ data: { 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/${request.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.uid, request.obj, userdata), ]) .then(async (values) => { let globallb = values[0].insertedAt; let dailylb = values[1].insertedAt; let ispb = values[2]; // console.log(values); if (obj.mode === "time" && String(obj.mode2) === "60") { incrementT60Bananas(request.uid, obj, userdata); } incrementTestCounter(request.uid, userdata); incrementStartedTestCounter( request.uid, obj.restartCount + 1, userdata ); incrementTimeSpentTyping(request.uid, obj, userdata); let usr = userdata.discordId !== undefined ? userdata.discordId : userdata.name; if ( globallb !== null && globallb.insertedAt >= 0 && globallb.insertedAt <= 9 && globallb.newBest ) { let lbstring = `${obj.mode} ${obj.mode2} global`; console.log( `sending command to the bot to announce lb update ${ userdata.discordId } ${globallb + 1} ${lbstring} ${obj.wpm}` ); announceLbUpdate( usr, globallb.insertedAt + 1, lbstring, obj.wpm, obj.rawWpm, obj.acc ); } let returnobj = { resultCode: null, globalLeaderboard: globallb, dailyLeaderboard: dailylb, lbBanned: banned, name: name, createdId: createdDocId, needsToVerify: values[0].needsToVerify, needsToVerifyEmail: values[0].needsToVerifyEmail, }; if (ispb) { let logobj = request.obj; logobj.keySpacing = "removed"; logobj.keyDuration = "removed"; console.log( `saved result for ${ request.uid } (new PB) - ${JSON.stringify(logobj)}` ); await db .collection(`users/${request.uid}/results/`) .doc(createdDocId) .update({ 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 ${request.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"; console.log( `saved result for ${request.uid} - ${JSON.stringify( logobj )}` ); returnobj.resultCode = 1; } response.status(200).send({ data: returnobj }); return; }) .catch((e) => { console.error( `error saving result when checking for PB / checking leaderboards for ${request.uid} - ${e.message}` ); response .status(200) .send({ data: { resultCode: -999, message: e.message } }); return; }); }) .catch((e) => { console.error( `error saving result when adding result to the db for ${request.uid} - ${e.message}` ); response .status(200) .send({ data: { resultCode: -999, message: e.message } }); return; }); }) .catch((e) => { console.error( `error saving result when getting user data for ${request.uid} - ${e.message}` ); response .status(200) .send({ data: { resultCode: -999, message: e.message } }); return; }); } catch (e) { console.error( `error saving result for ${request.uid} - ${JSON.stringify( request.obj )} - ${e}` ); response .status(200) .send({ data: { resultCode: -999, message: e.message } }); return; } }); exports.updateEmail = functions.https.onCall(async (request, response) => { try { let previousEmail = await admin.auth().getUser(request.uid); if (previousEmail.email !== request.previousEmail) { return { resultCode: -1 }; } else { await admin.auth().updateUser(request.uid, { email: request.newEmail, emailVerified: false, }); return { resultCode: 1, }; } } catch (e) { console.error(`error updating email for ${request.uid} - ${e}`); return { resultCode: -999, message: e.message, }; } }); function updateDiscordRole(discordId, wpm) { db.collection("bot-commands").add({ command: "updateRole", arguments: [discordId, wpm], executed: false, requestTimestamp: Date.now(), }); } function isTagValid(name) { if (name === null || name === undefined || name === "") return false; if (name.length > 16) return false; return /^[0-9a-zA-Z_.-]+$/.test(name); } exports.addTag = functions.https.onCall((request, response) => { try { if (!isTagValid(request.name)) { return { resultCode: -1 }; } else { return db .collection(`users/${request.uid}/tags`) .add({ name: request.name, }) .then((e) => { console.log(`user ${request.uid} created a tag: ${request.name}`); return { resultCode: 1, id: e.id, }; }) .catch((e) => { console.error( `error while creating tag for user ${request.uid}: ${e.message}` ); return { resultCode: -999 }; }); } } catch (e) { console.error(`error adding tag for ${request.uid} - ${e}`); return { resultCode: -999 }; } }); exports.editTag = functions.https.onCall((request, response) => { try { if (!isTagValid(request.name)) { return { resultCode: -1 }; } else { return db .collection(`users/${request.uid}/tags`) .doc(request.tagid) .update({ name: request.name, }) .then((e) => { console.log(`user ${request.uid} updated a tag: ${request.name}`); return { resultCode: 1, }; }) .catch((e) => { console.error( `error while updating tag for user ${request.uid}: ${e.message}` ); return { resultCode: -999 }; }); } } catch (e) { console.error(`error updating tag for ${request.uid} - ${e}`); return { resultCode: -999 }; } }); exports.removeTag = functions.https.onCall((request, response) => { try { return db .collection(`users/${request.uid}/tags`) .doc(request.tagid) .delete() .then((e) => { console.log(`user ${request.uid} deleted a tag`); return { resultCode: 1, }; }) .catch((e) => { console.error( `error deleting tag for user ${request.uid}: ${e.message}` ); return { resultCode: -999 }; }); } catch (e) { console.error(`error deleting tag for ${request.uid} - ${e}`); return { resultCode: -999 }; } }); exports.updateResultTags = functions.https.onCall((request, response) => { try { let validTags = true; request.tags.forEach((tag) => { if (!/^[0-9a-zA-Z]+$/.test(tag)) validTags = false; }); if (validTags) { return db .collection(`users/${request.uid}/results`) .doc(request.resultid) .update({ tags: request.tags, }) .then((e) => { console.log( `user ${request.uid} updated tags for result ${request.resultid}` ); return { resultCode: 1, }; }) .catch((e) => { console.error( `error while updating tags for result by user ${request.uid}: ${e.message}` ); return { resultCode: -999 }; }); } else { console.error(`invalid tags for user ${request.uid}: ${request.tags}`); return { resultCode: -1 }; } } catch (e) { console.error(`error updating tags by ${request.uid} - ${e}`); return { resultCode: -999 }; } }); function isConfigKeyValid(name) { if (name === null || name === undefined || name === "") return false; if (name.length > 30) return false; return /^[0-9a-zA-Z_.\-#+]+$/.test(name); } exports.saveConfig = functions.https.onCall((request, response) => { try { if (request.uid === undefined || request.obj === undefined) { console.error(`error saving config for ${request.uid} - missing input`); return { returnCode: -1, message: "Missing input", }; } let obj = request.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; 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 ${request.uid} - bad input - ${JSON.stringify( request.obj )}` ); return { returnCode: -1, message: "Bad input. " + errorMessage, }; } return db .collection(`users`) .doc(request.uid) .set( { config: obj, }, { merge: true } ) .then((e) => { return { returnCode: 1, message: "Saved", }; }) .catch((e) => { console.error( `error saving config to DB for ${request.uid} - ${e.message}` ); return { returnCode: -1, message: e.message, }; }); } catch (e) { console.error(`error saving config for ${request.uid} - ${e}`); return { resultCode: -999, message: e, }; } }); exports.saveLbMemory = functions.https.onCall((request, response) => { try { if (request.uid === undefined || request.obj === undefined) { console.error( `error saving lb memory for ${request.uid} - missing input` ); return { returnCode: -1, message: "Missing input", }; } let obj = request.obj; return db .collection(`users`) .doc(request.uid) .set( { lbMemory: obj, }, { merge: true } ) .then((e) => { return { returnCode: 1, message: "Saved", }; }) .catch((e) => { console.error( `error saving lb memory to DB for ${request.uid} - ${e.message}` ); return { returnCode: -1, message: e.message, }; }); } catch (e) { console.error(`error saving lb memory for ${request.uid} - ${e}`); return { resultCode: -999, message: e, }; } }); function generate(n) { var add = 1, max = 12 - add; if (n > max) { return generate(max) + generate(n - max); } max = Math.pow(10, n + add); var min = max / 10; // Math.pow(10, n) basically var number = Math.floor(Math.random() * (max - min + 1)) + min; return ("" + number).substring(add); } class Leaderboard { constructor(size, mode, mode2, type, starting) { this.size = size; this.board = []; this.mode = mode; this.mode2 = parseInt(mode2); this.type = type; if (starting !== undefined && starting !== null) { starting.forEach((entry) => { if ( entry.mode == this.mode && parseInt(entry.mode2) === parseInt(this.mode2) ) { let hid = entry.hidden === undefined ? false : entry.hidden; this.board.push({ uid: entry.uid, name: entry.name, wpm: parseFloat(entry.wpm), raw: parseFloat(entry.raw), acc: parseFloat(entry.acc), mode: entry.mode, mode2: parseInt(entry.mode2), timestamp: entry.timestamp, hidden: hid, }); } }); } this.sortBoard(); this.clipBoard(); } sortBoard() { this.board.sort((a, b) => { if (a.wpm === b.wpm) { if (a.acc === b.acc) { return a.timestamp - b.timestamp; } else { return b.acc - a.acc; } } else { return b.wpm - a.wpm; } }); } clipBoard() { let boardLength = this.board.length; if (boardLength > this.size) { while (this.board.length !== this.size) { this.board.pop(); } } } logBoard() { console.log(this.board); } removeDuplicates(insertedAt, uid) { //return true if a better result is found let found = false; // let ret; let foundAt = null; if (this.board !== undefined) { this.board.forEach((entry, index) => { if (entry.uid === uid) { if (found) { this.board.splice(index, 1); // if (index > insertedAt) { // //removed old result // ret = false; // } else { // ret = true; // } } else { found = true; foundAt = index; } } }); } // console.log(ret); // return ret; return foundAt; } insert(a) { let insertedAt = -1; if (a.mode == this.mode && parseInt(a.mode2) === parseInt(this.mode2)) { this.board.forEach((b, index) => { if (insertedAt !== -1) return; if (a.wpm === b.wpm) { if (a.acc === b.acc) { if (a.timestamp < b.timestamp) { this.board.splice(index, 0, { uid: a.uid, name: a.name, wpm: parseFloat(a.wpm), raw: parseFloat(a.rawWpm), acc: parseFloat(a.acc), mode: a.mode, mode2: parseInt(a.mode2), timestamp: a.timestamp, hidden: a.hidden === undefined ? false : a.hidden, }); insertedAt = index; } } else { if (a.acc > b.acc) { this.board.splice(index, 0, { uid: a.uid, name: a.name, wpm: parseFloat(a.wpm), raw: parseFloat(a.rawWpm), acc: parseFloat(a.acc), mode: a.mode, mode2: parseInt(a.mode2), timestamp: a.timestamp, hidden: a.hidden === undefined ? false : a.hidden, }); insertedAt = index; } } } else { if (a.wpm > b.wpm) { this.board.splice(index, 0, { uid: a.uid, name: a.name, wpm: parseFloat(a.wpm), raw: parseFloat(a.rawWpm), acc: parseFloat(a.acc), mode: a.mode, mode2: parseInt(a.mode2), timestamp: a.timestamp, hidden: a.hidden === undefined ? false : a.hidden, }); insertedAt = index; } } }); if (this.board.length < this.size && insertedAt === -1) { this.board.push({ uid: a.uid, name: a.name, wpm: parseFloat(a.wpm), raw: parseFloat(a.rawWpm), acc: parseFloat(a.acc), mode: a.mode, mode2: parseInt(a.mode2), timestamp: a.timestamp, hidden: a.hidden === undefined ? false : a.hidden, }); insertedAt = this.board.length - 1; } // console.log("before duplicate remove"); // console.log(this.board); let newBest = false; let foundAt = null; if (insertedAt >= 0) { // if (this.removeDuplicates(insertedAt, a.uid)) { // insertedAt = -2; // } foundAt = this.removeDuplicates(insertedAt, a.uid); if (foundAt >= insertedAt) { //new better result newBest = true; } } // console.log(this.board); this.clipBoard(); return { insertedAt: insertedAt, newBest: newBest, foundAt: foundAt, }; } else { return { insertedAt: -999, }; } } } // exports.generatePairingCode = functions // .runWith({ // timeoutSeconds: 100, // memory: "2GB", // }) // .https.onRequest((request, response) => { // response.set("Access-Control-Allow-Origin", "*"); // 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 = request.body.data; // try { // if (request === null) { // console.error( // `error while trying to generate discord pairing code - no input` // ); // response.status(200).send({ data: { status: -999 } }); // return; // } // return db // .collection("users") // .doc(request.uid) // .get() // .then(async (userDoc) => { // userDocData = userDoc.data(); // if ( // userDocData.discordPairingCode !== undefined && // userDocData.discordPairingCode !== null // ) { // console.log( // `user ${request.uid} already has code ${userDocData.discordPairingCode}` // ); // response.status(200).send({ // data: { // status: -999, // pairingCode: userDocData.discordPairingCode, // }, // }); // } else { // let stepSize = 1000; // let existingCodes = []; // let query = await db // .collection(`users`) // .where("discordPairingCode", ">", "") // .limit(stepSize) // .get(); // let lastDoc; // while (query.docs.length > 0) { // lastDoc = query.docs[query.docs.length - 1]; // query.docs.forEach((doc) => { // let docData = doc.data(); // if ( // docData.discordPairingCode !== undefined && // docData.discordPairingCode !== null // ) { // existingCodes.push(docData.discordPairingCode); // } // }); // query = await db // .collection(`users`) // .where("discordPairingCode", ">", "") // .limit(stepSize) // .startAfter(lastDoc) // .get(); // } // let randomCode = generate(9); // while (existingCodes.includes(randomCode)) { // randomCode = generate(9); // } // return db // .collection("users") // .doc(request.uid) // .update( // { // discordPairingCode: randomCode, // }, // { merge: true } // ) // .then((res) => { // console.log(`generated ${randomCode} for user ${request.uid}`); // response.status(200).send({ // data: { // status: 1, // pairingCode: randomCode, // }, // }); // return; // }) // .catch((e) => { // console.error( // `error while trying to set discord pairing code ${randomCode} for user ${request.uid} - ${e}` // ); // response.status(200).send({ // data: { // status: -999, // }, // }); // return; // }); // } // }); // } catch (e) { // console.error( // `error while trying to generate discord pairing code for user ${request.uid} - ${e}` // ); // response.status(200).send({ // data: { // status: -999, // }, // }); // return; // } // }); exports.unlinkDiscord = functions.https.onRequest((request, response) => { response.set("Access-Control-Allow-Origin", "*"); 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 = request.body.data; try { if (request === null || request.uid === undefined) { response .status(200) .send({ data: { status: -999, message: "Empty request" } }); return; } return db .collection(`users`) .doc(request.uid) .update({ discordId: null, }) .then((f) => { response.status(200).send({ data: { status: 1, message: "Unlinked", }, }); return; }) .catch((e) => { response.status(200).send({ data: { status: -999, message: e.message, }, }); return; }); } catch (e) { response.status(200).send({ data: { status: -999, message: e, }, }); return; } }); async function checkLeaderboards( resultObj, type, banned, name, verified, emailVerified ) { //lb disable // return { // insertedAt: null, // }; // try { if (resultObj.funbox !== "none") { return { insertedAt: null, }; } if (emailVerified === false) return { insertedAt: null, needsToVerifyEmail: true, }; if (!name) return { insertedAt: null, noName: true, }; if (banned) return { insertedAt: null, banned: true, }; if (verified === false) return { insertedAt: null, needsToVerify: true, }; if ( resultObj.mode === "time" && ["15", "60"].includes(String(resultObj.mode2)) && resultObj.language === "english" ) { return await db.runTransaction(async (t) => { const lbdoc = await t.get( db .collection("leaderboards") .where("mode", "==", String(resultObj.mode)) .where("mode2", "==", String(resultObj.mode2)) .where("type", "==", type) ); let lbData; let docid = `${String(resultObj.mode)}_${String( resultObj.mode2 )}_${type}`; if (lbdoc.docs.length === 0) { console.log( `no ${resultObj.mode} ${resultObj.mode2} ${type} leaderboard found - creating` ); let toAdd = { size: 20, mode: String(resultObj.mode), mode2: String(resultObj.mode2), type: type, }; await t.set( db .collection("leaderboards") .doc( `${String(resultObj.mode)}_${String(resultObj.mode2)}_${type}` ), toAdd ); lbData = toAdd; } else { lbData = lbdoc.docs[0].data(); } let boardInfo = lbData; let boardData = lbData.board; let lb = new Leaderboard( boardInfo.size, resultObj.mode, resultObj.mode2, boardInfo.type, boardData ); let insertResult = lb.insert(resultObj); if (insertResult.insertedAt >= 0) { //update the database here console.log( `leaderboard changed ${resultObj.mode} ${ resultObj.mode2 } ${type} - ${JSON.stringify(lb.board)}` ); await t.update(db.collection("leaderboards").doc(docid), { size: lb.size, type: lb.type, board: lb.board, }); } return { insertedAt: insertResult, }; }); } else { return { insertedAt: null, }; } } catch (e) { console.error( `error while checking leaderboards - ${e} - ${type} ${resultObj}` ); return { insertedAt: null, }; } } exports.getLeaderboard = functions.https.onCall((request, response) => { return db .collection("leaderboards") .where("mode", "==", String(request.mode)) .where("mode2", "==", String(request.mode2)) .where("type", "==", String(request.type)) .get() .then(async (data) => { // console.log("got data"); if (data.docs.length === 0) return null; let lbdata = data.docs[0].data(); if (lbdata.board !== undefined) { // console.log("replacing users"); // for (let i = 0; i < lbdata.board.length; i++) { // await db // .collection("users") // .doc(lbdata.board[i].uid) // .get() // .then((doc) => { // if ( // lbdata.board[i].uid !== null && // lbdata.board[i].uid === request.uid // ) { // lbdata.board[i].currentUser = true; // } // lbdata.board[i].name = doc.data().name; // lbdata.board[i].uid = null; // }); // } lbdata.board.forEach((boardentry) => { if (boardentry.uid !== null && boardentry.uid === request.uid) { boardentry.currentUser = true; } boardentry.uid = null; }); // console.log(lbdata); if (request.type === "daily") { let resetTime = new Date(Date.now()); resetTime.setHours(0, 0, 0, 0); resetTime.setDate(resetTime.getUTCDate() + 1); resetTime = resetTime.valueOf(); lbdata.resetTime = resetTime; } return lbdata; } else { if ( lbdata.board === undefined || lbdata.board === [] || lbdata.board.length === 0 ) { return lbdata; } else { return []; } } }); }); exports.scheduledFunctionCrontab = functions.pubsub .schedule("00 00 * * *") .timeZone("Africa/Abidjan") .onRun((context) => { try { console.log("moving daily leaderboards to history"); db.collection("leaderboards") .where("type", "==", "daily") .get() .then(async (res) => { for (let i = 0; i < res.docs.length; i++) { let doc = res.docs[i]; let lbdata = doc.data(); let winnerUid = lbdata.board[0].uid; await db .collection("users") .doc(winnerUid) .get() .then(async (userDoc) => { let userData = userDoc.data(); let lbwins = userData.dailyLbWins; let lbname = lbdata.mode + lbdata.mode2; if (lbwins === undefined) { //first win ever lbwins = { [lbname]: 1, }; } else { //object already exists if (lbwins[lbname] === undefined) { lbwins[lbname] = 1; } else { lbwins[lbname] = lbwins[lbname] + 1; } } await db.collection("users").doc(winnerUid).update({ dailyLbWins: lbwins, }); }); announceDailyLbResult(lbdata); t = new Date(); db.collection("leaderboards_history") .doc( `${t.getUTCDate()}_${t.getUTCMonth()}_${t.getUTCFullYear()}_${ lbdata.mode }_${lbdata.mode2}` ) .set(lbdata); db.collection("leaderboards").doc(doc.id).set( { board: [], }, { merge: true } ); } }); return null; } catch (e) { console.error(`error while moving daily leaderboards to history - ${e}`); } }); async function announceLbUpdate(discordId, pos, lb, wpm, raw, acc) { db.collection("bot-commands").add({ command: "sayLbUpdate", arguments: [discordId, pos, lb, wpm, raw, acc], executed: false, requestTimestamp: Date.now(), }); } async function announceDailyLbResult(lbdata) { db.collection("bot-commands").add({ command: "announceDailyLbResult", arguments: [lbdata], executed: false, requestTimestamp: Date.now(), }); }