Browse Source

Start work on muting

ghorsington 4 years ago
parent
commit
c4a5edfe1f
3 changed files with 160 additions and 0 deletions
  1. 2 0
      bot/package.json
  2. 90 0
      bot/src/plugins/mute.ts
  3. 68 0
      bot/src/util.ts

+ 2 - 0
bot/package.json

@@ -28,6 +28,7 @@
    "dependencies": {
       "@google-cloud/translate": "^5.3.0",
       "@types/cheerio": "^0.22.18",
+      "@types/humanize-duration": "^3.18.0",
       "@types/koa": "^2.11.3",
       "@types/koa-router": "^7.4.1",
       "@types/lowdb": "^1.0.9",
@@ -43,6 +44,7 @@
       "form-data": "^3.0.0",
       "got": "^11.2.0",
       "html2bbcode": "^1.2.6",
+      "humanize-duration": "^3.23.0",
       "interval-promise": "^1.4.0",
       "jimp": "^0.12.1",
       "koa": "^2.12.0",

+ 90 - 0
bot/src/plugins/mute.ts

@@ -0,0 +1,90 @@
+import { Plugin, ICommandData, Command } from "src/model/plugin";
+import { parseArgs, tryDo, parseDuration, UNIT_MEASURES } from "src/util";
+import { GuildMember, Guild, MessageEmbed } from "discord.js";
+import { logger } from "src/logging";
+import { client } from "src/client";
+import humanizeDuration from "humanize-duration";
+
+const MENTION_PATTERN = /<@!?(\d+)>/;
+
+@Plugin
+export class MutePlugin {
+    @Command({
+        type: "prefix",
+        pattern: "mute",
+        auth: true
+    })
+    async muteUser({ message }: ICommandData): Promise<void> {
+        if (!message.guild) {
+            await message.reply("cannot do in DMs!");
+            return;
+        }
+        const [, userId, duration, ...rest] = parseArgs(message.content);
+
+        if (!userId) {
+            await message.reply("no user specified!");
+            return;
+        }
+
+        const user = await this.resolveUser(message.guild, userId);
+
+        if (!user) {
+            await message.reply("couldn't find the given user!");
+            logger.error("Tried to mute user %s but couldn't find them by id!", userId);
+            return;
+        }
+
+        let durationMs = parseDuration(duration);
+        let reasonArray = rest;
+
+        if (!durationMs) {
+            durationMs = UNIT_MEASURES.d;
+            reasonArray = [duration, ...reasonArray];
+        }
+
+        let reason = reasonArray.join(" ");
+
+        if (!reason)
+            reason = "None given";
+
+        message.channel.send(new MessageEmbed({
+            title: "User has been muted for server violation",
+            color: 4944347,
+            timestamp: new Date(),
+            footer: {
+                text: client.botUser.username
+            },
+            author: {
+                name: client.botUser.username,
+                iconURL: client.botUser.avatarURL() ?? undefined
+            },
+            fields: [
+                {
+                    name: "Username",
+                    value: user.toString()
+                },
+                {
+                    name: "Duration",
+                    value: humanizeDuration(durationMs, { unitMeasures: UNIT_MEASURES })
+                },
+                {
+                    name: "Reason",
+                    value: reason
+                }
+            ]
+        }));
+    }
+
+    private async resolveUser(guild: Guild, id: string): Promise<GuildMember | undefined> {
+        const result = MENTION_PATTERN.exec(id);
+        if (result) {
+            const userId = result[1];
+            const fetchResult = await tryDo(guild.members.fetch(userId));
+            if (fetchResult.ok)
+                return fetchResult.result;
+        }
+        
+        const fetchResult = await tryDo(guild.members.fetch(id));
+        return fetchResult.result;
+    }
+}

+ 68 - 0
bot/src/util.ts

@@ -2,6 +2,7 @@ import { GuildMember } from "discord.js";
 import { getRepository, In } from "typeorm";
 import { KnownUser } from "@shared/db/entity/KnownUser";
 import { HTTPError } from "got/dist/source";
+import humanizeDuration from "humanize-duration";
 
 const VALID_EXTENSIONS = new Set([
     "png",
@@ -105,4 +106,71 @@ export function isHttpError(err?: unknown): err is HTTPError {
 
 export function hasStackTrace(reason?: unknown): reason is {stack: unknown} {
     return reason && Object.prototype.hasOwnProperty.call(reason, "stack");
+}
+
+export function parseArgs(str: string): string[] {
+    const result: string[] = [];
+
+    let quoteMode = false;
+    let escapeNext = false;
+    let buffer = "";
+    for (const char of str) {
+        if (!escapeNext)
+            if (char == "\"") {
+                if (!quoteMode) {
+                    quoteMode = true;
+                } else {
+                    quoteMode = false;
+                    result.push(buffer);
+                    buffer = "";
+                }
+                continue;
+            } else if (char == "\\") {
+                escapeNext = true;
+                continue;
+            } else if (/\s/.test(char)) {
+                if (buffer.length == 0)
+                    continue;
+                if (!quoteMode) {
+                    result.push(buffer);
+                    buffer = "";
+                    continue;
+                }
+            }
+        buffer += char;
+        escapeNext = false;
+    }
+    if (buffer.length != 0)
+        result.push(buffer);
+
+    return result;
+}
+
+export const UNIT_MEASURES: humanizeDuration.UnitMeasuresOptions = {
+    ms: 1,
+    s: 1000,
+    m: 60 * 1000,
+    h: 60 * 60 * 1000,
+    d: 24 * 60 * 60 * 1000,
+    w: 7 * 24 * 60 * 60 * 1000,
+    mo: 30 * 24 * 60 * 60 * 1000,
+    y: 365 * 24 * 60 * 60 * 1000
+};
+const durationUnits = UNIT_MEASURES as Record<string, number>;
+const durationPattern = new RegExp(`(\\d+(?:[.,]?\\d+)?)\\s*(${Object.keys(durationUnits).sort((a, b) => b.length - a.length).join("|")})`, "g");
+
+export function parseDuration(s: string): number | undefined {
+    if (!s)
+        return undefined;
+    s = s.trim();
+    let buffer = s;
+    let result = 0;
+    let match;
+    while ((match = durationPattern.exec(s))) {
+        buffer = buffer.replace(match[0], "").trim();
+        result += +match[1] * durationUnits[match[2]];
+    }
+    if (buffer.length != 0)
+        return undefined;
+    return result;
 }