Message.js (18950B)
1 'use strict'; 2 3 const APIMessage = require('./APIMessage'); 4 const Base = require('./Base'); 5 const ClientApplication = require('./ClientApplication'); 6 const MessageAttachment = require('./MessageAttachment'); 7 const Embed = require('./MessageEmbed'); 8 const Mentions = require('./MessageMentions'); 9 const ReactionCollector = require('./ReactionCollector'); 10 const { Error, TypeError } = require('../errors'); 11 const ReactionManager = require('../managers/ReactionManager'); 12 const Collection = require('../util/Collection'); 13 const { MessageTypes } = require('../util/Constants'); 14 const MessageFlags = require('../util/MessageFlags'); 15 const Permissions = require('../util/Permissions'); 16 const Util = require('../util/Util'); 17 18 /** 19 * Represents a message on Discord. 20 * @extends {Base} 21 */ 22 class Message extends Base { 23 /** 24 * @param {Client} client The instantiating client 25 * @param {Object} data The data for the message 26 * @param {TextChannel|DMChannel} channel The channel the message was sent in 27 */ 28 constructor(client, data, channel) { 29 super(client); 30 31 /** 32 * The channel that the message was sent in 33 * @type {TextChannel|DMChannel} 34 */ 35 this.channel = channel; 36 37 /** 38 * Whether this message has been deleted 39 * @type {boolean} 40 */ 41 this.deleted = false; 42 43 if (data) this._patch(data); 44 } 45 46 _patch(data) { 47 /** 48 * The ID of the message 49 * @type {Snowflake} 50 */ 51 this.id = data.id; 52 53 /** 54 * The type of the message 55 * @type {MessageType} 56 */ 57 this.type = MessageTypes[data.type]; 58 59 /** 60 * The content of the message 61 * @type {string} 62 */ 63 this.content = data.content; 64 65 /** 66 * The author of the message 67 * @type {?User} 68 */ 69 this.author = data.author ? this.client.users.add(data.author, !data.webhook_id) : null; 70 71 /** 72 * Whether or not this message is pinned 73 * @type {boolean} 74 */ 75 this.pinned = data.pinned; 76 77 /** 78 * Whether or not the message was Text-To-Speech 79 * @type {boolean} 80 */ 81 this.tts = data.tts; 82 83 /** 84 * A random number or string used for checking message delivery 85 * <warn>This is only received after the message was sent successfully, and 86 * lost if re-fetched</warn> 87 * @type {?string} 88 */ 89 this.nonce = data.nonce; 90 91 /** 92 * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications) 93 * @type {boolean} 94 */ 95 this.system = data.type !== 0; 96 97 /** 98 * A list of embeds in the message - e.g. YouTube Player 99 * @type {MessageEmbed[]} 100 */ 101 this.embeds = (data.embeds || []).map(e => new Embed(e, true)); 102 103 /** 104 * A collection of attachments in the message - e.g. Pictures - mapped by their ID 105 * @type {Collection<Snowflake, MessageAttachment>} 106 */ 107 this.attachments = new Collection(); 108 if (data.attachments) { 109 for (const attachment of data.attachments) { 110 this.attachments.set(attachment.id, new MessageAttachment(attachment.url, attachment.filename, attachment)); 111 } 112 } 113 114 /** 115 * The timestamp the message was sent at 116 * @type {number} 117 */ 118 this.createdTimestamp = new Date(data.timestamp).getTime(); 119 120 /** 121 * The timestamp the message was last edited at (if applicable) 122 * @type {?number} 123 */ 124 this.editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null; 125 126 /** 127 * A manager of the reactions belonging to this message 128 * @type {ReactionManager} 129 */ 130 this.reactions = new ReactionManager(this); 131 if (data.reactions && data.reactions.length > 0) { 132 for (const reaction of data.reactions) { 133 this.reactions.add(reaction); 134 } 135 } 136 137 /** 138 * All valid mentions that the message contains 139 * @type {MessageMentions} 140 */ 141 this.mentions = new Mentions(this, data.mentions, data.mention_roles, data.mention_everyone, data.mention_channels); 142 143 /** 144 * ID of the webhook that sent the message, if applicable 145 * @type {?Snowflake} 146 */ 147 this.webhookID = data.webhook_id || null; 148 149 /** 150 * Supplemental application information for group activities 151 * @type {?ClientApplication} 152 */ 153 this.application = data.application ? new ClientApplication(this.client, data.application) : null; 154 155 /** 156 * Group activity 157 * @type {?MessageActivity} 158 */ 159 this.activity = data.activity 160 ? { 161 partyID: data.activity.party_id, 162 type: data.activity.type, 163 } 164 : null; 165 166 /** 167 * The previous versions of the message, sorted with the most recent first 168 * @type {Message[]} 169 * @private 170 */ 171 this._edits = []; 172 173 if (this.member && data.member) { 174 this.member._patch(data.member); 175 } else if (data.member && this.guild && this.author) { 176 this.guild.members.add(Object.assign(data.member, { user: this.author })); 177 } 178 179 /** 180 * Flags that are applied to the message 181 * @type {Readonly<MessageFlags>} 182 */ 183 this.flags = new MessageFlags(data.flags).freeze(); 184 185 /** 186 * Reference data sent in a crossposted message. 187 * @typedef {Object} MessageReference 188 * @property {string} channelID ID of the channel the message was crossposted from 189 * @property {?string} guildID ID of the guild the message was crossposted from 190 * @property {?string} messageID ID of the message that was crossposted 191 */ 192 193 /** 194 * Message reference data 195 * @type {?MessageReference} 196 */ 197 this.reference = data.message_reference 198 ? { 199 channelID: data.message_reference.channel_id, 200 guildID: data.message_reference.guild_id, 201 messageID: data.message_reference.message_id, 202 } 203 : null; 204 } 205 206 /** 207 * Whether or not this message is a partial 208 * @type {boolean} 209 * @readonly 210 */ 211 get partial() { 212 return typeof this.content !== 'string' || !this.author; 213 } 214 215 /** 216 * Updates the message. 217 * @param {Object} data Raw Discord message update data 218 * @private 219 */ 220 patch(data) { 221 const clone = this._clone(); 222 this._edits.unshift(clone); 223 224 if ('edited_timestamp' in data) this.editedTimestamp = new Date(data.edited_timestamp).getTime(); 225 if ('content' in data) this.content = data.content; 226 if ('pinned' in data) this.pinned = data.pinned; 227 if ('tts' in data) this.tts = data.tts; 228 if ('embeds' in data) this.embeds = data.embeds.map(e => new Embed(e, true)); 229 else this.embeds = this.embeds.slice(); 230 231 if ('attachments' in data) { 232 this.attachments = new Collection(); 233 for (const attachment of data.attachments) { 234 this.attachments.set(attachment.id, new MessageAttachment(attachment.url, attachment.filename, attachment)); 235 } 236 } else { 237 this.attachments = new Collection(this.attachments); 238 } 239 240 this.mentions = new Mentions( 241 this, 242 'mentions' in data ? data.mentions : this.mentions.users, 243 'mention_roles' in data ? data.mention_roles : this.mentions.roles, 244 'mention_everyone' in data ? data.mention_everyone : this.mentions.everyone, 245 'mention_channels' in data ? data.mention_channels : this.mentions.crosspostedChannels, 246 ); 247 248 this.flags = new MessageFlags('flags' in data ? data.flags : 0).freeze(); 249 } 250 251 /** 252 * Represents the author of the message as a guild member. 253 * Only available if the message comes from a guild where the author is still a member 254 * @type {?GuildMember} 255 * @readonly 256 */ 257 get member() { 258 return this.guild ? this.guild.member(this.author) || null : null; 259 } 260 261 /** 262 * The time the message was sent at 263 * @type {Date} 264 * @readonly 265 */ 266 get createdAt() { 267 return new Date(this.createdTimestamp); 268 } 269 270 /** 271 * The time the message was last edited at (if applicable) 272 * @type {?Date} 273 * @readonly 274 */ 275 get editedAt() { 276 return this.editedTimestamp ? new Date(this.editedTimestamp) : null; 277 } 278 279 /** 280 * The guild the message was sent in (if in a guild channel) 281 * @type {?Guild} 282 * @readonly 283 */ 284 get guild() { 285 return this.channel.guild || null; 286 } 287 288 /** 289 * The url to jump to this message 290 * @type {string} 291 * @readonly 292 */ 293 get url() { 294 return `https://discordapp.com/channels/${this.guild ? this.guild.id : '@me'}/${this.channel.id}/${this.id}`; 295 } 296 297 /** 298 * The message contents with all mentions replaced by the equivalent text. 299 * If mentions cannot be resolved to a name, the relevant mention in the message content will not be converted. 300 * @type {string} 301 * @readonly 302 */ 303 get cleanContent() { 304 // eslint-disable-next-line eqeqeq 305 return this.content != null ? Util.cleanContent(this.content, this) : null; 306 } 307 308 /** 309 * Creates a reaction collector. 310 * @param {CollectorFilter} filter The filter to apply 311 * @param {ReactionCollectorOptions} [options={}] Options to send to the collector 312 * @returns {ReactionCollector} 313 * @example 314 * // Create a reaction collector 315 * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someID'; 316 * const collector = message.createReactionCollector(filter, { time: 15000 }); 317 * collector.on('collect', r => console.log(`Collected ${r.emoji.name}`)); 318 * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); 319 */ 320 createReactionCollector(filter, options = {}) { 321 return new ReactionCollector(this, filter, options); 322 } 323 324 /** 325 * An object containing the same properties as CollectorOptions, but a few more: 326 * @typedef {ReactionCollectorOptions} AwaitReactionsOptions 327 * @property {string[]} [errors] Stop/end reasons that cause the promise to reject 328 */ 329 330 /** 331 * Similar to createReactionCollector but in promise form. 332 * Resolves with a collection of reactions that pass the specified filter. 333 * @param {CollectorFilter} filter The filter function to use 334 * @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector 335 * @returns {Promise<Collection<string, MessageReaction>>} 336 * @example 337 * // Create a reaction collector 338 * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someID' 339 * message.awaitReactions(filter, { time: 15000 }) 340 * .then(collected => console.log(`Collected ${collected.size} reactions`)) 341 * .catch(console.error); 342 */ 343 awaitReactions(filter, options = {}) { 344 return new Promise((resolve, reject) => { 345 const collector = this.createReactionCollector(filter, options); 346 collector.once('end', (reactions, reason) => { 347 if (options.errors && options.errors.includes(reason)) reject(reactions); 348 else resolve(reactions); 349 }); 350 }); 351 } 352 353 /** 354 * An array of cached versions of the message, including the current version 355 * Sorted from latest (first) to oldest (last) 356 * @type {Message[]} 357 * @readonly 358 */ 359 get edits() { 360 const copy = this._edits.slice(); 361 copy.unshift(this); 362 return copy; 363 } 364 365 /** 366 * Whether the message is editable by the client user 367 * @type {boolean} 368 * @readonly 369 */ 370 get editable() { 371 return this.author.id === this.client.user.id; 372 } 373 374 /** 375 * Whether the message is deletable by the client user 376 * @type {boolean} 377 * @readonly 378 */ 379 get deletable() { 380 return ( 381 !this.deleted && 382 (this.author.id === this.client.user.id || 383 (this.guild && this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES, false))) 384 ); 385 } 386 387 /** 388 * Whether the message is pinnable by the client user 389 * @type {boolean} 390 * @readonly 391 */ 392 get pinnable() { 393 return ( 394 this.type === 'DEFAULT' && 395 (!this.guild || this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES, false)) 396 ); 397 } 398 399 /** 400 * Options that can be passed into editMessage. 401 * @typedef {Object} MessageEditOptions 402 * @property {string} [content] Content to be edited 403 * @property {Object} [embed] An embed to be added/edited 404 * @property {string|boolean} [code] Language for optional codeblock formatting to apply 405 * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content 406 */ 407 408 /** 409 * Edits the content of the message. 410 * @param {StringResolvable|APIMessage} [content] The new content for the message 411 * @param {MessageEditOptions|MessageEmbed} [options] The options to provide 412 * @returns {Promise<Message>} 413 * @example 414 * // Update the content of a message 415 * message.edit('This is my new content!') 416 * .then(msg => console.log(`Updated the content of a message to ${msg.content}`)) 417 * .catch(console.error); 418 */ 419 edit(content, options) { 420 const { data } = 421 content instanceof APIMessage ? content.resolveData() : APIMessage.create(this, content, options).resolveData(); 422 return this.client.api.channels[this.channel.id].messages[this.id].patch({ data }).then(d => { 423 const clone = this._clone(); 424 clone._patch(d); 425 return clone; 426 }); 427 } 428 429 /** 430 * Pins this message to the channel's pinned messages. 431 * @returns {Promise<Message>} 432 */ 433 pin() { 434 return this.client.api 435 .channels(this.channel.id) 436 .pins(this.id) 437 .put() 438 .then(() => this); 439 } 440 441 /** 442 * Unpins this message from the channel's pinned messages. 443 * @returns {Promise<Message>} 444 */ 445 unpin() { 446 return this.client.api 447 .channels(this.channel.id) 448 .pins(this.id) 449 .delete() 450 .then(() => this); 451 } 452 453 /** 454 * Adds a reaction to the message. 455 * @param {EmojiIdentifierResolvable} emoji The emoji to react with 456 * @returns {Promise<MessageReaction>} 457 * @example 458 * // React to a message with a unicode emoji 459 * message.react('🤔') 460 * .then(console.log) 461 * .catch(console.error); 462 * @example 463 * // React to a message with a custom emoji 464 * message.react(message.guild.emojis.cache.get('123456789012345678')) 465 * .then(console.log) 466 * .catch(console.error); 467 */ 468 react(emoji) { 469 emoji = this.client.emojis.resolveIdentifier(emoji); 470 if (!emoji) throw new TypeError('EMOJI_TYPE'); 471 472 return this.client.api 473 .channels(this.channel.id) 474 .messages(this.id) 475 .reactions(emoji, '@me') 476 .put() 477 .then( 478 () => 479 this.client.actions.MessageReactionAdd.handle({ 480 user: this.client.user, 481 channel: this.channel, 482 message: this, 483 emoji: Util.parseEmoji(emoji), 484 }).reaction, 485 ); 486 } 487 488 /** 489 * Deletes the message. 490 * @param {Object} [options] Options 491 * @param {number} [options.timeout=0] How long to wait to delete the message in milliseconds 492 * @param {string} [options.reason] Reason for deleting this message, if it does not belong to the client user 493 * @returns {Promise<Message>} 494 * @example 495 * // Delete a message 496 * message.delete() 497 * .then(msg => console.log(`Deleted message from ${msg.author.username}`)) 498 * .catch(console.error); 499 */ 500 delete(options = {}) { 501 if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true); 502 const { timeout = 0, reason } = options; 503 if (timeout <= 0) { 504 return this.channel.messages.delete(this.id, reason).then(() => this); 505 } else { 506 return new Promise(resolve => { 507 this.client.setTimeout(() => { 508 resolve(this.delete({ reason })); 509 }, timeout); 510 }); 511 } 512 } 513 514 /** 515 * Replies to the message. 516 * @param {StringResolvable|APIMessage} [content=''] The content for the message 517 * @param {MessageOptions|MessageAdditions} [options={}] The options to provide 518 * @returns {Promise<Message|Message[]>} 519 * @example 520 * // Reply to a message 521 * message.reply('Hey, I\'m a reply!') 522 * .then(() => console.log(`Sent a reply to ${message.author.username}`)) 523 * .catch(console.error); 524 */ 525 reply(content, options) { 526 return this.channel.send( 527 content instanceof APIMessage 528 ? content 529 : APIMessage.transformOptions(content, options, { reply: this.member || this.author }), 530 ); 531 } 532 533 /** 534 * Fetch this message. 535 * @returns {Promise<Message>} 536 */ 537 fetch() { 538 return this.channel.messages.fetch(this.id, true); 539 } 540 541 /** 542 * Fetches the webhook used to create this message. 543 * @returns {Promise<?Webhook>} 544 */ 545 fetchWebhook() { 546 if (!this.webhookID) return Promise.reject(new Error('WEBHOOK_MESSAGE')); 547 return this.client.fetchWebhook(this.webhookID); 548 } 549 550 /** 551 * Suppresses or unsuppresses embeds on a message 552 * @param {boolean} [suppress=true] If the embeds should be suppressed or not 553 * @returns {Promise<Message>} 554 */ 555 suppressEmbeds(suppress = true) { 556 const flags = new MessageFlags(this.flags.bitfield); 557 558 if (suppress) { 559 flags.add(MessageFlags.FLAGS.SUPPRESS_EMBEDS); 560 } else { 561 flags.remove(MessageFlags.FLAGS.SUPPRESS_EMBEDS); 562 } 563 564 return this.edit({ flags }); 565 } 566 567 /** 568 * Used mainly internally. Whether two messages are identical in properties. If you want to compare messages 569 * without checking all the properties, use `message.id === message2.id`, which is much more efficient. This 570 * method allows you to see if there are differences in content, embeds, attachments, nonce and tts properties. 571 * @param {Message} message The message to compare it to 572 * @param {Object} rawData Raw data passed through the WebSocket about this message 573 * @returns {boolean} 574 */ 575 equals(message, rawData) { 576 if (!message) return false; 577 const embedUpdate = !message.author && !message.attachments; 578 if (embedUpdate) return this.id === message.id && this.embeds.length === message.embeds.length; 579 580 let equal = 581 this.id === message.id && 582 this.author.id === message.author.id && 583 this.content === message.content && 584 this.tts === message.tts && 585 this.nonce === message.nonce && 586 this.embeds.length === message.embeds.length && 587 this.attachments.length === message.attachments.length; 588 589 if (equal && rawData) { 590 equal = 591 this.mentions.everyone === message.mentions.everyone && 592 this.createdTimestamp === new Date(rawData.timestamp).getTime() && 593 this.editedTimestamp === new Date(rawData.edited_timestamp).getTime(); 594 } 595 596 return equal; 597 } 598 599 /** 600 * When concatenated with a string, this automatically concatenates the message's content instead of the object. 601 * @returns {string} 602 * @example 603 * // Logs: Message: This is a message! 604 * console.log(`Message: ${message}`); 605 */ 606 toString() { 607 return this.content; 608 } 609 610 toJSON() { 611 return super.toJSON({ 612 channel: 'channelID', 613 author: 'authorID', 614 application: 'applicationID', 615 guild: 'guildID', 616 cleanContent: true, 617 member: false, 618 reactions: false, 619 }); 620 } 621 } 622 623 module.exports = Message;