Refactor and clean up serialization and APIFactory APIs, automatically unreference instance on destruction to clean up memory

This commit is contained in:
Daniil Gentili 2020-02-25 18:02:32 +01:00
parent 82599e686e
commit b5267beaa2
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
20 changed files with 779 additions and 493 deletions

View File

@ -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);

View File

@ -0,0 +1,188 @@
<?php
/**
* API wrapper module.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @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;
})());
}
}

View File

@ -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]);
}
}

View File

@ -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;

View File

@ -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.');

View File

@ -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 = '<!DOCTYPE html>
<html>
<head>
<title>MadelineProto</title>
</head>
<body>
<h1>MadelineProto</h1>
<p>%s</p>
<form method="POST">
%s
<button type="submit"/>Go</button>
</form>
</body>
</html>';
private function webAPIEchoTemplate($message, $form)
/**
* API template.
*
* @var string
*/
private $webApiTemplate = '<!DOCTYPE html><html><head><title>MadelineProto</title></head><body><h1>MadelineProto</h1><p>%s</p><form method="POST">%s<button type="submit"/>Go</button></form></body></html>';
/**
* 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<br><b>' . $message . '</b><ol>
yield $stdout->write($this->webAPIEchoTemplate('Enter your API ID and API hash<br><b>'.$message.'</b><ol>
<li>Login to my.telegram.org</li>
<li>Go to API development tools</li>
<li>
@ -79,19 +86,19 @@ trait ApiTemplates
<li>Click on create application</li>
</ol>', '<input type="string" name="api_id" placeholder="API ID" required/><input type="string" name="api_hash" placeholder="API hash" required/>'));
} else {
yield $stdout->write($this->webAPIEchoTemplate('Enter a phone number that is <b>already registered</b> on telegram to get the API ID<br><b>' . $message . '</b>', '<input type="text" name="phone_number" placeholder="Phone number" required/>'));
yield $stdout->write($this->webAPIEchoTemplate('Enter a phone number that is <b>already registered</b> on telegram to get the API ID<br><b>'.$message.'</b>', '<input type="text" name="phone_number" placeholder="Phone number" required/>'));
}
} else {
if ($message) {
$message = '<br><br>' . $message;
$message = '<br><br>'.$message;
}
yield $stdout->write($this->webAPIEchoTemplate('Do you want to enter the API id and the API hash manually or automatically?<br>Note that you can also provide it directly in the code using the <a href="https://docs.madelineproto.xyz/docs/SETTINGS.html#settingsapp_infoapi_id">settings</a>.<b>' . $message . '</b>', '<select name="type"><option value="automatic">Automatically</option><option value="manual">Manually</option></select>'));
yield $stdout->write($this->webAPIEchoTemplate('Do you want to enter the API id and the API hash manually or automatically?<br>Note that you can also provide it directly in the code using the <a href="https://docs.madelineproto.xyz/docs/SETTINGS.html#settingsapp_infoapi_id">settings</a>.<b>'.$message.'</b>', '<select name="type"><option value="automatic">Automatically</option><option value="manual">Manually</option></select>'));
}
} else {
if (!$this->my_telegram_org_wrapper->loggedIn()) {
yield $stdout->write($this->webAPIEchoTemplate('Enter your code<br><b>' . $message . '</b>', '<input type="text" name="code" placeholder="Code" required/>'));
if (!$this->myTelegramOrgWrapper->loggedIn()) {
yield $stdout->write($this->webAPIEchoTemplate('Enter your code<br><b>'.$message.'</b>', '<input type="text" name="code" placeholder="Code" required/>'));
} else {
yield $stdout->write($this->webAPIEchoTemplate('Enter the API info<br><b>' . $message . '</b>', '<input type="hidden" name="creating_app" value="yes" required/>
yield $stdout->write($this->webAPIEchoTemplate('Enter the API info<br><b>'.$message.'</b>', '<input type="hidden" name="creating_app" value="yes" required/>
Enter the app name, can be anything: <br><input type="text" name="app_title" required/><br>
<br>Enter the app&apos;s short name, alphanumeric, 5-32 chars: <br><input type="text" name="app_shortname" required/><br>
<br>Enter the app/website URL, or https://t.me/yourusername: <br><input type="text" name="app_url" required/><br>

View File

@ -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;
});
);
}
}

View File

@ -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);
}
}
/**

View File

@ -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<array>
* @return array|bool
*/
public function getSelf(array $extra = [])
{
return $this->__call(__FUNCTION__, [$extra]);
}
/**
* Get info about the logged-in user, not cached.
*
* @return \Generator<array|bool>
*/
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.

View File

@ -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;

View File

@ -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()

View File

@ -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;
}
}
}

View File

@ -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<array>
* @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<array|bool>
*/
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.
*

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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']['_']);
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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()];
}

View File

@ -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;
}