From 168d82a966edbb49aa670645f446a6bf0f7c534b Mon Sep 17 00:00:00 2001
From: Dragory <2606411+Dragory@users.noreply.github.com>
Date: Sat, 22 Jun 2019 18:50:43 +0300
Subject: [PATCH] Add CustomEvents plugin

---
 src/plugins/CustomEvents.ts     | 190 ++++++++++++++++++++++++++++++++
 src/plugins/availablePlugins.ts |   2 +
 2 files changed, 192 insertions(+)
 create mode 100644 src/plugins/CustomEvents.ts

diff --git a/src/plugins/CustomEvents.ts b/src/plugins/CustomEvents.ts
new file mode 100644
index 00000000..e411e90b
--- /dev/null
+++ b/src/plugins/CustomEvents.ts
@@ -0,0 +1,190 @@
+import { ZeppelinPlugin } from "./ZeppelinPlugin";
+import { IPluginOptions } from "knub";
+import { Message, TextChannel, VoiceChannel } from "eris";
+import { renderTemplate } from "../templateFormatter";
+import { stripObjectToScalars } from "../utils";
+import { CasesPlugin } from "./Cases";
+import { CaseTypes } from "../data/CaseTypes";
+
+// Triggers
+type CommandTrigger = {
+  type: "command";
+  name: string;
+  params: string;
+  can_use: boolean;
+};
+
+type AnyTrigger = CommandTrigger;
+
+// Actions
+type AddRoleAction = {
+  type: "add_role";
+  target: string;
+  role: string | string[];
+};
+
+type CreateCaseAction = {
+  type: "create_case";
+  case_type: string;
+  mod: string;
+  target: string;
+  reason: string;
+};
+
+type MoveToVoiceChannelAction = {
+  type: "move_to_vc";
+  target: string;
+  channel: string;
+};
+
+type MessageAction = {
+  type: "message";
+  channel: string;
+  content: string;
+};
+
+type AnyAction = AddRoleAction | CreateCaseAction | MoveToVoiceChannelAction | MessageAction;
+
+// Event
+type CustomEvent = {
+  name: string;
+  trigger: AnyTrigger;
+  actions: AnyAction[];
+};
+
+interface ICustomEventsPluginConfig {
+  events: {
+    [key: string]: CustomEvent;
+  };
+}
+
+class ActionError extends Error {}
+
+export class CustomEventsPlugin extends ZeppelinPlugin<ICustomEventsPluginConfig> {
+  public static pluginName = "custom_events";
+  private clearTriggers: () => void;
+
+  public static dependencies = ["cases"];
+
+  getDefaultOptions(): IPluginOptions<ICustomEventsPluginConfig> {
+    return {
+      config: {
+        events: {},
+      },
+    };
+  }
+
+  onLoad() {
+    for (const [key, event] of Object.entries(this.getConfig().events)) {
+      if (event.trigger.type === "command") {
+        this.commands.add(
+          event.trigger.name,
+          event.trigger.params,
+          (msg, args) => {
+            const strippedMsg = stripObjectToScalars(msg, ["channel", "author"]);
+            this.runEvent(event, { msg, args }, { args, msg: strippedMsg });
+          },
+          {
+            requiredPermission: `events.${key}.trigger.can_use`,
+            locks: [],
+          },
+        );
+      }
+    }
+  }
+
+  onUnload() {
+    // TODO: Run this.clearTriggers() once we actually have something there
+  }
+
+  async runEvent(event: CustomEvent, eventData: any, values: any) {
+    try {
+      for (const action of event.actions) {
+        if (action.type === "add_role") {
+          await this.addRoleAction(action, values, event, eventData);
+        } else if (action.type === "create_case") {
+          await this.createCaseAction(action, values, event, eventData);
+        } else if (action.type === "move_to_vc") {
+          await this.moveToVoiceChannelAction(action, values, event, eventData);
+        } else if (action.type === "message") {
+          await this.messageAction(action, values);
+        }
+      }
+    } catch (e) {
+      if (e instanceof ActionError) {
+        if (event.trigger.type === "command") {
+          this.sendErrorMessage((eventData.msg as Message).channel, e.message);
+        } else {
+          // TODO: Where to log action errors from other kinds of triggers?
+        }
+
+        return;
+      }
+
+      throw e;
+    }
+  }
+
+  async addRoleAction(action: AddRoleAction, values: any, event: CustomEvent, eventData: any) {
+    const targetId = await renderTemplate(action.target, values, false);
+    const target = await this.getMember(targetId);
+    if (!target) throw new ActionError(`Unknown target member: ${targetId}`);
+
+    if (event.trigger.type === "command" && !this.canActOn((eventData.msg as Message).member, target)) {
+      throw new ActionError("Missing permissions");
+    }
+
+    const rolesToAdd = Array.isArray(action.role) ? action.role : [action.role];
+    await target.edit({
+      roles: Array.from(new Set([...target.roles, ...rolesToAdd])),
+    });
+  }
+
+  async createCaseAction(action: CreateCaseAction, values: any, event: CustomEvent, eventData: any) {
+    const modId = await renderTemplate(action.mod, values, false);
+    const targetId = await renderTemplate(action.target, values, false);
+
+    const reason = await renderTemplate(action.reason, values, false);
+
+    if (CaseTypes[action.case_type] == null) {
+      throw new ActionError(`Invalid case type: ${action.type}`);
+    }
+
+    const casesPlugin = this.getPlugin<CasesPlugin>("cases");
+    await casesPlugin.createCase({
+      userId: targetId,
+      modId: modId,
+      type: CaseTypes[action.case_type],
+      reason: `__[${event.name}]__ ${reason}`,
+    });
+  }
+
+  async moveToVoiceChannelAction(action: MoveToVoiceChannelAction, values: any, event: CustomEvent, eventData: any) {
+    const targetId = await renderTemplate(action.target, values, false);
+    const target = await this.getMember(targetId);
+    if (!target) throw new ActionError("Unknown target member");
+
+    if (event.trigger.type === "command" && !this.canActOn((eventData.msg as Message).member, target)) {
+      throw new ActionError("Missing permissions");
+    }
+
+    const targetChannelId = await renderTemplate(action.channel, values, false);
+    const targetChannel = this.guild.channels.get(targetChannelId);
+    if (!targetChannel) throw new ActionError("Unknown target channel");
+    if (!(targetChannel instanceof VoiceChannel)) throw new ActionError("Target channel is not a voice channel");
+
+    if (!target.voiceState.channelID) return;
+    await target.edit({
+      channelID: targetChannel.id,
+    });
+  }
+
+  async messageAction(action: MessageAction, values: any) {
+    const targetChannelId = await renderTemplate(action.channel, values, false);
+    const targetChannel = this.guild.channels.get(targetChannelId);
+    if (!targetChannel) throw new ActionError("Unknown target channel");
+    if (!(targetChannel instanceof TextChannel)) throw new ActionError("Target channel is not a text channel");
+
+    await targetChannel.createMessage({ content: action.content });
+  }
+}
diff --git a/src/plugins/availablePlugins.ts b/src/plugins/availablePlugins.ts
index 6082dc74..856b3519 100644
--- a/src/plugins/availablePlugins.ts
+++ b/src/plugins/availablePlugins.ts
@@ -21,6 +21,7 @@ import { WelcomeMessagePlugin } from "./WelcomeMessage";
 import { BotControlPlugin } from "./BotControl";
 import { LogServerPlugin } from "./LogServer";
 import { UsernameSaver } from "./UsernameSaver";
+import { CustomEventsPlugin } from "./CustomEvents";
 
 /**
  * Plugins available to be loaded for individual guilds
@@ -46,6 +47,7 @@ export const availablePlugins = [
   SelfGrantableRolesPlugin,
   RemindersPlugin,
   WelcomeMessagePlugin,
+  CustomEventsPlugin,
 ];
 
 /**