More consistent log formatting. Store DM channel IDs and inbox message IDs with thread messages. Add !dm_channel_id. Add message numbers to the database in preparation for !edit and !delete. Some code reorganisation.

master
Dragory 2020-05-25 01:33:10 +03:00
parent 320fad9823
commit 6eb9b973c0
No known key found for this signature in database
GPG Key ID: 5F387BA66DF8AAC1
11 changed files with 417 additions and 172 deletions

View File

@ -0,0 +1,19 @@
const Knex = require('knex');
/**
* @param {Knex} knex
*/
exports.up = async function(knex) {
await knex.schema.table('thread_messages', table => {
table.integer('message_number').unsigned().nullable();
});
};
/**
* @param {Knex} knex
*/
exports.down = async function(knex) {
await knex.schema.table('thread_messages', table => {
table.dropColumn('message_number');
});
};

View File

@ -0,0 +1,19 @@
const Knex = require('knex');
/**
* @param {Knex} knex
*/
exports.up = async function(knex) {
await knex.schema.table('thread_messages', table => {
table.string('inbox_message_id', 20).nullable().unique();
});
};
/**
* @param {Knex} knex
*/
exports.down = async function(knex) {
await knex.schema.table('thread_messages', table => {
table.dropColumn('inbox_message_id');
});
};

View File

@ -0,0 +1,19 @@
const Knex = require('knex');
/**
* @param {Knex} knex
*/
exports.up = async function(knex) {
await knex.schema.table('thread_messages', table => {
table.string('dm_channel_id', 20).nullable();
});
};
/**
* @param {Knex} knex
*/
exports.down = async function(knex) {
await knex.schema.table('thread_messages', table => {
table.dropColumn('dm_channel_id');
});
};

View File

