'use strict'; /* eslint-disable import/order */ const MessageCollector = require('../MessageCollector'); const APIMessage = require('../APIMessage'); const Snowflake = require('../../util/Snowflake'); const Collection = require('../../util/Collection'); const { RangeError, TypeError } = require('../../errors'); /** * 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 ID of the last message in the channel, 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.cache.get(this.lastMessageID) || null; } /** * 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) : null; } /** * Options provided when sending or editing a message. * @typedef {Object} MessageOptions * @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 {MessageEmbed|Object} [embed] An embed 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 * @property {DisableMentionType} [disableMentions=this.client.options.disableMentions] Whether or not all mentions or * everyone/here mentions should be sanitized to prevent unexpected mentions * @property {FileOptions[]|BufferResolvable[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if * it exceeds the character limit. If an object is provided, these are the options for splitting the message * @property {MessageResolvable} [replyTo] The message to reply to (must be in the same channel) */ /** * 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] 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 */ /** * The type of mentions to disable. * - `none` * - `all` * - `everyone` * @typedef {string} DisableMentionType */ /** * @typedef {Object} FileOptions * @property {BufferResolvable} attachment File to attach * @property {string} [name='file.jpg'] Filename of the attachment */ /** * Options for splitting a message. * @typedef {Object} SplitOptions * @property {number} [maxLength=2000] Maximum character length per message piece * @property {string} [char='\n'] Character to split the message with * @property {string} [prepend=''] Text to prepend to every piece except the first * @property {string} [append=''] Text to append to every piece except the last */ /** * Sends a message to this channel. * @param {StringResolvable|APIMessage} [content=''] The content to send * @param {MessageOptions|MessageAdditions} [options={}] The options to provide * @returns {Promise} * @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' * }] * }) * .then(console.log) * .catch(console.error); * @example * // Send an embed with a local image inside * channel.send('This is an embed', { * embed: { * thumbnail: { * url: 'attachment://file.jpg' * } * }, * files: [{ * attachment: 'entire/path/to/file.jpg', * name: 'file.jpg' * }] * }) * .then(console.log) * .catch(console.error); */ async send(content, options) { const User = require('../User'); const GuildMember = require('../GuildMember'); if (this instanceof User || this instanceof GuildMember) { return this.createDM().then(dm => dm.send(content, options)); } let apiMessage; if (content instanceof APIMessage) { apiMessage = content.resolveData(); } else { apiMessage = APIMessage.create(this, content, options).resolveData(); if (Array.isArray(apiMessage.data.content)) { return Promise.all(apiMessage.split().map(this.send.bind(this))); } } const { data, files } = await apiMessage.resolveFiles(); return this.client.api.channels[this.id].messages .post({ data, files }) .then(d => this.client.actions.MessageCreate.handle(d).message); } /** * Starts a typing indicator in the channel. * @param {number} [count=1] The number of times startTyping should be considered to have been called * @returns {Promise} Resolves once the bot stops typing gracefully, or rejects when an error occurs * @example * // Start typing in a channel, or increase the typing count by one * channel.startTyping(); * @example * // Start typing in a channel with a typing count of five, or set it to five * channel.startTyping(5); */ startTyping(count) { if (typeof count !== 'undefined' && count < 1) throw new RangeError('TYPING_COUNT'); if (this.client.user._typing.has(this.id)) { const entry = this.client.user._typing.get(this.id); entry.count = count || entry.count + 1; return entry.promise; } const entry = {}; entry.promise = new Promise((resolve, reject) => { const endpoint = this.client.api.channels[this.id].typing; Object.assign(entry, { count: count || 1, interval: this.client.setInterval(() => { endpoint.post().catch(error => { this.client.clearInterval(entry.interval); this.client.user._typing.delete(this.id); reject(error); }); }, 9000), resolve, }); endpoint.post().catch(error => { this.client.clearInterval(entry.interval); this.client.user._typing.delete(this.id); reject(error); }); this.client.user._typing.set(this.id, entry); }); return entry.promise; } /** * Stops the typing indicator in the channel. * The indicator will only stop if this is called as many times as startTyping(). * It can take a few seconds for the client user to stop typing. * @param {boolean} [force=false] Whether or not to reset the call count and force the indicator to stop * @example * // Reduce the typing count by one and stop typing if it reached 0 * channel.stopTyping(); * @example * // Force typing to fully stop regardless of typing count * channel.stopTyping(true); */ stopTyping(force = false) { if (this.client.user._typing.has(this.id)) { const entry = this.client.user._typing.get(this.id); entry.count--; if (entry.count <= 0 || force) { this.client.clearInterval(entry.interval); this.client.user._typing.delete(this.id); entry.resolve(); } } } /** * Whether or not the typing indicator is being shown in the channel * @type {boolean} * @readonly */ get typing() { return this.client.user._typing.has(this.id); } /** * Number of times `startTyping` has been called * @type {number} * @readonly */ get typingCount() { if (this.client.user._typing.has(this.id)) return this.client.user._typing.get(this.id).count; return 0; } /** * Creates a Message Collector. * @param {CollectorFilter} filter The filter to create the collector with * @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: 15000 }); * collector.on('collect', m => console.log(`Collected ${m.content}`)); * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); */ createMessageCollector(filter, options = {}) { return new MessageCollector(this, filter, 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 {CollectorFilter} filter The filter function to use * @param {AwaitMessagesOptions} [options={}] Optional options to pass to the internal collector * @returns {Promise>} * @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: 60000, errors: ['time'] }) * .then(collected => console.log(collected.size)) * .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`)); */ awaitMessages(filter, options = {}) { return new Promise((resolve, reject) => { const collector = this.createMessageCollector(filter, options); collector.once('end', (collection, reason) => { if (options.errors && options.errors.includes(reason)) { reject(collection); } else { resolve(collection); } }); }); } /** * Bulk deletes given messages that are newer than two weeks. * @param {Collection|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>} 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.keyArray() : messages.map(m => m.id || m); if (filterOld) { messageIDs = messageIDs.filter(id => Date.now() - Snowflake.deconstruct(id).date.getTime() < 1209600000); } if (messageIDs.length === 0) return new Collection(); if (messageIDs.length === 1) { await this.client.api.channels(this.id).messages(messageIDs[0]).delete(); const message = this.client.actions.MessageDelete.getMessage( { message_id: messageIDs[0], }, this, ); return message ? new Collection([[message.id, message]]) : new Collection(); } await this.client.api.channels[this.id].messages['bulk-delete'].post({ data: { messages: messageIDs } }); return messageIDs.reduce( (col, id) => col.set( id, this.client.actions.MessageDeleteBulk.getMessage( { message_id: id, }, this, ), ), new Collection(), ); } if (!isNaN(messages)) { const msgs = await this.messages.fetch({ limit: messages }); return this.bulkDelete(msgs, filterOld); } throw new TypeError('MESSAGE_BULK_DELETE_TYPE'); } static applyToClass(structure, full = false, ignore = []) { const props = ['send']; if (full) { props.push( 'lastMessage', 'lastPinAt', 'bulkDelete', 'startTyping', 'stopTyping', 'typing', 'typingCount', 'createMessageCollector', 'awaitMessages', ); } 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 const MessageManager = require('../../managers/MessageManager');