Skip to content
This repository has been archived by the owner on Sep 8, 2024. It is now read-only.

Commit

Permalink
feat: new moderation system
Browse files Browse the repository at this point in the history
  • Loading branch information
danirod committed Oct 26, 2021
1 parent 0ff1c74 commit 738f187
Show file tree
Hide file tree
Showing 9 changed files with 564 additions and 119 deletions.
10 changes: 9 additions & 1 deletion src/Makibot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { HookManager } from "./lib/hook";
import { KarmaDatabase, openKarmaDatabase } from "./lib/karma/database";
import { SettingProvider } from "./lib/provider";
import { installCommandInteractionHandler } from "./lib/interaction";
import { ModerationRepository, newModRepository } from "./lib/modlog/database";

export default class Makibot extends Client {
readonly antiraid: AntiRaid;
Expand All @@ -18,6 +19,8 @@ export default class Makibot extends Client {

private _manager: HookManager;

private _modrepo: ModerationRepository;

public get manager(): HookManager {
return this._manager;
}
Expand Down Expand Up @@ -45,7 +48,8 @@ export default class Makibot extends Client {

this.once("ready", () => {
getDatabase()
.then((db) => {
.then(async (db) => {
this._modrepo = await newModRepository(db);
this._provider = new SettingProvider(db, this);
return this._provider.init();
})
Expand Down Expand Up @@ -81,6 +85,10 @@ export default class Makibot extends Client {
return this._provider;
}

get modrepo(): ModerationRepository {
return this._modrepo;
}

shutdown(exitCode = 0): void {
console.log("The bot was asked to shutdown.");
this.destroy();
Expand Down
44 changes: 44 additions & 0 deletions src/hooks/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Hook } from "../lib/hook";
import { applyAction } from "../lib/modlog/actions";
import { notifyPublicModlog } from "../lib/modlog/notifications";
import { ModEvent, ModEventType } from "../lib/modlog/types";
import Makibot from "../Makibot";

function castRevertType(event: ModEvent): ModEventType {
const types = {
WARN: "UNWARN",
MUTE: "UNMUTE",
};
return types[event.type];
}

function revertEvent(event: ModEvent): ModEvent {
return {
createdAt: new Date(),
expired: false,
guild: event.guild,
type: castRevertType(event),
mod: event.mod,
reason: "(expiración automática)",
target: event.target,
expiresAt: null,
};
}

export default class ModService implements Hook {
name = "moderation";

constructor(private client: Makibot) {
setInterval(() => this.cleanExpired(), 10000);
this.cleanExpired();
}

async cleanExpired(): Promise<void> {
const expired = await this.client.modrepo.retrieveExpired();
expired.forEach(async (event) => {
const reverseEvent = revertEvent(event);
const persisted = await applyAction(this.client, reverseEvent);
await notifyPublicModlog(this.client, persisted);
});
}
}
134 changes: 134 additions & 0 deletions src/interactions/commands/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { CommandInteraction } from "discord.js";
import { CommandInteractionHandler } from "../../lib/interaction";
import { applyAction } from "../../lib/modlog/actions";
import { notifyPublicModlog } from "../../lib/modlog/notifications";
import { ModEvent, ModEventType } from "../../lib/modlog/types";
import { createToast } from "../../lib/response";
import Server from "../../lib/server";
import Makibot from "../../Makibot";

/**
* Will coerce the subcommand of this command interaction into a valid type.
* While it should not happen, it is useful to test that the given subcommand
* name is one of the approved ones. Also, it is useful to cast this into a
* valid ModEventType for static typing purposes. If an invalid subcommand was
* given for some reason, this function will return null.
*
* @param command the received interaction
* @returns the coerced mod event type, or null if cannot match
*/
function castModEventType(command: CommandInteraction): ModEventType {
const modActions: { [type: string]: ModEventType } = {
warn: "WARN",
unwarn: "UNWARN",
mute: "MUTE",
unmute: "UNMUTE",
ban: "BAN",
kick: "KICK",
};
const subcommandName = command.options.getSubcommand();
return modActions[subcommandName] || null;
}

/**
* Will possibly get the given expiration timestamp parsing the command option.
* Some commands such as WARN and MUTE have an additional parameter regarding
* how much should the affected member wait until the moderation event expires.
* If this interaction had one of these parameters, the function will retrieve
* the proper value and convert it into the real expiration date.
*
* @param command the received interaction
* @returns either the Date object with the expiration date, or null
*/
function castExpirationDate(command: CommandInteraction): Date {
const keys = {
dia: 86400,
hora: 3600,
semana: 86400 * 7,
};
const givenDuration = command.options.getString("duracion", false) || "dia";
const duration = keys[givenDuration] || keys["dia"];
const expiresAt = Date.now() + duration * 1000;
return new Date(expiresAt);
}

/** Code to execute when invalid preconditions are met (user is not mod, etc). */
function replyNonModerator(interaction: CommandInteraction): Promise<void> {
return interaction.reply({
ephemeral: true,
embeds: [
createToast({
title: "Acción no permitida",
description: "No se puede aplicar el comando de moderación en este caso",
severity: "error",
target: interaction.user,
}),
],
});
}

function replyModerator(interaction: CommandInteraction): Promise<void> {
return interaction.reply({
ephemeral: true,
embeds: [
createToast({
title: "Acción de moderación aplicada correctamente",
target: interaction.user,
severity: "success",
}),
],
});
}

async function isValidModInteraction(event: CommandInteraction): Promise<boolean> {
const server = new Server(event.guild);
const originator = await server.member(event.user.id);
const target = await server.member(event.options.get("cuenta", true).value as string);
return event.inGuild() && originator.moderator && !target.moderator && !target.user.bot;
}

function translateInteractionToModEvent(event: CommandInteraction): ModEvent {
return {
createdAt: new Date(),
expired: false,
guild: event.guildId,
type: castModEventType(event),
mod: event.user.id,
reason: event.options.getString("razon", false) || null,
target: event.options.getUser("cuenta", true).id,
expiresAt: castExpirationDate(event),
};
}

function replyValidationError(event: CommandInteraction, error: string): Promise<void> {
return event.reply({
ephemeral: true,
embeds: [
createToast({
title: "Error al aplicar la acción",
description: error,
severity: "error",
target: event.user,
}),
],
});
}

export default class ModCommand implements CommandInteractionHandler {
name = "mod";

async handle(event: CommandInteraction): Promise<void> {
const valid = await isValidModInteraction(event);
if (valid) {
const modEvent = translateInteractionToModEvent(event);
await applyAction(event.client as Makibot, modEvent)
.then(async (persisted) => {
await notifyPublicModlog(event.client as Makibot, persisted);
replyModerator(event);
})
.catch((e: Error) => replyValidationError(event, e.message));
} else {
replyNonModerator(event);
}
}
}
45 changes: 0 additions & 45 deletions src/interactions/commands/unwarn.ts

This file was deleted.

73 changes: 0 additions & 73 deletions src/interactions/commands/warn.ts

This file was deleted.

Loading

0 comments on commit 738f187

Please sign in to comment.