From 296d1304a7ffeec1a4649ea7d61fd7d179097909 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 14 Aug 2020 00:42:32 +0300 Subject: [PATCH] Reproducible formatters, add full log formatter Format-specific parts of replies, including the role name and attachments, are now stored in separate columns. This allows us to store only one version of the actual message body and, by keeping format-specific data separate, reproduce formatter results regardless of when they are called. This cleans up code around message formats significantly and was required to support !edit/!delete properly. --- ...00813230319_separate_message_components.js | 21 ++ src/data/Thread.js | 121 ++++--- src/data/ThreadMessage.js | 32 ++ src/formatters.js | 295 ++++++++---------- src/modules/reply.js | 1 + src/modules/webserver.js | 61 +--- 6 files changed, 246 insertions(+), 285 deletions(-) create mode 100644 db/migrations/20200813230319_separate_message_components.js 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) {