This commit is contained in:
Elysia 2024-01-10 18:30:39 +07:00
parent b3cf4b7710
commit 59f254966c
14 changed files with 637 additions and 937 deletions

View File

@ -13,12 +13,15 @@ const Util = require('../util/Util');
class BaseClient extends EventEmitter {
constructor(options = {}) {
super();
if (options.intents) {
process.emitWarning('Intents is not available.', 'DeprecationWarning');
}
if (typeof options.captchaSolver === 'function') {
options.captchaService = 'custom';
}
/**
* The options the client was instantiated with
* @type {ClientOptions}

View File

@ -126,36 +126,15 @@ class WebSocketManager extends EventEmitter {
* @private
*/
async connect() {
// eslint-disable-next-line no-unused-vars
const invalidToken = new Error(WSCodes[4004]);
/*
BOT
const {
url: gatewayURL,
shards: recommendedShards,
session_start_limit: sessionStartLimit,
} = await this.client.api.gateway.bot.get().catch(error => {
throw error.httpStatus === 401 ? invalidToken : error;
});
*/
let gatewayURL = 'wss://gateway.discord.gg';
const { url } = await this.client.api.gateway.get({ auth: false }).catch(() => ({ url: gatewayURL }));
// eslint-disable-next-line no-unused-vars
/*
.catch(error => {
// Never throw error :v
// throw error.httpStatus === 401 ? invalidToken : error;
});
*/
if (url) gatewayURL = url;
const recommendedShards = 1;
const sessionStartLimit = {
total: Infinity,
remaining: Infinity,
};
await this.client.api.gateway
.get({ auth: false })
.then(r => (gatewayURL = r.url))
.catch(() => {});
const { total, remaining } = sessionStartLimit;
const total = Infinity;
const remaining = Infinity;
const recommendedShards = 1;
this.debug(`Fetched Gateway Information
URL: ${gatewayURL}
@ -294,7 +273,7 @@ class WebSocketManager extends EventEmitter {
} catch (error) {
this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`);
if (error.httpStatus !== 401) {
this.debug('Possible network error occurred. Retrying in 5s...');
this.debug(`Possible network error occurred. Retrying in 5s...`);
await sleep(5_000);
this.reconnecting = false;
return this.reconnect();
@ -368,11 +347,12 @@ class WebSocketManager extends EventEmitter {
/**
* Emitted whenever a packet isn't handled.
* @event Client#unhandledPacket
* @param {Object} packet The packet (t: Event name, d: Data)
* @param {Number} shard The shard that received the packet (Auto = 0)
* @param {Object} packet The packet (t: EVENT_NAME, d: any)
* @param {Number} shard The shard that received the packet (Shard 0)
*/
this.client.emit(Events.UNHANDLED_PACKET, packet, shard);
}
return true;
}

View File

@ -1,5 +1,6 @@
'use strict';
const { isJSONEncodable } = require('@discordjs/builders');
const { Collection } = require('@discordjs/collection');
const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager');
const CachedManager = require('./CachedManager');
@ -13,15 +14,14 @@ const Permissions = require('../util/Permissions');
* @extends {CachedManager}
*/
class ApplicationCommandManager extends CachedManager {
constructor(client, iterable, user) {
constructor(client, iterable) {
super(client, ApplicationCommand, iterable);
/**
* The manager for permissions of arbitrary commands on arbitrary guilds
* @type {ApplicationCommandPermissionsManager}
*/
this.permissions = new ApplicationCommandPermissionsManager(this, user);
this.user = user;
this.permissions = new ApplicationCommandPermissionsManager(this);
}
/**
@ -43,7 +43,7 @@ class ApplicationCommandManager extends CachedManager {
* @private
*/
commandPath({ id, guildId } = {}) {
let path = this.client.api.applications(this.user.id);
let path = this.client.api.applications(this.client.application.id);
if (this.guild ?? guildId) path = path.guilds(this.guild?.id ?? guildId);
return id ? path.commands(id) : path.commands;
}
@ -58,7 +58,7 @@ class ApplicationCommandManager extends CachedManager {
/* eslint-disable max-len */
/**
* Data that resolves to the data of an ApplicationCommand
* @typedef {ApplicationCommandDataResolvable|SlashCommandBuilder|ContextMenuCommandBuilder} ApplicationCommandDataResolvable
* @typedef {ApplicationCommandData|APIApplicationCommand|SlashCommandBuilder|ContextMenuCommandBuilder} ApplicationCommandDataResolvable
*/
/* eslint-enable max-len */
@ -94,7 +94,6 @@ class ApplicationCommandManager extends CachedManager {
* .catch(console.error);
*/
async fetch(id, { guildId, cache = true, force = false, locale, withLocalizations } = {}) {
// Change from user.createDM to opcode (risky action)
if (typeof id === 'object') {
({ guildId, cache = true, locale, withLocalizations } = id);
} else if (id) {
@ -102,11 +101,10 @@ class ApplicationCommandManager extends CachedManager {
const existing = this.cache.get(id);
if (existing) return existing;
}
await this.user.createDM().catch(() => {});
const command = await this.commandPath({ id, guildId }).get();
return this._add(command, cache);
}
await this.user.createDM().catch(() => {});
const data = await this.commandPath({ guildId }).get({
headers: {
'X-Discord-Locale': locale,
@ -132,7 +130,6 @@ class ApplicationCommandManager extends CachedManager {
* .catch(console.error);
*/
async create(command, guildId) {
if (!this.client.user.bot) throw new Error('INVALID_USER_METHOD');
const data = await this.commandPath({ guildId }).post({
data: this.constructor.transformCommand(command),
});
@ -162,7 +159,6 @@ class ApplicationCommandManager extends CachedManager {
* .catch(console.error);
*/
async set(commands, guildId) {
if (!this.client.user.bot) throw new Error('INVALID_USER_METHOD');
const data = await this.commandPath({ guildId }).put({
data: commands.map(c => this.constructor.transformCommand(c)),
});
@ -185,7 +181,6 @@ class ApplicationCommandManager extends CachedManager {
* .catch(console.error);
*/
async edit(command, data, guildId) {
if (!this.client.user.bot) throw new Error('INVALID_USER_METHOD');
const id = this.resolveId(command);
if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
@ -208,7 +203,6 @@ class ApplicationCommandManager extends CachedManager {
* .catch(console.error);
*/
async delete(command, guildId) {
if (!this.client.user.bot) throw new Error('INVALID_USER_METHOD');
const id = this.resolveId(command);
if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
@ -226,6 +220,8 @@ class ApplicationCommandManager extends CachedManager {
* @private
*/
static transformCommand(command) {
if (isJSONEncodable(command)) return command.toJSON();
let default_member_permissions;
if ('default_member_permissions' in command) {
@ -240,6 +236,7 @@ class ApplicationCommandManager extends CachedManager {
? new Permissions(command.defaultMemberPermissions).bitfield.toString()
: command.defaultMemberPermissions;
}
return {
name: command.name,
name_localizations: command.nameLocalizations ?? command.name_localizations,

View File

@ -10,7 +10,7 @@ const { ApplicationCommandPermissionTypes, APIErrors } = require('../util/Consta
* @extends {BaseManager}
*/
class ApplicationCommandPermissionsManager extends BaseManager {
constructor(manager, user) {
constructor(manager) {
super(manager.client);
/**
@ -37,8 +37,6 @@ class ApplicationCommandPermissionsManager extends BaseManager {
* @type {?Snowflake}
*/
this.commandId = manager.id ?? null;
this.user = user;
}
/**
@ -49,10 +47,7 @@ class ApplicationCommandPermissionsManager extends BaseManager {
* @private
*/
permissionsPath(guildId, commandId) {
return this.client.api
.applications(typeof this.user === 'string' ? this.user : this.user.id)
.guilds(guildId)
.commands(commandId).permissions;
return this.client.api.applications(this.client.application.id).guilds(guildId).commands(commandId).permissions;
}
/**
@ -164,7 +159,6 @@ class ApplicationCommandPermissionsManager extends BaseManager {
* .catch(console.error);
*/
async set({ guild, command, permissions, fullPermissions } = {}) {
if (!this.manager.client.user.bot) throw new Error('INVALID_USER_METHOD');
const { guildId, commandId } = this._validateOptions(guild, command);
if (commandId) {
@ -226,7 +220,6 @@ class ApplicationCommandPermissionsManager extends BaseManager {
* .catch(console.error);
*/
async add({ guild, command, permissions }) {
if (!this.manager.client.user.bot) throw new Error('INVALID_USER_METHOD');
const { guildId, commandId } = this._validateOptions(guild, command);
if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
if (!Array.isArray(permissions)) {
@ -278,13 +271,12 @@ class ApplicationCommandPermissionsManager extends BaseManager {
* .catch(console.error);
*/
async remove({ guild, command, users, roles }) {
if (!this.manager.client.user.bot) throw new Error('INVALID_USER_METHOD');
const { guildId, commandId } = this._validateOptions(guild, command);
if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
if (!users && !roles) throw new TypeError('INVALID_TYPE', 'users OR roles', 'Array or Resolvable', true);
const resolvedIds = [];
let resolvedIds = [];
if (Array.isArray(users)) {
users.forEach(user => {
const userId = this.client.users.resolveId(user);

View File

@ -3,15 +3,15 @@
const { Collection } = require('@discordjs/collection');
const CachedManager = require('./CachedManager');
const { Error } = require('../errors');
const { lazy } = require('../util/Util');
const User = lazy(() => require('../structures/User'));
const User = require('../structures/User');
/**
* Manages API methods for users who reacted to a reaction and stores their cache.
* @extends {CachedManager}
*/
class ReactionUserManager extends CachedManager {
constructor(reaction, iterable) {
super(reaction.client, User(), iterable);
super(reaction.client, User, iterable);
/**
* The reaction that this manager belongs to
@ -22,7 +22,7 @@ class ReactionUserManager extends CachedManager {
/**
* The cache of this manager
* @type {Collection<Snowflake, Discord.User>}
* @type {Collection<Snowflake, User>}
* @name ReactionUserManager#cache
*/
@ -36,7 +36,7 @@ class ReactionUserManager extends CachedManager {
/**
* Fetches all the users that gave this reaction. Resolves with a collection of users, mapped by their ids.
* @param {FetchReactionUsersOptions} [options] Options for fetching the users
* @returns {Promise<Collection<Snowflake, Discord.User>>}
* @returns {Promise<Collection<Snowflake, User>>}
*/
async fetch({ limit = 100, after } = {}) {
const message = this.reaction.message;

View File

@ -1,28 +1,17 @@
'use strict';
const { setTimeout } = require('node:timers');
const { findBestMatch } = require('string-similarity');
const Base = require('./Base');
const MessagePayload = require('./MessagePayload');
const ApplicationCommandPermissionsManager = require('../managers/ApplicationCommandPermissionsManager');
const {
ApplicationCommandOptionTypes,
ApplicationCommandTypes,
ChannelTypes,
Events,
InteractionTypes,
} = require('../util/Constants');
const { ApplicationCommandOptionTypes, ApplicationCommandTypes, ChannelTypes } = require('../util/Constants');
const Permissions = require('../util/Permissions');
const SnowflakeUtil = require('../util/SnowflakeUtil');
const { lazy, getAttachments, uploadFile } = require('../util/Util');
const Message = lazy(() => require('../structures/Message').Message);
/**
* Represents an application command.
* @extends {Base}
*/
class ApplicationCommand extends Base {
constructor(client, data) {
constructor(client, data, guild, guildId) {
super(client);
/**
@ -37,11 +26,24 @@ class ApplicationCommand extends Base {
*/
this.applicationId = data.application_id;
/**
* The guild this command is part of
* @type {?Guild}
*/
this.guild = guild ?? null;
/**
* The guild's id this command is part of, this may be non-null when `guild` is `null` if the command
* was fetched from the `ApplicationCommandManager`
* @type {?Snowflake}
*/
this.guildId = guild?.id ?? guildId ?? null;
/**
* The manager for permissions of this command on its guild or arbitrary guilds when the command is global
* @type {ApplicationCommandPermissionsManager}
*/
this.permissions = new ApplicationCommandPermissionsManager(this, this.applicationId);
this.permissions = new ApplicationCommandPermissionsManager(this);
/**
* The type of this application command
@ -49,30 +51,10 @@ class ApplicationCommand extends Base {
*/
this.type = ApplicationCommandTypes[data.type];
this.user = client.users.cache.get(this.applicationId);
this._patch(data);
}
/**
* The guild this command is part of
* @type {?Guild}
* @readonly
*/
get guild() {
return this.client.guilds.resolve(this.guildId);
}
_patch(data) {
if ('guild_id' in data) {
/**
* The guild's id this command is part of, this may be non-null when `guild` is `null` if the command
* was fetched from the `ApplicationCommandManager`
* @type {?Snowflake}
*/
this.guildId = data.guild_id ?? null;
}
if ('name' in data) {
/**
* The name of this command
@ -148,7 +130,6 @@ class ApplicationCommand extends Base {
*/
this.defaultPermission = data.default_permission;
}
/* eslint-disable max-len */
if ('default_member_permissions' in data) {
@ -336,7 +317,6 @@ class ApplicationCommand extends Base {
setDefaultPermission(defaultPermission = true) {
return this.edit({ defaultPermission });
}
/* eslint-enable max-len */
/**
@ -391,6 +371,7 @@ class ApplicationCommand extends Base {
equals(command, enforceOptionOrder = false) {
// If given an id, check if the id matches
if (command.id && this.id !== command.id) return false;
let defaultMemberPermissions = null;
let dmPermission = command.dmPermission ?? command.dm_permission;
@ -404,6 +385,7 @@ class ApplicationCommand extends Base {
defaultMemberPermissions =
command.defaultMemberPermissions !== null ? new Permissions(command.defaultMemberPermissions).bitfield : null;
}
// Check top level parameters
const commandType = typeof command.type === 'string' ? command.type : ApplicationCommandTypes[command.type];
if (
@ -446,9 +428,7 @@ class ApplicationCommand extends Base {
const newOptions = new Map(options.map(option => [option.name, option]));
for (const option of existing) {
const foundOption = newOptions.get(option.name);
if (!foundOption || !this._optionEquals(option, foundOption)) {
return false;
}
if (!foundOption || !this._optionEquals(option, foundOption)) return false;
}
return true;
}
@ -597,423 +577,6 @@ class ApplicationCommand extends Base {
[maxLengthKey]: option.maxLength ?? option.max_length,
};
}
/**
* Send Slash command to channel
* @param {Message} message Discord Message
* @param {Array<string>} subCommandArray SubCommand Array
* @param {Array<any>} options The options to Slash Command
* @returns {Promise<InteractionResponse>}
*/
// eslint-disable-next-line consistent-return
async sendSlashCommand(message, subCommandArray = [], options = []) {
// Todo: Refactor [Done]
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');
}
if (!Array.isArray(options)) {
throw new TypeError('The options must be an array of strings');
}
if (this.type !== 'CHAT_INPUT') throw new Error('This command is not a chat input [/]');
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, this.client);
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 = subGroup.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, subGroup) => {
const options_sub = subGroup ? subGroup.options : this.options;
const subCommand = options_sub.find(
o => (o.name == subCommandName || o.nameLocalized == subCommandName) && o.type == 'SUB_COMMAND',
);
if (!subCommand) {
throw buildError(
'SubCommand',
subCommandName,
options_sub.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 || 0;
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.nameLocalized == 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, subGroup);
return {
type: ApplicationCommandOptionTypes[subGroup.type],
name: subGroup.name,
options: [data],
};
};
async function addDataFromAttachment(data, client) {
const data_ = await MessagePayload.resolveFile(data);
if (!data_.file) {
throw new TypeError(
'The attachment data must be a BufferResolvable or Stream or FileOptions of MessageAttachment',
);
}
if (client.options.usingNewAttachmentAPI === true) {
const attachments_ = await getAttachments(client, message.channelId, data_);
await uploadFile(data_.file, attachments_[0].upload_url);
const id = attachments.length;
attachments.push({
id: id,
filename: data_.name,
uploaded_filename: attachments_[0].upload_filename,
});
return id;
} else {
const id = attachments.length;
attachments.push({
id: id,
filename: data_.name,
});
attachmentsBuffer.push(data_);
return id;
}
}
const getDataPost = (dataAdd = [], nonce, autocomplete = false) => {
if (!Array.isArray(dataAdd) && typeof dataAdd == 'object') {
dataAdd = [dataAdd];
}
const data = {
type: autocomplete ? InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE : InteractionTypes.APPLICATION_COMMAND,
application_id: this.applicationId,
guild_id: message.guildId,
channel_id: message.channelId,
session_id: this.client.sessionId,
data: {
version: this.version,
id: this.id,
name: this.name,
type: ApplicationCommandTypes[this.type],
options: dataAdd,
attachments: attachments,
},
nonce,
};
if (this.guildId) {
data.data.guild_id = message.guildId;
}
return data;
};
const getAutoResponse = async (sendData, value) => {
let nonce = SnowflakeUtil.generate();
const data = getDataPost(sendData, nonce, true);
await this.client.api.interactions.post({
data,
files: attachmentsBuffer,
});
return new Promise(resolve => {
const handler = data => {
timeout.refresh();
if (data.nonce !== nonce) return;
clearTimeout(timeout);
this.client.removeListener(Events.APPLICATION_COMMAND_AUTOCOMPLETE_RESPONSE, handler);
this.client.decrementMaxListeners();
if (data.choices.length > 1) {
// Find best match name
const bestMatch = findBestMatch(
value,
data.choices.map(c => c.name),
);
const result = data.choices.find(c => c.name == bestMatch.bestMatch.target);
resolve(result.value);
} else {
resolve(value);
}
};
const timeout = setTimeout(() => {
this.client.removeListener(Events.APPLICATION_COMMAND_AUTOCOMPLETE_RESPONSE, handler);
this.client.decrementMaxListeners();
resolve(value);
}, this.client.options.interactionTimeout).unref();
this.client.incrementMaxListeners();
this.client.on(Events.APPLICATION_COMMAND_AUTOCOMPLETE_RESPONSE, handler);
});
};
const sendData = async (optionsData = []) => {
let nonce = SnowflakeUtil.generate();
const data = getDataPost(optionsData, nonce);
await this.client.api.interactions.post({
data,
useFormDataPayloadJSON: true,
files: attachmentsBuffer,
});
this.client._interactionCache.set(nonce, {
channelId: message.channelId,
guildId: message.guildId,
metadata: data,
});
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(
new Error('INTERACTION_ERROR', {
cause: data,
}),
);
}
};
const timeout = setTimeout(() => {
this.client.removeListener('interactionResponse', handler);
this.client.decrementMaxListeners();
reject(
new Error('INTERACTION_TIMEOUT', {
cause: data,
}),
);
}, this.client.options.interactionTimeout).unref();
this.client.incrementMaxListeners();
this.client.on('interactionResponse', handler);
});
};
// 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 || 0;
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);
}
}
}
/**
* Message Context Menu
* @param {Message} message Discord Message
* @returns {Promise<InteractionResponse>}
*/
async sendContextMenu(message) {
if (!(message instanceof Message())) {
throw new TypeError('The message must be a Discord.Message');
}
if (this.type == 'CHAT_INPUT') return false;
const nonce = SnowflakeUtil.generate();
const data = {
type: InteractionTypes.APPLICATION_COMMAND,
application_id: this.applicationId,
guild_id: message.guildId,
channel_id: message.channelId,
session_id: this.client.sessionId,
data: {
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,
};
if (this.guildId) {
data.data.guild_id = message.guildId;
}
await this.client.api.interactions.post({
data,
useFormDataPayloadJSON: true,
});
this.client._interactionCache.set(nonce, {
channelId: message.channelId,
guildId: message.guildId,
metadata: data,
});
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(
new Error('INTERACTION_ERROR', {
cause: data,
}),
);
}
};
const timeout = setTimeout(() => {
this.client.removeListener('interactionResponse', handler);
this.client.decrementMaxListeners();
reject(
new Error('INTERACTION_TIMEOUT', {
cause: data,
}),
);
}, this.client.options.interactionTimeout).unref();
this.client.incrementMaxListeners();
this.client.on('interactionResponse', handler);
});
}
}
module.exports = ApplicationCommand;

View File

@ -3,7 +3,6 @@
const GuildChannel = require('./GuildChannel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const GuildTextThreadManager = require('../managers/GuildTextThreadManager');
const InteractionManager = require('../managers/InteractionManager');
const MessageManager = require('../managers/MessageManager');
/**
@ -21,12 +20,6 @@ class BaseGuildTextChannel extends GuildChannel {
*/
this.messages = new MessageManager(this);
/**
* A manager of the interactions sent to this channel
* @type {InteractionManager}
*/
this.interactions = new InteractionManager(this);
/**
* A manager of the threads belonging to this channel
* @type {GuildTextThreadManager}
@ -187,15 +180,10 @@ class BaseGuildTextChannel extends GuildChannel {
sendTyping() {}
createMessageCollector() {}
awaitMessages() {}
createMessageComponentCollector() {}
awaitMessageComponent() {}
bulkDelete() {}
fetchWebhooks() {}
createWebhook() {}
setRateLimitPerUser() {}
setNSFW() {}
sendSlash() {}
searchInteraction() {}
}
TextBasedChannel.applyToClass(BaseGuildTextChannel, true);

View File

@ -3,7 +3,6 @@
const { Collection } = require('@discordjs/collection');
const GuildChannel = require('./GuildChannel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const InteractionManager = require('../managers/InteractionManager');
const MessageManager = require('../managers/MessageManager');
const { VideoQualityModes } = require('../util/Constants');
const Permissions = require('../util/Permissions');
@ -28,12 +27,6 @@ class BaseGuildVoiceChannel extends GuildChannel {
*/
this.nsfw = Boolean(data.nsfw);
/**
* A manager of the interactions sent to this channel
* @type {InteractionManager}
*/
this.interactions = new InteractionManager(this);
this._patch(data);
}
@ -95,7 +88,7 @@ class BaseGuildVoiceChannel extends GuildChannel {
}
if ('nsfw' in data) {
this.nsfw = Boolean(data.nsfw);
this.nsfw = data.nsfw;
}
}
@ -170,11 +163,11 @@ class BaseGuildVoiceChannel extends GuildChannel {
* Sets the bitrate of the channel.
* @param {number} bitrate The new bitrate
* @param {string} [reason] Reason for changing the channel's bitrate
* @returns {Promise<VoiceChannel>}
* @returns {Promise<BaseGuildVoiceChannel>}
* @example
* // Set the bitrate of a voice channel
* voiceChannel.setBitrate(48_000)
* .then(vc => console.log(`Set bitrate to ${vc.bitrate}bps for ${vc.name}`))
* channel.setBitrate(48_000)
* .then(channel => console.log(`Set bitrate to ${channel.bitrate}bps for ${channel.name}`))
* .catch(console.error);
*/
setBitrate(bitrate, reason) {
@ -201,11 +194,11 @@ class BaseGuildVoiceChannel extends GuildChannel {
* Sets the user limit of the channel.
* @param {number} userLimit The new user limit
* @param {string} [reason] Reason for changing the user limit
* @returns {Promise<VoiceChannel>}
* @returns {Promise<BaseGuildVoiceChannel>}
* @example
* // Set the user limit of a voice channel
* voiceChannel.setUserLimit(42)
* .then(vc => console.log(`Set user limit to ${vc.userLimit} for ${vc.name}`))
* channel.setUserLimit(42)
* .then(channel => console.log(`Set user limit to ${channel.userLimit} for ${channel.name}`))
* .catch(console.error);
*/
setUserLimit(userLimit, reason) {
@ -216,7 +209,7 @@ class BaseGuildVoiceChannel extends GuildChannel {
* Sets the camera video quality mode of the channel.
* @param {VideoQualityMode|number} videoQualityMode The new camera video quality mode.
* @param {string} [reason] Reason for changing the camera video quality mode.
* @returns {Promise<VoiceChannel>}
* @returns {Promise<BaseGuildVoiceChannel>}
*/
setVideoQualityMode(videoQualityMode, reason) {
return this.edit({ videoQualityMode }, reason);
@ -229,9 +222,6 @@ class BaseGuildVoiceChannel extends GuildChannel {
sendTyping() {}
createMessageCollector() {}
awaitMessages() {}
createMessageComponentCollector() {}
awaitMessageComponent() {}
bulkDelete() {}
fetchWebhooks() {}
createWebhook() {}
setRateLimitPerUser() {}

View File

@ -7,7 +7,7 @@ const Base = require('./Base');
* Represents a call
* @extends {Base}
*/
class Call extends Base {
class CallState extends Base {
constructor(client, data) {
super(client);
/**
@ -16,14 +16,11 @@ class Call extends Base {
*/
this.channelId = data.channel_id;
/**
* The list of user ID who is ringing
* @type {Collection<Snowflake, User>}
*/
this.ringing = new Collection();
this._ringing = [];
this._patch(data);
}
_patch(data) {
if ('region' in data) {
/**
@ -33,11 +30,10 @@ class Call extends Base {
this.region = data.region;
}
if ('ringing' in data) {
for (const userId of data.ringing) {
this.ringing.set(userId, this.client.users.cache.get(userId));
}
this._ringing = data.ringing;
}
}
/**
* The channel of the call
* @type {?DMChannel|PartialGroupDMChannel}
@ -45,14 +41,23 @@ class Call extends Base {
get channel() {
return this.client.channels.cache.get(this.channelId);
}
/**
* Sets the voice region of the call
* @param {string} region Region of the call
* @returns {Promise<void>}
*/
setVoiceRegion(region) {
setRTCRegion(region) {
return this.client.api.channels(this.channelId).call.patch({ data: { region } });
}
/**
* The list of user ID who is ringing
* @type {Collection<Snowflake, User>}
*/
get ringing() {
return new Collection(this._ringing.map(id => [id, this.client.users.cache.get(id)]));
}
}
module.exports = Call;
module.exports = CallState;

View File

@ -224,7 +224,6 @@ class GuildAuditLogs {
this.applicationCommands.set(command.id, new ApplicationCommand(guild.client, command, guild));
}
}
/**
* Cached auto moderation rules.
* @type {Collection<Snowflake, AutoModerationRule>}
@ -487,7 +486,6 @@ class GuildAuditLogsEntry {
count: Number(data.options.count),
};
break;
case Actions.MESSAGE_PIN:
case Actions.MESSAGE_UNPIN:
this.extra = {
@ -533,13 +531,11 @@ class GuildAuditLogsEntry {
channel: guild.client.channels.cache.get(data.options?.channel_id) ?? { id: data.options?.channel_id },
};
break;
case Actions.APPLICATION_COMMAND_PERMISSION_UPDATE:
this.extra = {
applicationId: data.options.application_id,
};
break;
case Actions.AUTO_MODERATION_BLOCK_MESSAGE:
case Actions.AUTO_MODERATION_FLAG_TO_CHANNEL:
case Actions.AUTO_MODERATION_USER_COMMUNICATION_DISABLED:
@ -549,7 +545,6 @@ class GuildAuditLogsEntry {
channel: guild.client.channels.cache.get(data.options?.channel_id) ?? { id: data.options?.channel_id },
};
break;
default:
break;
}

View File

@ -3,7 +3,6 @@
const { Buffer } = require('node:buffer');
const BaseMessageComponent = require('./BaseMessageComponent');
const MessageEmbed = require('./MessageEmbed');
const WebEmbed = require('./WebEmbed');
const { RangeError } = require('../errors');
const ActivityFlags = require('../util/ActivityFlags');
const DataResolver = require('../util/DataResolver');
@ -42,7 +41,6 @@ class MessagePayload {
* @property {Buffer|string|Stream} attachment The original attachment that generated this file
* @property {string} name The name of this file
* @property {Buffer|Stream} file The file to be sent to the API
* @extends {APIAttachment}
*/
/**
@ -52,15 +50,6 @@ class MessagePayload {
this.files = null;
}
/**
* Whether or not using new API to upload files
* @type {boolean}
* @readonly
*/
get usingNewAttachmentAPI() {
return Boolean(this.options?.usingNewAttachmentAPI);
}
/**
* Whether or not the target is a {@link Webhook} or a {@link WebhookClient}
* @type {boolean}
@ -133,12 +122,12 @@ class MessagePayload {
* Resolves data.
* @returns {MessagePayload}
*/
async resolveData() {
resolveData() {
if (this.data) return this;
const isInteraction = this.isInteraction;
const isWebhook = this.isWebhook;
let content = this.makeContent();
const content = this.makeContent();
const tts = Boolean(this.options.tts);
let nonce;
@ -208,37 +197,6 @@ class MessagePayload {
this.options.attachments = attachments;
}
if (this.options.embeds) {
if (!Array.isArray(this.options.embeds)) {
this.options.embeds = [this.options.embeds];
}
const webembeds = this.options.embeds.filter(e => e instanceof WebEmbed);
this.options.embeds = this.options.embeds.filter(e => !(e instanceof WebEmbed));
if (webembeds.length > 0) {
if (!content) content = '';
// Add hidden embed link
content += `\n${WebEmbed.hiddenEmbed} \n`;
if (webembeds.length > 1) {
console.warn('[WARN] Multiple webembeds are not supported, this will be ignored.');
}
// Const embed = webembeds[0];
for (const webE of webembeds) {
const data = await webE.toMessage();
content += `\n${data}`;
}
}
// Check content
if (typeof content == 'string' && content.length > 2000) {
console.warn('[WARN] Content is longer than 2000 characters.');
}
if (typeof content == 'string' && content.length > 4000) {
// Max length if user has nitro boost
throw new RangeError('MESSAGE_EMBED_LINK_LENGTH');
}
}
// Activity
let activity;
if (
@ -247,7 +205,7 @@ class MessagePayload {
this.options.activity.type
) {
const type = ActivityFlags.resolve(this.options.activity.type);
const sessionId = this.target.client.sessionId;
const sessionId = this.target.client.ws.shards.first()?.sessionId;
const partyId = this.options.activity.partyId;
activity = {
type,
@ -348,7 +306,7 @@ module.exports = MessagePayload;
/**
* A target for a message.
* @typedef {TextBasedChannels|DMChannel|User|GuildMember|Webhook|WebhookClient|Interaction|InteractionWebhook|
* @typedef {TextBasedChannels|User|GuildMember|Webhook|WebhookClient|Interaction|InteractionWebhook|
* Message|MessageManager} MessageTarget
*/

View File

@ -3,8 +3,6 @@
const { Collection } = require('@discordjs/collection');
const Base = require('./Base');
const TeamMember = require('./TeamMember');
const User = require('./User');
const { Error } = require('../errors');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
@ -100,53 +98,6 @@ class Team extends Base {
return this.client.rest.cdn.TeamIcon(this.id, this.icon, { format, size });
}
/**
* Invite a team member to the team
* @param {User} user The user to invite to the team
* @param {number} MFACode The mfa code
* @returns {Promise<TeamMember>}
*/
async inviteMember(user, MFACode) {
if (!(user instanceof User)) return new Error('TEAM_MEMBER_FORMAT');
const regex = /([0-9]{6})/g;
const payload = {
username: user.username,
discriminator: user.discriminator,
};
if (MFACode && !regex.test(MFACode)) return new Error('MFA_INVALID');
if (MFACode) payload.code = MFACode;
const member = await this.client.api.teams(this.id).members.post({
data: payload,
});
this.members.set(member.user.id, new TeamMember(this, member));
return this.members.get(member.user.id);
}
/**
* Remove a member from the team
* @param {Snowflake} userID The ID of the user you want to remove
* @returns {boolean}
*/
async removeMember(userID) {
await this.client.api.teams[this.id].members[userID].delete();
return this.members.delete(userID);
}
/**
* Delete this team
* @param {number} MFACode The 2fa code
* @returns {Promise<boolean>}
*/
async delete(MFACode) {
const regex = /([0-9]{6})/g;
if (!regex.test(MFACode)) return new Error('MFA_INVALID');
await this.client.api.teams[this.id].delete({ data: { code: MFACode } });
return this.client.developerPortal.teams.delete(this.id);
}
/**
* When concatenated with a string, this automatically returns the Team's name instead of the
* Team object.

View File

@ -1,10 +1,13 @@
'use strict';
const process = require('node:process');
const { ApplicationFlags } = require('../../util/ApplicationFlags');
const { ClientApplicationAssetTypes, Endpoints } = require('../../util/Constants');
const Permissions = require('../../util/Permissions');
const SnowflakeUtil = require('../../util/SnowflakeUtil');
const { ApplicationRoleConnectionMetadata } = require('../ApplicationRoleConnectionMetadata');
const Base = require('../Base');
const Team = require('../Team');
const AssetTypes = Object.keys(ClientApplicationAssetTypes);
@ -67,6 +70,132 @@ class Application extends Base {
} else {
this.roleConnectionsVerificationURL ??= null;
}
// ClientApplication
/**
* The tags this application has (max of 5)
* @type {string[]}
*/
this.tags = data.tags ?? [];
if ('install_params' in data) {
/**
* Settings for this application's default in-app authorization
* @type {?ClientApplicationInstallParams}
*/
this.installParams = {
scopes: data.install_params.scopes,
permissions: new Permissions(data.install_params.permissions).freeze(),
};
} else {
this.installParams ??= null;
}
if ('custom_install_url' in data) {
/**
* This application's custom installation URL
* @type {?string}
*/
this.customInstallURL = data.custom_install_url;
} else {
this.customInstallURL = null;
}
if ('flags' in data) {
/**
* The flags this application has
* @type {ApplicationFlags}
*/
this.flags = new ApplicationFlags(data.flags).freeze();
}
if ('approximate_guild_count' in data) {
/**
* An approximate amount of guilds this application is in.
* @type {?number}
*/
this.approximateGuildCount = data.approximate_guild_count;
} else {
this.approximateGuildCount ??= null;
}
if ('guild_id' in data) {
/**
* The id of the guild associated with this application.
* @type {?Snowflake}
*/
this.guildId = data.guild_id;
} else {
this.guildId ??= null;
}
if ('cover_image' in data) {
/**
* The hash of the application's cover image
* @type {?string}
*/
this.cover = data.cover_image;
} else {
this.cover ??= null;
}
if ('rpc_origins' in data) {
/**
* The application's RPC origins, if enabled
* @type {string[]}
*/
this.rpcOrigins = data.rpc_origins;
} else {
this.rpcOrigins ??= [];
}
if ('bot_require_code_grant' in data) {
/**
* If this application's bot requires a code grant when using the OAuth2 flow
* @type {?boolean}
*/
this.botRequireCodeGrant = data.bot_require_code_grant;
} else {
this.botRequireCodeGrant ??= null;
}
if ('bot_public' in data) {
/**
* If this application's bot is public
* @type {?boolean}
*/
this.botPublic = data.bot_public;
} else {
this.botPublic ??= null;
}
/**
* The owner of this OAuth application
* @type {?(User|Team)}
*/
this.owner = data.team
? new Team(this.client, data.team)
: data.owner
? this.client.users._add(data.owner)
: this.owner ?? null;
}
/**
* The guild associated with this application.
* @type {?Guild}
* @readonly
*/
get guild() {
return this.client.guilds.cache.get(this.guildId) ?? null;
}
/**
* Whether this application is partial
* @type {boolean}
* @readonly
*/
get partial() {
return !this.name;
}
/**
@ -88,35 +217,29 @@ class Application extends Base {
}
/**
* Invites this application to a guild / server
* @param {Snowflake} guild_id The id of the guild that you want to invite the bot to
* @param {PermissionResolvable} [permissions] The permissions for the bot in number form (the default is 8 / Administrator)
* @param {string} [captcha] The captcha key to add
* @returns {Promise<void>} nothing :)
* Obtains this application from Discord.
* @returns {Promise<Application>}
*/
async invite(guild_id, permissions, captcha = null) {
permissions = Permissions.resolve(permissions || 0n);
const postData = {
authorize: true,
guild_id,
permissions: '0',
};
if (permissions) {
postData.permissions = permissions;
}
if (captcha && typeof captcha === 'string' && captcha.length > 0) {
postData.captcha = captcha;
}
await this.client.api.oauth2.authorize.post({
async fetch() {
const app = await this.client.api.oauth2.authorize.get({
query: {
client_id: this.id,
scope: 'bot applications.commands',
},
data: postData,
headers: {
referer: `https://discord.com/oauth2/authorize?client_id=${this.id}&permissions=${permissions}&scope=bot`,
},
});
const user = this.client.users._add(app.bot);
user._partial = false;
this._patch(app.application);
return this;
}
/**
* Gets this application's role connection metadata records
* @returns {Promise<ApplicationRoleConnectionMetadata[]>}
*/
async fetchRoleConnectionMetadataRecords() {
const metadata = await this.client.api.applications(this.id)('role-connections').metadata.get();
return metadata.map(data => new ApplicationRoleConnectionMetadata(data));
}
/**

View File

@ -1,17 +1,14 @@
'use strict';
/* eslint-disable import/order */
const InteractionManager = require('../../managers/InteractionManager');
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 { 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 { setTimeout } = require('node:timers');
const { s } = require('@sapphire/shapeshift');
const Util = require('../../util/Util');
const validateName = stringName =>
s.string
.lengthGreaterThanOrEqual(1)
@ -32,12 +29,6 @@ class TextBasedChannel {
*/
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
* @type {?Snowflake}
@ -76,7 +67,7 @@ class TextBasedChannel {
* @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|WebEmbed)>} [embeds] The embeds 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)
@ -84,7 +75,6 @@ class TextBasedChannel {
* @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
* @property {boolean} [usingNewAttachmentAPI] Whether to use the new attachment API (`channels/:id/attachments`)
*/
/**
@ -168,21 +158,16 @@ class TextBasedChannel {
let messagePayload;
if (options instanceof MessagePayload) {
messagePayload = await options.resolveData();
messagePayload = options.resolveData();
} else {
messagePayload = await MessagePayload.create(this, options).resolveData();
messagePayload = MessagePayload.create(this, options).resolveData();
}
let { data, files } = await messagePayload.resolveFiles();
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 { data, files } = await messagePayload.resolveFiles();
// New API
const attachments = await Util.getUploadURL(this.client, this.id, files);
const requestPromises = attachments.map(async attachment => {
await uploadFile(files[attachment.id].file, attachment.upload_url);
await Util.uploadFile(files[attachment.id].file, attachment.upload_url);
return {
id: attachment.id,
filename: files[attachment.id].name,
@ -195,14 +180,175 @@ class TextBasedChannel {
const attachmentsData = await Promise.all(requestPromises);
attachmentsData.sort((a, b) => parseInt(a.id) - parseInt(b.id));
data.attachments = attachmentsData;
files = [];
}
const d = await this.client.api.channels[this.id].messages.post({ data, files });
// 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);
}
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.
* @returns {Promise<void>} Resolves upon the typing status being sent
@ -261,101 +407,6 @@ 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.
* @returns {Promise<Collection<Snowflake, Webhook>>}
@ -414,139 +465,21 @@ class TextBasedChannel {
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 data = await this.searchInteraction(user.application?.id ?? user.id, 'CHAT_INPUT');
for (const command of data.application_commands) {
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 = []) {
const props = ['send'];
if (full) {
props.push(
'sendSlash',
'searchInteraction',
'lastMessage',
'lastPinAt',
'bulkDelete',
'sendTyping',
'createMessageCollector',
'awaitMessages',
'createMessageComponentCollector',
'awaitMessageComponent',
'fetchWebhooks',
'createWebhook',
'setRateLimitPerUser',
'setNSFW',
'sendSlash',
'searchInteraction',
);
}
for (const prop of props) {
@ -564,3 +497,225 @@ 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.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;
}