From d543b629a2a7df15c91722ba173265505128c9d2 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Sun, 14 Jul 2024 20:42:33 +0200 Subject: [PATCH] Feat: uncaught errors notification --- .env.docker.example | 11 ++++++-- .env.example | 11 ++++++-- composer.json | 1 + config.php | 13 +++++++++ src/Client.php | 69 +++++++++++++++++++++++++++++++++++++++++++-- src/Logger.php | 2 +- 6 files changed, 100 insertions(+), 7 deletions(-) diff --git a/.env.docker.example b/.env.docker.example index 43c14cf..fbc6723 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -15,7 +15,7 @@ TIMEZONE=UTC # VALUE IN SECOND # DECREASE TO REDUCE LATENCY, INCREASE TO REDUCE LOAD ON SERVER. # SET to 0 to DISABLE -REQUESTS_BULK_INTERVAL=0.5 +REQUESTS_BULK_INTERVAL=0.0 # List of allowed clients. Separate with comma. # Leave blanc, to allow requests from all IP (THIS WILL MAKE API UNSECURE!) @@ -67,4 +67,11 @@ DB_SERIALIZER=serialize # Enable to add cache info about users to database. Disable if you only read data from channels. DB_ENABLE_MIN_DATABASE=0 # Enable file metadata cache -DB_ENABLE_FILE_REFERENCE_DATABASE=0 \ No newline at end of file +DB_ENABLE_FILE_REFERENCE_DATABASE=0 + +ERROR_NOTIFICATION_BOT_TOKEN= +#User id or Chat id or username of the target channel to send error messages. Comma separated +#Example: 123456,@sometestchannel +ERROR_NOTIFICATION_PEERS= +# Program will continue to work after fatal error (not recommended) +RESUME_ON_ERROR=0 \ No newline at end of file diff --git a/.env.example b/.env.example index 2337688..ed6b620 100644 --- a/.env.example +++ b/.env.example @@ -16,7 +16,7 @@ TIMEZONE=UTC # VALUE IN SECOND # DECREASE TO REDUCE LATENCY, INCREASE TO REDUCE LOAD ON SERVER. # SET to 0 to DISABLE -REQUESTS_BULK_INTERVAL=0.5 +REQUESTS_BULK_INTERVAL=0.0 # List of allowed clients. Separate with comma. # Leave blanc, to allow requests from all IP (THIS WILL MAKE API UNSECURE!) @@ -65,4 +65,11 @@ DB_SERIALIZER=serialize # Enable to add cache info about users to database. Disable if you only read data from channels. DB_ENABLE_MIN_DATABASE=0 # Enable file metadata cache -DB_ENABLE_FILE_REFERENCE_DATABASE=0 \ No newline at end of file +DB_ENABLE_FILE_REFERENCE_DATABASE=0 + +ERROR_NOTIFICATION_BOT_TOKEN= +#User id or Chat id or username of the target channel to send error messages. Comma separated +#Example: 123456,@sometestchannel +ERROR_NOTIFICATION_PEERS= +# Program will continue to work after fatal error (not recommended) +RESUME_ON_ERROR=0 \ No newline at end of file diff --git a/composer.json b/composer.json index 3a6da8b..56ca06b 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "php": "^8.1", "ext-json": "*", "ext-mbstring": "*", + "ext-curl": "*", "amphp/http-server": "^v3", "amphp/http": "^v2", "amphp/http-server-router": "^2", diff --git a/config.php b/config.php index 19e886c..501cc72 100644 --- a/config.php +++ b/config.php @@ -9,6 +9,19 @@ $settings = [ 'port' => (int)getenv('SERVER_PORT'), 'real_ip_header' => (string)(getenv('REAL_IP_HEADER') ?? ''), ], + 'error' => [ + 'bot_token' => (string)getenv('ERROR_NOTIFICATION_BOT_TOKEN'), + 'peers' => array_filter( + array_map( + static function(string $peer): string|int { + $peer = trim($peer); + return is_numeric($peer) ? (int)$peer : $peer; + }, + explode(',', (string)getenv('ERROR_NOTIFICATION_PEERS')) + ), + ), + 'resume_on_error' => ((bool)getenv('RESUME_ON_ERROR')) + ], 'telegram' => [ 'app_info' => [ // obtained in https://my.telegram.org 'api_id' => (int)getenv('TELEGRAM_API_ID'), diff --git a/src/Client.php b/src/Client.php index bc6c2a3..68e538b 100644 --- a/src/Client.php +++ b/src/Client.php @@ -10,9 +10,11 @@ use danog\MadelineProto\SettingsAbstract; use InvalidArgumentException; use Psr\Log\LogLevel; use ReflectionProperty; +use Revolt\EventLoop; use RuntimeException; -use TelegramApiServer\EventObservers\EventHandler; use TelegramApiServer\EventObservers\EventObserver; +use function Amp\async; +use function Amp\delay; class Client { @@ -32,6 +34,8 @@ class Client { warning(PHP_EOL . 'Starting MadelineProto...' . PHP_EOL); + $this->setFatalErrorHandler(); + foreach ($sessionFiles as $file) { $sessionName = Files::getSessionName($file); $this->addSession($sessionName); @@ -160,7 +164,8 @@ class Client return $wrapper; } - private static function getSettingsFromArray(string $session, array $settings, SettingsAbstract $settingsObject = new Settings()): SettingsAbstract { + private static function getSettingsFromArray(string $session, array $settings, SettingsAbstract $settingsObject = new Settings()): SettingsAbstract + { foreach ($settings as $key => $value) { if (is_array($value) && $key !== 'proxies') { if ($key === 'db' && isset($value['type'])) { @@ -198,5 +203,65 @@ class Client return $settingsObject; } + private function setFatalErrorHandler(): void + { + + $token = Config::getInstance()->get('error.bot_token'); + $peers = Config::getInstance()->get('error.peers'); + $resume = Config::getInstance()->get('error.resume_on_error'); + + $currentHandler = EventLoop::getErrorHandler(); + EventLoop::setErrorHandler(static fn(\Throwable $e) => self::errorHandler($e, $currentHandler, $token, $peers, $resume)); + } + + private static function errorHandler(\Throwable $e, ?callable $currentHandler, string $token, array $peers, bool $resume): void { + if ($currentHandler) { + $currentHandler($e); + } + if ($e->getPrevious()) { + self::errorHandler($e->getPrevious(), $currentHandler, $token, $peers, true); + } + if ($peers && $token) { + try { + $ch = curl_init("https://api.telegram.org/bot$token/sendMessage"); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json')); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT,1); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + + foreach ($peers as $peer) { + $exceptionArray = Logger::getExceptionAsArray($e); + unset($exceptionArray['previous_exception']); + + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + 'chat_id' => $peer, + 'text' => "```json\n" . + json_encode($exceptionArray, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT) . + "\n```" + , + 'parse_mode' => 'MarkdownV2', + ])); + + $response = curl_exec($ch); + if (curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 200) { + Logger::getInstance()->error('Error notification bot response', [ + 'response' => $response, + 'error_code' => curl_errno($ch), + 'error' => curl_error($ch), + ]); + } + + } + } catch (\Throwable $curlException) { + Logger::getInstance()->error($curlException); + } + } + + if (!$resume) { + throw $e; + } + } } diff --git a/src/Logger.php b/src/Logger.php index bd0bf58..07701de 100644 --- a/src/Logger.php +++ b/src/Logger.php @@ -168,7 +168,7 @@ class Logger extends AbstractLogger 'line' => $exception->getLine(), 'code' => $exception->getCode(), 'backtrace' => array_slice($exception->getTrace(), 0, 3), - 'previous exception' => $exception->getPrevious(), + 'previous_exception' => $exception->getPrevious(), ]; } } \ No newline at end of file