From ab44ac200839b6da654976ec1978a27de7664dd0 Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 13:15:32 +0000 Subject: [PATCH 01/69] refactor: Jobs system prep. for rename to tasks + improvements. --- src/client.js | 32 +++++------ src/commands/jobs/disable.js | 39 ++++++++----- src/commands/jobs/enable.js | 39 ++++++++----- src/commands/jobs/info.js | 71 +++++++++++++++++------ src/commands/jobs/list.js | 29 ++++++---- src/jobs/ban.js | 57 ++++++++++++------- src/jobs/log.js | 74 +++++++++++++++++++++--- src/jobs/test.js | 6 +- src/jobs/warn.js | 54 +++++++++++++----- src/lib/job.js | 107 ++++++++++++++++++++++++----------- src/types/job.js | 8 +-- 11 files changed, 360 insertions(+), 156 deletions(-) diff --git a/src/client.js b/src/client.js index d6a8266..05849c7 100644 --- a/src/client.js +++ b/src/client.js @@ -9,7 +9,7 @@ const { NODE_ENV = 'development', } = process.env -const PATH_JOBS = join(__dirname, 'jobs') +const PATH_TASKS = join(__dirname, 'tasks') const PATH_TYPES = join(__dirname, 'types') const PATH_COMMANDS = join(__dirname, 'commands') @@ -19,22 +19,22 @@ const client = new CommandoClient({ }) /* - Initialise jobs. + Initialise tasks. */ -client.jobs = new Collection() +client.tasks = new Collection() -const jobFiles = readdirSync(PATH_JOBS).filter(file => file.endsWith('.js')) +const taskFiles = readdirSync(PATH_TASKS).filter(file => file.endsWith('.js')) -for (const file of jobFiles) { +for (const file of taskFiles) { try { - const { default: jobDefinition } = require(`./jobs/${file}`) + const { default: taskDefinition } = require(`./tasks/${file}`) - const jobInstance = new jobDefinition(client) + const taskInstance = new taskDefinition(client) - client.jobs.set(jobInstance.name, jobInstance) + client.tasks.set(taskInstance.name, taskInstance) } catch (e) { - console.warn('Could not load job file: ' + file) + console.warn('Could not load task file: ' + file) console.error(e) } } @@ -58,8 +58,8 @@ client.registry.registerGroups([ name: 'Moderation', }, { - id: 'jobs', - name: 'Jobs', + id: 'tasks', + name: 'Tasks', }, ]) @@ -103,11 +103,11 @@ client.on('message', msg => { return } - client.jobs - .filter(job => job.enabled) - .forEach(job => { - if (job.shouldExecute(msg)) { - job.run(msg) + client.tasks + .filter(task => task.enabled) + .forEach(task => { + if (task.shouldExecute(msg)) { + task.run(msg) } }) }) diff --git a/src/commands/jobs/disable.js b/src/commands/jobs/disable.js index 07d4efb..991191a 100644 --- a/src/commands/jobs/disable.js +++ b/src/commands/jobs/disable.js @@ -1,29 +1,30 @@ import { Command } from 'discord.js-commando' +import { RichEmbed } from 'discord.js' import { OWNER_IDS, BOT_DEVELOPER_IDS, MODERATOR_ROLE_IDS, } from '../../utils/constants' +import { inlineCode } from '../../utils/string' const ALLOWED_ROLES = [...MODERATOR_ROLE_IDS] const ALLOWED_USERS = [...OWNER_IDS, ...BOT_DEVELOPER_IDS] -module.exports = class JobsDisableCommand extends Command { +module.exports = class TasksDisableCommand extends Command { constructor(client) { super(client, { args: [ { - key: 'job', - type: 'job', - prompt: 'the job to disable?', + key: 'task', + type: 'task', + prompt: 'the task to disable?', }, ], - name: 'disable-job', - group: 'jobs', - aliases: ['jd'], + name: 'disable-task', + group: 'tasks', guildOnly: true, memberName: 'disable', - description: 'Disable a job', + description: 'Disable a task', }) } @@ -36,13 +37,23 @@ module.exports = class JobsDisableCommand extends Command { } async run(msg, args) { - const { job } = args + const { task } = args - if (job.enabled) { - job.enabled = false - return msg.channel.send(`Job "${job}" has been disabled.`) - } else { - return msg.channel.send(`Job "${job}" was already disabled.`) + let alreadyDisabled = !task.enabled + + if (task.enabled) { + task.enabled = false } + + msg.channel.send( + new RichEmbed() + .setTitle('Disable Task') + .setColor(alreadyDisabled ? 'ORANGE' : 'GREEN') + .setDescription( + alreadyDisabled + ? `Task ${inlineCode(task.name)} was already disabled.` + : `Sucessfully disabled task ${inlineCode(task.name)}.` + ) + ) } } diff --git a/src/commands/jobs/enable.js b/src/commands/jobs/enable.js index 4c2187a..7806ad9 100644 --- a/src/commands/jobs/enable.js +++ b/src/commands/jobs/enable.js @@ -1,29 +1,30 @@ import { Command } from 'discord.js-commando' +import { RichEmbed } from 'discord.js' import { OWNER_IDS, BOT_DEVELOPER_IDS, MODERATOR_ROLE_IDS, } from '../../utils/constants' +import { inlineCode } from '../../utils/string' const ALLOWED_ROLES = [...MODERATOR_ROLE_IDS] const ALLOWED_USERS = [...OWNER_IDS, ...BOT_DEVELOPER_IDS] -module.exports = class JobsEnableCommand extends Command { +module.exports = class TasksEnableCommand extends Command { constructor(client) { super(client, { args: [ { - key: 'job', - type: 'job', - prompt: 'the job to enable?', + key: 'task', + type: 'task', + prompt: 'the task to enable?', }, ], - name: 'enable-job', - group: 'jobs', - aliases: ['je'], + name: 'enable-task', + group: 'tasks', guildOnly: true, memberName: 'enable', - description: 'Enable a job', + description: 'Enable a task', }) } @@ -36,13 +37,23 @@ module.exports = class JobsEnableCommand extends Command { } async run(msg, args) { - const { job } = args + const { task } = args - if (!job.enabled) { - job.enabled = true - return msg.channel.send(`Job "${job}" has been enabled.`) - } else { - return msg.channel.send(`Job "${job}" was already enabled.`) + let alreadyEnabled = task.enabled + + if (!task.enabled) { + task.enabled = true } + + msg.channel.send( + new RichEmbed() + .setTitle('Enable Task') + .setColor(alreadyEnabled ? 'ORANGE' : 'GREEN') + .setDescription( + alreadyEnabled + ? `Task ${inlineCode(task.name)} was already enabled.` + : `Sucessfully enabled task ${inlineCode(task.name)}.` + ) + ) } } diff --git a/src/commands/jobs/info.js b/src/commands/jobs/info.js index 42356cc..1c8976a 100644 --- a/src/commands/jobs/info.js +++ b/src/commands/jobs/info.js @@ -1,31 +1,33 @@ import { Command } from 'discord.js-commando' import { RichEmbed } from 'discord.js' import { + EMOJIS, EMPTY_MESSAGE, OWNER_IDS, BOT_DEVELOPER_IDS, MODERATOR_ROLE_IDS, } from '../../utils/constants' +import { blockCode } from '../../utils/string' const ALLOWED_ROLES = [...MODERATOR_ROLE_IDS] const ALLOWED_USERS = [...OWNER_IDS, ...BOT_DEVELOPER_IDS] -module.exports = class JobsEnableCommand extends Command { +module.exports = class TasksEnableCommand extends Command { constructor(client) { super(client, { args: [ { - key: 'job', - type: 'job', - prompt: 'the job to view info about?', + key: 'task', + type: 'task', + prompt: 'the task to view info about?', }, ], - name: 'job-info', - group: 'jobs', - aliases: ['ji'], + name: 'task-info', + group: 'tasks', + aliases: ['task'], guildOnly: true, memberName: 'info', - description: 'View information about a job.', + description: 'View information about a task.', }) } @@ -38,22 +40,57 @@ module.exports = class JobsEnableCommand extends Command { } async run(msg, args) { - const { job } = args + const { task } = args - let ignoredRoles = job.ignored.roles.map(roleId => { - return msg.guild.roles.get(roleId).name - }) + let ignoredRoles = task.ignored.roles + .filter(roleId => { + return msg.guild.roles.has(roleId) + }) + .map(roleId => { + return msg.guild.roles.get(roleId) + }) + let ignoredUsers = task.ignored.users + .filter(userId => { + return msg.guild.members.has(userId) + }) + .map(userId => { + return this.client.users.get(userId) + }) + let ignoredChannels = task.ignored.channels + .filter(chanId => { + return msg.guild.channels.has(chanId) + }) + .map(chanId => { + return this.client.channels.get(chanId) + }) if (!ignoredRoles.length) { ignoredRoles = ['None'] } + if (!ignoredUsers.length) { + ignoredUsers = ['None'] + } + if (!ignoredChannels.length) { + ignoredChannels = ['None'] + } const embed = new RichEmbed() - embed.setTitle('Job (' + job.name + ')') - embed.setDescription(job.description) - embed.addField('Status', job.enabled) - embed.addField('Guild Only', job.guildOnly) - embed.addField('Ignored Roles', ignoredRoles.join(', ')) + embed.setTitle('Task (' + task.name + ')') + embed.setDescription(task.description) + embed.addField( + 'Enabled', + this.client.emojis.get(EMOJIS[task.getStatus().toUpperCase()]), + true + ) + embed.addField('Guild Only', task.guildOnly, true) + embed.addField('DM Only', task.dmOnly, true) + embed.addField('Ignored Users', ignoredUsers.join(', '), true) + embed.addField('Ignored Roles', ignoredRoles.join(', '), true) + embed.addField('Ignored Channels', ignoredChannels.join(', '), true) + embed.addField( + 'Configuration', + blockCode(JSON.stringify(task.config, null, 2), 'json') + ) return msg.channel.send(EMPTY_MESSAGE, { embed }) } diff --git a/src/commands/jobs/list.js b/src/commands/jobs/list.js index c6b7919..f6f309d 100644 --- a/src/commands/jobs/list.js +++ b/src/commands/jobs/list.js @@ -1,24 +1,26 @@ import { Command } from 'discord.js-commando' import { RichEmbed } from 'discord.js' import { + EMOJIS, EMPTY_MESSAGE, OWNER_IDS, BOT_DEVELOPER_IDS, MODERATOR_ROLE_IDS, } from '../../utils/constants' +import { inlineCode } from '../../utils/string' const ALLOWED_ROLES = [...MODERATOR_ROLE_IDS] const ALLOWED_USERS = [...OWNER_IDS, ...BOT_DEVELOPER_IDS] -module.exports = class JobsEnableCommand extends Command { +module.exports = class TasksEnableCommand extends Command { constructor(client) { super(client, { - name: 'list-jobs', - group: 'jobs', - aliases: ['jl'], + name: 'list-tasks', + group: 'tasks', + aliases: ['tasks'], guildOnly: true, memberName: 'list', - description: 'List all jobs.', + description: 'List all tasks.', }) } @@ -32,13 +34,18 @@ module.exports = class JobsEnableCommand extends Command { async run(msg) { const embed = new RichEmbed() - embed.setTitle('Job List') - embed.setDescription( - 'Jobs are basically micro-tasks which are executed for every message.' - ) + embed + .setTitle('Task List') + .setDescription( + `For more detailed info. type: ${inlineCode('!task ')}.` + ) - this.client.jobs.forEach(job => { - embed.addField(job.name, job.getStatus(), true) + this.client.tasks.forEach(task => { + embed.addField( + task.name, + this.client.emojis.get(EMOJIS[task.getStatus().toUpperCase()]), + true + ) }) return msg.channel.send(EMPTY_MESSAGE, { embed }) diff --git a/src/jobs/ban.js b/src/jobs/ban.js index ca4136f..578aa6c 100644 --- a/src/jobs/ban.js +++ b/src/jobs/ban.js @@ -1,28 +1,29 @@ import { RichEmbed } from 'discord.js' -import Job from '../lib/job' -import { banWords } from '../services/ban-words' +import Task from '../lib/task' +import moderation from '../services/moderation' import { MODERATOR_ROLE_IDS, PROTECTED_ROLE_IDS, EMPTY_MESSAGE, } from '../utils/constants' +import { blockCode } from '../utils/string' // Don't *actually* ban - real bans make testing hard! -const DEBUG_MODE = true +const DEBUG_MODE = process.env.NODE_ENV === 'development' -export default class BanJob extends Job { +export default class BanTask extends Task { constructor(client) { super(client, { name: 'ban', - description: 'Automatically bans users who violate the banned word list.', - enabled: false, + description: 'Auto-ban users who mention a "ban" trigger word.', + enabled: true, ignored: { - roles: [...MODERATOR_ROLE_IDS, ...PROTECTED_ROLE_IDS], + roles: [...new Set(MODERATOR_ROLE_IDS.concat(PROTECTED_ROLE_IDS))], }, guildOnly: true, config: { logChannel: { - name: 'ban-log', + name: 'moderation', }, }, }) @@ -31,16 +32,20 @@ export default class BanJob extends Job { shouldExecute(msg) { // None of the ban words were mentioned - bail. if ( - !banWords.some(word => - msg.content.toLowerCase().includes(word.toLowerCase()) - ) + !moderation + .get('triggers') + .filter(({ action }) => action === 'ban') + .some(({ trigger }) => { + return msg.content.toLowerCase().includes(trigger.toLowerCase()) + }) + .value() ) { return false } // We don't have permission to ban - bail. if (!msg.channel.permissionsFor(msg.client.user).has('BAN_MEMBERS')) { - return !!console.warn('[BanJob] Cannot ban - lacking permission.') + return !!console.warn('[BanTask] Cannot ban - lacking permission.') } const botMember = msg.guild.member(msg.client.user) @@ -49,7 +54,7 @@ export default class BanJob extends Job { // Our role is not high enough in the hierarchy to ban - bail. if (botHighestRole < userHighestRole) { - return !!console.warn('[BanJob] Cannot ban - role too low.') + return !!console.warn('[BanTask] Cannot ban - role too low.') } return true @@ -62,7 +67,7 @@ export default class BanJob extends Job { if (!logChannel) { return console.warn( - `WarnJob: Could not find channel with name ${this.config.logChannel.name}` + `[BanTask]: Could not find channel with name ${this.config.logChannel.name}` ) } @@ -77,18 +82,32 @@ export default class BanJob extends Job { } log(msg, logChannel) { + const excerpt = + msg.cleanContent.length > 150 + ? msg.cleanContent.substring(0, 150) + '...' + : msg.cleanContent + if (!logChannel) { return console.info( `Banned user: ${msg.author}`, - `Due to message: ${msg.cleanContent}` + `Due to message:`, + msg.cleanContent ) } const embed = new RichEmbed() - embed.setTitle('Banned User') - embed.setAuthor(msg.author, msg.author.avatarURL) - embed.setTimestamp() - embed.addField('Triggering Message', msg.content) + embed + .setTitle('Moderation - Ban Notice') + .setColor('RED') + .setDescription('Found one or more trigger words with action: `ban`.') + .addField('User', msg.member, true) + .addField('Channel', msg.channel, true) + .setTimestamp() + .addField('Message Excerpt', blockCode(excerpt)) + + if (DEBUG_MODE) { + embed.addField('NOTE', 'Debug mode enabled - no ban was actually issued.') + } logChannel.send(EMPTY_MESSAGE, { embed }) } diff --git a/src/jobs/log.js b/src/jobs/log.js index 62d147d..3f15533 100644 --- a/src/jobs/log.js +++ b/src/jobs/log.js @@ -1,14 +1,22 @@ -import Job from '../lib/job' +import { RichEmbed } from 'discord.js' +import Task from '../lib/task' import { trySend } from '../utils/messages' +import { inlineCode } from '../utils/string' -export default class LogJob extends Job { +export default class LogTask extends Task { constructor(client) { super(client, { name: 'log', - events: ['ready', 'resume', 'commandRun', 'unknownCommand'], + events: [ + 'ready', + 'resume', + 'commandRun', + 'commandError', + 'unknownCommand', + ], description: 'Logs various events (connection, command invocations etc.) for debugging purposes/to aid development.', - enabled: false, + enabled: true, config: { connectionChannel: { name: 'connection', @@ -27,28 +35,76 @@ export default class LogJob extends Job { ready() { trySend( this.config.connectionChannel, - `Successfully connected - I am now online.` + null, + this.buildEmbed(null, { title: 'Connection Initiated', color: 'GREEN' }) ) } resume() { trySend( - this.config.connectionChannel, - `The connection was lost but automatically resumed.` + this.config.commandChannel, + null, + this.buildEmbed(null, { title: 'Connection Resumed', color: 'BLUE' }) ) } commandRun(cmd, _, msg) { trySend( this.config.commandChannel, - `The command ${cmd.name} was ran in ${msg.channel}` + null, + this.buildEmbed(msg, { title: 'Command Invocation', color: 'GREEN' }, [ + { name: 'Command', value: inlineCode(msg.command.name) }, + ]) + ) + } + + commandError(cmd, err, msg) { + trySend( + this.config.commandChannel, + null, + this.buildEmbed(msg, { title: 'Command Error', color: 'RED' }, [ + { name: 'Command', value: inlineCode(cmd.name) }, + ]) ) } unknownCommand(msg) { trySend( this.config.commandChannel, - `Unknown command, triggered by message from ${msg.author} in ${msg.channel}.` + null, + this.buildEmbed(msg, { title: 'Unknown Command', color: 'ORANGE' }, [ + { + name: 'Command', + value: inlineCode(msg.cleanContent), + }, + ]) ) } + + buildEmbed(msg, options, fields = []) { + const embed = new RichEmbed().setTimestamp() + + if (options.title) { + embed.setTitle(options.title) + } + if (options.description) { + embed.setDescription(options.description) + } + if (options.color) { + embed.setColor(options.color) + } + + if (msg) { + embed + .addField('Guild', msg.guild ? msg.guild : 'DM', true) + .addField('Channel', msg.channel, true) + .addField('User', msg.author, true) + } + + for (const field of fields) { + embed.addField(field.name, field.value) + } + + return embed + } } diff --git a/src/jobs/test.js b/src/jobs/test.js index 6bc43e0..bdf8e6f 100644 --- a/src/jobs/test.js +++ b/src/jobs/test.js @@ -1,6 +1,6 @@ -import Job from '../lib/job' +import Task from '../lib/task' -export default class TestJob extends Job { +export default class TestTask extends Task { constructor(client) { super(client, { name: 'test', @@ -10,6 +10,6 @@ export default class TestJob extends Job { } run() { - console.log('test job executed') + console.log('[TestTask] Executed!') } } diff --git a/src/jobs/warn.js b/src/jobs/warn.js index 704d238..c5d9501 100644 --- a/src/jobs/warn.js +++ b/src/jobs/warn.js @@ -1,15 +1,22 @@ -import Job from '../lib/job' -import { MODERATOR_ROLE_IDS, PROTECTED_ROLE_IDS } from '../utils/constants' -import { banWords } from '../services/ban-words' +import { RichEmbed } from 'discord.js' +import Task from '../lib/task' +import { + EMPTY_MESSAGE, + MODERATOR_ROLE_IDS, + PROTECTED_ROLE_IDS, +} from '../utils/constants' +import { blockCode } from '../utils/string' +import moderation from '../services/moderation' -export default class WarnJob extends Job { +export default class WarnTask extends Task { constructor(client) { super(client, { name: 'warn', - description: 'Warn moderators when a user utters a banned word.', - enabled: false, + description: + 'Auto-warn moderators when users mention a "warn" trigger word.', + enabled: true, ignored: { - roles: [...MODERATOR_ROLE_IDS, ...PROTECTED_ROLE_IDS], + roles: [...new Set(MODERATOR_ROLE_IDS.concat(PROTECTED_ROLE_IDS))], }, guildOnly: true, config: { @@ -17,16 +24,20 @@ export default class WarnJob extends Job { name: 'Moderators', }, notifyChannel: { - name: 'spam-log', + name: 'moderation', }, }, }) } shouldExecute(msg) { - return banWords.some(word => - msg.content.toLowerCase().includes(word.toLowerCase()) - ) + return moderation + .get('triggers') + .filter(({ action }) => action === 'warn') + .some(({ trigger }) => { + return msg.content.toLowerCase().includes(trigger.toLowerCase()) + }) + .value() } run(msg) { @@ -39,12 +50,25 @@ export default class WarnJob extends Job { if (!notifyChannel) { return console.warn( - `WarnJob: Could not find channel with name ${this.config.notifyChannel.name}` + `[WarnTask] Could not find channel with name ${this.config.notifyChannel.name}` ) } - notifyChannel.send( - `${notifyRole} Suspicious user: ${msg.author} in channel ${msg.channel}` - ) + const excerpt = + msg.cleanContent.length > 150 + ? msg.cleanContent.substring(0, 150) + '...' + : msg.cleanContent + + const embed = new RichEmbed() + embed + .setTitle('Moderation - Auto-warning') + .setColor('ORANGE') + .setDescription('Found one or more trigger words with action: `warn`.') + .addField('User', msg.member, true) + .addField('Channel', msg.channel, true) + .setTimestamp() + .addField('Message Excerpt', blockCode(excerpt)) + + notifyChannel.send(notifyRole ? notifyRole : EMPTY_MESSAGE, { embed }) } } diff --git a/src/lib/job.js b/src/lib/job.js index d1e7983..cf5d832 100644 --- a/src/lib/job.js +++ b/src/lib/job.js @@ -1,7 +1,7 @@ import EventEmitter from 'events' /** - * A Job is a task which by default runs for every single message received so + * A Task is a task which by default runs for every single message received so * long as it is enabled and its shouldExecute() returns true. * * Example usages: @@ -9,25 +9,25 @@ import EventEmitter from 'events' * - Check message contents against a banned word list (and optionally warn/kick/ban) * - etc. * - * A Job does not necessarily need to process messages however - there is a - * concept of event-only jobs. Such a job should simply return false in - * shouldExecute and specify a list of Discord/Commando events via JobOptions.events. + * A Task does not necessarily need to process messages however - there is a + * concept of event-only tasks. Such a task should simply return false in + * shouldExecute and specify a list of Discord/Commando events via TaskOptions.events. * - * When the job is enabled, listeners for those events will be attached to the - * CommandoClient and when the job is disabled they will be removed. + * When the task is enabled, listeners for those events will be attached to the + * CommandoClient and when the task is disabled they will be removed. * - * See `src/jobs/log.js` for an example of an event-only job. + * See `src/tasks/log.js` for an example of an event-only task. * - * @event Job#enabled - * @event Job#disabled + * @event Task#enabled + * @event Task#disabled * @extends EventEmitter * @abstract */ -export default class Job extends EventEmitter { +export default class Task extends EventEmitter { /** - * Create a new Job. + * Create a new Task. * @param {CommandoClient} client The CommandoClient instance. - * @param {JobOptions} options The options for the Job. + * @param {TaskOptions} options The options for the Task. */ constructor(client, options = {}) { super() @@ -35,23 +35,23 @@ export default class Job extends EventEmitter { this.client = client /* - Jobs are stored as a key-value pair in a Collection (Map) on the client. + Tasks are stored as a key-value pair in a Collection (Map) on the client. The name is used as the key, as such it must be both provided and unique. */ if (!options.name) { - throw new Error('Job lacks required option - name.') + throw new Error('Task lacks required option - name.') } - if (client.jobs.has(options.name)) { + if (client.tasks.has(options.name)) { throw new Error( - `Job names must be unique, conflicting name - ${options.name}.` + `Task names must be unique, conflicting name - ${options.name}.` ) } /* A list of user, role, channel and category IDs. - If any of these match then the job will NEVER be executed. + If any of these match then the task will NEVER be executed. */ if (!options.ignored) { options.ignored = {} @@ -81,14 +81,23 @@ export default class Job extends EventEmitter { options.enabled = false } + if (typeof options.dmOnly === 'undefined') { + options.dmOnly = false + } + if (typeof options.guildOnly === 'undefined') { - options.guildOnly = true + options.guildOnly = false + } + + if (options.guildOnly && options.dmOnly) { + console.warn('Conflicting options - guildOnly and warnOnly.') } this.name = options.name this.events = options.events this.config = options.config this.ignored = options.ignored + this.dmOnly = options.dmOnly this.guildOnly = options.guildOnly this.description = options.description || '' @@ -105,8 +114,18 @@ export default class Job extends EventEmitter { this[event] = this[event].bind(this) } - this.on('enabled', this.attachEventListeners) - this.on('disabled', this.removeEventListeners) + if (this.inhibit) { + this._inhibit = this.inhibit.bind(this) + } + + this.on('enabled', () => { + this.attachEventListeners() + this.registerInhibitor() + }) + this.on('disabled', () => { + this.removeEventListeners() + this.unregisterInhibitor() + }) // NOTE: Must come last because the setter triggers an event (enabled). this.enabled = options.enabled @@ -152,21 +171,41 @@ export default class Job extends EventEmitter { } /** - * The job will not be ran if this returns `false` - even if the job is enabled. + * Register the inhibitor with the `DiscordClient`, if applicable. + */ + registerInhibitor() { + if (this.inhibit && typeof this.inhibit === 'function') { + this.client.dispatcher.addInhibitor(this._inhibit) + } + } + + /** + * Register the inhibitor with the `DiscordClient`, if applicable. + */ + unregisterInhibitor() { + if (this.inhibit && typeof this.inhibit === 'function') { + this.client.dispatcher.removeInhibitor(this._inhibit) + } + } + + /** + * The task will not be ran if this returns `false` - even if the task is enabled. * * By default it checks `this.ignored.roles|users|channels`, returning `false` for * any matches - if no matches are found, it returns `true`. * * @param {CommandoMessage} msg - * @returns {boolean} Whether to run (execute) the Job or not. + * @returns {boolean} Whether to run (execute) the Task or not. */ shouldExecute(msg) { if (msg.channel.type === 'dm') { if (this.guildOnly) { return false } - - return true + } else { + if (this.dmOnly) { + return false + } } if (this.ignored.roles.length) { @@ -187,7 +226,7 @@ export default class Job extends EventEmitter { } /** - * The job itself - ran if `enabled` is `true` and `shouldExecute` returns `true`. + * The task itself - ran if `enabled` is `true` and `shouldExecute` returns `true`. * * @param {CommandoMessage} message */ @@ -195,16 +234,16 @@ export default class Job extends EventEmitter { run(msg) {} /** - * Returns a string representation of the Job. + * Returns a string representation of the Task. * - * @return {string} The string representation (e.g. ). + * @return {string} The string representation (e.g. ). */ toString() { - return `` + return `` } /** - * Returns the enabled status of the Job as a string. + * Returns the enabled status of the Task as a string. * * @return {string} Either `enabled` or `disabled`. */ @@ -213,20 +252,20 @@ export default class Job extends EventEmitter { } /** - * Is the job enabled? + * Is the Task enabled? * - * @return {boolean} Is this job enabled? + * @return {boolean} Is this Task enabled? */ get enabled() { return this._enabled } /** - * Set a job as enabled or disabled. + * Set a Task as enabled or disabled. * * @param {boolean} enabled Set as enabled or disabled. - * @fires Job#enabled - * @fires Job#disabled + * @fires Task#enabled + * @fires Task#disabled */ set enabled(enabled) { this._enabled = enabled diff --git a/src/types/job.js b/src/types/job.js index 90b837a..e7226ad 100644 --- a/src/types/job.js +++ b/src/types/job.js @@ -1,15 +1,15 @@ import { ArgumentType } from 'discord.js-commando' -module.exports = class JobArgumentType extends ArgumentType { - constructor(client, id = 'job') { +module.exports = class TaskArgumentType extends ArgumentType { + constructor(client, id = 'task') { super(client, id) } validate(value) { - return this.client.jobs.has(value) + return this.client.tasks.has(value) } parse(value) { - return this.client.jobs.get(value) + return this.client.tasks.get(value) } } From 23720ed7a49a240295280aa0b6776907a7594e57 Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 13:16:22 +0000 Subject: [PATCH 02/69] chore: Rename jobs to tasks. --- src/commands/{jobs => tasks}/disable.js | 0 src/commands/{jobs => tasks}/enable.js | 0 src/commands/{jobs => tasks}/info.js | 0 src/commands/{jobs => tasks}/list.js | 0 src/{jobs => tasks}/ban.js | 0 src/{jobs => tasks}/log.js | 0 src/{jobs => tasks}/test.js | 0 src/{jobs => tasks}/warn.js | 0 src/types/{job.js => task.js} | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename src/commands/{jobs => tasks}/disable.js (100%) rename src/commands/{jobs => tasks}/enable.js (100%) rename src/commands/{jobs => tasks}/info.js (100%) rename src/commands/{jobs => tasks}/list.js (100%) rename src/{jobs => tasks}/ban.js (100%) rename src/{jobs => tasks}/log.js (100%) rename src/{jobs => tasks}/test.js (100%) rename src/{jobs => tasks}/warn.js (100%) rename src/types/{job.js => task.js} (100%) diff --git a/src/commands/jobs/disable.js b/src/commands/tasks/disable.js similarity index 100% rename from src/commands/jobs/disable.js rename to src/commands/tasks/disable.js diff --git a/src/commands/jobs/enable.js b/src/commands/tasks/enable.js similarity index 100% rename from src/commands/jobs/enable.js rename to src/commands/tasks/enable.js diff --git a/src/commands/jobs/info.js b/src/commands/tasks/info.js similarity index 100% rename from src/commands/jobs/info.js rename to src/commands/tasks/info.js diff --git a/src/commands/jobs/list.js b/src/commands/tasks/list.js similarity index 100% rename from src/commands/jobs/list.js rename to src/commands/tasks/list.js diff --git a/src/jobs/ban.js b/src/tasks/ban.js similarity index 100% rename from src/jobs/ban.js rename to src/tasks/ban.js diff --git a/src/jobs/log.js b/src/tasks/log.js similarity index 100% rename from src/jobs/log.js rename to src/tasks/log.js diff --git a/src/jobs/test.js b/src/tasks/test.js similarity index 100% rename from src/jobs/test.js rename to src/tasks/test.js diff --git a/src/jobs/warn.js b/src/tasks/warn.js similarity index 100% rename from src/jobs/warn.js rename to src/tasks/warn.js diff --git a/src/types/job.js b/src/types/task.js similarity index 100% rename from src/types/job.js rename to src/types/task.js From f94abdc47feffa54980a072492f63492b7f7967d Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 13:17:18 +0000 Subject: [PATCH 03/69] fix: Default NODE_ENV globally to development if it isn't set. --- src/client.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 05849c7..cc3d797 100644 --- a/src/client.js +++ b/src/client.js @@ -3,10 +3,14 @@ import { join } from 'path' import { Collection } from 'discord.js' import { CommandoClient } from 'discord.js-commando' +if (!process.env.NODE_ENV) { + process.env.NODE_ENV = 'development' +} + const { OWNERS_IDS = '269617876036616193', // Default to @evan#9589 COMMAND_PREFIX = '!', - NODE_ENV = 'development', + NODE_ENV, } = process.env const PATH_TASKS = join(__dirname, 'tasks') From 570d917bddc8ae9aeec3202a26751b8ba6cea808 Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 13:17:47 +0000 Subject: [PATCH 04/69] fix: Remove unused import. --- src/utils/messages.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/messages.js b/src/utils/messages.js index 25fec1c..080dd91 100644 --- a/src/utils/messages.js +++ b/src/utils/messages.js @@ -5,7 +5,6 @@ import { DELETE_ERRORS_AFTER_MS, DELETE_INVOCATIONS_AFTER_MS, } from './constants' -import { CommandMessage } from 'discord.js-commando' /* Delete a message safely, if possible. From 59b7af68a74da263ef13222bc764a5e928193d34 Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 13:18:01 +0000 Subject: [PATCH 05/69] fix: Default to empty message. --- src/utils/messages.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/messages.js b/src/utils/messages.js index 080dd91..42f3a16 100644 --- a/src/utils/messages.js +++ b/src/utils/messages.js @@ -1,5 +1,6 @@ import client from '../client' import { + EMPTY_MESSAGE, AUTOMATICALLY_DELETE_ERRORS, AUTOMATICALLY_DELETE_INVOCATIONS, DELETE_ERRORS_AFTER_MS, @@ -61,7 +62,7 @@ export function trySend(channelResolvable, message, embed = {}) { } } - channel.send(message, embed) + channel.send(message || EMPTY_MESSAGE, embed) } /* From 872db039e0f28fdd248e9f2204ee2c2c2c5b0511 Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 13:18:18 +0000 Subject: [PATCH 06/69] fix: Missing constant. --- src/utils/constants/production.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/constants/production.js b/src/utils/constants/production.js index 0dac097..ac68e1f 100644 --- a/src/utils/constants/production.js +++ b/src/utils/constants/production.js @@ -21,6 +21,7 @@ export const ROLES = Object.freeze({ LIBRARY_MAINTAINERS: '359877575738130432', NITRO_BOOSTERS: '585579626534010880', VUE_VIXENS: '504844406336258048', + BOT_DEVELOPERS: 'n/a', }) /* From eb1e781047f173de0c7226dc903f6b2c0d265cdb Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 13:18:59 +0000 Subject: [PATCH 07/69] fix: Missing constant. --- src/utils/constants/development.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/constants/development.js b/src/utils/constants/development.js index 888d650..9ef46f5 100644 --- a/src/utils/constants/development.js +++ b/src/utils/constants/development.js @@ -19,6 +19,7 @@ export const ROLES = Object.freeze({ LIBRARY_MAINTAINERS: '618043006506893322', NITRO_BOOSTERS: '630722169235832853', VUE_VIXENS: '630724481064632330', + BOT_DEVELOPERS: '618503802995212309', }) /* From 67feaab0a18b8d7764cd87393adf759bb42f3e44 Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 13:19:14 +0000 Subject: [PATCH 08/69] fix: Constant duplication. --- src/utils/constants/development.js | 17 ------------- src/utils/constants/index.js | 40 ++++++++++++++++++++++++------ src/utils/constants/production.js | 17 ------------- 3 files changed, 33 insertions(+), 41 deletions(-) diff --git a/src/utils/constants/development.js b/src/utils/constants/development.js index 9ef46f5..e3e7a63 100644 --- a/src/utils/constants/development.js +++ b/src/utils/constants/development.js @@ -1,13 +1,3 @@ -/* - Various important and or noteworthy user IDs. -*/ -export const USERS = Object.freeze({ - EVAN: '269617876036616193', - GUSTO: '287377476647124992', - ELFAYER: '248017273950830593', - SUSTAINED: '136620462821081088', -}) - /* Various important and or noteworthy role IDs. */ @@ -41,10 +31,3 @@ export const BOT_DEVELOPER_IDS = Object.freeze([ - can run commands set as ownerOnly */ export const OWNER_IDS = Object.freeze(BOT_DEVELOPER_IDS) - -/* - Protected roles. - - - moderation-related commands have no effect -*/ -export const PROTECTED_USER_IDS = Object.freeze([USERS.EVAN, USERS.GUSTO]) diff --git a/src/utils/constants/index.js b/src/utils/constants/index.js index 00aef4d..2aa4898 100644 --- a/src/utils/constants/index.js +++ b/src/utils/constants/index.js @@ -3,13 +3,25 @@ import { resolve } from 'path' const { NODE_ENV = 'development' } = process.env const IMPORT_FILE = `./${NODE_ENV}.js` -const { - USERS, - ROLES, - OWNER_IDS, - BOT_DEVELOPER_IDS, - PROTECTED_USER_IDS, -} = require(IMPORT_FILE) +const { ROLES, OWNER_IDS, BOT_DEVELOPER_IDS } = require(IMPORT_FILE) + +/* + Various important and or noteworthy user IDs. +*/ +const USERS = Object.freeze({ + BOT: '619109039166717972', + EVAN: '269617876036616193', + GUSTO: '287377476647124992', + ELFAYER: '248017273950830593', + SUSTAINED: '136620462821081088', +}) + +/* + Protected user IDs. + + - moderation-related commands have no effect +*/ +const PROTECTED_USER_IDS = Object.freeze([USERS.EVAN, USERS.GUSTO]) /* Protected roles. @@ -45,6 +57,9 @@ const EMPTY_MESSAGE = '\u200b' const DATA_DIR = resolve(__dirname, '../../../data') +/* + Configuration relating to the messages utility. +*/ const AUTOMATICALLY_DELETE_ERRORS = true const AUTOMATICALLY_DELETE_INVOCATIONS = true const DELETE_ERRORS_AFTER_MS = 30000 @@ -71,12 +86,23 @@ const EMOJIS = { FIRST: '661289441171865660', LAST: '661289441218002984', }, + ENABLED: '661514135573495818', + DISABLED: '661514135594467328', +} + +EMOJIS.SUCCESS = EMOJIS.ENABLED +EMOJIS.FAILURE = EMOJIS.DISABLED + +const GUILDS = { + TEST: '617839535727968282', + LIVE: '325477692906536972', } export { USERS, ROLES, EMOJIS, + GUILDS, OWNER_IDS, PROTECTED_USER_IDS, PROTECTED_ROLE_IDS, diff --git a/src/utils/constants/production.js b/src/utils/constants/production.js index ac68e1f..7cf9adb 100644 --- a/src/utils/constants/production.js +++ b/src/utils/constants/production.js @@ -1,15 +1,5 @@ const { OWNERS } = process.env -/* - Various important and or noteworthy user IDs. -*/ -export const USERS = Object.freeze({ - EVAN: '269617876036616193', - GUSTO: '287377476647124992', - ELFAYER: '248017273950830593', - SUSTAINED: '136620462821081088', -}) - /* Various important and or noteworthy role IDs. */ @@ -43,10 +33,3 @@ export const BOT_DEVELOPER_IDS = Object.freeze([ - can run commands set as ownerOnly */ export const OWNER_IDS = Object.freeze(OWNERS) - -/* - Protected roles. - - - moderation-related commands have no effect -*/ -export const PROTECTED_USER_IDS = Object.freeze([USERS.EVAN, USERS.GUSTO]) From f9c7742df0299924e199cdc266ae6a2efdfb6b63 Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 13:19:41 +0000 Subject: [PATCH 09/69] feat: Add beta task. --- src/tasks/beta.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/tasks/beta.js diff --git a/src/tasks/beta.js b/src/tasks/beta.js new file mode 100644 index 0000000..8cd68c1 --- /dev/null +++ b/src/tasks/beta.js @@ -0,0 +1,33 @@ +import Task from '../lib/task' +import { ROLES, GUILDS } from '../utils/constants' + +export default class BetaTask extends Task { + constructor(client) { + super(client, { + name: 'beta', + description: + 'Only allow specific roles to interact with the bot on the (live) server during the beta period.', + enabled: true, + guildOnly: false, + config: { + guild: GUILDS.LIVE, + allowedRoles: Object.values(ROLES), + allowDMs: true, + }, + }) + } + + inhibit(msg) { + const { allowDMs, allowedRoles } = this.config + + if (msg.guild && msg.guild.id === this.config.guild) { + if (!msg.member.roles.some(role => allowedRoles.includes(role.id))) { + return 'beta-restriction' + } + } else { + if (!allowDMs) { + return 'beta-restriction' + } + } + } +} From 75014619bc0e86786f527342c6070c0eeb872fed Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 13:20:51 +0000 Subject: [PATCH 10/69] feat: Moderation system improvements. --- data/ban-words.txt | 4 - data/moderation/db.json | 20 +++++ package-lock.json | 30 +++++++- package.json | 1 + src/commands/moderation/add-ban-word.js | 37 --------- src/commands/moderation/add-trigger.js | 88 ++++++++++++++++++++++ src/commands/moderation/list-ban-words.js | 25 ------ src/commands/moderation/list-triggers.js | 51 +++++++++++++ src/commands/moderation/remove-ban-word.js | 45 ----------- src/commands/moderation/remove-trigger.js | 73 ++++++++++++++++++ src/services/ban-words.js | 48 ------------ src/services/moderation.js | 30 ++++++++ 12 files changed, 291 insertions(+), 161 deletions(-) delete mode 100644 data/ban-words.txt create mode 100644 data/moderation/db.json delete mode 100644 src/commands/moderation/add-ban-word.js create mode 100644 src/commands/moderation/add-trigger.js delete mode 100644 src/commands/moderation/list-ban-words.js create mode 100644 src/commands/moderation/list-triggers.js delete mode 100644 src/commands/moderation/remove-ban-word.js create mode 100644 src/commands/moderation/remove-trigger.js delete mode 100644 src/services/ban-words.js create mode 100644 src/services/moderation.js diff --git a/data/ban-words.txt b/data/ban-words.txt deleted file mode 100644 index e92b87f..0000000 --- a/data/ban-words.txt +++ /dev/null @@ -1,4 +0,0 @@ -amazingsexdating.com -viewc.site -nakedphoto.club -privatepage.vip \ No newline at end of file diff --git a/data/moderation/db.json b/data/moderation/db.json new file mode 100644 index 0000000..8d0d02a --- /dev/null +++ b/data/moderation/db.json @@ -0,0 +1,20 @@ +{ + "triggers": [ + { + "trigger": "amazingsexdating.com", + "action": "ban" + }, + { + "trigger": "viewc.site", + "action": "ban" + }, + { + "trigger": "nakedphoto.club", + "action": "ban" + }, + { + "trigger": "privatepage.vip", + "action": "ban" + } + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7d9643f..4aa433d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2926,8 +2926,7 @@ "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", - "dev": true + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" }, "growly": { "version": "1.3.0", @@ -4203,6 +4202,25 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lowdb": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", + "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==", + "requires": { + "graceful-fs": "^4.1.3", + "is-promise": "^2.1.0", + "lodash": "4", + "pify": "^3.0.0", + "steno": "^0.4.1" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + } + } + }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -5810,6 +5828,14 @@ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", "dev": true }, + "steno": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", + "integrity": "sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs=", + "requires": { + "graceful-fs": "^4.1.3" + } + }, "string-length": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", diff --git a/package.json b/package.json index 3c31d66..5f03180 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "esm": "^3.2.25", "fuse.js": "^3.4.6", "hjson": "^3.1.2", + "lowdb": "^1.0.0", "prettier": "^1.18.2" }, "devDependencies": { diff --git a/src/commands/moderation/add-ban-word.js b/src/commands/moderation/add-ban-word.js deleted file mode 100644 index f1f21e4..0000000 --- a/src/commands/moderation/add-ban-word.js +++ /dev/null @@ -1,37 +0,0 @@ -import { Command } from 'discord.js-commando' - -import { banWords, saveToFile, toString } from '../../services/ban-words' -import { MODERATOR_ROLE_IDS } from '../../utils/constants' - -module.exports = class ModerationAddBanWordCommand extends Command { - constructor(client) { - super(client, { - args: [ - { - key: 'word', - type: 'string', - prompt: 'the word to add?', - }, - ], - name: 'add-ban-word', - group: 'moderation', - aliases: ['abw'], - guildOnly: true, - memberName: 'add-ban-word', - description: 'Add a word to the ban list.', - }) - } - - hasPermission(msg) { - return msg.member.roles.some(role => MODERATOR_ROLE_IDS.includes(role.id)) - } - - async run(msg, args) { - const { word } = args - - banWords.push(word) - saveToFile() - - return msg.channel.send(`Ban words: ${toString()}`) - } -} diff --git a/src/commands/moderation/add-trigger.js b/src/commands/moderation/add-trigger.js new file mode 100644 index 0000000..1c3dec3 --- /dev/null +++ b/src/commands/moderation/add-trigger.js @@ -0,0 +1,88 @@ +import { Command } from 'discord.js-commando' +import { RichEmbed } from 'discord.js' +import moderation from '../../services/moderation' +import { MODERATOR_ROLE_IDS, BOT_DEVELOPER_IDS } from '../../utils/constants' +import { inlineCode } from '../../utils/string' + +const { NODE_ENV } = process.env + +module.exports = class ModerationAddBanWordCommand extends Command { + constructor(client) { + super(client, { + args: [ + { + key: 'trigger', + type: 'string', + prompt: + 'the trigger word to add (use quotation marks if it contains spaces)?', + }, + { + key: 'action', + type: 'string', + prompt: 'which action should be taken (`warn`, `ban`)?', + validate(value) { + return ['warn', 'ban'].includes(value) + }, + }, + ], + name: 'add-trigger', + group: 'moderation', + guildOnly: true, + memberName: 'add', + description: 'Add a trigger word to the moderation system.', + }) + } + + hasPermission(msg) { + if (NODE_ENV === 'development') { + if (BOT_DEVELOPER_IDS.includes(msg.author.id)) { + return true + } + } + + return msg.member.roles.some(role => MODERATOR_ROLE_IDS.includes(role.id)) + } + + async run(msg, args) { + const { trigger, action } = args + + const exists = moderation + .get('triggers') + .find(item => item.trigger === trigger) + .value() + + console.log( + moderation + .get('triggers') + .find(item => item.trigger === trigger) + .value() + ) + + if (exists) { + msg.channel.send( + new RichEmbed() + .setTitle('Moderation - Add Trigger') + .setColor('ORANGE') + .setDescription( + `That trigger already exists (action: ${inlineCode(exists.action)})` + ) + ) + } else { + moderation + .get('triggers') + .push({ trigger, action }) + .write() + + msg.channel.send( + new RichEmbed() + .setTitle('Moderation - Add Trigger') + .setColor('GREEN') + .setDescription( + `Successfully added trigger word ${inlineCode( + trigger + )} with action ${inlineCode(action)}.` + ) + ) + } + } +} diff --git a/src/commands/moderation/list-ban-words.js b/src/commands/moderation/list-ban-words.js deleted file mode 100644 index 769fb53..0000000 --- a/src/commands/moderation/list-ban-words.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Command } from 'discord.js-commando' - -import { banWords } from '../../services/ban-words' -import { MODERATOR_ROLE_IDS } from '../../utils/constants' - -module.exports = class ModerationListBanWordsCommand extends Command { - constructor(client) { - super(client, { - name: 'list-ban-words', - group: 'moderation', - aliases: ['lbw'], - guildOnly: true, - memberName: 'list-ban-words', - description: 'List all banned words.', - }) - } - - hasPermission(msg) { - return msg.member.roles.some(role => MODERATOR_ROLE_IDS.includes(role.id)) - } - - async run(msg) { - return msg.channel.send(`Ban words: ${banWords.toString()}`) - } -} diff --git a/src/commands/moderation/list-triggers.js b/src/commands/moderation/list-triggers.js new file mode 100644 index 0000000..0ed2027 --- /dev/null +++ b/src/commands/moderation/list-triggers.js @@ -0,0 +1,51 @@ +import { Command } from 'discord.js-commando' +import { RichEmbed } from 'discord.js' +import moderation from '../../services/moderation' +import { MODERATOR_ROLE_IDS, BOT_DEVELOPER_IDS } from '../../utils/constants' +import { inlineCode } from '../../utils/string' + +const { NODE_ENV } = process.env + +module.exports = class ModerationListBanWordsCommand extends Command { + constructor(client) { + super(client, { + name: 'list-triggers', + group: 'moderation', + guildOnly: true, + aliases: ['triggers'], + memberName: 'list', + description: 'List all trigger words from the moderation system.', + }) + } + + hasPermission(msg) { + return msg.member.roles.some(role => { + if (MODERATOR_ROLE_IDS.includes(role.id)) { + return true + } + + if (NODE_ENV === 'development') { + if (BOT_DEVELOPER_IDS.includes(msg.member.id)) { + return true + } + } + }) + } + + async run(msg) { + const triggers = moderation.get('triggers').value() + + const embed = new RichEmbed() + .setTitle('Moderation Triggers') + .setColor('ORANGE') + .setDescription( + triggers + .map( + trigger => `**${trigger.trigger}** - ${inlineCode(trigger.action)}` + ) + .join('\n') + ) + + return msg.reply(embed) + } +} diff --git a/src/commands/moderation/remove-ban-word.js b/src/commands/moderation/remove-ban-word.js deleted file mode 100644 index 0d819ef..0000000 --- a/src/commands/moderation/remove-ban-word.js +++ /dev/null @@ -1,45 +0,0 @@ -import { Command } from 'discord.js-commando' - -import { banWords, saveToFile, toString } from '../../services/ban-words' -import { MODERATOR_ROLE_IDS } from '../../utils/constants' - -module.exports = class ModerationRemoveBanWordCommand extends Command { - constructor(client) { - super(client, { - args: [ - { - key: 'word', - type: 'string', - prompt: 'the word to add?', - }, - ], - name: 'remove-ban-word', - group: 'moderation', - aliases: ['rbw', 'del-ban-word', 'rm-ban-word'], - guildOnly: true, - memberName: 'remove-ban-word', - description: 'Remove a banned word.', - }) - } - - hasPermission(msg) { - return msg.member.roles.some(role => MODERATOR_ROLE_IDS.includes(role.id)) - } - - async run(msg, args) { - const { word } = args - const foundIndex = banWords.findIndex( - w => w.toLowerCase() === word.toLowerCase() - ) - - if (foundIndex >= 0) { - banWords.splice(foundIndex, 1) - saveToFile() - msg.channel.send(`Ban words: ${toString()}`) - } else { - msg.channel.send( - `I cannot find the word "${word}" in the banned words list.` - ) - } - } -} diff --git a/src/commands/moderation/remove-trigger.js b/src/commands/moderation/remove-trigger.js new file mode 100644 index 0000000..534a598 --- /dev/null +++ b/src/commands/moderation/remove-trigger.js @@ -0,0 +1,73 @@ +import { Command } from 'discord.js-commando' +import { RichEmbed } from 'discord.js' +import moderation from '../../services/moderation' +import { MODERATOR_ROLE_IDS, BOT_DEVELOPER_IDS } from '../../utils/constants' +import { inlineCode } from '../../utils/string' + +const { NODE_ENV } = process.env + +module.exports = class ModerationRemoveBanWordCommand extends Command { + constructor(client) { + super(client, { + args: [ + { + key: 'trigger', + type: 'string', + prompt: 'the trigger word to remove?', + }, + ], + name: 'remove-trigger', + group: 'moderation', + aliases: ['del-trigger', 'rem-trigger'], + guildOnly: true, + memberName: 'remove', + description: 'Remove a trigger word from the moderation system.', + }) + } + + hasPermission(msg) { + if (NODE_ENV === 'development') { + if (BOT_DEVELOPER_IDS.includes(msg.author.id)) { + return true + } + } + + return msg.member.roles.some(role => MODERATOR_ROLE_IDS.includes(role.id)) + } + + async run(msg, args) { + let { trigger } = args + + const exists = moderation + .get('triggers') + .find(item => item.trigger === trigger) + .value() + + if (!exists) { + return msg.channel.send( + new RichEmbed() + .setTitle('Moderation - Remove Trigger') + .setColor('ORANGE') + .setDescription(`That trigger doesn't exist!`) + ) + } + + const action = exists.action + + moderation + .get('triggers') + .remove({ trigger }) + .write() + + msg.channel.send( + new RichEmbed() + .setTitle('Moderation - Remove Trigger') + .setColor('GREEN') + .setDescription( + `Successfully removed trigger word ${inlineCode( + trigger + )} with action ${inlineCode(action)}.` + ) + ) + } +} diff --git a/src/services/ban-words.js b/src/services/ban-words.js deleted file mode 100644 index 5c4796e..0000000 --- a/src/services/ban-words.js +++ /dev/null @@ -1,48 +0,0 @@ -import fs from 'fs' -import { resolve } from 'path' - -const FILENAME = 'ban-words.txt' -const PATH = resolve(__dirname, '../../data/', FILENAME) -const SEPARATOR = '\n' - -export const banWords = [] - -export function saveToFile() { - fs.access(PATH, err => { - if (err) { - throw err - } - - fs.writeFile(PATH, banWords.join(SEPARATOR), 'utf8', err => { - if (err) { - throw err - } - console.log(`File "${PATH}" updated.`) - }) - }) -} - -export function toString() { - return banWords.reduce( - (acc, val) => (acc ? `${acc}, \`${val}\`` : `\`${val}\``), - '' - ) -} - -function _initFromFile() { - fs.access(PATH, err => { - if (err) { - throw err - } - - fs.readFile(PATH, 'utf8', (err, data) => { - if (err) { - throw err - } - - banWords.push(...data.split(/\r?\n/)) - }) - }) -} - -_initFromFile() diff --git a/src/services/moderation.js b/src/services/moderation.js new file mode 100644 index 0000000..e35e48b --- /dev/null +++ b/src/services/moderation.js @@ -0,0 +1,30 @@ +import { resolve, join } from 'path' +import low from 'lowdb' +import FileSync from 'lowdb/adapters/FileSync' +import { DATA_DIR } from '../utils/constants' + +const adapter = new FileSync(join(resolve(DATA_DIR), 'moderation', 'db.json')) +const db = low(adapter) + +db.defaults({ + triggers: [ + { + trigger: 'amazingsexdating.com', + action: 'ban', + }, + { + trigger: 'viewc.site', + action: 'ban', + }, + { + trigger: 'nakedphoto.club', + action: 'ban', + }, + { + trigger: 'privatepage.vip', + action: 'ban', + }, + ], +}).write() + +export default db From 517378c1e7f88de3990cf0a2f7fbffea15a6588c Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 14:59:30 +0000 Subject: [PATCH 11/69] feat: Persistent storage of task configuration. --- src/client.js | 6 ++ src/lib/{job.js => task.js} | 124 ++++++++++++++++++++++++++++++++++-- src/services/tasks.js | 22 +++++++ 3 files changed, 148 insertions(+), 4 deletions(-) rename src/lib/{job.js => task.js} (75%) create mode 100644 src/services/tasks.js diff --git a/src/client.js b/src/client.js index cc3d797..5074a03 100644 --- a/src/client.js +++ b/src/client.js @@ -2,6 +2,7 @@ import { readdirSync } from 'fs' import { join } from 'path' import { Collection } from 'discord.js' import { CommandoClient } from 'discord.js-commando' +import { setDefaults } from './services/tasks' if (!process.env.NODE_ENV) { process.env.NODE_ENV = 'development' @@ -43,6 +44,11 @@ for (const file of taskFiles) { } } +/* + Write configuration file if applicable (DB doesn't yet exist). +*/ +setDefaults(client.tasks.map(task => task.toJSON())) + /* Register command groups. diff --git a/src/lib/job.js b/src/lib/task.js similarity index 75% rename from src/lib/job.js rename to src/lib/task.js index cf5d832..2763bda 100644 --- a/src/lib/job.js +++ b/src/lib/task.js @@ -1,4 +1,5 @@ import EventEmitter from 'events' +import tasks, { isEmpty } from '../services/tasks' /** * A Task is a task which by default runs for every single message received so @@ -97,8 +98,6 @@ export default class Task extends EventEmitter { this.events = options.events this.config = options.config this.ignored = options.ignored - this.dmOnly = options.dmOnly - this.guildOnly = options.guildOnly this.description = options.description || '' // NOTE: We need to bind the events here else their `this` will be `CommandoClient`. @@ -127,8 +126,16 @@ export default class Task extends EventEmitter { this.unregisterInhibitor() }) - // NOTE: Must come last because the setter triggers an event (enabled). - this.enabled = options.enabled + // The DB is empty so we are safe to use the defauls from the Task file. + if (isEmpty()) { + this._dmOnly = options.dmOnly + this._guildOnly = options.guildOnly + this.enabled = options.enabled // NOTE: Must come last because fires an event. + } + // The DB is populated so we can't use the defaults from the Task file. + else { + this.readConfig() + } } /** @@ -268,7 +275,13 @@ export default class Task extends EventEmitter { * @fires Task#disabled */ set enabled(enabled) { + // NOTE: Without this check, in theory we can end up with duplicate event listeners. + if (this._enabled === enabled) { + return + } + this._enabled = enabled + this.writeConfig({ enabled: this._enabled }) if (enabled) { this.emit('enabled') @@ -276,6 +289,109 @@ export default class Task extends EventEmitter { this.emit('disabled') } } + + /** + * Is the Task DM-only? + * + * @return {boolean} Is this DM-only? + */ + get dmOnly() { + return this._dmOnly + } + + /** + * Set a Task as DM-only or not. + * + * @param {boolean} value Is the Task DM-only or not. + * @fires Task#enabled + * @fires Task#disabled + */ + set dmOnly(value) { + this._dmOnly = value + this.writeConfig({ dmOnly: this._dmOnly }) + } + + /** + * Is the Task DM-only? + * + * @return {boolean} Is this DM-only? + */ + get guildOnly() { + return this._guildOnly + } + + /** + * Set a Task as DM-only or not. + * + * @param {boolean} value Is the Task DM-only or not. + * @fires Task#enabled + * @fires Task#disabled + */ + set guildOnly(value) { + this._guildOnly = value + this.writeConfig({ guildOnly: this._guildOnly }) + } + + /** + * Create a JSON representation of the task. + */ + toJSON() { + return { + name: this.name, + guildOnly: this.guildOnly, + dmOnly: this.dmOnly, + config: this.config, + ignored: this.ignored, + enabled: this.enabled, + } + } + + /** + * Read the task's configuration from lowDB and apply it. + */ + readConfig() { + try { + const config = tasks + .get('tasks') + .find({ name: this.name }) + .value() + + if (!config) { + console.warn(`${this} Could not find task config in DB!`) + } + + this.guildOnly = config.guildOnly + this.dmOnly = config.dmOnly + this.config = config.config + this.ignored = config.ignored + this.enabled = config.enabled // NOTE: Must come last because fires an event. + + console.debug( + `${this} Read configuration from DB and applied to instance.` + ) + } catch (e) { + console.error(e) + } + } + + /** + * Write the task's configuration to lowDB. + */ + writeConfig(assign) { + try { + if (!assign) { + assign = this.toJSON() + } + + tasks + .get('tasks') + .find({ name: this.name }) + .assign(assign) + .write() + } catch (e) { + console.error(e) + } + } } /* diff --git a/src/services/tasks.js b/src/services/tasks.js new file mode 100644 index 0000000..6b53792 --- /dev/null +++ b/src/services/tasks.js @@ -0,0 +1,22 @@ +import { resolve, join } from 'path' +import low from 'lowdb' +import FileSync from 'lowdb/adapters/FileSync' +import { DATA_DIR } from '../utils/constants' + +const adapter = new FileSync(join(resolve(DATA_DIR), 'tasks', 'db.json')) +const db = low(adapter) + +export function isEmpty() { + return !db.has('tasks').value() +} + +export function setDefaults(tasks) { + if (!isEmpty()) { + return !!console.info('Skipping setting defaults - config file exists.') + } + db.defaults({ + tasks: [...tasks], + }).write() +} + +export default db From ec207aa2b89425057269d00616b4501e9f77eeba Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 14:59:54 +0000 Subject: [PATCH 12/69] fix: Display N/A when there isn't a guild. --- src/tasks/log.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/log.js b/src/tasks/log.js index 3f15533..42f9b39 100644 --- a/src/tasks/log.js +++ b/src/tasks/log.js @@ -96,7 +96,7 @@ export default class LogTask extends Task { if (msg) { embed - .addField('Guild', msg.guild ? msg.guild : 'DM', true) + .addField('Guild', msg.guild ? msg.guild : 'N/A', true) .addField('Channel', msg.channel, true) .addField('User', msg.author, true) } From dd2d277b6b04a62d180c639a7d6f7a9f0f0361a5 Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 15:00:25 +0000 Subject: [PATCH 13/69] other: Simplify task list output. --- src/commands/tasks/list.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/commands/tasks/list.js b/src/commands/tasks/list.js index f6f309d..4638723 100644 --- a/src/commands/tasks/list.js +++ b/src/commands/tasks/list.js @@ -36,16 +36,10 @@ module.exports = class TasksEnableCommand extends Command { const embed = new RichEmbed() embed .setTitle('Task List') - .setDescription( - `For more detailed info. type: ${inlineCode('!task ')}.` - ) + .setDescription(`For more info.: ${inlineCode('!task ')}.`) this.client.tasks.forEach(task => { - embed.addField( - task.name, - this.client.emojis.get(EMOJIS[task.getStatus().toUpperCase()]), - true - ) + embed.addField(task.name, inlineCode(task.getStatus()), true) }) return msg.channel.send(EMPTY_MESSAGE, { embed }) From 2ff1f920c592accea4c5a715d75a234a529187e2 Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 15:00:32 +0000 Subject: [PATCH 14/69] other: Ignore DB file. --- data/tasks/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 data/tasks/.gitignore diff --git a/data/tasks/.gitignore b/data/tasks/.gitignore new file mode 100644 index 0000000..58bd488 --- /dev/null +++ b/data/tasks/.gitignore @@ -0,0 +1 @@ +db.json From 99513b38b22dc0746a08a6279ec6853758ca9828 Mon Sep 17 00:00:00 2001 From: sustained Date: Tue, 31 Dec 2019 15:02:04 +0000 Subject: [PATCH 15/69] fix: Simplify output (redux). --- src/commands/tasks/info.js | 9 ++------- src/commands/tasks/list.js | 1 - 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/commands/tasks/info.js b/src/commands/tasks/info.js index 1c8976a..0381210 100644 --- a/src/commands/tasks/info.js +++ b/src/commands/tasks/info.js @@ -1,13 +1,12 @@ import { Command } from 'discord.js-commando' import { RichEmbed } from 'discord.js' import { - EMOJIS, EMPTY_MESSAGE, OWNER_IDS, BOT_DEVELOPER_IDS, MODERATOR_ROLE_IDS, } from '../../utils/constants' -import { blockCode } from '../../utils/string' +import { inlineCode, blockCode } from '../../utils/string' const ALLOWED_ROLES = [...MODERATOR_ROLE_IDS] const ALLOWED_USERS = [...OWNER_IDS, ...BOT_DEVELOPER_IDS] @@ -77,11 +76,7 @@ module.exports = class TasksEnableCommand extends Command { const embed = new RichEmbed() embed.setTitle('Task (' + task.name + ')') embed.setDescription(task.description) - embed.addField( - 'Enabled', - this.client.emojis.get(EMOJIS[task.getStatus().toUpperCase()]), - true - ) + embed.addField('Enabled', inlineCode(task.getStatus()), true) embed.addField('Guild Only', task.guildOnly, true) embed.addField('DM Only', task.dmOnly, true) embed.addField('Ignored Users', ignoredUsers.join(', '), true) diff --git a/src/commands/tasks/list.js b/src/commands/tasks/list.js index 4638723..d530ecc 100644 --- a/src/commands/tasks/list.js +++ b/src/commands/tasks/list.js @@ -1,7 +1,6 @@ import { Command } from 'discord.js-commando' import { RichEmbed } from 'discord.js' import { - EMOJIS, EMPTY_MESSAGE, OWNER_IDS, BOT_DEVELOPER_IDS, From 6bb1f64dfe08e8ccfb963afc2503e813b6818616 Mon Sep 17 00:00:00 2001 From: sustained Date: Thu, 2 Jan 2020 11:33:17 +0000 Subject: [PATCH 16/69] fix: Remove logging. --- src/commands/moderation/add-trigger.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/commands/moderation/add-trigger.js b/src/commands/moderation/add-trigger.js index 1c3dec3..02af74b 100644 --- a/src/commands/moderation/add-trigger.js +++ b/src/commands/moderation/add-trigger.js @@ -51,13 +51,6 @@ module.exports = class ModerationAddBanWordCommand extends Command { .find(item => item.trigger === trigger) .value() - console.log( - moderation - .get('triggers') - .find(item => item.trigger === trigger) - .value() - ) - if (exists) { msg.channel.send( new RichEmbed() From 83f7d0bb423447f9f2de463b6da0df4236d8ca8d Mon Sep 17 00:00:00 2001 From: sustained Date: Thu, 2 Jan 2020 11:33:27 +0000 Subject: [PATCH 17/69] fix: Rename commands. --- src/commands/moderation/add-trigger.js | 2 +- src/commands/moderation/list-triggers.js | 2 +- src/commands/moderation/remove-trigger.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/moderation/add-trigger.js b/src/commands/moderation/add-trigger.js index 02af74b..d01a681 100644 --- a/src/commands/moderation/add-trigger.js +++ b/src/commands/moderation/add-trigger.js @@ -6,7 +6,7 @@ import { inlineCode } from '../../utils/string' const { NODE_ENV } = process.env -module.exports = class ModerationAddBanWordCommand extends Command { +module.exports = class ModerationAddTriggerCommand extends Command { constructor(client) { super(client, { args: [ diff --git a/src/commands/moderation/list-triggers.js b/src/commands/moderation/list-triggers.js index 0ed2027..94eb1f9 100644 --- a/src/commands/moderation/list-triggers.js +++ b/src/commands/moderation/list-triggers.js @@ -6,7 +6,7 @@ import { inlineCode } from '../../utils/string' const { NODE_ENV } = process.env -module.exports = class ModerationListBanWordsCommand extends Command { +module.exports = class ModerationListTriggersCommand extends Command { constructor(client) { super(client, { name: 'list-triggers', diff --git a/src/commands/moderation/remove-trigger.js b/src/commands/moderation/remove-trigger.js index 534a598..9da9fbb 100644 --- a/src/commands/moderation/remove-trigger.js +++ b/src/commands/moderation/remove-trigger.js @@ -6,7 +6,7 @@ import { inlineCode } from '../../utils/string' const { NODE_ENV } = process.env -module.exports = class ModerationRemoveBanWordCommand extends Command { +module.exports = class ModerationRemoveTriggersCommand extends Command { constructor(client) { super(client, { args: [ From 04f5487ede7f12c8c0b9f9bdcd353e5c9075dfea Mon Sep 17 00:00:00 2001 From: sustained Date: Thu, 2 Jan 2020 11:36:57 +0000 Subject: [PATCH 18/69] fix: Ignore and remove moderation DB. --- data/moderation/.gitignore | 1 + data/moderation/db.json | 20 -------------------- 2 files changed, 1 insertion(+), 20 deletions(-) create mode 100644 data/moderation/.gitignore delete mode 100644 data/moderation/db.json diff --git a/data/moderation/.gitignore b/data/moderation/.gitignore new file mode 100644 index 0000000..e977909 --- /dev/null +++ b/data/moderation/.gitignore @@ -0,0 +1 @@ +db.json \ No newline at end of file diff --git a/data/moderation/db.json b/data/moderation/db.json deleted file mode 100644 index 8d0d02a..0000000 --- a/data/moderation/db.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "triggers": [ - { - "trigger": "amazingsexdating.com", - "action": "ban" - }, - { - "trigger": "viewc.site", - "action": "ban" - }, - { - "trigger": "nakedphoto.club", - "action": "ban" - }, - { - "trigger": "privatepage.vip", - "action": "ban" - } - ] -} \ No newline at end of file From 692837639eeec9a78a242ecec95237ab675e8582 Mon Sep 17 00:00:00 2001 From: sustained Date: Thu, 2 Jan 2020 11:37:12 +0000 Subject: [PATCH 19/69] fix: Make log message clearer. --- src/services/tasks.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/tasks.js b/src/services/tasks.js index 6b53792..c0b7305 100644 --- a/src/services/tasks.js +++ b/src/services/tasks.js @@ -12,7 +12,9 @@ export function isEmpty() { export function setDefaults(tasks) { if (!isEmpty()) { - return !!console.info('Skipping setting defaults - config file exists.') + return !!console.info( + 'Skipping setting default tasks - data/tasks/db.json exists.' + ) } db.defaults({ tasks: [...tasks], From db9afa30b5e9dca3f54d9cc0a4acfd39a50c105f Mon Sep 17 00:00:00 2001 From: sustained Date: Thu, 2 Jan 2020 12:17:21 +0000 Subject: [PATCH 20/69] fix: Ensure msg.member is set. --- src/lib/task.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/task.js b/src/lib/task.js index 2763bda..8af8d6e 100644 --- a/src/lib/task.js +++ b/src/lib/task.js @@ -215,7 +215,7 @@ export default class Task extends EventEmitter { } } - if (this.ignored.roles.length) { + if (this.ignored.roles.length && msg.member) { return msg.member.roles.some(role => this.ignored.roles.includes(role.id)) } From d5e832b9905b100b66238327df6fb2dbc9616e81 Mon Sep 17 00:00:00 2001 From: sustained Date: Thu, 2 Jan 2020 13:38:15 +0000 Subject: [PATCH 21/69] other: Remove warn/ban tasks. --- src/tasks/ban.js | 114 ---------------------------------------------- src/tasks/warn.js | 74 ------------------------------ 2 files changed, 188 deletions(-) delete mode 100644 src/tasks/ban.js delete mode 100644 src/tasks/warn.js diff --git a/src/tasks/ban.js b/src/tasks/ban.js deleted file mode 100644 index 578aa6c..0000000 --- a/src/tasks/ban.js +++ /dev/null @@ -1,114 +0,0 @@ -import { RichEmbed } from 'discord.js' -import Task from '../lib/task' -import moderation from '../services/moderation' -import { - MODERATOR_ROLE_IDS, - PROTECTED_ROLE_IDS, - EMPTY_MESSAGE, -} from '../utils/constants' -import { blockCode } from '../utils/string' - -// Don't *actually* ban - real bans make testing hard! -const DEBUG_MODE = process.env.NODE_ENV === 'development' - -export default class BanTask extends Task { - constructor(client) { - super(client, { - name: 'ban', - description: 'Auto-ban users who mention a "ban" trigger word.', - enabled: true, - ignored: { - roles: [...new Set(MODERATOR_ROLE_IDS.concat(PROTECTED_ROLE_IDS))], - }, - guildOnly: true, - config: { - logChannel: { - name: 'moderation', - }, - }, - }) - } - - shouldExecute(msg) { - // None of the ban words were mentioned - bail. - if ( - !moderation - .get('triggers') - .filter(({ action }) => action === 'ban') - .some(({ trigger }) => { - return msg.content.toLowerCase().includes(trigger.toLowerCase()) - }) - .value() - ) { - return false - } - - // We don't have permission to ban - bail. - if (!msg.channel.permissionsFor(msg.client.user).has('BAN_MEMBERS')) { - return !!console.warn('[BanTask] Cannot ban - lacking permission.') - } - - const botMember = msg.guild.member(msg.client.user) - const botHighestRole = botMember.highestRole.calculatedPosition - const userHighestRole = msg.member.highestRole.calculatedPosition - - // Our role is not high enough in the hierarchy to ban - bail. - if (botHighestRole < userHighestRole) { - return !!console.warn('[BanTask] Cannot ban - role too low.') - } - - return true - } - - run(msg) { - const logChannel = msg.client.channels.find( - channel => channel.name === this.config.logChannel.name - ) - - if (!logChannel) { - return console.warn( - `[BanTask]: Could not find channel with name ${this.config.logChannel.name}` - ) - } - - if (DEBUG_MODE) { - return this.log(msg, logChannel) - } - - msg.member - .ban(`[${msg.client.user.name}] Automated anti-spam measures.`) - .then(() => this.log(msg, logChannel)) - .catch(console.error) // Shouldn't happen due to shouldExecute checks but... - } - - log(msg, logChannel) { - const excerpt = - msg.cleanContent.length > 150 - ? msg.cleanContent.substring(0, 150) + '...' - : msg.cleanContent - - if (!logChannel) { - return console.info( - `Banned user: ${msg.author}`, - `Due to message:`, - msg.cleanContent - ) - } - - const embed = new RichEmbed() - embed - .setTitle('Moderation - Ban Notice') - .setColor('RED') - .setDescription('Found one or more trigger words with action: `ban`.') - .addField('User', msg.member, true) - .addField('Channel', msg.channel, true) - .setTimestamp() - .addField('Message Excerpt', blockCode(excerpt)) - - if (DEBUG_MODE) { - embed.addField('NOTE', 'Debug mode enabled - no ban was actually issued.') - } - - logChannel.send(EMPTY_MESSAGE, { embed }) - } -} diff --git a/src/tasks/warn.js b/src/tasks/warn.js deleted file mode 100644 index c5d9501..0000000 --- a/src/tasks/warn.js +++ /dev/null @@ -1,74 +0,0 @@ -import { RichEmbed } from 'discord.js' -import Task from '../lib/task' -import { - EMPTY_MESSAGE, - MODERATOR_ROLE_IDS, - PROTECTED_ROLE_IDS, -} from '../utils/constants' -import { blockCode } from '../utils/string' -import moderation from '../services/moderation' - -export default class WarnTask extends Task { - constructor(client) { - super(client, { - name: 'warn', - description: - 'Auto-warn moderators when users mention a "warn" trigger word.', - enabled: true, - ignored: { - roles: [...new Set(MODERATOR_ROLE_IDS.concat(PROTECTED_ROLE_IDS))], - }, - guildOnly: true, - config: { - notifyRole: { - name: 'Moderators', - }, - notifyChannel: { - name: 'moderation', - }, - }, - }) - } - - shouldExecute(msg) { - return moderation - .get('triggers') - .filter(({ action }) => action === 'warn') - .some(({ trigger }) => { - return msg.content.toLowerCase().includes(trigger.toLowerCase()) - }) - .value() - } - - run(msg) { - const notifyRole = msg.guild.roles.find( - role => role.name === this.config.notifyRole.name - ) - const notifyChannel = msg.client.channels.find( - channel => channel.name === this.config.notifyChannel.name - ) - - if (!notifyChannel) { - return console.warn( - `[WarnTask] Could not find channel with name ${this.config.notifyChannel.name}` - ) - } - - const excerpt = - msg.cleanContent.length > 150 - ? msg.cleanContent.substring(0, 150) + '...' - : msg.cleanContent - - const embed = new RichEmbed() - embed - .setTitle('Moderation - Auto-warning') - .setColor('ORANGE') - .setDescription('Found one or more trigger words with action: `warn`.') - .addField('User', msg.member, true) - .addField('Channel', msg.channel, true) - .setTimestamp() - .addField('Message Excerpt', blockCode(excerpt)) - - notifyChannel.send(notifyRole ? notifyRole : EMPTY_MESSAGE, { embed }) - } -} From 5cbc7420867257724262edcb9a5462935f07c561 Mon Sep 17 00:00:00 2001 From: sustained Date: Thu, 2 Jan 2020 13:38:54 +0000 Subject: [PATCH 22/69] feat: Merge moderation-related tasks. --- src/tasks/moderation.js | 171 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 src/tasks/moderation.js diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js new file mode 100644 index 0000000..483241a --- /dev/null +++ b/src/tasks/moderation.js @@ -0,0 +1,171 @@ +import { RichEmbed } from 'discord.js' +import Task from '../lib/task' +import moderation from '../services/moderation' +import { + MODERATOR_ROLE_IDS, + PROTECTED_ROLE_IDS, + EMPTY_MESSAGE, +} from '../utils/constants' +import { blockCode, inlineCode } from '../utils/string' + +// Don't *actually* ban - real bans make testing hard! +const DEBUG_MODE = process.env.NODE_ENV === 'development' + +export default class ModerationTask extends Task { + constructor(client) { + super(client, { + name: 'moderation', + description: + 'Takes action (warn, ban, notify) when users mention a trigger word.', + enabled: true, + ignored: { + roles: [...new Set(MODERATOR_ROLE_IDS.concat(PROTECTED_ROLE_IDS))], + }, + guildOnly: true, + config: { + logChannel: { + name: 'moderation', + }, + notifyRole: { + name: 'Moderators', + }, + }, + }) + + this.action = null // ban | warn | notify + } + + shouldExecute(msg) { + const match = moderation + .get('triggers') + .find(({ trigger }) => { + return msg.content.toLowerCase().includes(trigger.toLowerCase()) + }) + .value() + + if (!match) { + return true + } + + console.log(`Message is ${msg.cleanContent}`) + console.log(`Action to take is ${match.action}`, match) + this.action = match.action + } + + run(msg) { + const notifyRole = msg.guild.roles.find( + role => role.name === this.config.notifyRole.name + ) + const logChannel = msg.client.channels.find( + channel => channel.name === this.config.logChannel.name + ) + + if (!logChannel) { + return console.warn( + `[ModerationTask]: Could not find channel with name ${this.config.logChannel.name}!` + ) + } + + switch (this.action) { + case 'warn': + this.warn(msg, logChannel) + break + case 'ban': + this.ban(msg, logChannel) + break + case 'notify': + default: + this.notify(msg, logChannel, notifyRole) + break + } + } + + ban(msg, logChannel) { + // Can't ban a user from a DM. + if (msg.channel.type === 'dm') { + return !!console.warn('[ModerationTask] Cannot ban in a DM channel.') + } + + // We don't have permission to ban - bail. + if (!msg.channel.permissionsFor(msg.client.user).has('BAN_MEMBERS')) { + return !!console.warn('[ModerationTask] Cannot ban - lacking permission.') + } + + const botMember = msg.guild.member(msg.client.user) + const botHighestRole = botMember.highestRole.calculatedPosition + const userHighestRole = msg.member.highestRole.calculatedPosition + + // Our role is not high enough in the hierarchy to ban - bail. + if (botHighestRole < userHighestRole) { + return !!console.warn('[BanTask] Cannot ban - role too low.') + } + + if (DEBUG_MODE) { + const embed = this.createEmbed(msg, false, { color: 'RED' }).addField( + 'NOTE', + 'Debug mode enabled - no ban was actually issued.' + ) + return this.log(msg, logChannel, { embed }) + } + + msg.member + .ban(`[${msg.client.user.name}] Automated anti-spam measures.`) + .then(() => this.log(msg, logChannel)) + .catch(console.error) + } + + async warn(msg, logChannel) { + try { + const dmChannel = await msg.author.createDM() + this.log(msg, dmChannel, { color: 'ORANGE', isDMWarning: true }) + this.log(msg, logChannel, { color: 'ORANGE' }) + } catch (e) { + console.error(e) + } + } + + notify(msg, logChannel, notifyRole) { + this.log(msg, logChannel, { notifyRole }) + } + + log(msg, logChannel, options = {}) { + logChannel.send(options.notifyRole || EMPTY_MESSAGE, { + embed: + options.embed || + this.createEmbed(msg, options.isDMWarning || false, { + color: 'ORANGE', + }), + }) + } + + createEmbed(msg, isDMWarning = false, options = {}) { + const excerpt = + msg.cleanContent.length > 150 + ? msg.cleanContent.substring(0, 150) + '...' + : msg.cleanContent + + const embed = new RichEmbed() + .setTitle(`Moderation - ${options.title || this.action}`) + .setColor(options.color || 'RANDOM') + .setTimestamp() + .addField('Message Excerpt', blockCode(excerpt)) + + if (isDMWarning) { + embed.setDescription( + 'One of your messages triggered auto-moderation. Repeated infringements may result in a ban.' + ) + } else { + embed + .setDescription( + `Found one or more trigger words with action: ${inlineCode( + this.action + )}.` + ) + .addField('User', msg.member, true) + .addField('Channel', msg.channel, true) + .addField('Action Taken', inlineCode(this.action), true) + } + + return embed + } +} From 355cafcb9263999f2e89c0a15ba1339f6ae3288a Mon Sep 17 00:00:00 2001 From: sustained Date: Thu, 2 Jan 2020 13:45:12 +0000 Subject: [PATCH 23/69] other: Update actions. --- src/commands/moderation/add-trigger.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/moderation/add-trigger.js b/src/commands/moderation/add-trigger.js index d01a681..016ea93 100644 --- a/src/commands/moderation/add-trigger.js +++ b/src/commands/moderation/add-trigger.js @@ -19,9 +19,9 @@ module.exports = class ModerationAddTriggerCommand extends Command { { key: 'action', type: 'string', - prompt: 'which action should be taken (`warn`, `ban`)?', + prompt: 'which action should be taken (`warn`, `ban`, `notify`)?', validate(value) { - return ['warn', 'ban'].includes(value) + return ['warn', 'ban', 'notify'].includes(value) }, }, ], From be0072e19d56f468089a391cd158ed4ebda20f97 Mon Sep 17 00:00:00 2001 From: sustained Date: Thu, 2 Jan 2020 13:46:47 +0000 Subject: [PATCH 24/69] refactor: Extract actions. --- src/commands/moderation/add-trigger.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/commands/moderation/add-trigger.js b/src/commands/moderation/add-trigger.js index 016ea93..43419f3 100644 --- a/src/commands/moderation/add-trigger.js +++ b/src/commands/moderation/add-trigger.js @@ -5,6 +5,7 @@ import { MODERATOR_ROLE_IDS, BOT_DEVELOPER_IDS } from '../../utils/constants' import { inlineCode } from '../../utils/string' const { NODE_ENV } = process.env +const VALID_ACTIONS = ['warn', 'ban', 'notify'] module.exports = class ModerationAddTriggerCommand extends Command { constructor(client) { @@ -19,9 +20,11 @@ module.exports = class ModerationAddTriggerCommand extends Command { { key: 'action', type: 'string', - prompt: 'which action should be taken (`warn`, `ban`, `notify`)?', + prompt: `which action should be taken (${VALID_ACTIONS.map( + inlineCode + ).join(', ')})?`, validate(value) { - return ['warn', 'ban', 'notify'].includes(value) + return VALID_ACTIONS.includes(value) }, }, ], From e36471da2a8bdb0196ce370f87341599a02f7378 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:44:19 +0000 Subject: [PATCH 25/69] other: Remove beta task. --- src/tasks/beta.js | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 src/tasks/beta.js diff --git a/src/tasks/beta.js b/src/tasks/beta.js deleted file mode 100644 index 8cd68c1..0000000 --- a/src/tasks/beta.js +++ /dev/null @@ -1,33 +0,0 @@ -import Task from '../lib/task' -import { ROLES, GUILDS } from '../utils/constants' - -export default class BetaTask extends Task { - constructor(client) { - super(client, { - name: 'beta', - description: - 'Only allow specific roles to interact with the bot on the (live) server during the beta period.', - enabled: true, - guildOnly: false, - config: { - guild: GUILDS.LIVE, - allowedRoles: Object.values(ROLES), - allowDMs: true, - }, - }) - } - - inhibit(msg) { - const { allowDMs, allowedRoles } = this.config - - if (msg.guild && msg.guild.id === this.config.guild) { - if (!msg.member.roles.some(role => allowedRoles.includes(role.id))) { - return 'beta-restriction' - } - } else { - if (!allowDMs) { - return 'beta-restriction' - } - } - } -} From 73047b7d83c19abd8cbec002d939ef366a176d84 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:47:15 +0000 Subject: [PATCH 26/69] enhance: Add per-event config + checks. --- src/tasks/log.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/tasks/log.js b/src/tasks/log.js index 42f9b39..cdb6275 100644 --- a/src/tasks/log.js +++ b/src/tasks/log.js @@ -18,6 +18,13 @@ export default class LogTask extends Task { 'Logs various events (connection, command invocations etc.) for debugging purposes/to aid development.', enabled: true, config: { + shouldLog: { + readyEvents: false, // Log when the bot is ready / has connected? + resumeEvents: false, // Log when the bot resumes a lost connection? + invokedCommands: true, // Log all commands invocations? + erroredCommands: true, // Log commands which cause an error to be thrown? + unknownCommands: true, // Log unknown commands? + }, connectionChannel: { name: 'connection', }, @@ -28,11 +35,11 @@ export default class LogTask extends Task { }) } - shouldExecute() { - return false - } - ready() { + if (!this.config.shouldLog.readyEvents) { + return + } + trySend( this.config.connectionChannel, null, @@ -41,6 +48,10 @@ export default class LogTask extends Task { } resume() { + if (!this.config.shouldLog.resumeEvents) { + return + } + trySend( this.config.commandChannel, null, @@ -59,6 +70,10 @@ export default class LogTask extends Task { } commandError(cmd, err, msg) { + if (!this.config.shouldLog.erroredCommands) { + return + } + trySend( this.config.commandChannel, null, @@ -69,6 +84,10 @@ export default class LogTask extends Task { } unknownCommand(msg) { + if (!this.config.shouldLog.erroredCommands) { + return + } + trySend( this.config.commandChannel, null, From 995cc548bfd46fcbbe64f121fa6faf28d50f8eb3 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:47:37 +0000 Subject: [PATCH 27/69] style: Use inline code. --- src/commands/tasks/info.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/tasks/info.js b/src/commands/tasks/info.js index 0381210..627c05f 100644 --- a/src/commands/tasks/info.js +++ b/src/commands/tasks/info.js @@ -77,8 +77,8 @@ module.exports = class TasksEnableCommand extends Command { embed.setTitle('Task (' + task.name + ')') embed.setDescription(task.description) embed.addField('Enabled', inlineCode(task.getStatus()), true) - embed.addField('Guild Only', task.guildOnly, true) - embed.addField('DM Only', task.dmOnly, true) + embed.addField('Guild Only', inlineCode(task.guildOnly), true) + embed.addField('DM Only', inlineCode(task.dmOnly), true) embed.addField('Ignored Users', ignoredUsers.join(', '), true) embed.addField('Ignored Roles', ignoredRoles.join(', '), true) embed.addField('Ignored Channels', ignoredChannels.join(', '), true) From 45991ac17ea1adf331004742ffb8d2c130a54ffd Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:48:14 +0000 Subject: [PATCH 28/69] other: Add current guild. --- src/utils/constants/index.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/utils/constants/index.js b/src/utils/constants/index.js index 3cfdd99..311bc3c 100644 --- a/src/utils/constants/index.js +++ b/src/utils/constants/index.js @@ -94,9 +94,12 @@ const EMOJIS = { EMOJIS.SUCCESS = EMOJIS.ENABLED EMOJIS.FAILURE = EMOJIS.DISABLED -const GUILDS = { - TEST: '617839535727968282', - LIVE: '325477692906536972', +const TEST_GUILD = '617839535727968282' +const LIVE_GUILD = '325477692906536972' +let GUILDS = { + TEST: TEST_GUILD, + LIVE: LIVE_GUILD, + CURRENT: process.env.NODE_ENV === 'production' ? LIVE_GUILD : TEST_GUILD, } const CDN_BASE_URL = @@ -105,6 +108,7 @@ const CDN_BASE_URL = export { USERS, ROLES, + GUILDS, EMOJIS, CDN_BASE_URL, OWNER_IDS, From 430c46c0a5463f7cc8fec47db221173d72bad9f0 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:50:03 +0000 Subject: [PATCH 29/69] style: Explain code. --- src/client.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client.js b/src/client.js index 8e62966..63ef348 100644 --- a/src/client.js +++ b/src/client.js @@ -6,6 +6,10 @@ import { setDefaults } from './services/tasks' const { NODE_ENV, COMMAND_PREFIX = '!' } = process.env +/* + We allow a comma-separated list of owner IDs, so check for + that and apply it back onto process.env if found. +*/ let OWNER_IDS = process.env.OWNER_IDS || '269617876036616193' // Default to @evan#9589 if (OWNER_IDS.includes(',')) { OWNER_IDS = OWNER_IDS.split(',') From b1be1ed779d7a71ee097d48e4b482ad090587805 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:50:17 +0000 Subject: [PATCH 30/69] fix: NODE_ENV --- src/client.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/client.js b/src/client.js index 63ef348..62e6c91 100644 --- a/src/client.js +++ b/src/client.js @@ -18,6 +18,13 @@ if (OWNER_IDS.includes(',')) { } process.env.OWNER_IDS = OWNER_IDS +/* + Ensure that NODE_ENV is set to development if it is unset. +*/ +if (!NODE_ENV) { + process.env.NODE_ENV = 'development' +} + const PATH_TASKS = join(__dirname, 'tasks') const PATH_TYPES = join(__dirname, 'types') const PATH_COMMANDS = join(__dirname, 'commands') From 0e6df972ba80fbe69de24b7cb2e64b2916296f8e Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:51:17 +0000 Subject: [PATCH 31/69] other: Refactor task service slightly. --- src/client.js | 2 +- src/services/tasks.js | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index 62e6c91..95a4b23 100644 --- a/src/client.js +++ b/src/client.js @@ -58,7 +58,7 @@ for (const file of taskFiles) { /* Write configuration file if applicable (DB doesn't yet exist). */ -setDefaults(client.tasks.map(task => task.toJSON())) +setDefaults(client.tasks) /* Register command groups. diff --git a/src/services/tasks.js b/src/services/tasks.js index c0b7305..5ea9c54 100644 --- a/src/services/tasks.js +++ b/src/services/tasks.js @@ -1,21 +1,59 @@ import { resolve, join } from 'path' +import { unlinkSync } from 'fs' import low from 'lowdb' import FileSync from 'lowdb/adapters/FileSync' import { DATA_DIR } from '../utils/constants' -const adapter = new FileSync(join(resolve(DATA_DIR), 'tasks', 'db.json')) +const DATABASE_PATH = join(resolve(DATA_DIR), 'tasks', 'db.json') +const adapter = new FileSync(DATABASE_PATH) const db = low(adapter) +/** + * Check if the DB is empty. + * + * @returns {boolean} + */ export function isEmpty() { return !db.has('tasks').value() } +/** + * Reset the DB to the default values in the task classes. + * + * @param {Task[]} tasks + */ +export function reset(tasks) { + unlinkSync(DATABASE_PATH) + write(tasks) + + for (const task of tasks) { + task.readConfig() + } +} + +/** + * Write the tasks to disc, but only if the DB is empty. + * + * @param {Task[]} tasks + */ export function setDefaults(tasks) { if (!isEmpty()) { return !!console.info( 'Skipping setting default tasks - data/tasks/db.json exists.' ) } + + write(tasks) +} + +/** + * Write the tasks to disc. + * + * @param {Task[]} tasks + */ +export function write(tasks) { + tasks = tasks.map(task => task.toJSON()) + db.defaults({ tasks: [...tasks], }).write() From 0fd749bc29df1323713d2338d1c973cb63435ab6 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:51:33 +0000 Subject: [PATCH 32/69] feat: Add reset command. --- src/commands/tasks/reset.js | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/commands/tasks/reset.js diff --git a/src/commands/tasks/reset.js b/src/commands/tasks/reset.js new file mode 100644 index 0000000..14c0c59 --- /dev/null +++ b/src/commands/tasks/reset.js @@ -0,0 +1,42 @@ +import { Command } from 'discord.js-commando' +import { RichEmbed } from 'discord.js' +import { + OWNER_IDS, + BOT_DEVELOPER_IDS, + MODERATOR_ROLE_IDS, +} from '../../utils/constants' +import { reset } from '../../services/tasks' + +const ALLOWED_ROLES = [...MODERATOR_ROLE_IDS] +const ALLOWED_USERS = [...OWNER_IDS, ...BOT_DEVELOPER_IDS] + +module.exports = class TasksEnableCommand extends Command { + constructor(client) { + super(client, { + name: 'reset-tasks', + group: 'tasks', + guildOnly: true, + memberName: 'reset', + description: 'Reset task configurations.', + }) + } + + hasPermission(msg) { + if (msg.member.roles.some(role => ALLOWED_ROLES.includes(role.id))) { + return true + } + + return ALLOWED_USERS.includes(msg.author.id) + } + + async run(msg) { + reset() + + msg.channel.send( + new RichEmbed() + .setTitle('Reset Tasks') + .setColor('GREEN') + .setDescription('Reset task configurations to defaults in task files.') + ) + } +} From 69756ac17f8df18bad3d01e26fe80480cd5a080a Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:52:23 +0000 Subject: [PATCH 33/69] refactor: Log task. --- src/tasks/log.js | 72 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/src/tasks/log.js b/src/tasks/log.js index cdb6275..9c62f03 100644 --- a/src/tasks/log.js +++ b/src/tasks/log.js @@ -2,11 +2,13 @@ import { RichEmbed } from 'discord.js' import Task from '../lib/task' import { trySend } from '../utils/messages' import { inlineCode } from '../utils/string' +import { GUILDS } from '../utils/constants' export default class LogTask extends Task { constructor(client) { super(client, { name: 'log', + guild: GUILDS.CURRENT, events: [ 'ready', 'resume', @@ -43,7 +45,10 @@ export default class LogTask extends Task { trySend( this.config.connectionChannel, null, - this.buildEmbed(null, { title: 'Connection Initiated', color: 'GREEN' }) + this.buildEmbed(null, null, { + title: 'Connection Initiated', + color: 'GREEN', + }) ) } @@ -55,17 +60,26 @@ export default class LogTask extends Task { trySend( this.config.commandChannel, null, - this.buildEmbed(null, { title: 'Connection Resumed', color: 'BLUE' }) + this.buildEmbed(null, null, { + title: 'Connection Resumed', + color: 'BLUE', + }) ) } commandRun(cmd, _, msg) { + if (!this.config.shouldLog.invokedCommands) { + return + } + trySend( this.config.commandChannel, null, - this.buildEmbed(msg, { title: 'Command Invocation', color: 'GREEN' }, [ - { name: 'Command', value: inlineCode(msg.command.name) }, - ]) + this.buildEmbed(msg, cmd, { + title: 'Command Invocation', + color: 'GREEN', + addCommand: true, + }) ) } @@ -77,9 +91,22 @@ export default class LogTask extends Task { trySend( this.config.commandChannel, null, - this.buildEmbed(msg, { title: 'Command Error', color: 'RED' }, [ - { name: 'Command', value: inlineCode(cmd.name) }, - ]) + this.buildEmbed( + msg, + cmd, + { + title: 'Command Error', + color: 'RED', + addCommand: true, + }, + [ + { + name: 'Error', + value: err.message, + inline: true, + }, + ] + ) ) } @@ -91,17 +118,16 @@ export default class LogTask extends Task { trySend( this.config.commandChannel, null, - this.buildEmbed(msg, { title: 'Unknown Command', color: 'ORANGE' }, [ - { - name: 'Command', - value: inlineCode(msg.cleanContent), - }, - ]) + this.buildEmbed(msg, null, { + title: 'Unknown Command', + color: 'ORANGE', + addCommand: true, + }) ) } - buildEmbed(msg, options, fields = []) { - const embed = new RichEmbed().setTimestamp() + buildEmbed(msg, cmd, options, fields = []) { + const embed = new RichEmbed().setFooter(new Date().toUTCString()) if (options.title) { embed.setTitle(options.title) @@ -120,8 +146,20 @@ export default class LogTask extends Task { .addField('User', msg.author, true) } + if (options.addCommand) { + let commandString = cmd + ? msg.client.commandPrefix + msg.command.name + : msg.cleanContent + + if (msg.argString) { + commandString += msg.argString + } + + embed.addField('Command', inlineCode(commandString), true) + } + for (const field of fields) { - embed.addField(field.name, field.value) + embed.addField(field.name, field.value, field.inline || false) } return embed From e8f2343ce4f740abcb2b59b0fbcd3a9adadaecd6 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:52:44 +0000 Subject: [PATCH 34/69] feat: Add error command. --- src/commands/development/error.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/commands/development/error.js diff --git a/src/commands/development/error.js b/src/commands/development/error.js new file mode 100644 index 0000000..917c38f --- /dev/null +++ b/src/commands/development/error.js @@ -0,0 +1,27 @@ +import { Command } from 'discord.js-commando' + +/* + This development-only command just throws an error. +*/ +module.exports = class DevelopmentPaginateCommand extends Command { + constructor(client) { + super(client, { + enabled: process.env.NODE_ENV === 'development', + guarded: true, + name: 'error', + examples: ['!error'], + group: 'development', + guildOnly: false, + memberName: 'error', + description: 'Create a command error.', + }) + } + + hasPermission() { + return true + } + + async run() { + throw new Error('Well, you asked for it.') + } +} From f8035ad6eddfe690ec338c0eb3e53db6357791f5 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:53:06 +0000 Subject: [PATCH 35/69] fix: Lower-case triggers. --- src/commands/moderation/add-trigger.js | 2 +- src/commands/moderation/remove-trigger.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/moderation/add-trigger.js b/src/commands/moderation/add-trigger.js index 43419f3..e121398 100644 --- a/src/commands/moderation/add-trigger.js +++ b/src/commands/moderation/add-trigger.js @@ -66,7 +66,7 @@ module.exports = class ModerationAddTriggerCommand extends Command { } else { moderation .get('triggers') - .push({ trigger, action }) + .push({ trigger: trigger.toLowerCase(), action }) .write() msg.channel.send( diff --git a/src/commands/moderation/remove-trigger.js b/src/commands/moderation/remove-trigger.js index 9da9fbb..84fddde 100644 --- a/src/commands/moderation/remove-trigger.js +++ b/src/commands/moderation/remove-trigger.js @@ -40,7 +40,7 @@ module.exports = class ModerationRemoveTriggersCommand extends Command { const exists = moderation .get('triggers') - .find(item => item.trigger === trigger) + .find(item => item.trigger === trigger.toLowerCase()) .value() if (!exists) { From 62f4ef84bd161527737056cf7109575e2a087035 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:54:04 +0000 Subject: [PATCH 36/69] feat: Guild-specific tasks. --- src/lib/task.js | 120 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/src/lib/task.js b/src/lib/task.js index 8af8d6e..9c8163f 100644 --- a/src/lib/task.js +++ b/src/lib/task.js @@ -1,5 +1,6 @@ import EventEmitter from 'events' import tasks, { isEmpty } from '../services/tasks' +import { TextChannel } from 'discord.js' /** * A Task is a task which by default runs for every single message received so @@ -94,12 +95,18 @@ export default class Task extends EventEmitter { console.warn('Conflicting options - guildOnly and warnOnly.') } + if (options.guild !== 'undefined') { + this.guild = options.guild + } + this.name = options.name this.events = options.events this.config = options.config this.ignored = options.ignored this.description = options.description || '' + this.handlers = {} + // NOTE: We need to bind the events here else their `this` will be `CommandoClient`. for (const event of this.events) { if (!isValidEvent(event)) { @@ -110,7 +117,13 @@ export default class Task extends EventEmitter { console.warn(`Missing event handler for event ${event} for ${this}.`) } - this[event] = this[event].bind(this) + this.handlers[event] = this[event].bind(this) + + this[event] = (...args) => { + if (this.shouldEventFire(event, args)) { + this.handlers[event].apply(this, args) + } + } } if (this.inhibit) { @@ -338,6 +351,7 @@ export default class Task extends EventEmitter { toJSON() { return { name: this.name, + guild: this.guild, guildOnly: this.guildOnly, dmOnly: this.dmOnly, config: this.config, @@ -360,6 +374,7 @@ export default class Task extends EventEmitter { console.warn(`${this} Could not find task config in DB!`) } + this.guild = config.guild this.guildOnly = config.guildOnly this.dmOnly = config.dmOnly this.config = config.config @@ -392,6 +407,109 @@ export default class Task extends EventEmitter { console.error(e) } } + + /** + * Determine if an event for an enabled task should actually fire. + * + * Consider a guild-specific task, in which case the event should only fire if + * the event originated from the correct guild. + * + * @param {string} eventName The name of the event. + * @param {array} eventArgs The args passed to the event. + */ + shouldEventFire(eventName, eventArgs) { + if (!this.guild) { + return true // This task isn't guild-specific, there's nothing to check. + } + + try { + let guildId + + switch (eventName) { + case 'channelCreate': + case 'channelDelete': + case 'channelPinsUpdate': + case 'channelUpdate': + case 'emojiCreate': + case 'emojiDelete': + case 'emojiUpdate': + case 'guildMemberAdd': + case 'guildMemberAvailable': + case 'guildMemberRemove': + case 'guildMemberSpeaking': + case 'guildMemberUpdate': + case 'message': + case 'messageDelete': + case 'messageUpdate': + case 'presenceUpdate': + case 'roleCreate': + case 'roleDelete': + case 'roleUpdate': + case 'voiceStateUpdate': + case 'commandBlocked': + case 'unknownCommand': + guildId = eventArgs[0].guild.id + break + + case 'commandCancel': + case 'commandError': + case 'commandRun': + guildId = eventArgs[2].guild.id + break + + case 'typingStart': + case 'typingStop': + case 'webhookUpdate': + if (eventArgs[0] instanceof TextChannel) { + guildId = eventArgs[0].guild.id + } + break + + case 'messageDeleteBulk': + guildId = eventArgs[0].first().guild.id + break + + case 'guildBanAdd': + case 'guildBanRemove': + case 'guildCreate': + case 'guildDelete': + case 'guildIntegrationsUpdate': + case 'guildUnavailable': + case 'guildUpdate': + case 'commandPrefixChange': + case 'commandStatusChange': + guildId = eventArgs[0].id + break + + case 'guildMembersChunk': + guildId = eventArgs[1].id + break + + case 'messageReactionAdd': + case 'messageReactionRemove': + case 'messageReactionRemoveAll': + guildId = eventArgs[0].message.guild.id + break + + default: + return true + } + + if (!guildId) { + return false + } + + console.debug( + `eventShouldFire - checking that ${guildId} is equal to ${this.guild}` + ) + + return guildId === this.guild + } catch (e) { + console.error(e) + + return false + } + } } /* From d54315047396b41666290db15b214927b9199ae4 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:54:46 +0000 Subject: [PATCH 37/69] other: Reply instead. --- src/tasks/test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tasks/test.js b/src/tasks/test.js index bdf8e6f..7d75c34 100644 --- a/src/tasks/test.js +++ b/src/tasks/test.js @@ -9,7 +9,7 @@ export default class TestTask extends Task { }) } - run() { - console.log('[TestTask] Executed!') + run(msg) { + msg.reply('[TestTask] Executed!') } } From 35724bb4223e734479a44d464b94ae0b567eeb69 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:55:14 +0000 Subject: [PATCH 38/69] other: Check for guild-specific tasks. --- src/client.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 95a4b23..987e43b 100644 --- a/src/client.js +++ b/src/client.js @@ -136,7 +136,18 @@ client.on('message', msg => { } client.tasks - .filter(task => task.enabled) + .filter(task => { + if (!task.enabled) { + return false + } + + // Check for guild-specific tasks. + if (task.guild && msg.guild && task.guild !== msg.guild.id) { + return false + } + + return true + }) .forEach(task => { if (task.shouldExecute(msg)) { task.run(msg) From 82fc2c3e2dd24886f4f5a88fe95d0fd4d00ab995 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:55:54 +0000 Subject: [PATCH 39/69] enhance: Debug mode also affects ignored roles. --- src/tasks/moderation.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index 483241a..b451b81 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -8,7 +8,10 @@ import { } from '../utils/constants' import { blockCode, inlineCode } from '../utils/string' -// Don't *actually* ban - real bans make testing hard! +/* + In debug mode we don't *actually* ban even if a ban is triggered, additionally + we don't add any ignored roles, so that moderators etc. can test the command too. +*/ const DEBUG_MODE = process.env.NODE_ENV === 'development' export default class ModerationTask extends Task { @@ -19,7 +22,9 @@ export default class ModerationTask extends Task { 'Takes action (warn, ban, notify) when users mention a trigger word.', enabled: true, ignored: { - roles: [...new Set(MODERATOR_ROLE_IDS.concat(PROTECTED_ROLE_IDS))], + roles: DEBUG_MODE + ? [] + : [...new Set(MODERATOR_ROLE_IDS.concat(PROTECTED_ROLE_IDS))], }, guildOnly: true, config: { From 937cde5b4be7f8da36326364a3786e169165bfd3 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:56:18 +0000 Subject: [PATCH 40/69] fix: Logic error. --- src/tasks/moderation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index b451b81..da0b486 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -49,7 +49,7 @@ export default class ModerationTask extends Task { .value() if (!match) { - return true + return false } console.log(`Message is ${msg.cleanContent}`) From 7e2bbe78a97608aa2935aad40c8a6fce019e2bcf Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:56:41 +0000 Subject: [PATCH 41/69] style: Clean up comments. --- src/tasks/moderation.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index da0b486..ca9fb63 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -52,8 +52,6 @@ export default class ModerationTask extends Task { return false } - console.log(`Message is ${msg.cleanContent}`) - console.log(`Action to take is ${match.action}`, match) this.action = match.action } From d06aa365740e4e5892ee6c2a1a9fc04158072944 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:56:57 +0000 Subject: [PATCH 42/69] fix: Call super method. --- src/tasks/moderation.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index ca9fb63..975e51d 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -41,6 +41,10 @@ export default class ModerationTask extends Task { } shouldExecute(msg) { + if (!super.shouldExecute(msg)) { + return false + } + const match = moderation .get('triggers') .find(({ trigger }) => { From b085689669c0323859442687262fbd15e72fcbf3 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:57:31 +0000 Subject: [PATCH 43/69] fix: Channel could be on wrong guild. --- src/tasks/moderation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index 975e51d..d94be1b 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -63,7 +63,7 @@ export default class ModerationTask extends Task { const notifyRole = msg.guild.roles.find( role => role.name === this.config.notifyRole.name ) - const logChannel = msg.client.channels.find( + const logChannel = msg.guild.channels.find( channel => channel.name === this.config.logChannel.name ) From b86105b7bac462a9214236dbf0a28f6b8767ff9c Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:57:51 +0000 Subject: [PATCH 44/69] refactor: Slight refactor. --- src/tasks/moderation.js | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index d94be1b..a31e853 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -125,7 +125,7 @@ export default class ModerationTask extends Task { try { const dmChannel = await msg.author.createDM() this.log(msg, dmChannel, { color: 'ORANGE', isDMWarning: true }) - this.log(msg, logChannel, { color: 'ORANGE' }) + this.log(msg, logChannel, { color: 'ORANGE', isDMWarning: false }) } catch (e) { console.error(e) } @@ -136,13 +136,25 @@ export default class ModerationTask extends Task { } log(msg, logChannel, options = {}) { - logChannel.send(options.notifyRole || EMPTY_MESSAGE, { - embed: - options.embed || - this.createEmbed(msg, options.isDMWarning || false, { - color: 'ORANGE', - }), - }) + if (typeof options.notifyRole === 'undefined') { + options.notifyRole = EMPTY_MESSAGE + } + + if (typeof options.isDMWarning === 'undefined') { + options.isDMWarning = false + } + + const embed = + options.embed || + this.createEmbed(msg, options.isDMWarning, { + color: 'ORANGE', + }) + + if (options.notifyRole) { + logChannel.send(options.notifyRole, { embed }) + } else { + logChannel.send(embed) + } } createEmbed(msg, isDMWarning = false, options = {}) { From 2eae7b23147271d9f5a5ce3daf16d6136d75d905 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:58:07 +0000 Subject: [PATCH 45/69] other: Make guild-specific. --- src/tasks/moderation.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index a31e853..6da7423 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -5,6 +5,7 @@ import { MODERATOR_ROLE_IDS, PROTECTED_ROLE_IDS, EMPTY_MESSAGE, + GUILDS, } from '../utils/constants' import { blockCode, inlineCode } from '../utils/string' @@ -18,6 +19,7 @@ export default class ModerationTask extends Task { constructor(client) { super(client, { name: 'moderation', + guild: GUILDS.CURRENT, description: 'Takes action (warn, ban, notify) when users mention a trigger word.', enabled: true, From 3969a9666fa9f44418e9fd3d54c6a8dddf553f14 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:58:23 +0000 Subject: [PATCH 46/69] fix: Missing return. --- src/tasks/moderation.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index 6da7423..682a992 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -59,6 +59,8 @@ export default class ModerationTask extends Task { } this.action = match.action + + return true } run(msg) { From 6ddcc62baebabaa7c566aa62f3561a496c804463 Mon Sep 17 00:00:00 2001 From: sustained Date: Fri, 10 Jan 2020 18:58:38 +0000 Subject: [PATCH 47/69] deps: Update version. --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 1991fc7..a8a18ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vue-land-bot", - "version": "1.0.0", + "version": "0.1.0-beta-1", "lockfileVersion": 1, "requires": true, "dependencies": { From 7e894e63c11d74bdf69f545fd2b171e631ab82dd Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 11:43:09 +0000 Subject: [PATCH 48/69] style: Grammar. --- src/tasks/moderation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index 682a992..7f97b6d 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -21,7 +21,7 @@ export default class ModerationTask extends Task { name: 'moderation', guild: GUILDS.CURRENT, description: - 'Takes action (warn, ban, notify) when users mention a trigger word.', + 'Takes action (warn, ban, notify) when a user mention a trigger word.', enabled: true, ignored: { roles: DEBUG_MODE From 523afed5ab15840d977a828a005a69d6104ecc91 Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 11:43:49 +0000 Subject: [PATCH 49/69] fix: Switch to a case-insensitive match for roles and channels. --- src/tasks/moderation.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index 7f97b6d..69d4303 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -65,10 +65,12 @@ export default class ModerationTask extends Task { run(msg) { const notifyRole = msg.guild.roles.find( - role => role.name === this.config.notifyRole.name + role => + role.name.toLowerCase() === this.config.notifyRole.name.toLowerCase() ) const logChannel = msg.guild.channels.find( - channel => channel.name === this.config.logChannel.name + channel => + channel.name.toLowerCase() === this.config.logChannel.name.toLowerCase() ) if (!logChannel) { From 2fde58481f27ddddaaa162077e0ab68b90bca6d9 Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 11:45:04 +0000 Subject: [PATCH 50/69] refactor: Refactor bans since (new) kick logic is very similar. --- src/tasks/moderation.js | 80 +++++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 15 deletions(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index 69d4303..b6e55de 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -83,6 +83,9 @@ export default class ModerationTask extends Task { case 'warn': this.warn(msg, logChannel) break + case 'kick': + this.kick(msg, logChannel) + break case 'ban': this.ban(msg, logChannel) break @@ -94,37 +97,84 @@ export default class ModerationTask extends Task { } ban(msg, logChannel) { - // Can't ban a user from a DM. + const check = this.checkPermissionsFor(msg, logChannel, 'BAN_MEMBERS') + + if (check === false) { + return + } else if (check instanceof RichEmbed) { + return this.log(msg, logChannel, { embed: check }) + } + + msg.member + .ban(`[${msg.client.user.name}] Automated anti-spam measures.`) + .then(() => this.log(msg, logChannel, { color: 'PURPLE' })) + .catch(console.error) + } + + kick(msg, logChannel) { + const check = this.checkPermissionsFor(msg, logChannel, 'KICK_MEMBERS') + + if (check === false) { + return + } else if (check instanceof RichEmbed) { + return this.log(msg, logChannel, { embed: check }) + } + + msg.member + .kick(`[${msg.client.user.name}] Automated anti-spam measures.`) + .then(() => this.log(msg, logChannel, { color: 'PURPLE' })) + .catch(console.error) + } + + permissionToAction(permission) { + switch (permission) { + case 'KICK_MEMBERS': + return 'kick' + case 'BAN_MEMBERS': + return 'ban' + } + } + + checkPermissionsFor(msg, logChannel, permission) { + const action = this.permissionToAction(permission) + + // Can't kick a user from a DM. if (msg.channel.type === 'dm') { - return !!console.warn('[ModerationTask] Cannot ban in a DM channel.') + return !!console.warn( + `[ModerationTask] Cannot ${action} in a DM channel.` + ) } - // We don't have permission to ban - bail. - if (!msg.channel.permissionsFor(msg.client.user).has('BAN_MEMBERS')) { - return !!console.warn('[ModerationTask] Cannot ban - lacking permission.') + // We don't have permission to carry out the action - bail. + if (!msg.channel.permissionsFor(msg.client.user).has(permission)) { + console.warn(`[ModerationTask] Cannot ${action} - lacking permission.`) + + return this.createEmbed(msg, false, { color: 'PURPLE' }).addField( + 'NOTE', + `No ${action} was enacted as I lack ${inlineCode(permission)}.` + ) } const botMember = msg.guild.member(msg.client.user) const botHighestRole = botMember.highestRole.calculatedPosition const userHighestRole = msg.member.highestRole.calculatedPosition - // Our role is not high enough in the hierarchy to ban - bail. + // Our role is not high enough in the hierarchy to carry out the action - bail. if (botHighestRole < userHighestRole) { - return !!console.warn('[BanTask] Cannot ban - role too low.') + console.warn(`[ModerationTask] Cannot ${action} - role too low.`) + + return this.createEmbed(msg, false, { color: 'PURPLE' }).addField( + 'NOTE', + `No ${action} was enacted as the user is higher than me in the role hierarchy.` + ) } if (DEBUG_MODE) { - const embed = this.createEmbed(msg, false, { color: 'RED' }).addField( + return this.createEmbed(msg, false, { color: 'RED' }).addField( 'NOTE', - 'Debug mode enabled - no ban was actually issued.' + `Debug mode is enabled - no ${action} was actually issued.` ) - return this.log(msg, logChannel, { embed }) } - - msg.member - .ban(`[${msg.client.user.name}] Automated anti-spam measures.`) - .then(() => this.log(msg, logChannel)) - .catch(console.error) } async warn(msg, logChannel) { From 8c86083b118a61505bf7a0cf48a3bf8c6ce59054 Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 11:45:39 +0000 Subject: [PATCH 51/69] fix: We should still continue even if there is no log channel. --- src/tasks/moderation.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index b6e55de..f05df6b 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -74,7 +74,7 @@ export default class ModerationTask extends Task { ) if (!logChannel) { - return console.warn( + console.warn( `[ModerationTask]: Could not find channel with name ${this.config.logChannel.name}!` ) } @@ -206,6 +206,13 @@ export default class ModerationTask extends Task { color: 'ORANGE', }) + if (!logChannel) { + return !!console.log( + 'Was going to semd the following embed, but no logChannel exists.', + embed + ) + } + if (options.notifyRole) { logChannel.send(options.notifyRole, { embed }) } else { From 32ceca53abba37a57ad38d5fc224fbcee353d15f Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 11:51:50 +0000 Subject: [PATCH 52/69] other: Make it clear how important this is. --- src/tasks/moderation.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index f05df6b..64a70a6 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -43,6 +43,7 @@ export default class ModerationTask extends Task { } shouldExecute(msg) { + // NOTE: Never remove this check. if (!super.shouldExecute(msg)) { return false } From e4b13f7edf0d7846fb6f1839fff8be2d0f98b520 Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 11:52:08 +0000 Subject: [PATCH 53/69] fix: Update description. --- src/tasks/moderation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index 64a70a6..20f7ef1 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -21,7 +21,7 @@ export default class ModerationTask extends Task { name: 'moderation', guild: GUILDS.CURRENT, description: - 'Takes action (warn, ban, notify) when a user mention a trigger word.', + 'Takes action (warn, kick, ban, notify) when a user mention a trigger word.', enabled: true, ignored: { roles: DEBUG_MODE From 95ae97e1c11b91dea76a17f108916b7e7c92332e Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 11:52:51 +0000 Subject: [PATCH 54/69] style: The protected roles includes moderators, so... --- src/tasks/moderation.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index 20f7ef1..cf99fae 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -1,12 +1,7 @@ import { RichEmbed } from 'discord.js' import Task from '../lib/task' import moderation from '../services/moderation' -import { - MODERATOR_ROLE_IDS, - PROTECTED_ROLE_IDS, - EMPTY_MESSAGE, - GUILDS, -} from '../utils/constants' +import { PROTECTED_ROLE_IDS, EMPTY_MESSAGE, GUILDS } from '../utils/constants' import { blockCode, inlineCode } from '../utils/string' /* @@ -24,9 +19,7 @@ export default class ModerationTask extends Task { 'Takes action (warn, kick, ban, notify) when a user mention a trigger word.', enabled: true, ignored: { - roles: DEBUG_MODE - ? [] - : [...new Set(MODERATOR_ROLE_IDS.concat(PROTECTED_ROLE_IDS))], + roles: DEBUG_MODE ? [] : [...PROTECTED_ROLE_IDS], }, guildOnly: true, config: { From 7e3aced4b74d2430424357ba646dce5270ecf7d8 Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 11:53:45 +0000 Subject: [PATCH 55/69] style: Fix typo and add tag to align with other logs. --- src/tasks/moderation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index cf99fae..4084c38 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -202,7 +202,7 @@ export default class ModerationTask extends Task { if (!logChannel) { return !!console.log( - 'Was going to semd the following embed, but no logChannel exists.', + '[ModerationTask] Was going to send the following embed, but no logChannel exists.', embed ) } From b2cc959636f18eabcf02d0b622b6e523bb2e37fa Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 11:54:12 +0000 Subject: [PATCH 56/69] style: Simplify options. --- src/tasks/moderation.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index 4084c38..a1e86cb 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -194,11 +194,11 @@ export default class ModerationTask extends Task { options.isDMWarning = false } - const embed = - options.embed || - this.createEmbed(msg, options.isDMWarning, { - color: 'ORANGE', - }) + if (typeof options.color === 'undefined') { + options.color = 'ORANGE' + } + + const embed = options.embed || this.createEmbed(msg, options) if (!logChannel) { return !!console.log( @@ -214,7 +214,7 @@ export default class ModerationTask extends Task { } } - createEmbed(msg, isDMWarning = false, options = {}) { + createEmbed(msg, options = {}) { const excerpt = msg.cleanContent.length > 150 ? msg.cleanContent.substring(0, 150) + '...' @@ -226,7 +226,7 @@ export default class ModerationTask extends Task { .setTimestamp() .addField('Message Excerpt', blockCode(excerpt)) - if (isDMWarning) { + if (options.isDMWarning) { embed.setDescription( 'One of your messages triggered auto-moderation. Repeated infringements may result in a ban.' ) From d18faafaa12293d89d58035d027d03ae7d1c681b Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 11:54:50 +0000 Subject: [PATCH 57/69] feat: Add new kick action + reorder arguments (more-significant-first). --- src/commands/moderation/add-trigger.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/commands/moderation/add-trigger.js b/src/commands/moderation/add-trigger.js index e121398..6f5c059 100644 --- a/src/commands/moderation/add-trigger.js +++ b/src/commands/moderation/add-trigger.js @@ -5,18 +5,12 @@ import { MODERATOR_ROLE_IDS, BOT_DEVELOPER_IDS } from '../../utils/constants' import { inlineCode } from '../../utils/string' const { NODE_ENV } = process.env -const VALID_ACTIONS = ['warn', 'ban', 'notify'] +const VALID_ACTIONS = ['warn', 'kick', 'ban', 'notify'] module.exports = class ModerationAddTriggerCommand extends Command { constructor(client) { super(client, { args: [ - { - key: 'trigger', - type: 'string', - prompt: - 'the trigger word to add (use quotation marks if it contains spaces)?', - }, { key: 'action', type: 'string', @@ -27,6 +21,12 @@ module.exports = class ModerationAddTriggerCommand extends Command { return VALID_ACTIONS.includes(value) }, }, + { + key: 'trigger', + type: 'string', + prompt: + 'the trigger word to add (use quotation marks if it contains spaces)?', + }, ], name: 'add-trigger', group: 'moderation', @@ -74,7 +74,7 @@ module.exports = class ModerationAddTriggerCommand extends Command { .setTitle('Moderation - Add Trigger') .setColor('GREEN') .setDescription( - `Successfully added trigger word ${inlineCode( + `Successfully added trigger word/pharse ${inlineCode( trigger )} with action ${inlineCode(action)}.` ) From 2a3667b17ade996d5359b6e15140953a3b1e89d3 Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 12:08:01 +0000 Subject: [PATCH 58/69] style: JSDoc all the things + method reordering. --- src/tasks/moderation.js | 180 +++++++++++++++++++++++++++------------- 1 file changed, 122 insertions(+), 58 deletions(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index a1e86cb..62cc90e 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -35,6 +35,12 @@ export default class ModerationTask extends Task { this.action = null // ban | warn | notify } + /** + * Check if the message matches any of the trigger words/phrases. + * + * @param {CommandMessage} msg + * @returns {boolean} + */ shouldExecute(msg) { // NOTE: Never remove this check. if (!super.shouldExecute(msg)) { @@ -57,6 +63,11 @@ export default class ModerationTask extends Task { return true } + /** + * Determine the role to notify, the channel to log to, then carry out the action. + * + * @param {CommandMessage} msg + */ run(msg) { const notifyRole = msg.guild.roles.find( role => @@ -90,8 +101,15 @@ export default class ModerationTask extends Task { } } - ban(msg, logChannel) { - const check = this.checkPermissionsFor(msg, logChannel, 'BAN_MEMBERS') + /** + * ACTION: Kick the user from the server. + * + * @param {CommandMessage} msg + * @param {GuildChannel} logChannel + * @param {Role} notifyRole + */ + kick(msg, logChannel) { + const check = this.checkPermissionsFor(msg, logChannel, 'KICK_MEMBERS') if (check === false) { return @@ -100,13 +118,19 @@ export default class ModerationTask extends Task { } msg.member - .ban(`[${msg.client.user.name}] Automated anti-spam measures.`) + .kick(`[${msg.client.user.name}] Automated anti-spam measures.`) .then(() => this.log(msg, logChannel, { color: 'PURPLE' })) .catch(console.error) } - kick(msg, logChannel) { - const check = this.checkPermissionsFor(msg, logChannel, 'KICK_MEMBERS') + /** + * ACTION: Ban the user from the server. + * + * @param {CommandMessage} msg + * @param {GuildChannel} logChannel + */ + ban(msg, logChannel) { + const check = this.checkPermissionsFor(msg, logChannel, 'BAN_MEMBERS') if (check === false) { return @@ -115,62 +139,17 @@ export default class ModerationTask extends Task { } msg.member - .kick(`[${msg.client.user.name}] Automated anti-spam measures.`) + .ban(`[${msg.client.user.name}] Automated anti-spam measures.`) .then(() => this.log(msg, logChannel, { color: 'PURPLE' })) .catch(console.error) } - permissionToAction(permission) { - switch (permission) { - case 'KICK_MEMBERS': - return 'kick' - case 'BAN_MEMBERS': - return 'ban' - } - } - - checkPermissionsFor(msg, logChannel, permission) { - const action = this.permissionToAction(permission) - - // Can't kick a user from a DM. - if (msg.channel.type === 'dm') { - return !!console.warn( - `[ModerationTask] Cannot ${action} in a DM channel.` - ) - } - - // We don't have permission to carry out the action - bail. - if (!msg.channel.permissionsFor(msg.client.user).has(permission)) { - console.warn(`[ModerationTask] Cannot ${action} - lacking permission.`) - - return this.createEmbed(msg, false, { color: 'PURPLE' }).addField( - 'NOTE', - `No ${action} was enacted as I lack ${inlineCode(permission)}.` - ) - } - - const botMember = msg.guild.member(msg.client.user) - const botHighestRole = botMember.highestRole.calculatedPosition - const userHighestRole = msg.member.highestRole.calculatedPosition - - // Our role is not high enough in the hierarchy to carry out the action - bail. - if (botHighestRole < userHighestRole) { - console.warn(`[ModerationTask] Cannot ${action} - role too low.`) - - return this.createEmbed(msg, false, { color: 'PURPLE' }).addField( - 'NOTE', - `No ${action} was enacted as the user is higher than me in the role hierarchy.` - ) - } - - if (DEBUG_MODE) { - return this.createEmbed(msg, false, { color: 'RED' }).addField( - 'NOTE', - `Debug mode is enabled - no ${action} was actually issued.` - ) - } - } - + /** + * ACTION: Warn the member privately, via DM. + * + * @param {CommandMessage} msg + * @param {GuildChannel} logChannel + */ async warn(msg, logChannel) { try { const dmChannel = await msg.author.createDM() @@ -181,10 +160,24 @@ export default class ModerationTask extends Task { } } + /** + * ACTION: Notify the moderators. + * + * @param {CommandMessage} msg + * @param {GuildChannel} logChannel + * @param {Role} notifyRole + */ notify(msg, logChannel, notifyRole) { this.log(msg, logChannel, { notifyRole }) } + /** + * Log a moderation action. + * + * @param {CommandMessage} msg + * @param {GuildChannel} logChannel The channel to log to (except for DM warnings). + * @param {G} options + */ log(msg, logChannel, options = {}) { if (typeof options.notifyRole === 'undefined') { options.notifyRole = EMPTY_MESSAGE @@ -214,6 +207,12 @@ export default class ModerationTask extends Task { } } + /** + * Builds the RichEmbed that's used for all moderation task-related logging. + * + * @param {CommandMessage} msg + * @param {object} options + */ createEmbed(msg, options = {}) { const excerpt = msg.cleanContent.length > 150 @@ -223,7 +222,7 @@ export default class ModerationTask extends Task { const embed = new RichEmbed() .setTitle(`Moderation - ${options.title || this.action}`) .setColor(options.color || 'RANDOM') - .setTimestamp() + .setFooter(new Date().toUTCString()) .addField('Message Excerpt', blockCode(excerpt)) if (options.isDMWarning) { @@ -244,4 +243,69 @@ export default class ModerationTask extends Task { return embed } + + /** + * Check if we are able to carry out an action (kick/ban). + * + * @param {CommandMessage} msg + * @param {GuildChannel} logChannel + * @param {string} permission + * @returns {RichEmbed|boolean} + */ + checkPermissionsFor(msg, logChannel, permission) { + const action = this.permissionToAction(permission) + + // Can't kick a user from a DM. + if (msg.channel.type === 'dm') { + return !!console.warn( + `[ModerationTask] Cannot ${action} in a DM channel.` + ) + } + + // We don't have permission to carry out the action - bail. + if (!msg.channel.permissionsFor(msg.client.user).has(permission)) { + console.warn(`[ModerationTask] Cannot ${action} - lacking permission.`) + + return this.createEmbed(msg, false, { color: 'PURPLE' }).addField( + 'NOTE', + `No ${action} was enacted as I lack ${inlineCode(permission)}.` + ) + } + + const botMember = msg.guild.member(msg.client.user) + const botHighestRole = botMember.highestRole.calculatedPosition + const userHighestRole = msg.member.highestRole.calculatedPosition + + // Our role is not high enough in the hierarchy to carry out the action - bail. + if (botHighestRole < userHighestRole) { + console.warn(`[ModerationTask] Cannot ${action} - role too low.`) + + return this.createEmbed(msg, false, { color: 'PURPLE' }).addField( + 'NOTE', + `No ${action} was enacted as the user is higher than me in the role hierarchy.` + ) + } + + if (DEBUG_MODE) { + return this.createEmbed(msg, false, { color: 'RED' }).addField( + 'NOTE', + `Debug mode is enabled - no ${action} was actually issued.` + ) + } + } + + /** + * Convert a Discord permission string to an "action string". + * + * @param {string} permission + * @returns {string} + */ + permissionToAction(permission) { + switch (permission) { + case 'KICK_MEMBERS': + return 'kick' + case 'BAN_MEMBERS': + return 'ban' + } + } } From 1950e457bc836616b29a75f1019559c1ba994975 Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 12:08:13 +0000 Subject: [PATCH 59/69] style: Fix a comment. --- src/tasks/moderation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/moderation.js b/src/tasks/moderation.js index 62cc90e..ddd6ffa 100644 --- a/src/tasks/moderation.js +++ b/src/tasks/moderation.js @@ -80,7 +80,7 @@ export default class ModerationTask extends Task { if (!logChannel) { console.warn( - `[ModerationTask]: Could not find channel with name ${this.config.logChannel.name}!` + `[ModerationTask] Could not find logChannel: ${this.config.logChannel.name}!` ) } From 4faccf29adef1ed3f8f27899e3c76add4e690aa5 Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 12:09:59 +0000 Subject: [PATCH 60/69] style: Fix class name. --- src/commands/development/error.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/development/error.js b/src/commands/development/error.js index 917c38f..fecc08a 100644 --- a/src/commands/development/error.js +++ b/src/commands/development/error.js @@ -3,7 +3,7 @@ import { Command } from 'discord.js-commando' /* This development-only command just throws an error. */ -module.exports = class DevelopmentPaginateCommand extends Command { +module.exports = class DevelopmentErrorCommand extends Command { constructor(client) { super(client, { enabled: process.env.NODE_ENV === 'development', From 02cca57f8d9fdede49db621f166daf1fe2a91eb5 Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 12:26:54 +0000 Subject: [PATCH 61/69] fix: Development-only command registration in production. --- src/client.js | 30 ++++++++++++++++------------ src/commands/development/error.js | 1 - src/commands/development/paginate.js | 1 - 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/client.js b/src/client.js index 987e43b..3b72eaf 100644 --- a/src/client.js +++ b/src/client.js @@ -4,6 +4,13 @@ import { Collection } from 'discord.js' import { CommandoClient } from 'discord.js-commando' import { setDefaults } from './services/tasks' +/* + Ensure that NODE_ENV is set to development if it is unset. +*/ +if (!process.env.NODE_ENV) { + process.env.NODE_ENV = 'development' +} + const { NODE_ENV, COMMAND_PREFIX = '!' } = process.env /* @@ -18,13 +25,6 @@ if (OWNER_IDS.includes(',')) { } process.env.OWNER_IDS = OWNER_IDS -/* - Ensure that NODE_ENV is set to development if it is unset. -*/ -if (!NODE_ENV) { - process.env.NODE_ENV = 'development' -} - const PATH_TASKS = join(__dirname, 'tasks') const PATH_TYPES = join(__dirname, 'types') const PATH_COMMANDS = join(__dirname, 'commands') @@ -86,12 +86,12 @@ client.registry.registerGroups([ id: 'rfcs', name: 'RFCs', }, - { - id: 'development', - name: 'development', - }, ]) +if (NODE_ENV === 'development') { + client.registry.registerGroup('development', 'development') +} + /* Register default command groups, commands and argument types. @@ -103,7 +103,11 @@ client.registry.registerGroups([ */ client.registry.registerDefaults() client.registry.registerTypesIn(PATH_TYPES) -client.registry.registerCommandsIn(PATH_COMMANDS) +client.registry.registerCommandsIn({ + dirname: PATH_COMMANDS, + // NOTE: Exclude any commands in the development group, when in production. + excludeDirs: NODE_ENV === 'production' ? '^\\..*|development$' : undefined, +}) if (NODE_ENV === 'production') { const evalCommand = client.registry.findCommands('eval') @@ -112,10 +116,10 @@ if (NODE_ENV === 'production') { client.registry.unregisterCommand(evalCommand[0]) } } + /* Set up some global error handling and some purely informational event handlers. */ - client.on('warn', console.warn) client.on('error', console.error) diff --git a/src/commands/development/error.js b/src/commands/development/error.js index fecc08a..992bc3e 100644 --- a/src/commands/development/error.js +++ b/src/commands/development/error.js @@ -6,7 +6,6 @@ import { Command } from 'discord.js-commando' module.exports = class DevelopmentErrorCommand extends Command { constructor(client) { super(client, { - enabled: process.env.NODE_ENV === 'development', guarded: true, name: 'error', examples: ['!error'], diff --git a/src/commands/development/paginate.js b/src/commands/development/paginate.js index 6ab7143..bd00a7a 100644 --- a/src/commands/development/paginate.js +++ b/src/commands/development/paginate.js @@ -26,7 +26,6 @@ const items = [ module.exports = class DevelopmentPaginateCommand extends Command { constructor(client) { super(client, { - enabled: process.env.NODE_ENV === 'development', guarded: true, name: 'paginate', args: [ From a8a2c4d07d4f8dd765b7bcfd4aba3264aad380f2 Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 13:28:34 +0000 Subject: [PATCH 62/69] feat: If a new task is created, its config needs writing to the DB. --- src/lib/task.js | 51 ++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/lib/task.js b/src/lib/task.js index 9c8163f..494cf5c 100644 --- a/src/lib/task.js +++ b/src/lib/task.js @@ -35,6 +35,7 @@ export default class Task extends EventEmitter { super() this.client = client + this.options = options /* Tasks are stored as a key-value pair in a Collection (Map) on the client. @@ -95,7 +96,7 @@ export default class Task extends EventEmitter { console.warn('Conflicting options - guildOnly and warnOnly.') } - if (options.guild !== 'undefined') { + if (typeof options.guild !== 'undefined') { this.guild = options.guild } @@ -139,16 +140,15 @@ export default class Task extends EventEmitter { this.unregisterInhibitor() }) - // The DB is empty so we are safe to use the defauls from the Task file. - if (isEmpty()) { - this._dmOnly = options.dmOnly - this._guildOnly = options.guildOnly - this.enabled = options.enabled // NOTE: Must come last because fires an event. - } - // The DB is populated so we can't use the defaults from the Task file. - else { + this._dmOnly = options.dmOnly + this._guildOnly = options.guildOnly + + // The task DB is populated, so we'll override the defaults (`options`). + if (!isEmpty()) { this.readConfig() } + + this.enabled = options.enabled // NOTE: Must come last because fires an event. } /** @@ -356,7 +356,7 @@ export default class Task extends EventEmitter { dmOnly: this.dmOnly, config: this.config, ignored: this.ignored, - enabled: this.enabled, + enabled: this.enabled || this.options.enabled, // NOTE: Fallback to handle new tasks. } } @@ -365,25 +365,28 @@ export default class Task extends EventEmitter { */ readConfig() { try { - const config = tasks + let config = tasks .get('tasks') .find({ name: this.name }) .value() - if (!config) { - console.warn(`${this} Could not find task config in DB!`) + if (config) { + this.guild = config.guild + this.guildOnly = config.guildOnly + this.dmOnly = config.dmOnly + this.config = config.config + this.ignored = config.ignored + + console.debug( + `${this} Read configuration from DB and applied to instance.` + ) + } else { + tasks.get('tasks').push(this.toJSON()) + + console.debug( + `${this} New task (not in DB) detected - writing default configuration!` + ) } - - this.guild = config.guild - this.guildOnly = config.guildOnly - this.dmOnly = config.dmOnly - this.config = config.config - this.ignored = config.ignored - this.enabled = config.enabled // NOTE: Must come last because fires an event. - - console.debug( - `${this} Read configuration from DB and applied to instance.` - ) } catch (e) { console.error(e) } From c351e364c2cc3d904b55d2ba61756aa62887ab77 Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 13:29:20 +0000 Subject: [PATCH 63/69] fix: Address logic issues + clean up shouldEventFire. --- src/lib/task.js | 56 +++++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/lib/task.js b/src/lib/task.js index 494cf5c..2ed454c 100644 --- a/src/lib/task.js +++ b/src/lib/task.js @@ -414,19 +414,17 @@ export default class Task extends EventEmitter { /** * Determine if an event for an enabled task should actually fire. * - * Consider a guild-specific task, in which case the event should only fire if - * the event originated from the correct guild. + * Consider: + * - if a task is dmOnly but an event originates from a guild + * - if a task is guildOnly but an event originates from a DM + * - if a task is guild-specific but the guild does not match * * @param {string} eventName The name of the event. * @param {array} eventArgs The args passed to the event. */ shouldEventFire(eventName, eventArgs) { - if (!this.guild) { - return true // This task isn't guild-specific, there's nothing to check. - } - try { - let guildId + let message, channel, guild // We could be looking for any one of these. switch (eventName) { case 'channelCreate': @@ -451,25 +449,23 @@ export default class Task extends EventEmitter { case 'voiceStateUpdate': case 'commandBlocked': case 'unknownCommand': - guildId = eventArgs[0].guild.id + message = eventArgs[0] break case 'commandCancel': case 'commandError': case 'commandRun': - guildId = eventArgs[2].guild.id + message = eventArgs[2].message break case 'typingStart': case 'typingStop': case 'webhookUpdate': - if (eventArgs[0] instanceof TextChannel) { - guildId = eventArgs[0].guild.id - } + channel = eventArgs[0] break case 'messageDeleteBulk': - guildId = eventArgs[0].first().guild.id + message = eventArgs[0].first() break case 'guildBanAdd': @@ -481,35 +477,49 @@ export default class Task extends EventEmitter { case 'guildUpdate': case 'commandPrefixChange': case 'commandStatusChange': - guildId = eventArgs[0].id + guild = eventArgs[0] break case 'guildMembersChunk': - guildId = eventArgs[1].id + guild = eventArgs[1] break case 'messageReactionAdd': case 'messageReactionRemove': case 'messageReactionRemoveAll': - guildId = eventArgs[0].message.guild.id + message = eventArgs[0].message break + // The event cannot be tied to a message/channel/guild + // therefore options such as `dmOnly` are irrelevant. default: return true } - if (!guildId) { - return false + let guildId = -1 + + if (guild) { + guildId = guild.id + } else if (channel && channel.guild) { + guildId = channel.guild.id + } else if (message && message.guild) { + guildId = message.guild.id } - console.debug( - `eventShouldFire - checking that ${guildId} is equal to ${this.guild}` - ) + if (this.dmOnly && guildId !== -1) { + return false // The event originated from a guild - bail. + } else if (this.guildOnly && guildId === -1) { + return false // The event originated from a DM - bail. + } + + // This is a single-guild task - ensure a match. + if (this.guild && guildId === this.guild) { + return true + } - return guildId === this.guild + return false } catch (e) { console.error(e) - return false } } From 3c0d3cbc8f7af557a8b0d17fd463260d9695fee2 Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 13:30:09 +0000 Subject: [PATCH 64/69] style: Unused import. --- src/lib/task.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/task.js b/src/lib/task.js index 2ed454c..070ffe5 100644 --- a/src/lib/task.js +++ b/src/lib/task.js @@ -1,6 +1,5 @@ import EventEmitter from 'events' import tasks, { isEmpty } from '../services/tasks' -import { TextChannel } from 'discord.js' /** * A Task is a task which by default runs for every single message received so From 9af9e4e5c9a5b1a950b11e16c327e3c3fba4ca15 Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 13:40:47 +0000 Subject: [PATCH 65/69] docs: Better task documentation. --- src/lib/task.js | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/lib/task.js b/src/lib/task.js index 070ffe5..160acf3 100644 --- a/src/lib/task.js +++ b/src/lib/task.js @@ -2,22 +2,40 @@ import EventEmitter from 'events' import tasks, { isEmpty } from '../services/tasks' /** - * A Task is a task which by default runs for every single message received so - * long as it is enabled and its shouldExecute() returns true. + * A Task is like a command except it has no trigger - by default it will run + * for every single message received, so long as it is enabled and its + * `shouldExecute()` returns true. * - * Example usages: + * A Task can be configured to be `dmOnly` or `guildOnly`, in which case it will + * ignore messages from guilds or DMs (respectively). + * + * Tasks can be configured to ignore certain users, roles or channels. + * + * Additionally, a Task can be guild-specific - such a task only works in the guild + * specified. Note that a guild doesn't need to be `guildOnly` to be guild-specific. + * + * Tasks can also attach to both discord.js and Commando events, see `VALID_DISCORD_EVENTS` + * at the bottom of this file for a complete list. * - * - Check message contents against a banned word list (and optionally warn/kick/ban) - * - etc. + * When a task is enabled, its event handlers will be registered with the `CommandoClient` + * and when it's disabled they will be unregistered. * - * A Task does not necessarily need to process messages however - there is a - * concept of event-only tasks. Such a task should simply return false in - * shouldExecute and specify a list of Discord/Commando events via TaskOptions.events. + * In addition to the above options, tasks can also have arbitrary configuration + * attached to them. * - * When the task is enabled, listeners for those events will be attached to the - * CommandoClient and when the task is disabled they will be removed. + * Tasks, along with their options and their configs are persistent, see: data/tasks/db.json. + * + * Tasks can be controlled via the `!enable-task`, `!disable-task`, `task-info`, + * `!list-tasks` and `!reset-tasks` commands. + * + * Remember that a Task does not necessarily need to process messages. A task can be + * event-only, all it needs to do is return false in `shouldExecute`, in which + * case its `run` will never be called. + * + * Example usages: * - * See `src/tasks/log.js` for an example of an event-only task. + * - Check message contents against a banned word list (see: `tasks/moderation.js`) + * - Log command invocations, command errors etc. (see: `tasks/log.js`) * * @event Task#enabled * @event Task#disabled From 8f6597da9f3484c6a8f0bbc09b5feee9e5b809ad Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 13:42:51 +0000 Subject: [PATCH 66/69] docs: Mention the necessity of calling `super.shouldExecute()`. --- src/lib/task.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/task.js b/src/lib/task.js index 160acf3..3510b51 100644 --- a/src/lib/task.js +++ b/src/lib/task.js @@ -9,7 +9,9 @@ import tasks, { isEmpty } from '../services/tasks' * A Task can be configured to be `dmOnly` or `guildOnly`, in which case it will * ignore messages from guilds or DMs (respectively). * - * Tasks can be configured to ignore certain users, roles or channels. + * Tasks can be configured to ignore certain users, roles or channels. But it's + * important to note that if you provide your own `shouldExecute`, then you must + * also call the parent method e.g. `if (!super.shouldExecute()) return false`. * * Additionally, a Task can be guild-specific - such a task only works in the guild * specified. Note that a guild doesn't need to be `guildOnly` to be guild-specific. From 0054c78444a779d14038cb493fc7a0a8848da1da Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 14:00:29 +0000 Subject: [PATCH 67/69] style: Update examples, aliases, descriptions and prompts. --- src/commands/moderation/add-trigger.js | 17 ++++++++++++++++- src/commands/moderation/list-triggers.js | 3 ++- src/commands/moderation/remove-trigger.js | 11 ++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/commands/moderation/add-trigger.js b/src/commands/moderation/add-trigger.js index 6f5c059..fb00a21 100644 --- a/src/commands/moderation/add-trigger.js +++ b/src/commands/moderation/add-trigger.js @@ -30,9 +30,24 @@ module.exports = class ModerationAddTriggerCommand extends Command { ], name: 'add-trigger', group: 'moderation', + aliases: ['create-trigger'], + examples: [ + `${inlineCode( + '!add-trigger warn naughty' + )} - Warn via DM if message contains "naughty".`, + `${inlineCode( + '!add-trigger notify VueBot sucks' + )} - Notify moderators if message contains "VueBot sucks".`, + `${inlineCode( + '!add-trigger kick www.obvious-bot-spam-link.com' + )} - Kick user if message contains "www.obvious-bot-spam-link.com"`, + `${inlineCode( + '!add-trigger ban jQuery' + )} - Ban user if message contains "jQuery".'`, + ], guildOnly: true, memberName: 'add', - description: 'Add a trigger word to the moderation system.', + description: 'Add a trigger word/phrase to the moderation system.', }) } diff --git a/src/commands/moderation/list-triggers.js b/src/commands/moderation/list-triggers.js index 94eb1f9..622bf24 100644 --- a/src/commands/moderation/list-triggers.js +++ b/src/commands/moderation/list-triggers.js @@ -12,9 +12,10 @@ module.exports = class ModerationListTriggersCommand extends Command { name: 'list-triggers', group: 'moderation', guildOnly: true, + examples: [`${inlineCode('!list-triggers')} - List moderation triggers.`], aliases: ['triggers'], memberName: 'list', - description: 'List all trigger words from the moderation system.', + description: 'List all trigger words/phrases from the moderation system.', }) } diff --git a/src/commands/moderation/remove-trigger.js b/src/commands/moderation/remove-trigger.js index 84fddde..602759e 100644 --- a/src/commands/moderation/remove-trigger.js +++ b/src/commands/moderation/remove-trigger.js @@ -13,15 +13,20 @@ module.exports = class ModerationRemoveTriggersCommand extends Command { { key: 'trigger', type: 'string', - prompt: 'the trigger word to remove?', + prompt: 'the trigger word/phrase to remove?', }, ], name: 'remove-trigger', group: 'moderation', - aliases: ['del-trigger', 'rem-trigger'], + examples: [ + `${inlineCode( + '!remove-trigger naughty' + )} - Remove the trigger for the word/phrase "naughty".`, + ], + aliases: ['delete-trigger', 'del-trigger', 'rem-trigger', 'rm-trigger'], guildOnly: true, memberName: 'remove', - description: 'Remove a trigger word from the moderation system.', + description: 'Remove a trigger word/phrase from the moderation system.', }) } From 1b525a01ba87a5d842e3c0adfff3c2974eade4bf Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 14:00:44 +0000 Subject: [PATCH 68/69] fix: Re-order checks. --- src/commands/moderation/list-triggers.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commands/moderation/list-triggers.js b/src/commands/moderation/list-triggers.js index 622bf24..69c3986 100644 --- a/src/commands/moderation/list-triggers.js +++ b/src/commands/moderation/list-triggers.js @@ -20,15 +20,15 @@ module.exports = class ModerationListTriggersCommand extends Command { } hasPermission(msg) { - return msg.member.roles.some(role => { - if (MODERATOR_ROLE_IDS.includes(role.id)) { + if (NODE_ENV === 'development') { + if (BOT_DEVELOPER_IDS.includes(msg.member.id)) { return true } + } - if (NODE_ENV === 'development') { - if (BOT_DEVELOPER_IDS.includes(msg.member.id)) { - return true - } + return msg.member.roles.some(role => { + if (MODERATOR_ROLE_IDS.includes(role.id)) { + return true } }) } From acbc5152b794a92f27ec7e9548aec25a4647c74e Mon Sep 17 00:00:00 2001 From: sustained Date: Sun, 12 Jan 2020 14:01:45 +0000 Subject: [PATCH 69/69] other: Update default actions to notify. --- src/services/moderation.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/moderation.js b/src/services/moderation.js index e35e48b..50be0ba 100644 --- a/src/services/moderation.js +++ b/src/services/moderation.js @@ -10,19 +10,19 @@ db.defaults({ triggers: [ { trigger: 'amazingsexdating.com', - action: 'ban', + action: 'notify', }, { trigger: 'viewc.site', - action: 'ban', + action: 'notify', }, { trigger: 'nakedphoto.club', - action: 'ban', + action: 'notify', }, { trigger: 'privatepage.vip', - action: 'ban', + action: 'notify', }, ], }).write()