all repos — stealth-developers @ d206557e10ffca892f403def6d3d8f04e432f01a

apps/bot/src/feats/tickets/actions/assignment.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
 136
import {
	type Actor,
	addTicketComment,
	addTicketParticipant,
	editTicket,
} from "@stealth-developers/db";
import { ActionRowBuilder, ButtonBuilder, ChannelType } 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 { channelName, 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}> <t:${timeAgo}:R>.`,
		);
	}

	const newTicket = await editTicket(ticket.id, {
		claimedAt: new Date(),
		claimedBy: actor.id,
		lastClaimantActivityAt: new Date(),
	});

	if (ticketChannel) {
		const actionRow = new ActionRowBuilder<ButtonBuilder>().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}`),
		);

		if (ticketChannel.type === ChannelType.GuildText) {
			ticketChannel
				.edit({
					name: channelName(ticket.localId, "claimed", actor.displayName || actor.username!),
				})
				.catch((err) =>
					logger.error(err, `failed to update channel name on claim for ${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<ButtonBuilder>().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}`),
		);

		if (ticketChannel.type === ChannelType.GuildText) {
			ticketChannel
				.edit({
					name: channelName(ticket.localId, "open"),
				})
				.catch((err) =>
					logger.error(err, `Failed to update channel name on unclaim for ${ticketChannel.id}`),
				);
		}
	}

	return newTicket;
}

export async function addParticipant(
	guildId: string,
	data: {
		actor: Actor;
		ticketId: string;
		role: "participant" | "moderator" | "subject";
		reason?: string;
	},
) {
	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: `Added participant: ${data.actor.displayName || data.actor.username!} - ${data.reason ?? "no reason provided"}`,
	});
}