Skip to content

Commit

Permalink
Work on a moderation component abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremy-rifkin committed Aug 7, 2023
1 parent 6f6c1e0 commit 9f101c3
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 11 deletions.
163 changes: 163 additions & 0 deletions src/components/moderation/moderation-common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import * as Discord from "discord.js";

import { strict as assert } from "assert";

import { critical_error, unwrap } from "../../utils.js";
import { BotComponent } from "../../bot-component.js";
import { TextBasedCommand } from "../../command.js";
import { Wheatley } from "../../wheatley.js";

import * as mongo from "mongodb";

export type moderation_type = "mute" | "warn" | "ban" | "kick" | "no off-topic" | "rolepersist";

export type moderation_entry = {
case_number: number;
user: string;
user_name: string;
moderator: string;
moderator_name: string;
type: moderation_type;
reason: string | null;
issued_at: number; // milliseconds since epoch
duration: number; // milliseconds
active: boolean;
removal_mod?: string;
removal_mod_name?: string;
removal_timestamp?: number; // milliseconds since epoch
removal_reason?: string | null;
};

export const duration_regex = /(?:perm\b|\d+\s*[mhdwMy])/;

const INT_MAX = 0x7fffffff;

export function parse_duration(duration: string) {
// TODO
return 0;
}

export abstract class ModerationComponent extends BotComponent {
abstract get type(): moderation_type;

// Sorted by moderation end time
sleep_list: mongo.WithId<moderation_entry>[] = [];
timer: NodeJS.Timer | null = null;

constructor(wheatley: Wheatley) {
super(wheatley);
}

override async on_ready() {
// TODO: Implement catch-up / ensuring moderations are in place
const moderations = await this.wheatley.database.moderations.find({ type: "mute", active: true }).toArray();
if (moderations.length > 0) {
this.sleep_list = moderations.sort((a, b) => a.issued_at + a.duration - (b.issued_at + b.duration));
this.set_timer();
}
}

async handle_timer() {
this.timer = null;
try {
// sanity checks
assert(this.sleep_list.length > 0);
if (this.sleep_list[0].issued_at + this.sleep_list[0].duration > Date.now()) {
// can happen under excessively long sleeps
assert(this.sleep_list[0].duration > INT_MAX);
this.set_timer(); // set next timer
return;
}
// pop entry and remove role
const entry = this.sleep_list.shift()!;
await this.remove_moderation(entry);
// remove database entry
await this.wheatley.database.moderations.updateOne(
{ _id: entry._id },
{
$set: {
active: false,
removal_mod: this.wheatley.id,
removal_mod_name: "Wheatley",
removal_reason: "Auto",
removal_timestamp: Date.now(),
},
},
);
// reschedule, intentionally not rescheduling
if (this.sleep_list.length > 0) {
this.set_timer();
}
} catch (e) {
critical_error(e);
}
}

abstract add_moderation(entry: mongo.WithId<moderation_entry>): Promise<void>;
abstract remove_moderation(entry: mongo.WithId<moderation_entry>): Promise<void>;

set_timer() {
assert(this.timer == null);
assert(this.sleep_list.length > 0);
const next = this.sleep_list[0];
// next.issued_at + next.duration - Date.now() but make sure overflow is prevented
const sleep_time = next.issued_at - Date.now() + next.duration;
this.timer = setTimeout(
() => {
this.handle_timer().catch(critical_error);
},
Math.min(sleep_time, INT_MAX),
);
}

async register_moderation(moderation: mongo.WithId<moderation_entry>) {
// TODO
void 0;
}

async moderation_handler(command: TextBasedCommand, user: Discord.User, duration: string, reason: string) {
// TODO: Permissions?
try {
await this.wheatley.database.lock();
const case_number = unwrap(
(
await this.wheatley.database.wheatley.findOneAndUpdate(
{ id: "main" },
{
$inc: {
moderation_case_number: 1,
},
},
{
returnDocument: "after",
},
)
).value,
).moderation_case_number;
const member = await this.wheatley.TCCPP.members.fetch(command.user.id);
const document: moderation_entry = {
case_number,
user: user.id,
user_name: user.displayName,
moderator: command.user.id,
moderator_name: member.displayName,
type: this.type,
reason,
issued_at: Date.now(),
duration: parse_duration(duration),
active: true,
};
const res = await this.wheatley.database.moderations.insertOne(document);
await this.add_moderation({
_id: res.insertedId,
...document,
});
await this.register_moderation({
_id: res.insertedId,
...document,
});
} finally {
this.wheatley.database.unlock();
}
}
}
27 changes: 16 additions & 11 deletions src/components/moderation/mute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ import * as Discord from "discord.js";

