From 7c757d4b96ebcd4dec908d266193278143bdb1be Mon Sep 17 00:00:00 2001 From: Dark <7890309+DarkView@users.noreply.github.com> Date: Sun, 6 Jun 2021 04:57:05 +0200 Subject: [PATCH] WIP: Button Reactions This still needs some cleanup and some functionality straight up doesn't work or is only a POC --- .../ReactionRoles/ReactionRolesPlugin.ts | 5 ++ .../commands/PostButtonRolesCmd.ts | 65 +++++++++++++++ .../events/ButtonInteractionEvt.ts | 80 +++++++++++++++++++ backend/src/plugins/ReactionRoles/types.ts | 23 ++++++ .../ReactionRoles/util/buttonMenuActions.ts | 4 + 5 files changed, 177 insertions(+) create mode 100644 backend/src/plugins/ReactionRoles/commands/PostButtonRolesCmd.ts create mode 100644 backend/src/plugins/ReactionRoles/events/ButtonInteractionEvt.ts create mode 100644 backend/src/plugins/ReactionRoles/util/buttonMenuActions.ts diff --git a/backend/src/plugins/ReactionRoles/ReactionRolesPlugin.ts b/backend/src/plugins/ReactionRoles/ReactionRolesPlugin.ts index ff8d6330..7b7b55de 100644 --- a/backend/src/plugins/ReactionRoles/ReactionRolesPlugin.ts +++ b/backend/src/plugins/ReactionRoles/ReactionRolesPlugin.ts @@ -10,11 +10,14 @@ import { RefreshReactionRolesCmd } from "./commands/RefreshReactionRolesCmd"; import { ClearReactionRolesCmd } from "./commands/ClearReactionRolesCmd"; import { AddReactionRoleEvt } from "./events/AddReactionRoleEvt"; import { LogsPlugin } from "../Logs/LogsPlugin"; +import { PostButtonRolesCmd } from "./commands/PostButtonRolesCmd"; +import { ButtonInteractionEvt } from "./events/ButtonInteractionEvt"; const MIN_AUTO_REFRESH = 1000 * 60 * 15; // 15min minimum, let's not abuse the API const defaultOptions: PluginOptions = { config: { + button_groups: {}, auto_refresh_interval: MIN_AUTO_REFRESH, remove_user_reactions: true, @@ -47,11 +50,13 @@ export const ReactionRolesPlugin = zeppelinGuildPlugin( RefreshReactionRolesCmd, ClearReactionRolesCmd, InitReactionRolesCmd, + PostButtonRolesCmd, ], // prettier-ignore events: [ AddReactionRoleEvt, + ButtonInteractionEvt, ], beforeLoad(pluginData) { diff --git a/backend/src/plugins/ReactionRoles/commands/PostButtonRolesCmd.ts b/backend/src/plugins/ReactionRoles/commands/PostButtonRolesCmd.ts new file mode 100644 index 00000000..6dc4f2fd --- /dev/null +++ b/backend/src/plugins/ReactionRoles/commands/PostButtonRolesCmd.ts @@ -0,0 +1,65 @@ +import { reactionRolesCmd } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { MessageActionRow, MessageButton, MessageComponentInteraction, TextChannel } from "discord.js"; +import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; +import moment from "moment"; +import { ButtonMenuActions } from "../util/buttonMenuActions"; + +export const PostButtonRolesCmd = reactionRolesCmd({ + trigger: "reaction_roles post", + permission: "can_manage", + + signature: { + button_group: ct.string(), + }, + + async run({ message: msg, args, pluginData }) { + const cfg = pluginData.config.get(); + const group = cfg.button_groups[args.button_group]; + + if (!group) { + sendErrorMessage(pluginData, msg.channel, `No button group matches the name **${args.button_group}**`); + } + + const channel = pluginData.guild.channels.resolve(group.channel_id); + if (!channel) { + await sendErrorMessage( + pluginData, + msg.channel, + `The ID ${group.channel_id} does not match a channel on the server`, + ); + return; + } + + const buttons: MessageButton[] = []; + for (const button of Object.values(group.default_buttons)) { + let customId = ""; + if ((await pluginData.guild.roles.fetch(button.role_or_menu)) != null) { + // TODO: Make universal, currently can only handle custom emoji and not default ones + customId = `${args.button_group}::${ButtonMenuActions.GRANT_ROLE}::${button.role_or_menu}`; + } else { + customId = `${args.button_group}::${ButtonMenuActions.OPEN_MENU}::${button.role_or_menu}`; + } + + const btn = new MessageButton() + .setLabel(button.label) + .setStyle("PRIMARY") + .setType("BUTTON") + .setCustomID(customId); + + const emo = pluginData.client.emojis.resolve(button.emoji); + if (emo) btn.setEmoji(emo); + + buttons.push(btn); + } + const row = new MessageActionRow().addComponents(buttons); + + try { + await (channel as TextChannel).send({ content: group.message, components: [row], split: false }); + } catch (e) { + sendErrorMessage(pluginData, msg.channel, `Error trying to post message: ${e}`); + return; + } + await sendSuccessMessage(pluginData, msg.channel, `Successfully posted message in <#${channel.id}>`); + }, +}); diff --git a/backend/src/plugins/ReactionRoles/events/ButtonInteractionEvt.ts b/backend/src/plugins/ReactionRoles/events/ButtonInteractionEvt.ts new file mode 100644 index 00000000..b37b89d8 --- /dev/null +++ b/backend/src/plugins/ReactionRoles/events/ButtonInteractionEvt.ts @@ -0,0 +1,80 @@ +import { Interaction, MessageComponentInteraction, MessageComponentInteractionCollector } from "discord.js"; +import { LogType } from "src/data/LogType"; +import { pluginInfo } from "src/plugins/Automod/info"; +import { LogsPlugin } from "src/plugins/Logs/LogsPlugin"; +import { reactionRolesEvt } from "../types"; +import { ButtonMenuActions } from "../util/buttonMenuActions"; + +export const ButtonInteractionEvt = reactionRolesEvt({ + event: "interaction", + + async listener(meta) { + const int = meta.args.interaction.isMessageComponent() + ? (meta.args.interaction as MessageComponentInteraction) + : null; + if (!int) return; + const cfg = meta.pluginData.config.get(); + const split = int.customID.split("::"); + const [groupName, action, roleOrMenu] = [split[0], split[1], split[2]]; + + const group = cfg.button_groups[groupName]; + if (!group) { + await sendEphemeralReply(int, `A configuration error was encountered, please contact the Administrators!`); + meta.pluginData + .getPlugin(LogsPlugin) + .log( + LogType.BOT_ALERT, + `**A configuration error occured** on buttons for message ${int.message.id}, group **${groupName}** not found in config`, + ); + return; + } + + // Verify that detected action is known by us + if (!(Object).values(ButtonMenuActions).includes(action)) { + await sendEphemeralReply(int, `A internal error was encountered, please contact the Administrators!`); + meta.pluginData + .getPlugin(LogsPlugin) + .log( + LogType.BOT_ALERT, + `**A internal error occured** on buttons for message ${int.message.id}, action **${action}** is not known`, + ); + return; + } + + if (action === ButtonMenuActions.GRANT_ROLE) { + const role = await meta.pluginData.guild.roles.fetch(roleOrMenu); + if (!role) { + await sendEphemeralReply(int, `A configuration error was encountered, please contact the Administrators!`); + meta.pluginData + .getPlugin(LogsPlugin) + .log( + LogType.BOT_ALERT, + `**A configuration error occured** on buttons for message ${int.message.id}, group **${groupName}** not found in config`, + ); + return; + } + + const member = await meta.pluginData.guild.members.fetch(int.user.id); + if (member.roles.cache.has(role.id)) { + await member.roles.remove(role, `Button Roles on message ${int.message.id}`); + await sendEphemeralReply(int, `You have removed the role <@&${role.id}>`); + } else { + await member.roles.add(role, `Button Roles on message ${int.message.id}`); + await sendEphemeralReply(int, `You have added the role <@&${role.id}>`); + } + + return; + } + + // TODO: Send ephemeral reply with buttons that are part of the selected menu + if (action === ButtonMenuActions.OPEN_MENU) { + console.log("Disable TSLint error"); + } + + await sendEphemeralReply(int, split.join("\n")); // TODO: Remove debug output + }, +}); + +async function sendEphemeralReply(interaction: MessageComponentInteraction, message: string) { + await interaction.reply(message, { ephemeral: true }); +} diff --git a/backend/src/plugins/ReactionRoles/types.ts b/backend/src/plugins/ReactionRoles/types.ts index 36fed600..d0b2e5d9 100644 --- a/backend/src/plugins/ReactionRoles/types.ts +++ b/backend/src/plugins/ReactionRoles/types.ts @@ -4,7 +4,30 @@ import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildReactionRoles } from "../../data/GuildReactionRoles"; import { Queue } from "../../Queue"; +const ButtonOpts = t.type({ + label: t.string, + emoji: t.string, + role_or_menu: t.string, +}); +export type TButtonOpts = t.TypeOf; + +const MenuButtonOpts = t.type({ + label: t.string, + emoji: t.string, + role: t.string, +}); +export type TMenuButtonOpts = t.TypeOf; + +const ButtonPairOpts = t.type({ + channel_id: t.string, + message: t.string, + default_buttons: t.record(t.string, ButtonOpts), + button_menus: t.record(t.string, t.record(t.string, MenuButtonOpts)), +}); +export type TButtonPairOpts = t.TypeOf; + export const ConfigSchema = t.type({ + button_groups: t.record(t.string, ButtonPairOpts), auto_refresh_interval: t.number, remove_user_reactions: t.boolean, can_manage: t.boolean, diff --git a/backend/src/plugins/ReactionRoles/util/buttonMenuActions.ts b/backend/src/plugins/ReactionRoles/util/buttonMenuActions.ts new file mode 100644 index 00000000..3edcca0d --- /dev/null +++ b/backend/src/plugins/ReactionRoles/util/buttonMenuActions.ts @@ -0,0 +1,4 @@ +export enum ButtonMenuActions { + OPEN_MENU = "goto", + GRANT_ROLE = "grant", +}