diff --git a/src/index.js b/src/index.js index fd2239ade..1c9f068ce 100644 --- a/src/index.js +++ b/src/index.js @@ -51,6 +51,7 @@ module.exports = { // Structures Base: require('./structures/Base'), Activity: require('./structures/Presence').Activity, + APIMessage: require('./structures/APIMessage'), CategoryChannel: require('./structures/CategoryChannel'), Channel: require('./structures/Channel'), ClientApplication: require('./structures/ClientApplication'), diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js new file mode 100644 index 000000000..77eb4234b --- /dev/null +++ b/src/structures/APIMessage.js @@ -0,0 +1,292 @@ +const DataResolver = require('../util/DataResolver'); +const MessageEmbed = require('./MessageEmbed'); +const MessageAttachment = require('./MessageAttachment'); +const { browser } = require('../util/Constants'); +const Util = require('../util/Util'); +const { RangeError } = require('../errors'); + +/** + * Represents a message to be sent to the API. + */ +class APIMessage { + /** + * @param {MessageTarget} target - The target for this message to be sent to + * @param {MessageOptions|WebhookMessageOptions} options - Options passed in from send + */ + constructor(target, options) { + /** + * The target for this message to be sent to + * @type {MessageTarget} + */ + this.target = target; + + /** + * Options passed in from send + * @type {MessageOptions|WebhookMessageOptions} + */ + this.options = options; + } + + /** + * Whether or not the target is a webhook + * @type {boolean} + * @readonly + */ + get isWebhook() { + const Webhook = require('./Webhook'); + const WebhookClient = require('../client/WebhookClient'); + return this.target instanceof Webhook || this.target instanceof WebhookClient; + } + + /** + * Whether or not the target is a user + * @type {boolean} + * @readonly + */ + get isUser() { + const User = require('./User'); + const GuildMember = require('./GuildMember'); + return this.target instanceof User || this.target instanceof GuildMember; + } + + /** + * Makes the content of this message. + * @returns {string|string[]} + */ + makeContent() { // eslint-disable-line complexity + const GuildMember = require('./GuildMember'); + + // eslint-disable-next-line eqeqeq + let content = Util.resolveString(this.options.content == null ? '' : this.options.content); + const isSplit = typeof this.options.split !== 'undefined' && this.options.split !== false; + const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false; + const splitOptions = isSplit ? { ...this.options.split } : undefined; + + let mentionPart = ''; + if (this.options.reply && !this.isUser && this.target.type !== 'dm') { + const id = this.target.client.users.resolveID(this.options.reply); + mentionPart = `<@${this.options.reply instanceof GuildMember && this.options.reply.nickname ? '!' : ''}${id}>, `; + if (isSplit) { + splitOptions.prepend = `${mentionPart}${splitOptions.prepend || ''}`; + } + } + + if (content || mentionPart) { + if (isCode) { + const codeName = typeof this.options.code === 'string' ? this.options.code : ''; + content = `${mentionPart}\`\`\`${codeName}\n${Util.escapeMarkdown(content, true)}\n\`\`\``; + if (isSplit) { + splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`; + splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`; + } + } else if (mentionPart) { + content = `${mentionPart}${content}`; + } + + const disableEveryone = typeof this.options.disableEveryone === 'undefined' ? + this.target.client.options.disableEveryone : + this.options.disableEveryone; + if (disableEveryone) { + content = content.replace(/@(everyone|here)/g, '@\u200b$1'); + } + + if (isSplit) { + content = Util.splitMessage(content, splitOptions); + } + } + + return content; + } + + /** + * Resolves data. + * @returns {Object} + */ + resolveData() { + const content = this.makeContent(); + const tts = Boolean(this.options.tts); + let nonce; + if (typeof this.options.nonce !== 'undefined') { + nonce = parseInt(this.options.nonce); + if (isNaN(nonce) || nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE'); + } + + const embedLikes = []; + if (this.isWebhook) { + if (this.options.embeds) { + embedLikes.push(...this.options.embeds); + } + } else if (this.options.embed) { + embedLikes.push(this.options.embed); + } + const embeds = embedLikes.map(e => new MessageEmbed(e)._apiTransform()); + + let username; + let avatarURL; + if (this.isWebhook) { + username = this.options.username || this.target.name; + if (this.options.avatarURL) avatarURL = this.options.avatarURL; + } + + return { + content, + tts, + nonce, + embed: this.options.embed === null ? null : embeds[0], + embeds, + username, + avatar_url: avatarURL, + }; + } + + /** + * Resolves files. + * @returns {Promise} + */ + resolveFiles() { + const embedLikes = []; + if (this.isWebhook) { + if (this.options.embeds) { + embedLikes.push(...this.options.embeds); + } + } else if (this.options.embed) { + embedLikes.push(this.options.embed); + } + + const fileLikes = []; + if (this.options.files) { + fileLikes.push(...this.options.files); + } + for (const embed of embedLikes) { + if (embed.files) { + fileLikes.push(...embed.files); + } + } + + return Promise.all(fileLikes.map(f => this.constructor.resolveFile(f))); + } + + /** + * Resolves a single file into an object sendable to the API. + * @param {BufferResolvable|Stream|FileOptions|MessageAttachment} fileLike Something that could be resolved to a file + * @returns {Object} + */ + static async resolveFile(fileLike) { + let attachment; + let name; + + const findName = thing => { + if (typeof thing === 'string') { + return Util.basename(thing); + } + + if (thing.path) { + return Util.basename(thing.path); + } + + return 'file.jpg'; + }; + + const ownAttachment = typeof fileLike === 'string' || + fileLike instanceof (browser ? ArrayBuffer : Buffer) || + typeof fileLike.pipe === 'function'; + if (ownAttachment) { + attachment = fileLike; + name = findName(attachment); + } else { + attachment = fileLike.attachment; + name = fileLike.name || findName(attachment); + } + + const resource = await DataResolver.resolveFile(attachment); + return { attachment, name, file: resource }; + } + + /** + * Partitions embeds and attachments. + * @param {Array} items Items to partition + * @returns {Array} + */ + static partitionMessageAdditions(items) { + const embeds = []; + const files = []; + for (const item of items) { + if (item instanceof MessageEmbed) { + embeds.push(item); + } else if (item instanceof MessageAttachment) { + files.push(item); + } + } + + return [embeds, files]; + } + + /** + * Transforms the user-level arguments into a final options object. Passing a transformed options object alone into + * this method will keep it the same, allowing for the reuse of the final options object. + * @param {StringResolvable} [content=''] Content to send + * @param {MessageOptions|WebhookMessageOptions|MessageAdditions} [options={}] Options to use + * @param {MessageOptions|WebhookMessageOptions} [extra={}] Extra options to add onto transformed options + * @param {boolean} [isWebhook=false] Whether or not to use WebhookMessageOptions as the result + * @returns {MessageOptions|WebhookMessageOptions} + */ + static transformOptions(content, options, extra = {}, isWebhook = false) { + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } + + if (!options) { + options = {}; + } + + if (options instanceof MessageEmbed) { + return isWebhook ? { content, embeds: [options], ...extra } : { content, embed: options, ...extra }; + } + + if (options instanceof MessageAttachment) { + return { content, files: [options], ...extra }; + } + + if (options instanceof Array) { + const [embeds, files] = this.partitionMessageAdditions(options); + return isWebhook ? { content, embeds, files, ...extra } : { content, embed: embeds[0], files, ...extra }; + } else if (content instanceof Array) { + const [embeds, files] = this.partitionMessageAdditions(content); + if (embeds.length || files.length) { + return isWebhook ? { embeds, files, ...extra } : { embed: embeds[0], files, ...extra }; + } + } + + return { content, ...options, ...extra }; + } + + /** + * Creates an `APIMessage` from user-level arguments. + * @param {MessageTarget} target Target to send to + * @param {StringResolvable} [content=''] Content to send + * @param {MessageOptions|WebhookMessageOptions|MessageAdditions} [options={}] Options to use + * @param {MessageOptions|WebhookMessageOptions} [extra={}] - Extra options to add onto transformed options + * @returns {MessageOptions|WebhookMessageOptions} + */ + static create(target, content, options, extra = {}) { + const Webhook = require('./Webhook'); + const WebhookClient = require('../client/WebhookClient'); + + const isWebhook = target instanceof Webhook || target instanceof WebhookClient; + const transformed = this.transformOptions(content, options, extra, isWebhook); + return new this(target, transformed); + } +} + +module.exports = APIMessage; + +/** + * A target for a message. + * @typedef {TextChannel|DMChannel|GroupDMChannel|User|GuildMember|Webhook|WebhookClient} MessageTarget + */ + +/** + * Additional items that can be sent with a message. + * @typedef {MessageEmbed|MessageAttachment|Array} MessageAdditions + */ diff --git a/src/structures/Message.js b/src/structures/Message.js index 96ffacbb7..e229e20cf 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -10,7 +10,7 @@ const { MessageTypes } = require('../util/Constants'); const Permissions = require('../util/Permissions'); const Base = require('./Base'); const { Error, TypeError } = require('../errors'); -const { createMessage } = require('./shared'); +const APIMessage = require('./APIMessage'); /** * Represents a message on Discord. @@ -359,7 +359,7 @@ class Message extends Base { /** * Edits the content of the message. - * @param {StringResolvable} [content] The new content for the message + * @param {StringResolvable} [content=''] The new content for the message * @param {MessageEditOptions|MessageEmbed} [options] The options to provide * @returns {Promise} * @example @@ -368,17 +368,8 @@ class Message extends Base { * .then(msg => console.log(`Updated the content of a message to ${msg.content}`)) * .catch(console.error); */ - async edit(content, options) { - if (!options && typeof content === 'object' && !(content instanceof Array)) { - options = content; - content = null; - } else if (!options) { - options = {}; - } - if (!options.content) options.content = content; - - const { data } = await createMessage(this, options); - + edit(content, options) { + const data = APIMessage.create(this, content, options).resolveData(); return this.client.api.channels[this.channel.id].messages[this.id] .patch({ data }) .then(d => { @@ -467,8 +458,8 @@ class Message extends Base { /** * Replies to the message. - * @param {StringResolvable} [content] The content for the message - * @param {MessageOptions} [options] The options to provide + * @param {StringResolvable} [content=''] The content for the message + * @param {MessageOptions|MessageAdditions} [options={}] The options to provide * @returns {Promise} * @example * // Reply to a message @@ -477,13 +468,7 @@ class Message extends Base { * .catch(console.error); */ reply(content, options) { - if (!options && typeof content === 'object' && !(content instanceof Array)) { - options = content; - content = ''; - } else if (!options) { - options = {}; - } - return this.channel.send(content, Object.assign(options, { reply: this.member || this.author })); + return this.channel.send(APIMessage.transformOptions(content, options, { reply: this.member || this.author })); } /** diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js index 29f730764..a8d74ee67 100644 --- a/src/structures/MessageAttachment.js +++ b/src/structures/MessageAttachment.js @@ -2,77 +2,41 @@ const Util = require('../util/Util'); /** * Represents an attachment in a message. - * @param {BufferResolvable|Stream} file The file - * @param {string} [name] The name of the file, if any */ class MessageAttachment { - constructor(file, name, data) { - this.file = null; + /** + * @param {BufferResolvable|Stream} attachment The file + * @param {string} [name=null] The name of the file, if any + * @param {Object} [data] Extra data + */ + constructor(attachment, name = null, data) { + this.attachment = attachment; + this.name = name; if (data) this._patch(data); - if (name) this.setAttachment(file, name); - else this._attach(file); } /** - * The name of the file - * @type {?string} - * @readonly - */ - get name() { - return this.file.name; - } - - /** - * The file - * @type {?BufferResolvable|Stream} - * @readonly - */ - get attachment() { - return this.file.attachment; - } - - /** - * Sets the file of this attachment. - * @param {BufferResolvable|Stream} file The file - * @param {string} name The name of the file - * @returns {MessageAttachment} This attachment - */ - setAttachment(file, name) { - this.file = { attachment: file, name }; + * Sets the file of this attachment. + * @param {BufferResolvable|Stream} attachment The file + * @param {string} [name=null] The name of the file, if any + * @returns {MessageAttachment} This attachment + */ + setFile(attachment, name = null) { + this.attachment = attachment; + this.name = name; return this; } /** - * Sets the file of this attachment. - * @param {BufferResolvable|Stream} attachment The file - * @returns {MessageAttachment} This attachment - */ - setFile(attachment) { - this.file = { attachment }; - return this; - } - - /** - * Sets the name of this attachment. - * @param {string} name The name of the image - * @returns {MessageAttachment} This attachment - */ + * Sets the name of this attachment. + * @param {string} name The name of the file + * @returns {MessageAttachment} This attachment + */ setName(name) { - this.file.name = name; + this.name = name; return this; } - /** - * Sets the file of this attachment. - * @param {BufferResolvable|Stream} file The file - * @param {string} name The name of the file - * @private - */ - _attach(file, name) { - if (typeof file === 'string') this.file = file; - else this.setAttachment(file, name); - } - _patch(data) { /** * The ID of this attachment diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 72e22464e..2d0208b24 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -1,4 +1,3 @@ -const MessageAttachment = require('./MessageAttachment'); const Util = require('../util/Util'); const { RangeError } = require('../errors'); @@ -141,14 +140,8 @@ class MessageEmbed { * @type {Array} */ this.files = []; - if (data.files) { - this.files = data.files.map(file => { - if (file instanceof MessageAttachment) { - return typeof file.file === 'string' ? file.file : Util.cloneObject(file.file); - } - return file; - }); + this.files = data.files; } } @@ -203,7 +196,6 @@ class MessageEmbed { * @returns {MessageEmbed} */ attachFiles(files) { - files = files.map(file => file instanceof MessageAttachment ? file.file : file); this.files = this.files.concat(files); return this; } diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 088806ca2..711b3db79 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,6 +1,6 @@ const DataResolver = require('../util/DataResolver'); const Channel = require('./Channel'); -const { createMessage } = require('./shared'); +const APIMessage = require('./APIMessage'); /** * Represents a webhook. @@ -82,11 +82,10 @@ class Webhook { * it exceeds the character limit. If an object is provided, these are the options for splitting the message. */ - /* eslint-disable max-len */ /** * Sends a message with this webhook. - * @param {StringResolvable} [content] The content to send - * @param {WebhookMessageOptions|MessageEmbed|MessageAttachment|MessageAttachment[]} [options={}] The options to provide + * @param {StringResolvable} [content=''] The content to send + * @param {WebhookMessageOptions|MessageAdditions} [options={}] The options to provide * @returns {Promise} * @example * // Send a basic message @@ -127,20 +126,18 @@ class Webhook { * .catch(console.error); */ async send(content, options) { - if (!options && typeof content === 'object' && !(content instanceof Array)) { - options = content; - content = null; - } else if (!options) { - options = {}; - } - if (!options.content) options.content = content; - - const { data, files } = await createMessage(this, options); - + const apiMessage = APIMessage.create(this, content, options); + const data = apiMessage.resolveData(); if (data.content instanceof Array) { const messages = []; for (let i = 0; i < data.content.length; i++) { - const opt = i === data.content.length - 1 ? { embeds: data.embeds, files } : {}; + let opt; + if (i === data.content.length - 1) { + opt = { embeds: data.embeds, files: apiMessage.options.files }; + } else { + opt = {}; + } + Object.assign(opt, { avatarURL: data.avatar_url, content: data.content[i], username: data.username }); // eslint-disable-next-line no-await-in-loop const message = await this.send(data.content[i], opt); @@ -149,7 +146,7 @@ class Webhook { return messages; } - + const files = await apiMessage.resolveFiles(); return this.client.api.webhooks(this.id, this.token).post({ data, files, query: { wait: true }, diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index b7b487dfa..207f74248 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -1,8 +1,8 @@ const MessageCollector = require('../MessageCollector'); -const Shared = require('../shared'); const Snowflake = require('../../util/Snowflake'); const Collection = require('../../util/Collection'); const { RangeError, TypeError } = require('../../errors'); +const APIMessage = require('../APIMessage'); /** * Interface for classes that have text-channel-like features. @@ -66,8 +66,8 @@ class TextBasedChannel { /** * Sends a message to this channel. - * @param {StringResolvable} [content] Text for the message - * @param {MessageOptions|MessageEmbed|MessageAttachment|MessageAttachment[]} [options={}] Options for the message + * @param {StringResolvable} [content=''] The content to send + * @param {MessageOptions|MessageAdditions} [options={}] The options to provide * @returns {Promise} * @example * // Send a basic message @@ -107,16 +107,35 @@ class TextBasedChannel { * .then(console.log) * .catch(console.error); */ - send(content, options) { // eslint-disable-line complexity - if (!options && typeof content === 'object' && !(content instanceof Array)) { - options = content; - content = null; - } else if (!options) { - options = {}; + 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)); } - if (!options.content) options.content = content; - return Shared.sendMessage(this, options); + const apiMessage = APIMessage.create(this, content, options); + const data = apiMessage.resolveData(); + if (data.content instanceof Array) { + const messages = []; + for (let i = 0; i < data.content.length; i++) { + let opt; + if (i === data.content.length - 1) { + opt = { tts: data.tts, embed: data.embed, files: apiMessage.options.files }; + } else { + opt = { tts: data.tts }; + } + + // eslint-disable-next-line no-await-in-loop + const message = await this.send(data.content[i], opt); + messages.push(message); + } + return messages; + } + + const files = await apiMessage.resolveFiles(); + return this.client.api.channels[this.id].messages.post({ data, files }) + .then(d => this.client.actions.MessageCreate.handle(d).message); } /** diff --git a/src/structures/shared/CreateMessage.js b/src/structures/shared/CreateMessage.js deleted file mode 100644 index 8ba66c342..000000000 --- a/src/structures/shared/CreateMessage.js +++ /dev/null @@ -1,126 +0,0 @@ -const Embed = require('../MessageEmbed'); -const DataResolver = require('../../util/DataResolver'); -const MessageEmbed = require('../MessageEmbed'); -const MessageAttachment = require('../MessageAttachment'); -const { browser } = require('../../util/Constants'); -const Util = require('../../util/Util'); -const { RangeError } = require('../../errors'); - -// eslint-disable-next-line complexity -module.exports = async function createMessage(channel, options) { - const User = require('../User'); - const GuildMember = require('../GuildMember'); - const Webhook = require('../Webhook'); - const WebhookClient = require('../../client/WebhookClient'); - - const webhook = channel instanceof Webhook || channel instanceof WebhookClient; - - if (typeof options.nonce !== 'undefined') { - options.nonce = parseInt(options.nonce); - if (isNaN(options.nonce) || options.nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE'); - } - - let { content, reply } = options; - if (options instanceof MessageEmbed) options = webhook ? { embeds: [options] } : { embed: options }; - if (options instanceof MessageAttachment) options = { files: [options.file] }; - - if (content instanceof Array || options instanceof Array) { - const which = content instanceof Array ? content : options; - const attachments = which.filter(item => item instanceof MessageAttachment); - const embeds = which.filter(item => item instanceof MessageEmbed); - if (attachments.length) options = { files: attachments }; - if (embeds.length) options = { embeds }; - if ((embeds.length || attachments.length) && content instanceof Array) { - content = null; - options.content = ''; - } - } - - if (options.split && typeof options.split !== 'object') options.split = {}; - let mentionPart = ''; - if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { - const id = channel.client.users.resolveID(reply); - mentionPart = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>, `; - if (options.split) options.split.prepend = `${mentionPart}${options.split.prepend || ''}`; - } - - if (content || mentionPart) { - options.content = Util.resolveString(content || ''); - // Wrap everything in a code block - if (typeof options.code !== 'undefined' && (typeof options.code !== 'boolean' || options.code === true)) { - options.content = Util.escapeMarkdown(options.content, true); - options.content = `${mentionPart}\`\`\`${typeof options.code !== 'boolean' ? - options.code || '' : ''}\n${options.content}\n\`\`\``; - if (options.split) { - options.split.prepend = - `${options.split.prepend || ''}\`\`\`${typeof options.code !== 'boolean' ? options.code || '' : ''}\n`; - - options.split.append = `\n\`\`\`${options.split.append || ''}`; - } - } else if (mentionPart) { - options.content = mentionPart + (options.content || ''); - } - - // Add zero-width spaces to @everyone/@here - if (options.disableEveryone || - (typeof options.disableEveryone === 'undefined' && channel.client.options.disableEveryone)) { - options.content = options.content.replace(/@(everyone|here)/g, '@\u200b$1'); - } - - if (options.split) options.content = Util.splitMessage(options.content, options.split); - } - - if (options.embed && options.embed.files) { - if (options.files) options.files = options.files.concat(options.embed.files); - else options.files = options.embed.files; - } - - if (options.embed && webhook) options.embeds = [new Embed(options.embed)._apiTransform()]; - else if (options.embed) options.embed = new Embed(options.embed)._apiTransform(); - else if (options.embeds) options.embeds = options.embeds.map(e => new Embed(e)._apiTransform()); - - let files; - - if (options.files) { - for (let i = 0; i < options.files.length; i++) { - let file = options.files[i]; - if (typeof file === 'string' || (!browser && Buffer.isBuffer(file))) file = { attachment: file }; - if (!file.name) { - if (typeof file.attachment === 'string') { - file.name = Util.basename(file.attachment); - } else if (file.attachment && file.attachment.path) { - file.name = Util.basename(file.attachment.path); - } else if (file instanceof MessageAttachment) { - file = { attachment: file.file, name: Util.basename(file.file) || 'file.jpg' }; - } else { - file.name = 'file.jpg'; - } - } else if (file instanceof MessageAttachment) { - file = file.file; - } - options.files[i] = file; - } - - files = await Promise.all(options.files.map(file => - DataResolver.resolveFile(file.attachment).then(resource => { - file.file = resource; - return file; - }) - )); - } - - if (webhook) { - if (!options.username) options.username = this.name; - if (options.avatarURL) options.avatar_url = options.avatarURL; - } - - return { data: { - content: options.content, - tts: options.tts, - nonce: options.nonce, - embed: options.embed, - embeds: options.embeds, - username: options.username, - avatar_url: options.avatar_url, - }, files }; -}; diff --git a/src/structures/shared/SendMessage.js b/src/structures/shared/SendMessage.js deleted file mode 100644 index 95ea49ca3..000000000 --- a/src/structures/shared/SendMessage.js +++ /dev/null @@ -1,23 +0,0 @@ -const createMessage = require('./CreateMessage'); - -module.exports = async function sendMessage(channel, options) { // eslint-disable-line complexity - const User = require('../User'); - const GuildMember = require('../GuildMember'); - if (channel instanceof User || channel instanceof GuildMember) return channel.createDM().then(dm => dm.send(options)); - - const { data, files } = await createMessage(channel, options); - - if (data.content instanceof Array) { - const messages = []; - for (let i = 0; i < data.content.length; i++) { - const opt = i === data.content.length - 1 ? { tts: data.tts, embed: data.embed, files } : { tts: data.tts }; - // eslint-disable-next-line no-await-in-loop - const message = await channel.send(data.content[i], opt); - messages.push(message); - } - return messages; - } - - return channel.client.api.channels[channel.id].messages.post({ data, files }) - .then(d => channel.client.actions.MessageCreate.handle(d).message); -}; diff --git a/src/structures/shared/index.js b/src/structures/shared/index.js deleted file mode 100644 index 5e81ec3c0..000000000 --- a/src/structures/shared/index.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - sendMessage: require('./SendMessage'), - createMessage: require('./CreateMessage'), -}; diff --git a/src/util/DataResolver.js b/src/util/DataResolver.js index 95e0aa5ad..5fcfd0d43 100644 --- a/src/util/DataResolver.js +++ b/src/util/DataResolver.js @@ -103,7 +103,7 @@ class DataResolver { }); }); } - } else if (resource.pipe && typeof resource.pipe === 'function') { + } else if (typeof resource.pipe === 'function') { return new Promise((resolve, reject) => { const buffers = []; resource.once('error', reject); diff --git a/test/blobReach.png b/test/blobReach.png new file mode 100644 index 000000000..358bf4739 Binary files /dev/null and b/test/blobReach.png differ diff --git a/test/sendtest.js b/test/sendtest.js new file mode 100644 index 000000000..14d48260a --- /dev/null +++ b/test/sendtest.js @@ -0,0 +1,140 @@ +const Discord = require('../src'); +const { owner, token } = require('./auth.js'); + +const fetch = require('node-fetch'); +const fs = require('fs'); +const path = require('path'); +const util = require('util'); + +const client = new Discord.Client(); + +const fill = c => Array(4).fill(c.repeat(1000)); +const buffer = l => fetch(l).then(res => res.buffer()); +const read = util.promisify(fs.readFile); +const readStream = fs.createReadStream; +const wait = util.promisify(setTimeout); + +const linkA = 'https://lolisafe.moe/iiDMtAXA.png'; +const linkB = 'https://lolisafe.moe/9hSpedPh.png'; +const fileA = path.join(__dirname, 'blobReach.png'); + +const embed = () => new Discord.MessageEmbed(); +const attach = (attachment, name) => new Discord.MessageAttachment(attachment, name); + +const tests = [ + m => m.channel.send('x'), + m => m.channel.send(['x', 'y']), + + m => m.channel.send('x', { code: true }), + m => m.channel.send('1', { code: 'js' }), + m => m.channel.send('x', { code: '' }), + + m => m.channel.send(fill('x'), { split: true }), + m => m.channel.send(fill('1'), { code: 'js', split: true }), + m => m.channel.send(fill('x'), { reply: m.author, code: 'js', split: true }), + m => m.channel.send(fill('xyz '), { split: { char: ' ' } }), + + m => m.channel.send('x', { embed: { description: 'a' } }), + m => m.channel.send({ embed: { description: 'a' } }), + m => m.channel.send({ files: [{ attachment: linkA }] }), + m => m.channel.send({ + embed: { description: 'a' }, + files: [{ attachment: linkA, name: 'xyz.png' }], + }), + + m => m.channel.send('x', embed().setDescription('a')), + m => m.channel.send(embed().setDescription('a')), + m => m.channel.send({ embed: embed().setDescription('a') }), + m => m.channel.send([embed().setDescription('a'), embed().setDescription('b')]), + + m => m.channel.send('x', attach(linkA)), + m => m.channel.send(attach(linkA)), + m => m.channel.send({ files: [linkA] }), + m => m.channel.send({ files: [attach(linkA)] }), + async m => m.channel.send(attach(await buffer(linkA))), + async m => m.channel.send({ files: [await buffer(linkA)] }), + async m => m.channel.send({ files: [{ attachment: await buffer(linkA) }] }), + m => m.channel.send([attach(linkA), attach(linkB)]), + + m => m.channel.send({ embed: { description: 'a' } }).then(m2 => m2.edit('x')), + m => m.channel.send(embed().setDescription('a')).then(m2 => m2.edit('x')), + m => m.channel.send({ embed: embed().setDescription('a') }).then(m2 => m2.edit('x')), + + m => m.channel.send('x').then(m2 => m2.edit({ embed: { description: 'a' } })), + m => m.channel.send('x').then(m2 => m2.edit(embed().setDescription('a'))), + m => m.channel.send('x').then(m2 => m2.edit({ embed: embed().setDescription('a') })), + + m => m.channel.send({ embed: { description: 'a' } }).then(m2 => m2.edit({ embed: null })), + m => m.channel.send(embed().setDescription('a')).then(m2 => m2.edit({ embed: null })), + + m => m.channel.send(['x', 'y'], [embed().setDescription('a'), attach(linkB)]), + m => m.channel.send(['x', 'y'], [attach(linkA), attach(linkB)]), + + m => m.channel.send([embed().setDescription('a'), attach(linkB)]), + m => m.channel.send({ + embed: embed().setImage('attachment://two.png'), + files: [attach(linkB, 'two.png')], + }), + m => m.channel.send({ + embed: embed() + .setImage('attachment://two.png') + .attachFiles([attach(linkB, 'two.png')]), + }), + async m => m.channel.send(['x', 'y', 'z'], { + code: 'js', + embed: embed() + .setImage('attachment://two.png') + .attachFiles([attach(linkB, 'two.png')]), + files: [{ attachment: await buffer(linkA) }], + }), + + m => m.channel.send('x', attach(fileA)), + m => m.channel.send({ files: [fileA] }), + m => m.channel.send(attach(fileA)), + async m => m.channel.send({ files: [await read(fileA)] }), + async m => m.channel.send(fill('x'), { + reply: m.author, + code: 'js', + split: true, + embed: embed().setImage('attachment://zero.png'), + files: [attach(await buffer(linkA), 'zero.png')], + }), + + m => m.channel.send('x', attach(readStream(fileA))), + m => m.channel.send({ files: [readStream(fileA)] }), + m => m.channel.send({ files: [{ attachment: readStream(fileA) }] }), + async m => m.channel.send(fill('xyz '), { + reply: m.author, + code: 'js', + split: { char: ' ', prepend: 'hello! ', append: '!!!' }, + embed: embed().setImage('attachment://zero.png'), + files: [linkB, attach(await buffer(linkA), 'zero.png'), readStream(fileA)], + }), + + m => m.channel.send('Done!'), +]; + +client.on('message', async message => { + if (message.author.id !== owner) return; + const match = message.content.match(/^do (.+)$/); + if (match && match[1] === 'it') { + /* eslint-disable no-await-in-loop */ + for (const [i, test] of tests.entries()) { + await message.channel.send(`**#${i}**\n\`\`\`js\n${test.toString()}\`\`\``); + await test(message).catch(e => message.channel.send(`Error!\n\`\`\`\n${e}\`\`\``)); + await wait(1000); + } + /* eslint-enable no-await-in-loop */ + } else if (match) { + const n = parseInt(match[1]) || 0; + const test = tests.slice(n)[0]; + const i = tests.indexOf(test); + await message.channel.send(`**#${i}**\n\`\`\`js\n${test.toString()}\`\`\``); + await test(message).catch(e => message.channel.send(`Error!\n\`\`\`\n${e}\`\`\``)); + } +}); + +client.login(token); + +// eslint-disable-next-line no-console +process.on('unhandledRejection', console.error); diff --git a/typings/index.d.ts b/typings/index.d.ts index 3501f7e9d..a8cf2cf1e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -40,6 +40,33 @@ declare module 'discord.js' { public static FLAGS: Record; } + export class APIMessage { + constructor(target: MessageTarget, options: MessageOptions | WebhookMessageOptions); + public readonly isUser: boolean; + public readonly isWebhook: boolean; + public options: MessageOptions | WebhookMessageOptions; + public target: MessageTarget; + + public static create( + target: MessageTarget, + content?: StringResolvable, + options?: MessageOptions | WebhookMessageOptions | MessageAdditions, + extra?: MessageOptions | WebhookMessageOptions + ): APIMessage; + public static partitionMessageAdditions(items: (MessageEmbed | MessageAttachment)[]): [MessageEmbed[], MessageAttachment[]]; + public static resolveFile(fileLike: BufferResolvable | Stream | FileOptions | MessageAttachment): Promise; + public static transformOptions( + content: StringResolvable, + options: MessageOptions | WebhookMessageOptions | MessageAdditions, + extra?: MessageOptions | WebhookMessageOptions, + isWebhook?: boolean + ): MessageOptions | WebhookMessageOptions; + + public makeContent(): string | string[]; + public resolveData(): object; + public resolveFiles(): Promise; + } + export class Base { constructor (client: Client); public readonly client: Client; @@ -618,30 +645,29 @@ declare module 'discord.js' { public createReactionCollector(filter: CollectorFilter, options?: ReactionCollectorOptions): ReactionCollector; public delete(options?: { timeout?: number, reason?: string }): Promise; public edit(content: StringResolvable, options?: MessageEditOptions | MessageEmbed): Promise; + public edit(options: MessageEditOptions | MessageEmbed): Promise; public equals(message: Message, rawData: object): boolean; public fetchWebhook(): Promise; public pin(): Promise; public react(emoji: EmojiIdentifierResolvable): Promise; - public reply(content?: StringResolvable, options?: MessageOptions): Promise; - public reply(options?: MessageOptions): Promise; + public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; + public reply(options?: MessageOptions | MessageAdditions): Promise; public toJSON(): object; public toString(): string; public unpin(): Promise; } export class MessageAttachment { - constructor(file: BufferResolvable | Stream, name?: string); - private _attach(file: BufferResolvable | Stream, name: string): void; + constructor(attachment: BufferResolvable | Stream, name?: string); - public readonly attachment: BufferResolvable | Stream; + public attachment: BufferResolvable | Stream; public height: number; public id: Snowflake; - public readonly name: string; + public name?: string; public proxyURL: string; public url: string; public width: number; - public setAttachment(file: BufferResolvable | Stream, name: string): this; - public setFile(attachment: BufferResolvable | Stream): this; + public setFile(attachment: BufferResolvable | Stream, name?: string): this; public setName(name: string): this; public toJSON(): object; } @@ -1345,8 +1371,8 @@ declare module 'discord.js' { lastMessageID: Snowflake; lastMessageChannelID: Snowflake; readonly lastMessage: Message; - send(content?: StringResolvable, options?: MessageOptions | MessageEmbed | MessageAttachment): Promise; - send(options?: MessageOptions | MessageEmbed | MessageAttachment): Promise; + send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; + send(options?: MessageOptions | MessageAdditions): Promise; }; type TextBasedChannelFields = { @@ -1367,8 +1393,8 @@ declare module 'discord.js' { token: string; delete(reason?: string): Promise; edit(options: WebhookEditData): Promise; - send(content?: StringResolvable, options?: WebhookMessageOptions | MessageEmbed | MessageAttachment | MessageAttachment[]): Promise; - send(options?: WebhookMessageOptions | MessageEmbed | MessageAttachment | MessageAttachment[]): Promise; + send(content?: StringResolvable, options?: WebhookMessageOptions | MessageAdditions): Promise; + send(options?: WebhookMessageOptions | MessageAdditions): Promise; sendSlackMessage(body: object): Promise; }; @@ -1604,7 +1630,7 @@ declare module 'discord.js' { }; type FileOptions = { - attachment: BufferResolvable; + attachment: BufferResolvable | Stream; name?: string; }; @@ -1783,6 +1809,8 @@ declare module 'discord.js' { maxProcessed?: number; }; + type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[]; + type MessageEditOptions = { content?: string; embed?: MessageEmbedOptions | null; @@ -1810,7 +1838,7 @@ declare module 'discord.js' { content?: string; embed?: MessageEmbed | MessageEmbedOptions, disableEveryone?: boolean; - files?: (FileOptions | BufferResolvable | MessageAttachment)[]; + files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; reply?: UserResolvable; @@ -1820,6 +1848,8 @@ declare module 'discord.js' { type MessageResolvable = Message | Snowflake; + type MessageTarget = TextChannel | DMChannel | GroupDMChannel | User | GuildMember | Webhook | WebhookClient; + type MessageType = 'DEFAULT' | 'RECIPIENT_ADD' | 'RECIPIENT_REMOVE' @@ -1968,7 +1998,7 @@ declare module 'discord.js' { nonce?: string; embeds?: (MessageEmbed | object)[]; disableEveryone?: boolean; - files?: (FileOptions | BufferResolvable | MessageAttachment)[]; + files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; };