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.
cshd
Dragory 2020-08-14 00:42:32 +03:00
parent 98532be55a
commit 296d1304a7
No known key found for this signature in database
GPG Key ID: 5F387BA66DF8AAC1
6 changed files with 246 additions and 285 deletions

View File

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

View File

@ -178,7 +178,9 @@ class Thread {
* @returns {Promise<boolean>} Whether we were able to send the reply * @returns {Promise<boolean>} Whether we were able to send the reply
*/ */
async replyToUser(moderator, text, replyAttachments = [], isAnonymous = false) { 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 // Prepare attachments, if any
const files = []; 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 // Send the reply DM
const dmContent = formatters.formatStaffReplyDM(moderator, text, { isAnonymous }); const dmContent = formatters.formatStaffReplyDM(threadMessage);
let dmMessage; let dmMessage;
try { try {
dmMessage = await this._sendDMToUser(dmContent, files); dmMessage = await this._sendDMToUser(dmContent, files);
@ -208,21 +220,17 @@ class Thread {
} }
// Save the log entry // Save the log entry
const threadMessage = await this._addThreadMessageToDB({ threadMessage = await this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.TO_USER, ...threadMessage.getSQLProps(),
user_id: moderator.id, dm_message_id: dmMessage.id,
user_name: fullModeratorName,
body: "",
is_anonymous: (isAnonymous ? 1 : 0),
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 // 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); 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 // Interrupt scheduled closing, if in progress
if (this.scheduled_close_at) { if (this.scheduled_close_at) {
@ -238,43 +246,46 @@ class Thread {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async receiveUserReply(msg) { async receiveUserReply(msg) {
// Prepare attachments const fullUserName = `${msg.author.username}#${msg.author.discriminator}`;
const attachmentFiles = [];
const threadFormattedAttachments = []; // Prepare attachments
const logFormattedAttachments = []; 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) { for (const attachment of msg.attachments) {
const savedAttachment = await attachments.saveAttachment(attachment); 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 // Forward small attachments (<2MB) as attachments, link to larger ones
if (config.relaySmallAttachmentsAsAttachments && attachment.size <= config.smallAttachmentLimit) { if (config.relaySmallAttachmentsAsAttachments && attachment.size <= config.smallAttachmentLimit) {
const file = await attachments.attachmentToDiscordFileObject(attachment); const file = await attachments.attachmentToDiscordFileObject(attachment);
attachmentFiles.push(file); attachmentFiles.push(file);
} else { smallAttachments.push(savedAttachment.url);
threadFormattedAttachments.push(formatted);
} }
attachments.push(savedAttachment.url);
} }
// Save log entry // Save DB entry
const logContent = formatters.formatUserReplyLogMessage(msg.author, msg, { attachmentLinks: logFormattedAttachments }); let threadMessage = new ThreadMessage({
const threadMessage = await this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.FROM_USER, message_type: THREAD_MESSAGE_TYPE.FROM_USER,
user_id: this.user_id, user_id: this.user_id,
user_name: `${msg.author.username}#${msg.author.discriminator}`, user_name: fullUserName,
body: logContent, body: msg.content || "",
is_anonymous: 0, 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 // 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); 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 // Interrupt scheduled closing, if in progress
if (this.scheduled_close_at) { if (this.scheduled_close_at) {
@ -306,12 +317,11 @@ class Thread {
async postSystemMessage(content, file = null, opts = {}) { async postSystemMessage(content, file = null, opts = {}) {
const msg = await this._postToThreadChannel(content, file); const msg = await this._postToThreadChannel(content, file);
if (msg && opts.saveToLog !== false) { if (msg && opts.saveToLog !== false) {
const finalLogBody = opts.logBody || msg.content || "<empty message>";
await this._addThreadMessageToDB({ await this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.SYSTEM, message_type: THREAD_MESSAGE_TYPE.SYSTEM,
user_id: null, user_id: null,
user_name: "", user_name: "",
body: finalLogBody, body: msg.content || "<empty message>",
is_anonymous: 0, is_anonymous: 0,
inbox_message_id: msg.id, inbox_message_id: msg.id,
}); });
@ -329,12 +339,11 @@ class Thread {
async sendSystemMessageToUser(content, file = null, opts = {}) { async sendSystemMessageToUser(content, file = null, opts = {}) {
const msg = await this._sendDMToUser(content, file); const msg = await this._sendDMToUser(content, file);
if (opts.saveToLog !== false) { if (opts.saveToLog !== false) {
const finalLogBody = opts.logBody || msg.content || "<empty message>";
await this._addThreadMessageToDB({ await this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.SYSTEM_TO_USER, message_type: THREAD_MESSAGE_TYPE.SYSTEM_TO_USER,
user_id: null, user_id: null,
user_name: "", user_name: "",
body: finalLogBody, body: msg.content || "<empty message>",
is_anonymous: 0, is_anonymous: 0,
dm_message_id: msg.id, dm_message_id: msg.id,
}); });
@ -355,6 +364,7 @@ class Thread {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async saveChatMessageToLogs(msg) { async saveChatMessageToLogs(msg) {
// TODO: Save attachments?
return this._addThreadMessageToDB({ return this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.CHAT, message_type: THREAD_MESSAGE_TYPE.CHAT,
user_id: msg.author.id, user_id: msg.author.id,
@ -421,7 +431,7 @@ class Thread {
const data = await knex("thread_messages") const data = await knex("thread_messages")
.where("thread_id", this.id) .where("thread_id", this.id)
.where("message_number", messageNumber) .where("message_number", messageNumber)
.select(); .first();
return data ? new ThreadMessage(data) : null; return data ? new ThreadMessage(data) : null;
} }
@ -560,37 +570,23 @@ class Thread {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async editStaffReply(moderator, threadMessage, newText, opts = {}) { async editStaffReply(moderator, threadMessage, newText, opts = {}) {
const formattedThreadMessage = formatters.formatStaffReplyThreadMessage( const newThreadMessage = new ThreadMessage({
moderator, ...threadMessage.getSQLProps(),
newText, body: newText,
threadMessage.message_number, });
{ isAnonymous: threadMessage.is_anonymous }
);
const formattedDM = formatters.formatStaffReplyDM( const formattedThreadMessage = formatters.formatStaffReplyThreadMessage(newThreadMessage);
moderator, const formattedDM = formatters.formatStaffReplyDM(newThreadMessage);
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 }
);
await bot.editMessage(threadMessage.dm_channel_id, threadMessage.dm_message_id, formattedDM); await bot.editMessage(threadMessage.dm_channel_id, threadMessage.dm_message_id, formattedDM);
await bot.editMessage(this.channel_id, threadMessage.inbox_message_id, formattedThreadMessage); await bot.editMessage(this.channel_id, threadMessage.inbox_message_id, formattedThreadMessage);
if (! opts.quiet) { if (! opts.quiet) {
const threadNotification = formatters.formatStaffReplyEditNotificationThreadMessage(moderator, threadMessage, newText); const threadNotification = formatters.formatStaffReplyEditNotificationThreadMessage(threadMessage, newText, moderator);
const logNotification = formatters.formatStaffReplyEditNotificationLogMessage(moderator, threadMessage, newText); await this.postSystemMessage(threadNotification);
await this.postSystemMessage(threadNotification, null, { logBody: logNotification });
} }
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); await bot.deleteMessage(this.channel_id, threadMessage.inbox_message_id);
if (! opts.quiet) { if (! opts.quiet) {
const threadNotification = formatters.formatStaffReplyDeletionNotificationThreadMessage(moderator, threadMessage); const threadNotification = formatters.formatStaffReplyDeletionNotificationThreadMessage(threadMessage, moderator);
const logNotification = formatters.formatStaffReplyDeletionNotificationLogMessage(moderator, threadMessage); await this.postSystemMessage(threadNotification);
await this.postSystemMessage(threadNotification, null, { logBody: logNotification });
} }
await this._deleteThreadMessage(threadMessage.id); await this._deleteThreadMessage(threadMessage.id);

View File

@ -7,16 +7,48 @@ const utils = require("../utils");
* @property {Number} message_number * @property {Number} message_number
* @property {String} user_id * @property {String} user_id
* @property {String} user_name * @property {String} user_name
* @property {String} role_name
* @property {String} body * @property {String} body
* @property {Number} is_anonymous * @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_channel_id
* @property {String} dm_message_id * @property {String} dm_message_id
* @property {String} inbox_message_id * @property {String} inbox_message_id
* @property {String} created_at * @property {String} created_at
* @property {Number} use_legacy_format
*/ */
class ThreadMessage { class ThreadMessage {
constructor(props) { constructor(props) {
utils.setDataModelProps(this, 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;
}, {});
} }
} }

View File

@ -2,229 +2,208 @@ const Eris = require("eris");
const utils = require("./utils"); const utils = require("./utils");
const config = require("./cfg"); const config = require("./cfg");
const ThreadMessage = require("./data/ThreadMessage"); 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 * Function to format the DM that is sent to the user when a staff member replies to them via !reply
* @callback FormatStaffReplyDM * @callback FormatStaffReplyDM
* @param {Eris.Member} moderator Staff member that is replying * @param {ThreadMessage} threadMessage
* @param {string} text Reply text
* @param {{
* isAnonymous: boolean,
* }} opts={}
* @return {Eris.MessageContent} Message content to send as a DM * @return {Eris.MessageContent} Message content to send as a DM
*/ */
/** /**
* Function to format a staff reply in a thread channel * Function to format a staff reply in a thread channel
* @callback FormatStaffReplyThreadMessage * @callback FormatStaffReplyThreadMessage
* @param {Eris.Member} moderator * @param {ThreadMessage} threadMessage
* @param {string} text
* @param {number} messageNumber
* @param {{
* isAnonymous: boolean,
* }} opts={}
* @return {Eris.MessageContent} Message content to post in the thread channel * @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 * Function to format a user reply in a thread channel
* @callback FormatUserReplyThreadMessage * @callback FormatUserReplyThreadMessage
* @param {Eris.User} user Use that sent the reply * @param {ThreadMessage} threadMessage
* @param {Eris.Message} msg The message object that the user sent
* @param {{
* attachmentLinks: string[],
* }} opts
* @return {Eris.MessageContent} Message content to post in the thread channel * @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 * Function to format the inbox channel notification for a staff reply edit
* @callback FormatStaffReplyEditNotificationThreadMessage * @callback FormatStaffReplyEditNotificationThreadMessage
* @param {Eris.Member} moderator
* @param {ThreadMessage} threadMessage * @param {ThreadMessage} threadMessage
* @param {string} newText * @param {string} newText
* @param {Eris.Member} moderator Moderator that edited the message
* @return {Eris.MessageContent} Message content to post in the thread channel * @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 * Function to format the inbox channel notification for a staff reply deletion
* @callback FormatStaffReplyDeletionNotificationThreadMessage * @callback FormatStaffReplyDeletionNotificationThreadMessage
* @param {Eris.Member} moderator
* @param {ThreadMessage} threadMessage * @param {ThreadMessage} threadMessage
* @param {Eris.Member} moderator Moderator that deleted the message
* @return {Eris.MessageContent} Message content to post in the thread channel * @return {Eris.MessageContent} Message content to post in the thread channel
*/ */
/** /**
* Function to format the log notification for a staff reply deletion * @typedef {Object} FormatLogOptions
* @callback FormatStaffReplyDeletionNotificationLogMessage * @property {Boolean?} simple
* @param {Eris.Member} moderator * @property {Boolean?} verbose
* @param {ThreadMessage} threadMessage */
* @return {string} Text to show in the log
/**
* @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 * @typedef MessageFormatters
* @property {FormatStaffReplyDM} formatStaffReplyDM * @property {FormatStaffReplyDM} formatStaffReplyDM
* @property {FormatStaffReplyThreadMessage} formatStaffReplyThreadMessage * @property {FormatStaffReplyThreadMessage} formatStaffReplyThreadMessage
* @property {FormatStaffReplyLogMessage} formatStaffReplyLogMessage
* @property {FormatUserReplyThreadMessage} formatUserReplyThreadMessage * @property {FormatUserReplyThreadMessage} formatUserReplyThreadMessage
* @property {FormatUserReplyLogMessage} formatUserReplyLogMessage
* @property {FormatStaffReplyEditNotificationThreadMessage} formatStaffReplyEditNotificationThreadMessage * @property {FormatStaffReplyEditNotificationThreadMessage} formatStaffReplyEditNotificationThreadMessage
* @property {FormatStaffReplyEditNotificationLogMessage} formatStaffReplyEditNotificationLogMessage
* @property {FormatStaffReplyDeletionNotificationThreadMessage} formatStaffReplyDeletionNotificationThreadMessage * @property {FormatStaffReplyDeletionNotificationThreadMessage} formatStaffReplyDeletionNotificationThreadMessage
* @property {FormatStaffReplyDeletionNotificationLogMessage} formatStaffReplyDeletionNotificationLogMessage * @property {FormatLog} formatLog
*/ */
/** /**
* @type {MessageFormatters} * @type {MessageFormatters}
*/ */
const defaultFormatters = { const defaultFormatters = {
formatStaffReplyDM(moderator, text, opts = {}) { formatStaffReplyDM(threadMessage) {
const mainRole = utils.getMainRole(moderator); const modInfo = threadMessage.is_anonymous
const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username); ? (threadMessage.role_name ? threadMessage.role_name : "Moderator")
const modInfo = opts.isAnonymous : (threadMessage.role_name ? `(${threadMessage.role_name}) ${threadMessage.user_name}` : threadMessage.user_name);
? (mainRole ? mainRole.name : "Moderator")
: (mainRole ? `(${mainRole.name}) ${modName}` : modName);
return `**${modInfo}:** ${text}`; return `**${modInfo}:** ${threadMessage.body}`;
}, },
formatStaffReplyThreadMessage(moderator, text, messageNumber, opts = {}) { formatStaffReplyThreadMessage(threadMessage) {
const mainRole = utils.getMainRole(moderator); const modInfo = threadMessage.is_anonymous
const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username); ? `(Anonymous) (${threadMessage.user_name}) ${threadMessage.role_name || "Moderator"}`
const modInfo = opts.isAnonymous : (threadMessage.role_name ? `(${threadMessage.role_name}) ${threadMessage.user_name}` : threadMessage.user_name);
? `(Anonymous) (${modName}) ${mainRole ? mainRole.name : "Moderator"}`
: (mainRole ? `(${mainRole.name}) ${modName}` : modName);
let result = `**${modInfo}:** ${text}`; let result = `**${modInfo}:** ${threadMessage.body}`;
if (config.threadTimestamps) { if (config.threadTimestamps) {
const formattedTimestamp = utils.getTimestamp(); const formattedTimestamp = utils.getTimestamp(threadMessage.created_at);
result = `[${formattedTimestamp}] ${result}`; result = `[${formattedTimestamp}] ${result}`;
} }
result = `\`[${messageNumber}]\` ${result}`; result = `\`[${threadMessage.message_number}]\` ${result}`;
return result; return result;
}, },
formatStaffReplyLogMessage(moderator, text, messageNumber, opts = {}) { formatUserReplyThreadMessage(threadMessage) {
const mainRole = utils.getMainRole(moderator); let result = `**${threadMessage.user_name}:** ${threadMessage.body}`;
const modName = moderator.user.username;
// Mirroring the DM formatting here... for (const link of threadMessage.attachments) {
const modInfo = opts.isAnonymous result += `\n\n${link}`;
? (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)
? "<message contains embeds>"
: 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}`;
}
} }
if (config.threadTimestamps) { if (config.threadTimestamps) {
const formattedTimestamp = utils.getTimestamp(msg.timestamp, "x"); const formattedTimestamp = utils.getTimestamp(threadMessage.created_at);
result = `[${formattedTimestamp}] ${result}`; result = `[${formattedTimestamp}] ${result}`;
} }
return result; return result;
}, },
formatUserReplyLogMessage(user, msg, opts = {}) { formatStaffReplyEditNotificationThreadMessage(threadMessage, newText, moderator) {
const content = (msg.content.trim() === "" && msg.embeds.length) let content = `**${moderator.user.username}#${moderator.user.discriminator}** (\`${moderator.id}\`) edited reply \`[${threadMessage.message_number}]\`:`;
? "<message contains embeds>"
: 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}]\`:**`;
content += `\n\`B:\` ${threadMessage.body}`; content += `\n\`B:\` ${threadMessage.body}`;
content += `\n\`A:\` ${newText}`; content += `\n\`A:\` ${newText}`;
return utils.disableLinkPreviews(content); return utils.disableLinkPreviews(content);
}, },
formatStaffReplyEditNotificationLogMessage(moderator, threadMessage, newText) { formatStaffReplyDeletionNotificationThreadMessage(threadMessage, moderator) {
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) {
let content = `**${moderator.user.username}#${moderator.user.discriminator} (\`${moderator.id}\`) deleted reply \`[${threadMessage.message_number}]\`:**`; let content = `**${moderator.user.username}#${moderator.user.discriminator} (\`${moderator.id}\`) deleted reply \`[${threadMessage.message_number}]\`:**`;
content += `\n\`B:\` ${threadMessage.body}`; content += `\n\`B:\` ${threadMessage.body}`;
return utils.disableLinkPreviews(content); return utils.disableLinkPreviews(content);
}, },
formatStaffReplyDeletionNotificationLogMessage(moderator, threadMessage) { formatLog(thread, threadMessages, opts = {}) {
let content = `${moderator.user.username}#${moderator.user.discriminator} (${moderator.id}) deleted reply [${threadMessage.message_number}]:`; if (opts.simple) {
content += `\nB: ${threadMessage.body}`; threadMessages = threadMessages.filter(message => {
return content; 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 }; const formatters = { ...defaultFormatters };
module.exports = { 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 * @param {FormatStaffReplyDM} fn
@ -252,14 +235,6 @@ module.exports = {
formatters.formatStaffReplyThreadMessage = fn; formatters.formatStaffReplyThreadMessage = fn;
}, },
/**
* @param {FormatStaffReplyLogMessage} fn
* @return {void}
*/
setStaffReplyLogMessageFormatter(fn) {
formatters.formatStaffReplyLogMessage = fn;
},
/** /**
* @param {FormatUserReplyThreadMessage} fn * @param {FormatUserReplyThreadMessage} fn
* @return {void} * @return {void}
@ -268,14 +243,6 @@ module.exports = {
formatters.formatUserReplyThreadMessage = fn; formatters.formatUserReplyThreadMessage = fn;
}, },
/**
* @param {FormatUserReplyLogMessage} fn
* @return {void}
*/
setUserReplyLogMessageFormatter(fn) {
formatters.formatUserReplyLogMessage = fn;
},
/** /**
* @param {FormatStaffReplyEditNotificationThreadMessage} fn * @param {FormatStaffReplyEditNotificationThreadMessage} fn
* @return {void} * @return {void}
@ -284,14 +251,6 @@ module.exports = {
formatters.formatStaffReplyEditNotificationThreadMessage = fn; formatters.formatStaffReplyEditNotificationThreadMessage = fn;
}, },
/**
* @param {FormatStaffReplyEditNotificationLogMessage} fn
* @return {void}
*/
setStaffReplyEditNotificationLogMessageFormatter(fn) {
formatters.formatStaffReplyEditNotificationLogMessage = fn;
},
/** /**
* @param {FormatStaffReplyDeletionNotificationThreadMessage} fn * @param {FormatStaffReplyDeletionNotificationThreadMessage} fn
* @return {void} * @return {void}
@ -301,10 +260,10 @@ module.exports = {
}, },
/** /**
* @param {FormatStaffReplyDeletionNotificationLogMessage} fn * @param {FormatLog} fn
* @return {void} * @return {void}
*/ */
setStaffReplyDeletionNotificationLogMessageFormatter(fn) { setLogFormatter(fn) {
formatters.formatStaffReplyDeletionNotificationLogMessage = fn; formatters.formatLog = fn;
}, },
}; };

View File

@ -39,6 +39,7 @@ module.exports = ({ bot, knex, config, commands }) => {
return; return;
} }
console.log(threadMessage.user_id, msg.author.id);
if (threadMessage.user_id !== msg.author.id) { if (threadMessage.user_id !== msg.author.id) {
utils.postError(msg.channel, "You can only edit your own replies"); utils.postError(msg.channel, "You can only edit your own replies");
return; return;

View File

@ -7,8 +7,7 @@ const moment = require("moment");
const config = require("../cfg"); const config = require("../cfg");
const threads = require("../data/threads"); const threads = require("../data/threads");
const attachments = require("../data/attachments"); const attachments = require("../data/attachments");
const { formatters } = require("../formatters");
const {THREAD_MESSAGE_TYPE} = require("../data/constants");
function notfound(res) { function notfound(res) {
res.statusCode = 404; res.statusCode = 404;
@ -24,61 +23,15 @@ async function serveLogs(req, res, pathParts, query) {
let threadMessages = await thread.getThreadMessages(); let threadMessages = await thread.getThreadMessages();
if (query.simple) { const formatLogResult = await formatters.formatLog(thread, threadMessages, {
threadMessages = threadMessages.filter(message => { simple: Boolean(query.simple),
return ( verbose: Boolean(query.verbose),
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 openedAt = moment(thread.created_at).format("YYYY-MM-DD HH:mm:ss"); const contentType = formatLogResult.extra && formatLogResult.extra.contentType || "text/plain; charset=UTF-8";
const header = `# Modmail thread with ${thread.user_name} (${thread.user_id}) started at ${openedAt}. All times are in UTC+0.`;
const fullResponse = header + "\n\n" + lines.join("\n"); res.setHeader("Content-Type", contentType);
res.end(formatLogResult.content);
res.setHeader("Content-Type", "text/plain; charset=UTF-8");
res.end(fullResponse);
} }
function serveAttachments(req, res, pathParts) { function serveAttachments(req, res, pathParts) {