From cae2322930fab8eab3e1b54f17a589291f3d41ef Mon Sep 17 00:00:00 2001 From: unbravechimp Date: Mon, 4 May 2026 22:02:28 +0200 Subject: [PATCH 1/8] upd: rename ticket command + catch err of deletion --- .../utility/{setupTicket.ts => setupTickets.ts} | 4 ++-- .../ticket/removeTicketHandler.ts | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) rename src/commands/utility/{setupTicket.ts => setupTickets.ts} (94%) diff --git a/src/commands/utility/setupTicket.ts b/src/commands/utility/setupTickets.ts similarity index 94% rename from src/commands/utility/setupTicket.ts rename to src/commands/utility/setupTickets.ts index 4b58a30..9fe2219 100644 --- a/src/commands/utility/setupTicket.ts +++ b/src/commands/utility/setupTickets.ts @@ -9,7 +9,7 @@ import { PermissionFlagsBits } from 'discord.js'; -export class SetupTicketCommand extends Command { +export class SetupTicketsCommand extends Command { public constructor(context: Command.LoaderContext, options: Command.Options) { super(context, { ...options }); } @@ -17,7 +17,7 @@ export class SetupTicketCommand extends Command { public override registerApplicationCommands(registry: Command.Registry) { registry.registerChatInputCommand((builder) => builder - .setName('setup-ticket') + .setName('setup-tickets') .setDescription('Post the ticket panel in a channel.') .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .addChannelOption((option) => diff --git a/src/interaction-handlers/ticket/removeTicketHandler.ts b/src/interaction-handlers/ticket/removeTicketHandler.ts index 21f3ad8..254906e 100644 --- a/src/interaction-handlers/ticket/removeTicketHandler.ts +++ b/src/interaction-handlers/ticket/removeTicketHandler.ts @@ -87,10 +87,14 @@ export class ButtonHandler extends InteractionHandler { const ticket = await getTicketId(interaction.guild.id, channel.id); - if (ticket) { - await removeTicket(ticket.id); + try { + if (ticket) { + await removeTicket(ticket.id); + } + + await channel.delete(`Ticket closed by ${interaction.user.tag}.`); + } catch (err) { + console.log(err); } - - await channel.delete(`Ticket closed by ${interaction.user.tag}.`); } } From 7d9b128d426a0c84154847707f223538bf11edf9 Mon Sep 17 00:00:00 2001 From: unbravechimp Date: Tue, 5 May 2026 00:08:25 +0200 Subject: [PATCH 2/8] upd: autoroles! --- README.md | 7 + prisma/schema.prisma | 15 ++ src/commands/moderation/autorole/autorole.ts | 141 ++++++++++++++++++ src/commands/moderation/{ => ban}/ban.ts | 0 src/commands/moderation/{ => ban}/unban.ts | 0 src/commands/moderation/{ => mute}/mute.ts | 0 src/commands/moderation/{ => mute}/unmute.ts | 0 src/commands/moderation/{ => warn}/unwarn.ts | 0 src/commands/moderation/{ => warn}/warn.ts | 0 src/commands/moderation/{ => warn}/warns.ts | 0 src/commands/utility/help.ts | 33 ++++ .../utility/{ => reminder}/reminder.ts | 0 .../utility/{ => reminder}/reminders.ts | 0 src/lib/autorole.ts | 43 ++++++ src/listeners/guildMemberAdd.ts | 26 +++- 15 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 src/commands/moderation/autorole/autorole.ts rename src/commands/moderation/{ => ban}/ban.ts (100%) rename src/commands/moderation/{ => ban}/unban.ts (100%) rename src/commands/moderation/{ => mute}/mute.ts (100%) rename src/commands/moderation/{ => mute}/unmute.ts (100%) rename src/commands/moderation/{ => warn}/unwarn.ts (100%) rename src/commands/moderation/{ => warn}/warn.ts (100%) rename src/commands/moderation/{ => warn}/warns.ts (100%) create mode 100644 src/commands/utility/help.ts rename src/commands/utility/{ => reminder}/reminder.ts (100%) rename src/commands/utility/{ => reminder}/reminders.ts (100%) create mode 100644 src/lib/autorole.ts diff --git a/README.md b/README.md index 5c17bf5..49ea2b9 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,13 @@ An opensource modern Discord Bot built for moderation, utilities and support! +Quest is capable of: +- All moderation commands such as: /ban, /kick, /mute & /warn. +- Autoroles +- Utilties such as reminders. +- Complete ticket system. +- Welcoming new members. + ## Links [Status Page](https://status.questfoundation.dev), [Bot Documentation](https://docs.questfoundation.dev/quest-bot) & [Official Discord](https://discord.gg/ksuqZ77R88) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9ed48e4..66934cf 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,6 +21,7 @@ model Server { bans Ban[] reminders Reminder[] tickets Ticket[] + autoroles AutoRole[] @@map("server") } @@ -98,3 +99,17 @@ model Ticket { @@index([guildId, userId]) @@map("tickets") } + +model AutoRole { + id String @id @default(cuid()) + guildId String + roleId String + botRole Boolean? + createdAt DateTime @default(now()) + + server Server @relation(fields: [guildId], references: [id], onDelete: Cascade) + + @@unique([guildId, roleId]) + @@index([guildId]) + @@map("autoroles") +} \ No newline at end of file diff --git a/src/commands/moderation/autorole/autorole.ts b/src/commands/moderation/autorole/autorole.ts new file mode 100644 index 0000000..32a94ee --- /dev/null +++ b/src/commands/moderation/autorole/autorole.ts @@ -0,0 +1,141 @@ +import { Command } from '@sapphire/framework'; +import { MessageFlags } from 'discord.js'; +import { createAutoRole, getAutoRole, getAutoRoles, removeAutoRole } from '#lib/autorole.js'; +import { emojis } from '#utils/emoji.js'; + +export class AutoRoleCommand extends Command { + public constructor(context: Command.LoaderContext, options: Command.Options) { + super(context, { ...options }); + } + + public override registerApplicationCommands(registry: Command.Registry) { + registry.registerChatInputCommand((builder) => + builder + .setName('autorole') + .setDescription('Automatically assign roles to new members!') + .addSubcommand((sub) => + sub + .setName('add') + .setDescription('Create a new auto role.') + .addRoleOption((option) => + option.setName('role').setDescription('The role to assign to new members').setRequired(true) + ) + .addBooleanOption((option) => + option.setName('bot_role').setDescription('Whether this role should be assigned to bots').setRequired(true) + ) + ) + .addSubcommand((sub) => + sub + .setName('remove') + .setDescription('Remove an auto role.') + .addStringOption((option) => + option + .setName('role') + .setDescription('The auto role to remove') + .setAutocomplete(true) + .setRequired(true) + ) + ) + .addSubcommand((sub) => + sub + .setName('list') + .setDescription('List all auto roles.') + ) + ); + } + + public override async autocompleteRun(interaction: Command.AutocompleteInteraction) { + if (!interaction.guildId) { + await interaction.respond([]); + return; + } + + const focusedOption = interaction.options.getFocused(true); + + if (interaction.options.getSubcommand() !== 'remove' || focusedOption.name !== 'role') { + await interaction.respond([]); + return; + } + + const autoRoles = await getAutoRoles(interaction.guildId); + const choices = autoRoles.slice(0, 25).map((autoRole) => ({ + name: interaction.guild?.roles.cache.get(autoRole.roleId)?.name ?? autoRole.roleId, + value: autoRole.id + })); + + await interaction.respond(choices); + } + + public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { + if (!interaction.inCachedGuild()) { + await interaction.reply({ + content: `${emojis.rightArrow2} This command can only be used in a server.`, + flags: MessageFlags.Ephemeral + }); + return; + } + + const subcommand = interaction.options.getSubcommand(); + + if (subcommand === 'add') { + const role = interaction.options.getRole('role', true); + const botRole = interaction.options.getBoolean('bot_role', true); + + try { + await createAutoRole(interaction.guildId, interaction.guild.name, role.id, botRole); + await interaction.reply({ + content: `${emojis.rightArrow2} Added auto role ${role} (Bot Role: ${botRole}).`, + flags: MessageFlags.Ephemeral + }); + } catch (err) { + console.error(err); + await interaction.reply({ + content: `${emojis.rightArrow2} That role is already an auto role in this server.`, + flags: MessageFlags.Ephemeral + }); + } + } + + if (subcommand === 'remove') { + const autoRoleId = interaction.options.getString('id', true); + const autoRole = await getAutoRole(autoRoleId); + + if (!autoRole) { + await interaction.reply({ + content: `${emojis.rightArrow2} That auto role no longer exists.`, + flags: MessageFlags.Ephemeral + }); + return; + } + + await removeAutoRole(autoRole.id); + await interaction.reply({ + content: `${emojis.rightArrow2} Removed auto role for <@&${autoRole.roleId}>.`, + flags: MessageFlags.Ephemeral + }); + } + + if (subcommand === 'list') { + const autoRoles = await getAutoRoles(interaction.guildId); + if (autoRoles.length === 0) { + await interaction.reply({ + content: `${emojis.rightArrow2} There are no auto roles set up in this server.`, + flags: MessageFlags.Ephemeral + }); + return; + } + + const autoRoleList = autoRoles.map(autoRole => { + const role = interaction.guild?.roles.cache.get(autoRole.roleId); + const roleName = role ? role.name : `Unknown Role (${autoRole.roleId})`; + const botRoleText = autoRole.botRole ? ' (Bot Role)' : ''; + return `${emojis.rightArrow1} ${roleName}${botRoleText}`; + }).join('\n'); + + await interaction.reply({ + content: `**Auto Roles:**\n${autoRoleList}`, + flags: MessageFlags.Ephemeral + }); + } + } +} \ No newline at end of file diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban/ban.ts similarity index 100% rename from src/commands/moderation/ban.ts rename to src/commands/moderation/ban/ban.ts diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/ban/unban.ts similarity index 100% rename from src/commands/moderation/unban.ts rename to src/commands/moderation/ban/unban.ts diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute/mute.ts similarity index 100% rename from src/commands/moderation/mute.ts rename to src/commands/moderation/mute/mute.ts diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/mute/unmute.ts similarity index 100% rename from src/commands/moderation/unmute.ts rename to src/commands/moderation/mute/unmute.ts diff --git a/src/commands/moderation/unwarn.ts b/src/commands/moderation/warn/unwarn.ts similarity index 100% rename from src/commands/moderation/unwarn.ts rename to src/commands/moderation/warn/unwarn.ts diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn/warn.ts similarity index 100% rename from src/commands/moderation/warn.ts rename to src/commands/moderation/warn/warn.ts diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warn/warns.ts similarity index 100% rename from src/commands/moderation/warns.ts rename to src/commands/moderation/warn/warns.ts diff --git a/src/commands/utility/help.ts b/src/commands/utility/help.ts new file mode 100644 index 0000000..24675c6 --- /dev/null +++ b/src/commands/utility/help.ts @@ -0,0 +1,33 @@ +import { Command } from '@sapphire/framework'; +import { emojis } from '#utils/emoji.js'; +import { MessageFlags } from 'discord.js'; + +export class HelpCommand extends Command { + public constructor(context: Command.LoaderContext, options: Command.Options) { + super(context, { ...options }); + } + + public override registerApplicationCommands(registry: Command.Registry) { + registry.registerChatInputCommand((builder) => + builder.setName('help').setDescription("Show what the bot is capable of.") + ); + } + + public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { + const commands = this.container.stores.get('commands'); + + const commandList = Array.from(commands.values()) + .sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')) + .map((cmd) => { + const description = cmd.applicationCommandRegistry['apiCalls'][0]?.builtData.description ?? cmd.description; + + return `${emojis.rightArrow1} **/${cmd.name}** - ${description}`; + }) + .join('\n'); + + await interaction.reply({ + content: commandList + `\n\n**Status:** https://status.questfoundation.dev/\n**Official Discord Server:** https://discord.gg/F4HYE8frK2\n**Documentation:** https://docs.questfoundation.dev/`, + flags: MessageFlags.SuppressEmbeds + }); + } +} \ No newline at end of file diff --git a/src/commands/utility/reminder.ts b/src/commands/utility/reminder/reminder.ts similarity index 100% rename from src/commands/utility/reminder.ts rename to src/commands/utility/reminder/reminder.ts diff --git a/src/commands/utility/reminders.ts b/src/commands/utility/reminder/reminders.ts similarity index 100% rename from src/commands/utility/reminders.ts rename to src/commands/utility/reminder/reminders.ts diff --git a/src/lib/autorole.ts b/src/lib/autorole.ts new file mode 100644 index 0000000..22770d3 --- /dev/null +++ b/src/lib/autorole.ts @@ -0,0 +1,43 @@ +import { prisma } from './prisma.js'; + +export async function createAutoRole( + guildId: string, + guildName: string, + roleId: string, + botRole?: boolean +) { + if (guildId && guildName) { + await prisma.server.upsert({ + where: { id: guildId }, + create: { id: guildId, name: guildName }, + update: { name: guildName } + }); + + return prisma.autoRole.create({ + data: { guildId, roleId, botRole } + }); + } + + return prisma.autoRole.create({ + data: { guildId, roleId, botRole } + }); +} + +export async function getAutoRoles(guildId: string) { + return prisma.autoRole.findMany({ + where: { guildId }, + orderBy: { createdAt: 'asc' } + }); +} + +export async function removeAutoRole(autoRoleId: string) { + return prisma.autoRole.delete({ where: { id: autoRoleId } }); +} + +export async function clearAutoRoles(guildId: string) { + return prisma.autoRole.deleteMany({ where: { guildId } }); +} + +export async function getAutoRole(autoRoleId: string) { + return prisma.autoRole.findUnique({ where: { id: autoRoleId } }); +} \ No newline at end of file diff --git a/src/listeners/guildMemberAdd.ts b/src/listeners/guildMemberAdd.ts index ed47e2b..3bd7fa6 100644 --- a/src/listeners/guildMemberAdd.ts +++ b/src/listeners/guildMemberAdd.ts @@ -1,6 +1,7 @@ import { Listener } from '@sapphire/framework'; -import { Events, type GuildMember } from 'discord.js'; +import { Events, PermissionFlagsBits, type GuildMember } from 'discord.js'; import { enforceMute } from '#lib/mutes.js'; +import { getAutoRoles } from '#lib/autorole.js'; import { sendWelcome } from '#lib/welcomeModule.js'; export class GuildMemberAddListener extends Listener { @@ -12,6 +13,29 @@ export class GuildMemberAddListener extends Listener console.error(err)); + await sendWelcome(member).catch((err) => console.error(err)); + return; + } + + const autoRoles = await getAutoRoles(member.guild.id).catch((err) => { + console.error(err); + return []; + }); + + const rolesToAdd = autoRoles + .filter((autoRole) => Boolean(autoRole.botRole) === member.user.bot) + .map((autoRole) => member.guild.roles.cache.get(autoRole.roleId)) + .filter((role): role is NonNullable => Boolean(role?.editable)) + .map((role) => role.id); + + if (rolesToAdd.length > 0) { + await member.roles.add(rolesToAdd).catch((err) => console.error(err)); + } + await enforceMute(member.guild, member.id).catch((err) => console.error(err)); await sendWelcome(member).catch((err) => console.error(err)); } From e4642cae4f518602154b5f276e666a0c6a489d91 Mon Sep 17 00:00:00 2001 From: unbravechimp Date: Tue, 5 May 2026 00:43:53 +0200 Subject: [PATCH 3/8] upd: limits! --- src/commands/moderation/autorole/autorole.ts | 9 ++++ src/commands/utility/reminder/reminder.ts | 52 +++++++++++++------- src/lib/autorole.ts | 9 ++++ src/lib/limits.ts | 8 +++ src/lib/reminders.ts | 11 ++++- 5 files changed, 69 insertions(+), 20 deletions(-) create mode 100644 src/lib/limits.ts diff --git a/src/commands/moderation/autorole/autorole.ts b/src/commands/moderation/autorole/autorole.ts index 32a94ee..befa384 100644 --- a/src/commands/moderation/autorole/autorole.ts +++ b/src/commands/moderation/autorole/autorole.ts @@ -1,6 +1,7 @@ import { Command } from '@sapphire/framework'; import { MessageFlags } from 'discord.js'; import { createAutoRole, getAutoRole, getAutoRoles, removeAutoRole } from '#lib/autorole.js'; +import { LimitError } from '#lib/limits.js'; import { emojis } from '#utils/emoji.js'; export class AutoRoleCommand extends Command { @@ -89,6 +90,14 @@ export class AutoRoleCommand extends Command { }); } catch (err) { console.error(err); + if (err instanceof LimitError) { + await interaction.reply({ + content: `${emojis.rightArrow2} ${err.message}`, + flags: MessageFlags.Ephemeral + }); + return; + } + await interaction.reply({ content: `${emojis.rightArrow2} That role is already an auto role in this server.`, flags: MessageFlags.Ephemeral diff --git a/src/commands/utility/reminder/reminder.ts b/src/commands/utility/reminder/reminder.ts index 2e93389..69d31de 100644 --- a/src/commands/utility/reminder/reminder.ts +++ b/src/commands/utility/reminder/reminder.ts @@ -2,6 +2,7 @@ import { Command } from '@sapphire/framework'; import { MessageFlags } from 'discord.js'; import ms, { type StringValue } from 'ms'; import { createReminder, getReminder, removeReminder } from '#lib/reminders.js'; +import { LimitError } from '#lib/limits.js'; import { emojis } from '#utils/emoji.js'; export class ReminderCommand extends Command { @@ -62,26 +63,39 @@ export class ReminderCommand extends Command { const remindAt = new Date(Date.now() + duration); - const reminder = interaction.inCachedGuild() - ? await createReminder( - interaction.user.id, - message, - remindAt, - interaction.guild.id, - interaction.guild.name, - interaction.channelId - ) - : await createReminder(interaction.user.id, message, remindAt); - - const unix = Math.floor(remindAt.getTime() / 1000); - await interaction.reply({ - content: `${emojis.rightArrow2} Reminder set to go off in message: ${message}\nID: \`${reminder.id}\`` - }); + try { + const reminder = interaction.inCachedGuild() + ? await createReminder( + interaction.user.id, + message, + remindAt, + interaction.guild.id, + interaction.guild.name, + interaction.channelId + ) + : await createReminder(interaction.user.id, message, remindAt); - setTimeout(() => { - interaction.deleteReply().catch(() => {}); - }, 5000); - return; + const unix = Math.floor(remindAt.getTime() / 1000); + await interaction.reply({ + content: `${emojis.rightArrow2} Reminder set to go off in message: ${message}\nID: \`${reminder.id}\`` + }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 5000); + return; + } catch (err) { + console.error(err); + if (err instanceof LimitError) { + await interaction.reply({ + content: `${emojis.rightArrow2} ${err.message}`, + flags: MessageFlags.Ephemeral + }); + return; + } + + throw err; + } } if (subcommand === 'remove') { diff --git a/src/lib/autorole.ts b/src/lib/autorole.ts index 22770d3..b84d2bf 100644 --- a/src/lib/autorole.ts +++ b/src/lib/autorole.ts @@ -1,4 +1,5 @@ import { prisma } from './prisma.js'; +import { LIMITS_ENABLED, LimitError } from './limits.js'; export async function createAutoRole( guildId: string, @@ -6,6 +7,14 @@ export async function createAutoRole( roleId: string, botRole?: boolean ) { + if (LIMITS_ENABLED) { + const autoRoleCount = await prisma.autoRole.count({ where: { guildId } }); + + if (autoRoleCount >= 5) { + throw new LimitError('A guild can only have up to 5 auto roles.'); + } + } + if (guildId && guildName) { await prisma.server.upsert({ where: { id: guildId }, diff --git a/src/lib/limits.ts b/src/lib/limits.ts new file mode 100644 index 0000000..634e256 --- /dev/null +++ b/src/lib/limits.ts @@ -0,0 +1,8 @@ +export const LIMITS_ENABLED = process.env.LIMITS === 'true'; + +export class LimitError extends Error { + public constructor(message: string) { + super(message); + this.name = 'LimitError'; + } +} \ No newline at end of file diff --git a/src/lib/reminders.ts b/src/lib/reminders.ts index fb10899..cf8bcaf 100644 --- a/src/lib/reminders.ts +++ b/src/lib/reminders.ts @@ -1,4 +1,5 @@ -import { prisma } from "./prisma.js"; +import { prisma } from './prisma.js'; +import { LIMITS_ENABLED, LimitError } from './limits.js'; export async function createReminder( userId: string, @@ -8,6 +9,14 @@ export async function createReminder( guildName?: string, channelId?: string, ) { + if (LIMITS_ENABLED) { + const reminderCount = await prisma.reminder.count({ where: { userId } }); + + if (reminderCount >= 5) { + throw new LimitError('You can only have up to 5 reminders.'); + } + } + if (guildId && guildName) { await prisma.server.upsert({ where: { id: guildId }, From bc1efd0e6767484f6bd732897bef71b60c31b3a0 Mon Sep 17 00:00:00 2001 From: unbravechimp Date: Tue, 5 May 2026 00:50:22 +0200 Subject: [PATCH 4/8] fix: autorole list --- src/commands/moderation/autorole/autorole.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/moderation/autorole/autorole.ts b/src/commands/moderation/autorole/autorole.ts index befa384..04a583e 100644 --- a/src/commands/moderation/autorole/autorole.ts +++ b/src/commands/moderation/autorole/autorole.ts @@ -136,7 +136,7 @@ export class AutoRoleCommand extends Command { const autoRoleList = autoRoles.map(autoRole => { const role = interaction.guild?.roles.cache.get(autoRole.roleId); - const roleName = role ? role.name : `Unknown Role (${autoRole.roleId})`; + const roleName = role ? `<@&${role.id}>` : `Unknown Role (${autoRole.roleId})`; const botRoleText = autoRole.botRole ? ' (Bot Role)' : ''; return `${emojis.rightArrow1} ${roleName}${botRoleText}`; }).join('\n'); From 3bd018376f8710406277f7acf96a0b1b75d75ebe Mon Sep 17 00:00:00 2001 From: unbravechimp Date: Tue, 5 May 2026 00:55:10 +0200 Subject: [PATCH 5/8] fix: ephemeral help cmd --- src/commands/utility/help.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/utility/help.ts b/src/commands/utility/help.ts index 24675c6..557ea67 100644 --- a/src/commands/utility/help.ts +++ b/src/commands/utility/help.ts @@ -27,7 +27,7 @@ export class HelpCommand extends Command { await interaction.reply({ content: commandList + `\n\n**Status:** https://status.questfoundation.dev/\n**Official Discord Server:** https://discord.gg/F4HYE8frK2\n**Documentation:** https://docs.questfoundation.dev/`, - flags: MessageFlags.SuppressEmbeds + flags: MessageFlags.SuppressEmbeds | MessageFlags.Ephemeral }); } } \ No newline at end of file From 0a5f22db94c3c1b5ed669a647e878b1d8b4d8a17 Mon Sep 17 00:00:00 2001 From: unbravechimp Date: Tue, 5 May 2026 01:53:03 +0200 Subject: [PATCH 6/8] upd: ticket staff role! --- src/commands/utility/settings.ts | 33 ++++++++-- .../ticket/createTicketHandler.ts | 66 ++++++++++++------- src/lib/settings.ts | 2 + 3 files changed, 73 insertions(+), 28 deletions(-) diff --git a/src/commands/utility/settings.ts b/src/commands/utility/settings.ts index 35a629c..9007c3a 100644 --- a/src/commands/utility/settings.ts +++ b/src/commands/utility/settings.ts @@ -9,6 +9,7 @@ import { InteractionContextType, MessageFlags, PermissionFlagsBits, + RoleSelectMenuBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from 'discord.js'; @@ -42,7 +43,7 @@ function buildWelcomePanel(settings: ServerSettings, guild: Guild, status?: stri .setValue('enable'), new StringSelectMenuOptionBuilder() .setLabel('Disable') - .setDescription('Do not send a message when a user joins the server.') + .setDescription("Don't send a message when a user joins the server.") .setValue('disable') ); @@ -79,6 +80,20 @@ function buildTicketPanel(settings: ServerSettings, guild: Guild, status?: strin .setLabel('Remove Category') .setStyle(ButtonStyle.Danger) .setDisabled(!settings.ticketCategoryId); + + const currentStaffRole = settings.staffRole + ? guild.roles.cache.get(settings.staffRole)?.name + : null; + + const staffRole = new RoleSelectMenuBuilder() + .setCustomId('staffRole') + .setPlaceholder(currentStaffRole ?? 'Select a ticket staff role'); + + const removeStaffRoleButton = new ButtonBuilder() + .setCustomId('removeStaffRole') + .setLabel('Remove Staff Role') + .setStyle(ButtonStyle.Danger) + .setDisabled(!settings.staffRole); return { content: status @@ -86,7 +101,9 @@ function buildTicketPanel(settings: ServerSettings, guild: Guild, status?: strin : `${emojis.rightArrow1} **Tickets** module:`, components: [ new ActionRowBuilder().addComponents(categoryMenu), - new ActionRowBuilder().addComponents(removeButton) + new ActionRowBuilder().addComponents(removeButton), + new ActionRowBuilder().addComponents(staffRole), + new ActionRowBuilder().addComponents(removeStaffRoleButton) ] }; } @@ -206,13 +223,21 @@ export class SettingsCommand extends Command { } else if (i.customId === 'ticketCategory' && i.isChannelSelectMenu()) { const categoryId = i.values[0]; const next = await updateSettings(guildId, guild.name, { ticketCategoryId: categoryId }); - const categoryName = guild.channels.cache.get(categoryId)?.name ?? 'selected category'; - await i.update(buildTicketPanel(next, guild, `Ticket category set to **${categoryName}**.`)); + await i.update(buildTicketPanel(next, guild, `Ticket category set to <#${categoryId}>.`)); } else if (i.customId === 'ticketCategoryRemove' && i.isButton()) { const next = await updateSettings(guildId, guild.name, { ticketCategoryId: null }); await i.update(buildTicketPanel(next, guild, 'Ticket category removed.')); + } else if (i.customId === 'staffRole' && i.isRoleSelectMenu()) { + const roleId = i.values[0]; + const next = await updateSettings(guildId, guild.name, { staffRole: roleId }); + + await i.update(buildTicketPanel(next, guild, `Ticket staff role set to <@&${roleId}>.`)); + } else if (i.customId === 'removeStaffRole' && i.isButton()) { + const next = await updateSettings(guildId, guild.name, { staffRole: null }); + + await i.update(buildTicketPanel(next, guild, 'Ticket staff role removed.')); } }); diff --git a/src/interaction-handlers/ticket/createTicketHandler.ts b/src/interaction-handlers/ticket/createTicketHandler.ts index 5900237..e4d1913 100644 --- a/src/interaction-handlers/ticket/createTicketHandler.ts +++ b/src/interaction-handlers/ticket/createTicketHandler.ts @@ -87,34 +87,50 @@ export class ButtonHandler extends InteractionHandler { const ticket = await createTicket(interaction.guild.id, interaction.guild.name, interaction.user.id, reason); + const permissionOverwrites = [ + { + id: interaction.guild.id, + deny: [PermissionFlagsBits.ViewChannel] + }, + { + id: interaction.user.id, + allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.ReadMessageHistory + ] + }, + { + id: interaction.client.user.id, + allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.ReadMessageHistory, + PermissionFlagsBits.ManageChannels + ] + } + ] as any[]; + + const staffRoleId = settings.staffRole; + const staffRoleExists = typeof staffRoleId === 'string' && interaction.guild.roles.cache.has(staffRoleId); + + if (staffRoleExists) { + permissionOverwrites.push({ + id: staffRoleId, + allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.ReadMessageHistory + ] + }); + } + const channel = await interaction.guild.channels .create({ name: `${ticket.ticketNumber}-${interaction.user.username}`, type: ChannelType.GuildText, parent, - permissionOverwrites: [ - { - id: interaction.guild.id, - deny: [PermissionFlagsBits.ViewChannel] - }, - { - id: interaction.user.id, - allow: [ - PermissionFlagsBits.ViewChannel, - PermissionFlagsBits.SendMessages, - PermissionFlagsBits.ReadMessageHistory - ] - }, - { - id: interaction.client.user.id, - allow: [ - PermissionFlagsBits.ViewChannel, - PermissionFlagsBits.SendMessages, - PermissionFlagsBits.ReadMessageHistory, - PermissionFlagsBits.ManageChannels - ] - } - ] + permissionOverwrites }) .catch(async (error) => { await removeTicket(ticket.id).catch(() => null); @@ -135,8 +151,10 @@ export class ButtonHandler extends InteractionHandler { const closeRow = new ActionRowBuilder().addComponents(closeButton); + const staffMention = staffRoleExists ? `<@&${staffRoleId}> ` : ''; + await channel.send({ - content: `<@${interaction.user.id}>, your ticket has been created!\n**Reason:** ${reason}`, + content: `${staffMention}<@${interaction.user.id}>, your ticket has been created!\n**Reason:** ${reason}`, components: [closeRow] }); } diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 334fbc9..08fc0f3 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -5,12 +5,14 @@ export type ServerSettings = { welcomePeople: boolean; welcomeChannelId: string | null; ticketCategoryId: string | null; + staffRole: string | null; }; export const DefaultSettings: ServerSettings = { welcomePeople: false, welcomeChannelId: null, ticketCategoryId: null, + staffRole: null, }; export async function getSettings(guildId: string, guildName: string): Promise { From 8e9b28ad9624b261014768ac3817ca2814c60f87 Mon Sep 17 00:00:00 2001 From: unbravechimp Date: Tue, 5 May 2026 01:59:55 +0200 Subject: [PATCH 7/8] fix: autorole removal --- src/commands/moderation/autorole/autorole.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/moderation/autorole/autorole.ts b/src/commands/moderation/autorole/autorole.ts index 04a583e..7a83096 100644 --- a/src/commands/moderation/autorole/autorole.ts +++ b/src/commands/moderation/autorole/autorole.ts @@ -106,7 +106,7 @@ export class AutoRoleCommand extends Command { } if (subcommand === 'remove') { - const autoRoleId = interaction.options.getString('id', true); + const autoRoleId = interaction.options.getString('role', true); const autoRole = await getAutoRole(autoRoleId); if (!autoRole) { From 698360c1860e079e459ea95641492659bc27edc1 Mon Sep 17 00:00:00 2001 From: unbravechimp Date: Tue, 5 May 2026 02:01:45 +0200 Subject: [PATCH 8/8] fix: rendering of cmds in /help --- src/commands/utility/help.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/utility/help.ts b/src/commands/utility/help.ts index 557ea67..7e4904d 100644 --- a/src/commands/utility/help.ts +++ b/src/commands/utility/help.ts @@ -20,8 +20,9 @@ export class HelpCommand extends Command { .sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')) .map((cmd) => { const description = cmd.applicationCommandRegistry['apiCalls'][0]?.builtData.description ?? cmd.description; + const commandName = cmd.applicationCommandRegistry['apiCalls'][0]?.builtData.name ?? cmd.name; - return `${emojis.rightArrow1} **/${cmd.name}** - ${description}`; + return `${emojis.rightArrow1} **/${commandName}** - ${description}`; }) .join('\n');