2017-02-09 23:36:47 -05:00
|
|
|
const Eris = require('eris');
|
2017-02-09 21:56:36 -05:00
|
|
|
const fs = require('fs');
|
|
|
|
const https = require('https');
|
2018-02-14 01:53:34 -05:00
|
|
|
const {promisify} = require('util');
|
2019-03-06 16:31:24 -05:00
|
|
|
const tmp = require('tmp');
|
|
|
|
const config = require('../config');
|
|
|
|
const utils = require('../utils');
|
2019-09-17 18:34:17 -04:00
|
|
|
const mv = promisify(require('mv'));
|
2017-09-19 13:23:55 -04:00
|
|
|
|
2017-12-24 15:04:08 -05:00
|
|
|
const getUtils = () => require('../utils');
|
2017-02-09 21:56:36 -05:00
|
|
|
|
2018-02-14 01:53:34 -05:00
|
|
|
const access = promisify(fs.access);
|
|
|
|
const readFile = promisify(fs.readFile);
|
|
|
|
|
2019-03-06 16:31:24 -05:00
|
|
|
const localAttachmentDir = config.attachmentDir || `${__dirname}/../../attachments`;
|
2017-02-09 21:56:36 -05:00
|
|
|
|
2018-02-14 01:53:34 -05:00
|
|
|
const attachmentSavePromises = {};
|
|
|
|
|
2019-09-17 18:52:16 -04:00
|
|
|
const attachmentStorageTypes = {};
|
|
|
|
|
2019-03-06 16:31:24 -05:00
|
|
|
function getErrorResult(msg = null) {
|
|
|
|
return {
|
|
|
|
url: `Attachment could not be saved${msg ? ': ' + msg : ''}`,
|
|
|
|
failed: true
|
|
|
|
};
|
2017-02-09 21:56:36 -05:00
|
|
|
}
|
|
|
|
|
2017-02-09 23:36:47 -05:00
|
|
|
/**
|
|
|
|
* Attempts to download and save the given attachement
|
|
|
|
* @param {Object} attachment
|
|
|
|
* @param {Number=0} tries
|
2019-03-06 16:31:24 -05:00
|
|
|
* @returns {Promise<{ url: string }>}
|
2017-02-09 23:36:47 -05:00
|
|
|
*/
|
2019-03-06 16:31:24 -05:00
|
|
|
async function saveLocalAttachment(attachment) {
|
|
|
|
const targetPath = getLocalAttachmentPath(attachment.id);
|
2018-02-14 01:53:34 -05:00
|
|
|
|
|
|
|
try {
|
|
|
|
// If the file already exists, resolve immediately
|
2019-03-06 16:31:24 -05:00
|
|
|
await access(targetPath);
|
|
|
|
const url = await getLocalAttachmentUrl(attachment.id, attachment.filename);
|
|
|
|
return { url };
|
2018-02-14 01:53:34 -05:00
|
|
|
} catch (e) {}
|
|
|
|
|
2019-03-06 16:31:24 -05:00
|
|
|
// Download the attachment
|
|
|
|
const downloadResult = await downloadAttachment(attachment);
|
2018-02-14 01:53:34 -05:00
|
|
|
|
2019-03-06 16:31:24 -05:00
|
|
|
// Move the temp file to the attachment folder
|
2019-09-17 18:34:17 -04:00
|
|
|
await mv(downloadResult.path, targetPath);
|
2019-03-06 16:31:24 -05:00
|
|
|
|
|
|
|
// Resolve the attachment URL
|
|
|
|
const url = await getLocalAttachmentUrl(attachment.id, attachment.filename);
|
|
|
|
|
|
|
|
return { url };
|
2018-02-14 01:53:34 -05:00
|
|
|
}
|
|
|
|
|
2019-03-06 16:31:24 -05:00
|
|
|
/**
|
|
|
|
* @param {Object} attachment
|
|
|
|
* @param {Number} tries
|
|
|
|
* @returns {Promise<{ path: string, cleanup: function }>}
|
|
|
|
*/
|
|
|
|
function downloadAttachment(attachment, tries = 0) {
|
2017-02-09 21:56:36 -05:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
if (tries > 3) {
|
|
|
|
console.error('Attachment download failed after 3 tries:', attachment);
|
|
|
|
reject('Attachment download failed after 3 tries');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-03-06 16:31:24 -05:00
|
|
|
tmp.file((err, filepath, fd, cleanupCallback) => {
|
|
|
|
const writeStream = fs.createWriteStream(filepath);
|
|
|
|
|
|
|
|
https.get(attachment.url, (res) => {
|
|
|
|
res.pipe(writeStream);
|
|
|
|
writeStream.on('finish', () => {
|
|
|
|
writeStream.end();
|
|
|
|
resolve({
|
|
|
|
path: filepath,
|
|
|
|
cleanup: cleanupCallback
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}).on('error', (err) => {
|
|
|
|
fs.unlink(filepath);
|
|
|
|
console.error('Error downloading attachment, retrying');
|
|
|
|
resolve(downloadAttachment(attachment, tries++));
|
2017-02-09 21:56:36 -05:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-02-09 23:36:47 -05:00
|
|
|
/**
|
2019-03-06 16:31:24 -05:00
|
|
|
* Returns the filesystem path for the given attachment id
|
|
|
|
* @param {String} attachmentId
|
|
|
|
* @returns {String}
|
2017-02-09 23:36:47 -05:00
|
|
|
*/
|
2019-03-06 16:31:24 -05:00
|
|
|
function getLocalAttachmentPath(attachmentId) {
|
|
|
|
return `${localAttachmentDir}/${attachmentId}`;
|
2017-02-09 21:56:36 -05:00
|
|
|
}
|
|
|
|
|
2017-02-09 23:36:47 -05:00
|
|
|
/**
|
|
|
|
* Returns the self-hosted URL to the given attachment ID
|
|
|
|
* @param {String} attachmentId
|
|
|
|
* @param {String=null} desiredName Custom name for the attachment as a hint for the browser
|
2019-03-06 16:31:24 -05:00
|
|
|
* @returns {Promise<String>}
|
2017-02-09 23:36:47 -05:00
|
|
|
*/
|
2019-03-06 16:31:24 -05:00
|
|
|
function getLocalAttachmentUrl(attachmentId, desiredName = null) {
|
2017-02-09 21:56:36 -05:00
|
|
|
if (desiredName == null) desiredName = 'file.bin';
|
2017-09-19 13:23:55 -04:00
|
|
|
return getUtils().getSelfUrl(`attachments/${attachmentId}/${desiredName}`);
|
2017-02-09 21:56:36 -05:00
|
|
|
}
|
|
|
|
|
2019-03-06 16:31:24 -05:00
|
|
|
/**
|
|
|
|
* @param {Object} attachment
|
|
|
|
* @returns {Promise<{ url: string }>}
|
|
|
|
*/
|
|
|
|
async function saveDiscordAttachment(attachment) {
|
|
|
|
if (attachment.size > 1024 * 1024 * 8) {
|
|
|
|
return getErrorResult('attachment too large (max 8MB)');
|
|
|
|
}
|
|
|
|
|
|
|
|
const attachmentChannelId = config.attachmentStorageChannelId;
|
|
|
|
const inboxGuild = utils.getInboxGuild();
|
|
|
|
|
|
|
|
if (! inboxGuild.channels.has(attachmentChannelId)) {
|
|
|
|
throw new Error('Attachment storage channel not found!');
|
|
|
|
}
|
|
|
|
|
|
|
|
const attachmentChannel = inboxGuild.channels.get(attachmentChannelId);
|
|
|
|
if (! (attachmentChannel instanceof Eris.TextChannel)) {
|
|
|
|
throw new Error('Attachment storage channel must be a text channel!');
|
|
|
|
}
|
|
|
|
|
|
|
|
const file = await attachmentToFile(attachment);
|
|
|
|
const savedAttachment = await createDiscordAttachmentMessage(attachmentChannel, file);
|
|
|
|
if (! savedAttachment) return getErrorResult();
|
|
|
|
|
|
|
|
return { url: savedAttachment.url };
|
|
|
|
}
|
|
|
|
|
|
|
|
async function createDiscordAttachmentMessage(channel, file, tries = 0) {
|
|
|
|
tries++;
|
|
|
|
|
|
|
|
try {
|
|
|
|
const attachmentMessage = await channel.createMessage(undefined, file);
|
|
|
|
return attachmentMessage.attachments[0];
|
|
|
|
} catch (e) {
|
|
|
|
if (tries > 3) {
|
|
|
|
console.error(`Attachment storage message could not be created after 3 tries: ${e.message}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return createDiscordAttachmentMessage(channel, file, tries);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Turns the given attachment into a file object that can be sent forward as a new attachment
|
|
|
|
* @param {Object} attachment
|
|
|
|
* @returns {Promise<{file, name: string}>}
|
|
|
|
*/
|
2018-02-14 01:53:34 -05:00
|
|
|
async function attachmentToFile(attachment) {
|
2019-03-06 16:31:24 -05:00
|
|
|
const downloadResult = await downloadAttachment(attachment);
|
|
|
|
const data = await readFile(downloadResult.path);
|
|
|
|
downloadResult.cleanup();
|
2018-02-14 01:53:34 -05:00
|
|
|
return {file: data, name: attachment.filename};
|
|
|
|
}
|
|
|
|
|
2019-03-06 16:31:24 -05:00
|
|
|
/**
|
|
|
|
* Saves the given attachment based on the configured storage system
|
|
|
|
* @param {Object} attachment
|
|
|
|
* @returns {Promise<{ url: string }>}
|
|
|
|
*/
|
|
|
|
function saveAttachment(attachment) {
|
|
|
|
if (attachmentSavePromises[attachment.id]) {
|
|
|
|
return attachmentSavePromises[attachment.id];
|
|
|
|
}
|
|
|
|
|
2019-09-17 18:52:16 -04:00
|
|
|
if (attachmentStorageTypes[config.attachmentStorage]) {
|
|
|
|
attachmentSavePromises[attachment.id] = Promise.resolve(attachmentStorageTypes[config.attachmentStorage](attachment));
|
2019-03-06 16:31:24 -05:00
|
|
|
} else {
|
|
|
|
throw new Error(`Unknown attachment storage option: ${config.attachmentStorage}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
attachmentSavePromises[attachment.id].then(() => {
|
|
|
|
delete attachmentSavePromises[attachment.id];
|
|
|
|
});
|
|
|
|
|
|
|
|
return attachmentSavePromises[attachment.id];
|
|
|
|
}
|
|
|
|
|
2019-09-17 18:52:16 -04:00
|
|
|
function addStorageType(name, handler) {
|
|
|
|
attachmentStorageTypes[name] = handler;
|
|
|
|
}
|
|
|
|
|
|
|
|
attachmentStorageTypes.local = saveLocalAttachment;
|
|
|
|
attachmentStorageTypes.discord = saveDiscordAttachment;
|
|
|
|
|
2017-02-09 21:56:36 -05:00
|
|
|
module.exports = {
|
2019-03-06 16:31:24 -05:00
|
|
|
getLocalAttachmentPath,
|
|
|
|
attachmentToFile,
|
2019-09-17 18:52:16 -04:00
|
|
|
saveAttachment,
|
|
|
|
addStorageType
|
2017-02-09 21:56:36 -05:00
|
|
|
};
|