From c03812bae78ae4c63d5b349e6f143ae370212777 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:46:22 +0000 Subject: [PATCH] feat(consents): implement comprehensive consents API with GDPR and HB 805 support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ConsentType enum (MEDIA_ITEM, MESSAGE_ITEM, EXTERNAL_DATA_PROCESSING, BOTS_AI, CUSTOM) - Add ConsentStatus enum (PENDING, GRANTED, PROVISIONAL, RETRACTED) - Extend UserConsent entity with target_user_id for HB 805 pairwise consents (O(n²)) - Add basis_document_url/hash fields for off-platform consent references - Create ConsentGrant entity for GNAP-like grant negotiation (RFC 9635) - Add consent-grants routes for interactive consent negotiation flow - Update consent routes to support item_id and target_user_id filtering - Add UNKNOWN_CONSENT and UNKNOWN_CONSENT_GRANT errors to Constants - Add PostgreSQL migration for new consent fields and consent_grants table Co-authored-by: Erkin Alp Güney --- .../users/#id/consents/#service_id/index.ts | 35 +- src/api/routes/users/#id/consents/index.ts | 70 +- .../routes/users/@me/consent-grants/index.ts | 335 +++++ .../users/@me/consents/#service_id/index.ts | 213 ++- src/api/routes/users/@me/consents/index.ts | 104 +- src/util/entities/ConsentGrant.ts | 130 ++ src/util/entities/UserConsent.ts | 114 +- src/util/entities/index.ts | 1 + .../postgres/1770139822000-ConsentsAPI.ts | 142 ++ src/util/util/Constants.ts | 1152 ++++++----------- 10 files changed, 1406 insertions(+), 890 deletions(-) create mode 100644 src/api/routes/users/@me/consent-grants/index.ts create mode 100644 src/util/entities/ConsentGrant.ts create mode 100644 src/util/migration/postgres/1770139822000-ConsentsAPI.ts diff --git a/src/api/routes/users/#id/consents/#service_id/index.ts b/src/api/routes/users/#id/consents/#service_id/index.ts index 5f37da6e2..22be76941 100644 --- a/src/api/routes/users/#id/consents/#service_id/index.ts +++ b/src/api/routes/users/#id/consents/#service_id/index.ts @@ -5,24 +5,23 @@ import { UserConsent } from "@spacebar/util"; const router: Router = Router(); router.delete( - "/", - route({ - right: "MANAGE_USERS", - summary: - "Revoke consent for a service for the specified user (admin only)", - responses: { 204: { body: "null" } }, - }), - async (req: Request, res: Response) => { - const user_id = req.params.id; - const service_id = req.params.service_id; - const existing = await UserConsent.findOne({ - where: { user_id, service_id }, - }); - if (existing) { - await existing.remove(); - } - return res.status(204).send(); - }, + "/", + route({ + right: "MANAGE_USERS", + summary: "Revoke consent for a service for the specified user (admin only)", + responses: { 204: { body: "null" } }, + }), + async (req: Request, res: Response) => { + const user_id = req.params.id; + const service_id = req.params.service_id; + const existing = await UserConsent.findOne({ + where: { user_id, service_id }, + }); + if (existing) { + await existing.remove(); + } + return res.status(204).send(); + }, ); export default router; diff --git a/src/api/routes/users/#id/consents/index.ts b/src/api/routes/users/#id/consents/index.ts index a36047746..2a5fd1d14 100644 --- a/src/api/routes/users/#id/consents/index.ts +++ b/src/api/routes/users/#id/consents/index.ts @@ -5,44 +5,44 @@ import { UserConsent } from "@spacebar/util"; const router: Router = Router(); router.get( - "/", - route({ - right: "MANAGE_USERS", - summary: "List consents for a specified user (admin only)", - responses: { - 200: { body: "any" }, - 403: { body: "APIErrorResponse" }, - }, - }), - async (req: Request, res: Response) => { - const target_user_id = req.params.id; - const consents = await UserConsent.find({ - where: { user_id: target_user_id }, - }); - res.json( - consents.map((c) => ({ - service_id: c.service_id, - consented_at: c.created_at, - })), - ); - }, + "/", + route({ + right: "MANAGE_USERS", + summary: "List consents for a specified user (admin only)", + responses: { + 200: { body: "any" }, + 403: { body: "APIErrorResponse" }, + }, + }), + async (req: Request, res: Response) => { + const target_user_id = req.params.id; + const consents = await UserConsent.find({ + where: { user_id: target_user_id }, + }); + res.json( + consents.map((c) => ({ + service_id: c.service_id, + consented_at: c.created_at, + })), + ); + }, ); router.delete( - "/", - route({ - right: "OPERATOR", - summary: "Revoke all consents for a specified user (operator only)", - responses: { - 204: { body: "null" }, - 403: { body: "APIErrorResponse" }, - }, - }), - async (req: Request, res: Response) => { - const target_user_id = req.params.id; - await UserConsent.delete({ user_id: target_user_id }); - return res.status(204).send(); - }, + "/", + route({ + right: "OPERATOR", + summary: "Revoke all consents for a specified user (operator only)", + responses: { + 204: { body: "null" }, + 403: { body: "APIErrorResponse" }, + }, + }), + async (req: Request, res: Response) => { + const target_user_id = req.params.id; + await UserConsent.delete({ user_id: target_user_id }); + return res.status(204).send(); + }, ); export default router; diff --git a/src/api/routes/users/@me/consent-grants/index.ts b/src/api/routes/users/@me/consent-grants/index.ts new file mode 100644 index 000000000..1c8339d4e --- /dev/null +++ b/src/api/routes/users/@me/consent-grants/index.ts @@ -0,0 +1,335 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; +import { ConsentGrant, ConsentGrantStatus, ConsentType, UserConsent, ConsentStatus, DiscordApiErrors, User } from "@spacebar/util"; +import { FindOptionsWhere } from "typeorm"; + +const router: Router = Router(); + +router.get( + "/", + route({ + summary: "List consent grant requests", + description: "Returns all consent grant requests for the authenticated user (both as grantor and requester). Based on GNAP (RFC 9635) grant negotiation principles.", + query: { + status: { + type: "string", + required: false, + description: "Filter by grant status", + values: Object.values(ConsentGrantStatus), + }, + consent_type: { + type: "string", + required: false, + description: "Filter by consent type", + values: Object.values(ConsentType), + }, + as_requester: { + type: "boolean", + required: false, + description: "If true, list grants where user is the requester; if false, list grants where user is the grantor", + }, + }, + responses: { + 200: { body: "ConsentGrantListResponse" }, + }, + }), + async (req: Request, res: Response) => { + const user_id = req.user_id!; + const { status, consent_type, as_requester } = req.query; + + const where: FindOptionsWhere = {}; + + if (as_requester === "true") { + where.requester_id = user_id; + } else { + where.user_id = user_id; + } + + if (status) { + where.status = status as ConsentGrantStatus; + } + if (consent_type) { + where.consent_type = consent_type as ConsentType; + } + + const grants = await ConsentGrant.find({ where }); + + res.json({ + grants: grants.map((g) => g.toJSON()), + }); + }, +); + +router.post( + "/", + route({ + summary: "Request a consent grant (GNAP-like)", + description: + "Initiates a consent grant request to another user. Based on GNAP (RFC 9635) grant negotiation. The target user will receive the request and can approve, deny, or negotiate.", + requestBody: "ConsentGrantRequestSchema", + responses: { + 200: { body: "ConsentGrantResponse" }, + 404: { body: "APIErrorResponse" }, + }, + }), + async (req: Request, res: Response) => { + const requester_id = req.user_id!; + const { user_id, consent_type = ConsentType.CUSTOM, service_id, item_id, requested_access, interaction_url, expires_at, extra_data } = req.body; + + const targetUser = await User.findOne({ where: { id: user_id } }); + if (!targetUser) { + throw DiscordApiErrors.UNKNOWN_USER; + } + + const grant = ConsentGrant.create({ + user_id, + requester_id, + consent_type, + service_id, + item_id, + status: ConsentGrantStatus.PENDING, + requested_access, + interaction_url, + expires_at: expires_at ? new Date(expires_at) : undefined, + extra_data, + }); + + grant.generateContinueToken(); + await grant.save(); + + res.json(grant.toJSON()); + }, +); + +router.get( + "/:grant_id", + route({ + summary: "Get consent grant details", + description: "Returns details of a specific consent grant request. Only accessible by the grantor or requester.", + responses: { + 200: { body: "ConsentGrantResponse" }, + 404: { body: "APIErrorResponse" }, + }, + }), + async (req: Request, res: Response) => { + const user_id = req.user_id!; + const grant_id = req.params.grant_id; + + const grant = await ConsentGrant.findOne({ + where: [ + { id: grant_id, user_id }, + { id: grant_id, requester_id: user_id }, + ], + }); + + if (!grant) { + throw DiscordApiErrors.UNKNOWN_CONSENT_GRANT; + } + + res.json(grant.toJSON()); + }, +); + +router.post( + "/:grant_id/approve", + route({ + summary: "Approve a consent grant request", + description: "Approves a pending consent grant request. Creates the corresponding UserConsent record. Only the target user (grantor) can approve.", + requestBody: "ConsentGrantApproveSchema", + responses: { + 200: { body: "ConsentGrantResponse" }, + 404: { body: "APIErrorResponse" }, + }, + }), + async (req: Request, res: Response) => { + const user_id = req.user_id!; + const grant_id = req.params.grant_id; + const { granted_access, provisional, expires_at, extra_data } = req.body; + + const grant = await ConsentGrant.findOne({ + where: { id: grant_id, user_id, status: ConsentGrantStatus.PENDING }, + }); + + if (!grant) { + throw DiscordApiErrors.UNKNOWN_CONSENT_GRANT; + } + + grant.status = ConsentGrantStatus.APPROVED; + grant.responded_at = new Date(); + grant.granted_access = granted_access || grant.requested_access; + grant.generateAccessToken(); + + if (extra_data) { + grant.extra_data = { ...grant.extra_data, ...extra_data }; + } + + await grant.save(); + + const consent = UserConsent.create({ + user_id, + service_id: grant.service_id || "gnap_grant", + consent_type: grant.consent_type, + item_id: grant.item_id, + target_user_id: grant.requester_id, + status: provisional ? ConsentStatus.PROVISIONAL : ConsentStatus.GRANTED, + granted_at: new Date(), + expires_at: expires_at ? new Date(expires_at) : grant.expires_at || undefined, + extra_data: { + grant_id: grant.id, + granted_access: grant.granted_access, + }, + }); + await consent.save(); + + res.json(grant.toJSON()); + }, +); + +router.post( + "/:grant_id/deny", + route({ + summary: "Deny a consent grant request", + description: "Denies a pending consent grant request. Only the target user (grantor) can deny.", + responses: { + 200: { body: "ConsentGrantResponse" }, + 404: { body: "APIErrorResponse" }, + }, + }), + async (req: Request, res: Response) => { + const user_id = req.user_id!; + const grant_id = req.params.grant_id; + + const grant = await ConsentGrant.findOne({ + where: { id: grant_id, user_id, status: ConsentGrantStatus.PENDING }, + }); + + if (!grant) { + throw DiscordApiErrors.UNKNOWN_CONSENT_GRANT; + } + + grant.status = ConsentGrantStatus.DENIED; + grant.responded_at = new Date(); + await grant.save(); + + res.json(grant.toJSON()); + }, +); + +router.post( + "/:grant_id/continue", + route({ + summary: "Continue grant negotiation (GNAP-like)", + description: "Continues the grant negotiation process using the continuation token. Used for multi-step negotiation flows per GNAP (RFC 9635).", + requestBody: "ConsentGrantContinueSchema", + responses: { + 200: { body: "ConsentGrantResponse" }, + 404: { body: "APIErrorResponse" }, + }, + }), + async (req: Request, res: Response) => { + const user_id = req.user_id!; + const grant_id = req.params.grant_id; + const { continue_token, updated_access, interaction_ref } = req.body; + + const grant = await ConsentGrant.findOne({ + where: [ + { id: grant_id, user_id }, + { id: grant_id, requester_id: user_id }, + ], + }); + + if (!grant) { + throw DiscordApiErrors.UNKNOWN_CONSENT_GRANT; + } + + if (grant.continue_token !== continue_token) { + throw DiscordApiErrors.UNKNOWN_CONSENT_GRANT; + } + + if (updated_access) { + grant.requested_access = updated_access; + } + + if (interaction_ref) { + grant.extra_data = { ...grant.extra_data, interaction_ref }; + } + + grant.generateContinueToken(); + await grant.save(); + + res.json(grant.toJSON()); + }, +); + +router.delete( + "/:grant_id", + route({ + summary: "Cancel/revoke a consent grant", + description: "Cancels a pending grant request (if requester) or revokes an approved grant (if grantor). Per GDPR Article 7(3), revocation is as easy as granting.", + responses: { + 204: { body: "null" }, + 404: { body: "APIErrorResponse" }, + }, + }), + async (req: Request, res: Response) => { + const user_id = req.user_id!; + const grant_id = req.params.grant_id; + + const grant = await ConsentGrant.findOne({ + where: [ + { id: grant_id, user_id }, + { id: grant_id, requester_id: user_id }, + ], + }); + + if (!grant) { + throw DiscordApiErrors.UNKNOWN_CONSENT_GRANT; + } + + if (grant.user_id === user_id) { + if (grant.status === ConsentGrantStatus.APPROVED) { + const consent = await UserConsent.findOne({ + where: { + user_id, + target_user_id: grant.requester_id, + consent_type: grant.consent_type, + item_id: grant.item_id, + }, + }); + if (consent) { + consent.status = ConsentStatus.RETRACTED; + consent.retracted_at = new Date(); + await consent.save(); + } + } + grant.status = ConsentGrantStatus.DENIED; + } else { + grant.status = ConsentGrantStatus.EXPIRED; + } + + grant.responded_at = new Date(); + await grant.save(); + + res.status(204).send(); + }, +); + +export default router; diff --git a/src/api/routes/users/@me/consents/#service_id/index.ts b/src/api/routes/users/@me/consents/#service_id/index.ts index f2937b4dc..b1f04ceb2 100644 --- a/src/api/routes/users/@me/consents/#service_id/index.ts +++ b/src/api/routes/users/@me/consents/#service_id/index.ts @@ -1,46 +1,191 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + import { route } from "@spacebar/api"; import { Request, Response, Router } from "express"; -import { UserConsent } from "@spacebar/util"; +import { UserConsent, ConsentType, ConsentStatus, DiscordApiErrors } from "@spacebar/util"; const router: Router = Router(); +router.get( + "/", + route({ + summary: "Get consent details for a specific service", + description: + "Returns the consent record for a specific service for the authenticated user. For HB 805 pairwise consents, use item_id and target_user_id to identify specific consent records.", + query: { + consent_type: { + type: "string", + required: false, + description: "Filter by consent type", + values: Object.values(ConsentType), + }, + item_id: { + type: "string", + required: false, + description: "Filter by item ID (for per-item consents)", + }, + target_user_id: { + type: "string", + required: false, + description: "Filter by target user ID (for HB 805 pairwise consents)", + }, + }, + responses: { + 200: { body: "UserConsentResponse" }, + 404: { body: "APIErrorResponse" }, + }, + }), + async (req: Request, res: Response) => { + const user_id = req.user_id!; + const service_id = req.params.service_id; + const consent_type = (req.query.consent_type as ConsentType) || ConsentType.CUSTOM; + const item_id = req.query.item_id as string | undefined; + const target_user_id = req.query.target_user_id as string | undefined; + + const consent = await UserConsent.findOne({ + where: { + user_id, + service_id, + consent_type, + ...(item_id && { item_id }), + ...(target_user_id && { target_user_id }), + }, + }); + + if (!consent) { + throw DiscordApiErrors.UNKNOWN_CONSENT; + } + + res.json(consent.toJSON()); + }, +); + router.put( - "/", - route({ - summary: "Grant consent for a service for the current user", - responses: { 204: { body: "null" } }, - }), - async (req: Request, res: Response) => { - const user_id = req.user_id!; - const service_id = req.params.service_id; - const existing = await UserConsent.findOne({ - where: { user_id, service_id }, - }); - if (!existing) { - const uc = UserConsent.create({ user_id, service_id }); - await uc.save(); - } - return res.status(204).send(); - }, + "/", + route({ + summary: "Grant consent for a service", + description: + "Grants consent for a service. Supports GDPR-compliant consent with basis documents for off-platform consents. For HB 805 pairwise consents (O(n²) where n is number of persons), use target_user_id to specify who receives consent to view/share. Based on GNAP (RFC 9635) grant negotiation principles.", + requestBody: "UserConsentGrantSchema", + responses: { + 200: { body: "UserConsentResponse" }, + 204: { body: "null" }, + }, + }), + async (req: Request, res: Response) => { + const user_id = req.user_id!; + const service_id = req.params.service_id; + const { consent_type = ConsentType.CUSTOM, item_id, target_user_id, basis_document_url, basis_document_hash, expires_at, provisional, extra_data } = req.body; + + const existing = await UserConsent.findOne({ + where: { + user_id, + service_id, + consent_type, + ...(item_id && { item_id }), + ...(target_user_id && { target_user_id }), + }, + }); + + if (existing) { + existing.status = provisional ? ConsentStatus.PROVISIONAL : ConsentStatus.GRANTED; + existing.granted_at = new Date(); + existing.retracted_at = undefined; + existing.basis_document_url = basis_document_url; + existing.basis_document_hash = basis_document_hash; + existing.expires_at = expires_at ? new Date(expires_at) : undefined; + existing.item_id = item_id; + existing.target_user_id = target_user_id; + existing.extra_data = extra_data; + await existing.save(); + return res.json(existing.toJSON()); + } + + const consent = UserConsent.create({ + user_id, + service_id, + consent_type, + item_id, + target_user_id, + status: provisional ? ConsentStatus.PROVISIONAL : ConsentStatus.GRANTED, + basis_document_url, + basis_document_hash, + granted_at: new Date(), + expires_at: expires_at ? new Date(expires_at) : undefined, + extra_data, + }); + await consent.save(); + return res.json(consent.toJSON()); + }, ); router.delete( - "/", - route({ - summary: "Revoke consent for a service for the current user", - responses: { 204: { body: "null" } }, - }), - async (req: Request, res: Response) => { - const user_id = req.user_id!; - const service_id = req.params.service_id; - const existing = await UserConsent.findOne({ - where: { user_id, service_id }, - }); - if (existing) { - await existing.remove(); - } - return res.status(204).send(); - }, + "/", + route({ + summary: "Revoke consent for a service (GDPR-compliant)", + description: + "Revokes consent for a service. Per GDPR Article 7(3), withdrawal of consent is as easy as giving consent. For HB 805 pairwise consents, specify item_id and target_user_id. The consent record is retained with RETRACTED status for audit purposes.", + query: { + consent_type: { + type: "string", + required: false, + description: "Consent type to revoke", + values: Object.values(ConsentType), + }, + item_id: { + type: "string", + required: false, + description: "Item ID for per-item consent revocation", + }, + target_user_id: { + type: "string", + required: false, + description: "Target user ID for HB 805 pairwise consent revocation", + }, + }, + responses: { 204: { body: "null" } }, + }), + async (req: Request, res: Response) => { + const user_id = req.user_id!; + const service_id = req.params.service_id; + const consent_type = (req.query.consent_type as ConsentType) || ConsentType.CUSTOM; + const item_id = req.query.item_id as string | undefined; + const target_user_id = req.query.target_user_id as string | undefined; + + const existing = await UserConsent.findOne({ + where: { + user_id, + service_id, + consent_type, + ...(item_id && { item_id }), + ...(target_user_id && { target_user_id }), + }, + }); + + if (existing) { + existing.status = ConsentStatus.RETRACTED; + existing.retracted_at = new Date(); + await existing.save(); + } + + return res.status(204).send(); + }, ); export default router; diff --git a/src/api/routes/users/@me/consents/index.ts b/src/api/routes/users/@me/consents/index.ts index 76a5617d3..e6c303296 100644 --- a/src/api/routes/users/@me/consents/index.ts +++ b/src/api/routes/users/@me/consents/index.ts @@ -1,27 +1,95 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + import { route } from "@spacebar/api"; import { Request, Response, Router } from "express"; -import { UserConsent } from "@spacebar/util"; +import { UserConsent, ConsentType, ConsentStatus } from "@spacebar/util"; +import { FindOptionsWhere } from "typeorm"; const router: Router = Router(); router.get( - "/", - route({ - summary: "List consents for the current user", - responses: { - 200: { body: "any" }, - }, - }), - async (req: Request, res: Response) => { - const user_id = req.user_id!; - const consents = await UserConsent.find({ where: { user_id } }); - res.json( - consents.map((c) => ({ - service_id: c.service_id, - consented_at: c.created_at, - })), - ); - }, + "/", + route({ + summary: "List consents for the current user", + description: + "Returns all consents for the authenticated user. Supports filtering by consent_type, status, service_id, item_id, and target_user_id. For HB 805 pairwise consents (O(n²) per item), use item_id and target_user_id filters.", + query: { + consent_type: { + type: "string", + required: false, + description: "Filter by consent type", + values: Object.values(ConsentType), + }, + status: { + type: "string", + required: false, + description: "Filter by consent status", + values: Object.values(ConsentStatus), + }, + service_id: { + type: "string", + required: false, + description: "Filter by service ID", + }, + item_id: { + type: "string", + required: false, + description: "Filter by item ID (for per-item consents)", + }, + target_user_id: { + type: "string", + required: false, + description: "Filter by target user ID (for HB 805 pairwise consents)", + }, + }, + responses: { + 200: { body: "UserConsentListResponse" }, + }, + }), + async (req: Request, res: Response) => { + const user_id = req.user_id!; + const { consent_type, status, service_id, item_id, target_user_id } = req.query; + + const where: FindOptionsWhere = { user_id }; + + if (consent_type) { + where.consent_type = consent_type as ConsentType; + } + if (status) { + where.status = status as ConsentStatus; + } + if (service_id) { + where.service_id = service_id as string; + } + if (item_id) { + where.item_id = item_id as string; + } + if (target_user_id) { + where.target_user_id = target_user_id as string; + } + + const consents = await UserConsent.find({ where }); + + res.json({ + consents: consents.map((c) => c.toJSON()), + }); + }, ); export default router; diff --git a/src/util/entities/ConsentGrant.ts b/src/util/entities/ConsentGrant.ts new file mode 100644 index 000000000..6031c19c9 --- /dev/null +++ b/src/util/entities/ConsentGrant.ts @@ -0,0 +1,130 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { User } from "./User"; +import { ConsentType } from "./UserConsent"; +import { Snowflake } from "../util/Snowflake"; + +export enum ConsentGrantStatus { + PENDING = "pending", + APPROVED = "approved", + DENIED = "denied", + EXPIRED = "expired", +} + +@Entity({ name: "consent_grants" }) +@Index(["user_id", "requester_id", "service_id", "consent_type"]) +export class ConsentGrant extends BaseClass { + @Column() + @Index() + user_id: string; + + @Column() + @Index() + requester_id: string; + + @Column({ type: "varchar", default: ConsentType.CUSTOM }) + consent_type: ConsentType; + + @Column({ nullable: true }) + service_id?: string; + + @Column({ nullable: true }) + item_id?: string; + + @Column({ type: "varchar", default: ConsentGrantStatus.PENDING }) + status: ConsentGrantStatus; + + @Column() + requested_at: Date = new Date(); + + @Column({ nullable: true }) + responded_at?: Date; + + @Column({ nullable: true }) + expires_at?: Date; + + @Column({ nullable: true }) + interaction_url?: string; + + @Column({ nullable: true }) + continue_token?: string; + + @Column({ nullable: true }) + access_token?: string; + + @Column({ type: "simple-json", nullable: true }) + requested_access?: { + type: string; + actions?: string[]; + locations?: string[]; + datatypes?: string[]; + }[]; + + @Column({ type: "simple-json", nullable: true }) + granted_access?: { + type: string; + actions?: string[]; + locations?: string[]; + datatypes?: string[]; + }[]; + + @Column({ type: "simple-json", nullable: true }) + extra_data?: Record; + + @ManyToOne(() => User, { onDelete: "CASCADE" }) + @JoinColumn({ name: "user_id" }) + user?: User; + + @ManyToOne(() => User, { onDelete: "CASCADE" }) + @JoinColumn({ name: "requester_id" }) + requester?: User; + + generateContinueToken(): string { + this.continue_token = Snowflake.generate(); + return this.continue_token; + } + + generateAccessToken(): string { + this.access_token = Snowflake.generate(); + return this.access_token; + } + + toJSON() { + return { + id: this.id, + user_id: this.user_id, + requester_id: this.requester_id, + consent_type: this.consent_type, + service_id: this.service_id, + item_id: this.item_id, + status: this.status, + requested_at: this.requested_at, + responded_at: this.responded_at, + expires_at: this.expires_at, + interaction_url: this.interaction_url, + continue_token: this.continue_token, + access_token: this.access_token, + requested_access: this.requested_access, + granted_access: this.granted_access, + extra_data: this.extra_data, + }; + } +} diff --git a/src/util/entities/UserConsent.ts b/src/util/entities/UserConsent.ts index cbf4197cc..679ee8ed1 100644 --- a/src/util/entities/UserConsent.ts +++ b/src/util/entities/UserConsent.ts @@ -1,20 +1,110 @@ -import { Column, Entity, Index, ManyToOne } from "typeorm"; +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm"; import { BaseClass } from "./BaseClass"; import { User } from "./User"; -import { dbEngine } from "../util/Database"; -@Entity({ name: "user_consents", engine: dbEngine }) -@Index(["user_id", "service_id"], { unique: true }) +export enum ConsentType { + MEDIA_ITEM = "media_item", + MESSAGE_ITEM = "message_item", + EXTERNAL_DATA_PROCESSING = "external_data_processing", + BOTS_AI = "bots_ai", + CUSTOM = "custom", +} + +export enum ConsentStatus { + PENDING = "pending", + GRANTED = "granted", + PROVISIONAL = "provisional", + RETRACTED = "retracted", +} + +@Entity({ name: "user_consents" }) +@Index(["user_id", "service_id", "consent_type", "item_id", "target_user_id"], { + unique: true, +}) export class UserConsent extends BaseClass { - @Column() - user_id: string; + @Column() + @Index() + user_id: string; + + @Column() + service_id: string; + + @Column({ type: "varchar", default: ConsentType.CUSTOM }) + consent_type: ConsentType; + + @Column({ nullable: true }) + @Index() + item_id?: string; + + @Column({ nullable: true }) + @Index() + target_user_id?: string; + + @Column({ type: "varchar", default: ConsentStatus.GRANTED }) + status: ConsentStatus; + + @Column({ nullable: true }) + basis_document_url?: string; + + @Column({ nullable: true }) + basis_document_hash?: string; + + @Column({ nullable: true }) + granted_at?: Date; + + @Column({ nullable: true }) + retracted_at?: Date; + + @Column({ nullable: true }) + expires_at?: Date; + + @Column({ type: "simple-json", nullable: true }) + extra_data?: Record; + + @Column() + created_at: Date = new Date(); - @Column() - service_id: string; + @ManyToOne(() => User, (user) => user.id, { onDelete: "CASCADE" }) + @JoinColumn({ name: "user_id" }) + user?: User; - @Column() - created_at: Date = new Date(); + @ManyToOne(() => User, { onDelete: "CASCADE" }) + @JoinColumn({ name: "target_user_id" }) + target_user?: User; - @ManyToOne(() => User, (user) => user.id, { onDelete: "CASCADE" }) - user?: User; + toJSON() { + return { + id: this.id, + service_id: this.service_id, + consent_type: this.consent_type, + item_id: this.item_id, + target_user_id: this.target_user_id, + status: this.status, + basis_document_url: this.basis_document_url, + basis_document_hash: this.basis_document_hash, + granted_at: this.granted_at, + retracted_at: this.retracted_at, + expires_at: this.expires_at, + extra_data: this.extra_data, + created_at: this.created_at, + }; + } } diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts index 63e20a3a8..1ef24525a 100644 --- a/src/util/entities/index.ts +++ b/src/util/entities/index.ts @@ -63,3 +63,4 @@ export * from "./ValidRegistrationTokens"; export * from "./VoiceState"; export * from "./Webhook"; export * from "./UserConsent"; +export * from "./ConsentGrant"; diff --git a/src/util/migration/postgres/1770139822000-ConsentsAPI.ts b/src/util/migration/postgres/1770139822000-ConsentsAPI.ts new file mode 100644 index 000000000..cd1e2639a --- /dev/null +++ b/src/util/migration/postgres/1770139822000-ConsentsAPI.ts @@ -0,0 +1,142 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class ConsentsAPI1770139822000 implements MigrationInterface { + name = "ConsentsAPI1770139822000"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "user_consents" + ADD COLUMN IF NOT EXISTS "consent_type" character varying DEFAULT 'custom', + ADD COLUMN IF NOT EXISTS "item_id" character varying, + ADD COLUMN IF NOT EXISTS "target_user_id" character varying, + ADD COLUMN IF NOT EXISTS "status" character varying DEFAULT 'granted', + ADD COLUMN IF NOT EXISTS "basis_document_url" character varying, + ADD COLUMN IF NOT EXISTS "basis_document_hash" character varying, + ADD COLUMN IF NOT EXISTS "granted_at" timestamp, + ADD COLUMN IF NOT EXISTS "retracted_at" timestamp, + ADD COLUMN IF NOT EXISTS "expires_at" timestamp, + ADD COLUMN IF NOT EXISTS "extra_data" text + `); + + await queryRunner.query(` + DROP INDEX IF EXISTS "IDX_user_consents_user_id_service_id" + `); + + await queryRunner.query(` + CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_consents_user_service_type_item_target" + ON "user_consents" ("user_id", "service_id", "consent_type", "item_id", "target_user_id") + `); + + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "IDX_user_consents_item_id" ON "user_consents" ("item_id") + `); + + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "IDX_user_consents_target_user_id" ON "user_consents" ("target_user_id") + `); + + await queryRunner.query(` + ALTER TABLE "user_consents" + ADD CONSTRAINT "FK_user_consents_target_user_id" + FOREIGN KEY ("target_user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION + `); + + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "consent_grants" ( + "id" character varying NOT NULL, + "user_id" character varying NOT NULL, + "requester_id" character varying NOT NULL, + "consent_type" character varying DEFAULT 'custom', + "service_id" character varying, + "item_id" character varying, + "status" character varying DEFAULT 'pending', + "requested_at" timestamp NOT NULL DEFAULT now(), + "responded_at" timestamp, + "expires_at" timestamp, + "interaction_url" character varying, + "continue_token" character varying, + "access_token" character varying, + "requested_access" text, + "granted_access" text, + "extra_data" text, + CONSTRAINT "PK_consent_grants" PRIMARY KEY ("id") + ) + `); + + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "IDX_consent_grants_user_id" ON "consent_grants" ("user_id") + `); + + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "IDX_consent_grants_requester_id" ON "consent_grants" ("requester_id") + `); + + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "IDX_consent_grants_user_requester_service_type" + ON "consent_grants" ("user_id", "requester_id", "service_id", "consent_type") + `); + + await queryRunner.query(` + ALTER TABLE "consent_grants" + ADD CONSTRAINT "FK_consent_grants_user_id" + FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION + `); + + await queryRunner.query(` + ALTER TABLE "consent_grants" + ADD CONSTRAINT "FK_consent_grants_requester_id" + FOREIGN KEY ("requester_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "consent_grants" DROP CONSTRAINT IF EXISTS "FK_consent_grants_requester_id"`); + await queryRunner.query(`ALTER TABLE "consent_grants" DROP CONSTRAINT IF EXISTS "FK_consent_grants_user_id"`); + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_consent_grants_user_requester_service_type"`); + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_consent_grants_requester_id"`); + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_consent_grants_user_id"`); + await queryRunner.query(`DROP TABLE IF EXISTS "consent_grants"`); + + await queryRunner.query(`ALTER TABLE "user_consents" DROP CONSTRAINT IF EXISTS "FK_user_consents_target_user_id"`); + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_user_consents_target_user_id"`); + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_user_consents_item_id"`); + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_user_consents_user_service_type_item_target"`); + + await queryRunner.query(` + ALTER TABLE "user_consents" + DROP COLUMN IF EXISTS "consent_type", + DROP COLUMN IF EXISTS "item_id", + DROP COLUMN IF EXISTS "target_user_id", + DROP COLUMN IF EXISTS "status", + DROP COLUMN IF EXISTS "basis_document_url", + DROP COLUMN IF EXISTS "basis_document_hash", + DROP COLUMN IF EXISTS "granted_at", + DROP COLUMN IF EXISTS "retracted_at", + DROP COLUMN IF EXISTS "expires_at", + DROP COLUMN IF EXISTS "extra_data" + `); + + await queryRunner.query(` + CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_consents_user_id_service_id" + ON "user_consents" ("user_id", "service_id") + `); + } +} diff --git a/src/util/util/Constants.ts b/src/util/util/Constants.ts index ae0df0ece..2b81c87a6 100644 --- a/src/util/util/Constants.ts +++ b/src/util/util/Constants.ts @@ -19,12 +19,12 @@ import { ApiError } from "./ApiError"; export const WSCodes = { - 1000: "WS_CLOSE_REQUESTED", - 4004: "TOKEN_INVALID", - 4010: "SHARDING_INVALID", - 4011: "SHARDING_REQUIRED", - 4013: "INVALID_INTENTS", - 4014: "DISALLOWED_INTENTS", + 1000: "WS_CLOSE_REQUESTED", + 4004: "TOKEN_INVALID", + 4010: "SHARDING_INVALID", + 4011: "SHARDING_REQUIRED", + 4013: "INVALID_INTENTS", + 4014: "DISALLOWED_INTENTS", }; /** @@ -41,102 +41,102 @@ export const WSCodes = { * @typedef {number} Status */ export const WsStatus = { - READY: 0, - CONNECTING: 1, - RECONNECTING: 2, - IDLE: 3, - NEARLY: 4, - DISCONNECTED: 5, - WAITING_FOR_GUILDS: 6, - IDENTIFYING: 7, - RESUMING: 8, + READY: 0, + CONNECTING: 1, + RECONNECTING: 2, + IDLE: 3, + NEARLY: 4, + DISCONNECTED: 5, + WAITING_FOR_GUILDS: 6, + IDENTIFYING: 7, + RESUMING: 8, }; export const OPCodes = { - DISPATCH: 0, - HEARTBEAT: 1, - IDENTIFY: 2, - STATUS_UPDATE: 3, - VOICE_STATE_UPDATE: 4, - VOICE_GUILD_PING: 5, - RESUME: 6, - RECONNECT: 7, - REQUEST_GUILD_MEMBERS: 8, - INVALID_SESSION: 9, - HELLO: 10, - HEARTBEAT_ACK: 11, + DISPATCH: 0, + HEARTBEAT: 1, + IDENTIFY: 2, + STATUS_UPDATE: 3, + VOICE_STATE_UPDATE: 4, + VOICE_GUILD_PING: 5, + RESUME: 6, + RECONNECT: 7, + REQUEST_GUILD_MEMBERS: 8, + INVALID_SESSION: 9, + HELLO: 10, + HEARTBEAT_ACK: 11, }; export const Events = { - RATE_LIMIT: "rateLimit", - CLIENT_READY: "ready", - GUILD_CREATE: "guildCreate", - GUILD_DELETE: "guildDelete", - GUILD_UPDATE: "guildUpdate", - GUILD_UNAVAILABLE: "guildUnavailable", - GUILD_AVAILABLE: "guildAvailable", - GUILD_MEMBER_ADD: "guildMemberAdd", - GUILD_MEMBER_REMOVE: "guildMemberRemove", - GUILD_MEMBER_UPDATE: "guildMemberUpdate", - GUILD_MEMBER_AVAILABLE: "guildMemberAvailable", - GUILD_MEMBER_SPEAKING: "guildMemberSpeaking", - GUILD_MEMBERS_CHUNK: "guildMembersChunk", - GUILD_INTEGRATIONS_UPDATE: "guildIntegrationsUpdate", - GUILD_ROLE_CREATE: "roleCreate", - GUILD_ROLE_DELETE: "roleDelete", - INVITE_CREATE: "inviteCreate", - INVITE_DELETE: "inviteDelete", - GUILD_ROLE_UPDATE: "roleUpdate", - GUILD_EMOJI_CREATE: "emojiCreate", - GUILD_EMOJI_DELETE: "emojiDelete", - GUILD_EMOJI_UPDATE: "emojiUpdate", - GUILD_BAN_ADD: "guildBanAdd", - GUILD_BAN_REMOVE: "guildBanRemove", - CHANNEL_CREATE: "channelCreate", - CHANNEL_DELETE: "channelDelete", - CHANNEL_UPDATE: "channelUpdate", - CHANNEL_PINS_UPDATE: "channelPinsUpdate", - MESSAGE_CREATE: "message", - MESSAGE_DELETE: "messageDelete", - MESSAGE_UPDATE: "messageUpdate", - MESSAGE_BULK_DELETE: "messageDeleteBulk", - MESSAGE_REACTION_ADD: "messageReactionAdd", - MESSAGE_REACTION_REMOVE: "messageReactionRemove", - MESSAGE_REACTION_REMOVE_ALL: "messageReactionRemoveAll", - MESSAGE_REACTION_REMOVE_EMOJI: "messageReactionRemoveEmoji", - USER_UPDATE: "userUpdate", - PRESENCE_UPDATE: "presenceUpdate", - VOICE_SERVER_UPDATE: "voiceServerUpdate", - VOICE_STATE_UPDATE: "voiceStateUpdate", - VOICE_BROADCAST_SUBSCRIBE: "subscribe", - VOICE_BROADCAST_UNSUBSCRIBE: "unsubscribe", - TYPING_START: "typingStart", - TYPING_STOP: "typingStop", - WEBHOOKS_UPDATE: "webhookUpdate", - LOBBY_CREATE: "lobbyCreate", - LOBBY_UPDATE: "lobbyUpdate", - LOBBY_DELETE: "lobbyDelete", - LOBBY_MEMBER_ADD: "lobbyMemberAdd", - LOBBY_MEMBER_REMOVE: "lobbyMemberRemove", - ERROR: "error", - WARN: "warn", - DEBUG: "debug", - SHARD_DISCONNECT: "shardDisconnect", - SHARD_ERROR: "shardError", - SHARD_RECONNECTING: "shardReconnecting", - SHARD_READY: "shardReady", - SHARD_RESUME: "shardResume", - INVALIDATED: "invalidated", - RAW: "raw", + RATE_LIMIT: "rateLimit", + CLIENT_READY: "ready", + GUILD_CREATE: "guildCreate", + GUILD_DELETE: "guildDelete", + GUILD_UPDATE: "guildUpdate", + GUILD_UNAVAILABLE: "guildUnavailable", + GUILD_AVAILABLE: "guildAvailable", + GUILD_MEMBER_ADD: "guildMemberAdd", + GUILD_MEMBER_REMOVE: "guildMemberRemove", + GUILD_MEMBER_UPDATE: "guildMemberUpdate", + GUILD_MEMBER_AVAILABLE: "guildMemberAvailable", + GUILD_MEMBER_SPEAKING: "guildMemberSpeaking", + GUILD_MEMBERS_CHUNK: "guildMembersChunk", + GUILD_INTEGRATIONS_UPDATE: "guildIntegrationsUpdate", + GUILD_ROLE_CREATE: "roleCreate", + GUILD_ROLE_DELETE: "roleDelete", + INVITE_CREATE: "inviteCreate", + INVITE_DELETE: "inviteDelete", + GUILD_ROLE_UPDATE: "roleUpdate", + GUILD_EMOJI_CREATE: "emojiCreate", + GUILD_EMOJI_DELETE: "emojiDelete", + GUILD_EMOJI_UPDATE: "emojiUpdate", + GUILD_BAN_ADD: "guildBanAdd", + GUILD_BAN_REMOVE: "guildBanRemove", + CHANNEL_CREATE: "channelCreate", + CHANNEL_DELETE: "channelDelete", + CHANNEL_UPDATE: "channelUpdate", + CHANNEL_PINS_UPDATE: "channelPinsUpdate", + MESSAGE_CREATE: "message", + MESSAGE_DELETE: "messageDelete", + MESSAGE_UPDATE: "messageUpdate", + MESSAGE_BULK_DELETE: "messageDeleteBulk", + MESSAGE_REACTION_ADD: "messageReactionAdd", + MESSAGE_REACTION_REMOVE: "messageReactionRemove", + MESSAGE_REACTION_REMOVE_ALL: "messageReactionRemoveAll", + MESSAGE_REACTION_REMOVE_EMOJI: "messageReactionRemoveEmoji", + USER_UPDATE: "userUpdate", + PRESENCE_UPDATE: "presenceUpdate", + VOICE_SERVER_UPDATE: "voiceServerUpdate", + VOICE_STATE_UPDATE: "voiceStateUpdate", + VOICE_BROADCAST_SUBSCRIBE: "subscribe", + VOICE_BROADCAST_UNSUBSCRIBE: "unsubscribe", + TYPING_START: "typingStart", + TYPING_STOP: "typingStop", + WEBHOOKS_UPDATE: "webhookUpdate", + LOBBY_CREATE: "lobbyCreate", + LOBBY_UPDATE: "lobbyUpdate", + LOBBY_DELETE: "lobbyDelete", + LOBBY_MEMBER_ADD: "lobbyMemberAdd", + LOBBY_MEMBER_REMOVE: "lobbyMemberRemove", + ERROR: "error", + WARN: "warn", + DEBUG: "debug", + SHARD_DISCONNECT: "shardDisconnect", + SHARD_ERROR: "shardError", + SHARD_RECONNECTING: "shardReconnecting", + SHARD_READY: "shardReady", + SHARD_RESUME: "shardResume", + INVALIDATED: "invalidated", + RAW: "raw", }; export const ShardEvents = { - CLOSE: "close", - DESTROYED: "destroyed", - INVALID_SESSION: "invalidSession", - READY: "ready", - RESUMED: "resumed", - ALL_READY: "allReady", + CLOSE: "close", + DESTROYED: "destroyed", + INVALID_SESSION: "invalidSession", + READY: "ready", + RESUMED: "resumed", + ALL_READY: "allReady", }; /** @@ -150,13 +150,7 @@ export const ShardEvents = { * sidebar for more information. * @typedef {string} PartialType */ -export const PartialTypes = keyMirror([ - "USER", - "CHANNEL", - "GUILD_MEMBER", - "MESSAGE", - "REACTION", -]); +export const PartialTypes = keyMirror(["USER", "CHANNEL", "GUILD_MEMBER", "MESSAGE", "REACTION"]); /** * The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events: @@ -199,42 +193,42 @@ export const PartialTypes = keyMirror([ * @typedef {string} WSEventType */ export const WSEvents = keyMirror([ - "READY", - "RESUMED", - "GUILD_CREATE", - "GUILD_DELETE", - "GUILD_UPDATE", - "INVITE_CREATE", - "INVITE_DELETE", - "GUILD_MEMBER_ADD", - "GUILD_MEMBER_REMOVE", - "GUILD_MEMBER_UPDATE", - "GUILD_MEMBERS_CHUNK", - "GUILD_INTEGRATIONS_UPDATE", - "GUILD_ROLE_CREATE", - "GUILD_ROLE_DELETE", - "GUILD_ROLE_UPDATE", - "GUILD_BAN_ADD", - "GUILD_BAN_REMOVE", - "GUILD_EMOJIS_UPDATE", - "CHANNEL_CREATE", - "CHANNEL_DELETE", - "CHANNEL_UPDATE", - "CHANNEL_PINS_UPDATE", - "MESSAGE_CREATE", - "MESSAGE_DELETE", - "MESSAGE_UPDATE", - "MESSAGE_DELETE_BULK", - "MESSAGE_REACTION_ADD", - "MESSAGE_REACTION_REMOVE", - "MESSAGE_REACTION_REMOVE_ALL", - "MESSAGE_REACTION_REMOVE_EMOJI", - "USER_UPDATE", - "PRESENCE_UPDATE", - "TYPING_START", - "VOICE_STATE_UPDATE", - "VOICE_SERVER_UPDATE", - "WEBHOOKS_UPDATE", + "READY", + "RESUMED", + "GUILD_CREATE", + "GUILD_DELETE", + "GUILD_UPDATE", + "INVITE_CREATE", + "INVITE_DELETE", + "GUILD_MEMBER_ADD", + "GUILD_MEMBER_REMOVE", + "GUILD_MEMBER_UPDATE", + "GUILD_MEMBERS_CHUNK", + "GUILD_INTEGRATIONS_UPDATE", + "GUILD_ROLE_CREATE", + "GUILD_ROLE_DELETE", + "GUILD_ROLE_UPDATE", + "GUILD_BAN_ADD", + "GUILD_BAN_REMOVE", + "GUILD_EMOJIS_UPDATE", + "CHANNEL_CREATE", + "CHANNEL_DELETE", + "CHANNEL_UPDATE", + "CHANNEL_PINS_UPDATE", + "MESSAGE_CREATE", + "MESSAGE_DELETE", + "MESSAGE_UPDATE", + "MESSAGE_DELETE_BULK", + "MESSAGE_REACTION_ADD", + "MESSAGE_REACTION_REMOVE", + "MESSAGE_REACTION_REMOVE_ALL", + "MESSAGE_REACTION_REMOVE_EMOJI", + "USER_UPDATE", + "PRESENCE_UPDATE", + "TYPING_START", + "VOICE_STATE_UPDATE", + "VOICE_SERVER_UPDATE", + "WEBHOOKS_UPDATE", ]); /** @@ -258,26 +252,26 @@ export const WSEvents = keyMirror([ * @typedef {string} MessageType */ export const MessageTypes = [ - "DEFAULT", - "RECIPIENT_ADD", - "RECIPIENT_REMOVE", - "CALL", - "CHANNEL_NAME_CHANGE", - "CHANNEL_ICON_CHANGE", - "PINS_ADD", - "GUILD_MEMBER_JOIN", - "USER_PREMIUM_GUILD_SUBSCRIPTION", - "USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1", - "USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2", - "USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3", - "CHANNEL_FOLLOW_ADD", - null, - "GUILD_DISCOVERY_DISQUALIFIED", - "GUILD_DISCOVERY_REQUALIFIED", - null, - null, - null, - "REPLY", + "DEFAULT", + "RECIPIENT_ADD", + "RECIPIENT_REMOVE", + "CALL", + "CHANNEL_NAME_CHANGE", + "CHANNEL_ICON_CHANGE", + "PINS_ADD", + "GUILD_MEMBER_JOIN", + "USER_PREMIUM_GUILD_SUBSCRIPTION", + "USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1", + "USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2", + "USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3", + "CHANNEL_FOLLOW_ADD", + null, + "GUILD_DISCOVERY_DISQUALIFIED", + "GUILD_DISCOVERY_REQUALIFIED", + null, + null, + null, + "REPLY", ]; /** @@ -286,9 +280,7 @@ export const MessageTypes = [ * * REPLY * @typedef {string} SystemMessageType */ -export const SystemMessageTypes = MessageTypes.filter( - (type: string | null) => type && type !== "DEFAULT" && type !== "REPLY", -); +export const SystemMessageTypes = MessageTypes.filter((type: string | null) => type && type !== "DEFAULT" && type !== "REPLY"); /** * Bots cannot set a `CUSTOM_STATUS`, it is only for custom statuses received from users @@ -301,80 +293,73 @@ export const SystemMessageTypes = MessageTypes.filter( * * COMPETING * @typedef {string} ActivityType */ -export const ActivityTypes = [ - "PLAYING", - "STREAMING", - "LISTENING", - "WATCHING", - "CUSTOM_STATUS", - "COMPETING", -]; +export const ActivityTypes = ["PLAYING", "STREAMING", "LISTENING", "WATCHING", "CUSTOM_STATUS", "COMPETING"]; export const ChannelTypes = { - TEXT: 0, - DM: 1, - VOICE: 2, - GROUP: 3, - CATEGORY: 4, - NEWS: 5, - STORE: 6, + TEXT: 0, + DM: 1, + VOICE: 2, + GROUP: 3, + CATEGORY: 4, + NEWS: 5, + STORE: 6, }; export const ClientApplicationAssetTypes = { - SMALL: 1, - BIG: 2, + SMALL: 1, + BIG: 2, }; export const AutomodActionTypes = { - BLOCK_MESSAGE: 1, - SEND_ALERT: 2, - TIMEOUT_MEMBER: 3, + BLOCK_MESSAGE: 1, + SEND_ALERT: 2, + TIMEOUT_MEMBER: 3, }; // Automod trigger types mapping: 1–6 follow discord-api-docs; 30+ are Spacebar extensions. export const AutomodTriggerTypes = { - CUSTOM_WORDS: 1, - HARMFUL_LINKS: 2, - SUSPECTED_SPAM_CONTENT: 3, - COMMONLY_FLAGGED_WORDS: 4, - MENTION_SPAM: 5, - MEMBER_PROFILE: 6, - PROHIBITED_LANGUAGES: 30, - SIMILARITY: 31, - ARTIFICIAL_CONTENT: 32, - EXECUTABLE_FILES: 33, + CUSTOM_WORDS: 1, + HARMFUL_LINKS: 2, + SUSPECTED_SPAM_CONTENT: 3, + COMMONLY_FLAGGED_WORDS: 4, + MENTION_SPAM: 5, + MEMBER_PROFILE: 6, + PROHIBITED_LANGUAGES: 30, + SIMILARITY: 31, + ARTIFICIAL_CONTENT: 32, + EXECUTABLE_FILES: 33, }; export const Colors = { - DEFAULT: 0x000000, - WHITE: 0xffffff, - AQUA: 0x1abc9c, - GREEN: 0x2ecc71, - BLUE: 0x3498db, - YELLOW: 0xffff00, - PURPLE: 0x9b59b6, - LUMINOUS_VIVID_PINK: 0xe91e63, - GOLD: 0xf1c40f, - ORANGE: 0xe67e22, - RED: 0xe74c3c, - GREY: 0x95a5a6, - NAVY: 0x34495e, - DARK_AQUA: 0x11806a, - DARK_GREEN: 0x1f8b4c, - DARK_BLUE: 0x206694, - DARK_PURPLE: 0x71368a, - DARK_VIVID_PINK: 0xad1457, - DARK_GOLD: 0xc27c0e, - DARK_ORANGE: 0xa84300, - DARK_RED: 0x992d22, - DARK_GREY: 0x979c9f, - DARKER_GREY: 0x7f8c8d, - LIGHT_GREY: 0xbcc0c0, - DARK_NAVY: 0x2c3e50, - BLURPLE: 0x7289da, - GREYPLE: 0x99aab5, - DARK_BUT_NOT_BLACK: 0x2c2f33, - NOT_QUITE_BLACK: 0x23272a, + DEFAULT: 0x000000, + WHITE: 0xffffff, + AQUA: 0x1abc9c, + GREEN: 0x2ecc71, + BLUE: 0x3498db, + YELLOW: 0xffff00, + PURPLE: 0x9b59b6, + LUMINOUS_VIVID_PINK: 0xe91e63, + GOLD: 0xf1c40f, + ORANGE: 0xe67e22, + RED: 0xe74c3c, + GREY: 0x95a5a6, + NAVY: 0x34495e, + DARK_AQUA: 0x11806a, + DARK_GREEN: 0x1f8b4c, + DARK_BLUE: 0x206694, + DARK_PURPLE: 0x71368a, + DARK_VIVID_PINK: 0xad1457, + DARK_GOLD: 0xc27c0e, + DARK_ORANGE: 0xa84300, + DARK_RED: 0x992d22, + DARK_GREY: 0x979c9f, + DARKER_GREY: 0x7f8c8d, + LIGHT_GREY: 0xbcc0c0, + DARK_NAVY: 0x2c3e50, + BLURPLE: 0x7289da, + GREYPLE: 0x99aab5, + DARK_BUT_NOT_BLACK: 0x2c2f33, + NOT_QUITE_BLACK: 0x23272a, }; /** @@ -384,11 +369,7 @@ export const Colors = { * * ALL_MEMBERS * @typedef {string} ExplicitContentFilterLevel */ -export const ExplicitContentFilterLevels = [ - "DISABLED", - "MEMBERS_WITHOUT_ROLES", - "ALL_MEMBERS", -]; +export const ExplicitContentFilterLevels = ["DISABLED", "MEMBERS_WITHOUT_ROLES", "ALL_MEMBERS"]; /** * The value set for the verification levels for a guild: @@ -399,13 +380,7 @@ export const ExplicitContentFilterLevels = [ * * VERY_HIGH * @typedef {string} VerificationLevel */ -export const VerificationLevels = [ - "NONE", - "LOW", - "MEDIUM", - "HIGH", - "VERY_HIGH", -]; +export const VerificationLevels = ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"]; /** * An error encountered while performing an API request. Here are the potential errors: @@ -551,556 +526,187 @@ export const VerificationLevels = [ * @typedef {string} APIError */ export const DiscordApiErrors = { - //https://discord.com/developers/docs/topics/opcodes-and-status-codes#json-json-error-codes - GENERAL_ERROR: new ApiError( - "General error (such as a malformed request body, amongst other things)", - 0, - ), - UNKNOWN_ACCOUNT: new ApiError("Unknown account", 10001), - UNKNOWN_APPLICATION: new ApiError("Unknown application", 10002), - UNKNOWN_CHANNEL: new ApiError("Unknown channel", 10003), - UNKNOWN_GUILD: new ApiError("Unknown guild", 10004), - UNKNOWN_INTEGRATION: new ApiError("Unknown integration", 10005), - UNKNOWN_INVITE: new ApiError("Unknown invite", 10006), - UNKNOWN_MEMBER: new ApiError("Unknown member", 10007), - UNKNOWN_MESSAGE: new ApiError("Unknown message", 10008), - UNKNOWN_OVERWRITE: new ApiError("Unknown permission overwrite", 10009), - UNKNOWN_PROVIDER: new ApiError("Unknown provider", 10010), - UNKNOWN_ROLE: new ApiError("Unknown role", 10011), - UNKNOWN_TOKEN: new ApiError("Unknown token", 10012), - UNKNOWN_USER: new ApiError("Unknown user", 10013), - UNKNOWN_EMOJI: new ApiError("Unknown emoji", 10014), - UNKNOWN_WEBHOOK: new ApiError("Unknown webhook", 10015, 404), - UNKNOWN_WEBHOOK_SERVICE: new ApiError("Unknown webhook service", 10016), - UNKNOWN_CONNECTION: new ApiError("Unknown connection", 10017, 400), - UNKNOWN_SESSION: new ApiError("Unknown session", 10020), - UNKNOWN_BAN: new ApiError("Unknown ban", 10026), - UNKNOWN_SKU: new ApiError("Unknown SKU", 10027), - UNKNOWN_STORE_LISTING: new ApiError("Unknown Store Listing", 10028), - UNKNOWN_ENTITLEMENT: new ApiError("Unknown entitlement", 10029), - UNKNOWN_BUILD: new ApiError("Unknown build", 10030), - UNKNOWN_LOBBY: new ApiError("Unknown lobby", 10031), - UNKNOWN_BRANCH: new ApiError("Unknown branch", 10032), - UNKNOWN_STORE_DIRECTORY_LAYOUT: new ApiError( - "Unknown store directory layout", - 10033, - ), - UNKNOWN_REDISTRIBUTABLE: new ApiError("Unknown redistributable", 10036), - UNKNOWN_GIFT_CODE: new ApiError("Unknown gift code", 10038), - UNKNOWN_STREAM: new ApiError("Unknown stream", 10049), - UNKNOWN_PREMIUM_SERVER_SUBSCRIBE_COOLDOWN: new ApiError( - "Unknown premium server subscribe cooldown", - 10050, - ), - UNKNOWN_GUILD_TEMPLATE: new ApiError("Unknown guild template", 10057), - UNKNOWN_DISCOVERABLE_SERVER_CATEGORY: new ApiError( - "Unknown discoverable server category", - 10059, - ), - UNKNOWN_STICKER: new ApiError("Unknown sticker", 10060), - UNKNOWN_INTERACTION: new ApiError("Unknown interaction", 10062), - UNKNOWN_APPLICATION_COMMAND: new ApiError( - "Unknown application command", - 10063, - ), - UNKNOWN_APPLICATION_COMMAND_PERMISSIONS: new ApiError( - "Unknown application command permissions", - 10066, - ), - UNKNOWN_STAGE_INSTANCE: new ApiError("Unknown Stage Instance", 10067), - UNKNOWN_GUILD_MEMBER_VERIFICATION_FORM: new ApiError( - "Unknown Guild Member Verification Form", - 10068, - ), - UNKNOWN_GUILD_WELCOME_SCREEN: new ApiError( - "Unknown Guild Welcome Screen", - 10069, - ), - UNKNOWN_GUILD_SCHEDULED_EVENT: new ApiError( - "Unknown Guild Scheduled Event", - 10070, - ), - UNKNOWN_GUILD_SCHEDULED_EVENT_USER: new ApiError( - "Unknown Guild Scheduled Event User", - 10071, - ), - BOT_PROHIBITED_ENDPOINT: new ApiError( - "Bots cannot use this endpoint", - 20001, - ), - BOT_ONLY_ENDPOINT: new ApiError("Only bots can use this endpoint", 20002), - EXPLICIT_CONTENT_CANNOT_BE_SENT_TO_RECIPIENT: new ApiError( - "Explicit content cannot be sent to the desired recipient(s)", - 20009, - ), - ACTION_NOT_AUTHORIZED_ON_APPLICATION: new ApiError( - "You are not authorized to perform this action on this application", - 20012, - ), - SLOWMODE_RATE_LIMIT: new ApiError( - "This action cannot be performed due to slowmode rate limit", - 20016, - ), - ONLY_OWNER: new ApiError( - "Only the owner of this account can perform this action", - 20018, - ), - ANNOUNCEMENT_RATE_LIMITS: new ApiError( - "This message cannot be edited due to announcement rate limits", - 20022, - ), - CHANNEL_WRITE_RATELIMIT: new ApiError( - "The channel you are writing has hit the write rate limit", - 20028, - ), - WORDS_NOT_ALLOWED: new ApiError( - "Your Stage topic, server name, server description, or channel names contain words that are not allowed", - 20031, - ), - GUILD_PREMIUM_LEVEL_TOO_LOW: new ApiError( - "Guild premium subscription level too low", - 20035, - ), - MAXIMUM_GUILDS: new ApiError( - "Maximum number of guilds reached ({})", - 30001, - undefined, - ["100"], - ), - MAXIMUM_FRIENDS: new ApiError( - "Maximum number of friends reached ({})", - 30002, - undefined, - ["1000"], - ), - MAXIMUM_PINS: new ApiError( - "Maximum number of pins reached for the channel ({})", - 30003, - undefined, - ["50"], - ), - MAXIMUM_NUMBER_OF_RECIPIENTS_REACHED: new ApiError( - "Maximum number of recipients reached ({})", - 30004, - undefined, - ["10"], - ), - MAXIMUM_ROLES: new ApiError( - "Maximum number of guild roles reached ({})", - 30005, - undefined, - ["250"], - ), - MAXIMUM_WEBHOOKS: new ApiError( - "Maximum number of webhooks reached ({})", - 30007, - undefined, - ["10"], - ), - MAXIMUM_NUMBER_OF_EMOJIS_REACHED: new ApiError( - "Maximum number of emojis reached", - 30008, - ), - MAXIMUM_REACTIONS: new ApiError( - "Maximum number of reactions reached ({})", - 30010, - undefined, - ["20"], - ), - MAXIMUM_CHANNELS: new ApiError( - "Maximum number of guild channels reached ({})", - 30013, - undefined, - ["500"], - ), - MAXIMUM_ATTACHMENTS: new ApiError( - "Maximum number of attachments in a message reached ({})", - 30015, - undefined, - ["10"], - ), - MAXIMUM_INVITES: new ApiError( - "Maximum number of invites reached ({})", - 30016, - undefined, - ["1000"], - ), - MAXIMUM_ANIMATED_EMOJIS: new ApiError( - "Maximum number of animated emojis reached", - 30018, - ), - MAXIMUM_SERVER_MEMBERS: new ApiError( - "Maximum number of server members reached", - 30019, - ), - MAXIMUM_SERVER_CATEGORIES: new ApiError( - "Maximum number of server categories has been reached ({})", - 30030, - undefined, - ["5"], - ), - GUILD_ALREADY_HAS_TEMPLATE: new ApiError( - "Guild already has a template", - 30031, - ), - MAXIMUM_THREAD_PARTICIPANTS: new ApiError( - "Max number of thread participants has been reached", - 30033, - ), - MAXIMUM_BANS_FOR_NON_GUILD_MEMBERS: new ApiError( - "Maximum number of bans for non-guild members have been exceeded", - 30035, - ), - MAXIMUM_BANS_FETCHES: new ApiError( - "Maximum number of bans fetches has been reached", - 30037, - ), - MAXIMUM_STICKERS: new ApiError("Maximum number of stickers reached", 30039), - MAXIMUM_PRUNE_REQUESTS: new ApiError( - "Maximum number of prune requests has been reached. Try again later", - 30040, - ), - UNAUTHORIZED: new ApiError( - "Unauthorized. Provide a valid token and try again", - 40001, - ), - ACCOUNT_VERIFICATION_REQUIRED: new ApiError( - "You need to verify your account in order to perform this action", - 40002, - ), - OPENING_DIRECT_MESSAGES_TOO_FAST: new ApiError( - "You are opening direct messages too fast", - 40003, - ), - REQUEST_ENTITY_TOO_LARGE: new ApiError( - "Request entity too large. Try sending something smaller in size", - 40005, - ), - FEATURE_TEMPORARILY_DISABLED: new ApiError( - "This feature has been temporarily disabled server-side", - 40006, - ), - USER_BANNED: new ApiError("The user is banned from this guild", 40007), - CONNECTION_REVOKED: new ApiError( - "The connection has been revoked", - 40012, - 400, - ), - TARGET_USER_IS_NOT_CONNECTED_TO_VOICE: new ApiError( - "Target user is not connected to voice", - 40032, - ), - ALREADY_CROSSPOSTED: new ApiError( - "This message has already been crossposted", - 40033, - ), - APPLICATION_COMMAND_ALREADY_EXISTS: new ApiError( - "An application command with that name already exists", - 40041, - ), - MISSING_ACCESS: new ApiError("Missing access", 50001), - INVALID_ACCOUNT_TYPE: new ApiError("Invalid account type", 50002), - CANNOT_EXECUTE_ON_DM: new ApiError( - "Cannot execute action on a DM channel", - 50003, - ), - EMBED_DISABLED: new ApiError("Widget Disabled", 50004), - CANNOT_EDIT_MESSAGE_BY_OTHER: new ApiError( - "Cannot edit a message authored by another user", - 50005, - ), - CANNOT_SEND_EMPTY_MESSAGE: new ApiError( - "Cannot send an empty message", - 50006, - ), - CANNOT_MESSAGE_USER: new ApiError( - "Cannot send messages to this user", - 50007, - ), - CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: new ApiError( - "Cannot send messages in a voice channel", - 50008, - ), - CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: new ApiError( - "Channel verification level is too high for you to gain access", - 50009, - ), - OAUTH2_APPLICATION_BOT_ABSENT: new ApiError( - "OAuth2 application does not have a bot", - 50010, - ), - MAXIMUM_OAUTH2_APPLICATIONS: new ApiError( - "OAuth2 application limit reached", - 50011, - ), - INVALID_OAUTH_STATE: new ApiError("Invalid OAuth2 state", 50012), - MISSING_PERMISSIONS: new ApiError( - "You lack permissions to perform that action ({})", - 50013, - undefined, - [""], - ), - INVALID_AUTHENTICATION_TOKEN: new ApiError( - "Invalid authentication token provided", - 50014, - ), - NOTE_TOO_LONG: new ApiError("Note was too long", 50015), - INVALID_BULK_DELETE_QUANTITY: new ApiError( - "Provided too few or too many messages to delete. Must provide at least {} and fewer than {} messages to delete", - 50016, - undefined, - ["2", "100"], - ), - CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: new ApiError( - "A message can only be pinned to the channel it was sent in", - 50019, - ), - INVALID_OR_TAKEN_INVITE_CODE: new ApiError( - "Invite code was either invalid or taken", - 50020, - ), - CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: new ApiError( - "Cannot execute action on a system message", - 50021, - ), - CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE: new ApiError( - "Cannot execute action on this channel type", - 50024, - ), - INVALID_OAUTH_TOKEN: new ApiError( - "Invalid OAuth2 access token provided", - 50025, - ), - MISSING_REQUIRED_OAUTH2_SCOPE: new ApiError( - "Missing required OAuth2 scope", - 50026, - ), - INVALID_WEBHOOK_TOKEN_PROVIDED: new ApiError( - "Invalid webhook token provided", - 50027, - ), - INVALID_ROLE: new ApiError("Invalid role", 50028), - INVALID_RECIPIENT: new ApiError("Invalid Recipient(s)", 50033), - BULK_DELETE_MESSAGE_TOO_OLD: new ApiError( - "A message provided was too old to bulk delete", - 50034, - ), - INVALID_FORM_BODY: new ApiError( - "Invalid form body (returned for both application/json and multipart/form-data bodies), or invalid Content-Type provided", - 50035, - ), - INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: new ApiError( - "An invite was accepted to a guild the application's bot is not in", - 50036, - ), - INVALID_API_VERSION: new ApiError("Invalid API version provided", 50041), - FILE_EXCEEDS_MAXIMUM_SIZE: new ApiError( - "File uploaded exceeds the maximum size", - 50045, - ), - INVALID_FILE_UPLOADED: new ApiError("Invalid file uploaded", 50046), - CANNOT_SELF_REDEEM_GIFT: new ApiError( - "Cannot self-redeem this gift", - 50054, - ), - PAYMENT_SOURCE_REQUIRED: new ApiError( - "Payment source required to redeem gift", - 50070, - ), - CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: new ApiError( - "Cannot delete a channel required for Community guilds", - 50074, - ), - INVALID_STICKER_SENT: new ApiError("Invalid sticker sent", 50081), - CANNOT_EDIT_ARCHIVED_THREAD: new ApiError( - "Tried to perform an operation on an archived thread, such as editing a message or adding a user to the thread", - 50083, - ), - INVALID_THREAD_NOTIFICATION_SETTINGS: new ApiError( - "Invalid thread notification settings", - 50084, - ), - BEFORE_EARLIER_THAN_THREAD_CREATION_DATE: new ApiError( - "before value is earlier than the thread creation date", - 50085, - ), - SERVER_NOT_AVAILABLE_IN_YOUR_LOCATION: new ApiError( - "This server is not available in your location", - 50095, - ), - SERVER_NEEDS_MONETIZATION_ENABLED: new ApiError( - "This server needs monetization enabled in order to perform this action", - 50097, - ), - TWO_FACTOR_REQUIRED: new ApiError( - "Two factor is required for this operation", - 60003, - ), - NO_USERS_WITH_DISCORDTAG_EXIST: new ApiError( - "No users with DiscordTag exist", - 80004, - ), - REACTION_BLOCKED: new ApiError("Reaction was blocked", 90001), - RESOURCE_OVERLOADED: new ApiError( - "API resource is currently overloaded. Try again a little later", - 130000, - ), - STAGE_ALREADY_OPEN: new ApiError("The Stage is already open", 150006), - THREAD_ALREADY_CREATED_FOR_THIS_MESSAGE: new ApiError( - "A thread has already been created for this message", - 160004, - ), - THREAD_IS_LOCKED: new ApiError("Thread is locked", 160005), - MAXIMUM_NUMBER_OF_ACTIVE_THREADS: new ApiError( - "Maximum number of active threads reached", - 160006, - ), - MAXIMUM_NUMBER_OF_ACTIVE_ANNOUNCEMENT_THREADS: new ApiError( - "Maximum number of active announcement threads reached", - 160007, - ), - INVALID_JSON_FOR_UPLOADED_LOTTIE_FILE: new ApiError( - "Invalid JSON for uploaded Lottie file", - 170001, - ), - LOTTIES_CANNOT_CONTAIN_RASTERIZED_IMAGES: new ApiError( - "Uploaded Lotties cannot contain rasterized images such as PNG or JPEG", - 170002, - ), - STICKER_MAXIMUM_FRAMERATE: new ApiError( - "Sticker maximum framerate exceeded", - 170003, - ), - STICKER_MAXIMUM_FRAME_COUNT: new ApiError( - "Sticker frame count exceeds maximum of {} frames", - 170004, - undefined, - ["1000"], - ), - LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS: new ApiError( - "Lottie animation maximum dimensions exceeded", - 170005, - ), - STICKER_FRAME_RATE_TOO_SMALL_OR_TOO_LARGE: new ApiError( - "Sticker frame rate is either too small or too large", - 170006, - ), - STICKER_ANIMATION_DURATION_MAXIMUM: new ApiError( - "Sticker animation duration exceeds maximum of {} seconds", - 170007, - undefined, - ["5"], - ), - AUTOMODERATOR_BLOCK: new ApiError( - "Message was blocked by automatic moderation", - 200000, - ), - BULK_BAN_FAILED: new ApiError("Failed to ban users", 500000), + //https://discord.com/developers/docs/topics/opcodes-and-status-codes#json-json-error-codes + GENERAL_ERROR: new ApiError("General error (such as a malformed request body, amongst other things)", 0), + UNKNOWN_ACCOUNT: new ApiError("Unknown account", 10001), + UNKNOWN_APPLICATION: new ApiError("Unknown application", 10002), + UNKNOWN_CHANNEL: new ApiError("Unknown channel", 10003), + UNKNOWN_GUILD: new ApiError("Unknown guild", 10004), + UNKNOWN_INTEGRATION: new ApiError("Unknown integration", 10005), + UNKNOWN_INVITE: new ApiError("Unknown invite", 10006), + UNKNOWN_MEMBER: new ApiError("Unknown member", 10007), + UNKNOWN_MESSAGE: new ApiError("Unknown message", 10008), + UNKNOWN_OVERWRITE: new ApiError("Unknown permission overwrite", 10009), + UNKNOWN_PROVIDER: new ApiError("Unknown provider", 10010), + UNKNOWN_ROLE: new ApiError("Unknown role", 10011), + UNKNOWN_TOKEN: new ApiError("Unknown token", 10012), + UNKNOWN_USER: new ApiError("Unknown user", 10013), + UNKNOWN_EMOJI: new ApiError("Unknown emoji", 10014), + UNKNOWN_WEBHOOK: new ApiError("Unknown webhook", 10015, 404), + UNKNOWN_WEBHOOK_SERVICE: new ApiError("Unknown webhook service", 10016), + UNKNOWN_CONNECTION: new ApiError("Unknown connection", 10017, 400), + UNKNOWN_SESSION: new ApiError("Unknown session", 10020), + UNKNOWN_BAN: new ApiError("Unknown ban", 10026), + UNKNOWN_SKU: new ApiError("Unknown SKU", 10027), + UNKNOWN_STORE_LISTING: new ApiError("Unknown Store Listing", 10028), + UNKNOWN_ENTITLEMENT: new ApiError("Unknown entitlement", 10029), + UNKNOWN_BUILD: new ApiError("Unknown build", 10030), + UNKNOWN_LOBBY: new ApiError("Unknown lobby", 10031), + UNKNOWN_BRANCH: new ApiError("Unknown branch", 10032), + UNKNOWN_STORE_DIRECTORY_LAYOUT: new ApiError("Unknown store directory layout", 10033), + UNKNOWN_REDISTRIBUTABLE: new ApiError("Unknown redistributable", 10036), + UNKNOWN_GIFT_CODE: new ApiError("Unknown gift code", 10038), + UNKNOWN_STREAM: new ApiError("Unknown stream", 10049), + UNKNOWN_PREMIUM_SERVER_SUBSCRIBE_COOLDOWN: new ApiError("Unknown premium server subscribe cooldown", 10050), + UNKNOWN_GUILD_TEMPLATE: new ApiError("Unknown guild template", 10057), + UNKNOWN_DISCOVERABLE_SERVER_CATEGORY: new ApiError("Unknown discoverable server category", 10059), + UNKNOWN_STICKER: new ApiError("Unknown sticker", 10060), + UNKNOWN_INTERACTION: new ApiError("Unknown interaction", 10062), + UNKNOWN_APPLICATION_COMMAND: new ApiError("Unknown application command", 10063), + UNKNOWN_APPLICATION_COMMAND_PERMISSIONS: new ApiError("Unknown application command permissions", 10066), + UNKNOWN_STAGE_INSTANCE: new ApiError("Unknown Stage Instance", 10067), + UNKNOWN_GUILD_MEMBER_VERIFICATION_FORM: new ApiError("Unknown Guild Member Verification Form", 10068), + UNKNOWN_GUILD_WELCOME_SCREEN: new ApiError("Unknown Guild Welcome Screen", 10069), + UNKNOWN_GUILD_SCHEDULED_EVENT: new ApiError("Unknown Guild Scheduled Event", 10070), + UNKNOWN_GUILD_SCHEDULED_EVENT_USER: new ApiError("Unknown Guild Scheduled Event User", 10071), + UNKNOWN_CONSENT: new ApiError("Unknown consent", 10080, 404), + UNKNOWN_CONSENT_GRANT: new ApiError("Unknown consent grant", 10081, 404), + BOT_PROHIBITED_ENDPOINT: new ApiError("Bots cannot use this endpoint", 20001), + BOT_ONLY_ENDPOINT: new ApiError("Only bots can use this endpoint", 20002), + EXPLICIT_CONTENT_CANNOT_BE_SENT_TO_RECIPIENT: new ApiError("Explicit content cannot be sent to the desired recipient(s)", 20009), + ACTION_NOT_AUTHORIZED_ON_APPLICATION: new ApiError("You are not authorized to perform this action on this application", 20012), + SLOWMODE_RATE_LIMIT: new ApiError("This action cannot be performed due to slowmode rate limit", 20016), + ONLY_OWNER: new ApiError("Only the owner of this account can perform this action", 20018), + ANNOUNCEMENT_RATE_LIMITS: new ApiError("This message cannot be edited due to announcement rate limits", 20022), + CHANNEL_WRITE_RATELIMIT: new ApiError("The channel you are writing has hit the write rate limit", 20028), + WORDS_NOT_ALLOWED: new ApiError("Your Stage topic, server name, server description, or channel names contain words that are not allowed", 20031), + GUILD_PREMIUM_LEVEL_TOO_LOW: new ApiError("Guild premium subscription level too low", 20035), + MAXIMUM_GUILDS: new ApiError("Maximum number of guilds reached ({})", 30001, undefined, ["100"]), + MAXIMUM_FRIENDS: new ApiError("Maximum number of friends reached ({})", 30002, undefined, ["1000"]), + MAXIMUM_PINS: new ApiError("Maximum number of pins reached for the channel ({})", 30003, undefined, ["50"]), + MAXIMUM_NUMBER_OF_RECIPIENTS_REACHED: new ApiError("Maximum number of recipients reached ({})", 30004, undefined, ["10"]), + MAXIMUM_ROLES: new ApiError("Maximum number of guild roles reached ({})", 30005, undefined, ["250"]), + MAXIMUM_WEBHOOKS: new ApiError("Maximum number of webhooks reached ({})", 30007, undefined, ["10"]), + MAXIMUM_NUMBER_OF_EMOJIS_REACHED: new ApiError("Maximum number of emojis reached", 30008), + MAXIMUM_REACTIONS: new ApiError("Maximum number of reactions reached ({})", 30010, undefined, ["20"]), + MAXIMUM_CHANNELS: new ApiError("Maximum number of guild channels reached ({})", 30013, undefined, ["500"]), + MAXIMUM_ATTACHMENTS: new ApiError("Maximum number of attachments in a message reached ({})", 30015, undefined, ["10"]), + MAXIMUM_INVITES: new ApiError("Maximum number of invites reached ({})", 30016, undefined, ["1000"]), + MAXIMUM_ANIMATED_EMOJIS: new ApiError("Maximum number of animated emojis reached", 30018), + MAXIMUM_SERVER_MEMBERS: new ApiError("Maximum number of server members reached", 30019), + MAXIMUM_SERVER_CATEGORIES: new ApiError("Maximum number of server categories has been reached ({})", 30030, undefined, ["5"]), + GUILD_ALREADY_HAS_TEMPLATE: new ApiError("Guild already has a template", 30031), + MAXIMUM_THREAD_PARTICIPANTS: new ApiError("Max number of thread participants has been reached", 30033), + MAXIMUM_BANS_FOR_NON_GUILD_MEMBERS: new ApiError("Maximum number of bans for non-guild members have been exceeded", 30035), + MAXIMUM_BANS_FETCHES: new ApiError("Maximum number of bans fetches has been reached", 30037), + MAXIMUM_STICKERS: new ApiError("Maximum number of stickers reached", 30039), + MAXIMUM_PRUNE_REQUESTS: new ApiError("Maximum number of prune requests has been reached. Try again later", 30040), + UNAUTHORIZED: new ApiError("Unauthorized. Provide a valid token and try again", 40001), + ACCOUNT_VERIFICATION_REQUIRED: new ApiError("You need to verify your account in order to perform this action", 40002), + OPENING_DIRECT_MESSAGES_TOO_FAST: new ApiError("You are opening direct messages too fast", 40003), + REQUEST_ENTITY_TOO_LARGE: new ApiError("Request entity too large. Try sending something smaller in size", 40005), + FEATURE_TEMPORARILY_DISABLED: new ApiError("This feature has been temporarily disabled server-side", 40006), + USER_BANNED: new ApiError("The user is banned from this guild", 40007), + CONNECTION_REVOKED: new ApiError("The connection has been revoked", 40012, 400), + TARGET_USER_IS_NOT_CONNECTED_TO_VOICE: new ApiError("Target user is not connected to voice", 40032), + ALREADY_CROSSPOSTED: new ApiError("This message has already been crossposted", 40033), + APPLICATION_COMMAND_ALREADY_EXISTS: new ApiError("An application command with that name already exists", 40041), + MISSING_ACCESS: new ApiError("Missing access", 50001), + INVALID_ACCOUNT_TYPE: new ApiError("Invalid account type", 50002), + CANNOT_EXECUTE_ON_DM: new ApiError("Cannot execute action on a DM channel", 50003), + EMBED_DISABLED: new ApiError("Widget Disabled", 50004), + CANNOT_EDIT_MESSAGE_BY_OTHER: new ApiError("Cannot edit a message authored by another user", 50005), + CANNOT_SEND_EMPTY_MESSAGE: new ApiError("Cannot send an empty message", 50006), + CANNOT_MESSAGE_USER: new ApiError("Cannot send messages to this user", 50007), + CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: new ApiError("Cannot send messages in a voice channel", 50008), + CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: new ApiError("Channel verification level is too high for you to gain access", 50009), + OAUTH2_APPLICATION_BOT_ABSENT: new ApiError("OAuth2 application does not have a bot", 50010), + MAXIMUM_OAUTH2_APPLICATIONS: new ApiError("OAuth2 application limit reached", 50011), + INVALID_OAUTH_STATE: new ApiError("Invalid OAuth2 state", 50012), + MISSING_PERMISSIONS: new ApiError("You lack permissions to perform that action ({})", 50013, undefined, [""]), + INVALID_AUTHENTICATION_TOKEN: new ApiError("Invalid authentication token provided", 50014), + NOTE_TOO_LONG: new ApiError("Note was too long", 50015), + INVALID_BULK_DELETE_QUANTITY: new ApiError("Provided too few or too many messages to delete. Must provide at least {} and fewer than {} messages to delete", 50016, undefined, [ + "2", + "100", + ]), + CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: new ApiError("A message can only be pinned to the channel it was sent in", 50019), + INVALID_OR_TAKEN_INVITE_CODE: new ApiError("Invite code was either invalid or taken", 50020), + CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: new ApiError("Cannot execute action on a system message", 50021), + CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE: new ApiError("Cannot execute action on this channel type", 50024), + INVALID_OAUTH_TOKEN: new ApiError("Invalid OAuth2 access token provided", 50025), + MISSING_REQUIRED_OAUTH2_SCOPE: new ApiError("Missing required OAuth2 scope", 50026), + INVALID_WEBHOOK_TOKEN_PROVIDED: new ApiError("Invalid webhook token provided", 50027), + INVALID_ROLE: new ApiError("Invalid role", 50028), + INVALID_RECIPIENT: new ApiError("Invalid Recipient(s)", 50033), + BULK_DELETE_MESSAGE_TOO_OLD: new ApiError("A message provided was too old to bulk delete", 50034), + INVALID_FORM_BODY: new ApiError("Invalid form body (returned for both application/json and multipart/form-data bodies), or invalid Content-Type provided", 50035), + INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: new ApiError("An invite was accepted to a guild the application's bot is not in", 50036), + INVALID_API_VERSION: new ApiError("Invalid API version provided", 50041), + FILE_EXCEEDS_MAXIMUM_SIZE: new ApiError("File uploaded exceeds the maximum size", 50045), + INVALID_FILE_UPLOADED: new ApiError("Invalid file uploaded", 50046), + CANNOT_SELF_REDEEM_GIFT: new ApiError("Cannot self-redeem this gift", 50054), + PAYMENT_SOURCE_REQUIRED: new ApiError("Payment source required to redeem gift", 50070), + CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: new ApiError("Cannot delete a channel required for Community guilds", 50074), + INVALID_STICKER_SENT: new ApiError("Invalid sticker sent", 50081), + CANNOT_EDIT_ARCHIVED_THREAD: new ApiError("Tried to perform an operation on an archived thread, such as editing a message or adding a user to the thread", 50083), + INVALID_THREAD_NOTIFICATION_SETTINGS: new ApiError("Invalid thread notification settings", 50084), + BEFORE_EARLIER_THAN_THREAD_CREATION_DATE: new ApiError("before value is earlier than the thread creation date", 50085), + SERVER_NOT_AVAILABLE_IN_YOUR_LOCATION: new ApiError("This server is not available in your location", 50095), + SERVER_NEEDS_MONETIZATION_ENABLED: new ApiError("This server needs monetization enabled in order to perform this action", 50097), + TWO_FACTOR_REQUIRED: new ApiError("Two factor is required for this operation", 60003), + NO_USERS_WITH_DISCORDTAG_EXIST: new ApiError("No users with DiscordTag exist", 80004), + REACTION_BLOCKED: new ApiError("Reaction was blocked", 90001), + RESOURCE_OVERLOADED: new ApiError("API resource is currently overloaded. Try again a little later", 130000), + STAGE_ALREADY_OPEN: new ApiError("The Stage is already open", 150006), + THREAD_ALREADY_CREATED_FOR_THIS_MESSAGE: new ApiError("A thread has already been created for this message", 160004), + THREAD_IS_LOCKED: new ApiError("Thread is locked", 160005), + MAXIMUM_NUMBER_OF_ACTIVE_THREADS: new ApiError("Maximum number of active threads reached", 160006), + MAXIMUM_NUMBER_OF_ACTIVE_ANNOUNCEMENT_THREADS: new ApiError("Maximum number of active announcement threads reached", 160007), + INVALID_JSON_FOR_UPLOADED_LOTTIE_FILE: new ApiError("Invalid JSON for uploaded Lottie file", 170001), + LOTTIES_CANNOT_CONTAIN_RASTERIZED_IMAGES: new ApiError("Uploaded Lotties cannot contain rasterized images such as PNG or JPEG", 170002), + STICKER_MAXIMUM_FRAMERATE: new ApiError("Sticker maximum framerate exceeded", 170003), + STICKER_MAXIMUM_FRAME_COUNT: new ApiError("Sticker frame count exceeds maximum of {} frames", 170004, undefined, ["1000"]), + LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS: new ApiError("Lottie animation maximum dimensions exceeded", 170005), + STICKER_FRAME_RATE_TOO_SMALL_OR_TOO_LARGE: new ApiError("Sticker frame rate is either too small or too large", 170006), + STICKER_ANIMATION_DURATION_MAXIMUM: new ApiError("Sticker animation duration exceeds maximum of {} seconds", 170007, undefined, ["5"]), + AUTOMODERATOR_BLOCK: new ApiError("Message was blocked by automatic moderation", 200000), + BULK_BAN_FAILED: new ApiError("Failed to ban users", 500000), - //Other errors - UNKNOWN_VOICE_STATE: new ApiError("Unknown Voice State", 10065, 404), + //Other errors + UNKNOWN_VOICE_STATE: new ApiError("Unknown Voice State", 10065, 404), }; export const TicketFlags = { - RESOLVED: 1 << 0, - ARCHIVED: 1 << 1, + RESOLVED: 1 << 0, + ARCHIVED: 1 << 1, } as const; /** * An error encountered while performing an API request (Spacebar only). Here are the potential errors: */ export const SpacebarApiErrors = { - MANUALLY_TRIGGERED_ERROR: new ApiError( - "This is an artificial error", - 1, - 500, - ), - PREMIUM_DISABLED_FOR_GUILD: new ApiError( - "This guild cannot be boosted", - 25001, - ), - NO_FURTHER_PREMIUM: new ApiError( - "This guild does not receive further boosts", - 25002, - ), - GUILD_PREMIUM_DISABLED_FOR_YOU: new ApiError( - "This guild cannot be boosted by you", - 25003, - 403, - ), - CANNOT_FRIEND_SELF: new ApiError("Cannot friend oneself", 25009), - USER_SPECIFIC_INVITE_WRONG_RECIPIENT: new ApiError( - "This invite is not meant for you", - 25010, - ), - USER_SPECIFIC_INVITE_FAILED: new ApiError("Failed to invite user", 25011), - CANNOT_MODIFY_USER_GROUP: new ApiError( - "This user cannot manipulate this group", - 25050, - 403, - ), - CANNOT_REMOVE_SELF_FROM_GROUP: new ApiError( - "This user cannot remove oneself from user group", - 25051, - ), - CANNOT_BAN_OPERATOR: new ApiError( - "Non-OPERATOR cannot ban OPERATOR from instance", - 25052, - ), - CANNOT_LEAVE_GUILD: new ApiError( - "You are not allowed to leave guilds that you joined by yourself", - 25059, - 403, - ), - EDITS_DISABLED: new ApiError( - "You are not allowed to edit your own messages", - 25060, - 403, - ), - DELETE_MESSAGE_DISABLED: new ApiError( - "You are not allowed to delete your own messages", - 25061, - 403, - ), - FEATURE_PERMANENTLY_DISABLED: new ApiError( - "This feature has been disabled server-side", - 45006, - 501, - ), - FEATURE_IS_IMMUTABLE: new ApiError( - "The feature ({}) cannot be edited.", - 45007, - 403, - ), - MISSING_RIGHTS: new ApiError( - "You lack rights to perform that action ({})", - 50013, - undefined, - [""], - ), - CANNOT_REPLACE_BY_BACKFILL: new ApiError( - "Cannot backfill to message ID that already exists", - 55002, - 409, - ), - CANNOT_BACKFILL_TO_THE_FUTURE: new ApiError( - "You cannot backfill messages in the future", - 55003, - ), - CANNOT_GRANT_PERMISSIONS_EXCEEDING_RIGHTS: new ApiError( - "You cannot grant permissions exceeding your own rights", - 50050, - ), - ROUTES_LOOPING: new ApiError( - "Loops in the route definition ({})", - 50060, - undefined, - [""], - ), - CANNOT_REMOVE_ROUTE: new ApiError( - "Cannot remove message route while it is in effect and being used", - 50061, - ), + MANUALLY_TRIGGERED_ERROR: new ApiError("This is an artificial error", 1, 500), + PREMIUM_DISABLED_FOR_GUILD: new ApiError("This guild cannot be boosted", 25001), + NO_FURTHER_PREMIUM: new ApiError("This guild does not receive further boosts", 25002), + GUILD_PREMIUM_DISABLED_FOR_YOU: new ApiError("This guild cannot be boosted by you", 25003, 403), + CANNOT_FRIEND_SELF: new ApiError("Cannot friend oneself", 25009), + USER_SPECIFIC_INVITE_WRONG_RECIPIENT: new ApiError("This invite is not meant for you", 25010), + USER_SPECIFIC_INVITE_FAILED: new ApiError("Failed to invite user", 25011), + CANNOT_MODIFY_USER_GROUP: new ApiError("This user cannot manipulate this group", 25050, 403), + CANNOT_REMOVE_SELF_FROM_GROUP: new ApiError("This user cannot remove oneself from user group", 25051), + CANNOT_BAN_OPERATOR: new ApiError("Non-OPERATOR cannot ban OPERATOR from instance", 25052), + CANNOT_LEAVE_GUILD: new ApiError("You are not allowed to leave guilds that you joined by yourself", 25059, 403), + EDITS_DISABLED: new ApiError("You are not allowed to edit your own messages", 25060, 403), + DELETE_MESSAGE_DISABLED: new ApiError("You are not allowed to delete your own messages", 25061, 403), + FEATURE_PERMANENTLY_DISABLED: new ApiError("This feature has been disabled server-side", 45006, 501), + FEATURE_IS_IMMUTABLE: new ApiError("The feature ({}) cannot be edited.", 45007, 403), + MISSING_RIGHTS: new ApiError("You lack rights to perform that action ({})", 50013, undefined, [""]), + CANNOT_REPLACE_BY_BACKFILL: new ApiError("Cannot backfill to message ID that already exists", 55002, 409), + CANNOT_BACKFILL_TO_THE_FUTURE: new ApiError("You cannot backfill messages in the future", 55003), + CANNOT_GRANT_PERMISSIONS_EXCEEDING_RIGHTS: new ApiError("You cannot grant permissions exceeding your own rights", 50050), + ROUTES_LOOPING: new ApiError("Loops in the route definition ({})", 50060, undefined, [""]), + CANNOT_REMOVE_ROUTE: new ApiError("Cannot remove message route while it is in effect and being used", 50061), }; /** @@ -1131,7 +737,7 @@ export const MembershipStates = ["INSERTED", "INVITED", "ACCEPTED"]; export const WebhookTypes = ["Custom", "Incoming", "Channel Follower"]; function keyMirror(arr: string[]) { - const tmp = Object.create(null); - for (const value of arr) tmp[value] = value; - return tmp; + const tmp = Object.create(null); + for (const value of arr) tmp[value] = value; + return tmp; }