all repos — stealth-developers @ 2c1d94cc8c22d2f411f9b6449232111101a7113c

bot: extract some new logic to actions.ts
vi did:web:vt3e.cat
Fri, 26 Jun 2026 19:43:03 +0100
commit

2c1d94cc8c22d2f411f9b6449232111101a7113c

parent

a589dc8e61388a32c604b15ce207ad4f80154ad8

M apps/bot/src/feats/tickets/actions.tsapps/bot/src/feats/tickets/actions.ts

@@ -3,6 +3,7 @@ import {

TextChannel, ChannelType, type Guild, + type Channel, ContainerBuilder, TextDisplayBuilder, ActionRowBuilder,

@@ -25,12 +26,14 @@ type TicketWithRelations,

} from "@stealth-developers/db"; import { upsertUser, getCategory, PublicError, logger } from "@/lib"; +import { hasAccessToTicket } from "@/lib/tickets"; +import { client } from "@/client"; import type { AnyUser, MessageSendPayload } from "@/types"; + import { TicketButtonPresets } from "./buttons"; import { TicketModalPresets } from "./modals"; -import { client } from "@/client"; import { getGuildActor } from "../guild-actor"; -import type { Channel } from "discord.js"; +import type { TicketContext } from "./types"; type CreateTicketArgs = | undefined

@@ -38,6 +41,8 @@ | {

for: AnyUser; reason?: string; }; + +const text = (content: string) => new TextDisplayBuilder().setContent(content); export async function createTicket( opener: AnyUser,

@@ -209,8 +214,6 @@

return TicketModalPresets.close(ticket.id, Boolean(dbUser.moderator)); } -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

