Rewrite how modpoints are handled

This commit is contained in:
Erik 2023-12-28 03:00:02 +02:00
parent bc787738ac
commit 0a2a37f23e
6 changed files with 172 additions and 139 deletions

View File

@ -23,7 +23,7 @@ import {
Warn Warn
} from '../infractions/index.js'; } from '../infractions/index.js';
import { CommandOption, Infraction as InfractionClass, Initialisable } from '../interfaces/index.js'; import { CommandOption, Infraction as InfractionClass, Initialisable } from '../interfaces/index.js';
import { InvokerWrapper, MemberWrapper, UserWrapper } from './wrappers/index.js'; import { GuildWrapper, InvokerWrapper, MemberWrapper, UserWrapper } from './wrappers/index.js';
import { InfractionFail, InfractionSuccess } from '../../../@types/Infractions.js'; import { InfractionFail, InfractionSuccess } from '../../../@types/Infractions.js';
@ -393,7 +393,7 @@ class ModerationManager implements Initialisable
if (infraction.targetType === 'CHANNEL') if (infraction.targetType === 'CHANNEL')
return verification; return verification;
const oldPoints = await targetUser!.totalPoints(guild); const oldPoints = await this.calculatePoints(targetUser!, guild);
if (infraction.points === null) if (infraction.points === null)
throw new Error('Null points when they should be defined'); throw new Error('Null points when they should be defined');
const newPoints = oldPoints + infraction.points; const newPoints = oldPoints + infraction.points;
@ -498,16 +498,16 @@ class ModerationManager implements Initialisable
{ {
if (!targetUser) if (!targetUser)
throw new Error('Missing target user for a user type infraction'); throw new Error('Missing target user for a user type infraction');
response.infraction.totalPoints = await targetUser.totalPoints(guild); response.infraction.totalPoints = await this.calculatePoints(targetUser, guild);
response.infraction.totalPoints += points; response.infraction.totalPoints += points;
} }
const result = await response.infraction.execute(); const result = await response.infraction.execute();
// Infraction doesn't have an ID until it is executed, hence this after execute // Infraction doesn't have an ID until it is executed, hence this after execute
if (response.infraction.targetType === 'USER') // if (response.infraction.targetType === 'USER')
await targetUser!.totalPoints(guild, { // await targetUser!.totalPoints(guild, {
points, expiration, timestamp: response.infraction.timestamp, id: response.infraction.id // points, expiration, timestamp: response.infraction.timestamp, id: response.infraction.id
}); // });
return result; return result;
} }
@ -537,7 +537,7 @@ class ModerationManager implements Initialisable
if (!undoClass) if (!undoClass)
return false; return false;
const guild = this.#client.getGuildWrapper(i.guild!); const guild = await this.#client.getGuildWrapper(i.guild!);
if (!guild) if (!guild)
throw new Error('Missing guild'); throw new Error('Missing guild');
// await guild.settings(); //just incase // await guild.settings(); //just incase
@ -636,7 +636,7 @@ class ModerationManager implements Initialisable
{ $set: { _callbacked: true } } { $set: { _callbacked: true } }
).catch((e) => ).catch((e) =>
{ {
this.#logger.error(`Error during update of infraction ${infraction.id}:\n${e.stack || e}\n${e.errInfo}`); this.#logger.error(`Error during update of infraction ${infraction.id}:\n${e.stack || e}\n${inspect(e.errInfo)}`);
}); });
const cb = this.#callbacks.get(infraction.id); const cb = this.#callbacks.get(infraction.id);
if (cb) if (cb)
@ -691,6 +691,29 @@ class ModerationManager implements Initialisable
return result || null; return result || null;
} }
async calculatePoints (user: UserWrapper, guild: GuildWrapper)
{
const [ result ] = await this.#client.mongodb.infractions.aggregate([{
$match: {
target: user.id,
guild: guild.id,
resolved: false,
$or: [{ expiration: { $gt: Date.now() } }, { expiration: 0 }, { expiration: null }],
}
}, {
$group: {
_id: null,
points: {
$sum: '$points'
}
}
}]);
let points = 0;
if (result)
({ points } = result);
return points;
}
} }
export default ModerationManager; export default ModerationManager;

