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 {String} text
* @param {Eris~MessageFile[]} replyAttachments
* @param {Boolean} isAnonymous
* @returns {Promise<boolean>} Whether we were able to send the reply
* @param {Eris.Member} moderator
* @param {string} text
* @param {boolean} isAnonymous
* @returns {string}
* @private
*/
async replyToUser(moderator, text, replyAttachments = [], isAnonymous = false) {
// Username to reply with
let modUsername, logModUsername;
_formatStaffReplyDM(moderator, text, isAnonymous) {
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) {
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;
return `**${modInfo}:** ${text}`;
}
// Build the reply message
let dmContent = `**${modUsername}:** ${text}`;
let threadContent = `**${logModUsername}:** ${text}`;
let logContent = text;
/**
* @param {Eris.Member} moderator
* @param {string} 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) {
const timestamp = utils.getTimestamp();
threadContent = `[${timestamp}] » ${threadContent}`;
const formattedTimestamp = timestamp ? utils.getTimestamp(timestamp, 'x') : utils.getTimestamp();
result = `[${formattedTimestamp}] ${result}`;
}
// Prepare attachments, if any
let files = [];
if (replyAttachments.length > 0) {
for (const attachment of replyAttachments) {
let savedAttachment;
await Promise.all([
attachments.attachmentToDiscordFileObject(attachment).then(file => {
files.push(file);
}),
attachments.saveAttachment(attachment).then(result => {
savedAttachment = result;
})
]);
logContent += `\n\n**Attachment:** ${savedAttachment.url}`;
}
}
// Send the reply DM
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;
return result;
}
/**
* @param {Eris~Message} msg
* @returns {Promise<void>}
* @param {Eris.Member} moderator
* @param {string} text
* @param {boolean} isAnonymous
* @param {string[]} attachmentLinks
* @returns {string}
* @private
*/
async receiveUserReply(msg) {
let content = msg.content;
if (msg.content.trim() === '' && msg.embeds.length) {
content = '<message contains embeds>';
_formatStaffReplyLogMessage(moderator, text, isAnonymous, attachmentLinks = []) {
const mainRole = utils.getMainRole(moderator);
const modName = moderator.user.username;
// Mirroring the DM formatting here...
const modInfo = isAnonymous
? (mainRole ? mainRole.name : 'Moderator')
: (mainRole ? `(${mainRole.name}) ${modName}` : modName);
let result = `**${modInfo}:** ${text}`;
if (attachmentLinks.length) {
result += '\n';
for (const link of attachmentLinks) {
result += `\n**Attachment:** ${link}`;
}
}
let threadContent = `**${msg.author.username}#${msg.author.discriminator}:** ${content}`;
let logContent = msg.content;
return result;
}
/**
* @param {Eris.User} user
* @param {string} body
* @param {Eris.EmbedBase[]} embeds
* @param {string[]} formattedAttachments
* @param {number} timestamp
* @return string
* @private
*/
_formatUserReplyThreadMessage(user, body, embeds, formattedAttachments = [], timestamp) {
const content = (body.trim() === '' && embeds.length)
? '<message contains embeds>'
: body;
let result = `**${user.username}#${user.discriminator}:** ${content}`;
if (formattedAttachments.length) {
for (const formatted of formattedAttachments) {
result += `\n\n${formatted}`;
}
}
if (config.threadTimestamps) {
const timestamp = utils.getTimestamp(msg.timestamp, 'x');
threadContent = `[${timestamp}] « ${threadContent}`;
const formattedTimestamp = timestamp ? utils.getTimestamp(timestamp, 'x') : utils.getTimestamp();
result = `[${formattedTimestamp}] ${result}`;
}
// 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);
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
});
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}`);
}
return result;
}
/**
* @returns {Promise<PrivateChannel>}
* @param {Eris.User} user
* @param {string} body
* @param {Eris.EmbedBase[]} embeds
* @param {string[]} formattedAttachments
* @return string
* @private
*/
getDMChannel() {
return bot.getDMChannel(this.user_id);
_formatUserReplyLogMessage(user, body, embeds, formattedAttachments = []) {
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 {Eris~MessageFile|Eris~MessageFile[]} file
* @returns {Promise<Eris~Message>}
* @param {string} text
* @param {Eris.MessageFile|Eris.MessageFile[]} file
* @returns {Promise<Eris.Message>}
* @throws Error
* @private
*/
async postToUser(text, file = null) {
async _sendDMToUser(text, file = null) {
// Try to open a DM channel with the user
const dmChannel = await this.getDMChannel();
if (! dmChannel) {
@ -206,9 +181,10 @@ class Thread {
}
/**
* @returns {Promise<Eris~Message>}
* @returns {Promise<Eris.Message>}
* @private
*/
async postToThreadChannel(...args) {
async _postToThreadChannel(...args) {
try {
if (typeof args[0] === 'string') {
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
* @returns {Promise<void>}
*/
async postSystemMessage(text, ...args) {
const msg = await this.postToThreadChannel(text, ...args);
await this.addThreadMessageToDB({
async postSystemMessage(content, ...args) {
const msg = await this._postToThreadChannel(content, ...args);
await this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.SYSTEM,
user_id: null,
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,
dm_message_id: msg.id
});
@ -253,15 +420,15 @@ class Thread {
* @returns {Promise<void>}
*/
async postNonLogMessage(...args) {
await this.postToThreadChannel(...args);
await this._postToThreadChannel(...args);
}
/**
* @param {Eris.Message} msg
* @returns {Promise<void>}
*/
async saveChatMessage(msg) {
return this.addThreadMessageToDB({
async saveChatMessageToLogs(msg) {
return this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.CHAT,
user_id: msg.author.id,
user_name: `${msg.author.username}#${msg.author.discriminator}`,
@ -271,8 +438,8 @@ class Thread {
});
}
async saveCommandMessage(msg) {
return this.addThreadMessageToDB({
async saveCommandMessageToLogs(msg) {
return this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.COMMAND,
user_id: msg.author.id,
user_name: `${msg.author.username}#${msg.author.discriminator}`,
@ -286,7 +453,7 @@ class Thread {
* @param {Eris.Message} msg
* @returns {Promise<void>}
*/
async updateChatMessage(msg) {
async updateChatMessageInLogs(msg) {
await knex('thread_messages')
.where('thread_id', this.id)
.where('dm_message_id', msg.id)
@ -299,26 +466,13 @@ class Thread {
* @param {String} messageId
* @returns {Promise<void>}
*/
async deleteChatMessage(messageId) {
async deleteChatMessageFromLogs(messageId) {
await knex('thread_messages')
.where('thread_id', this.id)
.where('dm_message_id', messageId)
.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[]>}
*/
@ -332,6 +486,19 @@ class Thread {
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>}
*/

View File

@ -4,11 +4,14 @@ const utils = require("../utils");
* @property {Number} id
* @property {String} thread_id
* @property {Number} message_type
* @property {Number} message_number
* @property {String} user_id
* @property {String} user_name
* @property {String} body
* @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
*/
class ThreadMessage {

View File

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

View File

@ -61,7 +61,12 @@ process.on('unhandledRejection', err => {
(async function() {
// Make sure the database is up to date
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
if (await legacyMigrator.shouldMigrate()) {

View File

@ -94,8 +94,7 @@ function initBaseMessageHandlers() {
if (msg.content.startsWith(config.prefix) || msg.content.startsWith(config.snippetPrefix)) {
// Save commands as "command messages"
if (msg.content.startsWith(config.snippetPrefix)) return; // Ignore snippets
thread.saveCommandMessage(msg);
thread.saveCommandMessageToLogs(msg);
} else if (config.alwaysReply) {
// 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
@ -104,7 +103,7 @@ function initBaseMessageHandlers() {
if (replied) msg.delete();
} else {
// 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);
}
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 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;
// 1) Edit in DMs
// 1) If this edit was in DMs
if (msg.channel instanceof Eris.PrivateChannel) {
const thread = await threads.findOpenThreadByUserId(msg.author.id);
if (! thread) return;
@ -163,12 +164,12 @@ function initBaseMessageHandlers() {
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)) {
const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
if (! thread) return;
thread.updateChatMessage(msg);
thread.updateChatMessageInLogs(msg);
}
});
@ -184,7 +185,7 @@ function initBaseMessageHandlers() {
const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
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) => {
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')}] `;
if (message.message_type === THREAD_MESSAGE_TYPE.SYSTEM) {
// System messages don't need the username
line += message.body;
} else if (message.message_type === THREAD_MESSAGE_TYPE.FROM_USER) {
line += `[FROM USER] ${message.user_name}: ${message.body}`;
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}`;
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}`;
line += `[${message.user_name}] ${message.body}`;
}
return line;

View File

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