TextBasedChannel.js (13718B)
1 'use strict'; 2 3 /* eslint-disable import/order */ 4 const MessageCollector = require('../MessageCollector'); 5 const APIMessage = require('../APIMessage'); 6 const Snowflake = require('../../util/Snowflake'); 7 const Collection = require('../../util/Collection'); 8 const { RangeError, TypeError } = require('../../errors'); 9 10 /** 11 * Interface for classes that have text-channel-like features. 12 * @interface 13 */ 14 class TextBasedChannel { 15 constructor() { 16 /** 17 * A manager of the messages sent to this channel 18 * @type {MessageManager} 19 */ 20 this.messages = new MessageManager(this); 21 22 /** 23 * The ID of the last message in the channel, if one was sent 24 * @type {?Snowflake} 25 */ 26 this.lastMessageID = null; 27 28 /** 29 * The timestamp when the last pinned message was pinned, if there was one 30 * @type {?number} 31 */ 32 this.lastPinTimestamp = null; 33 } 34 35 /** 36 * The Message object of the last message in the channel, if one was sent 37 * @type {?Message} 38 * @readonly 39 */ 40 get lastMessage() { 41 return this.messages.cache.get(this.lastMessageID) || null; 42 } 43 44 /** 45 * The date when the last pinned message was pinned, if there was one 46 * @type {?Date} 47 * @readonly 48 */ 49 get lastPinAt() { 50 return this.lastPinTimestamp ? new Date(this.lastPinTimestamp) : null; 51 } 52 53 /** 54 * Options provided when sending or editing a message. 55 * @typedef {Object} MessageOptions 56 * @property {boolean} [tts=false] Whether or not the message should be spoken aloud 57 * @property {string} [nonce=''] The nonce for the message 58 * @property {string} [content=''] The content for the message 59 * @property {MessageEmbed|Object} [embed] An embed for the message 60 * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) 61 * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content 62 * @property {DisableMentionType} [disableMentions=this.client.options.disableMentions] Whether or not all mentions or 63 * everyone/here mentions should be sanitized to prevent unexpected mentions 64 * @property {FileOptions[]|BufferResolvable[]} [files] Files to send with the message 65 * @property {string|boolean} [code] Language for optional codeblock formatting to apply 66 * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if 67 * it exceeds the character limit. If an object is provided, these are the options for splitting the message 68 * @property {UserResolvable} [reply] User to reply to (prefixes the message with a mention, except in DMs) 69 */ 70 71 /** 72 * Options provided to control parsing of mentions by Discord 73 * @typedef {Object} MessageMentionOptions 74 * @property {MessageMentionTypes[]} [parse] Types of mentions to be parsed 75 * @property {Snowflake[]} [users] Snowflakes of Users to be parsed as mentions 76 * @property {Snowflake[]} [roles] Snowflakes of Roles to be parsed as mentions 77 */ 78 79 /** 80 * Types of mentions to enable in MessageMentionOptions. 81 * - `roles` 82 * - `users` 83 * - `everyone` 84 * @typedef {string} MessageMentionTypes 85 */ 86 87 /** 88 * The type of mentions to disable. 89 * - `none` 90 * - `all` 91 * - `everyone` 92 * @typedef {string} DisableMentionType 93 */ 94 95 /** 96 * @typedef {Object} FileOptions 97 * @property {BufferResolvable} attachment File to attach 98 * @property {string} [name='file.jpg'] Filename of the attachment 99 */ 100 101 /** 102 * Options for splitting a message. 103 * @typedef {Object} SplitOptions 104 * @property {number} [maxLength=2000] Maximum character length per message piece 105 * @property {string} [char='\n'] Character to split the message with 106 * @property {string} [prepend=''] Text to prepend to every piece except the first 107 * @property {string} [append=''] Text to append to every piece except the last 108 */ 109 110 /** 111 * Sends a message to this channel. 112 * @param {StringResolvable|APIMessage} [content=''] The content to send 113 * @param {MessageOptions|MessageAdditions} [options={}] The options to provide 114 * @returns {Promise<Message|Message[]>} 115 * @example 116 * // Send a basic message 117 * channel.send('hello!') 118 * .then(message => console.log(`Sent message: ${message.content}`)) 119 * .catch(console.error); 120 * @example 121 * // Send a remote file 122 * channel.send({ 123 * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] 124 * }) 125 * .then(console.log) 126 * .catch(console.error); 127 * @example 128 * // Send a local file 129 * channel.send({ 130 * files: [{ 131 * attachment: 'entire/path/to/file.jpg', 132 * name: 'file.jpg' 133 * }] 134 * }) 135 * .then(console.log) 136 * .catch(console.error); 137 * @example 138 * // Send an embed with a local image inside 139 * channel.send('This is an embed', { 140 * embed: { 141 * thumbnail: { 142 * url: 'attachment://file.jpg' 143 * } 144 * }, 145 * files: [{ 146 * attachment: 'entire/path/to/file.jpg', 147 * name: 'file.jpg' 148 * }] 149 * }) 150 * .then(console.log) 151 * .catch(console.error); 152 */ 153 async send(content, options) { 154 const User = require('../User'); 155 const GuildMember = require('../GuildMember'); 156 157 if (this instanceof User || this instanceof GuildMember) { 158 return this.createDM().then(dm => dm.send(content, options)); 159 } 160 161 let apiMessage; 162 163 if (content instanceof APIMessage) { 164 apiMessage = content.resolveData(); 165 } else { 166 apiMessage = APIMessage.create(this, content, options).resolveData(); 167 if (Array.isArray(apiMessage.data.content)) { 168 return Promise.all(apiMessage.split().map(this.send.bind(this))); 169 } 170 } 171 172 const { data, files } = await apiMessage.resolveFiles(); 173 return this.client.api.channels[this.id].messages 174 .post({ data, files }) 175 .then(d => this.client.actions.MessageCreate.handle(d).message); 176 } 177 178 /** 179 * Starts a typing indicator in the channel. 180 * @param {number} [count=1] The number of times startTyping should be considered to have been called 181 * @returns {Promise} Resolves once the bot stops typing gracefully, or rejects when an error occurs 182 * @example 183 * // Start typing in a channel, or increase the typing count by one 184 * channel.startTyping(); 185 * @example 186 * // Start typing in a channel with a typing count of five, or set it to five 187 * channel.startTyping(5); 188 */ 189 startTyping(count) { 190 if (typeof count !== 'undefined' && count < 1) throw new RangeError('TYPING_COUNT'); 191 if (this.client.user._typing.has(this.id)) { 192 const entry = this.client.user._typing.get(this.id); 193 entry.count = count || entry.count + 1; 194 return entry.promise; 195 } 196 197 const entry = {}; 198 entry.promise = new Promise((resolve, reject) => { 199 const endpoint = this.client.api.channels[this.id].typing; 200 Object.assign(entry, { 201 count: count || 1, 202 interval: this.client.setInterval(() => { 203 endpoint.post().catch(error => { 204 this.client.clearInterval(entry.interval); 205 this.client.user._typing.delete(this.id); 206 reject(error); 207 }); 208 }, 9000), 209 resolve, 210 }); 211 endpoint.post().catch(error => { 212 this.client.clearInterval(entry.interval); 213 this.client.user._typing.delete(this.id); 214 reject(error); 215 }); 216 this.client.user._typing.set(this.id, entry); 217 }); 218 return entry.promise; 219 } 220 221 /** 222 * Stops the typing indicator in the channel. 223 * The indicator will only stop if this is called as many times as startTyping(). 224 * <info>It can take a few seconds for the client user to stop typing.</info> 225 * @param {boolean} [force=false] Whether or not to reset the call count and force the indicator to stop 226 * @example 227 * // Reduce the typing count by one and stop typing if it reached 0 228 * channel.stopTyping(); 229 * @example 230 * // Force typing to fully stop regardless of typing count 231 * channel.stopTyping(true); 232 */ 233 stopTyping(force = false) { 234 if (this.client.user._typing.has(this.id)) { 235 const entry = this.client.user._typing.get(this.id); 236 entry.count--; 237 if (entry.count <= 0 || force) { 238 this.client.clearInterval(entry.interval); 239 this.client.user._typing.delete(this.id); 240 entry.resolve(); 241 } 242 } 243 } 244 245 /** 246 * Whether or not the typing indicator is being shown in the channel 247 * @type {boolean} 248 * @readonly 249 */ 250 get typing() { 251 return this.client.user._typing.has(this.id); 252 } 253 254 /** 255 * Number of times `startTyping` has been called 256 * @type {number} 257 * @readonly 258 */ 259 get typingCount() { 260 if (this.client.user._typing.has(this.id)) return this.client.user._typing.get(this.id).count; 261 return 0; 262 } 263 264 /** 265 * Creates a Message Collector. 266 * @param {CollectorFilter} filter The filter to create the collector with 267 * @param {MessageCollectorOptions} [options={}] The options to pass to the collector 268 * @returns {MessageCollector} 269 * @example 270 * // Create a message collector 271 * const filter = m => m.content.includes('discord'); 272 * const collector = channel.createMessageCollector(filter, { time: 15000 }); 273 * collector.on('collect', m => console.log(`Collected ${m.content}`)); 274 * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); 275 */ 276 createMessageCollector(filter, options = {}) { 277 return new MessageCollector(this, filter, options); 278 } 279 280 /** 281 * An object containing the same properties as CollectorOptions, but a few more: 282 * @typedef {MessageCollectorOptions} AwaitMessagesOptions 283 * @property {string[]} [errors] Stop/end reasons that cause the promise to reject 284 */ 285 286 /** 287 * Similar to createMessageCollector but in promise form. 288 * Resolves with a collection of messages that pass the specified filter. 289 * @param {CollectorFilter} filter The filter function to use 290 * @param {AwaitMessagesOptions} [options={}] Optional options to pass to the internal collector 291 * @returns {Promise<Collection<Snowflake, Message>>} 292 * @example 293 * // Await !vote messages 294 * const filter = m => m.content.startsWith('!vote'); 295 * // Errors: ['time'] treats ending because of the time limit as an error 296 * channel.awaitMessages(filter, { max: 4, time: 60000, errors: ['time'] }) 297 * .then(collected => console.log(collected.size)) 298 * .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`)); 299 */ 300 awaitMessages(filter, options = {}) { 301 return new Promise((resolve, reject) => { 302 const collector = this.createMessageCollector(filter, options); 303 collector.once('end', (collection, reason) => { 304 if (options.errors && options.errors.includes(reason)) { 305 reject(collection); 306 } else { 307 resolve(collection); 308 } 309 }); 310 }); 311 } 312 313 /** 314 * Bulk deletes given messages that are newer than two weeks. 315 * @param {Collection<Snowflake, Message>|Message[]|Snowflake[]|number} messages 316 * Messages or number of messages to delete 317 * @param {boolean} [filterOld=false] Filter messages to remove those which are older than two weeks automatically 318 * @returns {Promise<Collection<Snowflake, Message>>} Deleted messages 319 * @example 320 * // Bulk delete messages 321 * channel.bulkDelete(5) 322 * .then(messages => console.log(`Bulk deleted ${messages.size} messages`)) 323 * .catch(console.error); 324 */ 325 async bulkDelete(messages, filterOld = false) { 326 if (Array.isArray(messages) || messages instanceof Collection) { 327 let messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id || m); 328 if (filterOld) { 329 messageIDs = messageIDs.filter(id => Date.now() - Snowflake.deconstruct(id).date.getTime() < 1209600000); 330 } 331 if (messageIDs.length === 0) return new Collection(); 332 if (messageIDs.length === 1) { 333 await this.client.api 334 .channels(this.id) 335 .messages(messageIDs[0]) 336 .delete(); 337 const message = this.client.actions.MessageDelete.getMessage( 338 { 339 message_id: messageIDs[0], 340 }, 341 this, 342 ); 343 return message ? new Collection([[message.id, message]]) : new Collection(); 344 } 345 await this.client.api.channels[this.id].messages['bulk-delete'].post({ data: { messages: messageIDs } }); 346 return messageIDs.reduce( 347 (col, id) => 348 col.set( 349 id, 350 this.client.actions.MessageDeleteBulk.getMessage( 351 { 352 message_id: id, 353 }, 354 this, 355 ), 356 ), 357 new Collection(), 358 ); 359 } 360 if (!isNaN(messages)) { 361 const msgs = await this.messages.fetch({ limit: messages }); 362 return this.bulkDelete(msgs, filterOld); 363 } 364 throw new TypeError('MESSAGE_BULK_DELETE_TYPE'); 365 } 366 367 static applyToClass(structure, full = false, ignore = []) { 368 const props = ['send']; 369 if (full) { 370 props.push( 371 'lastMessage', 372 'lastPinAt', 373 'bulkDelete', 374 'startTyping', 375 'stopTyping', 376 'typing', 377 'typingCount', 378 'createMessageCollector', 379 'awaitMessages', 380 ); 381 } 382 for (const prop of props) { 383 if (ignore.includes(prop)) continue; 384 Object.defineProperty( 385 structure.prototype, 386 prop, 387 Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop), 388 ); 389 } 390 } 391 } 392 393 module.exports = TextBasedChannel; 394 395 // Fixes Circular 396 const MessageManager = require('../../managers/MessageManager');