From 0f69473c0596cba3e6b7e052f519ac1310614bcd Mon Sep 17 00:00:00 2001
From: Dragory <2606411+Dragory@users.noreply.github.com>
Date: Wed, 29 Jul 2020 00:28:04 +0300
Subject: [PATCH] Port CompanionChannels

---
 .../CompanionChannelsPlugin.ts                | 18 +++++
 .../events/VoiceChannelJoinEvt.ts             | 10 +++
 .../events/VoiceChannelLeaveEvt.ts            | 10 +++
 .../events/VoiceChannelSwitchEvt.ts           | 10 +++
 ...etCompanionChannelOptsForVoiceChannelId.ts | 17 +++++
 .../functions/handleCompanionPermissions.ts   | 65 +++++++++++++++++++
 .../src/plugins/CompanionChannels/types.ts    | 28 ++++++++
 backend/src/plugins/availablePlugins.ts       |  2 +
 8 files changed, 160 insertions(+)
 create mode 100644 backend/src/plugins/CompanionChannels/CompanionChannelsPlugin.ts
 create mode 100644 backend/src/plugins/CompanionChannels/events/VoiceChannelJoinEvt.ts
 create mode 100644 backend/src/plugins/CompanionChannels/events/VoiceChannelLeaveEvt.ts
 create mode 100644 backend/src/plugins/CompanionChannels/events/VoiceChannelSwitchEvt.ts
 create mode 100644 backend/src/plugins/CompanionChannels/functions/getCompanionChannelOptsForVoiceChannelId.ts
 create mode 100644 backend/src/plugins/CompanionChannels/functions/handleCompanionPermissions.ts
 create mode 100644 backend/src/plugins/CompanionChannels/types.ts

