From c70dc733e24e5ac32f7659d17ae5ec9d04f5bcff Mon Sep 17 00:00:00 2001 From: March 7th <71698422+aiko-chan-ai@users.noreply.github.com> Date: Sun, 27 Nov 2022 19:45:58 +0700 Subject: [PATCH] feat: Using new attachment API --- src/client/Client.js | 3 +++ src/managers/GuildForumThreadManager.js | 24 +++++++++++++++-- src/managers/MessageManager.js | 23 +++++++++++++++- src/structures/MessagePayload.js | 9 +++++++ src/structures/interfaces/TextBasedChannel.js | 26 +++++++++++++++++-- src/util/Options.js | 2 ++ src/util/Util.js | 20 ++++++++++++++ typings/index.d.ts | 3 +++ 8 files changed, 105 insertions(+), 5 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index ea60974..d0c372d 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -1036,6 +1036,9 @@ class Client extends BaseClient { if (options && options.password && typeof options.password !== 'string') { throw new TypeError('CLIENT_INVALID_OPTION', 'password', 'a string'); } + if (options && options.usingNewAttachmentAPI && typeof options.usingNewAttachmentAPI !== 'boolean') { + throw new TypeError('CLIENT_INVALID_OPTION', 'usingNewAttachmentAPI', 'a boolean'); + } if (options && options.interactionTimeout && typeof options.interactionTimeout !== 'number') { throw new TypeError('CLIENT_INVALID_OPTION', 'interactionTimeout', 'a number'); } diff --git a/src/managers/GuildForumThreadManager.js b/src/managers/GuildForumThreadManager.js index 83b1412..8e9f551 100644 --- a/src/managers/GuildForumThreadManager.js +++ b/src/managers/GuildForumThreadManager.js @@ -3,7 +3,7 @@ const ThreadManager = require('./ThreadManager'); const { TypeError } = require('../errors'); const MessagePayload = require('../structures/MessagePayload'); -const { resolveAutoArchiveMaxLimit } = require('../util/Util'); +const { resolveAutoArchiveMaxLimit, getAttachments, uploadFile } = require('../util/Util'); /** * Manages API methods for threads in forum channels and stores their cache. @@ -67,7 +67,27 @@ class GuildForumThreadManager extends ThreadManager { messagePayload = MessagePayload.create(this, message).resolveData(); } - const { data: body, files } = await messagePayload.resolveFiles(); + let { data: body, files } = await messagePayload.resolveFiles(); + + if (typeof message == 'object' && typeof message.usingNewAttachmentAPI !== 'boolean') { + message.usingNewAttachmentAPI = this.client.options.usingNewAttachmentAPI; + } + + if (message?.usingNewAttachmentAPI === true) { + const attachments = await getAttachments(this.client, this.channel.id, ...files); + const requestPromises = attachments.map(async attachment => { + await uploadFile(files[attachment.id].file, attachment.upload_url); + return { + id: attachment.id, + filename: files[attachment.id].name, + uploaded_filename: attachment.upload_filename, + }; + }); + const attachmentsData = await Promise.all(requestPromises); + attachmentsData.sort((a, b) => parseInt(a.id) - parseInt(b.id)); + body.attachments = attachmentsData; + files = []; + } if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild); diff --git a/src/managers/MessageManager.js b/src/managers/MessageManager.js index 9291ef7..803bc78 100644 --- a/src/managers/MessageManager.js +++ b/src/managers/MessageManager.js @@ -130,7 +130,28 @@ class MessageManager extends CachedManager { } else { messagePayload = await MessagePayload.create(message instanceof Message ? message : this, options).resolveData(); } - const { data, files } = await messagePayload.resolveFiles(); + let { data, files } = await messagePayload.resolveFiles(); + + if (typeof options == 'object' && typeof options.usingNewAttachmentAPI !== 'boolean') { + options.usingNewAttachmentAPI = this.client.options.usingNewAttachmentAPI; + } + + if (options?.usingNewAttachmentAPI === true) { + const attachments = await Util.getAttachments(this.client, this.channel.id, ...files); + const requestPromises = attachments.map(async attachment => { + await Util.uploadFile(files[attachment.id].file, attachment.upload_url); + return { + id: attachment.id, + filename: files[attachment.id].name, + uploaded_filename: attachment.upload_filename, + }; + }); + const attachmentsData = await Promise.all(requestPromises); + attachmentsData.sort((a, b) => parseInt(a.id) - parseInt(b.id)); + data.attachments = attachmentsData; + files = []; + } + const d = await this.client.api.channels[this.channel.id].messages[messageId].patch({ data, files }); const existing = this.cache.get(messageId); diff --git a/src/structures/MessagePayload.js b/src/structures/MessagePayload.js index b598624..cd27478 100644 --- a/src/structures/MessagePayload.js +++ b/src/structures/MessagePayload.js @@ -51,6 +51,15 @@ class MessagePayload { this.files = null; } + /** + * Whether or not using new API to upload files + * @type {boolean} + * @readonly + */ + get usingNewAttachmentAPI() { + return Boolean(this.options?.usingNewAttachmentAPI); + } + /** * Whether or not the target is a {@link Webhook} or a {@link WebhookClient} * @type {boolean} diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 3a70462..c2419df 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -9,7 +9,7 @@ const { Collection } = require('@discordjs/collection'); const { InteractionTypes, MaxBulkDeletableMessageAge } = require('../../util/Constants'); const { TypeError, Error } = require('../../errors'); const InteractionCollector = require('../InteractionCollector'); -const { lazy } = require('../../util/Util'); +const { lazy, getAttachments, uploadFile } = require('../../util/Util'); const Message = lazy(() => require('../Message').Message); const { s } = require('@sapphire/shapeshift'); const validateName = stringName => @@ -84,6 +84,7 @@ class TextBasedChannel { * @property {MessageActionRow[]|MessageActionRowOptions[]} [components] * Action rows containing interactive components for the message (buttons, select menus) * @property {MessageAttachment[]} [attachments] Attachments to send in the message + * @property {boolean} [usingNewAttachmentAPI] Whether to use the new attachment API (`channels/:id/attachments`) */ /** @@ -190,7 +191,28 @@ class TextBasedChannel { messagePayload = await MessagePayload.create(this, options).resolveData(); } - const { data, files } = await messagePayload.resolveFiles(); + let { data, files } = await messagePayload.resolveFiles(); + + if (typeof options == 'object' && typeof options.usingNewAttachmentAPI !== 'boolean') { + options.usingNewAttachmentAPI = this.client.options.usingNewAttachmentAPI; + } + + if (options?.usingNewAttachmentAPI === true) { + const attachments = await getAttachments(this.client, this.id, ...files); + const requestPromises = attachments.map(async attachment => { + await uploadFile(files[attachment.id].file, attachment.upload_url); + return { + id: attachment.id, + filename: files[attachment.id].name, + uploaded_filename: attachment.upload_filename, + }; + }); + const attachmentsData = await Promise.all(requestPromises); + attachmentsData.sort((a, b) => parseInt(a.id) - parseInt(b.id)); + data.attachments = attachmentsData; + files = []; + } + const d = await this.client.api.channels[this.id].messages.post({ data, files }); return this.messages.cache.get(d.id) ?? this.messages._add(d); diff --git a/src/util/Options.js b/src/util/Options.js index 35d5521..faea320 100644 --- a/src/util/Options.js +++ b/src/util/Options.js @@ -42,6 +42,7 @@ const { randomUA } = require('../util/Constants'); * @property {string} [captchaService=null] Captcha service to use for solving captcha {@link captchaServices} * @property {string} [captchaKey=null] Captcha service key * @property {string} [password=null] Your Discord account password + * @property {boolean} [usingNewAttachmentAPI=true] Use new attachment API * @property {string} [interactionTimeout=15000] The amount of time in milliseconds to wait for an interaction response, before rejecting * @property {boolean} [autoRedeemNitro=false] Automaticlly redeems nitro codes * @property {string} [proxy] Proxy to use for the WebSocket + REST connection (proxy-agent uri type) {@link https://www.npmjs.com/package/proxy-agent}. @@ -157,6 +158,7 @@ class Options extends null { DMSync: false, patchVoice: false, password: null, + usingNewAttachmentAPI: true, interactionTimeout: 15_000, waitGuildTimeout: 15_000, messageCreateEventGuildTimeout: 100, diff --git a/src/util/Util.js b/src/util/Util.js index 62dcc50..3e38b92 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -3,6 +3,7 @@ const { parse } = require('node:path'); const process = require('node:process'); const { Collection } = require('@discordjs/collection'); +const axios = require('axios'); const fetch = require('node-fetch'); const { Colors, Endpoints } = require('./Constants'); const Options = require('./Options'); @@ -728,6 +729,25 @@ class Util extends null { emoji_name: defaultReaction.name, }; } + + static async getAttachments(client, channelId, ...files) { + files = files.flat(2).map((file, i) => ({ + filename: file.name ?? file.attachment?.name ?? file.attachment?.filename ?? 'file.jpg', + // 8MB = 8388608 bytes + file_size: Math.floor((8_388_608 / 10) * Math.random()), + id: `${i}`, + })); + const { attachments } = await client.api.channels[channelId].attachments.post({ + data: { + files, + }, + }); + return attachments; + } + + static uploadFile(data, url) { + return axios.put(url, data); + } } module.exports = Util; diff --git a/typings/index.d.ts b/typings/index.d.ts index 5c62dfd..53f24db 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2220,6 +2220,7 @@ export class MessageMentions { export class MessagePayload { public constructor(target: MessageTarget, options: MessageOptions | WebhookMessageOptions); public data: RawMessagePayloadData | null; + public readonly usingNewAttachmentAPI: boolean; public readonly isUser: boolean; public readonly isWebhook: boolean; public readonly isMessage: boolean; @@ -4736,6 +4737,7 @@ export interface ClientOptions { captchaService?: string; captchaKey?: string; interactionTimeout?: number; + usingNewAttachmentAPI?: boolean; } // end copy @@ -6265,6 +6267,7 @@ export interface MessageOptions { stickers?: StickerResolvable[]; attachments?: MessageAttachment[]; flags?: BitFieldResolvable<'SUPPRESS_EMBEDS', number>; + usingNewAttachmentAPI?: boolean; } export type MessageReactionResolvable =