From 347caf9dcefb4b847621e8d4e7bd73d7cca629d6 Mon Sep 17 00:00:00 2001
From: March 7th <71698422+aiko-chan-ai@users.noreply.github.com>
Date: Mon, 15 Aug 2022 19:51:01 +0700
Subject: [PATCH] =?UTF-8?q?refactor(SendSlash):=20Perfect=20=F0=9F=90=9B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Document/SlashCommand.md | 16 +-
src/errors/Messages.js | 1 +
src/structures/ApplicationCommand.js | 501 ++++++++++--------
src/structures/interfaces/TextBasedChannel.js | 25 +-
typings/index.d.ts | 2 +-
5 files changed, 308 insertions(+), 237 deletions(-)
diff --git a/Document/SlashCommand.md b/Document/SlashCommand.md
index d1c2fc1..53f2e86 100644
--- a/Document/SlashCommand.md
+++ b/Document/SlashCommand.md
@@ -2,6 +2,10 @@
- Support Autocomplete feature (half)
- Unused `guild.searchInteraction()` (Use only if not working properly)
+# BREAKING CHANGE: Using Slash Command (Sub Command / Sub Group Command) will not accept subCommand argument in args. That means Command Name needs to be changed same as Discord Client
+
+# All image demo : v2.3
+
# Slash Command (no options)
### Demo
@@ -14,7 +18,6 @@
```js
await message.channel.sendSlash('botid', 'aiko')
-// Return nonce (view document)
```
### Result
@@ -23,15 +26,17 @@ await message.channel.sendSlash('botid', 'aiko')
# Slash Command + Sub option (group)
-### Demo
+### Demo (v2.5)

