2017-12-24 15:04:08 -05:00
|
|
|
const Eris = require('eris');
|
|
|
|
const moment = require('moment');
|
2018-02-18 17:23:29 -05:00
|
|
|
const transliterate = require('transliteration');
|
2017-12-24 15:04:08 -05:00
|
|
|
|
|
|
|
const config = require('./config');
|
|
|
|
const bot = require('./bot');
|
|
|
|
const Queue = require('./queue');
|
|
|
|
const utils = require('./utils');
|
2018-02-11 14:54:30 -05:00
|
|
|
const threadUtils = require('./threadUtils');
|
2017-12-24 15:04:08 -05:00
|
|
|
const blocked = require('./data/blocked');
|
|
|
|
const threads = require('./data/threads');
|
|
|
|
|
2018-02-11 14:54:30 -05:00
|
|
|
const snippets = require('./plugins/snippets');
|
|
|
|
const webserver = require('./plugins/webserver');
|
|
|
|
const greeting = require('./plugins/greeting');
|
2018-02-14 01:53:34 -05:00
|
|
|
const attachments = require("./data/attachments");
|
2018-02-18 15:52:37 -05:00
|
|
|
const {ACCIDENTAL_THREAD_MESSAGES} = require('./data/constants');
|
2017-12-31 19:16:05 -05:00
|
|
|
|
2018-02-11 14:54:30 -05:00
|
|
|
const messageQueue = new Queue();
|
2017-12-31 19:16:05 -05:00
|
|
|
|
2018-02-11 14:54:30 -05:00
|
|
|
const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args);
|
2017-12-31 19:16:05 -05:00
|
|
|
|
2017-12-24 15:04:08 -05:00
|
|
|
// Once the bot has connected, set the status/"playing" message
|
|
|
|
bot.on('ready', () => {
|
|
|
|
bot.editStatus(null, {name: config.status});
|
|
|
|
});
|
|
|
|
|
2018-02-11 14:54:30 -05:00
|
|
|
/**
|
|
|
|
* When a moderator posts in a modmail thread...
|
|
|
|
* 1) If alwaysReply is enabled, reply to the user
|
|
|
|
* 2) If alwaysReply is disabled, save that message as a chat message in the thread
|
|
|
|
*/
|
2017-12-31 19:16:05 -05:00
|
|
|
bot.on('messageCreate', async msg => {
|
|
|
|
if (! utils.messageIsOnInboxServer(msg)) return;
|
2018-02-18 14:07:26 -05:00
|
|
|
if (! utils.isStaff(msg.member)) return;
|
2017-12-31 19:16:05 -05:00
|
|
|
if (msg.author.bot) return;
|
|
|
|
if (msg.content.startsWith(config.prefix) || msg.content.startsWith(config.snippetPrefix)) return;
|
|
|
|
|
|
|
|
const thread = await threads.findByChannelId(msg.channel.id);
|
|
|
|
if (! thread) return;
|
2017-12-24 15:04:08 -05:00
|
|
|
|
2017-12-31 19:16:05 -05:00
|
|
|
if (config.alwaysReply) {
|
|
|
|
// AUTO-REPLY: If config.alwaysReply is enabled, send all chat messages in thread channels as replies
|
2018-02-14 01:53:34 -05:00
|
|
|
if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
|
2017-12-31 19:16:05 -05:00
|
|
|
await thread.replyToUser(msg.member, msg.content.trim(), msg.attachments, config.alwaysReplyAnon || false);
|
|
|
|
msg.delete();
|
|
|
|
} else {
|
|
|
|
// Otherwise just save the messages as "chat" in the logs
|
2018-02-11 14:54:30 -05:00
|
|
|
thread.saveChatMessage(msg);
|
2017-12-31 19:16:05 -05:00
|
|
|
}
|
|
|
|
});
|
2017-12-24 15:04:08 -05:00
|
|
|
|
2018-02-11 14:54:30 -05:00
|
|
|
/**
|
|
|
|
* When we get a private message...
|
|
|
|
* 1) Find the open modmail thread for this user, or create a new one
|
|
|
|
* 2) Post the message as a user reply in the thread
|
|
|
|
*/
|
2017-12-24 15:04:08 -05:00
|
|
|
bot.on('messageCreate', async msg => {
|
|
|
|
if (! (msg.channel instanceof Eris.PrivateChannel)) return;
|
2018-02-11 14:54:30 -05:00
|
|
|
if (msg.author.bot) return;
|
|
|
|
if (msg.type !== 0) return; // Ignore pins etc.
|
2017-12-24 15:04:08 -05:00
|
|
|
|
|
|
|
if (await blocked.isBlocked(msg.author.id)) return;
|
|
|
|
|
|
|
|
// Private message handling is queued so e.g. multiple message in quick succession don't result in multiple channels being created
|
|
|
|
messageQueue.add(async () => {
|
2018-02-18 15:52:37 -05:00
|
|
|
let thread = await threads.findOpenThreadByUserId(msg.author.id);
|
|
|
|
|
|
|
|
// New thread
|
|
|
|
if (! thread) {
|
|
|
|
// Ignore messages that shouldn't usually open new threads, such as "ok", "thanks", etc.
|
2018-02-20 05:57:34 -05:00
|
|
|
if (config.ignoreAccidentalThreads && msg.content && ACCIDENTAL_THREAD_MESSAGES.includes(msg.content.trim().toLowerCase())) return;
|
2018-02-18 15:52:37 -05:00
|
|
|
|
|
|
|
thread = await threads.createNewThreadForUser(msg.author);
|
|
|
|
}
|
|
|
|
|
2018-02-11 14:54:30 -05:00
|
|
|
await thread.receiveUserReply(msg);
|
2017-12-24 15:04:08 -05:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2018-02-11 14:54:30 -05:00
|
|
|
/**
|
|
|
|
* When a message is edited...
|
|
|
|
* 1) If that message was in DMs, and we have a thread open with that user, post the edit as a system message in the thread
|
|
|
|
* 2) If that message was moderator chatter in the thread, update the corresponding chat message in the DB
|
|
|
|
*/
|
2017-12-24 15:04:08 -05:00
|
|
|
bot.on('messageUpdate', async (msg, oldMessage) => {
|
2018-02-18 19:03:53 -05:00
|
|
|
if (! msg || ! msg.author) return;
|
2018-02-11 14:54:30 -05:00
|
|
|
if (msg.author.bot) return;
|
2017-12-24 15:04:08 -05:00
|
|
|
if (await blocked.isBlocked(msg.author.id)) return;
|
|
|
|
|
|
|
|
// Old message content doesn't persist between bot restarts
|
2018-02-18 15:30:10 -05:00
|
|
|
const oldContent = oldMessage && oldMessage.content || '*Unavailable due to bot restart*';
|
|
|
|
const newContent = msg.content;
|
2017-12-24 15:04:08 -05:00
|
|
|
|
|
|
|
// Ignore bogus edit events with no changes
|
|
|
|
if (newContent.trim() === oldContent.trim()) return;
|
|
|
|
|
2018-02-11 14:54:30 -05:00
|
|
|
// 1) Edit in DMs
|
|
|
|
if (msg.channel instanceof Eris.PrivateChannel) {
|
|
|
|
const thread = await threads.findOpenThreadByUserId(msg.author.id);
|
|
|
|
const editMessage = utils.disableLinkPreviews(`**The user edited their message:**\n\`B:\` ${oldContent}\n\`A:\` ${newContent}`);
|
|
|
|
|
|
|
|
thread.postSystemMessage(editMessage);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 2) Edit 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);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When a staff message is deleted in a modmail thread, delete it from the database as well
|
|
|
|
*/
|
|
|
|
bot.on('messageDelete', async msg => {
|
2018-02-18 19:03:53 -05:00
|
|
|
if (! msg.author) return;
|
2018-02-11 14:54:30 -05:00
|
|
|
if (msg.author.bot) return;
|
|
|
|
if (! utils.messageIsOnInboxServer(msg)) return;
|
|
|
|
if (! utils.isStaff(msg.member)) return;
|
|
|
|
|
|
|
|
const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
|
2017-12-24 15:04:08 -05:00
|
|
|
if (! thread) return;
|
|
|
|
|
2018-02-11 14:54:30 -05:00
|
|
|
thread.deleteChatMessage(msg.id);
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When the bot is mentioned on the main server, ping staff in the log channel about it
|
|
|
|
*/
|
|
|
|
bot.on('messageCreate', async msg => {
|
|
|
|
if (! utils.messageIsOnMainServer(msg)) return;
|
|
|
|
if (! msg.mentions.some(user => user.id === bot.user.id)) return;
|
|
|
|
|
|
|
|
// If the person who mentioned the modmail bot is also on the modmail server, ignore them
|
|
|
|
if (utils.getInboxGuild().members.get(msg.author.id)) return;
|
|
|
|
|
|
|
|
// If the person who mentioned the bot is blocked, ignore them
|
|
|
|
if (await blocked.isBlocked(msg.author.id)) return;
|
|
|
|
|
|
|
|
bot.createMessage(utils.getLogChannel(bot).id, {
|
|
|
|
content: `@here Bot mentioned in ${msg.channel.mention} by **${msg.author.username}#${msg.author.discriminator}**: "${msg.cleanContent}"`,
|
|
|
|
disableEveryone: false,
|
|
|
|
});
|
2017-12-24 15:04:08 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
// Mods can reply to modmail threads using !r or !reply
|
|
|
|
// These messages get relayed back to the DM thread between the bot and the user
|
2018-02-11 14:54:30 -05:00
|
|
|
addInboxServerCommand('reply', async (msg, args, thread) => {
|
2017-12-31 19:16:05 -05:00
|
|
|
if (! thread) return;
|
2018-02-11 14:54:30 -05:00
|
|
|
|
2017-12-24 15:04:08 -05:00
|
|
|
const text = args.join(' ').trim();
|
2018-02-14 01:53:34 -05:00
|
|
|
if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
|
2018-02-11 14:54:30 -05:00
|
|
|
await thread.replyToUser(msg.member, text, msg.attachments, false);
|
|
|
|
msg.delete();
|
2017-12-24 15:04:08 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
bot.registerCommandAlias('r', 'reply');
|
|
|
|
|
|
|
|
// Anonymous replies only show the role, not the username
|
2018-02-11 14:54:30 -05:00
|
|
|
addInboxServerCommand('anonreply', async (msg, args, thread) => {
|
2017-12-31 19:16:05 -05:00
|
|
|
if (! thread) return;
|
2018-02-11 14:54:30 -05:00
|
|
|
|
2017-12-24 15:04:08 -05:00
|
|
|
const text = args.join(' ').trim();
|
2018-02-14 01:53:34 -05:00
|
|
|
if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
|
2018-02-11 14:54:30 -05:00
|
|
|
await thread.replyToUser(msg.member, text, msg.attachments, true);
|
|
|
|
msg.delete();
|
2017-12-24 15:04:08 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
bot.registerCommandAlias('ar', 'anonreply');
|
|
|
|
|
|
|
|
// Close a thread. Closing a thread saves a log of the channel's contents and then deletes the channel.
|
2017-12-31 19:16:05 -05:00
|
|
|
addInboxServerCommand('close', async (msg, args, thread) => {
|
2017-12-24 15:04:08 -05:00
|
|
|
if (! thread) return;
|
2018-02-18 14:21:03 -05:00
|
|
|
await thread.close();
|
|
|
|
|
|
|
|
const logUrl = await thread.getLogUrl();
|
|
|
|
utils.postLog(utils.trimAll(`
|
|
|
|
Modmail thread with ${thread.user_name} (${thread.user_id}) was closed by ${msg.author.username}
|
|
|
|
Logs: ${logUrl}
|
|
|
|
`));
|
2017-12-24 15:04:08 -05:00
|
|
|
});
|
|
|
|
|
2017-12-31 19:16:05 -05:00
|
|
|
addInboxServerCommand('block', (msg, args, thread) => {
|
2017-12-24 15:04:08 -05:00
|
|
|
async function block(userId) {
|
2018-02-24 06:02:54 -05:00
|
|
|
const user = bot.users.get(userId);
|
|
|
|
await blocked.block(userId, (user ? `${user.username}#${user.discriminator}` : ''), msg.author.id);
|
2017-12-24 15:04:08 -05:00
|
|
|
msg.channel.createMessage(`Blocked <@${userId}> (id ${userId}) from modmail`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args.length > 0) {
|
|
|
|
// User mention/id as argument
|
|
|
|
const userId = utils.getUserMention(args.join(' '));
|
|
|
|
if (! userId) return;
|
|
|
|
block(userId);
|
|
|
|
} else if (thread) {
|
|
|
|
// Calling !block without args in a modmail thread blocks the user of that thread
|
2018-02-11 14:54:30 -05:00
|
|
|
block(thread.user_id);
|
2017-12-24 15:04:08 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-12-31 19:16:05 -05:00
|
|
|
addInboxServerCommand('unblock', (msg, args, thread) => {
|
2017-12-24 15:04:08 -05:00
|
|
|
async function unblock(userId) {
|
|
|
|
await blocked.unblock(userId);
|
|
|
|
msg.channel.createMessage(`Unblocked <@${userId}> (id ${userId}) from modmail`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args.length > 0) {
|
|
|
|
// User mention/id as argument
|
|
|
|
const userId = utils.getUserMention(args.join(' '));
|
|
|
|
if (! userId) return;
|
|
|
|
unblock(userId);
|
|
|
|
} else if (thread) {
|
|
|
|
// Calling !unblock without args in a modmail thread unblocks the user of that thread
|
2018-02-11 14:54:30 -05:00
|
|
|
unblock(thread.user_id);
|
2017-12-24 15:04:08 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-12-31 19:16:05 -05:00
|
|
|
addInboxServerCommand('logs', (msg, args, thread) => {
|
2017-12-24 15:04:08 -05:00
|
|
|
async function getLogs(userId) {
|
2017-12-31 19:16:05 -05:00
|
|
|
const userThreads = await threads.getClosedThreadsByUserId(userId);
|
2018-02-18 14:09:52 -05:00
|
|
|
|
|
|
|
// Descending by date
|
|
|
|
userThreads.sort((a, b) => {
|
|
|
|
if (a.created_at > b.created_at) return -1;
|
|
|
|
if (a.created_at < b.created_at) return 1;
|
|
|
|
return 0;
|
|
|
|
});
|
2018-02-18 12:43:32 -05:00
|
|
|
|
2017-12-31 19:16:05 -05:00
|
|
|
const threadLines = await Promise.all(userThreads.map(async thread => {
|
|
|
|
const logUrl = await thread.getLogUrl();
|
|
|
|
const formattedDate = moment.utc(thread.created_at).format('MMM Do [at] HH:mm [UTC]');
|
|
|
|
return `\`${formattedDate}\`: <${logUrl}>`;
|
|
|
|
}));
|
2017-12-24 15:04:08 -05:00
|
|
|
|
2017-12-31 19:16:05 -05:00
|
|
|
const message = `**Log files for <@${userId}>:**\n${threadLines.join('\n')}`;
|
2017-12-24 15:04:08 -05:00
|
|
|
|
|
|
|
// Send the list of logs in chunks of 15 lines per message
|
|
|
|
const lines = message.split('\n');
|
|
|
|
const chunks = utils.chunk(lines, 15);
|
|
|
|
|
|
|
|
let root = Promise.resolve();
|
|
|
|
chunks.forEach(lines => {
|
|
|
|
root = root.then(() => msg.channel.createMessage(lines.join('\n')));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args.length > 0) {
|
|
|
|
// User mention/id as argument
|
|
|
|
const userId = utils.getUserMention(args.join(' '));
|
|
|
|
if (! userId) return;
|
|
|
|
getLogs(userId);
|
|
|
|
} else if (thread) {
|
|
|
|
// Calling !logs without args in a modmail thread returns the logs of the user of that thread
|
2018-02-11 14:54:30 -05:00
|
|
|
getLogs(thread.user_id);
|
2017-12-24 15:04:08 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-02-18 17:23:29 -05:00
|
|
|
addInboxServerCommand('move', async (msg, args, thread) => {
|
|
|
|
if (! config.allowMove) return;
|
|
|
|
|
|
|
|
if (! thread) return;
|
|
|
|
|
|
|
|
const searchStr = args[0];
|
|
|
|
if (! searchStr || searchStr.trim() === '') return;
|
|
|
|
|
|
|
|
const normalizedSearchStr = transliterate.slugify(searchStr);
|
|
|
|
|
|
|
|
const categories = msg.channel.guild.channels.filter(c => {
|
|
|
|
// Filter to categories that are not the thread's current parent category
|
|
|
|
return (c instanceof Eris.CategoryChannel) && (c.id !== msg.channel.parentID);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (categories.length === 0) return;
|
|
|
|
|
|
|
|
// See if any category name contains a part of the search string
|
|
|
|
const containsRankings = categories.map(cat => {
|
|
|
|
const normalizedCatName = transliterate.slugify(cat.name);
|
|
|
|
|
|
|
|
let i;
|
|
|
|
for (i = 1; i < normalizedSearchStr.length; i++) {
|
|
|
|
if (! normalizedCatName.includes(normalizedSearchStr.slice(0, i))) {
|
|
|
|
i--;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return [cat, i];
|
|
|
|
});
|
|
|
|
|
|
|
|
// Sort by best match
|
|
|
|
containsRankings.sort((a, b) => {
|
|
|
|
return a[1] > b[1] ? -1 : 1;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (containsRankings[0][1] === 0) {
|
|
|
|
thread.postNonLogMessage('No matching category');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const targetCategory = containsRankings[0][0];
|
|
|
|
|
|
|
|
await bot.editChannel(thread.channel_id, {
|
|
|
|
parentID: targetCategory.id
|
|
|
|
});
|
|
|
|
|
|
|
|
thread.postSystemMessage(`Thread moved to ${targetCategory.name.toUpperCase()}`);
|
|
|
|
});
|
|
|
|
|
2018-02-18 17:45:56 -05:00
|
|
|
addInboxServerCommand('loglink', async (msg, args, thread) => {
|
|
|
|
if (! thread) return;
|
|
|
|
const logUrl = await thread.getLogUrl();
|
|
|
|
thread.postNonLogMessage(`Log URL: ${logUrl}`);
|
|
|
|
});
|
|
|
|
|
2018-02-11 14:54:30 -05:00
|
|
|
module.exports = {
|
|
|
|
async start() {
|
|
|
|
// Load plugins
|
|
|
|
console.log('Loading plugins...');
|
|
|
|
await snippets(bot);
|
|
|
|
await greeting(bot);
|
|
|
|
await webserver(bot);
|
2017-12-24 15:04:08 -05:00
|
|
|
|
2018-02-11 14:54:30 -05:00
|
|
|
console.log('Connecting to Discord...');
|
|
|
|
await bot.connect();
|
2017-12-24 15:04:08 -05:00
|
|
|
|
2018-02-11 14:54:30 -05:00
|
|
|
console.log('Done! Now listening to DMs.');
|
2017-12-24 15:04:08 -05:00
|
|
|
}
|
|
|
|
};
|