import { type Actor, addTicketComment, addTicketParticipant, editTicket, } from "@stealth-developers/db"; import { ActionRowBuilder, ButtonBuilder } from "discord.js"; import { getGuildActor } from "f/guild-actor"; import { PublicError, logger } from "@/lib"; import type { TicketContext } from "../types"; import { TicketButtonPresets } from "../buttons"; import { hasAccessToTicket } from "../lib"; import { messageChannel } from "./helpers"; export async function claimTicket(ctx: TicketContext) { const { ticket, actor, ticketChannel, guild } = ctx; if (ticket.status !== "open") throw new Error("Ticket is not open"); const canAccess = hasAccessToTicket(actor, guild, ticket, { requireModerator: true }); if (!canAccess) throw new PublicError("You don't have permission to reopen this ticket."); const lastClaimerActive = ticket.lastClaimantActivityAt ? new Date(ticket.lastClaimantActivityAt) : null; // if the last claimer was active in the last 15 minutes, don't allow claiming if (lastClaimerActive && lastClaimerActive.getTime() > Date.now() - 1000 * 60 * 15) { const timeAgo = Math.floor(lastClaimerActive.getTime() / 1000); throw new PublicError( `This ticket was claimed by <@${ticket.claimedByActor?.discordId}> .`, ); } const newTicket = await editTicket(ticket.id, { claimedAt: new Date(), claimedBy: actor.id, lastClaimantActivityAt: new Date(), }); if (ticketChannel) { const actionRow = new ActionRowBuilder().addComponents( TicketButtonPresets.unclaimTicketButton(ticket.id), ); messageChannel(ticketChannel, { content: `<@${actor.discordId}> has claimed this ticket, they will be with you shortly.`, flags: ["SuppressNotifications"], components: [actionRow], }).catch((err) => logger.error(err, `failed to send claim message to channel ${ticketChannel.id}`), ); } return newTicket; } export async function unclaimTicket(ctx: TicketContext) { const { ticket, actor, ticketChannel, guild } = ctx; if (ticket.status !== "open") throw new Error("Ticket is not open"); const canAccess = hasAccessToTicket(actor, guild, ticket, { requireModerator: true }); if (!canAccess) throw new PublicError("You don't have permission to reopen this ticket."); if (ticket.claimedBy !== actor.id) throw new PublicError("You have not claimed this ticket."); const newTicket = await editTicket(ticket.id, { claimedAt: null, claimedBy: null, lastClaimantActivityAt: null, }); if (ticketChannel) { const actionRow = new ActionRowBuilder().addComponents( TicketButtonPresets.claimTicketButton(ticket.id), ); messageChannel(ticketChannel, { content: `<@${actor.discordId}> unclaimed this ticket, someone else will be with you shortly.`, flags: ["SuppressNotifications"], components: [actionRow], }).catch((err) => logger.error(err, `Failed to send unclaim message to channel ${ticketChannel.id}`), ); } return newTicket; } export async function addParticipant( guildId: string, data: { actor: Actor; ticketId: string; role: "participant" | "moderator" | "subject"; reason?: string; }, redactUser = false, ) { const botActor = getGuildActor(guildId); if (!botActor) throw new PublicError("Failed to close ticket: Bot actor not found."); await addTicketParticipant({ actorId: data.actor.id, ticketId: data.ticketId, role: data.role, reason: data.reason, }); await addTicketComment({ actorId: botActor.id, ticketId: data.ticketId, content: redactUser ? `Added participant - ${data.reason ?? "no reason provided"}` : `Added participant: ${data.actor.displayName || data.actor.username!} - ${data.reason ?? "no reason provided"}`, }); }