From 99b1fef1f773b72857ff7989dc783745bd59b386 Mon Sep 17 00:00:00 2001 From: UnbraveChimp Date: Mon, 4 May 2026 17:28:53 +0200 Subject: [PATCH 1/2] upd: ticket improvements! --- prisma/schema.prisma | 18 +++++ .../ticket/createTicketHandler.ts | 68 ++++++++++-------- .../ticket/removeTicketHandler.ts | 55 ++++++++++++-- src/lib/tickets.ts | 71 +++++++++++++++++++ 4 files changed, 178 insertions(+), 34 deletions(-) create mode 100644 src/lib/tickets.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 83cc338..9ed48e4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,6 +11,7 @@ model Server { id String @id name String blacklisted Boolean @default(false) + nextTicketNumber Int @default(1) settings Json @default("{}") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -19,6 +20,7 @@ model Server { mutes Mute[] bans Ban[] reminders Reminder[] + tickets Ticket[] @@map("server") } @@ -80,3 +82,19 @@ model Reminder { @@index([remindAt]) @@index([userId]) } + +model Ticket { + id String @id @default(cuid()) + guildId String + ticketNumber Int + channelId String? @unique + userId String + reason String? + createdAt DateTime @default(now()) + + server Server @relation(fields: [guildId], references: [id], onDelete: Cascade) + + @@unique([guildId, ticketNumber]) + @@index([guildId, userId]) + @@map("tickets") +} diff --git a/src/interaction-handlers/ticket/createTicketHandler.ts b/src/interaction-handlers/ticket/createTicketHandler.ts index 893f423..5900237 100644 --- a/src/interaction-handlers/ticket/createTicketHandler.ts +++ b/src/interaction-handlers/ticket/createTicketHandler.ts @@ -11,6 +11,7 @@ import { } from 'discord.js'; import { TextInputBuilder, TextInputStyle, type ButtonInteraction } from 'discord.js'; import { getSettings, updateSettings } from '#lib/settings.js'; +import { createTicket, removeTicket, setTicketChannelId } from '#lib/tickets.js'; import { emojis } from '#utils/emoji.js'; export class ButtonHandler extends InteractionHandler { @@ -84,37 +85,46 @@ export class ButtonHandler extends InteractionHandler { } } - const channel = await interaction.guild.channels.create({ - name: `ticket-${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 - ] - } - ] - }); + const ticket = await createTicket(interaction.guild.id, interaction.guild.name, interaction.user.id, reason); + + 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 + ] + } + ] + }) + .catch(async (error) => { + await removeTicket(ticket.id).catch(() => null); + throw error; + }); + + await setTicketChannelId(ticket.id, channel.id); await modalSubmit.reply({ - content: `${emojis.rightArrow2} Created a ticket!`, + content: `${emojis.rightArrow2} Created ticket <#${channel.id}>!`, flags: MessageFlags.Ephemeral }); diff --git a/src/interaction-handlers/ticket/removeTicketHandler.ts b/src/interaction-handlers/ticket/removeTicketHandler.ts index 09cc843..21f3ad8 100644 --- a/src/interaction-handlers/ticket/removeTicketHandler.ts +++ b/src/interaction-handlers/ticket/removeTicketHandler.ts @@ -1,7 +1,8 @@ import { InteractionHandler, InteractionHandlerTypes } from '@sapphire/framework'; -import { MessageFlags } from 'discord.js'; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from 'discord.js'; import type { ButtonInteraction } from 'discord.js'; import { emojis } from '#utils/emoji.js'; +import { getTicketId, removeTicket } from '#lib/tickets.js'; export class ButtonHandler extends InteractionHandler { public constructor(ctx: InteractionHandler.LoaderContext, options: InteractionHandler.Options) { @@ -12,7 +13,13 @@ export class ButtonHandler extends InteractionHandler { } public override parse(interaction: ButtonInteraction) { - if (interaction.customId !== 'remove-ticket') return this.none(); + if ( + interaction.customId !== 'remove-ticket' && + interaction.customId !== 'confirm-remove-ticket' && + interaction.customId !== 'cancel-remove-ticket' + ) { + return this.none(); + } return this.some(); } @@ -34,6 +41,14 @@ export class ButtonHandler extends InteractionHandler { return; } + if (interaction.customId === 'cancel-remove-ticket') { + await interaction.update({ + content: `${emojis.rightArrow2} Ticket closure cancelled.`, + components: [] + }); + return; + } + const channel = interaction.channel; if (!channel || !('deletable' in channel) || !channel.deletable) { @@ -44,8 +59,38 @@ export class ButtonHandler extends InteractionHandler { return; } - await channel.delete( - `Ticket channel "${channel.name}" (${channel.id}) closed by ${interaction.user.tag} (${interaction.user.id}).` - ); + if (interaction.customId === 'remove-ticket') { + const confirmButton = new ButtonBuilder() + .setCustomId('confirm-remove-ticket') + .setLabel('Confirm Close') + .setStyle(ButtonStyle.Danger); + + const cancelButton = new ButtonBuilder() + .setCustomId('cancel-remove-ticket') + .setLabel('Cancel') + .setStyle(ButtonStyle.Secondary); + + const row = new ActionRowBuilder().addComponents(confirmButton, cancelButton); + + await interaction.reply({ + content: `${emojis.rightArrow2} Are you sure you want to close this ticket?`, + components: [row], + flags: MessageFlags.Ephemeral + }); + return; + } + + await interaction.update({ + content: `${emojis.rightArrow2} Closing ticket...`, + components: [] + }); + + const ticket = await getTicketId(interaction.guild.id, channel.id); + + if (ticket) { + await removeTicket(ticket.id); + } + + await channel.delete(`Ticket closed by ${interaction.user.tag}.`); } } diff --git a/src/lib/tickets.ts b/src/lib/tickets.ts new file mode 100644 index 0000000..823a00e --- /dev/null +++ b/src/lib/tickets.ts @@ -0,0 +1,71 @@ +import { prisma } from "./prisma.js"; + +export async function createTicket( + guildId: string, + guildName: string, + userId: string, + reason: string, +) { + return prisma.$transaction(async (tx) => { + await tx.server.upsert({ + where: { id: guildId }, + create: { id: guildId, name: guildName }, + update: { name: guildName }, + }); + + const server = await tx.server.update({ + where: { id: guildId }, + data: { + nextTicketNumber: { + increment: 1, + }, + }, + select: { + nextTicketNumber: true, + }, + }); + + return tx.ticket.create({ + data: { + guildId, + ticketNumber: server.nextTicketNumber - 1, + userId, + reason, + }, + }); + }); +} + +export async function setTicketChannelId(ticketId: string, channelId: string) { + return prisma.ticket.update({ + where: { id: ticketId }, + data: { channelId }, + }); +} + +export async function getTickets(guildId: string, userId: string) { + return prisma.ticket.findMany({ + where: { guildId, userId }, + orderBy: { createdAt: 'desc' }, + }); +} + +export async function removeTicket(ticketId: string) { + return prisma.ticket.delete({ where: { id: ticketId } }); +} + +export async function clearTickets(guildId: string, userId: string) { + return prisma.ticket.deleteMany({ where: { guildId, userId } }); +} + +export async function getTicket(ticketId: string, guildId: string) { + const ticket = await prisma.ticket.findUnique({ where: { id: ticketId } }); + if (!ticket || ticket.guildId !== guildId) return null; + return ticket; +} + +export async function getTicketId(guildId: string, channelId: string) { + return prisma.ticket.findFirst({ + where: { guildId, channelId } + }); +} From f0beab800ee276db3346da9c3b6e76565ac0de5c Mon Sep 17 00:00:00 2001 From: unbravechimp Date: Mon, 4 May 2026 21:42:02 +0200 Subject: [PATCH 2/2] upd: change status Co-authored-by: Copilot --- src/index.ts | 12 +++++++++++- src/listeners/ready.ts | 13 +------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/index.ts b/src/index.ts index 804d698..0a8e11c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import { RegisterBehavior, SapphireClient } from '@sapphire/framework'; -import { GatewayIntentBits } from 'discord.js'; +import { ActivityType, GatewayIntentBits } from 'discord.js'; ApplicationCommandRegistries.setDefaultBehaviorWhenNotIdentical( RegisterBehavior.BulkOverwrite @@ -13,6 +13,16 @@ ApplicationCommandRegistries.setDefaultBehaviorWhenNotIdentical( const client = new SapphireClient({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers], + presence: { + activities: [ + { + name: 'Tickets!', + type: ActivityType.Playing, + state: 'New Feature' + } + ], + status: 'online' + }, baseUserDirectory: fileURLToPath(new URL('.', import.meta.url)) }); diff --git a/src/listeners/ready.ts b/src/listeners/ready.ts index 35e10f2..82ff336 100644 --- a/src/listeners/ready.ts +++ b/src/listeners/ready.ts @@ -1,5 +1,5 @@ import { Listener } from '@sapphire/framework'; -import { ActivityType, Events, type Client } from 'discord.js'; +import { Events, type Client } from 'discord.js'; import { enforceMute, getActiveMutes } from '#lib/mutes.js'; import { heartbeat } from '#utils/heartbeat.js'; import { purgeExpiredBans } from '#lib/bans.js'; @@ -21,17 +21,6 @@ export class ReadyListener extends Listener { heartbeat(client); reminderScheduler(client); - client.user.setPresence({ - activities: [ - { - name: '🎫 Tickets!', - type: ActivityType.Watching, - state: 'New Feature!' - } - ], - status: 'idle' - }); - const enforceMutes = async () => { const mutes = await getActiveMutes(); for (const mute of mutes) {