all repos — stealth-developers @ 9f330c936258e221eed58cf587262ebb4d796ba8

apps/bot/src/feats/tickets/actions/helpers.ts (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
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<boolean> {
	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<Message<boolean>>(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<ButtonBuilder>();
	if (limitedDetails) actionRow.addComponents(TicketButtonPresets.thankButton(ticket.id));
	if (!limitedDetails)
		actionRow.addComponents(
			TicketButtonPresets.viewCommentsButton(ticket.id, ticket.comments.length),
		);
	actionRow.addComponents(
		TicketButtonPresets.transcriptsLink(
			`https://tickets.vt3e.cat/guilds/${ticket.guild!.discordId}/tickets/${ticket.localId}`,
		),
	);

	container.addActionRowComponents(actionRow);
	return container;
}