galactic-bot/structure/moderation/ModerationManager.js

451 lines
17 KiB
JavaScript
Raw Normal View History

const { stripIndents } = require('common-tags');
const { Collection, Util, Emojis, Constants } = require('../../util/');
2021-06-17 00:51:42 +02:00
const { User } = require('../extensions/');
const { Warn, Unmute, Mute, Kick, Softban, Unban, Ban, Addrole, Removerole, Lockdown, Unlockdown } = require('./infractions/');
const Constant = {
2020-06-16 00:15:13 +02:00
MaxTargets: 10, //10+(10*premium-tier), theoretical max = 40
Infractions: {
2020-07-16 09:54:39 +02:00
WARN: Warn,
2020-06-16 00:15:13 +02:00
UNMUTE: Unmute,
2020-07-16 09:54:39 +02:00
MUTE: Mute,
KICK: Kick,
SOFTBAN: Softban,
UNBAN: Unban,
BAN: Ban,
ADDROLE: Addrole,
REMOVEROLE: Removerole,
LOCKDOWN: Lockdown,
UNLOCKDOWN: Unlockdown
2020-07-16 09:54:39 +02:00
},
Hierarchy: {
WARN: 0,
MUTE: 1,
KICK: 2,
SOFTBAN: 3,
BAN: 4
2020-06-16 00:15:13 +02:00
}
};
class ModerationManager {
constructor(client) {
this.client = client;
this.callbacks = new Collection();
}
2020-06-16 00:15:13 +02:00
async initialize() {
//TODO: Load infractions for non-cached guilds...
this.client.storageManager.mongodb.infractions.find({
duration: { $gt: 0 },
guild: { $in: this.client.guilds.cache.keyArray() },
_callbacked: false
2020-06-16 00:15:13 +02:00
}).then((results) => {
2020-08-13 22:47:09 +02:00
this.client.logger.info(`Filtering ${results.length} infractions for callback.`);
2020-06-16 00:15:13 +02:00
this._handleExpirations(results);
});
}
2020-09-30 23:02:54 +02:00
/**
*
*
* @param {Infraction} Infraction
* @param {Message} message
* @param {object} { targets, reason, duration, data }
* @return {object|Message} The successfully moderated targets of the response message for failure
* @memberof ModerationManager
*/
async handleInfraction(Infraction, message, info) {
const { targets } = info;
const maxTargets = Constant.MaxTargets + message.guild.premium*Constant.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' });
}
2020-06-04 19:59:09 +02:00
const silent = Boolean(message.guild._settings.silent || message.arguments.silent);
const force = message.arguments?.force?.value || false;
2020-07-16 09:54:39 +02:00
const responses = [];
2020-09-30 23:02:54 +02:00
for (const target of targets) {
const response = await this._handleTarget(Infraction, target, {
2021-06-17 19:25:09 +02:00
message,
guild: message.guild,
channel: message.channel,
executor: message.member,
2021-06-17 19:25:09 +02:00
arguments: message.arguments,
2021-06-17 00:51:42 +02:00
points: message.arguments?.points?.value,
expiration: message.arguments?.expiration?.value,
reason: info.reason,
duration: info.duration,
data: info.data,
2021-06-17 00:51:42 +02:00
force,
silent
});
responses.push({
escalation: Infraction.type !== response.infraction.type,
...response
2020-07-16 09:54:39 +02:00
});
}
2020-07-16 09:54:39 +02:00
const success = Boolean(responses.some((r) => !r.error));
const succeeded = responses.filter((r) => !r.error);
const failed = responses.filter((r) => r.error);
2020-06-16 00:15:13 +02:00
const fatals = failed.filter((f) => f.fatal);
if(fatals.length > 0) {
const [ error ] = fatals;
return message.respond(message.format(error.reason), { emoji: 'failure' });
2020-06-16 00:15:13 +02:00
}
2020-07-16 09:54:39 +02:00
const successes = {};
const fails = {};
for(const response of responses) {
const { type, target } = response.infraction;
2020-07-16 09:54:39 +02:00
if(!response.error) {
if(!silent) {
if(successes[type]) {
successes[type].targets.push(target.display);
2020-07-16 09:54:39 +02:00
} else {
successes[type] = {
targets: [target.display],
infraction: response.infraction,
escalation: response.escalation
2020-07-16 09:54:39 +02:00
};
}
}
} else {
if(fails[type]) {
fails[type].targets.push(target.display);
2020-07-16 09:54:39 +02:00
} else {
fails[type] = {
targets: [target.display],
2020-07-16 09:54:39 +02:00
infraction: response.infraction,
reason: response.reason
};
}
}
}
2020-07-20 00:42:21 +02:00
const succeededTargets = succeeded.map((s) => s.infraction.target);
const actions = await this._handleArguments(message, succeededTargets); //eslint-disable-line no-unused-vars
let string = "";
2021-06-17 00:51:42 +02:00
if(!silent) {
for(const [ , data ] of Object.entries(successes)) {
const { dictionary, targetType } = data.infraction;
const reason = data.escalation ? message.format('INFRACTION_ESCALATIONREASON') : Util.escapeMarkdown(data.infraction.reason);
const str = `${Emojis.success} ${message.format('INFRACTION_SUCCESS', {
infraction: dictionary.past,
targetType: `${targetType.toLowerCase()}${data.targets.length === 1 ? '' : 's'}`,
target: data.targets.map((t) => `**${Util.escapeMarkdown(t)}**`).join(' '),
text: !data.escalation ? ` ${reason.length > 120 ? `for: \`${reason.substring(0, 117)}...\`` : `for: \`${reason}\``}` : ` because \`${reason}\``
})}`;
data.escalation ? string += str : string = `${str}\n${string}`; //eslint-disable-line
}
}
for(const [ , data ] of Object.entries(fails)) {
2020-07-16 09:54:39 +02:00
const { dictionary, targetType } = data.infraction;
const str = `${Emojis.failure} ${message.format('INFRACTION_FAIL', {
infraction: dictionary.present,
targetType: `${targetType.toLowerCase()}${data.targets.length === 1 ? '' : 's'}`,
2020-07-16 09:54:39 +02:00
target: data.targets.map((t) => `**${Util.escapeMarkdown(t)}**`).join(' '),
2020-07-29 21:21:58 +02:00
reason: message.format(data.reason)
2020-07-16 09:54:39 +02:00
})}`;
(!data.escalation && !success) //eslint-disable-line
2020-07-16 09:54:39 +02:00
? string = `${str}\n${string}`
: string += str;
}
if(success && silent) { //Delete message if silent.
try {
await message.delete();
} catch(e) {} //eslint-disable-line no-empty
}
2020-07-16 09:54:39 +02:00
if(string) message.respond(string);
2020-06-04 19:59:09 +02:00
return succeeded;
}
2021-05-04 16:16:04 +02:00
2021-06-17 19:25:09 +02:00
async _handleTarget(Infraction, target, info) {
const { guild, reason, force } = info;
2021-06-17 00:51:42 +02:00
const { autoModeration, moderationPoints } = guild._settings;
const { type } = Infraction;
let points = 0,
expiration = 0;
if(moderationPoints.enabled) {
2021-06-17 00:51:42 +02:00
points = info.points || moderationPoints.points[type];
expiration = info.expiration || moderationPoints.expirations[type];
for(const [ phrase, amount ] of Object.entries(moderationPoints.associations)) {
if(reason.toLowerCase().includes(phrase)) points = amount;
}
}
const verify = async (infraction, escalated = false) => {
2021-06-17 00:51:42 +02:00
let verification = infraction.verify(info.executor, target, info.channel);
if(verification instanceof Promise) verification = await verification;
2021-06-17 00:51:42 +02:00
2021-06-16 08:54:51 +02:00
if(verification.error) return verification;
if(infraction.targetType === 'USER') {
const userTarget = target instanceof User ? target : target.user;
const oldPoints = await userTarget.totalPoints(guild);
const newPoints = oldPoints + infraction.points;
2021-06-17 00:51:42 +02:00
if(autoModeration.enabled && points > 0 && !force && !escalated) {
let result = null;
for(let [ threshold, action ] of Object.entries(autoModeration.thresholds)) { //eslint-disable-line prefer-const
threshold = parseInt(threshold);
if(oldPoints >= threshold) {
if(autoModeration.usePrevious) {
result = {
threshold,
...action
};
}
continue;
}
if(newPoints >= threshold) {
result = {
threshold,
...action
};
}
}
if(result) {
return {
error: false,
escalation: result,
infraction
};
}
}
}
return { error: false, infraction };
};
const infraction = new Infraction(this.client, {
target,
type,
2021-06-17 19:25:09 +02:00
message: info.message || null,
arguments: info.arguments,
guild: info.guild,
channel: info.channel,
executor: info.executor,
reason: info.reason,
duration: info.duration,
data: info.data,
points,
expiration,
2021-06-17 00:51:42 +02:00
silent: info.silent
});
let response = await verify(infraction);
if(response.escalation) {
if(Constant.Hierarchy[infraction.type] <= Constant.Hierarchy[response.escalation.type]) {
const escalationClass = Constant.Infractions[response.escalation.type];
const escalationInfraction = new escalationClass(this.client, {
target,
2021-06-17 19:25:09 +02:00
message: info.message || null,
arguments: info.arguments,
type: escalationClass.type,
guild: info.guild,
channel: info.channel,
executor: info.executor,
reason: stripIndents`${reason}
*${guild.format('INFRACTION_AUTOMODESCALATION')}*`,
duration: info.duration,
data: info.data,
points,
expiration,
2021-06-17 00:51:42 +02:00
silent: info.silent
});
response = await verify(escalationInfraction, true);
}
}
2021-06-16 08:54:51 +02:00
if(response.error) return response;
2021-06-17 19:25:09 +02:00
if(response.infraction.targetType === 'USER') {
response.infraction.totalPoints = await response.infraction.target.totalPoints(guild, {
points, expiration, timestamp: response.infraction.timestamp
});
}
return response.infraction.execute();
}
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
});
2020-07-20 00:42:21 +02:00
messages = messages.filter((m) => {
return users.includes(m.author.id) && m.deletable;
});
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)) {
2020-07-20 00:42:21 +02:00
// console.log(arg, targets);
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;
}
2020-06-16 00:15:13 +02:00
async _handleExpirations(infractions = []) {
const currentDate = Date.now();
const resolve = async (i) => {
this.client.logger.debug("Resolving infraction");
const undoClass = Constant.Infractions[Constants.InfractionOpposites[i.type]];
2020-06-16 00:15:13 +02:00
if(!undoClass) return false;
const guild = this.client.guilds.resolve(i.guild);
await guild.settings(); //just incase
2020-06-16 00:15:13 +02:00
let target = null;
if(i.targetType === 'USER') {
target = guild.members.resolve(i.target);
2020-06-16 00:15:13 +02:00
if(!target) {
try {
target = await guild.members.fetch(i.target);
} catch(e) {} //eslint-disable-line no-empty
if(!target && i.type === 'BAN') {
try {
target = await this.client.users.fetch(i.target);
} catch(e) {} //eslint-disable-line no-empty
}
2020-06-16 00:15:13 +02:00
}
} else if(i.targetType === 'CHANNEL') {
2020-06-16 00:15:13 +02:00
target = guild.channels.resolve(i.target);
}
if(target) {
const executor = guild.members.resolve(i.executor) || guild.me;
try {
await new undoClass(this.client, {
type: undoClass.type,
reason: `AUTO-${Constants.InfractionOpposites[i.type]} from Case ${i.case}`,
channel: guild.channels.resolve(i.channel),
hyperlink: i.modLogMessage && i.modLogChannel ? `https://discord.com/channels/${i.guild}/${i.modLogChannel}/${i.modLogMessage}` : null,
data: i.data,
guild,
target,
executor
}).execute();
} catch(error) {
this.client.logger.error(`Error when resolving infraction:\n${error.stack||error}`);
}
} else {
//Target left guild or channel was removed from the guild. What should happen in this situation?
2021-06-21 23:20:44 +02:00
//Maybe continue checking if the user rejoins, but the channel will always be gone.
2020-06-16 00:15:13 +02:00
}
//TODO: Log this, should never error... hopefully.
await this.client.storageManager.mongodb.infractions.updateOne(
{ id: i.id },
{ _callbacked: true }
).catch((e) => {}); //eslint-disable-line no-empty, no-unused-vars, no-empty-function
2020-06-16 00:15:13 +02:00
return true;
};
for(const infraction of infractions) {
const expiration = infraction.timestamp + infraction.duration*1000;
2020-08-09 01:23:19 +02:00
if(expiration-currentDate <= 0) {
2020-06-16 00:15:13 +02:00
await resolve(infraction);
continue;
}
this.client.logger.debug(`Going to resolve infraction in: ${expiration-currentDate}`);
this.callbacks.set(infraction.id, {
timeout: setTimeout(async () => {
await resolve(infraction);
2020-06-16 00:15:13 +02:00
}, expiration-currentDate),
infraction
});
}
}
async _removeExpiration(expiration) {
2020-07-16 09:54:39 +02:00
this.client.logger.debug(`Expired infraction ${expiration.infraction.type} for user ${expiration.infraction.target}.`);
2020-08-09 01:23:19 +02:00
await this.client.storageManager.mongodb.infractions.updateOne(
{ id: expiration.infraction.id },
{ _callbacked: true }
).catch((e) => {}); //eslint-disable-line no-empty, no-unused-vars, no-empty-function
2020-07-16 09:54:39 +02:00
clearTimeout(expiration.timeout); //just incase node.js is doing some bullshit
this.callbacks.delete(expiration.infraction.id);
}
async _fetchTarget(guild, targetId, targetType, user = false) {
let target = null;
if(targetType === 'USER') {
try {
target = await guild.members.fetch(targetId);
} catch(error) {
if(user) {
target = await this.client.users.fetch(targetId);
}
}
} else if(targetType === 'CHANNEL') {
target = guild.channels.resolve(targetId);
}
return target;
}
async findLatestInfraction(type, target) {
const callback = this.callbacks.filter((c) => {
return c.infraction.type === type
&& c.infraction.target === target.id;
}).first();
if(callback) return callback.infraction;
const result = await this.client.storageManager.mongodb.infractions.findOne(
{ type, target },
{ sort: { timestamp: -1 } }
);
return result || null;
}
}
module.exports = ModerationManager;