all repos — stealth-developers @ 029d0f7869ef4560f712596897a81bd0ef5b39c4

bot: allow creating tickets on behalf of; function to upsert user from discord user obj
vi did:web:vt3e.cat
Wed, 03 Jun 2026 21:56:58 +0100
commit

029d0f7869ef4560f712596897a81bd0ef5b39c4

parent

70413b495b9134bcec2d716c0de27cce778ede81

M apps/bot/src/commands/tickets/command.tsapps/bot/src/commands/tickets/command.ts

@@ -5,9 +5,10 @@ import { Subcommand } from "@sapphire/plugin-subcommands";

import { ApplyOptions } from "@sapphire/decorators"; import { container, Result } from "@sapphire/framework"; -import db, { eq, tickets, upsertUser } from "@stealth-developers/db"; +import db, { eq, tickets } from "@stealth-developers/db"; import config from "@stealth-developers/config"; import { getCategory, PublicError } from "@/lib"; +import { upsertUser } from "$/db"; @ApplyOptions<Subcommand.Options>({ description: "ticket-related commands",

@@ -52,6 +53,17 @@ builder

.setName("ticket") .setDescription("root ticket command") .addSubcommand((command) => command.setName("new").setDescription("Open a new ticket")) + .addSubcommand((command) => + command + .setName("for") + .setDescription("Open a ticket for a user") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to open the ticket for") + .setRequired(true), + ), + ) .addSubcommandGroup((group) => group .setName("action")

@@ -85,48 +97,39 @@ ),

); } - public async createTicket(interaction: Subcommand.ChatInputCommandInteraction) { + private async doCreateTicket(interaction: Subcommand.ChatInputCommandInteraction) { + const isOnBehalf = interaction.options.getSubcommand(true) === "for"; const publicId = randomUUID(); - const userResult = await Result.fromAsync(() => - upsertUser({ - id: interaction.user.id, - avatar: interaction.member?.avatar || interaction.user.avatar, - username: interaction.user.username, - displayName: interaction.user.displayName, - }), - ); - - if (userResult.isErr()) { - const err = userResult.unwrapErr(); - const message = err instanceof Error ? err.message : String(err); + if (!interaction.member) { return interaction.reply({ - content: `Failed to create ticket: ${message}`, + content: "Failed to create ticket: Member not found", flags: ["Ephemeral"], }); } - const user = userResult.unwrap(); - if (!user) { + const creator = await upsertUser(interaction.member); + if (!creator) return interaction.reply({ content: "Failed to create ticket: User not found", flags: ["Ephemeral"], }); - } - const catResult = await Result.fromAsync(() => - getCategory(config.discord.server.ticket_category), - ); - if (catResult.isErr()) { - const err = catResult.unwrapErr(); - const message = err instanceof Error ? err.message : String(err); + const subject = isOnBehalf ? interaction.options.getUser("user") : interaction.user; + const subjectUser = subject ? await upsertUser(subject) : creator; + if (!subjectUser) return interaction.reply({ - content: `Failed to create ticket: ${message}`, + content: "Failed to create ticket: Subject not found", + flags: ["Ephemeral"], + }); + + if (isOnBehalf && !creator.moderator) + return interaction.reply({ + content: "You don't have permission to create a ticket for another user.", flags: ["Ephemeral"], }); - } - const ticketCategory = catResult.unwrap(); + const ticketCategory = await getCategory(config.discord.server.ticket_category); let createdChannel: TextChannel | undefined = undefined; const txResult = await Result.fromAsync(async () => {

@@ -137,8 +140,8 @@ .values({

anonymousId: publicId, status: "open", openedAt: new Date(), - openedBy: user.id, - subject: user.id, + openedBy: creator.id, + subject: subjectUser.id, }) .returning();

@@ -181,10 +184,17 @@ });

} const channelId = txResult.unwrap(); - return interaction.reply({ + interaction.reply({ content: `Ticket created! See <#${channelId}>`, flags: ["Ephemeral"], }); + } + + public async createTicket(interaction: Subcommand.ChatInputCommandInteraction) { + await this.doCreateTicket(interaction); + } + public async createTicketFor(interaction: Subcommand.ChatInputCommandInteraction) { + await this.doCreateTicket(interaction); } public async openTicket(interaction: Subcommand.ChatInputCommandInteraction) {
A apps/bot/src/lib/db.ts

@@ -0,0 +1,28 @@

+import { User, type APIInteractionGuildMember } from "discord.js"; +import { GuildMember } from "discord.js"; +import { upsertUser as _upsertUser } from "@stealth-developers/db"; + +export async function upsertUser(input: APIInteractionGuildMember | GuildMember | User) { + if ("user" in input) { + const member = input; + const user = member.user; + + const nick = member instanceof GuildMember ? member.nickname : member.nick; + const displayName = user instanceof User ? user.globalName : user.global_name; + + return _upsertUser({ + id: user.id, + avatar: member.avatar || member.user.avatar, + username: user.username, + displayName: nick || displayName, + }); + } + + const user = input; + return _upsertUser({ + id: user.id, + avatar: user.avatar, + username: user.username, + displayName: user.displayName, + }); +}
M apps/bot/src/lib/index.tsapps/bot/src/lib/index.ts

@@ -5,3 +5,4 @@ export * from "./logger";

export * from "./permissions"; export * from "./setup"; export * from "./utils"; +export * from "./db";
A apps/bot/src/lib/tickets.ts

@@ -0,0 +1,22 @@

+import db, { tickets, eq, getUser, getUserByDiscordId } from "@stealth-developers/db"; + +export async function hasAccessToTicket(userId: string, ticketId: number) { + const [ticket] = await db.select().from(tickets).where(eq(tickets.id, ticketId)).limit(1); + if (!ticket) return false; + + const userPromise = getUserByDiscordId(userId); + const subjectPromise = getUser(ticket.subject); + const openedByPromise = ticket.subject === ticket.openedBy ? null : getUser(ticket.openedBy); + + const [user, subject, openedByResult] = await Promise.all([ + userPromise, + subjectPromise, + openedByPromise, + ]); + const openedBy = ticket.subject === ticket.openedBy ? subject : openedByResult; + if (!user) return false; + + if (user.moderator) return true; + if (!subject || !openedBy) return false; + if (user.id === subject.id || user.id === openedBy.id) return true; +}