import { Message } from "revolt.js/dist/maps/Messages"; import { client } from "../../.."; import CustomRuleTrigger from "../../../struct/antispam/CustomRuleTrigger"; import VM from 'vm'; let ruleTimeouts: Map = new Map(); async function messageContentTrigger(message: Message, trigger: CustomRuleTrigger): Promise { /* Match message content */ let matched = false; if (trigger.matcher) { if (trigger.channelFilter) { if (trigger.channelFilter.mode == 'WHITELIST' && !trigger.channelFilter.channels.includes(message.channel_id)) return false; if (trigger.channelFilter.mode == 'BLACKLIST' && trigger.channelFilter.channels.includes(message.channel_id)) return false; } if (trigger.matcher instanceof RegExp) { /** * Since users will eventually be able to provide regexes, we need to protect * againt ReDoS (Regular Expression Denial of Service). * Therefore all RegExes will have an execution limit of 2ms. * An additional mitigation step would be to disable rules that are frequently * found to hit that limit, but that is out of scope for now. * * (If someone finds a flaw in my logic, please report it) */ // We use node's built-in `vm` module to limit execution time. // No idea if this works the way I want it to try { let ctx = VM.createContext({ regex: trigger.matcher!, matchedStrings: [], content: message.content as string, }); let script = new VM.Script('matchedStrings = content.match(regex);', { timeout: 2 }); script.runInContext(ctx); if (ctx.matchedStrings?.length) matched = true; } catch(e) { console.error('Exception thrown while parsing RegEx: ' + e); } } else { if ((message.content as string).includes(trigger.matcher)) matched = true; } } if (!matched) return false; /* Timeouts */ let timeoutKeys = { global: trigger._id, channel: trigger._id + '/channel/' + message.channel_id, user: trigger._id + '/user/' + message.author_id, } let timeoutPass = true; // Global if (trigger.timeout?.global && ruleTimeouts.has(timeoutKeys.global)) { if (ruleTimeouts.get(timeoutKeys.global)! + (trigger.timeout.global * 1000) < Date.now()) { ruleTimeouts.set(timeoutKeys.global, Date.now()); } else timeoutPass = false; } // Per Channel if (trigger.timeout?.perChannel && ruleTimeouts.has(timeoutKeys.channel)) { if (ruleTimeouts.get(timeoutKeys.channel)! + (trigger.timeout.perChannel * 1000) < Date.now()) { ruleTimeouts.set(timeoutKeys.channel, Date.now()); } else timeoutPass = false; } // Per User if (trigger.timeout?.perUser && ruleTimeouts.has(timeoutKeys.user)) { if (ruleTimeouts.get(timeoutKeys.user)! + (trigger.timeout.perUser * 1000) < Date.now()) { ruleTimeouts.set(timeoutKeys.user, Date.now()); } else timeoutPass = false; } if (!timeoutPass) return false; /* User/bot filter comes last because we want to avoid fetching users if possible */ if (trigger.userFilter && trigger.userFilter != 'any') { let user = message.author || await client.users.fetch(message.author_id); if (trigger.userFilter == 'bot' && !user.bot) return false; if (trigger.userFilter == 'user' && user.bot) return false; } return true; } export default messageContentTrigger;