all repos — stealth-developers @ 93b840a419826e84796815ea7fcb8dc530a60015

feat(tickets): allow mods to create tickets for other users
vi v@vt3e.cat
Sat, 11 Apr 2026 18:18:14 +0100
commit

93b840a419826e84796815ea7fcb8dc530a60015

parent

088cc4c2cae7362659fea0a843b40f034d63f1be

D src/bot.ts

@@ -1,27 +0,0 @@

-import { Client, Events, GatewayIntentBits } from "discord.js"; - -import registerEvents from "./handlers/events.ts"; -import registerInteractions from "./handlers/interactions.ts"; - -import { startTicketWatcher } from "./tasks/ticketWatcher.ts"; -import logger from "./utils/logging.ts"; - -const client = new Client({ - intents: [ - GatewayIntentBits.GuildMessages, - GatewayIntentBits.Guilds, - GatewayIntentBits.MessageContent, - ], -}); - -client.on(Events.ClientReady, async (client) => { - logger.info(`logged in as ${client.user?.tag}!`); - - await Promise.all([registerEvents(client), registerInteractions(client)]); - logger.info("events and interactions registered!"); - console.log(); - - startTicketWatcher(client); -}); - -export default client;
M src/database/schema/tickets.tssrc/database/schema/tickets.ts

@@ -19,6 +19,7 @@ .default("open")

.notNull(), topic: text("topic"), + openedBy: text("opened_by"), createdAt: integer("created_at", { mode: "timestamp" }) .default(sql`(unixepoch())`)
M src/index.tssrc/index.ts

@@ -1,7 +1,7 @@

