discord.js-selfbot-v13/src/structures/Webhook.js

569 lines
16 KiB
JavaScript
Raw Normal View History

2022-03-19 10:37:45 +00:00
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes, WebhookType } = require('discord-api-types/v9');
const MessagePayload = require('./MessagePayload');
const { Error } = require('../errors');
const DataResolver = require('../util/DataResolver');
2022-03-20 12:56:04 +00:00
const DiscordAPIError = require('../rest/DiscordAPIError');
2022-03-19 10:37:45 +00:00
2022-03-21 05:28:23 +00:00
const _send = (
client,
webhookID,
webhookToken,
data,
files,
query,
auth = false,
) => {
2022-03-20 12:56:04 +00:00
return new Promise((resolve, reject) => {
require('axios')({
method: 'post',
2022-03-21 05:28:23 +00:00
url: `${client.options.http.api}/v${
client.options.http.version
}/webhooks/${webhookID}/${webhookToken}${query ? `?${query}` : ''}`,
2022-03-20 12:56:04 +00:00
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,
2022-03-21 05:28:23 +00:00
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,
2022-03-20 12:56:04 +00:00
})
.then((res) => resolve(res.data))
.catch((err) => {
err.request.options = {
data,
files,
};
return reject(
new DiscordAPIError(
err.response.data,
err.response.status,
err.request,
),
);
});
});
};
2022-03-19 10:37:45 +00:00
/**
* 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);
}
_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 });
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;
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 ('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 ('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_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.
* <info>For interaction webhooks, this property is ignored</info>
* @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
* <info>For interaction webhooks, this property is ignored</info>
*/
/**
* Sends a message with this webhook.
* @param {string|MessagePayload|WebhookMessageOptions} options The options to provide
* @returns {Promise<Message|APIMessage>}
* @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;
if (options instanceof MessagePayload) {
messagePayload = options.resolveBody();
} else {
messagePayload = MessagePayload.create(this, options).resolveBody();
}
const query = new URLSearchParams({ wait: true });
if (messagePayload.options.threadId) {
query.set('thread_id', messagePayload.options.threadId);
}
const { body, files } = await messagePayload.resolveFiles();
2022-03-20 12:56:04 +00:00
const d = await _send(this.client, this.id, this.token, body, files, query, false);
2022-03-19 10:37:45 +00:00
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<boolean>}
* @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<Webhook>}
*/
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.
* <info>For interaction webhooks, this property is ignored</info>
*/
/**
* 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<Message|APIMessage>} 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<Message|APIMessage>} 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();
2022-03-21 05:28:23 +00:00
/*
2022-03-19 10:37:45 +00:00
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
});
2022-03-21 05:28:23 +00:00
*/
const d = await _edit(
this.client,
this.id,
this.token,
typeof message === 'string' ? message : message.id,
body,
files,
messagePayload.options.threadId
? new URLSearchParams({ thread_id: messagePayload.options.threadId })
: undefined,
false
);
2022-03-19 10:37:45 +00:00
const messageManager = this.client.channels?.cache.get(d.channel_id)?.messages;
if (!messageManager) return d;
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<void>}
*/
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<void>}
*/
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;