From dc1ca7c005d3aa24800e4cfe516ac05d04d66597 Mon Sep 17 00:00:00 2001 From: Jonathan Ho <48591478+H01001000@users.noreply.github.com> Date: Fri, 17 Jun 2022 23:38:18 +0800 Subject: [PATCH 1/2] feat: reverse transformers (#2305) * feat: reverse transformer * fix: cap * deno fmt * refactor: change to use toggles list * Revert "refactor: change to use toggles list" This reverts commit 31f170f31be2d72732fb7d39a36871fea0f94479. * fix: forget reverse transformer --- bot.ts | 14 +++++++++ transformers/mod.ts | 2 ++ transformers/reverse/activity.ts | 46 +++++++++++++++++++++++++++++ transformers/reverse/application.ts | 26 ++++++++++++++++ transformers/reverse/member.ts | 38 ++++++++++++++++++++++++ transformers/reverse/mod.ts | 6 ++++ transformers/reverse/team.ts | 21 +++++++++++++ 7 files changed, 153 insertions(+) create mode 100644 transformers/reverse/activity.ts create mode 100644 transformers/reverse/application.ts create mode 100644 transformers/reverse/member.ts create mode 100644 transformers/reverse/mod.ts create mode 100644 transformers/reverse/team.ts diff --git a/bot.ts b/bot.ts index a0455068c..37c2ffc48 100644 --- a/bot.ts +++ b/bot.ts @@ -138,6 +138,10 @@ import { } from "./transformers/applicationCommandOptionChoice.ts"; import { transformEmbedToDiscordEmbed } from "./transformers/reverse/embed.ts"; import { transformComponentToDiscordComponent } from "./transformers/reverse/component.ts"; +import { transformActivityToDiscordActivity } from "./transformers/reverse/activity.ts"; +import { transformTeamToDiscordTeam } from "./transformers/reverse/team.ts"; +import { transformMemberToDiscordMember, transformUserToDiscordUser } from "./transformers/reverse/member.ts"; +import { transformApplicationToDiscordApplication } from "./transformers/reverse/application.ts"; import { getBotIdFromToken, removeTokenPrefix } from "./util/token.ts"; import { CreateShardManager } from "./gateway/manager/shardManager.ts"; import { AutoModerationRule, transformAutoModerationRule } from "./transformers/automodRule.ts"; @@ -396,6 +400,11 @@ export interface Transformers { reverse: { embed: (bot: Bot, payload: Embed) => DiscordEmbed; component: (bot: Bot, payload: Component) => DiscordComponent; + activity: (bot: Bot, payload: Activity) => DiscordActivity; + member: (bot: Bot, payload: Member) => DiscordMember; + user: (bot: Bot, payload: User) => DiscordUser; + team: (bot: Bot, payload: Team) => DiscordTeam; + application: (bot: Bot, payload: Application) => DiscordApplication; }; snowflake: (snowflake: string) => bigint; gatewayBot: (payload: DiscordGetGatewayBot) => GetGatewayBot; @@ -449,6 +458,11 @@ export function createTransformers(options: Partial) { reverse: { embed: options.reverse?.embed || transformEmbedToDiscordEmbed, component: options.reverse?.component || transformComponentToDiscordComponent, + activity: options.reverse?.activity || transformActivityToDiscordActivity, + member: options.reverse?.member || transformMemberToDiscordMember, + user: options.reverse?.user || transformUserToDiscordUser, + team: options.reverse?.team || transformTeamToDiscordTeam, + application: options.reverse?.application || transformApplicationToDiscordApplication, }, automodRule: options.automodRule || transformAutoModerationRule, automodActionExecution: options.automodActionExecution || transformAutoModerationActionExecution, diff --git a/transformers/mod.ts b/transformers/mod.ts index d373feda1..d58ec74bf 100644 --- a/transformers/mod.ts +++ b/transformers/mod.ts @@ -32,3 +32,5 @@ export * from "./webhook.ts"; export * from "./welcomeScreen.ts"; export * from "./widget.ts"; export * from "./widgetSettings.ts"; + +export * from "./reverse/mod.ts"; diff --git a/transformers/reverse/activity.ts b/transformers/reverse/activity.ts new file mode 100644 index 000000000..a5c8a6335 --- /dev/null +++ b/transformers/reverse/activity.ts @@ -0,0 +1,46 @@ +import { Bot } from "../../bot.ts"; +import { DiscordActivity } from "../../types/discord.ts"; +import { Activity } from "../activity.ts"; + +export function transformActivityToDiscordActivity(bot: Bot, payload: Activity): DiscordActivity { + return { + name: payload.name, + type: payload.type, + url: payload.url ?? undefined, + created_at: payload.createdAt, + timestamps: { + start: payload.startedAt, + end: payload.endedAt, + }, + application_id: payload.applicationId ? bot.utils.bigintToSnowflake(payload.applicationId) : undefined, + details: payload.details ?? undefined, + state: payload.state ?? undefined, + emoji: payload.emoji + ? { + name: payload.emoji.name, + animated: payload.emoji.animated, + id: payload.emoji.id ? bot.utils.bigintToSnowflake(payload.emoji.id) : undefined, + } + : undefined, + party: { + id: payload.partyId, + size: payload.partyCurrentSize && payload.partyMaxSize + ? [payload.partyCurrentSize, payload.partyMaxSize] + : undefined, + }, + assets: { + large_image: payload.largeImage, + large_text: payload.largeText, + small_image: payload.largeImage, + small_text: payload.largeText, + }, + secrets: { + join: payload.join, + spectate: payload.spectate, + match: payload.match, + }, + instance: payload.instance, + flags: payload.flags, + buttons: payload.buttons, + }; +} diff --git a/transformers/reverse/application.ts b/transformers/reverse/application.ts new file mode 100644 index 000000000..db746e255 --- /dev/null +++ b/transformers/reverse/application.ts @@ -0,0 +1,26 @@ +import { Bot } from "../../bot.ts"; +import { DiscordApplication } from "../../types/discord.ts"; +import { Application } from "../application.ts"; + +export function transformApplicationToDiscordApplication(bot: Bot, payload: Application): DiscordApplication { + return { + name: payload.name, + description: payload.description, + rpc_origins: payload.rpcOrigins, + bot_public: payload.botPublic, + bot_require_code_grant: payload.botRequireCodeGrant, + terms_of_service_url: payload.termsOfServiceUrl, + privacy_policy_url: payload.privacyPolicyUrl, + verify_key: payload.verifyKey, + primary_sku_id: payload.primarySkuId, + slug: payload.slug, + cover_image: payload.coverImage ? bot.utils.iconBigintToHash(payload.coverImage) : undefined, + flags: payload.flags, + + id: bot.utils.bigintToSnowflake(payload.id), + icon: payload.icon ? bot.utils.iconBigintToHash(payload.icon) : null, + owner: payload.owner ? bot.transformers.reverse.user(bot, payload.owner) : undefined, + team: payload.team ? bot.transformers.reverse.team(bot, payload.team) : null, + guild_id: payload.guildId ? bot.utils.bigintToSnowflake(payload.guildId) : undefined, + }; +} diff --git a/transformers/reverse/member.ts b/transformers/reverse/member.ts new file mode 100644 index 000000000..c3aa8aa18 --- /dev/null +++ b/transformers/reverse/member.ts @@ -0,0 +1,38 @@ +import type { Bot } from "../../bot.ts"; +import { DiscordMember, DiscordUser } from "../../types/discord.ts"; +import { Member, User } from "../member.ts"; + +export function transformUserToDiscordUser(bot: Bot, payload: User): DiscordUser { + return { + id: bot.utils.bigintToSnowflake(payload.id), + username: payload.username, + discriminator: payload.discriminator, + avatar: payload.avatar ? bot.utils.iconBigintToHash(payload.avatar) : null, + locale: payload.locale, + email: payload.email ?? undefined, + flags: payload.flags, + premium_type: payload.premiumType, + public_flags: payload.publicFlags, + bot: payload.toggles.bot, + system: payload.toggles.system, + mfa_enabled: payload.toggles.mfaEnabled, + verified: payload.toggles.verified, + }; +} + +export function transformMemberToDiscordMember(bot: Bot, payload: Member): DiscordMember { + return { + nick: payload.nick ?? undefined, + roles: payload.roles.map((id) => bot.utils.bigintToSnowflake(id)), + joined_at: new Date(payload.joinedAt).toISOString(), + premium_since: payload.premiumSince ? new Date(payload.premiumSince).toISOString() : undefined, + avatar: payload.avatar ? bot.utils.iconBigintToHash(payload.avatar) : undefined, + permissions: payload.permissions ? bot.utils.bigintToSnowflake(payload.permissions) : undefined, + communication_disabled_until: payload.communicationDisabledUntil + ? new Date(payload.communicationDisabledUntil).toISOString() + : undefined, + deaf: payload.toggles.deaf, + mute: payload.toggles.mute, + pending: payload.toggles.pending, + }; +} diff --git a/transformers/reverse/mod.ts b/transformers/reverse/mod.ts new file mode 100644 index 000000000..01b32d0f1 --- /dev/null +++ b/transformers/reverse/mod.ts @@ -0,0 +1,6 @@ +export * from "./activity.ts"; +export * from "./application.ts"; +export * from "./component.ts"; +export * from "./embed.ts"; +export * from "./member.ts"; +export * from "./team.ts"; diff --git a/transformers/reverse/team.ts b/transformers/reverse/team.ts new file mode 100644 index 000000000..dd3ad6f99 --- /dev/null +++ b/transformers/reverse/team.ts @@ -0,0 +1,21 @@ +import { Bot } from "../../bot.ts"; +import { DiscordTeam } from "../../types/discord.ts"; +import { Team } from "../team.ts"; + +export function transformTeamToDiscordTeam(bot: Bot, payload: Team): DiscordTeam { + const id = bot.utils.bigintToSnowflake(payload.id); + + return { + name: payload.name, + + id, + icon: payload.icon ? bot.utils.iconBigintToHash(payload.icon) : null, + owner_user_id: bot.utils.bigintToSnowflake(payload.ownerUserId), + members: payload.members.map((member) => ({ + membership_state: member.membershipState, + permissions: member.permissions, + team_id: id, + user: bot.transformers.reverse.user(bot, member.user), + })), + }; +} From aa2e5131ef86f51419558bf40e7a5cca32b4d647 Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Fri, 17 Jun 2022 13:01:26 -0400 Subject: [PATCH 2/2] fix: test local queue idea (#2301) * fix: test local queue idea * I HATE STUPIDEST DENO FMT * try new error * fix: url * fix: with proper headers * fix: cleanup --- bot.ts | 5 + .../interactions/sendInteractionResponse.ts | 111 ++++--------- rest/createRequestBody.ts | 53 +++--- rest/mod.ts | 1 + rest/processGlobalQueue.ts | 128 ++------------- rest/rest.ts | 4 +- rest/restManager.ts | 3 + rest/runMethod.ts | 4 +- rest/sendRequest.ts | 155 ++++++++++++++++++ transformers/reverse/allowedMentions.ts | 13 ++ transformers/reverse/embed.ts | 1 - types/discord.ts | 2 +- 12 files changed, 257 insertions(+), 223 deletions(-) create mode 100644 rest/sendRequest.ts create mode 100644 transformers/reverse/allowedMentions.ts diff --git a/bot.ts b/bot.ts index 37c2ffc48..ce19dd5f2 100644 --- a/bot.ts +++ b/bot.ts @@ -69,6 +69,7 @@ import { transformStageInstance } from "./transformers/stageInstance.ts"; import { StickerPack, transformSticker, transformStickerPack } from "./transformers/sticker.ts"; import { GetGatewayBot, transformGatewayBot } from "./transformers/gatewayBot.ts"; import { + DiscordAllowedMentions, DiscordApplicationCommandOptionChoice, DiscordAutoModerationActionExecution, DiscordAutoModerationRule, @@ -150,6 +151,8 @@ import { transformAutoModerationActionExecution, } from "./transformers/automodActionExecution.ts"; import { routes } from "./util/routes.ts"; +import { transformAllowedMentionsToDiscordAllowedMentions } from "./transformers/reverse/allowedMentions.ts"; +import { AllowedMentions } from "./mod.ts"; export function createBot(options: CreateBotOptions): Bot { const bot = { @@ -398,6 +401,7 @@ export function createBaseHelpers(options: Partial) { export interface Transformers { reverse: { + allowedMentions: (bot: Bot, payload: AllowedMentions) => DiscordAllowedMentions; embed: (bot: Bot, payload: Embed) => DiscordEmbed; component: (bot: Bot, payload: Component) => DiscordComponent; activity: (bot: Bot, payload: Activity) => DiscordActivity; @@ -456,6 +460,7 @@ export interface Transformers { export function createTransformers(options: Partial) { return { reverse: { + allowedMentions: options.reverse?.allowedMentions || transformAllowedMentionsToDiscordAllowedMentions, embed: options.reverse?.embed || transformEmbedToDiscordEmbed, component: options.reverse?.component || transformComponentToDiscordComponent, activity: options.reverse?.activity || transformActivityToDiscordActivity, diff --git a/helpers/interactions/sendInteractionResponse.ts b/helpers/interactions/sendInteractionResponse.ts index b7aeafc5e..8ae1665c3 100644 --- a/helpers/interactions/sendInteractionResponse.ts +++ b/helpers/interactions/sendInteractionResponse.ts @@ -23,99 +23,46 @@ export async function sendInteractionResponse( // DRY code a little bit const data = { - content: options.data.content, tts: options.data.tts, - embeds: options.data.embeds?.map((embed) => bot.transformers.reverse.embed(bot, embed)), - allowed_mentions: { - parse: options.data.allowedMentions!.parse, - replied_user: options.data.allowedMentions!.repliedUser, - users: options.data.allowedMentions!.users?.map((id) => id.toString()), - roles: options.data.allowedMentions!.roles?.map((id) => id.toString()), - }, - custom_id: options.data.customId, title: options.data.title, - components: options.data.components?.map((component) => ({ - type: component.type, - components: component.components.map((subComponent) => { - if (subComponent.type === MessageComponentTypes.InputText) { - return { - type: subComponent.type, - style: subComponent.style, - custom_id: subComponent.customId, - label: subComponent.label, - placeholder: subComponent.placeholder, - value: subComponent.value, - min_length: subComponent.minLength, - max_length: subComponent.maxLength, - required: subComponent.required, - }; - } - - if (subComponent.type === MessageComponentTypes.SelectMenu) { - return { - type: subComponent.type, - custom_id: subComponent.customId, - placeholder: subComponent.placeholder, - min_values: subComponent.minValues, - max_values: subComponent.maxValues, - options: subComponent.options.map((option) => ({ - label: option.label, - value: option.value, - description: option.description, - emoji: option.emoji - ? { - id: option.emoji.id?.toString(), - name: option.emoji.name, - animated: option.emoji.animated, - } - : undefined, - default: option.default, - })), - }; - } - - return { - type: subComponent.type, - custom_id: subComponent.customId, - label: subComponent.label, - style: subComponent.style, - emoji: "emoji" in subComponent && subComponent.emoji - ? { - id: subComponent.emoji.id?.toString(), - name: subComponent.emoji.name, - animated: subComponent.emoji.animated, - } - : undefined, - url: "url" in subComponent ? subComponent.url : undefined, - disabled: "disabled" in subComponent ? subComponent.disabled : undefined, - }; - }), - })), flags: options.data.flags, + content: options.data.content, choices: options.data.choices, + custom_id: options.data.customId, + embeds: options.data.embeds?.map((embed) => bot.transformers.reverse.embed(bot, embed)), + allowed_mentions: bot.transformers.reverse.allowedMentions(bot, options.data.allowedMentions!), + components: options.data.components?.map((component) => bot.transformers.reverse.component(bot, component)), }; // A reply has never been send if (bot.cache.unrepliedInteractions.delete(id)) { - return await bot.rest.runMethod( - bot.rest, - "POST", - bot.constants.routes.INTERACTION_ID_TOKEN(id, token), - { - type: options.type, - data, - file: options.data.file, - }, - ); + return await bot.rest.sendRequest(bot.rest, { + url: bot.constants.routes.INTERACTION_ID_TOKEN(id, token), + method: "POST", + payload: bot.rest.createRequestBody(bot.rest, { + method: "POST", + body: { type: options.type, data, file: options.data.file }, + headers: { + // remove authorization header + Authorization: "", + }, + }), + }); } // If its already been executed, we need to send a followup response - const result = await bot.rest.runMethod( - bot.rest, - "POST", - bot.constants.routes.WEBHOOK(bot.applicationId, token), - { ...data, file: options.data.file }, - ); + const result = await bot.rest.sendRequest(bot.rest, { + url: bot.constants.routes.WEBHOOK(bot.applicationId, token), + method: "POST", + payload: bot.rest.createRequestBody(bot.rest, { + method: "POST", + body: { ...data, file: options.data.file }, + headers: { + // remove authorization header + Authorization: "", + }, + }), + }); return bot.transformers.message(bot, result); } diff --git a/rest/createRequestBody.ts b/rest/createRequestBody.ts index 49c65b04c..97855debd 100644 --- a/rest/createRequestBody.ts +++ b/rest/createRequestBody.ts @@ -1,58 +1,67 @@ import { RestManager } from "./restManager.ts"; import { FileContent } from "../types/discordeno.ts"; import { USER_AGENT } from "../util/constants.ts"; -import { RestPayload, RestRequest } from "./rest.ts"; +import { RequestMethod, RestPayload, RestRequest } from "./rest.ts"; /** Creates the request body and headers that are necessary to send a request. Will handle different types of methods and everything necessary for discord. */ -export function createRequestBody(rest: RestManager, queuedRequest: { request: RestRequest; payload: RestPayload }) { +// export function createRequestBody(rest: RestManager, queuedRequest: { request: RestRequest; payload: RestPayload }) { +export function createRequestBody(rest: RestManager, options: CreateRequestBodyOptions) { const headers: Record = { - authorization: `Bot ${rest.token}`, "user-agent": USER_AGENT, }; + if (!options.unauthorized) headers["authorization"] = `Bot ${rest.token}`; + // SOMETIMES SPECIAL HEADERS (E.G. CUSTOM AUTHORIZATION) NEED TO BE USED - if (queuedRequest.payload.headers) { - for (const key in queuedRequest.payload.headers) { - headers[key] = queuedRequest.payload.headers[key]; + if (options.headers) { + for (const key in options.headers) { + headers[key.toLowerCase()] = options.headers[key]; } } // GET METHODS SHOULD NOT HAVE A BODY - if (queuedRequest.request.method === "GET") { - queuedRequest.payload.body = undefined; + if (options.method === "GET") { + options.body = undefined; } // IF A REASON IS PROVIDED ENCODE IT IN HEADERS - if (queuedRequest.payload.body?.reason) { - headers["X-Audit-Log-Reason"] = encodeURIComponent(queuedRequest.payload.body.reason as string); - queuedRequest.payload.body.reason = undefined; + if (options.body?.reason) { + headers["X-Audit-Log-Reason"] = encodeURIComponent(options.body.reason as string); + options.body.reason = undefined; } // IF A FILE/ATTACHMENT IS PRESENT WE NEED SPECIAL HANDLING - if (queuedRequest.payload.body?.file) { - if (!Array.isArray(queuedRequest.payload.body.file)) { - queuedRequest.payload.body.file = [queuedRequest.payload.body.file]; + if (options.body?.file) { + if (!Array.isArray(options.body.file)) { + options.body.file = [options.body.file]; } const form = new FormData(); - for (let i = 0; i < (queuedRequest.payload.body.file as FileContent[]).length; i++) { + for (let i = 0; i < (options.body.file as FileContent[]).length; i++) { form.append( `file${i}`, - (queuedRequest.payload.body.file as FileContent[])[i].blob, - (queuedRequest.payload.body.file as FileContent[])[i].name, + (options.body.file as FileContent[])[i].blob, + (options.body.file as FileContent[])[i].name, ); } - form.append("payload_json", JSON.stringify({ ...queuedRequest.payload.body, file: undefined })); - queuedRequest.payload.body.file = form; - } else if (queuedRequest.payload.body && !["GET", "DELETE"].includes(queuedRequest.request.method)) { + form.append("payload_json", JSON.stringify({ ...options.body, file: undefined })); + options.body.file = form; + } else if (options.body && !["GET", "DELETE"].includes(options.method)) { headers["Content-Type"] = "application/json"; } return { headers, - body: (queuedRequest.payload.body?.file ?? JSON.stringify(queuedRequest.payload.body)) as FormData | string, - method: queuedRequest.request.method, + body: (options.body?.file ?? JSON.stringify(options.body)) as FormData | string, + method: options.method, }; } + +export interface CreateRequestBodyOptions { + headers?: Record; + method: RequestMethod; + body?: Record; + unauthorized?: boolean; +} diff --git a/rest/mod.ts b/rest/mod.ts index f881eacf4..5713808c9 100644 --- a/rest/mod.ts +++ b/rest/mod.ts @@ -11,3 +11,4 @@ export * from "./restManager.ts"; export * from "./runMethod.ts"; export * from "./simplifyUrl.ts"; export * from "./convertRestError.ts"; +export * from "./sendRequest.ts"; diff --git a/rest/processGlobalQueue.ts b/rest/processGlobalQueue.ts index ef8f9bb04..8877ad875 100644 --- a/rest/processGlobalQueue.ts +++ b/rest/processGlobalQueue.ts @@ -60,120 +60,20 @@ export async function processGlobalQueue(rest: RestManager) { continue; } - try { - // CUSTOM HANDLER FOR USER TO LOG OR WHATEVER WHENEVER A FETCH IS MADE - rest.debug(`[REST - fetching] URL: ${request.urlToUse} | ${JSON.stringify(request.payload)}`); - - const response = await fetch(request.urlToUse, rest.createRequestBody(rest, request)); - rest.debug(`[REST - fetched] URL: ${request.urlToUse} | ${JSON.stringify(request.payload)}`); - - const bucketIdFromHeaders = rest.processRequestHeaders(rest, request.basicURL, response.headers); - // SET THE BUCKET Id IF IT WAS PRESENT - if (bucketIdFromHeaders) { - request.payload.bucketId = bucketIdFromHeaders; - } - - if (response.status < 200 || response.status >= 400) { - rest.debug( - `[REST - httpError] Payload: ${JSON.stringify(request.payload)} | Response: ${JSON.stringify(response)}`, - ); - - let error = "REQUEST_UNKNOWN_ERROR"; - switch (response.status) { - case HTTPResponseCodes.BadRequest: - error = "The request was improperly formatted, or the server couldn't understand it."; - break; - case HTTPResponseCodes.Unauthorized: - error = "The Authorization header was missing or invalid."; - break; - case HTTPResponseCodes.Forbidden: - error = "The Authorization token you passed did not have permission to the resource."; - break; - case HTTPResponseCodes.NotFound: - error = "The resource at the location specified doesn't exist."; - break; - case HTTPResponseCodes.MethodNotAllowed: - error = "The HTTP method used is not valid for the location specified."; - break; - case HTTPResponseCodes.GatewayUnavailable: - error = "There was not a gateway available to process your request. Wait a bit and retry."; - break; - } - - if ( - rest.invalidRequestErrorStatuses.includes(response.status) && - !(response.status === 429 && response.headers.get("X-RateLimit-Scope")) - ) { - // INCREMENT CURRENT INVALID REQUESTS - ++rest.invalidRequests; - - if (!rest.invalidRequestsTimeoutId) { - rest.invalidRequestsTimeoutId = setTimeout(() => { - rest.debug(`[REST - processGlobalQueue] Resetting invalid requests counter in setTimeout.`); - rest.invalidRequests = 0; - rest.invalidRequestsTimeoutId = 0; - }, rest.invalidRequestsInterval); - } - } - - // If NOT rate limited remove from queue - if (response.status !== 429) { - let json = undefined; - if (response.type) { - json = JSON.stringify(await response.json()); - } - request.request.reject({ - ok: false, - status: response.status, - error, - body: json, - }); - } else { - if (request.payload.retryCount++ >= rest.maxRetryCount) { - rest.debug(`[REST - RetriesMaxed] ${JSON.stringify(request.payload)}`); - // REMOVE ITEM FROM QUEUE TO PREVENT RETRY - request.request.reject({ - ok: false, - status: response.status, - error: "The request was rate limited and it maxed out the retries limit.", - }); - continue; - } - - // WAS RATE LIMITED. PUSH TO END OF GLOBAL QUEUE, SO WE DON'T BLOCK OTHER REQUESTS. - rest.globalQueue.push(request); - } - - continue; - } - - // SOMETIMES DISCORD RETURNS AN EMPTY 204 RESPONSE THAT CAN'T BE MADE TO JSON - if (response.status === 204) { - rest.debug(`[REST - FetchSuccess] URL: ${request.urlToUse} | ${JSON.stringify(request.payload)}`); - request.request.respond({ - ok: true, - status: 204, - }); - } else { - // CONVERT THE RESPONSE TO JSON - const json = JSON.stringify(await response.json()); - - rest.debug(`[REST - fetchSuccess] ${JSON.stringify(request.payload)}`); - request.request.respond({ - ok: true, - status: 200, - body: json, - }); - } - } catch (error) { - // SOMETHING WENT WRONG, LOG AND RESPOND WITH ERROR - rest.debug(`[REST - fetchFailed] Payload: ${JSON.stringify(request.payload)} | Error: ${error}`); - request.request.reject({ - ok: false, - status: 599, - error: "Internal Proxy Error", - }); - } + await rest.sendRequest(rest, { + url: request.urlToUse, + method: request.request.method, + bucketId: request.payload.bucketId, + reject: request.request.reject, + respond: request.request.respond, + retryCount: request.payload.retryCount ?? 0, + payload: rest.createRequestBody(rest, { + method: request.request.method, + body: request.payload.body, + }), + }) + // Should be handled in sendRequest, this catch just prevents bots from dying + .catch(() => null); } // ALLOW OTHER QUEUES TO START WHEN NEW REQUEST IS MADE diff --git a/rest/rest.ts b/rest/rest.ts index 7b189ae5d..cc7257f13 100644 --- a/rest/rest.ts +++ b/rest/rest.ts @@ -1,6 +1,6 @@ export interface RestRequest { url: string; - method: string; + method: RequestMethod; respond: (payload: RestRequestResponse) => unknown; reject: (payload: RestRequestRejection) => unknown; } @@ -27,3 +27,5 @@ export interface RestRateLimitedPath { resetTimestamp: number; bucketId?: string; } + +export type RequestMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; diff --git a/rest/restManager.ts b/rest/restManager.ts index 07861c1e4..efe4c8d12 100644 --- a/rest/restManager.ts +++ b/rest/restManager.ts @@ -13,6 +13,7 @@ import { simplifyUrl } from "./simplifyUrl.ts"; import { baseEndpoints } from "../util/constants.ts"; import { API_VERSION } from "../util/constants.ts"; import { removeTokenPrefix } from "../util/token.ts"; +import { sendRequest } from "./sendRequest.ts"; export function createRestManager(options: CreateRestManagerOptions) { const version = options.version || API_VERSION; @@ -73,6 +74,7 @@ export function createRestManager(options: CreateRestManagerOptions) { simplifyUrl: options.simplifyUrl || simplifyUrl, processGlobalQueue: options.processGlobalQueue || processGlobalQueue, convertRestError: options.convertRestError || convertRestError, + sendRequest: options.sendRequest || sendRequest, }; } @@ -94,6 +96,7 @@ export interface CreateRestManagerOptions { simplifyUrl?: typeof simplifyUrl; processGlobalQueue?: typeof processGlobalQueue; convertRestError?: typeof convertRestError; + sendRequest?: typeof sendRequest; } export type RestManager = ReturnType; diff --git a/rest/runMethod.ts b/rest/runMethod.ts index 498878717..f1c3d18e5 100644 --- a/rest/runMethod.ts +++ b/rest/runMethod.ts @@ -1,10 +1,10 @@ import { RestManager } from "./restManager.ts"; import { API_VERSION, BASE_URL, baseEndpoints } from "../util/constants.ts"; -import { RestRequestRejection, RestRequestResponse } from "./rest.ts"; +import { RequestMethod, RestRequestRejection, RestRequestResponse } from "./rest.ts"; export async function runMethod( rest: RestManager, - method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", + method: RequestMethod, route: string, body?: unknown, options?: { diff --git a/rest/sendRequest.ts b/rest/sendRequest.ts new file mode 100644 index 000000000..ef140821b --- /dev/null +++ b/rest/sendRequest.ts @@ -0,0 +1,155 @@ +import { HTTPResponseCodes } from "../types/shared.ts"; +import { BASE_URL } from "../util/constants.ts"; +import { RequestMethod } from "./rest.ts"; +import { RestManager } from "./restManager.ts"; + +export interface RestSendRequestOptions { + url: string; + method: RequestMethod; + bucketId?: string; + reject?: Function; + respond?: Function; + retryCount?: number; + payload?: { + headers: Record; + body: string | FormData; + }; +} + +export async function sendRequest(rest: RestManager, options: RestSendRequestOptions): Promise { + try { + // CUSTOM HANDLER FOR USER TO LOG OR WHATEVER WHENEVER A FETCH IS MADE + rest.debug(`[REST - fetching] URL: ${options.url} | ${JSON.stringify(options)}`); + + const response = await fetch( + options.url.startsWith(BASE_URL) ? options.url : `${BASE_URL}/v${rest.version}/${options.url}`, + { + method: options.method, + headers: options.payload?.headers, + body: options.payload?.body, + }, + ); + rest.debug(`[REST - fetched] URL: ${options.url} | ${JSON.stringify(options)}`); + + const bucketIdFromHeaders = rest.processRequestHeaders( + rest, + rest.simplifyUrl(options.url, options.method), + response.headers, + ); + // SET THE BUCKET Id IF IT WAS PRESENT + if (bucketIdFromHeaders) { + options.bucketId = bucketIdFromHeaders; + } + + if (response.status < 200 || response.status >= 400) { + rest.debug( + `[REST - httpError] Payload: ${JSON.stringify(options)} | Response: ${JSON.stringify(response)}`, + ); + + let error = "REQUEST_UNKNOWN_ERROR"; + switch (response.status) { + case HTTPResponseCodes.BadRequest: + error = "The options was improperly formatted, or the server couldn't understand it."; + break; + case HTTPResponseCodes.Unauthorized: + error = "The Authorization header was missing or invalid."; + break; + case HTTPResponseCodes.Forbidden: + error = "The Authorization token you passed did not have permission to the resource."; + break; + case HTTPResponseCodes.NotFound: + error = "The resource at the location specified doesn't exist."; + break; + case HTTPResponseCodes.MethodNotAllowed: + error = "The HTTP method used is not valid for the location specified."; + break; + case HTTPResponseCodes.GatewayUnavailable: + error = "There was not a gateway available to process your options. Wait a bit and retry."; + break; + } + + if ( + rest.invalidRequestErrorStatuses.includes(response.status) && + !(response.status === 429 && response.headers.get("X-RateLimit-Scope")) + ) { + // INCREMENT CURRENT INVALID REQUESTS + ++rest.invalidRequests; + + if (!rest.invalidRequestsTimeoutId) { + rest.invalidRequestsTimeoutId = setTimeout(() => { + rest.debug(`[REST - processGlobalQueue] Resetting invalid optionss counter in setTimeout.`); + rest.invalidRequests = 0; + rest.invalidRequestsTimeoutId = 0; + }, rest.invalidRequestsInterval); + } + } + + // If NOT rate limited remove from queue + if (response.status !== 429) { + options.reject?.({ + ok: false, + status: response.status, + error, + body: response.type ? JSON.stringify(await response.json()) : undefined, + }); + + throw new Error( + JSON.stringify({ + ok: false, + status: response.status, + error, + body: response.type ? JSON.stringify(await response.json()) : undefined, + }), + ); + } else { + if (options.retryCount && options.retryCount++ >= rest.maxRetryCount) { + rest.debug(`[REST - RetriesMaxed] ${JSON.stringify(options)}`); + // REMOVE ITEM FROM QUEUE TO PREVENT RETRY + options.reject?.({ + ok: false, + status: response.status, + error: "The options was rate limited and it maxed out the retries limit.", + }); + + // @ts-ignore Code should never reach here + return; + } + } + } + + // SOMETIMES DISCORD RETURNS AN EMPTY 204 RESPONSE THAT CAN'T BE MADE TO JSON + if (response.status === 204) { + rest.debug(`[REST - FetchSuccess] URL: ${options.url} | ${JSON.stringify(options)}`); + options.respond?.({ + ok: true, + status: 204, + }); + // @ts-ignore 204 will be void + return; + } else { + // CONVERT THE RESPONSE TO JSON + const json = JSON.stringify(await response.json()); + + rest.debug(`[REST - fetchSuccess] ${JSON.stringify(options)}`); + options.respond?.({ + ok: true, + status: 200, + body: json, + }); + + return JSON.parse(json); + } + } catch (error) { + // SOMETHING WENT WRONG, LOG AND RESPOND WITH ERROR + rest.debug(`[REST - fetchFailed] Payload: ${JSON.stringify(options)} | Error: ${error}`); + options.reject?.({ + ok: false, + status: 599, + error: "Internal Proxy Error", + }); + + throw new Error("Something went wrong in sendRequest", { + cause: error, + }); + } +} diff --git a/transformers/reverse/allowedMentions.ts b/transformers/reverse/allowedMentions.ts new file mode 100644 index 000000000..690a68fd0 --- /dev/null +++ b/transformers/reverse/allowedMentions.ts @@ -0,0 +1,13 @@ +import { AllowedMentions, Bot, DiscordAllowedMentions } from "../../mod.ts"; + +export function transformAllowedMentionsToDiscordAllowedMentions( + bot: Bot, + mentions: AllowedMentions, +): DiscordAllowedMentions { + return { + parse: mentions.parse, + replied_user: mentions.repliedUser, + users: mentions.users?.map((id) => id.toString()), + roles: mentions.roles?.map((id) => id.toString()), + }; +} diff --git a/transformers/reverse/embed.ts b/transformers/reverse/embed.ts index ac7aeee92..02bc31fdd 100644 --- a/transformers/reverse/embed.ts +++ b/transformers/reverse/embed.ts @@ -1,6 +1,5 @@ import { Bot } from "../../bot.ts"; import { DiscordEmbed } from "../../types/discord.ts"; -import { Optionalize } from "../../types/shared.ts"; import { Embed } from "../embed.ts"; export function transformEmbedToDiscordEmbed(bot: Bot, payload: Embed): DiscordEmbed { diff --git a/types/discord.ts b/types/discord.ts index 6d8286807..47b85414b 100644 --- a/types/discord.ts +++ b/types/discord.ts @@ -308,7 +308,7 @@ export interface DiscordAllowedMentions { /** An array of allowed mention types to parse from the content. */ parse?: AllowedMentionsTypes[]; /** For replies, whether to mention the author of the message being replied to (default false) */ - repliedUser?: boolean; + replied_user?: boolean; /** Array of role_ids to mention (Max size of 100) */ roles?: string[];