UNPKG

discord.js-selfbot-v13

Version:

A unofficial discord.js fork for creating selfbots [Based on discord.js v13]

761 lines (717 loc) 24.7 kB
'use strict'; /* eslint-disable import/order */ const MessageCollector = require('../MessageCollector'); const MessagePayload = require('../MessagePayload'); const { InteractionTypes, ApplicationCommandOptionTypes, Events } = require('../../util/Constants'); const { Error } = require('../../errors'); const SnowflakeUtil = require('../../util/SnowflakeUtil'); const { setTimeout } = require('node:timers'); const { s } = require('@sapphire/shapeshift'); const Util = require('../../util/Util'); const validateName = stringName => s.string .lengthGreaterThanOrEqual(1) .lengthLessThanOrEqual(32) .regex(/^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u) .setValidationEnabled(true) .parse(stringName); /** * Interface for classes that have text-channel-like features. * @interface */ class TextBasedChannel { constructor() { /** * A manager of the messages sent to this channel * @type {MessageManager} */ this.messages = new MessageManager(this); /** * The channel's last message id, if one was sent * @type {?Snowflake} */ this.lastMessageId = null; /** * The timestamp when the last pinned message was pinned, if there was one * @type {?number} */ this.lastPinTimestamp = null; } /** * The Message object of the last message in the channel, if one was sent * @type {?Message} * @readonly */ get lastMessage() { return this.messages.resolve(this.lastMessageId); } /** * The date when the last pinned message was pinned, if there was one * @type {?Date} * @readonly */ get lastPinAt() { return this.lastPinTimestamp ? new Date(this.lastPinTimestamp) : null; } /** * Represents the data for a poll answer. * @typedef {Object} PollAnswerData * @property {string} text The text for the poll answer * @property {EmojiIdentifierResolvable} [emoji] The emoji for the poll answer */ /** * Represents the data for a poll. * @typedef {Object} PollData * @property {PollQuestionMedia} question The question for the poll * @property {PollAnswerData[]} answers The answers for the poll * @property {number} duration The duration in hours for the poll * @property {boolean} allowMultiselect Whether the poll allows multiple answers * @property {PollLayoutType} [layoutType] The layout type for the poll */ /** * Base options provided when sending. * @typedef {Object} BaseMessageOptions * @property {MessageActivity} [activity] Group activity * @property {boolean} [tts=false] Whether or not the message should be spoken aloud * @property {string} [nonce=''] The nonce for the message * @property {string} [content=''] The content for the message * @property {Array<(MessageEmbed|APIEmbed)>} [embeds] The embeds for the message * (see [here](https://discord.com/developers/docs/resources/channel#embed-object) for more details) * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content * (see [here](https://discord.com/developers/docs/resources/channel#allowed-mentions-object) for more details) * @property {Array<(FileOptions|BufferResolvable|MessageAttachment[])>} [files] Files to send with the message * @property {Array<(MessageActionRow|MessageActionRowOptions)>} [components] * Action rows containing interactive components for the message (buttons, select menus) * @property {MessageAttachment[]} [attachments] Attachments to send in the message */ /** * The base message options for messages including a poll. * @typedef {BaseMessageOptions} BaseMessageOptionsWithPoll * @property {PollData} [poll] The poll to send with the message */ /** * @typedef {Object} ForwardOptions * @property {MessageResolvable} message The originating message * @property {TextBasedChannelResolvable} [channel] The channel of the originating message * @property {GuildResolvable} [guild] The guild of the originating message */ /** * Options provided when sending or editing a message. * @typedef {BaseMessageOptions} MessageOptions * @property {ReplyOptions} [reply] The options for replying to a message * @property {ForwardOptions} [forward] The options for forwarding a message * @property {StickerResolvable[]} [stickers=[]] Stickers to send in the message * @property {MessageFlags} [flags] Which flags to set for the message. * Only `SUPPRESS_EMBEDS`, `SUPPRESS_NOTIFICATIONS` and `IS_VOICE_MESSAGE` can be set. */ /** * Options provided to control parsing of mentions by Discord * @typedef {Object} MessageMentionOptions * @property {MessageMentionTypes[]} [parse] Types of mentions to be parsed * @property {Snowflake[]} [users] Snowflakes of Users to be parsed as mentions * @property {Snowflake[]} [roles] Snowflakes of Roles to be parsed as mentions * @property {boolean} [repliedUser=true] Whether the author of the Message being replied to should be pinged */ /** * Types of mentions to enable in MessageMentionOptions. * - `roles` * - `users` * - `everyone` * @typedef {string} MessageMentionTypes */ /** * @typedef {Object} FileOptions * @property {BufferResolvable} attachment File to attach * @property {string} [name='file.jpg'] Filename of the attachment * @property {string} description The description of the file */ /** * Options for sending a message with a reply. * @typedef {Object} ReplyOptions * @property {MessageResolvable} messageReference The message to reply to (must be in the same channel and not system) * @property {boolean} [failIfNotExists=true] Whether to error if the referenced message * does not exist (creates a standard message in this case when false) */ /** * Sends a message to this channel. * @param {string|MessagePayload|MessageOptions} options The options to provide * @returns {Promise<Message>} * @example * // Send a basic message * channel.send('hello!') * .then(message => console.log(`Sent message: ${message.content}`)) * .catch(console.error); * @example * // Send a remote file * channel.send({ * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] * }) * .then(console.log) * .catch(console.error); * @example * // Send a local file * channel.send({ * files: [{ * attachment: 'entire/path/to/file.jpg', * name: 'file.jpg', * description: 'A description of the file' * }] * }) * .then(console.log) * .catch(console.error); */ async send(options) { const User = require('../User'); const { GuildMember } = require('../GuildMember'); if (this instanceof User || this instanceof GuildMember) { const dm = await this.createDM(); return dm.send(options); } let messagePayload; if (options instanceof MessagePayload) { messagePayload = options.resolveData(); } else { messagePayload = MessagePayload.create(this, options).resolveData(); } const { data, files } = await messagePayload.resolveFiles(); // New API const attachments = await Util.getUploadURL(this.client, this.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, description: files[attachment.id].description, duration_secs: files[attachment.id].duration_secs, waveform: files[attachment.id].waveform, }; }); const attachmentsData = await Promise.all(requestPromises); attachmentsData.sort((a, b) => parseInt(a.id) - parseInt(b.id)); data.attachments = attachmentsData; // Empty Files const d = await this.client.api.channels[this.id].messages.post({ data }); return this.messages.cache.get(d.id) ?? this.messages._add(d); } searchInteractionFromGuildAndPrivateChannel() { // Support Slash / ContextMenu // API https://canary.discord.com/api/v9/guilds/:id/application-command-index // Guild // https://canary.discord.com/api/v9/channels/:id/application-command-index // DM Channel // Updated: 07/01/2023 return this.client.api[this.guild ? 'guilds' : 'channels'][this.guild?.id || this.id]['application-command-index'] .get() .catch(() => ({ application_commands: [], applications: [], version: '', })); } searchInteractionUserApps() { return this.client.api.users['@me']['application-command-index'].get().catch(() => ({ application_commands: [], applications: [], version: '', })); } searchInteraction() { return Promise.all([this.searchInteractionFromGuildAndPrivateChannel(), this.searchInteractionUserApps()]).then( ([dataA, dataB]) => ({ applications: [...dataA.applications, ...dataB.applications], application_commands: [...dataA.application_commands, ...dataB.application_commands], }), ); } async sendSlash(botOrApplicationId, commandNameString, ...args) { // Parse commandName /role add user const cmd = commandNameString.trim().split(' '); // Ex: role add user => [role, add, user] // Parse: name, subGr, sub const commandName = validateName(cmd[0]); // Parse: role const sub = cmd.slice(1); // Parse: [add, user] for (let i = 0; i < sub.length; i++) { if (sub.length > 2) { throw new Error('INVALID_COMMAND_NAME', cmd); } validateName(sub[i]); } // Search all const data = await this.searchInteraction(); // Find command... const filterCommand = data.application_commands.filter(obj => // Filter: name | name_default [obj.name, obj.name_default].includes(commandName), ); // Filter Bot botOrApplicationId = this.client.users.resolveId(botOrApplicationId); const application = data.applications.find( obj => obj.id == botOrApplicationId || obj.bot?.id == botOrApplicationId, ); if (!application) { throw new Error('INVALID_APPLICATION_COMMAND', "Bot/Application doesn't exist"); } // Find Command with application const command = filterCommand.find(command => command.application_id == application.id); if (!command) { throw new Error('INVALID_APPLICATION_COMMAND', application.id); } args = args.flat(2); let optionFormat = []; let attachments = []; let optionsMaxdepth, subGroup, subCommand; if (sub.length == 2) { // Subcommand Group > Subcommand // Find Sub group subGroup = command.options.find( obj => obj.type == ApplicationCommandOptionTypes.SUB_COMMAND_GROUP && [obj.name, obj.name_default].includes(sub[0]), ); if (!subGroup) throw new Error('SLASH_COMMAND_SUB_COMMAND_GROUP_INVALID', sub[0]); // Find Sub subCommand = subGroup.options.find( obj => obj.type == ApplicationCommandOptionTypes.SUB_COMMAND && [obj.name, obj.name_default].includes(sub[1]), ); if (!subCommand) throw new Error('SLASH_COMMAND_SUB_COMMAND_INVALID', sub[1]); // Options optionsMaxdepth = subCommand.options; } else if (sub.length == 1) { // Subcommand subCommand = command.options.find( obj => obj.type == ApplicationCommandOptionTypes.SUB_COMMAND && [obj.name, obj.name_default].includes(sub[0]), ); if (!subCommand) throw new Error('SLASH_COMMAND_SUB_COMMAND_INVALID', sub[0]); // Options optionsMaxdepth = subCommand.options; } else { optionsMaxdepth = command.options; } const valueRequired = optionsMaxdepth?.filter(o => o.required).length || 0; for (let i = 0; i < Math.min(args.length, optionsMaxdepth?.length || 0); i++) { const optionInput = optionsMaxdepth[i]; const value = args[i]; const parseData = await parseOption( this.client, optionInput, value, optionFormat, attachments, command, application.id, this.guild?.id, this.id, subGroup, subCommand, ); optionFormat = parseData.optionFormat; attachments = parseData.attachments; } if (valueRequired > args.length) { throw new Error('SLASH_COMMAND_REQUIRED_OPTIONS_MISSING', valueRequired, optionFormat.length); } // Post let postData; if (subGroup) { postData = [ { type: ApplicationCommandOptionTypes.SUB_COMMAND_GROUP, name: subGroup.name, options: [ { type: ApplicationCommandOptionTypes.SUB_COMMAND, name: subCommand.name, options: optionFormat, }, ], }, ]; } else if (subCommand) { postData = [ { type: ApplicationCommandOptionTypes.SUB_COMMAND, name: subCommand.name, options: optionFormat, }, ]; } else { postData = optionFormat; } const nonce = SnowflakeUtil.generate(); const body = createPostData( this.client, false, application.id, nonce, this.guild?.id, Boolean(command.guild_id), this.id, command.version, command.id, command.name_default || command.name, command.type, postData, attachments, ); this.client.api.interactions.post({ data: body, usePayloadJSON: true, }); return Util.createPromiseInteraction(this.client, nonce, 5000); } /** * Sends a typing indicator in the channel. * @returns {Promise<void>} Resolves upon the typing status being sent * @example * // Start typing in a channel * channel.sendTyping(); */ async sendTyping() { await this.client.api.channels(this.id).typing.post(); } /** * Creates a Message Collector. * @param {MessageCollectorOptions} [options={}] The options to pass to the collector * @returns {MessageCollector} * @example * // Create a message collector * const filter = m => m.content.includes('discord'); * const collector = channel.createMessageCollector({ filter, time: 15_000 }); * collector.on('collect', m => console.log(`Collected ${m.content}`)); * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); */ createMessageCollector(options = {}) { return new MessageCollector(this, options); } /** * An object containing the same properties as CollectorOptions, but a few more: * @typedef {MessageCollectorOptions} AwaitMessagesOptions * @property {string[]} [errors] Stop/end reasons that cause the promise to reject */ /** * Similar to createMessageCollector but in promise form. * Resolves with a collection of messages that pass the specified filter. * @param {AwaitMessagesOptions} [options={}] Optional options to pass to the internal collector * @returns {Promise<Collection<Snowflake, Message>>} * @example * // Await !vote messages * const filter = m => m.content.startsWith('!vote'); * // Errors: ['time'] treats ending because of the time limit as an error * channel.awaitMessages({ filter, max: 4, time: 60_000, errors: ['time'] }) * .then(collected => console.log(collected.size)) * .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`)); */ awaitMessages(options = {}) { return new Promise((resolve, reject) => { const collector = this.createMessageCollector(options); collector.once('end', (collection, reason) => { if (options.errors?.includes(reason)) { reject(collection); } else { resolve(collection); } }); }); } /** * Fetches all webhooks for the channel. * @returns {Promise<Collection<Snowflake, Webhook>>} * @example * // Fetch webhooks * channel.fetchWebhooks() * .then(hooks => console.log(`This channel has ${hooks.size} hooks`)) * .catch(console.error); */ fetchWebhooks() { return this.guild.channels.fetchWebhooks(this.id); } /** * Options used to create a {@link Webhook} in a guild text-based channel. * @typedef {Object} ChannelWebhookCreateOptions * @property {?(BufferResolvable|Base64Resolvable)} [avatar] Avatar for the webhook * @property {string} [reason] Reason for creating the webhook */ /** * Creates a webhook for the channel. * @param {string} name The name of the webhook * @param {ChannelWebhookCreateOptions} [options] Options for creating the webhook * @returns {Promise<Webhook>} Returns the created Webhook * @example * // Create a webhook for the current channel * channel.createWebhook('Snek', { * avatar: 'https://i.imgur.com/mI8XcpG.jpg', * reason: 'Needed a cool new Webhook' * }) * .then(console.log) * .catch(console.error) */ createWebhook(name, options = {}) { return this.guild.channels.createWebhook(this.id, name, options); } /** * Sets the rate limit per user (slowmode) for this channel. * @param {number} rateLimitPerUser The new rate limit in seconds * @param {string} [reason] Reason for changing the channel's rate limit * @returns {Promise<this>} */ setRateLimitPerUser(rateLimitPerUser, reason) { return this.edit({ rateLimitPerUser }, reason); } /** * Sets whether this channel is flagged as NSFW. * @param {boolean} [nsfw=true] Whether the channel should be considered NSFW * @param {string} [reason] Reason for changing the channel's NSFW flag * @returns {Promise<this>} */ setNSFW(nsfw = true, reason) { return this.edit({ nsfw }, reason); } static applyToClass(structure, full = false, ignore = []) { const props = ['send']; if (full) { props.push( 'sendSlash', 'searchInteraction', 'searchInteractionFromGuildAndPrivateChannel', 'searchInteractionUserApps', 'lastMessage', 'lastPinAt', 'sendTyping', 'createMessageCollector', 'awaitMessages', 'fetchWebhooks', 'createWebhook', 'setRateLimitPerUser', 'setNSFW', ); } for (const prop of props) { if (ignore.includes(prop)) continue; Object.defineProperty( structure.prototype, prop, Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop), ); } } } module.exports = TextBasedChannel; // Fixes Circular const MessageManager = require('../../managers/MessageManager'); // Utils function parseChoices(parent, list_choices, value) { if (value !== undefined) { if (Array.isArray(list_choices) && list_choices.length) { const choice = list_choices.find(c => [c.name, c.value].includes(value)); if (choice) { return choice.value; } else { throw new Error('INVALID_SLASH_COMMAND_CHOICES', parent, value); } } else { return value; } } else { return undefined; } } async function addDataFromAttachment(value, client, channelId, attachments) { value = await MessagePayload.resolveFile(value); if (!value?.file) { throw new TypeError('The attachment data must be a BufferResolvable or Stream or FileOptions of MessageAttachment'); } const data = await Util.getUploadURL(client, channelId, [value]); await Util.uploadFile(value.file, data[0].upload_url); const id = attachments.length; attachments.push({ id, filename: value.name, uploaded_filename: data[0].upload_filename, }); return { id, attachments, }; } async function parseOption( client, optionCommand, value, optionFormat, attachments, command, applicationId, guildId, channelId, subGroup, subCommand, ) { const data = { type: optionCommand.type, name: optionCommand.name, }; if (value !== undefined) { switch (optionCommand.type) { case ApplicationCommandOptionTypes.BOOLEAN: case 'BOOLEAN': { data.value = Boolean(value); break; } case ApplicationCommandOptionTypes.INTEGER: case 'INTEGER': { data.value = Number(value); break; } case ApplicationCommandOptionTypes.ATTACHMENT: case 'ATTACHMENT': { const parseData = await addDataFromAttachment(value, client, channelId, attachments); data.value = parseData.id; attachments = parseData.attachments; break; } case ApplicationCommandOptionTypes.SUB_COMMAND_GROUP: case 'SUB_COMMAND_GROUP': { break; } default: { value = parseChoices(optionCommand.name, optionCommand.choices, value); if (optionCommand.autocomplete) { const nonce = SnowflakeUtil.generate(); // Post let postData; if (subGroup) { postData = [ { type: ApplicationCommandOptionTypes.SUB_COMMAND_GROUP, name: subGroup.name, options: [ { type: ApplicationCommandOptionTypes.SUB_COMMAND, name: subCommand.name, options: [ { type: optionCommand.type, name: optionCommand.name, value, focused: true, }, ], }, ], }, ]; } else if (subCommand) { postData = [ { type: ApplicationCommandOptionTypes.SUB_COMMAND, name: subCommand.name, options: [ { type: optionCommand.type, name: optionCommand.name, value, focused: true, }, ], }, ]; } else { postData = [ { type: optionCommand.type, name: optionCommand.name, value, focused: true, }, ]; } const body = createPostData( client, true, applicationId, nonce, guildId, Boolean(command.guild_id), channelId, command.version, command.id, command.name_default || command.name, command.type, postData, [], ); await client.api.interactions.post({ data: body, }); data.value = await awaitAutocomplete(client, nonce, value); } else { data.value = value; } } } optionFormat.push(data); } return { optionFormat, attachments, }; } function awaitAutocomplete(client, nonce, defaultValue) { return new Promise(resolve => { const handler = data => { if (data.t !== 'APPLICATION_COMMAND_AUTOCOMPLETE_RESPONSE') return; if (data.d?.nonce !== nonce) return; clearTimeout(timeout); client.removeListener(Events.UNHANDLED_PACKET, handler); client.decrementMaxListeners(); if (data.d.choices.length >= 1) { resolve(data.d.choices[0].value); } else { resolve(defaultValue); } }; const timeout = setTimeout(() => { client.removeListener(Events.UNHANDLED_PACKET, handler); client.decrementMaxListeners(); resolve(defaultValue); }, 5_000).unref(); client.incrementMaxListeners(); client.on(Events.UNHANDLED_PACKET, handler); }); } function createPostData( client, isAutocomplete = false, applicationId, nonce, guildId, isGuildCommand, channelId, commandVersion, commandId, commandName, commandType, postData, attachments = [], ) { const data = { type: isAutocomplete ? InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE : InteractionTypes.APPLICATION_COMMAND, application_id: applicationId, guild_id: guildId, channel_id: channelId, session_id: client.sessionId, data: { version: commandVersion, id: commandId, name: commandName, type: commandType, options: postData, attachments: attachments, }, nonce, }; if (isGuildCommand) { data.data.guild_id = guildId; } return data; }