From 38ddcde06cd4cc87d893a7712fb2a32bdd9e63cc Mon Sep 17 00:00:00 2001 From: Tarrgon <61888458+Tarrgon@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:33:41 -0400 Subject: [PATCH] Role buttons --- sql/migrations/0002-add-role-buttons.sql | 13 +++++ src/buttons/role-button.ts | 32 ++++++++++++ src/commands/role-buttons.ts | 66 ++++++++++++++++++++++++ src/shared/Database.ts | 17 +++++- src/types/database-types.d.ts | 6 +++ 5 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 sql/migrations/0002-add-role-buttons.sql create mode 100644 src/buttons/role-button.ts create mode 100644 src/commands/role-buttons.ts diff --git a/sql/migrations/0002-add-role-buttons.sql b/sql/migrations/0002-add-role-buttons.sql new file mode 100644 index 0000000..684e816 --- /dev/null +++ b/sql/migrations/0002-add-role-buttons.sql @@ -0,0 +1,13 @@ +-------------------------------------------------------------------------------- +-- Up +-------------------------------------------------------------------------------- +CREATE TABLE role_buttons ( + id INTEGER PRIMARY KEY, + message_id TEXT NOT NULL, + role_id TEXT NOT NULL +); + +-------------------------------------------------------------------------------- +-- Down +-------------------------------------------------------------------------------- +DROP TABLE role_buttons; \ No newline at end of file diff --git a/src/buttons/role-button.ts b/src/buttons/role-button.ts new file mode 100644 index 0000000..c20fc36 --- /dev/null +++ b/src/buttons/role-button.ts @@ -0,0 +1,32 @@ +import { ButtonInteraction, Client, MessageFlags } from 'discord.js'; +import { Database } from '../shared/Database'; + +export default { + name: 'role-button', + handler: async function (client: Client, interaction: ButtonInteraction) { + await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); + + const roleId = await Database.getRoleFromButton(interaction.message.id); + + if (!roleId) await interaction.editReply({ content: 'Could not find role button.' }); + + const guild = await interaction.guild?.fetch(); + const member = await guild?.members.fetch(interaction.user.id); + + if (!member) return interaction.editReply({ content: 'Member not found.' }); + + try { + if (member.roles.cache.has(roleId!)) { + await member.roles.remove(roleId!); + interaction.editReply({ content: 'Role removed.' }); + } else { + await member.roles.add(roleId!); + interaction.editReply({ content: 'Role added.' }); + } + + } catch (e) { + console.error(e); + interaction.editReply({ content: 'Could not add role. Is it higher than mine?' }); + } + } +}; \ No newline at end of file diff --git a/src/commands/role-buttons.ts b/src/commands/role-buttons.ts new file mode 100644 index 0000000..da695fd --- /dev/null +++ b/src/commands/role-buttons.ts @@ -0,0 +1,66 @@ +import { ActionRowBuilder, ApplicationIntegrationType, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; +import { Database } from '../shared/Database'; + +export default { + name: 'role-buttons', + data: new SlashCommandBuilder() + .setName('role-buttons') + .setDescription('Manage role buttons.') + .setIntegrationTypes(ApplicationIntegrationType.GuildInstall) + .setContexts(InteractionContextType.Guild) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addSubcommand(subcommand => + subcommand + .setName('create') + .setDescription('Create a role button') + .addRoleOption(option => + option + .setName('role') + .setDescription('The role to give') + .setRequired(true) + ) + .addStringOption(option => + option + .setName('button-text') + .setDescription('The button text') + .setMaxLength(80) + .setRequired(true) + ) + .addStringOption(option => + option + .setName('message-text') + .setDescription('The message text') + .setRequired(true) + ) + ), + handler: async function (client: Client, interaction: ChatInputCommandInteraction) { + if (!interaction.guild) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Command must be ran in a guild.' }); + + const subcommand = interaction.options.getSubcommand(); + if (subcommand == 'create') { + if (!interaction.channel || !interaction.channel.isSendable()) + return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Missing permissions to send to channel.' }); + + const bot = await interaction.guild.members.fetchMe(); + + const role = interaction.options.getRole('role', true); + const label = interaction.options.getString('button-text', true); + const content = interaction.options.getString('message-text', true); + + if (role.position > bot.roles.highest.position) + return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Role is higher than my highest role.' }); + + const button = new ButtonBuilder() + .setCustomId('role-button') + .setStyle(ButtonStyle.Primary) + .setLabel(label); + + const row = new ActionRowBuilder() + .addComponents(button); + + const message = await interaction.channel.send({ components: [row], content }); + await Database.putRoleButton(message.id, role.id); + interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Created role button.' }); + } + } +}; \ No newline at end of file diff --git a/src/shared/Database.ts b/src/shared/Database.ts index 348e0af..23cbd75 100644 --- a/src/shared/Database.ts +++ b/src/shared/Database.ts @@ -3,7 +3,7 @@ import { open, Database as SqliteDatabase } from 'sqlite'; import sqlite3 from 'sqlite3'; import { fileURLToPath } from 'url'; import { Message } from '../events'; -import { AppealMessage, Ban, GithubUserMapping, GuildArraySetting, GuildSetting, GuildSettings, KnowledgebaseItem, LoggedMessage, Note, PrivateHelpTicket, TicketMessage, TicketPhrase } from '../types'; +import { AppealMessage, Ban, GithubUserMapping, GuildArraySetting, GuildSetting, GuildSettings, KnowledgebaseItem, LoggedMessage, Note, PrivateHelpTicket, RoleButton, TicketMessage, TicketPhrase } from '../types'; import { serializeMessage, wait } from '../utils'; import { readFileSync } from 'fs'; @@ -388,5 +388,20 @@ export class Database { return await Database.db.all('SELECT * from private_help_tickets WHERE status = ?', PrivateHelpTicketStatus.OPEN); } + //#endregion + + //#region Role Buttons + + static async putRoleButton(messageId: string, roleId: string) { + await Database.db.run('INSERT INTO role_buttons(message_id, role_id) VALUES (?, ?)', messageId, roleId); + } + + static async getRoleFromButton(messageId: string): Promise { + const data = await Database.db.get>('SELECT role_id from role_buttons WHERE message_id = ?', messageId); + + return data?.role_id ?? null; + } + + //#endregion } diff --git a/src/types/database-types.d.ts b/src/types/database-types.d.ts index a78a53e..135ccdd 100644 --- a/src/types/database-types.d.ts +++ b/src/types/database-types.d.ts @@ -84,3 +84,9 @@ export type PrivateHelpTicket = { status: PrivateHelpTicketStatus timestamp: string } + +export type RoleButton = { + id: number + message_id: string + role_id: string +} \ No newline at end of file