import { randomUUID } from "node:crypto"; import { TextChannel, ChannelType, type Guild, ContainerBuilder, TextDisplayBuilder, ActionRowBuilder, } from "discord.js"; import { Result } from "@sapphire/framework"; import db, { desc, eq, getGuildByDiscordId, getTicketByChannelId, tickets, } from "@stealth-developers/db"; export * from "./config"; import { upsertUser, getCategory, PublicError, logger } from "@/lib"; import type { AnyUser } from "@/types"; import { ButtonBuilder } from "discord.js"; import TicketButtonPresets from "@/components/buttons/tickets"; import { TicketModalPresets } from "@/components/modals"; type CreateTicketArgs = | undefined | { for: AnyUser; reason?: string; }; export async function createTicket( opener: AnyUser, guild: Guild, args?: CreateTicketArgs, ): Promise<{ channelId: string; publicId: string }> { const isOnBehalf = !!args?.for; const publicId = randomUUID(); const dbGuild = await getGuildByDiscordId(guild.id); if (!dbGuild) throw new PublicError("Failed to create ticket: Guild not found in database"); const creator = await upsertUser(opener, guild); if (!creator) throw new PublicError("Failed to create ticket: Creator user not found"); const subject = isOnBehalf ? args?.for : opener; const subjectUser = subject ? await upsertUser(subject, guild) : creator; if (!subjectUser) throw new PublicError("Failed to create ticket: Subject user not found"); if (isOnBehalf && !creator.moderator) throw new PublicError("You don't have permission to create a ticket for another user."); if (!dbGuild.ticketCategory) throw new PublicError( "Ticket category is not set up in this guild. Please contact an administrator.", ); const ticketCategory = await getCategory(dbGuild.ticketCategory); let createdChannel: TextChannel | undefined = undefined; const txResult = await Result.fromAsync(async () => { return await db.transaction(async (tx) => { const [maxTicket] = await tx .select({ localId: tickets.localId }) .from(tickets) .where(eq(tickets.guildId, creator.guildId)) .orderBy(desc(tickets.localId)) .limit(1); const localId = (maxTicket?.localId ?? 0) + 1; const [ticket] = await tx .insert(tickets) .values({ id: publicId, guildId: creator.guildId, localId: localId, status: "open", openedAt: new Date(), openedBy: creator.id, subject: subjectUser.id, }) .returning(); if (!ticket) throw new PublicError("failed to add ticket to database, please try again."); const ticketNumber = ticket.localId; const paddedTicketNumber = String(ticketNumber).padStart(4, "0"); const channel = await guild.channels.create({ name: `ticket-${paddedTicketNumber}`, type: ChannelType.GuildText, parent: ticketCategory, }); if (!channel) throw new PublicError("failed to create Discord channel."); createdChannel = channel; const greeting = dbGuild.ticketGreeting; if (greeting) { const container = new ContainerBuilder() .addTextDisplayComponents( new TextDisplayBuilder({ content: greeting.replaceAll("{user}", `<@${subjectUser.discordId}>`), }), ) .addActionRowComponents( new ActionRowBuilder().addComponents( TicketButtonPresets.claim(publicId), TicketButtonPresets.close(publicId), ), ); channel.send({ components: [container], flags: ["IsComponentsV2"] }); } await tx.update(tickets).set({ channelId: channel.id }).where(eq(tickets.id, ticket.id)); return channel; }); }); if (txResult.isErr()) { if (createdChannel) await Result.fromAsync(() => createdChannel?.delete()); const error = txResult.unwrapErr(); const message = error instanceof PublicError ? `Failed to create ticket: ${error.message}` : "There was an error creating the ticket."; if (error instanceof Error) logger.error(error.stack, error.message); throw new PublicError(message); } const channel = txResult.unwrap(); return { channelId: channel.id, publicId }; } export async function triggerCloseTicket(user: AnyUser, guild: Guild, channelId: string) { const dbGuild = await getGuildByDiscordId(guild.id); if (!dbGuild) throw new PublicError("Failed to create ticket: Guild not found in database"); const dbUser = await upsertUser(user, guild); if (!dbUser) throw new PublicError("Failed to create ticket: User not found in database"); const ticket = await getTicketByChannelId(channelId); if (!ticket) throw new PublicError("Failed to close ticket: Ticket not found in database"); return TicketModalPresets.close(ticket.id, Boolean(dbUser.moderator)); }