Discord: Support-Antworten im Ticket-Kanal als Antwort ins Ticket übernehmen
Nachrichten von verknüpftem Personal im Ticket-Kanal/-Thread werden wie eine Antwort im Kundencenter behandelt: Support-Nachricht, Status 'wartet auf Kunde', Mail an den Ersteller, Audit-Eintrag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8ef0b3bc05
commit
9336f46d0a
1 changed files with 19 additions and 7 deletions
|
|
@ -4,6 +4,7 @@ import { pool } from '@kc/platform/db';
|
||||||
import { decrypt } from '@kc/platform/crypto';
|
import { decrypt } from '@kc/platform/crypto';
|
||||||
import { enqueue } from '@kc/platform/jobs';
|
import { enqueue } from '@kc/platform/jobs';
|
||||||
import { config } from '@kc/platform/config';
|
import { config } from '@kc/platform/config';
|
||||||
|
import { audit } from '@kc/platform/audit';
|
||||||
|
|
||||||
interface DiscordSettings {
|
interface DiscordSettings {
|
||||||
enabled: boolean; token: string | null; guildId: string | null; adminChannelId: string | null; ticketChannelId: string | null;
|
enabled: boolean; token: string | null; guildId: string | null; adminChannelId: string | null; ticketChannelId: string | null;
|
||||||
|
|
@ -133,18 +134,29 @@ async function handle(i: ChatInputCommandInteraction): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Nachricht im Ticket-Thread bzw. Ticket-Kanal von einem Kunden: als Ticket-Nachricht übernehmen. Personal-Nachrichten werden ignoriert (Web-Oberfläche bleibt die Quelle). */
|
/** Nachricht im Ticket-Thread bzw. Ticket-Kanal übernehmen: vom Kunden als Kundenantwort, vom Personal (verknüpftes Konto)
|
||||||
|
* als Support-Antwort wie im Kundencenter (Status "wartet auf Kunde", Mail an den Ersteller). Andere Nachrichten werden ignoriert. */
|
||||||
async function handleThreadMessage(msg: Message, log: (m: string) => void): Promise<void> {
|
async function handleThreadMessage(msg: Message, log: (m: string) => void): Promise<void> {
|
||||||
if (msg.author.bot || !msg.inGuild()) return;
|
if (msg.author.bot || !msg.inGuild()) return;
|
||||||
const t = await pool.query('SELECT tt.ticket_id, tk.number, tk.subject, tk.org_id, tk.status FROM ticket_discord_threads tt JOIN tickets tk ON tk.id = tt.ticket_id WHERE tt.thread_id = ?', [msg.channel.id]).then(([r]: any) => r[0]);
|
const t = await pool.query('SELECT tt.ticket_id, tk.number, tk.subject, tk.org_id, tk.status, tk.created_by FROM ticket_discord_threads tt JOIN tickets tk ON tk.id = tt.ticket_id WHERE tt.thread_id = ?', [msg.channel.id]).then(([r]: any) => r[0]);
|
||||||
if (!t) return;
|
if (!t) return;
|
||||||
if (t.status === 'closed') { await msg.reply('Dieses Ticket ist geschlossen. Bitte im Kundencenter ein neues Ticket eröffnen oder den Befehl `/ticket` nutzen.').catch(() => undefined); return; }
|
if (t.status === 'closed') { await msg.reply('Dieses Ticket ist geschlossen. Bitte im Kundencenter ein neues Ticket eröffnen oder den Befehl `/ticket` nutzen.').catch(() => undefined); return; }
|
||||||
const u = await pool.query("SELECT id, name FROM users WHERE discord_user_id = ? AND kind = 'customer'", [msg.author.id]).then(([r]: any) => r[0]);
|
|
||||||
if (!u) return; // unbekannt oder Personal: Web-Oberfläche bleibt die Quelle für Personal-Antworten
|
|
||||||
const member = await pool.query('SELECT 1 AS x FROM memberships WHERE user_id = ? AND org_id = ?', [u.id, t.org_id]).then(([r]: any) => r[0]);
|
|
||||||
if (!member) return;
|
|
||||||
const body = msg.content.trim().slice(0, 10000);
|
const body = msg.content.trim().slice(0, 10000);
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
|
const staff = await linkedUser(msg.author.id, 'staff');
|
||||||
|
if (staff) {
|
||||||
|
await pool.query('INSERT INTO ticket_messages (id, ticket_id, author_id, author_kind, body) VALUES (?,?,?,?,?)', [randomUUID(), t.ticket_id, staff.id, 'staff', body]);
|
||||||
|
await pool.query("UPDATE tickets SET status = 'pending_customer', last_message_at = UTC_TIMESTAMP(3), resolved_at = NULL, closed_at = NULL WHERE id = ?", [t.ticket_id]);
|
||||||
|
await audit({ actorType: 'user', actorId: staff.id, orgId: t.org_id, action: 'ticket.message', resourceType: 'ticket', resourceId: t.ticket_id, after: { internalNote: false, via: 'discord' } });
|
||||||
|
const creator = await pool.query('SELECT email, name FROM users WHERE id = ?', [t.created_by]).then(([r]: any) => r[0]);
|
||||||
|
if (creator) await enqueue('mail.template', { to: creator.email, key: 'ticket_message', vars: { name: creator.name, number: t.number, subject: t.subject, ticketLink: ticketLink(t.ticket_id) } }, { idempotencyKey: `mail:discord-staff:${msg.id}` });
|
||||||
|
log(`Discord: Support-Antwort zu Ticket ${t.number} übernommen.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const u = await pool.query("SELECT id, name FROM users WHERE discord_user_id = ? AND kind = 'customer'", [msg.author.id]).then(([r]: any) => r[0]);
|
||||||
|
if (!u) return; // Discord-Konto ohne Verknüpfung: nicht zuordenbar
|
||||||
|
const member = await pool.query('SELECT 1 AS x FROM memberships WHERE user_id = ? AND org_id = ?', [u.id, t.org_id]).then(([r]: any) => r[0]);
|
||||||
|
if (!member) return;
|
||||||
await pool.query('INSERT INTO ticket_messages (id, ticket_id, author_id, author_kind, body) VALUES (?,?,?,?,?)', [randomUUID(), t.ticket_id, u.id, 'customer', body]);
|
await pool.query('INSERT INTO ticket_messages (id, ticket_id, author_id, author_kind, body) VALUES (?,?,?,?,?)', [randomUUID(), t.ticket_id, u.id, 'customer', body]);
|
||||||
await pool.query("UPDATE tickets SET status = 'pending_staff', last_message_at = UTC_TIMESTAMP(3), resolved_at = NULL, closed_at = NULL WHERE id = ?", [t.ticket_id]);
|
await pool.query("UPDATE tickets SET status = 'pending_staff', last_message_at = UTC_TIMESTAMP(3), resolved_at = NULL, closed_at = NULL WHERE id = ?", [t.ticket_id]);
|
||||||
await notify('ticket.message', { number: t.number }, `discord-in:${msg.id}`);
|
await notify('ticket.message', { number: t.number }, `discord-in:${msg.id}`);
|
||||||
|
|
@ -201,7 +213,7 @@ async function ensureTicketChannel(s: DiscordSettings, ticketId: string, withOpe
|
||||||
topic: `${t.number} · ${t.subject}`.slice(0, 1024), permissionOverwrites: overwrites(category.guild, s, customerIds), reason: `Ticket ${t.number}`,
|
topic: `${t.number} · ${t.subject}`.slice(0, 1024), permissionOverwrites: overwrites(category.guild, s, customerIds), reason: `Ticket ${t.number}`,
|
||||||
});
|
});
|
||||||
await pool.query('INSERT INTO ticket_discord_threads (ticket_id, thread_id) VALUES (?,?)', [ticketId, channel.id]);
|
await pool.query('INSERT INTO ticket_discord_threads (ticket_id, thread_id) VALUES (?,?)', [ticketId, channel.id]);
|
||||||
await channel.send({ content: `**${t.number} · ${t.subject}**\nKunde: ${t.org_name} (${t.customer_number})\nIm Kundencenter: ${ticketLink(t.id)}\n_Antworten des Kunden hier werden ins Ticket übernommen. Antworten des Supports bitte im Kundencenter schreiben._`.slice(0, 2000), allowedMentions: { parse: [] } });
|
await channel.send({ content: `**${t.number} · ${t.subject}**\nKunde: ${t.org_name} (${t.customer_number})\nIm Kundencenter: ${ticketLink(t.id)}\n_Nachrichten hier werden ins Ticket übernommen. Antworten des Supports gehen dem Kunden zusätzlich per Mail zu._`.slice(0, 2000), allowedMentions: { parse: [] } });
|
||||||
if (withOpening) {
|
if (withOpening) {
|
||||||
const first = await pool.query('SELECT m.body, m.author_kind FROM ticket_messages m WHERE m.ticket_id = ? AND m.internal_note = 0 ORDER BY m.created_at LIMIT 1', [ticketId]).then(([r]: any) => r[0]);
|
const first = await pool.query('SELECT m.body, m.author_kind FROM ticket_messages m WHERE m.ticket_id = ? AND m.internal_note = 0 ORDER BY m.created_at LIMIT 1', [ticketId]).then(([r]: any) => r[0]);
|
||||||
if (first) await channel.send({ content: `**${first.author_kind === 'staff' ? 'Support' : 'Kunde'}:** ${first.body}`.slice(0, 2000), allowedMentions: { parse: [] } });
|
if (first) await channel.send({ content: `**${first.author_kind === 'staff' ? 'Support' : 'Kunde'}:** ${first.body}`.slice(0, 2000), allowedMentions: { parse: [] } });
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue