Browse Source

Add ability to autoadd roles

ghorsington 3 years ago
parent
commit
fe185b4ffb

+ 10 - 0
bot/package-lock.json

@@ -2985,6 +2985,11 @@
          "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
          "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
       },
+      "fp-ts": {
+         "version": "2.10.4",
+         "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.10.4.tgz",
+         "integrity": "sha512-vMTB5zNc9PnE20q145PNbkiL9P9WegwmKVOFloi/NfHnPdAlcob6I3AKqlH/9u3k3/M/GOftZhcJdBrb+NtnDA=="
+      },
       "fragment-cache": {
          "version": "0.2.1",
          "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
@@ -3394,6 +3399,11 @@
          "resolved": "https://registry.npmjs.org/interval-promise/-/interval-promise-1.4.0.tgz",
          "integrity": "sha512-PUwEmGqUglJhb6M01JNvMDvxr4DA8FCeYoYCLHPEcBBZiq/8yOpCchfs1VJui7fXj69l170gAxzF1FeSA0nSlg=="
       },
+      "io-ts": {
+         "version": "2.2.16",
+         "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.16.tgz",
+         "integrity": "sha512-y5TTSa6VP6le0hhmIyN0dqEXkrZeJLeC5KApJq6VLci3UEKF80lZ+KuoUs02RhBxNWlrqSNxzfI7otLX1Euv8Q=="
+      },
       "ipaddr.js": {
          "version": "1.9.1",
          "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",

+ 2 - 0
bot/package.json

@@ -42,11 +42,13 @@
       "emoji-regex": "^9.2.2",
       "express": "^4.17.1",
       "form-data": "^3.0.1",
+      "fp-ts": "^2.10.4",
       "google-protobuf": "^3.15.8",
       "got": "^11.8.2",
       "html2bbcode": "^1.2.6",
       "humanize-duration": "^3.25.2",
       "interval-promise": "^1.4.0",
+      "io-ts": "^2.2.16",
       "jimp": "^0.16.1",
       "koa": "^2.13.1",
       "koa-body": "^4.2.0",

+ 1 - 1
bot/src/plugin_manager.ts

@@ -118,7 +118,7 @@ export class PluginManager {
             let matchData: string | RegExpMatchArray = "";
             if (typeof (c.pattern) == "string" && lowerCaseContent.startsWith(c.pattern)) {
                 match = true;
-                matchData = c.pattern;
+                matchData = content;
             }
             else if (c.pattern instanceof RegExp) {
                 const result = c.pattern.exec(content);

+ 162 - 0
bot/src/plugins/give_role_for_react.ts

@@ -0,0 +1,162 @@
+import { parseYaml } from "../util";
+import { Command, ICommandData, Plugin } from "src/model/plugin";
+import { tryDo } from "@shared/common/async_utils";
+import * as t from "io-ts";
+import { logger } from "src/logging";
+import { getRepository } from "typeorm";
+import { GiveRoleMessage } from "@shared/db/entity/GiveRoleMessage";
+import { Message, MessageReaction, ReactionCollector, ReactionEmoji, TextChannel, User } from "discord.js";
+import { client } from "src/client";
+
+const ReactRoleMessageParams = t.type({
+    message: t.string,
+    roleId: t.string
+});
+
+const REACT_EMOTE = "⭐";
+const MSG_COLOR = 9830318;
+
+@Plugin
+export class GiveRoleForReact {
+    @Command({
+        type: "mention",
+        pattern: "add role message",
+        documentation: {
+            description: "Add role giver message",
+            example: "add role message { message: \"This is a role!\", roleId: \"0237894783782\" }"
+        },
+        allowDM: false,
+        auth: true
+    })
+    async makeRoleMessage({ message, contents }: ICommandData): Promise<void> {
+        const textContent = (contents as string).substring("add role message".length).trim();
+        
+        tryDo(message.delete());
+
+        const opts = parseYaml(ReactRoleMessageParams, textContent);
+        if (!opts.ok) {
+            await this.sendDM(message.author, `Sorry, I don't understand the command! Got the following errors: ${opts.errors.join("\n")}`);
+            return;
+        }
+
+        const params = opts.result;
+
+        if (!message.guild?.roles.cache.has(params.roleId)) {
+            await this.sendDM(message.author, "Sorry, the role ID is not a valid role on the server!");
+            return;
+        }
+
+        const msgSendResult = await tryDo(message.channel.send({
+            embed: {
+                title: `React with ${REACT_EMOTE} to gain role`,
+                description: params.message,
+                color: MSG_COLOR
+            }
+        }));
+
+        if (!msgSendResult.ok) {
+            logger.error(`GiveRoleForReact: failed to create message because ${msgSendResult.error}`);
+            return;
+        }
+
+        const msg = msgSendResult.result;
+        await msg.react(REACT_EMOTE);
+        const roleGiveMessageRepo = getRepository(GiveRoleMessage);
+
+        if (!msg.guild) {
+            logger.error("GiveRoleForReact: tried to set up role react for DMs (this should never happen!)");
+            return;
+        }
+        
+        await roleGiveMessageRepo.save({
+            messageId: msg.id,
+            guildId: msg.guild.id,
+            channelId: msg.channel.id,
+            roleToGive: params.roleId
+        });
+
+        this.initReactCollector(msg, params.roleId);
+    }
+
+    private initReactCollector(msg: Message, role: string) {
+        const check = (reaction: MessageReaction) => {
+            return reaction.emoji.name == REACT_EMOTE;
+        };
+        const collector = new ReactionCollector(msg, check, {
+            dispose: true
+        });
+
+        const guild = msg.guild;
+        if (!guild) {
+            throw new Error("Tried to initialize role collector for non-guild channel.");
+        }
+
+        collector.on("collect", async (_, user) => {
+            if (user.bot) {
+                return;
+            }
+            const gu = guild.member(user);
+            if (!gu) {
+                return;
+            }
+            const result = await tryDo(gu.roles.add(role));
+            if (!result.ok) {
+                logger.error("GiveRoleForReact: Can't add role %s to user %s: %s", role, gu.id, result.error);
+            }
+        });
+
+        collector.on("remove", async (_, user) => {
+            if (user.bot) {
+                return;
+            }
+            const gu = guild.member(user);
+            if (!gu) {
+                return;
+            }
+            const result = await tryDo(gu.roles.remove(role));
+            if (!result.ok) {
+                logger.error("GiveRoleForReact: Can't remove role %s to user %s: %s", role, gu.id, result.error);
+            }
+        });
+    }
+
+    private async sendDM(usr: User, messageText: string) {
+        const dmResult = await tryDo(usr.createDM());
+        if (dmResult.ok) {
+            tryDo(dmResult.result.send(messageText));
+        }
+    }
+
+    async start(): Promise<void> {
+        logger.info("Initializing role give messages");
+        const roleGiveMessageRepo = getRepository(GiveRoleMessage);
+
+        const reactMessages = await roleGiveMessageRepo.find();
+
+        const staleEntities = [];
+        for (const reactMessage of reactMessages) {
+            const guildResult = await tryDo(client.bot.guilds.fetch(reactMessage.guildId));
+            if (!guildResult.ok) {
+                staleEntities.push(reactMessage);
+                continue;
+            }
+
+            const guild = guildResult.result;
+            const channel = guild.channels.resolve(reactMessage.channelId);
+            if (!channel || !(channel instanceof TextChannel)) {
+                staleEntities.push(reactMessage);
+                continue;
+            }
+
+            const msgResult = await tryDo(channel.messages.fetch(reactMessage.messageId));
+            if (!msgResult.ok) {
+                staleEntities.push(reactMessage);
+                continue;
+            }
+            
+            this.initReactCollector(msgResult.result, reactMessage.roleToGive);
+        }
+
+        await roleGiveMessageRepo.remove(staleEntities);
+    }
+}

+ 13 - 0
bot/src/util.ts

@@ -1,7 +1,11 @@
 import { GuildMember, User } from "discord.js";
 import { getRepository, In } from "typeorm";
 import { KnownUser } from "@shared/db/entity/KnownUser";
+import { Option, } from "@shared/common/async_utils";
 import humanizeDuration from "humanize-duration";
+import * as t from "io-ts";
+import YAML from "yaml";
+import { PathReporter } from "io-ts/PathReporter";
 
 const VALID_EXTENSIONS = new Set([
     "png",
@@ -19,6 +23,15 @@ export function isDevelopment(): boolean {
     return process.env.NODE_ENV == "dev";
 }
 
+export function parseYaml<A>(type: t.Type<A>, yaml: string): Option<{ result: A }, { errors: string[] }> {
+    const t = type.decode(YAML.parse(yaml));
+    if (t._tag == "Left"){
+        return { ok: false, errors: PathReporter.report(t) };
+    } else {
+        return { ok: true, result: t.right };
+    }
+}
+
 export function isValidImage(fileName?: string | null): boolean {
     if (!fileName)
         return false;

+ 2 - 0
shared/src/db/entities.ts

@@ -18,6 +18,7 @@ import { GuildGreeting } from "./entity/GuildGreeting";
 import { Violation, Mute, Kick, Ban } from "./entity/Violation";
 import { GuildViolationSettings } from "./entity/GuildViolationSettings";
 import { GuildVerification } from "./entity/GuildVerification";
+import { GiveRoleMessage } from "./entity/GiveRoleMessage";
 
 export const DB_ENTITIES = [
     AggroNewsItem,
@@ -25,6 +26,7 @@ export const DB_ENTITIES = [
     FaceCaptionMessage,
     Guide,
     GuideKeyword,
+    GiveRoleMessage,
     KnownChannel,
     KnownUser,
     User,

+ 16 - 0
shared/src/db/entity/GiveRoleMessage.ts

@@ -0,0 +1,16 @@
+import {Entity, PrimaryColumn, Column} from "typeorm";
+
+@Entity()
+export class GiveRoleMessage {
+    @PrimaryColumn()
+    messageId: string;
+
+    @PrimaryColumn()
+    guildId: string;
+
+    @PrimaryColumn()
+    channelId: string;
+
+    @Column()
+    roleToGive: string;
+}