Allow log storage handlers to store data. Add shouldSave() function to log storage handlers.
parent
3937c0a838
commit
0d2202d38c
|
@ -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;
|
||||||
|
|
102
src/data/logs.js
102
src/data/logs.js
|
@ -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);
|
||||||
|
|
|
@ -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");
|
||||||
|
});
|
||||||
|
};
|
|
@ -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`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue