Initial commit
This commit is contained in:
109
src/structures/interfaces/Application.js
Normal file
109
src/structures/interfaces/Application.js
Normal file
@@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
|
||||
const { DiscordSnowflake } = require('@sapphire/snowflake');
|
||||
const Base = require('../Base');
|
||||
|
||||
/**
|
||||
* Represents an OAuth2 Application.
|
||||
* @abstract
|
||||
*/
|
||||
class Application extends Base {
|
||||
constructor(client, data) {
|
||||
super(client);
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
if(!data) return;
|
||||
|
||||
/**
|
||||
* The application's id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
if ('name' in data) {
|
||||
/**
|
||||
* The name of the application
|
||||
* @type {?string}
|
||||
*/
|
||||
this.name = data.name;
|
||||
} else {
|
||||
this.name ??= null;
|
||||
}
|
||||
|
||||
if ('description' in data) {
|
||||
/**
|
||||
* The application's description
|
||||
* @type {?string}
|
||||
*/
|
||||
this.description = data.description;
|
||||
} else {
|
||||
this.description ??= null;
|
||||
}
|
||||
|
||||
if ('icon' in data) {
|
||||
/**
|
||||
* The application's icon hash
|
||||
* @type {?string}
|
||||
*/
|
||||
this.icon = data.icon;
|
||||
} else {
|
||||
this.icon ??= null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp the application was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
return DiscordSnowflake.timestampFrom(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the application was created at
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* A link to the application's icon.
|
||||
* @param {ImageURLOptions} [options={}] Options for the image URL
|
||||
* @returns {?string}
|
||||
*/
|
||||
iconURL(options = {}) {
|
||||
return this.icon && this.client.rest.cdn.appIcon(this.id, this.icon, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* A link to this application's cover image.
|
||||
* @param {ImageURLOptions} [options={}] Options for the image URL
|
||||
* @returns {?string}
|
||||
*/
|
||||
coverURL(options = {}) {
|
||||
return this.cover && this.client.rest.cdn.appIcon(this.id, this.cover, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* When concatenated with a string, this automatically returns the application's name instead of the
|
||||
* Application object.
|
||||
* @returns {?string}
|
||||
* @example
|
||||
* // Logs: Application name: My App
|
||||
* console.log(`Application name: ${application}`);
|
||||
*/
|
||||
toString() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return super.toJSON({ createdTimestamp: true });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Application;
|
299
src/structures/interfaces/Collector.js
Normal file
299
src/structures/interfaces/Collector.js
Normal file
@@ -0,0 +1,299 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('node:events');
|
||||
const { setTimeout, clearTimeout } = require('node:timers');
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { TypeError } = require('../../errors');
|
||||
const Util = require('../../util/Util');
|
||||
|
||||
/**
|
||||
* Filter to be applied to the collector.
|
||||
* @typedef {Function} CollectorFilter
|
||||
* @param {...*} args Any arguments received by the listener
|
||||
* @param {Collection} collection The items collected by this collector
|
||||
* @returns {boolean|Promise<boolean>}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options to be applied to the collector.
|
||||
* @typedef {Object} CollectorOptions
|
||||
* @property {CollectorFilter} [filter] The filter applied to this collector
|
||||
* @property {number} [time] How long to run the collector for in milliseconds
|
||||
* @property {number} [idle] How long to stop the collector after inactivity in milliseconds
|
||||
* @property {boolean} [dispose=false] Whether to dispose data when it's deleted
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstract class for defining a new Collector.
|
||||
* @abstract
|
||||
*/
|
||||
class Collector extends EventEmitter {
|
||||
constructor(client, options = {}) {
|
||||
super();
|
||||
|
||||
/**
|
||||
* The client that instantiated this Collector
|
||||
* @name Collector#client
|
||||
* @type {Client}
|
||||
* @readonly
|
||||
*/
|
||||
Object.defineProperty(this, 'client', { value: client });
|
||||
|
||||
/**
|
||||
* The filter applied to this collector
|
||||
* @type {CollectorFilter}
|
||||
* @returns {boolean|Promise<boolean>}
|
||||
*/
|
||||
this.filter = options.filter ?? (() => true);
|
||||
|
||||
/**
|
||||
* The options of this collector
|
||||
* @type {CollectorOptions}
|
||||
*/
|
||||
this.options = options;
|
||||
|
||||
/**
|
||||
* The items collected by this collector
|
||||
* @type {Collection}
|
||||
*/
|
||||
this.collected = new Collection();
|
||||
|
||||
/**
|
||||
* Whether this collector has finished collecting
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.ended = false;
|
||||
|
||||
/**
|
||||
* Timeout for cleanup
|
||||
* @type {?Timeout}
|
||||
* @private
|
||||
*/
|
||||
this._timeout = null;
|
||||
|
||||
/**
|
||||
* Timeout for cleanup due to inactivity
|
||||
* @type {?Timeout}
|
||||
* @private
|
||||
*/
|
||||
this._idletimeout = null;
|
||||
|
||||
if (typeof this.filter !== 'function') {
|
||||
throw new TypeError('INVALID_TYPE', 'options.filter', 'function');
|
||||
}
|
||||
|
||||
this.handleCollect = this.handleCollect.bind(this);
|
||||
this.handleDispose = this.handleDispose.bind(this);
|
||||
|
||||
if (options.time) this._timeout = setTimeout(() => this.stop('time'), options.time).unref();
|
||||
if (options.idle) this._idletimeout = setTimeout(() => this.stop('idle'), options.idle).unref();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this to handle an event as a collectable element. Accepts any event data as parameters.
|
||||
* @param {...*} args The arguments emitted by the listener
|
||||
* @returns {Promise<void>}
|
||||
* @emits Collector#collect
|
||||
*/
|
||||
async handleCollect(...args) {
|
||||
const collect = await this.collect(...args);
|
||||
|
||||
if (collect && (await this.filter(...args, this.collected))) {
|
||||
this.collected.set(collect, args[0]);
|
||||
|
||||
/**
|
||||
* Emitted whenever an element is collected.
|
||||
* @event Collector#collect
|
||||
* @param {...*} args The arguments emitted by the listener
|
||||
*/
|
||||
this.emit('collect', ...args);
|
||||
|
||||
if (this._idletimeout) {
|
||||
clearTimeout(this._idletimeout);
|
||||
this._idletimeout = setTimeout(() => this.stop('idle'), this.options.idle).unref();
|
||||
}
|
||||
}
|
||||
this.checkEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this to remove an element from the collection. Accepts any event data as parameters.
|
||||
* @param {...*} args The arguments emitted by the listener
|
||||
* @returns {Promise<void>}
|
||||
* @emits Collector#dispose
|
||||
*/
|
||||
async handleDispose(...args) {
|
||||
if (!this.options.dispose) return;
|
||||
|
||||
const dispose = this.dispose(...args);
|
||||
if (!dispose || !(await this.filter(...args)) || !this.collected.has(dispose)) return;
|
||||
this.collected.delete(dispose);
|
||||
|
||||
/**
|
||||
* Emitted whenever an element is disposed of.
|
||||
* @event Collector#dispose
|
||||
* @param {...*} args The arguments emitted by the listener
|
||||
*/
|
||||
this.emit('dispose', ...args);
|
||||
this.checkEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves with the next collected element;
|
||||
* rejects with collected elements if the collector finishes without receiving a next element
|
||||
* @type {Promise}
|
||||
* @readonly
|
||||
*/
|
||||
get next() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.ended) {
|
||||
reject(this.collected);
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
this.removeListener('collect', onCollect);
|
||||
this.removeListener('end', onEnd);
|
||||
};
|
||||
|
||||
const onCollect = item => {
|
||||
cleanup();
|
||||
resolve(item);
|
||||
};
|
||||
|
||||
const onEnd = () => {
|
||||
cleanup();
|
||||
reject(this.collected); // eslint-disable-line prefer-promise-reject-errors
|
||||
};
|
||||
|
||||
this.on('collect', onCollect);
|
||||
this.on('end', onEnd);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops this collector and emits the `end` event.
|
||||
* @param {string} [reason='user'] The reason this collector is ending
|
||||
* @emits Collector#end
|
||||
*/
|
||||
stop(reason = 'user') {
|
||||
if (this.ended) return;
|
||||
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = null;
|
||||
}
|
||||
if (this._idletimeout) {
|
||||
clearTimeout(this._idletimeout);
|
||||
this._idletimeout = null;
|
||||
}
|
||||
this.ended = true;
|
||||
|
||||
/**
|
||||
* Emitted when the collector is finished collecting.
|
||||
* @event Collector#end
|
||||
* @param {Collection} collected The elements collected by the collector
|
||||
* @param {string} reason The reason the collector ended
|
||||
*/
|
||||
this.emit('end', this.collected, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to reset the timeout and idle timer of a {@link Collector}.
|
||||
* @typedef {Object} CollectorResetTimerOptions
|
||||
* @property {number} [time] How long to run the collector for (in milliseconds)
|
||||
* @property {number} [idle] How long to wait to stop the collector after inactivity (in milliseconds)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resets the collector's timeout and idle timer.
|
||||
* @param {CollectorResetTimerOptions} [options] Options for resetting
|
||||
*/
|
||||
resetTimer({ time, idle } = {}) {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = setTimeout(() => this.stop('time'), time ?? this.options.time).unref();
|
||||
}
|
||||
if (this._idletimeout) {
|
||||
clearTimeout(this._idletimeout);
|
||||
this._idletimeout = setTimeout(() => this.stop('idle'), idle ?? this.options.idle).unref();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the collector should end, and if so, ends it.
|
||||
* @returns {boolean} Whether the collector ended or not
|
||||
*/
|
||||
checkEnd() {
|
||||
const reason = this.endReason;
|
||||
if (reason) this.stop(reason);
|
||||
return Boolean(reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows collectors to be consumed with for-await-of loops
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of}
|
||||
*/
|
||||
async *[Symbol.asyncIterator]() {
|
||||
const queue = [];
|
||||
const onCollect = (...item) => queue.push(item);
|
||||
this.on('collect', onCollect);
|
||||
|
||||
try {
|
||||
while (queue.length || !this.ended) {
|
||||
if (queue.length) {
|
||||
yield queue.shift();
|
||||
} else {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise(resolve => {
|
||||
const tick = () => {
|
||||
this.removeListener('collect', tick);
|
||||
this.removeListener('end', tick);
|
||||
return resolve();
|
||||
};
|
||||
this.on('collect', tick);
|
||||
this.on('end', tick);
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.removeListener('collect', onCollect);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return Util.flatten(this);
|
||||
}
|
||||
|
||||
/* eslint-disable no-empty-function */
|
||||
/**
|
||||
* The reason this collector has ended with, or null if it hasn't ended yet
|
||||
* @type {?string}
|
||||
* @readonly
|
||||
* @abstract
|
||||
*/
|
||||
get endReason() {}
|
||||
|
||||
/**
|
||||
* Handles incoming events from the `handleCollect` function. Returns null if the event should not
|
||||
* be collected, or returns an object describing the data that should be stored.
|
||||
* @see Collector#handleCollect
|
||||
* @param {...*} args Any args the event listener emits
|
||||
* @returns {?(*|Promise<?*>)} Data to insert into collection, if any
|
||||
* @abstract
|
||||
*/
|
||||
collect() {}
|
||||
|
||||
/**
|
||||
* Handles incoming events from the `handleDispose`. Returns null if the event should not
|
||||
* be disposed, or returns the key that should be removed.
|
||||
* @see Collector#handleDispose
|
||||
* @param {...*} args Any args the event listener emits
|
||||
* @returns {?*} Key to remove from the collection, if any
|
||||
* @abstract
|
||||
*/
|
||||
dispose() {}
|
||||
/* eslint-enable no-empty-function */
|
||||
}
|
||||
|
||||
module.exports = Collector;
|
251
src/structures/interfaces/InteractionResponses.js
Normal file
251
src/structures/interfaces/InteractionResponses.js
Normal file
@@ -0,0 +1,251 @@
|
||||
'use strict';
|
||||
|
||||
const { InteractionResponseType, MessageFlags, Routes } = require('discord-api-types/v9');
|
||||
const { Error } = require('../../errors');
|
||||
const MessagePayload = require('../MessagePayload');
|
||||
|
||||
/**
|
||||
* Interface for classes that support shared interaction response types.
|
||||
* @interface
|
||||
*/
|
||||
class InteractionResponses {
|
||||
/**
|
||||
* Options for deferring the reply to an {@link Interaction}.
|
||||
* @typedef {Object} InteractionDeferReplyOptions
|
||||
* @property {boolean} [ephemeral] Whether the reply should be ephemeral
|
||||
* @property {boolean} [fetchReply] Whether to fetch the reply
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for deferring and updating the reply to a {@link MessageComponentInteraction}.
|
||||
* @typedef {Object} InteractionDeferUpdateOptions
|
||||
* @property {boolean} [fetchReply] Whether to fetch the reply
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for a reply to an {@link Interaction}.
|
||||
* @typedef {BaseMessageOptions} InteractionReplyOptions
|
||||
* @property {boolean} [ephemeral] Whether the reply should be ephemeral
|
||||
* @property {boolean} [fetchReply] Whether to fetch the reply
|
||||
* @property {MessageFlags} [flags] Which flags to set for the message.
|
||||
* Only `MessageFlags.SuppressEmbeds` and `MessageFlags.Ephemeral` can be set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for updating the message received from a {@link MessageComponentInteraction}.
|
||||
* @typedef {MessageEditOptions} InteractionUpdateOptions
|
||||
* @property {boolean} [fetchReply] Whether to fetch the reply
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defers the reply to this interaction.
|
||||
* @param {InteractionDeferReplyOptions} [options] Options for deferring the reply to this interaction
|
||||
* @returns {Promise<Message|APIMessage|void>}
|
||||
* @example
|
||||
* // Defer the reply to this interaction
|
||||
* interaction.deferReply()
|
||||
* .then(console.log)
|
||||
* .catch(console.error)
|
||||
* @example
|
||||
* // Defer to send an ephemeral reply later
|
||||
* interaction.deferReply({ ephemeral: true })
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async deferReply(options = {}) {
|
||||
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
|
||||
this.ephemeral = options.ephemeral ?? false;
|
||||
await this.client.api.interactions(this.id, this.token).callback.post({
|
||||
body: {
|
||||
type: InteractionResponseType.DeferredChannelMessageWithSource,
|
||||
data: {
|
||||
flags: options.ephemeral ? MessageFlags.Ephemeral : undefined,
|
||||
},
|
||||
},
|
||||
auth: false,
|
||||
})
|
||||
this.deferred = true;
|
||||
|
||||
return options.fetchReply ? this.fetchReply() : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reply to this interaction.
|
||||
* <info>Use the `fetchReply` option to get the bot's reply message.</info>
|
||||
* @param {string|MessagePayload|InteractionReplyOptions} options The options for the reply
|
||||
* @returns {Promise<Message|APIMessage|void>}
|
||||
* @example
|
||||
* // Reply to the interaction and fetch the response
|
||||
* interaction.reply({ content: 'Pong!', fetchReply: true })
|
||||
* .then((message) => console.log(`Reply sent with content ${message.content}`))
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Create an ephemeral reply with an embed
|
||||
* const embed = new Embed().setDescription('Pong!');
|
||||
*
|
||||
* interaction.reply({ embeds: [embed], ephemeral: true })
|
||||
* .then(() => console.log('Reply sent.'))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async reply(options) {
|
||||
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
|
||||
this.ephemeral = options.ephemeral ?? false;
|
||||
|
||||
let messagePayload;
|
||||
if (options instanceof MessagePayload) messagePayload = options;
|
||||
else messagePayload = MessagePayload.create(this, options);
|
||||
|
||||
const { body: data, files } = await messagePayload.resolveBody().resolveFiles();
|
||||
|
||||
await this.client.api.interactions(this.id, this.token).callback.post({
|
||||
body: {
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data,
|
||||
},
|
||||
files,
|
||||
auth: false,
|
||||
});
|
||||
this.replied = true;
|
||||
|
||||
return options.fetchReply ? this.fetchReply() : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the initial reply to this interaction.
|
||||
* @see Webhook#fetchMessage
|
||||
* @returns {Promise<Message|APIMessage>}
|
||||
* @example
|
||||
* // Fetch the reply to this interaction
|
||||
* interaction.fetchReply()
|
||||
* .then(reply => console.log(`Replied with ${reply.content}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
fetchReply() {
|
||||
return this.webhook.fetchMessage('@original');
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the initial reply to this interaction.
|
||||
* @see Webhook#editMessage
|
||||
* @param {string|MessagePayload|WebhookEditMessageOptions} options The new options for the message
|
||||
* @returns {Promise<Message|APIMessage>}
|
||||
* @example
|
||||
* // Edit the reply to this interaction
|
||||
* interaction.editReply('New content')
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async editReply(options) {
|
||||
if (!this.deferred && !this.replied) throw new Error('INTERACTION_NOT_REPLIED');
|
||||
const message = await this.webhook.editMessage('@original', options);
|
||||
this.replied = true;
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the initial reply to this interaction.
|
||||
* @see Webhook#deleteMessage
|
||||
* @returns {Promise<void>}
|
||||
* @example
|
||||
* // Delete the reply to this interaction
|
||||
* interaction.deleteReply()
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async deleteReply() {
|
||||
if (this.ephemeral) throw new Error('INTERACTION_EPHEMERAL_REPLIED');
|
||||
await this.webhook.deleteMessage('@original');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a follow-up message to this interaction.
|
||||
* @param {string|MessagePayload|InteractionReplyOptions} options The options for the reply
|
||||
* @returns {Promise<Message|APIMessage>}
|
||||
*/
|
||||
followUp(options) {
|
||||
if (!this.deferred && !this.replied) return Promise.reject(new Error('INTERACTION_NOT_REPLIED'));
|
||||
return this.webhook.send(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defers an update to the message to which the component was attached.
|
||||
* @param {InteractionDeferUpdateOptions} [options] Options for deferring the update to this interaction
|
||||
* @returns {Promise<Message|APIMessage|void>}
|
||||
* @example
|
||||
* // Defer updating and reset the component's loading state
|
||||
* interaction.deferUpdate()
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async deferUpdate(options = {}) {
|
||||
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
|
||||
await this.client.api.interactions(this.id, this.token).callback.post({
|
||||
body: {
|
||||
type: InteractionResponseType.DeferredMessageUpdate,
|
||||
},
|
||||
auth: false,
|
||||
});
|
||||
this.deferred = true;
|
||||
|
||||
return options.fetchReply ? this.fetchReply() : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the original message of the component on which the interaction was received on.
|
||||
* @param {string|MessagePayload|InteractionUpdateOptions} options The options for the updated message
|
||||
* @returns {Promise<Message|APIMessage|void>}
|
||||
* @example
|
||||
* // Remove the components from the message
|
||||
* interaction.update({
|
||||
* content: "A component interaction was received",
|
||||
* components: []
|
||||
* })
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async update(options) {
|
||||
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
|
||||
|
||||
let messagePayload;
|
||||
if (options instanceof MessagePayload) messagePayload = options;
|
||||
else messagePayload = MessagePayload.create(this, options);
|
||||
|
||||
const { body: data, files } = await messagePayload.resolveBody().resolveFiles();
|
||||
|
||||
await this.client.api.interactions(this.id, this.token).callback.post({
|
||||
body: {
|
||||
type: InteractionResponseType.UpdateMessage,
|
||||
data,
|
||||
},
|
||||
files,
|
||||
auth: false,
|
||||
});
|
||||
this.replied = true;
|
||||
|
||||
return options.fetchReply ? this.fetchReply() : undefined;
|
||||
}
|
||||
|
||||
static applyToClass(structure, ignore = []) {
|
||||
const props = [
|
||||
'deferReply',
|
||||
'reply',
|
||||
'fetchReply',
|
||||
'editReply',
|
||||
'deleteReply',
|
||||
'followUp',
|
||||
'deferUpdate',
|
||||
'update',
|
||||
];
|
||||
|
||||
for (const prop of props) {
|
||||
if (ignore.includes(prop)) continue;
|
||||
Object.defineProperty(
|
||||
structure.prototype,
|
||||
prop,
|
||||
Object.getOwnPropertyDescriptor(InteractionResponses.prototype, prop),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InteractionResponses;
|
363
src/structures/interfaces/TextBasedChannel.js
Normal file
363
src/structures/interfaces/TextBasedChannel.js
Normal file
@@ -0,0 +1,363 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { DiscordSnowflake } = require('@sapphire/snowflake');
|
||||
const { InteractionType, Routes } = require('discord-api-types/v9');
|
||||
const { TypeError, Error } = require('../../errors');
|
||||
const InteractionCollector = require('../InteractionCollector');
|
||||
const MessageCollector = require('../MessageCollector');
|
||||
const MessagePayload = require('../MessagePayload');
|
||||
|
||||
/**
|
||||
* Interface for classes that have text-channel-like features.
|
||||
* @interface
|
||||
*/
|
||||
class TextBasedChannel {
|
||||
constructor() {
|
||||
/**
|
||||
* A manager of the messages sent to this channel
|
||||
* @type {MessageManager}
|
||||
*/
|
||||
this.messages = new MessageManager(this);
|
||||
|
||||
/**
|
||||
* The channel's last message id, if one was sent
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.lastMessageId = null;
|
||||
|
||||
/**
|
||||
* The timestamp when the last pinned message was pinned, if there was one
|
||||
* @type {?number}
|
||||
*/
|
||||
this.lastPinTimestamp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Message object of the last message in the channel, if one was sent
|
||||
* @type {?Message}
|
||||
* @readonly
|
||||
*/
|
||||
get lastMessage() {
|
||||
return this.messages.resolve(this.lastMessageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The date when the last pinned message was pinned, if there was one
|
||||
* @type {?Date}
|
||||
* @readonly
|
||||
*/
|
||||
get lastPinAt() {
|
||||
return this.lastPinTimestamp && new Date(this.lastPinTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base options provided when sending.
|
||||
* @typedef {Object} BaseMessageOptions
|
||||
* @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 {Embed[]|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)
|
||||
* @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to send with the message
|
||||
* @property {ActionRow[]|ActionRowOptions[]} [components]
|
||||
* Action rows containing interactive components for the message (buttons, select menus)
|
||||
* @property {MessageAttachment[]} [attachments] Attachments to send in the message
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options provided when sending or editing a message.
|
||||
* @typedef {BaseMessageOptions} MessageOptions
|
||||
* @property {ReplyOptions} [reply] The options for replying to a message
|
||||
* @property {StickerResolvable[]} [stickers=[]] Stickers to send in the message
|
||||
* @property {MessageFlags} [flags] Which flags to set for the message. Only `MessageFlags.SuppressEmbeds` can be set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options provided to control parsing of mentions by Discord
|
||||
* @typedef {Object} MessageMentionOptions
|
||||
* @property {MessageMentionTypes[]} [parse] Types of mentions to be parsed
|
||||
* @property {Snowflake[]} [users] Snowflakes of Users to be parsed as mentions
|
||||
* @property {Snowflake[]} [roles] Snowflakes of Roles to be parsed as mentions
|
||||
* @property {boolean} [repliedUser=true] Whether the author of the Message being replied to should be pinged
|
||||
*/
|
||||
|
||||
/**
|
||||
* Types of mentions to enable in MessageMentionOptions.
|
||||
* - `roles`
|
||||
* - `users`
|
||||
* - `everyone`
|
||||
* @typedef {string} MessageMentionTypes
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} FileOptions
|
||||
* @property {BufferResolvable} attachment File to attach
|
||||
* @property {string} [name='file.jpg'] Filename of the attachment
|
||||
* @property {string} description The description of the file
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for sending a message with a reply.
|
||||
* @typedef {Object} ReplyOptions
|
||||
* @property {MessageResolvable} messageReference The message to reply to (must be in the same channel and not system)
|
||||
* @property {boolean} [failIfNotExists=this.client.options.failIfNotExists] Whether to error if the referenced
|
||||
* message does not exist (creates a standard message in this case when false)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sends a message to this channel.
|
||||
* @param {string|MessagePayload|MessageOptions} options The options to provide
|
||||
* @returns {Promise<Message>}
|
||||
* @example
|
||||
* // Send a basic message
|
||||
* channel.send('hello!')
|
||||
* .then(message => console.log(`Sent message: ${message.content}`))
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Send a remote file
|
||||
* channel.send({
|
||||
* files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048']
|
||||
* })
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Send a local file
|
||||
* channel.send({
|
||||
* files: [{
|
||||
* attachment: 'entire/path/to/file.jpg',
|
||||
* name: 'file.jpg',
|
||||
* description: 'A description of the file'
|
||||
* }]
|
||||
* })
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Send an embed with a local image inside
|
||||
* channel.send({
|
||||
* content: 'This is an embed',
|
||||
* embeds: [
|
||||
* {
|
||||
* thumbnail: {
|
||||
* url: 'attachment://file.jpg'
|
||||
* }
|
||||
* }
|
||||
* ],
|
||||
* files: [{
|
||||
* attachment: 'entire/path/to/file.jpg',
|
||||
* name: 'file.jpg',
|
||||
* description: 'A description of the file'
|
||||
* }]
|
||||
* })
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async send(options) {
|
||||
await this.client.api.channels(this.id).typing.post();
|
||||
const User = require('../User');
|
||||
const { GuildMember } = require('../GuildMember');
|
||||
|
||||
if (this instanceof User || this instanceof GuildMember) {
|
||||
const dm = await this.createDM();
|
||||
return dm.send(options);
|
||||
}
|
||||
|
||||
let messagePayload;
|
||||
|
||||
if (options instanceof MessagePayload) {
|
||||
messagePayload = options.resolveBody();
|
||||
} else {
|
||||
messagePayload = MessagePayload.create(this, options).resolveBody();
|
||||
}
|
||||
|
||||
const { body, files } = await messagePayload.resolveFiles();
|
||||
const d = await this.client.api.channels[this.id].messages.post({ body, files });
|
||||
|
||||
await this.client.api.channels(this.id).typing.delete();
|
||||
return this.messages.cache.get(d.id) ?? this.messages._add(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a typing indicator in the channel.
|
||||
* @returns {Promise<void>} Resolves upon the typing status being sent
|
||||
* @example
|
||||
* // Start typing in a channel
|
||||
* channel.sendTyping();
|
||||
*/
|
||||
async sendTyping() {
|
||||
await this.client.api.channels(this.id).typing.post();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Message Collector.
|
||||
* @param {MessageCollectorOptions} [options={}] The options to pass to the collector
|
||||
* @returns {MessageCollector}
|
||||
* @example
|
||||
* // Create a message collector
|
||||
* const filter = m => m.content.includes('discord');
|
||||
* const collector = channel.createMessageCollector({ filter, time: 15_000 });
|
||||
* collector.on('collect', m => console.log(`Collected ${m.content}`));
|
||||
* collector.on('end', collected => console.log(`Collected ${collected.size} items`));
|
||||
*/
|
||||
createMessageCollector(options = {}) {
|
||||
return new MessageCollector(this, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* An object containing the same properties as CollectorOptions, but a few more:
|
||||
* @typedef {MessageCollectorOptions} AwaitMessagesOptions
|
||||
* @property {string[]} [errors] Stop/end reasons that cause the promise to reject
|
||||
*/
|
||||
|
||||
/**
|
||||
* Similar to createMessageCollector but in promise form.
|
||||
* Resolves with a collection of messages that pass the specified filter.
|
||||
* @param {AwaitMessagesOptions} [options={}] Optional options to pass to the internal collector
|
||||
* @returns {Promise<Collection<Snowflake, Message>>}
|
||||
* @example
|
||||
* // Await !vote messages
|
||||
* const filter = m => m.content.startsWith('!vote');
|
||||
* // Errors: ['time'] treats ending because of the time limit as an error
|
||||
* channel.awaitMessages({ filter, max: 4, time: 60_000, errors: ['time'] })
|
||||
* .then(collected => console.log(collected.size))
|
||||
* .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`));
|
||||
*/
|
||||
awaitMessages(options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const collector = this.createMessageCollector(options);
|
||||
collector.once('end', (collection, reason) => {
|
||||
if (options.errors?.includes(reason)) {
|
||||
reject(collection);
|
||||
} else {
|
||||
resolve(collection);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: InteractionType.MessageComponent,
|
||||
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>>} 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 (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() - DiscordSnowflake.timestampFrom(id) < 1_209_600_000);
|
||||
}
|
||||
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({ body: { 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');
|
||||
}
|
||||
|
||||
static applyToClass(structure, full = false, ignore = []) {
|
||||
const props = ['send'];
|
||||
if (full) {
|
||||
props.push(
|
||||
'lastMessage',
|
||||
'lastPinAt',
|
||||
'bulkDelete',
|
||||
'sendTyping',
|
||||
'createMessageCollector',
|
||||
'awaitMessages',
|
||||
'createMessageComponentCollector',
|
||||
'awaitMessageComponent',
|
||||
);
|
||||
}
|
||||
for (const prop of props) {
|
||||
if (ignore.includes(prop)) continue;
|
||||
Object.defineProperty(
|
||||
structure.prototype,
|
||||
prop,
|
||||
Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TextBasedChannel;
|
||||
|
||||
// Fixes Circular
|
||||
// eslint-disable-next-line import/order
|
||||
const MessageManager = require('../../managers/MessageManager');
|
Reference in New Issue
Block a user