137 lines
7.1 KiB
PHP
137 lines
7.1 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
/**
|
|
* assistant.php: Schnittstelle des digitalen Assistenten im Kundenportal (JSON).
|
|
* status -> {active, name, greeting, helpTopics, user}
|
|
* ask {message} -> {type: "answer"|"none"|"ticket", ...}
|
|
* ticket {name, email, subject, message, helpTopicId?} -> {ok, number}
|
|
* Jeder Aufruf braucht den CSRF-Token der Sitzung (csrf_token). Fragen und Tickets sind je Sitzung bzw. Adresse begrenzt.
|
|
*/
|
|
require_once __DIR__ . '/includes/bootstrap.php';
|
|
require_once __DIR__ . '/includes/assistant.php';
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Cache-Control: no-store');
|
|
|
|
function assistant_json(array $data, int $status = 200): never
|
|
{
|
|
http_response_code($status);
|
|
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
$pdo = get_db();
|
|
$action = (string) ($_POST['action'] ?? $_GET['action'] ?? '');
|
|
|
|
if (!in_array($action, ['status', 'ask', 'ticket', 'feedback'], true)) assistant_json(['error' => 'Unbekannte Aktion.'], 400);
|
|
|
|
// Anfragen müssen vom eigenen Formular der Seite kommen (Token der Sitzung)
|
|
$token = (string) ($_POST['csrf_token'] ?? $_GET['csrf_token'] ?? '');
|
|
if ($token === '' || !hash_equals((string) ($_SESSION['csrf_token'] ?? ''), $token)) {
|
|
assistant_json(['error' => 'Die Sitzung ist abgelaufen. Bitte laden Sie die Seite neu.', 'csrfExpired' => true], 403);
|
|
}
|
|
|
|
$active = assistant_active($pdo);
|
|
$texts = assistant_texts();
|
|
|
|
if ($action === 'status') {
|
|
$user = current_user();
|
|
assistant_json([
|
|
'active' => $active,
|
|
'name' => $texts['name'],
|
|
'greeting' => $texts['greeting'],
|
|
'noAnswer' => $texts['no_answer'],
|
|
'helpTopics' => $active ? assistant_help_topics($pdo) : [],
|
|
'user' => $user ? ['name' => (string) ($user['name'] ?? ''), 'email' => (string) ($user['email'] ?? '')] : null,
|
|
]);
|
|
}
|
|
|
|
if (!$active) assistant_json(['error' => 'Der Assistent ist gerade nicht verfügbar.', 'active' => false], 409);
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') assistant_json(['error' => 'Nur POST.'], 405);
|
|
|
|
if ($action === 'feedback') {
|
|
// Rückmeldung zu einer Antwort ("Das hat geholfen" oder Ticket statt Antwort); nur für Antworten dieser Sitzung
|
|
$id = (int) ($_POST['logId'] ?? 0);
|
|
if ($id > 0 && in_array($id, array_map('intval', (array) ($_SESSION['assistant_log_ids'] ?? [])), true)) {
|
|
try {
|
|
$pdo->prepare("UPDATE assistant_log SET helpful = ? WHERE id = ? AND result IN ('custom','kb')")->execute([!empty($_POST['helpful']) ? 1 : 0, $id]);
|
|
} catch (Throwable $e) {
|
|
// Datenbank noch nicht aktualisiert
|
|
}
|
|
}
|
|
assistant_json(['ok' => true]);
|
|
}
|
|
|
|
if ($action === 'ask') {
|
|
// höchstens 30 Fragen je 10 Minuten und Sitzung
|
|
$now = time();
|
|
$log = array_values(array_filter((array) ($_SESSION['assistant_asks'] ?? []), fn($t) => $t > $now - 600));
|
|
if (count($log) >= 30) assistant_json(['error' => 'Das waren viele Fragen in kurzer Zeit. Bitte warten Sie einen Moment.'], 429);
|
|
$log[] = $now;
|
|
$_SESSION['assistant_asks'] = $log;
|
|
|
|
$message = trim(mb_substr((string) ($_POST['message'] ?? ''), 0, 300));
|
|
if ($message === '') assistant_json(['error' => 'Bitte eine Frage eingeben.'], 422);
|
|
|
|
// Wunsch nach einem Ticket oder einem Menschen: nicht erst suchen
|
|
$norm = assistant_normalize($message);
|
|
if (preg_match('/\b(ticket|mitarbeiter|support|beschwerde|reklamation|kundenservice|ansprechpartner)\b/', $norm)
|
|
&& preg_match('/\b(erstell|eroeffn|anleg|oeffn|schreib|melden|sprechen|kontakt|will|moechte|brauche|bitte)/', $norm)) {
|
|
assistant_log_write($pdo, $message, 'ticket_request');
|
|
assistant_json(['type' => 'ticket', 'text' => 'Gern, ich erstelle ein Ticket für Sie.']);
|
|
}
|
|
// Begrüßung und Dank
|
|
if (preg_match('/^(hallo|hi|hey|guten (tag|morgen|abend)|moin|servus)\b/', $norm) && count(assistant_tokens($message)) === 0) {
|
|
assistant_json(['type' => 'smalltalk', 'text' => 'Hallo! Was möchten Sie wissen?']);
|
|
}
|
|
if (preg_match('/^(danke|vielen dank|dankeschoen|super danke|ok danke)\b/', $norm)) {
|
|
assistant_json(['type' => 'smalltalk', 'text' => 'Gern geschehen! Kann ich noch etwas für Sie tun?']);
|
|
}
|
|
|
|
$found = assistant_search($pdo, $message);
|
|
if (!$found) {
|
|
assistant_log_write($pdo, $message, 'none');
|
|
assistant_json(['type' => 'none', 'text' => $texts['no_answer']]);
|
|
}
|
|
$logId = assistant_log_write($pdo, $message, $found['source'] === 'custom' ? 'custom' : 'kb', ['source' => $found['source'], 'id' => $found['id'], 'title' => $found['title']]);
|
|
if ($logId) $_SESSION['assistant_log_ids'] = array_slice(array_merge((array) ($_SESSION['assistant_log_ids'] ?? []), [$logId]), -50);
|
|
assistant_json(['logId' => $logId, 'type' => 'answer', 'source' => $found['source'], 'title' => $found['title'], 'text' => $found['text'], 'link' => $found['link'], 'others' => $found['others']]);
|
|
}
|
|
|
|
// ── Ticket erstellen ─────────────────────────────────────────────────────────
|
|
if (!empty($_POST['hp_website'])) assistant_json(['error' => 'Anfrage abgelehnt.'], 400); // Spam-Falle: das versteckte Feld bleibt bei Menschen leer
|
|
|
|
$ip = client_ip();
|
|
// höchstens 3 Tickets je Stunde und Adresse über den Assistenten
|
|
$recent = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE requester_ip = ? AND created_at >= ? AND id IN (SELECT ticket_id FROM activity_log WHERE action LIKE 'Ticket über den digitalen Assistenten%')");
|
|
$recent->execute([$ip, date('Y-m-d H:i:s', time() - 3600)]);
|
|
if ((int) $recent->fetchColumn() >= 3) assistant_json(['error' => 'Über den Assistenten wurden bereits mehrere Tickets erstellt. Bitte versuchen Sie es später noch einmal oder antworten Sie auf die E-Mail zu Ihrem Ticket.'], 429);
|
|
|
|
$in = [
|
|
'name' => trim((string) ($_POST['name'] ?? '')),
|
|
'email' => trim((string) ($_POST['email'] ?? '')),
|
|
'subject' => trim((string) ($_POST['subject'] ?? '')),
|
|
'message' => trim((string) ($_POST['message'] ?? '')),
|
|
];
|
|
$topicId = (int) ($_POST['helpTopicId'] ?? 0);
|
|
if ($topicId > 0) {
|
|
$ok = false;
|
|
foreach (assistant_help_topics($pdo) as $t) if ($t['id'] === $topicId) $ok = true;
|
|
if (!$ok) assistant_json(['error' => 'Das gewählte Thema gibt es nicht.'], 422);
|
|
$in['helpTopicId'] = $topicId;
|
|
}
|
|
// Abteilung und Priorität für Assistenten-Tickets (Einstellung); ein gewähltes Hilfethema bestimmt sie selbst
|
|
if ($topicId <= 0) {
|
|
$dept = (int) get_setting('assistant_department_id', '0');
|
|
$prio = (int) get_setting('assistant_priority_id', '0');
|
|
if ($dept > 0) $in['departmentId'] = $dept;
|
|
if ($prio > 0) $in['priorityId'] = $prio;
|
|
}
|
|
try {
|
|
$created = public_ticket_create($pdo, $in, $ip, ['activity' => 'Ticket über den digitalen Assistenten erstellt', 'tag' => 'Assistent']);
|
|
} catch (AdminModelException $e) {
|
|
assistant_json(['error' => $e->getMessage()], $e->httpStatus());
|
|
}
|
|
assistant_log_write($pdo, mb_substr($in['subject'], 0, 300), 'ticket_created', null, $created['number']);
|
|
assistant_json(['ok' => true, 'number' => $created['number']], 201);
|