diff --git a/src/data/Thread.js b/src/data/Thread.js index e3b7b1f..ac32c9d 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -25,11 +25,31 @@ const {THREAD_MESSAGE_TYPE, THREAD_STATUS, DISCORD_MESSAGE_ACTIVITY_TYPES} = req * @property {String} scheduled_close_name * @property {Number} scheduled_close_silent * @property {String} alert_ids + * @property {String} log_storage_type + * @property {Object} log_storage_data * @property {String} created_at */ class Thread { constructor(props) { utils.setDataModelProps(this, props); + + if (props.log_storage_data) { + if (typeof props.log_storage_data === "string") { + this.log_storage_data = JSON.parse(props.log_storage_data); + } + } + } + + getSQLProps() { + return Object.entries(this).reduce((obj, [key, value]) => { + if (typeof value === "function") return obj; + if (typeof value === "object" && value != null) { + obj[key] = JSON.stringify(value); + } else { + obj[key] = value; + } + return obj; + }, {}); } /** @@ -506,6 +526,7 @@ class Thread { } // Update DB status + this.status = THREAD_STATUS.CLOSED; await knex("threads") .where("id", this.id) .update({ @@ -727,6 +748,25 @@ class Thread { await this._deleteThreadMessage(threadMessage.id); } + + /** + * @param {String} storageType + * @param {Object|null} storageData + * @returns {Promise} + */ + async updateLogStorageValues(storageType, storageData) { + this.log_storage_type = storageType; + this.log_storage_data = storageData; + + const { log_storage_type, log_storage_data } = this.getSQLProps(); + + await knex("threads") + .where("id", this.id) + .update({ + log_storage_type, + log_storage_data, + }); + } } module.exports = Thread; diff --git a/src/data/logs.js b/src/data/logs.js index a47521e..89e78fe 100644 --- a/src/data/logs.js +++ b/src/data/logs.js @@ -9,7 +9,8 @@ const { formatters } = require("../formatters"); /** * @typedef {object} LogStorageTypeHandler - * @property {LogStorageTypeHandlerSaveFn?} save + * @property {LogStorageTypeHandlerSaveFn} save + * @property {LogStorageTypeHandlerShouldSaveFn?} shouldSave * @property {LogStorageTypeHandlerGetUrlFn?} getUrl * @property {LogStorageTypeHandlerGetFileFn?} getFile * @property {LogStorageTypeHandlerGetCustomResponseFn?} getCustomResponse @@ -19,18 +20,24 @@ const { formatters } = require("../formatters"); * @callback LogStorageTypeHandlerSaveFn * @param {Thread} thread * @param {ThreadMessage[]} threadMessages - * @return {void|Promise} + * @return {Object|Promise|null|Promise} Information about the saved log that can be used to retrieve the log later + */ + +/** + * @callback LogStorageTypeHandlerShouldSaveFn + * @param {Thread} thread + * @return {boolean|Promise} Whether the log should be saved at this time */ /** * @callback LogStorageTypeHandlerGetUrlFn - * @param {string} threadId + * @param {Thread} thread * @return {string|Promise|null|Promise} */ /** * @callback LogStorageTypeHandlerGetFileFn - * @param {string} threadId + * @param {Thread} thread * @return {Eris.MessageFile|Promise|null|Promise>} */ @@ -42,7 +49,7 @@ const { formatters } = require("../formatters"); /** * @callback LogStorageTypeHandlerGetCustomResponseFn - * @param {string} threadId + * @param {Thread} thread * @return {LogStorageTypeHandlerGetCustomResponseResult|Promise|null|Promise>} */ @@ -70,95 +77,110 @@ const addStorageType = (name, handler) => { /** * @type {SaveLogToStorageFn} */ -const saveLogToStorage = async (thread, threadMessages) => { - const { save } = logStorageTypes[config.logStorage]; +const saveLogToStorage = async (thread, overrideType = null) => { + const storageType = overrideType || config.logStorage; + + const { save, shouldSave } = logStorageTypes[storageType] || {}; + if (shouldSave && ! await shouldSave(thread)) return; + if (save) { - await save(thread, threadMessages); + const threadMessages = await thread.getThreadMessages(); + const storageData = await save(thread, threadMessages); + await thread.updateLogStorageValues(storageType, storageData); } }; /** * @callback GetLogUrlFn - * @param {string} threadId + * @param {Thread} thread * @returns {Promise} */ /** * @type {GetLogUrlFn} */ -const getLogUrl = async (threadId) => { - const { getUrl } = logStorageTypes[config.logStorage]; +const getLogUrl = async (thread) => { + if (! thread.log_storage_type) { + await saveLogToStorage(thread); + } + + const { getUrl } = logStorageTypes[thread.log_storage_type] || {}; return getUrl - ? getUrl(threadId) + ? getUrl(thread) : null; }; /** * @callback GetLogFileFn - * @param {string} threadId + * @param {Thread} thread * @returns {Promise} */ /** * @type {GetLogFileFn} */ -const getLogFile = async (threadId) => { - const { getFile } = logStorageTypes[config.logStorage]; +const getLogFile = async (thread) => { + if (! thread.log_storage_type) { + await saveLogToStorage(thread); + } + + const { getFile } = logStorageTypes[thread.log_storage_type] || {}; return getFile - ? getFile(threadId) + ? getFile(thread) : null; }; /** * @callback GetLogCustomResponseFn - * @param {string} threadId + * @param {Thread} threadId * @returns {Promise} */ /** * @type {GetLogCustomResponseFn} */ -const getLogCustomResponse = async (threadId) => { - const { getCustomResponse } = logStorageTypes[config.logStorage]; +const getLogCustomResponse = async (thread) => { + if (! thread.log_storage_type) { + await saveLogToStorage(thread); + } + + const { getCustomResponse } = logStorageTypes[thread.log_storage_type] || {}; return getCustomResponse - ? getCustomResponse(threadId) + ? getCustomResponse(thread) : null; }; addStorageType("local", { - getUrl(threadId) { - return utils.getSelfUrl(`logs/${threadId}`); + save() { + return null; + }, + + getUrl(thread) { + return utils.getSelfUrl(`logs/${thread.id}`); }, }); const getLogAttachmentFilename = threadId => { const filename = `${threadId}.txt`; - const fullPath = path.resolve(config.logOptions.attachmentDirectory, filename); + const fullPath = path.join(config.logOptions.attachmentDirectory, filename); return { filename, fullPath }; }; addStorageType("attachment", { + shouldSave(thread) { + return thread.status === THREAD_STATUS.CLOSED; + }, + async save(thread, threadMessages) { - const { fullPath } = getLogAttachmentFilename(thread.id); + const { fullPath, filename } = getLogAttachmentFilename(thread.id); const formatLogResult = await formatters.formatLog(thread, threadMessages); fs.writeFileSync(fullPath, formatLogResult.content, { encoding: "utf8" }); + + return { fullPath, filename }; }, - async getUrl(threadId) { - if (! config.logOptions.allowAttachmentUrlFallback) { - return null; - } - - const { fullPath } = getLogAttachmentFilename(threadId); - try { - fs.accessSync(fullPath); - return null; - } catch (e) { - return utils.getSelfUrl(`logs/${threadId}`); - } - }, - - async getFile(threadId) { - const { filename, fullPath } = getLogAttachmentFilename(threadId); + async getFile(thread) { + const { fullPath, filename } = thread.log_storage_data || {}; + if (! fullPath) return; try { fs.accessSync(fullPath); diff --git a/src/data/migrations/20201003153122_add_log_storage_fields_to_threads.js b/src/data/migrations/20201003153122_add_log_storage_fields_to_threads.js new file mode 100644 index 0000000..5757186 --- /dev/null +++ b/src/data/migrations/20201003153122_add_log_storage_fields_to_threads.js @@ -0,0 +1,13 @@ +exports.up = async function(knex) { + await knex.schema.table("threads", table => { + table.string("log_storage_type", 255).nullable().defaultTo(null); + table.text("log_storage_data").nullable().defaultTo(null); + }); +}; + +exports.down = async function(knex) { + await knex.schema.table("threads", table => { + table.dropColumn("log_storage_type"); + table.dropColumn("log_storage_data"); + }); +}; diff --git a/src/modules/close.js b/src/modules/close.js index eadece2..196ba7e 100644 --- a/src/modules/close.js +++ b/src/modules/close.js @@ -7,15 +7,15 @@ const { messageQueue } = require("../queue"); const { getLogUrl, getLogFile, getLogCustomResponse } = require("../data/logs"); module.exports = ({ bot, knex, config, commands }) => { - async function sendCloseNotification(threadId, body) { - const logCustomResponse = await getLogCustomResponse(threadId); + async function sendCloseNotification(thread, body) { + const logCustomResponse = await getLogCustomResponse(thread); if (logCustomResponse) { await utils.postLog(body); await utils.postLog(logCustomResponse.content, logCustomResponse.file); return; } - const logUrl = await getLogUrl(threadId); + const logUrl = await getLogUrl(thread); if (logUrl) { utils.postLog(utils.trimAll(` ${body} @@ -24,7 +24,7 @@ module.exports = ({ bot, knex, config, commands }) => { return; } - const logFile = await getLogFile(threadId); + const logFile = await getLogFile(thread); if (logFile) { utils.postLog(body, logFile); return; @@ -44,7 +44,7 @@ module.exports = ({ bot, knex, config, commands }) => { await thread.close(false, thread.scheduled_close_silent); - await sendCloseNotification(thread.id, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed as scheduled by ${thread.scheduled_close_name}`); + await sendCloseNotification(thread, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed as scheduled by ${thread.scheduled_close_name}`); } } @@ -143,7 +143,7 @@ module.exports = ({ bot, knex, config, commands }) => { await thread.sendSystemMessageToUser(closeMessage).catch(() => {}); } - await sendCloseNotification(thread.id, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed by ${closedBy}`); + await sendCloseNotification(thread, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed by ${closedBy}`); }); // Auto-close threads if their channel is deleted @@ -162,6 +162,6 @@ module.exports = ({ bot, knex, config, commands }) => { await thread.close(true); - await sendCloseNotification(thread.id, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed automatically because the channel was deleted`); + await sendCloseNotification(thread, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed automatically because the channel was deleted`); }); }; diff --git a/src/modules/logs.js b/src/modules/logs.js index 916a80f..5e567c3 100644 --- a/src/modules/logs.js +++ b/src/modules/logs.js @@ -30,7 +30,7 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { userThreads = userThreads.slice((page - 1) * LOG_LINES_PER_PAGE, page * LOG_LINES_PER_PAGE); const threadLines = await Promise.all(userThreads.map(async thread => { - const logUrl = await getLogUrl(thread.id); + const logUrl = await getLogUrl(thread); const formattedLogUrl = logUrl ? `<${logUrl}>` : `View log with \`${config.prefix}log ${thread.id}\`` @@ -58,22 +58,25 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { }); }; - const logCmd = async (msg, args, thread) => { - const threadId = args.threadId || (thread && thread.id); + const logCmd = async (msg, args, _thread) => { + const threadId = args.threadId || (_thread && _thread.id); if (! threadId) return; - const customResponse = await getLogCustomResponse(threadId); + const thread = await threads.findById(threadId); + if (! thread) return; + + const customResponse = await getLogCustomResponse(thread); if (customResponse && (customResponse.content || customResponse.file)) { msg.channel.createMessage(customResponse.content, customResponse.file); } - const logUrl = await getLogUrl(threadId); + const logUrl = await getLogUrl(thread); if (logUrl) { msg.channel.createMessage(`Open the following link to view the log:\n<${logUrl}>`); return; } - const logFile = await getLogFile(threadId); + const logFile = await getLogFile(thread); if (logFile) { msg.channel.createMessage("Download the following file to view the log:", logFile); return; @@ -90,7 +93,6 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { hooks.afterThreadClose(async ({ threadId }) => { const thread = await threads.findById(threadId); - const threadMessages = await thread.getThreadMessages(); - await saveLogToStorage(thread, threadMessages); + await saveLogToStorage(thread); }); };