buddy

node MVC discord bot
Log | Files | Refs | README

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  */