deploy #2

Merged
smueller merged 24 commits from deploy into main 2026-07-29 07:44:03 +00:00
7 changed files with 118 additions and 30 deletions
Showing only changes of commit 5f932f5d63 - Show all commits

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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) {
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,9 +1136,13 @@ export async function closeTicket(
await channel.delete(`Ticket ${ticket.id} closed`);
}
} catch (error) {
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');
}
}
}
}
export async function rateTicket(

View File

@@ -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

View File

@@ -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.',