View File

@ -23,7 +23,7 @@ class CaseCommand extends SlashCommand
minimum: 0, minimum: 0,
required: true required: true
}, { }, {
name: [ 'export', 'verbose', 'changes', 'delete' ], // name: [ 'raw', 'verbose', 'changes', 'delete' ], //
type: CommandOptionType.BOOLEAN, type: CommandOptionType.BOOLEAN,
description: [ description: [
'Print out raw infraction data in JSON form', 'Print out raw infraction data in JSON form',
@ -45,7 +45,7 @@ class CaseCommand extends SlashCommand
async execute (invoker: InvokerWrapper<true>, opts: CommandParams) async execute (invoker: InvokerWrapper<true>, opts: CommandParams)
{ {
const { id, verbose, export: exp, changes, delete: remove } = opts; const { id, verbose, raw, changes, delete: remove } = opts;
const guild = invoker.guild!; const guild = invoker.guild!;
const infraction = await new Infraction(this.client, this.logger, { guild, case: id!.asNumber }).fetch(true).catch(() => null); const infraction = await new Infraction(this.client, this.logger, { guild, case: id!.asNumber }).fetch(true).catch(() => null);
@ -62,9 +62,9 @@ class CaseCommand extends SlashCommand
}); });
return { emoji: 'success', index: 'COMMAND_CASE_DELETED', params: { caseID: id!.asNumber } }; return { emoji: 'success', index: 'COMMAND_CASE_DELETED', params: { caseID: id!.asNumber } };
} }
if (exp?.asString) if (raw?.asBool)
return `\`\`\`js\n${inspect(infraction)}\`\`\``; return `\`\`\`js\n${inspect(infraction.json)}\`\`\``;
if (changes?.asString) if (changes?.asBool)
return { embed: await this._listChanges(invoker, infraction.json) }; return { embed: await this._listChanges(invoker, infraction.json) };
const target = infraction.targetType === 'USER' ? await this.client.resolver.resolveUser(infraction.targetId!) : await guild.resolveChannel(infraction.targetId); const target = infraction.targetType === 'USER' ? await this.client.resolver.resolveUser(infraction.targetId!) : await guild.resolveChannel(infraction.targetId);

View File

