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.
parent
320fad9823
commit
6eb9b973c0
|
@ -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');
|
||||||
|
});
|
||||||
|
};
|
|
@ -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');
|
||||||
|
});
|
||||||
|
};
|
|
@ -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');
|
||||||
|
});
|
||||||
|
};
|
|
@ -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) {
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Eris~Message} msg
|
* @param {Eris.Member} moderator
|
||||||
* @returns {Promise<void>}
|
* @param {string} text
|
||||||
|
* @param {boolean} isAnonymous
|
||||||
|
* @param {string[]} attachmentLinks
|
||||||
|
* @returns {string}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
async receiveUserReply(msg) {
|
_formatStaffReplyLogMessage(moderator, text, isAnonymous, attachmentLinks = []) {
|
||||||
let content = msg.content;
|
const mainRole = utils.getMainRole(moderator);
|
||||||
if (msg.content.trim() === '' && msg.embeds.length) {
|
const modName = moderator.user.username;
|
||||||
content = '<message contains embeds>';
|
|
||||||
|
// 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}`;
|
return result;
|
||||||
let logContent = msg.content;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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) {
|
if (config.threadTimestamps) {
|
||||||
const timestamp = utils.getTimestamp(msg.timestamp, 'x');
|
const formattedTimestamp = timestamp ? utils.getTimestamp(timestamp, 'x') : utils.getTimestamp();
|
||||||
threadContent = `[${timestamp}] « ${threadContent}`;
|
result = `[${formattedTimestamp}] ${result}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare attachments, if any
|
return result;
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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>}
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
|
@ -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
|
||||||
|
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();
|
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()) {
|
||||||
|
|
19
src/main.js
19
src/main.js
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
Loading…
Reference in New Issue