2020-06-02 12:09:28 +02:00
|
|
|
const { stripIndents } = require('common-tags');
|
2020-07-04 12:23:10 +02:00
|
|
|
const { ObjectId } = require('mongodb');
|
2020-06-02 12:09:28 +02:00
|
|
|
|
2020-06-04 19:59:09 +02:00
|
|
|
const { Collection, Util } = require('../../util/');
|
2020-06-16 00:15:13 +02:00
|
|
|
const { Unmute, Unban } = require('./infractions/');
|
2020-06-02 12:09:28 +02:00
|
|
|
|
2020-05-25 13:13:34 +02:00
|
|
|
const Constants = {
|
2020-06-16 00:15:13 +02:00
|
|
|
MaxTargets: 10, //10+(10*premium-tier), theoretical max = 40
|
|
|
|
Infractions: {
|
|
|
|
UNMUTE: Unmute,
|
|
|
|
UNBAN: Unban
|
|
|
|
},
|
|
|
|
Opposites: {
|
|
|
|
MUTE: "UNMUTE",
|
|
|
|
BAN: "UNBAN"
|
|
|
|
}
|
2020-05-25 13:13:34 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
class ModerationManager {
|
|
|
|
|
|
|
|
constructor(client) {
|
|
|
|
|
|
|
|
this.client = client;
|
2020-07-04 12:23:10 +02:00
|
|
|
this.callbacks = new Collection();
|
2020-05-25 13:13:34 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-06-16 00:15:13 +02:00
|
|
|
async initialize() {
|
|
|
|
|
|
|
|
this.client.transactionHandler.send({
|
|
|
|
provider: 'mongodb',
|
|
|
|
request: {
|
|
|
|
collection: 'infractions',
|
|
|
|
type: 'find',
|
|
|
|
query: {
|
|
|
|
duration: { $gt: 0 },
|
|
|
|
guild: { $in: this.client.guilds.cache.keyArray() },
|
2020-07-04 12:23:10 +02:00
|
|
|
callbacked: false
|
|
|
|
}
|
2020-06-16 00:15:13 +02:00
|
|
|
}
|
|
|
|
}).then((results) => {
|
2020-07-04 12:23:10 +02:00
|
|
|
console.log(results);
|
|
|
|
this.client.logger.debug(`Filtering ${results.length} infractions for callback.`);
|
2020-06-16 00:15:13 +02:00
|
|
|
this._handleExpirations(results);
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-07-04 12:23:10 +02:00
|
|
|
async handleInfraction(Infraction, message, { targets, reason, duration, data }) {
|
2020-05-25 13:13:34 +02:00
|
|
|
|
|
|
|
const maxTargets = Constants.MaxTargets + message.guild.premium*Constants.MaxTargets;
|
2020-06-02 12:09:28 +02:00
|
|
|
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);
|
|
|
|
this.client.logger.debug(`Silent infraction: ${silent}`);
|
|
|
|
|
2020-06-02 12:09:28 +02:00
|
|
|
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,
|
2020-06-04 19:59:09 +02:00
|
|
|
duration,
|
2020-07-04 12:23:10 +02:00
|
|
|
silent,
|
|
|
|
data
|
2020-06-02 12:09:28 +02:00
|
|
|
}).execute());
|
|
|
|
}
|
|
|
|
|
|
|
|
const responses = await Promise.all(promises);
|
|
|
|
|
2020-06-04 19:59:09 +02:00
|
|
|
let success = Boolean(responses.some((r) => !r.error));
|
2020-06-02 12:09:28 +02:00
|
|
|
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;
|
|
|
|
|
2020-06-16 00:15:13 +02:00
|
|
|
//Handle fatal errors, if necessary.
|
|
|
|
const fatals = failed.filter((f) => f.fatal);
|
|
|
|
if(fatals.length > 0) {
|
|
|
|
const [ error ] = fatals;
|
2020-07-04 12:23:10 +02:00
|
|
|
return message.respond(message.format(error.reason), { emoji: 'failure' });
|
2020-06-16 00:15:13 +02:00
|
|
|
}
|
|
|
|
|
2020-06-02 12:09:28 +02:00
|
|
|
let string = "";
|
2020-06-04 19:59:09 +02:00
|
|
|
if(success && !silent) {
|
2020-06-02 12:09:28 +02:00
|
|
|
string = message.format('MODERATIONMANAGER_INFRACTION_SUCCESS', {
|
|
|
|
infraction: dictionary.past,
|
|
|
|
targetType: `${targetType}${succeeded.length > 1 ? 's' : ''}`,
|
2020-06-04 19:59:09 +02:00
|
|
|
target: succeeded.map((s) => `**${Util.escapeMarkdown(s.infraction.targetName)}**`).join(', '),
|
2020-06-02 12:09:28 +02:00
|
|
|
action: actions.prune ? ` and pruned \`${actions.prune}\` message${actions.prune > 1 ? 's' : ''}` : ''
|
|
|
|
});
|
2020-07-04 12:23:10 +02:00
|
|
|
} else if(failed.length > 0) {
|
2020-06-04 19:59:09 +02:00
|
|
|
if(silent) success = false;
|
2020-06-02 12:09:28 +02:00
|
|
|
const format = failed.length === 1 ? "MODERATIONMANAGER_INFRACTION_SINGULARFAIL" : "MODERATIONMANAGER_INFRACTION_MULTIPLEFAIL";
|
|
|
|
string = message.format(format, {
|
|
|
|
infraction: dictionary.present,
|
|
|
|
targetType: `${targetType}${failed.length > 1 ? 's' : ''}`,
|
2020-06-04 19:59:09 +02:00
|
|
|
target: failed.length === 1 ? `**${Util.escapeMarkdown(failed[0].infraction.targetName)}**` : failed.map((f) => `**${f.infraction.targetName}**`).join(', '),
|
2020-07-04 12:23:10 +02:00
|
|
|
reason: message.format(failed[0].reason)
|
2020-06-02 12:09:28 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-07-04 12:23:10 +02:00
|
|
|
if(success && failed.length > 0
|
|
|
|
|| !success && failed.length > 1) {
|
2020-06-02 12:09:28 +02:00
|
|
|
for(const fail of failed) {
|
|
|
|
string += `\n${message.format('MODERATIONMANAGER_INFRACTION_FAIL', {
|
|
|
|
infraction: dictionary.present,
|
2020-06-04 19:59:09 +02:00
|
|
|
target: Util.escapeMarkdown(fail.infraction.targetName),
|
2020-07-04 12:23:10 +02:00
|
|
|
reason: message.format(fail.reason)
|
2020-06-02 12:09:28 +02:00
|
|
|
})}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-04 12:23:10 +02:00
|
|
|
if(success && silent) { //Delete message if silent.
|
|
|
|
try {
|
|
|
|
await message.delete();
|
|
|
|
} catch(e) {} //eslint-disable-line no-empty
|
|
|
|
}
|
|
|
|
|
2020-06-04 19:59:09 +02:00
|
|
|
if(string) message.respond(string, { emoji: success ? 'success' : 'failure' });
|
|
|
|
return succeeded;
|
2020-06-02 12:09:28 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
async _handleArguments(message, targets) {
|
2020-05-25 13:13:34 +02:00
|
|
|
|
2020-06-02 12:09:28 +02:00
|
|
|
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-04 12:23:10 +02:00
|
|
|
messages = messages.filter((m) => users.includes(m.author.id) && m.deleteable);
|
2020-06-02 12:09:28 +02:00
|
|
|
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;
|
2020-05-25 13:13:34 +02:00
|
|
|
|
|
|
|
}
|
2020-06-16 00:15:13 +02:00
|
|
|
|
|
|
|
async _handleExpirations(infractions = []) {
|
|
|
|
|
|
|
|
const currentDate = Date.now();
|
|
|
|
|
|
|
|
const resolve = async (i) => {
|
2020-07-04 12:23:10 +02:00
|
|
|
this.client.logger.debug("Resolving infraction");
|
2020-06-16 00:15:13 +02:00
|
|
|
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
|
2020-07-04 12:23:10 +02:00
|
|
|
|
2020-06-16 00:15:13 +02:00
|
|
|
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
|
2020-07-04 12:23:10 +02:00
|
|
|
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') {
|
|
|
|
target = guild.channels.resolve(i.target);
|
|
|
|
}
|
2020-07-04 12:23:10 +02:00
|
|
|
|
2020-06-16 00:15:13 +02:00
|
|
|
if(!target) {
|
2020-07-04 12:23:10 +02:00
|
|
|
this.client.logger.debug(`User left the guild or channel was deleted? Unable to resolve target.\n${i}`);
|
2020-06-16 00:15:13 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const executor = guild.members.resolve(i.executor) || guild.me;
|
|
|
|
|
2020-07-04 12:23:10 +02:00
|
|
|
await new undoClass(this.client, {
|
2020-06-16 00:15:13 +02:00
|
|
|
reason: `AUTO-${Constants.Opposites[i.type]} from Case ${i.case}`,
|
|
|
|
channel: guild.channels.resolve(i.channel),
|
2020-07-04 12:23:10 +02:00
|
|
|
hyperlink: i.logMessage && i.moderationLog ? `https://discord.com/channels/${i.guild}/${i.moderationLog}/${i.logMessage}` : null,
|
2020-06-16 00:15:13 +02:00
|
|
|
data: i.data,
|
|
|
|
guild,
|
|
|
|
target,
|
|
|
|
executor
|
|
|
|
}).execute();
|
|
|
|
|
2020-07-04 12:23:10 +02:00
|
|
|
const obj = await this.client.transactionHandler.send({
|
|
|
|
provider: 'mongodb',
|
|
|
|
request: {
|
|
|
|
type: 'updateOne',
|
|
|
|
collection: 'infractions',
|
|
|
|
query: {
|
|
|
|
_id: i._id
|
|
|
|
},
|
|
|
|
upsert: false,
|
|
|
|
data: {
|
|
|
|
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) {
|
2020-07-04 12:23:10 +02:00
|
|
|
const expiration = infraction.timestamp + infraction.duration*1000;
|
2020-06-16 00:15:13 +02:00
|
|
|
if(expiration-currentDate < 0) {
|
|
|
|
await resolve(infraction);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-07-04 12:23:10 +02:00
|
|
|
this.client.logger.debug(`Going to resolve infraction in: ${expiration-currentDate}`);
|
|
|
|
|
|
|
|
this.callbacks.set(infraction.id, {
|
2020-06-16 00:15:13 +02:00
|
|
|
timeout: setTimeout(() => {
|
|
|
|
resolve(infraction);
|
|
|
|
}, expiration-currentDate),
|
|
|
|
infraction
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2020-07-04 12:23:10 +02:00
|
|
|
|
|
|
|
async _removeExpiration(expiration) {
|
|
|
|
console.log(`Removing expiration: ${expiration.infraction.id}`);
|
|
|
|
await this.client.transactionHandler.send({
|
|
|
|
provider: 'mongodb',
|
|
|
|
request: {
|
|
|
|
type: 'updateOne',
|
|
|
|
collection: 'infractions',
|
|
|
|
query: {
|
|
|
|
id: expiration.infraction.id
|
|
|
|
},
|
|
|
|
data: {
|
|
|
|
callbacked: true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
clearInterval(expiration.timeout); //just incase node.js is doing some bullshit
|
|
|
|
this.callbacks.delete(expiration.infraction.id);
|
|
|
|
}
|
2020-05-25 13:13:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = ModerationManager;
|