Update TextBasedChannel.js
This commit is contained in:
parent
4a1d615673
commit
a4ff0fd12d
@ -1,14 +1,17 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/* eslint-disable import/order */
|
/* eslint-disable import/order */
|
||||||
|
const InteractionManager = require('../../managers/InteractionManager');
|
||||||
const MessageCollector = require('../MessageCollector');
|
const MessageCollector = require('../MessageCollector');
|
||||||
const MessagePayload = require('../MessagePayload');
|
const MessagePayload = require('../MessagePayload');
|
||||||
const { InteractionTypes, ApplicationCommandOptionTypes, Events } = require('../../util/Constants');
|
|
||||||
const { Error } = require('../../errors');
|
|
||||||
const SnowflakeUtil = require('../../util/SnowflakeUtil');
|
const SnowflakeUtil = require('../../util/SnowflakeUtil');
|
||||||
const { setTimeout } = require('node:timers');
|
const { Collection } = require('@discordjs/collection');
|
||||||
|
const { InteractionTypes, MaxBulkDeletableMessageAge } = require('../../util/Constants');
|
||||||
|
const { TypeError, Error } = require('../../errors');
|
||||||
|
const InteractionCollector = require('../InteractionCollector');
|
||||||
|
const { lazy, getAttachments, uploadFile } = require('../../util/Util');
|
||||||
|
const Message = lazy(() => require('../Message').Message);
|
||||||
const { s } = require('@sapphire/shapeshift');
|
const { s } = require('@sapphire/shapeshift');
|
||||||
const Util = require('../../util/Util');
|
|
||||||
const validateName = stringName =>
|
const validateName = stringName =>
|
||||||
s.string
|
s.string
|
||||||
.lengthGreaterThanOrEqual(1)
|
.lengthGreaterThanOrEqual(1)
|
||||||
@ -29,6 +32,12 @@ class TextBasedChannel {
|
|||||||
*/
|
*/
|
||||||
this.messages = new MessageManager(this);
|
this.messages = new MessageManager(this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A manager of the interactions sent to this channel
|
||||||
|
* @type {InteractionManager}
|
||||||
|
*/
|
||||||
|
this.interactions = new InteractionManager(this);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The channel's last message id, if one was sent
|
* The channel's last message id, if one was sent
|
||||||
* @type {?Snowflake}
|
* @type {?Snowflake}
|
||||||
@ -67,7 +76,7 @@ class TextBasedChannel {
|
|||||||
* @property {boolean} [tts=false] Whether or not the message should be spoken aloud
|
* @property {boolean} [tts=false] Whether or not the message should be spoken aloud
|
||||||
* @property {string} [nonce=''] The nonce for the message
|
* @property {string} [nonce=''] The nonce for the message
|
||||||
* @property {string} [content=''] The content for the message
|
* @property {string} [content=''] The content for the message
|
||||||
* @property {Array<(MessageEmbed|APIEmbed)>} [embeds] The embeds for the message
|
* @property {Array<(MessageEmbed|APIEmbed|WebEmbed)>} [embeds] The embeds for the message
|
||||||
* (see [here](https://discord.com/developers/docs/resources/channel#embed-object) for more details)
|
* (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
|
* @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)
|
* (see [here](https://discord.com/developers/docs/resources/channel#allowed-mentions-object) for more details)
|
||||||
@ -75,6 +84,7 @@ class TextBasedChannel {
|
|||||||
* @property {Array<(MessageActionRow|MessageActionRowOptions)>} [components]
|
* @property {Array<(MessageActionRow|MessageActionRowOptions)>} [components]
|
||||||
* Action rows containing interactive components for the message (buttons, select menus)
|
* Action rows containing interactive components for the message (buttons, select menus)
|
||||||
* @property {MessageAttachment[]} [attachments] Attachments to send in the message
|
* @property {MessageAttachment[]} [attachments] Attachments to send in the message
|
||||||
|
* @property {boolean} [usingNewAttachmentAPI] Whether to use the new attachment API (`channels/:id/attachments`)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -158,16 +168,21 @@ class TextBasedChannel {
|
|||||||
let messagePayload;
|
let messagePayload;
|
||||||
|
|
||||||
if (options instanceof MessagePayload) {
|
if (options instanceof MessagePayload) {
|
||||||
messagePayload = options.resolveData();
|
messagePayload = await options.resolveData();
|
||||||
} else {
|
} else {
|
||||||
messagePayload = MessagePayload.create(this, options).resolveData();
|
messagePayload = await MessagePayload.create(this, options).resolveData();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, files } = await messagePayload.resolveFiles();
|
let { data, files } = await messagePayload.resolveFiles();
|
||||||
// New API
|
|
||||||
const attachments = await Util.getUploadURL(this.client, this.id, files);
|
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 => {
|
const requestPromises = attachments.map(async attachment => {
|
||||||
await Util.uploadFile(files[attachment.id].file, attachment.upload_url);
|
await uploadFile(files[attachment.id].file, attachment.upload_url);
|
||||||
return {
|
return {
|
||||||
id: attachment.id,
|
id: attachment.id,
|
||||||
filename: files[attachment.id].name,
|
filename: files[attachment.id].name,
|
||||||
@ -180,175 +195,14 @@ class TextBasedChannel {
|
|||||||
const attachmentsData = await Promise.all(requestPromises);
|
const attachmentsData = await Promise.all(requestPromises);
|
||||||
attachmentsData.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
attachmentsData.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
||||||
data.attachments = attachmentsData;
|
data.attachments = attachmentsData;
|
||||||
// Empty Files
|
files = [];
|
||||||
const d = await this.client.api.channels[this.id].messages.post({ data });
|
}
|
||||||
|
|
||||||
|
const d = await this.client.api.channels[this.id].messages.post({ data, files });
|
||||||
|
|
||||||
return this.messages.cache.get(d.id) ?? this.messages._add(d);
|
return this.messages.cache.get(d.id) ?? this.messages._add(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
searchInteraction() {
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
// Find Command with application
|
|
||||||
const command = filterCommand.find(command => command.application_id == 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 new Promise((resolve, reject) => {
|
|
||||||
const timeoutMs = 5_000;
|
|
||||||
// Waiting for MsgCreate / ModalCreate
|
|
||||||
const handler = data => {
|
|
||||||
if (data.nonce !== nonce) return;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
this.client.removeListener(Events.MESSAGE_CREATE, handler);
|
|
||||||
this.client.removeListener(Events.INTERACTION_MODAL_CREATE, handler);
|
|
||||||
this.client.decrementMaxListeners();
|
|
||||||
resolve(data);
|
|
||||||
};
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
this.client.removeListener(Events.MESSAGE_CREATE, handler);
|
|
||||||
this.client.removeListener(Events.INTERACTION_MODAL_CREATE, handler);
|
|
||||||
this.client.decrementMaxListeners();
|
|
||||||
reject(new Error('INTERACTION_FAILED'));
|
|
||||||
}, timeoutMs).unref();
|
|
||||||
this.client.incrementMaxListeners();
|
|
||||||
this.client.on(Events.MESSAGE_CREATE, handler);
|
|
||||||
this.client.on(Events.INTERACTION_MODAL_CREATE, handler);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a typing indicator in the channel.
|
* Sends a typing indicator in the channel.
|
||||||
* @returns {Promise<void>} Resolves upon the typing status being sent
|
* @returns {Promise<void>} Resolves upon the typing status being sent
|
||||||
@ -407,6 +261,101 @@ class TextBasedChannel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a component interaction collector.
|
||||||
|
* @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector
|
||||||
|
* @returns {InteractionCollector}
|
||||||
|
* @example
|
||||||
|
* // Create a button interaction collector
|
||||||
|
* const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId';
|
||||||
|
* const collector = channel.createMessageComponentCollector({ filter, time: 15_000 });
|
||||||
|
* collector.on('collect', i => console.log(`Collected ${i.customId}`));
|
||||||
|
* collector.on('end', collected => console.log(`Collected ${collected.size} items`));
|
||||||
|
*/
|
||||||
|
createMessageComponentCollector(options = {}) {
|
||||||
|
return new InteractionCollector(this.client, {
|
||||||
|
...options,
|
||||||
|
interactionType: InteractionTypes.MESSAGE_COMPONENT,
|
||||||
|
channel: this,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects a single component interaction that passes the filter.
|
||||||
|
* The Promise will reject if the time expires.
|
||||||
|
* @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector
|
||||||
|
* @returns {Promise<MessageComponentInteraction>}
|
||||||
|
* @example
|
||||||
|
* // Collect a message component interaction
|
||||||
|
* const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId';
|
||||||
|
* channel.awaitMessageComponent({ filter, time: 15_000 })
|
||||||
|
* .then(interaction => console.log(`${interaction.customId} was clicked!`))
|
||||||
|
* .catch(console.error);
|
||||||
|
*/
|
||||||
|
awaitMessageComponent(options = {}) {
|
||||||
|
const _options = { ...options, max: 1 };
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const collector = this.createMessageComponentCollector(_options);
|
||||||
|
collector.once('end', (interactions, reason) => {
|
||||||
|
const interaction = interactions.first();
|
||||||
|
if (interaction) resolve(interaction);
|
||||||
|
else reject(new Error('INTERACTION_COLLECTOR_ERROR', reason));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk deletes given messages that are newer than two weeks.
|
||||||
|
* @param {Collection<Snowflake, Message>|MessageResolvable[]|number} messages
|
||||||
|
* Messages or number of messages to delete
|
||||||
|
* @param {boolean} [filterOld=false] Filter messages to remove those which are older than two weeks automatically
|
||||||
|
* @returns {Promise<Collection<Snowflake, Message|undefined>>} Returns the deleted messages
|
||||||
|
* @example
|
||||||
|
* // Bulk delete messages
|
||||||
|
* channel.bulkDelete(5)
|
||||||
|
* .then(messages => console.log(`Bulk deleted ${messages.size} messages`))
|
||||||
|
* .catch(console.error);
|
||||||
|
*/
|
||||||
|
async bulkDelete(messages, filterOld = false) {
|
||||||
|
if (!this.client.user.bot) throw new Error('INVALID_USER_METHOD');
|
||||||
|
if (Array.isArray(messages) || messages instanceof Collection) {
|
||||||
|
let messageIds = messages instanceof Collection ? [...messages.keys()] : messages.map(m => m.id ?? m);
|
||||||
|
if (filterOld) {
|
||||||
|
messageIds = messageIds.filter(id => Date.now() - SnowflakeUtil.timestampFrom(id) < MaxBulkDeletableMessageAge);
|
||||||
|
}
|
||||||
|
if (messageIds.length === 0) return new Collection();
|
||||||
|
if (messageIds.length === 1) {
|
||||||
|
await this.client.api.channels(this.id).messages(messageIds[0]).delete();
|
||||||
|
const message = this.client.actions.MessageDelete.getMessage(
|
||||||
|
{
|
||||||
|
message_id: messageIds[0],
|
||||||
|
},
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
return message ? new Collection([[message.id, message]]) : new Collection();
|
||||||
|
}
|
||||||
|
await this.client.api.channels[this.id].messages['bulk-delete'].post({ data: { messages: messageIds } });
|
||||||
|
return messageIds.reduce(
|
||||||
|
(col, id) =>
|
||||||
|
col.set(
|
||||||
|
id,
|
||||||
|
this.client.actions.MessageDeleteBulk.getMessage(
|
||||||
|
{
|
||||||
|
message_id: id,
|
||||||
|
},
|
||||||
|
this,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
new Collection(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!isNaN(messages)) {
|
||||||
|
const msgs = await this.messages.fetch({ limit: messages });
|
||||||
|
return this.bulkDelete(msgs, filterOld);
|
||||||
|
}
|
||||||
|
throw new TypeError('MESSAGE_BULK_DELETE_TYPE');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches all webhooks for the channel.
|
* Fetches all webhooks for the channel.
|
||||||
* @returns {Promise<Collection<Snowflake, Webhook>>}
|
* @returns {Promise<Collection<Snowflake, Webhook>>}
|
||||||
@ -465,21 +414,142 @@ class TextBasedChannel {
|
|||||||
return this.edit({ nsfw }, reason);
|
return this.edit({ nsfw }, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search Slash Command (return raw data)
|
||||||
|
* @param {Snowflake} applicationId Application ID
|
||||||
|
* @param {?ApplicationCommandType} type Command Type
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
searchInteraction(applicationId, type = 'CHAT_INPUT') {
|
||||||
|
switch (type) {
|
||||||
|
case 'USER':
|
||||||
|
case 2:
|
||||||
|
type = 2;
|
||||||
|
break;
|
||||||
|
case 'MESSAGE':
|
||||||
|
case 3:
|
||||||
|
type = 3;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
type = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return this.client.api.channels[this.id]['application-commands'].search.get({
|
||||||
|
query: {
|
||||||
|
type,
|
||||||
|
application_id: applicationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send Slash to this channel
|
||||||
|
* @param {UserResolvable} bot Bot user (BotID, not applicationID)
|
||||||
|
* @param {string} commandString Command name (and sub / group formats)
|
||||||
|
* @param {...?any|any[]} args Command arguments
|
||||||
|
* @returns {Promise<InteractionResponse>}
|
||||||
|
* @example
|
||||||
|
* // Send a basic slash
|
||||||
|
* channel.sendSlash('botid', 'ping')
|
||||||
|
* .then(console.log)
|
||||||
|
* .catch(console.error);
|
||||||
|
* @example
|
||||||
|
* // Send a remote file
|
||||||
|
* channel.sendSlash('botid', 'emoji upload', 'https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048', 'test')
|
||||||
|
* .then(console.log)
|
||||||
|
* .catch(console.error);
|
||||||
|
* @see {@link https://github.com/aiko-chan-ai/discord.js-selfbot-v13/blob/main/Document/SlashCommand.md}
|
||||||
|
*/
|
||||||
|
async sendSlash(bot, commandString, ...args) {
|
||||||
|
const perms =
|
||||||
|
this.type != 'DM'
|
||||||
|
? this.permissionsFor(this.client.user).toArray()
|
||||||
|
: ['USE_APPLICATION_COMMANDS', `${this.recipient.relationships == 'BLOCKED' ? '' : 'SEND_MESSAGES'}`];
|
||||||
|
if (!perms.includes('SEND_MESSAGES')) {
|
||||||
|
throw new Error(
|
||||||
|
'INTERACTION_SEND_FAILURE',
|
||||||
|
`Cannot send Slash to ${this.toString()} ${
|
||||||
|
this.recipient ? 'because bot has been blocked' : 'due to missing SEND_MESSAGES permission'
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!perms.includes('USE_APPLICATION_COMMANDS')) {
|
||||||
|
throw new Error(
|
||||||
|
'INTERACTION_SEND_FAILURE',
|
||||||
|
`Cannot send Slash to ${this.toString()} due to missing USE_APPLICATION_COMMANDS permission`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
args = args.flat(2);
|
||||||
|
const cmd = commandString.trim().split(' ');
|
||||||
|
// Validate CommandName
|
||||||
|
const commandName = validateName(cmd[0]);
|
||||||
|
const sub = cmd.slice(1);
|
||||||
|
for (let i = 0; i < sub.length; i++) {
|
||||||
|
if (sub.length > 2) {
|
||||||
|
throw new Error('INVALID_COMMAND_NAME', cmd);
|
||||||
|
}
|
||||||
|
validateName(sub[i]);
|
||||||
|
}
|
||||||
|
if (!bot) throw new Error('MUST_SPECIFY_BOT');
|
||||||
|
const botId = this.client.users.resolveId(bot);
|
||||||
|
const user = await this.client.users.fetch(botId).catch(() => {});
|
||||||
|
if (!user || !user.bot || !user.application) {
|
||||||
|
throw new Error('botId is not a bot or does not have an application slash command');
|
||||||
|
}
|
||||||
|
if (user._partial) await user.getProfile().catch(() => {});
|
||||||
|
if (!commandName || typeof commandName !== 'string') throw new Error('Command name is required');
|
||||||
|
const API =
|
||||||
|
this.client.api[this.guild ? 'guilds' : 'channels'][this.guild?.id || this.id]['application-command-index'];
|
||||||
|
const data = await API.get();
|
||||||
|
for (const command of data.application_commands) {
|
||||||
|
if (command.type !== 1) continue;
|
||||||
|
if (user.id == command.application_id || user.application.id == command.application_id) {
|
||||||
|
user.application?.commands?._add(command, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove
|
||||||
|
const commandTarget = user.application?.commands?.cache.find(
|
||||||
|
c => c.name === commandName && c.type === 'CHAT_INPUT',
|
||||||
|
);
|
||||||
|
if (!commandTarget) {
|
||||||
|
throw new Error(
|
||||||
|
'INTERACTION_SEND_FAILURE',
|
||||||
|
`SlashCommand ${commandName} is not found (With search)\nDebug:\n+ botId: ${botId} (ApplicationId: ${
|
||||||
|
user.application?.id
|
||||||
|
})\n+ args: ${args.join(' | ') || null}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return commandTarget.sendSlashCommand(
|
||||||
|
new (Message())(this.client, {
|
||||||
|
channel_id: this.id,
|
||||||
|
guild_id: this.guild?.id || null,
|
||||||
|
author: this.client.user,
|
||||||
|
content: '',
|
||||||
|
id: this.client.user.id,
|
||||||
|
}),
|
||||||
|
sub && sub.length > 0 ? sub : [],
|
||||||
|
args && args.length ? args : [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static applyToClass(structure, full = false, ignore = []) {
|
static applyToClass(structure, full = false, ignore = []) {
|
||||||
const props = ['send'];
|
const props = ['send'];
|
||||||
if (full) {
|
if (full) {
|
||||||
props.push(
|
props.push(
|
||||||
'sendSlash',
|
|
||||||
'searchInteraction',
|
|
||||||
'lastMessage',
|
'lastMessage',
|
||||||
'lastPinAt',
|
'lastPinAt',
|
||||||
|
'bulkDelete',
|
||||||
'sendTyping',
|
'sendTyping',
|
||||||
'createMessageCollector',
|
'createMessageCollector',
|
||||||
'awaitMessages',
|
'awaitMessages',
|
||||||
|
'createMessageComponentCollector',
|
||||||
|
'awaitMessageComponent',
|
||||||
'fetchWebhooks',
|
'fetchWebhooks',
|
||||||
'createWebhook',
|
'createWebhook',
|
||||||
'setRateLimitPerUser',
|
'setRateLimitPerUser',
|
||||||
'setNSFW',
|
'setNSFW',
|
||||||
|
'sendSlash',
|
||||||
|
'searchInteraction',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (const prop of props) {
|
for (const prop of props) {
|
||||||
@ -497,225 +567,3 @@ module.exports = TextBasedChannel;
|
|||||||
|
|
||||||
// Fixes Circular
|
// Fixes Circular
|
||||||
const MessageManager = require('../../managers/MessageManager');
|
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.ws.shards.first()?.sessionId,
|
|
||||||
data: {
|
|
||||||
version: commandVersion,
|
|
||||||
id: commandId,
|
|
||||||
name: commandName,
|
|
||||||
type: commandType,
|
|
||||||
options: postData,
|
|
||||||
attachments: attachments,
|
|
||||||
},
|
|
||||||
nonce,
|
|
||||||
};
|
|
||||||
if (isGuildCommand) {
|
|
||||||
data.data.guild_id = guildId;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user