ramirez/src/index.js

510 lines
18 KiB
JavaScript
Raw Normal View History

2017-02-09 21:56:36 -05:00
const Eris = require('eris');
2017-02-09 23:36:47 -05:00
const fs = require('fs');
2017-02-09 21:56:36 -05:00
const moment = require('moment');
const humanizeDuration = require('humanize-duration');
2017-02-09 21:56:36 -05:00
const config = require('../config');
2017-02-09 23:36:47 -05:00
const Queue = require('./queue');
2017-02-09 21:56:36 -05:00
const utils = require('./utils');
const blocked = require('./blocked');
const threads = require('./threads');
const logs = require('./logs');
const attachments = require('./attachments');
2017-07-23 20:27:21 -04:00
const snippets = require('./snippets');
2017-02-09 21:56:36 -05:00
const webserver = require('./webserver');
2017-02-10 00:04:23 -05:00
const greeting = require('./greeting');
2017-02-09 21:56:36 -05:00
2017-07-23 20:27:21 -04:00
const prefix = config.prefix || '!';
const snippetPrefix = config.snippetPrefix || prefix.repeat(2);
2017-02-09 21:56:36 -05:00
const bot = new Eris.CommandClient(config.token, {}, {
2017-07-23 20:27:21 -04:00
prefix: prefix,
2017-02-09 21:56:36 -05:00
ignoreSelf: true,
ignoreBots: true,
defaultHelpCommand: false,
2017-07-23 19:13:04 -04:00
defaultCommandOptions: {
caseInsensitive: true,
},
2017-02-09 21:56:36 -05:00
});
const restBot = new Eris.Client(`Bot ${config.token}`, {
restMode: true,
});
2017-02-09 21:56:36 -05:00
const messageQueue = new Queue();
bot.on('ready', () => {
bot.editStatus(null, {name: config.status || 'Message me for help'});
console.log('Bot started, listening to DMs');
});
restBot.on('ready', () => {
console.log('Rest client ready');
});
2017-02-09 23:36:47 -05:00
function formatAttachment(attachment) {
let filesize = attachment.size || 0;
filesize /= 1024;
return attachments.getUrl(attachment.id, attachment.filename).then(attachmentUrl => {
return `**Attachment:** ${attachment.filename} (${filesize.toFixed(1)}KB)\n${attachmentUrl}`;
});
}
2017-07-23 19:23:34 -04:00
// If the alwaysReply option is set to true, send all messages in modmail threads as replies, unless they start with the prefix
2017-05-01 05:14:28 -04:00
if (config.alwaysReply) {
bot.on('messageCreate', msg => {
if (! msg.channel.guild) return;
if (msg.channel.guild.id !== utils.getModmailGuild(bot).id) return;
if (! msg.member.permission.has('manageRoles')) return;
if (msg.author.bot) return;
if (msg.content[0] == bot.commandOptions.prefix) return;
reply(msg, msg.content.trim(), config.alwaysReplyAnon || false);
});
}
2017-05-01 03:56:49 -04:00
2017-02-09 21:56:36 -05:00
// "Bot was mentioned in #general-discussion"
bot.on('messageCreate', msg => {
if (msg.author.id === bot.user.id) return;
if (msg.mentions.some(user => user.id === bot.user.id)) {
2017-02-10 00:04:23 -05:00
// If the person who mentioned the modmail bot is on the modmail server, don't ping about it
if (utils.getModmailGuild(bot).members.get(msg.author.id)) return;
2017-02-09 21:56:36 -05:00
blocked.isBlocked(msg.author.id).then(isBlocked => {
if (isBlocked) return;
bot.createMessage(utils.getModmailGuild(bot).id, {
content: `@here Bot mentioned in ${msg.channel.mention} by **${msg.author.username}#${msg.author.discriminator}**: "${msg.cleanContent}"`,
disableEveryone: false,
});
});
}
});
// When we get a private message, forward the contents to the corresponding modmail thread
bot.on('messageCreate', (msg) => {
if (! (msg.channel instanceof Eris.PrivateChannel)) return;
if (msg.author.id === bot.user.id) return;
blocked.isBlocked(msg.author.id).then(isBlocked => {
if (isBlocked) return;
// Download and save copies of attachments in the background
const attachmentSavePromise = attachments.saveAttachmentsInMessage(msg);
2017-02-09 21:56:36 -05:00
2017-02-09 23:36:47 -05:00
let thread, userLogs;
let threadCreationFailed = false;
2017-02-09 21:56:36 -05:00
// Private message handling is queued so e.g. multiple message in quick succession don't result in multiple channels being created
2017-02-09 21:56:36 -05:00
messageQueue.add(() => {
return threads.getForUser(bot, msg.author, true, msg)
2017-02-09 21:56:36 -05:00
.then(userThread => {
thread = userThread;
return logs.getLogsByUserId(msg.author.id);
}, err => {
console.log(`[ERROR] Modmail channel for ${msg.author.username}#${msg.author.discriminator} could not be created:\n${err.message}`);
threadCreationFailed = true;
2017-02-09 21:56:36 -05:00
})
2017-02-09 23:36:47 -05:00
.then(foundUserLogs => {
userLogs = foundUserLogs;
2017-02-09 21:56:36 -05:00
})
.then(() => {
let content = msg.content;
if (threadCreationFailed) {
// If the thread could not be created, send a warning about this to all mods so they can DM the user directly instead
2017-02-09 21:56:36 -05:00
let warningMessage = `
@here Error creating modmail thread for ${msg.author.username}#${msg.author.discriminator} (${msg.author.id})!
Here's what their message contained:
\`\`\`${content}\`\`\`
`.trim();
2017-02-09 21:56:36 -05:00
bot.createMessage(utils.getModmailGuild(bot).id, {
content: warningMessage,
2017-02-09 21:56:36 -05:00
disableEveryone: false,
});
return;
} else if (! thread) {
// No thread but creation didn't fail either -> probably ignored
2017-02-09 21:56:36 -05:00
return;
}
let threadInitDonePromise = Promise.resolve();
2017-02-09 21:56:36 -05:00
// If the thread was just created, do some extra stuff
if (thread._wasCreated) {
const mainGuild = utils.getMainGuild(restBot);
const memberPromise = (mainGuild ? mainGuild.getRESTMember(msg.author.id) : Promise.resolve());
threadInitDonePromise = memberPromise
.catch(err => {
2017-02-14 18:04:57 -05:00
console.log(`Member ${msg.author.id} not found in main guild ${config.mainGuildId}`);
2017-02-14 18:10:44 -05:00
console.error(String(err));
2017-02-14 18:04:57 -05:00
})
.then(member => {
let mainGuildNickname = null;
if (member && member.nick) mainGuildNickname = member.nick;
else if (member && member.user) mainGuildNickname = member.user.username;
else if (member == null) mainGuildNickname = 'NOT ON SERVER';
if (mainGuildNickname == null) mainGuildNickname = 'UNKNOWN';
const accountAge = humanizeDuration(Date.now() - msg.author.createdAt, {largest: 2});
const infoHeader = `ACCOUNT AGE **${accountAge}**, ID **${msg.author.id}**, NICKNAME **${mainGuildNickname}**, LOGS **${userLogs.length}**\n-------------------------------`;
return bot.createMessage(thread.channelId, infoHeader);
})
.then(() => {
// Ping mods of the new thread
bot.createMessage(thread.channelId, {
content: `@here New modmail thread (${msg.author.username}#${msg.author.discriminator})`,
disableEveryone: false,
});
});
2017-02-09 21:56:36 -05:00
// Send an automatic reply to the user informing them of the successfully created modmail thread
2017-05-01 05:14:28 -04:00
msg.channel.createMessage(config.responseMessage || "Thank you for your message! Our mod team will reply to you here as soon as possible.").then(null, (err) => {
2017-02-09 23:36:47 -05:00
bot.createMessage(utils.getModmailGuild(bot).id, {
2017-02-09 21:56:36 -05:00
content: `There is an issue sending messages to ${msg.author.username}#${msg.author.discriminator} (id ${msg.author.id}); consider messaging manually`
});
});
}
const attachmentsPendingStr = '\n\n*Attachments pending...*';
if (msg.attachments.length > 0) content += attachmentsPendingStr;
threadInitDonePromise.then(() => {
const timestamp = utils.getTimestamp();
bot.createMessage(thread.channelId, `[${timestamp}] « **${msg.author.username}#${msg.author.discriminator}:** ${content}`).then(createdMsg => {
if (msg.attachments.length === 0) return;
// Once attachments have been saved, add links to them to the message
attachmentSavePromise.then(() => {
const attachmentFormatPromises = msg.attachments.map(formatAttachment);
Promise.all(attachmentFormatPromises).then(formattedAttachments => {
let attachmentMsg = '';
formattedAttachments.forEach(str => {
attachmentMsg += `\n\n${str}`;
});
createdMsg.edit(createdMsg.content.replace(attachmentsPendingStr, attachmentMsg));
});
});
});
});
2017-02-09 23:36:47 -05:00
});
2017-02-09 21:56:36 -05:00
});
});
});
// Edits in DMs
bot.on('messageUpdate', (msg, oldMessage) => {
if (! (msg.channel instanceof Eris.PrivateChannel)) return;
if (msg.author.id === bot.user.id) return;
blocked.isBlocked(msg.author.id).then(isBlocked => {
if (isBlocked) return;
let oldContent = oldMessage.content;
const newContent = msg.content;
if (oldContent == null) oldContent = '*Unavailable due to bot restart*';
// Ignore bogus edit events with no changes
if (newContent.trim() === oldContent.trim()) return;
2017-02-09 21:56:36 -05:00
threads.getForUser(bot, msg.author).then(thread => {
if (! thread) return;
2017-07-23 19:43:49 -04:00
const editMessage = utils.disableLinkPreviews(`**The user edited their message:**\n\`B:\` ${oldContent}\n\`A:\` ${newContent}`);
2017-02-09 21:56:36 -05:00
bot.createMessage(thread.channelId, editMessage);
});
});
});
2017-02-10 00:04:23 -05:00
function reply(msg, text, anonymous = false) {
2017-02-09 21:56:36 -05:00
threads.getByChannelId(msg.channel.id).then(thread => {
if (! thread) return;
2017-02-09 23:36:47 -05:00
attachments.saveAttachmentsInMessage(msg).then(() => {
2017-02-09 21:56:36 -05:00
bot.getDMChannel(thread.userId).then(dmChannel => {
let modUsername, logModUsername;
2017-02-09 23:36:47 -05:00
const mainRole = utils.getMainRole(msg.member);
2017-02-09 21:56:36 -05:00
2017-02-10 00:04:23 -05:00
if (anonymous) {
modUsername = (mainRole ? mainRole.name : 'Moderator');
logModUsername = `(Anonymous) (${msg.author.username}) ${mainRole ? mainRole.name : 'Moderator'}`;
2017-02-10 00:04:23 -05:00
} else {
const name = (config.useNicknames ? msg.member.nick || msg.author.username : msg.author.username);
modUsername = (mainRole ? `(${mainRole.name}) ${name}` : name);
logModUsername = modUsername;
2017-02-10 00:04:23 -05:00
}
let content = `**${modUsername}:** ${text}`;
let logContent = `**${logModUsername}:** ${text}`;
2017-02-09 21:56:36 -05:00
function sendMessage(file, attachmentUrl) {
dmChannel.createMessage(content, file).then(() => {
if (attachmentUrl) {
content += `\n\n**Attachment:** ${attachmentUrl}`;
logContent += `\n\n**Attachment:** ${attachmentUrl}`;
}
2017-02-09 21:56:36 -05:00
// Show the message in the modmail thread as well
2017-02-09 23:36:47 -05:00
const timestamp = utils.getTimestamp();
msg.channel.createMessage(`[${timestamp}] » ${logContent}`);
2017-02-09 21:56:36 -05:00
}, (err) => {
if (err.resp && err.resp.statusCode === 403) {
msg.channel.createMessage(`Could not send reply; the user has likely left the server or blocked the bot`);
2017-02-09 21:56:36 -05:00
} else if (err.resp) {
msg.channel.createMessage(`Could not send reply; error code ${err.resp.statusCode}`);
} else {
msg.channel.createMessage(`Could not send reply: ${err.toString()}`);
}
});
msg.delete();
};
// If the reply has an attachment, relay it as is
if (msg.attachments.length > 0) {
2017-02-09 23:36:47 -05:00
fs.readFile(attachments.getPath(msg.attachments[0].id), (err, data) => {
2017-02-09 21:56:36 -05:00
const file = {file: data, name: msg.attachments[0].filename};
2017-02-09 23:36:47 -05:00
attachments.getUrl(msg.attachments[0].id, msg.attachments[0].filename).then(attachmentUrl => {
2017-02-09 21:56:36 -05:00
sendMessage(file, attachmentUrl);
});
});
} else {
sendMessage();
}
});
});
});
2017-02-10 00:04:23 -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
bot.registerCommand('reply', (msg, args) => {
if (! msg.channel.guild) return;
if (msg.channel.guild.id !== utils.getModmailGuild(bot).id) return;
if (! msg.member.permission.has('manageRoles')) return;
const text = args.join(' ').trim();
reply(msg, text, false);
2017-02-09 21:56:36 -05:00
});
bot.registerCommandAlias('r', 'reply');
2017-02-10 00:04:23 -05:00
// Anonymous replies only show the role, not the username
bot.registerCommand('anonreply', (msg, args) => {
if (! msg.channel.guild) return;
if (msg.channel.guild.id !== utils.getModmailGuild(bot).id) return;
if (! msg.member.permission.has('manageRoles')) return;
const text = args.join(' ').trim();
reply(msg, text, true);
});
bot.registerCommandAlias('ar', 'anonreply');
2017-02-09 21:56:36 -05:00
bot.registerCommand('close', (msg, args) => {
if (! msg.channel.guild) return;
2017-02-09 23:36:47 -05:00
if (msg.channel.guild.id !== utils.getModmailGuild(bot).id) return;
2017-02-09 21:56:36 -05:00
if (! msg.member.permission.has('manageRoles')) return;
threads.getByChannelId(msg.channel.id).then(thread => {
if (! thread) return;
msg.channel.createMessage('Saving logs and closing channel...');
msg.channel.getMessages(10000).then(messages => {
const log = messages.reverse().map(msg => {
const date = moment.utc(msg.timestamp, 'x').format('YYYY-MM-DD HH:mm:ss');
return `[${date}] ${msg.author.username}#${msg.author.discriminator}: ${msg.content}`;
}).join('\n') + '\n';
logs.getNewLogFile(thread.userId).then(logFilename => {
logs.saveLogFile(logFilename, log)
2017-02-09 23:36:47 -05:00
.then(() => logs.getLogFileUrl(logFilename))
2017-02-09 21:56:36 -05:00
.then(url => {
const closeMessage = `Modmail thread with ${thread.username} (${thread.userId}) was closed by ${msg.author.username}
2017-02-09 21:56:36 -05:00
Logs: <${url}>`;
bot.createMessage(utils.getModmailGuild(bot).id, closeMessage);
threads.close(thread.channelId).then(() => msg.channel.delete());
});
});
});
});
});
bot.registerCommand('block', (msg, args) => {
if (! msg.channel.guild) return;
if (msg.channel.guild.id !== utils.getModmailGuild(bot).id) return;
if (! msg.member.permission.has('manageRoles')) return;
function block(userId) {
blocked.block(userId).then(() => {
msg.channel.createMessage(`Blocked <@${userId}> (id ${userId}) from modmail`);
});
}
if (args.length > 0) {
const userId = utils.getUserMention(args.join(' '));
if (! userId) return;
block(userId);
} else {
// Calling !block without args in a modmail thread blocks the user of that thread
threads.getByChannelId(msg.channel.id).then(thread => {
if (! thread) return;
2017-02-09 23:36:47 -05:00
block(thread.userId);
2017-02-09 21:56:36 -05:00
});
}
});
bot.registerCommand('unblock', (msg, args) => {
if (! msg.channel.guild) return;
if (msg.channel.guild.id !== utils.getModmailGuild(bot).id) return;
if (! msg.member.permission.has('manageRoles')) return;
function unblock(userId) {
blocked.unblock(userId).then(() => {
msg.channel.createMessage(`Unblocked <@${userId}> (id ${userId}) from modmail`);
});
}
if (args.length > 0) {
const userId = utils.getUserMention(args.join(' '));
if (! userId) return;
unblock(userId);
} else {
// Calling !unblock without args in a modmail thread unblocks the user of that thread
threads.getByChannelId(msg.channel.id).then(thread => {
if (! thread) return;
2017-02-09 23:36:47 -05:00
unblock(thread.userId);
2017-02-09 21:56:36 -05:00
});
}
});
bot.registerCommand('logs', (msg, args) => {
if (! msg.channel.guild) return;
if (msg.channel.guild.id !== utils.getModmailGuild(bot).id) return;
if (! msg.member.permission.has('manageRoles')) return;
function getLogs(userId) {
2017-02-09 23:36:47 -05:00
logs.getLogsWithUrlByUserId(userId).then(infos => {
2017-02-09 21:56:36 -05:00
let message = `**Log files for <@${userId}>:**\n`;
message += infos.map(info => {
const formattedDate = moment.utc(info.date, 'YYYY-MM-DD HH:mm:ss').format('MMM Do [at] HH:mm [UTC]');
return `\`${formattedDate}\`: <${info.url}>`;
}).join('\n');
// Send list of logs in chunks of 15 lines per message
const lines = message.split('\n');
const chunks = [];
const chunkSize = 15;
for (let i = 0; i < lines.length; i += chunkSize) {
chunks.push(lines.slice(i, i + chunkSize).join('\n'));
}
let root = Promise.resolve();
chunks.forEach(chunk => {
root = root.then(() => msg.channel.createMessage(chunk));
});
2017-02-09 21:56:36 -05:00
});
}
if (args.length > 0) {
const userId = utils.getUserMention(args.join(' '));
if (! userId) return;
getLogs(userId);
} else {
// Calling !logs without args in a modmail thread returns the logs of the user of that thread
threads.getByChannelId(msg.channel.id).then(thread => {
if (! thread) return;
2017-02-09 23:36:47 -05:00
getLogs(thread.userId);
2017-02-09 21:56:36 -05:00
});
}
});
2017-07-23 20:27:21 -04:00
// Snippets
bot.on('messageCreate', async msg => {
if (! msg.channel.guild) return;
if (msg.channel.guild.id !== utils.getModmailGuild(bot).id) return;
if (! msg.member.permission.has('manageRoles')) return;
if (msg.author.bot) return;
if (! msg.content) return;
if (! msg.content.startsWith(snippetPrefix)) return;
const shortcut = msg.content.replace(snippetPrefix, '').toLowerCase();
const snippet = await snippets.get(shortcut);
if (! snippet) return;
reply(msg, snippet.text, snippet.isAnonymous);
});
// Show or add a snippet
bot.registerCommand('snippet', async (msg, args) => {
if (! msg.channel.guild) return;
if (msg.channel.guild.id !== utils.getModmailGuild(bot).id) return;
if (! msg.member.permission.has('manageRoles')) return;
const shortcut = args[0];
const text = args.slice(1).join(' ').trim();
if (! shortcut) return;
const snippet = await snippets.get(shortcut);
if (snippet) {
if (text) {
// If the snippet exists and we're trying to create a new one, inform the user the snippet already exists
msg.channel.createMessage(`Snippet "${shortcut}" already exists! You can delete it with ${prefix}delete_snippet.`);
} else {
// If the snippet exists and we're NOT trying to create a new one, show info about the existing snippet
msg.channel.createMessage(`\`${snippetPrefix}${shortcut}\` replies ${snippet.isAnonymous ? 'anonymously ' : ''}with:\n${snippet.text}`);
2017-07-23 20:27:21 -04:00
}
} else {
if (text) {
// If the snippet doesn't exist and the user wants to create it, create it
await snippets.add(shortcut, text, false);
msg.channel.createMessage(`Snippet "${shortcut}" created!`);
} else {
// If the snippet doesn't exist and the user isn't trying to create it, inform them how to create it
msg.channel.createMessage(`Snippet "${shortcut}" doesn't exist! You can create it with \`${prefix}snippet ${shortcut} text\``);
return;
}
}
});
bot.registerCommandAlias('s', 'snippet');
bot.registerCommand('delete_snippet', async (msg, args) => {
if (! msg.channel.guild) return;
if (msg.channel.guild.id !== utils.getModmailGuild(bot).id) return;
if (! msg.member.permission.has('manageRoles')) return;
const shortcut = args[0];
if (! shortcut) return;
const snippet = await snippets.get(shortcut);
if (! snippet) {
msg.channel.createMessage(`Snippet "${shortcut}" doesn't exist!`);
return;
}
await snippets.del(shortcut);
msg.channel.createMessage(`Snippet "${shortcut}" deleted!`);
});
bot.registerCommandAlias('ds', 'delete_snippet');
2017-02-09 21:56:36 -05:00
bot.connect();
restBot.connect();
2017-02-09 21:56:36 -05:00
webserver.run();
greeting.init(bot);