diff --git a/api/assets/schemas.json b/api/assets/schemas.json
new file mode 100644
index 00000000..b575bf60
--- /dev/null
+++ b/api/assets/schemas.json
@@ -0,0 +1,939 @@
+{
+ "DmChannelCreateSchema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "recipients": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "recipients"
+ ],
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "ChannelModifySchema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "type": {
+ "type": "integer"
+ },
+ "topic": {
+ "type": "string"
+ },
+ "bitrate": {
+ "type": "integer"
+ },
+ "user_limit": {
+ "type": "integer"
+ },
+ "rate_limit_per_user": {
+ "type": "integer"
+ },
+ "position": {
+ "type": "integer"
+ },
+ "permission_overwrites": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "integer"
+ },
+ "allow": {
+ "type": "bigint"
+ },
+ "deny": {
+ "type": "bigint"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "allow",
+ "deny",
+ "id",
+ "type"
+ ]
+ }
+ },
+ "parent_id": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "nsfw": {
+ "type": "boolean"
+ },
+ "rtc_region": {
+ "type": "string"
+ },
+ "default_auto_archive_duration": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "name",
+ "type"
+ ]
+ },
+ "ChannelGuildPositionUpdateSchema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "position": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "id"
+ ]
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "InviteCreateSchema": {
+ "type": "object",
+ "properties": {
+ "target_user_id": {
+ "type": "string"
+ },
+ "target_type": {
+ "type": "string"
+ },
+ "validate": {
+ "type": "string"
+ },
+ "max_age": {
+ "type": "integer"
+ },
+ "max_uses": {
+ "type": "integer"
+ },
+ "temporary": {
+ "type": "boolean"
+ },
+ "unique": {
+ "type": "boolean"
+ },
+ "target_user": {
+ "type": "string"
+ },
+ "target_user_type": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "EmbedType": {
+ "enum": [
+ "article",
+ "gifv",
+ "image",
+ "link",
+ "rich",
+ "video"
+ ],
+ "type": "string"
+ },
+ "EmbedImage": {
+ "type": "object",
+ "properties": {
+ "url": {
+ "type": "string"
+ },
+ "proxy_url": {
+ "type": "string"
+ },
+ "height": {
+ "type": "integer"
+ },
+ "width": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false
+ },
+ "MessageCreateSchema": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "string"
+ },
+ "nonce": {
+ "type": "string"
+ },
+ "tts": {
+ "type": "boolean"
+ },
+ "flags": {
+ "type": "string"
+ },
+ "embed": {
+ "additionalProperties": false,
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "$ref": "#/definitions/EmbedType"
+ },
+ "description": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "timestamp": {
+ "type": "string"
+ },
+ "color": {
+ "type": "integer"
+ },
+ "footer": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "string"
+ },
+ "icon_url": {
+ "type": "string"
+ },
+ "proxy_icon_url": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "text"
+ ]
+ },
+ "image": {
+ "$ref": "#/definitions/EmbedImage"
+ },
+ "thumbnail": {
+ "$ref": "#/definitions/EmbedImage"
+ },
+ "video": {
+ "$ref": "#/definitions/EmbedImage"
+ },
+ "provider": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "author": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "icon_url": {
+ "type": "string"
+ },
+ "proxy_icon_url": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "fields": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ },
+ "inline": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "name",
+ "value"
+ ]
+ }
+ }
+ }
+ },
+ "allowed_mentions": {
+ "type": "object",
+ "properties": {
+ "parse": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "roles": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "users": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "replied_user": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ },
+ "message_reference": {
+ "type": "object",
+ "properties": {
+ "message_id": {
+ "type": "string"
+ },
+ "channel_id": {
+ "type": "string"
+ },
+ "guild_id": {
+ "type": "string"
+ },
+ "fail_if_not_exists": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "channel_id",
+ "message_id"
+ ]
+ },
+ "payload_json": {
+ "type": "string"
+ },
+ "file": {}
+ },
+ "additionalProperties": false,
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "BanCreateSchema": {
+ "type": "object",
+ "properties": {
+ "delete_message_days": {
+ "type": "string"
+ },
+ "reason": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "GuildCreateSchema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ },
+ "icon": {
+ "type": "string"
+ },
+ "channels": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/ChannelModifySchema"
+ }
+ },
+ "guild_template_code": {
+ "type": "string"
+ },
+ "system_channel_id": {
+ "type": "string"
+ },
+ "rules_channel_id": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "name"
+ ],
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "GuildUpdateSchema": {
+ "type": "object",
+ "properties": {
+ "banner": {
+ "type": "string"
+ },
+ "splash": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "features": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "verification_level": {
+ "type": "integer"
+ },
+ "default_message_notifications": {
+ "type": "integer"
+ },
+ "system_channel_flags": {
+ "type": "integer"
+ },
+ "explicit_content_filter": {
+ "type": "integer"
+ },
+ "public_updates_channel_id": {
+ "type": "string"
+ },
+ "afk_timeout": {
+ "type": "integer"
+ },
+ "afk_channel_id": {
+ "type": "string"
+ },
+ "preferred_locale": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ },
+ "icon": {
+ "type": "string"
+ },
+ "guild_template_code": {
+ "type": "string"
+ },
+ "system_channel_id": {
+ "type": "string"
+ },
+ "rules_channel_id": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "name"
+ ],
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "GuildTemplateCreateSchema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "avatar": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "name"
+ ],
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "GuildUpdateWelcomeScreenSchema": {
+ "type": "object",
+ "properties": {
+ "welcome_channels": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "channel_id": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "emoji_id": {
+ "type": "string"
+ },
+ "emoji_name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "channel_id",
+ "description",
+ "emoji_name"
+ ]
+ }
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "description": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "VoiceStateUpdateSchema": {
+ "type": "object",
+ "properties": {
+ "channel_id": {
+ "type": "string"
+ },
+ "suppress": {
+ "type": "boolean"
+ },
+ "request_to_speak_timestamp": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "channel_id"
+ ],
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "MemberCreateSchema": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "nick": {
+ "type": "string"
+ },
+ "guild_id": {
+ "type": "string"
+ },
+ "joined_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "guild_id",
+ "id",
+ "joined_at",
+ "nick"
+ ],
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "MemberNickChangeSchema": {
+ "type": "object",
+ "properties": {
+ "nick": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "nick"
+ ],
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "MemberChangeSchema": {
+ "type": "object",
+ "properties": {
+ "roles": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false,
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "RoleModifySchema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "permissions": {
+ "type": "bigint"
+ },
+ "color": {
+ "type": "integer"
+ },
+ "hoist": {
+ "type": "boolean"
+ },
+ "mentionable": {
+ "type": "boolean"
+ },
+ "position": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "RolePositionUpdateSchema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "position": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "position"
+ ]
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "TemplateCreateSchema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "name"
+ ],
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "TemplateModifySchema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "name"
+ ],
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "EmojiCreateSchema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "image": {
+ "type": "string"
+ },
+ "roles": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "image",
+ "name"
+ ],
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "UserModifySchema": {
+ "type": "object",
+ "properties": {
+ "username": {
+ "type": "string"
+ },
+ "avatar": {
+ "type": "string"
+ },
+ "bio": {
+ "type": "string"
+ },
+ "accent_color": {
+ "type": "integer"
+ },
+ "banner": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ },
+ "new_password": {
+ "type": "string"
+ },
+ "code": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "UserSettingsSchema": {
+ "type": "object",
+ "properties": {
+ "afk_timeout": {
+ "type": "integer"
+ },
+ "allow_accessibility_detection": {
+ "type": "boolean"
+ },
+ "animate_emoji": {
+ "type": "boolean"
+ },
+ "animate_stickers": {
+ "type": "integer"
+ },
+ "contact_sync_enabled": {
+ "type": "boolean"
+ },
+ "convert_emoticons": {
+ "type": "boolean"
+ },
+ "custom_status": {
+ "type": "object",
+ "properties": {
+ "emoji_id": {
+ "type": "string"
+ },
+ "emoji_name": {
+ "type": "string"
+ },
+ "expires_at": {
+ "type": "integer"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "default_guilds_restricted": {
+ "type": "boolean"
+ },
+ "detect_platform_accounts": {
+ "type": "boolean"
+ },
+ "developer_mode": {
+ "type": "boolean"
+ },
+ "disable_games_tab": {
+ "type": "boolean"
+ },
+ "enable_tts_command": {
+ "type": "boolean"
+ },
+ "explicit_content_filter": {
+ "type": "integer"
+ },
+ "friend_source_flags": {
+ "type": "object",
+ "properties": {
+ "all": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "all"
+ ]
+ },
+ "gateway_connected": {
+ "type": "boolean"
+ },
+ "gif_auto_play": {
+ "type": "boolean"
+ },
+ "guild_folders": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "color": {
+ "type": "integer"
+ },
+ "guild_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "color",
+ "guild_ids",
+ "id",
+ "name"
+ ]
+ }
+ },
+ "guild_positions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "inline_attachment_media": {
+ "type": "boolean"
+ },
+ "inline_embed_media": {
+ "type": "boolean"
+ },
+ "locale": {
+ "type": "string"
+ },
+ "message_display_compact": {
+ "type": "boolean"
+ },
+ "native_phone_integration_enabled": {
+ "type": "boolean"
+ },
+ "render_embeds": {
+ "type": "boolean"
+ },
+ "render_reactions": {
+ "type": "boolean"
+ },
+ "restricted_guilds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "show_current_game": {
+ "type": "boolean"
+ },
+ "status": {
+ "enum": [
+ "dnd",
+ "idle",
+ "offline",
+ "online"
+ ],
+ "type": "string"
+ },
+ "stream_notifications_enabled": {
+ "type": "boolean"
+ },
+ "theme": {
+ "enum": [
+ "dark",
+ "white"
+ ],
+ "type": "string"
+ },
+ "timezone_offset": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "afk_timeout",
+ "allow_accessibility_detection",
+ "animate_emoji",
+ "animate_stickers",
+ "contact_sync_enabled",
+ "convert_emoticons",
+ "custom_status",
+ "default_guilds_restricted",
+ "detect_platform_accounts",
+ "developer_mode",
+ "disable_games_tab",
+ "enable_tts_command",
+ "explicit_content_filter",
+ "friend_source_flags",
+ "gateway_connected",
+ "gif_auto_play",
+ "guild_folders",
+ "guild_positions",
+ "inline_attachment_media",
+ "inline_embed_media",
+ "locale",
+ "message_display_compact",
+ "native_phone_integration_enabled",
+ "render_embeds",
+ "render_reactions",
+ "restricted_guilds",
+ "show_current_game",
+ "status",
+ "stream_notifications_enabled",
+ "theme",
+ "timezone_offset"
+ ],
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "WidgetModifySchema": {
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "channel_id": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "channel_id",
+ "enabled"
+ ],
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ }
+}
\ No newline at end of file
diff --git a/api/patches/ajv+8.6.2.patch b/api/patches/ajv+8.6.2.patch
new file mode 100644
index 00000000..3f54881b
--- /dev/null
+++ b/api/patches/ajv+8.6.2.patch
@@ -0,0 +1,249 @@
+diff --git a/node_modules/ajv/dist/compile/jtd/parse.js b/node_modules/ajv/dist/compile/jtd/parse.js
+index 1eeb1be..7684121 100644
+--- a/node_modules/ajv/dist/compile/jtd/parse.js
++++ b/node_modules/ajv/dist/compile/jtd/parse.js
+@@ -239,6 +239,9 @@ function parseType(cxt) {
+ gen.if(fail, () => parsingError(cxt, codegen_1.str `invalid timestamp`));
+ break;
+ }
++ case "bigint":
++ parseBigInt(cxt);
++ break
+ case "float32":
+ case "float64":
+ parseNumber(cxt);
+@@ -284,6 +287,15 @@ function parseNumber(cxt, maxDigits) {
+ skipWhitespace(cxt);
+ gen.if(codegen_1._ `"-0123456789".indexOf(${jsonSlice(1)}) < 0`, () => jsonSyntaxError(cxt), () => parseWith(cxt, parseJson_1.parseJsonNumber, maxDigits));
+ }
++function parseBigInt(cxt, maxDigits) {
++ const {gen} = cxt
++ skipWhitespace(cxt)
++ gen.if(
++ _`"-0123456789".indexOf(${jsonSlice(1)}) < 0`,
++ () => jsonSyntaxError(cxt),
++ () => parseWith(cxt, parseJson_1.parseJsonBigInt, maxDigits)
++ )
++}
+ function parseBooleanToken(bool, fail) {
+ return (cxt) => {
+ const { gen, data } = cxt;
+diff --git a/node_modules/ajv/dist/compile/rules.js b/node_modules/ajv/dist/compile/rules.js
+index 82a591f..1ebd8fe 100644
+--- a/node_modules/ajv/dist/compile/rules.js
++++ b/node_modules/ajv/dist/compile/rules.js
+@@ -1,7 +1,7 @@
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.getRules = exports.isJSONType = void 0;
+-const _jsonTypes = ["string", "number", "integer", "boolean", "null", "object", "array"];
++const _jsonTypes = ["string", "number", "integer", "boolean", "null", "object", "array","bigint"];
+ const jsonTypes = new Set(_jsonTypes);
+ function isJSONType(x) {
+ return typeof x == "string" && jsonTypes.has(x);
+@@ -13,10 +13,11 @@ function getRules() {
+ string: { type: "string", rules: [] },
+ array: { type: "array", rules: [] },
+ object: { type: "object", rules: [] },
++ bigint: {type: "bigint", rules: []}
+ };
+ return {
+- types: { ...groups, integer: true, boolean: true, null: true },
+- rules: [{ rules: [] }, groups.number, groups.string, groups.array, groups.object],
++ types: { ...groups, integer: true, boolean: true, null: true, bigint: true },
++ rules: [{ rules: [] }, groups.number, groups.string, groups.array, groups.object, groups.bigint],
+ post: { rules: [] },
+ all: {},
+ keywords: {},
+diff --git a/node_modules/ajv/dist/compile/validate/dataType.js b/node_modules/ajv/dist/compile/validate/dataType.js
+index 6319e76..8b50b4c 100644
+--- a/node_modules/ajv/dist/compile/validate/dataType.js
++++ b/node_modules/ajv/dist/compile/validate/dataType.js
+@@ -52,7 +52,7 @@ function coerceAndCheckDataType(it, types) {
+ return checkTypes;
+ }
+ exports.coerceAndCheckDataType = coerceAndCheckDataType;
+-const COERCIBLE = new Set(["string", "number", "integer", "boolean", "null"]);
++const COERCIBLE = new Set(["string", "number", "integer", "boolean", "null","bigint"]);
+ function coerceToTypes(types, coerceTypes) {
+ return coerceTypes
+ ? types.filter((t) => COERCIBLE.has(t) || (coerceTypes === "array" && t === "array"))
+@@ -83,6 +83,14 @@ function coerceData(it, types, coerceTo) {
+ });
+ function coerceSpecificType(t) {
+ switch (t) {
++ case "bigint":
++ gen
++ .elseIf(
++ codegen_1._`${dataType} == "boolean" || ${data} === null
++ || (${dataType} == "string" && ${data} && ${data} == BigInt(${data}))`
++ )
++ .assign(coerced, codegen_1._`BigInt(${data})`)
++ return
+ case "string":
+ gen
+ .elseIf(codegen_1._ `${dataType} == "number" || ${dataType} == "boolean"`)
+@@ -143,6 +151,9 @@ function checkDataType(dataType, data, strictNums, correct = DataType.Correct) {
+ case "number":
+ cond = numCond();
+ break;
++ case "bigint":
++ cond = codegen_1._`typeof ${data} == "bigint" && isFinite(${data})`
++ break
+ default:
+ return codegen_1._ `typeof ${data} ${EQ} ${dataType}`;
+ }
+diff --git a/node_modules/ajv/dist/refs/json-schema-2019-09/meta/validation.json b/node_modules/ajv/dist/refs/json-schema-2019-09/meta/validation.json
+index 7027a12..25679c8 100644
+--- a/node_modules/ajv/dist/refs/json-schema-2019-09/meta/validation.json
++++ b/node_modules/ajv/dist/refs/json-schema-2019-09/meta/validation.json
+@@ -78,7 +78,7 @@
+ "default": 0
+ },
+ "simpleTypes": {
+- "enum": ["array", "boolean", "integer", "null", "number", "object", "string"]
++ "enum": ["array", "boolean", "integer", "null", "number", "object", "string","bigint"]
+ },
+ "stringArray": {
+ "type": "array",
+diff --git a/node_modules/ajv/dist/refs/json-schema-2020-12/meta/validation.json b/node_modules/ajv/dist/refs/json-schema-2020-12/meta/validation.json
+index e0ae13d..57c9036 100644
+--- a/node_modules/ajv/dist/refs/json-schema-2020-12/meta/validation.json
++++ b/node_modules/ajv/dist/refs/json-schema-2020-12/meta/validation.json
+@@ -78,7 +78,7 @@
+ "default": 0
+ },
+ "simpleTypes": {
+- "enum": ["array", "boolean", "integer", "null", "number", "object", "string"]
++ "enum": ["array", "boolean", "integer", "null", "number", "object", "string","bigint"]
+ },
+ "stringArray": {
+ "type": "array",
+diff --git a/node_modules/ajv/dist/refs/json-schema-draft-06.json b/node_modules/ajv/dist/refs/json-schema-draft-06.json
+index 5410064..774435b 100644
+--- a/node_modules/ajv/dist/refs/json-schema-draft-06.json
++++ b/node_modules/ajv/dist/refs/json-schema-draft-06.json
+@@ -16,7 +16,7 @@
+ "allOf": [{"$ref": "#/definitions/nonNegativeInteger"}, {"default": 0}]
+ },
+ "simpleTypes": {
+- "enum": ["array", "boolean", "integer", "null", "number", "object", "string"]
++ "enum": ["array", "boolean", "integer", "null", "number", "object", "string","bigint"]
+ },
+ "stringArray": {
+ "type": "array",
+diff --git a/node_modules/ajv/dist/refs/json-schema-draft-07.json b/node_modules/ajv/dist/refs/json-schema-draft-07.json
+index 6a74851..fc6dd7d 100644
+--- a/node_modules/ajv/dist/refs/json-schema-draft-07.json
++++ b/node_modules/ajv/dist/refs/json-schema-draft-07.json
+@@ -16,7 +16,7 @@
+ "allOf": [{"$ref": "#/definitions/nonNegativeInteger"}, {"default": 0}]
+ },
+ "simpleTypes": {
+- "enum": ["array", "boolean", "integer", "null", "number", "object", "string"]
++ "enum": ["array", "boolean", "integer", "null", "number", "object", "string","bigint"]
+ },
+ "stringArray": {
+ "type": "array",
+diff --git a/node_modules/ajv/dist/refs/jtd-schema.js b/node_modules/ajv/dist/refs/jtd-schema.js
+index 1ee940a..1148887 100644
+--- a/node_modules/ajv/dist/refs/jtd-schema.js
++++ b/node_modules/ajv/dist/refs/jtd-schema.js
+@@ -38,6 +38,7 @@ const typeForm = (root) => ({
+ "uint16",
+ "int32",
+ "uint32",
++ "bigint",
+ ],
+ },
+ },
+diff --git a/node_modules/ajv/dist/runtime/parseJson.js b/node_modules/ajv/dist/runtime/parseJson.js
+index 2576a6e..e7447b1 100644
+--- a/node_modules/ajv/dist/runtime/parseJson.js
++++ b/node_modules/ajv/dist/runtime/parseJson.js
+@@ -97,6 +97,71 @@ exports.parseJsonNumber = parseJsonNumber;
+ parseJsonNumber.message = undefined;
+ parseJsonNumber.position = 0;
+ parseJsonNumber.code = 'require("ajv/dist/runtime/parseJson").parseJsonNumber';
++
++function parseJsonBigInt(s, pos, maxDigits) {
++ let numStr = "";
++ let c;
++ parseJsonBigInt.message = undefined;
++ if (s[pos] === "-") {
++ numStr += "-";
++ pos++;
++ }
++ if (s[pos] === "0") {
++ numStr += "0";
++ pos++;
++ }
++ else {
++ if (!parseDigits(maxDigits)) {
++ errorMessage();
++ return undefined;
++ }
++ }
++ if (maxDigits) {
++ parseJsonBigInt.position = pos;
++ return BigInt(numStr);
++ }
++ if (s[pos] === ".") {
++ numStr += ".";
++ pos++;
++ if (!parseDigits()) {
++ errorMessage();
++ return undefined;
++ }
++ }
++ if (((c = s[pos]), c === "e" || c === "E")) {
++ numStr += "e";
++ pos++;
++ if (((c = s[pos]), c === "+" || c === "-")) {
++ numStr += c;
++ pos++;
++ }
++ if (!parseDigits()) {
++ errorMessage();
++ return undefined;
++ }
++ }
++ parseJsonBigInt.position = pos;
++ return BigInt(numStr);
++ function parseDigits(maxLen) {
++ let digit = false;
++ while (((c = s[pos]), c >= "0" && c <= "9" && (maxLen === undefined || maxLen-- > 0))) {
++ digit = true;
++ numStr += c;
++ pos++;
++ }
++ return digit;
++ }
++ function errorMessage() {
++ parseJsonBigInt.position = pos;
++ parseJsonBigInt.message = pos < s.length ? `unexpected token ${s[pos]}` : "unexpected end";
++ }
++}
++exports.parseJsonBigInt = parseJsonBigInt;
++parseJsonBigInt.message = undefined;
++parseJsonBigInt.position = 0;
++parseJsonBigInt.code = 'require("ajv/dist/runtime/parseJson").parseJsonBigInt';
++
++
+ const escapedChars = {
+ b: "\b",
+ f: "\f",
+diff --git a/node_modules/ajv/dist/vocabularies/jtd/type.js b/node_modules/ajv/dist/vocabularies/jtd/type.js
+index 428bddb..fbc3070 100644
+--- a/node_modules/ajv/dist/vocabularies/jtd/type.js
++++ b/node_modules/ajv/dist/vocabularies/jtd/type.js
+@@ -45,6 +45,9 @@ const def = {
+ cond = timestampCode(cxt);
+ break;
+ }
++ case "bigint":
++ cond = codegen_1._`typeof ${data} == "bigint" || typeof ${data} == "string"`
++ break
+ case "float32":
+ case "float64":
+ cond = codegen_1._ `typeof ${data} == "number"`;
\ No newline at end of file
diff --git a/api/scripts/generate_body_schema.ts b/api/scripts/generate_body_schema.ts
new file mode 100644
index 00000000..69e62085
--- /dev/null
+++ b/api/scripts/generate_body_schema.ts
@@ -0,0 +1,59 @@
+// https://mermade.github.io/openapi-gui/#
+// https://editor.swagger.io/
+import path from "path";
+import fs from "fs";
+import * as TJS from "typescript-json-schema";
+import "missing-native-js-functions";
+const schemaPath = path.join(__dirname, "..", "assets", "schemas.json");
+
+const settings: TJS.PartialArgs = {
+ required: true,
+ ignoreErrors: true,
+ excludePrivate: true,
+ defaultNumberType: "integer",
+ noExtraProps: true,
+ defaultProps: false
+};
+const compilerOptions: TJS.CompilerOptions = {
+ strictNullChecks: false
+};
+const ExcludedSchemas = ["DefaultSchema", "Schema", "EntitySchema"];
+
+function main() {
+ const program = TJS.getProgramFromFiles(walk(path.join(__dirname, "..", "src", "routes")), compilerOptions);
+ const generator = TJS.buildGenerator(program, settings);
+ if (!generator || !program) return;
+
+ const schemas = generator.getUserSymbols().filter((x) => x.endsWith("Schema") && !ExcludedSchemas.includes(x));
+ console.log(schemas);
+
+ var definitions: any = {};
+
+ for (const name of schemas) {
+ const part = TJS.generateSchema(program, name, settings, [], generator as TJS.JsonSchemaGenerator);
+ if (!part) continue;
+
+ definitions = { ...definitions, ...part.definitions, [name]: { ...part, definitions: undefined } };
+ }
+
+ fs.writeFileSync(schemaPath, JSON.stringify(definitions, null, 4));
+}
+
+main();
+
+function walk(dir: string) {
+ var results = [] as string[];
+ var list = fs.readdirSync(dir);
+ list.forEach(function (file) {
+ file = dir + "/" + file;
+ var stat = fs.statSync(file);
+ if (stat && stat.isDirectory()) {
+ /* Recurse into a subdirectory */
+ results = results.concat(walk(file));
+ } else {
+ if (!file.endsWith(".ts")) return;
+ results.push(file);
+ }
+ });
+ return results;
+}
diff --git a/api/src/routes/guilds/#guild_id/bans.ts b/api/src/routes/guilds/#guild_id/bans.ts
index 86bff6b4..41a97b6a 100644
--- a/api/src/routes/guilds/#guild_id/bans.ts
+++ b/api/src/routes/guilds/#guild_id/bans.ts
@@ -1,19 +1,18 @@
import { Request, Response, Router } from "express";
import { emitEvent, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, Guild, Ban, User, Member } from "@fosscord/util";
import { HTTPError } from "lambert-server";
-import { getIpAdress, check } from "@fosscord/api";
+import { getIpAdress, check, route } from "@fosscord/api";
import { BanCreateSchema } from "@fosscord/api/schema/Ban";
const router: Router = Router();
-
-router.get("/", async (req: Request, res: Response) => {
+router.get("/", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => {
const { guild_id } = req.params;
var bans = await Ban.find({ guild_id: guild_id });
return res.json(bans);
});
-router.get("/:user", async (req: Request, res: Response) => {
+router.get("/:user", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => {
const { guild_id } = req.params;
const user_id = req.params.ban;
@@ -21,15 +20,14 @@ router.get("/:user", async (req: Request, res: Response) => {
return res.json(ban);
});
-router.put("/:user_id", check(BanCreateSchema), async (req: Request, res: Response) => {
+router.put("/:user_id", route({ body: "BanCreateSchema", permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => {
const { guild_id } = req.params;
const banned_user_id = req.params.user_id;
const banned_user = await User.getPublicUser(banned_user_id);
- const perms = await getPermission(req.user_id, guild_id);
- perms.hasThrow("BAN_MEMBERS");
+
if (req.user_id === banned_user_id) throw new HTTPError("You can't ban yourself", 400);
- if (perms.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400);
+ if (req.permission?.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400);
const ban = new Ban({
user_id: banned_user_id,
@@ -55,17 +53,14 @@ router.put("/:user_id", check(BanCreateSchema), async (req: Request, res: Respon
return res.json(ban);
});
-router.delete("/:user_id", async (req: Request, res: Response) => {
- var { guild_id } = req.params;
- var banned_user_id = req.params.user_id;
+router.delete("/:user_id", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => {
+ const { guild_id, user_id } = req.params;
- const banned_user = await User.getPublicUser(banned_user_id);
- const perms = await getPermission(req.user_id, guild_id);
- perms.hasThrow("BAN_MEMBERS");
+ const banned_user = await User.getPublicUser(user_id);
await Promise.all([
Ban.delete({
- user_id: banned_user_id,
+ user_id: user_id,
guild_id
}),
diff --git a/api/src/util/route.ts b/api/src/util/route.ts
new file mode 100644
index 00000000..0302f3ec
--- /dev/null
+++ b/api/src/util/route.ts
@@ -0,0 +1,66 @@
+import { DiscordApiErrors, Event, EventData, getPermission, PermissionResolvable, Permissions } from "@fosscord/util";
+import { NextFunction, Request, Response } from "express";
+import fs from "fs";
+import path from "path";
+import Ajv from "ajv";
+import { AnyValidateFunction } from "ajv/dist/core";
+import { FieldErrors } from "..";
+
+const SchemaPath = path.join(__dirname, "..", "..", "assets", "schemas.json");
+const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" }));
+export const ajv = new Ajv({ allErrors: true, parseDate: true, allowDate: true, schemas, messages: true });
+
+declare global {
+ namespace Express {
+ interface Request {
+ permission?: Permissions;
+ }
+ }
+}
+
+export type RouteSchema = string; // typescript interface name
+export type RouteResponse = { status: number; body?: RouteSchema; headers?: Record<string, string> };
+
+export interface RouteOptions {
+ permission?: PermissionResolvable;
+ body?: RouteSchema;
+ response?: RouteResponse;
+ example?: {
+ body?: any;
+ path?: string;
+ event?: EventData;
+ headers?: Record<string, string>;
+ };
+}
+
+export function route(opts: RouteOptions) {
+ var validate: AnyValidateFunction<any>;
+ if (opts.body) {
+ // @ts-ignore
+ validate = ajv.getSchema(opts.body);
+ if (!validate) throw new Error(`Body schema ${opts.body} not found`);
+ }
+
+ return async (req: Request, res: Response, next: NextFunction) => {
+ if (opts.permission) {
+ const required = new Permissions(opts.permission);
+ const permission = await getPermission(req.user_id, req.params.guild_id, req.params.channel_id);
+ console.log(required.bitfield, permission.bitfield, permission.bitfield & required.bitfield);
+
+ // bitfield comparison: check if user lacks certain permission
+ if (!permission.has(required)) {
+ throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(opts.permission as string);
+ }
+
+ if (validate) {
+ const valid = validate(req.body);
+ if (!valid) {
+ const fields: Record<string, { code?: string; message: string }> = {};
+ validate.errors?.forEach((x) => (fields[x.instancePath] = { code: x.keyword, message: x.message || "" }));
+ throw FieldErrors(fields);
+ }
+ }
+ }
+ next();
+ };
+}
|