This commit is contained in:
Elysia 2024-01-10 19:27:04 +07:00
parent af1f561cfd
commit 4a1d615673
6 changed files with 345 additions and 347 deletions

View File

@ -113,14 +113,13 @@ class ChannelManager extends CachedManager {
} }
const data = await this.client.api.channels(id).get(); const data = await this.client.api.channels(id).get();
// Delete in cache
this._remove(id);
return this._add(data, null, { cache, allowUnknownGuild }); return this._add(data, null, { cache, allowUnknownGuild });
} }
/** /**
* Create Group DM * Create Group DM
* @param {UserResolvable[]} recipients Array of recipients * @param {UserResolvable[]} recipients Array of recipients
* @returns {Promise<PartialGroupDMChannel>} Channel * @returns {Promise<GroupDMChannel>} Channel
*/ */
async createGroupDM(recipients) { async createGroupDM(recipients) {
// Check // Check

View File

@ -3,7 +3,7 @@
const ThreadManager = require('./ThreadManager'); const ThreadManager = require('./ThreadManager');
const { TypeError } = require('../errors'); const { TypeError } = require('../errors');
const MessagePayload = require('../structures/MessagePayload'); const MessagePayload = require('../structures/MessagePayload');
const { resolveAutoArchiveMaxLimit, getAttachments, uploadFile } = require('../util/Util'); const { resolveAutoArchiveMaxLimit, getUploadURL, uploadFile } = require('../util/Util');
/** /**
* Manages API methods for threads in forum channels and stores their cache. * Manages API methods for threads in forum channels and stores their cache.
@ -20,7 +20,7 @@ class GuildForumThreadManager extends ThreadManager {
* @typedef {BaseMessageOptions} GuildForumThreadMessageCreateOptions * @typedef {BaseMessageOptions} GuildForumThreadMessageCreateOptions
* @property {StickerResolvable} [stickers] The stickers to send with the message * @property {StickerResolvable} [stickers] The stickers to send with the message
* @property {BitFieldResolvable} [flags] The flags to send with the message. * @property {BitFieldResolvable} [flags] The flags to send with the message.
* Only `SUPPRESS_EMBEDS`, `SUPPRESS_NOTIFICATIONS` and `IS_VOICE_MESSAGE` can be set. * Only `SUPPRESS_EMBEDS` and `SUPPRESS_NOTIFICATIONS` can be set.
*/ */
/** /**
@ -63,19 +63,15 @@ class GuildForumThreadManager extends ThreadManager {
let messagePayload; let messagePayload;
if (message instanceof MessagePayload) { if (message instanceof MessagePayload) {
messagePayload = await message.resolveData(); messagePayload = message.resolveData();
} else { } else {
messagePayload = await MessagePayload.create(this, message).resolveData(); messagePayload = MessagePayload.create(this, message).resolveData();
} }
let { data: body, files } = await messagePayload.resolveFiles(); const { data: body, files } = await messagePayload.resolveFiles();
if (typeof message == 'object' && typeof message.usingNewAttachmentAPI !== 'boolean') { // New API
message.usingNewAttachmentAPI = this.client.options.usingNewAttachmentAPI; const attachments = await getUploadURL(this.client, this.channel.id, files);
}
if (message?.usingNewAttachmentAPI === true) {
const attachments = await getAttachments(this.client, this.channel.id, ...files);
const requestPromises = attachments.map(async attachment => { const requestPromises = attachments.map(async attachment => {
await uploadFile(files[attachment.id].file, attachment.upload_url); await uploadFile(files[attachment.id].file, attachment.upload_url);
return { return {
@ -89,9 +85,7 @@ class GuildForumThreadManager extends ThreadManager {
}); });
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));
body.attachments = attachmentsData; data.attachments = attachmentsData;
files = [];
}
if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild); if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild);
@ -103,7 +97,7 @@ class GuildForumThreadManager extends ThreadManager {
applied_tags: appliedTags, applied_tags: appliedTags,
message: body, message: body,
}, },
files, files: [],
reason, reason,
}); });

View File

@ -2,7 +2,7 @@
const { Collection } = require('@discordjs/collection'); const { Collection } = require('@discordjs/collection');
const CachedManager = require('./CachedManager'); const CachedManager = require('./CachedManager');
const { TypeError, Error } = require('../errors'); const { TypeError } = require('../errors');
const { Message } = require('../structures/Message'); const { Message } = require('../structures/Message');
const MessagePayload = require('../structures/MessagePayload'); const MessagePayload = require('../structures/MessagePayload');
const Util = require('../util/Util'); const Util = require('../util/Util');
@ -123,20 +123,15 @@ class MessageManager extends CachedManager {
const messageId = this.resolveId(message); const messageId = this.resolveId(message);
if (!messageId) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); if (!messageId) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
let messagePayload; const { data, files } = await (options instanceof MessagePayload
if (options instanceof MessagePayload) { ? options
messagePayload = await options.resolveData(); : MessagePayload.create(message instanceof Message ? message : this, options)
} else { )
messagePayload = await MessagePayload.create(message instanceof Message ? message : this, options).resolveData(); .resolveData()
} .resolveFiles();
let { data, files } = await messagePayload.resolveFiles();
if (typeof options == 'object' && typeof options.usingNewAttachmentAPI !== 'boolean') { // New API
options.usingNewAttachmentAPI = this.client.options.usingNewAttachmentAPI; const attachments = await Util.getUploadURL(this.client, this.channel.id, files);
}
if (options?.usingNewAttachmentAPI === true) {
const attachments = await Util.getAttachments(this.client, this.channel.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 Util.uploadFile(files[attachment.id].file, attachment.upload_url);
return { return {
@ -151,10 +146,9 @@ class MessageManager extends CachedManager {
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;
files = []; // Empty Files
}
const d = await this.client.api.channels[this.channel.id].messages[messageId].patch({ data, files }); const d = await this.client.api.channels[this.channel.id].messages[messageId].patch({ data });
const existing = this.cache.get(messageId); const existing = this.cache.get(messageId);
if (existing) { if (existing) {
@ -251,12 +245,16 @@ class MessageManager extends CachedManager {
const existing = this.cache.get(messageId); const existing = this.cache.get(messageId);
if (existing && !existing.partial) return existing; if (existing && !existing.partial) return existing;
} }
// https://discord.com/api/v9/channels/:id/messages?limit=50&around=:msgid // https://discord.com/api/v9/channels/:id/messages?limit=50&around=:msgid
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this._fetchMany({ this._fetchMany(
{
around: messageId, around: messageId,
limit: 50, limit: 50,
}) },
cache,
)
.then(data_ => .then(data_ =>
data_.has(messageId) ? resolve(data_.get(messageId)) : reject(new Error('MESSAGE_ID_NOT_FOUND')), data_.has(messageId) ? resolve(data_.get(messageId)) : reject(new Error('MESSAGE_ID_NOT_FOUND')),
) )
@ -264,13 +262,6 @@ class MessageManager extends CachedManager {
}); });
} }
async _fetchMany(options = {}, cache) {
const data = await this.client.api.channels[this.channel.id].messages.get({ query: options });
const messages = new Collection();
for (const message of data) messages.set(message.id, this._add(message, cache));
return messages;
}
/** /**
* @typedef {object} MessageSearchOptions * @typedef {object} MessageSearchOptions
* @property {Array<UserResolvable>} [authors] An array of author to filter by * @property {Array<UserResolvable>} [authors] An array of author to filter by
@ -388,6 +379,13 @@ class MessageManager extends CachedManager {
total: data.total_results, total: data.total_results,
}; };
} }
async _fetchMany(options = {}, cache) {
const data = await this.client.api.channels[this.channel.id].messages.get({ query: options });
const messages = new Collection();
for (const message of data) messages.set(message.id, this._add(message, cache));
return messages;
}
} }
module.exports = MessageManager; module.exports = MessageManager;

View File

@ -1,22 +1,26 @@
'use strict'; 'use strict';
const process = require('node:process'); const process = require('node:process');
const { setTimeout } = require('node:timers');
const { Collection } = require('@discordjs/collection'); const { Collection } = require('@discordjs/collection');
const Base = require('./Base'); const Base = require('./Base');
const BaseMessageComponent = require('./BaseMessageComponent'); const BaseMessageComponent = require('./BaseMessageComponent');
const ClientApplication = require('./ClientApplication');
const InteractionCollector = require('./InteractionCollector');
const MessageAttachment = require('./MessageAttachment'); const MessageAttachment = require('./MessageAttachment');
const MessageButton = require('./MessageButton');
const Embed = require('./MessageEmbed'); const Embed = require('./MessageEmbed');
const Mentions = require('./MessageMentions'); const Mentions = require('./MessageMentions');
const MessagePayload = require('./MessagePayload'); const MessagePayload = require('./MessagePayload');
const MessageSelectMenu = require('./MessageSelectMenu');
const ReactionCollector = require('./ReactionCollector'); const ReactionCollector = require('./ReactionCollector');
const { Sticker } = require('./Sticker'); const { Sticker } = require('./Sticker');
const Application = require('./interfaces/Application');
const { Error } = require('../errors'); const { Error } = require('../errors');
const ReactionManager = require('../managers/ReactionManager'); const ReactionManager = require('../managers/ReactionManager');
const { InteractionTypes, MessageTypes, SystemMessageTypes, MaxBulkDeletableMessageAge } = require('../util/Constants'); const {
InteractionTypes,
MessageTypes,
SystemMessageTypes,
MessageComponentTypes,
Events,
} = require('../util/Constants');
const MessageFlags = require('../util/MessageFlags'); const MessageFlags = require('../util/MessageFlags');
const Permissions = require('../util/Permissions'); const Permissions = require('../util/Permissions');
const SnowflakeUtil = require('../util/SnowflakeUtil'); const SnowflakeUtil = require('../util/SnowflakeUtil');
@ -256,9 +260,9 @@ class Message extends Base {
if ('application' in data) { if ('application' in data) {
/** /**
* Supplemental application information for group activities * Supplemental application information for group activities
* @type {?ClientApplication} * @type {?Application}
*/ */
this.groupActivityApplication = new ClientApplication(this.client, data.application); this.groupActivityApplication = new Application(this.client, data.application);
} else { } else {
this.groupActivityApplication ??= null; this.groupActivityApplication ??= null;
} }
@ -533,65 +537,6 @@ class Message extends Base {
}); });
} }
/**
* @typedef {CollectorOptions} MessageComponentCollectorOptions
* @property {MessageComponentType} [componentType] The type of component to listen for
* @property {number} [max] The maximum total amount of interactions to collect
* @property {number} [maxComponents] The maximum number of components to collect
* @property {number} [maxUsers] The maximum number of users to interact
*/
/**
* Creates a message component interaction collector.
* @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector
* @returns {InteractionCollector}
* @example
* // Create a message component interaction collector
* const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId';
* const collector = message.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,
message: this,
});
}
/**
* An object containing the same properties as CollectorOptions, but a few more:
* @typedef {Object} AwaitMessageComponentOptions
* @property {CollectorFilter} [filter] The filter applied to this collector
* @property {number} [time] Time to wait for an interaction before rejecting
* @property {MessageComponentType} [componentType] The type of component interaction to collect
*/
/**
* 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';
* message.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));
});
});
}
/** /**
* Whether the message is editable by the client user * Whether the message is editable by the client user
* @type {boolean} * @type {boolean}
@ -653,14 +598,7 @@ class Message extends Base {
* channel.bulkDelete(messages.filter(message => message.bulkDeletable)); * channel.bulkDelete(messages.filter(message => message.bulkDeletable));
*/ */
get bulkDeletable() { get bulkDeletable() {
return ( return false;
(this.inGuild() &&
this.client.user.bot &&
Date.now() - this.createdTimestamp < MaxBulkDeletableMessageAge &&
this.deletable &&
this.channel?.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES, false)) ??
false
);
} }
/** /**
@ -722,7 +660,7 @@ class Message extends Base {
* @property {MessageAttachment[]} [attachments] An array of attachments to keep, * @property {MessageAttachment[]} [attachments] An array of attachments to keep,
* all attachments will be kept if omitted * all attachments will be kept if omitted
* @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to add to the message * @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to add to the message
* @property {Array<(MessageActionRow|MessageActionRowOptions)>} [components] * @property {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)
*/ */
@ -792,7 +730,7 @@ class Message extends Base {
/** /**
* Adds a reaction to the message. * Adds a reaction to the message.
* @param {EmojiIdentifierResolvable} emoji The emoji to react with * @param {EmojiIdentifierResolvable} emoji The emoji to react with
* @param {boolean} [burst=false] Super Reactions (Discord Nitro only) * @param {boolean} [burst=false] Super Reactions
* @returns {Promise<MessageReaction>} * @returns {Promise<MessageReaction>}
* @example * @example
* // React to a message with a unicode emoji * // React to a message with a unicode emoji
@ -1024,13 +962,221 @@ class Message extends Base {
reactions: false, reactions: false,
}); });
} }
// Added
// TypeScript
/**
* Check data
* @type {boolean}
* @readonly
*/
get isMessage() {
return true;
}
/**
* Click specific button with X and Y
* @typedef {Object} MessageButtonLocation
* @property {number} X Index of the row
* @property {number} Y Index of the column
*/
/**
* Click specific button or automatically click first button if no button is specified.
* @param {MessageButtonLocation|string|undefined} button button
* @returns {Promise<Message|Modal>}
* @example
* // Demo msg
* Some content
* > X from 0
* [button1] [button2] [button3]
* [button4] [button5] [button6]
*
* Y from 0
* // Click button6 with X and Y
* [0,0] [1,0] [2,0]
* [0,1] [1,1] [2,1]
* // Code
* message.clickButton({
* X: 2, Y: 1,
* });
* // Click button with customId (Ex button 5)
* message.clickButton('button5');
* // Click button 1
* message.clickButton();
*/
clickButton(button) {
if (typeof button == 'undefined') {
button = this.components
.flatMap(row => row.components)
.find(b => b.type === 'BUTTON' && b.customId && !b.disabled);
} else if (typeof button == 'string') {
button = this.components.flatMap(row => row.components).find(b => b.type === 'BUTTON' && b.customId == button);
} else {
button = this.components[button.Y]?.components[button.X];
}
button = button.toJSON();
if (!button) throw new TypeError('BUTTON_NOT_FOUND');
if (!button.custom_id || button.disabled) throw new TypeError('BUTTON_CANNOT_CLICK');
const nonce = SnowflakeUtil.generate();
const data = {
type: InteractionTypes.MESSAGE_COMPONENT,
nonce,
guild_id: this.guildId,
channel_id: this.channelId,
message_id: this.id,
application_id: this.applicationId ?? this.author.id,
session_id: this.client.ws.shards.first()?.sessionId,
message_flags: this.flags.bitfield,
data: {
component_type: MessageComponentTypes.BUTTON,
custom_id: button.custom_id,
},
};
this.client.api.interactions.post({
data,
});
return new Promise((resolve, reject) => {
const timeoutMs = 5_000;
// Waiting for MsgCreate / ModalCreate
const handler = data => {
// UnhandledPacket
if (data.d?.nonce == nonce && data.t == 'INTERACTION_SUCCESS') {
// Interaction#deferUpdate
this.client.removeListener(Events.MESSAGE_CREATE, handler);
this.client.removeListener(Events.UNHANDLED_PACKET, handler);
this.client.removeListener(Events.INTERACTION_MODAL_CREATE, handler);
resolve(this);
}
if (data.nonce !== nonce) return;
clearTimeout(timeout);
this.client.removeListener(Events.MESSAGE_CREATE, handler);
this.client.removeListener(Events.UNHANDLED_PACKET, 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.UNHANDLED_PACKET, 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.UNHANDLED_PACKET, handler);
this.client.on(Events.INTERACTION_MODAL_CREATE, handler);
});
}
/**
* Select specific menu
* @param {number|string} menu Target
* @param {Array<UserResolvable | RoleResolvable | ChannelResolvable | string>} values Any value
* @returns {Promise<Message|Modal>}
*/
selectMenu(menu, values = []) {
let selectMenu;
if (/[0-4]/.test(menu)) {
selectMenu = this.components[menu]?.components[0];
} else {
selectMenu = this.components
.flatMap(row => row.components)
.find(
b =>
['STRING_SELECT', 'USER_SELECT', 'ROLE_SELECT', 'MENTIONABLE_SELECT', 'CHANNEL_SELECT'].includes(b.type) &&
b.customId == menu &&
!b.disabled,
);
}
if (values.length < selectMenu.minValues) {
throw new RangeError(`[SELECT_MENU_MIN_VALUES] The minimum number of values is ${selectMenu.minValues}`);
}
if (values.length > selectMenu.maxValues) {
throw new RangeError(`[SELECT_MENU_MAX_VALUES] The maximum number of values is ${selectMenu.maxValues}`);
}
values = values.map(value => {
switch (selectMenu.type) {
case 'STRING_SELECT': {
return selectMenu.options.find(obj => obj.value === value || obj.label === value).value;
}
case 'USER_SELECT': {
return this.client.users.resolveId(value);
}
case 'ROLE_SELECT': {
return this.guild.roles.resolveId(value);
}
case 'MENTIONABLE_SELECT': {
return this.client.users.resolveId(value) || this.guild.roles.resolveId(value);
}
case 'CHANNEL_SELECT': {
return this.client.channels.resolveId(value);
}
default: {
return value;
}
}
});
const nonce = SnowflakeUtil.generate();
const data = {
type: InteractionTypes.MESSAGE_COMPONENT,
guild_id: this.guildId,
channel_id: this.channelId,
message_id: this.id,
application_id: this.applicationId ?? this.author.id,
session_id: this.client.ws.shards.first()?.sessionId,
message_flags: this.flags.bitfield,
data: {
component_type: MessageComponentTypes[selectMenu.type],
custom_id: selectMenu.customId,
type: MessageComponentTypes[selectMenu.type],
values,
},
nonce,
};
this.client.api.interactions.post({
data,
});
return new Promise((resolve, reject) => {
const timeoutMs = 5_000;
// Waiting for MsgCreate / ModalCreate
const handler = data => {
// UnhandledPacket
if (data.d?.nonce == nonce && data.t == 'INTERACTION_SUCCESS') {
// Interaction#deferUpdate
this.client.removeListener(Events.MESSAGE_CREATE, handler);
this.client.removeListener(Events.UNHANDLED_PACKET, handler);
this.client.removeListener(Events.INTERACTION_MODAL_CREATE, handler);
resolve(this);
}
if (data.nonce !== nonce) return;
clearTimeout(timeout);
this.client.removeListener(Events.MESSAGE_CREATE, handler);
this.client.removeListener(Events.UNHANDLED_PACKET, 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.UNHANDLED_PACKET, 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.UNHANDLED_PACKET, handler);
this.client.on(Events.INTERACTION_MODAL_CREATE, handler);
});
}
/** /**
* Marks the message as unread. * Marks the message as unread.
* @returns {Promise<boolean>} * @returns {Promise<void>}
*/ */
async markUnread() { markUnread() {
await this.client.api.channels[this.channelId].messages[this.id].ack.post({ return this.client.api.channels[this.channelId].messages[this.id].ack.post({
data: { data: {
manual: true, manual: true,
mention_count: mention_count:
@ -1042,145 +1188,18 @@ class Message extends Base {
: 0, : 0,
}, },
}); });
return true;
} }
/** /**
* Marks the message as read. * Marks the message as read.
* @returns {Promise<boolean>} * @returns {Promise<void>}
*/ */
async markRead() { markRead() {
await this.client.api.channels[this.channelId].messages[this.id].ack.post({ return this.client.api.channels[this.channelId].messages[this.id].ack.post({
data: { data: {
token: null, token: null,
}, },
}); });
return true;
}
/**
* @typedef {Object} MessageButtonLocation
* @property {number} row Index of the row
* @property {number} col Index of the column
*/
/**
* Click specific button or automatically click first button if no button is specified.
* @param {MessageButton|MessageButtonLocation|string} button Button ID
* @returns {Promise<InteractionResponse>}
* @example
* client.on('messageCreate', async message => {
* if (message.components.length) {
* // Find first button and click it
* await message.clickButton();
* // Click with button ID
* await message.clickButton('button-id');
* // Click with button location
* await message.clickButton({ row: 0, col: 0 });
* // Click with class MessageButton
* const button = message.components[0].components[0];
* await message.clickButton(button);
* // Click with class MessageButton (2)
* button.click(message);
* }
* });
*/
clickButton(button) {
if (!button) {
button = this.components.flatMap(row => row.components).find(b => b.type === 'BUTTON')?.customId;
} else if (button instanceof MessageButton) {
button = button.customId;
} else if (typeof button === 'object') {
if (!('row' in button) || !('col' in button)) throw new TypeError('INVALID_BUTTON_LOCATION');
button = this.components[button.row]?.components[button.col]?.customId;
}
if (!button) throw new TypeError('BUTTON_NOT_FOUND');
button = this.components.flatMap(row => row.components).find(b => b.customId === button && b.type === 'BUTTON');
return button ? button.click(this) : Promise.reject(new TypeError('BUTTON_NOT_FOUND'));
}
/**
* Select specific menu or First Menu
* @param {MessageSelectMenu|string|number|Array<any>} menuID MenuId / MessageSelectMenu / Row of Menu / Array of Values (first menu)
* @param {Array<any>} options Array of Values
* @returns {Promise<InteractionResponse>}
* @example
* client.on('messageCreate', async message => {
* if (message.components.length) {
* // Row
* await message.selectMenu(1, [message.channel]); // row 1, type: Channel, multi: false
* // Id
* await message.selectMenu('menu-id', ['uid1', client.user, message.member]); // MenuId, type: User, multi: true
* // First Menu
* await message.selectMenu(['role-id']); // First Menu, type: role, multi: false
* // class MessageSelectMenu
* const menu = message.components[0].components[0];
* await message.selectMenu(menu, ['option1', 'option2']);
* // MessageSelectMenu (2)
* menu.select(message, ['option1', 'option2']);
* }
* });
*/
selectMenu(menuID, options = []) {
if (!this.components[0]) throw new TypeError('MESSAGE_NO_COMPONENTS');
if (menuID instanceof MessageSelectMenu) {
//
} else if (/[0-4]/.test(menuID)) {
menuID = this.components[menuID]?.components[0];
} else {
const menuAll = this.components
.flatMap(row => row.components)
.filter(b =>
[
'STRING_SELECT',
'USER_SELECT',
'ROLE_SELECT',
'MENTIONABLE_SELECT',
'CHANNEL_SELECT',
'SELECT_MENU',
].includes(b.type),
);
if (menuAll.length == 0) throw new TypeError('MENU_NOT_FOUND');
if (menuID) {
menuID = menuAll.find(b => b.customId === menuID);
} else {
menuID = menuAll[0];
}
}
if (!menuID.type.includes('SELECT')) throw new TypeError('MENU_NOT_FOUND');
return menuID.select(this, Array.isArray(menuID) ? menuID : options);
}
//
/**
* Send context Menu v2
* @param {Snowflake} botId Bot id
* @param {string} commandName Command name in Context Menu
* @returns {Promise<InteractionResponse>}
*/
async contextMenu(botId, commandName) {
if (!botId) throw new Error('Bot ID is required');
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 (!commandName || typeof commandName !== 'string') {
throw new Error('Command name is required');
}
let contextCMD;
const data = await this.channel.searchInteraction(botId, 'MESSAGE');
for (const command of data.application_commands) {
user.application?.commands?._add(command, true);
}
contextCMD = user.application?.commands?.cache.find(c => c.name == commandName && c.type === 'MESSAGE');
if (!contextCMD) {
throw new Error(
'INTERACTION_SEND_FAILURE',
`Command ${commandName} is not found (with search)\nList command avalible: ${user.application?.commands?.cache
.filter(a => a.type == 'MESSAGE')
.map(a => a.name)
.join(', ')}`,
);
}
return contextCMD.sendContextMenu(this, true);
} }
} }

View File

@ -31,7 +31,7 @@ class MessageReaction {
this.me = data.me || data.me_burst; this.me = data.me || data.me_burst;
/** /**
* Super reaction * Is super reaction
* @type {boolean} * @type {boolean}
*/ */
this.isBurst = Boolean(data.me_burst || data.burst); this.isBurst = Boolean(data.me_burst || data.burst);

View File

@ -86,11 +86,15 @@ class VoiceState extends Base {
// The self_stream is property is omitted if false, check for another property // The self_stream is property is omitted if false, check for another property
// here to avoid incorrectly clearing this when partial data is specified // here to avoid incorrectly clearing this when partial data is specified
if ('self_stream' in data) {
/** /**
* Whether this member is streaming using "Screen Share" * Whether this member is streaming using "Screen Share"
* @type {boolean} * @type {boolean}
*/ */
this.streaming = data.self_stream ?? false; this.streaming = data.self_stream ?? false;
} else {
this.streaming ??= null;
}
if ('channel_id' in data) { if ('channel_id' in data) {
/** /**
@ -144,12 +148,11 @@ class VoiceState extends Base {
/** /**
* The channel that the member is connected to * The channel that the member is connected to
* @type {?(VoiceChannel|StageChannel)} * @type {?(VoiceChannel|StageChannel|DMChannel|GroupDMChannel)}
* @readonly * @readonly
*/ */
get channel() { get channel() {
if (!this.guild?.id) return this.guild.client.channels.cache.get(this.channelId) ?? null; return (this.guild || this.client).channels.cache.get(this.channelId) ?? null;
return this.guild.channels.cache.get(this.channelId) ?? null;
} }
/** /**
@ -177,7 +180,6 @@ class VoiceState extends Base {
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
*/ */
setMute(mute = true, reason) { setMute(mute = true, reason) {
if (!this.guild?.id) return null;
return this.guild.members.edit(this.id, { mute }, reason); return this.guild.members.edit(this.id, { mute }, reason);
} }
@ -188,7 +190,6 @@ class VoiceState extends Base {
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
*/ */
setDeaf(deaf = true, reason) { setDeaf(deaf = true, reason) {
if (!this.guild?.id) return null;
return this.guild.members.edit(this.id, { deaf }, reason); return this.guild.members.edit(this.id, { deaf }, reason);
} }
@ -198,7 +199,6 @@ class VoiceState extends Base {
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
*/ */
disconnect(reason) { disconnect(reason) {
if (!this.guild?.id) return this.callVoice?.disconnect();
return this.setChannel(null, reason); return this.setChannel(null, reason);
} }
@ -210,36 +210,9 @@ class VoiceState extends Base {
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
*/ */
setChannel(channel, reason) { setChannel(channel, reason) {
if (!this.guild?.id) return null;
return this.guild.members.edit(this.id, { channel }, reason); return this.guild.members.edit(this.id, { channel }, reason);
} }
/**
* Sets the status of the voice channel
* @param {string} status The message to set the channel status to
* @example
* // Setting the status to something
* guild.members.me.voice.setStatus("something")
* @example
* // Removing the status
* guild.members.me.voice.setStatus("")
* @returns {Promise<void>}
*/
async setStatus(status) {
// PUT https://discord.com/api/v9/channels/channelID/voice-status
if (this.channel?.id) {
// You can set the staus in normal voice channels and in stages so any type starting with GUILD should work
if (!this.channel?.type.startsWith('GUILD')) throw new Error('VOICE_NOT_IN_GUILD');
await this.client.api.channels(this.channel.id, 'voice-status').put({
data: {
status,
},
versioned: true,
});
}
}
/** /**
* Toggles the request to speak in the channel. * Toggles the request to speak in the channel.
* Only applicable for stage channels and for the client's own voice state. * Only applicable for stage channels and for the client's own voice state.
@ -253,7 +226,6 @@ class VoiceState extends Base {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async setRequestToSpeak(request = true) { async setRequestToSpeak(request = true) {
if (this.guild?.id) {
if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL'); if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL');
if (this.client.user.id !== this.id) throw new Error('VOICE_STATE_NOT_OWN'); if (this.client.user.id !== this.id) throw new Error('VOICE_STATE_NOT_OWN');
@ -265,7 +237,6 @@ class VoiceState extends Base {
}, },
}); });
} }
}
/** /**
* Suppress/unsuppress the user. Only applicable for stage channels. * Suppress/unsuppress the user. Only applicable for stage channels.
@ -285,7 +256,6 @@ class VoiceState extends Base {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async setSuppressed(suppressed = true) { async setSuppressed(suppressed = true) {
if (this.guild?.id) {
if (typeof suppressed !== 'boolean') throw new TypeError('VOICE_STATE_INVALID_TYPE', 'suppressed'); if (typeof suppressed !== 'boolean') throw new TypeError('VOICE_STATE_INVALID_TYPE', 'suppressed');
if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL'); if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL');
@ -300,6 +270,25 @@ class VoiceState extends Base {
}, },
}); });
} }
/**
* Sets the status of the voice channel
* @param {string} [status=""] The message to set the channel status to
* @example
* // Setting the status to something
* guild.members.me.voice.setStatus("something")
* @example
* // Removing the status
* guild.members.me.voice.setStatus()
* @returns {Promise<void>}
*/
setStatus(status = '') {
// PUT https://discord.com/api/v9/channels/:id/voice-status
return this.client.api.channels(this.channel.id, 'voice-status').put({
data: {
status,
},
});
} }
/** /**
@ -322,19 +311,18 @@ class VoiceState extends Base {
* @param {string} base64Image Base64 URI (data:image/jpeg;base64,data) * @param {string} base64Image Base64 URI (data:image/jpeg;base64,data)
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async postPreview(base64Image) { postPreview(base64Image) {
if (!this.client.user.id === this.id || !this.streaming) throw new Error('USER_NOT_STREAMING'); if (!this.client.user.id === this.id || !this.streaming) throw new Error('USER_NOT_STREAMING');
// URL: https://discord.com/api/v9/streams/guild:guildid:voicechannelid:userid/preview // URL: https://discord.com/api/v9/streams/guild:guildid:voicechannelid:userid/preview
// URL: https://discord.com/api/v9/streams/call:channelId:userId/preview // URL: https://discord.com/api/v9/streams/call:channelId:userId/preview
const streamKey = this.guild.id const streamKey = this.guild.id
? `guild:${this.guild.id}:${this.channelId}:${this.id}` ? `guild:${this.guild.id}:${this.channelId}:${this.id}`
: `call:${this.channelId}:${this.id}`; : `call:${this.channelId}:${this.id}`;
await this.client.api.streams[encodeURIComponent(streamKey)].preview.post({ return this.client.api.streams[encodeURIComponent(streamKey)].preview.post({
data: { data: {
thumbnail: base64Image, thumbnail: base64Image,
}, },
}); });
return true;
} }
toJSON() { toJSON() {