update
This commit is contained in:
parent
af1f561cfd
commit
4a1d615673
@ -113,14 +113,13 @@ class ChannelManager extends CachedManager {
|
||||
}
|
||||
|
||||
const data = await this.client.api.channels(id).get();
|
||||
// Delete in cache
|
||||
this._remove(id);
|
||||
return this._add(data, null, { cache, allowUnknownGuild });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Group DM
|
||||
* @param {UserResolvable[]} recipients Array of recipients
|
||||
* @returns {Promise<PartialGroupDMChannel>} Channel
|
||||
* @returns {Promise<GroupDMChannel>} Channel
|
||||
*/
|
||||
async createGroupDM(recipients) {
|
||||
// Check
|
||||
|
@ -3,7 +3,7 @@
|
||||
const ThreadManager = require('./ThreadManager');
|
||||
const { TypeError } = require('../errors');
|
||||
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.
|
||||
@ -20,7 +20,7 @@ class GuildForumThreadManager extends ThreadManager {
|
||||
* @typedef {BaseMessageOptions} GuildForumThreadMessageCreateOptions
|
||||
* @property {StickerResolvable} [stickers] The stickers 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,35 +63,29 @@ class GuildForumThreadManager extends ThreadManager {
|
||||
let messagePayload;
|
||||
|
||||
if (message instanceof MessagePayload) {
|
||||
messagePayload = await message.resolveData();
|
||||
messagePayload = message.resolveData();
|
||||
} 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') {
|
||||
message.usingNewAttachmentAPI = this.client.options.usingNewAttachmentAPI;
|
||||
}
|
||||
|
||||
if (message?.usingNewAttachmentAPI === true) {
|
||||
const attachments = await getAttachments(this.client, this.channel.id, ...files);
|
||||
const requestPromises = attachments.map(async attachment => {
|
||||
await uploadFile(files[attachment.id].file, attachment.upload_url);
|
||||
return {
|
||||
id: attachment.id,
|
||||
filename: files[attachment.id].name,
|
||||
uploaded_filename: attachment.upload_filename,
|
||||
description: files[attachment.id].description,
|
||||
duration_secs: files[attachment.id].duration_secs,
|
||||
waveform: files[attachment.id].waveform,
|
||||
};
|
||||
});
|
||||
const attachmentsData = await Promise.all(requestPromises);
|
||||
attachmentsData.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
||||
body.attachments = attachmentsData;
|
||||
files = [];
|
||||
}
|
||||
// New API
|
||||
const attachments = await getUploadURL(this.client, this.channel.id, files);
|
||||
const requestPromises = attachments.map(async attachment => {
|
||||
await uploadFile(files[attachment.id].file, attachment.upload_url);
|
||||
return {
|
||||
id: attachment.id,
|
||||
filename: files[attachment.id].name,
|
||||
uploaded_filename: attachment.upload_filename,
|
||||
description: files[attachment.id].description,
|
||||
duration_secs: files[attachment.id].duration_secs,
|
||||
waveform: files[attachment.id].waveform,
|
||||
};
|
||||
});
|
||||
const attachmentsData = await Promise.all(requestPromises);
|
||||
attachmentsData.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
||||
data.attachments = attachmentsData;
|
||||
|
||||
if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild);
|
||||
|
||||
@ -103,7 +97,7 @@ class GuildForumThreadManager extends ThreadManager {
|
||||
applied_tags: appliedTags,
|
||||
message: body,
|
||||
},
|
||||
files,
|
||||
files: [],
|
||||
reason,
|
||||
});
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const CachedManager = require('./CachedManager');
|
||||
const { TypeError, Error } = require('../errors');
|
||||
const { TypeError } = require('../errors');
|
||||
const { Message } = require('../structures/Message');
|
||||
const MessagePayload = require('../structures/MessagePayload');
|
||||
const Util = require('../util/Util');
|
||||
@ -123,38 +123,32 @@ class MessageManager extends CachedManager {
|
||||
const messageId = this.resolveId(message);
|
||||
if (!messageId) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
|
||||
|
||||
let messagePayload;
|
||||
if (options instanceof MessagePayload) {
|
||||
messagePayload = await options.resolveData();
|
||||
} else {
|
||||
messagePayload = await MessagePayload.create(message instanceof Message ? message : this, options).resolveData();
|
||||
}
|
||||
let { data, files } = await messagePayload.resolveFiles();
|
||||
const { data, files } = await (options instanceof MessagePayload
|
||||
? options
|
||||
: MessagePayload.create(message instanceof Message ? message : this, options)
|
||||
)
|
||||
.resolveData()
|
||||
.resolveFiles();
|
||||
|
||||
if (typeof options == 'object' && typeof options.usingNewAttachmentAPI !== 'boolean') {
|
||||
options.usingNewAttachmentAPI = this.client.options.usingNewAttachmentAPI;
|
||||
}
|
||||
// New API
|
||||
const attachments = await Util.getUploadURL(this.client, this.channel.id, files);
|
||||
const requestPromises = attachments.map(async attachment => {
|
||||
await Util.uploadFile(files[attachment.id].file, attachment.upload_url);
|
||||
return {
|
||||
id: attachment.id,
|
||||
filename: files[attachment.id].name,
|
||||
uploaded_filename: attachment.upload_filename,
|
||||
description: files[attachment.id].description,
|
||||
duration_secs: files[attachment.id].duration_secs,
|
||||
waveform: files[attachment.id].waveform,
|
||||
};
|
||||
});
|
||||
const attachmentsData = await Promise.all(requestPromises);
|
||||
attachmentsData.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
||||
data.attachments = attachmentsData;
|
||||
// Empty Files
|
||||
|
||||
if (options?.usingNewAttachmentAPI === true) {
|
||||
const attachments = await Util.getAttachments(this.client, this.channel.id, ...files);
|
||||
const requestPromises = attachments.map(async attachment => {
|
||||
await Util.uploadFile(files[attachment.id].file, attachment.upload_url);
|
||||
return {
|
||||
id: attachment.id,
|
||||
filename: files[attachment.id].name,
|
||||
uploaded_filename: attachment.upload_filename,
|
||||
description: files[attachment.id].description,
|
||||
duration_secs: files[attachment.id].duration_secs,
|
||||
waveform: files[attachment.id].waveform,
|
||||
};
|
||||
});
|
||||
const attachmentsData = await Promise.all(requestPromises);
|
||||
attachmentsData.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
||||
data.attachments = attachmentsData;
|
||||
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);
|
||||
if (existing) {
|
||||
@ -251,12 +245,16 @@ class MessageManager extends CachedManager {
|
||||
const existing = this.cache.get(messageId);
|
||||
if (existing && !existing.partial) return existing;
|
||||
}
|
||||
|
||||
// https://discord.com/api/v9/channels/:id/messages?limit=50&around=:msgid
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fetchMany({
|
||||
around: messageId,
|
||||
limit: 50,
|
||||
})
|
||||
this._fetchMany(
|
||||
{
|
||||
around: messageId,
|
||||
limit: 50,
|
||||
},
|
||||
cache,
|
||||
)
|
||||
.then(data_ =>
|
||||
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
|
||||
* @property {Array<UserResolvable>} [authors] An array of author to filter by
|
||||
@ -388,6 +379,13 @@ class MessageManager extends CachedManager {
|
||||
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;
|
||||
|
@ -1,22 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
const process = require('node:process');
|
||||
const { setTimeout } = require('node:timers');
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Base = require('./Base');
|
||||
const BaseMessageComponent = require('./BaseMessageComponent');
|
||||
const ClientApplication = require('./ClientApplication');
|
||||
const InteractionCollector = require('./InteractionCollector');
|
||||
const MessageAttachment = require('./MessageAttachment');
|
||||
const MessageButton = require('./MessageButton');
|
||||
const Embed = require('./MessageEmbed');
|
||||
const Mentions = require('./MessageMentions');
|
||||
const MessagePayload = require('./MessagePayload');
|
||||
const MessageSelectMenu = require('./MessageSelectMenu');
|
||||
const ReactionCollector = require('./ReactionCollector');
|
||||
const { Sticker } = require('./Sticker');
|
||||
const Application = require('./interfaces/Application');
|
||||
const { Error } = require('../errors');
|
||||
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 Permissions = require('../util/Permissions');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
@ -256,9 +260,9 @@ class Message extends Base {
|
||||
if ('application' in data) {
|
||||
/**
|
||||
* 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 {
|
||||
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
|
||||
* @type {boolean}
|
||||
@ -653,14 +598,7 @@ class Message extends Base {
|
||||
* channel.bulkDelete(messages.filter(message => message.bulkDeletable));
|
||||
*/
|
||||
get bulkDeletable() {
|
||||
return (
|
||||
(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
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -722,7 +660,7 @@ class Message extends Base {
|
||||
* @property {MessageAttachment[]} [attachments] An array of attachments to keep,
|
||||
* all attachments will be kept if omitted
|
||||
* @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)
|
||||
*/
|
||||
|
||||
@ -792,7 +730,7 @@ class Message extends Base {
|
||||
/**
|
||||
* Adds a reaction to the message.
|
||||
* @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>}
|
||||
* @example
|
||||
* // React to a message with a unicode emoji
|
||||
@ -1024,13 +962,221 @@ class Message extends Base {
|
||||
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.
|
||||
* @returns {Promise<boolean>}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async markUnread() {
|
||||
await this.client.api.channels[this.channelId].messages[this.id].ack.post({
|
||||
markUnread() {
|
||||
return this.client.api.channels[this.channelId].messages[this.id].ack.post({
|
||||
data: {
|
||||
manual: true,
|
||||
mention_count:
|
||||
@ -1042,145 +1188,18 @@ class Message extends Base {
|
||||
: 0,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the message as read.
|
||||
* @returns {Promise<boolean>}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async markRead() {
|
||||
await this.client.api.channels[this.channelId].messages[this.id].ack.post({
|
||||
markRead() {
|
||||
return this.client.api.channels[this.channelId].messages[this.id].ack.post({
|
||||
data: {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ class MessageReaction {
|
||||
this.me = data.me || data.me_burst;
|
||||
|
||||
/**
|
||||
* Super reaction
|
||||
* Is super reaction
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.isBurst = Boolean(data.me_burst || data.burst);
|
||||
|
@ -86,11 +86,15 @@ class VoiceState extends Base {
|
||||
|
||||
// The self_stream is property is omitted if false, check for another property
|
||||
// here to avoid incorrectly clearing this when partial data is specified
|
||||
/**
|
||||
* Whether this member is streaming using "Screen Share"
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.streaming = data.self_stream ?? false;
|
||||
if ('self_stream' in data) {
|
||||
/**
|
||||
* Whether this member is streaming using "Screen Share"
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.streaming = data.self_stream ?? false;
|
||||
} else {
|
||||
this.streaming ??= null;
|
||||
}
|
||||
|
||||
if ('channel_id' in data) {
|
||||
/**
|
||||
@ -144,12 +148,11 @@ class VoiceState extends Base {
|
||||
|
||||
/**
|
||||
* The channel that the member is connected to
|
||||
* @type {?(VoiceChannel|StageChannel)}
|
||||
* @type {?(VoiceChannel|StageChannel|DMChannel|GroupDMChannel)}
|
||||
* @readonly
|
||||
*/
|
||||
get channel() {
|
||||
if (!this.guild?.id) return this.guild.client.channels.cache.get(this.channelId) ?? null;
|
||||
return this.guild.channels.cache.get(this.channelId) ?? null;
|
||||
return (this.guild || this.client).channels.cache.get(this.channelId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -177,7 +180,6 @@ class VoiceState extends Base {
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
setMute(mute = true, reason) {
|
||||
if (!this.guild?.id) return null;
|
||||
return this.guild.members.edit(this.id, { mute }, reason);
|
||||
}
|
||||
|
||||
@ -188,7 +190,6 @@ class VoiceState extends Base {
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
setDeaf(deaf = true, reason) {
|
||||
if (!this.guild?.id) return null;
|
||||
return this.guild.members.edit(this.id, { deaf }, reason);
|
||||
}
|
||||
|
||||
@ -198,7 +199,6 @@ class VoiceState extends Base {
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
disconnect(reason) {
|
||||
if (!this.guild?.id) return this.callVoice?.disconnect();
|
||||
return this.setChannel(null, reason);
|
||||
}
|
||||
|
||||
@ -210,36 +210,9 @@ class VoiceState extends Base {
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
setChannel(channel, reason) {
|
||||
if (!this.guild?.id) return null;
|
||||
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.
|
||||
* Only applicable for stage channels and for the client's own voice state.
|
||||
@ -253,18 +226,16 @@ class VoiceState extends Base {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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');
|
||||
|
||||
await this.client.api.guilds(this.guild.id, 'voice-states', '@me').patch({
|
||||
data: {
|
||||
channel_id: this.channelId,
|
||||
request_to_speak_timestamp: request ? new Date().toISOString() : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
await this.client.api.guilds(this.guild.id, 'voice-states', '@me').patch({
|
||||
data: {
|
||||
channel_id: this.channelId,
|
||||
request_to_speak_timestamp: request ? new Date().toISOString() : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -285,21 +256,39 @@ class VoiceState extends Base {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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');
|
||||
|
||||
const target = this.client.user.id === this.id ? '@me' : this.id;
|
||||
const target = this.client.user.id === this.id ? '@me' : this.id;
|
||||
|
||||
await this.client.api.guilds(this.guild.id, 'voice-states', target).patch({
|
||||
data: {
|
||||
channel_id: this.channelId,
|
||||
suppress: suppressed,
|
||||
request_to_speak_timestamp: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
await this.client.api.guilds(this.guild.id, 'voice-states', target).patch({
|
||||
data: {
|
||||
channel_id: this.channelId,
|
||||
suppress: suppressed,
|
||||
request_to_speak_timestamp: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async postPreview(base64Image) {
|
||||
postPreview(base64Image) {
|
||||
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/call:channelId:userId/preview
|
||||
const streamKey = this.guild.id
|
||||
? `guild:${this.guild.id}:${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: {
|
||||
thumbnail: base64Image,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
|
Loading…
Reference in New Issue
Block a user