Ticket-System, Web-Composer und Nachrichten-Bearbeitung

- Tickets: /ticket-setup postet 🎫-Button; Klick erstellt privaten Thread
  (ein offenes Ticket pro User), Schließen-Button sperrt + archiviert
- Composer auf der Setup-Seite: Nachricht/Embed als Bot in beliebigen Kanal
  senden oder per Message-ID bearbeiten (nur eigene Bot-Posts)
- API v1: PATCH /api/v1/message — Skripte können ihre Bot-Posts aktualisieren
- Setting ticket_channel_id; README ergänzt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 13:43:27 +02:00
co-authored by Claude Opus 4.8
parent f631c0e2ab
commit 4d1b0a956d
9 changed files with 310 additions and 4 deletions
+81 -3
View File
@@ -1,11 +1,16 @@
// Discord-Client: Commands laden, registrieren und Interactions verarbeiten
import { Client, Collection, Events, GatewayIntentBits, MessageFlags, Partials, REST, Routes } from 'discord.js';
import { config } from '../config.js';
import { devlogChannelId, devlogPingRoleId, playtesterRoleId } from '../runtime-settings.js';
import { devlogChannelId, devlogPingRoleId, playtesterRoleId, ticketChannelId } from '../runtime-settings.js';
import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js';
import { registerCommunityListeners } from './community.js';
import { registerModTools } from './mod-tools.js';
import { addPlaytester, removePlaytester, isWish, bumpWish, getGiveaway, toggleGiveawayEntry, giveawayEntries } from '../db.js';
import {
addPlaytester, removePlaytester, isWish, bumpWish,
getGiveaway, toggleGiveawayEntry, giveawayEntries,
saveTicket, getTicket, closeTicket, openTicketOf,
} from '../db.js';
import { ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
import * as ping from './commands/ping.js';
import * as devlogBackfill from './commands/devlog-backfill.js';
import * as bug from './commands/bug.js';
@@ -13,9 +18,10 @@ import * as playtesterSetup from './commands/playtester-setup.js';
import * as galerieBackfill from './commands/galerie-backfill.js';
import * as wunsch from './commands/wunsch.js';
import * as giveaway from './commands/giveaway.js';
import * as ticketSetup from './commands/ticket-setup.js';
// Alle Commands hier eintragen — jedes Modul exportiert { data, execute }
const commandModules = [ping, devlogBackfill, bug, playtesterSetup, galerieBackfill, wunsch, giveaway];
const commandModules = [ping, devlogBackfill, bug, playtesterSetup, galerieBackfill, wunsch, giveaway, ticketSetup];
export async function startBot() {
const client = new Client({
@@ -117,6 +123,78 @@ export async function startBot() {
return;
}
// 🎫-Button: Ticket erstellen (privater Thread) / schließen
if (interaction.isButton() && interaction.customId === 'ticket_create') {
try {
const channelId = ticketChannelId();
if (!channelId) throw new Error('Kein Ticket-Kanal konfiguriert');
// Pro User nur ein offenes Ticket
const existing = openTicketOf(interaction.user.id);
if (existing) {
await interaction.reply({
content: `❕ Du hast schon ein offenes Ticket: <#${existing.thread_id}>`,
flags: MessageFlags.Ephemeral,
});
return;
}
const channel = await interaction.client.channels.fetch(channelId);
const thread = await channel.threads.create({
name: `🎫 ${interaction.user.username}`,
type: ChannelType.PrivateThread,
invitable: false,
autoArchiveDuration: 10080,
});
await thread.members.add(interaction.user.id);
saveTicket(thread.id, interaction.user.id);
await thread.send({
content: `<@${interaction.user.id}>`,
embeds: [
new EmbedBuilder()
.setColor(0xf5c518)
.setDescription(
'Willkommen in deinem Ticket! Beschreib dein Anliegen — das Team meldet sich.\n' +
'Wenn alles geklärt ist, kann das Ticket hier geschlossen werden.'
)
.setFooter({ text: 'D4RKST3R // SUPPORT' }),
],
components: [
new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId('ticket_close').setStyle(ButtonStyle.Danger).setLabel('Ticket schließen').setEmoji('🔒')
),
],
});
await interaction.reply({
content: `✅ Dein Ticket: <#${thread.id}>`,
flags: MessageFlags.Ephemeral,
});
} catch (error) {
console.error('[ticket] Erstellung fehlgeschlagen:', error);
await interaction
.reply({ content: '❌ Ticket konnte nicht erstellt werden — dem Bot fehlen vermutlich Thread-Rechte.', flags: MessageFlags.Ephemeral })
.catch(() => {});
}
return;
}
if (interaction.isButton() && interaction.customId === 'ticket_close') {
try {
const ticket = getTicket(interaction.channelId);
if (!ticket) {
await interaction.reply({ content: '❌ Das ist kein Ticket-Thread.', flags: MessageFlags.Ephemeral });
return;
}
closeTicket(interaction.channelId);
await interaction.reply(`🔒 Ticket geschlossen von **${interaction.member?.displayName ?? interaction.user.username}**.`);
await interaction.channel.setLocked(true).catch(() => {});
await interaction.channel.setArchived(true).catch(() => {});
} catch (error) {
console.error('[ticket] Schließen fehlgeschlagen:', error);
}
return;
}
// 🎉-Button: Giveaway-Teilnahme togglen
if (interaction.isButton() && interaction.customId === 'giveaway_enter') {
const g = getGiveaway(interaction.message.id);
+42
View File
@@ -0,0 +1,42 @@
// /ticket-setup — postet das Ticket-Embed mit 🎫-Button in den aktuellen Kanal (Admin)
import {
SlashCommandBuilder, PermissionFlagsBits, MessageFlags,
ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder,
} from 'discord.js';
import { ticketChannelId } from '../../runtime-settings.js';
export const data = new SlashCommandBuilder()
.setName('ticket-setup')
.setDescription('Postet das Ticket-Embed mit Button in diesen Kanal')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator);
export async function execute(interaction) {
if (!ticketChannelId()) {
await interaction.reply({
content: '❌ Erst auf der Setup-Seite einen Ticket-Kanal wählen.',
flags: MessageFlags.Ephemeral,
});
return;
}
const embed = new EmbedBuilder()
.setColor(0xf5c518)
.setTitle('🎫 Support-Ticket')
.setDescription(
'Du hast eine Frage, ein Problem oder ein privates Anliegen?\n\n' +
'Klick auf den Button — es öffnet sich ein **privater Thread**, ' +
'den nur du und das Team sehen können.'
)
.setFooter({ text: 'D4RKST3R // SUPPORT' });
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('ticket_create')
.setStyle(ButtonStyle.Primary)
.setLabel('Ticket erstellen')
.setEmoji('🎫')
);
await interaction.channel.send({ embeds: [embed], components: [row] });
await interaction.reply({ content: '✅ Ticket-Post erstellt.', flags: MessageFlags.Ephemeral });
}