import { strict as assert } from "assert";

import { M } from "../../utils.js";
import { colors } from "../../common.js";
import { BotComponent } from "../../bot-component.js";
import { M, unwrap } from "../../utils.js";
import { Wheatley } from "../../wheatley.js";
import { TextBasedCommand, TextBasedCommandBuilder } from "../../command.js";
import { TextBasedCommandBuilder } from "../../command.js";
import { ModerationComponent, duration_regex, moderation_entry, moderation_type } from "./moderation-common.js";

import * as mongo from "mongodb";

/**
* Implements !mute
*/
export default class Mute extends BotComponent {
static override get is_freestanding() {
return true;
export default class Mute extends ModerationComponent {
get type(): moderation_type {
return "mute";
}

constructor(wheatley: Wheatley) {
Expand All @@ -30,19 +31,23 @@ export default class Mute extends BotComponent {
.add_string_option({
title: "duration",
description: "Duration",
regex: /(?:perm\b|\d+\s*[mhdwMy])/,
regex: duration_regex,
required: true,
})
.add_string_option({
title: "reason",
description: "Reason",
required: true,
})
.set_handler(this.handler.bind(this)),
.set_handler(this.moderation_handler.bind(this)),
);
}

async handler(command: TextBasedCommand, user: Discord.User, time: string, reason: string) {
await command.reply(JSON.stringify([user.displayName, time, reason]));
async add_moderation(entry: mongo.WithId<moderation_entry>) {
// TODO
}

async remove_moderation(entry: mongo.WithId<moderation_entry>) {
// TODO
}
}
2 changes: 2 additions & 0 deletions src/components/nodistractions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { TextBasedCommand, TextBasedCommandBuilder } from "../command.js";
* - Remove role and database entry
*/

// TODO: Rephrase in terms of a moderation component

function parse_unit(u: string) {
let factor = 1000; // in ms
switch (u) {
Expand Down
3 changes: 3 additions & 0 deletions src/infra/database-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { auto_delete_threshold_notifications, starboard_entry } from "../compone
import { button_scoreboard_entry } from "../components/the-button.js";
import { TRACKER_START_TIME, suggestion_entry } from "../components/server-suggestion-tracker.js";
import { link_blacklist_entry } from "../private-types.js";
import { moderation_entry } from "../components/moderation/moderation-common.js";

export class WheatleyDatabase {
private mutex = new Mutex();
Expand Down Expand Up @@ -74,6 +75,7 @@ export class WheatleyDatabase {
ignored_emojis: [],
negative_emojis: [],
},
moderation_case_number: 0,
};
const ires = await wheatley.insertOne(document);
assert(ires.acknowledged);
Expand Down Expand Up @@ -116,6 +118,7 @@ export type WheatleyDatabaseProxy = WheatleyDatabase & {
server_suggestions: mongo.Collection<suggestion_entry>;
starboard_entries: mongo.Collection<starboard_entry>;
wheatley: mongo.Collection<wheatley_db_info>;
moderations: mongo.Collection<moderation_entry>;
};
// & {
// [key: string] : Promise<mongo.Collection>
Expand Down
1 change: 1 addition & 0 deletions src/wheatley.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export type wheatley_db_info = {
ignored_emojis: string[];
negative_emojis: string[];
};
moderation_case_number: number;
};

export class Wheatley extends EventEmitter {
Expand Down

0 comments on commit 9f101c3

Please sign in to comment.