diff --git a/packages/bot/src/typings.ts b/packages/bot/src/typings.ts index aabf048f6..6fb04298f 100644 --- a/packages/bot/src/typings.ts +++ b/packages/bot/src/typings.ts @@ -104,8 +104,8 @@ export interface BotInteractionCallbackData { embeds?: Embed[] /** Allowed mentions for the message */ allowedMentions?: AllowedMentions - /** The contents of the file being sent */ - file?: FileContent | FileContent[] + /** The contents of the files being sent */ + files?: FileContent[] /** The customId you want to use for this modal response. */ customId?: string /** The title you want to use for this modal response. */ diff --git a/packages/rest/src/manager.ts b/packages/rest/src/manager.ts index 4f64bad74..30dde9404 100644 --- a/packages/rest/src/manager.ts +++ b/packages/rest/src/manager.ts @@ -691,16 +691,16 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage return obj }, - createRequest(options) { + createRequestBody(method, options) { const headers: Record = { 'user-agent': `DiscordBot (https://github.com/discordeno/discordeno, v${version})`, } - if (!options.unauthorized) headers.authorization = `Bot ${rest.token}` + if (!options?.unauthorized) headers.authorization = `Bot ${rest.token}` // IF A REASON IS PROVIDED ENCODE IT IN HEADERS - if (options.reason !== undefined) { - headers['x-audit-log-reason'] = encodeURIComponent(options.reason) + if (options?.reason !== undefined) { + headers['x-audit-log-reason'] = encodeURIComponent(options?.reason) } let body: string | FormData | undefined @@ -709,38 +709,36 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage // Since GET does not allow bodies // Have to check for attachments first, since body then has to be send in a different way. - if (options.attachments !== undefined) { + if (options?.files !== undefined) { const form = new FormData() - for (let i = 0; i < options.attachments.length; ++i) { - form.append(`file${i}`, options.attachments[i].blob, options.attachments[i].name) + for (let i = 0; i < options.files.length; ++i) { + form.append(`file${i}`, options.files[i].blob, options.files[i].name) } - form.append('payload_json', JSON.stringify(options.body)) + form.append('payload_json', JSON.stringify({ ...options.body, files: undefined })) body = form - // TODO: boundary? - // `multipart/form-data; boundary=${form.getBoundary()}` - headers['content-type'] = `multipart/form-data` - } else if (options.body !== undefined) { + // No need to set the `content-type` header since `fetch` does that automatically for us when we use a `FormData` object. + } else if (options?.body !== undefined) { if (options.body instanceof FormData) { body = options.body - headers['content-type'] = `multipart/form-data` + // No need to set the `content-type` header since `fetch` does that automatically for us when we use a `FormData` object. } else { - body = JSON.stringify(options.body) + body = JSON.stringify(rest.changeToDiscordFormat(options.body)) headers['content-type'] = `application/json` } } // SOMETIMES SPECIAL HEADERS (E.G. CUSTOM AUTHORIZATION) NEED TO BE USED - if (options.headers) { + if (options?.headers) { Object.assign(headers, options.headers) } return { body, headers, - method: options.method, + method, } }, @@ -852,7 +850,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage async sendRequest(options) { const url = options.url.startsWith('https://') ? options.url : `${rest.baseUrl}/v${rest.version}${options.url}` - const payload = rest.createRequest({ method: options.method, url: options.url, body: options.options?.body, ...options.options }) + const payload = rest.createRequestBody(options.method, options.requestBodyOptions) logger.debug(`sending request to ${url}`, 'with payload:', { ...payload, headers: { ...payload.headers, authorization: 'Bot tokenhere' } }) const response = await fetch(url, payload) @@ -986,7 +984,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage }) if (!result.ok) { - const err = (await result.json().catch(() => {})) as Record + const err = (await result.json().catch(() => { })) as Record // Legacy Handling to not break old code or when body is missing if (!err?.body) throw new Error(`Error: ${err.message ?? result.statusText}`) throw new Error(JSON.stringify(err)) @@ -999,9 +997,9 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage const payload: SendRequestOptions = { url, method, - options, + requestBodyOptions: options, retryCount: 0, - retryRequest: async function (payload: SendRequestOptions) { + retryRequest: async function(payload: SendRequestOptions) { rest.processRequest(payload) }, resolve: (data) => { @@ -1014,24 +1012,24 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage }) }, - async get>(url: string, body?: Omit) { - return camelize(await rest.makeRequest('GET', url, { body })) as Camelize + async get>(url: string, options?: Omit) { + return camelize(await rest.makeRequest('GET', url, options)) as Camelize }, - async post>(url: string, body?: Omit) { - return camelize(await rest.makeRequest('POST', url, { body })) as Camelize + async post>(url: string, options?: Omit) { + return camelize(await rest.makeRequest('POST', url, options)) as Camelize }, - async delete(url: string, body?: Omit) { - camelize(await rest.makeRequest('DELETE', url, { body })) + async delete(url: string, options?: Omit) { + camelize(await rest.makeRequest('DELETE', url, options)) }, - async patch>(url: string, body?: Omit) { - return camelize(await rest.makeRequest('PATCH', url, { body })) as Camelize + async patch>(url: string, options?: Omit) { + return camelize(await rest.makeRequest('PATCH', url, options)) as Camelize }, - async put(url: string, body?: Omit) { - return camelize(await rest.makeRequest('PUT', url, { body })) as Camelize + async put(url: string, options?: Omit) { + return camelize(await rest.makeRequest('PUT', url, options)) as Camelize }, async addReaction(channelId, messageId, reaction) { @@ -1110,7 +1108,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage }, async createForumThread(channelId, body) { - return await rest.post(rest.routes.channels.forum(channelId), { body }) + return await rest.post(rest.routes.channels.forum(channelId), { body, files: body.files }) }, async createInvite(channelId, body = {}) { @@ -1296,7 +1294,10 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage }, async editFollowupMessage(token, messageId, body) { - return await rest.patch(rest.routes.interactions.responses.message(rest.applicationId, token, messageId), { body }) + return await rest.patch(rest.routes.interactions.responses.message(rest.applicationId, token, messageId), { + body, + files: body.files, + }) }, async editGlobalApplicationCommand(commandId, body) { @@ -1330,7 +1331,10 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage }, async editOriginalInteractionResponse(token, body) { - return await rest.patch(rest.routes.interactions.responses.original(rest.applicationId, token), { body }) + return await rest.patch(rest.routes.interactions.responses.original(rest.applicationId, token), { + body, + files: body.files, + }) }, async editOriginalWebhookMessage(webhookId, token, options) { @@ -1339,6 +1343,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage type: InteractionResponseTypes.UpdateMessage, data: options, }, + files: options.files, }) }, @@ -1378,7 +1383,10 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage }, async editWebhookMessage(webhookId, token, messageId, options) { - return await rest.patch(rest.routes.webhooks.message(webhookId, token, messageId, options), { body: options }) + return await rest.patch(rest.routes.webhooks.message(webhookId, token, messageId, options), { + body: options, + files: options.files, + }) }, async editWebhookWithToken(webhookId, token, body) { @@ -1682,10 +1690,10 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage return await new Promise((resolve, reject) => { rest.sendRequest({ url: rest.routes.webhooks.webhook(rest.applicationId, token), - method: 'POST', - options: { body: options }, + method: 'POST', + requestBodyOptions: { body: options, files: options.files }, retryCount: 0, - retryRequest: async function (options: SendRequestOptions) { + retryRequest: async function(options: SendRequestOptions) { // TODO: should change to reprocess queue item await rest.sendRequest(options) }, @@ -1703,9 +1711,9 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage rest.sendRequest({ url: rest.routes.interactions.responses.callback(interactionId, token), method: 'POST', - options: { body: options }, + requestBodyOptions: { body: options }, retryCount: 0, - retryRequest: async function (options: SendRequestOptions) { + retryRequest: async function(options: SendRequestOptions) { // TODO: should change to reprocess queue item await rest.sendRequest(options) }, @@ -1718,7 +1726,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage }, async sendMessage(channelId, body) { - return await rest.post(rest.routes.channels.messages(channelId), { body }) + return await rest.post(rest.routes.channels.messages(channelId), { body, files: body.files }) }, async startThreadWithMessage(channelId, messageId, body) { diff --git a/packages/rest/src/types.ts b/packages/rest/src/types.ts index f6b717a5c..5151455b6 100644 --- a/packages/rest/src/types.ts +++ b/packages/rest/src/types.ts @@ -159,7 +159,7 @@ export interface RestManager { /** Reshapes and modifies the obj as needed to make it ready for discords api. */ changeToDiscordFormat: (obj: any) => any /** Creates the request body and headers that are necessary to send a request. Will handle different types of methods and everything necessary for discord. */ - createRequest: (options: CreateRequestBodyOptions) => RequestBody + createRequestBody: (method: RequestMethods, options?: CreateRequestBodyOptions) => RequestBody /** This will create a infinite loop running in 1 seconds using tail recursion to keep rate limits clean. When a rate limit resets, this will remove it so the queue can proceed. */ processRateLimitedPaths: () => void /** Processes the rate limit headers and determines if it needs to be rate limited and returns the bucket id if available */ @@ -2454,12 +2454,10 @@ export interface CreateWebhook { export interface CreateRequestBodyOptions { headers?: Record - method: RequestMethods body?: any unauthorized?: boolean - url?: string reason?: string - attachments?: FileContent[] + files?: FileContent[] } export interface RequestBody { @@ -2471,7 +2469,7 @@ export interface RequestBody { export interface SendRequestOptions { /** The url to send the request to. */ url: string - /** The method to use when sending the request. */ + /** The method to use for sending the request. */ method: RequestMethods /** The amount of times this request has been retried. */ retryCount: number @@ -2484,7 +2482,7 @@ export interface SendRequestOptions { /** If this request has a bucket id which it falls under for rate limit */ bucketId?: string /** Additional request options, used for things like overriding authorization header. */ - options?: Record + requestBodyOptions?: CreateRequestBodyOptions } export interface RestRateLimitedPath { diff --git a/packages/rest/tests/e2e/member.spec.ts b/packages/rest/tests/e2e/member.spec.ts index cea9f76c2..70b8c6b86 100644 --- a/packages/rest/tests/e2e/member.spec.ts +++ b/packages/rest/tests/e2e/member.spec.ts @@ -57,7 +57,7 @@ describe('Member tests', () => { it('Send a direct message', async () => { // DM test only on dd unit testing bot - if (rest.applicationId.toString() !== "770381961553510451") return; + if (rest.applicationId.toString() !== '770381961553510451') return // Itoh Alt ID const channel = await rest.getDmChannel(750661528360845322n) expect(channel?.id).to.exist diff --git a/packages/rest/tests/e2e/message.spec.ts b/packages/rest/tests/e2e/message.spec.ts index 4dd4487fb..8346d66e8 100644 --- a/packages/rest/tests/e2e/message.spec.ts +++ b/packages/rest/tests/e2e/message.spec.ts @@ -21,7 +21,7 @@ after(async () => { describe('Send a message', () => { it('With content', async () => { - const message = await rest.sendMessage('1041029705790402611', { content: 'testing rate limit manager' }) + const message = await rest.sendMessage(e2ecache.channel.id, { content: 'testing rate limit manager' }) expect(message.content).to.be.equal('testing rate limit manager') const edited = await rest.editMessage(message.channelId, message.id, { content: 'testing rate limit manager edited' }) @@ -37,7 +37,7 @@ describe('Send a message', () => { expect(image).to.not.be.undefined if (!image) throw new Error('Was not able to fetch the image.') - const message = await rest.sendMessage('1041029705790402611', { file: { blob: image, name: 'gamer' } }) + const message = await rest.sendMessage(e2ecache.channel.id, { files: [{ blob: image, name: 'gamer' }] }) expect(message.attachments.length).to.be.greaterThan(0) const [attachment] = message.attachments diff --git a/packages/rest/tests/e2e/utils.ts b/packages/rest/tests/e2e/utils.ts index 3e18c3e55..c693a7e91 100644 --- a/packages/rest/tests/e2e/utils.ts +++ b/packages/rest/tests/e2e/utils.ts @@ -11,8 +11,12 @@ export const rest = createRestManager({ }) rest.deleteQueueDelay = 10000 +const guild = await rest.createGuild({ name: 'ddenotester' }); +const channel = await rest.createChannel(guild.id, { name: "ddenotestchannel" }); + export const e2ecache = { - guild: await rest.createGuild({ name: 'ddenotester' }), + guild, + channel, deletedGuild: false, communityGuildId: E2E_TEST_GUILD_ID, } diff --git a/packages/types/src/discord.ts b/packages/types/src/discord.ts index a9a557863..22fd787bb 100644 --- a/packages/types/src/discord.ts +++ b/packages/types/src/discord.ts @@ -1,4 +1,3 @@ -import type { FileContent } from './discordeno.js' import type { ActivityTypes, AllowedMentionsTypes, @@ -2564,8 +2563,6 @@ export interface DiscordCreateForumPostWithMessage { components?: DiscordMessageComponents[] /** IDs of up to 3 stickers in the server to send in the message */ sticker_ids?: string[] - /** Contents of the file being sent. See {@link https://discord.com/developers/docs/reference#uploading-files Uploading Files} */ - file: FileContent | FileContent[] | undefined /** JSON-encoded body of non-file params, only for multipart/form-data requests. See {@link https://discord.com/developers/docs/reference#uploading-files Uploading Files} */ payload_json?: string /** Attachment objects with filename and description. See {@link https://discord.com/developers/docs/reference#uploading-files Uploading Files} */ diff --git a/packages/types/src/discordeno.ts b/packages/types/src/discordeno.ts index 080e1640f..021618fa3 100644 --- a/packages/types/src/discordeno.ts +++ b/packages/types/src/discordeno.ts @@ -62,8 +62,8 @@ export interface CreateMessageOptions { /** When sending, whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message, default true */ failIfNotExists: boolean } - /** The contents of the file being sent */ - file?: FileContent | FileContent[] + /** The contents of the files being sent */ + files?: FileContent[] /** The components you would like to have sent in this message */ components?: MessageComponents /** IDs of up to 3 stickers in the server to send in the message */ @@ -431,8 +431,8 @@ export interface InteractionCallbackData { embeds?: Array> /** Allowed mentions for the message */ allowedMentions?: AllowedMentions - /** The contents of the file being sent */ - file?: FileContent | FileContent[] + /** The contents of the files being sent */ + files?: FileContent[] /** The customId you want to use for this modal response. */ customId?: string /** The title you want to use for this modal response. */ @@ -673,8 +673,8 @@ export interface ExecuteWebhook { avatarUrl?: string /** True if this is a TTS message */ tts?: boolean - /** The contents of the file being sent */ - file?: FileContent | FileContent[] + /** The contents of the files being sent */ + files?: FileContent[] /** Embedded `rich` content */ embeds?: Array> /** Allowed mentions for the message */ @@ -706,8 +706,8 @@ export interface CreateForumPostWithMessage extends WithReason { embeds?: Array> /** Allowed mentions for the message */ allowedMentions?: AllowedMentions - /** The contents of the file being sent */ - file?: FileContent | FileContent[] + /** The contents of the files being sent */ + files?: FileContent[] /** The components you would like to have sent in this message */ components?: MessageComponents } @@ -890,8 +890,8 @@ export interface EditMessage { embeds?: Array> | null /** Edit the flags of the message (only `SUPPRESS_EMBEDS` can currently be set/unset) */ flags?: 4 | null - /** The contents of the file being sent/edited */ - file?: FileContent | FileContent[] | null + /** The contents of the files being sent/edited */ + files?: FileContent[] | null /** Allowed mentions for the message */ allowedMentions?: AllowedMentions /** When specified (adding new attachments), attachments which are not provided in this list will be removed. */