### Code test
-```js
-await message.channel.sendSlash('450323683840491530', 'animal', 'chat', 'bye')
-// Return nonce (view document)
+```diff
+ v2.5
+- await message.channel.sendSlash('450323683840491530', 'animal', 'chat', 'bye')
+ v2.6+
++ await message.channel.sendSlash('450323683840491530', 'animal chat', 'bye')
```
### Result
@@ -51,7 +56,6 @@ const { MessageAttachment } = require('discord.js-selfbot-v13')
const fs = require('fs')
const a = new MessageAttachment(fs.readFileSync('./wallpaper.jpg') , 'test.jpg')
await message.channel.sendSlash('718642000898818048', 'sauce', a)
-// Return nonce (view document)
```
### Result
diff --git a/src/errors/Messages.js b/src/errors/Messages.js
index c4c8b54..0bb80ce 100644
--- a/src/errors/Messages.js
+++ b/src/errors/Messages.js
@@ -50,6 +50,7 @@ const Messages = {
/* Add */
MISSING_PERMISSIONS: (...permission) => `You can't do this action [Missing Permission(s): ${permission.join(', ')}]`,
EMBED_PROVIDER_NAME: 'MessageEmbed provider name must be a string.',
+ INVALID_COMMAND_NAME: allCMD => `Could not parse subGroupCommand and subCommand due to too long: ${allCMD.join(' ')}`,
BUTTON_LABEL: 'MessageButton label must be a string',
BUTTON_URL: 'MessageButton URL must be a string',
diff --git a/src/structures/ApplicationCommand.js b/src/structures/ApplicationCommand.js
index 4786c32..8c48151 100644
--- a/src/structures/ApplicationCommand.js
+++ b/src/structures/ApplicationCommand.js
@@ -10,6 +10,7 @@ const Permissions = require('../util/Permissions');
const SnowflakeUtil = require('../util/SnowflakeUtil');
const { lazy } = require('../util/Util');
const Message = lazy(() => require('../structures/Message').Message);
+
/**
* Represents an application command.
* @extends {Base}
@@ -588,11 +589,15 @@ class ApplicationCommand extends Base {
/**
* Send Slash command to channel
* @param {Message} message Discord Message
+ * @param {Array} subCommandArray SubCommand Array
* @param {Array} options The options to Slash Command
* @returns {Promise}
*/
- async sendSlashCommand(message, options = []) {
+ // eslint-disable-next-line consistent-return
+ async sendSlashCommand(message, subCommandArray = [], options = []) {
// Todo: Refactor
+ const buildError = (type, value, array, msg) =>
+ new Error(`Invalid ${type}: ${value} ${msg}\nList of ${type}:\n${array}`);
// Check Options
if (!(message instanceof Message())) {
throw new TypeError('The message must be a Discord.Message');
@@ -604,6 +609,167 @@ class ApplicationCommand extends Base {
const optionFormat = [];
const attachments = [];
const attachmentsBuffer = [];
+ const parseChoices = (list_choices, value) => {
+ if (value !== undefined) {
+ if (Array.isArray(list_choices) && list_choices.length) {
+ const choice = list_choices.find(c => c.name === value) || list_choices.find(c => c.value === value);
+ if (choice) {
+ return choice.value;
+ }
+ throw buildError(
+ 'choice',
+ value,
+ list_choices.map((c, i) => ` #${i + 1} Name: ${c.name} Value: ${c.value}`).join('\n'),
+ 'is not a valid choice for this option',
+ );
+ } else {
+ return value;
+ }
+ } else {
+ return undefined;
+ }
+ };
+ const parseOption = async (optionCommand, value) => {
+ const data = {
+ type: ApplicationCommandOptionTypes[optionCommand.type],
+ name: optionCommand.name,
+ };
+ if (value !== undefined) {
+ value = parseChoices(optionCommand.choices, value);
+ switch (optionCommand.type) {
+ case 'BOOLEAN': {
+ data.value = Boolean(value);
+ break;
+ }
+ case 'INTEGER': {
+ data.value = Number(value);
+ break;
+ }
+ case 'ATTACHMENT': {
+ data.value = await addDataFromAttachment(value);
+ break;
+ }
+ case 'SUB_COMMAND_GROUP': {
+ break;
+ }
+ default: {
+ if (optionCommand.autocomplete) {
+ let optionsBuild;
+ switch (subCommandArray.length) {
+ case 0: {
+ optionsBuild = [
+ ...optionFormat,
+ {
+ type: ApplicationCommandOptionTypes[optionCommand.type],
+ name: optionCommand.name,
+ value,
+ focused: true,
+ },
+ ];
+ break;
+ }
+ case 1: {
+ const subCommand = this.options.find(o => o.name == subCommandArray[0] && o.type == 'SUB_COMMAND');
+ optionsBuild = [
+ {
+ type: ApplicationCommandOptionTypes[subCommand.type],
+ name: subCommand.name,
+ options: [
+ ...optionFormat,
+ {
+ type: ApplicationCommandOptionTypes[optionCommand.type],
+ name: optionCommand.name,
+ value,
+ focused: true,
+ },
+ ],
+ },
+ ];
+ break;
+ }
+ case 2: {
+ const subGroup = this.options.find(
+ o => o.name == subCommandArray[0] && o.type == 'SUB_COMMAND_GROUP',
+ );
+ const subCommand = this.options.find(o => o.name == subCommandArray[1] && o.type == 'SUB_COMMAND');
+ optionsBuild = [
+ {
+ type: ApplicationCommandOptionTypes[subGroup.type],
+ name: subGroup.name,
+ options: [
+ {
+ type: ApplicationCommandOptionTypes[subCommand.type],
+ name: subCommand.name,
+ options: [
+ ...optionFormat,
+ {
+ type: ApplicationCommandOptionTypes[optionCommand.type],
+ name: optionCommand.name,
+ value,
+ focused: true,
+ },
+ ],
+ },
+ ],
+ },
+ ];
+ break;
+ }
+ }
+ const autoValue = await getAutoResponse(optionsBuild, value);
+ data.value = autoValue;
+ } else {
+ data.value = value;
+ }
+ }
+ }
+ optionFormat.push(data);
+ }
+ return optionFormat;
+ };
+ const parseSubCommand = async (subCommandName, options) => {
+ const subCommand = this.options.find(o => o.name == subCommandName && o.type == 'SUB_COMMAND');
+ if (!subCommand) {
+ throw buildError(
+ 'SubCommand',
+ subCommandName,
+ this.options.map((o, i) => ` #${i + 1} Name: ${o.name}`).join('\n'),
+ 'is not a valid sub command',
+ );
+ }
+ const valueRequired = subCommand.options.filter(o => o.required).length;
+ for (let i = 0; i < options.length; i++) {
+ const optionInput = subCommand.options[i];
+ const value = options[i];
+ await parseOption(optionInput, value);
+ }
+ if (valueRequired > options.length) {
+ throw new Error(`Value required missing\nDebug:
+ Required: ${valueRequired} - Options: ${optionFormat.length}`);
+ }
+ return {
+ type: ApplicationCommandOptionTypes[subCommand.type],
+ name: subCommand.name,
+ options: optionFormat,
+ };
+ };
+ const parseSubGroupCommand = async (subGroupName, subName) => {
+ const subGroup = this.options.find(o => o.name == subGroupName && o.type == 'SUB_COMMAND_GROUP');
+ if (!subGroup) {
+ throw buildError(
+ 'SubGroupCommand',
+ subGroupName,
+ this.options.map((o, i) => ` #${i + 1} Name: ${o.name}`).join('\n'),
+ 'is not a valid sub group command',
+ );
+ }
+ const data = await parseSubCommand(subName, options);
+ return {
+ type: ApplicationCommandOptionTypes[subGroup.type],
+ name: subGroup.name,
+ options: [data],
+ };
+ };
async function addDataFromAttachment(data) {
if (!(data instanceof MessageAttachment)) {
throw new TypeError('The attachment data must be a Discord.MessageAttachment');
@@ -617,49 +783,54 @@ class ApplicationCommand extends Base {
attachmentsBuffer.push({ attachment: data.attachment, name: data.name, file: resource });
return id;
}
- let option_ = [];
- let i = 0;
- // Check Command type is Sub group ?
- const subCommandCheck = this.options.some(option => ['SUB_COMMAND', 'SUB_COMMAND_GROUP'].includes(option.type));
- let subCommand;
- if (subCommandCheck) {
- subCommand = this.options.find(option => option.name == options[0]);
- options.shift();
- option_[0] = {
- type: ApplicationCommandOptionTypes[subCommand.type],
- name: subCommand.name,
- options: optionFormat,
- };
- } else {
- option_ = optionFormat;
- }
- // Autoresponse
- const getAutoResponse = async (options_, type, name, value) => {
- const op = Array.from(options_);
- op.push({
- type,
- name,
- value,
- focused: true,
- });
+ const getDataPost = (guildAdd = false, dataAdd = [], nonce, autocomplete = false) => {
+ if (!Array.isArray(dataAdd) && typeof dataAdd == 'object') {
+ dataAdd = [dataAdd];
+ }
+ if (guildAdd) {
+ return {
+ type: autocomplete ? 4 : 2, // Slash command, context menu
+ // Type: 4: Auto-complete
+ application_id: this.applicationId,
+ guild_id: message.guildId,
+ channel_id: message.channelId,
+ session_id: this.client.session_id,
+ data: {
+ // ApplicationCommandData
+ version: this.version,
+ id: this.id,
+ name: this.name,
+ type: ApplicationCommandTypes[this.type],
+ options: dataAdd,
+ attachments: attachments,
+ guild_id: message.guildId,
+ },
+ nonce,
+ };
+ } else {
+ return {
+ type: autocomplete ? 4 : 2, // Slash command, context menu
+ // Type: 4: Auto-complete
+ application_id: this.applicationId,
+ guild_id: message.guildId,
+ channel_id: message.channelId,
+ session_id: this.client.session_id,
+ data: {
+ // ApplicationCommandData
+ version: this.version,
+ id: this.id,
+ name: this.name,
+ type: ApplicationCommandTypes[this.type],
+ options: dataAdd,
+ attachments: attachments,
+ },
+ nonce,
+ };
+ }
+ };
+ const getAutoResponse = async (sendData, value) => {
let nonce = SnowflakeUtil.generate();
- const data = {
- type: 4, // Auto-complete
- application_id: this.applicationId,
- guild_id: message.guildId,
- channel_id: message.channelId,
- session_id: this.client.session_id,
- data: {
- // ApplicationCommandData
- version: this.version,
- id: this.id,
- name: this.name,
- type: ApplicationCommandTypes[this.type],
- options: op,
- attachments: attachments,
- },
- nonce,
- };
+ const data = getDataPost(false, sendData, nonce, true);
await this.client.api.interactions
.post({
data,
@@ -667,10 +838,9 @@ class ApplicationCommand extends Base {
})
.catch(async () => {
nonce = SnowflakeUtil.generate();
- data.data.guild_id = message.guildId;
- data.nonce = nonce;
+ const data_ = getDataPost(true, sendData, nonce);
await this.client.api.interactions.post({
- data,
+ body: data_,
files: attachmentsBuffer,
});
});
@@ -693,193 +863,68 @@ class ApplicationCommand extends Base {
this.client.on(Events.APPLICATION_COMMAND_AUTOCOMPLETE_RESPONSE, handler);
});
};
- for (i; i < options.length; i++) {
- const value = options[i];
- if (!subCommandCheck && !this?.options[i]) continue;
- if (subCommandCheck && subCommand?.options && !subCommand?.options[i]) continue;
- if (!subCommandCheck) {
- // Check value is invalid
- let choice;
- if (this.options[i].choices && this.options[i].choices.length > 0) {
- choice =
- this.options[i].choices.find(c => c.name == value) || this.options[i].choices.find(c => c.value == value);
- if (!choice && value !== undefined) {
- throw new Error(
- `Invalid option: ${value} is not a valid choice for this option\nList of choices:\n${this.options[
- i
- ].choices
- .map((c, i) => ` #${i + 1} Name: ${c.name} Value: ${c.value}`)
- .join('\n')}`,
- );
- }
- }
- const data = {
- type: ApplicationCommandOptionTypes[this.options[i].type],
- name: this.options[i].name,
- value:
- choice?.value || this.options[i].type == 'ATTACHMENT'
- ? await addDataFromAttachment(value)
- : this.options[i].type == 'INTEGER'
- ? Number(value)
- : this.options[i].type == 'BOOLEAN'
- ? Boolean(value)
- : this.options[i].autocomplete
- ? await getAutoResponse(
- optionFormat,
- ApplicationCommandOptionTypes[this.options[i].type],
- this.options[i].name,
- value,
- )
- : value,
- };
- if (value !== undefined) optionFormat.push(data);
- } else {
- // First element is sub command and removed
- if (!value) continue;
- // Check value is invalid
- let choice;
- if (subCommand?.options && subCommand.options[i].choices && subCommand.options[i].choices.length > 0) {
- choice =
- subCommand.options[i].choices.find(c => c.name == value) ||
- subCommand.options[i].choices.find(c => c.value == value);
- if (!choice && value !== undefined) {
- throw new Error(
- `Invalid option: ${value} is not a valid choice for this option\nList of choices: \n${subCommand.options[
- i
- ].choices
- .map((c, i) => `#${i + 1} Name: ${c.name} Value: ${c.value}\n`)
- .join('')}`,
- );
- }
- }
- const data = {
- type: ApplicationCommandOptionTypes[subCommand.options[i].type],
- name: subCommand.options[i].name,
- value:
- choice?.value || subCommand.options[i].type == 'ATTACHMENT'
- ? await addDataFromAttachment(value)
- : subCommand.options[i].type == 'INTEGER'
- ? Number(value)
- : subCommand.options[i].type == 'BOOLEAN'
- ? Boolean(value)
- : this.options[i].autocomplete
- ? await getAutoResponse(
- optionFormat,
- ApplicationCommandOptionTypes[subCommand.options[i].type],
- subCommand.options[i].name,
- value,
- )
- : value,
- };
- if (value !== undefined) optionFormat.push(data);
- }
- }
- if (!subCommandCheck && this.options[i]?.required) {
- throw new Error('Value required missing');
- }
- if (subCommandCheck && subCommand?.options && subCommand?.options[i]?.required) {
- throw new Error('Value required missing');
- }
- let nonce = SnowflakeUtil.generate();
- const data = {
- type: 2, // Slash command, context menu
- // Type: 4: Auto-complete
- application_id: this.applicationId,
- guild_id: message.guildId,
- channel_id: message.channelId,
- session_id: this.client.session_id,
- data: {
- // ApplicationCommandData
- version: this.version,
- id: this.id,
- name: this.name,
- type: ApplicationCommandTypes[this.type],
- options: option_,
- attachments: attachments,
- },
- nonce,
- };
- await this.client.api.interactions
- .post({
- body: data,
- files: attachmentsBuffer,
- })
- .catch(async () => {
- nonce = SnowflakeUtil.generate();
- data.data.guild_id = message.guildId;
- data.nonce = nonce;
- await this.client.api.interactions.post({
+ const sendData = async (optionsData = []) => {
+ let nonce = SnowflakeUtil.generate();
+ const data = getDataPost(false, optionsData, nonce);
+ await this.client.api.interactions
+ .post({
body: data,
files: attachmentsBuffer,
+ })
+ .catch(async () => {
+ nonce = SnowflakeUtil.generate();
+ const data_ = getDataPost(true, optionsData, nonce);
+ await this.client.api.interactions.post({
+ body: data_,
+ files: attachmentsBuffer,
+ });
});
+ return new Promise((resolve, reject) => {
+ const handler = data => {
+ timeout.refresh();
+ if (data.metadata.nonce !== nonce) return;
+ clearTimeout(timeout);
+ this.client.removeListener('interactionResponse', handler);
+ this.client.decrementMaxListeners();
+ if (data.status) resolve(data.metadata);
+ else reject(data.metadata);
+ };
+ const timeout = setTimeout(() => {
+ this.client.removeListener('interactionResponse', handler);
+ this.client.decrementMaxListeners();
+ reject(new Error('INTERACTION_TIMEOUT'));
+ }, 15_000).unref();
+ this.client.incrementMaxListeners();
+ this.client.on('interactionResponse', handler);
});
- return new Promise((resolve, reject) => {
- const handler = data => {
- timeout.refresh();
- if (data.metadata.nonce !== nonce) return;
- clearTimeout(timeout);
- this.client.removeListener('interactionResponse', handler);
- this.client.decrementMaxListeners();
- if (data.status) resolve(data.metadata);
- else reject(data.metadata);
- };
- const timeout = setTimeout(() => {
- this.client.removeListener('interactionResponse', handler);
- this.client.decrementMaxListeners();
- reject(new Error('INTERACTION_TIMEOUT'));
- }, 15_000).unref();
- this.client.incrementMaxListeners();
- this.client.on('interactionResponse', handler);
- });
- }
- /**
- * Message Context Menu
- * @param {Message} message Discord Message
- * @param {boolean} sendFromMessage nothing .-. not used
- * @returns {Promise}
- */
- async sendContextMenu(message, sendFromMessage = false) {
- if (!sendFromMessage && !(message instanceof Message())) {
- throw new TypeError('The message must be a Discord.Message');
+ };
+ // SubCommandArray length max 2
+ // length = 0 => no sub command
+ // length = 1 => sub command
+ // length = 2 => sub command group + sub command
+ switch (subCommandArray.length) {
+ case 0: {
+ const valueRequired = this.options.filter(o => o.required).length;
+ for (let i = 0; i < options.length; i++) {
+ const optionInput = this.options[i];
+ const value = options[i];
+ await parseOption(optionInput, value);
+ }
+ if (valueRequired > options.length) {
+ throw new Error(`Value required missing\nDebug:
+ Required: ${valueRequired} - Options: ${optionFormat.length}`);
+ }
+ return sendData(optionFormat);
+ }
+ case 1: {
+ const optionsData = await parseSubCommand(subCommandArray[0], options);
+ return sendData(optionsData);
+ }
+ case 2: {
+ const optionsData = await parseSubGroupCommand(subCommandArray[0], subCommandArray[1], options);
+ return sendData(optionsData);
+ }
}
- if (this.type == 'CHAT_INPUT') return false;
- const nonce = SnowflakeUtil.generate();
- await this.client.api.interactions.post({
- body: {
- type: 2, // Slash command, context menu
- application_id: this.applicationId,
- guild_id: message.guildId,
- channel_id: message.channelId,
- session_id: this.client.session_id,
- data: {
- // ApplicationCommandData
- version: this.version,
- id: this.id,
- name: this.name,
- type: ApplicationCommandTypes[this.type],
- target_id: ApplicationCommandTypes[this.type] == 1 ? message.author.id : message.id,
- },
- nonce,
- },
- });
- return new Promise((resolve, reject) => {
- const handler = data => {
- timeout.refresh();
- if (data.metadata.nonce !== nonce) return;
- clearTimeout(timeout);
- this.client.removeListener('interactionResponse', handler);
- this.client.decrementMaxListeners();
- if (data.status) resolve(data.metadata);
- else reject(data.metadata);
- };
- const timeout = setTimeout(() => {
- this.client.removeListener('interactionResponse', handler);
- this.client.decrementMaxListeners();
- reject(new Error('INTERACTION_TIMEOUT'));
- }, 15_000).unref();
- this.client.incrementMaxListeners();
- this.client.on('interactionResponse', handler);
- });
}
}
diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js
index d19c439..fce2300 100644
--- a/src/structures/interfaces/TextBasedChannel.js
+++ b/src/structures/interfaces/TextBasedChannel.js
@@ -10,6 +10,14 @@ const { TypeError, Error } = require('../../errors');
const InteractionCollector = require('../InteractionCollector');
const { lazy } = require('../../util/Util');
const Message = lazy(() => require('../Message').Message);
+const { s } = require('@sapphire/shapeshift');
+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.
@@ -395,7 +403,7 @@ class TextBasedChannel {
/**
* Send Slash to this channel
* @param {Snowflake} botId Bot Id (Supports application ID - not bot)
- * @param {string} commandName Command name
+ * @param {string} commandString Command name (and sub / group formats)
* @param {...?string|string[]} args Command arguments
* @returns {Promise}
* @example
@@ -407,9 +415,21 @@ class TextBasedChannel {
* channel.sendSlash('123456789012345678', 'embed', 'title', 'description', 'author', '#00ff00')
* // Send embed with Title and Color:
* channel.sendSlash('123456789012345678', 'embed', 'title', undefined, undefined, '#00ff00')
+ * // CommandName is Group Command / Sub Command
+ * channel.sendSlash('123456789012345678', 'embed title', 'description', 'author', '#00ff00')
*/
- async sendSlash(botId, commandName, ...args) {
+ async sendSlash(botId, commandString, ...args) {
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 (!botId) throw new Error('Bot ID is required');
// ? maybe ...
const user = await this.client.users.fetch(botId).catch(() => {});
@@ -463,6 +483,7 @@ class TextBasedChannel {
content: '',
id: this.client.user.id,
}),
+ sub && sub.length > 0 ? sub : [],
args && args.length ? args : [],
);
}
diff --git a/typings/index.d.ts b/typings/index.d.ts
index baf0fce..7f21125 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -474,7 +474,7 @@ export class ApplicationCommand extends Base {
private static transformCommand(command: ApplicationCommandData): RESTPostAPIApplicationCommandsJSONBody;
private static isAPICommandData(command: object): command is RESTPostAPIApplicationCommandsJSONBody;
// Add
- public static sendSlashCommand(message: Message, options?: string[]): Promise;
+ public static sendSlashCommand(message: Message, subCommandArray?: string[], options?: string[]): Promise;
public static sendContextMenu(message: Message): Promise;
}