mirror of
https://github.com/danog/TelegramApiServer.git
synced 2024-11-30 04:19:13 +01:00
Websocket EventHandler
This commit is contained in:
parent
6d2efe4c0f
commit
eb289abf6e
@ -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`
|
||||
|
@ -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"
|
||||
},
|
||||
|
195
composer.lock
generated
195
composer.lock
generated
@ -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",
|
||||
|
35
examples/websocket-events.php
Normal file
35
examples/websocket-events.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Get all updates from MadelineProto EventHandler running inside TelegramApiServer via websocket
|
||||
* @see \TelegramApiServer\Controllers\EventsController
|
||||
*/
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
use Amp\Websocket\Client\Connection;
|
||||
use Amp\Websocket\Message;
|
||||
use function Amp\Websocket\Client\connect;
|
||||
|
||||
$shortopts = 'u::';
|
||||
$longopts = [
|
||||
'url::',
|
||||
];
|
||||
$options = getopt($shortopts, $longopts);
|
||||
$options = [
|
||||
'url' => $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);
|
||||
}
|
||||
});
|
@ -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] = '';
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use danog\MadelineProto\TL\Conversion\BotAPI;
|
||||
use function Amp\call;
|
||||
use \danog\MadelineProto;
|
||||
|
||||
class CustomMethods
|
||||
class ClientCustomMethods
|
||||
{
|
||||
use BotAPI;
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -1,19 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace TelegramApiServer;
|
||||
namespace TelegramApiServer\Controllers;
|
||||
|
||||
use Amp\ByteStream\ResourceInputStream;
|
||||
use Amp\Http\Server\Request;
|
||||
use Amp\Http\Server\RequestHandler\CallableRequestHandler;
|
||||
use Amp\Http\Server\Response;
|
||||
use Amp\Http\Server\Router;
|
||||
use Amp\Promise;
|
||||
use TelegramApiServer\Client;
|
||||
use TelegramApiServer\Config;
|
||||
use TelegramApiServer\ClientCustomMethods;
|
||||
|
||||
class RequestCallback
|
||||
class ApiController
|
||||
{
|
||||
|
||||
private $client;
|
||||
private const PAGES = ['index', 'api'];
|
||||
/** @var array */
|
||||
private $ipWhiteList;
|
||||
public $page = [
|
||||
private Client $client;
|
||||
private array $ipWhiteList;
|
||||
public array $page = [
|
||||
'headers' => [
|
||||
'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
|
||||
{
|
71
src/Controllers/EventsController.php
Normal file
71
src/Controllers/EventsController.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace TelegramApiServer\Controllers;
|
||||
|
||||
use Amp\Http\Server\Request;
|
||||
use Amp\Http\Server\Response;
|
||||
use Amp\Http\Server\Router;
|
||||
use Amp\Promise;
|
||||
use Amp\Success;
|
||||
use Amp\Websocket\Server\Websocket;
|
||||
use TelegramApiServer\Client;
|
||||
use TelegramApiServer\EventHandlers\EventHandler;
|
||||
use function Amp\call;
|
||||
|
||||
class EventsController extends Websocket
|
||||
{
|
||||
private Client $client;
|
||||
|
||||
public static function getRouterCallback(Client $client): EventsController
|
||||
{
|
||||
$class = new static();
|
||||
$class->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]);
|
||||
});
|
||||
}
|
||||
}
|
39
src/EventHandlers/EventHandler.php
Normal file
39
src/EventHandlers/EventHandler.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace TelegramApiServer\EventHandlers;
|
||||
|
||||
use danog\MadelineProto\CombinedEventHandler;
|
||||
use danog\MadelineProto\Logger;
|
||||
use TelegramApiServer\Client;
|
||||
|
||||
class EventHandler extends CombinedEventHandler
|
||||
{
|
||||
/** @var callable[] */
|
||||
public static array $eventListeners = [];
|
||||
|
||||
public static function addEventListener($clientId, callable $callback)
|
||||
{
|
||||
Logger::log("Add event listener. ClientId: {$clientId}");
|
||||
static::$eventListeners[$clientId] = $callback;
|
||||
}
|
||||
|
||||
public static function removeEventListener($clientId): void
|
||||
{
|
||||
Logger::log("Removing listener: {$clientId}");
|
||||
unset(static::$eventListeners[$clientId]);
|
||||
if (!static::$eventListeners) {
|
||||
static::$eventListeners = [];
|
||||
}
|
||||
}
|
||||
|
||||
public function onAny($update, $sessionFile): void
|
||||
{
|
||||
$session = Client::getSessionName($sessionFile);
|
||||
Logger::log("Got update from session: {$session}");
|
||||
|
||||
foreach (static::$eventListeners as $clientId => $callback) {
|
||||
Logger::log("Pass update to callback. ClientId: {$clientId}");
|
||||
$callback($update, $session);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
108
src/Server.php
108
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().
|
||||
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();
|
||||
exit;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Установить конфигурацию для 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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user