refactor: rewrite message creation (#2774)

* Rework createMessage
- MessageAttachment is now structurally similar to FileOptions
- No longer mutates the object passed as options
- Supports more permutations of arguments

* Ignore complexity warning

* Refactor name finding

* Fix typo

* Update typings

* Default name to null for MessageAttachment

* Make Message#reply use transformOptions

* Move transformOptions

* Fix Message#reply

* Fix mutation

* Update tests

* Fix options passing

* Refactor into APIMessage

* Fix webhook send

* Expose APIMessage

* Add documentation

* Add types

* Fix type doc

* Fix another type doc

* Fix another another type doc (is this one even right!?)

* Remove trailing comma

* Properly clone split options

* Add support for sending file as stream

* Missed a doc

* Resolve files only once when splitting messages

* This looks nicer

* Assign directly

* Don't cache data and files

* Missing return type

* Use object spread instead Object.assign

* Document constructors

* Crawl is a little dot

* comp pls

* tests: sanitize local file path, disable no-await-in-loop
This commit is contained in:
1Computer1 2018-08-21 12:22:29 -04:00 committed by Crawl
parent 55c58b60e7
commit 19c298f5cc
14 changed files with 551 additions and 284 deletions

View file

@ -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'),

View file

@ -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<Object[]>}
*/
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<MessageEmbed|MessageAttachment>} items Items to partition
* @returns {Array<MessageEmbed[], MessageAttachment[]>}
*/
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<MessageEmbed|MessageAttachment>} MessageAdditions
*/

View file

@ -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<Message>}
* @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<Message|Message[]>}
* @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 }));
}
/**

View file

@ -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

View file

@ -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<FileOptions|string|MessageAttachment>}
*/
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;
}

View file

@ -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<Message|Object>}
* @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 },

View file

@ -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<Message|Message[]>}
* @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);
}
/**

View file

@ -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 };
};

View file

@ -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);
};

View file

@ -1,4 +0,0 @@
module.exports = {
sendMessage: require('./SendMessage'),
createMessage: require('./CreateMessage'),
};

View file

@ -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);

BIN
test/blobReach.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

140
test/sendtest.js Normal file
View file

@ -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);

60
typings/index.d.ts vendored
View file

@ -40,6 +40,33 @@ declare module 'discord.js' {
public static FLAGS: Record<ActivityFlagsString, number>;
}
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<object>;
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<object[]>;
}
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<Message>;
public edit(content: StringResolvable, options?: MessageEditOptions | MessageEmbed): Promise<Message>;
public edit(options: MessageEditOptions | MessageEmbed): Promise<Message>;
public equals(message: Message, rawData: object): boolean;
public fetchWebhook(): Promise<Webhook>;
public pin(): Promise<Message>;
public react(emoji: EmojiIdentifierResolvable): Promise<MessageReaction>;
public reply(content?: StringResolvable, options?: MessageOptions): Promise<Message | Message[]>;
public reply(options?: MessageOptions): Promise<Message | Message[]>;
public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise<Message | Message[]>;
public reply(options?: MessageOptions | MessageAdditions): Promise<Message | Message[]>;
public toJSON(): object;
public toString(): string;
public unpin(): Promise<Message>;
}
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<Message | Message[]>;
send(options?: MessageOptions | MessageEmbed | MessageAttachment): Promise<Message | Message[]>;
send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise<Message | Message[]>;
send(options?: MessageOptions | MessageAdditions): Promise<Message | Message[]>;
};
type TextBasedChannelFields = {
@ -1367,8 +1393,8 @@ declare module 'discord.js' {
token: string;
delete(reason?: string): Promise<void>;
edit(options: WebhookEditData): Promise<Webhook>;
send(content?: StringResolvable, options?: WebhookMessageOptions | MessageEmbed | MessageAttachment | MessageAttachment[]): Promise<Message | Message[]>;
send(options?: WebhookMessageOptions | MessageEmbed | MessageAttachment | MessageAttachment[]): Promise<Message | Message[]>;
send(content?: StringResolvable, options?: WebhookMessageOptions | MessageAdditions): Promise<Message | Message[]>;
send(options?: WebhookMessageOptions | MessageAdditions): Promise<Message | Message[]>;
sendSlackMessage(body: object): Promise<Message|object>;
};
@ -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;
};