Skip to content

Commit

Permalink
Add Attachment structure (#1731)
Browse files Browse the repository at this point in the history
* Add Attachment structure

* Fix linter issues + @Private

* Fixed array sends, also added embed sends

* fixed proving path to attachment

* fixed incorrect name assumption from path

* linting fix

* ;)

* im really good at this

* changes as requested by gus

and computer from #1459

* am a dum

* update webhook#send

* readonly addition to getters

* i... uh... oops

* farming deez commits

* fix webhook split

* removed some ugly

* removed .every checks
  • Loading branch information
Lewdcario authored and Gawdl3y committed Aug 6, 2017
1 parent 317a352 commit 62fc9fc
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 22 deletions.
22 changes: 22 additions & 0 deletions src/client/ClientDataResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,28 @@ class ClientDataResolver {
return Promise.reject(new TypeError('REQ_RESOURCE_TYPE'));
}

/**
* Converts a Stream to a Buffer.
* @param {Stream} resource The stream to convert
* @returns {Promise<Buffer>}
*/
resolveFile(resource) {
return resource ? this.resolveBuffer(resource)
.catch(() => {
if (resource.pipe && typeof resource.pipe === 'function') {
return new Promise((resolve, reject) => {
const buffers = [];
resource.once('error', reject);
resource.on('data', data => buffers.push(data));
resource.once('end', () => resolve(Buffer.concat(buffers)));
});
} else {
throw new TypeError('REQ_RESOURCE_TYPE');
}
}) :
Promise.reject(new TypeError('REQ_RESOURCE_TYPE'));
}

/**
* Data that can be resolved to give an emoji identifier. This can be:
* * The unicode representation of an emoji
Expand Down
2 changes: 1 addition & 1 deletion src/errors/Messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const Messages = {
UDP_CONNECTION_EXISTS: 'There is already an existing UDP connection.',

REQ_BODY_TYPE: 'The response body isn\'t a Buffer.',
REQ_RESOURCE_TYPE: 'The resource must be a string or Buffer.',
REQ_RESOURCE_TYPE: 'The resource must be a string, Buffer or a valid file stream.',

IMAGE_FORMAT: format => `Invalid image format: ${format}`,
IMAGE_SIZE: size => `Invalid image size: ${size}`,
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = {
splitMessage: Util.splitMessage,

// Structures
Attachment: require('./structures/Attachment'),
Channel: require('./structures/Channel'),
ClientUser: require('./structures/ClientUser'),
ClientUserSettings: require('./structures/ClientUserSettings'),
Expand Down
73 changes: 73 additions & 0 deletions src/structures/Attachment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Represents an attachment in a message
*/
class Attachment {
constructor(file, name) {
this.file = null;
this._attach(file, name);
}

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

/**
* Set the file of this attachment.
* @param {BufferResolvable|Stream} file The file
* @param {string} name The name of the file
* @returns {Attachment} This attachment
*/
setAttachment(file, name) {
this.file = { attachment: file, name };
return this;
}

/**
* Set the file of this attachment.
* @param {BufferResolvable|Stream} attachment The file
* @returns {Attachment} This attachment
*/
setFile(attachment) {
this.file.attachment = attachment;
return this;
}

/**
* Set the name of this attachment.
* @param {string} name The name of the image
* @returns {Attachment} This attachment
*/
setName(name) {
this.file.name = name;
return this;
}

/**
* Set the file of this attachment.
* @param {BufferResolvable|Stream} file The file
* @param {string} name The name of the file
* @private
*/
_attach(file, name) {
if (file) {
if (typeof file === 'string') this.file = file;
else this.setAttachment(file, name);
}
}
}

module.exports = Attachment;
22 changes: 21 additions & 1 deletion src/structures/MessageEmbed.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Attachment = require('./Attachment');
const Util = require('../util/Util');
const { RangeError } = require('../errors');

Expand Down Expand Up @@ -129,6 +130,17 @@ class MessageEmbed {
iconURL: data.footer.iconURL || data.footer.icon_url,
proxyIconURL: data.footer.proxyIconURL || data.footer.proxy_icon_url,
} : null;

