feat(tickets): allow mods to create tickets for other users
vi v@vt3e.cat
Sat, 11 Apr 2026 18:18:14 +0100
6 files changed,
94 insertions(+),
52 deletions(-)
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.ts
→
src/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.ts
→
src/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/actions.ts
→
src/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.ts
→
src/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,