import config from "./config.ts"; import logger from "./utils/logging.ts"; -import client from "./bot.ts"; +import client from "./discord.ts"; async function main() { await client.login(config.discord.token);
M src/interactions/commands/tickets/_shared.tssrc/interactions/commands/tickets/_shared.ts

@@ -1,4 +1,5 @@

import type { Attachment, Ticket } from "@/database"; +import { text } from "@/utils/discord/components"; import { getPublicUrl } from "@/utils/s3"; import { generateTranscript } from "@/utils/tickets/transcripts"; import {

@@ -110,18 +111,37 @@ .setLabel("View Transcript")

.setStyle(ButtonStyle.Primary), ); + if (isPublic) { + buttons.addComponents( + new ButtonBuilder() + .setCustomId(`ticket:thank:${ticket.id}`) + .setLabel("Thank your moderator") + .setEmoji("💖") + .setStyle(ButtonStyle.Primary), + ); + } + + const wasBehalfTicket = ticket.openedBy !== ticket.authorId; + const reasonText = ticket.topic + ? `for reason: ${ticket.topic}` + : "without a provided reason"; + const behalfText = wasBehalfTicket + ? `**Opened By**: <@${ticket.openedBy}> ${reasonText}` + : null; + const container = new ContainerBuilder() .addTextDisplayComponents( - new TextDisplayBuilder().setContent( + text( [ `## Ticket #${ticket.id} (${ticket.anonymousId}) - ${ticket.status}`, `**Closed By**: ${closedByString}`, + behalfText, ].join("\n"), ), ) .addSeparatorComponents(new SeparatorBuilder().setDivider(false)) .addTextDisplayComponents( - new TextDisplayBuilder().setContent( + text( [ reasonContructor("Public", reasons?.publicReason), isPublic ? null : reasonContructor("Private", reasons?.privateReason),

@@ -134,7 +154,7 @@ .addSeparatorComponents(

new SeparatorBuilder().setDivider(false).setSpacing(2), ) .addTextDisplayComponents( - new TextDisplayBuilder().setContent( + text( [ attachmentsString.length > 0 ? `-# **Attachments:** ${attachmentsString}`
M src/interactions/commands/tickets/actions.tssrc/interactions/commands/tickets/actions.ts

@@ -16,7 +16,7 @@ ButtonBuilder,

type ButtonInteraction, ButtonStyle, ChannelType, - type ChatInputCommandInteraction, + ChatInputCommandInteraction, type Client, ContainerBuilder, DiscordAPIError,

@@ -45,6 +45,26 @@ export async function handleCreate(

client: Client, interaction: ChatInputCommandInteraction | ButtonInteraction, ) { + let targetUser: User | null = null; + let openingReason: string | null = null; + if ( + interaction instanceof ChatInputCommandInteraction && + interaction.options.getSubcommand() === "create-for" + ) { + const isModerator = await hasManagerPermissions( + interaction.member as GuildMember, + ); + if (!isModerator) { + await interaction.reply({ + content: + "❌ You don't have permission to create tickets for other users.", + }); + } + + targetUser = interaction.options.getUser("user", true); + openingReason = interaction.options.getString("reason")?.trim() ?? null; + } + if (!interaction.guild) throw new Error("Guild is not available"); if (!client.user) throw new Error("Client user is not available"); await interaction.deferReply({ flags: ["Ephemeral"] });

@@ -110,10 +130,12 @@ .values({

guildId: interaction.guild.id, channelId: channel.id, ticketNumber: ticketNumber, - authorId: interaction.user.id, + authorId: targetUser ? targetUser.id : interaction.user.id, + openedBy: interaction.user.id, anonymousId: anonymousId, type: "general", status: "open", + topic: openingReason, }) .returning();

@@ -127,20 +149,38 @@ .setStyle(ButtonStyle.Danger)

.setEmoji("🔒"), ); - const container = new ContainerBuilder() - .setAccentColor(0x40a02b) - .addTextDisplayComponents( - new TextDisplayBuilder().setContent("# Ticket created!"), - new TextDisplayBuilder().setContent(formatted), - ) - .addActionRowComponents(controls); + const containers = [ + new ContainerBuilder() + .setAccentColor(0x40a02b) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent("# Ticket created!"), + new TextDisplayBuilder().setContent(formatted), + ) + .addActionRowComponents(controls), + ]; + if (targetUser) { + const container = new ContainerBuilder().setAccentColor(0x40a02b); + + container.addTextDisplayComponents( + text( + [ + `Ticket created by <@${interaction.user.id}>`, + openingReason ? `> ${openingReason}` : undefined, + ] + .filter(Boolean) + .join("\n"), + ), + ); + + containers.push(container); + } await channel.send({ - components: [container], + components: containers, flags: ["IsComponentsV2"], }); - const canDm = await canDmUser(interaction.user); + const canDm = await canDmUser(targetUser ?? interaction.user); if (!canDm) { const formatCommand = (name: string) => `</ticket ${name}:1477532047785332906>`;

@@ -294,16 +334,6 @@ };

const messageAuthor = async () => { try { - const thankYou = new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder() - .setCustomId(`ticket:thank:${ticket.id}`) - .setLabel("Thank your moderator") - .setEmoji("💖") - .setStyle(ButtonStyle.Primary), - ); - - publicMessage.container.addActionRowComponents(thankYou); - await author.send({ components: [publicMessage.container], flags: ["IsComponentsV2"],
M src/interactions/commands/tickets/index.tssrc/interactions/commands/tickets/index.ts

@@ -38,6 +38,23 @@ .setDescription("Create a new ticket"),

) .addSubcommand( new SlashCommandSubcommandBuilder() + .setName("create-for") + .setDescription("Create a new ticket for a specific user") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to create the ticket for") + .setRequired(true), + ) + .addStringOption((option) => + option + .setName("reason") + .setDescription("Reason for creating the ticket, shown to the user") + .setRequired(false), + ), + ) + .addSubcommand( + new SlashCommandSubcommandBuilder() .setName("close") .setDescription("Close the ticket in the current channel") .addStringOption((option) =>

@@ -104,6 +121,7 @@ const action = interaction.options.getSubcommand();

const actions = { create: handleCreate, + "create-for": handleCreate, list: handleList, export: handleExport, view: handleViewTicket,