all repos — stealth-developers @ e9cebecae99c7f4cd8ba9e4c42d7b1fb84782a53

apps/bot/src/commands/tickets/actions.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
import { randomUUID } from "node:crypto";
import { TextChannel, ChannelType, type Guild } from "discord.js";
import { container, Result } from "@sapphire/framework";

import db, { desc, eq, getGuildByDiscordId, tickets } from "@stealth-developers/db";

export * from "./config";

import { upsertUser, getCategory, PublicError } from "@/lib";
import type { AnyUser } from "@/types";

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;

			await tx.update(tickets).set({ channelId: channel.id }).where(eq(tickets.id, ticket.id));
			await channel.send({ content: `Hi! Thanks for submitting a report.` });

			return channel.id;
		});
	});

	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) container.logger.error(error.message, error.stack);

		throw new PublicError(message);
	}

	const channelId = txResult.unwrap();
	return { channelId, publicId };
}