From 56f0d105be42dbb137fb7c05650f3b2dfd440381 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Fri, 26 Jan 2024 20:59:05 +0000 Subject: [PATCH] Create button event to gain the server access role (#398) # Description Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. - Add the ability for button event interactions to be created - Added a button event interaction which gives a user the role which is specified in the config `verification.role` - Added a command to generate a button to activate the verify button event #232 ## Type of change Please delete options that are not relevant. - [x] New feature (non-breaking change which adds functionality) # How Has This Been Tested? Please describe the tests that you ran to verify the changes. Provide instructions so we can reproduce. Please also list any relevant details to your test configuration. # Checklist - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that provde my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules Reviewed-on: https://gitea.vylpes.xyz/RabbitLabs/vylbot-app/pulls/398 Reviewed-by: VylpesTester Co-authored-by: Ethan Lane Co-committed-by: Ethan Lane --- src/buttonEvents/verify.ts | 42 ++++++++++++++ src/client/client.ts | 17 ++++++ src/client/events.ts | 34 ++--------- src/client/interactionCreate/button.ts | 17 ++++++ .../interactionCreate/chatInputCommand.ts | 27 +++++++++ src/commands/rules.ts | 57 +++++++++++++++++-- src/constants/DefaultValues.ts | 1 + src/contracts/ButtonEventItem.ts | 8 +++ src/registry.ts | 7 +++ src/type/buttonEvent.ts | 5 ++ src/vylbot.ts | 1 + 11 files changed, 183 insertions(+), 33 deletions(-) create mode 100644 src/buttonEvents/verify.ts create mode 100644 src/client/interactionCreate/button.ts create mode 100644 src/client/interactionCreate/chatInputCommand.ts create mode 100644 src/contracts/ButtonEventItem.ts create mode 100644 src/type/buttonEvent.ts diff --git a/src/buttonEvents/verify.ts b/src/buttonEvents/verify.ts new file mode 100644 index 0000000..c3f696c --- /dev/null +++ b/src/buttonEvents/verify.ts @@ -0,0 +1,42 @@ +import { ButtonInteraction, CacheType } from "discord.js"; +import { ButtonEvent } from "../type/buttonEvent"; +import SettingsHelper from "../helpers/SettingsHelper"; + +export default class Verify extends ButtonEvent { + public override async execute(interaction: ButtonInteraction) { + if (!interaction.guildId || !interaction.guild) return; + + const roleName = await SettingsHelper.GetSetting("verification.role", interaction.guildId); + + if (!roleName) return; + + const role = interaction.guild.roles.cache.find(x => x.name == roleName); + + if (!role) { + await interaction.reply({ + content: `Unable to find the role, ${roleName}`, + ephemeral: true, + }); + + return; + } + + const member = interaction.guild.members.cache.find(x => x.id == interaction.user.id); + + if (!member || !member.manageable) { + await interaction.reply({ + content: "Unable to give role to user", + ephemeral: true, + }); + + return; + } + + await member.roles.add(role); + + await interaction.reply({ + content: "Given role", + ephemeral: true, + }); + } +} \ No newline at end of file diff --git a/src/client/client.ts b/src/client/client.ts index 9e8f4ef..415d645 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -9,10 +9,13 @@ import { Command } from "../type/command"; import { Events } from "./events"; import { Util } from "./util"; import AppDataSource from "../database/dataSources/appDataSource"; +import ButtonEventItem from "../contracts/ButtonEventItem"; +import { ButtonEvent } from "../type/buttonEvent"; export class CoreClient extends Client { private static _commandItems: ICommandItem[]; private static _eventItems: IEventItem[]; + private static _buttonEvents: ButtonEventItem[]; private _events: Events; private _util: Util; @@ -25,12 +28,17 @@ export class CoreClient extends Client { return this._eventItems; } + public static get buttonEvents(): ButtonEventItem[] { + return this._buttonEvents; + } + constructor(intents: number[], partials: Partials[]) { super({ intents: intents, partials: partials }); dotenv.config(); CoreClient._commandItems = []; CoreClient._eventItems = []; + CoreClient._buttonEvents = []; this._events = new Events(); this._util = new Util(); @@ -73,4 +81,13 @@ export class CoreClient extends Client { CoreClient._eventItems.push(item); } + + public static RegisterButtonEvent(buttonId: string, event: ButtonEvent) { + const item: ButtonEventItem = { + ButtonId: buttonId, + Event: event, + }; + + CoreClient._buttonEvents.push(item); + } } diff --git a/src/client/events.ts b/src/client/events.ts index 2d36001..97d8cc7 100644 --- a/src/client/events.ts +++ b/src/client/events.ts @@ -1,40 +1,18 @@ import { Interaction } from "discord.js"; -import ICommandItem from "../contracts/ICommandItem"; -import SettingsHelper from "../helpers/SettingsHelper"; -import { CoreClient } from "./client"; +import ChatInputCommand from "./interactionCreate/chatInputCommand"; +import Button from "./interactionCreate/button"; export class Events { public async onInteractionCreate(interaction: Interaction) { - if (!interaction.isChatInputCommand()) return; if (!interaction.guildId) return; - const disabledCommandsString = await SettingsHelper.GetSetting("commands.disabled", interaction.guildId); - const disabledCommands = disabledCommandsString?.split(","); - - const disabledCommandsMessage = await SettingsHelper.GetSetting("commands.disabled.message", interaction.guildId); - - if (disabledCommands?.find(x => x == interaction.commandName)) { - await interaction.reply(disabledCommandsMessage || "This command is disabled."); - return; + if (interaction.isChatInputCommand()) { + ChatInputCommand.onChatInput(interaction); } - const item = CoreClient.commandItems.find(x => x.Name == interaction.commandName && !x.ServerId); - const itemForServer = CoreClient.commandItems.find(x => x.Name == interaction.commandName && x.ServerId == interaction.guildId); - - let itemToUse: ICommandItem; - - if (!itemForServer) { - if (!item) { - await interaction.reply('Command not found'); - return; - } - - itemToUse = item; - } else { - itemToUse = itemForServer; + if (interaction.isButton()) { + Button.onButtonClicked(interaction); } - - itemToUse.Command.execute(interaction); } // Emit when bot is logged in and ready to use diff --git a/src/client/interactionCreate/button.ts b/src/client/interactionCreate/button.ts new file mode 100644 index 0000000..f64f6c7 --- /dev/null +++ b/src/client/interactionCreate/button.ts @@ -0,0 +1,17 @@ +import { ButtonInteraction } from "discord.js"; +import { CoreClient } from "../client"; + +export default class Button { + public static async onButtonClicked(interaction: ButtonInteraction) { + if (!interaction.isButton) return; + + const item = CoreClient.buttonEvents.find(x => x.ButtonId == interaction.customId.split(" ")[0]); + + if (!item) { + await interaction.reply("Event not found."); + return; + } + + item.Event.execute(interaction); + } +} \ No newline at end of file diff --git a/src/client/interactionCreate/chatInputCommand.ts b/src/client/interactionCreate/chatInputCommand.ts new file mode 100644 index 0000000..8690994 --- /dev/null +++ b/src/client/interactionCreate/chatInputCommand.ts @@ -0,0 +1,27 @@ +import { Interaction } from "discord.js"; +import { CoreClient } from "../client"; +import ICommandItem from "../../contracts/ICommandItem"; + +export default class ChatInputCommand { + public static async onChatInput(interaction: Interaction) { + if (!interaction.isChatInputCommand()) return; + + const item = CoreClient.commandItems.find(x => x.Name == interaction.commandName && !x.ServerId); + const itemForServer = CoreClient.commandItems.find(x => x.Name == interaction.commandName && x.ServerId == interaction.guildId); + + let itemToUse: ICommandItem; + + if (!itemForServer) { + if (!item) { + await interaction.reply("Command not found."); + return; + } + + itemToUse = item; + } else { + itemToUse = itemForServer; + } + + itemToUse.Command.execute(interaction); + } +} \ No newline at end of file diff --git a/src/commands/rules.ts b/src/commands/rules.ts index 930138c..c50cb36 100644 --- a/src/commands/rules.ts +++ b/src/commands/rules.ts @@ -1,7 +1,8 @@ -import { CommandInteraction, EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from "discord.js"; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from "discord.js"; import { existsSync, readFileSync } from "fs"; import EmbedColours from "../constants/EmbedColours"; import { Command } from "../type/command"; +import SettingsHelper from "../helpers/SettingsHelper"; interface IRules { title?: string; @@ -14,13 +15,36 @@ export default class Rules extends Command { constructor() { super(); - super.CommandBuilder = new SlashCommandBuilder() - .setName("rules") - .setDescription("Send the rules embeds for this server") - .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator); + this.CommandBuilder = new SlashCommandBuilder() + .setName('rules') + .setDescription("Rules-related commands") + .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator) + .addSubcommand(x => + x + .setName('embeds') + .setDescription('Send the rules embeds for this server')) + .addSubcommand(x => + x + .setName('access') + .setDescription('Send the server verification embed button')); } public override async execute(interaction: CommandInteraction) { + if (!interaction.isChatInputCommand()) return; + + switch (interaction.options.getSubcommand()) { + case "embeds": + await this.SendEmbeds(interaction); + break; + case "access": + await this.SendAccessButton(interaction); + break; + default: + await interaction.reply("Subcommand doesn't exist."); + } + } + + private async SendEmbeds(interaction: CommandInteraction) { if (!interaction.guildId) return; if (!existsSync(`${process.cwd()}/data/rules/${interaction.guildId}.json`)) { @@ -71,4 +95,27 @@ export default class Rules extends Command { await interaction.reply({ embeds: [ successEmbed ], ephemeral: true }); } + + private async SendAccessButton(interaction: CommandInteraction) { + if (!interaction.guildId) return; + + const buttonLabel = await SettingsHelper.GetSetting("rules.access.label", interaction.guildId); + + const row = new ActionRowBuilder() + .addComponents([ + new ButtonBuilder() + .setCustomId('verify') + .setStyle(ButtonStyle.Primary) + .setLabel(buttonLabel || "Verify") + ]); + + await interaction.channel?.send({ + components: [ row ] + }); + + await interaction.reply({ + content: "Success", + ephemeral: true, + }); + } } \ No newline at end of file diff --git a/src/constants/DefaultValues.ts b/src/constants/DefaultValues.ts index 8608fbb..177ddee 100644 --- a/src/constants/DefaultValues.ts +++ b/src/constants/DefaultValues.ts @@ -31,6 +31,7 @@ export default class DefaultValues { // Rules (Command) this.values.push({ Key: "rules.file", Value: "data/rules/rules" }); + this.values.push({ Key: "rules.access.label", Value: "Verify" }); // Channels this.values.push({ Key: "channels.logs.message", Value: "message-logs" }); diff --git a/src/contracts/ButtonEventItem.ts b/src/contracts/ButtonEventItem.ts new file mode 100644 index 0000000..4a4c368 --- /dev/null +++ b/src/contracts/ButtonEventItem.ts @@ -0,0 +1,8 @@ +import { ButtonEvent } from "../type/buttonEvent"; + +interface ButtonEventItem { + ButtonId: string, + Event: ButtonEvent, +} + +export default ButtonEventItem; \ No newline at end of file diff --git a/src/registry.ts b/src/registry.ts index 1e03201..d3a73a7 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -37,6 +37,9 @@ import MessageDelete from "./events/MessageEvents/MessageDelete"; import MessageUpdate from "./events/MessageEvents/MessageUpdate"; import MessageCreate from "./events/MessageEvents/MessageCreate"; +// Button Event Imports +import Verify from "./buttonEvents/verify"; + export default class Registry { public static RegisterCommands() { CoreClient.RegisterCommand("about", new About()); @@ -84,4 +87,8 @@ export default class Registry { CoreClient.RegisterEvent(EventType.MessageUpdate, MessageUpdate); CoreClient.RegisterEvent(EventType.MessageCreate, MessageCreate); } + + public static RegisterButtonEvents() { + CoreClient.RegisterButtonEvent("verify", new Verify()); + } } \ No newline at end of file diff --git a/src/type/buttonEvent.ts b/src/type/buttonEvent.ts new file mode 100644 index 0000000..8a45bd0 --- /dev/null +++ b/src/type/buttonEvent.ts @@ -0,0 +1,5 @@ +import { ButtonInteraction } from "discord.js"; + +export abstract class ButtonEvent { + abstract execute(interaction: ButtonInteraction): Promise; +} \ No newline at end of file diff --git a/src/vylbot.ts b/src/vylbot.ts index ad30c48..c57ebd6 100644 --- a/src/vylbot.ts +++ b/src/vylbot.ts @@ -37,5 +37,6 @@ const client = new CoreClient([ registry.RegisterCommands(); registry.RegisterEvents(); +registry.RegisterButtonEvents(); client.start();