Allow log storage handlers to store data. Add shouldSave() function to log storage handlers.

cshd
Dragory 2020-10-03 16:10:27 +03:00
parent 3937c0a838
commit 0d2202d38c
No known key found for this signature in database
GPG Key ID: 5F387BA66DF8AAC1
5 changed files with 132 additions and 55 deletions

View File

@ -25,11 +25,31 @@ const {THREAD_MESSAGE_TYPE, THREAD_STATUS, DISCORD_MESSAGE_ACTIVITY_TYPES} = req
* @property {String} scheduled_close_name * @property {String} scheduled_close_name
* @property {Number} scheduled_close_silent * @property {Number} scheduled_close_silent
* @property {String} alert_ids * @property {String} alert_ids
* @property {String} log_storage_type
* @property {Object} log_storage_data
* @property {String} created_at * @property {String} created_at
*/ */
class Thread { class Thread {
constructor(props) { constructor(props) {
utils.setDataModelProps(this, 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 // Update DB status
this.status = THREAD_STATUS.CLOSED;
await knex("threads") await knex("threads")
.where("id", this.id) .where("id", this.id)
.update({ .update({
@ -727,6 +748,25 @@ class Thread {
await this._deleteThreadMessage(threadMessage.id); await this._deleteThreadMessage(threadMessage.id);
} }
/**
* @param {String} storageType
* @param {Object|null} storageData
* @returns {Promise<void>}
*/
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; module.exports = Thread;

View File

@ -9,7 +9,8 @@ const { formatters } = require("../formatters");
/** /**
* @typedef {object} LogStorageTypeHandler * @typedef {object} LogStorageTypeHandler
* @property {LogStorageTypeHandlerSaveFn?} save * @property {LogStorageTypeHandlerSaveFn} save
* @property {LogStorageTypeHandlerShouldSaveFn?} shouldSave
* @property {LogStorageTypeHandlerGetUrlFn?} getUrl * @property {LogStorageTypeHandlerGetUrlFn?} getUrl
* @property {LogStorageTypeHandlerGetFileFn?} getFile * @property {LogStorageTypeHandlerGetFileFn?} getFile
* @property {LogStorageTypeHandlerGetCustomResponseFn?} getCustomResponse * @property {LogStorageTypeHandlerGetCustomResponseFn?} getCustomResponse
@ -19,18 +20,24 @@ const { formatters } = require("../formatters");
* @callback LogStorageTypeHandlerSaveFn * @callback LogStorageTypeHandlerSaveFn
* @param {Thread} thread * @param {Thread} thread
* @param {ThreadMessage[]} threadMessages * @param {ThreadMessage[]} threadMessages
* @return {void|Promise<void>} * @return {Object|Promise<Object>|null|Promise<null>} Information about the saved log that can be used to retrieve the log later
*/
/**
* @callback LogStorageTypeHandlerShouldSaveFn
* @param {Thread} thread
* @return {boolean|Promise<boolean>} Whether the log should be saved at this time
*/ */
/** /**
* @callback LogStorageTypeHandlerGetUrlFn * @callback LogStorageTypeHandlerGetUrlFn
* @param {string} threadId * @param {Thread} thread
* @return {string|Promise<string>|null|Promise<null>} * @return {string|Promise<string>|null|Promise<null>}
*/ */
/** /**
* @callback LogStorageTypeHandlerGetFileFn * @callback LogStorageTypeHandlerGetFileFn
* @param {string} threadId * @param {Thread} thread
* @return {Eris.MessageFile|Promise<Eris.MessageFile>|null|Promise<null>>} * @return {Eris.MessageFile|Promise<Eris.MessageFile>|null|Promise<null>>}
*/ */
@ -42,7 +49,7 @@ const { formatters } = require("../formatters");
/** /**
* @callback LogStorageTypeHandlerGetCustomResponseFn * @callback LogStorageTypeHandlerGetCustomResponseFn
* @param {string} threadId * @param {Thread} thread
* @return {LogStorageTypeHandlerGetCustomResponseResult|Promise<LogStorageTypeHandlerGetCustomResponseResult>|null|Promise<null>>} * @return {LogStorageTypeHandlerGetCustomResponseResult|Promise<LogStorageTypeHandlerGetCustomResponseResult>|null|Promise<null>>}
*/ */
@ -70,95 +77,110 @@ const addStorageType = (name, handler) => {
/** /**
* @type {SaveLogToStorageFn} * @type {SaveLogToStorageFn}
*/ */
const saveLogToStorage = async (thread, threadMessages) => { const saveLogToStorage = async (thread, overrideType = null) => {
const { save } = logStorageTypes[config.logStorage]; const storageType = overrideType || config.logStorage;
const { save, shouldSave } = logStorageTypes[storageType] || {};
if (shouldSave && ! await shouldSave(thread)) return;
if (save) { if (save) {
await save(thread, threadMessages); const threadMessages = await thread.getThreadMessages();
const storageData = await save(thread, threadMessages);
await thread.updateLogStorageValues(storageType, storageData);
} }
}; };
/** /**
* @callback GetLogUrlFn * @callback GetLogUrlFn
* @param {string} threadId * @param {Thread} thread
* @returns {Promise<string|null>} * @returns {Promise<string|null>}
*/ */
/** /**
* @type {GetLogUrlFn} * @type {GetLogUrlFn}
*/ */
const getLogUrl = async (threadId) => { const getLogUrl = async (thread) => {
const { getUrl } = logStorageTypes[config.logStorage]; if (! thread.log_storage_type) {
await saveLogToStorage(thread);
}
const { getUrl } = logStorageTypes[thread.log_storage_type] || {};
return getUrl return getUrl
? getUrl(threadId) ? getUrl(thread)
: null; : null;
}; };
/** /**
* @callback GetLogFileFn * @callback GetLogFileFn
* @param {string} threadId * @param {Thread} thread
* @returns {Promise<Eris.MessageFile|null>} * @returns {Promise<Eris.MessageFile|null>}
*/ */
/** /**
* @type {GetLogFileFn} * @type {GetLogFileFn}
*/ */
const getLogFile = async (threadId) => { const getLogFile = async (thread) => {
const { getFile } = logStorageTypes[config.logStorage]; if (! thread.log_storage_type) {
await saveLogToStorage(thread);
}
const { getFile } = logStorageTypes[thread.log_storage_type] || {};
return getFile return getFile
? getFile(threadId) ? getFile(thread)
: null; : null;
}; };
/** /**
* @callback GetLogCustomResponseFn * @callback GetLogCustomResponseFn
* @param {string} threadId * @param {Thread} threadId
* @returns {Promise<LogStorageTypeHandlerGetCustomResult|null>} * @returns {Promise<LogStorageTypeHandlerGetCustomResult|null>}
*/ */
/** /**
* @type {GetLogCustomResponseFn} * @type {GetLogCustomResponseFn}
*/ */
const getLogCustomResponse = async (threadId) => { const getLogCustomResponse = async (thread) => {
const { getCustomResponse } = logStorageTypes[config.logStorage]; if (! thread.log_storage_type) {
await saveLogToStorage(thread);
}
const { getCustomResponse } = logStorageTypes[thread.log_storage_type] || {};
return getCustomResponse return getCustomResponse
? getCustomResponse(threadId) ? getCustomResponse(thread)
: null; : null;
}; };
addStorageType("local", { addStorageType("local", {
getUrl(threadId) { save() {
return utils.getSelfUrl(`logs/${threadId}`); return null;
},
getUrl(thread) {
return utils.getSelfUrl(`logs/${thread.id}`);
}, },
}); });
const getLogAttachmentFilename = threadId => { const getLogAttachmentFilename = threadId => {
const filename = `${threadId}.txt`; const filename = `${threadId}.txt`;
const fullPath = path.resolve(config.logOptions.attachmentDirectory, filename); const fullPath = path.join(config.logOptions.attachmentDirectory, filename);
return { filename, fullPath }; return { filename, fullPath };
}; };
addStorageType("attachment", { addStorageType("attachment", {
shouldSave(thread) {
return thread.status === THREAD_STATUS.CLOSED;
},
async save(thread, threadMessages) { async save(thread, threadMessages) {
const { fullPath } = getLogAttachmentFilename(thread.id); const { fullPath, filename } = getLogAttachmentFilename(thread.id);
const formatLogResult = await formatters.formatLog(thread, threadMessages); const formatLogResult = await formatters.formatLog(thread, threadMessages);
fs.writeFileSync(fullPath, formatLogResult.content, { encoding: "utf8" }); fs.writeFileSync(fullPath, formatLogResult.content, { encoding: "utf8" });
return { fullPath, filename };
}, },
async getUrl(threadId) { async getFile(thread) {
if (! config.logOptions.allowAttachmentUrlFallback) { const { fullPath, filename } = thread.log_storage_data || {};
return null; if (! fullPath) return;
}
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);
try { try {
fs.accessSync(fullPath); fs.accessSync(fullPath);

View File

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

View File

@ -7,15 +7,15 @@ const { messageQueue } = require("../queue");
const { getLogUrl, getLogFile, getLogCustomResponse } = require("../data/logs"); const { getLogUrl, getLogFile, getLogCustomResponse } = require("../data/logs");
module.exports = ({ bot, knex, config, commands }) => { module.exports = ({ bot, knex, config, commands }) => {
async function sendCloseNotification(threadId, body) { async function sendCloseNotification(thread, body) {
const logCustomResponse = await getLogCustomResponse(threadId); const logCustomResponse = await getLogCustomResponse(thread);
if (logCustomResponse) { if (logCustomResponse) {
await utils.postLog(body); await utils.postLog(body);
await utils.postLog(logCustomResponse.content, logCustomResponse.file); await utils.postLog(logCustomResponse.content, logCustomResponse.file);
return; return;
} }
const logUrl = await getLogUrl(threadId); const logUrl = await getLogUrl(thread);
if (logUrl) { if (logUrl) {
utils.postLog(utils.trimAll(` utils.postLog(utils.trimAll(`
${body} ${body}
@ -24,7 +24,7 @@ module.exports = ({ bot, knex, config, commands }) => {
return; return;
} }
const logFile = await getLogFile(threadId); const logFile = await getLogFile(thread);
if (logFile) { if (logFile) {
utils.postLog(body, logFile); utils.postLog(body, logFile);
return; return;
@ -44,7 +44,7 @@ module.exports = ({ bot, knex, config, commands }) => {
await thread.close(false, thread.scheduled_close_silent); 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 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 // Auto-close threads if their channel is deleted
@ -162,6 +162,6 @@ module.exports = ({ bot, knex, config, commands }) => {
await thread.close(true); 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`);
}); });
}; };

View File

@ -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); userThreads = userThreads.slice((page - 1) * LOG_LINES_PER_PAGE, page * LOG_LINES_PER_PAGE);
const threadLines = await Promise.all(userThreads.map(async thread => { const threadLines = await Promise.all(userThreads.map(async thread => {
const logUrl = await getLogUrl(thread.id); const logUrl = await getLogUrl(thread);
const formattedLogUrl = logUrl const formattedLogUrl = logUrl
? `<${logUrl}>` ? `<${logUrl}>`
: `View log with \`${config.prefix}log ${thread.id}\`` : `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 logCmd = async (msg, args, _thread) => {
const threadId = args.threadId || (thread && thread.id); const threadId = args.threadId || (_thread && _thread.id);
if (! threadId) return; 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)) { if (customResponse && (customResponse.content || customResponse.file)) {
msg.channel.createMessage(customResponse.content, customResponse.file); msg.channel.createMessage(customResponse.content, customResponse.file);
} }
const logUrl = await getLogUrl(threadId); const logUrl = await getLogUrl(thread);
if (logUrl) { if (logUrl) {
msg.channel.createMessage(`Open the following link to view the log:\n<${logUrl}>`); msg.channel.createMessage(`Open the following link to view the log:\n<${logUrl}>`);
return; return;
} }
const logFile = await getLogFile(threadId); const logFile = await getLogFile(thread);
if (logFile) { if (logFile) {
msg.channel.createMessage("Download the following file to view the log:", logFile); msg.channel.createMessage("Download the following file to view the log:", logFile);
return; return;
@ -90,7 +93,6 @@ module.exports = ({ bot, knex, config, commands, hooks }) => {
hooks.afterThreadClose(async ({ threadId }) => { hooks.afterThreadClose(async ({ threadId }) => {
const thread = await threads.findById(threadId); const thread = await threads.findById(threadId);
const threadMessages = await thread.getThreadMessages(); await saveLogToStorage(thread);
await saveLogToStorage(thread, threadMessages);
}); });
}; };