adding monkeytype
Some checks failed
Mark Stale PRs / stale (push) Has been cancelled

This commit is contained in:
Benjamin Falch
2026-04-23 13:53:44 +02:00
parent e214a2fd35
commit 2bc741fb78
1930 changed files with 7590652 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
{
"ignorePatterns": ["node_modules", "dist", ".turbo"],
"extends": [
"../oxlint-config/index.jsonc"
// "@monkeytype/oxlint-config"
]
}

View File

@@ -0,0 +1,83 @@
import { describe, it, expect } from "vitest";
import { CustomBackgroundSchema } from "@monkeytype/schemas/configs";
describe("config schema", () => {
describe("CustomBackgroundSchema", () => {
it.for([
{
name: "http",
input: `http://example.com/path/image.png`,
},
{
name: "https",
input: `https://example.com/path/image.png`,
},
{
name: "png",
input: `https://example.com/path/image.png`,
},
{
name: "gif",
input: `https://example.com/path/image.gif?width=5`,
},
{
name: "jpeg",
input: `https://example.com/path/image.jpeg`,
},
{
name: "jpg",
input: `https://example.com/path/image.jpg`,
},
{
name: "tiff",
input: `https://example.com/path/image.tiff`,
expectedError: "Unsupported image format",
},
{
name: "non-url",
input: `test`,
expectedError: "Needs to be an URI",
},
{
name: "single quotes",
input: `https://example.com/404.jpg?q=alert('1')`,
expectedError: "May not contain quotes",
},
{
name: "double quotes",
input: `https://example.com/404.jpg?q=alert("1")`,
expectedError: "May not contain quotes",
},
{
name: "back tick",
input: `https://example.com/404.jpg?q=alert(\`1\`)`,
expectedError: "May not contain quotes",
},
{
name: "javascript url",
input: `javascript:alert('asdf');//https://example.com/img.jpg`,
expectedError: "Unsupported protocol",
},
{
name: "data url",
input: `data:image/gif;base64,data`,
expectedError: "Unsupported protocol",
},
{
name: "long url",
input: `https://example.com/path/image.jpeg?q=${new Array(2048)
.fill("x")
.join()}`,
expectedError: "URL is too long",
},
])(`$name`, ({ input, expectedError }) => {
const parsed = CustomBackgroundSchema.safeParse(input);
if (expectedError !== undefined) {
expect(parsed.success).toEqual(false);
expect(parsed.error?.issues[0]?.message).toEqual(expectedError);
} else {
expect(parsed.success).toEqual(true);
}
});
});
});

View File

@@ -0,0 +1,7 @@
{
"extends": "@monkeytype/typescript-config/base.json",
"compilerOptions": {
"noEmit": true
},
"include": ["./**/*.ts", "./**/*.spec.ts", "./setup-tests.ts"]
}

View File

@@ -0,0 +1,56 @@
import { describe, it, expect } from "vitest";
import { nameWithSeparators, slug } from "../src/util";
describe("Schema Validation Tests", () => {
describe("nameWithSeparators", () => {
const schema = nameWithSeparators();
it("accepts valid names", () => {
expect(schema.safeParse("valid_name").success).toBe(true);
expect(schema.safeParse("valid-name").success).toBe(true);
expect(schema.safeParse("valid123").success).toBe(true);
expect(schema.safeParse("Valid_Name-Check").success).toBe(true);
});
it("rejects leading/trailing separators", () => {
expect(schema.safeParse("_invalid").success).toBe(false);
expect(schema.safeParse("invalid-").success).toBe(false);
});
it("rejects consecutive separators", () => {
expect(schema.safeParse("inv__alid").success).toBe(false);
expect(schema.safeParse("inv--alid").success).toBe(false);
expect(schema.safeParse("inv-_alid").success).toBe(false);
});
it("rejects dots", () => {
expect(schema.safeParse("invalid.dot").success).toBe(false);
expect(schema.safeParse(".invalid").success).toBe(false);
});
});
describe("slug", () => {
const schema = slug();
it("accepts valid slugs", () => {
expect(schema.safeParse("valid-slug.123_test").success).toBe(true);
expect(schema.safeParse("valid.dots").success).toBe(true);
expect(schema.safeParse("_leading_underscore_is_fine").success).toBe(
true,
);
expect(schema.safeParse("-leading_hyphen_is_fine").success).toBe(true);
expect(schema.safeParse("trailing_is_fine_in_slug_").success).toBe(true);
});
it("rejects leading dots", () => {
expect(schema.safeParse(".invalid").success).toBe(false);
});
it("rejects invalid characters", () => {
expect(schema.safeParse("invalid,comma").success).toBe(false);
expect(schema.safeParse(",invalid").success).toBe(false);
expect(schema.safeParse("invalid space").success).toBe(false);
expect(schema.safeParse("invalid#hash").success).toBe(false);
});
});
});

View File

