From 5f932f5d630bf7145dc9372e33fe2b1d0747610c Mon Sep 17 00:00:00 2001 From: TheOnlyMace <0815cracky@gmail.com> Date: Sat, 25 Jul 2026 16:32:30 +0200 Subject: [PATCH] Refactor ephemeral message handling in ticket interactions - Updated ticket command and interaction responses to utilize the new `ephemeral()` function for consistent ephemeral message handling. - Enhanced error handling for member fetching in ticket interactions, ensuring proper responses when members are not found. - Improved localization for new error messages related to member presence in ticket commands. - Documented changes in phase tracking to reflect updates in ticket interaction responses. --- apps/bot/src/index.ts | 5 +- apps/bot/src/interaction-registry.ts | 5 +- apps/bot/src/modules/tickets/commands.ts | 69 ++++++++++++++------ apps/bot/src/modules/tickets/interactions.ts | 45 +++++++++++-- apps/bot/src/modules/tickets/service.ts | 21 +++++- docs/PHASE-TRACKING.md | 1 + packages/shared/src/index.ts | 2 + 7 files changed, 118 insertions(+), 30 deletions(-) diff --git a/apps/bot/src/index.ts b/apps/bot/src/index.ts index a1ba7af..dae601a 100644 --- a/apps/bot/src/index.ts +++ b/apps/bot/src/index.ts @@ -15,6 +15,7 @@ import { ensureRecurringJobs, startWorkers } from './jobs.js'; import type { BotContext } from './types.js'; import { startHealthServer } from './health.js'; import { getGuildLocale } from './i18n.js'; +import { ephemeral } from './interaction-reply.js'; import { t } from '@nexumi/shared'; import { handleAutoModMessage } from './modules/automod/actions.js'; import { registerLoggingEvents } from './modules/logging/events.js'; @@ -227,9 +228,9 @@ if (!isShard) { const message = t(locale, 'generic.error'); if (interaction.isRepliable()) { if (interaction.replied || interaction.deferred) { - await interaction.followUp({ content: message, ephemeral: true }); + await interaction.followUp({ content: message, ...ephemeral() }); } else { - await interaction.reply({ content: message, ephemeral: true }); + await interaction.reply({ content: message, ...ephemeral() }); } } } catch (replyError) { diff --git a/apps/bot/src/interaction-registry.ts b/apps/bot/src/interaction-registry.ts index e4f67e2..a05fb40 100644 --- a/apps/bot/src/interaction-registry.ts +++ b/apps/bot/src/interaction-registry.ts @@ -10,6 +10,7 @@ import type { import { decodeComponentCustomId, t, type DashboardModuleId } from '@nexumi/shared'; import { env } from './env.js'; import { getGuildLocale } from './i18n.js'; +import { ephemeral } from './interaction-reply.js'; import { isModuleEnabled, moduleForComponent } from './module-gates.js'; import { isMaintenanceMode } from './presence.js'; import type { BotContext } from './types.js'; @@ -176,7 +177,7 @@ export async function routeComponentInteraction( if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: maintenance.message?.trim() || t(locale, 'generic.maintenance'), - ephemeral: true + ...ephemeral() }); } return true; @@ -198,7 +199,7 @@ export async function routeComponentInteraction( if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: t(locale, 'generic.moduleDisabled'), - ephemeral: true + ...ephemeral() }); } return true; diff --git a/apps/bot/src/modules/tickets/commands.ts b/apps/bot/src/modules/tickets/commands.ts index d17c9de..2c0edfc 100644 --- a/apps/bot/src/modules/tickets/commands.ts +++ b/apps/bot/src/modules/tickets/commands.ts @@ -3,6 +3,7 @@ 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, @@ -26,7 +27,7 @@ async function ensureManageGuild( locale: 'de' | 'en' ): Promise { if (!interaction.inGuild()) { - await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true }); + await interaction.reply({ content: t(locale, 'generic.guildOnly'), ...ephemeral() }); return false; } return requirePermission(interaction, PermissionFlagsBits.ManageGuild, locale); @@ -37,7 +38,7 @@ const ticketCommand: SlashCommand = { async execute(interaction, context) { const locale = await getGuildLocale(context.prisma, interaction.guildId); if (!interaction.inGuild()) { - await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true }); + await interaction.reply({ content: t(locale, 'generic.guildOnly'), ...ephemeral() }); return; } @@ -54,7 +55,10 @@ const ticketCommand: SlashCommand = { const categories = await getGuildCategories(context, interaction.guildId!); if (categories.length === 0) { - await interaction.reply({ content: t(locale, 'ticket.error.no_categories'), ephemeral: true }); + await interaction.reply({ + content: t(locale, 'ticket.error.no_categories'), + ...ephemeral() + }); return; } @@ -62,7 +66,7 @@ const ticketCommand: SlashCommand = { if (!channel?.isTextBased() || channel.isDMBased()) { await interaction.reply({ content: t(locale, 'ticket.error.invalid_channel'), - ephemeral: true + ...ephemeral() }); return; } @@ -73,7 +77,7 @@ const ticketCommand: SlashCommand = { title: name, description }); - await interaction.reply({ content: t(locale, 'ticket.panel.created'), ephemeral: true }); + await interaction.reply({ content: t(locale, 'ticket.panel.created'), ...ephemeral() }); } catch (error) { if (error instanceof TicketPanelError) { const key = @@ -82,7 +86,7 @@ const ticketCommand: SlashCommand = { : error.code === 'not_configured' ? 'ticket.error.invalid_channel' : 'ticket.error.missing_permission'; - await interaction.reply({ content: t(locale, key), ephemeral: true }); + await interaction.reply({ content: t(locale, key), ...ephemeral() }); return; } throw error; @@ -105,18 +109,28 @@ const ticketCommand: SlashCommand = { ); await interaction.reply({ content: tf(locale, 'ticket.category.created', { name: category.name }), - ephemeral: true + ...ephemeral() }); return; } const ticket = await findTicketByChannel(context.prisma, interaction.channelId!); if (!ticket) { - await interaction.reply({ content: t(locale, 'ticket.error.not_in_ticket'), ephemeral: true }); + await interaction.reply({ + content: t(locale, 'ticket.error.not_in_ticket'), + ...ephemeral() + }); return; } - const member = await interaction.guild!.members.fetch(interaction.user.id); + 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') { @@ -131,40 +145,57 @@ const ticketCommand: SlashCommand = { const isOpener = ticket.openerId === interaction.user.id; if (!isStaff && !isOpener) { - await interaction.reply({ content: t(locale, 'ticket.error.not_staff'), ephemeral: true }); + 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: true }); + 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: true }); + 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); + 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: true + ...ephemeral() }); return; } if (sub === 'remove') { const user = interaction.options.getUser('user', true); - const target = await interaction.guild!.members.fetch(user.id); + 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: true + ...ephemeral() }); return; } @@ -174,7 +205,7 @@ const ticketCommand: SlashCommand = { await renameTicket(context, ticket, member, name, locale); await interaction.reply({ content: tf(locale, 'ticket.rename.success', { name }), - ephemeral: true + ...ephemeral() }); return; } @@ -186,14 +217,14 @@ const ticketCommand: SlashCommand = { content: tf(locale, 'ticket.priority.success', { priority: t(locale, `ticket.priority.${level}`) }), - ephemeral: true + ...ephemeral() }); } } catch (error) { if (error instanceof TicketError) { await interaction.reply({ content: t(locale, `ticket.error.${error.code}`), - ephemeral: true + ...ephemeral() }); return; } diff --git a/apps/bot/src/modules/tickets/interactions.ts b/apps/bot/src/modules/tickets/interactions.ts index 2a086d5..98c5eb0 100644 --- a/apps/bot/src/modules/tickets/interactions.ts +++ b/apps/bot/src/modules/tickets/interactions.ts @@ -88,7 +88,14 @@ async function handleOpenTicketRequest( } try { - const member = await interaction.guild.members.fetch(interaction.user.id); + 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; + } const ticket = await openTicketForCategory( context, interaction.guild, @@ -126,7 +133,14 @@ async function handleTicketControl( return; } - const member = await interaction.guild.members.fetch(interaction.user.id); + 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 (interaction.isButton() && interaction.customId === TICKET_CTRL_CLOSE) { @@ -177,7 +191,14 @@ async function handleTicketControl( await interaction.reply({ content: t(locale, 'ticket.error.not_found'), ...ephemeral() }); return; } - const target = await interaction.guild.members.fetch(userId); + const target = await interaction.guild.members.fetch(userId).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: `<@${userId}>` }), @@ -192,7 +213,14 @@ async function handleTicketControl( await interaction.reply({ content: t(locale, 'ticket.error.not_found'), ...ephemeral() }); return; } - const target = await interaction.guild.members.fetch(userId); + const target = await interaction.guild.members.fetch(userId).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: `<@${userId}>` }), @@ -298,7 +326,14 @@ export async function handleTicketInteraction( } try { - const member = await interaction.guild.members.fetch(interaction.user.id); + 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; + } const ticket = await openTicketForCategory( context, interaction.guild, diff --git a/apps/bot/src/modules/tickets/service.ts b/apps/bot/src/modules/tickets/service.ts index 02dce78..99ee497 100644 --- a/apps/bot/src/modules/tickets/service.ts +++ b/apps/bot/src/modules/tickets/service.ts @@ -71,6 +71,15 @@ export class TicketPanelError extends Error { } } +function isUnknownChannelError(error: unknown): boolean { + return Boolean( + error && + typeof error === 'object' && + 'code' in error && + (error as { code: unknown }).code === 10003 + ); +} + function botCanPostPanel( channel: GuildBasedChannel, meId: string @@ -707,7 +716,11 @@ async function fetchTicketMessages( timestamp: message.createdAt.toISOString() })); } catch (error) { - logger.warn({ error, ticketId: ticket.id }, 'Failed to fetch ticket messages'); + if (isUnknownChannelError(error)) { + logger.debug({ ticketId: ticket.id }, 'Ticket channel already gone while fetching messages'); + } else { + logger.warn({ error, ticketId: ticket.id }, 'Failed to fetch ticket messages'); + } return []; } } @@ -1123,7 +1136,11 @@ export async function closeTicket( await channel.delete(`Ticket ${ticket.id} closed`); } } catch (error) { - logger.warn({ error, ticketId: ticket.id }, 'Failed to delete ticket channel'); + if (isUnknownChannelError(error)) { + logger.debug({ ticketId: ticket.id }, 'Ticket channel already gone while deleting'); + } else { + logger.warn({ error, ticketId: ticket.id }, 'Failed to delete ticket channel'); + } } } } diff --git a/docs/PHASE-TRACKING.md b/docs/PHASE-TRACKING.md index 11cf984..a0968d5 100644 --- a/docs/PHASE-TRACKING.md +++ b/docs/PHASE-TRACKING.md @@ -509,6 +509,7 @@ - Control-Panel im Ticket: Claim, Close, User-Select Add/Remove (wie TempVoice-Pattern) - Opener darf schließen; Claim/Add/Remove nur Support-Staff - Prioritäts-Dropdown im Discord-Control-Panel (`ticket:ctrl:priority`) +- Robustheit: Unknown Channel (10003) beim Close nur noch debug; Unknown Member (10007) bei Add/Remove/Interactions abgefangen; Ticket-Commands nutzen `ephemeral()` flags ### Manuell testen diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index e8de529..c11bfb9 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -478,6 +478,7 @@ const de: Dictionary = { 'ticket.error.no_parent_channel': 'Kein Elternkanal für Thread-Tickets konfiguriert.', 'ticket.error.invalid_parent_channel': 'Der konfigurierte Elternkanal ist ungültig.', 'ticket.error.not_found': 'Ticket nicht gefunden.', + 'ticket.error.member_not_found': 'Mitglied nicht auf dem Server gefunden.', 'ticket.error.already_rated': 'Du hast dieses Ticket bereits bewertet.', 'birthdays.error.invalidDate': 'Ungültiges Datum. Prüfe Tag, Monat und optionales Jahr.', 'birthdays.error.invalidTimezone': 'Ungültige Zeitzone. Nutze z. B. Europe/Berlin.', @@ -1090,6 +1091,7 @@ const en: Dictionary = { 'ticket.error.no_parent_channel': 'No parent channel configured for thread tickets.', 'ticket.error.invalid_parent_channel': 'The configured parent channel is invalid.', 'ticket.error.not_found': 'Ticket not found.', + 'ticket.error.member_not_found': 'Member is not on this server.', 'ticket.error.already_rated': 'You already rated this ticket.', 'birthdays.error.invalidDate': 'Invalid date. Check day, month, and optional year.', 'birthdays.error.invalidTimezone': 'Invalid timezone. Use e.g. Europe/Berlin.',