diff --git a/CHANGELOG.md b/CHANGELOG.md index 473629b..67acbf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v2.5.0 +* Commands used in inbox threads are now saved in logs again +* Moved more of the code to individual plugin files + ## v2.4.1-v2.4.4 * Fix errors on first run after upgrading to v2.2.0 * Various other fixes diff --git a/src/data/Thread.js b/src/data/Thread.js index a5fcc44..92ffc49 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -234,6 +234,17 @@ class Thread { }); } + async saveCommandMessage(msg) { + return this.addThreadMessageToDB({ + message_type: THREAD_MESSAGE_TYPE.COMMAND, + user_id: msg.author.id, + user_name: `${msg.author.username}#${msg.author.discriminator}`, + body: msg.content, + is_anonymous: 0, + dm_message_id: msg.id + }); + } + /** * @param {Eris.Message} msg * @returns {Promise} diff --git a/src/data/constants.js b/src/data/constants.js index db811fa..1b4a912 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -10,7 +10,8 @@ module.exports = { CHAT: 2, FROM_USER: 3, TO_USER: 4, - LEGACY: 5 + LEGACY: 5, + COMMAND: 6 }, ACCIDENTAL_THREAD_MESSAGES: [ diff --git a/src/main.js b/src/main.js index 6cfc5ba..0fbcc99 100644 --- a/src/main.js +++ b/src/main.js @@ -11,6 +11,10 @@ const blocked = require('./data/blocked'); const threads = require('./data/threads'); const snippets = require('./plugins/snippets'); +const logCommands = require('./plugins/logCommands'); +const moving = require('./plugins/moving'); +const blocking = require('./plugins/blocking'); +const suspending = require('./plugins/suspending'); const webserver = require('./plugins/webserver'); const greeting = require('./plugins/greeting'); const attachments = require("./data/attachments"); @@ -32,15 +36,19 @@ bot.on('ready', () => { */ bot.on('messageCreate', async msg => { if (! utils.messageIsOnInboxServer(msg)) return; - if (! utils.isStaff(msg.member)) return; 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; - if (config.alwaysReply) { + 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); + } 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 + if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg); await thread.replyToUser(msg.member, msg.content.trim(), msg.attachments, config.alwaysReplyAnon || false); msg.delete(); @@ -259,7 +267,7 @@ addInboxServerCommand('close', async (msg, args, thread) => { // Set a timed close const delay = utils.convertDelayStringToMS(args.join(' ')); if (delay === 0) { - thread.postNonLogMessage(`Invalid delay specified. Format: "1h30m"`); + thread.postSystemMessage(`Invalid delay specified. Format: "1h30m"`); return; } @@ -280,163 +288,15 @@ addInboxServerCommand('close', async (msg, args, thread) => { `)); }); -addInboxServerCommand('block', (msg, args, thread) => { - async function block(userId) { - const user = bot.users.get(userId); - await blocked.block(userId, (user ? `${user.username}#${user.discriminator}` : ''), msg.author.id); - 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 - block(thread.user_id); - } -}); - -addInboxServerCommand('unblock', (msg, args, thread) => { - 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 - unblock(thread.user_id); - } -}); - -addInboxServerCommand('logs', (msg, args, thread) => { - async function getLogs(userId) { - const userThreads = await threads.getClosedThreadsByUserId(userId); - - // 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; - }); - - 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}>`; - })); - - const message = `**Log files for <@${userId}>:**\n${threadLines.join('\n')}`; - - // 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 - getLogs(thread.user_id); - } -}); - -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()}`); -}); - -addInboxServerCommand('loglink', async (msg, args, thread) => { - if (! thread) return; - const logUrl = await thread.getLogUrl(); - thread.postNonLogMessage(`Log URL: ${logUrl}`); -}); - -addInboxServerCommand('suspend', async (msg, args, thread) => { - if (! thread) return; - await thread.suspend(); - thread.postSystemMessage(`**Thread suspended!** This thread will act as closed until unsuspended with \`!unsuspend\``); -}); - -addInboxServerCommand('unsuspend', async (msg, args) => { - const thread = await threads.findSuspendedThreadByChannelId(msg.channel.id); - if (! thread) return; - - const otherOpenThread = await threads.findOpenThreadByUserId(thread.user_id); - if (otherOpenThread) { - thread.postSystemMessage(`Cannot unsuspend; there is another open thread with this user: <#${otherOpenThread.channel_id}>`); - return; - } - - await thread.unsuspend(); - thread.postSystemMessage(`**Thread unsuspended!**`); -}); - module.exports = { async start() { // Load plugins console.log('Loading plugins...'); + await logCommands(bot); + await blocking(bot); + await moving(bot); await snippets(bot); + await suspending(bot); await greeting(bot); await webserver(bot); diff --git a/src/plugins/blocking.js b/src/plugins/blocking.js new file mode 100644 index 0000000..e913968 --- /dev/null +++ b/src/plugins/blocking.js @@ -0,0 +1,42 @@ +const threadUtils = require('../threadUtils'); +const blocked = require("../data/blocked"); +const utils = require("../utils"); + +module.exports = bot => { + const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); + + addInboxServerCommand('block', (msg, args, thread) => { + async function block(userId) { + const user = bot.users.get(userId); + await blocked.block(userId, (user ? `${user.username}#${user.discriminator}` : ''), msg.author.id); + 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 + block(thread.user_id); + } + }); + + addInboxServerCommand('unblock', (msg, args, thread) => { + 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 + unblock(thread.user_id); + } + }); +}; diff --git a/src/plugins/logCommands.js b/src/plugins/logCommands.js new file mode 100644 index 0000000..90594e3 --- /dev/null +++ b/src/plugins/logCommands.js @@ -0,0 +1,54 @@ +const threadUtils = require('../threadUtils'); +const threads = require("../data/threads"); +const moment = require('moment'); +const utils = require("../utils"); + +module.exports = bot => { + const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); + + addInboxServerCommand('logs', (msg, args, thread) => { + async function getLogs(userId) { + const userThreads = await threads.getClosedThreadsByUserId(userId); + + // 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; + }); + + 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}>`; + })); + + const message = `**Log files for <@${userId}>:**\n${threadLines.join('\n')}`; + + // 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 + getLogs(thread.user_id); + } + }); + + addInboxServerCommand('loglink', async (msg, args, thread) => { + if (! thread) return; + const logUrl = await thread.getLogUrl(); + thread.postSystemMessage(`Log URL: ${logUrl}`); + }); +}; diff --git a/src/plugins/moving.js b/src/plugins/moving.js new file mode 100644 index 0000000..2dbdca9 --- /dev/null +++ b/src/plugins/moving.js @@ -0,0 +1,59 @@ +const config = require('../config'); +const Eris = require('eris'); +const threadUtils = require('../threadUtils'); +const transliterate = require("transliteration"); + +module.exports = bot => { + const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); + + 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.postSystemMessage('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()}`); + }); +}; diff --git a/src/plugins/snippets.js b/src/plugins/snippets.js index 3f20ac9..4108e2c 100644 --- a/src/plugins/snippets.js +++ b/src/plugins/snippets.js @@ -30,7 +30,7 @@ module.exports = bot => { }); // Show or add a snippet - addInboxServerCommand('snippet', async (msg, args) => { + addInboxServerCommand('snippet', async (msg, args, thread) => { const trigger = args[0]; if (! trigger) return @@ -40,42 +40,42 @@ module.exports = bot => { 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 "${trigger}" already exists! You can edit or delete it with ${config.prefix}edit_snippet and ${config.prefix}delete_snippet respectively.`); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${trigger}" already exists! You can edit or delete it with ${config.prefix}edit_snippet and ${config.prefix}delete_snippet respectively.`); } else { // If the snippet exists and we're NOT trying to create a new one, show info about the existing snippet - msg.channel.createMessage(`\`${config.snippetPrefix}${trigger}\` replies ${snippet.is_anonymous ? 'anonymously ' : ''}with:\n${snippet.body}`); + utils.postSystemMessageWithFallback(msg.channel, thread, `\`${config.snippetPrefix}${trigger}\` replies ${snippet.is_anonymous ? 'anonymously ' : ''}with:\n${snippet.body}`); } } else { if (text) { // If the snippet doesn't exist and the user wants to create it, create it await snippets.add(trigger, text, false); - msg.channel.createMessage(`Snippet "${trigger}" created!`); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${trigger}" 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 "${trigger}" doesn't exist! You can create it with \`${config.prefix}snippet ${trigger} text\``); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${trigger}" doesn't exist! You can create it with \`${config.prefix}snippet ${trigger} text\``); } } }); bot.registerCommandAlias('s', 'snippet'); - addInboxServerCommand('delete_snippet', async (msg, args) => { + addInboxServerCommand('delete_snippet', async (msg, args, thread) => { const trigger = args[0]; if (! trigger) return; const snippet = await snippets.get(trigger); if (! snippet) { - msg.channel.createMessage(`Snippet "${trigger}" doesn't exist!`); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${trigger}" doesn't exist!`); return; } await snippets.del(trigger); - msg.channel.createMessage(`Snippet "${trigger}" deleted!`); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${trigger}" deleted!`); }); bot.registerCommandAlias('ds', 'delete_snippet'); - addInboxServerCommand('edit_snippet', async (msg, args) => { + addInboxServerCommand('edit_snippet', async (msg, args, thread) => { const trigger = args[0]; if (! trigger) return; @@ -84,23 +84,23 @@ module.exports = bot => { const snippet = await snippets.get(trigger); if (! snippet) { - msg.channel.createMessage(`Snippet "${trigger}" doesn't exist!`); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${trigger}" doesn't exist!`); return; } await snippets.del(trigger); await snippets.add(trigger, text, snippet.isAnonymous); - msg.channel.createMessage(`Snippet "${trigger}" edited!`); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${trigger}" edited!`); }); bot.registerCommandAlias('es', 'edit_snippet'); - addInboxServerCommand('snippets', async msg => { + addInboxServerCommand('snippets', async (msg, args, thread) => { const allSnippets = await snippets.all(); const triggers = allSnippets.map(s => s.trigger); triggers.sort(); - msg.channel.createMessage(`Available snippets (prefix ${config.snippetPrefix}):\n${triggers.join(', ')}`); + utils.postSystemMessageWithFallback(msg.channel, thread, `Available snippets (prefix ${config.snippetPrefix}):\n${triggers.join(', ')}`); }); }; diff --git a/src/plugins/suspending.js b/src/plugins/suspending.js new file mode 100644 index 0000000..7ede6b5 --- /dev/null +++ b/src/plugins/suspending.js @@ -0,0 +1,26 @@ +const threadUtils = require('../threadUtils'); +const threads = require("../data/threads"); + +module.exports = bot => { + const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); + + addInboxServerCommand('suspend', async (msg, args, thread) => { + if (! thread) return; + await thread.suspend(); + thread.postSystemMessage(`**Thread suspended!** This thread will act as closed until unsuspended with \`!unsuspend\``); + }); + + addInboxServerCommand('unsuspend', async msg => { + const thread = await threads.findSuspendedThreadByChannelId(msg.channel.id); + if (! thread) return; + + const otherOpenThread = await threads.findOpenThreadByUserId(thread.user_id); + if (otherOpenThread) { + thread.postSystemMessage(`Cannot unsuspend; there is another open thread with this user: <#${otherOpenThread.channel_id}>`); + return; + } + + await thread.unsuspend(); + thread.postSystemMessage(`**Thread unsuspended!**`); + }); +}; diff --git a/src/plugins/webserver.js b/src/plugins/webserver.js index 5a92468..acd6e34 100644 --- a/src/plugins/webserver.js +++ b/src/plugins/webserver.js @@ -53,7 +53,7 @@ function serveAttachments(res, pathParts) { const id = pathParts[pathParts.length - 2]; if (id.match(/^[0-9]+$/) === null) return notfound(res); - if (desiredFilename.match(/^[0-9a-z\._-]+$/i) === null) return notfound(res); + if (desiredFilename.match(/^[0-9a-z._-]+$/i) === null) return notfound(res); const attachmentPath = attachments.getPath(id); fs.access(attachmentPath, (err) => { diff --git a/src/utils.js b/src/utils.js index dfbb933..6510f49 100644 --- a/src/utils.js +++ b/src/utils.js @@ -223,6 +223,14 @@ function getInboxMention() { else return `<@&${config.mentionRole}> `; } +function postSystemMessageWithFallback(channel, thread, text) { + if (thread) { + thread.postSystemMessage(text); + } else { + channel.createMessage(text); + } +} + module.exports = { BotError, @@ -245,6 +253,7 @@ module.exports = { getMainRole, convertDelayStringToMS, getInboxMention, + postSystemMessageWithFallback, chunk, trimAll,