diff --git a/backend/__tests__/dal/leaderboards.spec.ts b/backend/__tests__/dal/leaderboards.spec.ts index b677abf78676..1bb4a8dfcc93 100644 --- a/backend/__tests__/dal/leaderboards.spec.ts +++ b/backend/__tests__/dal/leaderboards.spec.ts @@ -116,7 +116,7 @@ describe("LeaderboardsDal", () => { }); function expectedLbEntry(rank: number, user: MonkeyTypes.User, time: string) { - const lbBest: MonkeyTypes.PersonalBest = + const lbBest: SharedTypes.PersonalBest = user.lbPersonalBests?.time[time].english; return { @@ -166,8 +166,8 @@ async function createUser( } function lbBests( - pb15?: MonkeyTypes.PersonalBest, - pb60?: MonkeyTypes.PersonalBest + pb15?: SharedTypes.PersonalBest, + pb60?: SharedTypes.PersonalBest ): MonkeyTypes.LbPersonalBests { const result = { time: {} }; if (pb15) result.time["15"] = { english: pb15 }; @@ -179,7 +179,7 @@ function pb( wpm: number, acc: number = 90, timestamp: number = 1 -): MonkeyTypes.PersonalBest { +): SharedTypes.PersonalBest { return { acc, consistency: 100, diff --git a/backend/__tests__/dal/result.spec.ts b/backend/__tests__/dal/result.spec.ts index ede258ca6f43..bb5133ac08f4 100644 --- a/backend/__tests__/dal/result.spec.ts +++ b/backend/__tests__/dal/result.spec.ts @@ -2,7 +2,7 @@ import * as ResultDal from "../../src/dal/result"; import { ObjectId } from "mongodb"; import * as UserDal from "../../src/dal/user"; -type MonkeyTypesResult = MonkeyTypes.Result; +type MonkeyTypesResult = SharedTypes.DBResult; let uid: string = ""; const timestamp = Date.now() - 60000; @@ -55,6 +55,8 @@ async function createDummyData( keyDurationStats: { average: 0, sd: 0 }, difficulty: "normal", language: "english", + isPb: false, + name: "Test", } as MonkeyTypesResult); } } diff --git a/backend/__tests__/dal/user.spec.ts b/backend/__tests__/dal/user.spec.ts index 4cf84536ecaa..b6eee4da9e05 100644 --- a/backend/__tests__/dal/user.spec.ts +++ b/backend/__tests__/dal/user.spec.ts @@ -15,9 +15,13 @@ const mockPersonalBest = { timestamp: 13123123, }; -const mockResultFilter = { - _id: new ObjectId(), +const mockResultFilter: SharedTypes.ResultFilters = { + _id: "id", name: "sfdkjhgdf", + pb: { + no: true, + yes: true, + }, difficulty: { normal: true, expert: false, diff --git a/backend/src/api/controllers/result.ts b/backend/src/api/controllers/result.ts index c8a1c91b2b6f..3fab0f2d02ec 100644 --- a/backend/src/api/controllers/result.ts +++ b/backend/src/api/controllers/result.ts @@ -43,6 +43,7 @@ import _ from "lodash"; import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard"; import { UAParser } from "ua-parser-js"; import { canFunboxGetPb } from "../../utils/pb"; +import { buildDbResult } from "../../utils/result"; try { if (anticheatImplemented() === false) throw new Error("undefined"); @@ -194,32 +195,37 @@ export async function addResult( ); } - //todo add a type here - const result = Object.assign({}, req.body.result); - if (!user.lbOptOut && result.acc < 75) { + const completedEvent = Object.assign( + {}, + req.body.result + ) as SharedTypes.CompletedEvent; + if (!user.lbOptOut && completedEvent.acc < 75) { throw new MonkeyError( 400, "Cannot submit a result with less than 75% accuracy" ); } - result.uid = uid; - if (isTestTooShort(result)) { + completedEvent.uid = uid; + if (isTestTooShort(completedEvent)) { const status = MonkeyStatusCodes.TEST_TOO_SHORT; throw new MonkeyError(status.code, status.message); } - const resulthash = result.hash; - delete result.hash; - delete result.stringified; + const resulthash = completedEvent.hash; + if (!resulthash) { + throw new MonkeyError(400, "Missing result hash"); + } + delete completedEvent.hash; + delete completedEvent.stringified; if (req.ctx.configuration.results.objectHashCheckEnabled) { - const serverhash = objectHash(result); + const serverhash = objectHash(completedEvent); if (serverhash !== resulthash) { Logger.logToDb( "incorrect_result_hash", { serverhash, resulthash, - result, + result: completedEvent, }, uid ); @@ -228,43 +234,41 @@ export async function addResult( } } - if (result.funbox) { - const funboxes = result.funbox.split("#"); + if (completedEvent.funbox) { + const funboxes = completedEvent.funbox.split("#"); if (funboxes.length !== _.uniq(funboxes).length) { throw new MonkeyError(400, "Duplicate funboxes"); } } - if (!areFunboxesCompatible(result.funbox)) { + if (!areFunboxesCompatible(completedEvent.funbox ?? "")) { throw new MonkeyError(400, "Impossible funbox combination"); } - try { - result.keySpacingStats = { + if (completedEvent.keySpacing !== "toolong") { + completedEvent.keySpacingStats = { average: - result.keySpacing.reduce((previous, current) => (current += previous)) / - result.keySpacing.length, - sd: stdDev(result.keySpacing), + completedEvent.keySpacing.reduce( + (previous, current) => (current += previous) + ) / completedEvent.keySpacing.length, + sd: stdDev(completedEvent.keySpacing), }; - } catch (e) { - // } - try { - result.keyDurationStats = { + + if (completedEvent.keyDuration !== "toolong") { + completedEvent.keyDurationStats = { average: - result.keyDuration.reduce( + completedEvent.keyDuration.reduce( (previous, current) => (current += previous) - ) / result.keyDuration.length, - sd: stdDev(result.keyDuration), + ) / completedEvent.keyDuration.length, + sd: stdDev(completedEvent.keyDuration), }; - } catch (e) { - // } if (anticheatImplemented()) { if ( !validateResult( - result, + completedEvent, (req.headers["x-client-version"] || req.headers["client-version"]) as string, JSON.stringify(new UAParser(req.headers["user-agent"]).getResult()), @@ -305,7 +309,7 @@ export async function addResult( // } //convert result test duration to miliseconds - const testDurationMilis = result.testDuration * 1000; + const testDurationMilis = completedEvent.testDuration * 1000; //get latest result ordered by timestamp let lastResultTimestamp; try { @@ -314,7 +318,7 @@ export async function addResult( lastResultTimestamp = null; } - result.timestamp = Math.floor(Date.now() / 1000) * 1000; + completedEvent.timestamp = Math.floor(Date.now() / 1000) * 1000; //check if now is earlier than last result plus duration (-1 second as a buffer) const earliestPossible = lastResultTimestamp + testDurationMilis; @@ -337,22 +341,22 @@ export async function addResult( //check keyspacing and duration here for bots if ( - result.mode === "time" && - result.wpm > 130 && - result.testDuration < 122 && + completedEvent.mode === "time" && + completedEvent.wpm > 130 && + completedEvent.testDuration < 122 && (user.verified === false || user.verified === undefined) && user.lbOptOut !== true && user.banned !== true //no need to check again if user is already banned ) { - if (!result.keySpacingStats || !result.keyDurationStats) { + if (!completedEvent.keySpacingStats || !completedEvent.keyDurationStats) { const status = MonkeyStatusCodes.MISSING_KEY_DATA; throw new MonkeyError(status.code, "Missing key data"); } - if (result.keyOverlap === undefined) { + if (completedEvent.keyOverlap === undefined) { throw new MonkeyError(400, "Old key data format"); } if (anticheatImplemented()) { - if (!validateKeys(result, uid)) { + if (!validateKeys(completedEvent, uid)) { //autoban const autoBanConfig = req.ctx.configuration.users.autoBan; if (autoBanConfig.enabled) { @@ -383,15 +387,6 @@ export async function addResult( } } - delete result.keySpacing; - delete result.keyDuration; - delete result.smoothConsistency; - delete result.wpmConsistency; - delete result.keyOverlap; - delete result.lastKeyToEnd; - delete result.startToFirstKey; - delete result.charTotal; - if (req.ctx.configuration.users.lastHashesCheck.enabled) { let lastHashes = user.lastReultHashes ?? []; if (lastHashes.includes(resulthash)) { @@ -400,7 +395,7 @@ export async function addResult( { lastHashes, resulthash, - result, + result: completedEvent, }, uid ); @@ -416,67 +411,73 @@ export async function addResult( } } - result.name = user.name; - - try { - result.keyDurationStats.average = roundTo2(result.keyDurationStats.average); - result.keyDurationStats.sd = roundTo2(result.keyDurationStats.sd); - result.keySpacingStats.average = roundTo2(result.keySpacingStats.average); - result.keySpacingStats.sd = roundTo2(result.keySpacingStats.sd); - } catch (e) { - // + if (completedEvent.keyDurationStats) { + completedEvent.keyDurationStats.average = roundTo2( + completedEvent.keyDurationStats.average + ); + completedEvent.keyDurationStats.sd = roundTo2( + completedEvent.keyDurationStats.sd + ); + } + if (completedEvent.keySpacingStats) { + completedEvent.keySpacingStats.average = roundTo2( + completedEvent.keySpacingStats.average + ); + completedEvent.keySpacingStats.sd = roundTo2( + completedEvent.keySpacingStats.sd + ); } let isPb = false; let tagPbs: string[] = []; - if (!result.bailedOut) { + if (!completedEvent.bailedOut) { [isPb, tagPbs] = await Promise.all([ - checkIfPb(uid, user, result), - checkIfTagPb(uid, user, result), + checkIfPb(uid, user, completedEvent), + checkIfTagPb(uid, user, completedEvent), ]); } - if (isPb) { - result.isPb = true; - } - - if (result.mode === "time" && result.mode2 === "60") { - incrementBananas(uid, result.wpm); + if (completedEvent.mode === "time" && completedEvent.mode2 === "60") { + incrementBananas(uid, completedEvent.wpm); if (isPb && user.discordId) { - GeorgeQueue.updateDiscordRole(user.discordId, result.wpm); + GeorgeQueue.updateDiscordRole(user.discordId, completedEvent.wpm); } } if ( - result.challenge && - AutoRoleList.includes(result.challenge) && + completedEvent.challenge && + AutoRoleList.includes(completedEvent.challenge) && user.discordId ) { - GeorgeQueue.awardChallenge(user.discordId, result.challenge); + GeorgeQueue.awardChallenge(user.discordId, completedEvent.challenge); } else { - delete result.challenge; + delete completedEvent.challenge; } - const afk = result.afkDuration ?? 0; + const afk = completedEvent.afkDuration ?? 0; const totalDurationTypedSeconds = - result.testDuration + result.incompleteTestSeconds - afk; - updateTypingStats(uid, result.restartCount, totalDurationTypedSeconds); - PublicDAL.updateStats(result.restartCount, totalDurationTypedSeconds); + completedEvent.testDuration + completedEvent.incompleteTestSeconds - afk; + updateTypingStats( + uid, + completedEvent.restartCount, + totalDurationTypedSeconds + ); + PublicDAL.updateStats(completedEvent.restartCount, totalDurationTypedSeconds); const dailyLeaderboardsConfig = req.ctx.configuration.dailyLeaderboards; const dailyLeaderboard = getDailyLeaderboard( - result.language, - result.mode, - result.mode2, + completedEvent.language, + completedEvent.mode, + completedEvent.mode2, dailyLeaderboardsConfig ); let dailyLeaderboardRank = -1; const validResultCriteria = - canFunboxGetPb(result) && - !result.bailedOut && + canFunboxGetPb(completedEvent) && + !completedEvent.bailedOut && user.banned !== true && user.lbOptOut !== true && (isDevEnvironment() || (user.timeTyping ?? 0) > 7200); @@ -484,15 +485,19 @@ export async function addResult( const selectedBadgeId = user.inventory?.badges?.find((b) => b.selected)?.id; if (dailyLeaderboard && validResultCriteria) { - incrementDailyLeaderboard(result.mode, result.mode2, result.language); + incrementDailyLeaderboard( + completedEvent.mode, + completedEvent.mode2, + completedEvent.language + ); dailyLeaderboardRank = await dailyLeaderboard.addResult( { name: user.name, - wpm: result.wpm, - raw: result.rawWpm, - acc: result.acc, - consistency: result.consistency, - timestamp: result.timestamp, + wpm: completedEvent.wpm, + raw: completedEvent.rawWpm, + acc: completedEvent.acc, + consistency: completedEvent.consistency, + timestamp: completedEvent.timestamp, uid, discordAvatar: user.discordAvatar, discordId: user.discordId, @@ -502,7 +507,7 @@ export async function addResult( ); } - const streak = await UserDAL.updateStreak(uid, result.timestamp); + const streak = await UserDAL.updateStreak(uid, completedEvent.timestamp); const shouldGetBadge = streak >= 365 && @@ -532,7 +537,7 @@ export async function addResult( } const xpGained = await calculateXp( - result, + completedEvent, req.ctx.configuration.users.xp, uid, user.xp ?? 0, @@ -545,7 +550,7 @@ export async function addResult( "Calculated XP is negative", JSON.stringify({ xpGained, - result, + result: completedEvent, }), uid ); @@ -583,32 +588,20 @@ export async function addResult( ); } - if (result.bailedOut === false) delete result.bailedOut; - if (result.blindMode === false) delete result.blindMode; - if (result.lazyMode === false) delete result.lazyMode; - if (result.difficulty === "normal") delete result.difficulty; - if (result.funbox === "none") delete result.funbox; - if (result.language === "english") delete result.language; - if (result.numbers === false) delete result.numbers; - if (result.punctuation === false) delete result.punctuation; - if (result.mode !== "custom") delete result.customText; - if (result.restartCount === 0) delete result.restartCount; - if (result.incompleteTestSeconds === 0) delete result.incompleteTestSeconds; - if (result.afkDuration === 0) delete result.afkDuration; - if (result.tags.length === 0) delete result.tags; - - delete result.incompleteTests; + const dbresult = buildDbResult(completedEvent, user.name, isPb); - const addedResult = await ResultDAL.addResult(uid, result); + const addedResult = await ResultDAL.addResult(uid, dbresult); await UserDAL.incrementXp(uid, xpGained.xp); if (isPb) { Logger.logToDb( "user_new_pb", - `${result.mode + " " + result.mode2} ${result.wpm} ${result.acc}% ${ - result.rawWpm - } ${result.consistency}% (${addedResult.insertedId})`, + `${completedEvent.mode + " " + completedEvent.mode2} ${ + completedEvent.wpm + } ${completedEvent.acc}% ${completedEvent.rawWpm} ${ + completedEvent.consistency + }% (${addedResult.insertedId})`, uid ); } @@ -631,7 +624,7 @@ export async function addResult( data.weeklyXpLeaderboardRank = weeklyXpLeaderboardRank; } - incrementResult(result); + incrementResult(completedEvent); return new MonkeyResponse("Result saved", data); } diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 1b88bd3445b7..2509f1035ccd 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -571,7 +571,7 @@ export async function updateLbMemory( ): Promise { const { uid } = req.ctx.decodedToken; const { mode, language, rank } = req.body; - const mode2 = req.body.mode2 as MonkeyTypes.Mode2; + const mode2 = req.body.mode2 as SharedTypes.Mode2; await UserDAL.updateLbMemory(uid, mode, mode2, language, rank); return new MonkeyResponse("Leaderboard memory updated"); diff --git a/backend/src/dal/result.ts b/backend/src/dal/result.ts index cf13943b4e08..dd8b1b872ff8 100644 --- a/backend/src/dal/result.ts +++ b/backend/src/dal/result.ts @@ -5,7 +5,7 @@ import * as db from "../init/db"; import { getUser, getTags } from "./user"; -type MonkeyTypesResult = MonkeyTypes.Result; +type MonkeyTypesResult = SharedTypes.DBResult; export async function addResult( uid: string, diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index f6344a47b63f..01d803fd86b1 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -10,6 +10,8 @@ import { flattenObjectDeep, isToday, isYesterday } from "../utils/misc"; const SECONDS_PER_HOUR = 3600; +type Result = Omit, "_id" | "name">; + // Export for use in tests export const getUsersCollection = (): Collection> => db.collection("users"); @@ -230,7 +232,7 @@ export async function isDiscordIdAvailable( export async function addResultFilterPreset( uid: string, - filter: MonkeyTypes.ResultFilters, + filter: SharedTypes.ResultFilters, maxFiltersPerUser: number ): Promise { // ensure limit not reached @@ -261,8 +263,8 @@ export async function removeResultFilterPreset( const filterId = new ObjectId(_id); if ( user.resultFilterPresets === undefined || - user.resultFilterPresets.filter((t) => t._id.toHexString() === _id) - .length === 0 + user.resultFilterPresets.filter((t) => t._id.toString() === _id).length === + 0 ) { throw new MonkeyError(404, "Custom filter not found"); } @@ -383,8 +385,8 @@ export async function removeTagPb(uid: string, _id: string): Promise { export async function updateLbMemory( uid: string, - mode: MonkeyTypes.Mode, - mode2: MonkeyTypes.Mode2, + mode: SharedTypes.Mode, + mode2: SharedTypes.Mode2, language: string, rank: number ): Promise { @@ -406,7 +408,7 @@ export async function updateLbMemory( export async function checkIfPb( uid: string, user: MonkeyTypes.User, - result: MonkeyTypes.Result + result: Result ): Promise { const { mode } = result; @@ -448,7 +450,7 @@ export async function checkIfPb( export async function checkIfTagPb( uid: string, user: MonkeyTypes.User, - result: MonkeyTypes.Result + result: Result ): Promise { if (user.tags === undefined || user.tags.length === 0) { return []; @@ -463,11 +465,11 @@ export async function checkIfTagPb( const tagsToCheck: MonkeyTypes.UserTag[] = []; user.tags.forEach((userTag) => { - resultTags.forEach((resultTag) => { + for (const resultTag of resultTags ?? []) { if (resultTag === userTag._id.toHexString()) { tagsToCheck.push(userTag); } - }); + } }); const ret: string[] = []; @@ -676,7 +678,7 @@ export async function getPersonalBests( uid: string, mode: string, mode2?: string -): Promise { +): Promise { const user = await getUser(uid, "get personal bests"); if (mode2) { diff --git a/backend/src/types/types.d.ts b/backend/src/types/types.d.ts index 1852728882c4..60edbac3d43d 100644 --- a/backend/src/types/types.d.ts +++ b/backend/src/types/types.d.ts @@ -71,7 +71,7 @@ declare namespace MonkeyTypes { lbPersonalBests?: LbPersonalBests; name: string; customThemes?: CustomTheme[]; - personalBests: PersonalBests; + personalBests: SharedTypes.PersonalBests; quoteRatings?: UserQuoteRatings; startedTests?: number; tags?: UserTag[]; @@ -86,7 +86,7 @@ declare namespace MonkeyTypes { favoriteQuotes?: Record; needsToChangeName?: boolean; discordAvatar?: string; - resultFilterPresets?: ResultFilters[]; + resultFilterPresets?: WithObjectIdArray; profileDetails?: UserProfileDetails; inventory?: UserInventory; xp?: number; @@ -114,89 +114,36 @@ declare namespace MonkeyTypes { selected?: boolean; } - interface ResultFilters { - _id: ObjectId; - name: string; - difficulty: { - normal: boolean; - expert: boolean; - master: boolean; - }; - mode: { - words: boolean; - time: boolean; - quote: boolean; - zen: boolean; - custom: boolean; - }; - words: { - 10: boolean; - 25: boolean; - 50: boolean; - 100: boolean; - custom: boolean; - }; - time: { - 15: boolean; - 30: boolean; - 60: boolean; - 120: boolean; - custom: boolean; - }; - quoteLength: { - short: boolean; - medium: boolean; - long: boolean; - thicc: boolean; - }; - punctuation: { - on: boolean; - off: boolean; - }; - numbers: { - on: boolean; - off: boolean; - }; - date: { - last_day: boolean; - last_week: boolean; - last_month: boolean; - last_3months: boolean; - all: boolean; - }; - tags: { - [tagId: string]: boolean; - }; - language: { - [language: string]: boolean; - }; - funbox: { - none: boolean; - [funbox: string]: boolean; - }; - } - type UserQuoteRatings = Record>; interface LbPersonalBests { time: { [key: number]: { - [key: string]: PersonalBest; + [key: string]: SharedTypes.PersonalBest; }; }; } + type WithObjectId = Omit & { + _id: ObjectId; + }; + + type WithObjectIdArray = Omit & + { + _id: ObjectId; + }[]; + interface UserTag { _id: ObjectId; name: string; - personalBests: PersonalBests; + personalBests: SharedTypes.PersonalBests; } interface LeaderboardEntry { _id: ObjectId; acc: number; consistency: number; - difficulty: Difficulty; + difficulty: SharedTypes.Difficulty; lazyMode: boolean; language: string; punctuation: boolean; @@ -238,105 +185,6 @@ declare namespace MonkeyTypes { approved: boolean; } - type Mode = keyof PersonalBests; - - type Mode2 = keyof PersonalBests[M]; - - type StringNumber = `${number}`; - - type Difficulty = "normal" | "expert" | "master"; - - interface PersonalBest { - acc: number; - consistency: number; - difficulty: Difficulty; - lazyMode: boolean; - language: string; - punctuation: boolean; - raw: number; - wpm: number; - timestamp: number; - } - - interface PersonalBests { - time: Record; - words: Record; - quote: Record; - custom: Partial>; - zen: Partial>; - } - - interface ChartData { - wpm: number[]; - raw: number[]; - err: number[]; - } - - interface KeyStats { - average: number; - sd: number; - } - - interface IncompleteTest { - acc: number; - seconds: number; - } - - interface Result { - _id: ObjectId; - wpm: number; - rawWpm: number; - charStats: number[]; - correctChars?: number; // -------------- - incorrectChars?: number; // legacy results - acc: number; - mode: M; - mode2: Mode2; - quoteLength: number; - timestamp: number; - restartCount: number; - incompleteTestSeconds: number; - incompleteTests: IncompleteTest[]; - testDuration: number; - afkDuration: number; - tags: string[]; - consistency: number; - keyConsistency: number; - chartData: ChartData | "toolong"; - uid: string; - keySpacingStats: KeyStats; - keyDurationStats: KeyStats; - isPb?: boolean; - bailedOut?: boolean; - blindMode?: boolean; - lazyMode?: boolean; - difficulty: Difficulty; - funbox?: string; - language: string; - numbers?: boolean; - punctuation?: boolean; - hash?: string; - } - - interface CompletedEvent extends MonkeyTypes.Result { - keySpacing: number[] | "toolong"; - keyDuration: number[] | "toolong"; - customText: MonkeyTypes.CustomText; - wpmConsistency: number; - lang: string; - challenge?: string | null; - } - - interface CustomText { - text: string[]; - isWordRandom: boolean; - isTimeRandom: boolean; - word: number; - time: number; - delimiter: string; - textLen?: number; - } - interface PSA { sticky?: boolean; message: string; diff --git a/backend/src/utils/pb.ts b/backend/src/utils/pb.ts index 45a4621296fa..036dd5ce7507 100644 --- a/backend/src/utils/pb.ts +++ b/backend/src/utils/pb.ts @@ -3,15 +3,13 @@ import FunboxList from "../constants/funbox-list"; interface CheckAndUpdatePbResult { isPb: boolean; - personalBests: MonkeyTypes.PersonalBests; + personalBests: SharedTypes.PersonalBests; lbPersonalBests?: MonkeyTypes.LbPersonalBests; } -type Result = MonkeyTypes.Result; +type Result = Omit, "_id" | "name">; -export function canFunboxGetPb( - result: MonkeyTypes.Result -): boolean { +export function canFunboxGetPb(result: Result): boolean { const funbox = result.funbox; if (!funbox || funbox === "none") return true; @@ -29,19 +27,19 @@ export function canFunboxGetPb( } export function checkAndUpdatePb( - userPersonalBests: MonkeyTypes.PersonalBests, + userPersonalBests: SharedTypes.PersonalBests, lbPersonalBests: MonkeyTypes.LbPersonalBests | undefined, result: Result ): CheckAndUpdatePbResult { const mode = result.mode; - const mode2 = result.mode2 as MonkeyTypes.Mode2<"time">; + const mode2 = result.mode2 as SharedTypes.Mode2<"time">; const userPb = userPersonalBests ?? {}; userPb[mode] ??= {}; userPb[mode][mode2] ??= []; const personalBestMatch = userPb[mode][mode2].find( - (pb: MonkeyTypes.PersonalBest) => matchesPersonalBest(result, pb) + (pb: SharedTypes.PersonalBest) => matchesPersonalBest(result, pb) ); let isPb = true; @@ -66,7 +64,7 @@ export function checkAndUpdatePb( function matchesPersonalBest( result: Result, - personalBest: MonkeyTypes.PersonalBest + personalBest: SharedTypes.PersonalBest ): boolean { if ( result.difficulty === undefined || @@ -88,7 +86,7 @@ function matchesPersonalBest( } function updatePersonalBest( - personalBest: MonkeyTypes.PersonalBest, + personalBest: SharedTypes.PersonalBest, result: Result ): boolean { if (personalBest.wpm >= result.wpm) { @@ -121,7 +119,7 @@ function updatePersonalBest( return true; } -function buildPersonalBest(result: Result): MonkeyTypes.PersonalBest { +function buildPersonalBest(result: Result): SharedTypes.PersonalBest { if ( result.difficulty === undefined || result.language === undefined || @@ -148,7 +146,7 @@ function buildPersonalBest(result: Result): MonkeyTypes.PersonalBest { } function updateLeaderboardPersonalBests( - userPersonalBests: MonkeyTypes.PersonalBests, + userPersonalBests: SharedTypes.PersonalBests, lbPersonalBests: MonkeyTypes.LbPersonalBests, result: Result ): void { @@ -157,7 +155,7 @@ function updateLeaderboardPersonalBests( } const mode = result.mode; - const mode2 = result.mode2 as MonkeyTypes.Mode2<"time">; + const mode2 = result.mode2 as SharedTypes.Mode2<"time">; lbPersonalBests[mode] = lbPersonalBests[mode] ?? {}; const lbMode2 = lbPersonalBests[mode][mode2]; @@ -167,7 +165,7 @@ function updateLeaderboardPersonalBests( const bestForEveryLanguage = {}; - userPersonalBests[mode][mode2].forEach((pb: MonkeyTypes.PersonalBest) => { + userPersonalBests[mode][mode2].forEach((pb: SharedTypes.PersonalBest) => { const language = pb.language; if ( !bestForEveryLanguage[language] || @@ -179,7 +177,7 @@ function updateLeaderboardPersonalBests( _.each( bestForEveryLanguage, - (pb: MonkeyTypes.PersonalBest, language: string) => { + (pb: SharedTypes.PersonalBest, language: string) => { const languageDoesNotExist = !lbPersonalBests[mode][mode2][language]; if ( diff --git a/backend/src/utils/prometheus.ts b/backend/src/utils/prometheus.ts index 07e0a1c7b322..9db873b1225b 100644 --- a/backend/src/utils/prometheus.ts +++ b/backend/src/utils/prometheus.ts @@ -89,7 +89,7 @@ export function setLeaderboard( } export function incrementResult( - res: MonkeyTypes.Result + res: SharedTypes.Result ): void { const { mode, diff --git a/backend/src/utils/result.ts b/backend/src/utils/result.ts new file mode 100644 index 000000000000..ddccc2fd3342 --- /dev/null +++ b/backend/src/utils/result.ts @@ -0,0 +1,63 @@ +import { ObjectId } from "mongodb"; + +type Result = SharedTypes.DBResult; + +export function buildDbResult( + completedEvent: SharedTypes.CompletedEvent, + userName: string, + isPb: boolean +): Result { + const ce = completedEvent; + const res: Result = { + _id: new ObjectId(), + uid: ce.uid, + wpm: ce.wpm, + rawWpm: ce.rawWpm, + charStats: ce.charStats, + acc: ce.acc, + mode: ce.mode, + mode2: ce.mode2, + quoteLength: ce.quoteLength, + timestamp: ce.timestamp, + restartCount: ce.restartCount, + incompleteTestSeconds: ce.incompleteTestSeconds, + testDuration: ce.testDuration, + afkDuration: ce.afkDuration, + tags: ce.tags, + consistency: ce.consistency, + keyConsistency: ce.keyConsistency, + chartData: ce.chartData, + language: ce.language, + lazyMode: ce.lazyMode, + difficulty: ce.difficulty, + funbox: ce.funbox, + numbers: ce.numbers, + punctuation: ce.punctuation, + keySpacingStats: ce.keySpacingStats, + keyDurationStats: ce.keyDurationStats, + isPb: isPb, + bailedOut: ce.bailedOut, + blindMode: ce.blindMode, + name: userName, + }; + + if (ce.bailedOut === false) delete res.bailedOut; + if (ce.blindMode === false) delete res.blindMode; + if (ce.lazyMode === false) delete res.lazyMode; + if (ce.difficulty === "normal") delete res.difficulty; + if (ce.funbox === "none") delete res.funbox; + if (ce.language === "english") delete res.language; + if (ce.numbers === false) delete res.numbers; + if (ce.punctuation === false) delete res.punctuation; + if (ce.mode !== "custom") delete res.customText; + if (ce.mode !== "quote") delete res.quoteLength; + if (ce.restartCount === 0) delete res.restartCount; + if (ce.incompleteTestSeconds === 0) delete res.incompleteTestSeconds; + if (ce.afkDuration === 0) delete res.afkDuration; + if (ce.tags.length === 0) delete res.tags; + + if (ce.keySpacingStats === undefined) delete res.keySpacingStats; + if (ce.keyDurationStats === undefined) delete res.keyDurationStats; + + return res; +} diff --git a/backend/src/utils/validation.ts b/backend/src/utils/validation.ts index fd742e887187..ebb14515cfd4 100644 --- a/backend/src/utils/validation.ts +++ b/backend/src/utils/validation.ts @@ -57,7 +57,7 @@ export function isTagPresetNameValid(name: string): boolean { return VALID_NAME_PATTERN.test(name); } -export function isTestTooShort(result: MonkeyTypes.CompletedEvent): boolean { +export function isTestTooShort(result: SharedTypes.CompletedEvent): boolean { const { mode, mode2, customText, testDuration, bailedOut } = result; if (mode === "time") { diff --git a/frontend/src/ts/account/mini-result-chart.ts b/frontend/src/ts/account/mini-result-chart.ts index 8d77e2b5978b..30b040fde5bf 100644 --- a/frontend/src/ts/account/mini-result-chart.ts +++ b/frontend/src/ts/account/mini-result-chart.ts @@ -16,7 +16,7 @@ function hide(): void { $(".pageAccount .miniResultChartBg").stop(true, true).fadeOut(125); } -export function updateData(data: MonkeyTypes.ChartData): void { +export function updateData(data: SharedTypes.ChartData): void { // let data = filteredResults[filteredId].chartData; let labels = []; for (let i = 1; i <= data.wpm.length; i++) { diff --git a/frontend/src/ts/account/pb-tables.ts b/frontend/src/ts/account/pb-tables.ts index aea742110104..1153c1d9ef29 100644 --- a/frontend/src/ts/account/pb-tables.ts +++ b/frontend/src/ts/account/pb-tables.ts @@ -81,7 +81,7 @@ function clearTables(isProfile: boolean): void { } export function update( - personalBests?: MonkeyTypes.PersonalBests, + personalBests?: SharedTypes.PersonalBests, isProfile = false ): void { clearTables(isProfile); @@ -94,8 +94,8 @@ export function update( $(`.page${source} .profile .pbsTime`).html(""); $(`.page${source} .profile .pbsWords`).html(""); - const timeMode2s: MonkeyTypes.Mode2<"time">[] = ["15", "30", "60", "120"]; - const wordMode2s: MonkeyTypes.Mode2<"words">[] = ["10", "25", "50", "100"]; + const timeMode2s: SharedTypes.Mode2<"time">[] = ["15", "30", "60", "120"]; + const wordMode2s: SharedTypes.Mode2<"words">[] = ["10", "25", "50", "100"]; timeMode2s.forEach((mode2) => { text += buildPbHtml(personalBests, "time", mode2); @@ -122,9 +122,9 @@ export function update( } function buildPbHtml( - pbs: MonkeyTypes.PersonalBests, + pbs: SharedTypes.PersonalBests, mode: "time" | "words", - mode2: MonkeyTypes.StringNumber + mode2: SharedTypes.StringNumber ): string { let retval = ""; let dateText = ""; diff --git a/frontend/src/ts/account/result-filters.ts b/frontend/src/ts/account/result-filters.ts index fdcf3ed75bc8..602abe35cb52 100644 --- a/frontend/src/ts/account/result-filters.ts +++ b/frontend/src/ts/account/result-filters.ts @@ -6,7 +6,7 @@ import Ape from "../ape/index"; import * as Loader from "../elements/loader"; import { showNewResultFilterPresetPopup } from "../popups/new-result-filter-preset-popup"; -export const defaultResultFilters: MonkeyTypes.ResultFilters = { +export const defaultResultFilters: SharedTypes.ResultFilters = { _id: "default-result-filters-id", name: "default result filters", pb: { @@ -198,12 +198,12 @@ export async function setFilterPreset(id: string): Promise { } function deepCopyFilter( - filter: MonkeyTypes.ResultFilters -): MonkeyTypes.ResultFilters { + filter: SharedTypes.ResultFilters +): SharedTypes.ResultFilters { return JSON.parse(JSON.stringify(filter)); } -function addFilterPresetToSnapshot(filter: MonkeyTypes.ResultFilters): void { +function addFilterPresetToSnapshot(filter: SharedTypes.ResultFilters): void { const snapshot = DB.getSnapshot(); if (!snapshot) return; DB.setSnapshot({ @@ -270,13 +270,13 @@ function deSelectFilterPreset(): void { ).removeClass("active"); } -function getFilters(): MonkeyTypes.ResultFilters { +function getFilters(): SharedTypes.ResultFilters { return filters; } -function getGroup( +function getGroup( group: G -): MonkeyTypes.ResultFilters[G] { +): SharedTypes.ResultFilters[G] { return filters[group]; } @@ -284,15 +284,15 @@ function getGroup( // filters[group][filter] = value; // } -export function getFilter( +export function getFilter( group: G, filter: MonkeyTypes.Filter -): MonkeyTypes.ResultFilters[G][MonkeyTypes.Filter] { +): SharedTypes.ResultFilters[G][MonkeyTypes.Filter] { return filters[group][filter]; } function setAllFilters( - group: keyof MonkeyTypes.ResultFilters, + group: keyof SharedTypes.ResultFilters, value: boolean ): void { Object.keys(getGroup(group)).forEach((filter) => { @@ -313,7 +313,7 @@ export function reset(): void { } type AboveChartDisplay = Partial< - Record + Record >; export function updateActive(): void { @@ -359,7 +359,7 @@ export function updateActive(): void { }); }); - function addText(group: keyof MonkeyTypes.ResultFilters): string { + function addText(group: keyof SharedTypes.ResultFilters): string { let ret = ""; ret += "
"; if (group === "difficulty") { @@ -457,7 +457,7 @@ export function updateActive(): void { }, 0); } -function toggle( +function toggle( group: G, filter: MonkeyTypes.Filter ): void { @@ -470,7 +470,7 @@ function toggle( } const newValue = !filters[group][ filter - ] as unknown as MonkeyTypes.ResultFilters[G][MonkeyTypes.Filter]; + ] as unknown as SharedTypes.ResultFilters[G][MonkeyTypes.Filter]; filters[group][filter] = newValue; save(); } catch (e) { @@ -490,7 +490,7 @@ $( ).on("click", "button", (e) => { const group = $(e.target) .parents(".buttons") - .attr("group") as keyof MonkeyTypes.ResultFilters; + .attr("group") as keyof SharedTypes.ResultFilters; const filter = $(e.target).attr("filter") as MonkeyTypes.Filter; if ($(e.target).hasClass("allFilters")) { Misc.typedKeys(getFilters()).forEach((group) => { @@ -737,11 +737,11 @@ $(".group.presetFilterButtons .filterBtns").on( ); function verifyResultFiltersStructure( - filterIn: MonkeyTypes.ResultFilters -): MonkeyTypes.ResultFilters { + filterIn: SharedTypes.ResultFilters +): SharedTypes.ResultFilters { const filter = deepCopyFilter(filterIn); Object.entries(defaultResultFilters).forEach((entry) => { - const key = entry[0] as keyof MonkeyTypes.ResultFilters; + const key = entry[0] as keyof SharedTypes.ResultFilters; const value = entry[1]; if (filter[key] === undefined) { filter[key] = value; diff --git a/frontend/src/ts/ape/endpoints/leaderboards.ts b/frontend/src/ts/ape/endpoints/leaderboards.ts index 1fa7ad5839e3..e2e5ac1d58b4 100644 --- a/frontend/src/ts/ape/endpoints/leaderboards.ts +++ b/frontend/src/ts/ape/endpoints/leaderboards.ts @@ -2,7 +2,7 @@ const BASE_PATH = "/leaderboards"; interface LeaderboardQuery { language: string; - mode: MonkeyTypes.Mode; + mode: SharedTypes.Mode; mode2: string; isDaily?: boolean; daysBefore?: number; diff --git a/frontend/src/ts/ape/endpoints/results.ts b/frontend/src/ts/ape/endpoints/results.ts index c6dbd6268f89..2e5c6594845e 100644 --- a/frontend/src/ts/ape/endpoints/results.ts +++ b/frontend/src/ts/ape/endpoints/results.ts @@ -10,7 +10,7 @@ export default class Results { } async save( - result: MonkeyTypes.Result + result: SharedTypes.Result ): Ape.EndpointResponse { return await this.httpClient.post(BASE_PATH, { payload: { result }, diff --git a/frontend/src/ts/ape/endpoints/users.ts b/frontend/src/ts/ape/endpoints/users.ts index 1fc1f0556a4d..93e383175739 100644 --- a/frontend/src/ts/ape/endpoints/users.ts +++ b/frontend/src/ts/ape/endpoints/users.ts @@ -47,9 +47,9 @@ export default class Users { }); } - async updateLeaderboardMemory( + async updateLeaderboardMemory( mode: string, - mode2: MonkeyTypes.Mode2, + mode2: SharedTypes.Mode2, language: string, rank: number ): Ape.EndpointResponse { @@ -82,7 +82,7 @@ export default class Users { } async addResultFilterPreset( - filter: MonkeyTypes.ResultFilters + filter: SharedTypes.ResultFilters ): Ape.EndpointResponse { return await this.httpClient.post(`${BASE_PATH}/resultFilterPresets`, { payload: filter, diff --git a/frontend/src/ts/config.ts b/frontend/src/ts/config.ts index 0760c22a12dc..f685baba30b9 100644 --- a/frontend/src/ts/config.ts +++ b/frontend/src/ts/config.ts @@ -105,7 +105,7 @@ export function setPunctuation(punc: boolean, nosave?: boolean): boolean { return true; } -export function setMode(mode: MonkeyTypes.Mode, nosave?: boolean): boolean { +export function setMode(mode: SharedTypes.Mode, nosave?: boolean): boolean { if ( !isConfigValueValid("mode", mode, [ ["time", "words", "quote", "zen", "custom"], @@ -205,7 +205,7 @@ export function setSoundVolume( //difficulty export function setDifficulty( - diff: MonkeyTypes.Difficulty, + diff: SharedTypes.Difficulty, nosave?: boolean ): boolean { if ( diff --git a/frontend/src/ts/controllers/challenge-controller.ts b/frontend/src/ts/controllers/challenge-controller.ts index 02a25fe4fd4a..ab6730a18b14 100644 --- a/frontend/src/ts/controllers/challenge-controller.ts +++ b/frontend/src/ts/controllers/challenge-controller.ts @@ -23,7 +23,7 @@ export function clearActive(): void { } export function verify( - result: MonkeyTypes.Result + result: SharedTypes.Result ): string | null { try { if (TestState.activeChallenge) { @@ -295,10 +295,10 @@ export async function setup(challengeName: string): Promise { } else if (challenge.parameters[1] === "time") { UpdateConfig.setTimeConfig(challenge.parameters[2] as number, true); } - UpdateConfig.setMode(challenge.parameters[1] as MonkeyTypes.Mode, true); + UpdateConfig.setMode(challenge.parameters[1] as SharedTypes.Mode, true); if (challenge.parameters[3] !== undefined) { UpdateConfig.setDifficulty( - challenge.parameters[3] as MonkeyTypes.Difficulty, + challenge.parameters[3] as SharedTypes.Difficulty, true ); } diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 0bcfa9f98a3c..41c891331062 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -96,7 +96,7 @@ export async function initSnapshot(): Promise< }; for (const mode of ["time", "words", "quote", "zen", "custom"]) { - snap.personalBests[mode as keyof MonkeyTypes.PersonalBests] ??= {}; + snap.personalBests[mode as keyof SharedTypes.PersonalBests] ??= {}; } snap.banned = userData.banned; @@ -162,9 +162,10 @@ export async function initSnapshot(): Promise< // } // LoadingPage.updateText("Downloading tags..."); snap.customThemes = userData.customThemes ?? []; - snap.tags = userData.tags || []; - snap.tags.forEach((tag) => { + const userDataTags: MonkeyTypes.Tag[] = userData.tags ?? []; + + userDataTags.forEach((tag) => { tag.display = tag.name.replaceAll("_", " "); tag.personalBests ??= { time: {}, @@ -175,10 +176,12 @@ export async function initSnapshot(): Promise< }; for (const mode of ["time", "words", "quote", "zen", "custom"]) { - tag.personalBests[mode as keyof MonkeyTypes.PersonalBests] ??= {}; + tag.personalBests[mode as keyof SharedTypes.PersonalBests] ??= {}; } }); + snap.tags = userDataTags; + snap.tags = snap.tags?.sort((a, b) => { if (a.name > b.name) { return 1; @@ -188,6 +191,7 @@ export async function initSnapshot(): Promise< return 0; } }); + // if (ActivePage.get() === "loading") { // LoadingPage.updateBar(90); // } else { @@ -245,7 +249,7 @@ export async function getUserResults(offset?: number): Promise { return false; } - const results = response.data as MonkeyTypes.Result[]; + const results = response.data as SharedTypes.DBResult[]; results?.sort((a, b) => b.timestamp - a.timestamp); results.forEach((result) => { if (result.bailedOut === undefined) result.bailedOut = false; @@ -265,6 +269,13 @@ export async function getUserResults(offset?: number): Promise { } if (result.afkDuration === undefined) result.afkDuration = 0; if (result.tags === undefined) result.tags = []; + + if ( + result.correctChars !== undefined && + result.incorrectChars !== undefined + ) { + result.charStats = [result.correctChars, result.incorrectChars, 0, 0]; + } }); if (dbSnapshot.results !== undefined && dbSnapshot.results.length > 0) { @@ -274,9 +285,12 @@ export async function getUserResults(offset?: number): Promise { const resultsWithoutDuplicates = results.filter( (it) => it.timestamp < oldestTimestamp ); - dbSnapshot.results.push(...resultsWithoutDuplicates); + dbSnapshot.results.push( + ...(resultsWithoutDuplicates as unknown as SharedTypes.Result[]) + ); } else { - dbSnapshot.results = results; + dbSnapshot.results = + results as unknown as SharedTypes.Result[]; } return true; } @@ -367,12 +381,12 @@ export async function deleteCustomTheme(themeId: string): Promise { return true; } -async function _getUserHighestWpm( +async function _getUserHighestWpm( mode: M, - mode2: MonkeyTypes.Mode2, + mode2: SharedTypes.Mode2, punctuation: boolean, language: string, - difficulty: MonkeyTypes.Difficulty, + difficulty: SharedTypes.Difficulty, lazyMode: boolean ): Promise { function cont(): number { @@ -401,12 +415,12 @@ async function _getUserHighestWpm( return retval; } -export async function getUserAverage10( +export async function getUserAverage10( mode: M, - mode2: MonkeyTypes.Mode2, + mode2: SharedTypes.Mode2, punctuation: boolean, language: string, - difficulty: MonkeyTypes.Difficulty, + difficulty: SharedTypes.Difficulty, lazyMode: boolean ): Promise<[number, number]> { const snapshot = getSnapshot(); @@ -484,12 +498,12 @@ export async function getUserAverage10( return retval; } -export async function getUserDailyBest( +export async function getUserDailyBest( mode: M, - mode2: MonkeyTypes.Mode2, + mode2: SharedTypes.Mode2, punctuation: boolean, language: string, - difficulty: MonkeyTypes.Difficulty, + difficulty: SharedTypes.Difficulty, lazyMode: boolean ): Promise { const snapshot = getSnapshot(); @@ -547,12 +561,12 @@ export async function getUserDailyBest( return retval; } -export async function getLocalPB( +export async function getLocalPB( mode: M, - mode2: MonkeyTypes.Mode2, + mode2: SharedTypes.Mode2, punctuation: boolean, language: string, - difficulty: MonkeyTypes.Difficulty, + difficulty: SharedTypes.Difficulty, lazyMode: boolean, funbox: string ): Promise { @@ -572,7 +586,7 @@ export async function getLocalPB( ( dbSnapshot.personalBests[mode][ mode2 - ] as unknown as MonkeyTypes.PersonalBest[] + ] as unknown as SharedTypes.PersonalBest[] ).forEach((pb) => { if ( pb.punctuation === punctuation && @@ -596,12 +610,12 @@ export async function getLocalPB( return retval; } -export async function saveLocalPB( +export async function saveLocalPB( mode: M, - mode2: MonkeyTypes.Mode2, + mode2: SharedTypes.Mode2, punctuation: boolean, language: string, - difficulty: MonkeyTypes.Difficulty, + difficulty: SharedTypes.Difficulty, lazyMode: boolean, wpm: number, acc: number, @@ -627,12 +641,12 @@ export async function saveLocalPB( }; dbSnapshot.personalBests[mode][mode2] ??= - [] as unknown as MonkeyTypes.PersonalBests[M][MonkeyTypes.Mode2]; + [] as unknown as SharedTypes.PersonalBests[M][SharedTypes.Mode2]; ( dbSnapshot.personalBests[mode][ mode2 - ] as unknown as MonkeyTypes.PersonalBest[] + ] as unknown as SharedTypes.PersonalBest[] ).forEach((pb) => { if ( pb.punctuation === punctuation && @@ -655,7 +669,7 @@ export async function saveLocalPB( ( dbSnapshot.personalBests[mode][ mode2 - ] as unknown as MonkeyTypes.PersonalBest[] + ] as unknown as SharedTypes.PersonalBest[] ).push({ language, difficulty, @@ -675,13 +689,13 @@ export async function saveLocalPB( } } -export async function getLocalTagPB( +export async function getLocalTagPB( tagId: string, mode: M, - mode2: MonkeyTypes.Mode2, + mode2: SharedTypes.Mode2, punctuation: boolean, language: string, - difficulty: MonkeyTypes.Difficulty, + difficulty: SharedTypes.Difficulty, lazyMode: boolean ): Promise { function cont(): number { @@ -706,10 +720,10 @@ export async function getLocalTagPB( }; filteredtag.personalBests[mode][mode2] ??= - [] as unknown as MonkeyTypes.PersonalBests[M][MonkeyTypes.Mode2]; + [] as unknown as SharedTypes.PersonalBests[M][SharedTypes.Mode2]; const personalBests = (filteredtag.personalBests[mode][mode2] ?? - []) as MonkeyTypes.PersonalBest[]; + []) as SharedTypes.PersonalBest[]; ret = personalBests.find( @@ -729,13 +743,13 @@ export async function getLocalTagPB( return retval; } -export async function saveLocalTagPB( +export async function saveLocalTagPB( tagId: string, mode: M, - mode2: MonkeyTypes.Mode2, + mode2: SharedTypes.Mode2, punctuation: boolean, language: string, - difficulty: MonkeyTypes.Difficulty, + difficulty: SharedTypes.Difficulty, lazyMode: boolean, wpm: number, acc: number, @@ -762,7 +776,7 @@ export async function saveLocalTagPB( }; filteredtag.personalBests[mode][mode2] ??= - [] as unknown as MonkeyTypes.PersonalBests[M][MonkeyTypes.Mode2]; + [] as unknown as SharedTypes.PersonalBests[M][SharedTypes.Mode2]; try { let found = false; @@ -770,7 +784,7 @@ export async function saveLocalTagPB( ( filteredtag.personalBests[mode][ mode2 - ] as unknown as MonkeyTypes.PersonalBest[] + ] as unknown as SharedTypes.PersonalBest[] ).forEach((pb) => { if ( pb.punctuation === punctuation && @@ -793,7 +807,7 @@ export async function saveLocalTagPB( ( filteredtag.personalBests[mode][ mode2 - ] as unknown as MonkeyTypes.PersonalBest[] + ] as unknown as SharedTypes.PersonalBest[] ).push({ language, difficulty, @@ -827,7 +841,7 @@ export async function saveLocalTagPB( timestamp: Date.now(), consistency: consistency, }, - ] as unknown as MonkeyTypes.PersonalBests[M][MonkeyTypes.Mode2]; + ] as unknown as SharedTypes.PersonalBests[M][SharedTypes.Mode2]; } } @@ -838,9 +852,9 @@ export async function saveLocalTagPB( return; } -export async function updateLbMemory( +export async function updateLbMemory( mode: M, - mode2: MonkeyTypes.Mode2, + mode2: SharedTypes.Mode2, language: string, rank: number, api = false @@ -884,7 +898,7 @@ export async function saveConfig(config: MonkeyTypes.Config): Promise { } export function saveLocalResult( - result: MonkeyTypes.Result + result: SharedTypes.Result ): void { const snapshot = getSnapshot(); if (!snapshot) return; diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index 2f40ecd6e26c..8c3221f05937 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -33,7 +33,7 @@ export function toggleFilterDebug(): void { } } -let filteredResults: MonkeyTypes.Result[] = []; +let filteredResults: SharedTypes.Result[] = []; let visibleTableLines = 0; function loadMoreLines(lineIndex?: number): void { @@ -152,12 +152,7 @@ function loadMoreLines(lineIndex?: number): void { pb = ""; } - let charStats = "-"; - if (result.charStats) { - charStats = result.charStats.join("/"); - } else { - charStats = result.correctChars + "/" + result.incorrectChars + "/-/-"; - } + const charStats = result.charStats.join("/"); const date = new Date(result.timestamp); $(".pageAccount .history table tbody").append(` @@ -279,7 +274,7 @@ async function fillContent(): Promise { $(".pageAccount .history table tbody").empty(); DB.getSnapshot()?.results?.forEach( - (result: MonkeyTypes.Result) => { + (result: SharedTypes.Result) => { // totalSeconds += tt; //apply filters @@ -311,7 +306,7 @@ async function fillContent(): Promise { } if (result.mode === "time") { - let timefilter: MonkeyTypes.Mode2<"time"> | "custom" = "custom"; + let timefilter: SharedTypes.Mode2<"time"> | "custom" = "custom"; if ( ["15", "30", "60", "120"].includes( `${result.mode2}` //legacy results could have a number in mode2 @@ -331,7 +326,7 @@ async function fillContent(): Promise { return; } } else if (result.mode === "words") { - let wordfilter: MonkeyTypes.Mode2Custom<"words"> = "custom"; + let wordfilter: SharedTypes.Mode2Custom<"words"> = "custom"; if ( ["10", "25", "50", "100", "200"].includes( `${result.mode2}` //legacy results could have a number in mode2 @@ -1156,8 +1151,8 @@ function sortAndRefreshHistory( temp.push(filteredResults[idx]); parsedIndexes.push(idx); } - filteredResults = temp as MonkeyTypes.Result< - keyof MonkeyTypes.PersonalBests + filteredResults = temp as SharedTypes.Result< + keyof SharedTypes.PersonalBests >[]; $(".pageAccount .history table tbody").empty(); @@ -1207,7 +1202,7 @@ $(".pageAccount").on("click", ".miniResultChartButton", (event) => { const filteredId = $(event.currentTarget).attr("filteredResultsId"); if (filteredId === undefined) return; MiniResultChart.updateData( - filteredResults[parseInt(filteredId)]?.chartData as MonkeyTypes.ChartData + filteredResults[parseInt(filteredId)]?.chartData as SharedTypes.ChartData ); MiniResultChart.show(); MiniResultChart.updatePosition( diff --git a/frontend/src/ts/popups/mobile-test-config-popup.ts b/frontend/src/ts/popups/mobile-test-config-popup.ts index d5b067734b83..e3b4513a0cbc 100644 --- a/frontend/src/ts/popups/mobile-test-config-popup.ts +++ b/frontend/src/ts/popups/mobile-test-config-popup.ts @@ -174,7 +174,7 @@ el.find(".numbers").on("click", () => { el.find(".modeGroup button").on("click", (e) => { if ($(e.currentTarget).hasClass("active")) return; const mode = $(e.currentTarget).attr("data-mode"); - UpdateConfig.setMode(mode as MonkeyTypes.Mode); + UpdateConfig.setMode(mode as SharedTypes.Mode); ManualRestart.set(); TestLogic.restart(); }); diff --git a/frontend/src/ts/popups/pb-tables-popup.ts b/frontend/src/ts/popups/pb-tables-popup.ts index e9b3709c13ea..71f9780d8142 100644 --- a/frontend/src/ts/popups/pb-tables-popup.ts +++ b/frontend/src/ts/popups/pb-tables-popup.ts @@ -3,13 +3,13 @@ import format from "date-fns/format"; import * as Skeleton from "./skeleton"; import { isPopupVisible } from "../utils/misc"; -interface PersonalBest extends MonkeyTypes.PersonalBest { - mode2: MonkeyTypes.Mode2; +interface PersonalBest extends SharedTypes.PersonalBest { + mode2: SharedTypes.Mode2; } const wrapperId = "pbTablesPopupWrapper"; -function update(mode: MonkeyTypes.Mode): void { +function update(mode: SharedTypes.Mode): void { $("#pbTablesPopup table tbody").empty(); $($("#pbTablesPopup table thead tr td")[0] as HTMLElement).text(mode); @@ -23,7 +23,7 @@ function update(mode: MonkeyTypes.Mode): void { if (allmode2 === undefined) return; const list: PersonalBest[] = []; - (Object.keys(allmode2) as MonkeyTypes.Mode2[]).forEach( + (Object.keys(allmode2) as SharedTypes.Mode2[]).forEach( function (key) { let pbs = allmode2[key] ?? []; pbs = pbs.sort(function (a, b) { @@ -40,7 +40,7 @@ function update(mode: MonkeyTypes.Mode): void { } ); - let mode2memory: MonkeyTypes.Mode2; + let mode2memory: SharedTypes.Mode2; list.forEach((pb) => { let dateText = `-
-`; @@ -78,7 +78,7 @@ function update(mode: MonkeyTypes.Mode): void { }); } -function show(mode: MonkeyTypes.Mode): void { +function show(mode: SharedTypes.Mode): void { Skeleton.append(wrapperId); if (!isPopupVisible(wrapperId)) { update(mode); diff --git a/frontend/src/ts/popups/quote-rate-popup.ts b/frontend/src/ts/popups/quote-rate-popup.ts index ad45a0b470a7..cc961059ff6a 100644 --- a/frontend/src/ts/popups/quote-rate-popup.ts +++ b/frontend/src/ts/popups/quote-rate-popup.ts @@ -245,6 +245,10 @@ $("#quoteRatePopupWrapper .submitButton").on("click", () => { }); $(".pageTest #rateQuoteButton").on("click", async () => { + if (TestWords.randomQuote === null) { + Notifications.add("Failed to show quote rating popup: no quote", -1); + return; + } show(TestWords.randomQuote); }); diff --git a/frontend/src/ts/popups/quote-report-popup.ts b/frontend/src/ts/popups/quote-report-popup.ts index 459233859335..84e11fbd345d 100644 --- a/frontend/src/ts/popups/quote-report-popup.ts +++ b/frontend/src/ts/popups/quote-report-popup.ts @@ -168,6 +168,10 @@ $("#quoteReportPopupWrapper .submit").on("click", async () => { }); $(".pageTest #reportQuoteButton").on("click", async () => { + if (TestWords.randomQuote === null) { + Notifications.add("Failed to show quote report popup: no quote", -1); + return; + } show({ quoteId: TestWords.randomQuote?.id, noAnim: false, diff --git a/frontend/src/ts/popups/result-tags-popup.ts b/frontend/src/ts/popups/result-tags-popup.ts index 8f54ac634a0e..2737399a14dc 100644 --- a/frontend/src/ts/popups/result-tags-popup.ts +++ b/frontend/src/ts/popups/result-tags-popup.ts @@ -155,7 +155,7 @@ $("#resultEditTagsPanelWrapper .confirmButton").on("click", async () => { duration: 2, }); DB.getSnapshot()?.results?.forEach( - (result: MonkeyTypes.Result) => { + (result: SharedTypes.Result) => { if (result._id === resultId) { result.tags = newTags; } diff --git a/frontend/src/ts/popups/share-test-settings-popup.ts b/frontend/src/ts/popups/share-test-settings-popup.ts index c0271c052b08..0115a6b2dc2b 100644 --- a/frontend/src/ts/popups/share-test-settings-popup.ts +++ b/frontend/src/ts/popups/share-test-settings-popup.ts @@ -14,13 +14,13 @@ function getCheckboxValue(checkbox: string): boolean { } type SharedTestSettings = [ - MonkeyTypes.Mode | null, - MonkeyTypes.Mode2 | null, - MonkeyTypes.CustomText | null, + SharedTypes.Mode | null, + SharedTypes.Mode2 | null, + SharedTypes.CustomText | null, boolean | null, boolean | null, string | null, - MonkeyTypes.Difficulty | null, + SharedTypes.Difficulty | null, string | null ]; @@ -45,7 +45,7 @@ function updateURL(): void { settings[1] = getMode2( Config, randomQuote - ) as MonkeyTypes.Mode2; + ) as SharedTypes.Mode2; } if (getCheckboxValue("customText")) { diff --git a/frontend/src/ts/test/funbox/funbox.ts b/frontend/src/ts/test/funbox/funbox.ts index c88d7b76f979..b270771812a9 100644 --- a/frontend/src/ts/test/funbox/funbox.ts +++ b/frontend/src/ts/test/funbox/funbox.ts @@ -639,7 +639,7 @@ export async function activate(funbox?: string): Promise { if (check.result === false) { if (check.forcedConfigs && check.forcedConfigs.length > 0) { if (configKey === "mode") { - UpdateConfig.setMode(check.forcedConfigs[0] as MonkeyTypes.Mode); + UpdateConfig.setMode(check.forcedConfigs[0] as SharedTypes.Mode); } if (configKey === "words") { UpdateConfig.setWordCount(check.forcedConfigs[0] as number); diff --git a/frontend/src/ts/test/pace-caret.ts b/frontend/src/ts/test/pace-caret.ts index be94cf206e41..502542890697 100644 --- a/frontend/src/ts/test/pace-caret.ts +++ b/frontend/src/ts/test/pace-caret.ts @@ -62,7 +62,7 @@ export async function init(): Promise { const mode2 = Misc.getMode2( Config, TestWords.randomQuote - ) as MonkeyTypes.Mode2; + ) as SharedTypes.Mode2; let wpm; if (Config.paceCaret === "pb") { wpm = await DB.getLocalPB( diff --git a/frontend/src/ts/test/practise-words.ts b/frontend/src/ts/test/practise-words.ts index 9384c3705c5f..b10181d86bd4 100644 --- a/frontend/src/ts/test/practise-words.ts +++ b/frontend/src/ts/test/practise-words.ts @@ -19,7 +19,7 @@ interface BeforeCustomText { } interface Before { - mode: MonkeyTypes.Mode | null; + mode: SharedTypes.Mode | null; punctuation: boolean | null; numbers: boolean | null; customText: BeforeCustomText | null; diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index c8755fe73b77..41dda1446429 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -29,7 +29,7 @@ import confetti from "canvas-confetti"; import type { AnnotationOptions } from "chartjs-plugin-annotation"; import Ape from "../ape"; -let result: MonkeyTypes.Result; +let result: SharedTypes.Result; let maxChartVal: number; let useUnsmoothedRaw = false; @@ -546,7 +546,7 @@ async function updateTags(dontSave: boolean): Promise { }); } -function updateTestType(randomQuote: MonkeyTypes.Quote): void { +function updateTestType(randomQuote: MonkeyTypes.Quote | null): void { let testType = ""; testType += Config.mode; @@ -556,7 +556,7 @@ function updateTestType(randomQuote: MonkeyTypes.Quote): void { } else if (Config.mode === "words") { testType += " " + Config.words; } else if (Config.mode === "quote") { - if (randomQuote.group !== undefined) { + if (randomQuote?.group !== undefined) { testType += " " + ["short", "medium", "long", "thicc"][randomQuote.group]; } } @@ -653,8 +653,15 @@ function updateOther( } } -export function updateRateQuote(randomQuote: MonkeyTypes.Quote): void { +export function updateRateQuote(randomQuote: MonkeyTypes.Quote | null): void { if (Config.mode === "quote") { + if (randomQuote === null) { + console.error( + "Failed to update quote rating button: randomQuote is null" + ); + return; + } + const userqr = DB.getSnapshot()?.quoteRatings?.[randomQuote.language]?.[randomQuote.id]; if (userqr) { @@ -674,39 +681,48 @@ export function updateRateQuote(randomQuote: MonkeyTypes.Quote): void { } } -function updateQuoteFavorite(randomQuote: MonkeyTypes.Quote): void { - quoteLang = Config.mode === "quote" ? randomQuote.language : ""; - quoteId = Config.mode === "quote" ? randomQuote.id.toString() : ""; - +function updateQuoteFavorite(randomQuote: MonkeyTypes.Quote | null): void { const icon = $(".pageTest #result #favoriteQuoteButton .icon"); - if (Config.mode === "quote" && Auth?.currentUser) { - const userFav = QuotesController.isQuoteFavorite(randomQuote); - - icon.removeClass(userFav ? "far" : "fas").addClass(userFav ? "fas" : "far"); - icon.parent().removeClass("hidden"); - } else { + if (Config.mode !== "quote" || Auth?.currentUser === null) { icon.parent().addClass("hidden"); + return; } + + if (randomQuote === null) { + console.error( + "Failed to update quote favorite button: randomQuote is null" + ); + return; + } + + quoteLang = Config.mode === "quote" ? randomQuote.language : ""; + quoteId = Config.mode === "quote" ? randomQuote.id.toString() : ""; + + const userFav = QuotesController.isQuoteFavorite(randomQuote); + icon.removeClass(userFav ? "far" : "fas").addClass(userFav ? "fas" : "far"); + icon.parent().removeClass("hidden"); } -function updateQuoteSource(randomQuote: MonkeyTypes.Quote): void { +function updateQuoteSource(randomQuote: MonkeyTypes.Quote | null): void { if (Config.mode === "quote") { $("#result .stats .source").removeClass("hidden"); - $("#result .stats .source .bottom").html(randomQuote.source); + $("#result .stats .source .bottom").html( + randomQuote?.source ?? "Error: Source unknown" + ); } else { $("#result .stats .source").addClass("hidden"); } } export async function update( - res: MonkeyTypes.Result, + res: SharedTypes.Result, difficultyFailed: boolean, failReason: string, afkDetected: boolean, isRepeated: boolean, tooShort: boolean, - randomQuote: MonkeyTypes.Quote, + randomQuote: MonkeyTypes.Quote | null, dontSave: boolean ): Promise { resultAnnotation = []; diff --git a/frontend/src/ts/test/test-config.ts b/frontend/src/ts/test/test-config.ts index 9e54ff4967bd..62f6fa9e4a52 100644 --- a/frontend/src/ts/test/test-config.ts +++ b/frontend/src/ts/test/test-config.ts @@ -73,8 +73,8 @@ export async function instantUpdate(): Promise { } export async function update( - previous: MonkeyTypes.Mode, - current: MonkeyTypes.Mode + previous: SharedTypes.Mode, + current: SharedTypes.Mode ): Promise { if (previous === current) return; $("#testConfig .mode .textButton").removeClass("active"); @@ -282,8 +282,8 @@ ConfigEvent.subscribe((eventKey, eventValue, _nosave, eventPreviousValue) => { if (ActivePage.get() !== "test") return; if (eventKey === "mode") { update( - eventPreviousValue as MonkeyTypes.Mode, - eventValue as MonkeyTypes.Mode + eventPreviousValue as SharedTypes.Mode, + eventValue as SharedTypes.Mode ); let m2; diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index b8671765f9cc..1c0e9923c1ec 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -60,8 +60,7 @@ import * as ArabicLazyMode from "../states/arabic-lazy-mode"; let failReason = ""; const koInputVisual = document.getElementById("koInputVisual") as HTMLElement; -export let notSignedInLastResult: MonkeyTypes.Result | null = - null; +export let notSignedInLastResult: SharedTypes.CompletedEvent | null = null; export function clearNotSignedInResult(): void { notSignedInLastResult = null; @@ -609,7 +608,7 @@ export async function addWord(): Promise { TestWords.words.length >= CustomText.text.length) || (Config.mode === "quote" && TestWords.words.length >= - (TestWords.randomQuote.textSplit?.length ?? 0)) || + (TestWords.randomQuote?.textSplit?.length ?? 0)) || (Config.mode === "custom" && CustomText.isSectionRandom && WordsGenerator.sectionIndex >= CustomText.section && @@ -676,25 +675,8 @@ export async function addWord(): Promise { TestUI.addWord(randomWord.word); } -interface CompletedEvent extends MonkeyTypes.Result { - keySpacing: number[] | "toolong"; - keyDuration: number[] | "toolong"; - customText: MonkeyTypes.CustomText; - wpmConsistency: number; - lang: string; - challenge?: string | null; - keyOverlap: number; - lastKeyToEnd: number; - startToFirstKey: number; - charTotal: number; -} - -type PartialCompletedEvent = Omit, "chartData"> & { - chartData: Partial; -}; - interface RetrySaving { - completedEvent: CompletedEvent | null; + completedEvent: SharedTypes.CompletedEvent | null; canRetry: boolean; } @@ -733,86 +715,30 @@ export async function retrySavingResult(): Promise { saveResult(completedEvent, true); } -function buildCompletedEvent(difficultyFailed: boolean): CompletedEvent { +function buildCompletedEvent( + difficultyFailed: boolean +): SharedTypes.CompletedEvent { //build completed event object - const completedEvent: PartialCompletedEvent = { - wpm: undefined, - rawWpm: undefined, - charStats: undefined, - charTotal: undefined, - acc: undefined, - mode: Config.mode, - mode2: undefined, - quoteLength: -1, - punctuation: Config.punctuation, - numbers: Config.numbers, - lazyMode: Config.lazyMode, - timestamp: Date.now(), - language: Config.language, - restartCount: TestStats.restartCount, - incompleteTests: TestStats.incompleteTests, - incompleteTestSeconds: - TestStats.incompleteSeconds < 0 - ? 0 - : Misc.roundTo2(TestStats.incompleteSeconds), - difficulty: Config.difficulty, - blindMode: Config.blindMode, - tags: undefined, - keySpacing: TestInput.keypressTimings.spacing.array, - keyDuration: TestInput.keypressTimings.duration.array, - keyOverlap: Misc.roundTo2(TestInput.keyOverlap.total), - lastKeyToEnd: undefined, - startToFirstKey: undefined, - consistency: undefined, - keyConsistency: undefined, - funbox: Config.funbox, - bailedOut: TestState.bailedOut, - chartData: { - wpm: TestInput.wpmHistory, - raw: undefined, - err: undefined, - }, - customText: undefined, - testDuration: undefined, - afkDuration: undefined, - }; - - const stfk = Misc.roundTo2( + let stfk = Misc.roundTo2( TestInput.keypressTimings.spacing.first - TestStats.start ); - - if (stfk < 0) { - completedEvent.startToFirstKey = 0; - } else { - completedEvent.startToFirstKey = stfk; + if (stfk < 0 || Config.mode === "zen") { + stfk = 0; } - const lkte = Misc.roundTo2( + let lkte = Misc.roundTo2( TestStats.end - TestInput.keypressTimings.spacing.last ); - if (lkte < 0 || Config.mode === "zen") { - completedEvent.lastKeyToEnd = 0; - } else { - completedEvent.lastKeyToEnd = lkte; + lkte = 0; } - // stats const stats = TestStats.calculateStats(); if (stats.time % 1 !== 0 && Config.mode !== "time") { TestStats.setLastSecondNotRound(); } - PaceCaret.setLastTestWpm(stats.wpm); - completedEvent.wpm = stats.wpm; - completedEvent.rawWpm = stats.wpmRaw; - completedEvent.charStats = [ - stats.correctChars + stats.correctSpaces, - stats.incorrectChars, - stats.extraChars, - stats.missedChars, - ]; - completedEvent.charTotal = stats.allChars; - completedEvent.acc = stats.acc; + + PaceCaret.setLastTestWpm(stats.wpm); //todo why is this in here? // if the last second was not rounded, add another data point to the history if (TestStats.lastSecondNotRound && !difficultyFailed) { @@ -865,60 +791,99 @@ function buildCompletedEvent(difficultyFailed: boolean): CompletedEvent { if (!keyConsistency || isNaN(keyConsistency)) { keyConsistency = 0; } - completedEvent.keyConsistency = keyConsistency; - completedEvent.consistency = consistency; - completedEvent.chartData.raw = rawPerSecond; - - //wpm consistency - const stddev3 = Misc.stdDev(completedEvent.chartData.wpm ?? []); - const avg3 = Misc.mean(completedEvent.chartData.wpm ?? []); - const wpmConsistency = Misc.roundTo2(Misc.kogasa(stddev3 / avg3)); - completedEvent.wpmConsistency = isNaN(wpmConsistency) ? 0 : wpmConsistency; - - completedEvent.testDuration = parseFloat(stats.time.toString()); - completedEvent.afkDuration = TestStats.calculateAfkSeconds( - completedEvent.testDuration - ); - completedEvent.chartData.err = []; + const chartErr = []; for (let i = 0; i < TestInput.errorHistory.length; i++) { - completedEvent.chartData.err.push(TestInput.errorHistory[i]?.count ?? 0); + chartErr.push(TestInput.errorHistory[i]?.count ?? 0); } - if (Config.mode === "quote") { - completedEvent.quoteLength = TestWords.randomQuote.group; - completedEvent.language = Config.language.replace(/_\d*k$/g, ""); - } else { - delete completedEvent.quoteLength; - } + const chartData = { + wpm: TestInput.wpmHistory, + raw: rawPerSecond, + err: chartErr, + }; - completedEvent.mode2 = Misc.getMode2(Config, TestWords.randomQuote); + //wpm consistency + const stddev3 = Misc.stdDev(chartData.wpm ?? []); + const avg3 = Misc.mean(chartData.wpm ?? []); + const wpmCons = Misc.roundTo2(Misc.kogasa(stddev3 / avg3)); + const wpmConsistency = isNaN(wpmCons) ? 0 : wpmCons; + let customText: SharedTypes.CustomText | null = null; if (Config.mode === "custom") { - completedEvent.customText = {}; - completedEvent.customText.textLen = CustomText.text.length; - completedEvent.customText.isWordRandom = CustomText.isWordRandom; - completedEvent.customText.isTimeRandom = CustomText.isTimeRandom; - completedEvent.customText.word = CustomText.word; - completedEvent.customText.time = CustomText.time; - } else { - delete completedEvent.customText; + customText = {}; + customText.textLen = CustomText.text.length; + customText.isWordRandom = CustomText.isWordRandom; + customText.isTimeRandom = CustomText.isTimeRandom; + customText.word = CustomText.word; + customText.time = CustomText.time; } //tags const activeTagsIds: string[] = []; - try { - DB.getSnapshot()?.tags?.forEach((tag) => { - if (tag.active === true) { - activeTagsIds.push(tag._id); - } - }); - } catch (e) {} - completedEvent.tags = activeTagsIds; + for (const tag of DB.getSnapshot()?.tags ?? []) { + if (tag.active === true) { + activeTagsIds.push(tag._id); + } + } + + const duration = parseFloat(stats.time.toString()); + const afkDuration = TestStats.calculateAfkSeconds(duration); + let language = Config.language; + if (Config.mode === "quote") { + language = Config.language.replace(/_\d*k$/g, ""); + } + + const quoteLength = TestWords.randomQuote?.group ?? -1; + + const completedEvent = { + wpm: stats.wpm, + rawWpm: stats.wpmRaw, + charStats: [ + stats.correctChars + stats.correctSpaces, + stats.incorrectChars, + stats.extraChars, + stats.missedChars, + ], + charTotal: stats.allChars, + acc: stats.acc, + mode: Config.mode, + mode2: Misc.getMode2(Config, TestWords.randomQuote), + quoteLength: quoteLength, + punctuation: Config.punctuation, + numbers: Config.numbers, + lazyMode: Config.lazyMode, + timestamp: Date.now(), + language: language, + restartCount: TestStats.restartCount, + incompleteTests: TestStats.incompleteTests, + incompleteTestSeconds: + TestStats.incompleteSeconds < 0 + ? 0 + : Misc.roundTo2(TestStats.incompleteSeconds), + difficulty: Config.difficulty, + blindMode: Config.blindMode, + tags: activeTagsIds, + keySpacing: TestInput.keypressTimings.spacing.array, + keyDuration: TestInput.keypressTimings.duration.array, + keyOverlap: Misc.roundTo2(TestInput.keyOverlap.total), + lastKeyToEnd: lkte, + startToFirstKey: stfk, + consistency: consistency, + wpmConsistency: wpmConsistency, + keyConsistency: keyConsistency, + funbox: Config.funbox, + bailedOut: TestState.bailedOut, + chartData: chartData, + customText: customText, + testDuration: duration, + afkDuration: afkDuration, + } as SharedTypes.CompletedEvent; if (completedEvent.mode !== "custom") delete completedEvent.customText; + if (completedEvent.mode !== "quote") delete completedEvent.quoteLength; - return completedEvent; + return completedEvent; } export async function finish(difficultyFailed = false): Promise { @@ -1210,7 +1175,7 @@ export async function finish(difficultyFailed = false): Promise { } async function saveResult( - completedEvent: CompletedEvent, + completedEvent: SharedTypes.CompletedEvent, isRetrying: boolean ): Promise { if (!TestState.savingEnabled) { @@ -1417,7 +1382,7 @@ $(".pageTest").on("click", "#restartTestButtonWithSameWordset", () => { $(".pageTest").on("click", "#testConfig .mode .textButton", (e) => { if (TestUI.testRestarting) return; if ($(e.currentTarget).hasClass("active")) return; - const mode = ($(e.currentTarget).attr("mode") ?? "time") as MonkeyTypes.Mode; + const mode = ($(e.currentTarget).attr("mode") ?? "time") as SharedTypes.Mode; if (mode === undefined) return; UpdateConfig.setMode(mode); ManualRestart.set(); diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index 85dd52b0f632..afd5e661ae80 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -35,10 +35,10 @@ export let start: number, end: number; export let start2: number, end2: number; export let lastSecondNotRound = false; -export let lastResult: MonkeyTypes.Result; +export let lastResult: SharedTypes.Result; export function setLastResult( - result: MonkeyTypes.Result + result: SharedTypes.Result ): void { lastResult = result; } @@ -104,7 +104,7 @@ export function restart(): void { export let restartCount = 0; export let incompleteSeconds = 0; -export let incompleteTests: MonkeyTypes.IncompleteTest[] = []; +export let incompleteTests: SharedTypes.IncompleteTest[] = []; export function incrementRestartCount(): void { restartCount++; diff --git a/frontend/src/ts/test/test-words.ts b/frontend/src/ts/test/test-words.ts index bea947ec9486..e9e8ffaa27a3 100644 --- a/frontend/src/ts/test/test-words.ts +++ b/frontend/src/ts/test/test-words.ts @@ -70,7 +70,7 @@ export const words = new Words(); export let hasTab = false; export let hasNewline = false; export let hasNumbers = false; -export let randomQuote = null as unknown as MonkeyTypes.Quote; +export let randomQuote = null as MonkeyTypes.Quote | null; export function setRandomQuote(rq: MonkeyTypes.Quote): void { randomQuote = rq; diff --git a/frontend/src/ts/test/words-generator.ts b/frontend/src/ts/test/words-generator.ts index 1303d2f7721a..1cbf00b7f35b 100644 --- a/frontend/src/ts/test/words-generator.ts +++ b/frontend/src/ts/test/words-generator.ts @@ -323,7 +323,8 @@ async function applyBritishEnglishToWord( ): Promise { if (!Config.britishEnglish) return word; if (!/english/.test(Config.language)) return word; - if (Config.mode === "quote" && TestWords.randomQuote.britishText) return word; + if (Config.mode === "quote" && TestWords.randomQuote?.britishText) + return word; return await BritishEnglish.replace(word, previousWord); } @@ -608,6 +609,10 @@ async function generateQuoteWords( TestWords.setRandomQuote(rq); + if (TestWords.randomQuote === null) { + throw new WordGenError("Random quote is null"); + } + if (TestWords.randomQuote.textSplit === undefined) { throw new WordGenError("Random quote textSplit is undefined"); } diff --git a/frontend/src/ts/types/types.d.ts b/frontend/src/ts/types/types.d.ts index dc533609c4b1..4f0e93d7a246 100644 --- a/frontend/src/ts/types/types.d.ts +++ b/frontend/src/ts/types/types.d.ts @@ -10,16 +10,6 @@ declare namespace MonkeyTypes { | "profileSearch" | "404"; - type Difficulty = "normal" | "expert" | "master"; - - type Mode = keyof PersonalBests; - - type Mode2 = M extends M ? keyof PersonalBests[M] : never; - - type StringNumber = `${number}`; - - type Mode2Custom = Mode2 | "custom"; - interface LanguageGroup { name: string; languages: string[]; @@ -294,16 +284,6 @@ declare namespace MonkeyTypes { hasCSS?: boolean; } - interface CustomText { - text: string[]; - isWordRandom: boolean; - isTimeRandom: boolean; - word: number; - time: number; - delimiter: string; - textLen?: number; - } - interface PresetConfig extends MonkeyTypes.Config { tags: string[]; } @@ -315,31 +295,11 @@ declare namespace MonkeyTypes { config: ConfigChanges; } - interface PersonalBest { - acc: number; - consistency: number; - difficulty: Difficulty; - lazyMode: boolean; - language: string; - punctuation: boolean; - raw: number; - wpm: number; - timestamp: number; - } - - interface PersonalBests { - time: Record; - words: Record; - quote: Record; - custom: Partial>; - zen: Partial>; - } - interface Tag { _id: string; name: string; display: string; - personalBests: PersonalBests; + personalBests: SharedTypes.PersonalBests; active?: boolean; } @@ -358,59 +318,6 @@ declare namespace MonkeyTypes { completedTests: number; } - interface ChartData { - wpm: number[]; - raw: number[]; - err: number[]; - unsmoothedRaw?: number[]; - } - - interface KeyStats { - average: number; - sd: number; - } - - interface IncompleteTest { - acc: number; - seconds: number; - } - - interface Result { - _id: string; - wpm: number; - rawWpm: number; - charStats: number[]; - correctChars?: number; // -------------- - incorrectChars?: number; // legacy results - acc: number; - mode: M; - mode2: Mode2; - quoteLength: number; - timestamp: number; - restartCount: number; - incompleteTestSeconds: number; - incompleteTests: IncompleteTest[]; - testDuration: number; - afkDuration: number; - tags: string[]; - consistency: number; - keyConsistency: number; - chartData: ChartData | "toolong"; - uid: string; - keySpacingStats: KeyStats; - keyDurationStats: KeyStats; - isPb?: boolean; - bailedOut?: boolean; - blindMode?: boolean; - lazyMode?: boolean; - difficulty: Difficulty; - funbox?: string; - language: string; - numbers?: boolean; - punctuation?: boolean; - hash?: string; - } - interface ApeKey { name: string; enabled: boolean; @@ -440,12 +347,12 @@ declare namespace MonkeyTypes { numbers: boolean; words: WordsModes; time: TimeModes; - mode: Mode; + mode: SharedTypes.Mode; quoteLength: QuoteLength[]; language: string; fontSize: number; freedomMode: boolean; - difficulty: Difficulty; + difficulty: SharedTypes.Difficulty; blindMode: boolean; quickEnd: boolean; caretStyle: CaretStyle; @@ -518,7 +425,7 @@ declare namespace MonkeyTypes { | string[] | MonkeyTypes.QuoteLength[] | MonkeyTypes.HighlightMode - | MonkeyTypes.ResultFilters + | SharedTypes.ResultFilters | MonkeyTypes.CustomBackgroundFilter | null | undefined; @@ -574,9 +481,9 @@ declare namespace MonkeyTypes { banned?: boolean; emailVerified?: boolean; quoteRatings?: QuoteRatings; - results?: Result[]; + results?: SharedTypes.Result[]; verified?: boolean; - personalBests: PersonalBests; + personalBests: SharedTypes.PersonalBests; name: string; customThemes: CustomTheme[]; presets?: Preset[]; @@ -593,7 +500,7 @@ declare namespace MonkeyTypes { details?: UserDetails; inventory?: UserInventory; addedAt: number; - filterPresets: ResultFilters[]; + filterPresets: SharedTypes.ResultFilters[]; xp: number; inboxUnreadSize: number; streak: number; @@ -624,74 +531,14 @@ declare namespace MonkeyTypes { type FavoriteQuotes = Record; - interface ResultFilters { - _id: string; - name: string; - pb: { - no: boolean; - yes: boolean; - }; - difficulty: { - normal: boolean; - expert: boolean; - master: boolean; - }; - mode: { - words: boolean; - time: boolean; - quote: boolean; - zen: boolean; - custom: boolean; - }; - words: { - "10": boolean; - "25": boolean; - "50": boolean; - "100": boolean; - custom: boolean; - }; - time: { - "15": boolean; - "30": boolean; - "60": boolean; - "120": boolean; - custom: boolean; - }; - quoteLength: { - short: boolean; - medium: boolean; - long: boolean; - thicc: boolean; - }; - punctuation: { - on: boolean; - off: boolean; - }; - numbers: { - on: boolean; - off: boolean; - }; - date: { - last_day: boolean; - last_week: boolean; - last_month: boolean; - last_3months: boolean; - all: boolean; - }; - tags: Record; - language: Record; - funbox: { - none?: boolean; - } & Record; - } - - type Group = G extends G - ? ResultFilters[G] - : never; + type Group< + G extends keyof SharedTypes.ResultFilters = keyof SharedTypes.ResultFilters + > = G extends G ? SharedTypes.ResultFilters[G] : never; - type Filter = G extends keyof ResultFilters - ? keyof ResultFilters[G] - : never; + type Filter = + G extends keyof SharedTypes.ResultFilters + ? keyof SharedTypes.ResultFilters[G] + : never; interface TimerStats { dateNow: number; diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 46cbb67dc9a6..3d8e733c0a7d 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -1013,7 +1013,7 @@ export function canQuickRestart( mode: string, words: number, time: number, - CustomText: MonkeyTypes.CustomText, + CustomText: SharedTypes.CustomText, customTextIsLong: boolean ): boolean { const wordsLong = mode === "words" && (words >= 1000 || words === 0); @@ -1173,10 +1173,10 @@ export async function swapElements( return; } -export function getMode2( +export function getMode2( config: MonkeyTypes.Config, - randomQuote: MonkeyTypes.Quote -): MonkeyTypes.Mode2 { + randomQuote: MonkeyTypes.Quote | null +): SharedTypes.Mode2 { const mode = config.mode; let retVal: string; @@ -1189,16 +1189,16 @@ export function getMode2( } else if (mode === "zen") { retVal = "zen"; } else if (mode === "quote") { - retVal = randomQuote.id.toString(); + retVal = `${randomQuote?.id ?? -1}`; } else { throw new Error("Invalid mode"); } - return retVal as MonkeyTypes.Mode2; + return retVal as SharedTypes.Mode2; } export async function downloadResultsCSV( - array: MonkeyTypes.Result[] + array: SharedTypes.Result[] ): Promise { Loader.show(); const csvString = [ @@ -1228,7 +1228,7 @@ export async function downloadResultsCSV( "tags", "timestamp", ], - ...array.map((item: MonkeyTypes.Result) => [ + ...array.map((item: SharedTypes.Result) => [ item._id, item.isPb, item.wpm, diff --git a/frontend/src/ts/utils/url-handler.ts b/frontend/src/ts/utils/url-handler.ts index 752bf8b20618..6f7bc8b0937b 100644 --- a/frontend/src/ts/utils/url-handler.ts +++ b/frontend/src/ts/utils/url-handler.ts @@ -101,13 +101,13 @@ export function loadCustomThemeFromUrl(getOverride?: string): void { } type SharedTestSettings = [ - MonkeyTypes.Mode | null, - MonkeyTypes.Mode2 | null, - MonkeyTypes.CustomText | null, + SharedTypes.Mode | null, + SharedTypes.Mode2 | null, + SharedTypes.CustomText | null, boolean | null, boolean | null, string | null, - MonkeyTypes.Difficulty | null, + SharedTypes.Difficulty | null, string | null ]; diff --git a/shared-types/types.d.ts b/shared-types/types.d.ts index 9c75e1c4dc7c..3c350084d06c 100644 --- a/shared-types/types.d.ts +++ b/shared-types/types.d.ts @@ -107,4 +107,213 @@ declare namespace SharedTypes { }; }; } + + type Difficulty = "normal" | "expert" | "master"; + + type Mode = keyof PersonalBests; + + type Mode2 = M extends M ? keyof PersonalBests[M] : never; + + type StringNumber = `${number}`; + + type Mode2Custom = Mode2 | "custom"; + + interface PersonalBest { + acc: number; + consistency: number; + difficulty: Difficulty; + lazyMode: boolean; + language: string; + punctuation: boolean; + raw: number; + wpm: number; + timestamp: number; + } + + interface PersonalBests { + time: Record; + words: Record; + quote: Record; + custom: Partial>; + zen: Partial>; + } + + interface IncompleteTest { + acc: number; + seconds: number; + } + + interface ChartData { + wpm: number[]; + raw: number[]; + err: number[]; + } + + interface KeyStats { + average: number; + sd: number; + } + + interface Result { + _id: string; + wpm: number; + rawWpm: number; + charStats: number[]; + acc: number; + mode: M; + mode2: Mode2; + quoteLength?: number; + timestamp: number; + restartCount: number; + incompleteTestSeconds: number; + incompleteTests: IncompleteTest[]; + testDuration: number; + afkDuration: number; + tags: string[]; + consistency: number; + keyConsistency: number; + chartData: ChartData | "toolong"; + uid: string; + keySpacingStats?: KeyStats; + keyDurationStats?: KeyStats; + isPb: boolean; + bailedOut: boolean; + blindMode: boolean; + lazyMode: boolean; + difficulty: Difficulty; + funbox: string; + language: string; + numbers: boolean; + punctuation: boolean; + } + + interface CustomText { + text: string[]; + isWordRandom: boolean; + isTimeRandom: boolean; + word: number; + time: number; + delimiter: string; + textLen?: number; + } + + type WithObjectId = Omit & { + _id: ObjectId; + }; + + type DBResult = WithObjectId< + Omit< + SharedTypes.Result, + | "bailedOut" + | "blindMode" + | "lazyMode" + | "difficulty" + | "funbox" + | "language" + | "numbers" + | "punctuation" + | "restartCount" + | "incompleteTestSeconds" + | "afkDuration" + | "tags" + | "incompleteTests" + | "customText" + | "quoteLength" + > & { + correctChars?: number; // -------------- + incorrectChars?: number; // legacy results + // -------------- + name: string; + // -------------- fields that might be removed to save space + bailedOut?: boolean; + blindMode?: boolean; + lazyMode?: boolean; + difficulty?: SharedTypes.Difficulty; + funbox?: string; + language?: string; + numbers?: boolean; + punctuation?: boolean; + restartCount?: number; + incompleteTestSeconds?: number; + afkDuration?: number; + tags?: string[]; + customText?: CustomText; + quoteLength?: number; + } + >; + + interface CompletedEvent extends Result { + keySpacing: number[] | "toolong"; + keyDuration: number[] | "toolong"; + customText?: CustomText; + wpmConsistency: number; + challenge?: string | null; + keyOverlap: number; + lastKeyToEnd: number; + startToFirstKey: number; + charTotal: number; + stringified?: string; + hash?: string; + } + + interface ResultFilters { + _id: string; + name: string; + pb: { + no: boolean; + yes: boolean; + }; + difficulty: { + normal: boolean; + expert: boolean; + master: boolean; + }; + mode: { + words: boolean; + time: boolean; + quote: boolean; + zen: boolean; + custom: boolean; + }; + words: { + "10": boolean; + "25": boolean; + "50": boolean; + "100": boolean; + custom: boolean; + }; + time: { + "15": boolean; + "30": boolean; + "60": boolean; + "120": boolean; + custom: boolean; + }; + quoteLength: { + short: boolean; + medium: boolean; + long: boolean; + thicc: boolean; + }; + punctuation: { + on: boolean; + off: boolean; + }; + numbers: { + on: boolean; + off: boolean; + }; + date: { + last_day: boolean; + last_week: boolean; + last_month: boolean; + last_3months: boolean; + all: boolean; + }; + tags: Record; + language: Record; + funbox: { + none?: boolean; + } & Record; + } }