This commit is contained in:
7
packages/schemas/.oxlintrc.json
Normal file
7
packages/schemas/.oxlintrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"ignorePatterns": ["node_modules", "dist", ".turbo"],
|
||||
"extends": [
|
||||
"../oxlint-config/index.jsonc"
|
||||
// "@monkeytype/oxlint-config"
|
||||
]
|
||||
}
|
||||
83
packages/schemas/__tests__/config.spec.ts
Normal file
83
packages/schemas/__tests__/config.spec.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
7
packages/schemas/__tests__/tsconfig.json
Normal file
7
packages/schemas/__tests__/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@monkeytype/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["./**/*.ts", "./**/*.spec.ts", "./setup-tests.ts"]
|
||||
}
|
||||
56
packages/schemas/__tests__/util.spec.ts
Normal file
56
packages/schemas/__tests__/util.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
38
packages/schemas/package.json
Normal file
38
packages/schemas/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
19
packages/schemas/src/ape-keys.ts
Normal file
19
packages/schemas/src/ape-keys.ts
Normal 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>;
|
||||
52
packages/schemas/src/challenges.ts
Normal file
52
packages/schemas/src/challenges.ts
Normal 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>;
|
||||
518
packages/schemas/src/configs.ts
Normal file
518
packages/schemas/src/configs.ts
Normal 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>;
|
||||
131
packages/schemas/src/configuration.ts
Normal file
131
packages/schemas/src/configuration.ts
Normal 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>;
|
||||
24
packages/schemas/src/connections.ts
Normal file
24
packages/schemas/src/connections.ts
Normal 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>;
|
||||
61
packages/schemas/src/fonts.ts
Normal file
61
packages/schemas/src/fonts.ts
Normal 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>;
|
||||
466
packages/schemas/src/languages.ts
Normal file
466
packages/schemas/src/languages.ts
Normal 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>;
|
||||
308
packages/schemas/src/layouts.ts
Normal file
308
packages/schemas/src/layouts.ts
Normal 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>;
|
||||
66
packages/schemas/src/leaderboards.ts
Normal file
66
packages/schemas/src/leaderboards.ts
Normal 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>;
|
||||
51
packages/schemas/src/presets.ts
Normal file
51
packages/schemas/src/presets.ts
Normal 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>;
|
||||
12
packages/schemas/src/psas.ts
Normal file
12
packages/schemas/src/psas.ts
Normal 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>;
|
||||
15
packages/schemas/src/public.ts
Normal file
15
packages/schemas/src/public.ts
Normal 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>;
|
||||
71
packages/schemas/src/quotes.ts
Normal file
71
packages/schemas/src/quotes.ts
Normal 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>;
|
||||
182
packages/schemas/src/results.ts
Normal file
182
packages/schemas/src/results.ts
Normal 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>;
|
||||
83
packages/schemas/src/shared.ts
Normal file
83
packages/schemas/src/shared.ts
Normal 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";
|
||||
197
packages/schemas/src/themes.ts
Normal file
197
packages/schemas/src/themes.ts
Normal 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"),
|
||||
},
|
||||
);
|
||||
389
packages/schemas/src/users.ts
Normal file
389
packages/schemas/src/users.ts
Normal 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>;
|
||||
66
packages/schemas/src/util.ts
Normal file
66
packages/schemas/src/util.ts
Normal 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"),
|
||||
});
|
||||
}
|
||||
42
packages/schemas/src/validation/homoglyphs.ts
Normal file
42
packages/schemas/src/validation/homoglyphs.ts
Normal 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;
|
||||
}
|
||||
442
packages/schemas/src/validation/validation.ts
Normal file
442
packages/schemas/src/validation/validation.ts
Normal 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 };
|
||||
10
packages/schemas/tsconfig.json
Normal file
10
packages/schemas/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@monkeytype/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"target": "ES6"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
3
packages/schemas/tsup.config.js
Normal file
3
packages/schemas/tsup.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { extendConfig } from "@monkeytype/tsup-config";
|
||||
|
||||
export default extendConfig();
|
||||
10
packages/schemas/vitest.config.ts
Normal file
10
packages/schemas/vitest.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
coverage: {
|
||||
include: ["**/*.ts"],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user