From e203aac2f5e268df6436495eae900218bcdb3761 Mon Sep 17 00:00:00 2001 From: janis steiner Date: Wed, 6 May 2026 18:25:15 +0200 Subject: [PATCH] adding telegram notifications --- public/index.html | 60 +++++++++++++++++++++++++ src/Api/Router.php | 21 +++++++++ src/Notifier/TelegramNotifier.php | 73 +++++++++++++++++++++++++++++++ src/Storage/Repository.php | 15 ++++++- src/Worker/Orchestrator.php | 9 ++++ 5 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/Notifier/TelegramNotifier.php diff --git a/public/index.html b/public/index.html index 74a9f93..ae295fd 100644 --- a/public/index.html +++ b/public/index.html @@ -306,6 +306,23 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius +
+
Telegram Notifications
+
+

Send alerts to a Telegram chat via a bot.

+
+ + +
+
+ + +
+ + + +
+
How to get your user token
@@ -873,6 +890,12 @@ async function loadSettings() { const tokens = res.tokens || []; document.getElementById('allowedTokensInput').value = tokens.join('\n'); } catch (e) { console.error('load tokens error', e); } + + try { + const res = await api('/config/telegram'); + document.getElementById('telegramBotToken').value = res.bot_token || ''; + document.getElementById('telegramChatId').value = res.chat_id || ''; + } catch (e) { console.error('load telegram error', e); } } document.getElementById('saveTokensBtn').addEventListener('click', async () => { @@ -897,6 +920,43 @@ document.getElementById('saveTokensBtn').addEventListener('click', async () => { } }); +document.getElementById('saveTelegramBtn').addEventListener('click', async () => { + const botToken = document.getElementById('telegramBotToken').value.trim(); + const chatId = document.getElementById('telegramChatId').value.trim(); + const statusEl = document.getElementById('telegramSaveStatus'); + statusEl.textContent = 'Saving...'; + statusEl.className = 'ms-2 text-secondary'; + try { + await api('/config/telegram', { + method: 'PUT', + body: JSON.stringify({ bot_token: botToken, chat_id: chatId }), + }); + statusEl.textContent = 'Saved'; + statusEl.className = 'ms-2 text-success'; + toast('Telegram config saved'); + setTimeout(() => { statusEl.textContent = ''; }, 3000); + } catch (e) { + statusEl.textContent = 'Failed'; + statusEl.className = 'ms-2 text-danger'; + toast('Failed to save Telegram config', 'danger'); + } +}); + +document.getElementById('testTelegramBtn').addEventListener('click', async () => { + const btn = document.getElementById('testTelegramBtn'); + btn.disabled = true; + btn.innerHTML = ' Sending...'; + try { + await api('/ingest', { + method: 'POST', + body: JSON.stringify({ line: 'Jakach Logging: test notification from settings', source: 'test' }), + }); + toast('Test alert sent. Check Telegram.'); + } catch (e) { toast('Failed to send test', 'danger'); } + btn.disabled = false; + btn.innerHTML = ' Test'; +}); + // --- LOGS --- async function loadLogs(query) { if (!query) { diff --git a/src/Api/Router.php b/src/Api/Router.php index ccb73c3..1dfc502 100644 --- a/src/Api/Router.php +++ b/src/Api/Router.php @@ -84,6 +84,11 @@ class Router $path === '/config/allowed_tokens' && $method === 'PUT' => $this->updateAllowedTokens(), + $path === '/config/telegram' && $method === 'GET' + => $this->getTelegramConfig(), + $path === '/config/telegram' && $method === 'PUT' + => $this->updateTelegramConfig(), + default => throw new \RuntimeException('Not found', 404), }; @@ -271,4 +276,20 @@ class Router return ['status' => 'ingested', 'line' => substr($line, 0, 100)]; } + + private function getTelegramConfig(): array + { + return [ + 'bot_token' => $this->repo->getConfig('telegram_bot_token', ''), + 'chat_id' => $this->repo->getConfig('telegram_chat_id', ''), + ]; + } + + private function updateTelegramConfig(): array + { + $body = json_decode(file_get_contents('php://input'), true); + $this->repo->setConfig('telegram_bot_token', $body['bot_token'] ?? ''); + $this->repo->setConfig('telegram_chat_id', $body['chat_id'] ?? ''); + return $this->getTelegramConfig(); + } } \ No newline at end of file diff --git a/src/Notifier/TelegramNotifier.php b/src/Notifier/TelegramNotifier.php new file mode 100644 index 0000000..741114e --- /dev/null +++ b/src/Notifier/TelegramNotifier.php @@ -0,0 +1,73 @@ +botToken = $this->repo->getConfig('telegram_bot_token'); + $this->chatId = $this->repo->getConfig('telegram_chat_id'); + } + + public function isConfigured(): bool + { + return !empty($this->botToken) && !empty($this->chatId); + } + + public function send(Alert $alert): bool + { + if (!$this->isConfigured()) { + return false; + } + + $emoji = match ($alert->severity->value) { + 'emergency', 'critical_high', 'critical' => "\xF0\x9F\x94\xA5", + 'critical_low', 'error' => "\xE2\x9D\x8C", + 'warning_high', 'warning' => "\xE2\x9A\xA0\xEF\xB8\x8F", + 'warning_low' => "\xF0\x9F\x93\xA2", + default => "\xE2\x84\xB9\xEF\xB8\x8F", + }; + + $text = sprintf( + "%s *ALERT #%d* [%s]\n", + $emoji, + $alert->id, + strtoupper($alert->severity->value) + ); + $text .= sprintf("*Rule:* %s\n", $alert->ruleName); + $text .= sprintf("*Source:* %s\n", $alert->sourceName ?? '—'); + $text .= sprintf("*Message:* ```\n%s\n```", mb_substr($alert->message, 0, 1000)); + + $url = 'https://api.telegram.org/bot' . $this->botToken . '/sendMessage'; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ + 'chat_id' => $this->chatId, + 'text' => $text, + 'parse_mode' => 'Markdown', + 'disable_web_page_preview' => true, + ])); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + fprintf(STDERR, "Telegram send failed (HTTP %d): %s\n", $httpCode, $response); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Storage/Repository.php b/src/Storage/Repository.php index 0cbb14b..476e6b2 100644 --- a/src/Storage/Repository.php +++ b/src/Storage/Repository.php @@ -239,12 +239,25 @@ class Repository } public function setAllowedUserTokens(array $tokens): void + { + $this->setConfig('allowed_user_tokens', json_encode(array_values($tokens))); + } + + public function getConfig(string $key, mixed $default = null): mixed + { + $stmt = $this->db->pdo()->prepare("SELECT value FROM config WHERE key = ?"); + $stmt->execute([$key]); + $row = $stmt->fetch(); + return $row ? $row['value'] : $default; + } + + public function setConfig(string $key, string $value): void { $stmt = $this->db->pdo()->prepare( "INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value" ); - $stmt->execute(['allowed_user_tokens', json_encode(array_values($tokens))]); + $stmt->execute([$key, $value]); } // --- Rate Limiting --- diff --git a/src/Worker/Orchestrator.php b/src/Worker/Orchestrator.php index 4536049..1c70f16 100644 --- a/src/Worker/Orchestrator.php +++ b/src/Worker/Orchestrator.php @@ -3,6 +3,7 @@ namespace Jakach\Logging\Worker; use Jakach\Logging\Model\{LogSource, Alert}; +use Jakach\Logging\Notifier\TelegramNotifier; use Jakach\Logging\RuleEngine\Engine; use Jakach\Logging\Storage\Repository; @@ -12,6 +13,7 @@ class Orchestrator private SocketListener $socketListener; private Repository $repo; private Engine $engine; + private TelegramNotifier $telegram; private array $sourceMap = []; private bool $running = true; @@ -19,6 +21,7 @@ class Orchestrator { $this->repo = $repo; $this->engine = $engine; + $this->telegram = new TelegramNotifier($repo); $this->socketListener = new SocketListener(function (string $line, int $sourceId) { $this->handleLine($line, $sourceId); }); @@ -31,6 +34,10 @@ class Orchestrator { $this->loadSources(); + if ($this->telegram->isConfigured()) { + fprintf(STDERR, "Telegram notifier configured\n"); + } + pcntl_signal(SIGTERM, function () { $this->running = false; }); pcntl_signal(SIGINT, function () { $this->running = false; }); @@ -74,6 +81,8 @@ class Orchestrator ); fprintf(STDERR, "%s\n", $msg); echo $msg . "\n"; + + $this->telegram->send($alert); } }