APIMessage.js (11551B)
1 'use strict'; 2 3 const MessageAttachment = require('./MessageAttachment'); 4 const MessageEmbed = require('./MessageEmbed'); 5 const { RangeError } = require('../errors'); 6 const { browser } = require('../util/Constants'); 7 const DataResolver = require('../util/DataResolver'); 8 const MessageFlags = require('../util/MessageFlags'); 9 const Util = require('../util/Util'); 10 11 /** 12 * Represents a message to be sent to the API. 13 */ 14 class APIMessage { 15 /** 16 * @param {MessageTarget} target - The target for this message to be sent to 17 * @param {MessageOptions|WebhookMessageOptions} options - Options passed in from send 18 */ 19 constructor(target, options) { 20 /** 21 * The target for this message to be sent to 22 * @type {MessageTarget} 23 */ 24 this.target = target; 25 26 /** 27 * Options passed in from send 28 * @type {MessageOptions|WebhookMessageOptions} 29 */ 30 this.options = options; 31 32 /** 33 * Data sendable to the API 34 * @type {?Object} 35 */ 36 this.data = null; 37 38 /** 39 * Files sendable to the API 40 * @type {?Object[]} 41 */ 42 this.files = null; 43 } 44 45 /** 46 * Whether or not the target is a webhook 47 * @type {boolean} 48 * @readonly 49 */ 50 get isWebhook() { 51 const Webhook = require('./Webhook'); 52 const WebhookClient = require('../client/WebhookClient'); 53 return this.target instanceof Webhook || this.target instanceof WebhookClient; 54 } 55 56 /** 57 * Whether or not the target is a user 58 * @type {boolean} 59 * @readonly 60 */ 61 get isUser() { 62 const User = require('./User'); 63 const GuildMember = require('./GuildMember'); 64 return this.target instanceof User || this.target instanceof GuildMember; 65 } 66 67 /** 68 * Whether or not the target is a message 69 * @type {boolean} 70 * @readonly 71 */ 72 get isMessage() { 73 const Message = require('./Message'); 74 return this.target instanceof Message; 75 } 76 77 /** 78 * Makes the content of this message. 79 * @returns {?(string|string[])} 80 */ 81 makeContent() { 82 const GuildMember = require('./GuildMember'); 83 84 let content; 85 if (this.options.content === null) { 86 content = ''; 87 } else if (typeof this.options.content !== 'undefined') { 88 content = Util.resolveString(this.options.content); 89 } 90 91 const disableMentions = 92 typeof this.options.disableMentions === 'undefined' 93 ? this.target.client.options.disableMentions 94 : this.options.disableMentions; 95 if (disableMentions === 'all') { 96 content = Util.removeMentions(content || ''); 97 } else if (disableMentions === 'everyone') { 98 content = (content || '').replace(/@([^<>@ ]*)/gmsu, (match, target) => { 99 if (target.match(/^[&!]?\d+$/)) { 100 return `@${target}`; 101 } else { 102 return `@\u200b${target}`; 103 } 104 }); 105 } 106 107 const isSplit = typeof this.options.split !== 'undefined' && this.options.split !== false; 108 const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false; 109 const splitOptions = isSplit ? { ...this.options.split } : undefined; 110 111 let mentionPart = ''; 112 if (this.options.reply && !this.isUser && this.target.type !== 'dm') { 113 const id = this.target.client.users.resolveID(this.options.reply); 114 mentionPart = `<@${this.options.reply instanceof GuildMember && this.options.reply.nickname ? '!' : ''}${id}>, `; 115 if (isSplit) { 116 splitOptions.prepend = `${mentionPart}${splitOptions.prepend || ''}`; 117 } 118 } 119 120 if (content || mentionPart) { 121 if (isCode) { 122 const codeName = typeof this.options.code === 'string' ? this.options.code : ''; 123 content = `${mentionPart}\`\`\`${codeName}\n${Util.cleanCodeBlockContent(content || '')}\n\`\`\``; 124 if (isSplit) { 125 splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`; 126 splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`; 127 } 128 } else if (mentionPart) { 129 content = `${mentionPart}${content || ''}`; 130 } 131 132 if (isSplit) { 133 content = Util.splitMessage(content || '', splitOptions); 134 } 135 } 136 137 return content; 138 } 139 140 /** 141 * Resolves data. 142 * @returns {APIMessage} 143 */ 144 resolveData() { 145 if (this.data) return this; 146 147 const content = this.makeContent(); 148 const tts = Boolean(this.options.tts); 149 150 let nonce; 151 if (typeof this.options.nonce !== 'undefined') { 152 nonce = parseInt(this.options.nonce); 153 if (isNaN(nonce) || nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE'); 154 } 155 156 const embedLikes = []; 157 if (this.isWebhook) { 158 if (this.options.embeds) { 159 embedLikes.push(...this.options.embeds); 160 } 161 } else if (this.options.embed) { 162 embedLikes.push(this.options.embed); 163 } 164 const embeds = embedLikes.map(e => new MessageEmbed(e).toJSON()); 165 166 let username; 167 let avatarURL; 168 if (this.isWebhook) { 169 username = this.options.username || this.target.name; 170 if (this.options.avatarURL) avatarURL = this.options.avatarURL; 171 } 172 173 let flags; 174 if (this.isMessage) { 175 // eslint-disable-next-line eqeqeq 176 flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags.bitfield; 177 } 178 179 const allowedMentions = 180 typeof this.options.allowedMentions === 'undefined' 181 ? this.target.client.options.allowedMentions 182 : this.options.allowedMentions; 183 184 this.data = { 185 content, 186 tts, 187 nonce, 188 embed: this.options.embed === null ? null : embeds[0], 189 embeds, 190 username, 191 avatar_url: avatarURL, 192 allowed_mentions: allowedMentions, 193 flags, 194 }; 195 return this; 196 } 197 198 /** 199 * Resolves files. 200 * @returns {Promise<APIMessage>} 201 */ 202 async resolveFiles() { 203 if (this.files) return this; 204 205 const embedLikes = []; 206 if (this.isWebhook) { 207 if (this.options.embeds) { 208 embedLikes.push(...this.options.embeds); 209 } 210 } else if (this.options.embed) { 211 embedLikes.push(this.options.embed); 212 } 213 214 const fileLikes = []; 215 if (this.options.files) { 216 fileLikes.push(...this.options.files); 217 } 218 for (const embed of embedLikes) { 219 if (embed.files) { 220 fileLikes.push(...embed.files); 221 } 222 } 223 224 this.files = await Promise.all(fileLikes.map(f => this.constructor.resolveFile(f))); 225 return this; 226 } 227 228 /** 229 * Converts this APIMessage into an array of APIMessages for each split content 230 * @returns {APIMessage[]} 231 */ 232 split() { 233 if (!this.data) this.resolveData(); 234 235 if (!Array.isArray(this.data.content)) return [this]; 236 237 const apiMessages = []; 238 239 for (let i = 0; i < this.data.content.length; i++) { 240 let data; 241 let opt; 242 243 if (i === this.data.content.length - 1) { 244 data = { ...this.data, content: this.data.content[i] }; 245 opt = { ...this.options, content: this.data.content[i] }; 246 } else { 247 data = { content: this.data.content[i], tts: this.data.tts }; 248 opt = { content: this.data.content[i], tts: this.data.tts }; 249 } 250 251 const apiMessage = new APIMessage(this.target, opt); 252 apiMessage.data = data; 253 apiMessages.push(apiMessage); 254 } 255 256 return apiMessages; 257 } 258 259 /** 260 * Resolves a single file into an object sendable to the API. 261 * @param {BufferResolvable|Stream|FileOptions|MessageAttachment} fileLike Something that could be resolved to a file 262 * @returns {Object} 263 */ 264 static async resolveFile(fileLike) { 265 let attachment; 266 let name; 267 268 const findName = thing => { 269 if (typeof thing === 'string') { 270 return Util.basename(thing); 271 } 272 273 if (thing.path) { 274 return Util.basename(thing.path); 275 } 276 277 return 'file.jpg'; 278 }; 279 280 const ownAttachment = 281 typeof fileLike === 'string' || 282 fileLike instanceof (browser ? ArrayBuffer : Buffer) || 283 typeof fileLike.pipe === 'function'; 284 if (ownAttachment) { 285 attachment = fileLike; 286 name = findName(attachment); 287 } else { 288 attachment = fileLike.attachment; 289 name = fileLike.name || findName(attachment); 290 } 291 292 const resource = await DataResolver.resolveFile(attachment); 293 return { attachment, name, file: resource }; 294 } 295 296 /** 297 * Partitions embeds and attachments. 298 * @param {Array<MessageEmbed|MessageAttachment>} items Items to partition 299 * @returns {Array<MessageEmbed[], MessageAttachment[]>} 300 */ 301 static partitionMessageAdditions(items) { 302 const embeds = []; 303 const files = []; 304 for (const item of items) { 305 if (item instanceof MessageEmbed) { 306 embeds.push(item); 307 } else if (item instanceof MessageAttachment) { 308 files.push(item); 309 } 310 } 311 312 return [embeds, files]; 313 } 314 315 /** 316 * Transforms the user-level arguments into a final options object. Passing a transformed options object alone into 317 * this method will keep it the same, allowing for the reuse of the final options object. 318 * @param {StringResolvable} [content] Content to send 319 * @param {MessageOptions|WebhookMessageOptions|MessageAdditions} [options={}] Options to use 320 * @param {MessageOptions|WebhookMessageOptions} [extra={}] Extra options to add onto transformed options 321 * @param {boolean} [isWebhook=false] Whether or not to use WebhookMessageOptions as the result 322 * @returns {MessageOptions|WebhookMessageOptions} 323 */ 324 static transformOptions(content, options, extra = {}, isWebhook = false) { 325 if (!options && typeof content === 'object' && !Array.isArray(content)) { 326 options = content; 327 content = undefined; 328 } 329 330 if (!options) { 331 options = {}; 332 } else if (options instanceof MessageEmbed) { 333 return isWebhook ? { content, embeds: [options], ...extra } : { content, embed: options, ...extra }; 334 } else if (options instanceof MessageAttachment) { 335 return { content, files: [options], ...extra }; 336 } 337 338 if (Array.isArray(options)) { 339 const [embeds, files] = this.partitionMessageAdditions(options); 340 return isWebhook ? { content, embeds, files, ...extra } : { content, embed: embeds[0], files, ...extra }; 341 } else if (Array.isArray(content)) { 342 const [embeds, files] = this.partitionMessageAdditions(content); 343 if (embeds.length || files.length) { 344 return isWebhook ? { embeds, files, ...extra } : { embed: embeds[0], files, ...extra }; 345 } 346 } 347 348 return { content, ...options, ...extra }; 349 } 350 351 /** 352 * Creates an `APIMessage` from user-level arguments. 353 * @param {MessageTarget} target Target to send to 354 * @param {StringResolvable} [content] Content to send 355 * @param {MessageOptions|WebhookMessageOptions|MessageAdditions} [options={}] Options to use 356 * @param {MessageOptions|WebhookMessageOptions} [extra={}] - Extra options to add onto transformed options 357 * @returns {MessageOptions|WebhookMessageOptions} 358 */ 359 static create(target, content, options, extra = {}) { 360 const Webhook = require('./Webhook'); 361 const WebhookClient = require('../client/WebhookClient'); 362 363 const isWebhook = target instanceof Webhook || target instanceof WebhookClient; 364 const transformed = this.transformOptions(content, options, extra, isWebhook); 365 return new this(target, transformed); 366 } 367 } 368 369 module.exports = APIMessage; 370 371 /** 372 * A target for a message. 373 * @typedef {TextChannel|DMChannel|User|GuildMember|Webhook|WebhookClient} MessageTarget 374 */ 375 376 /** 377 * Additional items that can be sent with a message. 378 * @typedef {MessageEmbed|MessageAttachment|Array<MessageEmbed|MessageAttachment>} MessageAdditions 379 */