diff --git a/package.json b/package.json index bdf9019..3988aef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "discord.js-selfbot-v13", - "version": "0.2.4", + "version": "0.3.0", "description": "A unofficial discord.js fork for creating selfbots [Based on discord.js v13]", "main": "./src/index.js", "types": "./typings/index.d.ts", diff --git a/src/managers/MessageManager.js b/src/managers/MessageManager.js index ca928b0..9331d05 100644 --- a/src/managers/MessageManager.js +++ b/src/managers/MessageManager.js @@ -9,49 +9,6 @@ const MessagePayload = require('../structures/MessagePayload'); const Util = require('../util/Util'); const DiscordAPIError = require('../rest/DiscordAPIError'); -const _edit = (client, channelID, messageID, data, files) => { - return new Promise((resolve, reject) => { - require('axios')({ - method: 'patch', - url: `${client.options.http.api}/v${client.options.http.version}/channels/${channelID}/messages/${messageID}`, - headers: { - authorization: client.token, - Accept: '*/*', - 'Accept-Language': 'en-US,en;q=0.9', - 'Cache-Control': 'no-cache', - Pragma: 'no-cache', - Referer: 'https://discord.com/channels/@me', - 'Sec-Ch-Ua': '" Not A;Brand";v="99" "', - 'Sec-Ch-Ua-Mobile': '?0', - 'Sec-Ch-Ua-Platform': '"iOS"', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'X-Debug-Options': 'bugReporterEnabled', - 'X-Discord-Locale': 'en-US', - Origin: 'https://discord.com', - }, - data, - files, - }) - .then((res) => resolve(res.data)) - .catch((err) => { - err.request.options = { - data, - files, - }; - return reject( - new DiscordAPIError( - err.response.data, - err.response.status, - err.request, - ), - ); - }); - }); -}; - - /** * Manages API methods for Messages and holds their cache. * @extends {CachedManager} @@ -174,8 +131,7 @@ class MessageManager extends CachedManager { ) .resolveBody() .resolveFiles(); - // const d = await this.client.api.channels(this.channel.id).messages(messageId).patch({ body, files }); - const d = await _edit(this.client, this.channel.id, messageId, body, files); + const d = await this.client.api.channels(this.channel.id).messages(messageId).patch({ body, files }); const existing = this.cache.get(messageId); if (existing) { const clone = existing._clone(); diff --git a/src/rest/APIRequest.js b/src/rest/APIRequest.js index beeeb50..4a73346 100644 --- a/src/rest/APIRequest.js +++ b/src/rest/APIRequest.js @@ -2,7 +2,6 @@ const https = require('node:https'); const { setTimeout } = require('node:timers'); -const FormData = require('form-data'); const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); const { UserAgent } = require('../util/Constants'); @@ -47,23 +46,32 @@ class APIRequest { let body; if (this.options.files?.length) { - body = new FormData(); - for (const [index, file] of this.options.files.entries()) { - if (file?.file) body.append(file.key ?? `files[${index}]`, file.file, file.name); - } - if (typeof this.options.data !== 'undefined') { - if (this.options.dontUsePayloadJSON) { - for (const [key, value] of Object.entries(this.options.data)) body.append(key, value); - } else { - body.append('payload_json', JSON.stringify(this.options.data)); - } - } - headers = Object.assign(headers, body.getHeaders()); - // eslint-disable-next-line eqeqeq - } else if (this.options.data != null) { - body = JSON.stringify(this.options.data); - headers['Content-Type'] = 'application/json'; - } + body = new FormData(); + for (const [index, file] of this.options.files.entries()) { + if (file?.file) + body.append(file.key ?? `files[${index}]`, file.file, file.name); + } + if ( + typeof this.options.data !== 'undefined' || + typeof this.options.body !== 'undefined' + ) { + if (this.options.dontUsePayloadJSON) { + for (const [key, value] of Object.entries(this.options.data || this.options.body)) { + body.append(key, value); + } + } else { + body.append('payload_json', JSON.stringify(this.options.data || this.options.body)); + } + } + headers = Object.assign(headers, body.getHeaders()); + // eslint-disable-next-line eqeqeq + } else if (this.options.data != null) { + body = JSON.stringify(this.options.data); + headers['Content-Type'] = 'application/json'; + } else if (this.options.body != null) { + body = JSON.stringify(this.options.body); + headers['Content-Type'] = 'application/json'; + } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), this.client.options.restRequestTimeout).unref(); diff --git a/src/structures/MessagePayload.js b/src/structures/MessagePayload.js index 5380277..0713a2a 100644 --- a/src/structures/MessagePayload.js +++ b/src/structures/MessagePayload.js @@ -12,186 +12,204 @@ const Util = require('../util/Util'); * Represents a message to be sent to the API. */ class MessagePayload { - /** - * @param {MessageTarget} target The target for this message to be sent to - * @param {MessageOptions|WebhookMessageOptions} options Options passed in from send - */ - constructor(target, options) { - /** - * The target for this message to be sent to - * @type {MessageTarget} - */ - this.target = target; + /** + * @param {MessageTarget} target The target for this message to be sent to + * @param {MessageOptions|WebhookMessageOptions} options Options passed in from send + */ + constructor(target, options) { + /** + * The target for this message to be sent to + * @type {MessageTarget} + */ + this.target = target; - /** - * Options passed in from send - * @type {MessageOptions|WebhookMessageOptions} - */ - this.options = options; + /** + * Options passed in from send + * @type {MessageOptions|WebhookMessageOptions} + */ + this.options = options; - /** - * Body sendable to the API - * @type {?APIMessage} - */ - this.body = null; + /** + * Body sendable to the API + * @type {?APIMessage} + */ + this.body = null; - /** - * Files sendable to the API - * @type {?RawFile[]} - */ - this.files = null; - } + /** + * Files sendable to the API + * @type {?RawFile[]} + */ + this.files = null; + } - /** - * Whether or not the target is a {@link Webhook} or a {@link WebhookClient} - * @type {boolean} - * @readonly - */ - get isWebhook() { - const Webhook = require('./Webhook'); - const WebhookClient = require('../client/WebhookClient'); - return this.target instanceof Webhook || this.target instanceof WebhookClient; - } + /** + * Whether or not the target is a {@link Webhook} or a {@link WebhookClient} + * @type {boolean} + * @readonly + */ + get isWebhook() { + const Webhook = require('./Webhook'); + const WebhookClient = require('../client/WebhookClient'); + return ( + this.target instanceof Webhook || this.target instanceof WebhookClient + ); + } - /** - * Whether or not the target is a {@link User} - * @type {boolean} - * @readonly - */ - get isUser() { - const User = require('./User'); - const { GuildMember } = require('./GuildMember'); - return this.target instanceof User || this.target instanceof GuildMember; - } + /** + * Whether or not the target is a {@link User} + * @type {boolean} + * @readonly + */ + get isUser() { + const User = require('./User'); + const { GuildMember } = require('./GuildMember'); + return this.target instanceof User || this.target instanceof GuildMember; + } - /** - * Whether or not the target is a {@link Message} - * @type {boolean} - * @readonly - */ - get isMessage() { - const { Message } = require('./Message'); - return this.target instanceof Message; - } + /** + * Whether or not the target is a {@link Message} + * @type {boolean} + * @readonly + */ + get isMessage() { + const { Message } = require('./Message'); + return this.target instanceof Message; + } - /** - * Whether or not the target is a {@link MessageManager} - * @type {boolean} - * @readonly - */ - get isMessageManager() { - const MessageManager = require('../managers/MessageManager'); - return this.target instanceof MessageManager; - } + /** + * Whether or not the target is a {@link MessageManager} + * @type {boolean} + * @readonly + */ + get isMessageManager() { + const MessageManager = require('../managers/MessageManager'); + return this.target instanceof MessageManager; + } - /** - * Whether or not the target is an {@link Interaction} or an {@link InteractionWebhook} - * @type {boolean} - * @readonly - */ - get isInteraction() { - const Interaction = require('./Interaction'); - const InteractionWebhook = require('./InteractionWebhook'); - return this.target instanceof Interaction || this.target instanceof InteractionWebhook; - } + /** + * Whether or not the target is an {@link Interaction} or an {@link InteractionWebhook} + * @type {boolean} + * @readonly + */ + get isInteraction() { + const Interaction = require('./Interaction'); + const InteractionWebhook = require('./InteractionWebhook'); + return ( + this.target instanceof Interaction || + this.target instanceof InteractionWebhook + ); + } - /** - * Makes the content of this message. - * @returns {?string} - */ - makeContent() { - let content; - if (this.options.content === null) { - content = ''; - } else if (typeof this.options.content !== 'undefined') { - content = Util.verifyString(this.options.content, RangeError, 'MESSAGE_CONTENT_TYPE', false); - } + /** + * Makes the content of this message. + * @returns {?string} + */ + makeContent() { + let content; + if (this.options.content === null) { + content = ''; + } else if (typeof this.options.content !== 'undefined') { + content = Util.verifyString( + this.options.content, + RangeError, + 'MESSAGE_CONTENT_TYPE', + false, + ); + } - return content; - } - /** - * Resolves the body. - * @returns {MessagePayload} - */ - resolveBody() { - if (this.data) return this; - const isInteraction = this.isInteraction; - const isWebhook = this.isWebhook; + return content; + } + /** + * Resolves the body. + * @returns {MessagePayload} + */ + resolveBody() { + if (this.data) return this; + const isInteraction = this.isInteraction; + const isWebhook = this.isWebhook; - const content = this.makeContent(); - const tts = Boolean(this.options.tts); + const content = this.makeContent(); + const tts = Boolean(this.options.tts); - let nonce; - if (typeof this.options.nonce !== 'undefined') { - nonce = this.options.nonce; - // eslint-disable-next-line max-len - if (typeof nonce === 'number' ? !Number.isInteger(nonce) : typeof nonce !== 'string') { - throw new RangeError('MESSAGE_NONCE_TYPE'); - } - } + let nonce; + if (typeof this.options.nonce !== 'undefined') { + nonce = this.options.nonce; + // eslint-disable-next-line max-len + if ( + typeof nonce === 'number' + ? !Number.isInteger(nonce) + : typeof nonce !== 'string' + ) { + throw new RangeError('MESSAGE_NONCE_TYPE'); + } + } - const components = this.options.components?.map((c) => + const components = this.options.components?.map((c) => BaseMessageComponent.create(c).toJSON(), ); - let username; - let avatarURL; - if (isWebhook) { - username = this.options.username ?? this.target.name; - if (this.options.avatarURL) avatarURL = this.options.avatarURL; - } + let username; + let avatarURL; + if (isWebhook) { + username = this.options.username ?? this.target.name; + if (this.options.avatarURL) avatarURL = this.options.avatarURL; + } - let flags; - if ( - typeof this.options.flags !== 'undefined' || - (this.isMessage && typeof this.options.reply === 'undefined') || - this.isMessageManager - ) { - flags = - // eslint-disable-next-line eqeqeq - this.options.flags != null - ? new MessageFlagsBitField(this.options.flags).bitfield - : this.target.flags?.bitfield; - } + let flags; + if ( + typeof this.options.flags !== 'undefined' || + (this.isMessage && typeof this.options.reply === 'undefined') || + this.isMessageManager + ) { + flags = + // eslint-disable-next-line eqeqeq + this.options.flags != null + ? new MessageFlagsBitField(this.options.flags).bitfield + : this.target.flags?.bitfield; + } - if (isInteraction && this.options.ephemeral) { - flags |= MessageFlags.Ephemeral; - } + if (isInteraction && this.options.ephemeral) { + flags |= MessageFlags.Ephemeral; + } - let allowedMentions = - typeof this.options.allowedMentions === 'undefined' - ? this.target.client.options.allowedMentions - : this.options.allowedMentions; + let allowedMentions = + typeof this.options.allowedMentions === 'undefined' + ? this.target.client.options.allowedMentions + : this.options.allowedMentions; - if (allowedMentions) { - allowedMentions = Util.cloneObject(allowedMentions); - allowedMentions.replied_user = allowedMentions.repliedUser; - delete allowedMentions.repliedUser; - } + if (allowedMentions) { + allowedMentions = Util.cloneObject(allowedMentions); + allowedMentions.replied_user = allowedMentions.repliedUser; + delete allowedMentions.repliedUser; + } - let message_reference; - if (typeof this.options.reply === 'object') { - const reference = this.options.reply.messageReference; - const message_id = this.isMessage ? reference.id ?? reference : this.target.messages.resolveId(reference); - if (message_id) { - message_reference = { - message_id, - fail_if_not_exists: this.options.reply.failIfNotExists ?? this.target.client.options.failIfNotExists, - }; - } - } + let message_reference; + if (typeof this.options.reply === 'object') { + const reference = this.options.reply.messageReference; + const message_id = this.isMessage + ? reference.id ?? reference + : this.target.messages.resolveId(reference); + if (message_id) { + message_reference = { + message_id, + fail_if_not_exists: + this.options.reply.failIfNotExists ?? + this.target.client.options.failIfNotExists, + }; + } + } - const attachments = this.options.files?.map((file, index) => ({ - id: index.toString(), - description: file.description, - })); - if (Array.isArray(this.options.attachments)) { - this.options.attachments.push(...(attachments ?? [])); - } else { - this.options.attachments = attachments; - } + const attachments = this.options.files?.map((file, index) => ({ + id: index.toString(), + description: file.description, + })); + if (Array.isArray(this.options.attachments)) { + this.options.attachments.push(...(attachments ?? [])); + } else { + this.options.attachments = attachments; + } - this.body = { + this.body = { content, tts, nonce, @@ -213,68 +231,74 @@ class MessagePayload { (sticker) => sticker.id ?? sticker, ), }; - return this; - } + return this; + } - /** - * Resolves files. - * @returns {Promise} - */ - async resolveFiles() { - if (this.files) return this; + /** + * Resolves files. + * @returns {Promise} + */ + async resolveFiles() { + if (this.files) return this; - this.files = await Promise.all(this.options.files?.map(file => this.constructor.resolveFile(file)) ?? []); - return this; - } + this.files = await Promise.all( + this.options.files?.map((file) => this.constructor.resolveFile(file)) ?? + [], + ); + return this; + } - /** - * Resolves a single file into an object sendable to the API. - * @param {BufferResolvable|Stream|FileOptions|MessageAttachment} fileLike Something that could be resolved to a file - * @returns {Promise} - */ - static async resolveFile(fileLike) { - let attachment; - let name; + /** + * Resolves a single file into an object sendable to the API. + * @param {BufferResolvable|Stream|FileOptions|MessageAttachment} fileLike Something that could be resolved to a file + * @returns {Promise} + */ + static async resolveFile(fileLike) { + let attachment; + let name; - const findName = thing => { - if (typeof thing === 'string') { - return Util.basename(thing); - } + const findName = (thing) => { + if (typeof thing === 'string') { + return Util.basename(thing); + } - if (thing.path) { - return Util.basename(thing.path); - } + if (thing.path) { + return Util.basename(thing.path); + } - return 'file.jpg'; - }; + return 'file.jpg'; + }; - const ownAttachment = - typeof fileLike === 'string' || fileLike instanceof Buffer || typeof fileLike.pipe === 'function'; - if (ownAttachment) { - attachment = fileLike; - name = findName(attachment); - } else { - attachment = fileLike.attachment; - name = fileLike.name ?? findName(attachment); - } + const ownAttachment = + typeof fileLike === 'string' || + fileLike instanceof Buffer || + typeof fileLike.pipe === 'function'; + if (ownAttachment) { + attachment = fileLike; + name = findName(attachment); + } else { + attachment = fileLike.attachment; + name = fileLike.name ?? findName(attachment); + } - const data = await DataResolver.resolveFile(attachment); - return { data, name }; - } - - /** - * Creates a {@link MessagePayload} from user-level arguments. - * @param {MessageTarget} target Target to send to - * @param {string|MessageOptions|WebhookMessageOptions} options Options or content to use - * @param {MessageOptions|WebhookMessageOptions} [extra={}] Extra options to add onto specified options - * @returns {MessagePayload} - */ - static create(target, options, extra = {}) { - return new this( - target, - typeof options !== 'object' || options === null ? { content: options, ...extra } : { ...options, ...extra }, - ); - } + const resource = await DataResolver.resolveFile(attachment); + return { attachment, name, file: resource }; + } + /** + * Creates a {@link MessagePayload} from user-level arguments. + * @param {MessageTarget} target Target to send to + * @param {string|MessageOptions|WebhookMessageOptions} options Options or content to use + * @param {MessageOptions|WebhookMessageOptions} [extra={}] Extra options to add onto specified options + * @returns {MessagePayload} + */ + static create(target, options, extra = {}) { + return new this( + target, + typeof options !== 'object' || options === null + ? { content: options, ...extra } + : { ...options, ...extra }, + ); + } } module.exports = MessagePayload; diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 1c40feb..72b1478 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -7,562 +7,493 @@ const { Error } = require('../errors'); const DataResolver = require('../util/DataResolver'); const DiscordAPIError = require('../rest/DiscordAPIError'); -const _send = ( - client, - webhookID, - webhookToken, - data, - files, - query, - auth = false, -) => { - return new Promise((resolve, reject) => { - require('axios')({ - method: 'post', - url: `${client.options.http.api}/v${ - client.options.http.version - }/webhooks/${webhookID}/${webhookToken}${query ? `?${query}` : ''}`, - headers: { - authorization: client.token, - Accept: '*/*', - 'Accept-Language': 'en-US,en;q=0.9', - 'Cache-Control': 'no-cache', - Pragma: 'no-cache', - Referer: 'https://discord.com/channels/@me', - 'Sec-Ch-Ua': '" Not A;Brand";v="99" "', - 'Sec-Ch-Ua-Mobile': '?0', - 'Sec-Ch-Ua-Platform': '"iOS"', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'X-Debug-Options': 'bugReporterEnabled', - 'X-Discord-Locale': 'en-US', - Origin: 'https://discord.com', - }, - data, - files, - auth, - }) - .then((res) => resolve(res.data)) - .catch((err) => { - err.request.options = { - data, - files, - }; - return reject( - new DiscordAPIError( - err.response.data, - err.response.status, - err.request, - ), - ); - }); - }); -}; -const _edit = ( - client, - webhookID, - webhookToken, - messageID, - data, - files, - query, - auth = false, -) => { - return new Promise((resolve, reject) => { - require('axios')({ - method: 'patch', - url: `${client.options.http.api}/v${client.options.http.version}/webhooks/${webhookID}/${webhookToken}/messages/${messageID}${query ? `?${query}` : ''}`, - headers: { - authorization: client.token, - Accept: '*/*', - 'Accept-Language': 'en-US,en;q=0.9', - 'Cache-Control': 'no-cache', - Pragma: 'no-cache', - Referer: 'https://discord.com/channels/@me', - 'Sec-Ch-Ua': '" Not A;Brand";v="99" "', - 'Sec-Ch-Ua-Mobile': '?0', - 'Sec-Ch-Ua-Platform': '"iOS"', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'X-Debug-Options': 'bugReporterEnabled', - 'X-Discord-Locale': 'en-US', - Origin: 'https://discord.com', - }, - data, - files, - auth, - }) - .then((res) => resolve(res.data)) - .catch((err) => { - err.request.options = { - data, - files, - }; - return reject( - new DiscordAPIError( - err.response.data, - err.response.status, - err.request, - ), - ); - }); - }); -}; /** * Represents a webhook. */ class Webhook { - constructor(client, data) { - /** - * The client that instantiated the webhook - * @name Webhook#client - * @type {Client} - * @readonly - */ - Object.defineProperty(this, 'client', { value: client }); - if (data) this._patch(data); - } + constructor(client, data) { + /** + * The client that instantiated the webhook + * @name Webhook#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + if (data) this._patch(data); + } - _patch(data) { - if ('name' in data) { - /** - * The name of the webhook - * @type {string} - */ - this.name = data.name; - } + _patch(data) { + if ('name' in data) { + /** + * The name of the webhook + * @type {string} + */ + this.name = data.name; + } - /** - * The token for the webhook, unavailable for follower webhooks and webhooks owned by another application. - * @name Webhook#token - * @type {?string} - */ - Object.defineProperty(this, 'token', { value: data.token ?? null, writable: true, configurable: true }); + /** + * The token for the webhook, unavailable for follower webhooks and webhooks owned by another application. + * @name Webhook#token + * @type {?string} + */ + Object.defineProperty(this, 'token', { + value: data.token ?? null, + writable: true, + configurable: true, + }); - if ('avatar' in data) { - /** - * The avatar for the webhook - * @type {?string} - */ - this.avatar = data.avatar; - } + if ('avatar' in data) { + /** + * The avatar for the webhook + * @type {?string} + */ + this.avatar = data.avatar; + } - /** - * The webhook's id - * @type {Snowflake} - */ - this.id = data.id; + /** + * The webhook's id + * @type {Snowflake} + */ + this.id = data.id; - if ('type' in data) { - /** - * The type of the webhook - * @type {WebhookType} - */ - this.type = data.type; - } + if ('type' in data) { + /** + * The type of the webhook + * @type {WebhookType} + */ + this.type = data.type; + } - if ('guild_id' in data) { - /** - * The guild the webhook belongs to - * @type {Snowflake} - */ - this.guildId = data.guild_id; - } + if ('guild_id' in data) { + /** + * The guild the webhook belongs to + * @type {Snowflake} + */ + this.guildId = data.guild_id; + } - if ('channel_id' in data) { - /** - * The channel the webhook belongs to - * @type {Snowflake} - */ - this.channelId = data.channel_id; - } + if ('channel_id' in data) { + /** + * The channel the webhook belongs to + * @type {Snowflake} + */ + this.channelId = data.channel_id; + } - if ('user' in data) { - /** - * The owner of the webhook - * @type {?(User|APIUser)} - */ - this.owner = this.client.users?._add(data.user) ?? data.user; - } else { - this.owner ??= null; - } + if ('user' in data) { + /** + * The owner of the webhook + * @type {?(User|APIUser)} + */ + this.owner = this.client.users?._add(data.user) ?? data.user; + } else { + this.owner ??= null; + } - if ('application_id' in data) { - /** - * The application that created this webhook - * @type {?Snowflake} - */ - this.applicationId = data.application_id; - } else { - this.applicationId ??= null; - } + if ('application_id' in data) { + /** + * The application that created this webhook + * @type {?Snowflake} + */ + this.applicationId = data.application_id; + } else { + this.applicationId ??= null; + } - if ('source_guild' in data) { - /** - * The source guild of the webhook - * @type {?(Guild|APIGuild)} - */ - this.sourceGuild = this.client.guilds?.resolve(data.source_guild.id) ?? data.source_guild; - } else { - this.sourceGuild ??= null; - } + if ('source_guild' in data) { + /** + * The source guild of the webhook + * @type {?(Guild|APIGuild)} + */ + this.sourceGuild = + this.client.guilds?.resolve(data.source_guild.id) ?? data.source_guild; + } else { + this.sourceGuild ??= null; + } - if ('source_channel' in data) { - /** - * The source channel of the webhook - * @type {?(NewsChannel|APIChannel)} - */ - this.sourceChannel = this.client.channels?.resolve(data.source_channel?.id) ?? data.source_channel; - } else { - this.sourceChannel ??= null; - } - } + if ('source_channel' in data) { + /** + * The source channel of the webhook + * @type {?(NewsChannel|APIChannel)} + */ + this.sourceChannel = + this.client.channels?.resolve(data.source_channel?.id) ?? + data.source_channel; + } else { + this.sourceChannel ??= null; + } + } - /** - * Options that can be passed into send. - * @typedef {BaseMessageOptions} WebhookMessageOptions - * @property {string} [username=this.name] Username override for the message - * @property {string} [avatarURL] Avatar URL override for the message - * @property {Snowflake} [threadId] The id of the thread in the channel to send to. - * For interaction webhooks, this property is ignored - * @property {MessageFlags} [flags] Which flags to set for the message. Only `SUPPRESS_EMBEDS` can be set. - */ + /** + * Options that can be passed into send. + * @typedef {BaseMessageOptions} WebhookMessageOptions + * @property {string} [username=this.name] Username override for the message + * @property {string} [avatarURL] Avatar URL override for the message + * @property {Snowflake} [threadId] The id of the thread in the channel to send to. + * For interaction webhooks, this property is ignored + * @property {MessageFlags} [flags] Which flags to set for the message. Only `SUPPRESS_EMBEDS` can be set. + */ - /** - * Options that can be passed into editMessage. - * @typedef {Object} WebhookEditMessageOptions - * @property {Embed[]|APIEmbed[]} [embeds] See {@link WebhookMessageOptions#embeds} - * @property {string} [content] See {@link BaseMessageOptions#content} - * @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] See {@link BaseMessageOptions#files} - * @property {MessageMentionOptions} [allowedMentions] See {@link BaseMessageOptions#allowedMentions} - * @property {MessageAttachment[]} [attachments] Attachments to send with the message - * @property {ActionRow[]|ActionRowOptions[]} [components] - * Action rows containing interactive components for the message (buttons, select menus) - * @property {Snowflake} [threadId] The id of the thread this message belongs to - * For interaction webhooks, this property is ignored - */ + /** + * Options that can be passed into editMessage. + * @typedef {Object} WebhookEditMessageOptions + * @property {Embed[]|APIEmbed[]} [embeds] See {@link WebhookMessageOptions#embeds} + * @property {string} [content] See {@link BaseMessageOptions#content} + * @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] See {@link BaseMessageOptions#files} + * @property {MessageMentionOptions} [allowedMentions] See {@link BaseMessageOptions#allowedMentions} + * @property {MessageAttachment[]} [attachments] Attachments to send with the message + * @property {ActionRow[]|ActionRowOptions[]} [components] + * Action rows containing interactive components for the message (buttons, select menus) + * @property {Snowflake} [threadId] The id of the thread this message belongs to + * For interaction webhooks, this property is ignored + */ - /** - * Sends a message with this webhook. - * @param {string|MessagePayload|WebhookMessageOptions} options The options to provide - * @returns {Promise} - * @example - * // Send a basic message - * webhook.send('hello!') - * .then(message => console.log(`Sent message: ${message.content}`)) - * .catch(console.error); - * @example - * // Send a basic message in a thread - * webhook.send({ content: 'hello!', threadId: '836856309672348295' }) - * .then(message => console.log(`Sent message: ${message.content}`)) - * .catch(console.error); - * @example - * // Send a remote file - * webhook.send({ - * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] - * }) - * .then(console.log) - * .catch(console.error); - * @example - * // Send a local file - * webhook.send({ - * files: [{ - * attachment: 'entire/path/to/file.jpg', - * name: 'file.jpg' - * }] - * }) - * .then(console.log) - * .catch(console.error); - * @example - * // Send an embed with a local image inside - * webhook.send({ - * content: 'This is an embed', - * embeds: [{ - * thumbnail: { - * url: 'attachment://file.jpg' - * } - * }], - * files: [{ - * attachment: 'entire/path/to/file.jpg', - * name: 'file.jpg' - * }] - * }) - * .then(console.log) - * .catch(console.error); - */ - async send(options) { - if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); + /** + * Sends a message with this webhook. + * @param {string|MessagePayload|WebhookMessageOptions} options The options to provide + * @returns {Promise} + * @example + * // Send a basic message + * webhook.send('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + * @example + * // Send a basic message in a thread + * webhook.send({ content: 'hello!', threadId: '836856309672348295' }) + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + * @example + * // Send a remote file + * webhook.send({ + * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send a local file + * webhook.send({ + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * }] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send an embed with a local image inside + * webhook.send({ + * content: 'This is an embed', + * embeds: [{ + * thumbnail: { + * url: 'attachment://file.jpg' + * } + * }], + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * }] + * }) + * .then(console.log) + * .catch(console.error); + */ + async send(options) { + if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); - let messagePayload; + let messagePayload; - if (options instanceof MessagePayload) { - messagePayload = options.resolveBody(); - } else { - messagePayload = MessagePayload.create(this, options).resolveBody(); - } + if (options instanceof MessagePayload) { + messagePayload = options.resolveBody(); + } else { + messagePayload = MessagePayload.create(this, options).resolveBody(); + } - const query = new URLSearchParams({ wait: true }); + const query = new URLSearchParams({ wait: true }); - if (messagePayload.options.threadId) { - query.set('thread_id', messagePayload.options.threadId); - } + if (messagePayload.options.threadId) { + query.set('thread_id', messagePayload.options.threadId); + } - const { body, files } = await messagePayload.resolveFiles(); - const d = await _send(this.client, this.id, this.token, body, files, query, false); - return this.client.channels?.cache.get(d.channel_id)?.messages._add(d, false) ?? d; - } - - /** - * Sends a raw slack message with this webhook. - * @param {Object} body The raw body to send - * @returns {Promise} - * @example - * // Send a slack message - * webhook.sendSlackMessage({ - * 'username': 'Wumpus', - * 'attachments': [{ - * 'pretext': 'this looks pretty cool', - * 'color': '#F0F', - * 'footer_icon': 'http://snek.s3.amazonaws.com/topSnek.png', - * 'footer': 'Powered by sneks', - * 'ts': Date.now() / 1_000 - * }] - * }).catch(console.error); - * @see {@link https://api.slack.com/messaging/webhooks} - */ - async sendSlackMessage(body) { - if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); - - const data = await this.client.api.webhooks(this.id, this.token).slack.post({ - query: new URLSearchParams({ wait: true }), - auth: false, - body, - }); - return data.toString() === 'ok'; - } - - /** - * Options used to edit a {@link Webhook}. - * @typedef {Object} WebhookEditData - * @property {string} [name=this.name] The new name for the webhook - * @property {?(BufferResolvable)} [avatar] The new avatar for the webhook - * @property {GuildTextChannelResolvable} [channel] The new channel for the webhook - */ - - /** - * Edits this webhook. - * @param {WebhookEditData} options Options for editing the webhook - * @param {string} [reason] Reason for editing the webhook - * @returns {Promise} - */ - async edit({ name = this.name, avatar, channel }, reason) { - if (avatar && !(typeof avatar === 'string' && avatar.startsWith('data:'))) { - avatar = await DataResolver.resolveImage(avatar); - } - channel &&= channel.id ?? channel; - const data = await this.client.api.webhooks(this.id, channel ? undefined : this.token).patch({ - data: { name, avatar, channel_id: channel }, - reason, - auth: !this.token || Boolean(channel), - }); - - this.name = data.name; - this.avatar = data.avatar; - this.channelId = data.channel_id; - return this; - } - - /** - * Options that can be passed into fetchMessage. - * @typedef {options} WebhookFetchMessageOptions - * @property {boolean} [cache=true] Whether to cache the message. - * @property {Snowflake} [threadId] The id of the thread this message belongs to. - * For interaction webhooks, this property is ignored - */ - - /** - * Gets a message that was sent by this webhook. - * @param {Snowflake|'@original'} message The id of the message to fetch - * @param {WebhookFetchMessageOptions} [options={}] The options to provide to fetch the message. - * @returns {Promise} Returns the raw message data if the webhook was instantiated as a - * {@link WebhookClient} or if the channel is uncached, otherwise a {@link Message} will be returned - */ - async fetchMessage(message, { cache = true, threadId } = {}) { - if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); - - const data = await this.client.api.webhooks(this.id, this.token).messages(message).get({ - query: threadId - ? new URLSearchParams({ - thread_id: threadId, - }) - : undefined, - auth: false - }); - return this.client.channels?.cache.get(data.channel_id)?.messages._add(data, cache) ?? data; - } - - /** - * Edits a message that was sent by this webhook. - * @param {MessageResolvable|'@original'} message The message to edit - * @param {string|MessagePayload|WebhookEditMessageOptions} options The options to provide - * @returns {Promise} Returns the raw message data if the webhook was instantiated as a - * {@link WebhookClient} or if the channel is uncached, otherwise a {@link Message} will be returned - */ - async editMessage(message, options) { - if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); - - let messagePayload; - - if (options instanceof MessagePayload) messagePayload = options; - else messagePayload = MessagePayload.create(this, options); - - const { body, files } = await messagePayload.resolveBody().resolveFiles(); - - /* - const d = await this.client.api.webhooks(this.id, this.token).messages(typeof message === 'string' ? message : message.id).patch({ - body, files, query: messagePayload.options.threadId ? new URLSearchParams({ thread_id: messagePayload.options.threadId }) : undefined, auth: false - }); - */ - - const d = await _edit( - this.client, - this.id, - this.token, - typeof message === 'string' ? message : message.id, + const { body, files } = await messagePayload.resolveFiles(); + const d = await this.client.api.webhooks(this.id, this.token).post({ body, files, - messagePayload.options.threadId - ? new URLSearchParams({ thread_id: messagePayload.options.threadId }) - : undefined, - false + query, + auth: false, + }); + return ( + this.client.channels?.cache.get(d.channel_id)?.messages._add(d, false) ?? + d ); + } - const messageManager = this.client.channels?.cache.get(d.channel_id)?.messages; - if (!messageManager) return d; + /** + * Sends a raw slack message with this webhook. + * @param {Object} body The raw body to send + * @returns {Promise} + * @example + * // Send a slack message + * webhook.sendSlackMessage({ + * 'username': 'Wumpus', + * 'attachments': [{ + * 'pretext': 'this looks pretty cool', + * 'color': '#F0F', + * 'footer_icon': 'http://snek.s3.amazonaws.com/topSnek.png', + * 'footer': 'Powered by sneks', + * 'ts': Date.now() / 1_000 + * }] + * }).catch(console.error); + * @see {@link https://api.slack.com/messaging/webhooks} + */ + async sendSlackMessage(body) { + if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); - const existing = messageManager.cache.get(d.id); - if (!existing) return messageManager._add(d); + const data = await this.client.api + .webhooks(this.id, this.token) + .slack.post({ + query: new URLSearchParams({ wait: true }), + auth: false, + body, + }); + return data.toString() === 'ok'; + } - const clone = existing._clone(); - clone._patch(d); - return clone; - } + /** + * Options used to edit a {@link Webhook}. + * @typedef {Object} WebhookEditData + * @property {string} [name=this.name] The new name for the webhook + * @property {?(BufferResolvable)} [avatar] The new avatar for the webhook + * @property {GuildTextChannelResolvable} [channel] The new channel for the webhook + */ - /** - * Deletes the webhook. - * @param {string} [reason] Reason for deleting this webhook - * @returns {Promise} - */ - async delete(reason) { - await this.client.api.webhooks(this.id, this.token).delete({ reason, auth: !this.token }); - } + /** + * Edits this webhook. + * @param {WebhookEditData} options Options for editing the webhook + * @param {string} [reason] Reason for editing the webhook + * @returns {Promise} + */ + async edit({ name = this.name, avatar, channel }, reason) { + if (avatar && !(typeof avatar === 'string' && avatar.startsWith('data:'))) { + avatar = await DataResolver.resolveImage(avatar); + } + channel &&= channel.id ?? channel; + const data = await this.client.api + .webhooks(this.id, channel ? undefined : this.token) + .patch({ + data: { name, avatar, channel_id: channel }, + reason, + auth: !this.token || Boolean(channel), + }); - /** - * Delete a message that was sent by this webhook. - * @param {MessageResolvable|'@original'} message The message to delete - * @param {Snowflake} [threadId] The id of the thread this message belongs to - * @returns {Promise} - */ - async deleteMessage(message, threadId) { - if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); + this.name = data.name; + this.avatar = data.avatar; + this.channelId = data.channel_id; + return this; + } - await this.client.api.webhooks(this.id, this.token).messages(typeof message === 'string' ? message : message.id ).delete({ - query: threadId - ? new URLSearchParams({ - thread_id: threadId, - }) - : undefined, - auth: false, - }) - } + /** + * Options that can be passed into fetchMessage. + * @typedef {options} WebhookFetchMessageOptions + * @property {boolean} [cache=true] Whether to cache the message. + * @property {Snowflake} [threadId] The id of the thread this message belongs to. + * For interaction webhooks, this property is ignored + */ - /** - * The timestamp the webhook was created at - * @type {number} - * @readonly - */ - get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); - } + /** + * Gets a message that was sent by this webhook. + * @param {Snowflake|'@original'} message The id of the message to fetch + * @param {WebhookFetchMessageOptions} [options={}] The options to provide to fetch the message. + * @returns {Promise} Returns the raw message data if the webhook was instantiated as a + * {@link WebhookClient} or if the channel is uncached, otherwise a {@link Message} will be returned + */ + async fetchMessage(message, { cache = true, threadId } = {}) { + if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); - /** - * The time the webhook was created at - * @type {Date} - * @readonly - */ - get createdAt() { - return new Date(this.createdTimestamp); - } + const data = await this.client.api + .webhooks(this.id, this.token) + .messages(message) + .get({ + query: threadId + ? new URLSearchParams({ + thread_id: threadId, + }) + : undefined, + auth: false, + }); + return ( + this.client.channels?.cache + .get(data.channel_id) + ?.messages._add(data, cache) ?? data + ); + } - /** - * The URL of this webhook - * @type {string} - * @readonly - */ - get url() { - return this.client.options.rest.api + Routes.webhook(this.id, this.token); - } + /** + * Edits a message that was sent by this webhook. + * @param {MessageResolvable|'@original'} message The message to edit + * @param {string|MessagePayload|WebhookEditMessageOptions} options The options to provide + * @returns {Promise} Returns the raw message data if the webhook was instantiated as a + * {@link WebhookClient} or if the channel is uncached, otherwise a {@link Message} will be returned + */ + async editMessage(message, options) { + if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); - /** - * A link to the webhook's avatar. - * @param {ImageURLOptions} [options={}] Options for the image URL - * @returns {?string} - */ - avatarURL(options = {}) { - return this.avatar && this.client.rest.cdn.avatar(this.id, this.avatar, options); - } + let messagePayload; - /** - * Whether this webhook is created by a user. - * @returns {boolean} - */ - isUserCreated() { - return Boolean(this.type === WebhookType.Incoming && this.owner && !this.owner.bot); - } + if (options instanceof MessagePayload) messagePayload = options; + else messagePayload = MessagePayload.create(this, options); - /** - * Whether this webhook is created by an application. - * @returns {boolean} - */ - isApplicationCreated() { - return this.type === WebhookType.Application; - } + const { body, files } = await messagePayload.resolveBody().resolveFiles(); - /** - * Whether or not this webhook is a channel follower webhook. - * @returns {boolean} - */ - isChannelFollower() { - return this.type === WebhookType.ChannelFollower; - } + const d = await this.client.api + .webhooks(this.id, this.token) + .messages(typeof message === 'string' ? message : message.id) + .patch({ + body, + files, + query: messagePayload.options.threadId + ? new URLSearchParams({ thread_id: messagePayload.options.threadId }) + : undefined, + auth: false, + }); - /** - * Whether or not this webhook is an incoming webhook. - * @returns {boolean} - */ - isIncoming() { - return this.type === WebhookType.Incoming; - } + const messageManager = this.client.channels?.cache.get( + d.channel_id, + )?.messages; + if (!messageManager) return d; - static applyToClass(structure, ignore = []) { - for (const prop of [ - 'send', - 'sendSlackMessage', - 'fetchMessage', - 'edit', - 'editMessage', - 'delete', - 'deleteMessage', - 'createdTimestamp', - 'createdAt', - 'url', - ]) { - if (ignore.includes(prop)) continue; - Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(Webhook.prototype, prop)); - } - } + const existing = messageManager.cache.get(d.id); + if (!existing) return messageManager._add(d); + + const clone = existing._clone(); + clone._patch(d); + return clone; + } + + /** + * Deletes the webhook. + * @param {string} [reason] Reason for deleting this webhook + * @returns {Promise} + */ + async delete(reason) { + await this.client.api + .webhooks(this.id, this.token) + .delete({ reason, auth: !this.token }); + } + + /** + * Delete a message that was sent by this webhook. + * @param {MessageResolvable|'@original'} message The message to delete + * @param {Snowflake} [threadId] The id of the thread this message belongs to + * @returns {Promise} + */ + async deleteMessage(message, threadId) { + if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); + + await this.client.api + .webhooks(this.id, this.token) + .messages(typeof message === 'string' ? message : message.id) + .delete({ + query: threadId + ? new URLSearchParams({ + thread_id: threadId, + }) + : undefined, + auth: false, + }); + } + + /** + * The timestamp the webhook was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time the webhook was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The URL of this webhook + * @type {string} + * @readonly + */ + get url() { + return this.client.options.rest.api + Routes.webhook(this.id, this.token); + } + + /** + * A link to the webhook's avatar. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + avatarURL(options = {}) { + return ( + this.avatar && this.client.rest.cdn.avatar(this.id, this.avatar, options) + ); + } + + /** + * Whether this webhook is created by a user. + * @returns {boolean} + */ + isUserCreated() { + return Boolean( + this.type === WebhookType.Incoming && this.owner && !this.owner.bot, + ); + } + + /** + * Whether this webhook is created by an application. + * @returns {boolean} + */ + isApplicationCreated() { + return this.type === WebhookType.Application; + } + + /** + * Whether or not this webhook is a channel follower webhook. + * @returns {boolean} + */ + isChannelFollower() { + return this.type === WebhookType.ChannelFollower; + } + + /** + * Whether or not this webhook is an incoming webhook. + * @returns {boolean} + */ + isIncoming() { + return this.type === WebhookType.Incoming; + } + + static applyToClass(structure, ignore = []) { + for (const prop of [ + 'send', + 'sendSlackMessage', + 'fetchMessage', + 'edit', + 'editMessage', + 'delete', + 'deleteMessage', + 'createdTimestamp', + 'createdAt', + 'url', + ]) { + if (ignore.includes(prop)) continue; + Object.defineProperty( + structure.prototype, + prop, + Object.getOwnPropertyDescriptor(Webhook.prototype, prop), + ); + } + } } module.exports = Webhook; diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index ab4a796..484cf00 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -1,5 +1,5 @@ 'use strict'; - +const FormData = require('form-data'); const { Collection } = require('@discordjs/collection'); const { DiscordSnowflake } = require('@sapphire/snowflake'); const { InteractionType, Routes } = require('discord-api-types/v9'); @@ -9,47 +9,6 @@ const MessageCollector = require('../MessageCollector'); const MessagePayload = require('../MessagePayload'); const DiscordAPIError = require('../../rest/DiscordAPIError'); -const _send = (client, channelID, data, files) => { - return new Promise((resolve, reject) => { - require('axios')({ - method: 'post', - url: `${client.options.http.api}/v${client.options.http.version}/channels/${channelID}/messages`, - headers: { - authorization: client.token, - Accept: '*/*', - 'Accept-Language': 'en-US,en;q=0.9', - 'Cache-Control': 'no-cache', - Pragma: 'no-cache', - Referer: 'https://discord.com/channels/@me', - 'Sec-Ch-Ua': '" Not A;Brand";v="99" "', - 'Sec-Ch-Ua-Mobile': '?0', - 'Sec-Ch-Ua-Platform': '"iOS"', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'X-Debug-Options': 'bugReporterEnabled', - 'X-Discord-Locale': 'en-US', - Origin: 'https://discord.com', - }, - data, - files, - }) - .then((res) => resolve(res.data)) - .catch((err) => { - err.request.options = { - data, - files, - }; - return reject( - new DiscordAPIError( - err.response.data, - err.response.status, - err.request, - ), - ); - }); - }); -} /** * Interface for classes that have text-channel-like features. * @interface @@ -215,8 +174,8 @@ class TextBasedChannel { } const { body, files } = await messagePayload.resolveFiles(); - // const d = await this.client.api.channels[this.id].messages.post({ body, files }); - const d = await _send(this.client, this.id, body, files); + console.log(body) + const d = await this.client.api.channels[this.id].messages.post({ body, 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 5be7be5..f92c14d 100644 --- a/src/util/Options.js +++ b/src/util/Options.js @@ -109,7 +109,7 @@ class Options extends null { // 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'en-US,en;q=0.9', 'Cache-Control': 'no-cache', - 'Content-Type': 'application/json', + // 'Content-Type': 'application/json', Pragma: 'no-cache', Referer: 'https://discord.com/channels/@me', 'Sec-Ch-Ua': '" Not A;Brand";v="99" "',