all repos — stealth-developers @ 4f5dd279f3b91bc869c6255b69bfe4d5d05c5f21

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
import { randomUUID } from "node:crypto";
import { TextChannel, ChannelType } from "discord.js";
import { container, Result } from "@sapphire/framework";
import type { Subcommand } from "@sapphire/plugin-subcommands";

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

export * from "./config";

import { errorMessage, upsertUser, getCategory, PublicError } from "@/lib";

export async function createTicket(interaction: Subcommand.ChatInputCommandInteraction) {
	if (!interaction.guild)
		return errorMessage(interaction, "Failed to create ticket: Guild not found");

	const isOnBehalf = interaction.options.getSubcommand(true) === "for";
	const publicId = randomUUID();

	if (!interaction.member)
		return errorMessage(interaction, "Failed to create ticket: Member not found");

	const creator = await upsertUser(interaction.member, interaction.guild);
	if (!creator) return errorMessage(interaction, "Failed to create ticket: Creator not found");

	const subject = isOnBehalf ? interaction.options.getUser("user") : interaction.user;
	const subjectUser = subject ? await upsertUser(subject, interaction.guild!) : creator;
	if (!subjectUser)
		return errorMessage(interaction, "Failed to create ticket: Subject user not found");

	if (isOnBehalf && !creator.moderator)
		return errorMessage(
			interaction,
			"You don't have permission to create a ticket for another user.",
		);

	const ticketCategory = await getCategory(config.discord.server.ticket_category);
	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.");

			const ticketNumber = ticket.localId;
			const paddedTicketNumber = String(ticketNumber).padStart(4, "0");

			const channel = await interaction.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);

		return errorMessage(interaction, message);
	}

	const channelId = txResult.unwrap();
	interaction.reply({
		content: `Ticket created! See <#${channelId}>`,
		flags: ["Ephemeral"],
	});
}