1
0
mirror of https://github.com/danog/MadelineProto.git synced 2024-12-02 12:17:47 +01:00
MadelineProto/src/API.php

464 lines
16 KiB
PHP

<?php
declare(strict_types=1);
/**
* API 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-2023 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\CancelledException;
use Amp\DeferredFuture;
use Amp\Future;
use Amp\Future\UnhandledFutureError;
use Amp\Ipc\Sync\ChannelledSocket;
use Amp\SignalException;
use Amp\TimeoutException;
use danog\MadelineProto\ApiWrappers\Start;
use danog\MadelineProto\Ipc\Client;
use danog\MadelineProto\Ipc\Server;
use danog\MadelineProto\Settings\Database\DriverDatabaseAbstract;
use danog\MadelineProto\Settings\Ipc as SettingsIpc;
use danog\MadelineProto\Settings\Logger as SettingsLogger;
use Revolt\EventLoop;
use Revolt\EventLoop\UncaughtThrowable;
use Throwable;
use Webmozart\Assert\Assert;
use function Amp\async;
use function Amp\Future\await;
use function Amp\Future\awaitFirst;
/**
* Main API wrapper for MadelineProto.
*/
final class API extends AbstractAPI
{
/**
* Release version.
*
* @var string
*/
public const RELEASE = '8.0.0-beta198';
/**
* We're not logged in.
*
* @var int
*/
public const NOT_LOGGED_IN = 0;
/**
* We're waiting for the login code.
*
* @var int
*/
public const WAITING_CODE = 1;
/**
* We're waiting for parameters to sign up.
*
* @var int
*/
public const WAITING_SIGNUP = -1;
/**
* We're waiting for the 2FA password.
*
* @var int
*/
public const WAITING_PASSWORD = 2;
/**
* We're logged in.
*
* @var int
*/
public const LOGGED_IN = 3;
/**
* We're logged out, the session will be deleted ASAP.
*
* @var int
*/
public const LOGGED_OUT = 4;
/**
* This peer is a user.
*
* @var string
*/
public const PEER_TYPE_USER = 'user';
/**
* This peer is a bot.
*
* @var string
*/
public const PEER_TYPE_BOT = 'bot';
/**
* This peer is a normal group.
*
* @var string
*/
public const PEER_TYPE_GROUP = 'chat';
/**
* This peer is a supergroup.
*
* @var string
*/
public const PEER_TYPE_SUPERGROUP = 'supergroup';
/**
* This peer is a channel.
*
* @var string
*/
public const PEER_TYPE_CHANNEL = 'channel';
/**
* Whether to generate only peer information.
*/
public const INFO_TYPE_PEER = 0;
/**
* Whether to generate only constructor information.
*/
public const INFO_TYPE_CONSTRUCTOR = 1;
/**
* Whether to generate only ID information.
*/
public const INFO_TYPE_ID = 2;
/**
* Whether to generate all information.
*/
public const INFO_TYPE_ALL = 3;
/**
* Whether to generate all usernames.
*/
public const INFO_TYPE_USERNAMES = 4;
/**
* Whether to generate just type info.
*/
public const INFO_TYPE_TYPE = 5;
use Start;
/**
* Session paths.
*
* @internal
*/
private SessionPaths $session;
/**
* Unlock callback.
*
* @var ?callable
*/
private $unlock = null;
/**
* Obtain the API ID UI template.
*/
public function getWebAPITemplate(): string
{
return $this->wrapper->getWebApiTemplate();
}
/**
* Set the API ID UI template.
*/
public function setWebApiTemplate(string $template): void
{
$this->wrapper->setWebApiTemplate($template);
}
/**
* Constructor function.
*
* @param string $session Session name
* @param SettingsAbstract $settings Settings
*/
public function __construct(string $session, ?SettingsAbstract $settings = null)
{
Magic::start(light: true);
$settings ??= new SettingsEmpty;
$this->session = new SessionPaths($session);
$this->wrapper = new APIWrapper($this->session);
$this->exportNamespaces();
Logger::constructorFromSettings($settings instanceof Settings
? $settings->getLogger()
: ($settings instanceof SettingsLogger ? $settings : new SettingsLogger));
if ($this->connectToMadelineProto($settings)) {
if (!$settings instanceof SettingsEmpty) {
EventLoop::queue($this->updateSettings(...), $settings);
}
return; // OK
}
if (!$settings instanceof Settings) {
$newSettings = new Settings;
$newSettings->merge($settings);
$settings = $newSettings;
}
$appInfo = $settings->getAppInfo();
if (!$appInfo->hasApiInfo()) {
if (!$appInfo->getShowPrompt()) {
throw new Exception("No API ID or API hash was provided, please specify them in the settings!");
}
$app = $this->APIStart($settings);
if (!$app) {
die();
}
$appInfo->setApiId($app['api_id']);
$appInfo->setApiHash($app['api_hash']);
}
$this->wrapper->setAPI(new MTProto($settings, $this->wrapper));
$this->wrapper->logger('Prompting initial serialization...');
$this->wrapper->serialize();
$this->wrapper->logger('Done initial serialization!');
$this->wrapper->logger(Lang::$current_lang['madelineproto_ready'], Logger::NOTICE);
}
/**
* Reconnect to full instance.
*/
protected function reconnectFull(): bool
{
if ($this->wrapper->getAPI() instanceof Client) {
$this->wrapper->logger('Restarting to full instance...');
try {
if (!isset($_GET['MadelineSelfRestart']) && (($this->hasEventHandler()) || !($this->isIpcWorker()))) {
$this->wrapper->logger('Restarting to full instance: the bot is already running!');
Tools::closeConnection($this->getWebMessage(Lang::$current_lang['botAlreadyRunning']));
return false;
}
$this->wrapper->logger('Restarting to full instance: stopping IPC server...');
$this->wrapper->getAPI()->stopIpcServer();
} catch (SecurityException|SignalException $e) {
throw $e;
} catch (Throwable $e) {
if ($e instanceof UncaughtThrowable) {
$e = $e->getPrevious();
if ($e instanceof SecurityException || $e instanceof SignalException) {
throw $e;
}
}
$this->wrapper->logger("Restarting to full instance: error $e");
}
$this->wrapper->logger('Restarting to full instance: reconnecting...');
$cancel = new DeferredFuture;
$cb = function () use ($cancel, &$cb): void {
[$result] = Serialization::tryConnect($this->session->getIpcPath(), $cancel->getFuture());
if ($result instanceof ChannelledSocket) {
try {
if (!$this->wrapper->getAPI() instanceof Client) {
$this->wrapper->logger('Restarting to full instance (again): the bot is already running!');
$result->disconnect();
return;
}
$this->wrapper->logger('Restarting to full instance (again): sending shutdown signal!');
$result->send(Server::SHUTDOWN);
$result->disconnect();
} catch (SecurityException|SignalException $e) {
throw $e;
} catch (Throwable $e) {
if ($e instanceof UncaughtThrowable) {
$e = $e->getPrevious();
if ($e instanceof SecurityException || $e instanceof SignalException) {
throw $e;
}
}
$this->wrapper->logger("Restarting to full instance: error in stop loop $e");
}
EventLoop::queue($cb);
}
};
EventLoop::queue($cb);
$this->connectToMadelineProto(new SettingsEmpty, true);
$cancel->complete(new Exception('Connected!'));
}
return true;
}
/**
* Connect to MadelineProto.
*
* @param SettingsAbstract $settings Settings
* @param bool $forceFull Whether to force full initialization
*/
protected function connectToMadelineProto(SettingsAbstract $settings, bool $forceFull = false, bool $tryReconnect = true): bool
{
if ($settings instanceof SettingsIpc) {
$forceFull = $forceFull || $settings->getSlow();
} elseif ($settings instanceof Settings) {
$forceFull = $forceFull || $settings->getIpc()->getSlow();
$db = $settings->getDb();
if ($db instanceof DriverDatabaseAbstract) {
$forceFull = (bool) $db->getEphemeralFilesystemPrefix();
}
} elseif ($settings instanceof DriverDatabaseAbstract) {
$forceFull = (bool) $settings->getEphemeralFilesystemPrefix();
}
$forceFull = $forceFull || isset($_GET['MadelineSelfRestart']) || Magic::$altervista;
try {
[$unserialized, $this->unlock] = async(
Serialization::unserialize(...),
$this->session,
$settings,
$forceFull
)->await(Tools::getTimeoutCancellation(30.0));
} catch (CancelledException $e) {
if (!$e->getPrevious() instanceof TimeoutException) {
throw $e;
}
[$unserialized, $this->unlock] = [0, null];
}
if ($unserialized === 0) {
// Timeout
throw new Exception(Lang::$current_lang['could_not_connect_to_MadelineProto']);
} elseif ($unserialized instanceof Throwable) {
// IPC server error
throw $unserialized;
} elseif ($unserialized instanceof ChannelledSocket) {
// Success, IPC client
$this->wrapper->setAPI(new Client($unserialized, $this->session, Logger::$default));
return true;
} elseif ($unserialized) {
// Success, full session
$this->wrapper->getAPI()?->unreference();
$this->wrapper = $unserialized;
$this->wrapper->setSession($this->session);
$this->exportNamespaces();
if ($this->wrapper->getAPI()) {
unset($unserialized);
if ($settings instanceof SettingsIpc) {
$settings = new SettingsEmpty;
}
$this->wrapper->getAPI()->wakeup($settings, $this->wrapper);
$this->wrapper->logger(Lang::$current_lang['madelineproto_ready'], Logger::NOTICE);
return true;
}
}
return false;
}
/**
* Wakeup function.
*/
public function __wakeup(): void
{
$this->__construct($this->session->getSessionDirectoryPath());
}
public function __sleep(): array
{
return ['session'];
}
/**
* @var array<Future<null>>
*/
private static array $destructors = [];
/**
* @internal
*/
public static function finalize(): void
{
if (self::$destructors) {
await(self::$destructors);
}
}
/**
* Destruct function.
*
* @internal
*/
public function __destruct()
{
$id = \count(self::$destructors);
self::$destructors[$id] = async(function () use ($id): void {
$this->wrapper->logger('Shutting down MadelineProto ('.static::class.')');
$this->wrapper->getAPI()?->unreference();
if (isset($this->wrapper)) {
$this->wrapper->logger('Prompting final serialization...');
$this->wrapper->serialize();
$this->wrapper->logger('Done final serialization!');
}
if ($this->unlock) {
($this->unlock)();
}
unset(self::$destructors[$id]);
});
}
/**
* Start multiple instances of MadelineProto and the event handlers (enables async).
*
* @param array<API> $instances Instances of madeline
* @param array<class-string<EventHandler>>|class-string<EventHandler> $eventHandler Event handler(s)
*/
public static function startAndLoopMulti(array $instances, array|string $eventHandler): void
{
if (\is_string($eventHandler)) {
Assert::classExists($eventHandler);
$eventHandler::cachePlugins($eventHandler);
$eventHandler = array_fill_keys(array_keys($instances), $eventHandler);
} else {
Assert::notEmpty($eventHandler);
Assert::allClassExists($eventHandler);
foreach ($eventHandler as $c) {
$c::cachePlugins($c);
}
}
$errors = [];
$started = array_fill_keys(array_keys($instances), false);
$instanceOne = array_values($instances)[0];
$prev = EventLoop::getErrorHandler();
EventLoop::setErrorHandler(
$cb = static function (Throwable $e) use ($instanceOne, &$errors, &$started, $eventHandler): void {
if ($e instanceof UnhandledFutureError) {
$e = $e->getPrevious();
}
if ($e instanceof SecurityException || $e instanceof SignalException) {
throw $e;
}
if (str_starts_with($e->getMessage(), 'Could not connect to DC ')) {
throw $e;
}
$t = time();
$errors = [$t => $errors[$t] ?? 0];
$errors[$t]++;
if ($errors[$t] > 10 && array_sum($started) !== \count($eventHandler)) {
$instanceOne->wrapper->logger('More than 10 errors in a second and not inited, exiting!', Logger::FATAL_ERROR);
return;
}
echo $e;
$instanceOne->wrapper->logger((string) $e, Logger::FATAL_ERROR);
$instanceOne->report("Surfaced: $e");
}
);
try {
$promises = [];
foreach ($instances as $k => $instance) {
$instance->start();
$promises []= async(static function () use ($k, $instance, $eventHandler, &$started): void {
$instance->startAndLoopLogic($eventHandler[$k], $started[$k]);
});
}
awaitFirst($promises);
} finally {
if (EventLoop::getErrorHandler() === $cb) {
EventLoop::setErrorHandler($prev);
}
}
}
}