import type { TicketWithRelations } from "@stealth-developers/db"; import { Result } from "@sapphire/framework"; import { ActionRowBuilder, ButtonBuilder, type Channel, ContainerBuilder, DiscordAPIError, Message, TextDisplayBuilder, } from "discord.js"; import type { AnyUser, MessageSendPayload } from "@/types"; import { client } from "@/client"; import { PublicError, logger } from "@/lib"; import TicketButtonPresets from "../buttons"; export async function canDmUser(user: AnyUser): Promise { if (!("send" in user)) return false; return user .send({ content: "Hi! I'm just checking if your DMs are open.", flags: ["SuppressNotifications"], }) .then(() => true) .catch(() => false); } export function channelName(localId: number, status: string, claimedBy?: string) { const initNumber = localId.toString().padStart(4, "0"); // discord errors if we try to create channels with certain phrases in them const bad = ["1488", "1919"]; if (bad.includes(initNumber)) return `${status}-filtered`; if (claimedBy) { const firstLetters = claimedBy.slice(0, 3); return `${firstLetters}-${status}-${initNumber}`; } return `${status}-ticket-${localId}`; } export async function messageUser(user: AnyUser, message: MessageSendPayload) { if (!("send" in user)) throw new PublicError("Failed to message user: User is not a Discord user"); const result = await Result.fromAsync(user.send(message)); if (result.isErr()) { const error = result.unwrapErr(); if (error instanceof DiscordAPIError && [50007, 50278].includes(error.code as number)) throw new PublicError("Failed to message user: User has DMs disabled"); throw error; } return result.unwrap(); } export async function messageChannel(channelIn: string | Channel, message: MessageSendPayload) { const channel = typeof channelIn === "string" ? await client.channels.fetch(channelIn) : channelIn; if (!channel) throw new PublicError("Failed to message channel: Channel not found"); if (!("send" in channel)) throw new PublicError("Failed to message channel: Channel is not a text channel"); const result = await Result.fromAsync>(channel.send(message)); if (result.isErr()) { const error = result.unwrapErr(); logger.warn({ error }, "Failed to message channel"); if (error instanceof DiscordAPIError && [50007, 50278].includes(error.code as number)) throw new PublicError("Failed to message channel: Channel is not a text channel"); throw error; } return result.unwrap(); } export const text = (content: string) => new TextDisplayBuilder().setContent(content); /** * @param ticket the ticket to get the container for * @param limitedDetails whether to exclude comments & other internal details */ export function getTicketContainer(ticket: TicketWithRelations, limitedDetails = false) { const container = new ContainerBuilder(); container.addTextDisplayComponents(text(`# Ticket #${ticket.localId} - ${ticket.status}`)); let closedBySubject = true; if (ticket.status !== "open" && ticket.closedByActor && ticket.subjectActor) { closedBySubject = ticket.subjectActor.id === ticket.closedByActor.id; container.addTextDisplayComponents( text(`Closed by ${closedBySubject ? "the subject" : `<@${ticket.closedByActor.discordId}>`}`), ); container.addTextDisplayComponents(text(["**Reason**", `> ${ticket.closeReason}`].join("\n"))); if (!limitedDetails && ticket.privateReason) container.addTextDisplayComponents( text(["**Private Reason**", `> ${ticket.privateReason}`].join("\n")), ); } const hasAttachments = ticket.attachments.length > 0; if (hasAttachments) container.addTextDisplayComponents(text(`**Attachments:** ${ticket.attachments.length}`)); else container.addTextDisplayComponents(text("**Attachments:** no attachments")); container.addTextDisplayComponents(text(`-# ${ticket.id}`)); const actionRow = new ActionRowBuilder(); if (limitedDetails) actionRow.addComponents(TicketButtonPresets.thankButton(ticket.id)); if (!limitedDetails) actionRow.addComponents( TicketButtonPresets.viewCommentsButton(ticket.id, ticket.comments.length), ); actionRow.addComponents(TicketButtonPresets.transcriptsLink(ticket.id)); container.addActionRowComponents(actionRow); return container; }