diff --git a/structure/client/components/settings/moderation/WordFilter.js b/structure/client/components/settings/moderation/WordFilter.js index be9b67a..847fa21 100644 --- a/structure/client/components/settings/moderation/WordFilter.js +++ b/structure/client/components/settings/moderation/WordFilter.js @@ -1,6 +1,5 @@ -const { lang } = require('moment'); const { Setting } = require('../../../../interfaces/'); -const validInfractions = ['WARN', 'MUTE', 'KICK', 'BAN', 'SOFTBAN'] +const validInfractions = ['WARN', 'MUTE', 'KICK', 'BAN', 'SOFTBAN']; module.exports = class WordFilter extends Setting { @@ -22,7 +21,7 @@ module.exports = class WordFilter extends Setting { 'wordfilter bypass add role "rolename with several words"', 'wordfilter ignore set #channel channel-name', 'wordfilter silent on', - 'wordfilter actions word mute 5 min 1 point force' // time reg: (\d{1,3}\s?[a-z]{1,7} ?){1,2} | points reg: \d{1,3}\s?(points?|pts?|p) -- parse points first + 'wordfilter actions ' // time reg: (\d{1,3}\s?[a-z]{1,7} ?){1,2} | points reg: \d{1,3}\s?(points?|pts?|p) -- parse points first ], default: { wordFilter: { @@ -32,15 +31,16 @@ module.exports = class WordFilter extends Setting { fuzzy: [], tokenized: [], whitelist: [], - actions: { // Link certain words to actions with or without points ex fuck: { action: 'MUTE', points: null, force: false, duration: 300 } - // fuck: { + actions: [ // Link certain words to actions with or without points ex fuck: { action: 'MUTE', points: null, force: false, duration: 300 } + // { // action: 'MUTE', // points: 10, // force: true, // duration: 300, - // prune: true + // prune: true, + // trigger: 'generic' || 'fuzzy' || 'explicit' || 'tokenized' || [words] // } - }, + ], ignore: [], bypass: [] } @@ -58,7 +58,8 @@ module.exports = class WordFilter extends Setting { const { guild } = message; const setting = guild._settings[this.index]; - let index = null, langParams = {}; + let index = null, + langParams = {}; const bool = resolver.resolveBoolean(method); if(bool === null && !args.length) return { @@ -66,10 +67,6 @@ module.exports = class WordFilter extends Setting { msg: message.format('MISSING_ARGS') }; - console.log(message.content) - console.log(message._clean) - console.log(args) - if (bool || bool === false) { index = 'S_WORDFILTER_TOGGLE'; @@ -90,7 +87,7 @@ module.exports = class WordFilter extends Setting { } else if (['ignore', 'channelignore', 'ignorechannel'].includes(method)) { - const resolved = await resolver.resolveMethod(args, null, setting.ignore, resolver.resolveChannels.bind(resolver), guild, false, ['add','remove','set','reset']); + const resolved = await resolver.resolveMethod(args, null, setting.ignore, resolver.resolveChannels.bind(resolver), guild, false, ['add', 'remove', 'set', 'reset']); if (!resolved) return { error: true, msg: message.format('ERR_INVALID_METHOD', { method: args[0] }) @@ -101,7 +98,7 @@ module.exports = class WordFilter extends Setting { } else if (['bypass', 'roleignore', 'ignorerole'].includes(method)) { - const resolved = await resolver.resolveMethod(args, null, setting.bypass, resolver.resolveRoles.bind(resolver), guild, false, ['add','remove','set','reset']); + const resolved = await resolver.resolveMethod(args, null, setting.bypass, resolver.resolveRoles.bind(resolver), guild, false, ['add', 'remove', 'set', 'reset']); if (!resolved) return { error: true, msg: message.format('ERR_INVALID_METHOD', { method: args[0] }) @@ -124,48 +121,80 @@ module.exports = class WordFilter extends Setting { } else if (['action', 'actions'].includes(method)) { - if(args.length < 2) return { - error: true, - msg: message.format('S_WORDFILTER_ACTION_ARGS') - } - const word = args.shift().toLowerCase(); + const submethod = args.shift().toLowerCase(); + const resolved = await resolver.resolveMethod([submethod], null, null, null, null, false, ['add', 'remove', 'edit', 'list']); + if (!resolved) return { error: true, msg: message.format('ERR_INVALID_METHOD', { method: submethod }) }; - if(['remove', 'delete'].includes(word)) { - - for(const arg of args) { - delete setting.actions[arg.toLowerCase()]; - } - index = 'S_WORDFILTER_ACTION_DELETE'; - langParams.changed = args.join('`, `'); - - } else { - - if (!setting.explicit.includes(word) && - !setting.fuzzy.includes(word) && - !setting.tokenized.includes(word)) return { - error: true, - msg: message.format('S_WORDFILTER_WORD_NOT_INCLUDED') - }; - - const actionObj = resolver.resolveModAction(args.join(' '), validInfractions) - if(actionObj.error) return { - error: true, - msg: message.format(actionObj.template, { valid: validInfractions.join(', ') }) - }; - - setting.actions[word] = actionObj; + let result = null; + if (resolved.method === 'add') { + result = await this._createAction(message, setting); index = 'S_WORDFILTER_ACTION_ADD'; - langParams = { - type: actionObj.action, - duration: resolver.timeAgo(actionObj.duration), - points: actionObj.points, - force: actionObj.force, - prune: actionObj.prune, - word - } - + } else if (resolved.method === 'edit') { + result = await this._editAction(message, setting); + index = 'S_WORDFILTER_ACTION_EDIT'; + } else if (resolved.method === 'remove') { + result = await this._removeAction(message, setting); + index = 'S_WORDFILTER_ACTION_REMOVE'; + } else if (resolved.method === 'list') { + this._listActions(message, setting); + return; } + if (result.error) return result; + + //setting.actions.push(result); + console.log(result); + langParams = { + type: result.action, + duration: result.duration ? resolver.timeAgo(result.duration) : 'INF', + points: result.points || message.format('ON_OFF_TOGGLE', { toggle: false }, true), + force: message.format('ON_OFF_TOGGLE', { toggle: result.force }, true), + prune: message.format('ON_OFF_TOGGLE', { toggle: result.prune }, true), + trigger: result.trigger instanceof Array ? result.trigger.join(', ') : result.trigger + }; + + // if(args.length < 2) return { + // error: true, + // msg: message.format('S_WORDFILTER_ACTION_ARGS') + // } + // const word = args.shift().toLowerCase(); + + // if(['remove', 'delete'].includes(word)) { + + // for(const arg of args) { + // delete setting.actions[arg.toLowerCase()]; + // } + // index = 'S_WORDFILTER_ACTION_DELETE'; + // langParams.changed = args.join('`, `'); + + // } else { + + // if (!setting.explicit.includes(word) && + // !setting.fuzzy.includes(word) && + // !setting.tokenized.includes(word)) return { + // error: true, + // msg: message.format('S_WORDFILTER_WORD_NOT_INCLUDED') + // }; + + // const actionObj = resolver.resolveModAction(args.join(' '), validInfractions) + // if(actionObj.error) return { + // error: true, + // msg: message.format(actionObj.template, { valid: validInfractions.join(', ') }) + // }; + + // setting.actions[word] = actionObj; + // index = 'S_WORDFILTER_ACTION_ADD'; + // langParams = { + // type: actionObj.action, + // duration: resolver.timeAgo(actionObj.duration), + // points: actionObj.points, + // force: actionObj.force, + // prune: actionObj.prune, + // word + // } + + // } + } await message.guild._updateSettings({ [this.index]: setting }); @@ -176,6 +205,425 @@ module.exports = class WordFilter extends Setting { } + async _createAction(message, setting) { + + const actionObject = { action: null, duration: null, points: null, force: false, prune: false }; + + let response = await message.prompt(message.format('S_WORDFILTER_ACTION_ADD_START', { valid: validInfractions.join('`, `') })); + if (!response) return { error: true, msg: message.format('ERR_TIMEOUT') }; + + const { resolver } = this.client; + const [action] = response.content.split(' '); + + if (['cancel', 'abort', 'exit'].includes(action.toLowerCase())) return { + error: true, + msg: message.format('ERR_CANCEL') + }; + + const infType = resolver.resolveInfraction(action); + if (!infType || !validInfractions.includes(infType)) return { error: true, msg: message.format('S_WORDFILTER_INVALID_INFRACTION', { valid: validInfractions.join('`, `') }) }; + actionObject.action = infType; + + //Add a duration to the action + if (['MUTE', 'BAN'].includes(infType)) { + + response = await message.prompt(message.format('S_WORDFILTER_ACTION_ADD_TIMER', { action: infType })); + if (!response) return { error: true, msg: message.format('ERR_TIMEOUT') }; + if (['cancel', 'abort', 'exit'].includes(action.toLowerCase())) return { + error: true, + msg: message.format('ERR_CANCEL') + }; + + if (!['no', 'n'].includes(response.content.toLowerCase())) { + const time = resolver.resolveTime(response.content); + if (!time) message.formattedRespond('S_WORDFILTER_ACTION_ADD_TIMER_FAIL'); + else actionObject.duration = time; + } + + } + + const settings = message.guild._settings; + //Add points to action if modpoints are enabled + if (settings.moderationPoints?.enabled) { + + response = await message.prompt(message.format('S_WORDFILTER_ACTION_ADD_POINTS')); + if (!response) return { error: true, msg: message.format('ERR_TIMEOUT') }; + if (['cancel', 'abort', 'exit'].includes(action.toLowerCase())) return { + error: true, + msg: message.format('ERR_CANCEL') + }; + + if (!['no', 'n'].includes(response.content.toLowerCase())) { + + const points = /(\d{1,3})\s?(points?|pts?|p)?/iu; + const match = response.content.match(points); + if (!match) message.formattedRespond('S_WORDFILTER_ACTION_ADD_POINTS_FAIL'); + else { + let value = parseInt(match[1]); + if (value < 0 || value > 100) { + if (value < 0) value = 0; + else value = 100; + message.formattedRespond('S_WORDFILTER_ACTION_ADD_POINTS_RANGE', { params: { value } }); + } + actionObject.points = value; + } + + } + + } + + //Should it force the action if automod is enabled + if (settings.autoModeration?.enabled) { + + response = await message.prompt(message.format('S_WORDFILTER_ACTION_ADD_FORCE')); + if (!response) return { error: true, msg: message.format('ERR_TIMEOUT') }; + if (['cancel', 'abort', 'exit'].includes(action.toLowerCase())) return { + error: true, + msg: message.format('ERR_CANCEL') + }; + + if (['yes', 'ye', 'y'].includes(response.content.toLowerCase())) { + actionObject.force = true; + } + + } + + //Should the action also prune the user's messages + if (infType !== 'SOFTBAN') { + response = await message.prompt(message.format('S_WORDFILTER_ACTION_ADD_PRUNE')); + if (!response) return { error: true, msg: message.format('ERR_TIMEOUT') }; + if (['cancel', 'abort', 'exit'].includes(action.toLowerCase())) return { + error: true, + msg: message.format('ERR_CANCEL') + }; + + if (['yes', 'ye', 'y'].includes(response.content.toLowerCase())) { + actionObject.prune = true; + } + } + + //Which words should trigger the action? + response = await message.prompt(message.format('S_WORDFILTER_ACTION_ADD_TRIGGERS'), { time: 60*1000 }); + if (!response) { + setting.actions.push(actionObject); + actionObject.trigger = 'generic'; + return actionObject; + } + if (['cancel', 'abort', 'exit'].includes(action.toLowerCase())) return { + error: true, + msg: message.format('ERR_CANCEL') + }; + + const { params } = await this._parseArguments(response.content.split(' '), message.guild); + let first = params[0].toLowerCase(); + if (['generic', 'gen', 'explicit', 'ex', 'fuzzy', 'fuz', 'tokenized', 'token'].includes(first)) { + if (first === 'gen') first = 'generic'; + else if (first === 'ex') first = 'explicit'; + else if (first === 'fuz') first = 'fuzzy'; + else if (first === 'token') first = 'tokenized'; + const existing = setting.actions.find((ac) => ac.trigger === first); + if (existing) return { + error: true, + msg: message.format('S_WORDFILTER_ACTIONS_EXISTING', { trigger: first }) + }; + actionObject.trigger = first; + } else { + const { actions } = setting; + const words = params.map((word) => word.toLowerCase()); + const removed = []; + for (const word of words) { + const existing = actions.find((ac) => ac.trigger.includes(word)); + if (!existing) continue; + words.splice(words.indexOf(word), 1); + removed.push(word); + } + actionObject.trigger = words; + if (removed.length) message.formattedRespond('S_WORDFILTER_WORD_IN_ACTION', { params: { words: removed.join('`, `') } }); + } + + if (!actionObject.trigger.length) return { error: true, msg: message.format('S_WORDFILTER_INVALID_TRIGGER') }; + + setting.actions.push(actionObject); + return actionObject; + + } + + async _removeAction(message, setting) { + + const { actions } = setting; + const embed = this._createActionEmbed(message, setting); + const response = await message.prompt({ content: message.format('S_WORDFILTER_ACTION_REMOVE_START'), embed }, { time: 60*1000 }); + if (!response) return { error: true, msg: message.format('ERR_TIMEOUT') }; + + if (['cancel', 'abort', 'exit'].includes(response.content.toLowerCase())) return { + error: true, + msg: message.format('ERR_CANCEL') + }; + + const index = parseInt(response.content); + if (isNaN(index)) return { error: true, msg: message.format('ERR_NAN') }; + if (index < 0 || index > actions.length - 1) return { error: true, msg: message.format('ERR_INDEX_OUT_OF_BOUNDS') }; + + return setting.actions.splice(index, 1)[0]; + + } + + async _editAction(message, setting) { + + const { actions } = setting; + const embed = this._createActionEmbed(message, setting); + let response = await message.prompt({ content: message.format('S_WORDFILTER_ACTION_EDIT_START'), embed }, { time: 60 * 1000 }); + + if (!response) return { error: true, msg: message.format('ERR_TIMEOUT') }; + if (['cancel', 'abort', 'exit'].includes(response.content.toLowerCase())) return { + error: true, + msg: message.format('ERR_CANCEL') + }; + + const index = parseInt(response.content); + if (isNaN(index)) return { error: true, msg: message.format('ERR_NAN') }; + if (index < 0 || index > actions.length - 1) return { error: true, msg: message.format('ERR_INDEX_OUT_OF_BOUNDS') }; + + const action = actions[index]; + + //Which property do you want to edit? + const properties = Object.keys(action); + response = await message.prompt(message.format('S_WORDFILTER_ACTION_EDIT_SELECT', { properties: properties.join('`, `') })); + + if (!response) return { error: true, msg: message.format('ERR_TIMEOUT') }; + if (['cancel', 'abort', 'exit'].includes(response.content.toLowerCase())) return { + error: true, + msg: message.format('ERR_CANCEL') + }; + + const prop = response.content.toLowerCase(); + if (!properties.includes(prop)) return { + error: true, + msg: message.format('S_WORDFILTER_ACTION_EDIT_BADPROP') + }; + + if (prop === 'trigger') return this._editTrigger(message, setting, action); + else if (prop === 'duration') return this._editDuration(message, setting, action); + else if (prop === 'points') return this._editPoints(message, setting, action); + else if (prop === 'action') return this._editType(message, setting, action); + else if (['force', 'prune'].includes(prop)) return this._editBool(message, setting, action, prop); + + } + + async _editBool(message, setting, action, prop) { + + const response = await message.prompt(message.format('S_WORDFILTER_ACTION_EDIT_BOOL', { prop }), { time: 120 * 1000 }); + const { resolver } = this.client; + + if (!response) return { error: true, msg: message.format('ERR_TIMEOUT') }; + if (['cancel', 'abort', 'exit'].includes(response.content.toLowerCase())) return { + error: true, + msg: message.format('ERR_CANCEL') + }; + + const bool = resolver.resolveBoolean(response.content); + if (bool === null) return { + error: true, + msg: message.format('S_WORDFILTER_ACTION_EDIT_BOOL_INVALID') + }; + + action[prop] = bool; + return action; + + } + + async _editPoints(message, setting, action) { + + if (!message.guild._settings.moderationPoints?.enabled) return { + error: true, + msg: message.format('S_WORDFILTER_ACTION_EDIT_POINTS_DISABLED') + }; + + const response = await message.prompt(message.format('S_WORDFILTER_ACTION_EDIT_POINTS', {}), { time: 120 * 1000 }); + + if (!response) return { error: true, msg: message.format('ERR_TIMEOUT') }; + if (['cancel', 'abort', 'exit'].includes(response.content.toLowerCase())) return { + error: true, + msg: message.format('ERR_CANCEL') + }; + + const reg = /(\d{1,3})\s?(points?|pts?|p)?/iu; + if(!reg.test(response.content)) return { + error: true, + msg: message.format('S_WORDFILTER_ACTION_EDIT_POINTS_FAIL') + }; + + const match = response.content.match(reg); + let points = parseInt(match[1]); + if (points < 0 || points > 100) { + if (points < 0) points = 0; + else if (points > 100) points = 100; + message.respond('S_WORDFILTER_ACTION_ADD_POINTS_RANGE', { value: points }); + } + + action.points = points; + return action; + + } + + async _editType(message, setting, action) { + + const response = await message.prompt(message.format('S_WORDFILTER_ACTION_EDIT_TYPE', { valid: validInfractions.join('`, `') }), { time: 120 * 1000 }); + const { resolver } = this.client; + + if (!response) return { error: true, msg: message.format('ERR_TIMEOUT') }; + if (['cancel', 'abort', 'exit'].includes(response.content.toLowerCase())) return { + error: true, + msg: message.format('ERR_CANCEL') + }; + + if (!validInfractions.includes(response.content.toUpperCase())) return { + error: true, + msg: message.format('S_WORDFILTER_ACTION_EDIT_INVALID_TYPE') + }; + + action.action = response.content.toUpperCase(); + if (['BAN', 'MUTE'].includes(action.action) && !action.duration) return this._editDuration(message, setting, action); + + action.duration = null; + return action; + + } + + async _editDuration(message, setting, action) { + + if (!['MUTE', 'BAN'].includes(action.action)) return { + error: true, + msg: message.format('S_WORDFILTER_ACTION_EDIT_DURATION_ERR', { action: action.action }) + }; + + const response = await message.prompt(message.format('S_WORDFILTER_ACTION_EDIT_DURATION', {}), { time: 120 * 1000 }); + const { resolver } = this.client; + + if (!response) return { error: true, msg: message.format('ERR_TIMEOUT') }; + if (['cancel', 'abort', 'exit'].includes(response.content.toLowerCase())) return { + error: true, + msg: message.format('ERR_CANCEL') + }; + + if (response.content === '0') { + action.duration = null; + } else { + const time = resolver.resolveTime(response.content); + if (!time) return { + error: true, + msg: message.format('S_WORDFILTER_ACTION_EDIT_DURATION_ERR2') + }; + action.duration = time; + } + + return action; + + } + + async _editTrigger(message, setting, action) { + + const response = await message.prompt(message.format('S_WORDFILTER_ACTION_EDIT_TRIGGER', {}), { time: 120*1000 }); + const { resolver } = this.client; + + if (!response) return { error: true, msg: message.format('ERR_TIMEOUT') }; + if (['cancel', 'abort', 'exit'].includes(response.content.toLowerCase())) return { + error: true, + msg: message.format('ERR_CANCEL') + }; + + let { params } = await this._parseArguments(response.content.split(' '), message.guild); + params = params.map((elem) => elem.toLowerCase()); + const first = params.shift(); + + const resolved = await resolver.resolveMethod([first], null, null, null, null, false, ['add', 'remove', 'set']); + if (!['generic', 'explicit', 'fuzzy', 'tokenized'].includes(first) && !resolved) return { + error: true, + msg: message.format('S_WORDFILTER_ACTION_EDIT_INVALID') + }; + + if (['generic', 'explicit', 'fuzzy', 'tokenized'].includes(first)) { + const existing = setting.actions.find((action) => action.trigger === first); + if (existing) return { + error: true, + msg: message.format('S_WORDFILTER_ACTIONS_EXISTING', { trigger: first }) + }; + action.trigger = first; + return action; + } + + if (!params.length) return { + error: true, + msg: message.format('MISSING_ARGS') + }; + + const skipped = []; + for (const a of setting.actions) { + if (a.trigger instanceof Array) { + const existing = a.trigger.filter((word) => params.includes(word)); + if (existing.length) continue; + skipped.push(...existing); + } + } + + if (skipped.length) message.respond('S_WORDFILTER_WORD_IN_ACTION', { words: skipped.join(' ') }); + + let method = first; + if (method === 'set') action.trigger = params.filter((word) => !skipped.includes(word)); + else { + if (!(action.trigger instanceof Array)) action.trigger = []; + for (const word of params) { + + if (word === 'add') method = word; + else if (word === 'remove') method = word; + + if (skipped.includes(word)) continue; + if (method === 'add' && !action.trigger.includes(word)) action.trigger.push(word); + else if (method === 'remove' && action.trigger.includes(word)) action.trigger.splice(action.trigger.indexOf(word), 1); + + } + } + + return action; + + } + + async _listActions(message, setting) { + + if (!setting.actions.length) return message.formattedRespond('S_WORDFILTER_NOACTIONS'); + return message.respond(message.format('S_WORDFILTER_CURRENT_ACTIONS'), { embed: this._createActionEmbed(message, setting) }); + + } + + _createActionEmbed(message, setting) { + + const { resolver } = this.client; + const { actions } = setting; + const embed = { + fields: actions.map((action) => { + return { + name: `**[${actions.indexOf(action)}]** ${action.action}`, + value: message.format('S_WORDFILTER_ACTION_PROPERTIES', { + duration: action.duration ? resolver.timeAgo(action.duration) : 'INF', + points: action.points || message.format('ON_OFF_TOGGLE', { toggle: false }, true), + prune: message.format('ON_OFF_TOGGLE', { toggle: action.prune }, true), + force: message.format('ON_OFF_TOGGLE', { toggle: action.force }, true), + trigger: action.trigger instanceof Array ? action.trigger.join(', ') : '`' + action.trigger + '`' + }), + inline: true + }; + }), + color: 619452 + }; + if (embed.fields.length % 3 === 2) embed.fields.push({ + name: '\u200b', + value: '\u200b', + inline: true + }); + return embed; + + } + async fields(guild) { const setting = guild._settings[this.index]; const { resolver } = this.client; @@ -228,12 +676,13 @@ module.exports = class WordFilter extends Setting { }, { name: '》 Actions', - value: Object.entries(setting.actions).reduce((acc, [key, val]) => { - let str = `\`${key}\`: **${val.action}**`; + value: setting.actions.reduce((acc, val) => { + let str = `**${val.action}**`; if (val.points) str += ` (${val.points} points)`; if (val.duration) str += ` for ${resolver.timeAgo(val.duration, true, true, true)}`; if (val.force) str += ` - **FORCE**`; if (val.prune) str += ` - **PRUNE**`; + str += `\n__**Triggers:**__ ${val.trigger instanceof Array ? val.trigger.join(', ') : '`' + val.trigger + '`'}`; //result.trigger instanceof Array ? result.trigger.join(', ') : result.trigger acc.push(str); return acc; }, []).join('\n') || '`N/A`'