import type { Embed, TopLevelComponent } from "discord.js"; import { and, desc, eq } from "drizzle-orm"; import { randomUUID } from "node:crypto"; import db, { type Ticket, ticketAttachments, ticketComments, ticketMessages, ticketParticipants, tickets, } from ".."; export const ticketWithRelations = { guild: true, opener: { with: { user: true, }, }, subjectActor: { with: { user: true, }, }, claimedByActor: { with: { user: true, }, }, closedByActor: { with: { user: true, }, }, participants: { with: { actor: { with: { user: true, }, }, }, }, messages: { orderBy: { createdAt: "asc", }, with: { actor: { with: { user: true, }, }, attachments: true, }, }, attachments: { with: { actor: { with: { user: true, }, }, }, }, comments: { with: { actor: { with: { user: true, }, }, }, }, } as const; export async function getTicketById(id: string) { return await db.query.tickets.findFirst({ with: ticketWithRelations, where: { id: id, }, }); } export type TicketWithRelations = NonNullable>>; export async function getTicket(ticketId: string): Promise; export async function getTicket( guildId: number, ticketId: number, ): Promise; export async function getTicket( idOrGuildId: string | number, ticketId?: number, ): Promise { return typeof idOrGuildId === "string" ? getTicketById(idOrGuildId) : getTicketByGuild(idOrGuildId, ticketId); } export async function getTicketByGuild( guildId: number, localId?: number, ): Promise { if (localId === undefined) return undefined; return await db.query.tickets.findFirst({ where: { guildId: guildId, localId: localId, }, with: ticketWithRelations, }); } export async function getTicketByUser( userId: number, ticketNumber: number, ): Promise { return await db.query.tickets.findFirst({ where: { openedBy: userId, localId: ticketNumber, }, with: ticketWithRelations, }); } export async function getTicketByChannelId( channelId: string, ): Promise { return await db.query.tickets.findFirst({ where: { channelId: channelId, }, with: ticketWithRelations, }); } export async function editTicket( ticketId: string, data: Partial, ): Promise { const [result] = await db.update(tickets).set(data).where(eq(tickets.id, ticketId)).returning(); if (!result) return undefined; return await getTicketById(ticketId); } export async function getTicketsByUser(userId: number): Promise { return await db.query.tickets.findMany({ where: { openedBy: userId, }, with: ticketWithRelations, }); } // standard ops export async function createTicket(data: { guildId: number; openedBy: number; subject: number; status: string; channelId?: string | null; openReason?: string | null; }): Promise { const id = randomUUID(); return await db.transaction(async (tx) => { const [lastTicket] = await tx .select({ localId: tickets.localId }) .from(tickets) .where(eq(tickets.guildId, data.guildId)) .orderBy(desc(tickets.localId)) .limit(1); const nextLocalId = lastTicket ? lastTicket.localId + 1 : 1; const [newTicket] = await tx .insert(tickets) .values({ id: id, guildId: data.guildId, localId: nextLocalId, status: data.status, channelId: data.channelId, openedBy: data.openedBy, subject: data.subject, openReason: data.openReason, openedAt: new Date(), }) .returning(); if (!newTicket) throw new Error("Failed to create ticket"); await tx.insert(ticketParticipants).values({ ticketId: newTicket.id, actorId: data.openedBy, role: data.openedBy === data.subject ? "subject" : "participant", createdAt: new Date(), }); const populatedTicket = await tx.query.tickets.findFirst({ where: { id: newTicket.id, }, with: ticketWithRelations, }); if (!populatedTicket) throw new Error("Failed to retrieve created ticket"); return populatedTicket as TicketWithRelations; }); } export async function closeTicket(data: { ticketId: string; closedBy: number; reason?: string | null; status?: string; }): Promise { const [closedTicket] = await db .update(tickets) .set({ status: data.status ?? "closed", closedAt: new Date(), closedBy: data.closedBy, closeReason: data.reason, }) .where(eq(tickets.id, data.ticketId)) .returning(); if (!closedTicket) return undefined; return await getTicketById(data.ticketId); } export async function claimTicket( ticketId: string, actorId: number, ): Promise { const [ticket] = await db .update(tickets) .set({ claimedBy: actorId, claimedAt: new Date(), }) .where(eq(tickets.id, ticketId)) .returning(); if (!ticket) return undefined; return await getTicketById(ticketId); } export async function unclaimTicket(ticketId: string): Promise { const [ticket] = await db .update(tickets) .set({ claimedBy: null, claimedAt: null, }) .where(eq(tickets.id, ticketId)) .returning(); if (!ticket) return undefined; return await getTicketById(ticketId); } // messages export async function createTicketMessageWithAttachments(data: { id: string; ticketId: string; actorId: number; messageId?: string | null; content: string; actorRole: "participant" | "moderator" | "subject" | "bot"; isPrivate?: boolean; embeds?: Embed[]; components?: TopLevelComponent[]; attachments?: { id: string; fileName: string; fileType: string; fileSize: number; }[]; }) { return await db.transaction(async (tx) => { const [message] = await tx .insert(ticketMessages) .values({ id: data.id, messageId: data.messageId, ticketId: data.ticketId, actorId: data.actorId, isPrivate: data.isPrivate, content: data.content, actorRole: data.actorRole, embeds: data.embeds, components: data.components, createdAt: new Date(), }) .returning(); if (data.attachments && data.attachments.length > 0) { await tx.insert(ticketAttachments).values( data.attachments.map((att) => ({ id: att.id, messageId: message?.id, ticketId: data.ticketId, actorId: data.actorId, fileName: att.fileName, fileType: att.fileType, fileSize: att.fileSize, })), ); } const ticketUpdate: Partial = { lastMessageAt: new Date(), }; if (data.actorRole === "moderator") ticketUpdate.hasModeratorMessage = true; else if (data.actorRole === "subject") ticketUpdate.hasAuthorMessage = true; await tx.update(tickets).set(ticketUpdate).where(eq(tickets.id, data.ticketId)); return message; }); } export async function getTicketTranscript(ticketId: string) { return await db.query.tickets.findFirst({ where: { id: ticketId }, with: { opener: true, subjectActor: true, claimedByActor: true, participants: { with: { actor: true, }, }, messages: { orderBy: (messages, { asc }) => [asc(messages.createdAt)], with: { actor: true, attachments: true, }, }, }, }); } export const ticketMessageWithRelations = { actor: { with: { user: true, }, }, attachments: true, } as const; export async function getTicketMessage(id: string) { return await db.query.ticketMessages.findFirst({ where: { id: id, }, with: ticketMessageWithRelations, }); } export type TicketMessageWithRelations = NonNullable>>; export async function getTicketMessageByMessageId( messageId: string, ): Promise { return await db.query.ticketMessages.findFirst({ where: { messageId: messageId, }, with: ticketMessageWithRelations, }); } export async function getTicketMessages( ticketId: string, options?: { includeDeleted?: boolean }, ): Promise { return await db.query.ticketMessages.findMany({ where: options?.includeDeleted ? { ticketId: ticketId } : { ticketId: ticketId, deletedAt: { isNull: true }, }, orderBy: { createdAt: "asc", }, with: ticketMessageWithRelations, }); } export async function editTicketMessage( id: string, data: { content?: string; embeds?: Embed[]; components?: TopLevelComponent[] }, ): Promise { const [updated] = await db .update(ticketMessages) .set({ editedAt: new Date(), ...data, }) .where(eq(ticketMessages.id, id)) .returning(); if (!updated) return undefined; return await getTicketMessage(id); } export async function deleteTicketMessage( id: string, ): Promise { const [deleted] = await db .update(ticketMessages) .set({ deletedAt: new Date(), }) .where(eq(ticketMessages.id, id)) .returning(); if (!deleted) return undefined; return await getTicketMessage(id); } export async function restoreTicketMessage( id: string, ): Promise { const [restored] = await db .update(ticketMessages) .set({ deletedAt: null, }) .where(eq(ticketMessages.id, id)) .returning(); if (!restored) return undefined; return await getTicketMessage(id); } export async function setTicketMessagePrivacy( id: string, isPrivate: boolean, ): Promise { const [updated] = await db .update(ticketMessages) .set({ isPrivate, }) .where(eq(ticketMessages.id, id)) .returning(); if (!updated) return undefined; return await getTicketMessage(id); } export async function setTicketMessageExternalId( id: string, messageId: string, ): Promise { const [updated] = await db .update(ticketMessages) .set({ messageId, }) .where(eq(ticketMessages.id, id)) .returning(); if (!updated) return undefined; return await getTicketMessage(id); } // participants export async function addTicketParticipant(data: { ticketId: string; actorId: number; role: "participant" | "moderator" | "subject"; reason?: string | null; }) { const participant = await db.insert(ticketParticipants).values({ ticketId: data.ticketId, actorId: data.actorId, role: data.role, reason: data.reason, createdAt: new Date(), }); return participant; } export async function addTicketParticipants( data: { ticketId: string; actorId: number; role: "participant" | "moderator" | "subject"; reason?: string | null; }[], ) { const participants = data.map((item) => ({ ticketId: item.ticketId, actorId: item.actorId, role: item.role, reason: item.reason, createdAt: new Date(), })); const result = await db.insert(ticketParticipants).values(participants).returning(); return result; } export async function removeTicketParticipant(ticketId: string, actorId: number) { const [removed] = await db .delete(ticketParticipants) .where(and(eq(ticketParticipants.ticketId, ticketId), eq(ticketParticipants.actorId, actorId))) .returning(); return removed; } export async function getTicketParticipants(ticketId: string) { return await db.query.ticketParticipants.findMany({ where: { ticketId: ticketId, }, with: { actor: true, }, }); } // comments export async function getTicketComments(ticketId: string) { return await db.query.ticketComments.findMany({ where: { ticketId: ticketId, }, with: { actor: { with: { user: true, }, }, }, }); } export async function addTicketComment(data: { ticketId: string; actorId: number; content: string; }) { const [comment] = await db .insert(ticketComments) .values({ id: randomUUID(), ticketId: data.ticketId, actorId: data.actorId, content: data.content, createdAt: new Date(), updatedAt: new Date(), }) .returning(); return comment; } export async function removeTicketComment(commentId: string) { const [removed] = await db .delete(ticketComments) .where(eq(ticketComments.id, commentId)) .returning(); return removed; } export async function editTicketComment(commentId: string, content: string) { const [updated] = await db .update(ticketComments) .set({ content: content, updatedAt: new Date(), }) .where(eq(ticketComments.id, commentId)) .returning(); return updated; } // attachments export async function getTicketAttachments(ticketId: string) { return await db.query.ticketAttachments.findMany({ where: { ticketId: ticketId, }, with: { actor: { with: { user: true, }, }, }, }); }