diff --git a/README.md b/README.md index 4e8e57e..0eb71bf 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,13 @@ Fast, simple, async php telegram api server: * `http://127.0.0.1:9503/api/session/getSelf` Each session is store in `{$session}.madeline` file in root folder of library. + * EventHandler updates via websocket. Connect to `ws://127.0.0.1:9503/events`. You will get all events in json. + Each event stored inside object, where key is name of session which created event. + + When using CombinedAPI (multiple account) name of session can be added to path of websocket endpoint. + `ws://127.0.0.1:9503/events/session_name`. This endpoint will emmit events only from given session. + + PHP websocket client example: [websocket-events.php](https://github.com/xtrime-ru/TelegramApiServer/blob/master/examples/websocket-events.php) Examples: * get_info about channel/user: `http://127.0.0.1:9503/api/getInfo/?id=@xtrime` diff --git a/composer.json b/composer.json index 40a7be8..cdcf0f5 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,8 @@ "php": ">=7.4.0", "ext-json": "*", "amphp/http-server": "^2", + "amphp/http-server-router": "^1", + "amphp/websocket-server": "^2", "vlucas/phpdotenv": "^4", "danog/madelineproto":"^5" }, diff --git a/composer.lock b/composer.lock index 4e26b7c..73201ef 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8d2aca34a6b1620d5e86005db622b2dd", + "content-hash": "634e5782aa030ceebc6caeed35e42977", "packages": [ { "name": "amphp/amp", @@ -340,16 +340,16 @@ }, { "name": "amphp/hpack", - "version": "v3.0.0", + "version": "v3.1.0", "source": { "type": "git", "url": "https://github.com/amphp/hpack.git", - "reference": "84fb1373b8a3cfdf7462a87a3e79efe503f0e101" + "reference": "0dcd35f9a8d9fc04d5fb8af0aeb109d4474cfad8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/hpack/zipball/84fb1373b8a3cfdf7462a87a3e79efe503f0e101", - "reference": "84fb1373b8a3cfdf7462a87a3e79efe503f0e101", + "url": "https://api.github.com/repos/amphp/hpack/zipball/0dcd35f9a8d9fc04d5fb8af0aeb109d4474cfad8", + "reference": "0dcd35f9a8d9fc04d5fb8af0aeb109d4474cfad8", "shasum": "" }, "require": { @@ -394,7 +394,7 @@ "hpack", "http-2" ], - "time": "2019-12-12T21:37:06+00:00" + "time": "2020-01-11T19:33:14+00:00" }, { "name": "amphp/http", @@ -666,6 +666,69 @@ ], "time": "2020-01-04T18:10:10+00:00" }, + { + "name": "amphp/http-server-router", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/http-server-router.git", + "reference": "c6a1731f3833f3a4b4e4cd633889eb14b5ef635b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/http-server-router/zipball/c6a1731f3833f3a4b4e4cd633889eb14b5ef635b", + "reference": "c6a1731f3833f3a4b4e4cd633889eb14b5ef635b", + "shasum": "" + }, + "require": { + "amphp/http": "^1", + "amphp/http-server": "^2 || ^1 || ^0.8", + "cash/lrucache": "^1", + "nikic/fast-route": "^1" + }, + "require-dev": { + "amphp/log": "^1", + "amphp/phpunit-util": "^1", + "friendsofphp/php-cs-fixer": "^2.3", + "league/uri-schemes": "^1.1", + "phpunit/phpunit": "^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Http\\Server\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Router responder for Amp's HTTP server.", + "homepage": "https://github.com/amphp/http-server-router", + "keywords": [ + "http", + "router", + "server" + ], + "time": "2019-08-21T15:51:20+00:00" + }, { "name": "amphp/parallel", "version": "v1.2.0", @@ -1140,6 +1203,80 @@ ], "time": "2019-12-22T13:22:00+00:00" }, + { + "name": "amphp/websocket-server", + "version": "v2.0.0-rc1", + "source": { + "type": "git", + "url": "https://github.com/amphp/websocket-server.git", + "reference": "8c723e902a56a41eefbf30d7ee1475755743daa2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/websocket-server/zipball/8c723e902a56a41eefbf30d7ee1475755743daa2", + "reference": "8c723e902a56a41eefbf30d7ee1475755743daa2", + "shasum": "" + }, + "require": { + "amphp/amp": "^2.2", + "amphp/byte-stream": "^1.6.1", + "amphp/http": "^1.3", + "amphp/http-server": "^2", + "amphp/socket": "^1", + "amphp/websocket": "^1", + "php": ">=7.1" + }, + "require-dev": { + "amphp/http-client": "^4", + "amphp/http-server-router": "^1.0.2", + "amphp/http-server-static-content": "^1.0.4", + "amphp/log": "^1", + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.1", + "infection/infection": "^0.9.3", + "league/climate": "^3", + "league/uri-schemes": "^1.1", + "phpunit/phpunit": "^8 || ^7" + }, + "suggest": { + "ext-zlib": "Required for compression" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Websocket\\Server\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Websocket server for Amp's HTTP server.", + "homepage": "https://github.com/amphp/websocket-server", + "keywords": [ + "http", + "server", + "websocket" + ], + "time": "2019-08-21T17:09:20+00:00" + }, { "name": "amphp/windows-registry", "version": "v0.3.2", @@ -1965,6 +2102,52 @@ ], "time": "2018-11-22T07:55:51+00:00" }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "FastRoute\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "time": "2018-02-13T20:26:39+00:00" + }, { "name": "paragonie/constant_time_encoding", "version": "v1.0.4", diff --git a/examples/websocket-events.php b/examples/websocket-events.php new file mode 100644 index 0000000..159201a --- /dev/null +++ b/examples/websocket-events.php @@ -0,0 +1,35 @@ + $options['url'] ?? $options['u'] ?? 'ws://127.0.0.1:9503/events', +]; + +Amp\Loop::run(function () use($options) { + echo "Connecting to: {$options['url']}" . PHP_EOL; + + /** @var Connection $connection */ + $connection = yield connect($options['url']); + + echo 'Waiting for events...' . PHP_EOL; + while ($message = yield $connection->receive()) { + /** @var Message $message */ + $payload = yield $message->buffer(); + printf("Received event: %s\n", $payload); + } +}); \ No newline at end of file diff --git a/server.php b/server.php index 1544a2d..a67725c 100644 --- a/server.php +++ b/server.php @@ -53,7 +53,7 @@ foreach ($options['session'] as $session) { if (!$session) { $session = 'session'; } - $session = TelegramApiServer\Client::getSessionFileName($session); + $session = TelegramApiServer\Client::getSessionFile($session); $sessionFiles[$session] = ''; } diff --git a/src/Client.php b/src/Client.php index 2a69795..7befc08 100644 --- a/src/Client.php +++ b/src/Client.php @@ -2,13 +2,14 @@ namespace TelegramApiServer; +use Amp\Loop; use danog\MadelineProto; +use TelegramApiServer\EventHandlers\EventHandler; class Client { - /** @var MadelineProto\CombinedAPI */ - public MadelineProto\CombinedAPI $MadelineProto; - private ?string $defaultSession = null; + private static string $sessionExtension = '.madeline'; + public ?MadelineProto\CombinedAPI $MadelineProtoCombined = null; /** * Client constructor. @@ -28,9 +29,6 @@ class Client } unset($session); - if (count($sessions) === 1) { - $this->defaultSession = (string) array_key_first($sessions); - } $this->connect($sessions); } @@ -39,9 +37,24 @@ class Client * * @return string|null */ - public static function getSessionFileName(?string $session): ?string + public static function getSessionFile(?string $session): ?string { - return $session ? "{$session}.madeline" : null; + return $session ? ($session . static::$sessionExtension) : null; + } + + public static function getSessionName(?string $sessionFile): ?string + { + if (!$sessionFile) { + return null; + } + + $extensionPosition = strrpos($sessionFile, static::$sessionExtension); + if($extensionPosition === false) { + return null; + } + + $sessionName = substr_replace($sessionFile, '', $extensionPosition, strlen(static::$sessionExtension)); + return $sessionName ?: null; } /** @@ -52,17 +65,27 @@ class Client //При каждой инициализации настройки обновляются из массива $config echo PHP_EOL . 'Starting MadelineProto...' . PHP_EOL; $time = microtime(true); - $this->MadelineProto = new MadelineProto\CombinedAPI('combined_session.madeline', $sessions); - $this->MadelineProto->async(true); - $this->MadelineProto->loop(function() use($sessions) { - $res = []; + $this->MadelineProtoCombined = new MadelineProto\CombinedAPI('combined_session.madeline', $sessions); + //В сессии могут быть ссылки на несуществующие классы после обновления кода. Она нам не нужна. + $this->MadelineProtoCombined->session = null; + + $this->MadelineProtoCombined->async(true); + $this->MadelineProtoCombined->loop(function() use($sessions) { + $promises = []; foreach ($sessions as $session => $message) { MadelineProto\Logger::log("Starting session: {$session}", MadelineProto\Logger::WARNING); - $res[] = $this->MadelineProto->instances[$session]->start(); + $promises[]= $this->MadelineProtoCombined->instances[$session]->start(); } - yield $this->MadelineProto->all($res); + yield $this->MadelineProtoCombined::all($promises); + + $this->MadelineProtoCombined->setEventHandler(EventHandler::class); }); + + Loop::defer(function() { + $this->MadelineProtoCombined->loop(); + }); + $time = round(microtime(true) - $time, 3); $sessionsCount = count($sessions); MadelineProto\Logger::log( @@ -78,21 +101,24 @@ class Client * * @return MadelineProto\API */ - public function getInstance(?string $session): MadelineProto\API + public function getInstance(?string $session = null): MadelineProto\API { - $session = static::getSessionFileName($session) ?: $this->defaultSession; + if (count($this->MadelineProtoCombined->instances) === 1) { + $session = (string) array_key_first($this->MadelineProtoCombined->instances); + } else { + $session = static::getSessionFile($session); + } if (!$session) { throw new \InvalidArgumentException('Multiple sessions detected. You need to specify which session to use'); } - if (empty($this->MadelineProto->instances[$session])) { + if (empty($this->MadelineProtoCombined->instances[$session])) { throw new \InvalidArgumentException('Session not found'); } - return $this->MadelineProto->instances[$session]; + return $this->MadelineProtoCombined->instances[$session]; } - } diff --git a/src/CustomMethods.php b/src/ClientCustomMethods.php similarity index 99% rename from src/CustomMethods.php rename to src/ClientCustomMethods.php index 1da6331..a4d0ab0 100644 --- a/src/CustomMethods.php +++ b/src/ClientCustomMethods.php @@ -8,7 +8,7 @@ use danog\MadelineProto\TL\Conversion\BotAPI; use function Amp\call; use \danog\MadelineProto; -class CustomMethods +class ClientCustomMethods { use BotAPI; diff --git a/src/Config.php b/src/Config.php index aaecfb8..3568cb5 100644 --- a/src/Config.php +++ b/src/Config.php @@ -6,14 +6,11 @@ namespace TelegramApiServer; class Config { - /** - * @var self - */ - private static $instance; - private $config; + private static ?Config $instance = null; + private array $config; - public static function getInstance() + public static function getInstance(): Config { if (null === static::$instance) { static::$instance = new static(); @@ -57,13 +54,13 @@ class Config private function findByKey($key) { - $key = (string)$key; + $key = (string) $key; $path = explode('.', $key); $value = &$this->config; foreach ($path as $pathKey) { if (!is_array($value) || !array_key_exists($pathKey, $value)) { - return; + return null; } $value = &$value[$pathKey]; } diff --git a/src/RequestCallback.php b/src/Controllers/ApiController.php similarity index 71% rename from src/RequestCallback.php rename to src/Controllers/ApiController.php index 1c1a104..395bd9a 100644 --- a/src/RequestCallback.php +++ b/src/Controllers/ApiController.php @@ -1,19 +1,22 @@ [ 'Content-Type'=>'application/json;charset=utf-8', ], @@ -24,8 +27,23 @@ class RequestCallback ]; private array $parameters = []; private array $api; - private string $session = ''; + private ?string $session = ''; + public static function getRouterCallback($client): CallableRequestHandler + { + return new CallableRequestHandler( + static function (Request $request) use($client) { + $requestCallback = new static($client); + $response = yield from $requestCallback->process($request); + + return new Response( + $requestCallback->page['code'], + $requestCallback->page['headers'], + $response + ); + } + ); + } /** * RequestCallback constructor. @@ -34,9 +52,8 @@ class RequestCallback */ public function __construct(Client $client) { - $this->ipWhiteList = (array)Config::getInstance()->get('api.ip_whitelist', []); + $this->ipWhiteList = (array) Config::getInstance()->get('api.ip_whitelist', []); $this->client = $client; - } /** @@ -52,7 +69,7 @@ class RequestCallback } yield from $this - ->resolvePage($request->getUri()->getPath()) + ->resolvePath($request->getAttribute(Router::class)) ->resolveRequest($request->getUri()->getQuery(), $body, $request->getHeader('Content-Type')) ->generateResponse($request) ; @@ -60,38 +77,29 @@ class RequestCallback return $this->getResponse(); } - /** - * Определяет какую страницу запросили + * Получаем параметры из uri * - * @param $uri - * @return RequestCallback + * @param array $path + * + * @return ApiController */ - private function resolvePage($uri): self + private function resolvePath(array $path): self { - preg_match("~/(?'page'[^/]*)(?:/(?'session'[^/]*))?/(?'method'[^/]*)~", $uri, $matches); - - $page = $matches['page'] ?? null; - $this->session = $matches['session'] ?? null; - $this->api = explode('.', $matches['method'] ?? ''); - - if (!in_array($page, self::PAGES, true)) { - $this->setPageCode(404); - $this->page['errors'][] = 'Incorrect path'; - } - if (count($this->api) === 0) { - $this->setPageCode(404); - $this->page['errors'][] = 'No method specified'; - } + $this->session = $path['session'] ?? null; + $this->api = explode('.', $path['method'] ?? ''); return $this; } /** + * Получаем параметры из GET и POST + * * @param string $query * @param string|null $body * @param string|null $contentType - * @return RequestCallback + * + * @return ApiController */ private function resolveRequest(string $query, $body, $contentType) { @@ -115,7 +123,8 @@ class RequestCallback * Получает посты для формирования ответа * * @param Request $request - * @return RequestCallback + * + * @return ApiController * @throws \Throwable */ private function generateResponse(Request $request) @@ -151,8 +160,8 @@ class RequestCallback private function callApi() { $pathSize = count($this->api); - if ($pathSize === 1 && is_callable([CustomMethods::class,$this->api[0]])) { - $customMethods = new CustomMethods($this->client->getInstance($this->session)); + if ($pathSize === 1 && is_callable([ClientCustomMethods::class,$this->api[0]])) { + $customMethods = new ClientCustomMethods($this->client->getInstance($this->session)); $result = $customMethods->{$this->api[0]}(...$this->parameters); } else { //Проверяем нет ли в MadilineProto такого метода. @@ -176,7 +185,8 @@ class RequestCallback /** * @param \Throwable $e - * @return RequestCallback + * + * @return ApiController * @throws \Throwable */ private function setError(\Throwable $e): self @@ -221,16 +231,24 @@ class RequestCallback $data['success'] = 1; } - $result = json_encode($data, JSON_INVALID_UTF8_SUBSTITUTE|JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $result = json_encode( + $data, + JSON_THROW_ON_ERROR | + JSON_INVALID_UTF8_SUBSTITUTE | + JSON_PRETTY_PRINT | + JSON_UNESCAPED_SLASHES | + JSON_UNESCAPED_UNICODE + ); - return $result; + return $result . "\n"; } /** * Устанавливает http код ответа (200, 400, 404 и тд.) * * @param int $code - * @return RequestCallback + * + * @return ApiController */ private function setPageCode(int $code): self { diff --git a/src/Controllers/EventsController.php b/src/Controllers/EventsController.php new file mode 100644 index 0000000..62bd54b --- /dev/null +++ b/src/Controllers/EventsController.php @@ -0,0 +1,71 @@ +client = $client; + return $class; + } + + public function onHandshake(Request $request, Response $response): Promise + { + try { + $session = $request->getAttribute(Router::class)['session'] ?? null; + if ($session) { + $this->client->getInstance($session); + } + } catch (\Throwable $e){ + $response->setStatus(400); + $response->setBody($e->getMessage()); + } + + return new Success($response); + } + + public function onConnect(\Amp\Websocket\Client $client, Request $request, Response $response): Promise + { + return call(function() use($client, $request) { + $requestedSession = $request->getAttribute(Router::class)['session'] ?? null; + + $this->subscribeForUpdates($client, $requestedSession); + + while ($message = yield $client->receive()) { + // Messages received on the connection are ignored and discarded. + // Messages must be received properly to maintain connection with client (ping-pong check). + } + }); + } + + private function subscribeForUpdates(\Amp\Websocket\Client $client, ?string $requestedSession): void + { + $clientId = $client->getId(); + + $client->onClose(static function() use($clientId) { + EventHandler::removeEventListener($clientId); + }); + + EventHandler::addEventListener($clientId, function($update, string $session) use($clientId, $requestedSession) { + if ($requestedSession && $session !== $requestedSession) { + return; + } + $update = [$session => $update]; + $this->multicast(json_encode($update, JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_UNICODE), [$clientId]); + }); + } +} \ No newline at end of file diff --git a/src/EventHandlers/EventHandler.php b/src/EventHandlers/EventHandler.php new file mode 100644 index 0000000..cc05f8d --- /dev/null +++ b/src/EventHandlers/EventHandler.php @@ -0,0 +1,39 @@ + $callback) { + Logger::log("Pass update to callback. ClientId: {$clientId}"); + $callback($update, $session); + } + } +} \ No newline at end of file diff --git a/src/Logger.php b/src/Logger.php index 1be9c28..637537e 100644 --- a/src/Logger.php +++ b/src/Logger.php @@ -11,9 +11,15 @@ namespace TelegramApiServer; +use DateTimeInterface; use Psr\Log\AbstractLogger; use Psr\Log\InvalidArgumentException; use Psr\Log\LogLevel; +use danog\MadelineProto; +use function get_class; +use function gettype; +use function is_object; +use const PHP_EOL; /** * Minimalist PSR-3 logger designed to write in stderr or any other stream. @@ -22,7 +28,7 @@ use Psr\Log\LogLevel; */ class Logger extends AbstractLogger { - private static $levels = [ + private static array $levels = [ LogLevel::DEBUG => 0, LogLevel::INFO => 1, LogLevel::NOTICE => 2, @@ -33,8 +39,20 @@ class Logger extends AbstractLogger LogLevel::EMERGENCY => 7, ]; - private $minLevelIndex; - private $formatter; + private static array $madelineLevels = [ + LogLevel::DEBUG => MadelineProto\Logger::ULTRA_VERBOSE, + LogLevel::INFO => MadelineProto\Logger::VERBOSE, + LogLevel::NOTICE => MadelineProto\Logger::NOTICE, + LogLevel::WARNING => MadelineProto\Logger::WARNING, + LogLevel::ERROR => MadelineProto\Logger::ERROR, + LogLevel::CRITICAL => MadelineProto\Logger::FATAL_ERROR, + LogLevel::ALERT => MadelineProto\Logger::FATAL_ERROR, + LogLevel::EMERGENCY => MadelineProto\Logger::FATAL_ERROR, + ]; + + private static string $dateTimeFormat = 'Y-m-d H:i:s'; + private int $minLevelIndex; + private array $formatter; public function __construct(string $minLevel = LogLevel::WARNING, callable $formatter = null) { @@ -60,7 +78,7 @@ class Logger extends AbstractLogger /** * {@inheritdoc} */ - public function log($level, $message, array $context = []) + public function log($level, $message, array $context = []): void { if (!isset(self::$levels[$level])) { throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level)); @@ -72,8 +90,7 @@ class Logger extends AbstractLogger $formatter = $this->formatter; - //TODO: Convert LogLevel to MadelineProto loglevels. - \danog\MadelineProto\Logger::log($formatter($level, $message, $context), \danog\MadelineProto\Logger::NOTICE); + MadelineProto\Logger::log($formatter($level, $message, $context), static::$madelineLevels[$level]); } private function format(string $level, string $message, array $context): string @@ -81,20 +98,20 @@ class Logger extends AbstractLogger if (false !== strpos($message, '{')) { $replacements = []; foreach ($context as $key => $val) { - if (null === $val || is_scalar($val) || (\is_object($val) && method_exists($val, '__toString'))) { + if (null === $val || is_scalar($val) || (is_object($val) && method_exists($val, '__toString'))) { $replacements["{{$key}}"] = $val; - } elseif ($val instanceof \DateTimeInterface) { - $replacements["{{$key}}"] = $val->format(\DateTime::RFC3339); - } elseif (\is_object($val)) { - $replacements["{{$key}}"] = '[object '.\get_class($val).']'; + } elseif ($val instanceof DateTimeInterface) { + $replacements["{{$key}}"] = $val->format(static::$dateTimeFormat); + } elseif (is_object($val)) { + $replacements["{{$key}}"] = '[object '. get_class($val).']'; } else { - $replacements["{{$key}}"] = '['.\gettype($val).']'; + $replacements["{{$key}}"] = '['. gettype($val).']'; } } $message = strtr($message, $replacements); } - return sprintf('%s [%s] %s', date(\DateTime::RFC3339), $level, $message).\PHP_EOL; + return sprintf('[%s] [%s] %s', date(static::$dateTimeFormat), $level, $message). PHP_EOL; } } \ No newline at end of file diff --git a/src/Server.php b/src/Server.php index 0039625..96f8080 100644 --- a/src/Server.php +++ b/src/Server.php @@ -4,16 +4,14 @@ namespace TelegramApiServer; use Amp; use Amp\Http\Server\RequestHandler\CallableRequestHandler; -use Amp\Promise; -use Amp\Socket; use Amp\Http\Server\Request; use Amp\Http\Server\Response; use Psr\Log\LogLevel; +use TelegramApiServer\Controllers\ApiController; +use TelegramApiServer\Controllers\EventsController; class Server { - private $config = []; - /** * Server constructor. * @param Client $client @@ -21,31 +19,10 @@ class Server */ public function __construct(Client $client, array $options) { - $this->setConfig($options); - - Amp\Loop::run(function () use ($client) { - $sockets = [ - Socket\listen("{$this->config['address']}:{$this->config['port']}"), - ]; - + Amp\Loop::run(function () use ($client, $options) { $server = new Amp\Http\Server\Server( - $sockets, - new CallableRequestHandler(function (Request $request) use($client) { - //На каждый запрос должны создаваться новые экземпляры классов парсера и коллбеков, - //иначе их данные будут в области видимости всех запросов. - - //Телеграм клиент инициализируется 1 раз и используется во всех запросах. - - $requestCallback = new RequestCallback($client); - $response = yield from $requestCallback->process($request); - - return new Response( - $requestCallback->page['code'], - $requestCallback->page['headers'], - $response - ); - - }), + $this->getServerAddresses(static::getConfig($options)), + static::getRouter($client), new Logger(LogLevel::DEBUG), (new Amp\Http\Server\Options()) ->withCompression() @@ -54,43 +31,82 @@ class Server yield $server->start(); - // Stop the server gracefully when SIGINT is received. - // This is technically optional, but it is best to call Server::stop(). - if (defined('SIGINT')) { - Amp\Loop::onSignal(SIGINT, static function (string $watcherId) use ($server) { - Amp\Loop::cancel($watcherId); - yield $server->stop(); - exit; - }); - } + static::registerShutdown($server); }); + } + private static function getServerAddresses(array $config): array + { + return [ + Amp\Socket\Server::listen("{$config['address']}:{$config['port']}"), + ]; + } + + private static function getRouter(Client $client): Amp\Http\Server\Router + { + $router = new Amp\Http\Server\Router(); + foreach (['GET', 'POST'] as $method) { + $router->addRoute($method, '/api/{session}/{method}', ApiController::getRouterCallback($client)); + $router->addRoute($method, '/api/{method}', ApiController::getRouterCallback($client)); + + $router->addRoute($method, '/events[/{session}]', EventsController::getRouterCallback($client)); + } + + $router->setFallback(new CallableRequestHandler(static function (Request $request) { + return new Response( + Amp\Http\Status::NOT_FOUND, + [ 'Content-Type'=>'application/json;charset=utf-8'], + json_encode( + [ + 'success' => 0, + 'errors' => [ + [ + 'code' => 404, + 'message' => 'Path not found', + ] + ] + ], + JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT + ) . "\n" + ); + })); + + return $router; + } + + /** + * Stop the server gracefully when SIGINT is received. + * This is technically optional, but it is best to call Server::stop(). + * + * @throws Amp\Loop\UnsupportedFeatureException + */ + private static function registerShutdown(Amp\Http\Server\Server $server) + { + + if (defined('SIGINT')) { + Amp\Loop::onSignal(SIGINT, static function (string $watcherId) use ($server) { + Amp\Loop::cancel($watcherId); + yield $server->stop(); + }); + } } /** * Установить конфигурацию для http-сервера * * @param array $config - * @return Server + * @return array */ - private function setConfig(array $config = []): self + private static function getConfig(array $config = []): array { $config = array_filter($config); - $this->config = array_merge( - Config::getInstance()->get("server", []), + $config = array_merge( + Config::getInstance()->get('server', []), $config ); - return $this; - } - - public function resolvePromise(&$promise) { - if ($promise instanceof Promise) { - return yield $promise; - } - - return yield; + return $config; } } \ No newline at end of file