infraction resolving

This commit is contained in:
Erik 2022-04-30 22:11:35 +03:00
parent 9b85ca9585
commit 303f1dfe81
Signed by untrusted user: Navy.gif
GPG Key ID: 811EC0CD80E7E5FB
4 changed files with 219 additions and 55 deletions

View File

@ -0,0 +1,44 @@
const { SlashCommand, Infraction } = require("../../../interfaces");
class ResolveCommand extends SlashCommand {
constructor(client) {
super(client, {
name: 'resolve',
description: 'Resolve infraction(s)',
module: 'moderation',
memberPermissions: ['MANAGE_GUILD'],
options: [{
name: 'case',
type: 'INTEGER',
description: 'Case ID (Integer)',
required: true,
minimum: 0
}, {
name: 'reason',
description: 'Reason for resolving case',
type: 'STRING'
}, {
name: 'notify',
description: 'Attempt to notify the user about the resolve, may not always be possible',
type: 'BOOLEAN'
}] // Potentially add another option to enable a range of cases
});
}
async execute(invoker, { case: caseId, reason, notify }) {
const { guild, member } = invoker;
const infraction = await new Infraction(this.client, { guild, case: caseId.value }).fetch(true);//.catch(() => null);
if (!infraction) return { emoji: 'failure', index: 'INFRACTION_NOT_FOUND' };
if (infraction.resolved) return { emoji: 'failure', index: 'INFRACTION_ALREADY_RESOLVED' };
const response = await infraction.resolve(member, reason?.value || null, notify?.value || false);
if (response?.error) return { emoji: 'warning', index: 'COMMAND_RESOLVE_WARNING', params: { message: response.message } };
return { emoji: 'success', index: 'COMMAND_RESOLVE_SUCCESS' };
}
}
module.exports = ResolveCommand;

View File

