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
+25
View File
@@ -58,6 +58,31 @@ export function registerApiV1(app, client) {
return { ok: true, message_id: message.id };
});
// Eigene Bot-Nachricht nachträglich bearbeiten (z. B. Status-Posts von Skripten)
app.patch('/api/v1/message', async (request, reply) => {
if (!requireScope(request, reply, 'message')) return;
const { channel_id: channelId, message_id: messageId, content, embed } = request.body ?? {};
const cleanEmbed = sanitizeEmbed(embed);
if (!channelId || !messageId || (!content && !cleanEmbed)) {
return reply.code(400).send({ error: 'channel_id, message_id und content oder embed nötig' });
}
const channel = await client.channels.fetch(String(channelId)).catch(() => null);
if (!channel?.isTextBased()) {
return reply.code(404).send({ error: 'Kanal nicht gefunden' });
}
const message = await channel.messages.fetch(String(messageId)).catch(() => null);
if (!message) return reply.code(404).send({ error: 'Nachricht nicht gefunden' });
if (message.author?.id !== client.user?.id) {
return reply.code(403).send({ error: 'Nur eigene Bot-Nachrichten sind editierbar' });
}
await message.edit({
...(content !== undefined ? { content: String(content).slice(0, 2000) } : {}),
...(cleanEmbed ? { embeds: [cleanEmbed] } : {}),
});
return { ok: true, message_id: messageId };
});
// Direktnachricht an einen User
app.post('/api/v1/dm', async (request, reply) => {
if (!requireScope(request, reply, 'dm')) return;
+47 -1
View File
@@ -212,6 +212,7 @@ ${rssItems}
modlog_channel_id: getSetting('modlog_channel_id') ?? '',
status_channel_id: getSetting('status_channel_id') ?? '',
voting_channel_id: getSetting('voting_channel_id') ?? '',
ticket_channel_id: getSetting('ticket_channel_id') ?? '',
gameservers: getSetting('gameservers') ?? '',
};
}
@@ -244,7 +245,7 @@ ${rssItems}
const OPTIONAL_CHANNELS = [
'release_channel_id', 'backup_channel_id', 'starboard_channel_id',
'screenshot_channel_id', 'modmail_channel_id', 'welcome_channel_id',
'modlog_channel_id', 'status_channel_id', 'voting_channel_id',
'modlog_channel_id', 'status_channel_id', 'voting_channel_id', 'ticket_channel_id',
];
for (const key of ['commit_channel_id', 'devlog_channel_id', ...OPTIONAL_CHANNELS]) {
if (body[key] === undefined) continue;
@@ -312,6 +313,51 @@ ${rssItems}
return { ok: true, settings: currentSettings() };
});
// Composer (Admin): Nachricht/Embed als Bot senden oder eigene Posts bearbeiten
app.post('/api/compose', async (request, reply) => {
if (requireAdmin(request, reply)) return;
const { channel_id: channelId, message_id: messageId, content, embed_title: embedTitle } = request.body ?? {};
const text = String(content ?? '').trim();
if (!channelId || !text) {
return reply.code(400).send({ error: 'channel_id und content nötig' });
}
const channel = await client.channels.fetch(String(channelId)).catch(() => null);
if (!channel?.isTextBased()) {
return reply.code(404).send({ error: 'Kanal nicht gefunden' });
}
// Mit Embed-Titel → gebrandetes Embed, sonst Plaintext (Discord-Markdown)
const payload = embedTitle?.trim()
? {
content: null,
embeds: [
new EmbedBuilder()
.setColor(0xf5c518)
.setTitle(String(embedTitle).slice(0, 200))
.setDescription(text.slice(0, 4000))
.setFooter({ text: 'D4RKST3R' })
.setTimestamp(),
],
}
: { content: text.slice(0, 2000), embeds: [] };
if (messageId) {
const message = await channel.messages.fetch(String(messageId)).catch(() => null);
if (!message) return reply.code(404).send({ error: 'Nachricht nicht gefunden' });
if (message.author?.id !== client.user?.id) {
return reply.code(403).send({ error: 'Nur eigene Bot-Nachrichten sind editierbar' });
}
await message.edit(payload);
request.log.info(`Composer: Nachricht ${messageId} bearbeitet`);
return { ok: true, edited: true, message_id: String(messageId) };
}
const message = await channel.send(payload);
request.log.info(`Composer: Nachricht ${message.id} gesendet`);
return { ok: true, edited: false, message_id: message.id };
});
// --- API-Keys (Admin) — für /api/v1/* ---
const VALID_SCOPES = ['message', 'dm', 'roles', 'read'];