feat(discord): Ticket-Threads mit bidirektionaler Synchronisierung

Zweiter Teil des Discord-Ausbaus. Hat ein Kunde sein Konto verknüpft
(siehe vorheriger Commit), bekommt sein Ticket automatisch einen
privaten Thread im konfigurierten Ticket-Kanal:

- Neuer Job discord.ticket_sync: legt den Thread beim ersten Bedarf
  an (Name "ticket-<Nummer>", Kunde wird als Mitglied hinzugefügt)
  und postet dort jede neue, nicht-interne Nachricht (sowohl Kunden-
  als auch Personal-Antworten). Wird von /tickets (Erstellung durch
  Kunden) und /tickets/:id/messages (Antworten, keine internen
  Notizen) ausgelöst.
- Umgekehrte Richtung: Antwortet der Kunde direkt im Thread, übernimmt
  ein neuer messageCreate-Listener im Bot die Nachricht als
  Ticket-Antwort – nur wenn der Absender über discord_user_id
  eindeutig als Mitglied der Organisation des Tickets verifiziert ist.
  Personal-Nachrichten im Thread werden bewusst nicht übernommen (Web-
  Oberfläche bleibt die Quelle für Personal-Antworten, kein Echo).
- Dafür nötig: privilegierte Message Content Intent (war in der
  Anleitung aus dem vorigen Schritt schon vermerkt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kundencenter 2026-09-28 23:44:53 +02:00
parent fcbd34b0c6
commit 1cd1a8b03a
3 changed files with 54 additions and 5 deletions

View file

@ -73,6 +73,7 @@ export const ticketsModule: KcModule = {
if (!staff) {
await notify('ticket.created', { number, subject: b.subject.slice(0, 100) }, `ticket.created:${id}`, req.correlationId);
await mailTo(a.user.email, 'ticket_created', { name: a.user.name, number, subject: b.subject, ticketLink: ticketLink(id) }, `mail:ticket.created:${id}`, req.correlationId);
await enqueue('discord.ticket_sync', { ticketId: id, body: b.body, authorLabel: 'Kunde' }, { idempotencyKey: `discord:ticket.created:${id}`, correlationId: req.correlationId });
}
return { id, number, messageId };
});
@ -118,6 +119,7 @@ export const ticketsModule: KcModule = {
});
await audit({ actorType: 'user', actorId: a.user.id, orgId: t.org_id, action: 'ticket.message', resourceType: 'ticket', resourceId: id, correlationId: req.correlationId, ip: clientIp(req), after: { internalNote: b.internalNote } });
if (!b.internalNote && !staff) await notify('ticket.message', { number: t.number }, `ticket.message:${id}:${Date.now()}`, req.correlationId);
if (!b.internalNote) await enqueue('discord.ticket_sync', { ticketId: id, body: b.body, authorLabel: staff ? 'Support' : 'Kunde' }, { idempotencyKey: `discord:ticket.message:${messageId}`, correlationId: req.correlationId });
let emailed = false;
if (!b.internalNote && staff && b.notifyEmail) {
const creator = await one('SELECT email, name FROM users WHERE id = ?', [t.created_by]);