const { stripIndents } = require('common-tags'); const { Collection, Util } = require('../../util/'); const { Unmute, Unban } = require('./infractions/'); const Constants = { MaxTargets: 10, //10+(10*premium-tier), theoretical max = 40 Infractions: { UNMUTE: Unmute, UNBAN: Unban }, Opposites: { MUTE: "UNMUTE", BAN: "UNBAN" } }; class ModerationManager { constructor(client) { this.client = client; this.expirations = new Collection(); } async initialize() { this.client.transactionHandler.send({ provider: 'mongodb', request: { collection: 'infractions', type: 'find', query: { duration: { $gt: 0 }, guild: { $in: this.client.guilds.cache.keyArray() }, expired: false } //should definitely filter this more... } }).then((results) => { this.client.logger.debug(`Filtering ${results.length} infractions for expirations.`); this._handleExpirations(results); }); } async handleInfraction(Infraction, message, { targets, reason, duration }) { const maxTargets = Constants.MaxTargets + message.guild.premium*Constants.MaxTargets; if(targets.length > maxTargets) { return message.respond(stripIndents`${message.format('MODERATIONMANAGER_INFRACTION_MAXTARGETS', { maxTargets, type: Infraction.targetType })} ${maxTargets < 40 ? message.format('MODERATIONMANAGER_INFRACTION_MAXTARGETSALT') : ''}`, { emoji: 'failure' }); } const silent = Boolean(message.guild._settings.silent || message.arguments.silent); this.client.logger.debug(`Silent infraction: ${silent}`); const promises = []; for(const target of targets) { promises.push(new Infraction(this.client, { executor: message.member, guild: message.guild, channel: message.channel, arguments: message.arguments, message, target, reason, duration, silent }).execute()); } const responses = await Promise.all(promises); let success = Boolean(responses.some((r) => !r.error)); const succeeded = responses.filter((r) => !r.error); const failed = responses.filter((r) => r.error); const succeededTargets = succeeded.map((s) => s.infraction.target); const actions = await this._handleArguments(message, succeededTargets); //Handle prune arguments, etc. ONLY IF INFRACTION SUCCEEDS. //NOTE: I'm not translating: infraction types (KICK, MUTE, ETC.), infraction target types (channel(s), user(s)), /* Message Handling */ const { dictionary, targetType } = responses[0].infraction; //Handle fatal errors, if necessary. const fatals = failed.filter((f) => f.fatal); if(fatals.length > 0) { const [ error ] = fatals; return message.respond(error.reason, { emoji: 'failure' }); } let string = ""; if(success && !silent) { string = message.format('MODERATIONMANAGER_INFRACTION_SUCCESS', { infraction: dictionary.past, targetType: `${targetType}${succeeded.length > 1 ? 's' : ''}`, target: succeeded.map((s) => `**${Util.escapeMarkdown(s.infraction.targetName)}**`).join(', '), action: actions.prune ? ` and pruned \`${actions.prune}\` message${actions.prune > 1 ? 's' : ''}` : '' }); } else if((silent && failed.length > 0)) { if(silent) success = false; const format = failed.length === 1 ? "MODERATIONMANAGER_INFRACTION_SINGULARFAIL" : "MODERATIONMANAGER_INFRACTION_MULTIPLEFAIL"; string = message.format(format, { infraction: dictionary.present, targetType: `${targetType}${failed.length > 1 ? 's' : ''}`, target: failed.length === 1 ? `**${Util.escapeMarkdown(failed[0].infraction.targetName)}**` : failed.map((f) => `**${f.infraction.targetName}**`).join(', '), reason: failed[0].reason }); } if((success && failed.length > 0) || (!success && failed.length > 1)) { for(const fail of failed) { string += `\n${message.format('MODERATIONMANAGER_INFRACTION_FAIL', { infraction: dictionary.present, target: Util.escapeMarkdown(fail.infraction.targetName), reason: fail.reason })}`; } } if(string) message.respond(string, { emoji: success ? 'success' : 'failure' }); return succeeded; } async _handleArguments(message, targets) { const actions = { prune: async (message, argument, targets) => { const users = targets.map((t) => t.id); let messages = await message.channel.messages.fetch({ limit: argument.value }); messages = messages.filter((m) => users.includes(m.author.id)); try { await message.channel.bulkDelete(messages, true); } catch(err) {} //eslint-disable-line no-empty return messages.size; } }; const responses = {}; for(const arg of Object.values(message.arguments)) { if(actions[arg.name]) { let action = actions[arg.name](message, arg, targets); if(action instanceof Promise) action = await action; responses[arg.name] = action; } } return responses; } async _handleExpirations(infractions = []) { const currentDate = Date.now(); const resolve = async (i) => { const undoClass = Constants.Infractions[Constants.Opposites[i.type]]; if(!undoClass) return false; const guild = this.client.guilds.resolve(i.guild); await guild.settings(); //just incase let target = null; if(i.targetType === 'user') { target = await guild.members.resolve(i.target); if(!target) { try { target = await guild.members.fetch(i.target); } catch(e) {} //eslint-disable-line no-empty //Shouldn't attempt to fetch users. } } else if(i.targetType === 'channel') { target = guild.channels.resolve(i.target); } if(!target) { this.client.logger.debug(`User left the guild..? Unable to find a guild member.\n${i}`); return false; } const executor = guild.members.resolve(i.executor) || guild.me; const infrac = await new undoClass(this.client, { reason: `AUTO-${Constants.Opposites[i.type]} from Case ${i.case}`, channel: guild.channels.resolve(i.channel), hyperlink: i.logMessage && i.moderationLogs ? `https://discord.com/channels/${i.guild}/${i.moderationLog}/${i.logMessage}` : null, data: i.data, guild, target, executor }).execute(); return true; }; for(const infraction of infractions) { const expiration = infraction.timestamp + (infraction.duration*1000); if(expiration-currentDate < 0) { await resolve(infraction); continue; } this.expirations.set(infraction.id, { timeout: setTimeout(() => { resolve(infraction); }, expiration-currentDate), infraction }); } } } module.exports = ModerationManager;