import { PermissionFlagsBits } from 'discord.js'; import { t, tf } from '@nexumi/shared'; import type { SlashCommand } from '../../types.js'; import { getGuildLocale } from '../../i18n.js'; import { requirePermission } from '../../permissions.js'; import { ephemeral } from '../../interaction-reply.js'; import { ticketCommandData } from './command-definitions.js'; import { addUserToTicket, assertTicketStaff, claimTicket, closeTicket, createCategoryWithRole, findTicketByChannel, getGuildCategories, removeUserFromTicket, renameTicket, setTicketPriority, syncTicketPanel, TicketError, TicketPanelError, upsertCategoryByName } from './service.js'; async function ensureManageGuild( interaction: Parameters[0], locale: 'de' | 'en' ): Promise { if (!interaction.inGuild()) { await interaction.reply({ content: t(locale, 'generic.guildOnly'), ...ephemeral() }); return false; } return requirePermission(interaction, PermissionFlagsBits.ManageGuild, locale); } const ticketCommand: SlashCommand = { data: ticketCommandData, async execute(interaction, context) { const locale = await getGuildLocale(context.prisma, interaction.guildId); if (!interaction.inGuild()) { await interaction.reply({ content: t(locale, 'generic.guildOnly'), ...ephemeral() }); return; } const sub = interaction.options.getSubcommand(true); if (sub === 'panel_create') { if (!(await ensureManageGuild(interaction, locale))) { return; } const name = interaction.options.getString('name', true); const description = interaction.options.getString('description'); await upsertCategoryByName(context, interaction.guildId!, name, description); const categories = await getGuildCategories(context, interaction.guildId!); if (categories.length === 0) { await interaction.reply({ content: t(locale, 'ticket.error.no_categories'), ...ephemeral() }); return; } const channel = interaction.channel; if (!channel?.isTextBased() || channel.isDMBased()) { await interaction.reply({ content: t(locale, 'ticket.error.invalid_channel'), ...ephemeral() }); return; } try { await syncTicketPanel(context, interaction.guildId!, { channelId: channel.id, title: name, description }); await interaction.reply({ content: t(locale, 'ticket.panel.created'), ...ephemeral() }); } catch (error) { if (error instanceof TicketPanelError) { const key = error.code === 'no_categories' ? 'ticket.error.no_categories' : error.code === 'not_configured' ? 'ticket.error.invalid_channel' : 'ticket.error.missing_permission'; await interaction.reply({ content: t(locale, key), ...ephemeral() }); return; } throw error; } return; } if (sub === 'category_create') { if (!(await ensureManageGuild(interaction, locale))) { return; } const name = interaction.options.getString('name', true); const role = interaction.options.getRole('support_role', true); const category = await createCategoryWithRole( context, interaction.guildId!, name, role.id ); await interaction.reply({ content: tf(locale, 'ticket.category.created', { name: category.name }), ...ephemeral() }); return; } const ticket = await findTicketByChannel(context.prisma, interaction.channelId!); if (!ticket) { await interaction.reply({ content: t(locale, 'ticket.error.not_in_ticket'), ...ephemeral() }); return; } const member = await interaction.guild!.members.fetch(interaction.user.id).catch(() => null); if (!member) { await interaction.reply({ content: t(locale, 'ticket.error.member_not_found'), ...ephemeral() }); return; } try { if (sub === 'close') { const isStaff = await (async () => { try { await assertTicketStaff(member, ticket); return true; } catch { return false; } })(); const isOpener = ticket.openerId === interaction.user.id; if (!isStaff && !isOpener) { await interaction.reply({ content: t(locale, 'ticket.error.not_staff'), ...ephemeral() }); return; } const reason = interaction.options.getString('reason') ?? undefined; await closeTicket(context, ticket, interaction.user.id, locale, reason); await interaction.reply({ content: t(locale, 'ticket.close.success'), ...ephemeral() }); return; } if (sub === 'claim') { await claimTicket(context, ticket, member, locale); await interaction.reply({ content: t(locale, 'ticket.claim.success'), ...ephemeral() }); return; } if (sub === 'add') { const user = interaction.options.getUser('user', true); const target = await interaction.guild!.members.fetch(user.id).catch(() => null); if (!target) { await interaction.reply({ content: t(locale, 'ticket.error.member_not_found'), ...ephemeral() }); return; } await addUserToTicket(context, ticket, member, target, locale); await interaction.reply({ content: tf(locale, 'ticket.add.success', { user: `<@${user.id}>` }), ...ephemeral() }); return; } if (sub === 'remove') { const user = interaction.options.getUser('user', true); const target = await interaction.guild!.members.fetch(user.id).catch(() => null); if (!target) { await interaction.reply({ content: t(locale, 'ticket.error.member_not_found'), ...ephemeral() }); return; } await removeUserFromTicket(context, ticket, member, target, locale); await interaction.reply({ content: tf(locale, 'ticket.remove.success', { user: `<@${user.id}>` }), ...ephemeral() }); return; } if (sub === 'rename') { const name = interaction.options.getString('name', true); await renameTicket(context, ticket, member, name, locale); await interaction.reply({ content: tf(locale, 'ticket.rename.success', { name }), ...ephemeral() }); return; } if (sub === 'priority') { const level = interaction.options.getString('level', true); await setTicketPriority(context, ticket, member, level, locale); await interaction.reply({ content: tf(locale, 'ticket.priority.success', { priority: t(locale, `ticket.priority.${level}`) }), ...ephemeral() }); } } catch (error) { if (error instanceof TicketError) { await interaction.reply({ content: t(locale, `ticket.error.${error.code}`), ...ephemeral() }); return; } throw error; } } }; export const ticketsCommands = [ticketCommand];