all repos — stealth-developers @ de82bde6bbf10b37933301403c47e641dd8288e6

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
import {
	type Actor,
	addTicketComment,
	addTicketParticipant,
	editTicket,
} from "@stealth-developers/db";
import { ActionRowBuilder, ButtonBuilder } 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 { 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}`),
		);
	}

	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}`),
		);
	}

	return newTicket;
}

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