Upload via GUI (94 Dateien)

This commit is contained in:
2026-10-24 20:15:15 +00:00
parent 9108a34774
commit cbf6139e74
45 changed files with 3478 additions and 0 deletions
+354
View File
@@ -0,0 +1,354 @@
<?php
// Community-Bot: Discord- und Telegram-Anbindung. Alle Funktionen beginnen mit communitybot_.
const COMMUNITYBOT = 'community-bot';
function communitybot_config(): array
{
$secret = static function (string $key): string {
$v = (string) plugin_setting(COMMUNITYBOT, $key, '');
if ($v === '') return '';
try { return decrypt_secret($v); } catch (Throwable $e) { return ''; }
};
return [
'discord_key' => strtolower(trim((string) plugin_setting(COMMUNITYBOT, 'discord_key', ''))),
'discord_app' => trim((string) plugin_setting(COMMUNITYBOT, 'discord_app', '')),
'discord_token' => $secret('discord_token'),
'telegram_token' => $secret('telegram_token'),
'telegram_secret' => $secret('telegram_secret'),
'department_id' => (int) plugin_setting(COMMUNITYBOT, 'department_id', '0'),
'priority_id' => (int) plugin_setting(COMMUNITYBOT, 'priority_id', '0'),
'limit' => max(1, min(50, (int) plugin_setting(COMMUNITYBOT, 'limit', '3'))),
'notify' => plugin_setting(COMMUNITYBOT, 'notify', '1') === '1',
];
}
function communitybot_set_secret(string $key, string $value): void
{
plugin_set_setting(COMMUNITYBOT, $key, $value === '' ? '' : encrypt_secret($value));
}
/** HTTP-Aufruf (für Tests austauschbar über $GLOBALS['communitybot_http']). Liefert ['status' => int, 'body' => string]. */
function communitybot_http(string $method, string $url, array $headers = [], ?string $body = null): array
{
if (isset($GLOBALS['communitybot_http']) && is_callable($GLOBALS['communitybot_http'])) {
return ($GLOBALS['communitybot_http'])($method, $url, $headers, $body);
}
$ctx = stream_context_create(['http' => [
'method' => $method,
'header' => implode("\r\n", $headers),
'content' => $body ?? '',
'timeout' => 8,
'ignore_errors' => true,
]]);
$res = @file_get_contents($url, false, $ctx);
$status = 0;
foreach ($http_response_header ?? [] as $h) if (preg_match('#^HTTP/\S+\s+(\d+)#', $h, $m)) $status = (int) $m[1];
return ['status' => $status, 'body' => $res === false ? '' : (string) $res];
}
function communitybot_json(string $method, string $url, array $headers, ?array $data): array
{
$r = communitybot_http($method, $url, array_merge(['Content-Type: application/json', 'User-Agent: DeskHub-CommunityBot (https://deskhub.example, 1.0)'], $headers), $data === null ? null : json_encode($data, JSON_UNESCAPED_UNICODE));
return ['status' => $r['status'], 'json' => json_decode($r['body'], true) ?: [], 'body' => $r['body']];
}
function communitybot_clip(string $text, int $max): string
{
$text = trim(preg_replace('/\s+/u', ' ', strip_tags(html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'))));
return mb_strlen($text) > $max ? mb_substr($text, 0, $max - 1) . '…' : $text;
}
// ── Gemeinsame Befehle ─────────────────────────────────────────────────────
/** Platzhalter-Adresse; Mails an ".invalid" werden vom Kern nie verschickt. */
function communitybot_email(string $platform, string $user): string
{
return $platform . '-' . preg_replace('/[^a-z0-9]/i', '', $user) . '@' . $platform . '.invalid';
}
function communitybot_find_link(PDO $pdo, string $platform, string $user, string $number): ?array
{
$number = ltrim(trim($number), '#');
if ($number === '') return null;
$s = $pdo->prepare('SELECT t.*, l.ext_chat FROM plugin_communitybot_links l JOIN tickets t ON t.id = l.ticket_id WHERE l.platform = ? AND l.ext_user = ? AND t.ticket_number = ? ORDER BY l.id DESC LIMIT 1');
$s->execute([$platform, $user, $number]);
return $s->fetch() ?: null;
}
function communitybot_help(string $platform): string
{
$p = $platform === 'telegram' ? '/ticket Betreff | Nachricht' : '/ticket betreff:… nachricht:…';
return "So geht's:\n{$p} – neues Ticket anlegen\n/status Nummer – Stand eines Tickets\n/antwort Nummer Text – Ihrem Ticket etwas hinzufügen";
}
function communitybot_cmd_ticket(PDO $pdo, string $platform, string $user, string $chat, string $name, string $subject, string $message): string
{
$cfg = communitybot_config();
$subject = trim($subject);
$message = trim($message);
if ($message === '' && $subject !== '') { $message = $subject; $subject = ''; }
if ($message === '') return 'Bitte beschreiben Sie Ihr Anliegen: ' . ($platform === 'telegram' ? '/ticket Betreff | Nachricht' : '/ticket mit Betreff und Nachricht');
if ($subject === '') $subject = communitybot_clip($message, 60);
$subject = mb_substr($subject, 0, 200);
$message = mb_substr($message, 0, 4000);
$c = $pdo->prepare('SELECT COUNT(*) FROM plugin_communitybot_links WHERE platform = ? AND ext_user = ? AND created_at >= ?');
$c->execute([$platform, $user, date('Y-m-d H:i:s', time() - 3600)]);
if ((int) $c->fetchColumn() >= $cfg['limit']) return 'Sie haben in der letzten Stunde schon ' . $cfg['limit'] . ' Tickets angelegt. Bitte versuchen Sie es später noch einmal oder ergänzen Sie ein bestehendes Ticket mit /antwort.';
$label = $platform === 'discord' ? 'Discord' : 'Telegram';
$name = trim(preg_replace('/[\x00-\x1F]+/u', ' ', $name));
if ($name === '') $name = $label . '-Nutzer';
try {
$r = public_ticket_create($pdo, [
'name' => mb_substr($name, 0, 120),
'email' => communitybot_email($platform, $user),
'subject' => $subject,
'message' => $message,
'departmentId' => $cfg['department_id'] ?: null,
'priorityId' => $cfg['priority_id'] ?: null,
], '0.0.0.0', ['activity' => 'Ticket über ' . $label . ' erstellt', 'tag' => $label]);
} catch (AdminModelException $e) {
return 'Das Ticket konnte nicht angelegt werden: ' . $e->getMessage();
} catch (Throwable $e) {
error_log('communitybot ticket: ' . $e->getMessage());
return 'Das Ticket konnte gerade nicht angelegt werden. Bitte versuchen Sie es später noch einmal.';
}
$pdo->prepare('INSERT INTO plugin_communitybot_links (ticket_id, platform, ext_user, ext_chat, ext_name, created_at) VALUES (?,?,?,?,?,?)')
->execute([$r['id'], $platform, $user, $chat, mb_substr($name, 0, 120), db_now()]);
$hint = $cfg['notify'] ? ' Wir melden uns hier, sobald es eine Antwort gibt.' : '';
return 'Ihr Ticket #' . $r['number'] . ' wurde angelegt.' . $hint . ' Stand abfragen mit /status ' . $r['number'] . '.';
}
function communitybot_cmd_status(PDO $pdo, string $platform, string $user, string $number): string
{
if (trim($number) === '') {
$s = $pdo->prepare('SELECT t.ticket_number, t.subject, t.status FROM plugin_communitybot_links l JOIN tickets t ON t.id = l.ticket_id WHERE l.platform = ? AND l.ext_user = ? ORDER BY l.id DESC LIMIT 5');
$s->execute([$platform, $user]);
$rows = $s->fetchAll();
if (!$rows) return 'Sie haben über diesen Kanal noch kein Ticket angelegt.';
return "Ihre letzten Tickets:\n" . implode("\n", array_map(fn($t) => '#' . $t['ticket_number'] . ' – ' . status_label((string) $t['status'])[0] . ' – ' . communitybot_clip((string) $t['subject'], 50), $rows));
}
$t = communitybot_find_link($pdo, $platform, $user, $number);
if (!$t) return 'Dieses Ticket gibt es nicht oder es gehört nicht zu Ihnen.';
return 'Ticket #' . $t['ticket_number'] . ' „' . communitybot_clip((string) $t['subject'], 60) . '“: ' . status_label((string) $t['status'])[0] . ' (zuletzt geändert ' . format_dt((string) $t['updated_at']) . ').';
}
function communitybot_cmd_reply(PDO $pdo, string $platform, string $user, string $number, string $text): string
{
$text = trim($text);
if (trim($number) === '' || $text === '') return 'So geht es: /antwort Ticketnummer Ihre Nachricht';
$t = communitybot_find_link($pdo, $platform, $user, $number);
if (!$t) return 'Dieses Ticket gibt es nicht oder es gehört nicht zu Ihnen.';
if ($t['status'] === 'closed') return 'Das Ticket #' . $t['ticket_number'] . ' ist geschlossen. Bitte legen Sie mit /ticket ein neues an.';
$now = db_now();
$pdo->prepare('INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES (?,?,?,0,?)')->execute([$t['id'], 'customer', mb_substr($text, 0, 4000), $now]);
$reopen = in_array($t['status'], ['answered', 'resolved'], true);
$new = $reopen ? 'open' : $t['status'];
$pdo->prepare('UPDATE tickets SET status = ?, updated_at = ? WHERE id = ?')->execute([$new, $now, $t['id']]);
log_activity($pdo, (int) $t['id'], null, 'Kunde hat geantwortet (' . ($platform === 'discord' ? 'Discord' : 'Telegram') . ')');
if ($reopen) log_activity($pdo, (int) $t['id'], null, 'Ticket wieder geöffnet');
webhook_dispatch($pdo, 'ticket.replied', (int) $t['id'], ['author' => 'customer']);
if ($new !== $t['status']) webhook_dispatch($pdo, 'ticket.status_changed', (int) $t['id'], ['previousStatus' => $t['status']]);
return 'Ihre Nachricht wurde zu Ticket #' . $t['ticket_number'] . ' hinzugefügt.';
}
// ── Discord ────────────────────────────────────────────────────────────────
function communitybot_discord_verify(array $req, string $publicKeyHex): bool
{
$sig = (string) ($req['headers']['x-signature-ed25519'] ?? '');
$ts = (string) ($req['headers']['x-signature-timestamp'] ?? '');
if ($publicKeyHex === '' || !ctype_xdigit($publicKeyHex) || strlen($publicKeyHex) !== 64 || !ctype_xdigit($sig) || strlen($sig) !== 128 || !ctype_digit($ts)) return false;
if (abs(time() - (int) $ts) > 600) return false;
if (!function_exists('sodium_crypto_sign_verify_detached')) return false;
try {
return sodium_crypto_sign_verify_detached(hex2bin($sig), $ts . $req['body'], hex2bin($publicKeyHex));
} catch (Throwable $e) {
return false;
}
}
function communitybot_discord(array $req): array
{
$cfg = communitybot_config();
if (!communitybot_discord_verify($req, $cfg['discord_key'])) return ['status' => 401, 'json' => ['error' => 'Ungültige Signatur.']];
$in = json_decode($req['body'], true);
if (!is_array($in)) return ['status' => 400, 'json' => ['error' => 'Ungültige Anfrage.']];
if (($in['type'] ?? 0) === 1) return ['status' => 200, 'json' => ['type' => 1]];
if (($in['type'] ?? 0) !== 2) return ['status' => 200, 'json' => ['type' => 4, 'data' => ['content' => 'Das wird nicht unterstützt.', 'flags' => 64]]];
$user = $in['member']['user'] ?? $in['user'] ?? [];
$uid = (string) ($user['id'] ?? '');
if (!preg_match('/^\d{5,25}$/', $uid)) return ['status' => 400, 'json' => ['error' => 'Kein Nutzer.']];
$name = (string) ($user['global_name'] ?? $user['username'] ?? '');
$opt = [];
foreach ((array) ($in['data']['options'] ?? []) as $o) if (isset($o['name'])) $opt[(string) $o['name']] = (string) ($o['value'] ?? '');
$pdo = get_db();
$chat = (string) ($in['channel_id'] ?? '');
switch ((string) ($in['data']['name'] ?? '')) {
case 'ticket': $text = communitybot_cmd_ticket($pdo, 'discord', $uid, $chat, $name, $opt['betreff'] ?? '', $opt['nachricht'] ?? ''); break;
case 'status': $text = communitybot_cmd_status($pdo, 'discord', $uid, $opt['nummer'] ?? ''); break;
case 'antwort': $text = communitybot_cmd_reply($pdo, 'discord', $uid, $opt['nummer'] ?? '', $opt['text'] ?? ''); break;
default: $text = communitybot_help('discord');
}
// flags 64: nur der Absender sieht die Antwort; keine Erwähnungen aus Nutzertext
return ['status' => 200, 'json' => ['type' => 4, 'data' => ['content' => mb_substr($text, 0, 1900), 'flags' => 64, 'allowed_mentions' => ['parse' => []]]]];
}
function communitybot_discord_commands(): array
{
$str = fn(string $n, string $d, bool $req, int $max = 1000) => ['type' => 3, 'name' => $n, 'description' => $d, 'required' => $req, 'max_length' => $max];
return [
['name' => 'ticket', 'description' => 'Ein Support-Ticket anlegen', 'options' => [$str('betreff', 'Worum geht es?', true, 100), $str('nachricht', 'Bitte beschreiben Sie Ihr Anliegen', true, 1000)]],
['name' => 'status', 'description' => 'Stand eines Tickets abfragen', 'options' => [$str('nummer', 'Ticketnummer (leer lassen für Ihre letzten)', false, 40)]],
['name' => 'antwort', 'description' => 'Zu einem Ihrer Tickets etwas hinzufügen', 'options' => [$str('nummer', 'Ticketnummer', true, 40), $str('text', 'Ihre Nachricht', true, 1000)]],
];
}
function communitybot_discord_register(): array
{
$cfg = communitybot_config();
if ($cfg['discord_app'] === '' || $cfg['discord_token'] === '') return ['ok' => false, 'message' => 'Bitte Anwendungs-ID und Bot-Token eintragen.'];
$r = communitybot_json('PUT', 'https://discord.com/api/v10/applications/' . rawurlencode($cfg['discord_app']) . '/commands', ['Authorization: Bot ' . $cfg['discord_token']], communitybot_discord_commands());
if ($r['status'] >= 200 && $r['status'] < 300) return ['ok' => true, 'message' => 'Die Befehle /ticket, /status und /antwort sind bei Discord eingetragen (es kann bis zu einer Stunde dauern, bis sie überall erscheinen).'];
return ['ok' => false, 'message' => 'Discord hat abgelehnt (Status ' . $r['status'] . '): ' . communitybot_clip((string) ($r['json']['message'] ?? $r['body']), 200)];
}
function communitybot_discord_dm(string $userId, string $text): bool
{
$cfg = communitybot_config();
if ($cfg['discord_token'] === '') return false;
$h = ['Authorization: Bot ' . $cfg['discord_token']];
$ch = communitybot_json('POST', 'https://discord.com/api/v10/users/@me/channels', $h, ['recipient_id' => $userId]);
$id = (string) ($ch['json']['id'] ?? '');
if ($id === '') return false;
$m = communitybot_json('POST', 'https://discord.com/api/v10/channels/' . rawurlencode($id) . '/messages', $h, ['content' => mb_substr($text, 0, 1900), 'allowed_mentions' => ['parse' => []]]);
return $m['status'] >= 200 && $m['status'] < 300;
}
// ── Telegram ───────────────────────────────────────────────────────────────
function communitybot_telegram(array $req): array
{
$cfg = communitybot_config();
$given = (string) ($req['headers']['x-telegram-bot-api-secret-token'] ?? '');
if ($cfg['telegram_secret'] === '' || !hash_equals($cfg['telegram_secret'], $given)) return ['status' => 401, 'json' => ['error' => 'Ungültiges Token.']];
$in = json_decode($req['body'], true);
$msg = is_array($in) ? ($in['message'] ?? null) : null;
if (!is_array($msg) || !isset($msg['chat']['id'], $msg['from']['id']) || !is_string($msg['text'] ?? null)) return ['status' => 200, 'json' => new stdClass()];
if (!empty($msg['from']['is_bot'])) return ['status' => 200, 'json' => new stdClass()];
if (!preg_match('/^\/([a-z_]+)(?:@\w+)?(?:\s+(.*))?$/isu', trim($msg['text']), $m)) return ['status' => 200, 'json' => new stdClass()];
$uid = (string) $msg['from']['id'];
$chat = (string) $msg['chat']['id'];
$name = trim(((string) ($msg['from']['first_name'] ?? '')) . ' ' . ((string) ($msg['from']['last_name'] ?? '')));
if ($name === '') $name = (string) ($msg['from']['username'] ?? '');
$arg = trim((string) ($m[2] ?? ''));
$pdo = get_db();
switch (strtolower($m[1])) {
case 'ticket':
$parts = array_map('trim', explode('|', $arg, 2));
$text = communitybot_cmd_ticket($pdo, 'telegram', $uid, $chat, $name, count($parts) === 2 ? $parts[0] : '', count($parts) === 2 ? $parts[1] : $parts[0]);
break;
case 'status': $text = communitybot_cmd_status($pdo, 'telegram', $uid, $arg); break;
case 'antwort':
$p = preg_split('/\s+/u', $arg, 2) ?: [''];
$text = communitybot_cmd_reply($pdo, 'telegram', $uid, $p[0] ?? '', $p[1] ?? '');
break;
case 'start': case 'hilfe': case 'help': $text = "Hallo! Ich nehme Ihre Support-Anfragen entgegen.\n" . communitybot_help('telegram'); break;
default: return ['status' => 200, 'json' => new stdClass()];
}
// Antwort direkt im Webhook-Ergebnis: kein zusätzlicher Aufruf nötig
$out = ['method' => 'sendMessage', 'chat_id' => $msg['chat']['id'], 'text' => mb_substr($text, 0, 3900)];
if (isset($msg['message_id'])) $out['reply_to_message_id'] = $msg['message_id'];
return ['status' => 200, 'json' => $out];
}
function communitybot_telegram_send(string $chatId, string $text): bool
{
$cfg = communitybot_config();
if ($cfg['telegram_token'] === '') return false;
$r = communitybot_json('POST', 'https://api.telegram.org/bot' . $cfg['telegram_token'] . '/sendMessage', [], ['chat_id' => $chatId, 'text' => mb_substr($text, 0, 3900)]);
return $r['status'] >= 200 && $r['status'] < 300;
}
function communitybot_telegram_register(): array
{
$cfg = communitybot_config();
if ($cfg['telegram_token'] === '') return ['ok' => false, 'message' => 'Bitte das Bot-Token von @BotFather eintragen.'];
if ($cfg['telegram_secret'] === '') {
$cfg['telegram_secret'] = bin2hex(random_bytes(24));
communitybot_set_secret('telegram_secret', $cfg['telegram_secret']);
}
$r = communitybot_json('POST', 'https://api.telegram.org/bot' . $cfg['telegram_token'] . '/setWebhook', [], [
'url' => communitybot_url('telegram'), 'secret_token' => $cfg['telegram_secret'], 'allowed_updates' => ['message'], 'drop_pending_updates' => true,
]);
if (!empty($r['json']['ok'])) {
communitybot_json('POST', 'https://api.telegram.org/bot' . $cfg['telegram_token'] . '/setMyCommands', [], ['commands' => [
['command' => 'ticket', 'description' => 'Ticket anlegen: /ticket Betreff | Nachricht'],
['command' => 'status', 'description' => 'Stand eines Tickets'],
['command' => 'antwort', 'description' => 'Zu einem Ticket antworten'],
['command' => 'hilfe', 'description' => 'Hilfe'],
]]);
return ['ok' => true, 'message' => 'Der Telegram-Bot ist verbunden.'];
}
return ['ok' => false, 'message' => 'Telegram hat abgelehnt: ' . communitybot_clip((string) ($r['json']['description'] ?? $r['body']), 200)];
}
function communitybot_url(string $endpoint): string
{
return rtrim(base_url(), '/') . '/api/public/v1/plugin_webhook.php?p=' . COMMUNITYBOT . '&e=' . $endpoint;
}
// ── Benachrichtigungen ─────────────────────────────────────────────────────
function communitybot_send_to(array $link, string $text): bool
{
return $link['platform'] === 'discord'
? communitybot_discord_dm((string) $link['ext_user'], $text)
: communitybot_telegram_send((string) $link['ext_chat'], $text);
}
function communitybot_links(PDO $pdo, int $ticketId): array
{
// pro Nutzer nur einmal, auch wenn mehrere Verknüpfungen bestehen
$s = $pdo->prepare('SELECT platform, ext_user, MAX(ext_chat) AS ext_chat FROM plugin_communitybot_links WHERE ticket_id = ? GROUP BY platform, ext_user');
$s->execute([$ticketId]);
return $s->fetchAll();
}
function communitybot_notify_reply(PDO $pdo, int $ticketId): void
{
if (!communitybot_config()['notify']) return;
$links = communitybot_links($pdo, $ticketId);
if (!$links) return;
$t = $pdo->prepare('SELECT ticket_number, is_spam FROM tickets WHERE id = ?');
$t->execute([$ticketId]);
$ticket = $t->fetch();
if (!$ticket || !empty($ticket['is_spam'])) return;
$m = $pdo->prepare("SELECT message FROM ticket_threads WHERE ticket_id = ? AND author_type = 'staff' AND is_internal_note = 0 ORDER BY id DESC LIMIT 1");
$m->execute([$ticketId]);
$msg = (string) $m->fetchColumn();
if ($msg === '') return;
$text = 'Neue Antwort zu Ticket #' . $ticket['ticket_number'] . ":\n\n" . communitybot_clip($msg, 1200) . "\n\nAntworten: /antwort " . $ticket['ticket_number'] . ' Ihre Nachricht';
foreach ($links as $l) {
try { communitybot_send_to($l, $text); } catch (Throwable $e) { error_log('communitybot notify: ' . $e->getMessage()); }
}
}
function communitybot_notify_status(PDO $pdo, int $ticketId): void
{
if (!communitybot_config()['notify']) return;
$links = communitybot_links($pdo, $ticketId);
if (!$links) return;
$t = $pdo->prepare('SELECT ticket_number, status, is_spam FROM tickets WHERE id = ?');
$t->execute([$ticketId]);
$ticket = $t->fetch();
if (!$ticket || !empty($ticket['is_spam']) || !in_array($ticket['status'], ['resolved', 'closed'], true)) return;
$text = 'Ihr Ticket #' . $ticket['ticket_number'] . ' ist jetzt „' . status_label((string) $ticket['status'])[0] . '“. Wenn noch etwas offen ist, antworten Sie mit /antwort ' . $ticket['ticket_number'] . ' Ihre Nachricht.';
foreach ($links as $l) {
try { communitybot_send_to($l, $text); } catch (Throwable $e) { error_log('communitybot notify: ' . $e->getMessage()); }
}
}
@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS plugin_communitybot_links (
id INT AUTO_INCREMENT PRIMARY KEY,
ticket_id INT NOT NULL,
platform VARCHAR(20) NOT NULL,
ext_user VARCHAR(64) NOT NULL,
ext_chat VARCHAR(64) NOT NULL DEFAULT '',
ext_name VARCHAR(120) NOT NULL DEFAULT '',
created_at DATETIME NOT NULL,
KEY idx_plugin_communitybot_ticket (ticket_id),
KEY idx_plugin_communitybot_user (platform, ext_user, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS plugin_communitybot_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticket_id INTEGER NOT NULL,
platform TEXT NOT NULL,
ext_user TEXT NOT NULL,
ext_chat TEXT NOT NULL DEFAULT '',
ext_name TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_plugin_communitybot_ticket ON plugin_communitybot_links (ticket_id);
CREATE INDEX IF NOT EXISTS idx_plugin_communitybot_user ON plugin_communitybot_links (platform, ext_user, created_at)
@@ -0,0 +1,123 @@
<?php
// Seite des Community-Bot-Plugins. Verfügbar: $staff, $pdo.
$isAdmin = ($staff['role'] ?? '') === 'admin';
if (!$isAdmin) { echo '<h1>Discord und Telegram</h1><div class="flash error">Diese Seite ist nur für Administratoren zugänglich.</div>'; return; }
$message = null;
$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
if (!$isAdmin) {
$error = 'Einstellungen ändern darf nur ein Administrator.';
} else {
$action = $_POST['action'] ?? '';
if ($action === 'save') {
$key = strtolower(trim((string) ($_POST['discord_key'] ?? '')));
if ($key !== '' && !preg_match('/^[0-9a-f]{64}$/', $key)) {
$error = 'Der öffentliche Schlüssel von Discord besteht aus 64 Zeichen (0–9, a–f).';
} else {
plugin_set_setting(COMMUNITYBOT, 'discord_key', $key);
plugin_set_setting(COMMUNITYBOT, 'discord_app', preg_replace('/[^0-9]/', '', (string) ($_POST['discord_app'] ?? '')));
// Geheimnisse: leeres Feld lässt den bisherigen Wert stehen
foreach (['discord_token', 'telegram_token'] as $k) {
$v = trim((string) ($_POST[$k] ?? ''));
if ($v !== '') communitybot_set_secret($k, $v);
}
if (!empty($_POST['clear_discord'])) communitybot_set_secret('discord_token', '');
if (!empty($_POST['clear_telegram'])) { communitybot_set_secret('telegram_token', ''); communitybot_set_secret('telegram_secret', ''); }
plugin_set_setting(COMMUNITYBOT, 'department_id', (string) (int) ($_POST['department_id'] ?? 0));
plugin_set_setting(COMMUNITYBOT, 'priority_id', (string) (int) ($_POST['priority_id'] ?? 0));
plugin_set_setting(COMMUNITYBOT, 'limit', (string) max(1, min(50, (int) ($_POST['limit'] ?? 3))));
plugin_set_setting(COMMUNITYBOT, 'notify', !empty($_POST['notify']) ? '1' : '0');
$message = 'Einstellungen gespeichert.';
}
} elseif ($action === 'discord_register') {
$r = communitybot_discord_register();
if ($r['ok']) $message = $r['message']; else $error = $r['message'];
} elseif ($action === 'telegram_register') {
$r = communitybot_telegram_register();
if ($r['ok']) $message = $r['message']; else $error = $r['message'];
}
}
}
$cfg = communitybot_config();
$depts = $pdo->query('SELECT id, name FROM departments ORDER BY name')->fetchAll();
$prios = $pdo->query('SELECT id, name FROM priorities ORDER BY sort_order')->fetchAll();
$recent = $pdo->query('SELECT l.*, t.ticket_number, t.subject, t.status FROM plugin_communitybot_links l JOIN tickets t ON t.id = l.ticket_id ORDER BY l.id DESC LIMIT 30')->fetchAll();
$week = (int) $pdo->query("SELECT COUNT(*) FROM plugin_communitybot_links WHERE created_at >= '" . date('Y-m-d H:i:s', time() - 7 * 86400) . "'")->fetchColumn();
?>
<h1>Discord und Telegram</h1>
<?php if ($message): ?><div class="flash success"><?= e($message) ?></div><?php endif; ?>
<?php if ($error): ?><div class="flash error"><?= e($error) ?></div><?php endif; ?>
<div class="card" style="max-width:900px;margin-bottom:16px">
<h3>Zustand</h3>
<p style="font-size:.92rem"><strong><?= $week ?></strong> Tickets in den letzten 7 Tagen über Discord oder Telegram angelegt.</p>
<p style="font-size:.88rem">
Discord: <?= ($cfg['discord_key'] !== '' ? '<strong style="color:#15803d">Schlüssel eingetragen</strong>' : '<strong style="color:#b45309">Schlüssel fehlt</strong>') ?>
· Benachrichtigung per Direktnachricht: <?= $cfg['discord_token'] !== '' ? 'möglich' : '<strong style="color:#b45309">Bot-Token fehlt</strong>' ?><br>
Telegram: <?= $cfg['telegram_token'] !== '' ? '<strong style="color:#15803d">Token eingetragen</strong>' : '<strong style="color:#b45309">Token fehlt</strong>' ?>
<?php if (!function_exists('sodium_crypto_sign_verify_detached')): ?><br><strong style="color:#b91c1c">Auf diesem Server fehlt die PHP-Erweiterung „sodium“, Discord-Anfragen können nicht geprüft werden.</strong><?php endif; ?>
</p>
<p class="muted" style="font-size:.82rem">Adressen für die Einrichtung (nur aus dem Internet erreichbar, mit https):<br>
Discord „Interactions Endpoint URL“: <code><?= e(communitybot_url('discord')) ?></code><br>
Telegram wird mit dem Knopf unten automatisch verbunden.</p>
</div>
<?php if ($isAdmin): ?>
<form method="post" class="card" style="max-width:900px;margin-bottom:16px">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save">
<h3>Einstellungen</h3>
<h4 style="margin:10px 0 4px">Discord</h4>
<p class="muted" style="font-size:.82rem">Im Discord Developer Portal eine Anwendung anlegen. „General Information“ liefert die Anwendungs-ID und den öffentlichen Schlüssel (Public Key), „Bot“ das Token. Als „Interactions Endpoint URL“ die Adresse oben eintragen.</p>
<label>Anwendungs-ID</label><input type="text" name="discord_app" value="<?= e($cfg['discord_app']) ?>" inputmode="numeric">
<label style="margin-top:8px">Öffentlicher Schlüssel (Public Key)</label><input type="text" name="discord_key" value="<?= e($cfg['discord_key']) ?>" maxlength="64" autocomplete="off">
<label style="margin-top:8px">Bot-Token (für die Befehle und Direktnachrichten) <?= $cfg['discord_token'] !== '' ? '– gespeichert, leer lassen zum Behalten' : '' ?></label>
<input type="password" name="discord_token" value="" autocomplete="new-password">
<?php if ($cfg['discord_token'] !== ''): ?><label style="font-size:.82rem"><input type="checkbox" name="clear_discord" value="1"> Gespeichertes Bot-Token entfernen</label><?php endif; ?>
<h4 style="margin:14px 0 4px">Telegram</h4>
<p class="muted" style="font-size:.82rem">Bei @BotFather mit /newbot einen Bot anlegen und das Token hier eintragen.</p>
<label>Bot-Token <?= $cfg['telegram_token'] !== '' ? '– gespeichert, leer lassen zum Behalten' : '' ?></label>
<input type="password" name="telegram_token" value="" autocomplete="new-password">
<?php if ($cfg['telegram_token'] !== ''): ?><label style="font-size:.82rem"><input type="checkbox" name="clear_telegram" value="1"> Telegram-Token entfernen und Verbindung trennen</label><?php endif; ?>
<h4 style="margin:14px 0 4px">Neue Tickets</h4>
<div style="display:flex;gap:14px;flex-wrap:wrap">
<div><label>Abteilung</label><select name="department_id"><option value="0">Standard</option><?php foreach ($depts as $d): ?><option value="<?= (int) $d['id'] ?>" <?= (int) $d['id'] === $cfg['department_id'] ? 'selected' : '' ?>><?= e($d['name']) ?></option><?php endforeach; ?></select></div>
<div><label>Priorität</label><select name="priority_id"><option value="0">Standard</option><?php foreach ($prios as $p): ?><option value="<?= (int) $p['id'] ?>" <?= (int) $p['id'] === $cfg['priority_id'] ? 'selected' : '' ?>><?= e($p['name']) ?></option><?php endforeach; ?></select></div>
<div><label>Höchstens Tickets pro Stunde und Person</label><input type="number" name="limit" min="1" max="50" value="<?= (int) $cfg['limit'] ?>" style="max-width:120px"></div>
</div>
<label style="display:block;margin-top:10px"><input type="checkbox" name="notify" value="1" <?= $cfg['notify'] ? 'checked' : '' ?>> Absender bei Antworten und bei Lösung benachrichtigen</label>
<div class="btn-row"><button type="submit" class="btn">Speichern</button></div>
</form>
<div class="card" style="max-width:900px;margin-bottom:16px">
<h3>Verbinden</h3>
<p class="muted" style="font-size:.85rem">Erst speichern, dann verbinden.</p>
<form method="post" style="display:inline"><?= csrf_field() ?><input type="hidden" name="action" value="discord_register"><button class="btn secondary" type="submit">Discord-Befehle eintragen</button></form>
<form method="post" style="display:inline"><?= csrf_field() ?><input type="hidden" name="action" value="telegram_register"><button class="btn secondary" type="submit">Telegram-Bot verbinden</button></form>
</div>
<?php endif; ?>
<div class="card">
<h3>Zuletzt angelegt</h3>
<?php if (!$recent): ?><p class="muted">Noch keine Tickets über Discord oder Telegram.</p><?php else: ?>
<table style="width:100%">
<thead><tr><th>Ticket</th><th>Kanal</th><th>Absender</th><th>Status</th><th>Angelegt</th></tr></thead>
<tbody>
<?php foreach ($recent as $l): ?>
<tr>
<td><a href="/staff/ticket_view.php?n=<?= e(urlencode((string) $l['ticket_number'])) ?>">#<?= e((string) $l['ticket_number']) ?></a><div class="muted" style="font-size:.76rem"><?= e(mb_strimwidth((string) $l['subject'], 0, 50, '…')) ?></div></td>
<td><?= $l['platform'] === 'discord' ? 'Discord' : 'Telegram' ?></td>
<td><?= e((string) $l['ext_name']) ?></td>
<td><?= e(status_label((string) $l['status'])[0]) ?></td>
<td><?= e(format_dt((string) $l['created_at'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
@@ -0,0 +1,9 @@
{
"slug": "community-bot",
"name": "Discord- und Telegram-Bot",
"version": "1.0.1",
"description": "Mitglieder legen per /ticket im Discord oder in Telegram ein Ticket an, bekommen die Ticketnummer zurück und werden bei Antworten und bei Lösung benachrichtigt. Der Server nimmt die Anfragen direkt entgegen, ein eigener Bot-Server ist nicht nötig. Weitere Befehle: /status und /antwort.",
"author": "DeskHub",
"requires": "1.0.30",
"permissions": ["api.public", "hooks.tickets", "ui.menu", "db.migrations"]
}
+19
View File
@@ -0,0 +1,19 @@
<?php
// Community-Bot: nimmt Befehle von Discord und Telegram entgegen und meldet Antworten zurück.
require_once __DIR__ . '/lib.php';
plugin_register_webhook('discord', fn(array $req) => communitybot_discord($req));
plugin_register_webhook('telegram', fn(array $req) => communitybot_telegram($req));
// Mitarbeiter-Antwort (nicht intern) → Mitteilung an den Absender
plugin_add_hook('ticket.replied', function (array $payload) {
if (($payload['author'] ?? '') === 'staff' && empty($payload['internal']) && !empty($payload['ticketId'])) {
communitybot_notify_reply(get_db(), (int) $payload['ticketId']);
}
});
plugin_add_hook('ticket.status_changed', function (array $payload) {
if (!empty($payload['ticketId'])) communitybot_notify_status(get_db(), (int) $payload['ticketId']);
});
// Im Adminportal, nur für Administratoren (die Seite prüft das zusätzlich selbst)
plugin_register_menu('Discord/Telegram', 'community-bot', null, 'admin');