@ -1,10 +1,10 @@
import { ImageURLOptions, MessageCreateOptions, MessagePayload, User } from 'discord.js'; import { ImageURLOptions, MessageCreateOptions, MessagePayload, User } from 'discord.js';
import DiscordClient from '../../DiscordClient.js'; import DiscordClient from '../../DiscordClient.js';
import { UserSettings } from '../../../../@types/Settings.js'; import { UserSettings } from '../../../../@types/Settings.js';
import GuildWrapper from './GuildWrapper.js'; // import GuildWrapper from './GuildWrapper.js';
import { LoggerClient } from '@navy.gif/logger'; import { LoggerClient } from '@navy.gif/logger';
type Expiry = {points: number, expiration: number, id: string} // type Expiry = {points: number, expiration: number, id: string}
class UserWrapper class UserWrapper
{ {
@ -12,12 +12,12 @@ class UserWrapper
#user: User; #user: User;
#settings: UserSettings; #settings: UserSettings;
#points: { // #points: {
[key: string]: { // [key: string]: {
expirations: Expiry[], // expirations: Expiry[],
points: number // points: number
} // }
}; // };
#logger: LoggerClient; #logger: LoggerClient;
@ -28,7 +28,7 @@ class UserWrapper
this.#logger = client.createLogger({ name: `User: ${user.id}` }); this.#logger = client.createLogger({ name: `User: ${user.id}` });
this.#settings = {}; this.#settings = {};
this.#points = {}; // this.#points = {};
} }
async settings (forceFetch = false) async settings (forceFetch = false)
@ -44,105 +44,105 @@ class UserWrapper
return this.#settings; return this.#settings;
} }
async fetchPoints (guild: GuildWrapper) // async fetchPoints (guild: GuildWrapper)
{ // {
let index = this.#points[guild.id]; // let index = this.#points[guild.id];
if (index) // if (index)
return Promise.resolve(index); // return Promise.resolve(index);
this.#points[guild.id] = { // this.#points[guild.id] = {
expirations: [], // expirations: [],
points: 0 // points: 0
}; // };
index = this.#points[guild.id]; // index = this.#points[guild.id];
const filter = { // const filter = {
guild: guild.id, // guild: guild.id,
target: this.id, // target: this.id,
resolved: false, // resolved: false,
// points: { $gte: 0 }, // // points: { $gte: 0 },
// $or: [{ expiration: 0 }, { expiration: { $gte: now } }] // // $or: [{ expiration: 0 }, { expiration: { $gte: now } }]
}; // };
const find = await this.#client.mongodb.infractions.find( // const find = await this.#client.mongodb.infractions.find(
filter, // filter,
{ projection: { id: 1, points: 1, expiration: 1 } } // { projection: { id: 1, points: 1, expiration: 1 } }
); // );
if (find.length) // if (find.length)
{ // {
for (const { points: p, expiration, id } of find) // for (const { points: p, expiration, id } of find)
{ // {
let points = p; // let points = p;
// Imported cases may have false or null // // Imported cases may have false or null
if (typeof points !== 'number') // if (typeof points !== 'number')
points = 0; // points = 0;
if (expiration > 0) // if (expiration > 0)
{ // {
index.expirations.push({ points, expiration, id }); // index.expirations.push({ points, expiration, id });
} // }
else // else
{ // {
index.points += points; // index.points += points;
} // }
} // }
} // }
return index; // return index;
} // }
async editPoints (guild: GuildWrapper, { id, diff, expiration }: {id: string, diff: number, expiration: unknown }) // async editPoints (guild: GuildWrapper, { id, diff, expiration }: {id: string, diff: number, expiration: unknown })
{ // {
const points = await this.fetchPoints(guild); // const points = await this.fetchPoints(guild);
if (expiration) // if (expiration)
{ // {
const expiry = points.expirations.find((exp) => exp.id === id) as Expiry; // const expiry = points.expirations.find((exp) => exp.id === id) as Expiry;
expiry.points += diff; // expiry.points += diff;
} // }
else // else
points.points += diff; // points.points += diff;
return this.totalPoints(guild); // return this.totalPoints(guild);
} // }
async editExpiration (guild: GuildWrapper, { id, expiration, points }: { id: string, expiration: number, points: number }) // async editExpiration (guild: GuildWrapper, { id, expiration, points }: { id: string, expiration: number, points: number })
{ // {
const index = await this.fetchPoints(guild); // const index = await this.fetchPoints(guild);
const i = index.expirations.findIndex((exp) => exp.id === id); // const i = index.expirations.findIndex((exp) => exp.id === id);
if (i > -1) // if (i > -1)
index.expirations[i].expiration = expiration; // index.expirations[i].expiration = expiration;
else // else
{ // {
index.points -= points; // index.points -= points;
index.expirations.push({ id, points, expiration }); // index.expirations.push({ id, points, expiration });
} // }
return this.totalPoints(guild); // return this.totalPoints(guild);
} // }
async totalPoints (guild: GuildWrapper, point?: { id: string, points: number, expiration: number, timestamp: number }) // async totalPoints (guild: GuildWrapper, point?: { id: string, points: number, expiration: number, timestamp: number })
{ // point = { points: x, expiration: x, timestamp: x} // { // point = { points: x, expiration: x, timestamp: x}
const index = await this.fetchPoints(guild); // const index = await this.fetchPoints(guild);
const now = Date.now(); // const now = Date.now();
if (point) // if (point)
{ // {
if (point.expiration > 0) // if (point.expiration > 0)
{ // {
index.expirations.push({ id: point.id, points: point.points, expiration: point.expiration + point.timestamp }); // index.expirations.push({ id: point.id, points: point.points, expiration: point.expiration + point.timestamp });
} // }
else // else
{ // {
index.points += point.points; // index.points += point.points;
} // }
} // }
let expirationPoints = index.expirations.map((e) => // let expirationPoints = index.expirations.map((e) =>
{ // {
if (e.expiration >= now) // if (e.expiration >= now)
return e.points; // return e.points;
return 0; // return 0;
}); // });
if (expirationPoints.length === 0) // if (expirationPoints.length === 0)
expirationPoints = [ 0 ]; // expirationPoints = [ 0 ];
return expirationPoints.reduce((p, v) => p + v) + index.points; // return expirationPoints.reduce((p, v) => p + v) + index.points;
} // }
async updateSettings (data: UserSettings) async updateSettings (data: UserSettings)
{ // Update property (upsert true) - updateOne { // Update property (upsert true) - updateOne

View File

@ -60,8 +60,6 @@ class UnmuteInfraction extends Infraction
now = Date.now(); now = Date.now();
let callback = null; let callback = null;
// TODO: Make this not rely on a member wrapper
const memberWrapper = await this.guild.memberWrapper(this.member!).catch(() => null);
if (Object.keys(this.data).length) if (Object.keys(this.data).length)
{ {
removedRoles = this.data.removedRoles!; removedRoles = this.data.removedRoles!;
@ -70,6 +68,8 @@ class UnmuteInfraction extends Infraction
} }
else else
{ {
// TODO: Make this not rely on a member wrapper
const memberWrapper = await this.guild.memberWrapper(this.member!).catch(() => null);
callback = await memberWrapper?.getCallback('MUTE'); callback = await memberWrapper?.getCallback('MUTE');
if (callback) if (callback)
{ {

View File

@ -143,7 +143,15 @@ class Infraction
this.#silent = data.silent || false; this.#silent = data.silent || false;
this.#points = data.points || 0; this.#points = data.points || 0;
this.#expiration = typeof data.expiration !== 'undefined' && data.expiration > 0 ? Date.now() + data.expiration : null; // Time when the points expire in milliseconds
// Null, not set, 0 never expires, otherwise timestamp of when it expires
if (typeof data.expiration === 'undefined')
this.#expiration = null;
else if (data.expiration > 0)
this.#expiration = Date.now() + data.expiration;
else
this.#expiration = 0;
// this.#expiration = typeof data.expiration !== 'undefined' && data.expiration > 0 ? Date.now() + data.expiration : null; // Time when the points expire in milliseconds
this.#totalPoints = 0; this.#totalPoints = 0;
this.#data = data.data || {}; // Miscellaneous data that may need to be saved for future use. this.#data = data.data || {}; // Miscellaneous data that may need to be saved for future use.
@ -652,7 +660,7 @@ class Infraction
return this._succeed(); return this._succeed();
} }
#error (reason?: string) #errorCheck (reason?: string)
{ {
if (!this.#fetched) if (!this.#fetched)
throw new Error(reason || 'Cannot edit an unfetched infraction'); throw new Error(reason || 'Cannot edit an unfetched infraction');
@ -660,7 +668,7 @@ class Infraction
async updateMessages () async updateMessages ()
{ {
this.#error('Cannot update messages for unfetched infractions'); this.#errorCheck('Cannot update messages for unfetched infractions');
// console.log(this._dmLogMessage, this.dmLogMessageId, this.dmLogMessage); // console.log(this._dmLogMessage, this.dmLogMessageId, this.dmLogMessage);
if (this.#dmLogMessage) if (this.#dmLogMessage)
await this.#dmLogMessage.edit({ embeds: [ await this.#embed(true) ] }); await this.#dmLogMessage.edit({ embeds: [ await this.#embed(true) ] });
@ -670,7 +678,7 @@ class Infraction
async editReason (staff: UserResolveable, reason: string): Promise<InfractionEditResult> async editReason (staff: UserResolveable, reason: string): Promise<InfractionEditResult>
{ {
this.#error(); this.#errorCheck();
const log: InfractionChange = { const log: InfractionChange = {
reason: this.#reason, reason: this.#reason,
staff: typeof staff === 'string' ? staff : staff.id, staff: typeof staff === 'string' ? staff : staff.id,
@ -683,7 +691,7 @@ class Infraction
async editPoints (staff: UserResolveable, points: number): Promise<InfractionEditResult> async editPoints (staff: UserResolveable, points: number): Promise<InfractionEditResult>
{ {
this.#error(); this.#errorCheck();
const settings = await this.#guild.settings(); const settings = await this.#guild.settings();
if (this.#targetType !== 'USER') if (this.#targetType !== 'USER')
return { error: true, index: 'INFRACTION_EDIT_INVALID_TARGETTYPE' }; return { error: true, index: 'INFRACTION_EDIT_INVALID_TARGETTYPE' };
@ -697,18 +705,19 @@ class Infraction
}; };
const diff = points - (this.#points ?? 0); const diff = points - (this.#points ?? 0);
this.#points = points; this.#points = points;
if (!this.#target) // if (!this.#target)
throw new Error('Missing target?'); // throw new Error('Missing target?');
const userWrapper = await this.#client.getUserWrapper(this.#target.id); // const userWrapper = await this.#client.getUserWrapper(this.#target.id);
if (!userWrapper) // if (!userWrapper)
throw new Error('Missing user wrapper'); // throw new Error('Missing user wrapper');
this.#totalPoints = await userWrapper.editPoints(this.#guild, { diff, id: this.id, expiration: this.#expiration }); // this.#totalPoints = await userWrapper.editPoints(this.#guild, { diff, id: this.id, expiration: this.#expiration });
this.#totalPoints += diff;
this.#changes.push(log); this.#changes.push(log);
} }
async editExpiration (staff: UserResolveable, expiration: number): Promise<InfractionEditResult> async editExpiration (staff: UserResolveable, expiration: number): Promise<InfractionEditResult>
{ {
this.#error(); this.#errorCheck();
const settings = await this.#guild.settings(); const settings = await this.#guild.settings();
if (this.#targetType !== 'USER') if (this.#targetType !== 'USER')
return { error: true, index: 'INFRACTION_EDIT_INVALID_TARGETTYPE' }; return { error: true, index: 'INFRACTION_EDIT_INVALID_TARGETTYPE' };
@ -724,22 +733,22 @@ class Infraction
expiration: this.#expiration ? this.#expiration - this.#timestamp : null expiration: this.#expiration ? this.#expiration - this.#timestamp : null
}; };
this.#expiration = expiration + this.#timestamp; this.#expiration = expiration + this.#timestamp;
if (!this.#target) // if (!this.#target)
throw new Error('Missing target?'); // throw new Error('Missing target?');
const userWrapper = await this.#client.getUserWrapper(this.#target.id); // const userWrapper = await this.#client.getUserWrapper(this.#target.id);
if (!userWrapper) // if (!userWrapper)
throw new Error('Missing user wrapper'); // throw new Error('Missing user wrapper');
this.#totalPoints = await userWrapper.editExpiration(this.#guild, { // this.#totalPoints = await userWrapper.editExpiration(this.#guild, {
id: this.id, // id: this.id,
expiration: this.#expiration, // expiration: this.#expiration,
points: this.#points // points: this.#points
}); // });
this.#changes.push(log); this.#changes.push(log);
} }
async editDuration (staff: UserResolveable, duration: number): Promise<InfractionEditResult> async editDuration (staff: UserResolveable, duration: number): Promise<InfractionEditResult>
{ {
this.#error(); this.#errorCheck();
if (this.#resolved) if (this.#resolved)
return { error: true, index: 'INFRACTION_EDIT_DURATION_RESOLVED' }; return { error: true, index: 'INFRACTION_EDIT_DURATION_RESOLVED' };
if (this.#callbacked) if (this.#callbacked)

View File

@ -228,6 +228,7 @@ const FilterPresets: {
"restarts", "restarts",
"restarted", "restarted",
"reskin", "reskin",
"refraction",
"rated", "rated",
"suspicious", "suspicious",
"racoon", "racoon",