mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
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:
@@ -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'),
|
||||
|
||||
292
src/structures/APIMessage.js
Normal file
292
src/structures/APIMessage.js
Normal 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
|
||||
*/
|
||||
@@ -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 }));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
this.file = { attachment };
|
||||
setFile(attachment, name = null) {
|
||||
this.attachment = attachment;
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of this attachment.
|
||||
* @param {string} name The name of the image
|
||||
* @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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
sendMessage: require('./SendMessage'),
|
||||
createMessage: require('./CreateMessage'),
|
||||
};
|
||||
@@ -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
BIN
test/blobReach.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
140
test/sendtest.js
Normal file
140
test/sendtest.js
Normal 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
60
typings/index.d.ts
vendored
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user