bot(tickets): pass in context rather than fetching
vi did:web:vt3e.cat
Fri, 03 Jul 2026 19:58:43 +0100
5 files changed,
90 insertions(+),
35 deletions(-)
M
apps/bot/src/feats/tickets/actions/helpers.ts
→
apps/bot/src/feats/tickets/actions/helpers.ts
@@ -18,10 +18,14 @@ import { PublicError, logger } from "@/lib";
import TicketButtonPresets from "../buttons"; -export async function canDmUser(user: AnyUser): Promise<boolean> { - if (!("send" in user)) return false; +export async function canDmUser(user: AnyUser | string): Promise<boolean> { + let discordUser = typeof user !== "string" ? user : undefined; + if (typeof user === "string") discordUser = await client.users.fetch(user); + if (!discordUser) return false; + + if (!("send" in discordUser)) return false; - return user + return discordUser .send({ content: "Hi! I'm just checking if your DMs are open.", flags: ["SuppressNotifications"],
M
apps/bot/src/feats/tickets/actions/lifecycle.ts
→
apps/bot/src/feats/tickets/actions/lifecycle.ts
@@ -29,7 +29,7 @@
import { client } from "@/client"; import { PublicError, getCategory, logger, upsertUser } from "@/lib"; -import type { CreateTicketArgs, TicketContext } from "../types"; +import type { CreateTicketArgs, NewTicketContext, TicketContext } from "../types"; import { TicketButtonPresets } from "../buttons"; import { hasAccessToTicket } from "../lib";@@ -40,24 +40,23 @@ import { getTicketContainer } from "./helpers";
import { canDmUser, messageChannel, messageUser } from "./helpers"; export async function createTicket( - opener: AnyUser, - guild: Guild, + ctx: NewTicketContext, args?: CreateTicketArgs, ): Promise<{ channelId: string; publicId: string }> { const isOnBehalf = !!args?.for; const publicId = randomUUID(); - const dbGuild = await getGuildByDiscordId(guild.id); + const dbGuild = ctx.dbGuild; if (!dbGuild) throw new PublicError("Failed to create ticket: Guild not found in database"); const moderators = await getModerators(dbGuild.id); if (!moderators) throw new PublicError("Failed to create ticket: No moderators found"); - const creator = await upsertUser(opener, guild); + const creator = ctx.creatorActor; 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; + const subject = isOnBehalf ? args?.for : ctx.creatorUser; + const subjectUser = subject ? await upsertUser(subject, ctx.guild) : creator; if (!subjectUser) throw new PublicError("Failed to create ticket: Subject user not found"); if (isOnBehalf && !creator.moderator)@@ -118,13 +117,13 @@ const { ticket, localId } = txResult.unwrap();
ticketId = ticket.id; const setupResult = await Result.fromAsync(async () => { - const channel = await guild.channels.create({ + const channel = await ctx.guild.channels.create({ type: ChannelType.GuildText, parent: ticketCategory, name: `ticket-${String(localId).padStart(4, "0")}`, permissionOverwrites: [ { - id: guild.id, + id: ctx.guild.id, type: OverwriteType.Role, deny: [PermissionFlagsBits.ViewChannel], },@@ -274,14 +273,12 @@
throw new PublicError(message); } - await addParticipant(guild.id, { + await addParticipant(ctx.guild.id, { actor: subjectUser, ticketId: ticketId!, role: "subject", reason: "subject of ticket", }); - - console.log(subjectUser); const channel = setupResult.unwrap(); return { channelId: channel.id, publicId };
M
apps/bot/src/feats/tickets/commands.ts
→
apps/bot/src/feats/tickets/commands.ts
@@ -15,7 +15,6 @@ upsertActor,
} from "@stealth-developers/db"; import { ActionRowBuilder, ButtonBuilder, ContainerBuilder, TextDisplayBuilder } from "discord.js"; import { ChannelType } from "discord.js"; -import { OverwriteType } from "discord.js"; import { PublicError, errorMessage, getTextChannel, handleError, logger, upsertUser } from "@/lib"; import { useGetTicketData } from "@/middleware";@@ -33,11 +32,20 @@ });
ticketCommand.subcommand("new", { description: "Open a new ticket.", - async run(interaction) { - if (!interaction.guild) + async run(interaction, _, { actor, guild }) { + await interaction.deferReply({ flags: ["Ephemeral"] }); + if (!guild || !interaction.guild) { return errorMessage(interaction, "You must run this command in a guild"); + } else if (!actor) { + return errorMessage(interaction, "Failed to retrieve your user data"); + } - const func = actions.createTicket(interaction.member ?? interaction.user, interaction.guild); + const func = actions.createTicket({ + creatorActor: actor, + creatorUser: interaction.member ?? interaction.user, + guild: interaction.guild, + dbGuild: guild, + }); const ticketResult = await Result.fromAsync(async () => func); if (ticketResult.isErr()) {@@ -52,9 +60,8 @@ }
} const { channelId } = ticketResult.unwrap(); - return interaction.reply({ + return interaction.editReply({ content: `Opened ticket <#${channelId}>.`, - flags: ["Ephemeral"], }); }, });@@ -65,14 +72,44 @@ options: {
user: option.user("The user to open the ticket for", { required: true }), reason: option.string("The reason for opening the ticket"), }, - async run(interaction, args) { - if (!interaction.guild) + async run(interaction, args, { actor, guild }) { + await interaction.deferReply({ flags: ["Ephemeral"] }); + + if (!guild || !interaction.guild) { return errorMessage(interaction, "You must run this command in a guild"); + } else if (!actor) { + return errorMessage(interaction, "Failed to retrieve your user data"); + } - const func = actions.createTicket(interaction.member ?? interaction.user, interaction.guild, { - for: args.user, - reason: args.reason, + const forActor = await upsertActor({ + guild: { + discordId: interaction.guild.id, + icon: interaction.guild.icon, + name: interaction.guild.name, + }, + user: { + avatar: args.user.avatar, + discordId: args.user.id, + displayName: args.user.displayName || args.user.username, + username: args.user.username, + }, + moderator: false, }); + if (!forActor) return errorMessage(interaction, "Failed to upsert subject of the ticket"); + + const func = actions.createTicket( + { + creatorActor: actor, + creatorUser: interaction.member ?? interaction.user, + guild: interaction.guild, + dbGuild: guild, + }, + { + for: args.user, + dbFor: forActor, + reason: args.reason, + }, + ); const ticketResult = await Result.fromAsync(async () => func); if (ticketResult.isErr()) {@@ -87,9 +124,8 @@ }
} const { channelId } = ticketResult.unwrap(); - return interaction.reply({ + return interaction.editReply({ content: `Opened ticket <#${channelId}>.`, - flags: ["Ephemeral"], }); }, });
M
apps/bot/src/feats/tickets/types.ts
→
apps/bot/src/feats/tickets/types.ts
@@ -1,5 +1,5 @@
import type { Actor, Guild as DbGuild, TicketWithRelations } from "@stealth-developers/db"; -import type { Channel } from "discord.js"; +import type { Channel, Guild } from "discord.js"; import type { AnyUser } from "@/types";@@ -10,9 +10,18 @@ actor: Actor;
guild: DbGuild; }; +export type NewTicketContext = { + creatorUser: AnyUser; + creatorActor: Actor; + + guild: Guild; + dbGuild: DbGuild; +}; + export type CreateTicketArgs = | undefined | { + dbFor: Actor; for: AnyUser; reason?: string; };