diff --git a/package.json b/package.json index cf97897c0..38930750f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modbot", - "version": "0.4.0", + "version": "0.5.0", "description": "Discord Bot for the Aternos Discord server", "main": "index.js", "scripts": { diff --git a/src/AutoResponse.js b/src/AutoResponse.js index 6a7fcc99a..2ebdb2ac3 100644 --- a/src/AutoResponse.js +++ b/src/AutoResponse.js @@ -22,19 +22,18 @@ class AutoResponse extends ChatTriggeredFeature { * @return {AutoResponse} the auto response */ constructor(gid, json, id) { - super(id); - this.gid = gid; + super(id, json.trigger); + this.gid = gid; - if (json) { - this.trigger = json.trigger; - this.response = json.response; - this.global = json.global; - this.channels = json.channels; - } + if (json) { + this.response = json.response; + this.global = json.global; + this.channels = json.channels; + } - if (!this.channels) { - this.channels = []; - } + if (!this.channels) { + this.channels = []; + } } /** @@ -42,7 +41,7 @@ class AutoResponse extends ChatTriggeredFeature { * @returns {(*|string)[]} */ serialize() { - return [this.gid, JSON.stringify(this.trigger), this.response, this.global, this.channels.join(',')]; + return [this.gid, JSON.stringify(this.trigger), this.response, this.global, this.channels.join(',')]; } /** @@ -52,15 +51,15 @@ class AutoResponse extends ChatTriggeredFeature { * @returns {module:"discord.js".MessageEmbed} */ embed(title, color) { - return new Discord.MessageEmbed() - .setTitle(title + ` [${this.id}]`) - .setColor(color) - .addFields( - /** @type {any} */[ - {name: "Trigger", value: `${this.trigger.type}: \`${this.trigger.type === 'regex' ? '/' + this.trigger.content + '/' + this.trigger.flags : this.trigger.content}\``.substring(0, 1000)}, - {name: "Response", value: this.response.substring(0,1000)}, - {name: "Channels", value: this.global ? "global" : this.channels.map(c => `<#${c}>`).join(', ').substring(0, 1000)} - ]); + return new Discord.MessageEmbed() + .setTitle(title + ` [${this.id}]`) + .setColor(color) + .addFields( + /** @type {any} */[ + {name: 'Trigger', value: `${this.trigger.type}: \`${this.trigger.type === 'regex' ? '/' + this.trigger.content + '/' + this.trigger.flags : this.trigger.content}\``.substring(0, 1000)}, + {name: 'Response', value: this.response.substring(0,1000)}, + {name: 'Channels', value: this.global ? 'global' : this.channels.map(c => `<#${c}>`).join(', ').substring(0, 1000)} + ]); } } diff --git a/src/BadWord.js b/src/BadWord.js index 5d7e78dc8..174dbc270 100644 --- a/src/BadWord.js +++ b/src/BadWord.js @@ -1,84 +1,194 @@ const ChatTriggeredFeature = require('./ChatTriggeredFeature'); const Discord = require('discord.js'); +const util = require('./util'); /** * Class representing a bad word */ class BadWord extends ChatTriggeredFeature { - static punishmentTypes = ['none','ban','kick','mute','softban','strike','dm']; - - static defaultResponse = 'Your message includes words/phrases that are not allowed here!'; - - static tableName = 'badWords'; - - static columns = ['guildid', 'trigger', 'punishment', 'response', 'global', 'channels']; - - /** - * constructor - create a bad word - * @param {module:"discord.js".Snowflake} gid guild ID - * @param {Object} json options - * @param {Trigger} json.trigger filter that triggers the bad word - * @param {String|Punishment} json.punishment punishment for the members which trigger this - * @param {String} [json.response] a message that is send by this filter. It's automatically deleted after 5 seconds - * @param {Boolean} json.global does this apply to all channels in this guild - * @param {module:"discord.js".Snowflake[]} [json.channels] channels that this applies to - * @param {Number} [id] id in DB - * @return {BadWord} - */ - constructor(gid, json, id) { - super(id); - this.gid = gid; - - if (json) { - this.trigger = json.trigger; - this.punishment = typeof(json.punishment) === 'string' ? JSON.parse(json.punishment) : json.punishment; - this.response = json.response; - if (this.punishment && this.punishment.action === 'dm' && this.response && this.response !== 'disabled') { - if (this.response === 'default') { - this.punishment.message = BadWord.defaultResponse; - } else { - this.punishment.message = this.response; + static punishmentTypes = ['none', 'ban', 'kick', 'mute', 'softban', 'strike', 'dm']; + + static defaultResponse = 'Your message includes words/phrases that are not allowed here!'; + + static tableName = 'badWords'; + + static columns = ['guildid', 'trigger', 'punishment', 'response', 'global', 'channels', 'priority']; + + /** + * constructor - create a bad word + * @param {module:"discord.js".Snowflake} gid guild ID + * @param {Object} json options + * @param {Trigger} json.trigger filter that triggers the bad word + * @param {String|Punishment} json.punishment punishment for the members which trigger this + * @param {String} [json.response] a message that is send by this filter. It's automatically deleted after 5 seconds + * @param {Boolean} json.global does this apply to all channels in this guild + * @param {module:"discord.js".Snowflake[]} [json.channels] channels that this applies to + * @param {Number} [json.priority] badword priority (higher -> more important) + * @param {Number} [id] id in DB + * @return {BadWord} + */ + constructor(gid, json, id) { + super(id, json.trigger); + this.gid = gid; + + if (json) { + this.punishment = typeof (json.punishment) === 'string' ? JSON.parse(json.punishment) : json.punishment; + this.response = json.response; + if (this.punishment && this.punishment.action === 'dm' && this.response && this.response !== 'disabled') { + if (this.response === 'default') { + this.punishment.message = BadWord.defaultResponse; + } else { + this.punishment.message = this.response; + } + } + this.global = json.global; + this.channels = json.channels; + this.priority = json.priority || 0; } - } - this.global = json.global; - this.channels = json.channels; + + if (!this.channels) { + this.channels = []; + } + } + + /** + * serialize the bad word + * @returns {(*|string)[]} + */ + serialize() { + return [this.gid, JSON.stringify(this.trigger), JSON.stringify(this.punishment), this.response, this.global, this.channels.join(','), this.priority]; } - if (!this.channels) { - this.channels = []; + /** + * generate an Embed displaying the info of this bad word + * @param {String} title + * @param {Number} color + * @returns {module:"discord.js".MessageEmbed} + */ + embed(title, color) { + const duration = this.punishment.duration, trigger = this.trigger; + return new Discord.MessageEmbed() + .setTitle(title + ` [${this.id}]`) + .setColor(color) + .addFields( + /** @type {any} */[ + { + name: 'Trigger', + value: `${trigger.type}: \`${trigger.type === 'regex' ? '/' + trigger.content + '/' + trigger.flags : trigger.content}\``.substring(0, 1000), + inline: true + }, + { + name: 'Response', + value: this.response === 'default' ? BadWord.defaultResponse : this.response.substring(0, 1000), + inline: true + }, + { + name: 'Channels', + value: this.global ? 'global' : this.channels.map(c => `<#${c}>`).join(', ').substring(0, 1000), + inline: true + }, + { + name: 'Punishment', + value: `${this.punishment.action} ${duration ? `for ${duration}` : ''}`, + inline: true + }, + { + name: 'Priority', + value: this.priority, + inline: true + }, + ]); } - } - - /** - * serialize the bad word - * @returns {(*|string)[]} - */ - serialize() { - return [this.gid, JSON.stringify(this.trigger), JSON.stringify(this.punishment), this.response, this.global, this.channels.join(',')]; - } - - /** - * generate an Embed displaying the info of this bad word - * @param {String} title - * @param {Number} color - * @returns {module:"discord.js".MessageEmbed} - */ - embed(title, color) { - const embed = new Discord.MessageEmbed() - .setTitle(title + ` [${this.id}]`) - .setColor(color) - .addFields( - /** @type {any} */[ - {name: "Trigger", value: `${this.trigger.type}: \`${this.trigger.type === 'regex' ? '/' + this.trigger.content + '/' + this.trigger.flags : this.trigger.content}\``.substring(0, 1000)}, - {name: "Response", value: this.response === 'default' ? BadWord.defaultResponse :this.response.substring(0,1000)}, - {name: "Channels", value: this.global ? "global" : this.channels.map(c => `<#${c}>`).join(', ').substring(0, 1000)} - ]); - if (this.punishment.action) { - embed.addField("Punishment", `${this.punishment.action} ${this.punishment.duration ? `for ${this.punishment.duration}` : ''}`) + + /** + * create a new bad word + * @param {Snowflake} guildID + * @param {boolean} global + * @param {Snowflake[]|null} channels + * @param {String} triggerType + * @param {String} triggerContent + * @returns {Promise<{success:boolean, badWord: BadWord, message: String}>} + */ + static async new(guildID, global, channels, triggerType, triggerContent) { + + let trigger = this.getTrigger(triggerType, triggerContent); + if (!trigger.success) return trigger; + + const badWord = new BadWord(guildID, { + trigger: trigger.trigger, + punishment: {action: 'none'}, + global, + channels, + response: 'disabled' + }); + await badWord.save(); + return {success: true, badWord}; + } + + /** + * edit this badword + * @param {String} option option to change + * @param {String[]} args + * @param {module:"discord.js".Guild} guild + * @returns {Promise} response message + */ + async edit(option, args, guild) { + switch (option) { + case 'trigger': { + let trigger = this.constructor.getTrigger(args.shift(), args.join(' ')); + if (!trigger.success) return trigger.message; + this.trigger = trigger.trigger; + await this.save(); + return 'Successfully changed trigger'; + } + + case 'response': { + let response = args.join(' '); + if (!response) response = 'disabled'; + + this.response = response; + await this.save(); + return `Successfully ${response === 'disabled' ? 'disabled' : 'changed'} response`; + } + + case 'punishment': { + let action = args.shift().toLowerCase(), + duration = args.join(' '); + if (!this.constructor.punishmentTypes.includes(action)) return 'Unknown punishment'; + this.punishment = {action, duration}; + await this.save(); + return `Successfully ${action === 'none' ? 'disabled' : 'changed'} punishment`; + } + + case 'priority': { + let priority = parseInt(args.shift()); + if (Number.isNaN(priority)) return 'Invalid priority'; + this.priority = priority; + await this.save(); + return `Successfully changed priority to ${priority}`; + } + + case 'channels': { + if (args[0].toLowerCase() === 'global') { + this.global = true; + this.channels = []; + } + else { + let channels = util.channelMentions(guild, args); + if (!channels) return 'No valid channels specified'; + this.global = false; + this.channels = channels; + } + await this.save(); + return global ? 'Successfully made this badword global' : 'Successfully changed channels'; + } + + default: { + return 'Unknown option'; + } + } } - return embed; - } } module.exports = BadWord; diff --git a/src/ChatTriggeredFeature.js b/src/ChatTriggeredFeature.js index c7f166367..91d85b573 100644 --- a/src/ChatTriggeredFeature.js +++ b/src/ChatTriggeredFeature.js @@ -1,4 +1,5 @@ const Discord = require('discord.js'); +const Trigger = require('./Trigger'); /** * Database @@ -38,11 +39,21 @@ class ChatTriggeredFeature { */ static columns; + /** + * @type {Object} + * @property {String} type + * @property {String} content + * @property {String} [flags] + */ + trigger; + /** * @param {Number} id ID in the database + * @param {Trigger} trigger */ - constructor(id) { + constructor(id, trigger) { this.id = id; + this.trigger = new Trigger(trigger); } /** @@ -89,53 +100,72 @@ class ChatTriggeredFeature { */ matches(message) { switch (this.trigger.type) { - case "include": + case 'include': if (message.content.toLowerCase().includes(this.trigger.content.toLowerCase())) { return true; } break; - case "match": + case 'match': if (message.content.toLowerCase() === this.trigger.content.toLowerCase()) { return true; } break; - case "regex": - let regex = new RegExp(this.trigger.content,this.trigger.flags); + case 'regex': { + let regex = new RegExp(this.trigger.content, this.trigger.flags); if (regex.test(message.content)) { return true; } break; + } } return false; } + /** + * serialize this object + * must return data in same order as the static columns array + * @returns {(*|string)[]} + */ + serialize() { + throw 'Abstract method not overridden!'; + } + /** * Save to db and cache * @async * @return {Promise} id in db */ async save() { - if (!this.channels) this.channels = null; - - let dbentry = await database.queryAll(`INSERT INTO ${database.escapeId(this.constructor.tableName)} (${database.escapeIdArray(this.constructor.columns).join(', ')}) VALUES (${',?'.repeat(this.constructor.columns.length).slice(1)})`,this.serialize()); - - this.id = dbentry.insertId; + if (this.id) { + let assignments = [], + columns = this.constructor.columns, + data = this.serialize(); + for (let i = 0; i < columns.length; i++) { + assignments.push(`${database.escapeId(columns[i])}=${database.escapeValue(data[i])}`); + } + if (data.length !== columns.length) throw 'Unable to update, lengths differ!'; + await database.queryAll(`UPDATE ${database.escapeId(this.constructor.tableName)} SET ${assignments.join(', ')} WHERE id = ?`, [this.id]); + } + else { + let dbentry = await database.queryAll(`INSERT INTO ${database.escapeId(this.constructor.tableName)} (${database.escapeIdArray(this.constructor.columns).join(', ')}) VALUES (${',?'.repeat(this.constructor.columns.length).slice(1)})`,this.serialize()); + this.id = dbentry.insertId; + } if (this.global) { - if (!this.constructor.getGuildCache().has(this.gid)) this.constructor.getGuildCache().set(this.gid, new Discord.Collection()) + if (!this.constructor.getGuildCache().has(this.gid)) return this.id; this.constructor.getGuildCache().get(this.gid).set(this.id, this); } else { for (const channel of this.channels) { - if(!this.constructor.getChannelCache().has(channel)) this.constructor.getChannelCache().set(channel, new Discord.Collection()); + if(!this.constructor.getChannelCache().has(channel)) continue; this.constructor.getChannelCache().get(channel).set(this.id, this); } } - return dbentry.insertId; + return this.id; } /** @@ -160,6 +190,59 @@ class ChatTriggeredFeature { } } + /** + * create this object from data retrieved from the database + * @param data + * @returns {Promise} + */ + static fromData(data) { + return new this(data.guildid, { + trigger: JSON.parse(data.trigger), + punishment: data.punishment, + response: data.response, + global: data.global === 1, + channels: data.channels.split(','), + priority: data.priority + }, data.id); + } + + /** + * Get a single bad word / autoresponse + * @param {String|Number} id + * @returns {Promise} + */ + static async getByID(id) { + const result = await database.query(`SELECT * FROM ${database.escapeId(this.tableName)} WHERE id = ?`, [id]); + if (!result) return null; + return this.fromData(result); + } + + /** + * get a trigger + * @param {String} type trigger type + * @param {String} value trigger value + * @returns {{trigger: Trigger, success: boolean, message: string}} + */ + static getTrigger(type, value) { + if (!this.triggerTypes.includes(type)) return {success: false, message: 'Unknown trigger type'}; + if (!value) return {success: false, message:'Empty triggers are not allowed'}; + + let content = value, flags; + if (type === 'regex') { + /** @type {String[]}*/ + let parts = value.split(/(? a.id - b.id); @@ -213,14 +290,7 @@ class ChatTriggeredFeature { const newItems = new Discord.Collection(); for (const res of result) { - const o = new this(res.guildid, { - trigger: JSON.parse(res.trigger), - punishment: res.punishment, - response: res.response, - global: true, - channels: [] - }, res.id); - newItems.set(res.id, o); + newItems.set(res.id, this.fromData(res)); } this.getGuildCache().set(guildId, newItems); setTimeout(() => { @@ -238,13 +308,7 @@ class ChatTriggeredFeature { const newItems = new Discord.Collection(); for (const res of result) { - newItems.set(res.id, new this(res.guildid, { - trigger: JSON.parse(res.trigger), - response: res.response, - punishment: res.punishment, - global: false, - channels: res.channels.split(',') - }, res.id)); + newItems.set(res.id, this.fromData(res)); } this.getChannelCache().set(channelId, newItems); setTimeout(() => { diff --git a/src/Command.js b/src/Command.js index 7079b69eb..e70c25191 100644 --- a/src/Command.js +++ b/src/Command.js @@ -205,7 +205,7 @@ class Command { /** * generate a multi page response - * @param {function} generatePage generate a new page (index, ..args) + * @param {function} generatePage generate a new page (index) * @param {Number} [pages] number of possible pages * @param {Number} [duration] inactivity timeout in ms (default: 60s) */ diff --git a/src/Database.js b/src/Database.js index bdcd64760..8e43174c5 100644 --- a/src/Database.js +++ b/src/Database.js @@ -59,7 +59,7 @@ class Database { * @private */ _handleConnectionError(err) { - monitor.error('A fatal database error occurred', err) + monitor.error('A fatal database error occurred', err); if (err.code === 'ER_ACCESS_DENIED_ERROR') { console.error('Access to database denied. Make sure your config and database are set up correctly!'); process.exit(1); @@ -79,11 +79,11 @@ class Database { * @return {Promise} */ async createTables() { - await this.query("CREATE TABLE IF NOT EXISTS `channels` (`id` VARCHAR(20) NOT NULL, `config` TEXT NOT NULL, PRIMARY KEY (`id`), `guildid` VARCHAR(20))"); - await this.query("CREATE TABLE IF NOT EXISTS `guilds` (`id` VARCHAR(20) NOT NULL, `config` TEXT NOT NULL, PRIMARY KEY (`id`))"); - await this.query("CREATE TABLE IF NOT EXISTS `responses` (`id` int PRIMARY KEY AUTO_INCREMENT, `guildid` VARCHAR(20) NOT NULL, `trigger` TEXT NOT NULL, `response` TEXT NOT NULL, `global` BOOLEAN NOT NULL, `channels` TEXT NULL DEFAULT NULL)"); - await this.query("CREATE TABLE IF NOT EXISTS `badWords` (`id` int PRIMARY KEY AUTO_INCREMENT, `guildid` VARCHAR(20) NOT NULL, `trigger` TEXT NOT NULL, `punishment` TEXT NOT NULL, `response` TEXT NOT NULL, `global` BOOLEAN NOT NULL, `channels` TEXT NULL DEFAULT NULL)"); - await this.query("CREATE TABLE IF NOT EXISTS `moderations` (`id` int PRIMARY KEY AUTO_INCREMENT, `guildid` VARCHAR(20) NOT NULL, `userid` VARCHAR(20) NOT NULL, `action` VARCHAR(10) NOT NULL,`created` bigint NOT NULL, `value` int DEFAULT 0,`expireTime` bigint NULL DEFAULT NULL, `reason` TEXT,`moderator` VARCHAR(20) NULL DEFAULT NULL, `active` BOOLEAN DEFAULT TRUE)"); + await this.query('CREATE TABLE IF NOT EXISTS `channels` (`id` VARCHAR(20) NOT NULL, `config` TEXT NOT NULL, PRIMARY KEY (`id`), `guildid` VARCHAR(20))'); + await this.query('CREATE TABLE IF NOT EXISTS `guilds` (`id` VARCHAR(20) NOT NULL, `config` TEXT NOT NULL, PRIMARY KEY (`id`))'); + await this.query('CREATE TABLE IF NOT EXISTS `responses` (`id` int PRIMARY KEY AUTO_INCREMENT, `guildid` VARCHAR(20) NOT NULL, `trigger` TEXT NOT NULL, `response` TEXT NOT NULL, `global` BOOLEAN NOT NULL, `channels` TEXT NULL DEFAULT NULL)'); + await this.query('CREATE TABLE IF NOT EXISTS `badWords` (`id` int PRIMARY KEY AUTO_INCREMENT, `guildid` VARCHAR(20) NOT NULL, `trigger` TEXT NOT NULL, `punishment` TEXT NOT NULL, `response` TEXT NOT NULL, `global` BOOLEAN NOT NULL, `channels` TEXT NULL DEFAULT NULL, `priority` int NULL)'); + await this.query('CREATE TABLE IF NOT EXISTS `moderations` (`id` int PRIMARY KEY AUTO_INCREMENT, `guildid` VARCHAR(20) NOT NULL, `userid` VARCHAR(20) NOT NULL, `action` VARCHAR(10) NOT NULL,`created` bigint NOT NULL, `value` int DEFAULT 0,`expireTime` bigint NULL DEFAULT NULL, `reason` TEXT,`moderator` VARCHAR(20) NULL DEFAULT NULL, `active` BOOLEAN DEFAULT TRUE)'); } /** @@ -132,6 +132,15 @@ class Database { return mysql.escapeId(...args); } + /** + * escape a value + * @param args + * @returns {string} + */ + escapeValue(...args) { + return mysql.escape(...args); + } + /** * Escape an array of table/column names * @@ -159,11 +168,11 @@ class Database { */ async addModeration(guildId, userId, action, reason, duration, moderatorId) { //disable old moderations - await this.query("UPDATE moderations SET active = FALSE WHERE active = TRUE AND guildid = ? AND userid = ? AND action = ?", [guildId, userId, action]); + await this.query('UPDATE moderations SET active = FALSE WHERE active = TRUE AND guildid = ? AND userid = ? AND action = ?', [guildId, userId, action]); const now = Math.floor(Date.now()/1000); /** @property {Number} insertId*/ - const insert = await this.queryAll("INSERT INTO moderations (guildid, userid, action, created, expireTime, reason, moderator) VALUES (?,?,?,?,?,?,?)",[guildId, userId, action, now, duration ? now + duration : null, reason, moderatorId]); + const insert = await this.queryAll('INSERT INTO moderations (guildid, userid, action, created, expireTime, reason, moderator) VALUES (?,?,?,?,?,?,?)',[guildId, userId, action, now, duration ? now + duration : null, reason, moderatorId]); return insert.insertId; } } diff --git a/src/Trigger.js b/src/Trigger.js new file mode 100644 index 000000000..cd512b36b --- /dev/null +++ b/src/Trigger.js @@ -0,0 +1,37 @@ +class Trigger{ + /** + * @type {String} + */ + type; + + /** + * @type {String} + */ + content; + + /** + * @type {String} + */ + flags; + + /** + * @param {Object} data + * @property {String} type + * @property {String} content + * @property {String} [flags] + */ + constructor(data) { + this.type = data.type; + this.content = data.content; + this.flags = data.flags; + } + + /** + * @returns {string} + */ + asString() { + return `(${this.type}): ${this.type === 'regex' ? `/${this.content}/` : this.content}`; + } +} + +module.exports = Trigger; \ No newline at end of file diff --git a/src/Typedefs.js b/src/Typedefs.js index cef1d18db..1aece16f4 100644 --- a/src/Typedefs.js +++ b/src/Typedefs.js @@ -21,20 +21,6 @@ * @property {Object} tempbans */ -/** - * A trigger for an AutoResponse this can be: - * * a string that has to be included - * * a string that has to match the message - * * a regex - * @typedef {Object} Trigger - * @property {String} type the type of the trigger possible types: - * * regex - * * include - * * match - * @property {String} content the string or regex - * @property {String} [flags] flags for regex's - */ - /** * Data that resolves to give a Guild object. This can be: * * A Message object diff --git a/src/commands/legacy/badword.js b/src/commands/legacy/badword.js deleted file mode 100644 index 5c4222ffc..000000000 --- a/src/commands/legacy/badword.js +++ /dev/null @@ -1,51 +0,0 @@ -const command = {}; -const BadWord = require('../../BadWord'); -const util = require('../../util'); - -const list = require('./badword/list'); -const add = require('./badword/add'); -const remove = require('./badword/remove'); -const info = require('./badword/info'); - -command.description = 'Adds, removes and lists bad words'; - -command.usage = ' '; - -command.names = ['badword','badwords','blacklist']; - -command.execute = async (message, args, database, bot) => { - //Permission check - if (!message.member.hasPermission('MANAGE_GUILD')) { - await message.channel.send('You need the "Manage Server" permission to use this command.'); - return; - } - if (!args.length) { - return await message.channel.send(await util.usage(message,command.names[0])); - } - - let responses = await BadWord.getAll(message.guild.id); - - switch (args.shift().toLowerCase()) { - case 'list': - await list(responses,message); - break; - - case 'add': - await add(message); - break; - - case 'delete': - case 'remove': - await remove(responses, message, args); - break; - - case 'info': - await info(responses, message, args); - break; - - default: - return await message.channel.send(await util.usage(message,command.names[0])); - } -} - -module.exports = command; diff --git a/src/commands/legacy/badword/add.js b/src/commands/legacy/badword/add.js deleted file mode 100644 index 82f31300a..000000000 --- a/src/commands/legacy/badword/add.js +++ /dev/null @@ -1,101 +0,0 @@ -const BadWord = require('../../../BadWord'); -const util = require('../../../util.js'); - -/** - * add a bad word - * @param {module:"discord.js".Message} message - * @returns {Promise} - */ -module.exports = async (message) => { - await message.channel.send("Please enter your trigger type (\`regex\`, \`include\` or \`match\`)!"); - let type = await util.getResponse(message.channel,message.author.id); - - if (type === null) return; - - type = type.toLowerCase(); - - if (!BadWord.triggerTypes.includes(type)) { - return await message.channel.send("Not a valid trigger type!"); - } - - await message.channel.send(`Please enter your trigger (${ type === 'regex' ? '`/regex/flags`' :'`example trigger`'})!`); - let content = await util.getResponse(message.channel,message.author.id); - - if (content === null) return; - - content = content.replace(/^`(.*)`$/,(a,b) => b); - - let flags; - if (type === 'regex') { - let regex = content.split(/(?} - */ -module.exports = async (badWords, message, args) => { - if (!args.length) { - await message.channel.send("Provide the id of the bad word you want to view"); - return; - } - let badWord = badWords.get(parseInt(args.shift())); - if (!badWord) { - await message.channel.send("Invalid id!"); - return; - } - - await message.channel.send(badWord.embed("Bad word",util.color.green)); -}; diff --git a/src/commands/legacy/badword/list.js b/src/commands/legacy/badword/list.js deleted file mode 100644 index 0dfbf68d4..000000000 --- a/src/commands/legacy/badword/list.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * list bad words - * @param {module:"discord.js".Collection} badWords - * @param {module:"discord.js".Message} message - * @returns {Promise} - */ -module.exports = async (badWords, message) => { - if (!badWords.size) { - return await message.channel.send("No bad words!"); - } - - let text = ''; - for (const [id, badWord] of badWords) { - if(text.length > 1500){ - await message.channel.send(text.substring(0, 2000)); - text = ''; - } - text += `[${id}] ${badWord.global ? "global" : badWord.channels.map(c => `<#${c}>`).join(', ')} (${badWord.trigger.type}): \`${badWord.trigger.type === 'regex' ? '/' + badWord.trigger.content + '/' + badWord.trigger.flags : badWord.trigger.content}\` \n`; - } - await message.channel.send(text.substring(0, 2000)); -}; diff --git a/src/commands/legacy/badword/remove.js b/src/commands/legacy/badword/remove.js deleted file mode 100644 index 84f685ec6..000000000 --- a/src/commands/legacy/badword/remove.js +++ /dev/null @@ -1,45 +0,0 @@ -const util = require('../../../util.js'); -const icons = require('../../../icons'); - -/** - * remove a bad word - * @param {module:"discord.js".Collection} badWords - * @param {module:"discord.js".Message} message - * @param {String[]} args - * @returns {Promise} - */ -module.exports = async (badWords, message, args) => { - if (!args.length) { - await message.channel.send("Provide the id of the bad word you want to remove"); - return; - } - let badWord = badWords.get(parseInt(args.shift())); - if (!badWord) { - await message.channel.send("Invalid id!"); - return; - } - - let confirmation = await message.channel.send("Do you really want to delete this bad word?", badWord.embed("Remove bad word", util.color.red)); - { - let yes = confirmation.react(icons.yes); - let no = confirmation.react(icons.no); - await Promise.all([yes,no]); - } - - let confirmed; - try { - confirmed = (await confirmation.awaitReactions((reaction, user) => { - return user.id === message.author.id && (reaction.emoji.name === icons.yes || reaction.emoji.name === icons.no); - }, { max: 1, time: 15000, errors: ['time'] })).first().emoji.name === icons.yes; - } - catch { - return await message.channel.send("You took to long to react!"); - } - if (!confirmed) { - return await message.channel.send("Canceled!"); - } - - await badWord.remove(); - - await message.channel.send(`Removed the bad word with the id ${badWord.id}!`); -}; diff --git a/src/commands/settings/BadWordCommand.js b/src/commands/settings/BadWordCommand.js new file mode 100644 index 000000000..d6cbbea82 --- /dev/null +++ b/src/commands/settings/BadWordCommand.js @@ -0,0 +1,161 @@ +const Command = require('../../Command'); +const Discord = require('discord.js'); +const util = require('../../util'); +const BadWord = require('../../BadWord'); + +class BadWordCommand extends Command { + + static description = 'Configure bad words'; + + static usage = 'list|add|remove|show|edit'; + + static subCommands = { + list: { + usage:'', + description: 'List all bad words' + }, + add: { + usage: 'global| regex|include|match ', + description: 'Add a bad word' + }, + remove: { + usage: '', + description: 'Remove a bad word' + }, + show: { + usage: '', + description: 'Display a bad word' + }, + edit: { + usage: ' trigger|response|punishment|priority|channels ', + description: 'change options of a bad word' + } + } + + static names = ['badword','badwords','bw']; + + static userPerms = ['MANAGE_GUILD']; + + async execute() { + if (this.args.length === 0) { + await this.sendUsage(); + return; + } + + /** @type {module:"discord.js".Collection} */ + const badWords = await BadWord.getAll(/** @type {Snowflake} */ this.message.guild.id); + + switch (this.args.shift().toLowerCase()) { + case 'list': { + if (!badWords.size) return this.message.channel.send('No bad words!'); + + let text = ''; + for (const [id, badWord] of badWords) { + const info = `[${id}] ${badWord.global ? 'global' : badWord.channels.map(c => `\`${c}\``).join(', ')} ` + + badWord.trigger.asString() + '\n'; + + if (text.length + info.length < 2000) { + text += info; + } else { + await this.message.channel.send(text); + text = info; + } + } + if (text.length) await this.message.channel.send(text); + break; + } + + case 'add': { + if (this.args.length < 2) return this.sendSubCommandUsage('add'); + + const global = this.args[0].toLowerCase() === 'global'; + + let channels; + if (!global) channels = await util.channelMentions(this.message.guild, this.args); + else this.args.shift(); + + const type = this.args.shift().toLowerCase(); + const content = this.args.join(' '); + + let badWord = await BadWord.new(this.message.guild.id, global, channels, type, content); + if (!badWord.success) return this.message.channel.send(badWord.message); + + await this.message.channel.send(badWord.badWord.embed('Added new bad word', util.color.green)); + break; + } + + case 'remove': { + const badWord = await this.getBadWord(this.args.shift(), 'remove'); + if (!badWord) return; + await badWord.remove(); + await this.message.channel.send(badWord.embed(`Removed bad word ${badWord.id}`, util.color.red)); + break; + } + + case 'show': { + const badWord = await this.getBadWord(this.args.shift(), 'remove'); + if (!badWord) return; + await this.message.channel.send(badWord.embed(`Bad Word ${badWord.id}`, util.color.green)); + break; + } + + case 'edit': { + if (this.args.length < 3) return this.sendSubCommandUsage('edit'); + const badWord = await this.getBadWord(this.args.shift(), 'remove'); + if (!badWord) return; + + await this.message.channel.send(badWord.embed(await badWord.edit(this.args.shift(), this.args, this.message.guild), util.color.green)); + break; + } + + default: + await this.sendUsage(); + } + + } + + /** + * get a single bad word + * @param {String|Number} id + * @param {String} subCommand + * @returns {Promise} + */ + async getBadWord(id, subCommand) { + if (!id || !parseInt(id)) { + await this.sendSubCommandUsage(subCommand); + return null; + } + const result = await BadWord.getByID(id); + if (!result) { + await this.sendSubCommandUsage(subCommand); + return null; + } + return result; + } + + /** + * send usage for a subcommand + * @param {String} commandName sub command name + * @returns {Promise} + */ + async sendSubCommandUsage(commandName) { + commandName = commandName.toLowerCase(); + let subCommand = this.constructor.subCommands[commandName]; + if (!subCommand) throw 'Unknown subcommand'; + + return this.message.channel.send(new Discord.MessageEmbed() + .setAuthor(`Help for ${this.name} ${commandName} | Prefix: ${this.prefix}`) + .setFooter(`Command executed by ${util.escapeFormatting(this.message.author.tag)}`) + .addFields( + /** @type {any} */ + { name: 'Usage', value: `\`${this.prefix}${this.name} ${commandName} ${subCommand.usage}\``, inline: true}, + /** @type {any} */ { name: 'Description', value: subCommand.description, inline: true}, + /** @type {any} */ { name: 'Required Permissions', value: this.constructor.userPerms.map(p => `\`${p}\``).join(',') || 'none' } + ) + .setColor(util.color.green) + .setTimestamp() + ); + } +} + +module.exports = BadWordCommand; diff --git a/src/features/message/badwordmod.js b/src/features/message/badwordmod.js index 3ed376e9d..d38afcbdc 100644 --- a/src/features/message/badwordmod.js +++ b/src/features/message/badwordmod.js @@ -4,22 +4,22 @@ const Log = require('../../Log'); const strike = require('../../commands/legacy/strike'); exports.event = async (options, message) => { - if (!message.guild || await util.ignoresAutomod(message)) return; + if (!message.guild || await util.ignoresAutomod(message)) return; - const words = await BadWord.get(message.channel.id, message.guild.id); - for (let [,word] of words) { - if (word.matches(message)) { - const reason = 'Using forbidden words or phrases'; - await util.delete(message, { reason: reason } ); - if (word.response !== 'disabled') { - const response = await message.reply(word.response === 'default' ? BadWord.defaultResponse : word.response); - await util.delete(response, { timeout: 5000 }); - } - await Log.logMessageDeletion(message, reason); - if (word.punishment.action !== 'none') { - await strike.executePunishment(word.punishment, message.guild, message.author, options.bot, options.database, reason); - } - return; + const words = (await BadWord.get(message.channel.id, message.guild.id)).sort((a,b) => b.priority - a.priority); + for (let [,word] of words) { + if (word.matches(message)) { + const reason = 'Using forbidden words or phrases'; + await util.delete(message, { reason: reason } ); + if (word.response !== 'disabled') { + const response = await message.reply(word.response === 'default' ? BadWord.defaultResponse : word.response); + await util.delete(response, { timeout: 5000 }); + } + await Log.logMessageDeletion(message, reason); + if (word.punishment.action !== 'none') { + await strike.executePunishment(word.punishment, message.guild, message.author, options.bot, options.database, reason); + } + return; + } } - } }; diff --git a/update/0.5.0.js b/update/0.5.0.js new file mode 100644 index 000000000..ebe4159c8 --- /dev/null +++ b/update/0.5.0.js @@ -0,0 +1,18 @@ +const Database = require('../src/Database'); +const config = require('../config.json'); +const database = new Database(config.db); + +async function update() { + console.log('Starting update to v0.5.0'); + + console.log('Updating tables'); + await database.waitForConnection(); + await database.query('ALTER TABLE badWords ADD `priority` int NULL;'); + console.log('Done!'); + process.exit(0); +} + +update().catch(e => { + console.error(e); + process.exit(1); +});