/**
* The files of this embed
* @type {?Object}
* @property {Array<FileOptions|string|Attachment>} files Files to attach
*/
if (data.files) {
for (let file of data.files) {
if (file instanceof Attachment) file = file.file;
}
} else { data.files = null; }
}

/**
Expand Down Expand Up @@ -178,12 +190,15 @@ class MessageEmbed {
/**
* Sets the file to upload alongside the embed. This file can be accessed via `attachment://fileName.extension` when
* setting an embed image or author/footer icons. Only one file may be attached.
* @param {Array<FileOptions|string>} files Files to attach
* @param {Array<FileOptions|string|Attachment>} files Files to attach
* @returns {MessageEmbed} This embed
*/
attachFiles(files) {
if (this.files) this.files = this.files.concat(files);
else this.files = files;
for (let file of files) {
if (file instanceof Attachment) file = file.file;
}
return this;
}

Expand Down Expand Up @@ -286,6 +301,11 @@ class MessageEmbed {
return this;
}

/**
* Transforms the embed object to be processed.
* @returns {Object} The raw data of this embed
* @private
*/
_apiTransform() {
return {
title: this.title,
Expand Down
73 changes: 58 additions & 15 deletions src/structures/Webhook.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const path = require('path');
const Util = require('../util/Util');
const Embed = require('./MessageEmbed');
const Attachment = require('./Attachment');
const MessageEmbed = require('./MessageEmbed');

/**
* Represents a webhook.
Expand Down Expand Up @@ -82,7 +84,7 @@ class Webhook {
* (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details)
* @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here
* should be replaced with plain-text
* @property {FileOptions|string} [file] A file to send with the message
* @property {FileOptions|BufferResolvable} [file] A file to send with the message
* @property {FileOptions[]|string[]} [files] Files to send with the message
* @property {string|boolean} [code] Language for optional codeblock formatting to apply
* @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if
Expand All @@ -100,57 +102,78 @@ class Webhook {
* .then(message => console.log(`Sent message: ${message.content}`))
* .catch(console.error);
*/
send(content, options) {
send(content, options) { // eslint-disable-line complexity
if (!options && typeof content === 'object' && !(content instanceof Array)) {
options = content;
content = '';
} else if (!options) {
options = {};
}

if (!options.username) options.username = this.name;
if (options instanceof Attachment) options = { files: [options.file] };
if (options instanceof MessageEmbed) options = { embeds: [options] };
if (options.embed) options = { embeds: [options.embed] };

if (content instanceof Array || options instanceof Array) {
const which = content instanceof Array ? content : options;
const attachments = which.filter(item => item instanceof Attachment);
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 = '';
}

if (!options.username) options.username = this.name;
if (options.avatarURL) {
options.avatar_url = options.avatarURL;
options.avatarURL = null;
}

if (typeof content !== 'undefined') content = Util.resolveString(content);
if (content) {
if (options.disableEveryone ||
(typeof options.disableEveryone === 'undefined' && this.client.options.disableEveryone)
) {
content = Util.resolveString(content);
let { split, code, disableEveryone } = options;
if (split && typeof split !== 'object') split = {};
if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) {
content = Util.escapeMarkdown(content, true);
content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``;
if (split) {
split.prepend = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n`;
split.append = '\n```';
}
}
if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) {
content = content.replace(/@(everyone|here)/g, '@\u200b$1');
}

if (split) content = Util.splitMessage(content, split);
}
options.content = content;

if (options.embeds) options.embeds = options.embeds.map(embed => new Embed(embed)._apiTransform());

if (options.file) {
if (options.files) options.files.push(options.file);
else options.files = [options.file];
}

if (options.files) {
for (let i = 0; i < options.files.length; i++) {
let file = options.files[i];
if (typeof file === 'string') file = { attachment: file };
if (typeof file === 'string' || Buffer.isBuffer(file)) file = { attachment: file };
if (!file.name) {
if (typeof file.attachment === 'string') {
file.name = path.basename(file.attachment);
} else if (file.attachment && file.attachment.path) {
file.name = path.basename(file.attachment.path);
} else if (file instanceof Attachment) {
file = { attachment: file.file, name: path.basename(file.file) || 'file.jpg' };
} else {
file.name = 'file.jpg';
}
} else if (file instanceof Attachment) {
file = file.file;
}
options.files[i] = file;
}

return Promise.all(options.files.map(file =>
this.client.resolver.resolveBuffer(file.attachment).then(buffer => {
file.file = buffer;
this.client.resolver.resolveFile(file.attachment).then(resource => {
file.file = resource;
return file;
})
)).then(files => this.client.api.webhooks(this.id, this.token).post({
Expand All @@ -161,6 +184,26 @@ class Webhook {
}));
}

if (content instanceof Array) {
return new Promise((resolve, reject) => {
const messages = [];
(function sendChunk() {
const opt = content.length ? null : { embeds: options.embeds, files: options.files };
this.client.api.webhooks(this.id, this.token).post({
data: { content: content.shift(), opt },
query: { wait: true },
auth: false,
})
.then(message => {
messages.push(message);
if (content.length === 0) return resolve(messages);
return sendChunk.call(this);
})
.catch(reject);
}.call(this));
});
}

return this.client.api.webhooks(this.id, this.token).post({
data: options,
query: { wait: true },
Expand Down
28 changes: 23 additions & 5 deletions src/structures/interfaces/TextBasedChannel.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const MessageCollector = require('../MessageCollector');
const Shared = require('../shared');
const Collection = require('../../util/Collection');
const Snowflake = require('../../util/Snowflake');
const Attachment = require('../../structures/Attachment');
const MessageEmbed = require('../../structures/MessageEmbed');
const { Error, RangeError, TypeError } = require('../../errors');

/**
Expand Down Expand Up @@ -39,7 +41,7 @@ class TextBasedChannel {
* (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details)
* @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here
* should be replaced with plain-text
* @property {FileOptions[]|string[]} [files] Files to send with the message
* @property {FileOptions[]|BufferResolvable[]} [files] Files to send with the message
* @property {string|boolean} [code] Language for optional codeblock formatting to apply
* @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if
* it exceeds the character limit. If an object is provided, these are the options for splitting the message
Expand Down Expand Up @@ -72,14 +74,26 @@ class TextBasedChannel {
* .then(message => console.log(`Sent message: ${message.content}`))
* .catch(console.error);
*/
send(content, options) {
send(content, options) { // eslint-disable-line complexity
if (!options && typeof content === 'object' && !(content instanceof Array)) {
options = content;
content = '';
} else if (!options) {
options = {};
}

if (options instanceof MessageEmbed) options = { embed: options };
if (options instanceof Attachment) 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 Attachment);
if (attachments.length) {
options = { files: attachments };
if (content instanceof Array) content = '';
}
}

if (!options.content) options.content = content;

if (options.embed && options.embed.files) {
Expand All @@ -90,22 +104,26 @@ class TextBasedChannel {
if (options.files) {
for (let i = 0; i < options.files.length; i++) {
let file = options.files[i];
if (typeof file === 'string') file = { attachment: file };
if (typeof file === 'string' || Buffer.isBuffer(file)) file = { attachment: file };
if (!file.name) {
if (typeof file.attachment === 'string') {
file.name = path.basename(file.attachment);
} else if (file.attachment && file.attachment.path) {
file.name = path.basename(file.attachment.path);
} else if (file instanceof Attachment) {
file = { attachment: file.file, name: path.basename(file.file) || 'file.jpg' };
} else {
file.name = 'file.jpg';
}
} else if (file instanceof Attachment) {
file = file.file;
}
options.files[i] = file;
}

return Promise.all(options.files.map(file =>
this.client.resolver.resolveBuffer(file.attachment).then(buffer => {
file.file = buffer;
this.client.resolver.resolveFile(file.attachment).then(resource => {
file.file = resource;
return file;
})
)).then(files => {
Expand Down

0 comments on commit 62fc9fc

Please sign in to comment.