diff --git a/db/migrations/20200813230319_separate_message_components.js b/db/migrations/20200813230319_separate_message_components.js new file mode 100644 index 0000000..0b0e025 --- /dev/null +++ b/db/migrations/20200813230319_separate_message_components.js @@ -0,0 +1,21 @@ +exports.up = async function(knex) { + await knex.schema.table("thread_messages", table => { + table.string("role_name", 255).nullable(); + table.text("attachments").nullable(); + table.text("small_attachments").nullable(); + table.boolean("use_legacy_format").nullable(); + }); + + await knex("thread_messages").update({ + use_legacy_format: 1, + }); +}; + +exports.down = async function(knex) { + await knex.schema.table("thread_messages", table => { + table.dropColumn("role_name"); + table.dropColumn("attachments"); + table.dropColumn("small_attachments"); + table.dropColumn("use_legacy_format"); + }); +}; diff --git a/src/data/Thread.js b/src/data/Thread.js index 747c449..83fc5eb 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -178,7 +178,9 @@ class Thread { * @returns {Promise} Whether we were able to send the reply */ async replyToUser(moderator, text, replyAttachments = [], isAnonymous = false) { - const fullModeratorName = `${moderator.user.username}#${moderator.user.discriminator}`; + const moderatorName = config.useNicknames && moderator.nick ? moderator.nick : moderator.user.username; + const mainRole = utils.getMainRole(moderator); + const roleName = mainRole ? mainRole.name : null; // Prepare attachments, if any const files = []; @@ -197,8 +199,18 @@ class Thread { } } + let threadMessage = new ThreadMessage({ + message_type: THREAD_MESSAGE_TYPE.TO_USER, + user_id: moderator.id, + user_name: moderatorName, + body: text, + is_anonymous: (isAnonymous ? 1 : 0), + role_name: roleName, + attachments: attachmentLinks, + }); + // Send the reply DM - const dmContent = formatters.formatStaffReplyDM(moderator, text, { isAnonymous }); + const dmContent = formatters.formatStaffReplyDM(threadMessage); let dmMessage; try { dmMessage = await this._sendDMToUser(dmContent, files); @@ -208,21 +220,17 @@ class Thread { } // Save the log entry - const threadMessage = await this._addThreadMessageToDB({ - message_type: THREAD_MESSAGE_TYPE.TO_USER, - user_id: moderator.id, - user_name: fullModeratorName, - body: "", - is_anonymous: (isAnonymous ? 1 : 0), - dm_message_id: dmMessage.id + threadMessage = await this._addThreadMessageToDB({ + ...threadMessage.getSQLProps(), + dm_message_id: dmMessage.id, }); - const logContent = formatters.formatStaffReplyLogMessage(moderator, text, threadMessage.message_number, { isAnonymous, attachmentLinks }); - await this._updateThreadMessage(threadMessage.id, { body: logContent }); // Show the reply in the inbox thread - const inboxContent = formatters.formatStaffReplyThreadMessage(moderator, text, threadMessage.message_number, { isAnonymous }); + const inboxContent = formatters.formatStaffReplyThreadMessage(threadMessage); const inboxMessage = await this._postToThreadChannel(inboxContent, files); - if (inboxMessage) await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); + if (inboxMessage) { + await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); + } // Interrupt scheduled closing, if in progress if (this.scheduled_close_at) { @@ -238,43 +246,46 @@ class Thread { * @returns {Promise} */ async receiveUserReply(msg) { - // Prepare attachments - const attachmentFiles = []; - const threadFormattedAttachments = []; - const logFormattedAttachments = []; + const fullUserName = `${msg.author.username}#${msg.author.discriminator}`; + + // Prepare attachments + const attachments = []; + const smallAttachments = []; + const attachmentFiles = []; - // TODO: Save attachment info with the message, use that to re-create attachment formatting in - // TODO: this._formatUserReplyLogMessage and this._formatUserReplyThreadMessage for (const attachment of msg.attachments) { const savedAttachment = await attachments.saveAttachment(attachment); - const formatted = await utils.formatAttachment(attachment, savedAttachment.url); - logFormattedAttachments.push(formatted); - // Forward small attachments (<2MB) as attachments, link to larger ones if (config.relaySmallAttachmentsAsAttachments && attachment.size <= config.smallAttachmentLimit) { const file = await attachments.attachmentToDiscordFileObject(attachment); attachmentFiles.push(file); - } else { - threadFormattedAttachments.push(formatted); + smallAttachments.push(savedAttachment.url); } + + attachments.push(savedAttachment.url); } - // Save log entry - const logContent = formatters.formatUserReplyLogMessage(msg.author, msg, { attachmentLinks: logFormattedAttachments }); - const threadMessage = await this._addThreadMessageToDB({ + // Save DB entry + let threadMessage = new ThreadMessage({ message_type: THREAD_MESSAGE_TYPE.FROM_USER, user_id: this.user_id, - user_name: `${msg.author.username}#${msg.author.discriminator}`, - body: logContent, + user_name: fullUserName, + body: msg.content || "", is_anonymous: 0, - dm_message_id: msg.id + dm_message_id: msg.id, + attachments, + small_attachments: smallAttachments, }); + threadMessage = await this._addThreadMessageToDB(threadMessage.getSQLProps()); + // Show user reply in the inbox thread - const inboxContent = formatters.formatUserReplyThreadMessage(msg.author, msg, { attachmentLinks: threadFormattedAttachments }); + const inboxContent = formatters.formatUserReplyThreadMessage(threadMessage); const inboxMessage = await this._postToThreadChannel(inboxContent, attachmentFiles); - if (inboxMessage) await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); + if (inboxMessage) { + await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); + } // Interrupt scheduled closing, if in progress if (this.scheduled_close_at) { @@ -306,12 +317,11 @@ class Thread { async postSystemMessage(content, file = null, opts = {}) { const msg = await this._postToThreadChannel(content, file); if (msg && opts.saveToLog !== false) { - const finalLogBody = opts.logBody || msg.content || ""; await this._addThreadMessageToDB({ message_type: THREAD_MESSAGE_TYPE.SYSTEM, user_id: null, user_name: "", - body: finalLogBody, + body: msg.content || "", is_anonymous: 0, inbox_message_id: msg.id, }); @@ -329,12 +339,11 @@ class Thread { async sendSystemMessageToUser(content, file = null, opts = {}) { const msg = await this._sendDMToUser(content, file); if (opts.saveToLog !== false) { - const finalLogBody = opts.logBody || msg.content || ""; await this._addThreadMessageToDB({ message_type: THREAD_MESSAGE_TYPE.SYSTEM_TO_USER, user_id: null, user_name: "", - body: finalLogBody, + body: msg.content || "", is_anonymous: 0, dm_message_id: msg.id, }); @@ -355,6 +364,7 @@ class Thread { * @returns {Promise} */ async saveChatMessageToLogs(msg) { + // TODO: Save attachments? return this._addThreadMessageToDB({ message_type: THREAD_MESSAGE_TYPE.CHAT, user_id: msg.author.id, @@ -421,7 +431,7 @@ class Thread { const data = await knex("thread_messages") .where("thread_id", this.id) .where("message_number", messageNumber) - .select(); + .first(); return data ? new ThreadMessage(data) : null; } @@ -560,37 +570,23 @@ class Thread { * @returns {Promise} */ async editStaffReply(moderator, threadMessage, newText, opts = {}) { - const formattedThreadMessage = formatters.formatStaffReplyThreadMessage( - moderator, - newText, - threadMessage.message_number, - { isAnonymous: threadMessage.is_anonymous } - ); + const newThreadMessage = new ThreadMessage({ + ...threadMessage.getSQLProps(), + body: newText, + }); - const formattedDM = formatters.formatStaffReplyDM( - moderator, - newText, - { isAnonymous: threadMessage.is_anonymous } - ); - - // FIXME: Fix attachment links disappearing by moving them off the main message content in the DB - const formattedLog = formatters.formatStaffReplyLogMessage( - moderator, - newText, - threadMessage.message_number, - { isAnonymous: threadMessage.is_anonymous } - ); + const formattedThreadMessage = formatters.formatStaffReplyThreadMessage(newThreadMessage); + const formattedDM = formatters.formatStaffReplyDM(newThreadMessage); await bot.editMessage(threadMessage.dm_channel_id, threadMessage.dm_message_id, formattedDM); await bot.editMessage(this.channel_id, threadMessage.inbox_message_id, formattedThreadMessage); if (! opts.quiet) { - const threadNotification = formatters.formatStaffReplyEditNotificationThreadMessage(moderator, threadMessage, newText); - const logNotification = formatters.formatStaffReplyEditNotificationLogMessage(moderator, threadMessage, newText); - await this.postSystemMessage(threadNotification, null, { logBody: logNotification }); + const threadNotification = formatters.formatStaffReplyEditNotificationThreadMessage(threadMessage, newText, moderator); + await this.postSystemMessage(threadNotification); } - await this._updateThreadMessage(threadMessage.id, { body: formattedLog }); + await this._updateThreadMessage(threadMessage.id, { body: newText }); } /** @@ -605,9 +601,8 @@ class Thread { await bot.deleteMessage(this.channel_id, threadMessage.inbox_message_id); if (! opts.quiet) { - const threadNotification = formatters.formatStaffReplyDeletionNotificationThreadMessage(moderator, threadMessage); - const logNotification = formatters.formatStaffReplyDeletionNotificationLogMessage(moderator, threadMessage); - await this.postSystemMessage(threadNotification, null, { logBody: logNotification }); + const threadNotification = formatters.formatStaffReplyDeletionNotificationThreadMessage(threadMessage, moderator); + await this.postSystemMessage(threadNotification); } await this._deleteThreadMessage(threadMessage.id); diff --git a/src/data/ThreadMessage.js b/src/data/ThreadMessage.js index e132aaa..70e1d21 100644 --- a/src/data/ThreadMessage.js +++ b/src/data/ThreadMessage.js @@ -7,16 +7,48 @@ const utils = require("../utils"); * @property {Number} message_number * @property {String} user_id * @property {String} user_name + * @property {String} role_name * @property {String} body * @property {Number} is_anonymous + * @property {String[]} attachments + * @property {String[]} small_attachments The subset of attachments that were relayed when relaySmallAttachmentsAsAttachments is enabled * @property {String} dm_channel_id * @property {String} dm_message_id * @property {String} inbox_message_id * @property {String} created_at + * @property {Number} use_legacy_format */ class ThreadMessage { constructor(props) { utils.setDataModelProps(this, props); + + if (props.attachments) { + if (typeof props.attachments === "string") { + this.attachments = JSON.parse(props.attachments); + } + } else { + this.attachments = []; + } + + if (props.small_attachments) { + if (typeof props.small_attachments === "string") { + this.small_attachments = JSON.parse(props.small_attachments); + } + } else { + this.small_attachments = []; + } + } + + getSQLProps() { + return Object.entries(this).reduce((obj, [key, value]) => { + if (typeof value === "function") return obj; + if (typeof value === "object") { + obj[key] = JSON.stringify(value); + } else { + obj[key] = value; + } + return obj; + }, {}); } } diff --git a/src/formatters.js b/src/formatters.js index ea18eae..3e36e48 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -2,229 +2,208 @@ const Eris = require("eris"); const utils = require("./utils"); const config = require("./cfg"); const ThreadMessage = require("./data/ThreadMessage"); +const {THREAD_MESSAGE_TYPE} = require("./data/constants"); +const moment = require("moment"); /** * Function to format the DM that is sent to the user when a staff member replies to them via !reply * @callback FormatStaffReplyDM - * @param {Eris.Member} moderator Staff member that is replying - * @param {string} text Reply text - * @param {{ - * isAnonymous: boolean, - * }} opts={} + * @param {ThreadMessage} threadMessage * @return {Eris.MessageContent} Message content to send as a DM */ /** * Function to format a staff reply in a thread channel * @callback FormatStaffReplyThreadMessage - * @param {Eris.Member} moderator - * @param {string} text - * @param {number} messageNumber - * @param {{ - * isAnonymous: boolean, - * }} opts={} + * @param {ThreadMessage} threadMessage * @return {Eris.MessageContent} Message content to post in the thread channel */ -/** - * Function to format a staff reply in a log - * @callback FormatStaffReplyLogMessage - * @param {Eris.Member} moderator - * @param {string} text - * @param {number} messageNumber - * @param {{ - * isAnonymous: boolean, - * attachmentLinks: string[], - * }} opts={} - * @returns {string} Text to show in the log - */ - /** * Function to format a user reply in a thread channel * @callback FormatUserReplyThreadMessage - * @param {Eris.User} user Use that sent the reply - * @param {Eris.Message} msg The message object that the user sent - * @param {{ - * attachmentLinks: string[], - * }} opts + * @param {ThreadMessage} threadMessage * @return {Eris.MessageContent} Message content to post in the thread channel */ -/** - * Function to format a user reply in a log - * @callback FormatUserReplyLogMessage - * @param {Eris.User} user - * @param {Eris.Message} msg - * @param {{ - * attachmentLinks: string[], - * }} opts={} - * @return {string} Text to show in the log - */ - /** * Function to format the inbox channel notification for a staff reply edit * @callback FormatStaffReplyEditNotificationThreadMessage - * @param {Eris.Member} moderator * @param {ThreadMessage} threadMessage * @param {string} newText + * @param {Eris.Member} moderator Moderator that edited the message * @return {Eris.MessageContent} Message content to post in the thread channel */ -/** - * Function to format the log notification for a staff reply edit - * @callback FormatStaffReplyEditNotificationLogMessage - * @param {Eris.Member} moderator - * @param {ThreadMessage} threadMessage - * @param {string} newText - * @return {string} Text to show in the log - */ - /** * Function to format the inbox channel notification for a staff reply deletion * @callback FormatStaffReplyDeletionNotificationThreadMessage - * @param {Eris.Member} moderator * @param {ThreadMessage} threadMessage + * @param {Eris.Member} moderator Moderator that deleted the message * @return {Eris.MessageContent} Message content to post in the thread channel */ /** - * Function to format the log notification for a staff reply deletion - * @callback FormatStaffReplyDeletionNotificationLogMessage - * @param {Eris.Member} moderator - * @param {ThreadMessage} threadMessage - * @return {string} Text to show in the log + * @typedef {Object} FormatLogOptions + * @property {Boolean?} simple + * @property {Boolean?} verbose + */ + +/** + * @typedef {Object} FormatLogResult + * @property {String} content Contents of the entire log + * @property {*?} extra + */ + +/** + * Function to format the inbox channel notification for a staff reply deletion + * @callback FormatLog + * @param {Thread} thread + * @param {ThreadMessage[]} threadMessages + * @param {FormatLogOptions={}} opts + * @return {FormatLogResult} */ /** * @typedef MessageFormatters * @property {FormatStaffReplyDM} formatStaffReplyDM * @property {FormatStaffReplyThreadMessage} formatStaffReplyThreadMessage - * @property {FormatStaffReplyLogMessage} formatStaffReplyLogMessage * @property {FormatUserReplyThreadMessage} formatUserReplyThreadMessage - * @property {FormatUserReplyLogMessage} formatUserReplyLogMessage * @property {FormatStaffReplyEditNotificationThreadMessage} formatStaffReplyEditNotificationThreadMessage - * @property {FormatStaffReplyEditNotificationLogMessage} formatStaffReplyEditNotificationLogMessage * @property {FormatStaffReplyDeletionNotificationThreadMessage} formatStaffReplyDeletionNotificationThreadMessage - * @property {FormatStaffReplyDeletionNotificationLogMessage} formatStaffReplyDeletionNotificationLogMessage + * @property {FormatLog} formatLog */ /** * @type {MessageFormatters} */ const defaultFormatters = { - formatStaffReplyDM(moderator, text, opts = {}) { - const mainRole = utils.getMainRole(moderator); - const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username); - const modInfo = opts.isAnonymous - ? (mainRole ? mainRole.name : "Moderator") - : (mainRole ? `(${mainRole.name}) ${modName}` : modName); + formatStaffReplyDM(threadMessage) { + const modInfo = threadMessage.is_anonymous + ? (threadMessage.role_name ? threadMessage.role_name : "Moderator") + : (threadMessage.role_name ? `(${threadMessage.role_name}) ${threadMessage.user_name}` : threadMessage.user_name); - return `**${modInfo}:** ${text}`; + return `**${modInfo}:** ${threadMessage.body}`; }, - formatStaffReplyThreadMessage(moderator, text, messageNumber, opts = {}) { - const mainRole = utils.getMainRole(moderator); - const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username); - const modInfo = opts.isAnonymous - ? `(Anonymous) (${modName}) ${mainRole ? mainRole.name : "Moderator"}` - : (mainRole ? `(${mainRole.name}) ${modName}` : modName); + formatStaffReplyThreadMessage(threadMessage) { + const modInfo = threadMessage.is_anonymous + ? `(Anonymous) (${threadMessage.user_name}) ${threadMessage.role_name || "Moderator"}` + : (threadMessage.role_name ? `(${threadMessage.role_name}) ${threadMessage.user_name}` : threadMessage.user_name); - let result = `**${modInfo}:** ${text}`; + let result = `**${modInfo}:** ${threadMessage.body}`; if (config.threadTimestamps) { - const formattedTimestamp = utils.getTimestamp(); + const formattedTimestamp = utils.getTimestamp(threadMessage.created_at); result = `[${formattedTimestamp}] ${result}`; } - result = `\`[${messageNumber}]\` ${result}`; + result = `\`[${threadMessage.message_number}]\` ${result}`; return result; }, - formatStaffReplyLogMessage(moderator, text, messageNumber, opts = {}) { - const mainRole = utils.getMainRole(moderator); - const modName = moderator.user.username; + formatUserReplyThreadMessage(threadMessage) { + let result = `**${threadMessage.user_name}:** ${threadMessage.body}`; - // Mirroring the DM formatting here... - const modInfo = opts.isAnonymous - ? (mainRole ? mainRole.name : "Moderator") - : (mainRole ? `(${mainRole.name}) ${modName}` : modName); - - let result = `**${modInfo}:** ${text}`; - - if (opts.attachmentLinks && opts.attachmentLinks.length) { - result += "\n"; - for (const link of opts.attachmentLinks) { - result += `\n**Attachment:** ${link}`; - } - } - - result = `[${messageNumber}] ${result}`; - - return result; - }, - - formatUserReplyThreadMessage(user, msg, opts = {}) { - const content = (msg.content.trim() === "" && msg.embeds.length) - ? "" - : msg.content; - - let result = `**${user.username}#${user.discriminator}:** ${content}`; - - if (opts.attachmentLinks && opts.attachmentLinks.length) { - for (const link of opts.attachmentLinks) { - result += `\n\n${link}`; - } + for (const link of threadMessage.attachments) { + result += `\n\n${link}`; } if (config.threadTimestamps) { - const formattedTimestamp = utils.getTimestamp(msg.timestamp, "x"); + const formattedTimestamp = utils.getTimestamp(threadMessage.created_at); result = `[${formattedTimestamp}] ${result}`; } return result; }, - formatUserReplyLogMessage(user, msg, opts = {}) { - const content = (msg.content.trim() === "" && msg.embeds.length) - ? "" - : msg.content; - - let result = content; - - if (opts.attachmentLinks && opts.attachmentLinks.length) { - for (const link of opts.attachmentLinks) { - result += `\n\n${link}`; - } - } - - return result; - }, - - formatStaffReplyEditNotificationThreadMessage(moderator, threadMessage, newText) { - let content = `**${moderator.user.username}#${moderator.user.discriminator} (\`${moderator.id}\`) edited reply \`[${threadMessage.message_number}]\`:**`; + formatStaffReplyEditNotificationThreadMessage(threadMessage, newText, moderator) { + let content = `**${moderator.user.username}#${moderator.user.discriminator}** (\`${moderator.id}\`) edited reply \`[${threadMessage.message_number}]\`:`; content += `\n\`B:\` ${threadMessage.body}`; content += `\n\`A:\` ${newText}`; return utils.disableLinkPreviews(content); }, - formatStaffReplyEditNotificationLogMessage(moderator, threadMessage, newText) { - let content = `${moderator.user.username}#${moderator.user.discriminator} (${moderator.id}) edited reply [${threadMessage.message_number}]:`; - content += `\nB: ${threadMessage.body}`; - content += `\nA: ${newText}`; - return content; - }, - - formatStaffReplyDeletionNotificationThreadMessage(moderator, threadMessage) { + formatStaffReplyDeletionNotificationThreadMessage(threadMessage, moderator) { let content = `**${moderator.user.username}#${moderator.user.discriminator} (\`${moderator.id}\`) deleted reply \`[${threadMessage.message_number}]\`:**`; content += `\n\`B:\` ${threadMessage.body}`; return utils.disableLinkPreviews(content); }, - formatStaffReplyDeletionNotificationLogMessage(moderator, threadMessage) { - let content = `${moderator.user.username}#${moderator.user.discriminator} (${moderator.id}) deleted reply [${threadMessage.message_number}]:`; - content += `\nB: ${threadMessage.body}`; - return content; + formatLog(thread, threadMessages, opts = {}) { + if (opts.simple) { + threadMessages = threadMessages.filter(message => { + return ( + message.message_type !== THREAD_MESSAGE_TYPE.SYSTEM + && message.message_type !== THREAD_MESSAGE_TYPE.SYSTEM_TO_USER + && message.message_type !== THREAD_MESSAGE_TYPE.CHAT + && message.message_type !== THREAD_MESSAGE_TYPE.COMMAND + ); + }); + } + + const lines = threadMessages.map(message => { + // Legacy messages (from 2018) are the entire log in one message, so just serve them as they are + if (message.message_type === THREAD_MESSAGE_TYPE.LEGACY) { + return message.body; + } + + let line = `[${moment.utc(message.created_at).format("YYYY-MM-DD HH:mm:ss")}]`; + + if (opts.verbose) { + if (message.dm_channel_id) { + line += ` [DM CHA ${message.dm_channel_id}]`; + } + + if (message.dm_message_id) { + line += ` [DM MSG ${message.dm_message_id}]`; + } + } + + if (message.message_type === THREAD_MESSAGE_TYPE.FROM_USER) { + line += ` [FROM USER] [${message.user_name}] ${message.body}`; + } else if (message.message_type === THREAD_MESSAGE_TYPE.TO_USER) { + line += ` [TO USER] [${message.message_number || "0"}] [${message.user_name}]`; + if (message.use_legacy_format) { + // Legacy format (from pre-2.31.0) includes the role and username in the message body, so serve that as is + line += ` ${message.body}`; + } else if (message.is_anonymous) { + if (message.role_name) { + line += ` (Anonymous) ${message.role_name}: ${message.body}`; + } else { + line += ` (Anonymous) Moderator: ${message.body}`; + } + } else { + if (message.role_name) { + line += ` (${message.role_name}) ${message.user_name}: ${message.body}`; + } else { + line += ` ${message.user_name}: ${message.body}`; + } + } + } else if (message.message_type === THREAD_MESSAGE_TYPE.SYSTEM) { + line += ` [SYSTEM] ${message.body}`; + } else if (message.message_type === THREAD_MESSAGE_TYPE.SYSTEM_TO_USER) { + line += ` [SYSTEM TO USER] ${message.body}`; + } else if (message.message_type === THREAD_MESSAGE_TYPE.CHAT) { + line += ` [CHAT] [${message.user_name}] ${message.body}`; + } else if (message.message_type === THREAD_MESSAGE_TYPE.COMMAND) { + line += ` [COMMAND] [${message.user_name}] ${message.body}`; + } else { + line += ` [${message.user_name}] ${message.body}`; + } + + return line; + }); + + const openedAt = moment(thread.created_at).format("YYYY-MM-DD HH:mm:ss"); + const header = `# Modmail thread with ${thread.user_name} (${thread.user_id}) started at ${openedAt}. All times are in UTC+0.`; + + const fullResult = header + "\n\n" + lines.join("\n"); + + return { + content: fullResult, + }; }, }; @@ -234,7 +213,11 @@ const defaultFormatters = { const formatters = { ...defaultFormatters }; module.exports = { - formatters, + formatters: new Proxy(formatters, { + set() { + throw new Error("Please use the formatter setter functions instead of modifying the formatters directly"); + }, + }), /** * @param {FormatStaffReplyDM} fn @@ -252,14 +235,6 @@ module.exports = { formatters.formatStaffReplyThreadMessage = fn; }, - /** - * @param {FormatStaffReplyLogMessage} fn - * @return {void} - */ - setStaffReplyLogMessageFormatter(fn) { - formatters.formatStaffReplyLogMessage = fn; - }, - /** * @param {FormatUserReplyThreadMessage} fn * @return {void} @@ -268,14 +243,6 @@ module.exports = { formatters.formatUserReplyThreadMessage = fn; }, - /** - * @param {FormatUserReplyLogMessage} fn - * @return {void} - */ - setUserReplyLogMessageFormatter(fn) { - formatters.formatUserReplyLogMessage = fn; - }, - /** * @param {FormatStaffReplyEditNotificationThreadMessage} fn * @return {void} @@ -284,14 +251,6 @@ module.exports = { formatters.formatStaffReplyEditNotificationThreadMessage = fn; }, - /** - * @param {FormatStaffReplyEditNotificationLogMessage} fn - * @return {void} - */ - setStaffReplyEditNotificationLogMessageFormatter(fn) { - formatters.formatStaffReplyEditNotificationLogMessage = fn; - }, - /** * @param {FormatStaffReplyDeletionNotificationThreadMessage} fn * @return {void} @@ -301,10 +260,10 @@ module.exports = { }, /** - * @param {FormatStaffReplyDeletionNotificationLogMessage} fn + * @param {FormatLog} fn * @return {void} */ - setStaffReplyDeletionNotificationLogMessageFormatter(fn) { - formatters.formatStaffReplyDeletionNotificationLogMessage = fn; + setLogFormatter(fn) { + formatters.formatLog = fn; }, }; diff --git a/src/modules/reply.js b/src/modules/reply.js index 146469e..693850d 100644 --- a/src/modules/reply.js +++ b/src/modules/reply.js @@ -39,6 +39,7 @@ module.exports = ({ bot, knex, config, commands }) => { return; } + console.log(threadMessage.user_id, msg.author.id); if (threadMessage.user_id !== msg.author.id) { utils.postError(msg.channel, "You can only edit your own replies"); return; diff --git a/src/modules/webserver.js b/src/modules/webserver.js index 9f22f47..0a04401 100644 --- a/src/modules/webserver.js +++ b/src/modules/webserver.js @@ -7,8 +7,7 @@ const moment = require("moment"); const config = require("../cfg"); const threads = require("../data/threads"); const attachments = require("../data/attachments"); - -const {THREAD_MESSAGE_TYPE} = require("../data/constants"); +const { formatters } = require("../formatters"); function notfound(res) { res.statusCode = 404; @@ -24,61 +23,15 @@ async function serveLogs(req, res, pathParts, query) { let threadMessages = await thread.getThreadMessages(); - if (query.simple) { - threadMessages = threadMessages.filter(message => { - return ( - message.message_type !== THREAD_MESSAGE_TYPE.SYSTEM - && message.message_type !== THREAD_MESSAGE_TYPE.SYSTEM_TO_USER - && message.message_type !== THREAD_MESSAGE_TYPE.CHAT - && message.message_type !== THREAD_MESSAGE_TYPE.COMMAND - ); - }); - } - - const lines = threadMessages.map(message => { - // Legacy messages are the entire log in one message, so just serve them as they are - if (message.message_type === THREAD_MESSAGE_TYPE.LEGACY) { - return message.body; - } - - let line = `[${moment.utc(message.created_at).format("YYYY-MM-DD HH:mm:ss")}]`; - - if (query.verbose) { - if (message.dm_channel_id) { - line += ` [DM CHA ${message.dm_channel_id}]`; - } - - if (message.dm_message_id) { - line += ` [DM MSG ${message.dm_message_id}]`; - } - } - - if (message.message_type === THREAD_MESSAGE_TYPE.FROM_USER) { - line += ` [FROM USER] [${message.user_name}] ${message.body}`; - } else if (message.message_type === THREAD_MESSAGE_TYPE.TO_USER) { - line += ` [TO USER] [${message.user_name}] ${message.body}`; - } else if (message.message_type === THREAD_MESSAGE_TYPE.SYSTEM) { - line += ` [SYSTEM] ${message.body}`; - } else if (message.message_type === THREAD_MESSAGE_TYPE.SYSTEM_TO_USER) { - line += ` [SYSTEM TO USER] ${message.body}`; - } else if (message.message_type === THREAD_MESSAGE_TYPE.CHAT) { - line += ` [CHAT] [${message.user_name}] ${message.body}`; - } else if (message.message_type === THREAD_MESSAGE_TYPE.COMMAND) { - line += ` [COMMAND] [${message.user_name}] ${message.body}`; - } else { - line += ` [${message.user_name}] ${message.body}`; - } - - return line; + const formatLogResult = await formatters.formatLog(thread, threadMessages, { + simple: Boolean(query.simple), + verbose: Boolean(query.verbose), }); - const openedAt = moment(thread.created_at).format("YYYY-MM-DD HH:mm:ss"); - const header = `# Modmail thread with ${thread.user_name} (${thread.user_id}) started at ${openedAt}. All times are in UTC+0.`; + const contentType = formatLogResult.extra && formatLogResult.extra.contentType || "text/plain; charset=UTF-8"; - const fullResponse = header + "\n\n" + lines.join("\n"); - - res.setHeader("Content-Type", "text/plain; charset=UTF-8"); - res.end(fullResponse); + res.setHeader("Content-Type", contentType); + res.end(formatLogResult.content); } function serveAttachments(req, res, pathParts) {