@ -29,165 +29,140 @@ class Thread {
} }
/** /**
* @param {Eris~Member} moderator * @param {Eris.Member} moderator
* @param {String} text * @param {string} text
* @param {Eris~MessageFile[]} replyAttachments * @param {boolean} isAnonymous
* @param {Boolean} isAnonymous * @returns {string}
* @returns {Promise<boolean>} Whether we were able to send the reply * @private
*/ */
async replyToUser(moderator, text, replyAttachments = [], isAnonymous = false) { _formatStaffReplyDM(moderator, text, isAnonymous) {
// Username to reply with
let modUsername, logModUsername;
const mainRole = utils.getMainRole(moderator); const mainRole = utils.getMainRole(moderator);
const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username);
const modInfo = isAnonymous
? (mainRole ? mainRole.name : 'Moderator')
: (mainRole ? `(${mainRole.name}) ${modName}` : modName);
if (isAnonymous) { return `**${modInfo}:** ${text}`;
modUsername = (mainRole ? mainRole.name : 'Moderator'); }
logModUsername = `(Anonymous) (${moderator.user.username}) ${mainRole ? mainRole.name : 'Moderator'}`;
} else {
const name = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username);
modUsername = (mainRole ? `(${mainRole.name}) ${name}` : name);
logModUsername = modUsername;
}
// Build the reply message /**
let dmContent = `**${modUsername}:** ${text}`; * @param {Eris.Member} moderator
let threadContent = `**${logModUsername}:** ${text}`; * @param {string} text
let logContent = text; * @param {boolean} isAnonymous
* @param {number} messageNumber
* @param {number} timestamp
* @returns {string}
* @private
*/
_formatStaffReplyThreadMessage(moderator, text, isAnonymous, messageNumber, timestamp) {
const mainRole = utils.getMainRole(moderator);
const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username);
const modInfo = isAnonymous
? `(Anonymous) (${modName}) ${mainRole ? mainRole.name : 'Moderator'}`
: (mainRole ? `(${mainRole.name}) ${modName}` : modName);
// TODO: Add \`[${messageNumber}]\` here once !edit and !delete exist
let result = `**${modInfo}:** ${text}`;
if (config.threadTimestamps) { if (config.threadTimestamps) {
const timestamp = utils.getTimestamp(); const formattedTimestamp = timestamp ? utils.getTimestamp(timestamp, 'x') : utils.getTimestamp();
threadContent = `[${timestamp}] » ${threadContent}`; result = `[${formattedTimestamp}] ${result}`;
} }
// Prepare attachments, if any return result;
let files = []; }
if (replyAttachments.length > 0) { /**
for (const attachment of replyAttachments) { * @param {Eris.Member} moderator
let savedAttachment; * @param {string} text
* @param {boolean} isAnonymous
* @param {string[]} attachmentLinks
* @returns {string}
* @private
*/
_formatStaffReplyLogMessage(moderator, text, isAnonymous, attachmentLinks = []) {
const mainRole = utils.getMainRole(moderator);
const modName = moderator.user.username;
await Promise.all([ // Mirroring the DM formatting here...
attachments.attachmentToDiscordFileObject(attachment).then(file => { const modInfo = isAnonymous
files.push(file); ? (mainRole ? mainRole.name : 'Moderator')
}), : (mainRole ? `(${mainRole.name}) ${modName}` : modName);
attachments.saveAttachment(attachment).then(result => {
savedAttachment = result;
})
]);
logContent += `\n\n**Attachment:** ${savedAttachment.url}`; let result = `**${modInfo}:** ${text}`;
if (attachmentLinks.length) {
result += '\n';
for (const link of attachmentLinks) {
result += `\n**Attachment:** ${link}`;
} }
} }
// Send the reply DM return result;
let dmMessage;
try {
dmMessage = await this.postToUser(dmContent, files);
} catch (e) {
await this.addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.COMMAND,
user_id: moderator.id,
user_name: logModUsername,
body: logContent
});
await this.postSystemMessage(`Error while replying to user: ${e.message}`);
return false;
}
// Send the reply to the modmail thread
await this.postToThreadChannel(threadContent, files);
// Add the message to the database
await this.addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.TO_USER,
user_id: moderator.id,
user_name: logModUsername,
body: logContent,
is_anonymous: (isAnonymous ? 1 : 0),
dm_message_id: dmMessage.id
});
if (this.scheduled_close_at) {
await this.cancelScheduledClose();
await this.postSystemMessage(`Cancelling scheduled closing of this thread due to new reply`);
}
return true;
} }
/** /**
* @param {Eris~Message} msg * @param {Eris.User} user
* @returns {Promise<void>} * @param {string} body
* @param {Eris.EmbedBase[]} embeds
* @param {string[]} formattedAttachments
* @param {number} timestamp
* @return string
* @private
*/ */
async receiveUserReply(msg) { _formatUserReplyThreadMessage(user, body, embeds, formattedAttachments = [], timestamp) {
let content = msg.content; const content = (body.trim() === '' && embeds.length)
if (msg.content.trim() === '' && msg.embeds.length) { ? '<message contains embeds>'
content = '<message contains embeds>'; : body;
}
let threadContent = `**${msg.author.username}#${msg.author.discriminator}:** ${content}`; let result = `**${user.username}#${user.discriminator}:** ${content}`;
let logContent = msg.content;
if (config.threadTimestamps) { if (formattedAttachments.length) {
const timestamp = utils.getTimestamp(msg.timestamp, 'x'); for (const formatted of formattedAttachments) {
threadContent = `[${timestamp}] « ${threadContent}`; result += `\n\n${formatted}`;
}
// Prepare attachments, if any
let attachmentFiles = [];
for (const attachment of msg.attachments) {
const savedAttachment = await attachments.saveAttachment(attachment);
// Forward small attachments (<2MB) as attachments, just link to larger ones
const formatted = '\n\n' + await utils.formatAttachment(attachment, savedAttachment.url);
logContent += formatted; // Logs always contain the link
if (config.relaySmallAttachmentsAsAttachments && attachment.size <= 1024 * 1024 * 2) {
const file = await attachments.attachmentToDiscordFileObject(attachment);
attachmentFiles.push(file);
} else {
threadContent += formatted;
} }
} }
await this.postToThreadChannel(threadContent, attachmentFiles); if (config.threadTimestamps) {
await this.addThreadMessageToDB({ const formattedTimestamp = timestamp ? utils.getTimestamp(timestamp, 'x') : utils.getTimestamp();
message_type: THREAD_MESSAGE_TYPE.FROM_USER, result = `[${formattedTimestamp}] ${result}`;
user_id: this.user_id,
user_name: `${msg.author.username}#${msg.author.discriminator}`,
body: logContent,
is_anonymous: 0,
dm_message_id: msg.id
});
if (this.scheduled_close_at) {
await this.cancelScheduledClose();
await this.postSystemMessage(`<@!${this.scheduled_close_id}> Thread that was scheduled to be closed got a new reply. Cancelling.`);
} }
if (this.alert_id) { return result;
await this.setAlert(null);
await this.postSystemMessage(`<@!${this.alert_id}> New message from ${this.user_name}`);
}
} }
/** /**
* @returns {Promise<PrivateChannel>} * @param {Eris.User} user
* @param {string} body
* @param {Eris.EmbedBase[]} embeds
* @param {string[]} formattedAttachments
* @return string
* @private
*/ */
getDMChannel() { _formatUserReplyLogMessage(user, body, embeds, formattedAttachments = []) {
return bot.getDMChannel(this.user_id); const content = (body.trim() === '' && embeds.length)
? '<message contains embeds>'
: body;
let result = content;
if (formattedAttachments.length) {
for (const formatted of formattedAttachments) {
result += `\n\n${formatted}`;
}
}
return result;
} }
/** /**
* @param {String} text * @param {string} text
* @param {Eris~MessageFile|Eris~MessageFile[]} file * @param {Eris.MessageFile|Eris.MessageFile[]} file
* @returns {Promise<Eris~Message>} * @returns {Promise<Eris.Message>}
* @throws Error * @throws Error
* @private
*/ */
async postToUser(text, file = null) { async _sendDMToUser(text, file = null) {
// Try to open a DM channel with the user // Try to open a DM channel with the user
const dmChannel = await this.getDMChannel(); const dmChannel = await this.getDMChannel();
if (! dmChannel) { if (! dmChannel) {
@ -206,9 +181,10 @@ class Thread {
} }
/** /**
* @returns {Promise<Eris~Message>} * @returns {Promise<Eris.Message>}
* @private
*/ */
async postToThreadChannel(...args) { async _postToThreadChannel(...args) {
try { try {
if (typeof args[0] === 'string') { if (typeof args[0] === 'string') {
const chunks = utils.chunk(args[0], 2000); const chunks = utils.chunk(args[0], 2000);
@ -232,17 +208,208 @@ class Thread {
} }
/** /**
* @param {String} text * @param {Object} data
* @returns {Promise<ThreadMessage>}
* @private
*/
async _addThreadMessageToDB(data) {
if (data.message_type === THREAD_MESSAGE_TYPE.TO_USER) {
data.message_number = knex.raw(`IFNULL((${this._lastMessageNumberInThreadSQL()}), 0) + 1`);
}
const dmChannel = await this.getDMChannel();
const insertedIds = await knex('thread_messages').insert({
thread_id: this.id,
created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss'),
is_anonymous: 0,
dm_channel_id: dmChannel.id,
...data
});
const threadMessage = await knex('thread_messages')
.where('id', insertedIds[0])
.select();
return new ThreadMessage(threadMessage[0]);
}
/**
* @param {string} id
* @param {Object} data
* @returns {Promise<void>}
* @private
*/
async _updateThreadMessage(id, data) {
await knex('thread_messages')
.where('id', id)
.update(data);
}
/**
* @returns {string}
* @private
*/
_lastMessageNumberInThreadSQL() {
return knex('thread_messages')
.select(knex.raw('MAX(message_number)'))
.whereRaw(`thread_messages.thread_id = '${this.id}'`)
.toSQL()
.sql;
}
/**
* @param {Eris.Member} moderator
* @param {string} text
* @param {Eris.MessageFile[]} replyAttachments
* @param {boolean} isAnonymous
* @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}`;
// Prepare attachments, if any
const files = [];
const attachmentLinks = [];
if (replyAttachments.length > 0) {
for (const attachment of replyAttachments) {
await Promise.all([
attachments.attachmentToDiscordFileObject(attachment).then(file => {
files.push(file);
}),
attachments.saveAttachment(attachment).then(result => {
attachmentLinks.push(result.url);
})
]);
}
}
// Send the reply DM
const dmContent = this._formatStaffReplyDM(moderator, text, isAnonymous);
let dmMessage;
try {
dmMessage = await this._sendDMToUser(dmContent, files);
} catch (e) {
await this.postSystemMessage(`Error while replying to user: ${e.message}`);
return false;
}
// Save the log entry
const logContent = this._formatStaffReplyLogMessage(moderator, text, isAnonymous, attachmentLinks);
const threadMessage = await this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.TO_USER,
user_id: moderator.id,
user_name: fullModeratorName,
body: logContent,
is_anonymous: (isAnonymous ? 1 : 0),
dm_message_id: dmMessage.id
});
// Show the reply in the inbox thread
const inboxContent = this._formatStaffReplyThreadMessage(moderator, text, isAnonymous, threadMessage.message_number, null);
const inboxMessage = await this._postToThreadChannel(inboxContent, files);
await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id });
// Interrupt scheduled closing, if in progress
if (this.scheduled_close_at) {
await this.cancelScheduledClose();
await this.postSystemMessage(`Cancelling scheduled closing of this thread due to new reply`);
}
return true;
}
/**
* @param {Eris.Message} msg
* @returns {Promise<void>}
*/
async receiveUserReply(msg) {
// Prepare attachments
const attachmentFiles = [];
const threadFormattedAttachments = [];
const logFormattedAttachments = [];
// 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);
}
}
// Save log entry
const logContent = this._formatUserReplyLogMessage(msg.author, msg.content, msg.embeds, logFormattedAttachments);
const threadMessage = await this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.FROM_USER,
user_id: this.user_id,
user_name: `${msg.author.username}#${msg.author.discriminator}`,
body: logContent,
is_anonymous: 0,
dm_message_id: msg.id
});
// Show user reply in the inbox thread
const inboxContent = this._formatUserReplyThreadMessage(msg.author, msg.content, msg.embeds, threadFormattedAttachments, null);
const inboxMessage = await this._postToThreadChannel(inboxContent, attachmentFiles);
await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id });
// Interrupt scheduled closing, if in progress
if (this.scheduled_close_at) {
await this.cancelScheduledClose();
await this.postSystemMessage(`<@!${this.scheduled_close_id}> Thread that was scheduled to be closed got a new reply. Cancelling.`);
}
if (this.alert_id) {
await this.setAlert(null);
await this.postSystemMessage(`<@!${this.alert_id}> New message from ${this.user_name}`);
}
}
/**
* @returns {Promise<PrivateChannel>}
*/
getDMChannel() {
return bot.getDMChannel(this.user_id);
}
/**
* @param {string|Eris.MessageContent} content
* @param {*} args * @param {*} args
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async postSystemMessage(text, ...args) { async postSystemMessage(content, ...args) {
const msg = await this.postToThreadChannel(text, ...args); const msg = await this._postToThreadChannel(content, ...args);
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: typeof text === 'string' ? text : text.content, body: typeof content === 'string' ? content : content.content,
is_anonymous: 0,
dm_message_id: msg.id
});
}
/**
* @param {string|Eris.MessageContent} content
* @param {*} args
* @returns {Promise<void>}
*/
async sendSystemMessageToUser(content, ...args) {
const msg = await this._sendDMToUser(content, ...args);
await this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.SYSTEM_TO_USER,
user_id: null,
user_name: '',
body: typeof content === 'string' ? content : content.content,
is_anonymous: 0, is_anonymous: 0,
dm_message_id: msg.id dm_message_id: msg.id
}); });
@ -253,15 +420,15 @@ class Thread {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async postNonLogMessage(...args) { async postNonLogMessage(...args) {
await this.postToThreadChannel(...args); await this._postToThreadChannel(...args);
} }
/** /**
* @param {Eris.Message} msg * @param {Eris.Message} msg
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async saveChatMessage(msg) { async saveChatMessageToLogs(msg) {
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,
user_name: `${msg.author.username}#${msg.author.discriminator}`, user_name: `${msg.author.username}#${msg.author.discriminator}`,
@ -271,8 +438,8 @@ class Thread {
}); });
} }
async saveCommandMessage(msg) { async saveCommandMessageToLogs(msg) {
return this.addThreadMessageToDB({ return this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.COMMAND, message_type: THREAD_MESSAGE_TYPE.COMMAND,
user_id: msg.author.id, user_id: msg.author.id,
user_name: `${msg.author.username}#${msg.author.discriminator}`, user_name: `${msg.author.username}#${msg.author.discriminator}`,
@ -286,7 +453,7 @@ class Thread {
* @param {Eris.Message} msg * @param {Eris.Message} msg
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async updateChatMessage(msg) { async updateChatMessageInLogs(msg) {
await knex('thread_messages') await knex('thread_messages')
.where('thread_id', this.id) .where('thread_id', this.id)
.where('dm_message_id', msg.id) .where('dm_message_id', msg.id)
@ -299,26 +466,13 @@ class Thread {
* @param {String} messageId * @param {String} messageId
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async deleteChatMessage(messageId) { async deleteChatMessageFromLogs(messageId) {
await knex('thread_messages') await knex('thread_messages')
.where('thread_id', this.id) .where('thread_id', this.id)
.where('dm_message_id', messageId) .where('dm_message_id', messageId)
.delete(); .delete();
} }
/**
* @param {Object} data
* @returns {Promise<void>}
*/
async addThreadMessageToDB(data) {
await knex('thread_messages').insert({
thread_id: this.id,
created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss'),
is_anonymous: 0,
...data
});
}
/** /**
* @returns {Promise<ThreadMessage[]>} * @returns {Promise<ThreadMessage[]>}
*/ */
@ -332,6 +486,19 @@ class Thread {
return threadMessages.map(row => new ThreadMessage(row)); return threadMessages.map(row => new ThreadMessage(row));
} }
/**
* @param {number} messageNumber
* @returns {Promise<ThreadMessage>}
*/
async findThreadMessageByMessageNumber(messageNumber) {
const data = await knex('thread_messages')
.where('thread_id', this.id)
.where('message_number', messageNumber)
.select();
return data ? new ThreadMessage(data) : null;
}
/** /**
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */

