diff --git a/src/danog/MadelineProto/API.php b/src/danog/MadelineProto/API.php index dc7b2c5d..4c7c7fe2 100644 --- a/src/danog/MadelineProto/API.php +++ b/src/danog/MadelineProto/API.php @@ -19,13 +19,6 @@ namespace danog\MadelineProto; -use Amp\Deferred; -use Amp\Promise; -use function Amp\File\exists; -use function Amp\File\get; -use function Amp\File\put; -use function Amp\File\rename as renameAsync; - if (!\defined('MADELINEPROTO_TEST')) { \define('MADELINEPROTO_TEST', 'NOT PONY'); } @@ -36,16 +29,31 @@ if (!\defined('MADELINEPROTO_TEST')) { class API extends InternalDoc { use \danog\Serializable; - use \danog\MadelineProto\Wrappers\ApiStart; - use \danog\MadelineProto\Wrappers\ApiTemplates; - public $session; - public $serialized = 0; + use \danog\MadelineProto\ApiWrappers\Start; + use \danog\MadelineProto\ApiWrappers\Templates; + /** + * Session path. + * + * @internal + * + * @var string + */ + public string $session = ''; + /** * Instance of MadelineProto. * - * @var MTProto + * @var ?MTProto */ public $API; + + /** + * Storage for externally set properties to be serialized. + * + * @var array + */ + protected array $storage = []; + /** * Whether we're getting our API ID. * @@ -53,172 +61,107 @@ class API extends InternalDoc * * @var boolean */ - public $getting_api_id = false; + private bool $gettingApiId = false; + /** * my.telegram.org API wrapper. * * @internal * - * @var MyTelegramOrgWrapper + * @var ?MyTelegramOrgWrapper */ - public $my_telegram_org_wrapper; + private $myTelegramOrgWrapper; + /** - * Async ini tpromise. + * Whether this is an old instance. * - * @var Promise + * @var boolean */ - public $asyncAPIPromise; - private $oldInstance = false; - private $destructing = false; + private bool $oldInstance = false; + /** + * Whether we're destructing. + * + * @var boolean + */ + private bool $destructing = false; + + + /** + * API wrapper (to avoid circular references). + * + * @var APIWrapper + */ + private $wrapper; + + /** * Magic constructor function. * - * @param array $params Params - * @param array $settings Settings + * @param string $session Session name + * @param array $settings Settings * * @return void */ - public function __magic_construct($params = [], $settings = []): void + public function __magic_construct(string $session, array $settings = []): void { + $this->wrapper = new APIWrapper($this, $this->exportNamespace()); + Magic::classExists(); - $deferred = new Deferred(); - $this->asyncAPIPromise = $deferred->promise(); - $this->asyncAPIPromise->onResolve(function () { - $this->asyncAPIPromise = null; - }); - $this->setInitPromise($this->__construct_async($params, $settings, $deferred)); - foreach (\get_object_vars(new APIFactory('', $this, $this->async)) as $key => $var) { - if (\in_array($key, ['namespace', 'API', 'lua', 'async', 'asyncAPIPromise', 'methods', 'asyncInitPromise'])) { + $this->setInitPromise($this->__construct_async($session, $settings)); + foreach (\get_class_vars(APIFactory::class) as $key => $var) { + if (\in_array($key, ['namespace', 'API', 'lua', 'async', 'asyncAPIPromise', 'methods'])) { continue; } - if (\is_null($this->{$key})) { - $this->{$key} = new APIFactory($key, $this->API, $this->async); + if (!$this->{$key}) { + $this->{$key} = $this->exportNamespace($key); } } } /** * Async constructor function. * - * @param mixed $params Params - * @param mixed $settings Settings - * @param mixed $deferred Deferred + * @param string $session Session name + * @param array $settings Settings * * @return \Generator */ - public function __construct_async($params, $settings, $deferred): \Generator + public function __construct_async(string $session, array $settings = []): \Generator { - if (\is_string($params)) { - Logger::constructorFromSettings($settings); - $realpaths = Serialization::realpaths($params); - $this->session = $realpaths['file']; - if (yield exists($realpaths['file'])) { - Logger::log('Waiting for shared lock of serialization lockfile...'); - $unlock = yield Tools::flock($realpaths['lockfile'], LOCK_SH); - Logger::log('Shared lock acquired, deserializing...'); - try { - $tounserialize = yield get($realpaths['file']); - } finally { - $unlock(); - } - \danog\MadelineProto\Magic::classExists(); - try { - $unserialized = \unserialize($tounserialize); - } catch (\danog\MadelineProto\Bug74586Exception $e) { - \class_exists('\\Volatile'); - $tounserialize = \str_replace('O:26:"danog\\MadelineProto\\Button":', 'O:35:"danog\\MadelineProto\\TL\\Types\\Button":', $tounserialize); - foreach (['RSA', 'TL\\TLMethods', 'TL\\TLConstructors', 'MTProto', 'API', 'DataCenter', 'Connection', 'TL\\Types\\Button', 'TL\\Types\\Bytes', 'APIFactory'] as $class) { - \class_exists('\\danog\\MadelineProto\\' . $class); - } - $unserialized = \danog\Serialization::unserialize($tounserialize); - } catch (\danog\MadelineProto\Exception $e) { - if ($e->getFile() === 'MadelineProto' && $e->getLine() === 1) { - throw $e; - } - if (\MADELINEPROTO_TEST === 'pony') { - throw $e; - } - \class_exists('\\Volatile'); - foreach (['RSA', 'TL\\TLMethods', 'TL\\TLConstructors', 'MTProto', 'API', 'DataCenter', 'Connection', 'TL\\Types\\Button', 'TL\\Types\\Bytes', 'APIFactory'] as $class) { - \class_exists('\\danog\\MadelineProto\\' . $class); - } - $changed = false; - if (\strpos($tounserialize, 'O:26:"danog\\MadelineProto\\Button":') !== false) { - Logger::log("SUBBING BUTTONS!"); - $tounserialize = \str_replace('O:26:"danog\\MadelineProto\\Button":', 'O:35:"danog\\MadelineProto\\TL\\Types\\Button":', $tounserialize); - $changed = true; - } - if (\strpos($e->getMessage(), "Erroneous data format for unserializing 'phpseclib\\Math\\BigInteger'") === 0) { - Logger::log("SUBBING BIGINTEGOR!"); - $tounserialize = \str_replace('phpseclib\\Math\\BigInteger', 'phpseclib\\Math\\BigIntegor', $tounserialize); - $changed = true; - } - if (\strpos($tounserialize, 'C:25:"phpseclib\\Math\\BigInteger"') !== false) { - Logger::log("SUBBING TGSECLIB old!"); - $tounserialize = \str_replace('C:25:"phpseclib\\Math\\BigInteger"', 'C:24:"tgseclib\\Math\\BigInteger"', $tounserialize); - $changed = true; - } - if (\strpos($tounserialize, 'C:26:"phpseclib3\\Math\\BigInteger"') !== false) { - Logger::log("SUBBING TGSECLIB!"); - $tounserialize = \str_replace('C:26:"phpseclib3\\Math\\BigInteger"', 'C:24:"tgseclib\\Math\\BigInteger"', $tounserialize); - $changed = true; - } - Logger::log((string) $e, Logger::ERROR); - if (!$changed) { - throw $e; - } - try { - $unserialized = \danog\Serialization::unserialize($tounserialize); - } catch (\Throwable $e) { - $unserialized = \unserialize($tounserialize); - } - } catch (\Throwable $e) { - Logger::log((string) $e, Logger::ERROR); - throw $e; - } - if ($unserialized instanceof \danog\PlaceHolder) { - $unserialized = \danog\Serialization::unserialize($tounserialize); - } - if ($unserialized === false) { - throw new Exception(\danog\MadelineProto\Lang::$current_lang['deserialization_error']); - } - $this->web_api_template = $unserialized->web_api_template; - $this->my_telegram_org_wrapper = $unserialized->my_telegram_org_wrapper; - $this->getting_api_id = $unserialized->getting_api_id; - if (isset($unserialized->API)) { - $this->API = $unserialized->API; - $this->APIFactory(); - $unserialized->oldInstance = true; - $deferred->resolve(); - yield from $this->API->initAsynchronously(); - $this->APIFactory(); - //\danog\MadelineProto\Logger::log('Ping...', Logger::ULTRA_VERBOSE); - $this->asyncInitPromise = null; - //$pong = yield $this->ping(['ping_id' => 3], ['async' => true]); - //\danog\MadelineProto\Logger::log('Pong: '.$pong['ping_id'], Logger::ULTRA_VERBOSE); - \danog\MadelineProto\Logger::log(\danog\MadelineProto\Lang::$current_lang['madelineproto_ready'], Logger::NOTICE); - return; - } - } - $params = $settings; - } Logger::constructorFromSettings($settings); - if (!isset($params['app_info']['api_id']) || !$params['app_info']['api_id']) { - $app = (yield from $this->APIStart($params)); - $params['app_info']['api_id'] = $app['api_id']; - $params['app_info']['api_hash'] = $app['api_hash']; + $session = Absolute::absolute($session); + if ($unserialized = yield from Serialization::legacyUnserialize($session)) { + $unserialized->storage = $unserialized->storage ?? []; + $unserialized->session = $session; + APIWrapper::link($this, $unserialized); + APIWrapper::link($this->wrapper, $this); + var_dump($this, $unserialized); + if (isset($this->API)) { + $this->storage = $this->API->storage ?? $this->storage; + + unset($unserialized); + + yield from $this->API->initAsynchronously(); + $this->APIFactory(); + $this->logger->logger(Lang::$current_lang['madelineproto_ready'], Logger::NOTICE); + return; + } } - $this->API = new MTProto($params); - $this->APIFactory(); - $deferred->resolve(); - Logger::log(\danog\MadelineProto\Lang::$current_lang['apifactory_start'], Logger::VERBOSE); + + if (!isset($settings['app_info']['api_id']) || !$settings['app_info']['api_id']) { + $app = (yield from $this->APIStart($settings)); + if (!$app) { + $this->forceInit(true); + die(); + } + $settings['app_info']['api_id'] = $app['api_id']; + $settings['app_info']['api_hash'] = $app['api_hash']; + } + $this->API = new MTProto($settings); yield from $this->API->initAsynchronously(); $this->APIFactory(); - $this->asyncInitPromise = null; - //\danog\MadelineProto\Logger::log('Ping...', Logger::ULTRA_VERBOSE); - //$pong = yield $this->ping(['ping_id' => 3], ['async' => true]); - //\danog\MadelineProto\Logger::log('Pong: '.$pong['ping_id'], Logger::ULTRA_VERBOSE); - \danog\MadelineProto\Logger::log(\danog\MadelineProto\Lang::$current_lang['madelineproto_ready'], Logger::NOTICE); + $this->logger->logger(Lang::$current_lang['madelineproto_ready'], Logger::NOTICE); } + /** * Enable or disable async. * @@ -228,9 +171,9 @@ class API extends InternalDoc */ public function async(bool $async): void { - $this->async = $async; + parent::async($async); if ($this->API) { - if ($this->API->event_handler && \class_exists($this->API->event_handler) && \is_subclass_of($this->API->event_handler, '\\danog\\MadelineProto\\EventHandler')) { + if ($this->API->event_handler && \class_exists($this->API->event_handler) && \is_subclass_of($this->API->event_handler, EventHandler::class)) { $this->API->setEventHandler($this->API->event_handler); } } @@ -242,23 +185,27 @@ class API extends InternalDoc */ public function __destruct() { - if (\danog\MadelineProto\Magic::$has_thread && \is_object(\Thread::getCurrentThread()) || Magic::isFork()) { - return; - } - if ($this->asyncInitPromise) { - $this->init(); - } + $this->init(); if (!$this->oldInstance) { + $this->logger('Shutting down MadelineProto (API)'); if ($this->API) { - $this->API->logger('Shutting down MadelineProto (normally or due to an exception, idk)'); $this->API->destructing = true; - } else { - Logger::log('Shutting down MadelineProto (normally or due to an exception, idk)'); } $this->destructing = true; - Tools::wait($this->serialize(), true); + Tools::wait($this->wrapper->serialize(), true); + $this->API->unreference(); + } else { + $this->logger('Shutting down MadelineProto (old deserialized instance of API)'); } - //restore_error_handler(); + } + /** + * Wakeup function + * + * @return void + */ + public function __wakeup(): void + { + $this->oldInstance = true; } /** * Sleep function. @@ -269,18 +216,7 @@ class API extends InternalDoc */ public function __sleep(): array { - return ['API', 'web_api_template', 'getting_api_id', 'my_telegram_org_wrapper']; - } - /** - * Custom fast getSelf. - * - * @internal - * - * @return array|false - */ - public function myGetSelf() - { - return isset($this->API) && isset($this->API->authorization['user']) ? $this->API->authorization['user'] : false; + return APIWrapper::__sleep(); } /** * Init API wrapper. @@ -289,122 +225,20 @@ class API extends InternalDoc */ private function APIFactory(): void { - if ($this->API && !$this->API->asyncInitPromise) { + if ($this->API && $this->API->inited()) { foreach ($this->API->getMethodNamespaces() as $namespace) { - $this->{$namespace} = new APIFactory($namespace, $this->API, $this->async); - } - $methods = \get_class_methods($this->API); - foreach ($methods as $method) { - if ($method == 'methodCallAsyncRead') { - unset($methods[\array_search('methodCall', $methods)]); - } elseif (\stripos($method, 'async') !== false) { - if (\strpos($method, '_async') !== false) { - unset($methods[\array_search(\str_ireplace('_async', '', $method), $methods)]); - } else { - unset($methods[\array_search(\str_ireplace('async', '', $method), $methods)]); - } + if (!$this->{$namespace}) { + $this->{$namespace} = $this->exportNamespace($namespace); } } - $this->methods = []; - foreach ($methods as $method) { - $actual_method = $method; - if ($method == 'methodCallAsyncRead') { - $method = 'methodCall'; - } elseif (\stripos($method, 'async') !== false) { - if (\strpos($method, '_async') !== false) { - $method = \str_ireplace('_async', '', $method); - } else { - $method = \str_ireplace('async', '', $method); - } - } - $actual_method = $actual_method === 'getSelf' ? [$this, 'myGetSelf'] : [$this->API, $actual_method]; - $this->methods[\strtolower($method)] = $actual_method; - if (\strpos($method, '_') !== false) { - $this->methods[\strtolower(\str_replace('_', '', $method))] = $actual_method; - } else { - $this->methods[\strtolower(Tools::fromCamelCase($method))] = $actual_method; - } - } - $this->API->wrapper = $this; - if ($this->API->event_handler && \class_exists($this->API->event_handler) && \is_subclass_of($this->API->event_handler, '\\danog\\MadelineProto\\EventHandler')) { + $this->methods = self::getInternalMethodList($this->API); + $this->API->wrapper = $this->wrapper; + if ($this->API->event_handler && \class_exists($this->API->event_handler) && \is_subclass_of($this->API->event_handler, EventHandler::class)) { $this->API->setEventHandler($this->API->event_handler); } } } - /** - * Get full list of MTProto and API methods. - * - * @return array - */ - public function getAllMethods(): array - { - if ($this->asyncInitPromise) { - $this->init(); - } - $methods = []; - foreach ($this->API->methods->by_id as $method) { - $methods[] = $method['method']; - } - return \array_merge($methods, \get_class_methods($this->API)); - } - /** - * Serialize session. - * - * @param string $filename File name - * - * @internal Do not use this manually, the session is already serialized automatically - * - * @return Promise - */ - public function serialize(string $filename = ''): Promise - { - return Tools::callFork((function () use ($filename): \Generator { - if (empty($filename)) { - $filename = $this->session; - } - //Logger::log(\danog\MadelineProto\Lang::$current_lang['serializing_madelineproto']); - if ($filename == '') { - return; - } - if (isset($this->API->flushSettings) && $this->API->flushSettings) { - $this->API->flushSettings = false; - $this->API->__construct($this->API->settings); - } - if ($this->API === null && !$this->getting_api_id) { - return false; - } - if ($this->API && $this->API->asyncInitPromise) { - yield from $this->API->initAsynchronously(); - } - $this->serialized = \time(); - $realpaths = Serialization::realpaths($filename); - //Logger::log('Waiting for exclusive lock of serialization lockfile...'); - $unlock = yield Tools::flock($realpaths['lockfile'], LOCK_EX); - //Logger::log('Lock acquired, serializing'); - try { - if (!$this->getting_api_id) { - $update_closure = $this->API->settings['updates']['callback']; - if ($this->API->settings['updates']['callback'] instanceof \Closure) { - $this->API->settings['updates']['callback'] = [$this->API, 'noop']; - } - $logger_closure = $this->API->settings['logger']['logger_param']; - if ($this->API->settings['logger']['logger_param'] instanceof \Closure) { - $this->API->settings['logger']['logger_param'] = [$this->API, 'noop']; - } - } - $wrote = yield put($realpaths['tempfile'], \serialize($this)); - yield renameAsync($realpaths['tempfile'], $realpaths['file']); - } finally { - if (!$this->getting_api_id) { - $this->API->settings['updates']['callback'] = $update_closure; - $this->API->settings['logger']['logger_param'] = $logger_closure; - } - $unlock(); - } - //Logger::log('Done serializing'); - return $wrote; - })()); - } + /** * Start MadelineProto and the event handler (enables async). @@ -428,10 +262,10 @@ class API extends InternalDoc $this->loop(); } catch (\Throwable $e) { $thrown = true; + $this->logger((string) $e, Logger::FATAL_ERROR); try { $this->report("Surfaced: $e"); } catch (\Throwable $e) { - $this->logger((string) $e, Logger::FATAL_ERROR); } } } while ($thrown); diff --git a/src/danog/MadelineProto/APIWrapper.php b/src/danog/MadelineProto/APIWrapper.php new file mode 100644 index 00000000..1c334532 --- /dev/null +++ b/src/danog/MadelineProto/APIWrapper.php @@ -0,0 +1,188 @@ +. + * + * @author Daniil Gentili + * @copyright 2016-2020 Daniil Gentili + * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 + * + * @link https://docs.madelineproto.xyz MadelineProto documentation + */ + +namespace danog\MadelineProto; + +use Amp\Promise; +use Amp\Success; + +use function Amp\File\put; +use function Amp\File\rename as renameAsync; + +final class APIWrapper +{ + /** + * MTProto instance. + * + * @var ?MTProto + */ + private ?MTProto $API = null; + + /** + * Session path. + * + * @var string + */ + public string $session = ''; + + /** + * Getting API ID flag. + * + * @var bool + */ + private bool $gettingApiId = false; + + /** + * Web API template + * + * @var string + */ + private string $webApiTemplate = ''; + + /** + * My.telegram.org wrapper + * + * @var ?MyTelegramOrgWrapper + */ + private $myTelegramOrgWrapper; + + /** + * Serialization date. + * + * @var integer + */ + private int $serialized = 0; + + /** + * AbstractAPIFactory instance + * + * @var AbstractAPIFactory + */ + private AbstractAPIFactory $factory; + /** + * API wrapper + * + * @param API $API API instance to wrap + * @param AbstractAPIFactory $factory Factory + */ + public function __construct(API $API, AbstractAPIFactory $factory) + { + self::link($this, $API); + $this->factory = $factory; + } + + /** + * Link two APIWrapper and API instances + * + * @param API|APIWrapper $a Instance to which link + * @param API|APIWrapper $b Instance from which link + * + * @return void + */ + public static function link($a, $b): void + { + foreach (self::__sleep() as $var) { + Tools::setVar($a, $var, Tools::getVar($b, $var)); + } + Tools::setVar($a, 'session', Tools::getVar($b, 'session')); + } + + /** + * Sleep function. + * + * @internal + * + * @return array + */ + public static function __sleep(): array + { + return ['API', 'webApiTemplate', 'gettingApiId', 'myTelegramOrgWrapper', 'storage']; + } + + /** + * Get MTProto instance + * + * @return MTProto|null + */ + public function &getAPI(): ?MTProto + { + return $this->API; + } + + /** + * Get API factory + * + * @return AbstractAPIFactory + */ + public function getFactory(): AbstractAPIFactory + { + return $this->factory; + } + + /** + * Serialie session. + * + * @return Promise + */ + public function serialize(): Promise + { + if (!$this->session) { + Logger::log("Not serializing, no session"); + return new Success(); + } + return Tools::callFork((function (): \Generator { + if (isset($this->API->flushSettings) && $this->API->flushSettings) { + $this->API->flushSettings = false; + $this->API->__construct($this->API->settings); + } + if ($this->API === null && !$this->gettingApiId) { + return false; + } + if ($this->API) { + yield from $this->API->initAsynchronously(); + } + $this->serialized = \time(); + $realpaths = Serialization::realpaths($this->session); + //Logger::log('Waiting for exclusive lock of serialization lockfile...'); + $unlock = yield Tools::flock($realpaths['lockfile'], LOCK_EX); + //Logger::log('Lock acquired, serializing'); + try { + if (!$this->gettingApiId) { + $update_closure = $this->API->settings['updates']['callback']; + if ($this->API->settings['updates']['callback'] instanceof \Closure) { + $this->API->settings['updates']['callback'] = [$this->API, 'noop']; + } + $logger_closure = $this->API->settings['logger']['logger_param']; + if ($this->API->settings['logger']['logger_param'] instanceof \Closure) { + $this->API->settings['logger']['logger_param'] = [$this->API, 'noop']; + } + } + $wrote = yield put($realpaths['tempfile'], \serialize($this)); + yield renameAsync($realpaths['tempfile'], $realpaths['file']); + } finally { + if (!$this->gettingApiId) { + $this->API->settings['updates']['callback'] = $update_closure; + $this->API->settings['logger']['logger_param'] = $logger_closure; + } + $unlock(); + } + //Logger::log('Done serializing'); + return $wrote; + })()); + } +} diff --git a/src/danog/MadelineProto/AbstractAPIFactory.php b/src/danog/MadelineProto/AbstractAPIFactory.php index cec6da6e..cd79df9c 100644 --- a/src/danog/MadelineProto/AbstractAPIFactory.php +++ b/src/danog/MadelineProto/AbstractAPIFactory.php @@ -30,7 +30,7 @@ abstract class AbstractAPIFactory extends AsyncConstruct * * @var string */ - public $namespace = ''; + private string $namespace = ''; /** * MTProto instance. * @@ -46,7 +46,7 @@ abstract class AbstractAPIFactory extends AsyncConstruct * * @var boolean */ - public $lua = false; + public bool $lua = false; /** * Whether async is enabled. * @@ -54,24 +54,43 @@ abstract class AbstractAPIFactory extends AsyncConstruct * * @var boolean */ - public $async = false; - /** - * Async init promise. - * - * @var Promise - */ - public $asyncAPIPromise; + private bool $async = false; /** * Method list. * * @var string[] */ - protected $methods = []; - public function __construct($namespace, &$API, &$async) + protected array $methods = []; + /** + * Export APIFactory instance with the specified namespace. + * + * @param string $namespace Namespace + * + * @return self + */ + protected function exportNamespace(string $namespace = ''): self { - $this->namespace = $namespace.'.'; - $this->API =& $API; - $this->async =& $async; + $class = array_reverse(array_values(class_parents(static::class)))[3]; + $instance = new $class; + $instance->namespace = $namespace.'.'; + self::link($instance, $this); + + return $instance; + } + /** + * Link two APIFactory instances + * + * @param self $a First instance + * @param self $b Second instance + * + * @return void + */ + protected static function link(self $a, self $b): void + { + $a->API =& $b->API; + $a->lua =& $b->lua; + $a->async =& $b->async; + $a->methods =& $b->methods; } /** * Enable or disable async. @@ -101,7 +120,9 @@ abstract class AbstractAPIFactory extends AsyncConstruct if ($async) { return $yielded; } + $yielded = Tools::wait($yielded); + if (!$this->lua) { return $yielded; } @@ -124,30 +145,8 @@ abstract class AbstractAPIFactory extends AsyncConstruct */ public function __call_async(string $name, array $arguments): \Generator { - if ($this->asyncInitPromise) { - yield from $this->initAsynchronously(); - $this->API->logger->logger('Finished init asynchronously'); - } - if (!$this->API) { - throw new Exception('API did not init!'); - } - if ($this->API->asyncInitPromise) { - yield from $this->API->initAsynchronously(); - $this->API->logger->logger('Finished init asynchronously'); - } - if (isset($this->session) && !\is_null($this->session) && \time() - $this->serialized > $this->API->settings['serialization']['serialization_interval']) { - Logger::log("Didn't serialize in a while, doing that now..."); - $this->serialize($this->session); - } - if ($this->API->flushSettings) { - $this->API->flushSettings = false; - $this->API->__construct($this->API->settings); - yield from $this->API->initAsynchronously(); - } - if ($this->API->asyncInitPromise) { - yield from $this->API->initAsynchronously(); - $this->API->logger->logger('Finished init asynchronously'); - } + yield from $this->initAsynchronously(); + $lower_name = \strtolower($name); if ($this->namespace !== '' || !isset($this->methods[$lower_name])) { $name = $this->namespace.$name; @@ -160,6 +159,61 @@ abstract class AbstractAPIFactory extends AsyncConstruct $res = $this->methods[$lower_name](...$arguments); return $res instanceof \Generator ? yield from $res : yield $res; } + /** + * Get fully resolved method list for object, including snake_case and camelCase variants. + * + * @param API $value Value + * + * @return array + */ + protected static function getInternalMethodList($value): array + { + static $cache = []; + $class = \get_class($value); + if (isset($cache[$class])) { + return \array_map( + static function ($v) use ($value) { + return [$value, $v]; + }, + $cache[$class] + ); + } + + $methods = \get_class_methods($value); + foreach ($methods as $method) { + if ($method == 'methodCallAsyncRead') { + unset($methods[\array_search('methodCall', $methods)]); + } elseif (\stripos($method, 'async') !== false) { + if (\strpos($method, '_async') !== false) { + unset($methods[\array_search(\str_ireplace('_async', '', $method), $methods)]); + } else { + unset($methods[\array_search(\str_ireplace('async', '', $method), $methods)]); + } + } + } + $finalMethods = []; + foreach ($methods as $method) { + $actual_method = $method; + if ($method == 'methodCallAsyncRead') { + $method = 'methodCall'; + } elseif (\stripos($method, 'async') !== false) { + if (\strpos($method, '_async') !== false) { + $method = \str_ireplace('_async', '', $method); + } else { + $method = \str_ireplace('async', '', $method); + } + } + $finalMethods[\strtolower($method)] = $actual_method; + if (\strpos($method, '_') !== false) { + $finalMethods[\strtolower(\str_replace('_', '', $method))] = $actual_method; + } else { + $finalMethods[\strtolower(Tools::toSnakeCase($method))] = $actual_method; + } + } + + $cache[$class] = $finalMethods; + return self::getInternalMethodList($value); + } /** * Get attribute. * @@ -171,17 +225,13 @@ abstract class AbstractAPIFactory extends AsyncConstruct */ public function &__get(string $name) { - if ($this->asyncAPIPromise) { - Tools::wait($this->asyncAPIPromise); - } - if ($name === 'settings') { - $this->API->flushSettings = true; - return $this->API->settings; - } if ($name === 'logger') { - return $this->API->logger; + if (isset($this->API)) { + return $this->API->logger; + } + return Logger::$default; } - return $this->API->storage[$name]; + return $this->storage[$name]; } /** * Set an attribute. @@ -195,16 +245,7 @@ abstract class AbstractAPIFactory extends AsyncConstruct */ public function __set(string $name, $value) { - if ($this->asyncAPIPromise) { - Tools::wait($this->asyncAPIPromise); - } - if ($name === 'settings') { - if ($this->API->asyncInitPromise) { - $this->API->init(); - } - return $this->API->__construct(\array_replace_recursive($this->API->settings, $value)); - } - return $this->API->storage[$name] = $value; + return $this->storage[$name] = $value; } /** * Whether an attribute exists. @@ -215,10 +256,7 @@ abstract class AbstractAPIFactory extends AsyncConstruct */ public function __isset(string $name): bool { - if ($this->asyncAPIPromise) { - Tools::wait($this->asyncAPIPromise); - } - return isset($this->API->storage[$name]); + return isset($this->storage[$name]); } /** * Unset attribute. @@ -229,9 +267,6 @@ abstract class AbstractAPIFactory extends AsyncConstruct */ public function __unset(string $name): void { - if ($this->asyncAPIPromise) { - Tools::wait($this->asyncAPIPromise); - } - unset($this->API->storage[$name]); + unset($this->storage[$name]); } } diff --git a/src/danog/MadelineProto/AnnotationsBuilder.php b/src/danog/MadelineProto/AnnotationsBuilder.php index 4980d4b7..0b09eff2 100644 --- a/src/danog/MadelineProto/AnnotationsBuilder.php +++ b/src/danog/MadelineProto/AnnotationsBuilder.php @@ -184,7 +184,7 @@ class AnnotationsBuilder $name = \str_ireplace('async', '', $name); } } - $name = Tools::fromSnakeCase($name); + $name = Tools::toCamelCase($name); $name = \str_ireplace(['mtproto', 'api'], ['MTProto', 'API'], $name); $doc = 'public function '; $doc .= $name; diff --git a/src/danog/MadelineProto/Wrappers/ApiStart.php b/src/danog/MadelineProto/ApiWrappers/Start.php similarity index 72% rename from src/danog/MadelineProto/Wrappers/ApiStart.php rename to src/danog/MadelineProto/ApiWrappers/Start.php index 0a0f8a21..829f2afd 100644 --- a/src/danog/MadelineProto/Wrappers/ApiStart.php +++ b/src/danog/MadelineProto/ApiWrappers/Start.php @@ -17,17 +17,25 @@ * @link https://docs.madelineproto.xyz MadelineProto documentation */ -namespace danog\MadelineProto\Wrappers; +namespace danog\MadelineProto\ApiWrappers; +use danog\MadelineProto\MyTelegramOrgWrapper; use danog\MadelineProto\Tools; use function Amp\ByteStream\getStdout; /** * Manages simple logging in and out. */ -trait ApiStart +trait Start { - public function APIStart($settings): \Generator + /** + * Start API ID generation process. + * + * @param array $settings Settings + * + * @return \Generator + */ + private function APIStart(array $settings): \Generator { if (PHP_SAPI === 'cli') { $stdout = getStdout(); @@ -46,66 +54,65 @@ Note that you can also provide the API parameters directly in the code using the $app['api_hash'] = yield Tools::readLine('6) Enter your API hash: '); return $app; } - $this->my_telegram_org_wrapper = new \danog\MadelineProto\MyTelegramOrgWrapper($settings); - yield from $this->my_telegram_org_wrapper->login(yield Tools::readLine('Enter a phone number that is already registered on Telegram: ')); - yield from $this->my_telegram_org_wrapper->completeLogin(yield Tools::readLine('Enter the verification code you received in telegram: ')); - if (!(yield from $this->my_telegram_org_wrapper->hasApp())) { + $this->myTelegramOrgWrapper = new \danog\MadelineProto\MyTelegramOrgWrapper($settings); + yield from $this->myTelegramOrgWrapper->login(yield Tools::readLine('Enter a phone number that is already registered on Telegram: ')); + yield from $this->myTelegramOrgWrapper->completeLogin(yield Tools::readLine('Enter the verification code you received in telegram: ')); + if (!(yield from $this->myTelegramOrgWrapper->hasApp())) { $app_title = yield Tools::readLine('Enter the app\'s name, can be anything: '); $short_name = yield Tools::readLine('Enter the app\'s short name, can be anything: '); $url = yield Tools::readLine('Enter the app/website\'s URL, or t.me/yourusername: '); $description = yield Tools::readLine('Describe your app: '); - $app = (yield from $this->my_telegram_org_wrapper->createApp(['app_title' => $app_title, 'app_shortname' => $short_name, 'app_url' => $url, 'app_platform' => 'web', 'app_desc' => $description])); + $app = (yield from $this->myTelegramOrgWrapper->createApp(['app_title' => $app_title, 'app_shortname' => $short_name, 'app_url' => $url, 'app_platform' => 'web', 'app_desc' => $description])); } else { - $app = (yield from $this->my_telegram_org_wrapper->getApp()); + $app = (yield from $this->myTelegramOrgWrapper->getApp()); } return $app; } - $this->getting_api_id = true; - if (!isset($this->my_telegram_org_wrapper)) { + $this->gettingApiId = true; + if (!isset($this->myTelegramOrgWrapper)) { if (isset($_POST['api_id']) && isset($_POST['api_hash'])) { $app['api_id'] = (int) $_POST['api_id']; $app['api_hash'] = $_POST['api_hash']; - $this->getting_api_id = false; + $this->gettingApiId = false; return $app; } elseif (isset($_POST['phone_number'])) { yield from $this->webAPIPhoneLogin($settings); } else { yield from $this->webAPIEcho(); } - } elseif (!$this->my_telegram_org_wrapper->loggedIn()) { + } elseif (!$this->myTelegramOrgWrapper->loggedIn()) { if (isset($_POST['code'])) { yield from $this->webAPICompleteLogin(); - if (yield from $this->my_telegram_org_wrapper->hasApp()) { - return yield from $this->my_telegram_org_wrapper->getApp(); + if (yield from $this->myTelegramOrgWrapper->hasApp()) { + return yield from $this->myTelegramOrgWrapper->getApp(); } yield from $this->webAPIEcho(); } elseif (isset($_POST['api_id']) && isset($_POST['api_hash'])) { $app['api_id'] = (int) $_POST['api_id']; $app['api_hash'] = $_POST['api_hash']; - $this->getting_api_id = false; + $this->gettingApiId = false; return $app; } elseif (isset($_POST['phone_number'])) { yield from $this->webAPIPhoneLogin($settings); } else { - $this->my_telegram_org_wrapper = null; + $this->myTelegramOrgWrapper = null; yield from $this->webAPIEcho(); } } else { if (isset($_POST['app_title'], $_POST['app_shortname'], $_POST['app_url'], $_POST['app_platform'], $_POST['app_desc'])) { $app = (yield from $this->webAPICreateApp()); - $this->getting_api_id = false; + $this->gettingApiId = false; return $app; } yield from $this->webAPIEcho("You didn't provide all of the required parameters!"); } - $this->asyncInitPromise = null; - exit; + return null; } - private function webAPIPhoneLogin($settings): \Generator + private function webAPIPhoneLogin(array $settings): \Generator { try { - $this->my_telegram_org_wrapper = new \danog\MadelineProto\MyTelegramOrgWrapper($settings); - yield from $this->my_telegram_org_wrapper->login($_POST['phone_number']); + $this->myTelegramOrgWrapper = new MyTelegramOrgWrapper($settings); + yield from $this->myTelegramOrgWrapper->login($_POST['phone_number']); yield from $this->webAPIEcho(); } catch (\Throwable $e) { yield from $this->webAPIEcho('ERROR: ' . $e->getMessage() . '. Try again.'); @@ -114,7 +121,7 @@ Note that you can also provide the API parameters directly in the code using the private function webAPICompleteLogin(): \Generator { try { - yield from $this->my_telegram_org_wrapper->completeLogin($_POST['code']); + yield from $this->myTelegramOrgWrapper->completeLogin($_POST['code']); } catch (\danog\MadelineProto\RPCErrorException $e) { yield from $this->webAPIEcho('ERROR: ' . $e->getMessage() . '. Try again.'); } catch (\danog\MadelineProto\Exception $e) { @@ -126,7 +133,7 @@ Note that you can also provide the API parameters directly in the code using the try { $params = $_POST; unset($params['creating_app']); - $app = (yield from $this->my_telegram_org_wrapper->createApp($params)); + $app = (yield from $this->myTelegramOrgWrapper->createApp($params)); return $app; } catch (\danog\MadelineProto\RPCErrorException $e) { yield from $this->webAPIEcho('ERROR: ' . $e->getMessage() . ' Try again.'); diff --git a/src/danog/MadelineProto/Wrappers/ApiTemplates.php b/src/danog/MadelineProto/ApiWrappers/Templates.php similarity index 71% rename from src/danog/MadelineProto/Wrappers/ApiTemplates.php rename to src/danog/MadelineProto/ApiWrappers/Templates.php index 7d9e3407..2ad67d66 100644 --- a/src/danog/MadelineProto/Wrappers/ApiTemplates.php +++ b/src/danog/MadelineProto/ApiWrappers/Templates.php @@ -17,29 +17,29 @@ * @link https://docs.madelineproto.xyz MadelineProto documentation */ -namespace danog\MadelineProto\Wrappers; +namespace danog\MadelineProto\ApiWrappers; use function Amp\ByteStream\getOutputBufferStream; -trait ApiTemplates +trait Templates { - private $web_api_template = ' - - - MadelineProto - - -

MadelineProto

-

%s

-
- %s - -
- - '; - private function webAPIEchoTemplate($message, $form) + /** + * API template. + * + * @var string + */ + private $webApiTemplate = 'MadelineProto

MadelineProto

%s

%s
'; + /** + * Generate page from template. + * + * @param string $message Message + * @param string $form Form + * + * @return string + */ + private function webAPIEchoTemplate(string $message, string $form): string { - return \sprintf($this->web_api_template, $message, $form); + return \sprintf($this->webApiTemplate, $message, $form); } /** * Get web API login HTML template string. @@ -48,24 +48,31 @@ trait ApiTemplates */ public function getWebAPITemplate(): string { - return $this->web_template; + return $this->webApiTemplate; } /** * Set web API login HTML template string. * * @return string */ - public function setWebAPITemplate(string $template) + public function setWebAPITemplate(string $template): void { - $this->web_template = $template; + $this->webApiTemplate = $template; } + /** + * Echo to browser. + * + * @param string $message Message to echo + * + * @return \Generator + */ private function webAPIEcho(string $message = ''): \Generator { $stdout = getOutputBufferStream(); - if (!isset($this->my_telegram_org_wrapper)) { + if (!isset($this->myTelegramOrgWrapper)) { if (isset($_POST['type'])) { if ($_POST['type'] === 'manual') { - yield $stdout->write($this->webAPIEchoTemplate('Enter your API ID and API hash
' . $message . '
    + yield $stdout->write($this->webAPIEchoTemplate('Enter your API ID and API hash
    '.$message.'
    1. Login to my.telegram.org
    2. Go to API development tools
    3. @@ -79,19 +86,19 @@ trait ApiTemplates
    4. Click on create application
    ', '')); } else { - yield $stdout->write($this->webAPIEchoTemplate('Enter a phone number that is already registered on telegram to get the API ID
    ' . $message . '', '')); + yield $stdout->write($this->webAPIEchoTemplate('Enter a phone number that is already registered on telegram to get the API ID
    '.$message.'', '')); } } else { if ($message) { - $message = '

    ' . $message; + $message = '

    '.$message; } - yield $stdout->write($this->webAPIEchoTemplate('Do you want to enter the API id and the API hash manually or automatically?
    Note that you can also provide it directly in the code using the settings.' . $message . '', '')); + yield $stdout->write($this->webAPIEchoTemplate('Do you want to enter the API id and the API hash manually or automatically?
    Note that you can also provide it directly in the code using the settings.'.$message.'', '')); } } else { - if (!$this->my_telegram_org_wrapper->loggedIn()) { - yield $stdout->write($this->webAPIEchoTemplate('Enter your code
    ' . $message . '', '')); + if (!$this->myTelegramOrgWrapper->loggedIn()) { + yield $stdout->write($this->webAPIEchoTemplate('Enter your code
    '.$message.'', '')); } else { - yield $stdout->write($this->webAPIEchoTemplate('Enter the API info
    ' . $message . '', ' + yield $stdout->write($this->webAPIEchoTemplate('Enter the API info
    '.$message.'', ' Enter the app name, can be anything:


    Enter the app's short name, alphanumeric, 5-32 chars:


    Enter the app/website URL, or https://t.me/yourusername:

    diff --git a/src/danog/MadelineProto/Async/AsyncConstruct.php b/src/danog/MadelineProto/Async/AsyncConstruct.php index 45e41412..3e7e5718 100644 --- a/src/danog/MadelineProto/Async/AsyncConstruct.php +++ b/src/danog/MadelineProto/Async/AsyncConstruct.php @@ -35,7 +35,7 @@ class AsyncConstruct * * @var Promise */ - public $asyncInitPromise; + private $asyncInitPromise; /** * Blockingly init. * @@ -58,10 +58,30 @@ class AsyncConstruct yield $this->asyncInitPromise; } } + /** + * Check if we've already inited. + * + * @return boolean + */ + public function inited(): bool + { + return !$this->asyncInitPromise; + } + /** + * Mark instance as (de)inited forcefully + * + * @param boolean $inited Whether to mark the instance as inited or deinited + * + * @return void + */ + public function forceInit(bool $inited): void + { + $this->asyncInitPromise = $inited ? null : true; + } /** * Set init promise. * - * @param Promise $promise Promise + * @param Promise|\Generator $promise Promise * * @internal * @@ -69,12 +89,14 @@ class AsyncConstruct */ public function setInitPromise($promise): void { - $this->asyncInitPromise = Tools::callFork($promise); - $this->asyncInitPromise->onResolve(function ($error, $result) { - if ($error) { - throw $error; + $this->asyncInitPromise = Tools::call($promise); + $this->asyncInitPromise->onResolve( + function (?\Throwable $error, $result): void { + if ($error) { + throw $error; + } + $this->asyncInitPromise = null; } - $this->asyncInitPromise = null; - }); + ); } } diff --git a/src/danog/MadelineProto/EventHandler.php b/src/danog/MadelineProto/EventHandler.php index 55010dae..e6473478 100644 --- a/src/danog/MadelineProto/EventHandler.php +++ b/src/danog/MadelineProto/EventHandler.php @@ -27,18 +27,17 @@ class EventHandler extends InternalDoc /** * Constructor. * - * @param API|null $MadelineProto MadelineProto instance + * @param APIWrapper|null $MadelineProto MadelineProto instance */ - public function __construct(?API $MadelineProto) + public function __construct(?APIWrapper $MadelineProto) { if (!$MadelineProto) { return; } - $this->API = $MadelineProto->API; - $this->async =& $MadelineProto->async; - $this->methods =& $MadelineProto->methods; + self::link($this, $MadelineProto->getFactory()); + $this->API =& $MadelineProto->getAPI(); foreach ($this->API->getMethodNamespaces() as $namespace) { - $this->{$namespace} = new APIFactory($namespace, $this->API, $this->async); + $this->{$namespace} = $this->exportNamespace($namespace); } } /** diff --git a/src/danog/MadelineProto/InternalDoc.php b/src/danog/MadelineProto/InternalDoc.php index 446c2827..525611f0 100644 --- a/src/danog/MadelineProto/InternalDoc.php +++ b/src/danog/MadelineProto/InternalDoc.php @@ -4134,6 +4134,18 @@ class InternalDoc extends APIFactory { return \danog\MadelineProto\MTProto::getSettings($settings, $previousSettings); } + /** + * Parse, update and store settings. + * + * @param array $settings Settings + * @param bool $reinit Whether to reinit the instance + * + * @return void + */ + public function updateSettings(array $settings, bool $reinit = true, array $extra = []) + { + return $this->__call(__FUNCTION__, [$settings, $reinit, $extra]); + } /** * Setup logger. * @@ -4207,14 +4219,23 @@ class InternalDoc extends APIFactory return $this->__call(__FUNCTION__, [$config, $options, $extra]); } /** - * Get info about the logged-in user. + * Get info about the logged-in user, cached. * - * @return \Generator + * @return array|bool */ public function getSelf(array $extra = []) { return $this->__call(__FUNCTION__, [$extra]); } + /** + * Get info about the logged-in user, not cached. + * + * @return \Generator + */ + public function fullGetSelf(array $extra = []) + { + return $this->__call(__FUNCTION__, [$extra]); + } /** * Check if has report peers. * @@ -4246,6 +4267,15 @@ class InternalDoc extends APIFactory { return $this->__call(__FUNCTION__, [$message, $extra]); } + /** + * Get full list of MTProto and API methods. + * + * @return array + */ + public function getAllMethods(): array + { + return $this->API->getAllMethods(); + } /** * Call method and wait asynchronously for response. * @@ -5250,24 +5280,24 @@ class InternalDoc extends APIFactory /** * Convert to camelCase. * - * @param string $input + * @param string $input String * * @return string */ - public function fromSnakeCase(string $input): string + public function toCamelCase(string $input): string { - return \danog\MadelineProto\MTProto::fromSnakeCase($input); + return \danog\MadelineProto\MTProto::toCamelCase($input); } /** * Convert to snake_case. * - * @param string $input + * @param string $input String * * @return string */ - public function fromCamelCase(string $input): string + public function toSnakeCase(string $input): string { - return \danog\MadelineProto\MTProto::fromCamelCase($input); + return \danog\MadelineProto\MTProto::toSnakeCase($input); } /** * Create array. diff --git a/src/danog/MadelineProto/Loop/Generic/PeriodicLoop.php b/src/danog/MadelineProto/Loop/Generic/PeriodicLoop.php index ab838049..12b6cb66 100644 --- a/src/danog/MadelineProto/Loop/Generic/PeriodicLoop.php +++ b/src/danog/MadelineProto/Loop/Generic/PeriodicLoop.php @@ -29,18 +29,33 @@ use danog\MadelineProto\MTProto; */ class PeriodicLoop extends ResumableSignalLoop { + /** + * Callback + * + * @var callable + */ private $callback; - private $name; + /** + * Loop name + * + * @var string + */ + private string $name; + /** + * Loop timeeout + * + * @var int + */ private $timeout; /** * Constructor. * - * @param \danog\MadelineProto\MTProto $API Instance of MTProto class - * @param callable $callback Callback to call - * @param string $name Loop name - * @param int $timeout Loop timeout + * @param \danog\MadelineProto\API $API Instance of MTProto class + * @param callable $callback Callback to call + * @param string $name Loop name + * @param int|float $timeout Loop timeout */ - public function __construct(MTProto $API, $callback, string $name, $timeout) + public function __construct($API, callable $callback, string $name, $timeout) { $this->API = $API; $this->callback = $callback; diff --git a/src/danog/MadelineProto/Loop/Impl/Loop.php b/src/danog/MadelineProto/Loop/Impl/Loop.php index 5c87c004..6bb733f2 100644 --- a/src/danog/MadelineProto/Loop/Impl/Loop.php +++ b/src/danog/MadelineProto/Loop/Impl/Loop.php @@ -54,7 +54,6 @@ abstract class Loop implements LoopInterface } private function loopImpl(): \Generator { - //yield ['my_trace' => debug_backtrace(0, 1)[0], (string) $this]; $this->startedLoop(); $this->API->logger->logger("Entered {$this}", Logger::ULTRA_VERBOSE); try { @@ -62,7 +61,6 @@ abstract class Loop implements LoopInterface } finally { $this->exitedLoop(); $this->API->logger->logger("Physically exited {$this}", Logger::ULTRA_VERBOSE); - //return null; } } public function exitedLoop() diff --git a/src/danog/MadelineProto/Loop/Impl/ResumableSignalLoop.php b/src/danog/MadelineProto/Loop/Impl/ResumableSignalLoop.php index 0b72cda9..34b5e5d4 100644 --- a/src/danog/MadelineProto/Loop/Impl/ResumableSignalLoop.php +++ b/src/danog/MadelineProto/Loop/Impl/ResumableSignalLoop.php @@ -36,7 +36,7 @@ abstract class ResumableSignalLoop extends SignalLoop implements ResumableLoopIn use Tools; private $resume; private $pause; - private $resumeWatcher; + protected $resumeWatcher; public function pause($time = null): Promise { if (!\is_null($time)) { @@ -80,4 +80,13 @@ abstract class ResumableSignalLoop extends SignalLoop implements ResumableLoopIn Loop::defer([$this, 'resume']); return $this->pause ? $this->pause->promise() : null; } + + public function exitedLoop() + { + parent::exitedLoop(); + if ($this->resumeWatcher) { + Loop::cancel($this->resumeWatcher); + $this->resumeWatcher = null; + } + } } diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index 1471bc70..0415806f 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -22,6 +22,7 @@ namespace danog\MadelineProto; use Amp\Dns\Resolver; use Amp\Http\Client\DelegateHttpClient; use Amp\Loop; +use Amp\Success; use danog\MadelineProto\Async\AsyncConstruct; use danog\MadelineProto\Loop\Generic\PeriodicLoop; use danog\MadelineProto\Loop\Update\FeedLoop; @@ -204,7 +205,7 @@ class MTProto extends AsyncConstruct implements TLCallback /** * Instance of wrapper API. * - * @var API|null + * @var ?APIWrapper */ public $wrapper; /** @@ -321,18 +322,6 @@ class MTProto extends AsyncConstruct implements TLCallback * @var array */ private $dialog_params = ['limit' => 0, 'offset_date' => 0, 'offset_id' => 0, 'offset_peer' => ['_' => 'inputPeerEmpty'], 'count' => 0]; - /** - * Whether new settings were set and should be applied. - * - * @var boolean - */ - public $flushSettings = false; - /** - * Storage for arbitrary data. - * - * @var array - */ - public $storage = []; /** * Support user ID. * @@ -439,7 +428,7 @@ class MTProto extends AsyncConstruct implements TLCallback { Magic::classExists(); // Parse and store settings - $this->parseSettings($settings); + yield from $this->updateSettings($settings, false); $this->logger->logger(Lang::$current_lang['inst_dc'], Logger::ULTRA_VERBOSE); $this->cleanupProperties(); // Load rsa keys @@ -544,9 +533,6 @@ class MTProto extends AsyncConstruct implements TLCallback 'temp_requested_secret_chats', 'temp_rekeyed_secret_chats', - // Object storage - 'storage', - // Report URI 'reportDest' ]; @@ -659,13 +645,14 @@ class MTProto extends AsyncConstruct implements TLCallback /** * Prompt serialization of instance. * + * @internal + * * @return void */ public function serialize() { - if ($this->wrapper instanceof API && isset($this->wrapper->session) && !\is_null($this->wrapper->session) && !$this->asyncInitPromise) { - //$this->logger->logger("Didn't serialize in a while, doing that now..."); - $this->wrapper->serialize($this->wrapper->session); + if ($this->wrapper && $this->inited()) { + $this->wrapper->serialize(); } } /** @@ -852,8 +839,9 @@ class MTProto extends AsyncConstruct implements TLCallback */ public function __wakeup() { - $backtrace = \debug_backtrace(0, 3); - $this->asyncInitPromise = true; + $backtrace = \debug_backtrace(0, 4); + $backtrace = end($backtrace); + $this->forceInit(false); $this->setInitPromise($this->__wakeup_async($backtrace)); } /** @@ -884,9 +872,9 @@ class MTProto extends AsyncConstruct implements TLCallback } $force = false; $this->resetMTProtoSession(); - if (isset($backtrace[2]['function'], $backtrace[2]['class'], $backtrace[2]['args']) && $backtrace[2]['class'] === 'danog\\MadelineProto\\API' && $backtrace[2]['function'] === '__construct_async') { - if (\count($backtrace[2]['args']) >= 2) { - $this->parseSettings(\array_replace_recursive($this->settings, $backtrace[2]['args'][1])); + if (isset($backtrace['function'], $backtrace['class'], $backtrace['args']) && $backtrace['class'] === 'danog\\MadelineProto\\API' && $backtrace['function'] === '__construct_async') { + if (\count($backtrace['args']) >= 2) { + yield from $this->updateSettings($backtrace['args'][1], false); } } if (($this->settings['tl_schema']['src']['botAPI'] ?? '') !== __DIR__.'/TL_botAPI.tl') { @@ -922,7 +910,7 @@ class MTProto extends AsyncConstruct implements TLCallback } } $this->startLoops(); - if (yield from $this->getSelf()) { + if (yield from $this->fullGetSelf()) { $this->authorized = self::LOGGED_IN; } if ($this->authorized === self::LOGGED_IN) { @@ -940,9 +928,13 @@ class MTProto extends AsyncConstruct implements TLCallback $this->updaters[false]->start(); } /** - * Destructor. + * Unreference instance, allowing destruction. + * + * @internal + * + * @return void */ - public function __destruct() + public function unreference() { $this->stopLoops(); if (isset($this->seqUpdater)) { @@ -957,13 +949,21 @@ class MTProto extends AsyncConstruct implements TLCallback if (isset($this->feeders[$channelId])) { $this->feeders[$channelId]->signal(true); } - if (!isset($this->updaters[$channelId])) { + if (isset($this->updaters[$channelId])) { $this->updaters[$channelId]->signal(true); } } foreach ($this->datacenter->getDataCenterConnections() as $datacenter) { $datacenter->disconnect(); } + } + /** + * Destructor. + */ + public function __destruct() + { + $this->logger('Shutting down MadelineProto (MTProto)'); + $this->unreference(); $this->logger("Successfully destroyed MadelineProto"); } /** @@ -1284,13 +1284,14 @@ class MTProto extends AsyncConstruct implements TLCallback return $settings; } /** - * Parse and store settings. + * Parse, update and store settings. * * @param array $settings Settings + * @param bool $reinit Whether to reinit the instance * * @return void */ - private function parseSettings(array $settings): void + public function updateSettings(array $settings, bool $reinit = true): \Generator { $settings = self::getSettings($settings, $this->settings); if ($settings['app_info'] === null) { @@ -1302,6 +1303,11 @@ class MTProto extends AsyncConstruct implements TLCallback } // Setup logger $this->setupLogger(); + + if ($reinit) { + $this->__construct(); + yield from $this->initAsynchronously(); + } } /** * Setup logger. @@ -1439,7 +1445,7 @@ class MTProto extends AsyncConstruct implements TLCallback $this->feeders[$channelId]->signal(true); unset($this->feeders[$channelId]); } - if (!isset($this->updaters[$channelId])) { + if (isset($this->updaters[$channelId])) { $this->updaters[$channelId]->signal(true); unset($this->updaters[$channelId]); } @@ -1487,7 +1493,7 @@ class MTProto extends AsyncConstruct implements TLCallback if (isset($this->feeders[$channelId])) { $this->feeders[$channelId]->signal(true); } - if (!isset($this->updaters[$channelId])) { + if (isset($this->updaters[$channelId])) { $this->updaters[$channelId]->signal(true); } } @@ -1505,7 +1511,7 @@ class MTProto extends AsyncConstruct implements TLCallback */ public function startUpdateSystem($anyway = false): void { - if ($this->asyncInitPromise && !$anyway) { + if (!$this->inited() && !$anyway) { $this->logger("Not starting update system"); return; } @@ -1652,11 +1658,20 @@ class MTProto extends AsyncConstruct implements TLCallback $this->datacenter->curdc = $curdc; } /** - * Get info about the logged-in user. + * Get info about the logged-in user, cached. * - * @return \Generator + * @return array|bool */ - public function getSelf(): \Generator + public function getSelf() + { + return $this->authorization['user'] ?? false; + } + /** + * Get info about the logged-in user, not cached. + * + * @return \Generator + */ + public function fullGetSelf(): \Generator { try { $this->authorization = ['user' => (yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [['_' => 'inputUserSelf']]], ['datacenter' => $this->datacenter->curdc]))[0]]; @@ -1718,6 +1733,19 @@ class MTProto extends AsyncConstruct implements TLCallback } } } + /** + * Get full list of MTProto and API methods. + * + * @return array + */ + public function getAllMethods(): array + { + $methods = []; + foreach ($this->getTL()->getMethods()->by_id as $method) { + $methods[] = $method['method']; + } + return \array_merge($methods, \get_class_methods(InternalDoc::class)); + } /** * Called right before serialization of method starts. * diff --git a/src/danog/MadelineProto/MTProtoTools/Files.php b/src/danog/MadelineProto/MTProtoTools/Files.php index 18ae74da..edb67793 100644 --- a/src/danog/MadelineProto/MTProtoTools/Files.php +++ b/src/danog/MadelineProto/MTProtoTools/Files.php @@ -22,8 +22,8 @@ namespace danog\MadelineProto\MTProtoTools; use Amp\ByteStream\InputStream; use Amp\ByteStream\IteratorStream; use Amp\ByteStream\OutputStream; -use Amp\ByteStream\ResourceOutputStream; use Amp\ByteStream\ResourceInputStream; +use Amp\ByteStream\ResourceOutputStream; use Amp\ByteStream\StreamException; use Amp\Deferred; use Amp\File\BlockingFile; diff --git a/src/danog/MadelineProto/Serialization.php b/src/danog/MadelineProto/Serialization.php index 2f8d3314..a8b25b7b 100644 --- a/src/danog/MadelineProto/Serialization.php +++ b/src/danog/MadelineProto/Serialization.php @@ -19,14 +19,109 @@ namespace danog\MadelineProto; +use function Amp\File\exists; +use function Amp\File\get; + /** * Manages serialization of the MadelineProto instance. */ class Serialization { - public static function realpaths($file) + /** + * Extract path components for serialization. + * + * @param string $file Session path + * + * @return array + */ + public static function realpaths(string $file): array { $file = Absolute::absolute($file); return ['file' => $file, 'lockfile' => $file . '.lock', 'tempfile' => $file . '.temp.session']; } + /** + * Unserialize legacy session. + * + * @param string $session Session name + * + * @internal + * + * @return \Generator + */ + public static function legacyUnserialize(string $session): \Generator + { + $realpaths = self::realpaths($session); + if (yield exists($realpaths['file'])) { + Logger::log('Waiting for shared lock of serialization lockfile...'); + $unlock = yield Tools::flock($realpaths['lockfile'], LOCK_SH); + Logger::log('Shared lock acquired, deserializing...'); + try { + $tounserialize = yield get($realpaths['file']); + } finally { + $unlock(); + } + \danog\MadelineProto\Magic::classExists(); + try { + $unserialized = \unserialize($tounserialize); + } catch (\danog\MadelineProto\Bug74586Exception $e) { + \class_exists('\\Volatile'); + $tounserialize = \str_replace('O:26:"danog\\MadelineProto\\Button":', 'O:35:"danog\\MadelineProto\\TL\\Types\\Button":', $tounserialize); + foreach (['RSA', 'TL\\TLMethods', 'TL\\TLConstructors', 'MTProto', 'API', 'DataCenter', 'Connection', 'TL\\Types\\Button', 'TL\\Types\\Bytes', 'APIFactory'] as $class) { + \class_exists('\\danog\\MadelineProto\\'.$class); + } + $unserialized = \danog\Serialization::unserialize($tounserialize); + } catch (\danog\MadelineProto\Exception $e) { + if ($e->getFile() === 'MadelineProto' && $e->getLine() === 1) { + throw $e; + } + if (\MADELINEPROTO_TEST === 'pony') { + throw $e; + } + \class_exists('\\Volatile'); + foreach (['RSA', 'TL\\TLMethods', 'TL\\TLConstructors', 'MTProto', 'API', 'DataCenter', 'Connection', 'TL\\Types\\Button', 'TL\\Types\\Bytes', 'APIFactory'] as $class) { + \class_exists('\\danog\\MadelineProto\\'.$class); + } + $changed = false; + if (\strpos($tounserialize, 'O:26:"danog\\MadelineProto\\Button":') !== false) { + Logger::log("SUBBING BUTTONS!"); + $tounserialize = \str_replace('O:26:"danog\\MadelineProto\\Button":', 'O:35:"danog\\MadelineProto\\TL\\Types\\Button":', $tounserialize); + $changed = true; + } + if (\strpos($e->getMessage(), "Erroneous data format for unserializing 'phpseclib\\Math\\BigInteger'") === 0) { + Logger::log("SUBBING BIGINTEGOR!"); + $tounserialize = \str_replace('phpseclib\\Math\\BigInteger', 'phpseclib\\Math\\BigIntegor', $tounserialize); + $changed = true; + } + if (\strpos($tounserialize, 'C:25:"phpseclib\\Math\\BigInteger"') !== false) { + Logger::log("SUBBING TGSECLIB old!"); + $tounserialize = \str_replace('C:25:"phpseclib\\Math\\BigInteger"', 'C:24:"tgseclib\\Math\\BigInteger"', $tounserialize); + $changed = true; + } + if (\strpos($tounserialize, 'C:26:"phpseclib3\\Math\\BigInteger"') !== false) { + Logger::log("SUBBING TGSECLIB!"); + $tounserialize = \str_replace('C:26:"phpseclib3\\Math\\BigInteger"', 'C:24:"tgseclib\\Math\\BigInteger"', $tounserialize); + $changed = true; + } + Logger::log((string) $e, Logger::ERROR); + if (!$changed) { + throw $e; + } + try { + $unserialized = \danog\Serialization::unserialize($tounserialize); + } catch (\Throwable $e) { + $unserialized = \unserialize($tounserialize); + } + } catch (\Throwable $e) { + Logger::log((string) $e, Logger::ERROR); + throw $e; + } + if ($unserialized instanceof \danog\PlaceHolder) { + $unserialized = \danog\Serialization::unserialize($tounserialize); + } + if ($unserialized === false) { + throw new Exception(\danog\MadelineProto\Lang::$current_lang['deserialization_error']); + } + return $unserialized; + } + } } diff --git a/src/danog/MadelineProto/TL/Conversion/BotAPI.php b/src/danog/MadelineProto/TL/Conversion/BotAPI.php index 6dfb24c6..1a07fc1e 100644 --- a/src/danog/MadelineProto/TL/Conversion/BotAPI.php +++ b/src/danog/MadelineProto/TL/Conversion/BotAPI.php @@ -540,6 +540,9 @@ trait BotAPI if (($arguments['message'] ?? '') === '' || !isset($arguments['parse_mode'])) { return $arguments; } + if (!(\is_string($arguments['message']) || \is_object($arguments['message']) && \method_exists($arguments['message'], '__toString'))) { + throw new Exception('Messages can only be strings'); + } if (isset($arguments['parse_mode']['_'])) { $arguments['parse_mode'] = \str_replace('textParseMode', '', $arguments['parse_mode']['_']); } diff --git a/src/danog/MadelineProto/Tools.php b/src/danog/MadelineProto/Tools.php index b0819c27..2229af66 100644 --- a/src/danog/MadelineProto/Tools.php +++ b/src/danog/MadelineProto/Tools.php @@ -30,6 +30,7 @@ use function Amp\ByteStream\getOutputBufferStream; use function Amp\ByteStream\getStdin; use function Amp\ByteStream\getStdout; use function Amp\File\exists; +use function Amp\File\get; use function Amp\Promise\all; use function Amp\Promise\any; use function Amp\Promise\first; @@ -304,6 +305,7 @@ trait Tools }); } catch (\Throwable $throwable) { Logger::log('Loop exceptionally stopped without resolving the promise', Logger::FATAL_ERROR); + Logger::log((string) $throwable, Logger::FATAL_ERROR); throw $throwable; } } while (!$resolved && !(Magic::$signaled && !$ignoreSignal)); @@ -411,15 +413,6 @@ trait Tools { if ($actual) { $promise = $actual; - } else { - $trace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]; - $file = ''; - if (isset($trace['file'])) { - $file .= \basename($trace['file'], '.php'); - } - if (isset($trace['line'])) { - $file .= ":{$trace['line']}"; - } } if ($promise instanceof \Generator) { $promise = new Coroutine($promise); @@ -632,22 +625,22 @@ trait Tools /** * Convert to camelCase. * - * @param string $input + * @param string $input String * * @return string */ - public static function fromSnakeCase(string $input): string + public static function toCamelCase(string $input): string { return \lcfirst(\str_replace('_', '', \ucwords($input, '_'))); } /** * Convert to snake_case. * - * @param string $input + * @param string $input String * * @return string */ - public static function fromCamelCase(string $input): string + public static function toSnakeCase(string $input): string { \preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches); $ret = $matches[0]; @@ -831,11 +824,34 @@ trait Tools * @return mixed * @access public */ - public static function getVar($obj, string $var) + public static function &getVar($obj, string $var) { - $reflection = new \ReflectionClass(\get_class($obj)); - $prop = $reflection->getProperty($var); - $prop->setAccessible(true); - return $prop->getValue($obj); + return \Closure::bind( + function & () use ($var) { + return $this->{$var}; + }, + $obj, + \get_class($obj) + )->__invoke(); + } + /** + * Sets a private variable in an object. + * + * @param object $obj Object + * @param string $var Attribute name + * @param mixed $val Attribute value + * + * @return mixed + * @access public + */ + public static function setVar($obj, string $var, &$val): void + { + \Closure::bind( + function () use ($var, &$val) { + $this->{$var} =& $val; + }, + $obj, + \get_class($obj) + )->__invoke(); } } diff --git a/src/danog/MadelineProto/Wrappers/Events.php b/src/danog/MadelineProto/Wrappers/Events.php index 082061be..5a63e6a5 100644 --- a/src/danog/MadelineProto/Wrappers/Events.php +++ b/src/danog/MadelineProto/Wrappers/Events.php @@ -82,7 +82,7 @@ trait Events $this->settings['updates']['callback'] = [$this, 'eventUpdateHandler']; $this->settings['updates']['handle_updates'] = true; $this->settings['updates']['run_callback'] = true; - if (!$this->asyncInitPromise) { + if ($this->inited()) { $this->startUpdateSystem(); } } diff --git a/src/danog/MadelineProto/Wrappers/Login.php b/src/danog/MadelineProto/Wrappers/Login.php index d4917c98..64809d66 100644 --- a/src/danog/MadelineProto/Wrappers/Login.php +++ b/src/danog/MadelineProto/Wrappers/Login.php @@ -170,7 +170,7 @@ trait Login $this->authorized = self::LOGGED_IN; yield from $this->initAuthorization(); yield from $this->getPhoneConfig(); - $res = (yield from $this->getSelf()); + $res = (yield from $this->fullGetSelf()); $callbacks = [$this, $this->referenceDatabase]; if (!($this->authorization['user']['bot'] ?? false)) { $callbacks[] = $this->minDatabase; @@ -189,7 +189,7 @@ trait Login if ($this->authorized !== self::LOGGED_IN) { throw new \danog\MadelineProto\Exception(\danog\MadelineProto\Lang::$current_lang['not_loggedIn']); } - yield from $this->getSelf(); + yield from $this->fullGetSelf(); $this->authorized_dc = $this->datacenter->curdc; return [$this->datacenter->curdc, $this->datacenter->getDataCenterConnection($this->datacenter->curdc)->getPermAuthKey()->getAuthKey()]; } diff --git a/src/danog/MadelineProto/Wrappers/Start.php b/src/danog/MadelineProto/Wrappers/Start.php index 3eafc617..e068011d 100644 --- a/src/danog/MadelineProto/Wrappers/Start.php +++ b/src/danog/MadelineProto/Wrappers/Start.php @@ -34,7 +34,7 @@ trait Start public function start(): \Generator { if ($this->authorized === self::LOGGED_IN) { - return yield from $this->getSelf(); + return yield from $this->fullGetSelf(); } if (PHP_SAPI === 'cli') { if (\strpos(yield Tools::readLine('Do you want to login as user or bot (u/b)? '), 'b') !== false) { @@ -50,7 +50,7 @@ trait Start } } $this->serialize(); - return yield from $this->getSelf(); + return yield from $this->fullGetSelf(); } if ($this->authorized === self::NOT_LOGGED_IN) { if (isset($_POST['phone_number'])) { @@ -81,7 +81,7 @@ trait Start } if ($this->authorized === self::LOGGED_IN) { $this->serialize(); - return yield from $this->getSelf(); + return yield from $this->fullGetSelf(); } exit; }