From 2fdc089f68b1c72f4a4b645fd5a6b1e6fb3f02a3 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Sat, 20 Jun 2026 13:59:17 -0500 Subject: [PATCH] init type updates --- src/webpage/app.html | 1 + src/webpage/index.ts | 12 ++++ src/webpage/localuser.ts | 5 ++ src/webpage/typeChecker/basetype.ts | 41 ++++++++++++ src/webpage/typeChecker/checkers/any.ts | 10 +++ src/webpage/typeChecker/checkers/array.ts | 26 +++++++ src/webpage/typeChecker/checkers/bool.ts | 10 +++ src/webpage/typeChecker/checkers/never.ts | 10 +++ src/webpage/typeChecker/checkers/null.ts | 10 +++ src/webpage/typeChecker/checkers/nullish.ts | 13 ++++ src/webpage/typeChecker/checkers/number.ts | 14 ++++ src/webpage/typeChecker/checkers/object.ts | 38 +++++++++++ src/webpage/typeChecker/checkers/optional.ts | 13 ++++ src/webpage/typeChecker/checkers/or.ts | 21 ++++++ src/webpage/typeChecker/checkers/string.ts | 17 +++++ src/webpage/typeChecker/chekerIndex.ts | 45 +++++++++++++ src/webpage/typeChecker/types/basicEvent.ts | 11 +++ .../typeChecker/types/events/hearbeat.ts | 10 +++ src/webpage/typeChecker/types/events/ready.ts | 51 ++++++++++++++ .../typeChecker/types/events/setInterval.ts | 12 ++++ src/webpage/typeChecker/types/objects/user.ts | 67 +++++++++++++++++++ src/webpage/typeChecker/types/snowflake.ts | 3 + translations/en.json | 4 +- 23 files changed, 442 insertions(+), 2 deletions(-) create mode 100644 src/webpage/typeChecker/basetype.ts create mode 100644 src/webpage/typeChecker/checkers/any.ts create mode 100644 src/webpage/typeChecker/checkers/array.ts create mode 100644 src/webpage/typeChecker/checkers/bool.ts create mode 100644 src/webpage/typeChecker/checkers/never.ts create mode 100644 src/webpage/typeChecker/checkers/null.ts create mode 100644 src/webpage/typeChecker/checkers/nullish.ts create mode 100644 src/webpage/typeChecker/checkers/number.ts create mode 100644 src/webpage/typeChecker/checkers/object.ts create mode 100644 src/webpage/typeChecker/checkers/optional.ts create mode 100644 src/webpage/typeChecker/checkers/or.ts create mode 100644 src/webpage/typeChecker/checkers/string.ts create mode 100644 src/webpage/typeChecker/chekerIndex.ts create mode 100644 src/webpage/typeChecker/types/basicEvent.ts create mode 100644 src/webpage/typeChecker/types/events/hearbeat.ts create mode 100644 src/webpage/typeChecker/types/events/ready.ts create mode 100644 src/webpage/typeChecker/types/events/setInterval.ts create mode 100644 src/webpage/typeChecker/types/objects/user.ts create mode 100644 src/webpage/typeChecker/types/snowflake.ts diff --git a/src/webpage/app.html b/src/webpage/app.html index 20e5160..68ddab3 100644 --- a/src/webpage/app.html +++ b/src/webpage/app.html @@ -147,6 +147,7 @@ + diff --git a/src/webpage/index.ts b/src/webpage/index.ts index b9bf317..fc57514 100644 --- a/src/webpage/index.ts +++ b/src/webpage/index.ts @@ -18,6 +18,7 @@ import "./oauth2/auth.js"; import "./audio/page.js"; import "./404.js"; import {Channel} from "./channel.js"; +import type * as C from "./typeChecker/chekerIndex.js"; if (window.location.pathname === "/app") { window.location.pathname = "/channels/@me"; @@ -25,6 +26,17 @@ if (window.location.pathname === "/app") { export interface CustomHTMLDivElement extends HTMLDivElement { markdown: MarkDown; } +declare global { + interface Window { + checker?: typeof C.Check; + } +} +if (localStorage.getItem("checkTypes")) { + const i = (await import( + "/typeChecker/chekerIndex.js" as "./typeChecker/chekerIndex.js" + )) as typeof C; + window.checker = i.Check; +} if (window.location.pathname.startsWith("/channels")) { let templateID = new URLSearchParams(window.location.search).get("templateID"); await I18n.done; diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index 482225c..01b23d8 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -722,6 +722,11 @@ class Localuser { } conectionChange = () => {}; async handleEvent(temp: wsjson) { + try { + window.checker?.checkEvent(temp); + } catch (e) { + console.error(e); + } if (temp.d._trace) this.handleTrace(temp.d._trace); if (getDeveloperSettings().gatewayLogging) console.debug(temp); if (temp.s) this.lastSequence = temp.s; diff --git a/src/webpage/typeChecker/basetype.ts b/src/webpage/typeChecker/basetype.ts new file mode 100644 index 0000000..189af85 --- /dev/null +++ b/src/webpage/typeChecker/basetype.ts @@ -0,0 +1,41 @@ +import {ArrayChecker} from "./checkers/array"; +import {BoolChecker} from "./checkers/bool"; +import {Never} from "./checkers/never"; +import {NumberChecker} from "./checkers/number"; +import {ObjectChecker} from "./checkers/object"; +import {StringChecker} from "./checkers/string"; + +export abstract class TypeChecker { + abstract check(obj: unknown): void; + static resolve(r: resolveable): TypeChecker { + if (r instanceof TypeChecker) return r; + if (typeof r === "string" || r instanceof RegExp) { + return new StringChecker(r); + } else if (r === String) { + return new StringChecker(); + } else if (r === Boolean) { + return new BoolChecker(); + } else if (r === Number) { + return new NumberChecker(); + } else if (typeof r === "number") { + return new NumberChecker(r); + } else if (r instanceof Array) { + return new ArrayChecker(r); + } else if (r instanceof Object) { + return new ObjectChecker(r as {[key: string]: resolveable}); + } else { + return new Never(); + } + } +} + +export type resolveable = + | typeof String + | string + | number + | typeof Number + | RegExp + | resolveable[] + | {[key: string]: resolveable} + | TypeChecker + | typeof Boolean; diff --git a/src/webpage/typeChecker/checkers/any.ts b/src/webpage/typeChecker/checkers/any.ts new file mode 100644 index 0000000..051c654 --- /dev/null +++ b/src/webpage/typeChecker/checkers/any.ts @@ -0,0 +1,10 @@ +import {TypeChecker} from "../basetype"; + +export class Any extends TypeChecker { + constructor() { + super(); + } + check() { + return; + } +} diff --git a/src/webpage/typeChecker/checkers/array.ts b/src/webpage/typeChecker/checkers/array.ts new file mode 100644 index 0000000..aa8dc88 --- /dev/null +++ b/src/webpage/typeChecker/checkers/array.ts @@ -0,0 +1,26 @@ +import {resolveable, TypeChecker} from "../basetype"; + +export class ArrayChecker extends TypeChecker { + arr: TypeChecker | TypeChecker[]; + constructor(arr: resolveable | resolveable[]) { + super(); + if (arr instanceof Array) { + this.arr = arr.map((_) => TypeChecker.resolve(_)); + } else { + this.arr = TypeChecker.resolve(arr); + } + } + check(obj: unknown) { + if (!(obj instanceof Array)) throw new Error(`${obj} is not of type array`); + if (this.arr instanceof Array) { + if (this.arr.length !== obj.length) throw new Error(`${obj} does not match array schema`); + for (let i = 0; i < obj.length; i++) { + this.arr[i].check(obj[i]); + } + } else { + for (const elm of obj) { + this.arr.check(elm); + } + } + } +} diff --git a/src/webpage/typeChecker/checkers/bool.ts b/src/webpage/typeChecker/checkers/bool.ts new file mode 100644 index 0000000..812fdf9 --- /dev/null +++ b/src/webpage/typeChecker/checkers/bool.ts @@ -0,0 +1,10 @@ +import {TypeChecker} from "../basetype"; + +export class BoolChecker extends TypeChecker { + constructor() { + super(); + } + check(obj: unknown): void { + if (typeof obj !== "boolean") throw new Error(`${obj} is not a bool`); + } +} diff --git a/src/webpage/typeChecker/checkers/never.ts b/src/webpage/typeChecker/checkers/never.ts new file mode 100644 index 0000000..bd03bd3 --- /dev/null +++ b/src/webpage/typeChecker/checkers/never.ts @@ -0,0 +1,10 @@ +import {TypeChecker} from "../basetype"; + +export class Never extends TypeChecker { + constructor() { + super(); + } + check(): void { + throw new Error(`never`); + } +} diff --git a/src/webpage/typeChecker/checkers/null.ts b/src/webpage/typeChecker/checkers/null.ts new file mode 100644 index 0000000..ec8eaf8 --- /dev/null +++ b/src/webpage/typeChecker/checkers/null.ts @@ -0,0 +1,10 @@ +import {TypeChecker} from "../basetype"; + +export class NullChecker extends TypeChecker { + constructor() { + super(); + } + check(obj: unknown): void { + if (obj !== null) throw new Error(`${obj} is not null`); + } +} diff --git a/src/webpage/typeChecker/checkers/nullish.ts b/src/webpage/typeChecker/checkers/nullish.ts new file mode 100644 index 0000000..426ea8c --- /dev/null +++ b/src/webpage/typeChecker/checkers/nullish.ts @@ -0,0 +1,13 @@ +import {resolveable, TypeChecker} from "../basetype"; + +export class Nullish extends TypeChecker { + checker: TypeChecker; + constructor(check: resolveable) { + super(); + this.checker = TypeChecker.resolve(check); + } + check(obj: unknown) { + if (obj === null) return; + this.checker.check(obj); + } +} diff --git a/src/webpage/typeChecker/checkers/number.ts b/src/webpage/typeChecker/checkers/number.ts new file mode 100644 index 0000000..75f915b --- /dev/null +++ b/src/webpage/typeChecker/checkers/number.ts @@ -0,0 +1,14 @@ +import {TypeChecker} from "../basetype"; + +export class NumberChecker extends TypeChecker { + num?: number; + constructor(num?: number) { + super(); + this.num = num; + } + check(obj: unknown): void { + if (this.num === undefined) { + if (typeof obj !== "number") throw new Error(`${obj} is not a number`); + } else if (this.num !== obj) throw new Error(`${obj} and ${this.num} do not match`); + } +} diff --git a/src/webpage/typeChecker/checkers/object.ts b/src/webpage/typeChecker/checkers/object.ts new file mode 100644 index 0000000..05982c0 --- /dev/null +++ b/src/webpage/typeChecker/checkers/object.ts @@ -0,0 +1,38 @@ +import {resolveable, TypeChecker} from "../basetype"; + +export class ObjectChecker extends TypeChecker { + obj: {[key: string]: TypeChecker} | [TypeChecker, TypeChecker] | [TypeChecker]; + constructor(obj: {[key: string]: resolveable} | [resolveable] | [resolveable, resolveable]) { + super(); + if (obj instanceof Array) { + this.obj = obj.map((_) => TypeChecker.resolve(_)) as + | [TypeChecker, TypeChecker] + | [TypeChecker]; + } else { + this.obj = Object.fromEntries( + Object.entries(obj).map(([key, value]) => [key, TypeChecker.resolve(value)]), + ); + } + } + check(obj: unknown) { + if (!(obj instanceof Object)) throw new Error(`${obj} is not of type object`); + if (this.obj instanceof Array) { + const valueCheck = this.obj[this.obj.length - 1]; + const keyCheck = this.obj.at(-2); + for (const [key, value] of Object.entries(obj)) { + keyCheck?.check(key); + valueCheck.check(value); + } + } else { + const thiskeys = Object.keys(this.obj); + for (const key of thiskeys) { + this.obj[key].check((obj as Record)[key]); + } + const objKeys = Object.keys(obj); + const extra = new Set(objKeys).difference(new Set(thiskeys)); + if (extra.size) { + throw new Error(`Object has extra keys ${[...extra]}`); + } + } + } +} diff --git a/src/webpage/typeChecker/checkers/optional.ts b/src/webpage/typeChecker/checkers/optional.ts new file mode 100644 index 0000000..2f4450d --- /dev/null +++ b/src/webpage/typeChecker/checkers/optional.ts @@ -0,0 +1,13 @@ +import {resolveable, TypeChecker} from "../basetype"; + +export class Optional extends TypeChecker { + checker: TypeChecker; + constructor(check: resolveable) { + super(); + this.checker = TypeChecker.resolve(check); + } + check(obj: unknown) { + if (obj === undefined) return; + this.checker.check(obj); + } +} diff --git a/src/webpage/typeChecker/checkers/or.ts b/src/webpage/typeChecker/checkers/or.ts new file mode 100644 index 0000000..15ed95f --- /dev/null +++ b/src/webpage/typeChecker/checkers/or.ts @@ -0,0 +1,21 @@ +import {resolveable, TypeChecker} from "../basetype"; + +export class OrChecker extends TypeChecker { + arr: TypeChecker[]; + constructor(arr: resolveable[]) { + super(); + this.arr = arr.map((_) => TypeChecker.resolve(_)); + } + check(obj: unknown) { + let err = new Error("or statement is missing things to or"); + for (const elm of this.arr) { + try { + elm.check(obj); + return; + } catch (e) { + err = e as Error; + } + } + throw err; + } +} diff --git a/src/webpage/typeChecker/checkers/string.ts b/src/webpage/typeChecker/checkers/string.ts new file mode 100644 index 0000000..12fcf92 --- /dev/null +++ b/src/webpage/typeChecker/checkers/string.ts @@ -0,0 +1,17 @@ +import {TypeChecker} from "../basetype"; + +export class StringChecker extends TypeChecker { + str?: string | RegExp; + constructor(str?: string | RegExp) { + super(); + this.str = str; + } + check(obj: unknown): void { + if (this.str === undefined) { + if (typeof obj !== "string") throw new Error(`${obj} is not a string`); + } else if (this.str instanceof RegExp) { + if (typeof obj !== "string") throw new Error(`${obj} is not a string`); + if (!obj.match(this.str)) throw new Error(`${obj} does not match ${this.str}`); + } else if (this.str !== obj) throw new Error(`${obj} does not match ${this.str}`); + } +} diff --git a/src/webpage/typeChecker/chekerIndex.ts b/src/webpage/typeChecker/chekerIndex.ts new file mode 100644 index 0000000..b8cc899 --- /dev/null +++ b/src/webpage/typeChecker/chekerIndex.ts @@ -0,0 +1,45 @@ +import {TypeChecker} from "./basetype"; +import {basicEvent} from "./types/basicEvent"; +import {heartbeat} from "./types/events/hearbeat"; +import {ready} from "./types/events/ready"; +import {initHeart} from "./types/events/setInterval"; +type basicgw = { + op: number; + d?: unknown; + s?: number; + t?: string; +}; +export class Check { + static apiMatch = [] as [RegExp, TypeChecker][]; + static checkAPI(path: string, obj: unknown) { + for (const [match, check] of this.apiMatch) { + if (path.match(match)) { + check.check(obj); + return; + } + } + console.warn(`could not check API path: ${path}`); + } + static checkDispatch(dis: basicgw) { + switch (dis.t) { + case "READY": + return ready.check(dis); + default: + console.warn(`could not check gateway dispatch: ${dis.t}`); + } + } + static checkEvent(obj: unknown) { + basicEvent.check(obj); + const o = obj as basicgw; + switch (o.op) { + case 0: + return this.checkDispatch(o); + case 10: + return initHeart.check(o); + case 11: + return heartbeat.check(o); + default: + console.warn(`could not check gateway: ${o.op}`); + } + } +} diff --git a/src/webpage/typeChecker/types/basicEvent.ts b/src/webpage/typeChecker/types/basicEvent.ts new file mode 100644 index 0000000..da044d4 --- /dev/null +++ b/src/webpage/typeChecker/types/basicEvent.ts @@ -0,0 +1,11 @@ +import {Any} from "../checkers/any"; +import {Nullish} from "../checkers/nullish"; +import {ObjectChecker} from "../checkers/object"; +import {Optional} from "../checkers/optional"; + +export const basicEvent = new ObjectChecker({ + op: Number, + d: new Nullish(new Optional(new Any())), + s: new Nullish(new Optional(Number)), + t: new Nullish(new Optional(String)), +}); diff --git a/src/webpage/typeChecker/types/events/hearbeat.ts b/src/webpage/typeChecker/types/events/hearbeat.ts new file mode 100644 index 0000000..9294fd4 --- /dev/null +++ b/src/webpage/typeChecker/types/events/hearbeat.ts @@ -0,0 +1,10 @@ +import {Nullish} from "../../checkers/nullish"; +import {ObjectChecker} from "../../checkers/object"; +import {Optional} from "../../checkers/optional"; + +export const heartbeat = new ObjectChecker({ + op: Number, + d: {}, + s: new Nullish(new Optional(Number)), + t: new Nullish(new Optional(String)), +}); diff --git a/src/webpage/typeChecker/types/events/ready.ts b/src/webpage/typeChecker/types/events/ready.ts new file mode 100644 index 0000000..13cd9a7 --- /dev/null +++ b/src/webpage/typeChecker/types/events/ready.ts @@ -0,0 +1,51 @@ +import {ArrayChecker} from "../../checkers/array"; +import {Nullish} from "../../checkers/nullish"; +import {ObjectChecker} from "../../checkers/object"; +import {Optional} from "../../checkers/optional"; +import {fulluser} from "../objects/user"; + +export const ready = new ObjectChecker({ + op: Number, + d: { + _trace: new Optional(new ArrayChecker(String)), + v: Number, + user: fulluser, + //TODO user_settings + user_settings_proto: String, + notification_settings: { + flags: Number, + }, + user_guild_settings: { + entries: new ArrayChecker({ + channel_overrides: new ArrayChecker({ + message_notifications: Number, + muted: Boolean, + mute_config: new Nullish({ + selected_time_window: Number, + end_time: Number, + }), + channel_id: String, + }), + message_notifications: Number, + flags: Number, + hide_muted_channels: Boolean, + mobile_push: Boolean, + mute_config: new Nullish({ + selected_time_window: Number, + end_time: Number, + }), + mute_scheduled_events: Boolean, + muted: Boolean, + notify_highlights: Number, + suppress_everyone: Boolean, + suppress_roles: Boolean, + version: Number, + guild_id: String, + }), + partial: Boolean, + version: Number, + }, + }, + s: new Nullish(new Optional(Number)), + t: new Nullish(new Optional(String)), +}); diff --git a/src/webpage/typeChecker/types/events/setInterval.ts b/src/webpage/typeChecker/types/events/setInterval.ts new file mode 100644 index 0000000..e8c15b9 --- /dev/null +++ b/src/webpage/typeChecker/types/events/setInterval.ts @@ -0,0 +1,12 @@ +import {Nullish} from "../../checkers/nullish"; +import {ObjectChecker} from "../../checkers/object"; +import {Optional} from "../../checkers/optional"; + +export const initHeart = new ObjectChecker({ + op: Number, + d: { + heartbeat_interval: Number, + }, + s: new Nullish(new Optional(Number)), + t: new Nullish(new Optional(String)), +}); diff --git a/src/webpage/typeChecker/types/objects/user.ts b/src/webpage/typeChecker/types/objects/user.ts new file mode 100644 index 0000000..4996170 --- /dev/null +++ b/src/webpage/typeChecker/types/objects/user.ts @@ -0,0 +1,67 @@ +import {Nullish} from "../../checkers/nullish"; +import {ObjectChecker} from "../../checkers/object"; +import {Optional} from "../../checkers/optional"; +import {snowflake} from "../snowflake"; +export const fulluser = new ObjectChecker({ + id: snowflake, + username: String, + discriminator: /\d\d\d\d/, + global_name: new Nullish(new Optional(String)), + avatar: new Nullish(String), + avatar_decoration_data: new Nullish( + new Optional({ + asset: String, + sku_id: String, + }), + ), + //TODO collectibles, display_name_styles, primary_guild, linked_users, premium_state + bot: new Optional(Boolean), + system: new Optional(Boolean), + mfa_enabled: Boolean, + nsfw_allowed: new Nullish(new Optional(Boolean)), + age_verification_status: new Optional(Number), + pronouns: new Optional(String), + bio: String, + banner: new Optional(new Nullish(String)), + accent_color: new Optional(new Nullish(Number)), + locale: new Optional(String), + verified: new Optional(Boolean), + email: new Nullish(String), + phone: new Optional(new Nullish(String)), + premium: Boolean, + premium_type: Number, + personal_connection_id: new Optional(snowflake), + flags: new Optional(Number), + public_flags: Number, + purchased_flags: new Optional(Number), + premium_usage_flags: new Optional(Number), + desktop: new Optional(Number), + mobile: new Optional(Number), + has_bounced_email: new Optional(Boolean), + authenticator_types: new Optional(new Array(Number)), + analytics_token: new Optional(String), +}); +export const partialuser = new ObjectChecker({ + id: snowflake, + username: String, + discriminator: /\d\d\d\d/, + global_name: new Nullish(new Optional(String)), + avatar: new Nullish(String), + avatar_decoration_data: new Nullish( + new Optional({ + asset: String, + sku_id: String, + }), + ), + //TODO collectibles, display_name_styles, primary_guild, linked_users + bot: new Optional(Boolean), + system: new Optional(Boolean), + mfa_enabled: Boolean, + nsfw_allowed: new Nullish(new Optional(Boolean)), + age_verification_status: new Optional(Number), + pronouns: new Optional(String), + bio: String, + banner: new Optional(new Nullish(String)), + accent_color: new Optional(new Nullish(Number)), + public_flags: new Optional(Number), +}); diff --git a/src/webpage/typeChecker/types/snowflake.ts b/src/webpage/typeChecker/types/snowflake.ts new file mode 100644 index 0000000..d125e01 --- /dev/null +++ b/src/webpage/typeChecker/types/snowflake.ts @@ -0,0 +1,3 @@ +import {StringChecker} from "../checkers/string"; + +export const snowflake = new StringChecker(/\d*/); diff --git a/translations/en.json b/translations/en.json index 4611454..e6aca65 100644 --- a/translations/en.json +++ b/translations/en.json @@ -224,7 +224,7 @@ "gatewayComp": "Disable Gateway compression:", "reportSystem":"Enable experimental reporting system:", "logGateway": "Log received gateway events (log level info):", - "name": "Developer Settings", + "name": "Developer settings", "traces": "Expose traces:" }, "deviceManage": { @@ -554,7 +554,7 @@ "themesAndSounds": "Themes & Sounds", "tokenDisplay": "Token: $1", "trace": "Traces", - "trusted": "Trusted Domains", + "trusted": "Trusted domains", "trustedDesc": "When you click on links sending you to these domain, you will ***not*** be prompted for permission to open like other links, only give this to domains you trust, such as 'https://fermi.chat'", "updateSettings": "Update settings", "updatesYay": "Updates found!",