View File

@ -4,11 +4,14 @@ const utils = require("../utils");
* @property {Number} id * @property {Number} id
* @property {String} thread_id * @property {String} thread_id
* @property {Number} message_type * @property {Number} message_type
* @property {Number} message_number
* @property {String} user_id * @property {String} user_id
* @property {String} user_name * @property {String} user_name
* @property {String} body * @property {String} body
* @property {Number} is_anonymous * @property {Number} is_anonymous
* @property {Number} dm_message_id * @property {String} dm_channel_id
* @property {String} dm_message_id
* @property {String} inbox_message_id
* @property {String} created_at * @property {String} created_at
*/ */
class ThreadMessage { class ThreadMessage {

View File

@ -11,7 +11,8 @@ module.exports = {
FROM_USER: 3, FROM_USER: 3,
TO_USER: 4, TO_USER: 4,
LEGACY: 5, LEGACY: 5,
COMMAND: 6 COMMAND: 6,
SYSTEM_TO_USER: 7
}, },
ACCIDENTAL_THREAD_MESSAGES: [ ACCIDENTAL_THREAD_MESSAGES: [

View File

@ -61,7 +61,12 @@ process.on('unhandledRejection', err => {
(async function() { (async function() {
// Make sure the database is up to date // Make sure the database is up to date
await knex.migrate.latest(); const migrationDelta = await knex.migrate.status();
if (migrationDelta !== 0) {
console.log('Updating database. This can take a while. Don\'t close the bot!');
await knex.migrate.latest();
console.log('Done!');
}
// Migrate legacy data if we need to // Migrate legacy data if we need to
if (await legacyMigrator.shouldMigrate()) { if (await legacyMigrator.shouldMigrate()) {

View File

@ -94,8 +94,7 @@ function initBaseMessageHandlers() {
if (msg.content.startsWith(config.prefix) || msg.content.startsWith(config.snippetPrefix)) { if (msg.content.startsWith(config.prefix) || msg.content.startsWith(config.snippetPrefix)) {
// Save commands as "command messages" // Save commands as "command messages"
if (msg.content.startsWith(config.snippetPrefix)) return; // Ignore snippets thread.saveCommandMessageToLogs(msg);
thread.saveCommandMessage(msg);
} else if (config.alwaysReply) { } else if (config.alwaysReply) {
// AUTO-REPLY: If config.alwaysReply is enabled, send all chat messages in thread channels as replies // AUTO-REPLY: If config.alwaysReply is enabled, send all chat messages in thread channels as replies
if (! utils.isStaff(msg.member)) return; // Only staff are allowed to reply if (! utils.isStaff(msg.member)) return; // Only staff are allowed to reply
@ -104,7 +103,7 @@ function initBaseMessageHandlers() {
if (replied) msg.delete(); if (replied) msg.delete();
} else { } else {
// Otherwise just save the messages as "chat" in the logs // Otherwise just save the messages as "chat" in the logs
thread.saveChatMessage(msg); thread.saveChatMessageToLogs(msg);
} }
}); });
@ -133,7 +132,9 @@ function initBaseMessageHandlers() {
thread = await threads.createNewThreadForUser(msg.author); thread = await threads.createNewThreadForUser(msg.author);
} }
if (thread) await thread.receiveUserReply(msg); if (thread) {
await thread.receiveUserReply(msg);
}
}); });
}); });
@ -151,10 +152,10 @@ function initBaseMessageHandlers() {
const oldContent = oldMessage && oldMessage.content || '*Unavailable due to bot restart*'; const oldContent = oldMessage && oldMessage.content || '*Unavailable due to bot restart*';
const newContent = msg.content; const newContent = msg.content;
// Ignore bogus edit events with no changes // Ignore edit events with changes only in embeds etc.
if (newContent.trim() === oldContent.trim()) return; if (newContent.trim() === oldContent.trim()) return;
// 1) Edit in DMs // 1) If this edit was in DMs
if (msg.channel instanceof Eris.PrivateChannel) { if (msg.channel instanceof Eris.PrivateChannel) {
const thread = await threads.findOpenThreadByUserId(msg.author.id); const thread = await threads.findOpenThreadByUserId(msg.author.id);
if (! thread) return; if (! thread) return;
@ -163,12 +164,12 @@ function initBaseMessageHandlers() {
thread.postSystemMessage(editMessage); thread.postSystemMessage(editMessage);
} }
// 2) Edit in the thread // 2) If this edit was a chat message in the thread
else if (utils.messageIsOnInboxServer(msg) && utils.isStaff(msg.member)) { else if (utils.messageIsOnInboxServer(msg) && utils.isStaff(msg.member)) {
const thread = await threads.findOpenThreadByChannelId(msg.channel.id); const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
if (! thread) return; if (! thread) return;
thread.updateChatMessage(msg); thread.updateChatMessageInLogs(msg);
} }
}); });
@ -184,7 +185,7 @@ function initBaseMessageHandlers() {
const thread = await threads.findOpenThreadByChannelId(msg.channel.id); const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
if (! thread) return; if (! thread) return;
thread.deleteChatMessage(msg.id); thread.deleteChatMessageFromLogs(msg.id);
}); });
/** /**

View File

@ -2,4 +2,9 @@ module.exports = ({ bot, knex, config, commands }) => {
commands.addInboxThreadCommand('id', [], async (msg, args, thread) => { commands.addInboxThreadCommand('id', [], async (msg, args, thread) => {
thread.postSystemMessage(thread.user_id); thread.postSystemMessage(thread.user_id);
}); });
commands.addInboxThreadCommand('dm_channel_id', [], async (msg, args, thread) => {
const dmChannel = await thread.getDMChannel();
thread.postSystemMessage(dmChannel.id);
});
}; };

View File

@ -30,15 +30,20 @@ async function serveLogs(res, pathParts) {
let line = `[${moment.utc(message.created_at).format('YYYY-MM-DD HH:mm:ss')}] `; let line = `[${moment.utc(message.created_at).format('YYYY-MM-DD HH:mm:ss')}] `;
if (message.message_type === THREAD_MESSAGE_TYPE.SYSTEM) { if (message.message_type === THREAD_MESSAGE_TYPE.FROM_USER) {
// System messages don't need the username line += `[FROM USER] [${message.user_name}] ${message.body}`;
line += message.body;
} else 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) { } else if (message.message_type === THREAD_MESSAGE_TYPE.TO_USER) {
line += `[TO USER] ${message.user_name}: ${message.body}`; 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 { } else {
line += `${message.user_name}: ${message.body}`; line += `[${message.user_name}] ${message.body}`;
} }
return line; return line;

View File

@ -73,12 +73,13 @@ function postError(channel, str, opts = {}) {
/** /**
* Returns whether the given member has permission to use modmail commands * Returns whether the given member has permission to use modmail commands
* @param member * @param {Eris.Member} member
* @returns {boolean} * @returns {boolean}
*/ */
function isStaff(member) { function isStaff(member) {
if (! member) return false; if (! member) return false;
if (config.inboxServerPermission.length === 0) return true; if (config.inboxServerPermission.length === 0) return true;
if (member.guild.ownerID === member.id) return true;
return config.inboxServerPermission.some(perm => { return config.inboxServerPermission.some(perm => {
if (isSnowflake(perm)) { if (isSnowflake(perm)) {