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
*/
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<void>}
*/
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);
}
// Save log entry
const logContent = formatters.formatUserReplyLogMessage(msg.author, msg, { attachmentLinks: logFormattedAttachments });
const threadMessage = await this._addThreadMessageToDB({
attachments.push(savedAttachment.url);
}
// 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 || "<empty message>";
await this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.SYSTEM,
user_id: null,
user_name: "",
body: finalLogBody,
body: msg.content || "<empty message>",
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 || "<empty message>";
await this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.SYSTEM_TO_USER,
user_id: null,
user_name: "",
body: finalLogBody,
body: msg.content || "<empty message>",
is_anonymous: 0,
dm_message_id: msg.id,
});
@ -355,6 +364,7 @@ class Thread {
* @returns {Promise<void>}
*/
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<void>}
*/
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);

View File

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

View File

@ -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)
? "<message contains embeds>"
: msg.content;
let result = `**${user.username}#${user.discriminator}:** ${content}`;
if (opts.attachmentLinks && opts.attachmentLinks.length) {
for (const link of opts.attachmentLinks) {
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)
? "<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}]\`:**`;
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;
},
};

View File

@ -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;

View File

@ -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) {