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.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.',
|
||||
|
||||
Reference in New Issue
Block a user