Tickets: Transcript beim Schließen + Thread-Löschung

- Schließen-Button erstellt jetzt ein Text-Transcript (chronologisch, inkl.
  Attachment-URLs, max 500 Nachrichten) und schickt es dem Ersteller per DM
- Kopie ins Mod-Log (Aufbewahrung fürs Team, mit Hinweis falls DM blockiert)
- Thread wird danach gelöscht statt nur archiviert (Fallback: sperren+archivieren,
  falls dem Bot Lösch-Rechte fehlen)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 01:11:16 +02:00
co-authored by Claude Fable 5
parent 32b7f5fde5
commit 19ec2802ac
3 changed files with 91 additions and 11 deletions
+4 -10
View File
@@ -9,11 +9,12 @@ import { registerLevels } from './levels.js';
import { registerPresence } from './presence.js';
import { registerExtras } from './extras.js';
import { handleApplyButton, handleApplyModal, handleReviewButton } from './app-forms.js';
import { closeTicketWithTranscript } from './tickets.js';
import { handleRoleMenuButton } from './role-menus.js';
import {
addPlaytester, removePlaytester, isWish, bumpWish,
getGiveaway, toggleGiveawayEntry, giveawayEntries,
saveTicket, getTicket, closeTicket, openTicketOf,
saveTicket, openTicketOf,
} from '../db.js';
import { ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
import * as ping from './commands/ping.js';
@@ -242,15 +243,8 @@ export async function startBot() {
}
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(() => {});
// Transcript per DM + Mod-Log sichern, dann Thread löschen
await closeTicketWithTranscript(interaction);
} catch (error) {
console.error('[ticket] Schließen fehlgeschlagen:', error);
}
+86
View File
@@ -0,0 +1,86 @@
// Ticket-Schließen: Transcript sichern (DM an den Ersteller + Kopie ins Mod-Log),
// danach wird der Thread gelöscht statt nur archiviert.
import { AttachmentBuilder, EmbedBuilder, MessageFlags } from 'discord.js';
import { getTicket, closeTicket } from '../db.js';
import { modlogChannelId, brandColor, brandFooter } from '../runtime-settings.js';
const MAX_MESSAGES = 500;
/** Alle Thread-Nachrichten chronologisch als Text-Transcript */
async function fetchTranscript(channel) {
const all = [];
let before;
while (all.length < MAX_MESSAGES) {
const batch = await channel.messages.fetch({ limit: 100, before });
if (batch.size === 0) break;
all.push(...batch.values());
before = batch.last().id;
}
all.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
return all
.map((m) => {
const ts = new Date(m.createdTimestamp).toISOString().slice(0, 16).replace('T', ' ');
const parts = [
m.content,
[...(m.attachments?.values?.() ?? [])].map((a) => a.url).join(' '),
m.embeds?.length ? '[Embed]' : '',
].filter(Boolean).join(' ');
return `[${ts}] ${m.member?.displayName ?? m.author?.username ?? '?'}: ${parts}`;
})
.join('\n');
}
/** 🔒-Button: Transcript verteilen, Thread löschen */
export async function closeTicketWithTranscript(interaction) {
const ticket = getTicket(interaction.channelId);
if (!ticket) {
await interaction.reply({ content: '❌ Das ist kein Ticket-Thread.', flags: MessageFlags.Ephemeral });
return;
}
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
closeTicket(interaction.channelId);
const threadName = interaction.channel.name ?? `ticket-${ticket.thread_id}`;
const closedBy = interaction.member?.displayName ?? interaction.user.username;
const transcript =
`Ticket: ${threadName}\nGeschlossen von: ${closedBy}\n${'—'.repeat(40)}\n` +
((await fetchTranscript(interaction.channel).catch(() => '')) || 'Keine Nachrichten.');
const fileName = `ticket-${threadName.replace(/[^\wäöüß-]+/g, '-').replace(/^-+|-+$/g, '') || ticket.thread_id}.txt`;
const makeFile = () => new AttachmentBuilder(Buffer.from(transcript, 'utf8'), { name: fileName });
// DM an den Ticket-Ersteller
let dmOk = true;
try {
const user = await interaction.client.users.fetch(ticket.user_id);
await user.send({
content: `🔒 Dein Ticket **${threadName}** wurde geschlossen — hier dein Transcript. Danke für deine Anfrage!`,
files: [makeFile()],
});
} catch {
dmOk = false;
}
// Kopie ins Mod-Log (Aufbewahrung fürs Team)
const logChannelId = modlogChannelId();
if (logChannelId) {
const log = await interaction.client.channels.fetch(logChannelId).catch(() => null);
await log?.send({
embeds: [
new EmbedBuilder()
.setColor(brandColor())
.setTitle(`🔒 Ticket geschlossen: ${threadName}`)
.setDescription(`Ersteller: <@${ticket.user_id}> · geschlossen von **${closedBy}**${dmOk ? '' : '\n⚠️ Transcript-DM nicht zustellbar'}`)
.setFooter({ text: brandFooter('SUPPORT') })
.setTimestamp(),
],
files: [makeFile()],
}).catch(() => {});
}
await interaction.editReply(`🔒 Ticket geschlossen${dmOk ? ', Transcript per DM verschickt' : ' (DM blockiert — Transcript liegt im Mod-Log)'} — Thread wird gelöscht.`);
await interaction.channel.delete().catch(async () => {
// Löschen nicht erlaubt → wenigstens sperren + archivieren
await interaction.channel.setLocked(true).catch(() => {});
await interaction.channel.setArchived(true).catch(() => {});
});
}