diff --git a/assets/client_test/developers.html b/assets/client_test/developers.html
index fbe0193c..47fb0f4b 100644
--- a/assets/client_test/developers.html
+++ b/assets/client_test/developers.html
@@ -18,7 +18,7 @@
API_VERSION: 9,
API_ENDPOINT: "/api",
WEBAPP_ENDPOINT: "",
- CDN_HOST: `${location.hostname}:3003`,
+ CDN_HOST: `${location.host}`, // TODO: make this file auto populate from config?
BRAINTREE_KEY: "production_5st77rrc_49pp2rp4phym7387",
STRIPE_KEY: "pk_live_CUQtlpQUF0vufWpnpUmQvcdi",
diff --git a/assets/schemas.json b/assets/schemas.json
index 368d22fc..1fe87326 100644
--- a/assets/schemas.json
+++ b/assets/schemas.json
@@ -21631,6 +21631,1575 @@
},
"$schema": "http://json-schema.org/draft-07/schema#"
},
+ "BotModifySchema": {
+ "type": "object",
+ "properties": {
+ "avatar": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "definitions": {
+ "ChannelPermissionOverwriteType": {
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "type": "number"
+ },
+ "Embed": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ "article",
+ "gifv",
+ "image",
+ "link",
+ "rich",
+ "video"
+ ],
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "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"
+ ]
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "EmbedImage": {
+ "type": "object",
+ "properties": {
+ "url": {
+ "type": "string"
+ },
+ "proxy_url": {
+ "type": "string"
+ },
+ "height": {
+ "type": "integer"
+ },
+ "width": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false
+ },
+ "ChannelModifySchema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "maxLength": 100,
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ 0,
+ 1,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 2,
+ 255,
+ 3,
+ 33,
+ 34,
+ 35,
+ 4,
+ 5,
+ 6,
+ 64,
+ 7,
+ 8,
+ 9
+ ],
+ "type": "number"
+ },
+ "topic": {
+ "type": "string"
+ },
+ "icon": {
+ "type": [
+ "null",
+ "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": {
+ "$ref": "#/definitions/ChannelPermissionOverwriteType"
+ },
+ "allow": {
+ "type": "string"
+ },
+ "deny": {
+ "type": "string"
+ }
+ },
+ "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"
+ },
+ "default_reaction_emoji": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "flags": {
+ "type": "integer"
+ },
+ "default_thread_rate_limit_per_user": {
+ "type": "integer"
+ },
+ "video_quality_mode": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false
+ },
+ "ActivitySchema": {
+ "type": "object",
+ "properties": {
+ "afk": {
+ "type": "boolean"
+ },
+ "status": {
+ "$ref": "#/definitions/Status"
+ },
+ "activities": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Activity"
+ }
+ },
+ "since": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "status"
+ ]
+ },
+ "Status": {
+ "enum": [
+ "dnd",
+ "idle",
+ "invisible",
+ "offline",
+ "online"
+ ],
+ "type": "string"
+ },
+ "Activity": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "type": {
+ "$ref": "#/definitions/ActivityType"
+ },
+ "url": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "integer"
+ },
+ "timestamps": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "integer"
+ },
+ "end": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "end",
+ "start"
+ ]
+ },
+ "application_id": {
+ "type": "string"
+ },
+ "details": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ },
+ "emoji": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "animated": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "animated",
+ "name"
+ ]
+ },
+ "party": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "size": {
+ "type": "array",
+ "items": [
+ {
+ "type": "integer"
+ }
+ ],
+ "minItems": 1,
+ "maxItems": 1
+ }
+ },
+ "additionalProperties": false
+ },
+ "assets": {
+ "type": "object",
+ "properties": {
+ "large_image": {
+ "type": "string"
+ },
+ "large_text": {
+ "type": "string"
+ },
+ "small_image": {
+ "type": "string"
+ },
+ "small_text": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "secrets": {
+ "type": "object",
+ "properties": {
+ "join": {
+ "type": "string"
+ },
+ "spectate": {
+ "type": "string"
+ },
+ "match": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "instance": {
+ "type": "boolean"
+ },
+ "flags": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "sync_id": {
+ "type": "string"
+ },
+ "metadata": {
+ "type": "object",
+ "properties": {
+ "context_uri": {
+ "type": "string"
+ },
+ "album_id": {
+ "type": "string"
+ },
+ "artist_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "album_id",
+ "artist_ids"
+ ]
+ },
+ "session_id": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "flags",
+ "name",
+ "session_id",
+ "type"
+ ]
+ },
+ "ActivityType": {
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 4,
+ 5
+ ],
+ "type": "number"
+ },
+ "Record": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "Partial": {
+ "type": "object",
+ "properties": {
+ "message_notifications": {
+ "type": "integer"
+ },
+ "mute_config": {
+ "$ref": "#/definitions/MuteConfig"
+ },
+ "muted": {
+ "type": "boolean"
+ },
+ "channel_id": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ },
+ "additionalProperties": false
+ },
+ "MuteConfig": {
+ "type": "object",
+ "properties": {
+ "end_time": {
+ "type": "integer"
+ },
+ "selected_time_window": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "end_time",
+ "selected_time_window"
+ ]
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "ApplicationModifySchema": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "icon": {
+ "type": "string"
+ },
+ "interactions_endpoint_url": {
+ "type": "string"
+ },
+ "max_participants": {
+ "type": [
+ "null",
+ "integer"
+ ]
+ },
+ "name": {
+ "type": "string"
+ },
+ "privacy_policy_url": {
+ "type": "string"
+ },
+ "role_connections_verification_url": {
+ "type": "string"
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "terms_of_service_url": {
+ "type": "string"
+ },
+ "bot_public": {
+ "type": "boolean"
+ },
+ "bot_require_code_grant": {
+ "type": "boolean"
+ },
+ "flags": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "definitions": {
+ "ChannelPermissionOverwriteType": {
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "type": "number"
+ },
+ "Embed": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ "article",
+ "gifv",
+ "image",
+ "link",
+ "rich",
+ "video"
+ ],
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "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"
+ ]
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "EmbedImage": {
+ "type": "object",
+ "properties": {
+ "url": {
+ "type": "string"
+ },
+ "proxy_url": {
+ "type": "string"
+ },
+ "height": {
+ "type": "integer"
+ },
+ "width": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false
+ },
+ "ChannelModifySchema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "maxLength": 100,
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ 0,
+ 1,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 2,
+ 255,
+ 3,
+ 33,
+ 34,
+ 35,
+ 4,
+ 5,
+ 6,
+ 64,
+ 7,
+ 8,
+ 9
+ ],
+ "type": "number"
+ },
+ "topic": {
+ "type": "string"
+ },
+ "icon": {
+ "type": [
+ "null",
+ "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": {
+ "$ref": "#/definitions/ChannelPermissionOverwriteType"
+ },
+ "allow": {
+ "type": "string"
+ },
+ "deny": {
+ "type": "string"
+ }
+ },
+ "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"
+ },
+ "default_reaction_emoji": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "flags": {
+ "type": "integer"
+ },
+ "default_thread_rate_limit_per_user": {
+ "type": "integer"
+ },
+ "video_quality_mode": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false
+ },
+ "ActivitySchema": {
+ "type": "object",
+ "properties": {
+ "afk": {
+ "type": "boolean"
+ },
+ "status": {
+ "$ref": "#/definitions/Status"
+ },
+ "activities": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Activity"
+ }
+ },
+ "since": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "status"
+ ]
+ },
+ "Status": {
+ "enum": [
+ "dnd",
+ "idle",
+ "invisible",
+ "offline",
+ "online"
+ ],
+ "type": "string"
+ },
+ "Activity": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "type": {
+ "$ref": "#/definitions/ActivityType"
+ },
+ "url": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "integer"
+ },
+ "timestamps": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "integer"
+ },
+ "end": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "end",
+ "start"
+ ]
+ },
+ "application_id": {
+ "type": "string"
+ },
+ "details": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ },
+ "emoji": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "animated": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "animated",
+ "name"
+ ]
+ },
+ "party": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "size": {
+ "type": "array",
+ "items": [
+ {
+ "type": "integer"
+ }
+ ],
+ "minItems": 1,
+ "maxItems": 1
+ }
+ },
+ "additionalProperties": false
+ },
+ "assets": {
+ "type": "object",
+ "properties": {
+ "large_image": {
+ "type": "string"
+ },
+ "large_text": {
+ "type": "string"
+ },
+ "small_image": {
+ "type": "string"
+ },
+ "small_text": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "secrets": {
+ "type": "object",
+ "properties": {
+ "join": {
+ "type": "string"
+ },
+ "spectate": {
+ "type": "string"
+ },
+ "match": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "instance": {
+ "type": "boolean"
+ },
+ "flags": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "sync_id": {
+ "type": "string"
+ },
+ "metadata": {
+ "type": "object",
+ "properties": {
+ "context_uri": {
+ "type": "string"
+ },
+ "album_id": {
+ "type": "string"
+ },
+ "artist_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "album_id",
+ "artist_ids"
+ ]
+ },
+ "session_id": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "flags",
+ "name",
+ "session_id",
+ "type"
+ ]
+ },
+ "ActivityType": {
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 4,
+ 5
+ ],
+ "type": "number"
+ },
+ "Record": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "Partial": {
+ "type": "object",
+ "properties": {
+ "message_notifications": {
+ "type": "integer"
+ },
+ "mute_config": {
+ "$ref": "#/definitions/MuteConfig"
+ },
+ "muted": {
+ "type": "boolean"
+ },
+ "channel_id": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ },
+ "additionalProperties": false
+ },
+ "MuteConfig": {
+ "type": "object",
+ "properties": {
+ "end_time": {
+ "type": "integer"
+ },
+ "selected_time_window": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "end_time",
+ "selected_time_window"
+ ]
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "ApplicationCreateSchema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "team_id": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "name"
+ ],
+ "definitions": {
+ "ChannelPermissionOverwriteType": {
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "type": "number"
+ },
+ "Embed": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ "article",
+ "gifv",
+ "image",
+ "link",
+ "rich",
+ "video"
+ ],
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "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"
+ ]
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "EmbedImage": {
+ "type": "object",
+ "properties": {
+ "url": {
+ "type": "string"
+ },
+ "proxy_url": {
+ "type": "string"
+ },
+ "height": {
+ "type": "integer"
+ },
+ "width": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false
+ },
+ "ChannelModifySchema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "maxLength": 100,
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ 0,
+ 1,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 2,
+ 255,
+ 3,
+ 33,
+ 34,
+ 35,
+ 4,
+ 5,
+ 6,
+ 64,
+ 7,
+ 8,
+ 9
+ ],
+ "type": "number"
+ },
+ "topic": {
+ "type": "string"
+ },
+ "icon": {
+ "type": [
+ "null",
+ "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": {
+ "$ref": "#/definitions/ChannelPermissionOverwriteType"
+ },
+ "allow": {
+ "type": "string"
+ },
+ "deny": {
+ "type": "string"
+ }
+ },
+ "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"
+ },
+ "default_reaction_emoji": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "flags": {
+ "type": "integer"
+ },
+ "default_thread_rate_limit_per_user": {
+ "type": "integer"
+ },
+ "video_quality_mode": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false
+ },
+ "ActivitySchema": {
+ "type": "object",
+ "properties": {
+ "afk": {
+ "type": "boolean"
+ },
+ "status": {
+ "$ref": "#/definitions/Status"
+ },
+ "activities": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Activity"
+ }
+ },
+ "since": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "status"
+ ]
+ },
+ "Status": {
+ "enum": [
+ "dnd",
+ "idle",
+ "invisible",
+ "offline",
+ "online"
+ ],
+ "type": "string"
+ },
+ "Activity": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "type": {
+ "$ref": "#/definitions/ActivityType"
+ },
+ "url": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "integer"
+ },
+ "timestamps": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "integer"
+ },
+ "end": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "end",
+ "start"
+ ]
+ },
+ "application_id": {
+ "type": "string"
+ },
+ "details": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ },
+ "emoji": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "animated": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "animated",
+ "name"
+ ]
+ },
+ "party": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "size": {
+ "type": "array",
+ "items": [
+ {
+ "type": "integer"
+ }
+ ],
+ "minItems": 1,
+ "maxItems": 1
+ }
+ },
+ "additionalProperties": false
+ },
+ "assets": {
+ "type": "object",
+ "properties": {
+ "large_image": {
+ "type": "string"
+ },
+ "large_text": {
+ "type": "string"
+ },
+ "small_image": {
+ "type": "string"
+ },
+ "small_text": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "secrets": {
+ "type": "object",
+ "properties": {
+ "join": {
+ "type": "string"
+ },
+ "spectate": {
+ "type": "string"
+ },
+ "match": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "instance": {
+ "type": "boolean"
+ },
+ "flags": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "sync_id": {
+ "type": "string"
+ },
+ "metadata": {
+ "type": "object",
+ "properties": {
+ "context_uri": {
+ "type": "string"
+ },
+ "album_id": {
+ "type": "string"
+ },
+ "artist_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "album_id",
+ "artist_ids"
+ ]
+ },
+ "session_id": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "flags",
+ "name",
+ "session_id",
+ "type"
+ ]
+ },
+ "ActivityType": {
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 4,
+ 5
+ ],
+ "type": "number"
+ },
+ "Record": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "Partial": {
+ "type": "object",
+ "properties": {
+ "message_notifications": {
+ "type": "integer"
+ },
+ "mute_config": {
+ "$ref": "#/definitions/MuteConfig"
+ },
+ "muted": {
+ "type": "boolean"
+ },
+ "channel_id": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ },
+ "additionalProperties": false
+ },
+ "MuteConfig": {
+ "type": "object",
+ "properties": {
+ "end_time": {
+ "type": "integer"
+ },
+ "selected_time_window": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "end_time",
+ "selected_time_window"
+ ]
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
"ActivitySchema": {
"$ref": "#/definitions/ActivitySchema",
"definitions": {
diff --git a/src/api/routes/applications/#id/bot/index.ts b/src/api/routes/applications/#id/bot/index.ts
index ad2399b8..ed5d6a70 100644
--- a/src/api/routes/applications/#id/bot/index.ts
+++ b/src/api/routes/applications/#id/bot/index.ts
@@ -1,81 +1,78 @@
import { Request, Response, Router } from "express";
import { route } from "@fosscord/api";
-import { Application, Config, FieldErrors, generateToken, OrmUtils, Snowflake, trimSpecial, User } from "@fosscord/util";
+import { Application, generateToken, User, BotModifySchema, handleFile, DiscordApiErrors } from "@fosscord/util";
import { HTTPError } from "lambert-server";
import { verifyToken } from "node-2fa";
const router: Router = Router();
router.post("/", route({}), async (req: Request, res: Response) => {
- const app = await Application.findOne({where: {id: req.params.id}});
- if(!app) return res.status(404);
- const username = trimSpecial(app.name);
- const discriminator = await User.generateDiscriminator(username);
- if (!discriminator) {
- // We've failed to generate a valid and unused discriminator
- throw FieldErrors({
- username: {
- code: "USERNAME_TOO_MANY_USERS",
- message: req?.t("auth:register.USERNAME_TOO_MANY_USERS"),
- },
- });
- }
+ const app = await Application.findOneOrFail({ where: { id: req.params.id }, relations: ["owner"] });
- const user = OrmUtils.mergeDeep(new User(), {
- created_at: new Date(),
- username: username,
- discriminator,
- id: app.id,
- bot: true,
- system: false,
- premium_since: new Date(),
- desktop: false,
- mobile: false,
- premium: true,
- premium_type: 2,
- bio: app.description,
- mfa_enabled: false,
- totp_secret: "",
- totp_backup_codes: [],
- verified: true,
- disabled: false,
- deleted: false,
- email: null,
- rights: Config.get().security.defaultRights,
- nsfw_allowed: true,
- public_flags: "0",
- flags: "0",
- data: {
- hash: null,
- valid_tokens_since: new Date(),
- },
- settings: {},
- extended_settings: {},
- fingerprints: [],
- notes: {},
+ if (app.owner.id != req.user_id)
+ throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
+
+ const user = await User.register({
+ username: app.name,
+ password: undefined,
+ req,
});
+
+ user.id = app.id;
+ user.premium_since = new Date();
+ user.bot = true;
+
await user.save();
- app.bot = user;
+
+ // flags is NaN here?
+ app.assign({ bot: user, flags: app.flags || 0 });
+
await app.save();
- res.send().status(204)
+
+ res.send().status(204);
});
router.post("/reset", route({}), async (req: Request, res: Response) => {
- let bot = await User.findOne({where: {id: req.params.id}});
- let owner = await User.findOne({where: {id: req.user_id}});
- if(!bot) return res.status(404);
- if(owner?.totp_secret && (!req.body.code || verifyToken(owner.totp_secret, req.body.code))) {
+ let bot = await User.findOneOrFail({ where: { id: req.params.id } });
+ let owner = await User.findOneOrFail({ where: { id: req.user_id } });
+
+ if (owner.id != req.user_id)
+ throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
+
+ if (owner.totp_secret && (!req.body.code || verifyToken(owner.totp_secret, req.body.code)))
throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
- }
+
bot.data = { hash: undefined, valid_tokens_since: new Date() };
+
await bot.save();
+
let token = await generateToken(bot.id);
- res.json({token}).status(200);
+
+ res.json({ token }).status(200);
});
-router.patch("/", route({}), async (req: Request, res: Response) => {
- delete req.body.avatar;
- let app = OrmUtils.mergeDeep(await User.findOne({where: {id: req.params.id}}), req.body);
+router.patch("/", route({ body: "BotModifySchema" }), async (req: Request, res: Response) => {
+ const body = req.body as BotModifySchema;
+ if (!body.avatar?.trim()) delete body.avatar;
+
+ const app = await Application.findOneOrFail({ where: { id: req.params.id }, relations: ["bot", "owner"] });
+
+ if (!app.bot)
+ throw DiscordApiErrors.BOT_ONLY_ENDPOINT;
+
+ if (app.owner.id != req.user_id)
+ throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
+
+ if (body.avatar)
+ body.avatar = await handleFile(
+ `/avatars/${app.id}`,
+ body.avatar as string,
+ );
+
+ app.bot.assign(body);
+
+ app.bot.save();
+
await app.save();
res.json(app).status(200);
});
diff --git a/src/api/routes/applications/#id/index.ts b/src/api/routes/applications/#id/index.ts
index 0aced582..79df256a 100644
--- a/src/api/routes/applications/#id/index.ts
+++ b/src/api/routes/applications/#id/index.ts
@@ -1,28 +1,55 @@
import { Request, Response, Router } from "express";
import { route } from "@fosscord/api";
-import { Application, OrmUtils, Team, trimSpecial, User } from "@fosscord/util";
+import { Application, OrmUtils, DiscordApiErrors, ApplicationModifySchema, User } from "@fosscord/util";
+import { verifyToken } from "node-2fa";
+import { HTTPError } from "lambert-server";
const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
- let results = await Application.findOne({where: {id: req.params.id}, relations: ["owner", "bot"] });
- res.json(results).status(200);
+ const app = await Application.findOneOrFail({ where: { id: req.params.id }, relations: ["owner", "bot"] });
+ if (app.owner.id != req.user_id)
+ throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
+
+ return res.json(app);
});
-router.patch("/", route({}), async (req: Request, res: Response) => {
- delete req.body.icon;
- let app = OrmUtils.mergeDeep(await Application.findOne({where: {id: req.params.id}, relations: ["owner", "bot"]}), req.body);
- if(app.bot) {
- app.bot.bio = req.body.description
- app.bot?.save();
+router.patch("/", route({ body: "ApplicationModifySchema" }), async (req: Request, res: Response) => {
+ const body = req.body as ApplicationModifySchema;
+
+ const app = await Application.findOneOrFail({ where: { id: req.params.id }, relations: ["owner", "bot"] });
+
+ if (app.owner.id != req.user_id)
+ throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
+
+ if (app.owner.totp_secret && (!req.body.code || verifyToken(app.owner.totp_secret, req.body.code)))
+ throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+
+ if (app.bot) {
+ app.bot.assign({ bio: body.description });
+ await app.bot.save();
}
- if(req.body.tags) app.tags = req.body.tags;
+
+ app.assign(body);
+
await app.save();
- res.json(app).status(200);
+
+ return res.json(app);
});
router.post("/delete", route({}), async (req: Request, res: Response) => {
- await Application.delete(req.params.id);
+ const app = await Application.findOneOrFail({ where: { id: req.params.id }, relations: ["bot", "owner"] });
+ if (app.owner.id != req.user_id)
+ throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
+
+ if (app.owner.totp_secret && (!req.body.code || verifyToken(app.owner.totp_secret, req.body.code)))
+ throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+
+ if (app.bot)
+ await User.delete({ id: app.bot.id });
+
+ await Application.delete({ id: app.id });
+
res.send().status(200);
});
diff --git a/src/api/routes/applications/index.ts b/src/api/routes/applications/index.ts
index 41ce35b5..70af84e6 100644
--- a/src/api/routes/applications/index.ts
+++ b/src/api/routes/applications/index.ts
@@ -1,35 +1,31 @@
import { Request, Response, Router } from "express";
import { route } from "@fosscord/api";
-import { Application, OrmUtils, Team, trimSpecial, User } from "@fosscord/util";
+import { Application, ApplicationCreateSchema, trimSpecial, User } from "@fosscord/util";
const router: Router = Router();
-export interface ApplicationCreateSchema {
- name: string;
- team_id?: string | number;
-}
-
router.get("/", route({}), async (req: Request, res: Response) => {
- //TODO
- let results = await Application.find({where: {owner: {id: req.user_id}}, relations: ["owner", "bot"] });
+ let results = await Application.find({ where: { owner: { id: req.user_id } }, relations: ["owner", "bot"] });
res.json(results).status(200);
});
-router.post("/", route({}), async (req: Request, res: Response) => {
+router.post("/", route({ body: "ApplicationCreateSchema" }), async (req: Request, res: Response) => {
const body = req.body as ApplicationCreateSchema;
- const user = await User.findOne({where: {id: req.user_id}})
- if(!user) res.status(420);
- let app = OrmUtils.mergeDeep(new Application(), {
+ const user = await User.findOneOrFail({ where: { id: req.user_id } });
+
+ const app = Application.create({
name: trimSpecial(body.name),
description: "",
bot_public: true,
bot_require_code_grant: false,
owner: user,
verify_key: "IMPLEMENTME",
- flags: ""
+ flags: 0,
});
+
await app.save();
- res.json(app).status(200);
+
+ res.json(app);
});
export default router;
\ No newline at end of file
diff --git a/src/util/entities/Application.ts b/src/util/entities/Application.ts
index 28381579..861c5bdd 100644
--- a/src/util/entities/Application.ts
+++ b/src/util/entities/Application.ts
@@ -37,6 +37,7 @@ export class Application extends BaseClass {
@ManyToOne(() => User)
owner: User;
+ // TODO: enum this? https://discord.com/developers/docs/resources/application#application-object-application-flags
@Column()
flags: number = 0;
diff --git a/src/util/schemas/ApplicationCreateSchema.ts b/src/util/schemas/ApplicationCreateSchema.ts
new file mode 100644
index 00000000..6a021b46
--- /dev/null
+++ b/src/util/schemas/ApplicationCreateSchema.ts
@@ -0,0 +1,4 @@
+export interface ApplicationCreateSchema {
+ name: string;
+ team_id?: string | number;
+}
\ No newline at end of file
diff --git a/src/util/schemas/ApplicationModifySchema.ts b/src/util/schemas/ApplicationModifySchema.ts
new file mode 100644
index 00000000..ab23d57e
--- /dev/null
+++ b/src/util/schemas/ApplicationModifySchema.ts
@@ -0,0 +1,14 @@
+export interface ApplicationModifySchema {
+ description?: string;
+ icon?: string;
+ interactions_endpoint_url?: string;
+ max_participants?: number | null;
+ name?: string;
+ privacy_policy_url?: string;
+ role_connections_verification_url?: string;
+ tags?: string[];
+ terms_of_service_url?: string;
+ bot_public?: boolean;
+ bot_require_code_grant?: boolean;
+ flags?: number;
+}
\ No newline at end of file
diff --git a/src/util/schemas/BotModifySchema.ts b/src/util/schemas/BotModifySchema.ts
new file mode 100644
index 00000000..b801ab27
--- /dev/null
+++ b/src/util/schemas/BotModifySchema.ts
@@ -0,0 +1,4 @@
+export interface BotModifySchema {
+ avatar?: string;
+ username?: string;
+}
\ No newline at end of file
diff --git a/src/util/schemas/Validator.ts b/src/util/schemas/Validator.ts
index e85cdf7b..9b7f0eca 100644
--- a/src/util/schemas/Validator.ts
+++ b/src/util/schemas/Validator.ts
@@ -22,6 +22,7 @@ export const ajv = new Ajv({
messages: true,
strict: true,
strictRequired: true,
+ allowUnionTypes: true,
});
addFormats(ajv);
diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts
index 780022c6..58565496 100644
--- a/src/util/schemas/index.ts
+++ b/src/util/schemas/index.ts
@@ -45,4 +45,7 @@ export * from "./UserGuildSettingsSchema";
export * from "./GatewayPayloadSchema";
export * from "./RolePositionUpdateSchema";
export * from "./ChannelReorderSchema";
-export * from "./UserSettingsSchema";
\ No newline at end of file
+export * from "./UserSettingsSchema";
+export * from "./BotModifySchema";
+export * from "./ApplicationModifySchema";
+export * from "./ApplicationCreateSchema";
\ No newline at end of file