@@ -0,0 +1,38 @@
{
"name": "@monkeytype/schemas",
"private": true,
"exports": {
".": {
"types": "./src/index.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./*": {
"types": "./src/*.ts",
"import": "./dist/*.mjs",
"require": "./dist/*.js"
}
},
"scripts": {
"dev": "tsup-node --watch",
"build": "npm run madge && tsup-node",
"test": "vitest run",
"madge": " madge --circular --extensions ts ./src",
"ts-check": "tsc --noEmit",
"lint": "oxlint . --type-aware --type-check",
"lint-fast": "oxlint ."
},
"devDependencies": {
"@monkeytype/tsup-config": "workspace:*",
"@monkeytype/typescript-config": "workspace:*",
"madge": "8.0.0",
"oxlint": "1.60.0",
"oxlint-tsgolint": "0.21.0",
"tsup": "8.4.0",
"typescript": "6.0.2",
"vitest": "4.1.0"
},
"peerDependencies": {
"zod": "3.23.8"
}
}

View File

@@ -0,0 +1,19 @@
import { z } from "zod";
import { IdSchema, slug } from "./util";
export const ApeKeyNameSchema = slug().max(20);
export const ApeKeyUserDefinedSchema = z.object({
name: ApeKeyNameSchema,
enabled: z.boolean(),
});
export const ApeKeySchema = ApeKeyUserDefinedSchema.extend({
createdOn: z.number().min(0),
modifiedOn: z.number().min(0),
lastUsedOn: z.number().min(0).or(z.literal(-1)),
});
export type ApeKey = z.infer<typeof ApeKeySchema>;
export const ApeKeysSchema = z.record(IdSchema, ApeKeySchema);
export type ApeKeys = z.infer<typeof ApeKeysSchema>;

View File

@@ -0,0 +1,52 @@
import { z } from "zod";
import { FunboxNameSchema, PartialConfigSchema } from "./configs";
const MinRequiredNumber = z.object({ min: z.number() }).strict();
const MaxRequiredNumber = z.object({ max: z.number() }).strict();
const ExactRequiredNumber = z.object({ exact: z.number() }).strict();
export const ChallengeSchema = z
.object({
name: z.string(),
display: z.string(),
autoRole: z.boolean().optional(),
type: z.enum([
"customTime",
"customWords",
"customText",
"script",
"accuracy",
"funbox",
"other",
]),
message: z.string().optional(),
parameters: z.array(
z
.string()
.or(z.null())
.or(z.number())
.or(z.boolean())
.or(z.array(FunboxNameSchema)),
),
requirements: z
.object({
wpm: ExactRequiredNumber.or(MinRequiredNumber),
acc: ExactRequiredNumber.or(MinRequiredNumber),
afk: MaxRequiredNumber,
time: MinRequiredNumber,
funbox: z
.object({
exact: z.array(FunboxNameSchema),
})
.partial(),
raw: ExactRequiredNumber,
con: ExactRequiredNumber,
config: PartialConfigSchema,
})
.partial()
.strict()
.optional(),
})
.strict();
export type Challenge = z.infer<typeof ChallengeSchema>;

View File

@@ -0,0 +1,518 @@
import { z, ZodSchema } from "zod";
import * as Shared from "./shared";
import * as Themes from "./themes";
import * as Layouts from "./layouts";
import { LanguageSchema } from "./languages";
import { FontNameSchema } from "./fonts";
export const SmoothCaretSchema = z.enum(["off", "slow", "medium", "fast"]);
export type SmoothCaret = z.infer<typeof SmoothCaretSchema>;
export const QuickRestartSchema = z.enum(["off", "esc", "tab", "enter"]);
export type QuickRestart = z.infer<typeof QuickRestartSchema>;
export const QuoteLengthSchema = z.union([
z.literal(-3),
z.literal(-2),
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(3),
]);
export type QuoteLength = z.infer<typeof QuoteLengthSchema>;
export const QuoteLengthConfigSchema = z
.array(QuoteLengthSchema)
.describe(
[
"|value|description|\n|-|-|",
"|-3|Favorite quotes|",
"|-2|Quote search|",
"|0|Short quotes|",
"|1|Medium quotes|",
"|2|Long quotes|",
"|3|Thicc quotes|",
].join("\n"),
);
export type QuoteLengthConfig = z.infer<typeof QuoteLengthConfigSchema>;
export const CaretStyleSchema = z.enum([
"off",
"default",
"block",
"outline",
"underline",
"carrot",
"banana",
"monkey",
]);
export type CaretStyle = z.infer<typeof CaretStyleSchema>;
export const ConfidenceModeSchema = z.enum(["off", "on", "max"]);
export type ConfidenceMode = z.infer<typeof ConfidenceModeSchema>;
export const IndicateTyposSchema = z.enum(["off", "below", "replace", "both"]);
export type IndicateTypos = z.infer<typeof IndicateTyposSchema>;
export const CompositionDisplaySchema = z.enum(["off", "below", "replace"]);
export type CompositionDisplay = z.infer<typeof CompositionDisplaySchema>;
export const TimerStyleSchema = z.enum([
"off",
"bar",
"text",
"mini",
"flash_text",
"flash_mini",
]);
export type TimerStyle = z.infer<typeof TimerStyleSchema>;
export const LiveSpeedAccBurstStyleSchema = z.enum(["off", "text", "mini"]);
export type LiveSpeedAccBurstStyle = z.infer<
typeof LiveSpeedAccBurstStyleSchema
>;
export const RandomThemeSchema = z.enum([
"off",
"on",
"fav",
"light",
"dark",
"custom",
"auto",
]);
export type RandomTheme = z.infer<typeof RandomThemeSchema>;
export const TimerColorSchema = z.enum(["black", "sub", "text", "main"]);
export type TimerColor = z.infer<typeof TimerColorSchema>;
export const TimerOpacitySchema = z.enum(["0.25", "0.5", "0.75", "1"]);
export type TimerOpacity = z.infer<typeof TimerOpacitySchema>;
export const StopOnErrorSchema = z.enum(["off", "word", "letter"]);
export type StopOnError = z.infer<typeof StopOnErrorSchema>;
export const KeymapModeSchema = z.enum(["off", "static", "react", "next"]);
export type KeymapMode = z.infer<typeof KeymapModeSchema>;
export const KeymapStyleSchema = z.enum([
"staggered",
"alice",
"matrix",
"split",
"split_matrix",
"steno",
"steno_matrix",
]);
export type KeymapStyle = z.infer<typeof KeymapStyleSchema>;
export const KeymapLegendStyleSchema = z.enum([
"lowercase",
"uppercase",
"blank",
"dynamic",
]);
export type KeymapLegendStyle = z.infer<typeof KeymapLegendStyleSchema>;
export const KeymapShowTopRowSchema = z.enum(["always", "layout", "never"]);
export type KeymapShowTopRow = z.infer<typeof KeymapShowTopRowSchema>;
export const KeymapSizeSchema = z.number().min(0.5).max(3.5).step(0.1);
export type KeymapSize = z.infer<typeof KeymapSizeSchema>;
export const SingleListCommandLineSchema = z.enum(["manual", "on"]);
export type SingleListCommandLine = z.infer<typeof SingleListCommandLineSchema>;
export const PlaySoundOnErrorSchema = z.enum(["off", "1", "2", "3", "4"]);
export type PlaySoundOnError = z.infer<typeof PlaySoundOnErrorSchema>;
export const PlaySoundOnClickSchema = z.enum([
"off",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
]);
export type PlaySoundOnClick = z.infer<typeof PlaySoundOnClickSchema>;
export const SoundVolumeSchema = z.number().min(0).max(1);
export type SoundVolume = z.infer<typeof SoundVolumeSchema>;
export const PaceCaretSchema = z.enum([
"off",
"average",
"pb",
"tagPb",
"last",
"custom",
"daily",
]);
export type PaceCaret = z.infer<typeof PaceCaretSchema>;
export const AccountChartSchema = z.tuple([
z.enum(["on", "off"]),
z.enum(["on", "off"]),
z.enum(["on", "off"]),
z.enum(["on", "off"]),
]);
export type AccountChart = z.infer<typeof AccountChartSchema>;
export const MinimumWordsPerMinuteSchema = z.enum(["off", "custom"]);
export type MinimumWordsPerMinute = z.infer<typeof MinimumWordsPerMinuteSchema>;
export const HighlightModeSchema = z.enum([
"off",
"letter",
"word",
"next_word",
"next_two_words",
"next_three_words",
]);
export type HighlightMode = z.infer<typeof HighlightModeSchema>;
export const TypedEffectSchema = z.enum(["keep", "hide", "fade", "dots"]);
export type TypedEffect = z.infer<typeof TypedEffectSchema>;
export const TapeModeSchema = z.enum(["off", "letter", "word"]);
export type TapeMode = z.infer<typeof TapeModeSchema>;
export const TapeMarginSchema = z.number().min(10).max(90);
export type TapeMargin = z.infer<typeof TapeMarginSchema>;
export const TypingSpeedUnitSchema = z.enum([
"wpm",
"cpm",
"wps",
"cps",
"wph",
]);
export type TypingSpeedUnit = z.infer<typeof TypingSpeedUnitSchema>;
export const AdsSchema = z.enum(["off", "result", "on", "sellout"]);
export type Ads = z.infer<typeof AdsSchema>;
export const MinimumAccuracySchema = z.enum(["off", "custom"]);
export type MinimumAccuracy = z.infer<typeof MinimumAccuracySchema>;
export const RepeatQuotesSchema = z.enum(["off", "typing"]);
export type RepeatQuotes = z.infer<typeof RepeatQuotesSchema>;
export const OppositeShiftModeSchema = z.enum(["off", "on", "keymap"]);
export type OppositeShiftMode = z.infer<typeof OppositeShiftModeSchema>;
export const CustomBackgroundSizeSchema = z.enum(["cover", "contain", "max"]);
export type CustomBackgroundSize = z.infer<typeof CustomBackgroundSizeSchema>;
export const CustomBackgroundFilterSchema = z.tuple([
z.number(),
z.number(),
z.number(),
z.number(),
]);
export type CustomBackgroundFilter = z.infer<
typeof CustomBackgroundFilterSchema
>;
export const CustomLayoutFluidSchema = z
.array(Layouts.LayoutNameSchema)
.min(2)
.max(15);
export type CustomLayoutFluid = z.infer<typeof CustomLayoutFluidSchema>;
export const CustomPolyglotSchema = z.array(LanguageSchema).min(2);
export type CustomPolyglot = z.infer<typeof CustomPolyglotSchema>;
export const MonkeyPowerLevelSchema = z.enum(["off", "1", "2", "3", "4"]);
export type MonkeyPowerLevel = z.infer<typeof MonkeyPowerLevelSchema>;
export const MinimumBurstSchema = z.enum(["off", "fixed", "flex"]);
export type MinimumBurst = z.infer<typeof MinimumBurstSchema>;
export const ShowAverageSchema = z.enum(["off", "speed", "acc", "both"]);
export type ShowAverage = z.infer<typeof ShowAverageSchema>;
export const ShowPbSchema = z.boolean();
export type ShowPb = z.infer<typeof ShowPbSchema>;
export const ColorHexValueSchema = z.string().regex(/^#([\da-f]{3}){1,2}$/i);
export type ColorHexValue = z.infer<typeof ColorHexValueSchema>;
export const DifficultySchema = Shared.DifficultySchema;
export type Difficulty = Shared.Difficulty;
export const CustomThemeColorsSchema = z.tuple([
ColorHexValueSchema,
ColorHexValueSchema,
ColorHexValueSchema,
ColorHexValueSchema,
ColorHexValueSchema,
ColorHexValueSchema,
ColorHexValueSchema,
ColorHexValueSchema,
ColorHexValueSchema,
ColorHexValueSchema,
]);
export type CustomThemeColors = z.infer<typeof CustomThemeColorsSchema>;
export const ThemeNameSchema = Themes.ThemeNameSchema;
export type ThemeName = z.infer<typeof ThemeNameSchema>;
export const FavThemesSchema = z.array(ThemeNameSchema);
export type FavThemes = z.infer<typeof FavThemesSchema>;
export const FunboxNameSchema = z.enum([
"58008",
"mirror",
"upside_down",
"nausea",
"round_round_baby",
"simon_says",
"tts",
"choo_choo",
"arrows",
"rAnDoMcAsE",
"sPoNgEcAsE",
"capitals",
"layout_mirror",
"layoutfluid",
"earthquake",
"space_balls",
"gibberish",
"ascii",
"specials",
"plus_one",
"plus_zero",
"plus_two",
"plus_three",
"read_ahead_easy",
"read_ahead",
"read_ahead_hard",
"memory",
"nospace",
"poetry",
"wikipedia",
"weakspot",
"pseudolang",
"IPv4",
"IPv6",
"binary",
"hexadecimal",
"zipf",
"morse",
"crt",
"backwards",
"ddoouubblleedd",
"instant_messaging",
"underscore_spaces",
"ALL_CAPS",
"polyglot",
"asl",
"rot13",
"no_quit",
]);
export type FunboxName = z.infer<typeof FunboxNameSchema>;
export const FunboxSchema = z.array(FunboxNameSchema).max(15);
export type Funbox = z.infer<typeof FunboxSchema>;
export const PaceCaretCustomSpeedSchema = z.number().nonnegative();
export type PaceCaretCustomSpeed = z.infer<typeof PaceCaretCustomSpeedSchema>;
export const MinWpmCustomSpeedSchema = z.number().nonnegative();
export type MinWpmCustomSpeed = z.infer<typeof MinWpmCustomSpeedSchema>;
export const MinimumAccuracyCustomSchema = z.number().nonnegative().max(100);
export type MinimumAccuracyCustom = z.infer<typeof MinimumAccuracyCustomSchema>;
export const MinimumBurstCustomSpeedSchema = z.number().nonnegative();
export type MinimumBurstCustomSpeed = z.infer<
typeof MinimumBurstCustomSpeedSchema
>;
export const TimeConfigSchema = z.number().int().nonnegative();
export type TimeConfig = z.infer<typeof TimeConfigSchema>;
export const WordCountSchema = z.number().int().nonnegative();
export type WordCount = z.infer<typeof WordCountSchema>;
export const KeymapLayoutSchema = z
.literal("overrideSync")
.or(Layouts.LayoutNameSchema);
export type KeymapLayout = z.infer<typeof KeymapLayoutSchema>;
export const LayoutSchema = z.literal("default").or(Layouts.LayoutNameSchema);
export type Layout = z.infer<typeof LayoutSchema>;
export const FontSizeSchema = z.number().positive();
export type FontSize = z.infer<typeof FontSizeSchema>;
export const MaxLineWidthSchema = z.number().min(20).max(1000).or(z.literal(0));
export type MaxLineWidth = z.infer<typeof MaxLineWidthSchema>;
export const CustomBackgroundSchema = z
.string()
.url("Needs to be an URI")
.regex(/^(https|http):\/\/.*/, "Unsupported protocol")
.regex(/^[^`'"]*$/, "May not contain quotes")
.regex(/.+(\.png|\.gif|\.jpeg|\.jpg|\.webp)/gi, "Unsupported image format")
.max(2048, "URL is too long")
.or(z.literal(""));
export type CustomBackground = z.infer<typeof CustomBackgroundSchema>;
export const PlayTimeWarningSchema = z
.enum(["off", "1", "3", "5", "10"])
.describe(
"How many seconds before the end of the test to play a warning sound.",
);
export type PlayTimeWarning = z.infer<typeof PlayTimeWarningSchema>;
export const ConfigSchema = z
.object({
// test
punctuation: z.boolean(),
numbers: z.boolean(),
words: WordCountSchema,
time: TimeConfigSchema,
mode: Shared.ModeSchema,
quoteLength: QuoteLengthConfigSchema,
language: LanguageSchema,
burstHeatmap: z.boolean(),
// behavior
difficulty: DifficultySchema,
quickRestart: QuickRestartSchema,
repeatQuotes: RepeatQuotesSchema,
resultSaving: z.boolean(),
blindMode: z.boolean(),
alwaysShowWordsHistory: z.boolean(),
singleListCommandLine: SingleListCommandLineSchema,
minWpm: MinimumWordsPerMinuteSchema,
minWpmCustomSpeed: MinWpmCustomSpeedSchema,
minAcc: MinimumAccuracySchema,
minAccCustom: MinimumAccuracyCustomSchema,
minBurst: MinimumBurstSchema,
minBurstCustomSpeed: MinimumBurstCustomSpeedSchema,
britishEnglish: z.boolean(),
funbox: FunboxSchema,
customLayoutfluid: CustomLayoutFluidSchema,
customPolyglot: CustomPolyglotSchema,
// input
freedomMode: z.boolean(),
strictSpace: z.boolean(),
oppositeShiftMode: OppositeShiftModeSchema,
stopOnError: StopOnErrorSchema,
confidenceMode: ConfidenceModeSchema,
quickEnd: z.boolean(),
indicateTypos: IndicateTyposSchema,
compositionDisplay: CompositionDisplaySchema,
hideExtraLetters: z.boolean(),
lazyMode: z.boolean(),
layout: LayoutSchema,
codeUnindentOnBackspace: z.boolean(),
// sound
soundVolume: SoundVolumeSchema,
playSoundOnClick: PlaySoundOnClickSchema,
playSoundOnError: PlaySoundOnErrorSchema,
playTimeWarning: PlayTimeWarningSchema,
// caret
smoothCaret: SmoothCaretSchema,
caretStyle: CaretStyleSchema,
paceCaret: PaceCaretSchema,
paceCaretCustomSpeed: PaceCaretCustomSpeedSchema,
paceCaretStyle: CaretStyleSchema,
repeatedPace: z.boolean(),
// appearance
timerStyle: TimerStyleSchema,
liveSpeedStyle: LiveSpeedAccBurstStyleSchema,
liveAccStyle: LiveSpeedAccBurstStyleSchema,
liveBurstStyle: LiveSpeedAccBurstStyleSchema,
timerColor: TimerColorSchema,
timerOpacity: TimerOpacitySchema,
highlightMode: HighlightModeSchema,
typedEffect: TypedEffectSchema,
tapeMode: TapeModeSchema,
tapeMargin: TapeMarginSchema,
smoothLineScroll: z.boolean(),
showAllLines: z.boolean(),
alwaysShowDecimalPlaces: z.boolean(),
typingSpeedUnit: TypingSpeedUnitSchema,
startGraphsAtZero: z.boolean(),
maxLineWidth: MaxLineWidthSchema,
fontSize: FontSizeSchema,
fontFamily: FontNameSchema,
keymapMode: KeymapModeSchema,
keymapLayout: KeymapLayoutSchema,
keymapStyle: KeymapStyleSchema,
keymapLegendStyle: KeymapLegendStyleSchema,
keymapShowTopRow: KeymapShowTopRowSchema,
keymapSize: KeymapSizeSchema,
// theme
flipTestColors: z.boolean(),
colorfulMode: z.boolean(),
customBackground: CustomBackgroundSchema,
customBackgroundSize: CustomBackgroundSizeSchema,
customBackgroundFilter: CustomBackgroundFilterSchema,
autoSwitchTheme: z.boolean(),
themeLight: ThemeNameSchema,
themeDark: ThemeNameSchema,
randomTheme: RandomThemeSchema,
favThemes: FavThemesSchema,
theme: ThemeNameSchema,
customTheme: z.boolean(),
customThemeColors: CustomThemeColorsSchema,
// hide elements
showKeyTips: z.boolean(),
showOutOfFocusWarning: z.boolean(),
capsLockWarning: z.boolean(),
showAverage: ShowAverageSchema,
showPb: ShowPbSchema,
// other (hidden)
accountChart: AccountChartSchema,
monkey: z.boolean(),
monkeyPowerLevel: MonkeyPowerLevelSchema,
// ads
ads: AdsSchema,
} satisfies Record<string, ZodSchema>)
.strict();
export type Config = z.infer<typeof ConfigSchema>;
export const ConfigKeySchema = ConfigSchema.keyof();
export type ConfigKey = z.infer<typeof ConfigKeySchema>;
export type ConfigValue = Config[keyof Config];
export const PartialConfigSchema = ConfigSchema.partial();
export type PartialConfig = z.infer<typeof PartialConfigSchema>;
export const ConfigGroupNameSchema = z.enum([
"test",
"behavior",
"input",
"sound",
"caret",
"appearance",
"theme",
"hideElements",
"hidden",
"ads",
]);
export type ConfigGroupName = z.infer<typeof ConfigGroupNameSchema>;

View File

@@ -0,0 +1,131 @@
import { z } from "zod";
/* ValidModeRuleSchema allows complex rules like `"mode2": "(15|60)"`. We don't want a strict validation here. */
export const ValidModeRuleSchema = z
.object({
language: z.string(),
mode: z.string(),
mode2: z.string(),
})
.strict();
export type ValidModeRule = z.infer<typeof ValidModeRuleSchema>;
export const RewardBracketSchema = z
.object({
minRank: z.number().int().nonnegative(),
maxRank: z.number().int().nonnegative(),
minReward: z.number().int().nonnegative(),
maxReward: z.number().int().nonnegative(),
})
.strict();
export type RewardBracket = z.infer<typeof RewardBracketSchema>;
export const ConfigurationSchema = z.object({
maintenance: z.boolean(),
dev: z.object({
responseSlowdownMs: z.number().int().nonnegative(),
}),
quotes: z.object({
reporting: z.object({
enabled: z.boolean(),
maxReports: z.number().int().nonnegative(),
contentReportLimit: z.number().int().nonnegative(),
}),
submissionsEnabled: z.boolean(),
maxFavorites: z.number().int().nonnegative(),
}),
results: z.object({
savingEnabled: z.boolean(),
objectHashCheckEnabled: z.boolean(),
filterPresets: z.object({
enabled: z.boolean(),
maxPresetsPerUser: z.number().int().nonnegative(),
}),
limits: z.object({
regularUser: z.number().int().nonnegative(),
premiumUser: z.number().int().nonnegative(),
}),
maxBatchSize: z.number().int().nonnegative(),
}),
users: z.object({
signUp: z.boolean(),
lastHashesCheck: z.object({
enabled: z.boolean(),
maxHashes: z.number().int().nonnegative(),
}),
autoBan: z.object({
enabled: z.boolean(),
maxCount: z.number().int().nonnegative(),
maxHours: z.number().int().nonnegative(),
}),
profiles: z.object({
enabled: z.boolean(),
}),
discordIntegration: z.object({
enabled: z.boolean(),
}),
xp: z.object({
enabled: z.boolean(),
funboxBonus: z.number(),
gainMultiplier: z.number(),
maxDailyBonus: z.number(),
minDailyBonus: z.number(),
streak: z.object({
enabled: z.boolean(),
maxStreakDays: z.number().nonnegative(),
maxStreakMultiplier: z.number(),
}),
}),
inbox: z.object({
enabled: z.boolean(),
maxMail: z.number().int().nonnegative(),
}),
premium: z.object({
enabled: z.boolean(),
}),
}),
admin: z.object({
endpointsEnabled: z.boolean(),
}),
apeKeys: z.object({
endpointsEnabled: z.boolean(),
acceptKeys: z.boolean(),
maxKeysPerUser: z.number().int().nonnegative(),
apeKeyBytes: z.number().int().nonnegative(),
apeKeySaltRounds: z.number().int().nonnegative(),
}),
rateLimiting: z.object({
badAuthentication: z.object({
enabled: z.boolean(),
penalty: z.number(),
flaggedStatusCodes: z.array(z.number().int().nonnegative()),
}),
}),
dailyLeaderboards: z.object({
enabled: z.boolean(),
leaderboardExpirationTimeInDays: z.number().nonnegative(),
maxResults: z.number().int().nonnegative(),
validModeRules: z.array(ValidModeRuleSchema),
scheduleRewardsModeRules: z.array(ValidModeRuleSchema),
topResultsToAnnounce: z.number().int().positive(), // This should never be 0. Setting to zero will announce all results.
xpRewardBrackets: z.array(RewardBracketSchema),
}),
leaderboards: z.object({
minTimeTyping: z
.number()
.min(0)
.describe(
"Minimum typing time (in seconds) the user needs to get on a leaderboard",
),
weeklyXp: z.object({
enabled: z.boolean(),
expirationTimeInDays: z.number().nonnegative(),
xpRewardBrackets: z.array(RewardBracketSchema),
}),
}),
connections: z.object({
enabled: z.boolean(),
maxPerUser: z.number().int().nonnegative(),
}),
});
export type Configuration = z.infer<typeof ConfigurationSchema>;

View File

@@ -0,0 +1,24 @@
import { z } from "zod";
import { IdSchema } from "./util";
export const ConnectionStatusSchema = z.enum([
"pending",
"accepted",
"blocked",
]);
export type ConnectionStatus = z.infer<typeof ConnectionStatusSchema>;
export const ConnectionTypeSchema = z.enum(["incoming", "outgoing"]);
export type ConnectionType = z.infer<typeof ConnectionTypeSchema>;
export const ConnectionSchema = z.object({
_id: IdSchema,
initiatorUid: IdSchema,
initiatorName: z.string(),
receiverUid: IdSchema,
receiverName: z.string(),
lastModified: z.number().int().nonnegative(),
status: ConnectionStatusSchema,
});
export type Connection = z.infer<typeof ConnectionSchema>;

View File

@@ -0,0 +1,61 @@
import { z } from "zod";
import { customEnumErrorHandler } from "./util";
const KnownFontNameSchema = z.enum(
[
"Roboto_Mono",
"Noto_Naskh_Arabic",
"Source_Code_Pro",
"IBM_Plex_Sans",
"Inconsolata",
"Fira_Code",
"JetBrains_Mono",
"Roboto",
"Montserrat",
"Titillium_Web",
"Lexend_Deca",
"Comic_Sans_MS",
"Oxygen",
"Nunito",
"Itim",
"Courier",
"Comfortaa",
"Coming_Soon",
"Atkinson_Hyperlegible",
"Lato",
"Lalezar",
"Boon",
"Open_Dyslexic",
"Ubuntu",
"Ubuntu_Mono",
"Georgia",
"Cascadia_Mono",
"IBM_Plex_Mono",
"Overpass_Mono",
"Hack",
"CommitMono",
"Mononoki",
"Parkinsans",
"Geist",
"Sarabun",
"Kanit",
"Geist_Mono",
"Iosevka",
"Proto",
"Adwaita_Mono",
"Inter_Tight",
"Space_Grotesk",
],
{
errorMap: customEnumErrorHandler("Must be a known font family"),
},
);
export type KnownFontName = z.infer<typeof KnownFontNameSchema>;
export const FontNameSchema = KnownFontNameSchema.or(
z
.string()
.max(50)
.regex(/^[a-zA-Z0-9_\-+.]+$/),
);
export type FontName = z.infer<typeof FontNameSchema>;

View File

@@ -0,0 +1,466 @@
import { z } from "zod";
import { customEnumErrorHandler } from "./util";
export const LanguageSchema = z.enum(
[
"english",
"english_1k",
"english_5k",
"english_10k",
"english_25k",
"english_450k",
"english_commonly_misspelled",
"english_contractions",
"english_doubleletter",
"english_shakespearean",
"english_old",
"english_medical",
"spanish",
"spanish_1k",
"spanish_10k",
"spanish_650k",
"french",
"french_1k",
"french_2k",
"french_10k",
"french_600k",
"french_bitoduc",
"nepali",
"nepali_1k",
"nepali_romanized",
"sanskrit",
"sanskrit_roman",
"santali",
"azerbaijani",
"azerbaijani_1k",
"arabic",
"arabic_10k",
"arabic_egypt",
"arabic_egypt_1k",
"arabic_morocco",
"malagasy",
"malagasy_1k",
"malay",
"malay_1k",
"mongolian",
"mongolian_10k",
"kannada",
"korean",
"korean_1k",
"korean_5k",
"khmer",
"chinese_simplified",
"chinese_simplified_1k",
"chinese_simplified_5k",
"chinese_simplified_10k",
"chinese_simplified_50k",
"chinese_traditional",
"chinese_traditional_1k",
"chinese_traditional_5k",
"chinese_traditional_10k",
"chinese_traditional_50k",
"russian",
"russian_1k",
"russian_5k",
"russian_10k",
"russian_25k",
"russian_50k",
"russian_375k",
"russian_contractions",
"russian_contractions_1k",
"russian_abbreviations",
"ukrainian",
"ukrainian_1k",
"ukrainian_10k",
"ukrainian_50k",
"ukrainian_endings",
"ukrainian_latynka",
"ukrainian_latynka_1k",
"ukrainian_latynka_10k",
"ukrainian_latynka_50k",
"ukrainian_latynka_endings",
"portuguese",
"portuguese_acentos_e_cedilha",
"portuguese_1k",
"portuguese_3k",
"portuguese_5k",
"portuguese_320k",
"portuguese_550k",
"indonesian",
"indonesian_1k",
"indonesian_10k",
"kurdish_central",
"kurdish_central_2k",
"kurdish_central_4k",
"german",
"german_1k",
"german_10k",
"german_250k",
"swiss_german",
"swiss_german_1k",
"swiss_german_2k",
"afrikaans",
"afrikaans_1k",
"afrikaans_10k",
"georgian",
"tamil",
"tamil_1k",
"tanglish",
"tamil_old",
"telugu",
"telugu_1k",
"greek",
"greek_1k",
"greek_5k",
"greek_10k",
"greek_25k",
"greeklish",
"greeklish_1k",
"greeklish_5k",
"greeklish_10k",
"greeklish_25k",
"turkish",
"turkish_1k",
"turkish_5k",
"irish",
"irish_1k",
"italian",
"italian_1k",
"italian_7k",
"italian_60k",
"italian_280k",
"friulian",
"latin",
"galician",
"thai",
"thai_1k",
"thai_5k",
"thai_10k",
"thai_20k",
"thai_50k",
"thai_60k",
"polish",
"polish_2k",
"polish_5k",
"polish_10k",
"polish_20k",
"polish_40k",
"polish_200k",
"czech",
"czech_1k",
"czech_10k",
"slovak",
"slovak_1k",
"slovak_10k",
"slovenian",
"slovenian_1k",
"slovenian_5k",
"croatian",
"croatian_1k",
"dutch",
"dutch_1k",
"dutch_10k",
"filipino",
"filipino_1k",
"danish",
"danish_1k",
"danish_10k",
"hungarian",
"hungarian_1k",
"hungarian_2k",
"norwegian_bokmal",
"norwegian_bokmal_1k",
"norwegian_bokmal_5k",
"norwegian_bokmal_10k",
"norwegian_bokmal_150k",
"norwegian_bokmal_600k",
"norwegian_nynorsk",
"norwegian_nynorsk_1k",
"norwegian_nynorsk_5k",
"norwegian_nynorsk_10k",
"norwegian_nynorsk_100k",
"norwegian_nynorsk_400k",
"hebrew",
"hebrew_1k",
"hebrew_5k",
"hebrew_10k",
"icelandic",
"icelandic_1k",
"romanian",
"romanian_1k",
"romanian_5k",
"romanian_10k",
"romanian_25k",
"romanian_50k",
"romanian_100k",
"romanian_200k",
"lorem_ipsum",
"finnish",
"finnish_1k",
"finnish_10k",
"estonian",
"estonian_1k",
"estonian_5k",
"estonian_10k",
"udmurt",
"welsh",
"welsh_1k",
"persian",
"persian_1k",
"persian_5k",
"persian_20k",
"persian_romanized",
"marathi",
"kazakh",
"kazakh_1k",
"vietnamese",
"vietnamese_1k",
"vietnamese_5k",
"jyutping",
"pinyin",
"pinyin_1k",
"pinyin_10k",
"hausa",
"hausa_1k",
"swedish",
"swedish_1k",
"swedish_diacritics",
"serbian_latin",
"serbian_latin_10k",
"serbian",
"serbian_10k",
"yoruba_1k",
"swahili_1k",
"maori_1k",
"catalan",
"catalan_1k",
"lojban_gismu",
"lojban_cmavo",
"lithuanian",
"lithuanian_1k",
"lithuanian_3k",
"bulgarian",
"bulgarian_1k",
"bulgarian_latin",
"bulgarian_latin_1k",
"bangla",
"bangla_letters",
"bangla_10k",
"bosnian",
"bosnian_4k",
"toki_pona",
"toki_pona_ku_suli",
"toki_pona_ku_lili",
"esperanto",
"esperanto_1k",
"esperanto_10k",
"esperanto_25k",
"esperanto_36k",
"esperanto_x_sistemo",
"esperanto_x_sistemo_1k",
"esperanto_x_sistemo_10k",
"esperanto_x_sistemo_25k",
"esperanto_x_sistemo_36k",
"esperanto_h_sistemo",
"esperanto_h_sistemo_1k",
"esperanto_h_sistemo_10k",
"esperanto_h_sistemo_25k",
"esperanto_h_sistemo_36k",
"kyrgyz",
"kyrgyz_1k",
"urdu",
"urdu_1k",
"urdu_5k",
"urdu_roman",
"urdish",
"albanian",
"albanian_1k",
"shona",
"shona_1k",
"armenian",
"armenian_1k",
"armenian_western",
"armenian_western_1k",
"myanmar_burmese",
"japanese_hiragana",
"japanese_katakana",
"japanese_romaji",
"japanese_romaji_1k",
"sinhala",
"latvian",
"latvian_1k",
"maltese",
"maltese_1k",
"twitch_emotes",
"git",
"pig_latin",
"hindi",
"hindi_1k",
"hinglish",
"gujarati",
"gujarati_1k",
"macedonian",
"macedonian_1k",
"macedonian_10k",
"macedonian_75k",
"belarusian",
"belarusian_1k",
"belarusian_5k",
"belarusian_10k",
"belarusian_25k",
"belarusian_50k",
"belarusian_100k",
"belarusian_lacinka",
"belarusian_lacinka_1k",
"tatar",
"tatar_1k",
"tatar_5k",
"tatar_9k",
"tatar_crimean",
"tatar_crimean_1k",
"tatar_crimean_5k",
"tatar_crimean_10k",
"tatar_crimean_15k",
"tatar_crimean_cyrillic",
"tatar_crimean_cyrillic_1k",
"tatar_crimean_cyrillic_5k",
"tatar_crimean_cyrillic_10k",
"tatar_crimean_cyrillic_15k",
"uzbek",
"uzbek_1k",
"uzbek_70k",
"malayalam",
"amharic",
"amharic_1k",
"amharic_5k",
"oromo",
"oromo_1k",
"oromo_5k",
"wordle",
"league_of_legends",
"wordle_1k",
"typing_of_the_dead",
"yiddish",
"frisian",
"frisian_1k",
"pashto",
"euskera",
"klingon",
"klingon_1k",
"quenya",
"occitan",
"occitan_1k",
"occitan_2k",
"occitan_5k",
"occitan_10k",
"bashkir",
"zulu",
"kabyle",
"kabyle_1k",
"kabyle_2k",
"kabyle_5k",
"kabyle_10k",
"hawaiian",
"hawaiian_1k",
"code_python",
"code_python_1k",
"code_python_2k",
"code_python_5k",
"code_fsharp",
"code_c",
"code_csharp",
"code_css",
"code_c++",
"code_dart",
"code_brainfck",
"code_javascript",
"code_javascript_1k",
"code_javascript_react",
"code_jule",
"code_julia",
"code_haskell",
"code_html",
"code_nim",
"code_nix",
"code_pascal",
"code_java",
"code_kotlin",
"code_go",
"code_rockstar",
"code_rust",
"code_ruby",
"code_r",
"code_r_2k",
"code_swift",
"code_scala",
"code_bash",
"code_powershell",
"code_lua",
"code_luau",
"code_latex",
"code_typst",
"code_matlab",
"code_sql",
"code_perl",
"code_php",
"code_vim",
"code_vimscript",
"code_opencl",
"code_visual_basic",
"code_arduino",
"code_systemverilog",
"code_elixir",
"code_gleam",
"code_zig",
"code_gdscript",
"code_gdscript_2",
"code_assembly",
"code_v",
"code_ook",
"code_typescript",
"code_ocaml",
"code_odin",
"xhosa",
"xhosa_3k",
"tibetan",
"tibetan_1k",
"code_cobol",
"code_clojure",
"code_common_lisp",
"code_erlang",
"docker_file",
"code_fortran",
"viossa",
"viossa_njutro",
"code_abap",
"code_abap_1k",
"code_yoptascript",
"code_cuda",
"kinyarwanda",
"pokemon_1k",
"kokanu",
"likanu",
],
{
errorMap: customEnumErrorHandler("Must be a supported language"),
},
);
export type Language = z.infer<typeof LanguageSchema>;
export const LanguageObjectSchema = z
.object({
name: LanguageSchema,
rightToLeft: z.boolean().optional(),
noLazyMode: z.boolean().optional(),
ligatures: z.boolean().optional(),
orderedByFrequency: z.boolean().optional(),
words: z.array(z.string()).min(1),
additionalAccents: z
.array(z.tuple([z.string().min(1), z.string().min(1)]))
.optional(),
bcp47: z.string().optional(),
originalPunctuation: z.boolean().optional(),
})
.strict();
export type LanguageObject = z.infer<typeof LanguageObjectSchema>;

View File

@@ -0,0 +1,308 @@
import { z } from "zod";
import { customEnumErrorHandler } from "./util";
export const LayoutNameSchema = z.enum(
[
"qwerty",
"dvorak",
"colemak",
"colemak_angle",
"colemak_wide",
"colemak_dh",
"colemak_dh_iso",
"colemak_dh_wide",
"colemak_dh_iso_wide",
"colemak_dhk",
"colemak_dh_matrix",
"colemak_dhk_iso",
"colemak_dhv",
"qwertz",
"swiss_german",
"swiss_french",
"workman",
"prog_workman",
"turkish_q",
"turkish_f",
"turkish_e",
"MTGAP_ASRT",
"norman",
"halmak",
"QGMLWB",
"QGMLWY",
"qwpr",
"uk_qwerty",
"spanish_qwerty",
"italian_qwerty",
"latam_qwerty",
"prog_dvorak",
"prog_dvorak_prime",
"german_dvorak",
"german_dvorak_imp",
"spanish_dvorak",
"swedish_colemak",
"swedish_dvorak",
"dvorak_L",
"dvorak_R",
"dvorak_fr",
"azerty",
"azerty_AFNOR",
"bepo",
"bepo_AFNOR",
"alpha",
"handsdown",
"hungarian",
"handsdown_alt",
"handsdown_promethium",
"handsdown_neu",
"handsdown_neu_inverted",
"typehack",
"MTGAP",
"MTGAP_full",
"ina",
"soul",
"niro",
"mongolian",
"JCUKEN",
"statica_3x5",
"Vestnik",
"Diktor",
"Diktor_VoronovMod",
"Redaktor",
"JUIYAF",
"Zubachev",
"ISRT",
"ISRT_Angle",
"colemak_Qix",
"colemak_Qi",
"colemaQ",
"colemaQ_F",
"engram",
"engrammer",
"semimak",
"semimak_jq",
"semimak_jqc",
"canary",
"canary_matrix",
"japanese_hiragana",
"boo",
"boo_mangle",
"APT",
"APT_angle",
"middlemak",
"middlemak-nh",
"hindi_inscript",
"thai_kedmanee",
"thai_pattachote",
"thai_manoonchai",
"persian_standard",
"persian_farsi",
"arabic_101",
"arabic_102",
"arabic_mac",
"hebrew",
"urdu_phonetic",
"brasileiro_nativo",
"Foalmak",
"quartz",
"arensito",
"ARTS",
"beakl_15",
"beakl_19",
"beakl_19_bis",
"capewell_dvorak",
"colman",
"heart",
"klauser",
"oneproduct",
"pine",
"pine_v4",
"real",
"rolll",
"stndc",
"three",
"uciea",
"asset",
"dwarf",
"flaw",
"whorf",
"whorf6",
"whorfmax",
"whorfmax_ortho",
"sertain",
"ctgap",
"octa8",
"polish_programmers",
"bulgarian",
"bulgarian_phonetic_traditional",
"belarusian",
"ukrainian",
"russian",
"neo",
"bone",
"AdNW",
"mine",
"noted",
"koy",
"3l",
"korean",
"ekverto_b",
"nerps",
"sturdy_angle_ansi",
"sturdy_angle_iso",
"sturdy_ortho",
"ABNT2",
"HiYou",
"xenia",
"xenia_alt",
"burmese",
"gallium",
"gallium_angle",
"gallium_v2",
"gallium_v2_matrix",
"gallium_nl",
"maya",
"gallaya_angle_ansi",
"gallaya_angle_iso",
"gallaya_matrix",
"nila",
"minimak_4k",
"minimak_8k",
"minimak_12k",
"optimot",
"norwegian_qwerty",
"portuguese_pt_qwerty_iso",
"portuguese_pt_qwerty_ansi",
"swedish_qwerty",
"danish_qwerty",
"noctum",
"graphite",
"graphite_angle",
"graphite_angle_vc",
"graphite_angle_kp",
"graphite_matrix",
"macedonian",
"UGJRMV",
"pashto",
"ORNATE",
"estonian",
"stronk",
"dhorf",
"gust",
"recurva",
"seht-drai",
"ints",
"rollla",
"wreathy",
"saiga",
"saiga-e",
"krai",
"mir",
"ergol",
"cascade",
"vylet",
"hyperroll",
"romak",
"scythe",
"inqwerted",
"rain",
"night",
"night_stic",
"whix2",
"haruka",
"kuntum",
"anishtro",
"Kuntem",
"kuntem-jq",
"BEAKL_Zi",
"snorkle",
"MALTRON",
"PRSTEN",
"RSTHD",
"dusk",
"zenith",
"focal",
"panini",
"panini_wide",
"ergopti",
"sword",
"opy",
"tarmak_1",
"tarmak_2",
"tarmak_3",
"tarmak_4",
"rulemak",
"persian_farsi_colemak",
"persian_standard_colemak",
"ergo_split46",
"tamil99",
"Gralmak",
"GralmakS",
"vitrimak",
"miligram",
],
{
errorMap: customEnumErrorHandler("Must be a supported layout"),
},
);
export type LayoutName = z.infer<typeof LayoutNameSchema>;
const charDefinitionSchema = z.array(z.string().length(1)).min(1).max(4);
const row5CharDefinitionSchema = z.array(z.string().length(1)).min(1).max(4);
const commonLayoutSchema = z
.object({
keymapShowTopRow: z.boolean(),
matrixShowRightColumn: z.boolean().optional(),
})
.strict();
const ansiLayoutSchema = commonLayoutSchema
.extend({
type: z.literal("ansi"),
keys: z
.object({
row1: z.array(charDefinitionSchema).length(13),
row2: z.array(charDefinitionSchema).length(13),
row3: z.array(charDefinitionSchema).length(11),
row4: z.array(charDefinitionSchema).length(10),
row5: z.array(row5CharDefinitionSchema).min(1).max(2),
})
.strict(),
})
.strict();
const isoLayoutSchema = commonLayoutSchema
.extend({
type: z.literal("iso"),
keys: z
.object({
row1: z.array(charDefinitionSchema).length(13),
row2: z.array(charDefinitionSchema).length(12),
row3: z.array(charDefinitionSchema).length(12),
row4: z.array(charDefinitionSchema).length(11),
row5: z.array(row5CharDefinitionSchema).min(1).max(2),
})
.strict(),
})
.strict();
const matrixLayoutSchema = commonLayoutSchema
.extend({
type: z.literal("matrix"),
keys: z
.object({
row1: z.array(charDefinitionSchema).length(0),
row2: z.array(charDefinitionSchema).length(10),
row3: z.array(charDefinitionSchema).length(10),
row4: z.array(charDefinitionSchema).length(4),
row5: z.array(row5CharDefinitionSchema).length(0),
})
.strict(),
})
.strict();
export const LayoutObjectSchema = ansiLayoutSchema
.or(isoLayoutSchema)
.or(matrixLayoutSchema);
export type LayoutObject = z.infer<typeof LayoutObjectSchema>;

View File

@@ -0,0 +1,66 @@
import { z } from "zod";
const FriendsRankSchema = z
.number()
.nonnegative()
.int()
.optional()
.describe("only available on friendsOnly leaderboard");
export const LeaderboardEntrySchema = z.object({
wpm: z.number().nonnegative(),
acc: z.number().nonnegative().min(0).max(100),
timestamp: z.number().int().nonnegative(),
raw: z.number().nonnegative(),
consistency: z.number().nonnegative().optional(),
uid: z.string(),
name: z.string(),
discordId: z.string().optional(),
discordAvatar: z.string().optional(),
rank: z.number().nonnegative().int(),
friendsRank: FriendsRankSchema,
badgeId: z.number().int().optional(),
isPremium: z.boolean().optional(),
});
export type LeaderboardEntry = z.infer<typeof LeaderboardEntrySchema>;
export const RedisDailyLeaderboardEntrySchema = LeaderboardEntrySchema.omit({
rank: true,
friendsRank: true,
});
export type RedisDailyLeaderboardEntry = z.infer<
typeof RedisDailyLeaderboardEntrySchema
>;
export const RedisXpLeaderboardEntrySchema = z.object({
uid: z.string(),
name: z.string(),
lastActivityTimestamp: z.number().int().nonnegative(),
timeTypedSeconds: z.number().nonnegative(),
// optionals
// discordId: z.string().optional(),
discordId: z //todo remove once weekly leaderboards reset twice and remove null values
.string()
.optional()
.or(z.null().transform((_val) => undefined)),
discordAvatar: z.string().optional(),
badgeId: z.number().int().optional(),
isPremium: z.boolean().optional(),
});
export type RedisXpLeaderboardEntry = z.infer<
typeof RedisXpLeaderboardEntrySchema
>;
export const RedisXpLeaderboardScoreSchema = z.number().int().nonnegative();
export type RedisXpLeaderboardScore = z.infer<
typeof RedisXpLeaderboardScoreSchema
>;
export const XpLeaderboardEntrySchema = RedisXpLeaderboardEntrySchema.extend({
//based on another redis collection
totalXp: RedisXpLeaderboardScoreSchema,
// dynamically added when generating response on the backend
rank: z.number().nonnegative().int(),
friendsRank: FriendsRankSchema,
});
export type XpLeaderboardEntry = z.infer<typeof XpLeaderboardEntrySchema>;

View File

@@ -0,0 +1,51 @@
import { z } from "zod";
import { IdSchema, nameWithSeparators, TagSchema } from "./util";
import {
ConfigGroupName,
ConfigGroupNameSchema,
PartialConfigSchema,
} from "./configs";
export const PresetNameSchema = nameWithSeparators().max(16);
export type PresetName = z.infer<typeof PresetNameSchema>;
export const PresetTypeSchema = z.enum(["full", "partial"]);
export type PresetType = z.infer<typeof PresetTypeSchema>;
const PresetSettingsGroupsSchema = z
.array(ConfigGroupNameSchema)
.min(1)
.superRefine((settingList, ctx) => {
ConfigGroupNameSchema.options.forEach(
(presetSettingGroup: ConfigGroupName) => {
const duplicateElemExits: boolean =
settingList.filter(
(settingGroup: ConfigGroupName) =>
settingGroup === presetSettingGroup,
).length > 1;
if (duplicateElemExits) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `No duplicates allowed.`,
});
}
},
);
});
export const PresetSchema = z.object({
_id: IdSchema,
name: PresetNameSchema,
settingGroups: PresetSettingsGroupsSchema.nullable().optional(),
config: PartialConfigSchema.extend({
tags: z.array(TagSchema).optional(),
}),
});
export type Preset = z.infer<typeof PresetSchema>;
export const EditPresetRequestSchema = PresetSchema.partial({
config: true,
settingGroups: true,
});
export type EditPresetRequest = z.infer<typeof EditPresetRequestSchema>;

View File

@@ -0,0 +1,12 @@
import { z } from "zod";
import { IdSchema } from "./util";
export const PSASchema = z.object({
_id: IdSchema,
message: z.string(),
date: z.number().int().min(0).optional(),
level: z.number().int().optional(),
sticky: z.boolean().optional(),
});
export type PSA = z.infer<typeof PSASchema>;

View File

@@ -0,0 +1,15 @@
import { z } from "zod";
import { StringNumberSchema } from "./util";
export const SpeedHistogramSchema = z.record(
StringNumberSchema,
z.number().int(),
);
export type SpeedHistogram = z.infer<typeof SpeedHistogramSchema>;
export const TypingStatsSchema = z.object({
timeTyping: z.number().nonnegative(),
testsCompleted: z.number().int().nonnegative(),
testsStarted: z.number().int().nonnegative(),
});
export type TypingStats = z.infer<typeof TypingStatsSchema>;

View File

@@ -0,0 +1,71 @@
import { z } from "zod";
import { IdSchema } from "./util";
import { LanguageSchema } from "./languages";
export const QuoteIdSchema = z
.number()
.int()
.nonnegative()
.or(z.string().regex(/^\d+$/).transform(Number));
export type QuoteId = z.infer<typeof QuoteIdSchema>;
export const ApproveQuoteSchema = z.object({
id: QuoteIdSchema,
text: z.string(),
source: z.string(),
length: z.number().int().positive(),
approvedBy: z.string().describe("The approvers name"),
});
export type ApproveQuote = z.infer<typeof ApproveQuoteSchema>;
export const QuoteSchema = z.object({
_id: IdSchema,
text: z.string(),
source: z.string(),
language: LanguageSchema,
submittedBy: IdSchema.describe("uid of the submitter"),
timestamp: z.number().int().nonnegative(),
approved: z.boolean(),
});
export type Quote = z.infer<typeof QuoteSchema>;
export const QuoteRatingSchema = z.object({
_id: IdSchema,
language: LanguageSchema,
quoteId: QuoteIdSchema,
average: z.number().nonnegative(),
ratings: z.number().int().nonnegative(),
totalRating: z.number().nonnegative(),
});
export type QuoteRating = z.infer<typeof QuoteRatingSchema>;
export const QuoteReportReasonSchema = z.enum([
"Grammatical error",
"Duplicate quote",
"Inappropriate content",
"Low quality content",
"Incorrect source",
]);
export type QuoteReportReason = z.infer<typeof QuoteReportReasonSchema>;
export const QuoteDataQuoteSchema = z
.object({
id: z.number(),
text: z.string(),
britishText: z.string().optional(),
source: z.string(),
length: z.number(),
approvedBy: z.string().optional(),
})
.strict();
export type QuoteDataQuote = z.infer<typeof QuoteDataQuoteSchema>;
export const QuoteDataSchema = z
.object({
language: LanguageSchema,
groups: z.array(z.tuple([z.number(), z.number()])).length(4),
quotes: z.array(QuoteDataQuoteSchema),
})
.strict();
export type QuoteData = z.infer<typeof QuoteDataSchema>;

View File

@@ -0,0 +1,182 @@
import { z } from "zod";
import {
CustomTextLimitModeSchema,
CustomTextModeSchema,
IdSchema,
PercentageSchema,
token,
WpmSchema,
} from "./util";
import { LanguageSchema } from "./languages";
import { Mode, Mode2, Mode2Schema, ModeSchema } from "./shared";
import { DifficultySchema, FunboxSchema } from "./configs";
export const IncompleteTestSchema = z.object({
acc: PercentageSchema,
seconds: z.number().nonnegative(),
});
export type IncompleteTest = z.infer<typeof IncompleteTestSchema>;
export const OldChartDataSchema = z.object({
wpm: z.array(z.number().nonnegative()).max(122),
raw: z.array(z.number().int().nonnegative()).max(122),
err: z.array(z.number().nonnegative()).max(122),
});
export type OldChartData = z.infer<typeof OldChartDataSchema>;
export const ChartDataSchema = z.object({
wpm: z.array(z.number().nonnegative()).max(122),
burst: z.array(z.number().int().nonnegative()).max(122),
err: z.array(z.number().nonnegative()).max(122),
});
export type ChartData = z.infer<typeof ChartDataSchema>;
export const KeyStatsSchema = z.object({
average: z.number().nonnegative(),
sd: z.number().nonnegative(),
});
export type KeyStats = z.infer<typeof KeyStatsSchema>;
export const CompletedEventCustomTextSchema = z.object({
textLen: z.number().int().nonnegative(),
mode: CustomTextModeSchema,
pipeDelimiter: z.boolean(),
limit: z.object({
mode: CustomTextLimitModeSchema,
value: z.number().nonnegative(),
}),
});
export type CompletedEventCustomText = z.infer<
typeof CompletedEventCustomTextSchema
>;
export const CustomTextSettingsSchema = CompletedEventCustomTextSchema.omit({
textLen: true,
}).extend({
text: z.array(z.string()).min(1),
});
export type CustomTextSettings = z.infer<typeof CustomTextSettingsSchema>;
export const CharStatsSchema = z.tuple([
z.number().int().nonnegative(),
z.number().int().nonnegative(),
z.number().int().nonnegative(),
z.number().int().nonnegative(),
]);
export type CharStats = z.infer<typeof CharStatsSchema>;
const ResultBaseSchema = z.object({
wpm: WpmSchema,
rawWpm: WpmSchema,
charStats: CharStatsSchema,
acc: PercentageSchema.min(50),
mode: ModeSchema,
mode2: Mode2Schema,
quoteLength: z.number().int().nonnegative().max(3).optional(),
timestamp: z.number().int().nonnegative(),
testDuration: z.number().min(1),
consistency: PercentageSchema,
keyConsistency: PercentageSchema,
chartData: ChartDataSchema.or(z.literal("toolong")),
uid: IdSchema,
//required on POST but optional in the database and might be removed to save space
restartCount: z.number().int().nonnegative().optional(),
incompleteTestSeconds: z.number().nonnegative().optional(),
afkDuration: z.number().nonnegative().optional(),
tags: z.array(IdSchema).optional(),
bailedOut: z.boolean().optional(),
blindMode: z.boolean().optional(),
lazyMode: z.boolean().optional(),
funbox: FunboxSchema.optional(),
language: LanguageSchema.optional(),
difficulty: DifficultySchema.optional(),
numbers: z.boolean().optional(),
punctuation: z.boolean().optional(),
});
export const ResultSchema = ResultBaseSchema.extend({
_id: IdSchema,
keySpacingStats: KeyStatsSchema.optional(),
keyDurationStats: KeyStatsSchema.optional(),
name: z.string(),
isPb: z.boolean().optional(), //true or undefined
});
export type Result<M extends Mode> = Omit<
z.infer<typeof ResultSchema>,
"mode" | "mode2"
> & {
mode: M;
mode2: Mode2<M>;
};
export const ResultMinifiedSchema = ResultSchema.omit({
name: true,
keySpacingStats: true,
keyDurationStats: true,
chartData: true,
});
export type ResultMinified = z.infer<typeof ResultMinifiedSchema>;
export const CompletedEventSchema = ResultBaseSchema.required({
restartCount: true,
incompleteTestSeconds: true,
afkDuration: true,
tags: true,
bailedOut: true,
blindMode: true,
lazyMode: true,
funbox: true,
language: true,
difficulty: true,
numbers: true,
punctuation: true,
})
.extend({
charTotal: z.number().int().nonnegative(),
challenge: token().max(100).optional(),
customText: CompletedEventCustomTextSchema.optional(),
hash: token().max(100),
keyDuration: z.array(z.number().nonnegative()).or(z.literal("toolong")),
keySpacing: z.array(z.number().nonnegative()).or(z.literal("toolong")),
keyOverlap: z.number().nonnegative(),
lastKeyToEnd: z.number().nonnegative(),
startToFirstKey: z.number().nonnegative(),
wpmConsistency: PercentageSchema,
stopOnLetter: z.boolean(),
incompleteTests: z.array(IncompleteTestSchema),
})
.strict();
export type CompletedEvent = z.infer<typeof CompletedEventSchema>;
export const XpBreakdownSchema = z.object({
base: z.number().int().optional(),
fullAccuracy: z.number().int().optional(),
quote: z.number().int().optional(),
corrected: z.number().int().optional(),
punctuation: z.number().int().optional(),
numbers: z.number().int().optional(),
funbox: z.number().int().optional(),
streak: z.number().int().optional(),
incomplete: z.number().int().optional(),
daily: z.number().int().optional(),
accPenalty: z.number().int().optional(),
configMultiplier: z.number().int().optional(),
});
export type XpBreakdown = z.infer<typeof XpBreakdownSchema>;
export const PostResultResponseSchema = z.object({
insertedId: IdSchema,
isPb: z.boolean(),
tagPbs: z.array(IdSchema),
dailyLeaderboardRank: z.number().int().nonnegative().optional(),
weeklyXpLeaderboardRank: z.number().int().nonnegative().optional(),
xp: z.number().int().nonnegative(),
dailyXpBonus: z.boolean(),
xpBreakdown: XpBreakdownSchema,
streak: z.number().int().nonnegative(),
});
export type PostResultResponse = z.infer<typeof PostResultResponseSchema>;

View File

@@ -0,0 +1,83 @@
import { literal, z } from "zod";
import { StringNumberSchema } from "./util";
import { LanguageSchema } from "./languages";
//used by config and shared
export const DifficultySchema = z.enum(["normal", "expert", "master"]);
export type Difficulty = z.infer<typeof DifficultySchema>;
//used by user and config
export const PersonalBestSchema = z.object({
acc: z.number().nonnegative().max(100),
consistency: z.number().nonnegative().max(100),
difficulty: DifficultySchema,
lazyMode: z.boolean().optional(),
language: LanguageSchema,
punctuation: z.boolean().optional(),
numbers: z.boolean().optional(),
raw: z.number().nonnegative(),
wpm: z.number().nonnegative(),
timestamp: z.number().nonnegative(),
});
export type PersonalBest = z.infer<typeof PersonalBestSchema>;
//used by user and config
export const PersonalBestsSchema = z.object({
time: z.record(
StringNumberSchema.describe("Number of seconds as string"),
z.array(PersonalBestSchema),
),
words: z.record(
StringNumberSchema.describe("Number of words as string"),
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 DefaultWordsModeSchema = z.union([
z.literal("10"),
z.literal("25"),
z.literal("50"),
z.literal("100"),
]);
export const DefaultTimeModeSchema = z.union([
z.literal("15"),
z.literal("30"),
z.literal("60"),
z.literal("120"),
]);
export const QuoteLengthSchema = z.union([
z.literal("short"),
z.literal("medium"),
z.literal("long"),
z.literal("thicc"),
]);
// // Step 1: Define the schema for specific string values "10" and "25"
// const SpecificKeySchema = z.union([z.literal("10"), z.literal("25")]);
// // Step 2: Use this schema as the key schema for another object
// export const ExampleSchema = z.record(SpecificKeySchema, z.string());
// //used by user, config, public
export const ModeSchema = PersonalBestsSchema.keyof();
export type Mode = z.infer<typeof ModeSchema>;
export const Mode2Schema = z.union(
[StringNumberSchema, literal("zen"), literal("custom")],
{
errorMap: () => ({
message: 'Needs to be either a number, "zen" or "custom".',
}),
},
);
export type Mode2<M extends Mode> = M extends M
? keyof PersonalBests[M]
: never;
export type Mode2Custom<M extends Mode> = Mode2<M> | "custom";

View File

@@ -0,0 +1,197 @@
import { z } from "zod";
import { customEnumErrorHandler } from "./util";
export const ThemeNameSchema = z.enum(
[
"8008",
"80s_after_dark",
"9009",
"aether",
"alduin",
"alpine",
"anti_hero",
"arch",
"aurora",
"beach",
"bento",
"bingsu",
"bliss",
"blue_dolphin",
"blueberry_dark",
"blueberry_light",
"botanical",
"bouquet",
"breeze",
"bushido",
"cafe",
"camping",
"carbon",
"catppuccin",
"chaos_theory",
"cheesecake",
"cherry_blossom",
"comfy",
"copper",
"creamsicle",
"cy_red",
"cyberspace",
"dark",
"dark_magic_girl",
"dark_note",
"darling",
"deku",
"desert_oasis",
"dev",
"diner",
"dino",
"discord",
"dmg",
"dollar",
"dots",
"dracula",
"drowning",
"dualshot",
"earthsong",
"everblush",
"evil_eye",
"ez_mode",
"fire",
"fledgling",
"fleuriste",
"floret",
"froyo",
"frozen_llama",
"fruit_chew",
"fundamentals",
"future_funk",
"github",
"godspeed",
"graen",
"grand_prix",
"grape",
"gruvbox_dark",
"gruvbox_light",
"hammerhead",
"hanok",
"hedge",
"honey",
"horizon",
"husqy",
"iceberg_dark",
"iceberg_light",
"incognito",
"ishtar",
"iv_clover",
"iv_spade",
"joker",
"laser",
"lavender",
"leather",
"lil_dragon",
"lilac_mist",
"lime",
"luna",
"macroblank",
"magic_girl",
"mashu",
"matcha_moccha",
"material",
"matrix",
"menthol",
"metaverse",
"metropolis",
"mexican",
"miami",
"miami_nights",
"midnight",
"milkshake",
"mint",
"mizu",
"modern_dolch",
"modern_dolch_light",
"modern_ink",
"monokai",
"moonlight",
"mountain",
"mr_sleeves",
"ms_cupcakes",
"muted",
"nautilus",
"nebula",
"night_runner",
"nord",
"nord_light",
"norse",
"oblivion",
"olive",
"olivia",
"onedark",
"our_theme",
"paper",
"passion_fruit",
"pastel",
"peach_blossom",
"peaches",
"phantom",
"pink_lemonade",
"pulse",
"purpleish",
"rainbow_trail",
"red_dragon",
"red_samurai",
"repose_dark",
"repose_light",
"retro",
"retrocast",
"rgb",
"rose_pine",
"rose_pine_dawn",
"rose_pine_moon",
"rudy",
"ryujinscales",
"serika",
"serika_dark",
"sewing_tin",
"sewing_tin_light",
"shadow",
"shoko",
"slambook",
"snes",
"soaring_skies",
"solarized_dark",
"solarized_light",
"solarized_osaka",
"sonokai",
"stealth",
"strawberry",
"striker",
"suisei",
"sunset",
"superuser",
"sweden",
"tangerine",
"taro",
"terminal",
"terra",
"terrazzo",
"terror_below",
"tiramisu",
"trackday",
"trance",
"tron_orange",
"vaporwave",
"vesper",
"vesper_light",
"viridescent",
"voc",
"vscode",
"watermelon",
"wavez",
"witch_girl",
"pale_nimbus",
"spiderman",
],
{
errorMap: customEnumErrorHandler("Must be a known theme"),
},
);

View File

@@ -0,0 +1,389 @@
import { z, ZodEffects, ZodOptional, ZodString } from "zod";
import { IdSchema, nameWithSeparators, slug, StringNumberSchema } from "./util";
import { LanguageSchema } from "./languages";
import {
ModeSchema,
Mode2Schema,
PersonalBestsSchema,
DefaultWordsModeSchema,
DefaultTimeModeSchema,
QuoteLengthSchema,
DifficultySchema,
PersonalBestSchema,
} from "./shared";
import { CustomThemeColorsSchema, FunboxNameSchema } from "./configs";
import { doesNotContainDisallowedWords } from "./validation/validation";
import { ConnectionSchema } from "./connections";
const NoneFilterSchema = z.literal("none");
export const ResultFiltersSchema = z.object({
_id: IdSchema,
name: slug().max(16),
pb: z
.object({
no: z.boolean(),
yes: z.boolean(),
})
.strict(),
difficulty: z.record(DifficultySchema, z.boolean()),
mode: z.record(ModeSchema, z.boolean()),
words: z.record(DefaultWordsModeSchema.or(z.literal("custom")), z.boolean()),
time: z.record(DefaultTimeModeSchema.or(z.literal("custom")), z.boolean()),
quoteLength: z.record(QuoteLengthSchema, z.boolean()),
punctuation: z
.object({
on: z.boolean(),
off: z.boolean(),
})
.strict(),
numbers: z
.object({
on: z.boolean(),
off: z.boolean(),
})
.strict(),
date: z
.object({
last_day: z.boolean(),
last_week: z.boolean(),
last_month: z.boolean(),
last_3months: z.boolean(),
all: z.boolean(),
})
.strict(),
tags: z.record(IdSchema.or(NoneFilterSchema), z.boolean()),
language: z.record(LanguageSchema, z.boolean()),
funbox: z.record(FunboxNameSchema.or(NoneFilterSchema), z.boolean()),
});
export type ResultFilters = z.infer<typeof ResultFiltersSchema>;
export const StreakHourOffsetSchema = z.number().min(-11).max(12).step(0.5);
export type StreakHourOffset = z.infer<typeof StreakHourOffsetSchema>;
export const UserStreakSchema = z
.object({
lastResultTimestamp: z.number().int().nonnegative(),
length: z.number().int().nonnegative(),
maxLength: z.number().int().nonnegative(),
hourOffset: StreakHourOffsetSchema.optional(),
})
.strict();
export type UserStreak = z.infer<typeof UserStreakSchema>;
export const TagNameSchema = nameWithSeparators().max(16);
export type TagName = z.infer<typeof TagNameSchema>;
export const UserTagSchema = z
.object({
_id: IdSchema,
name: TagNameSchema,
personalBests: PersonalBestsSchema,
})
.strict();
export type UserTag = z.infer<typeof UserTagSchema>;
function profileDetailsBase(
schema: ZodString,
): ZodEffects<ZodOptional<ZodEffects<ZodString>>> {
return doesNotContainDisallowedWords("word", schema)
.optional()
.transform((value) => (value === null ? undefined : value));
}
export const TwitterProfileSchema = profileDetailsBase(slug().max(20)).or(
z.literal(""),
);
export const GithubProfileSchema = profileDetailsBase(slug().max(39)).or(
z.literal(""),
);
export const WebsiteSchema = profileDetailsBase(
z.string().url().max(200).startsWith("https://"),
).or(z.literal(""));
export const UserProfileDetailsSchema = z
.object({
bio: profileDetailsBase(z.string().max(250)).or(z.literal("")),
keyboard: profileDetailsBase(z.string().max(75)).or(z.literal("")),
socialProfiles: z
.object({
twitter: TwitterProfileSchema,
github: GithubProfileSchema,
website: WebsiteSchema,
})
.strict()
.optional(),
showActivityOnPublicProfile: z.boolean().optional(),
})
.strict();
export type UserProfileDetails = z.infer<typeof UserProfileDetailsSchema>;
export const CustomThemeNameSchema = nameWithSeparators().max(16);
export type CustomThemeName = z.infer<typeof CustomThemeNameSchema>;
export const CustomThemeSchema = z
.object({
_id: IdSchema,
name: CustomThemeNameSchema,
colors: CustomThemeColorsSchema,
})
.strict();
export type CustomTheme = z.infer<typeof CustomThemeSchema>;
export const PremiumInfoSchema = z.object({
startTimestamp: z.number().int().nonnegative(),
expirationTimestamp: z
.number()
.int()
.nonnegative()
.or(z.literal(-1).describe("lifetime premium")),
});
export type PremiumInfo = z.infer<typeof PremiumInfoSchema>;
export const UserQuoteRatingsSchema = z.record(
LanguageSchema,
z.record(
StringNumberSchema.describe("quoteId as string"),
z.number().nonnegative(),
),
);
export type UserQuoteRatings = z.infer<typeof UserQuoteRatingsSchema>;
export const UserLbMemorySchema = z.record(
ModeSchema,
z.record(
Mode2Schema,
z.record(LanguageSchema, z.number().int().nonnegative()),
),
);
export type UserLbMemory = z.infer<typeof UserLbMemorySchema>;
export const RankAndCountSchema = z.object({
rank: z.number().int().nonnegative().optional(),
count: z.number().int().nonnegative(),
});
export type RankAndCount = z.infer<typeof RankAndCountSchema>;
export const AllTimeLbsSchema = z.object({
time: z.record(
Mode2Schema,
z.record(LanguageSchema, RankAndCountSchema.optional()),
),
});
export type AllTimeLbs = z.infer<typeof AllTimeLbsSchema>;
export const BadgeSchema = z
.object({
id: z.number().int().nonnegative(),
selected: z.boolean().optional(),
})
.strict();
export type Badge = z.infer<typeof BadgeSchema>;
export const UserInventorySchema = z
.object({
badges: z.array(BadgeSchema),
})
.strict();
export type UserInventory = z.infer<typeof UserInventorySchema>;
export const QuoteModSchema = z
.boolean()
.describe("Admin for all languages if true")
.or(LanguageSchema.describe("Admin for the given language"));
export type QuoteMod = z.infer<typeof QuoteModSchema>;
export const TestActivitySchema = z
.object({
testsByDays: z
.array(z.number().int().nonnegative().or(z.null()))
.describe(
"Number of tests by day. Last element of the array is on the date `lastDay`. `null` means no tests on that day.",
),
lastDay: z
.number()
.int()
.nonnegative()
.describe("Timestamp of the last day included in the test activity"),
})
.strict();
export type TestActivity = z.infer<typeof TestActivitySchema>;
export const CountByYearAndDaySchema = z.record(
StringNumberSchema.describe("year"),
z.array(
z
.number()
.int()
.nonnegative()
.nullable()
.describe(
"number of tests, position in the array is the day of the year",
),
),
);
export type CountByYearAndDay = z.infer<typeof CountByYearAndDaySchema>;
//Record<language, array with quoteIds as string
export const FavoriteQuotesSchema = z.record(
LanguageSchema,
z.array(StringNumberSchema),
);
export type FavoriteQuotes = z.infer<typeof FavoriteQuotesSchema>;
export const UserEmailSchema = z.string().email();
export const UserNameSchema = doesNotContainDisallowedWords(
"substring",
slug().min(1).max(16),
);
export const UserSchema = z.object({
name: UserNameSchema,
email: UserEmailSchema,
uid: z.string(), //defined by firebase, no validation should be applied
addedAt: z.number().int().nonnegative(),
personalBests: PersonalBestsSchema,
lastReultHashes: z.array(z.string()).optional(), //TODO: fix typo (it's in the db too)
completedTests: z.number().int().nonnegative().optional(),
startedTests: z.number().int().nonnegative().optional(),
timeTyping: z
.number()
.nonnegative()
.optional()
.describe("time typing in seconds"),
streak: UserStreakSchema.optional(),
xp: z.number().int().nonnegative().optional(),
discordId: z.string().optional(),
discordAvatar: z.string().optional(),
tags: z.array(UserTagSchema).optional(),
profileDetails: UserProfileDetailsSchema.optional(),
customThemes: z.array(CustomThemeSchema).optional(),
premium: PremiumInfoSchema.optional(),
isPremium: z.boolean().optional(),
quoteRatings: UserQuoteRatingsSchema.optional(),
favoriteQuotes: FavoriteQuotesSchema.optional(),
lbMemory: UserLbMemorySchema.optional(),
allTimeLbs: AllTimeLbsSchema,
inventory: UserInventorySchema.optional(),
banned: z.boolean().optional(),
lbOptOut: z.boolean().optional(),
verified: z.boolean().optional(),
needsToChangeName: z.boolean().optional(),
quoteMod: QuoteModSchema.optional(),
resultFilterPresets: z.array(ResultFiltersSchema).optional(),
testActivity: TestActivitySchema.optional(),
});
export type User = z.infer<typeof UserSchema>;
export type ResultFiltersGroup = keyof ResultFilters;
export type ResultFiltersGroupItem<T extends ResultFiltersGroup> =
keyof ResultFilters[T];
export const TypingStatsSchema = z.object({
completedTests: z.number().int().nonnegative().optional(),
startedTests: z.number().int().nonnegative().optional(),
timeTyping: z.number().int().nonnegative().optional(),
});
export type TypingStats = z.infer<typeof TypingStatsSchema>;
export const UserProfileSchema = UserSchema.pick({
uid: true,
name: true,
banned: true,
addedAt: true,
discordId: true,
discordAvatar: true,
xp: true,
lbOptOut: true,
isPremium: true,
inventory: true,
allTimeLbs: true,
testActivity: true,
})
.extend({
typingStats: TypingStatsSchema,
personalBests: PersonalBestsSchema.pick({ time: true, words: true }),
streak: z.number().int().nonnegative(),
maxStreak: z.number().int().nonnegative(),
details: UserProfileDetailsSchema,
})
.partial({
//omitted for banned users
inventory: true,
details: true,
allTimeLbs: true,
uid: true,
});
export type UserProfile = z.infer<typeof UserProfileSchema>;
export const RewardTypeSchema = z.enum(["xp", "badge"]);
export type RewardType = z.infer<typeof RewardTypeSchema>;
export const XpRewardSchema = z.object({
type: z.literal(RewardTypeSchema.enum.xp),
item: z.number().int(),
});
export type XpReward = z.infer<typeof XpRewardSchema>;
export const BadgeRewardSchema = z.object({
type: z.literal(RewardTypeSchema.enum.badge),
item: BadgeSchema,
});
export type BadgeReward = z.infer<typeof BadgeRewardSchema>;
export const AllRewardsSchema = XpRewardSchema.or(BadgeRewardSchema);
export type AllRewards = z.infer<typeof AllRewardsSchema>;
export const MonkeyMailSchema = z.object({
id: IdSchema,
subject: z.string(),
body: z.string(),
timestamp: z.number().int().nonnegative(),
read: z.boolean(),
rewards: z.array(AllRewardsSchema),
});
export type MonkeyMail = z.infer<typeof MonkeyMailSchema>;
export const ReportUserReasonSchema = z.enum([
"Inappropriate name",
"Inappropriate bio",
"Inappropriate social links",
"Suspected cheating",
]);
export type ReportUserReason = z.infer<typeof ReportUserReasonSchema>;
export const PasswordSchema = z
.string()
.min(8, { message: "must be at least 8 characters" })
.max(64, { message: "must be at most 64 characters" })
.regex(/[A-Z]/, { message: "must contain at least one capital letter" })
.regex(/[\d]/, { message: "must contain at least one number" })
.regex(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/, {
message: "must contain at least one special character",
});
export type Password = z.infer<typeof PasswordSchema>;
export const FriendSchema = UserSchema.pick({
uid: true,
name: true,
discordId: true,
discordAvatar: true,
startedTests: true,
completedTests: true,
timeTyping: true,
xp: true,
banned: true,
lbOptOut: true,
})
.extend({
connectionId: IdSchema.optional(),
top15: PersonalBestSchema.optional(),
top60: PersonalBestSchema.optional(),
badgeId: z.number().int().optional(),
isPremium: z.boolean().optional(),
streak: UserStreakSchema.pick({ length: true, maxLength: true }),
})
.merge(ConnectionSchema.pick({ lastModified: true }).partial());
export type Friend = z.infer<typeof FriendSchema>;

View File

@@ -0,0 +1,66 @@
import { z, ZodErrorMap, ZodString } from "zod";
export const StringNumberSchema = z
.string()
.regex(
/^\d+$/,
'Needs to be a number or a number represented as a string e.g. "10".',
)
.or(z.number().transform(String));
export type StringNumber = z.infer<typeof StringNumberSchema>;
export const token = (): ZodString => z.string().regex(/^[a-zA-Z0-9_]+$/);
export const slug = (): ZodString =>
z
.string()
.regex(
/^[0-9a-zA-Z_.-]+$/,
"Only letters, numbers, underscores, dots and hyphens allowed",
)
.regex(/^[^.].*$/, "Cannot start with a dot");
export const nameWithSeparators = (): ZodString =>
z
.string()
.regex(
/^[0-9a-zA-Z_-]+$/,
"Only letters, numbers, underscores and hyphens allowed",
)
.regex(
/^[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+)*$/,
"Separators cannot be at the start or end, or appear multiple times in a row",
);
export const IdSchema = token();
export type Id = z.infer<typeof IdSchema>;
export const TagSchema = token().max(50);
export type Tag = z.infer<typeof TagSchema>;
export const NullableStringSchema = z
.string()
.nullable()
.optional()
.transform((value) => value ?? undefined);
export type NullableString = z.infer<typeof NullableStringSchema>;
export const PercentageSchema = z.number().nonnegative().max(100);
export type Percentage = z.infer<typeof PercentageSchema>;
export const WpmSchema = z.number().nonnegative().max(420);
export type Wpm = z.infer<typeof WpmSchema>;
export const CustomTextModeSchema = z.enum(["repeat", "random", "shuffle"]);
export type CustomTextMode = z.infer<typeof CustomTextModeSchema>;
export const CustomTextLimitModeSchema = z.enum(["word", "time", "section"]);
export type CustomTextLimitMode = z.infer<typeof CustomTextLimitModeSchema>;
export function customEnumErrorHandler(message: string): ZodErrorMap {
return (issue, _ctx) => ({
message:
issue.code === "invalid_enum_value"
? `Invalid enum value. ${message}`
: (issue.message ?? "Required"),
});
}

View File

@@ -0,0 +1,42 @@
const obj = {
a: ["\u0430", "\u00e0", "\u00e1", "\u1ea1", "\u0105"],
c: ["\u0441", "\u0188", "\u010b"],
d: ["\u0501", "\u0257"],
e: ["\u0435", "\u1eb9", "\u0117", "\u0117", "\u00e9", "\u00e8"],
g: ["\u0121"],
h: ["\u04bb"],
i: ["\u0456", "\u00ed", "\u00ec", "\u00ef"],
j: ["\u0458", "\u029d"],
k: ["\u03ba"],
l: ["\u04cf", "\u1e37"],
n: ["\u0578"],
o: [
"\u043e",
"\u03bf",
"\u0585",
"\u022f",
"\u1ecd",
"\u1ecf",
"\u01a1",
"\u00f6",
"\u00f3",
"\u00f2",
],
p: ["\u0440"],
q: ["\u0566"],
s: ["\u0282"],
u: ["\u03c5", "\u057d", "\u00fc", "\u00fa", "\u00f9"],
v: ["\u03bd", "\u0475"],
x: ["\u0445", "\u04b3"],
y: ["\u0443", "\u00fd"],
z: ["\u0290", "\u017c"],
};
export function replaceHomoglyphs(str: string): string {
for (const key in obj) {
obj[key as keyof typeof obj].forEach((value) => {
str = str.replace(value, key);
});
}
return str;
}

View File

@@ -0,0 +1,442 @@
import { replaceHomoglyphs } from "./homoglyphs";
import { ZodEffects, ZodString } from "zod";
// Sorry for the bad words
const disallowedWords = [
"miodec",
"bitly",
"niqqa",
"niqqer",
"ni99a",
"ni99er",
"niggas",
"niga",
"niger",
"retard",
"ahole",
"anus",
"ash0le",
"asholes",
"assh0le",
"assh0lez",
"asshole",
"assholes",
"assholz",
"asswipe",
"azzhole",
"bassterds",
"bastard",
"bastards",
"bastardz",
"basterds",
"basterdz",
"biatch",
"bitch",
"bitches",
"blow job",
"boffing",
"butthole",
"buttwipe",
"c0ck",
"c0cks",
"c0k",
"carpet muncher",
"cawk",
"cawks",
"clit",
"cnts",
"cntz",
"cock",
"cockhead",
"cock-head",
"cocks",
"cocksucker",
"cock-sucker",
"crap",
"cum",
"cunt",
"cunts",
"cuntz",
"dick",
"dild0",
"dild0s",
"dildo",
"dildos",
"dilld0",
"dilld0s",
"dominatricks",
"dominatrics",
"dominatrix",
"dyke",
"enema",
"f u c k",
"f u c k e r",
"fag",
"fag1t",
"faget",
"fagg1t",
"faggit",
"faggot",
"fagit",
"fags",
"fagz",
"faigs",
"flipping the bird",
"fudge packer",
"fukah",
"fuken",
"fuker",
"fukin",
"fukk",
"fukkah",
"fukken",
"fukker",
"fukkin",
"g00k",
"gayboy",
"gaygirl",
"gayz",
"god-damned",
"h00r",
"h0ar",
"h0re",
"jackoff",
"japs",
"jerk-off",
"jisim",
"jiss",
"jizm",
"jizz",
"knob",
"knobs",
"knobz",
"kunt",
"kunts",
"kuntz",
"lezzian",
"lipshits",
"lipshitz",
"masochist",
"masokist",
"massterbait",
"masstrbait",
"masstrbate",
"masterbaiter",
"masterbate",
"masterbates",
"n1gr",
"nastt",
"nigger;",
"nigur;",
"niiger;",
"niigr;",
"orafis",
"orgasim;",
"orgasm",
"orgasum",
"oriface",
"orifice",
"orifiss",
"peeenus",
"peeenusss",
"peenus",
"peinus",
"pen1s",
"penas",
"penis",
"penis-breath",
"penus",
"penuus",
"phuc",
"phuck",
"phuk",
"phuker",
"phukker",
"poonani",
"pr1c",
"pr1ck",
"pr1k",
"puss",
"pussee",
"pussy",
"puuke",
"puuker",
"qweers",
"qweerz",
"qweir",
"recktum",
"rectum",
"retard",
"sadist",
"scank",
"schlong",
"semen",
"sex",
"sexy",
"sh!t",
"sh1t",
"sh1ter",
"sh1ts",
"sh1tter",
"sh1tz",
"shit",
"shits",
"shitter",
"shitty",
"shity",
"shitz",
"shyt",
"shyte",
"shytty",
"shyty",
"skanck",
"skank",
"skankee",
"skankey",
"skanks",
"skanky",
"slut",
"sluts",
"slutty",
"slutz",
"son-of-a-bitch",
"turd",
"va1jina",
"vag1na",
"vagiina",
"vagina",
"vaj1na",
"vajina",
"vullva",
"vulva",
"wh00r",
"wh0re",
"whore",
"xrated",
"b!+ch",
"bitch",
"blowjob",
"clit",
"arschloch",
"shit",
"asshole",
"b!tch",
"b17ch",
"b1tch",
"bastard",
"bi+ch",
"boiolas",
"buceta",
"c0ck",
"cawk",
"chink",
"cipa",
"clits",
"cum",
"cunt",
"dildo",
"dirsa",
"ejakulate",
"fatass",
"fcuk",
"fux0r",
"hoer",
"hore",
"jism",
"kawk",
"l3itch",
"l3i+ch",
"masturbate",
"masterbat",
"masterbat3",
"motherfucker",
"s.o.b.",
"mofo",
"nazi",
"nigga",
"nigger",
"nutsack",
"phuck",
"pimpis",
"pusse",
"pussy",
"scrotum",
"sh!t",
"shemale",
"shi+",
"sh!+",
"slut",
"smut",
"teets",
"tits",
"boobs",
"b00bs",
"teez",
"testical",
"testicle",
"titt",
"w00se",
"jackoff",
"wank",
"whoar",
"whore",
"dyke",
"fuck",
"shit",
"amcik",
"andskota",
"arse",
"assrammer",
"ayir",
"bi7ch",
"bitch",
"bollock",
"breasts",
"butt-pirate",
"cabron",
"cazzo",
"chraa",
"chuj",
"cunt",
"daygo",
"dego",
"dick",
"dike",
"dupa",
"dziwka",
"ejackulate",
"ekrem",
"enculer",
"fag",
"fanculo",
"fanny",
"feces",
"felcher",
"ficken",
"flikker",
"foreskin",
"fotze",
"futkretzn",
"gook",
"guiena",
"h4x0r",
"helvete",
"hoer",
"honkey",
"huevon",
"injun",
"jizz",
"kanker",
"klootzak",
"kraut",
"knulle",
"kuksuger",
"kurac",
"kurwa",
"kyrpa",
"lesbo",
"mamhoon",
"masturbat",
"merd",
"mibun",
"monkleigh",
"mouliewop",
"muie",
"mulkku",
"muschi",
"nazis",
"nepesaurio",
"nigger",
"orospu",
"paska",
"perse",
"picka",
"pierdol",
"pillu",
"pimmel",
"piss",
"pizda",
"poontsee",
"porn",
"p0rn",
"pr0n",
"preteen",
"pula",
"pule",
"puta",
"puto",
"qahbeh",
"queef",
"rautenberg",
"schaffer",
"scheiss",
"schlampe",
"schmuck",
"sh!t",
"sharmuta",
"sharmute",
"skurwysyn",
"sphencter",
"spierdalaj",
"splooge",
"suka",
"b00b",
"testicle",
"titt",
"twat",
"vittu",
"wank",
"wetback",
"wichser",
"zabourah",
];
function sanitizeString(str: string | undefined): string | undefined {
if (str === undefined || str === "") {
return str;
}
return str
.replace(/[\u0300-\u036F]/g, "")
.trim()
.replace(/\n{3,}/g, "\n\n")
.replace(/\s{3,}/g, " ");
}
function containsDisallowedWords(
text: string,
mode: "word" | "substring",
): boolean {
const normalizedText = text
.toLowerCase()
.split(/[.,"/#!?$%^&*;:{}=\-_`~()\s\n]+/g)
.map((str) => {
return replaceHomoglyphs(sanitizeString(str) ?? "");
});
const hasDisallowedWords = disallowedWords.some((disallowedWord) => {
return normalizedText.some((word) => {
return mode === "word"
? word.startsWith(disallowedWord)
: word.includes(disallowedWord);
});
});
return hasDisallowedWords;
}
export function doesNotContainDisallowedWords(
mode: "word" | "substring",
schema: ZodString,
): ZodEffects<ZodString> {
return schema.refine(
(val) => {
return !containsDisallowedWords(val, mode);
},
(val) => ({
message: `Disallowed word detected. Please remove it. If you believe this is a mistake, please contact us (${val}).`,
}),
);
}
export const __testing = { containsDisallowedWords };

View File

@@ -0,0 +1,10 @@
{
"extends": "@monkeytype/typescript-config/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"target": "ES6"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,3 @@
import { extendConfig } from "@monkeytype/tsup-config";
export default extendConfig();

View File

@@ -0,0 +1,10 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
coverage: {
include: ["**/*.ts"],
},
},
});