@ -8,7 +8,8 @@ class BanInfraction extends Infraction {
constructor(client, opts = {}) {
super(client, {
if (opts.fetched) super(client, opts);
else super(client, {
targetType: 'USER',
type: opts.type,
guild: opts.guild,
@ -61,7 +62,7 @@ class BanInfraction extends Infraction {
let alreadyBanned = null;
try {
alreadyBanned = await this.guild.fetchBan(this.member.id);
alreadyBanned = await this.guild.bans.fetch(this.member.id);
} catch (e) { } //eslint-disable-line no-empty
if (alreadyBanned) return super._fail('C_BAN_ALREADYBANNED');
@ -70,6 +71,25 @@ class BanInfraction extends Infraction {
}
async resolve(...args) {
const member = await this.guild.memberWrapper(this.targetId);
const callback = await member.getCallback(this.type);
if (callback) this.client.moderationManager.removeCallback(callback);
const banned = await this.guild.bans.fetch(this.targetId).catch(() => null);
if (banned) {
const inf = await this.client.mongodb.infractions.findOne({
type: this.type, guild: this.guild.id, target: this.targetId
}, {
sort: { timestamp: -1 },
projection: { id: 1 }
});
// Don't unban unless the resolved case is the same one that banned them
if(inf.id === this.id) await this.guild.bans.remove(this.targetId, `Case ${this.case} resolve`);
}
return super.resolve(...args);
}
}
module.exports = BanInfraction;

View File

@ -8,26 +8,29 @@ class MuteInfraction extends Infraction {
constructor(client, opts = {}) {
super(client, {
targetType: 'USER',
type: opts.type,
invoker: opts.invoker,
executor: opts.executor.user,
target: opts.target.user,
reason: opts.reason,
guild: opts.guild,
channel: opts.channel,
arguments: opts.arguments,
silent: opts.silent,
duration: opts.duration,
points: opts.points,
expiration: opts.expiration,
data: opts.data,
hyperlink: opts.hyperlink
});
if (opts.fetched) super(client, opts);
else {
super(client, {
targetType: 'USER',
type: opts.type,
invoker: opts.invoker,
executor: opts.executor.user,
target: opts.target.user,
reason: opts.reason,
guild: opts.guild,
channel: opts.channel,
arguments: opts.arguments,
silent: opts.silent,
duration: opts.duration,
points: opts.points,
expiration: opts.expiration,
data: opts.data,
hyperlink: opts.hyperlink
});
if (!(opts.target instanceof GuildMember)) throw new Error('Guild member required');
this.member = opts.target;
if (!(opts.target instanceof GuildMember)) throw new Error('Guild member required');
this.member = opts.target;
}
}
@ -47,7 +50,7 @@ class MuteInfraction extends Infraction {
this.member.roles.add(role, this._reason);
} catch (e) {
this.client.logger.debug(`Mute fail, type 1:\n${e.stack}`);
return this._fail('C_MUTE_1FAIL');
return this._fail('COMMAND_MUTE_1FAIL');
}
break;
case 1:
@ -63,7 +66,7 @@ class MuteInfraction extends Infraction {
], this._reason);
} catch (error) {
this.client.logger.error(`Mute infraction failed to calculate removeable roles, might want to check this out.\n${error.stack || error}`);
return this._fail('C_MUTE_2FAIL');
return this._fail('COMMAND_MUTE_2FAIL');
}
break;
case 2:
@ -76,7 +79,7 @@ class MuteInfraction extends Infraction {
r.id === this.guild.id), this._reason);
} catch (error) {
this.client.logger.error(`Mute infraction failed to calculate removeable roles, might want to check this out.\n${error.stack || error}`);
return this._fail('C_MUTE_3FAIL');
return this._fail('COMMAND_MUTE_3FAIL');
}
break;
}
@ -89,7 +92,6 @@ class MuteInfraction extends Infraction {
const callback = this.client.moderationManager.callbacks.filter((c) => c.infraction.type === 'MUTE'
&& c.infraction.target === this.target.id).first();
// console.log(callback);
if (callback) {
this.data.removedRoles = [...new Set([...this.data.removedRoles, ...callback.infraction.data.removedRoles])];
@ -124,6 +126,60 @@ class MuteInfraction extends Infraction {
}
async resolve(...args) {
const inf = await this.client.mongodb.infractions.findOne({
type: this.type, guild: this.guild.id, target: this.targetId
}, {
sort: { timestamp: -1 },
projection: { id: 1 }
});
let message = null,
error = false;
const { removedRoles, muteType, muteRole } = this.data;
const member = await this.guild.memberWrapper(this.targetId);
const callback = await member.getCallback(this.type);
if (callback) this.client.moderationManager.removeCallback(callback);
if (inf.id === this.id && member) {
const reason = `Case ${this.case} resolve`;
const roles = [...new Set([...member.roles.cache.map((r) => r.id), ...removedRoles])];
switch (muteType) {
case 0:
try {
await member.roles.remove(muteRole, reason);
} catch (e) {
this.client.logger.debug(`Mute fail, type 1:\n${e.stack}`);
error = true;
message = this.guild.format('INFRACTION_RESOLVE_MUTE_FAIL1');
}
break;
case 1:
// eslint-disable-next-line no-case-declarations
const index = roles.indexOf(muteRole);
if (index >= 0) roles.splice(index, 1);
try {
await member.roles.set(roles, reason);
} catch (e) {
this.client.logger.debug(`Mute fail, type 2:\n${e.stack}`);
error = true;
message = this.guild.format('INFRACTION_RESOLVE_MUTE_FAIL23');
}
break;
case 2:
try {
await member.roles.set(roles, reason);
} catch (e) {
this.client.logger.debug(`Mute fail, type 3:\n${e.stack}`);
error = true;
message = this.guild.format('INFRACTION_RESOLVE_MUTE_FAIL23');
}
break;
}
}
return { result: await super.resolve(...args), message, error };
}
}
module.exports = MuteInfraction;

View File

@ -56,7 +56,7 @@ class Infraction {
this.data = data.data || {}; //Miscellaneous data that may need to be saved for future use.
this.flags = data.arguments ? Object.keys(data.arguments) : [];
this.hyperlink = data.hyperlink || null; // To overwrite hyperlink (if it's from a callback)
this._hyperlink = data.hyperlink || null; // To overwrite hyperlink (if it's from a callback)
this.modLogMessageId = null;
this.dmLogMessageId = null;
@ -106,7 +106,7 @@ class Infraction {
}
}
if (dminfraction.enabled) {
if (dminfraction.enabled && !this.silent) {
if (this.targetType === 'USER') {
let message = dminfraction.messages[this.type] || dminfraction.messages.default;
if (!message) message = '';
@ -147,20 +147,35 @@ class Infraction {
});
}
hyperlink(bool = false) {
if (bool) return `https://discord.com/channels/${this.guildId}/${this.modlogId}/${this.modLogMessageId}`;
hyperlink(modLogMessage = false) {
if(this._hyperlink) return this._hyperlink;
if (modLogMessage) return `https://discord.com/channels/${this.guildId}/${this.modlogId}/${this.modLogMessageId}`;
return `https://discord.com/channels/${this.guildId}/${this.channelId}/${this.messageId}`;
}
_embed(dm) {
const embed = {
author: {
name: `${this.target.displayName || this.target.username || this.target.name} (${this.targetId})`,
icon_url: this.targetIcon //eslint-disable-line camelcase
},
timestamp: this.timestamp,
color: this.color,
footer: {
text: `》 Case ${this.case}`
},
fields: []
};
let description = "";
description += `${this.guild.format('INFRACTION_DESCRIPTION', {
type: this.dictionary.past.toUpperCase(),
type: this.dictionary.past.toUpperCase(),
moderator: `${Util.escapeMarkdown(this.executor.tag)}`,
reason: Util.escapeMarkdown(this.reason.length > Constants.MaxCharacters ?
`${this.reason.substring(0, Constants.MaxCharacters - 3)}...` :
this.reason, { italic: false, underline: false, strikethrough: false })
this.reason, { italic: false, underline: false, strikethrough: false }),
// resolved: this.resolved ? this.guild.format('INFRACTION_RESOLVED') : ''
})}`;
if (this.duration) {
@ -175,29 +190,30 @@ class Infraction {
})}`;
}
// Function implemented in subclasses for additional case data
if (this.description && this.description instanceof Function) {
description += this.description(dm);
}
if (!this.silent && (this.message || this.hyperlink)) {
description += `\n${this.guild.format('INFRACTION_DESCRIPTIONJUMPTO', {
name: this.hyperlink ? 'Case' : 'Message',
link: this.hyperlink ? this.hyperlink : `https://discord.com/channels/${this.guildId}/${this.channelId}/${this.messageId}`
})}`;
if (this.resolved) {
description += '\n' + this.guild.format('INFRACTION_RESOLVED');
if (!dm) {
const resolveReason = this.changes.sort((a, b) => b.timestamp - a.timestamp).find((change) => change.type === 'RESOLVE');
embed.fields.push({
name: this.guild.format('INFRACTION_RESOLVE_REASON'),
value: resolveReason.reason || this.guild.format(`INFRACTION_RESOLVE_NO_REASON`)
});
}
}
const embed = {
author: {
name: `${this.target.displayName || this.target.username || this.target.name} (${this.targetId})`,
icon_url: this.targetIcon //eslint-disable-line camelcase
},
timestamp: this.timestamp,
color: this.color,
footer: {
text: `》 Case ${this.case}`
},
description
};
if (!dm && (this.message || this._hyperlink)) {
description += `\n${this.guild.format('INFRACTION_DESCRIPTIONJUMPTO', {
name: this._hyperlink ? 'Case' : 'Message',
link: this._hyperlink || this.hyperlink() //`https://discord.com/channels/${this.guildId}/${this.channelId}/${this.messageId}`
})}`;
}
embed.description = description;
return embed;
@ -398,12 +414,44 @@ class Infraction {
this.changes.push(log);
}
async fetch() { //Data from Mongodb (id-based data)
async resolve(staff, reason, notify) {
this.changes.push({
type: 'RESOLVE',
staff: staff.id,
timestamp: Date.now(),
reason
});
this.resolved = true;
await this.updateMessages();
await this.save();
if(notify && this.target) await this.target.send(`Your infraction **#${this.case}** on **${this.guild.name}** was resolved.`);
}
/**
* @param {boolean} [resolveToRightClass=false] Whether the function should instead return
* @return {*}
* @memberof Infraction
*/
async fetch(resolveToRightClass = false) { //Data from Mongodb (id-based data)
const data = await this.client.storageManager.mongodb.infractions.findOne({ id: this.id });
if(!data) throw new Error('No such case');
if (!data) throw new Error('No such case');
if (resolveToRightClass) {
const InfClass = this.client.moderationManager.infractionClasses[data.type];
const infraction = new InfClass(this.client, { fetched: true, guild: this.guild, case: this.case });
await infraction._patch(data);
return infraction;
}
this._patch(data);
return this;
}
async _patch(data) {
this._mongoId = ObjectId(data._id);
this._callbacked = data._callbacked;
this._fetched = true;
this.targetType = data.targetType;
this.targetId = data.target;
@ -416,7 +464,6 @@ class Infraction {
this.timestamp = data.timestamp;
this.duration = data.duration;
// this.callback = data.callback;
this.expiration = data.expiration;
this.points = data.points;
@ -439,10 +486,7 @@ class Infraction {
if (logChannel) this._modLogMessage = await logChannel.messages.fetch(data.modLogMessage).catch(() => null);
const dm = await this.target.createDM().catch(() => null);
if (dm) this._dmLogMessage = await dm.messages.fetch(data.dmLogMessage).catch(() => null);
this._fetched = true;
return this;
}
async _fetchTarget(target, type = null) {