mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2024-09-20 07:16:17 +08:00
convert results, wip
This commit is contained in:
parent
6fac5dd3a9
commit
a711c891a7
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run pre-commit
|
||||
#npm run pre-commit
|
||||
|
|
|
@ -7,12 +7,11 @@ import {
|
|||
mapRange,
|
||||
roundTo2,
|
||||
stdDev,
|
||||
stringToNumberOrDefault,
|
||||
} from "../../utils/misc";
|
||||
import objectHash from "object-hash";
|
||||
import Logger from "../../utils/logger";
|
||||
import "dotenv/config";
|
||||
import { MonkeyResponse } from "../../utils/monkey-response";
|
||||
import { MonkeyResponse2 } from "../../utils/monkey-response";
|
||||
import MonkeyError from "../../utils/error";
|
||||
import { areFunboxesCompatible, isTestTooShort } from "../../utils/validation";
|
||||
import {
|
||||
|
@ -36,6 +35,16 @@ import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard";
|
|||
import { UAParser } from "ua-parser-js";
|
||||
import { canFunboxGetPb } from "../../utils/pb";
|
||||
import { buildDbResult } from "../../utils/result";
|
||||
import {
|
||||
CompletedEvent,
|
||||
CreateResult,
|
||||
CreateResultBody,
|
||||
GetResults,
|
||||
GetResultsQuery,
|
||||
Result,
|
||||
UpdateTags,
|
||||
UpdateTagsBody,
|
||||
} from "../../../../shared/contract/results.contract";
|
||||
|
||||
try {
|
||||
if (!anticheatImplemented()) throw new Error("undefined");
|
||||
|
@ -54,8 +63,8 @@ try {
|
|||
}
|
||||
|
||||
export async function getResults(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
req: MonkeyTypes.Request2<GetResultsQuery>
|
||||
): Promise<MonkeyResponse2<GetResults>> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const premiumFeaturesEnabled = req.ctx.configuration.users.premium.enabled;
|
||||
const userHasPremium = await UserDAL.checkIfUserIsPremium(uid);
|
||||
|
@ -65,15 +74,12 @@ export async function getResults(
|
|||
? req.ctx.configuration.results.limits.premiumUser
|
||||
: req.ctx.configuration.results.limits.regularUser;
|
||||
|
||||
const onOrAfterTimestamp = parseInt(
|
||||
req.query["onOrAfterTimestamp"] as string,
|
||||
10
|
||||
);
|
||||
let limit = stringToNumberOrDefault(
|
||||
req.query["limit"] as string,
|
||||
Math.min(req.ctx.configuration.results.maxBatchSize, maxLimit)
|
||||
);
|
||||
const offset = stringToNumberOrDefault(req.query["offset"] as string, 0);
|
||||
const onOrAfterTimestamp = req.query.onOrAfterTimestamp;
|
||||
let limit =
|
||||
req.query.limit ??
|
||||
Math.min(req.ctx.configuration.results.maxBatchSize, maxLimit);
|
||||
|
||||
const offset = req.query.offset ?? 0;
|
||||
|
||||
//check if premium features are disabled and current call exceeds the limit for regular users
|
||||
if (
|
||||
|
@ -93,11 +99,11 @@ export async function getResults(
|
|||
}
|
||||
}
|
||||
|
||||
const results = await ResultDAL.getResults(uid, {
|
||||
const results = (await ResultDAL.getResults(uid, {
|
||||
onOrAfterTimestamp,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
})) as unknown as Result[]; //TOOD clean mapping
|
||||
void Logger.logToDb(
|
||||
"user_results_requested",
|
||||
{
|
||||
|
@ -108,30 +114,30 @@ export async function getResults(
|
|||
},
|
||||
uid
|
||||
);
|
||||
return new MonkeyResponse("Results retrieved", results);
|
||||
return new MonkeyResponse2("Results retrieved", results);
|
||||
}
|
||||
|
||||
export async function getLastResult(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
req: MonkeyTypes.Request2
|
||||
): Promise<MonkeyResponse2<Result>> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const results = await ResultDAL.getLastResult(uid);
|
||||
return new MonkeyResponse("Result retrieved", results);
|
||||
const result = (await ResultDAL.getLastResult(uid)) as unknown as Result; //TODO clean mapping
|
||||
return new MonkeyResponse2("Result retrieved", result);
|
||||
}
|
||||
|
||||
export async function deleteAll(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
req: MonkeyTypes.Request2
|
||||
): Promise<MonkeyResponse2<UpdateTags>> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
|
||||
await ResultDAL.deleteAll(uid);
|
||||
void Logger.logToDb("user_results_deleted", "", uid);
|
||||
return new MonkeyResponse("All results deleted");
|
||||
return new MonkeyResponse2("All results deleted");
|
||||
}
|
||||
|
||||
export async function updateTags(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
req: MonkeyTypes.Request2<never, UpdateTagsBody>
|
||||
): Promise<MonkeyResponse2<UpdateTags>> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const { tagIds, resultId } = req.body;
|
||||
|
||||
|
@ -159,14 +165,14 @@ export async function updateTags(
|
|||
|
||||
const user = await UserDAL.getUser(uid, "update tags");
|
||||
const tagPbs = await UserDAL.checkIfTagPb(uid, user, result);
|
||||
return new MonkeyResponse("Result tags updated", {
|
||||
return new MonkeyResponse2("Result tags updated", {
|
||||
tagPbs,
|
||||
});
|
||||
}
|
||||
|
||||
export async function addResult(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
req: MonkeyTypes.Request2<never, CreateResultBody>
|
||||
): Promise<MonkeyResponse2<CreateResult>> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
|
||||
const user = await UserDAL.getUser(uid, "add result");
|
||||
|
@ -178,10 +184,8 @@ export async function addResult(
|
|||
);
|
||||
}
|
||||
|
||||
const completedEvent = Object.assign(
|
||||
{},
|
||||
req.body.result
|
||||
) as SharedTypes.CompletedEvent;
|
||||
const completedEvent = Object.assign({}, req.body.result) as CompletedEvent &
|
||||
Result;
|
||||
if (!user.lbOptOut && completedEvent.acc < 75) {
|
||||
throw new MonkeyError(
|
||||
400,
|
||||
|
@ -199,7 +203,7 @@ export async function addResult(
|
|||
throw new MonkeyError(400, "Missing result hash");
|
||||
}
|
||||
delete completedEvent.hash;
|
||||
delete completedEvent.stringified;
|
||||
//delete completedEvent.stringified;
|
||||
if (req.ctx.configuration.results.objectHashCheckEnabled) {
|
||||
const serverhash = objectHash(completedEvent);
|
||||
if (serverhash !== resulthash) {
|
||||
|
@ -230,6 +234,7 @@ export async function addResult(
|
|||
|
||||
if (
|
||||
completedEvent.keySpacing !== "toolong" &&
|
||||
completedEvent.keySpacing !== undefined &&
|
||||
completedEvent.keySpacing.length > 0
|
||||
) {
|
||||
completedEvent.keySpacingStats = {
|
||||
|
@ -243,6 +248,7 @@ export async function addResult(
|
|||
|
||||
if (
|
||||
completedEvent.keyDuration !== "toolong" &&
|
||||
completedEvent.keyDuration !== undefined &&
|
||||
completedEvent.keyDuration.length > 0
|
||||
) {
|
||||
completedEvent.keyDurationStats = {
|
||||
|
@ -258,9 +264,9 @@ export async function addResult(
|
|||
if (
|
||||
!validateResult(
|
||||
completedEvent,
|
||||
((req.headers["x-client-version"] as string) ||
|
||||
req.headers["client-version"]) as string,
|
||||
JSON.stringify(new UAParser(req.headers["user-agent"]).getResult()),
|
||||
((req.raw.headers["x-client-version"] as string) ||
|
||||
req.raw.headers["client-version"]) as string,
|
||||
JSON.stringify(new UAParser(req.raw.headers["user-agent"]).getResult()),
|
||||
user.lbOptOut === true
|
||||
)
|
||||
) {
|
||||
|
@ -582,7 +588,7 @@ export async function addResult(
|
|||
);
|
||||
}
|
||||
|
||||
const dbresult = buildDbResult(completedEvent, user.name, isPb);
|
||||
const dbresult = buildDbResult(completedEvent as any, user.name, isPb); //TODO fix
|
||||
const addedResult = await ResultDAL.addResult(uid, dbresult);
|
||||
|
||||
await UserDAL.incrementXp(uid, xpGained.xp);
|
||||
|
@ -622,7 +628,7 @@ export async function addResult(
|
|||
|
||||
incrementResult(completedEvent);
|
||||
|
||||
return new MonkeyResponse("Result saved", data);
|
||||
return new MonkeyResponse2("Result saved", data as unknown as CreateResult); //TODO clean mapping
|
||||
}
|
||||
|
||||
type XpResult = {
|
||||
|
@ -632,7 +638,7 @@ type XpResult = {
|
|||
};
|
||||
|
||||
async function calculateXp(
|
||||
result: SharedTypes.CompletedEvent,
|
||||
result: Result,
|
||||
xpConfiguration: SharedTypes.Configuration["users"]["xp"],
|
||||
uid: string,
|
||||
currentTotalXp: number,
|
||||
|
|
|
@ -5,7 +5,7 @@ import users from "./users";
|
|||
import { join } from "path";
|
||||
import quotes from "./quotes";
|
||||
import configs from "./configs";
|
||||
import results from "./results";
|
||||
//import results from "./results";
|
||||
import presets from "./presets";
|
||||
import apeKeys from "./ape-keys";
|
||||
import admin from "./admin";
|
||||
|
@ -35,7 +35,7 @@ const APP_START_TIME = Date.now();
|
|||
const API_ROUTE_MAP = {
|
||||
"/users": users,
|
||||
"/configs": configs,
|
||||
"/results": results,
|
||||
//"/results": results,
|
||||
"/presets": presets,
|
||||
"/psas": psas,
|
||||
"/public": publicStats,
|
||||
|
|
|
@ -5,11 +5,13 @@ import { MonkeyResponse2 } from "../../utils/monkey-response";
|
|||
import { contract } from "./../../../../shared/contract/index.contract";
|
||||
import { configRoutes } from "./configsV2";
|
||||
import { userRoutes } from "./usersV2";
|
||||
import { resultsRoutes } from "./results";
|
||||
|
||||
const s = initServer();
|
||||
const router = s.router(contract, {
|
||||
users: userRoutes,
|
||||
configs: configRoutes,
|
||||
results: resultsRoutes,
|
||||
});
|
||||
|
||||
export function applyApiRoutes(app: IRouter): void {
|
||||
|
|
|
@ -1,87 +1,45 @@
|
|||
import * as ResultController from "../controllers/result";
|
||||
import resultSchema from "../schemas/result-schema";
|
||||
import {
|
||||
asyncHandler,
|
||||
validateRequest,
|
||||
validateConfiguration,
|
||||
} from "../../middlewares/api-utils";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import { Router } from "express";
|
||||
import { authenticateRequest } from "../../middlewares/auth";
|
||||
import joi from "joi";
|
||||
import { initServer } from "@ts-rest/express";
|
||||
import { resultsContract } from "../../../../shared/contract/results.contract";
|
||||
import { withApeRateLimiter } from "../../middlewares/ape-rate-limit";
|
||||
import { validateConfiguration } from "../../middlewares/api-utils";
|
||||
import { authenticateRequestV2 } from "../../middlewares/auth";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import * as ResultController from "../controllers/result";
|
||||
import { callController } from "./index2";
|
||||
|
||||
const router = Router();
|
||||
const s = initServer();
|
||||
export const resultsRoutes = s.router(resultsContract, {
|
||||
get: {
|
||||
middleware: [
|
||||
authenticateRequestV2({ acceptApeKeys: true }),
|
||||
withApeRateLimiter(RateLimit.resultsGet, RateLimit.resultsGetApe) as any, //TODO
|
||||
],
|
||||
handler: async (r) => callController(ResultController.getResults)(r),
|
||||
},
|
||||
save: {
|
||||
middleware: [
|
||||
validateConfiguration({
|
||||
criteria: (configuration) => {
|
||||
return configuration.results.savingEnabled;
|
||||
},
|
||||
invalidMessage: "Results are not being saved at this time.",
|
||||
}),
|
||||
authenticateRequestV2(),
|
||||
RateLimit.resultsAdd,
|
||||
],
|
||||
handler: async (r) => callController(ResultController.addResult)(r),
|
||||
},
|
||||
|
||||
router.get(
|
||||
"/",
|
||||
authenticateRequest({
|
||||
acceptApeKeys: true,
|
||||
}),
|
||||
withApeRateLimiter(RateLimit.resultsGet, RateLimit.resultsGetApe),
|
||||
validateRequest({
|
||||
query: {
|
||||
onOrAfterTimestamp: joi.number().integer().min(1589428800000),
|
||||
limit: joi.number().integer().min(0).max(1000),
|
||||
offset: joi.number().integer().min(0),
|
||||
},
|
||||
}),
|
||||
asyncHandler(ResultController.getResults)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
validateConfiguration({
|
||||
criteria: (configuration) => {
|
||||
return configuration.results.savingEnabled;
|
||||
},
|
||||
invalidMessage: "Results are not being saved at this time.",
|
||||
}),
|
||||
authenticateRequest(),
|
||||
RateLimit.resultsAdd,
|
||||
validateRequest({
|
||||
body: {
|
||||
result: resultSchema,
|
||||
},
|
||||
}),
|
||||
asyncHandler(ResultController.addResult)
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/tags",
|
||||
authenticateRequest(),
|
||||
RateLimit.resultsTagsUpdate,
|
||||
validateRequest({
|
||||
body: {
|
||||
tagIds: joi
|
||||
.array()
|
||||
.items(joi.string().regex(/^[a-f\d]{24}$/i))
|
||||
.required(),
|
||||
resultId: joi
|
||||
.string()
|
||||
.regex(/^[a-f\d]{24}$/i)
|
||||
.required(),
|
||||
},
|
||||
}),
|
||||
asyncHandler(ResultController.updateTags)
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/",
|
||||
authenticateRequest({
|
||||
requireFreshToken: true,
|
||||
}),
|
||||
RateLimit.resultsDeleteAll,
|
||||
asyncHandler(ResultController.deleteAll)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/last",
|
||||
authenticateRequest({
|
||||
acceptApeKeys: true,
|
||||
}),
|
||||
withApeRateLimiter(RateLimit.resultsGet),
|
||||
asyncHandler(ResultController.getLastResult)
|
||||
);
|
||||
|
||||
export default router;
|
||||
updateTags: {
|
||||
middleware: [authenticateRequestV2(), RateLimit.resultsTagsUpdate],
|
||||
handler: async (r) => callController(ResultController.updateTags)(r),
|
||||
},
|
||||
delete: {
|
||||
middleware: [authenticateRequestV2()],
|
||||
handler: async (r) => callController(ResultController.deleteAll)(r),
|
||||
},
|
||||
getLast: {
|
||||
middleware: [authenticateRequestV2({ acceptApeKeys: true })],
|
||||
handler: async (r) => callController(ResultController.getLastResult)(r),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { flattenObjectDeep, isToday, isYesterday } from "../utils/misc";
|
|||
import { getCachedConfiguration } from "../init/configuration";
|
||||
import { getDayOfYear } from "date-fns";
|
||||
import { UTCDate } from "@date-fns/utc";
|
||||
import type { Result as ResultType } from "../../../shared/contract/results.contract";
|
||||
|
||||
const SECONDS_PER_HOUR = 3600;
|
||||
|
||||
|
@ -417,7 +418,7 @@ export async function updateLbMemory(
|
|||
export async function checkIfPb(
|
||||
uid: string,
|
||||
user: MonkeyTypes.DBUser,
|
||||
result: Result
|
||||
result: ResultType
|
||||
): Promise<boolean> {
|
||||
const { mode } = result;
|
||||
|
||||
|
@ -460,7 +461,7 @@ export async function checkIfPb(
|
|||
export async function checkIfTagPb(
|
||||
uid: string,
|
||||
user: MonkeyTypes.DBUser,
|
||||
result: Result
|
||||
result: ResultType | Result
|
||||
): Promise<string[]> {
|
||||
if (user.tags === undefined || user.tags.length === 0) {
|
||||
return [];
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import _ from "lodash";
|
||||
import FunboxList from "../constants/funbox-list";
|
||||
import type { Result as ResultType } from "../../../shared/contract/results.contract";
|
||||
|
||||
type CheckAndUpdatePbResult = {
|
||||
isPb: boolean;
|
||||
|
@ -12,7 +13,7 @@ type Result = Omit<
|
|||
"_id" | "name"
|
||||
>;
|
||||
|
||||
export function canFunboxGetPb(result: Result): boolean {
|
||||
export function canFunboxGetPb(result: ResultType | Result): boolean {
|
||||
const funbox = result.funbox;
|
||||
if (funbox === undefined || funbox === "" || funbox === "none") return true;
|
||||
|
||||
|
@ -32,7 +33,7 @@ export function canFunboxGetPb(result: Result): boolean {
|
|||
export function checkAndUpdatePb(
|
||||
userPersonalBests: SharedTypes.PersonalBests,
|
||||
lbPersonalBests: MonkeyTypes.LbPersonalBests | undefined,
|
||||
result: Result
|
||||
result: ResultType | Result
|
||||
): CheckAndUpdatePbResult {
|
||||
const mode = result.mode;
|
||||
const mode2 = result.mode2 as SharedTypes.Config.Mode2<"time">;
|
||||
|
@ -66,7 +67,7 @@ export function checkAndUpdatePb(
|
|||
}
|
||||
|
||||
function matchesPersonalBest(
|
||||
result: Result,
|
||||
result: ResultType | Result,
|
||||
personalBest: SharedTypes.PersonalBest
|
||||
): boolean {
|
||||
if (
|
||||
|
@ -99,7 +100,7 @@ function matchesPersonalBest(
|
|||
|
||||
function updatePersonalBest(
|
||||
personalBest: SharedTypes.PersonalBest,
|
||||
result: Result
|
||||
result: ResultType | Result
|
||||
): boolean {
|
||||
if (personalBest.wpm >= result.wpm) {
|
||||
return false;
|
||||
|
@ -133,7 +134,9 @@ function updatePersonalBest(
|
|||
return true;
|
||||
}
|
||||
|
||||
function buildPersonalBest(result: Result): SharedTypes.PersonalBest {
|
||||
function buildPersonalBest(
|
||||
result: ResultType | Result
|
||||
): SharedTypes.PersonalBest {
|
||||
if (
|
||||
result.difficulty === undefined ||
|
||||
result.language === undefined ||
|
||||
|
@ -164,7 +167,7 @@ function buildPersonalBest(result: Result): SharedTypes.PersonalBest {
|
|||
function updateLeaderboardPersonalBests(
|
||||
userPersonalBests: SharedTypes.PersonalBests,
|
||||
lbPersonalBests: MonkeyTypes.LbPersonalBests,
|
||||
result: Result
|
||||
result: ResultType | Result
|
||||
): void {
|
||||
if (!shouldUpdateLeaderboardPersonalBests(result)) {
|
||||
return;
|
||||
|
@ -207,7 +210,7 @@ function updateLeaderboardPersonalBests(
|
|||
);
|
||||
}
|
||||
|
||||
function shouldUpdateLeaderboardPersonalBests(result: Result): boolean {
|
||||
function shouldUpdateLeaderboardPersonalBests(result: ResultType): boolean {
|
||||
const isValidTimeMode =
|
||||
result.mode === "time" && (result.mode2 === "15" || result.mode2 === "60");
|
||||
return isValidTimeMode && !result.lazyMode;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import "dotenv/config";
|
||||
import { Counter, Histogram, Gauge } from "prom-client";
|
||||
import { TsRestRequest } from "@ts-rest/express";
|
||||
import { Result } from "@shared/contract/results.contract";
|
||||
|
||||
const auth = new Counter({
|
||||
name: "api_request_auth_total",
|
||||
|
@ -89,9 +90,7 @@ export function setLeaderboard(
|
|||
leaderboardUpdate.set({ language, mode, mode2, step: "index" }, times[3]);
|
||||
}
|
||||
|
||||
export function incrementResult(
|
||||
res: SharedTypes.Result<SharedTypes.Config.Mode>
|
||||
): void {
|
||||
export function incrementResult(res: Result): void {
|
||||
const {
|
||||
mode,
|
||||
mode2,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { replaceHomoglyphs } from "../constants/homoglyphs";
|
|||
import { profanities, regexProfanities } from "../constants/profanities";
|
||||
import { intersect, matchesAPattern, sanitizeString } from "./misc";
|
||||
import { default as FunboxList } from "../constants/funbox-list";
|
||||
import { CompletedEvent } from "@shared/contract/results.contract";
|
||||
|
||||
export function inRange(value: number, min: number, max: number): boolean {
|
||||
return value >= min && value <= max;
|
||||
|
@ -57,7 +58,7 @@ export function isTagPresetNameValid(name: string): boolean {
|
|||
return VALID_NAME_PATTERN.test(name);
|
||||
}
|
||||
|
||||
export function isTestTooShort(result: SharedTypes.CompletedEvent): boolean {
|
||||
export function isTestTooShort(result: CompletedEvent): boolean {
|
||||
const { mode, mode2, customText, testDuration, bailedOut } = result;
|
||||
|
||||
if (mode === "time") {
|
||||
|
|
|
@ -6,7 +6,7 @@ import Presets from "./presets";
|
|||
import Psas from "./psas";
|
||||
import Public from "./public";
|
||||
import Quotes from "./quotes";
|
||||
import Results from "./results";
|
||||
//import Results from "./results";
|
||||
import Users from "./users";
|
||||
|
||||
export default {
|
||||
|
@ -16,7 +16,7 @@ export default {
|
|||
Psas,
|
||||
Public,
|
||||
Quotes,
|
||||
Results,
|
||||
//Results,
|
||||
Users,
|
||||
ApeKeys,
|
||||
Configuration,
|
||||
|
|
|
@ -5,6 +5,7 @@ import axios from "axios";
|
|||
import { buildClient } from "./endpoints/ApeClient";
|
||||
import { configContract } from "./../../../../shared/contract/config.contract";
|
||||
import { userContract } from "./../../../../shared/contract/user.contract";
|
||||
import { resultsContract } from "./../../../../shared/contract/results.contract";
|
||||
|
||||
const API_PATH = "";
|
||||
const BASE_URL = envConfig.backendUrl;
|
||||
|
@ -20,7 +21,7 @@ const axiosClient = axios.create({
|
|||
const Ape = {
|
||||
users: new endpoints.Users(httpClient),
|
||||
configs: new endpoints.Configs(httpClient),
|
||||
results: new endpoints.Results(httpClient),
|
||||
//results: new endpoints.Results(httpClient),
|
||||
psas: new endpoints.Psas(httpClient),
|
||||
quotes: new endpoints.Quotes(httpClient),
|
||||
leaderboards: new endpoints.Leaderboards(httpClient),
|
||||
|
@ -30,6 +31,7 @@ const Ape = {
|
|||
configuration: new endpoints.Configuration(httpClient),
|
||||
usersV2: buildClient(userContract, axios, BASE_URL),
|
||||
configsV2: buildClient(configContract, axios, BASE_URL),
|
||||
results: buildClient(resultsContract, axios, BASE_URL),
|
||||
};
|
||||
|
||||
export default Ape;
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
import * as ConnectionState from "../states/connection";
|
||||
import { navigate } from "./route-controller";
|
||||
import { getHtmlByUserFlags } from "./user-flag-controller";
|
||||
import { MonkeyErrorType } from "@shared/contract/common.contract";
|
||||
|
||||
let signedOutThisSession = false;
|
||||
|
||||
|
@ -211,11 +212,14 @@ export async function loadUser(user: UserType): Promise<void> {
|
|||
if (TestLogic.notSignedInLastResult !== null && !signedOutThisSession) {
|
||||
TestLogic.setNotSignedInUid(user.uid);
|
||||
|
||||
const response = await Ape.results.save(TestLogic.notSignedInLastResult);
|
||||
const response = await Ape.results.save({
|
||||
body: { result: TestLogic.notSignedInLastResult },
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
return Notifications.add(
|
||||
"Failed to save last result: " + response.message,
|
||||
"Failed to save last result: " +
|
||||
(response.body as MonkeyErrorType).message,
|
||||
-1
|
||||
);
|
||||
}
|
||||
|
@ -602,7 +606,9 @@ async function signUp(): Promise<void> {
|
|||
if (TestLogic.notSignedInLastResult !== null) {
|
||||
TestLogic.setNotSignedInUid(createdAuthUser.user.uid);
|
||||
|
||||
const response = await Ape.results.save(TestLogic.notSignedInLastResult);
|
||||
const response = await Ape.results.save({
|
||||
body: { result: TestLogic.notSignedInLastResult },
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const result = TestLogic.notSignedInLastResult;
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
ModifiableTestActivityCalendar,
|
||||
} from "./elements/test-activity-calendar";
|
||||
import * as Loader from "./elements/loader";
|
||||
import { CompletedEvent, Result } from "@shared/contract/results.contract";
|
||||
import { MonkeyErrorType } from "@shared/contract/common.contract";
|
||||
|
||||
let dbSnapshot: MonkeyTypes.Snapshot | undefined;
|
||||
|
||||
|
@ -275,15 +277,17 @@ export async function getUserResults(offset?: number): Promise<boolean> {
|
|||
LoadingPage.updateBar(90);
|
||||
}
|
||||
|
||||
const response = await Ape.results.get(offset);
|
||||
const response = await Ape.results.get({ query: { offset: 0 } });
|
||||
|
||||
if (response.status !== 200) {
|
||||
Notifications.add("Error getting results: " + response.message, -1);
|
||||
Notifications.add(
|
||||
"Error getting results: " + (response.body as MonkeyErrorType).message,
|
||||
-1
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const results =
|
||||
response.data as SharedTypes.DBResult<SharedTypes.Config.Mode>[];
|
||||
const results = response.body.data;
|
||||
results?.sort((a, b) => b.timestamp - a.timestamp);
|
||||
results.forEach((result) => {
|
||||
if (result.bailedOut === undefined) result.bailedOut = false;
|
||||
|
@ -910,7 +914,7 @@ export async function saveConfig(config: SharedTypes.Config): Promise<void> {
|
|||
}
|
||||
|
||||
export function saveLocalResult(
|
||||
result: SharedTypes.Result<SharedTypes.Config.Mode>
|
||||
result: SharedTypes.Result<SharedTypes.Config.Mode> | CompletedEvent
|
||||
): void {
|
||||
const snapshot = getSnapshot();
|
||||
if (!snapshot) return;
|
||||
|
|
|
@ -7,6 +7,7 @@ import * as ConnectionState from "../states/connection";
|
|||
import { areUnsortedArraysEqual } from "../utils/arrays";
|
||||
import * as Result from "../test/result";
|
||||
import AnimatedModal from "../utils/animated-modal";
|
||||
import { MonkeyErrorType } from "@shared/contract/common.contract";
|
||||
|
||||
type State = {
|
||||
resultId: string;
|
||||
|
@ -104,7 +105,9 @@ function toggleTag(tagId: string): void {
|
|||
|
||||
async function save(): Promise<void> {
|
||||
Loader.show();
|
||||
const response = await Ape.results.updateTags(state.resultId, state.tags);
|
||||
const response = await Ape.results.updateTags({
|
||||
body: { resultId: state.resultId, tagIds: state.tags },
|
||||
});
|
||||
Loader.hide();
|
||||
|
||||
//if got no freaking idea why this is needed
|
||||
|
@ -114,13 +117,14 @@ async function save(): Promise<void> {
|
|||
|
||||
if (response.status !== 200) {
|
||||
return Notifications.add(
|
||||
"Failed to update result tags: " + response.message,
|
||||
"Failed to update result tags: " +
|
||||
(response.body as MonkeyErrorType).message,
|
||||
-1
|
||||
);
|
||||
}
|
||||
|
||||
//can do this because the response will not be null if the status is 200
|
||||
const responseTagPbs = response.data?.tagPbs ?? [];
|
||||
const responseTagPbs = response.body.data?.tagPbs ?? [];
|
||||
|
||||
Notifications.add("Tags updated", 1, {
|
||||
duration: 2,
|
||||
|
|
|
@ -95,9 +95,9 @@ async function apply(): Promise<void> {
|
|||
if (TestLogic.notSignedInLastResult !== null) {
|
||||
TestLogic.setNotSignedInUid(signedInUser.user.uid);
|
||||
|
||||
const resultsSaveResponse = await Ape.results.save(
|
||||
TestLogic.notSignedInLastResult
|
||||
);
|
||||
const resultsSaveResponse = await Ape.results.save({
|
||||
body: { result: TestLogic.notSignedInLastResult },
|
||||
});
|
||||
|
||||
if (resultsSaveResponse.status === 200) {
|
||||
const result = TestLogic.notSignedInLastResult;
|
||||
|
|
|
@ -58,11 +58,12 @@ import * as KeymapEvent from "../observables/keymap-event";
|
|||
import * as LayoutfluidFunboxTimer from "../test/funbox/layoutfluid-funbox-timer";
|
||||
import * as ArabicLazyMode from "../states/arabic-lazy-mode";
|
||||
import Format from "../utils/format";
|
||||
import { CompletedEvent } from "@shared/contract/results.contract";
|
||||
|
||||
let failReason = "";
|
||||
const koInputVisual = document.getElementById("koInputVisual") as HTMLElement;
|
||||
|
||||
export let notSignedInLastResult: SharedTypes.CompletedEvent | null = null;
|
||||
export let notSignedInLastResult: CompletedEvent | null = null;
|
||||
|
||||
export function clearNotSignedInResult(): void {
|
||||
notSignedInLastResult = null;
|
||||
|
|
4
frontend/src/ts/types/types.d.ts
vendored
4
frontend/src/ts/types/types.d.ts
vendored
|
@ -1,3 +1,5 @@
|
|||
import { CompletedEvent } from "@shared/contract/results.contract";
|
||||
|
||||
declare namespace MonkeyTypes {
|
||||
type PageName =
|
||||
| "loading"
|
||||
|
@ -235,7 +237,7 @@ declare namespace MonkeyTypes {
|
|||
config: SharedTypes.Config;
|
||||
tags: UserTag[];
|
||||
presets: SnapshotPreset[];
|
||||
results?: SharedTypes.Result<SharedTypes.Config.Mode>[];
|
||||
results?: SharedTypes.Result<SharedTypes.Config.Mode>[] | CompletedEvent[];
|
||||
xp: number;
|
||||
testActivity?: ModifiableTestActivityCalendar;
|
||||
testActivityByYear?: { [key: string]: TestActivityCalendar };
|
||||
|
|
|
@ -13,3 +13,46 @@ export const MonkeyErrorSchema = z.object({
|
|||
uid: z.string().optional(),
|
||||
});
|
||||
export type MonkeyErrorType = z.infer<typeof MonkeyErrorSchema>;
|
||||
|
||||
export const CustomTextModeSchema = z.enum(["repeat", "random", "suffle"]);
|
||||
export type CustomTextMode = z.infer<typeof CustomTextModeSchema>;
|
||||
|
||||
export const CustomTextLimitModeSchema = z.enum(["word", "time", "section"]);
|
||||
export type CustomTextLimitMode = z.infer<typeof CustomTextLimitModeSchema>;
|
||||
|
||||
export const DifficultySchema = z.enum(["normal", "expert", "master"]);
|
||||
export type Difficulty = z.infer<typeof DifficultySchema>;
|
||||
|
||||
const StringNumberSchema = z.custom<`${number}`>((val) => {
|
||||
return typeof val === "string" ? /^\d+$/.test(val) : false;
|
||||
});
|
||||
export type StringNumber = z.infer<typeof StringNumberSchema>;
|
||||
export const PersonalBestSchema = z.object({}); //TODO define
|
||||
export type PersonnalBest = z.infer<typeof PersonalBestsSchema>;
|
||||
|
||||
export const PersonalBestsSchema = z.object({
|
||||
time: z.record(StringNumberSchema, z.array(PersonalBestSchema)),
|
||||
words: z.record(StringNumberSchema, z.array(PersonalBestSchema)),
|
||||
quote: z.record(StringNumberSchema, z.array(PersonalBestSchema)),
|
||||
custom: z.record(z.literal("custom"), z.array(PersonalBestSchema)),
|
||||
zen: z.record(z.literal("zen"), z.array(PersonalBestSchema)),
|
||||
});
|
||||
export type PersonalBests = z.infer<typeof PersonalBestsSchema>;
|
||||
|
||||
export const ModeSchema = PersonalBestsSchema.keyof();
|
||||
export type Mode = z.infer<typeof ModeSchema>;
|
||||
|
||||
export const Mode2Schema = z.union([
|
||||
StringNumberSchema,
|
||||
z.literal("custom"),
|
||||
z.literal("zen"),
|
||||
]);
|
||||
export type Mode2 = z.infer<typeof Mode2Schema>;
|
||||
|
||||
export const KeyStatsSchema = z.object({
|
||||
average: z.number(),
|
||||
sd: z.number(),
|
||||
});
|
||||
export type KeyStats = z.infer<typeof KeyStatsSchema>;
|
||||
|
||||
export const IdSchema = z.string().regex(/^[a-f\d]{24}$/i);
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { initContract } from "@ts-rest/core";
|
||||
import { userContract } from "./user.contract";
|
||||
import { configContract } from "./config.contract";
|
||||
import { resultsContract } from "./results.contract";
|
||||
|
||||
const c = initContract();
|
||||
|
||||
export const contract = c.router({
|
||||
users: userContract,
|
||||
configs: configContract,
|
||||
results: resultsContract,
|
||||
});
|
||||
|
|
182
shared/contract/results.contract.ts
Normal file
182
shared/contract/results.contract.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
import * as Common from "./common.contract";
|
||||
import { MonkeyResponseSchema, MonkeyErrorSchema } from "./common.contract";
|
||||
|
||||
const c = initContract();
|
||||
|
||||
//@ts-ignore
|
||||
const token = () => z.string().regex(/^[\w.]+/);
|
||||
|
||||
const x = typeof token();
|
||||
|
||||
const ChartDataSchema = z.object({
|
||||
wpm: z.array(z.number().min(0)).max(122),
|
||||
raw: z.array(z.number().min(0)).max(122),
|
||||
err: z.array(z.number().min(0)).max(122),
|
||||
});
|
||||
|
||||
const GetResultsQuerySchema = z.object({
|
||||
onOrAfterTimestamp: z.number().int().min(1589428800000).optional(),
|
||||
limit: z.number().int().min(0).max(1000).optional(),
|
||||
offset: z.number().int().min(0).optional(),
|
||||
});
|
||||
export type GetResultsQuery = z.infer<typeof GetResultsQuerySchema>;
|
||||
|
||||
const ResultSchema = z.object({
|
||||
_id: z.string().readonly(),
|
||||
acc: z.number().min(50).max(100),
|
||||
afkDuration: z.number().min(0),
|
||||
bailedOut: z.boolean(),
|
||||
blindMode: z.boolean(),
|
||||
charStats: z.array(z.number().min(0)).length(4),
|
||||
chartData: ChartDataSchema.or(z.literal("toolong")),
|
||||
consistency: z.number().min(0).max(100),
|
||||
difficulty: Common.DifficultySchema,
|
||||
funbox: z
|
||||
.string()
|
||||
.max(100)
|
||||
.regex(/[\w#]+/),
|
||||
incompleteTestSeconds: z.number().min(0),
|
||||
incompleteTests: z.array(
|
||||
z.object({
|
||||
acc: z.number().min(0).max(100),
|
||||
seconds: z.number().min(0),
|
||||
})
|
||||
),
|
||||
isPb: z.boolean(),
|
||||
keyConsistency: z.number().min(0).max(100),
|
||||
keyDurationStats: Common.KeyStatsSchema,
|
||||
keySpacingStats: Common.KeyStatsSchema,
|
||||
language: token().max(100),
|
||||
lazyMode: z.boolean(),
|
||||
mode: Common.ModeSchema,
|
||||
mode2: Common.Mode2Schema,
|
||||
name: z.string(),
|
||||
numbers: z.boolean(),
|
||||
punctuation: z.boolean(),
|
||||
quoteLength: z.number().min(0).max(3).optional(),
|
||||
rawWpm: z.number().min(0).readonly(),
|
||||
restartCount: z.number(),
|
||||
tags: z.array(Common.IdSchema),
|
||||
testDuration: z.number().min(1),
|
||||
timestamp: z.number().int().min(1589428800000),
|
||||
uid: token().max(100),
|
||||
wpm: z.number().min(0).max(420),
|
||||
});
|
||||
export type Result = z.infer<typeof ResultSchema>;
|
||||
|
||||
const GetResultsSchema = z.array(ResultSchema);
|
||||
export type GetResults = z.infer<typeof GetResultsSchema>;
|
||||
|
||||
const CompletedEventSchema = ResultSchema.omit({
|
||||
_id: true,
|
||||
isPb: true,
|
||||
keyDurationStats: true,
|
||||
keySpacingStats: true,
|
||||
rawWpm: true,
|
||||
}).extend({
|
||||
challenge: token().max(100).optional(),
|
||||
charTotal: z.number().min(0).optional(),
|
||||
customText: z
|
||||
.object({
|
||||
textLen: z.number(),
|
||||
mode: Common.CustomTextModeSchema,
|
||||
pipeDelimiter: z.boolean(),
|
||||
limit: z.object({
|
||||
mode: Common.CustomTextLimitModeSchema,
|
||||
value: z.number().min(0),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
hash: token().max(100).optional(),
|
||||
keyDuration: z.array(z.number().min(0)).or(z.literal("toolong")).optional(),
|
||||
keySpacing: z.array(z.number().min(0)).or(z.literal("toolong")).optional(),
|
||||
keyOverlap: z.number().min(0).optional(),
|
||||
lastKeyToEnd: z.number().min(0).optional(),
|
||||
startToFirstKey: z.number().min(0).optional(),
|
||||
wpmConsistency: z.number().min(0).max(100),
|
||||
stopOnLetter: z.boolean(),
|
||||
});
|
||||
export type CompletedEvent = z.infer<typeof CompletedEventSchema>;
|
||||
|
||||
const CreateResultBodySchema = z.object({
|
||||
result: CompletedEventSchema,
|
||||
});
|
||||
export type CreateResultBody = z.infer<typeof CreateResultBodySchema>;
|
||||
|
||||
const CreateResultSchema = z.object({
|
||||
isPb: z.boolean(),
|
||||
tagPbs: z.array(z.string()),
|
||||
insertedId: z.string(),
|
||||
dailyLeaderboardRank: z.number().optional(),
|
||||
weeklyXpLeaderboardRank: z.number().optional(),
|
||||
xp: z.number(),
|
||||
dailyXpBonus: z.boolean(),
|
||||
xpBreakdown: z.record(z.string(), z.number()),
|
||||
streak: z.number(),
|
||||
});
|
||||
export type CreateResult = z.infer<typeof CreateResultSchema>;
|
||||
|
||||
const UpdateTagsBodySchema = z.object({
|
||||
tagIds: z.array(Common.IdSchema),
|
||||
resultId: Common.IdSchema,
|
||||
});
|
||||
export type UpdateTagsBody = z.infer<typeof UpdateTagsBodySchema>;
|
||||
|
||||
const UpdateTagsSchema = z.object({
|
||||
tagPbs: z.array(Common.IdSchema),
|
||||
});
|
||||
export type UpdateTags = z.infer<typeof UpdateTagsSchema>;
|
||||
|
||||
export const resultsContract = c.router(
|
||||
{
|
||||
get: {
|
||||
method: "GET",
|
||||
path: "/",
|
||||
query: GetResultsQuerySchema,
|
||||
responses: {
|
||||
200: MonkeyResponseSchema.extend({ data: GetResultsSchema }),
|
||||
400: MonkeyErrorSchema,
|
||||
},
|
||||
},
|
||||
save: {
|
||||
method: "POST",
|
||||
path: "/",
|
||||
body: CreateResultBodySchema,
|
||||
responses: {
|
||||
200: MonkeyResponseSchema.extend({ data: CreateResultSchema }),
|
||||
400: MonkeyErrorSchema,
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
method: "DELETE",
|
||||
path: "/",
|
||||
body: z.object({}),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
400: MonkeyErrorSchema,
|
||||
},
|
||||
},
|
||||
getLast: {
|
||||
method: "GET",
|
||||
path: "/last",
|
||||
responses: {
|
||||
200: MonkeyResponseSchema.extend({ data: ResultSchema }),
|
||||
400: MonkeyErrorSchema,
|
||||
},
|
||||
},
|
||||
updateTags: {
|
||||
method: "PATCH",
|
||||
path: "/tags",
|
||||
body: UpdateTagsBodySchema,
|
||||
responses: {
|
||||
200: MonkeyResponseSchema.extend({ data: UpdateTagsSchema }),
|
||||
400: MonkeyErrorSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/results",
|
||||
}
|
||||
);
|
|
@ -41,6 +41,5 @@ export const userContract = c.router(
|
|||
},
|
||||
{
|
||||
pathPrefix: "/v2/users",
|
||||
strictStatusCodes: true,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -13,5 +13,6 @@
|
|||
"types": ["node"],
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["./types/**/*"],
|
||||
"exclude": ["node_modules", "./dist/**/*"]
|
||||
}
|
||||
|
|
6
shared/types/config.d.ts
vendored
6
shared/types/config.d.ts
vendored
|
@ -12,9 +12,9 @@ declare namespace SharedTypes.Config {
|
|||
| "underline"
|
||||
| "carrot"
|
||||
| "banana";
|
||||
type Difficulty = "normal" | "expert" | "master";
|
||||
type Mode = keyof PersonalBests;
|
||||
type Mode2<M extends Mode> = M extends M ? keyof PersonalBests[M] : never;
|
||||
type Difficulty = "normal" | "expert" | "master"; //TODO moved to contract/common
|
||||
type Mode = keyof PersonalBests; //TODO moved to contract/common
|
||||
type Mode2<M extends Mode> = M extends M ? keyof PersonalBests[M] : never; //TODO moved to contract/common
|
||||
type Mode2Custom<M extends Mode> = Mode2<M> | "custom";
|
||||
type ConfidenceMode = "off" | "on" | "max";
|
||||
type IndicateTypos = "off" | "below" | "replace";
|
||||
|
|
15
shared/types/types.d.ts
vendored
15
shared/types/types.d.ts
vendored
|
@ -1,4 +1,6 @@
|
|||
// Shared types between server/client.
|
||||
//const ResultSchemas = import("../contract/results.contract");
|
||||
|
||||
declare namespace SharedTypes {
|
||||
interface ValidModeRule {
|
||||
language: string;
|
||||
|
@ -111,9 +113,10 @@ declare namespace SharedTypes {
|
|||
};
|
||||
}
|
||||
|
||||
type StringNumber = `${number}`;
|
||||
type StringNumber = `${number}`; //TODO moved to contract/custom
|
||||
|
||||
interface PersonalBest {
|
||||
//TODO moved to contract/custom
|
||||
acc: number;
|
||||
consistency?: number;
|
||||
difficulty: SharedTypes.Config.Difficulty;
|
||||
|
@ -127,6 +130,7 @@ declare namespace SharedTypes {
|
|||
}
|
||||
|
||||
interface PersonalBests {
|
||||
//TODO moved to contract/custom
|
||||
time: Record<StringNumber, PersonalBest[]>;
|
||||
words: Record<StringNumber, PersonalBest[]>;
|
||||
quote: Record<StringNumber, PersonalBest[]>;
|
||||
|
@ -146,6 +150,7 @@ declare namespace SharedTypes {
|
|||
}
|
||||
|
||||
interface KeyStats {
|
||||
//TODO moved to contracts/common
|
||||
average: number;
|
||||
sd: number;
|
||||
}
|
||||
|
@ -184,7 +189,7 @@ declare namespace SharedTypes {
|
|||
}
|
||||
|
||||
type DBResult<T extends SharedTypes.Config.Mode> = Omit<
|
||||
SharedTypes.Result<T>,
|
||||
Result<T>,
|
||||
| "bailedOut"
|
||||
| "blindMode"
|
||||
| "lazyMode"
|
||||
|
@ -226,6 +231,7 @@ declare namespace SharedTypes {
|
|||
};
|
||||
|
||||
interface CompletedEvent extends Result<SharedTypes.Config.Mode> {
|
||||
//TODO moved to contracts/result
|
||||
keySpacing: number[] | "toolong";
|
||||
keyDuration: number[] | "toolong";
|
||||
customText?: CustomTextDataWithTextLen;
|
||||
|
@ -240,8 +246,8 @@ declare namespace SharedTypes {
|
|||
stopOnLetter: boolean;
|
||||
}
|
||||
|
||||
type CustomTextMode = "repeat" | "random" | "shuffle";
|
||||
type CustomTextLimitMode = "word" | "time" | "section";
|
||||
type CustomTextMode = "repeat" | "random" | "shuffle"; //TODO moved to contract/common
|
||||
type CustomTextLimitMode = "word" | "time" | "section"; //TODO moved to contract/common
|
||||
type CustomTextLimit = {
|
||||
value: number;
|
||||
mode: CustomTextLimitMode;
|
||||
|
@ -463,6 +469,7 @@ declare namespace SharedTypes {
|
|||
}
|
||||
|
||||
type PostResultResponse = {
|
||||
//TODO moved to contracts/results
|
||||
isPb: boolean;
|
||||
tagPbs: string[];
|
||||
insertedId: string;
|
||||
|
|
Loading…
Reference in a new issue