@@ -352,6 +355,128 @@ }

} return result; +} + +export async function reopenTicket(ctx: TicketContext) { + const { actor, guild, ticket, ticketChannel } = ctx; + if (ticket.status !== "closed") throw new PublicError("This ticket is not closed."); + + const canAccess = hasAccessToTicket(actor, guild, ticket, { requireModerator: true }); + if (!canAccess) throw new PublicError("You don't have permission to reopen this ticket."); + + const newTicket = await editTicket(ticket.id, { + status: "open", + closedAt: null, + closedBy: null, + + claimedBy: actor.id, + claimedAt: new Date(), + lastClaimantActivityAt: new Date(), + }); + + if (ticketChannel) { + messageChannel(ticketChannel, { + content: `<@${actor.discordId}> reopened this ticket.`, + flags: ["SuppressNotifications"], + }); + + if (ticketChannel.type === ChannelType.GuildText) { + ticketChannel.edit({ + name: channelName(ticket.localId, "claimed", actor.displayName || actor.username!), + }); + } + } + + return newTicket; +} + +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) { + messageChannel(ticketChannel, { + content: `<@${actor.discordId}> has claimed this ticket, they will be with you shortly.`, + flags: ["SuppressNotifications"], + }); + + if (ticketChannel.type === ChannelType.GuildText) { + ticketChannel.edit({ + name: channelName(ticket.localId, "claimed", actor.displayName || actor.username!), + }); + } + } + + return newTicket; +} + +export async function addComment(ctx: TicketContext, comment: string) { + const { actor, guild, ticket } = ctx; + + const canAccess = hasAccessToTicket(actor, guild, ticket, { requireModerator: true }); + if (!canAccess) + throw new PublicError("You don't have permission to add a comment to this ticket."); + + const dbComment = await addTicketComment({ + ticketId: ticket.id, + actorId: actor.id, + content: comment, + }); + + if (!dbComment) throw new PublicError("Failed to add comment to ticket."); + return dbComment; +} + +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) { + messageChannel(ticketChannel, { + content: `<@${actor.discordId}> unclaimed this ticket, someone else will be with you shortly.`, + flags: ["SuppressNotifications"], + }); + + if (ticketChannel.type === ChannelType.GuildText) { + ticketChannel.edit({ + name: channelName(ticket.localId, "open"), + }); + } + } + + return newTicket; } export async function canDmUser(user: AnyUser): Promise<boolean> {
M apps/bot/src/feats/tickets/commands.tsapps/bot/src/feats/tickets/commands.ts

@@ -1,22 +1,16 @@

import { option } from "@purrkit/router"; import { Result } from "@sapphire/result"; import { ButtonBuilder, ContainerBuilder, TextDisplayBuilder, ActionRowBuilder } from "discord.js"; -import { - addTicketComment, - editTicket, - getTicketByChannelId, - getTicketByGuild, - getTicketByUser, -} from "@stealth-developers/db"; +import { getTicketByChannelId, getTicketByGuild, getTicketByUser } from "@stealth-developers/db"; -import { errorMessage, getTextChannel, logger, PublicError } from "@/lib"; +import { errorMessage, getTextChannel, handleError, logger, PublicError } from "@/lib"; +import { hasAccessToTicket } from "@/lib/tickets"; +import { useGetData } from "@/middleware"; + import { TicketButtonPresets } from "./buttons"; import * as actions from "./actions"; import { manageConfig } from "./config"; -import { useGetData } from "@/middleware"; -import { hasAccessToTicket } from "@/lib/tickets"; import { TicketModalPresets } from "./modals"; -import { ChannelType } from "discord-api-types/v9"; const useGetTicketData = useGetData.use(async (interaction, { guild }) => { if (interaction.isChatInputCommand()) {

@@ -160,37 +154,14 @@ });

group.subcommand("reopen", { description: "Reopen a ticket", - async run(interaction, _, { actor, guild, ticket, ticketChannel }) { - if (!interaction.guild || !guild || !actor) { - return errorMessage(interaction, "This command can only be used in a server."); - } else if (!actor.moderator) { - return errorMessage(interaction, "You do not have permission to use this command."); - } else if (!ticket) { - return errorMessage(interaction, "This command can only be used in a ticket."); - } - - await editTicket(ticket.id, { - status: "open", - closedAt: null, - closedBy: null, - - claimedBy: actor.id, - claimedAt: new Date(), - lastClaimantActivityAt: new Date(), - }); + async run(interaction, _, { actor, ticket, ticketChannel, guild }) { + if (!ticket || !ticketChannel || !actor || !guild) + return errorMessage(interaction, "Failed to find ticket"); - if (ticketChannel) { - actions.messageChannel(ticketChannel, { - content: `<@${interaction.user.id}> reopened this ticket.`, - flags: ["SuppressNotifications"], - }); - - if (ticketChannel.type === ChannelType.GuildText) { - ticketChannel.edit({ - name: actions.channelName(ticket.localId, "claimed", interaction.user.displayName), - }); - } - } + const result = await Result.fromAsync( + actions.reopenTicket({ ticket, ticketChannel, actor, guild }), + ); + if (result.isErr()) return handleError(interaction, result.unwrapErr()); await interaction.reply({ content: `Ticket has been reopened.`,

@@ -201,50 +172,14 @@ });

group.subcommand("claim", { description: "Claim the ticket", - async run(interaction, _, { actor, ticket, ticketChannel }) { - if (!interaction.guild) { - return errorMessage(interaction, "This command can only be used in a server."); - } + async run(interaction, _, { actor, ticket, ticketChannel, guild }) { + if (!ticket || !ticketChannel || !actor || !guild) + return errorMessage(interaction, "Failed to find ticket"); - if (!ticket) { - return errorMessage(interaction, "This command can only be used in a ticket."); - } else if (!actor?.moderator) { - return errorMessage(interaction, "You do not have permission to use this command."); - } else if (ticket.status !== "open") { - return errorMessage(interaction, "This ticket is not open."); - } - - 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); - return errorMessage( - interaction, - `This ticket was claimed by <@${ticket.claimedByActor?.discordId}> <t:${timeAgo}:R>.`, - ); - } - - await editTicket(ticket.id, { - claimedAt: new Date(), - claimedBy: actor.id, - lastClaimantActivityAt: new Date(), - }); - - if (ticketChannel) { - actions.messageChannel(ticketChannel, { - content: `<@${interaction.user.id}> has claimed this ticket, they will be with you shortly.`, - flags: ["SuppressNotifications"], - }); - - if (ticketChannel.type === ChannelType.GuildText) { - ticketChannel.edit({ - name: actions.channelName(ticket.localId, "claimed", interaction.user.displayName), - }); - } - } + const result = await Result.fromAsync( + actions.claimTicket({ ticket, ticketChannel, actor, guild }), + ); + if (result.isErr()) return handleError(interaction, result.unwrapErr()); await interaction.reply({ content: "Ticket claimed!",

@@ -255,41 +190,14 @@ });

group.subcommand("unclaim", { description: "Unclaim a ticket", - async run(interaction, _, { actor, ticket, ticketChannel }) { - if (!interaction.guild) { - return errorMessage(interaction, "This command can only be used in a server."); - } - - if (!ticket) { - return errorMessage(interaction, "This command can only be used in a ticket."); - } else if (!actor?.moderator) { - return errorMessage(interaction, "You do not have permission to use this command."); - } else if (ticket.status !== "open") { - return errorMessage(interaction, "This ticket is not open."); - } - - if (ticket.claimedBy !== actor.id) { - return errorMessage(interaction, "You have not claimed this ticket."); - } - - await editTicket(ticket.id, { - claimedAt: null, - claimedBy: null, - lastClaimantActivityAt: null, - }); - - if (ticketChannel) { - actions.messageChannel(ticketChannel, { - content: `<@${interaction.user.id}> unclaimed this ticket, someone else will be with you shortly.`, - flags: ["SuppressNotifications"], - }); + async run(interaction, _, { actor, ticket, ticketChannel, guild }) { + if (!ticket || !ticketChannel || !actor || !guild) + return errorMessage(interaction, "Failed to find ticket"); - if (ticketChannel.type === ChannelType.GuildText) { - ticketChannel.edit({ - name: actions.channelName(ticket.localId, "open"), - }); - } - } + const result = await Result.fromAsync( + actions.unclaimTicket({ ticket, ticketChannel, actor, guild }), + ); + if (result.isErr()) return handleError(interaction, result.unwrapErr()); await interaction.reply({ content: `You have unclaimed ticket #${ticket.localId}.`,

@@ -304,24 +212,21 @@ options: {

ticket: option.number("The ID of the ticket", { required: false }), comment: option.string("The comment to add", { required: false }), }, - async run(interaction, { comment }, { actor, guild, ticket }) { - if (!interaction.guild || !guild || !actor) - return errorMessage(interaction, "This command can only be used in a server."); + async run(interaction, { comment }, { actor, ticket, ticketChannel, guild }) { + if (!ticket || !ticketChannel || !actor || !guild) + return errorMessage(interaction, "Failed to find ticket"); if (!actor.moderator) - return errorMessage(interaction, "You do not have permission to use this command."); - - if (!ticket) return errorMessage(interaction, "Ticket not found."); + return errorMessage(interaction, "You must be a moderator to use this command"); if (comment) { - await addTicketComment({ - actorId: actor.id, - content: comment, - ticketId: ticket.id, - }); + const result = await Result.fromAsync( + actions.addComment({ actor, ticket, guild }, comment), + ); + if (result.isErr()) return handleError(interaction, result.unwrapErr()); await interaction.reply({ - content: `Comment added to ticket ${ticket.id}`, + content: `Comment added to ticket.`, flags: ["Ephemeral"], }); return;
A apps/bot/src/feats/tickets/types.ts

@@ -0,0 +1,9 @@

+import type { Guild as DbGuild, Actor, TicketWithRelations } from "@stealth-developers/db"; +import type { Channel } from "discord.js"; + +export type TicketContext = { + ticket: TicketWithRelations; + ticketChannel?: Channel | null; + actor: Actor; + guild: DbGuild; +};
M apps/bot/src/lib/error.tsapps/bot/src/lib/error.ts

@@ -37,16 +37,7 @@ fallbackMessage?: string,

) { const context = getInteractionContext(interaction); - if (error instanceof PublicError) { - logger.warn( - { - error: error instanceof Error ? { message: error.message, stack: error.stack } : error, - ...context, - }, - "A public error occurred during interaction execution", - ); - return await errorMessage(interaction, error.message); - } + if (error instanceof PublicError) return await errorMessage(interaction, error.message); logger.error( {
M apps/bot/src/lib/tickets.tsapps/bot/src/lib/tickets.ts

@@ -5,12 +5,18 @@ type TicketWithRelations,

type Guild, } from "@stealth-developers/db"; -export async function hasAccessToTicket( +export function hasAccessToTicket( user: Actor | User, guild: Guild, ticket: TicketWithRelations, + { requireModerator = false }: { requireModerator?: boolean } = {}, ) { + const isModerator = "moderator" in user && user.guildId === guild.id && user.moderator; + + if (requireModerator) return isModerator; + if (user.discordId === ticket.subjectActor?.discordId) return true; - if ("moderator" in user) if (user.guildId === guild.id && user.moderator) return true; + if (isModerator) return true; + return false; }