From c3270a5c63b80834f03af25a98df68c6a603d29b Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 23 Feb 2020 19:28:42 +0100 Subject: [PATCH] Add native error reporting functions and slash boilerplate --- examples/bot.php | 69 +++++++---- examples/combined_bot.php | 114 ++++++++++++------ examples/downloadRenameBot.php | 69 ++++++----- examples/secret_bot.php | 53 ++++++-- src/danog/MadelineProto/API.php | 31 +++++ .../MadelineProto/AbstractAPIFactory.php | 11 +- src/danog/MadelineProto/EventHandler.php | 9 ++ src/danog/MadelineProto/InternalDoc.php | 92 +++++++++++--- src/danog/MadelineProto/MTProto.php | 85 +++++++++++-- .../MadelineProto/MTProtoTools/Files.php | 4 +- .../MadelineProto/TL/Conversion/BotAPI.php | 3 +- src/danog/MadelineProto/Tools.php | 4 +- src/danog/MadelineProto/Wrappers/Events.php | 28 +++-- src/danog/MadelineProto/Wrappers/Loop.php | 48 ++++---- 14 files changed, 448 insertions(+), 172 deletions(-) diff --git a/examples/bot.php b/examples/bot.php index 53e5f753..d3be484e 100755 --- a/examples/bot.php +++ b/examples/bot.php @@ -19,7 +19,12 @@ * @link https://docs.madelineproto.xyz MadelineProto documentation */ - /* +use danog\MadelineProto\API; +use danog\MadelineProto\EventHandler; +use danog\MadelineProto\Exception; +use danog\MadelineProto\RPCErrorException; + +/* * Various ways to load MadelineProto */ if (\file_exists('vendor/autoload.php')) { @@ -34,13 +39,40 @@ if (\file_exists('vendor/autoload.php')) { /** * Event handler class. */ -class EventHandler extends \danog\MadelineProto\EventHandler +class MyEventHandler extends EventHandler { - public function onUpdateNewChannelMessage($update) + /** + * @var int|string Username or ID of bot admin + */ + const ADMIN = "danogentili"; // Change this + /** + * Get peer(s) where to report errors. + * + * @return int|string|array + */ + public function getReportPeers() { - yield $this->onUpdateNewMessage($update); + return [self::ADMIN]; } - public function onUpdateNewMessage($update) + /** + * Handle updates from supergroups and channels. + * + * @param array $update Update + * + * @return void + */ + public function onUpdateNewChannelMessage(array $update): \Generator + { + return $this->onUpdateNewMessage($update); + } + /** + * Handle updates from users. + * + * @param array $update Update + * + * @return \Generator + */ + public function onUpdateNewMessage(array $update): \Generator { if ($update['message']['_'] === 'messageEmpty' || $update['message']['out'] ?? false) { return; @@ -51,21 +83,13 @@ class EventHandler extends \danog\MadelineProto\EventHandler yield $this->messages->sendMessage(['peer' => $update, 'message' => "$res", 'reply_to_msg_id' => isset($update['message']['id']) ? $update['message']['id'] : null, 'parse_mode' => 'HTML']); if (isset($update['message']['media']) && $update['message']['media']['_'] !== 'messageMediaGame') { yield $this->messages->sendMedia(['peer' => $update, 'message' => $update['message']['message'], 'media' => $update]); - /* '_' => 'inputMediaUploadedDocument', - 'file' => $update, - 'attributes' => [ - ['_' => 'documentAttributeFilename', 'file_name' => 'document.txt'] - ] - ],]);*/ - //yield $this->downloadToDir($update, '/tmp'); } - } catch (\danog\MadelineProto\RPCErrorException $e) { - $this->logger((string) $e, \danog\MadelineProto\Logger::FATAL_ERROR); - } catch (\danog\MadelineProto\Exception $e) { + } catch (RPCErrorException $e) { + $this->report("Surfaced: $e"); + } catch (Exception $e) { if (\stripos($e->getMessage(), 'invalid constructor given') === false) { - $this->logger((string) $e, \danog\MadelineProto\Logger::FATAL_ERROR); + $this->report("Surfaced: $e"); } - //$this->messages->sendMessage(['peer' => '@danogentili', 'message' => $e->getCode().': '.$e->getMessage().PHP_EOL.$e->getTraceAsString()]); } } } @@ -78,11 +102,8 @@ $settings = [ ], ]; -$MadelineProto = new \danog\MadelineProto\API('bot.madeline', $settings); -$MadelineProto->async(true); -$MadelineProto->loop(function () use ($MadelineProto) { - yield $MadelineProto->start(); - yield $MadelineProto->setEventHandler('\EventHandler'); -}); +$MadelineProto = new API('bot.madeline', $settings); -$MadelineProto->loop(); +// Reduce boilerplate with new wrapper method. +// Also initializes error reporting, catching and reporting all errors surfacing from the event loop. +$MadelineProto->startAndLoop(MyEventHandler::class); diff --git a/examples/combined_bot.php b/examples/combined_bot.php index 6bb132b7..9b23fcb0 100755 --- a/examples/combined_bot.php +++ b/examples/combined_bot.php @@ -19,7 +19,12 @@ * @link https://docs.madelineproto.xyz MadelineProto documentation */ -\set_include_path(\get_include_path().':'.\realpath(\dirname(__FILE__).'/MadelineProto/')); +use danog\MadelineProto\API; +use danog\MadelineProto\EventHandler; +use danog\MadelineProto\Exception; +use danog\MadelineProto\Logger; +use danog\MadelineProto\RPCErrorException; +use danog\MadelineProto\Tools; /* * Various ways to load MadelineProto @@ -34,54 +39,89 @@ if (\file_exists(__DIR__.'/vendor/autoload.php')) { } /** - * Combined event handler class. + * Event handler class. */ -class EventHandler extends \danog\MadelineProto\CombinedEventHandler +class MyEventHandler extends EventHandler { - public function onUpdateNewChannelMessage($update, $path) + /** + * @var int|string Username or ID of bot admin + */ + const ADMIN = "danogentili"; // Change this + /** + * Get peer(s) where to report errors. + * + * @return int|string|array + */ + public function getReportPeers() { - yield $this->onUpdateNewMessage($update, $path); + return [self::ADMIN]; } - public function onUpdateNewMessage($update, $path) + /** + * Handle updates from supergroups and channels. + * + * @param array $update Update + * + * @return void + */ + public function onUpdateNewChannelMessage(array $update): \Generator { - if (isset($update['message']['out']) && $update['message']['out']) { + return $this->onUpdateNewMessage($update); + } + /** + * Handle updates from users. + * + * @param array $update Update + * + * @return \Generator + */ + public function onUpdateNewMessage(array $update): \Generator + { + if ($update['message']['_'] === 'messageEmpty' || $update['message']['out'] ?? false) { return; } - $MadelineProto = $this->{$path}; - - if (isset($update['message']['media'])) { - yield $MadelineProto->messages->sendMedia(['peer' => $update, 'message' => $update['message']['message'], 'media' => $update]); - } - $res = \json_encode($update, JSON_PRETTY_PRINT); - if ($res == '') { - $res = \var_export($update, true); - } - yield $MadelineProto->sleep(3); try { - yield $MadelineProto->messages->sendMessage(['peer' => $update, 'message' => "$res\n\nDopo 3 secondi, in modo asincrono", 'reply_to_msg_id' => isset($update['message']['id']) ? $update['message']['id'] : null, 'parse_mode' => 'HTML']); //'entities' => [['_' => 'messageEntityPre', 'offset' => 0, 'length' => strlen($res), 'language' => 'json']]]); - } catch (\danog\MadelineProto\RPCErrorException $e) { - \danog\MadelineProto\Logger::log((string) $e, \danog\MadelineProto\Logger::FATAL_ERROR); - } catch (\danog\MadelineProto\Exception $e) { - \danog\MadelineProto\Logger::log((string) $e, \danog\MadelineProto\Logger::FATAL_ERROR); - //$MadelineProto->messages->sendMessage(['peer' => '@danogentili', 'message' => $e->getCode().': '.$e->getMessage().PHP_EOL.$e->getTraceAsString()]); + yield $this->messages->sendMessage(['peer' => $update, 'message' => "$res", 'reply_to_msg_id' => isset($update['message']['id']) ? $update['message']['id'] : null, 'parse_mode' => 'HTML']); + if (isset($update['message']['media']) && $update['message']['media']['_'] !== 'messageMediaGame') { + yield $this->messages->sendMedia(['peer' => $update, 'message' => $update['message']['message'], 'media' => $update]); + } + } catch (RPCErrorException $e) { + $this->report("Surfaced: $e"); + } catch (Exception $e) { + if (\stripos($e->getMessage(), 'invalid constructor given') === false) { + $this->report("Surfaced: $e"); + } } } } -$settings = ['logger' => ['logger_level' => 5]]; -$CombinedMadelineProto = new \danog\MadelineProto\CombinedAPI('combined_session.madeline', ['bot.madeline' => $settings, 'user.madeline' => $settings]); +$MadelineProtos = []; +foreach ([ + 'bot.madeline' => 'Bot Login', + 'user.madeline' => 'Userbot login', + 'user2.madeline' => 'Userbot login (2)' +] as $session => $message) { + Logger::log($message, Logger::WARNING); + $MadelineProto = new API($session); + $MadelineProto->async(true); + $MadelineProto->loop(function () use ($MadelineProto) { + yield $MadelineProto->start(); + yield $MadelineProto->setEventHandler(MyEventHandler::class); + }); + $MadelineProtos []= $MadelineProto->loopFork(); +} -\danog\MadelineProto\Logger::log('Bot login', \danog\MadelineProto\Logger::WARNING); -$CombinedMadelineProto->instances['bot.madeline']->start(); - -\danog\MadelineProto\Logger::log('Userbot login'); -$CombinedMadelineProto->instances['user.madeline']->start(); - -$CombinedMadelineProto->setEventHandler('\EventHandler'); -$CombinedMadelineProto->loop(); - -$CombinedMadelineProto->async(true); -$CombinedMadelineProto->setEventHandler('\EventHandler'); -$CombinedMadelineProto->loop(); +do { + $thrown = false; + try { + Tools::wait(Tools::all($MadelineProtos)); + } catch (\Throwable $e) { + $thrown = true; + try { + $MadelineProto->report("Surfaced: $e"); + } catch (\Throwable $e) { + $MadelineProto->logger((string) $e, \danog\MadelineProto\Logger::FATAL_ERROR); + } + } +} while ($thrown); diff --git a/examples/downloadRenameBot.php b/examples/downloadRenameBot.php index e3de5930..6d5da954 100755 --- a/examples/downloadRenameBot.php +++ b/examples/downloadRenameBot.php @@ -21,7 +21,6 @@ use Amp\Http\Server\HttpServer; use danog\MadelineProto\API; -use danog\MadelineProto\Logger; use danog\MadelineProto\MTProtoTools\Files; use danog\MadelineProto\RPCErrorException; use danog\MadelineProto\Tools; @@ -49,7 +48,21 @@ class EventHandler extends \danog\MadelineProto\EventHandler "Usage: `https://example.com file name.ext`\n\n". "I can also rename Telegram files, just send me any file and I will rename it!\n\n". "Max 1.5GB, parallel upload and download powered by @MadelineProto."; - const ADMIN = 'danogentili'; + + /** + * @var int|string Username or ID of bot admin + */ + const ADMIN = 'danogentili'; // Change this + + /** + * Get peer(s) where to report errors. + * + * @return int|string|array + */ + public function getReportPeers() + { + return [self::ADMIN]; + } /** * Whether to allow uploads. @@ -65,26 +78,32 @@ class EventHandler extends \danog\MadelineProto\EventHandler /** * Constructor. * - * @param API $API API + * @param ?API $API API */ - public function __construct($API) + public function __construct(?API $API) { $this->UPLOAD = \class_exists(HttpServer::class); parent::__construct($API); } - public function onUpdateNewChannelMessage($update) + /** + * Handle updates from channels and supergroups. + * + * @param array $update Update + * + * @return \Generator + */ + public function onUpdateNewChannelMessage(array $update) { //yield $this->onUpdateNewMessage($update); } - public function report(string $message) - { - try { - $this->messages->sendMessage(['peer' => self::ADMIN, 'message' => $message]); - } catch (\Throwable $e) { - $this->logger("While reporting: $e", Logger::FATAL_ERROR); - } - } - public function onUpdateNewMessage($update) + /** + * Handle updates from users. + * + * @param array $update Update + * + * @return \Generator + */ + public function onUpdateNewMessage(array $update): \Generator { if ($update['message']['out'] ?? false) { return; @@ -111,7 +130,7 @@ class EventHandler extends \danog\MadelineProto\EventHandler unset($this->states[$peerId]); $update = Files::extractBotAPIFile(yield $this->MTProtoToBotAPI($update)); $file = [$update['file_size'], $update['mime_type']]; - \var_dump($update['file_id'].'.'.Tools::base64urlEncode(json_encode($file))."/".$update['file_name']); + \var_dump($update['file_id'].'.'.Tools::base64urlEncode(\json_encode($file))."/".$update['file_name']); return; } yield $this->messages->sendMessage(['peer' => $peerId, 'message' => 'Give me a new name for this file: ', 'reply_to_msg_id' => $messageId]); @@ -220,19 +239,7 @@ $settings = [ ]; $MadelineProto = new \danog\MadelineProto\API(($argv[1] ?? 'bot').'.madeline', $settings); -$MadelineProto->async(true); -while (true) { - try { - $MadelineProto->loop(function () use ($MadelineProto) { - yield $MadelineProto->start(); - yield $MadelineProto->setEventHandler('\EventHandler'); - }); - $MadelineProto->loop(); - } catch (\Throwable $e) { - try { - $MadelineProto->logger("Surfaced: $e"); - $MadelineProto->getEventHandler(['async' => false])->report("Surfaced: $e"); - } catch (\Throwable $e) { - } - } -} + +// Reduce boilerplate with new wrapper method. +// Also initializes error reporting, catching and reporting all errors surfacing from the event loop. +$MadelineProto->startAndLoop(MyEventHandler::class); diff --git a/examples/secret_bot.php b/examples/secret_bot.php index e4c3712e..6eafdc26 100755 --- a/examples/secret_bot.php +++ b/examples/secret_bot.php @@ -33,11 +33,48 @@ if (\file_exists(__DIR__.'/../vendor/autoload.php')) { include 'madeline.php'; } -class EventHandler extends \danog\MadelineProto\EventHandler +class SecretHandler extends \danog\MadelineProto\EventHandler { private $sent = [-440592694 => true]; - - public function onUpdateNewEncryptedMessage($update) + public function __construct($API) + { + parent::__construct($API); + $this->sent = []; + } + /** + * @var int|string Username or ID of bot admin + */ + const ADMIN = "danogentili"; // Change this + /** + * Get peer(s) where to report errors. + * + * @return int|string|array + */ + public function getReportPeers() + { + return [self::ADMIN]; + } + /** + * Handle updates from users. + * + * @param array $update Update + * + * @return \Generator + */ + public function onUpdateNewMessage(array $update): \Generator + { + if ($update['message']['message'] === 'request') { + yield $this->requestSecretChat($update); + } + } + /** + * Handle secret chat messages. + * + * @param array $update Update + * + * @return \Generator + */ + public function onUpdateNewEncryptedMessage(array $update): \Generator { try { if (isset($update['message']['decrypted_message']['media'])) { @@ -72,14 +109,14 @@ class EventHandler extends \danog\MadelineProto\EventHandler $secret_media['voice'] = ['peer' => $update, 'file' => 'tests/mosconi.mp3', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/faust.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => \mime_content_type('tests/mosconi.mp3'), 'caption' => 'test', 'file_name' => 'mosconi.mp3', 'size' => \filesize('tests/mosconi.mp3'), 'attributes' => [['_' => 'documentAttributeAudio', 'voice' => true, 'duration' => 1, 'title' => 'AH NON LO SO IO', 'performer' => 'IL DIO GERMANO MOSCONI']]]]]; foreach ($secret_media as $type => $smessage) { - yield $this->messages->sendEncryptedFile($smessage); + $promises = $this->messages->sendEncryptedFile($smessage); } + yield $promises; $i = 0; while ($i < 10) { $this->logger("SENDING MESSAGE $i TO ".$update['message']['chat_id']); // You can also use the sendEncrypted parameter for more options in secret chats - //yield $this->messages->sendEncrypted(['peer' => $update, 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => (string) ($i++)]]); yield $this->messages->sendMessage(['peer' => $update, 'message' => (string) ($i++)]); } $this->sent[$update['message']['chat_id']] = true; @@ -102,7 +139,5 @@ $settings = \json_decode(\getenv('MTPROTO_SETTINGS'), true) ?: []; $MadelineProto = new \danog\MadelineProto\API('s.madeline', $settings); -$MadelineProto->start(); -$MadelineProto->setEventHandler('\EventHandler'); -$MadelineProto->async(true); -$MadelineProto->loop(); +// Reduce boilerplate with new wrapper method +$MadelineProto->startAndLoop(MyEventHandler::class); diff --git a/src/danog/MadelineProto/API.php b/src/danog/MadelineProto/API.php index 6d1ec363..918209c3 100644 --- a/src/danog/MadelineProto/API.php +++ b/src/danog/MadelineProto/API.php @@ -405,4 +405,35 @@ class API extends InternalDoc return $wrote; })()); } + + /** + * Start MadelineProto and the event handler (enables async). + * + * Also initializes error reporting, catching and reporting all errors surfacing from the event loop. + * + * @param string $eventHandler Event handler class name + * + * @return void + */ + public function startAndLoop(string $eventHandler): void + { + $this->async(true); + do { + $thrown = false; + try { + $this->loop(function () use ($eventHandler) { + yield $this->start(); + yield $this->setEventHandler($eventHandler); + }); + $this->loop(); + } catch (\Throwable $e) { + $thrown = true; + try { + $this->report("Surfaced: $e"); + } catch (\Throwable $e) { + $this->logger((string) $e, Logger::FATAL_ERROR); + } + } + } while ($thrown); + } } diff --git a/src/danog/MadelineProto/AbstractAPIFactory.php b/src/danog/MadelineProto/AbstractAPIFactory.php index a5405dd0..765122ea 100644 --- a/src/danog/MadelineProto/AbstractAPIFactory.php +++ b/src/danog/MadelineProto/AbstractAPIFactory.php @@ -97,15 +97,15 @@ abstract class AbstractAPIFactory extends AsyncConstruct public function __call(string $name, array $arguments) { $yielded = Tools::call($this->__call_async($name, $arguments)); - $async = !$this->lua && ((is_array(\end($arguments)) ? \end($arguments) : [])['async'] ?? ($this->async && $name !== 'loop')); + $async = !$this->lua && ((\is_array(\end($arguments)) ? \end($arguments) : [])['async'] ?? ($this->async && $name !== 'loop')); if ($async) { return $yielded; } + $yielded = Tools::wait($yielded); if (!$this->lua) { - return Tools::wait($yielded); + return $yielded; } try { - $yielded = Tools::wait($yielded); Lua::convertObjects($yielded); return $yielded; } catch (\Throwable $e) { @@ -128,9 +128,6 @@ abstract class AbstractAPIFactory extends AsyncConstruct yield from $this->initAsynchronously(); $this->API->logger->logger('Finished init asynchronously'); } - if (Magic::isFork() && !Magic::$processed_fork) { - throw new Exception('Forking not supported, use async logic, instead: https://docs.madelineproto.xyz/docs/ASYNC.html'); - } if (!$this->API) { throw new Exception('API did not init!'); } @@ -160,7 +157,7 @@ abstract class AbstractAPIFactory extends AsyncConstruct $args = isset($arguments[0]) && \is_array($arguments[0]) ? $arguments[0] : []; return yield from $this->API->methodCallAsyncRead($name, $args, $aargs); } - $res = yield $this->methods[$lower_name](...$arguments); + $res = $this->methods[$lower_name](...$arguments); return $res instanceof \Generator ? yield from $res : yield $res; } /** diff --git a/src/danog/MadelineProto/EventHandler.php b/src/danog/MadelineProto/EventHandler.php index e84c5f1f..be46b709 100644 --- a/src/danog/MadelineProto/EventHandler.php +++ b/src/danog/MadelineProto/EventHandler.php @@ -41,4 +41,13 @@ class EventHandler extends InternalDoc $this->{$namespace} = new APIFactory($namespace, $this->API, $this->async); } } + /** + * Get peers where to send error reports. + * + * @return array|string|int + */ + public function getReportPeers() + { + return []; + } } diff --git a/src/danog/MadelineProto/InternalDoc.php b/src/danog/MadelineProto/InternalDoc.php index 5a042df8..446c2827 100644 --- a/src/danog/MadelineProto/InternalDoc.php +++ b/src/danog/MadelineProto/InternalDoc.php @@ -4215,6 +4215,37 @@ class InternalDoc extends APIFactory { return $this->__call(__FUNCTION__, [$extra]); } + /** + * Check if has report peers. + * + * @return boolean + */ + public function hasReportPeers(): bool + { + return $this->API->hasReportPeers(); + } + /** + * Set peer(s) where to send errors occurred in the event loop. + * + * @param int|string $userOrId Username(s) or peer ID(s) + * + * @return \Generator + */ + public function setReportPeers($userOrId, array $extra = []) + { + return $this->__call(__FUNCTION__, [$userOrId, $extra]); + } + /** + * Report an error to the previously set peer. + * + * @param string $message Error to report + * + * @return \Generator + */ + public function report(string $message, array $extra = []) + { + return $this->__call(__FUNCTION__, [$message, $extra]); + } /** * Call method and wait asynchronously for response. * @@ -5293,6 +5324,17 @@ class InternalDoc extends APIFactory { return \danog\MadelineProto\MTProto::rleEncode($string); } + /** + * Inflate stripped photosize to full JPG payload. + * + * @param string $stripped Stripped photosize + * + * @return string JPG payload + */ + public function inflateStripped(string $stripped): string + { + return \danog\MadelineProto\MTProto::inflateStripped($stripped); + } /** * Get final element of array. * @@ -5460,6 +5502,17 @@ class InternalDoc extends APIFactory { $this->API->setEventHandler($event_handler); } + /** + * Unset event handler. + * + * @param bool $disableUpdateHandling Whether to also disable internal update handling (will cause errors, otherwise will simply use the NOOP handler) + * + * @return void + */ + public function unsetEventHandler(bool $disableUpdateHandling = false): void + { + $this->API->unsetEventHandler($disableUpdateHandling); + } /** * Get event handler. * @@ -5469,6 +5522,15 @@ class InternalDoc extends APIFactory { return $this->API->getEventHandler(); } + /** + * Check if an event handler instance is present. + * + * @return boolean + */ + public function hasEventHandler(): bool + { + return $this->API->hasEventHandler(); + } /** * Set webhook update handler. * @@ -5591,17 +5653,6 @@ class InternalDoc extends APIFactory { return $this->__call(__FUNCTION__, [$params, $extra]); } - /** - * Set loop callback (DEPRECATED). - * - * @param callable $callback Callback - * - * @return void - */ - public function setLoopCallback($callback): void - { - $this->API->setLoopCallback($callback); - } /** * Start MadelineProto's update handling loop, or run the provided async callable. * @@ -5623,15 +5674,22 @@ class InternalDoc extends APIFactory $this->API->stop(); } /** - * Start MadelineProto's update handling loop in background, or run the provided async callable. + * Restart update loop. * - * @param callable $callback Async callable to run - * - * @return mixed + * @return void */ - public function loopFork($callback = null): void + public function restart(): void { - $this->API->loopFork($callback); + $this->API->restart(); + } + /** + * Start MadelineProto's update handling loop in background. + * + * @return Promise + */ + public function loopFork(array $extra = []) + { + return $this->__call(__FUNCTION__, [$extra]); } /** * Close connection with client, connected via web. diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index a25e720e..c42c49e6 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -493,12 +493,14 @@ class MTProto extends AsyncConstruct implements TLCallback 'referenceDatabase', 'minDatabase', 'channel_participants', + // Misc caching 'dialog_params', 'last_stored', 'qres', 'supportUser', 'tos', + // Event handler 'event_handler', 'event_handler_instance', @@ -506,34 +508,47 @@ class MTProto extends AsyncConstruct implements TLCallback 'updates', 'updates_key', 'hook_url', + // Web login template 'web_template', + // Settings 'settings', 'config', + // Authorization keys 'datacenter', + // Authorization state 'authorization', 'authorized', 'authorized_dc', + // Authorization cache 'rsa_keys', 'dh_config', + // Update state 'got_state', 'channels_state', 'msg_ids', + // Version 'v', + // TL 'TL', + // Secret chats 'secret_chats', 'temp_requested_secret_chats', 'temp_rekeyed_secret_chats', + // Object storage 'storage', + + // Report URI + 'reportDest' ]; } /** @@ -874,7 +889,7 @@ class MTProto extends AsyncConstruct implements TLCallback $this->parseSettings(\array_replace_recursive($this->settings, $backtrace[2]['args'][1])); } } - if (($this->settings['tl_schema']['src']['botAPI'] ?? '') !== __DIR__ . '/TL_botAPI.tl') { + if (($this->settings['tl_schema']['src']['botAPI'] ?? '') !== __DIR__.'/TL_botAPI.tl') { unset($this->v); } if (!\file_exists($this->settings['tl_schema']['src']['telegram'])) { @@ -1050,7 +1065,7 @@ class MTProto extends AsyncConstruct implements TLCallback $lang_pack = 'android'; } // Detect app version - $app_version = self::RELEASE . ' (' . self::V . ', ' . Magic::$revision . ')'; + $app_version = self::RELEASE.' ('.self::V.', '.Magic::$revision.')'; if (($settings['app_info']['api_id'] ?? 0) === 6) { // TG DEV NOTICE: these app info spoofing measures were implemented for NON-MALICIOUS purposes. // All accounts registered with a custom API ID require manual verification through recover@telegram.org, to avoid instant permabans. @@ -1169,16 +1184,16 @@ class MTProto extends AsyncConstruct implements TLCallback 'layer' => 109, // layer version 'src' => [ - 'mtproto' => __DIR__ . '/TL_mtproto_v1.tl', + 'mtproto' => __DIR__.'/TL_mtproto_v1.tl', // mtproto TL scheme - 'telegram' => __DIR__ . '/TL_telegram_v109.tl', + 'telegram' => __DIR__.'/TL_telegram_v109.tl', // telegram TL scheme - 'secret' => __DIR__ . '/TL_secret.tl', + 'secret' => __DIR__.'/TL_secret.tl', // secret chats TL scheme - 'calls' => __DIR__ . '/TL_calls.tl', + 'calls' => __DIR__.'/TL_calls.tl', // calls TL scheme //'td' => __DIR__.'/TL_td.tl', // telegram-cli TL scheme - 'botAPI' => __DIR__ . '/TL_botAPI.tl', + 'botAPI' => __DIR__.'/TL_botAPI.tl', ], ], 'logger' => [ // Logger settings @@ -1192,7 +1207,7 @@ class MTProto extends AsyncConstruct implements TLCallback * $message is an array containing the messages the log, $level, is the logging level */ // write to - 'logger_param' => Magic::$script_cwd . '/MadelineProto.log', + 'logger_param' => Magic::$script_cwd.'/MadelineProto.log', 'logger' => PHP_SAPI === 'cli' ? 3 : 2, // overwrite previous setting and echo logs 'logger_level' => Logger::VERBOSE, @@ -1651,6 +1666,58 @@ class MTProto extends AsyncConstruct implements TLCallback } return $this->authorization['user']; } + /** + * IDs of peers where to report errors. + * + * @var int[] + */ + private $reportDest = []; + /** + * Check if has report peers. + * + * @return boolean + */ + public function hasReportPeers(): bool + { + return (bool) $this->reportDest; + } + /** + * Set peer(s) where to send errors occurred in the event loop. + * + * @param int|string $userOrId Username(s) or peer ID(s) + * + * @return \Generator + */ + public function setReportPeers($userOrId): \Generator + { + if (!(\is_array($userOrId) && !isset($userOrId['_']) && !isset($userOrId['id']))) { + $userOrId = [$userOrId]; + } + foreach ($userOrId as &$peer) { + $peer = yield from $this->getInfo($userOrId)['bot_api_id']; + } + $this->reportDest = $userOrId; + } + /** + * Report an error to the previously set peer. + * + * @param string $message Error to report + * + * @return \Generator + */ + public function report(string $message): \Generator + { + if (!$this->reportDest) { + return; + } + foreach ($this->reportDest as $id) { + try { + yield from $this->methodCallAsyncRead('messages.sendMessage', ['peer' => $id, 'message' => $message]); + } catch (\Throwable $e) { + $this->logger("While reporting to $id: $e", Logger::FATAL_ERROR); + } + } + } /** * Called right before serialization of method starts. * @@ -1723,7 +1790,7 @@ class MTProto extends AsyncConstruct implements TLCallback */ public function __debugInfo(): array { - return ['MadelineProto instance ' . \spl_object_hash($this)]; + return ['MadelineProto instance '.\spl_object_hash($this)]; } const ALL_MIMES = ['webp' => [0 => 'image/webp'], 'png' => [0 => 'image/png', 1 => 'image/x-png'], 'bmp' => [0 => 'image/bmp', 1 => 'image/x-bmp', 2 => 'image/x-bitmap', 3 => 'image/x-xbitmap', 4 => 'image/x-win-bitmap', 5 => 'image/x-windows-bmp', 6 => 'image/ms-bmp', 7 => 'image/x-ms-bmp', 8 => 'application/bmp', 9 => 'application/x-bmp', 10 => 'application/x-win-bitmap'], 'gif' => [0 => 'image/gif'], 'jpeg' => [0 => 'image/jpeg', 1 => 'image/pjpeg'], 'xspf' => [0 => 'application/xspf+xml'], 'vlc' => [0 => 'application/videolan'], 'wmv' => [0 => 'video/x-ms-wmv', 1 => 'video/x-ms-asf'], 'au' => [0 => 'audio/x-au'], 'ac3' => [0 => 'audio/ac3'], 'flac' => [0 => 'audio/x-flac'], 'ogg' => [0 => 'audio/ogg', 1 => 'video/ogg', 2 => 'application/ogg'], 'kmz' => [0 => 'application/vnd.google-earth.kmz'], 'kml' => [0 => 'application/vnd.google-earth.kml+xml'], 'rtx' => [0 => 'text/richtext'], 'rtf' => [0 => 'text/rtf'], 'jar' => [0 => 'application/java-archive', 1 => 'application/x-java-application', 2 => 'application/x-jar'], 'zip' => [0 => 'application/x-zip', 1 => 'application/zip', 2 => 'application/x-zip-compressed', 3 => 'application/s-compressed', 4 => 'multipart/x-zip'], '7zip' => [0 => 'application/x-compressed'], 'xml' => [0 => 'application/xml', 1 => 'text/xml'], 'svg' => [0 => 'image/svg+xml'], '3g2' => [0 => 'video/3gpp2'], '3gp' => [0 => 'video/3gp', 1 => 'video/3gpp'], 'mp4' => [0 => 'video/mp4'], 'm4a' => [0 => 'audio/x-m4a'], 'f4v' => [0 => 'video/x-f4v'], 'flv' => [0 => 'video/x-flv'], 'webm' => [0 => 'video/webm'], 'aac' => [0 => 'audio/x-acc'], 'm4u' => [0 => 'application/vnd.mpegurl'], 'pdf' => [0 => 'application/pdf', 1 => 'application/octet-stream'], 'pptx' => [0 => 'application/vnd.openxmlformats-officedocument.presentationml.presentation'], 'ppt' => [0 => 'application/powerpoint', 1 => 'application/vnd.ms-powerpoint', 2 => 'application/vnd.ms-office', 3 => 'application/msword'], 'docx' => [0 => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], 'xlsx' => [0 => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 1 => 'application/vnd.ms-excel'], 'xl' => [0 => 'application/excel'], 'xls' => [0 => 'application/msexcel', 1 => 'application/x-msexcel', 2 => 'application/x-ms-excel', 3 => 'application/x-excel', 4 => 'application/x-dos_ms_excel', 5 => 'application/xls', 6 => 'application/x-xls'], 'xsl' => [0 => 'text/xsl'], 'mpeg' => [0 => 'video/mpeg'], 'mov' => [0 => 'video/quicktime'], 'avi' => [0 => 'video/x-msvideo', 1 => 'video/msvideo', 2 => 'video/avi', 3 => 'application/x-troff-msvideo'], 'movie' => [0 => 'video/x-sgi-movie'], 'log' => [0 => 'text/x-log'], 'txt' => [0 => 'text/plain'], 'css' => [0 => 'text/css'], 'html' => [0 => 'text/html'], 'wav' => [0 => 'audio/x-wav', 1 => 'audio/wave', 2 => 'audio/wav'], 'xhtml' => [0 => 'application/xhtml+xml'], 'tar' => [0 => 'application/x-tar'], 'tgz' => [0 => 'application/x-gzip-compressed'], 'psd' => [0 => 'application/x-photoshop', 1 => 'image/vnd.adobe.photoshop'], 'exe' => [0 => 'application/x-msdownload'], 'js' => [0 => 'application/x-javascript'], 'mp3' => [0 => 'audio/mpeg', 1 => 'audio/mpg', 2 => 'audio/mpeg3', 3 => 'audio/mp3'], 'rar' => [0 => 'application/x-rar', 1 => 'application/rar', 2 => 'application/x-rar-compressed'], 'gzip' => [0 => 'application/x-gzip'], 'hqx' => [0 => 'application/mac-binhex40', 1 => 'application/mac-binhex', 2 => 'application/x-binhex40', 3 => 'application/x-mac-binhex40'], 'cpt' => [0 => 'application/mac-compactpro'], 'bin' => [0 => 'application/macbinary', 1 => 'application/mac-binary', 2 => 'application/x-binary', 3 => 'application/x-macbinary'], 'oda' => [0 => 'application/oda'], 'ai' => [0 => 'application/postscript'], 'smil' => [0 => 'application/smil'], 'mif' => [0 => 'application/vnd.mif'], 'wbxml' => [0 => 'application/wbxml'], 'wmlc' => [0 => 'application/wmlc'], 'dcr' => [0 => 'application/x-director'], 'dvi' => [0 => 'application/x-dvi'], 'gtar' => [0 => 'application/x-gtar'], 'php' => [0 => 'application/x-httpd-php', 1 => 'application/php', 2 => 'application/x-php', 3 => 'text/php', 4 => 'text/x-php', 5 => 'application/x-httpd-php-source'], 'swf' => [0 => 'application/x-shockwave-flash'], 'sit' => [0 => 'application/x-stuffit'], 'z' => [0 => 'application/x-compress'], 'mid' => [0 => 'audio/midi'], 'aif' => [0 => 'audio/x-aiff', 1 => 'audio/aiff'], 'ram' => [0 => 'audio/x-pn-realaudio'], 'rpm' => [0 => 'audio/x-pn-realaudio-plugin'], 'ra' => [0 => 'audio/x-realaudio'], 'rv' => [0 => 'video/vnd.rn-realvideo'], 'jp2' => [0 => 'image/jp2', 1 => 'video/mj2', 2 => 'image/jpx', 3 => 'image/jpm'], 'tiff' => [0 => 'image/tiff'], 'eml' => [0 => 'message/rfc822'], 'pem' => [0 => 'application/x-x509-user-cert', 1 => 'application/x-pem-file'], 'p10' => [0 => 'application/x-pkcs10', 1 => 'application/pkcs10'], 'p12' => [0 => 'application/x-pkcs12'], 'p7a' => [0 => 'application/x-pkcs7-signature'], 'p7c' => [0 => 'application/pkcs7-mime', 1 => 'application/x-pkcs7-mime'], 'p7r' => [0 => 'application/x-pkcs7-certreqresp'], 'p7s' => [0 => 'application/pkcs7-signature'], 'crt' => [0 => 'application/x-x509-ca-cert', 1 => 'application/pkix-cert'], 'crl' => [0 => 'application/pkix-crl', 1 => 'application/pkcs-crl'], 'pgp' => [0 => 'application/pgp'], 'gpg' => [0 => 'application/gpg-keys'], 'rsa' => [0 => 'application/x-pkcs7'], 'ics' => [0 => 'text/calendar'], 'zsh' => [0 => 'text/x-scriptzsh'], 'cdr' => [0 => 'application/cdr', 1 => 'application/coreldraw', 2 => 'application/x-cdr', 3 => 'application/x-coreldraw', 4 => 'image/cdr', 5 => 'image/x-cdr', 6 => 'zz-application/zz-winassoc-cdr'], 'wma' => [0 => 'audio/x-ms-wma'], 'vcf' => [0 => 'text/x-vcard'], 'srt' => [0 => 'text/srt'], 'vtt' => [0 => 'text/vtt'], 'ico' => [0 => 'image/x-icon', 1 => 'image/x-ico', 2 => 'image/vnd.microsoft.icon'], 'csv' => [0 => 'text/x-comma-separated-values', 1 => 'text/comma-separated-values', 2 => 'application/vnd.msexcel'], 'json' => [0 => 'application/json', 1 => 'text/json']]; } diff --git a/src/danog/MadelineProto/MTProtoTools/Files.php b/src/danog/MadelineProto/MTProtoTools/Files.php index 9b6380de..d21edc3f 100644 --- a/src/danog/MadelineProto/MTProtoTools/Files.php +++ b/src/danog/MadelineProto/MTProtoTools/Files.php @@ -79,7 +79,7 @@ trait Files } elseif (\is_array($file)) { return yield from $this->uploadFromTgfile($file, $cb, $encrypted); } - if (\is_resource($file) || (is_object($file) && $file instanceof InputStream)) { + if (\is_resource($file) || (\is_object($file) && $file instanceof InputStream)) { return yield from $this->uploadFromStream($file, 0, '', $fileName, $cb, $encrypted); } if (!$this->settings['upload']['allow_automatic_upload']) { @@ -212,7 +212,7 @@ trait Files yield $stream->seek(0, \SEEK_END); $size = yield $stream->tell(); yield $stream->seek(0); - } else if (!$size) { + } elseif (!$size) { $this->logger->logger("No content length for stream, caching first"); $body = $stream; $stream = new BlockingFile(\fopen('php://temp', 'r+b'), 'php://temp', 'r+b'); diff --git a/src/danog/MadelineProto/TL/Conversion/BotAPI.php b/src/danog/MadelineProto/TL/Conversion/BotAPI.php index ab95135c..b02b79ed 100644 --- a/src/danog/MadelineProto/TL/Conversion/BotAPI.php +++ b/src/danog/MadelineProto/TL/Conversion/BotAPI.php @@ -240,7 +240,8 @@ trait BotAPI if (isset($data['fwd_from']['channel_id'])) { try { $newd['forward_from_chat'] = yield from $this->getPwrChat(PeerHandler::toSupergroup($data['fwd_from']['channel_id'])); - } catch (\Throwable $e) {} + } catch (\Throwable $e) { + } } if (isset($data['fwd_from']['date'])) { $newd['forward_date'] = $data['fwd_from']['date']; diff --git a/src/danog/MadelineProto/Tools.php b/src/danog/MadelineProto/Tools.php index ff7f8d22..308bca26 100644 --- a/src/danog/MadelineProto/Tools.php +++ b/src/danog/MadelineProto/Tools.php @@ -739,10 +739,10 @@ trait Tools return $new; } /** - * Inflate stripped photosize to full JPG payload + * Inflate stripped photosize to full JPG payload. * * @param string $stripped Stripped photosize - * + * * @return string JPG payload */ public static function inflateStripped(string $stripped): string diff --git a/src/danog/MadelineProto/Wrappers/Events.php b/src/danog/MadelineProto/Wrappers/Events.php index bd69a452..a5fc07c4 100644 --- a/src/danog/MadelineProto/Wrappers/Events.php +++ b/src/danog/MadelineProto/Wrappers/Events.php @@ -19,7 +19,7 @@ namespace danog\MadelineProto\Wrappers; -use EventHandler; +use danog\MadelineProto\EventHandler; /** * Event handler. @@ -35,7 +35,7 @@ trait Events /** * Event handler instance. * - * @var \danog\MadelineProto\EventHandler + * @var EventHandler */ private $event_handler_instance; /** @@ -78,6 +78,7 @@ trait Events $this->event_handler_methods[$method_name] = [$this->event_handler_instance, $method]; } } + $this->setReportPeers($this->event_handler_instance->getReportPeers()); $this->settings['updates']['callback'] = [$this, 'eventUpdateHandler']; $this->settings['updates']['handle_updates'] = true; $this->settings['updates']['run_callback'] = true; @@ -86,12 +87,12 @@ trait Events } } /** - * Unset event handler. - * - * @param bool $disableUpdateHandling Whether to also disable internal update handling (will cause errors, otherwise will simply use the NOOP handler) - * - * @return void - */ + * Unset event handler. + * + * @param bool $disableUpdateHandling Whether to also disable internal update handling (will cause errors, otherwise will simply use the NOOP handler) + * + * @return void + */ public function unsetEventHandler(bool $disableUpdateHandling = false): void { $this->event_handler = null; @@ -107,10 +108,19 @@ trait Events * * @return EventHandler */ - public function getEventHandler(): \danog\MadelineProto\EventHandler + public function getEventHandler(): EventHandler { return $this->event_handler_instance; } + /** + * Check if an event handler instance is present. + * + * @return boolean + */ + public function hasEventHandler(): bool + { + return isset($this->event_handler_instance); + } /** * Event update handler. * diff --git a/src/danog/MadelineProto/Wrappers/Loop.php b/src/danog/MadelineProto/Wrappers/Loop.php index f92ab2c0..ca955873 100644 --- a/src/danog/MadelineProto/Wrappers/Loop.php +++ b/src/danog/MadelineProto/Wrappers/Loop.php @@ -35,17 +35,7 @@ trait Loop * @var boolean */ private $stopLoop = false; - /** - * Set loop callback (DEPRECATED). - * - * @param callable $callback Callback - * - * @return void - */ - public function setLoopCallback($callback): void - { - $this->loop_callback = $callback; - } + /** * Start MadelineProto's update handling loop, or run the provided async callable. * @@ -75,7 +65,9 @@ trait Loop if (!\is_callable($this->loop_callback) || \is_array($this->loop_callback) && $this->loop_callback[1] === 'onLoop' && !\method_exists(...$this->loop_callback)) { $this->loop_callback = null; } - if (PHP_SAPI !== 'cli') { + + static $inited = false; + if (PHP_SAPI !== 'cli' && !$inited) { $needs_restart = true; try { \set_time_limit(-1); @@ -83,11 +75,11 @@ trait Loop $needs_restart = true; } if (isset($_REQUEST['MadelineSelfRestart'])) { - $this->logger->logger("Self-restarted, restart token " . $_REQUEST['MadelineSelfRestart']); + $this->logger->logger("Self-restarted, restart token ".$_REQUEST['MadelineSelfRestart']); } $this->logger->logger($needs_restart ? 'Will self-restart' : 'Will not self-restart'); $backtrace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - $lockfile = \dirname(\end($backtrace)['file']) . '/bot' . $this->authorization['user']['id'] . '.lock'; + $lockfile = \dirname(\end($backtrace)['file']).'/bot'.$this->authorization['user']['id'].'.lock'; unset($backtrace); $try_locking = true; if (!\file_exists($lockfile)) { @@ -120,7 +112,7 @@ trait Loop if ($needs_restart) { $logger =& $this->logger; Shutdown::addCallback(static function () use (&$logger) { - $address = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] ? 'tls' : 'tcp') . '://' . $_SERVER['SERVER_NAME']; + $address = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] ? 'tls' : 'tcp').'://'.$_SERVER['SERVER_NAME']; $port = $_SERVER['SERVER_PORT']; $uri = $_SERVER['REQUEST_URI']; $params = $_GET; @@ -128,7 +120,7 @@ trait Loop $url = \explode('?', $uri, 2)[0] ?? ''; $query = \http_build_query($params); $uri = \implode('?', [$url, $query]); - $payload = $_SERVER['REQUEST_METHOD'] . ' ' . $uri . ' ' . $_SERVER['SERVER_PROTOCOL'] . "\r\n" . 'Host: ' . $_SERVER['SERVER_NAME'] . "\r\n\r\n"; + $payload = $_SERVER['REQUEST_METHOD'].' '.$uri.' '.$_SERVER['SERVER_PROTOCOL']."\r\n".'Host: '.$_SERVER['SERVER_NAME']."\r\n\r\n"; $logger->logger("Connecting to {$address}:{$port}"); $a = \fsockopen($address, $port); $logger->logger("Sending self-restart payload"); @@ -140,6 +132,7 @@ trait Loop }, 'restarter'); } $this->closeConnection('Bot was started'); + $inited = true; } if (!$this->settings['updates']['handle_updates']) { $this->settings['updates']['handle_updates'] = true; @@ -179,15 +172,22 @@ trait Loop $this->signalUpdate(); } /** - * Start MadelineProto's update handling loop in background, or run the provided async callable. + * Restart update loop. * - * @param callable $callback Async callable to run - * - * @return mixed + * @return void */ - public function loopFork($callback = null): void + public function restart(): void { - Tools::callFork($this->loop($callback)); + $this->stop(); + } + /** + * Start MadelineProto's update handling loop in background. + * + * @return Promise + */ + public function loopFork(): Promise + { + return Tools::callFork($this->loop()); } /** * Close connection with client, connected via web. @@ -203,14 +203,14 @@ trait Loop } $this->logger->logger($message); $buffer = @\ob_get_clean() ?: ''; - $buffer .= '

' . \htmlentities($message) . '

'; + $buffer .= '

'.\htmlentities($message).'

'; \ignore_user_abort(true); \header('Connection: close'); \header('Content-Type: text/html'); echo $buffer; \flush(); $GLOBALS['exited'] = true; - if (\function_exists(\fastcgi_finish_request::class)) { + if (\function_exists('fastcgi_finish_request')) { \fastcgi_finish_request(); } }