diff --git a/backend/src/plugins/CompanionChannels/CompanionChannelsPlugin.ts b/backend/src/plugins/CompanionChannels/CompanionChannelsPlugin.ts
new file mode 100644
index 00000000..1a59e170
--- /dev/null
+++ b/backend/src/plugins/CompanionChannels/CompanionChannelsPlugin.ts
@@ -0,0 +1,18 @@
+import { zeppelinPlugin } from "../ZeppelinPluginBlueprint";
+import { CompanionChannelsPluginType, ConfigSchema, TCompanionChannelOpts } from "./types";
+import { VoiceChannelJoinEvt } from "./events/VoiceChannelJoinEvt";
+import { VoiceChannelSwitchEvt } from "./events/VoiceChannelSwitchEvt";
+import { VoiceChannelLeaveEvt } from "./events/VoiceChannelLeaveEvt";
+
+const defaultOptions = {
+  config: {
+    entries: {},
+  },
+};
+
+export const CompanionChannelsPlugin = zeppelinPlugin<CompanionChannelsPluginType>()("companion_channels", {
+  configSchema: ConfigSchema,
+  defaultOptions,
+
+  events: [VoiceChannelJoinEvt, VoiceChannelSwitchEvt, VoiceChannelLeaveEvt],
+});
diff --git a/backend/src/plugins/CompanionChannels/events/VoiceChannelJoinEvt.ts b/backend/src/plugins/CompanionChannels/events/VoiceChannelJoinEvt.ts
new file mode 100644
index 00000000..042a6101
--- /dev/null
+++ b/backend/src/plugins/CompanionChannels/events/VoiceChannelJoinEvt.ts
@@ -0,0 +1,10 @@
+import { eventListener } from "knub";
+import { CompanionChannelsPluginType } from "../types";
+import { handleCompanionPermissions } from "../functions/handleCompanionPermissions";
+
+export const VoiceChannelJoinEvt = eventListener<CompanionChannelsPluginType>()(
+  "voiceChannelJoin",
+  ({ pluginData, args: { member, newChannel } }) => {
+    handleCompanionPermissions(pluginData, member.id, newChannel.id);
+  },
+);
diff --git a/backend/src/plugins/CompanionChannels/events/VoiceChannelLeaveEvt.ts b/backend/src/plugins/CompanionChannels/events/VoiceChannelLeaveEvt.ts
new file mode 100644
index 00000000..cf584d7e
--- /dev/null
+++ b/backend/src/plugins/CompanionChannels/events/VoiceChannelLeaveEvt.ts
@@ -0,0 +1,10 @@
+import { eventListener } from "knub";
+import { CompanionChannelsPluginType } from "../types";
+import { handleCompanionPermissions } from "../functions/handleCompanionPermissions";
+
+export const VoiceChannelLeaveEvt = eventListener<CompanionChannelsPluginType>()(
+  "voiceChannelLeave",
+  ({ pluginData, args: { member, oldChannel } }) => {
+    handleCompanionPermissions(pluginData, member.id, null, oldChannel.id);
+  },
+);
diff --git a/backend/src/plugins/CompanionChannels/events/VoiceChannelSwitchEvt.ts b/backend/src/plugins/CompanionChannels/events/VoiceChannelSwitchEvt.ts
new file mode 100644
index 00000000..fe382578
--- /dev/null
+++ b/backend/src/plugins/CompanionChannels/events/VoiceChannelSwitchEvt.ts
@@ -0,0 +1,10 @@
+import { eventListener } from "knub";
+import { CompanionChannelsPluginType } from "../types";
+import { handleCompanionPermissions } from "../functions/handleCompanionPermissions";
+
+export const VoiceChannelSwitchEvt = eventListener<CompanionChannelsPluginType>()(
+  "voiceChannelSwitch",
+  ({ pluginData, args: { member, oldChannel, newChannel } }) => {
+    handleCompanionPermissions(pluginData, member.id, newChannel.id, oldChannel.id);
+  },
+);
diff --git a/backend/src/plugins/CompanionChannels/functions/getCompanionChannelOptsForVoiceChannelId.ts b/backend/src/plugins/CompanionChannels/functions/getCompanionChannelOptsForVoiceChannelId.ts
new file mode 100644
index 00000000..fc9693f4
--- /dev/null
+++ b/backend/src/plugins/CompanionChannels/functions/getCompanionChannelOptsForVoiceChannelId.ts
@@ -0,0 +1,17 @@
+import { PluginData } from "knub";
+import { CompanionChannelsPluginType, TCompanionChannelOpts } from "../types";
+
+const defaultCompanionChannelOpts: Partial<TCompanionChannelOpts> = {
+  enabled: true,
+};
+
+export function getCompanionChannelOptsForVoiceChannelId(
+  pluginData: PluginData<CompanionChannelsPluginType>,
+  userId: string,
+  voiceChannelId: string,
+): TCompanionChannelOpts[] {
+  const config = pluginData.config.getMatchingConfig({ userId, channelId: voiceChannelId });
+  return Object.values(config.entries)
+    .filter(opts => opts.voice_channel_ids.includes(voiceChannelId))
+    .map(opts => Object.assign({}, defaultCompanionChannelOpts, opts));
+}
diff --git a/backend/src/plugins/CompanionChannels/functions/handleCompanionPermissions.ts b/backend/src/plugins/CompanionChannels/functions/handleCompanionPermissions.ts
new file mode 100644
index 00000000..528e0546
--- /dev/null
+++ b/backend/src/plugins/CompanionChannels/functions/handleCompanionPermissions.ts
@@ -0,0 +1,65 @@
+import { CompanionChannelsPluginType, TCompanionChannelOpts } from "../types";
+import { getCompanionChannelOptsForVoiceChannelId } from "./getCompanionChannelOptsForVoiceChannelId";
+import { PluginData } from "knub";
+import { TextChannel } from "eris";
+
+export function handleCompanionPermissions(
+  pluginData: PluginData<CompanionChannelsPluginType>,
+  userId: string,
+  voiceChannelId: string,
+  oldChannelId?: string,
+);
+export function handleCompanionPermissions(
+  pluginData: PluginData<CompanionChannelsPluginType>,
+  userId: string,
+  voiceChannelId: null,
+  oldChannelId: string,
+);
+export function handleCompanionPermissions(
+  pluginData: PluginData<CompanionChannelsPluginType>,
+  userId: string,
+  voiceChannelId?: string,
+  oldChannelId?: string,
+) {
+  const permsToDelete: Set<string> = new Set(); // channelId[]
+  const oldPerms: Map<string, number> = new Map(); // channelId => permissions
+  const permsToSet: Map<string, number> = new Map(); // channelId => permissions
+
+  const oldChannelOptsArr: TCompanionChannelOpts[] = oldChannelId
+    ? getCompanionChannelOptsForVoiceChannelId(pluginData, userId, oldChannelId)
+    : [];
+  const newChannelOptsArr: TCompanionChannelOpts[] = voiceChannelId
+    ? getCompanionChannelOptsForVoiceChannelId(pluginData, userId, voiceChannelId)
+    : [];
+
+  for (const oldChannelOpts of oldChannelOptsArr) {
+    for (const channelId of oldChannelOpts.text_channel_ids) {
+      oldPerms.set(channelId, oldChannelOpts.permissions);
+      permsToDelete.add(channelId);
+    }
+  }
+
+  for (const newChannelOpts of newChannelOptsArr) {
+    for (const channelId of newChannelOpts.text_channel_ids) {
+      if (oldPerms.get(channelId) !== newChannelOpts.permissions) {
+        // Update text channel perms if the channel we transitioned from didn't already have the same text channel perms
+        permsToSet.set(channelId, newChannelOpts.permissions);
+      }
+      if (permsToDelete.has(channelId)) {
+        permsToDelete.delete(channelId);
+      }
+    }
+  }
+
+  for (const channelId of permsToDelete) {
+    const channel = pluginData.guild.channels.get(channelId);
+    if (!channel || !(channel instanceof TextChannel)) continue;
+    channel.deletePermission(userId, `Companion Channel for ${oldChannelId} | User Left`);
+  }
+
+  for (const [channelId, permissions] of permsToSet) {
+    const channel = pluginData.guild.channels.get(channelId);
+    if (!channel || !(channel instanceof TextChannel)) continue;
+    channel.editPermission(userId, permissions, 0, "member", `Companion Channel for ${voiceChannelId} | User Joined`);
+  }
+}
diff --git a/backend/src/plugins/CompanionChannels/types.ts b/backend/src/plugins/CompanionChannels/types.ts
new file mode 100644
index 00000000..e27277f3
--- /dev/null
+++ b/backend/src/plugins/CompanionChannels/types.ts
@@ -0,0 +1,28 @@
+import * as t from "io-ts";
+import { tNullable } from "../../utils";
+import { BasePluginType } from "knub";
+import { GuildLogs } from "../../data/GuildLogs";
+import { GuildSavedMessages } from "../../data/GuildSavedMessages";
+
+// Permissions using these numbers: https://abal.moe/Eris/docs/reference (add all allowed/denied ones up)
+export const CompanionChannelOpts = t.type({
+  voice_channel_ids: t.array(t.string),
+  text_channel_ids: t.array(t.string),
+  permissions: t.number,
+  enabled: tNullable(t.boolean),
+});
+export type TCompanionChannelOpts = t.TypeOf<typeof CompanionChannelOpts>;
+
+export const ConfigSchema = t.type({
+  entries: t.record(t.string, CompanionChannelOpts),
+});
+export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
+
+export interface ICompanionChannelMap {
+  [channelId: string]: TCompanionChannelOpts;
+}
+
+export interface CompanionChannelsPluginType extends BasePluginType {
+  config: TConfigSchema;
+  state: {};
+}
diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts
index 2b7cc9ed..a77e18d5 100644
--- a/backend/src/plugins/availablePlugins.ts
+++ b/backend/src/plugins/availablePlugins.ts
@@ -27,6 +27,7 @@ import { SelfGrantableRolesPlugin } from "./SelfGrantableRoles/SelfGrantableRole
 import { SpamPlugin } from "./Spam/SpamPlugin";
 import { ReactionRolesPlugin } from "./ReactionRoles/ReactionRolesPlugin";
 import { AutomodPlugin } from "./Automod/AutomodPlugin";
+import { CompanionChannelsPlugin } from "./CompanionChannels/CompanionChannelsPlugin";
 
 // prettier-ignore
 export const guildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
@@ -57,6 +58,7 @@ export const guildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
   CasesPlugin,
   MutesPlugin,
   AutomodPlugin,
+  CompanionChannelsPlugin,
 ];
 
 // prettier-ignore