adding telegram notifications
Deploy / deploy (push) Successful in 8s

This commit is contained in:
2026-05-06 18:25:15 +02:00
parent 08df9bfe5c
commit e203aac2f5
5 changed files with 177 additions and 1 deletions
+60
View File
@@ -306,6 +306,23 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius
<small id="tokenSaveStatus" class="ms-2"></small> <small id="tokenSaveStatus" class="ms-2"></small>
</div> </div>
</div> </div>
<div class="card mb-3">
<div class="card-header"><i class="bi bi-telegram me-1"></i>Telegram Notifications</div>
<div class="card-body">
<p class="small text-secondary">Send alerts to a Telegram chat via a bot.</p>
<div class="mb-2">
<label class="form-label">Bot Token</label>
<input type="text" class="form-control" id="telegramBotToken" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11">
</div>
<div class="mb-2">
<label class="form-label">Chat ID</label>
<input type="text" class="form-control" id="telegramChatId" placeholder="-1001234567890">
</div>
<button class="btn btn-primary btn-sm" id="saveTelegramBtn"><i class="bi bi-floppy"></i> Save</button>
<small id="telegramSaveStatus" class="ms-2"></small>
<button class="btn btn-outline-secondary btn-sm ms-2" id="testTelegramBtn"><i class="bi bi-send"></i> Test</button>
</div>
</div>
<div class="card"> <div class="card">
<div class="card-header"><i class="bi bi-info-circle me-1"></i>How to get your user token</div> <div class="card-header"><i class="bi bi-info-circle me-1"></i>How to get your user token</div>
<div class="card-body small"> <div class="card-body small">
@@ -873,6 +890,12 @@ async function loadSettings() {
const tokens = res.tokens || []; const tokens = res.tokens || [];
document.getElementById('allowedTokensInput').value = tokens.join('\n'); document.getElementById('allowedTokensInput').value = tokens.join('\n');
} catch (e) { console.error('load tokens error', e); } } 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 () => { 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 = '<span class="spinner-border spinner-border-sm"></span> 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 = '<i class="bi bi-send"></i> Test';
});
// --- LOGS --- // --- LOGS ---
async function loadLogs(query) { async function loadLogs(query) {
if (!query) { if (!query) {
+21
View File
@@ -84,6 +84,11 @@ class Router
$path === '/config/allowed_tokens' && $method === 'PUT' $path === '/config/allowed_tokens' && $method === 'PUT'
=> $this->updateAllowedTokens(), => $this->updateAllowedTokens(),
$path === '/config/telegram' && $method === 'GET'
=> $this->getTelegramConfig(),
$path === '/config/telegram' && $method === 'PUT'
=> $this->updateTelegramConfig(),
default => throw new \RuntimeException('Not found', 404), default => throw new \RuntimeException('Not found', 404),
}; };
@@ -271,4 +276,20 @@ class Router
return ['status' => 'ingested', 'line' => substr($line, 0, 100)]; 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();
}
} }
+73
View File
@@ -0,0 +1,73 @@
<?php
namespace Jakach\Logging\Notifier;
use Jakach\Logging\Model\Alert;
use Jakach\Logging\Storage\Repository;
class TelegramNotifier
{
private ?string $botToken;
private ?string $chatId;
public function __construct(
private Repository $repo,
) {
$this->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;
}
}
+14 -1
View File
@@ -239,12 +239,25 @@ class Repository
} }
public function setAllowedUserTokens(array $tokens): void 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( $stmt = $this->db->pdo()->prepare(
"INSERT INTO config (key, value) VALUES (?, ?) "INSERT INTO config (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value" 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 --- // --- Rate Limiting ---
+9
View File
@@ -3,6 +3,7 @@
namespace Jakach\Logging\Worker; namespace Jakach\Logging\Worker;
use Jakach\Logging\Model\{LogSource, Alert}; use Jakach\Logging\Model\{LogSource, Alert};
use Jakach\Logging\Notifier\TelegramNotifier;
use Jakach\Logging\RuleEngine\Engine; use Jakach\Logging\RuleEngine\Engine;
use Jakach\Logging\Storage\Repository; use Jakach\Logging\Storage\Repository;
@@ -12,6 +13,7 @@ class Orchestrator
private SocketListener $socketListener; private SocketListener $socketListener;
private Repository $repo; private Repository $repo;
private Engine $engine; private Engine $engine;
private TelegramNotifier $telegram;
private array $sourceMap = []; private array $sourceMap = [];
private bool $running = true; private bool $running = true;
@@ -19,6 +21,7 @@ class Orchestrator
{ {
$this->repo = $repo; $this->repo = $repo;
$this->engine = $engine; $this->engine = $engine;
$this->telegram = new TelegramNotifier($repo);
$this->socketListener = new SocketListener(function (string $line, int $sourceId) { $this->socketListener = new SocketListener(function (string $line, int $sourceId) {
$this->handleLine($line, $sourceId); $this->handleLine($line, $sourceId);
}); });
@@ -31,6 +34,10 @@ class Orchestrator
{ {
$this->loadSources(); $this->loadSources();
if ($this->telegram->isConfigured()) {
fprintf(STDERR, "Telegram notifier configured\n");
}
pcntl_signal(SIGTERM, function () { $this->running = false; }); pcntl_signal(SIGTERM, function () { $this->running = false; });
pcntl_signal(SIGINT, function () { $this->running = false; }); pcntl_signal(SIGINT, function () { $this->running = false; });
@@ -74,6 +81,8 @@ class Orchestrator
); );
fprintf(STDERR, "%s\n", $msg); fprintf(STDERR, "%s\n", $msg);
echo $msg . "\n"; echo $msg . "\n";
$this->telegram->send($alert);
} }
} }