diff --git a/README.md b/README.md index 201c0b4..7bb6e50 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Fast, simple, async php telegram api server: * Full access to telegram api: bot and user * Multiple sessions * Stream media (view files in browser) +* Upload media * Websocket endpoint for events **Architecture Example** @@ -63,10 +64,10 @@ Fast, simple, async php telegram api server: Also options can be set in .env file (see .env.example) ``` -1. Access telegram api directly with simple GET/POST requests. +1. Access Telegram API with simple GET/POST requests. Regular and application/json POST supported. - Its recommended to use http_build_query when using GET requests. + Its recommended to use http_build_query, when using GET requests. **Rules:** * All methods from MadelineProto supported: [Methods List](https://docs.madelineproto.xyz/API_docs/methods/) @@ -92,49 +93,115 @@ Fast, simple, async php telegram api server: * copy message from one channel to another (not repost): `http://127.0.0.1:9503/api/copyMessages/?data[from_peer]=@xtrime&data[to_peer]=@xtrime&data[id][0]=1` ## Advanced features +#### Uploading files. -* Multiple sessions support. - When running multiple sessions, need to define which session to use for request. - Each session is stored in `sessions/{$session}.madeline`. Nested folders supported. - **Examples:** - * `php server.php --session=bot --session=users/xtrime --session=users/user1` - * `http://127.0.0.1:9503/api/bot/getSelf` - * `http://127.0.0.1:9503/api/users/xtrime/getSelf` - * `http://127.0.0.1:9503/api/users/user1/getSelf` - * sessions file paths are: `sessions/bot.madeline`, `sessions/users/xtrime.madeline` and `sessions/users/user1.madeline` - * glob syntax for sessions: - * `--session=*` to use all `sessions/*.madeline` files. - * `--session=users/* --session=bots/*` to use all session files from `sessions/bots` and `sessions/users` folders. -* Session management (**Use with caution, can be unstable**) - - **Examples:** - * Session list: `http://127.0.0.1:9503/system/getSessionList` - * Adding session: `http://127.0.0.1:9503/system/addSession?session=users/xtrime` - * [optional] Adding session with custom settings: `http://127.0.0.1:9503/system/addSession?session=users/xtrime&settings[app_info][app_id]=xxx&&settings[app_info][app_hash]=xxx` - * Removing session: `http://127.0.0.1:9503/system/removeSession?session=users/xtrime` - - If there is no authorization in session, or session file is blank, authorization required: - - User: - * `http://127.0.0.1:9503/api/users/xtrime/phoneLogin?phone=+7123...` - * `http://127.0.0.1:9503/api/users/xtrime/completePhoneLogin?code=123456` - * (optional) `http://127.0.0.1:9503/api/users/xtrime/complete2falogin?password=123456` - * (optional) `http://127.0.0.1:9503/api/users/xtrime/completeSignup?firstName=MyExampleName` - - Bot: - * `http://127.0.0.1:9503/api/bot/botLogin?token=34298141894:aflknsaflknLKNFS` - - After authorization eventHandler need to be set, to receive updates for new session in `/events` websocket: - * `http://127.0.0.1:9503/api/users/xtrime/setEventHandler` - * `http://127.0.0.1:9503/api/bot/setEventHandler` +To upload files from POST request use custom `uploadMediaForm` method: -* EventHandler updates via websocket. Connect to `ws://127.0.0.1:9503/events`. You will get all events in json. - Each event is json object in [json-rpc 2.0 format](https://www.jsonrpc.org/specification#response_object). Example: +`curl "http://127.0.0.1:9503/api/uploadMediaForm" -g -F "file=@/Users/xtrime/Downloads/test.txt"` +Method supports `application/x-www-form-urlencoded` and `multipart/form-data`. + +Send result from `uploadMediaForm` to `messages.sendMedia`: +``` +curl --location --request POST 'http://127.0.0.1:9503/api/sendMedia' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "data":{ + "peer": "@xtrime", + "media": { + "_": "inputMediaUploadedDocument", + "file": { + "_": "inputFile", + "id": 1164670976363200575, + "parts": 1, + "name": "test.txt", + "mime_type": "text/plain", + "md5_checksum": "" + }, + "attributes": [ + { + "_": "documentAttributeFilename", + "file_name": "test.txt" + } + ] + } + } +}' +``` +Also see: https://docs.madelineproto.xyz/docs/FILES.html#uploading-files + +#### Downloading files + +``` +curl --location --request POST '127.0.0.1:9503/api/downloadToResponse' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "media": { + "_": "messageMediaDocument", + "document": { + "_": "document", + "id": 5470079466401169993, + "access_hash": -6754208767885394084, + "file_reference": { + "_": "bytes", + "bytes": "AkKdqJkAACnyXiaBgp3M3DfBh8C0+mGKXwSsGUY=" + }, + "date": 1551713685, + "mime_type": "video/mp4", + "size": 400967, + "dc_id": 2 + } + } +}' +``` + +Also see: https://docs.madelineproto.xyz/docs/FILES.html#downloading-files + +#### Multiple sessions support. + +When running multiple sessions, need to define which session to use for request. +Each session is stored in `sessions/{$session}.madeline`. Nested folders supported. +**Examples:** +* `php server.php --session=bot --session=users/xtrime --session=users/user1` +* `http://127.0.0.1:9503/api/bot/getSelf` +* `http://127.0.0.1:9503/api/users/xtrime/getSelf` +* `http://127.0.0.1:9503/api/users/user1/getSelf` +* sessions file paths are: `sessions/bot.madeline`, `sessions/users/xtrime.madeline` and `sessions/users/user1.madeline` +* glob syntax for sessions: + * `--session=*` to use all `sessions/*.madeline` files. + * `--session=users/* --session=bots/*` to use all session files from `sessions/bots` and `sessions/users` folders. + +#### Session management - When using CombinedAPI (multiple accounts) name of session can be added to path of websocket endpoint: - This endpoint will send events only from `users/xtrime` session: `ws://127.0.0.1:9503/events/users/xtrime` - - PHP websocket client example: [websocket-events.php](https://github.com/xtrime-ru/TelegramApiServer/blob/master/examples/websocket-events.php) +**Examples:** +* Session list: `http://127.0.0.1:9503/system/getSessionList` +* Adding session: `http://127.0.0.1:9503/system/addSession?session=users/xtrime` +* [optional] Adding session with custom settings: `http://127.0.0.1:9503/system/addSession?session=users/xtrime&settings[app_info][app_id]=xxx&&settings[app_info][app_hash]=xxx` +* Removing session: `http://127.0.0.1:9503/system/removeSession?session=users/xtrime` + +If there is no authorization in session, or session file is blank, authorization required: + +User: +* `http://127.0.0.1:9503/api/users/xtrime/phoneLogin?phone=+7123...` +* `http://127.0.0.1:9503/api/users/xtrime/completePhoneLogin?code=123456` +* (optional) `http://127.0.0.1:9503/api/users/xtrime/complete2falogin?password=123456` +* (optional) `http://127.0.0.1:9503/api/users/xtrime/completeSignup?firstName=MyExampleName` + +Bot: +* `http://127.0.0.1:9503/api/bot/botLogin?token=34298141894:aflknsaflknLKNFS` + +After authorization eventHandler need to be set, to receive updates for new session in `/events` websocket: +* `http://127.0.0.1:9503/api/users/xtrime/setEventHandler` +* `http://127.0.0.1:9503/api/bot/setEventHandler` + +#### EventHandler updates via websocket. + +Connect to `ws://127.0.0.1:9503/events`. You will get all events in json. +Each event is json object in [json-rpc 2.0 format](https://www.jsonrpc.org/specification#response_object). Example: + +When using CombinedAPI (multiple accounts) name of session can be added to path of websocket endpoint: +This endpoint will send events only from `users/xtrime` session: `ws://127.0.0.1:9503/events/users/xtrime` + +PHP websocket client example: [websocket-events.php](https://github.com/xtrime-ru/TelegramApiServer/blob/master/examples/websocket-events.php) ## Contacts diff --git a/composer.json b/composer.json index aa45db3..541711b 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "amphp/websocket-server": "^2", "amphp/websocket-client": "dev-master#53f7883b325b09864095300ec8ff81e84e772c3b", "vlucas/phpdotenv": "^4", - "danog/madelineproto":"dev-master" + "danog/madelineproto":"dev-master", + "amphp/http-server-form-parser": "^1.1" }, "require-dev": { "roave/security-advisories": "dev-master" diff --git a/composer.lock b/composer.lock index 7c568cd..7db8a89 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": "fde2f21e919da4755aa6b9f1f24c217e", + "content-hash": "adb3f78b0ae79061e4ad9976499da64b", "packages": [ { "name": "amphp/amp", @@ -676,6 +676,74 @@ ], "time": "2020-01-04T18:10:10+00:00" }, + { + "name": "amphp/http-server-form-parser", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/http-server-form-parser.git", + "reference": "f26313797fb5ffd936c8fa865fde61523b6d05f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/http-server-form-parser/zipball/f26313797fb5ffd936c8fa865fde61523b6d05f2", + "reference": "f26313797fb5ffd936c8fa865fde61523b6d05f2", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "amphp/byte-stream": "^1.3", + "amphp/http": "^1", + "amphp/http-server": "^2 || ^1 || ^0.8", + "php": ">=7" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.1.2", + "phpunit/phpunit": "^8 || ^7 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Http\\Server\\FormParser\\": "src" + }, + "files": [ + "src/functions.php" + ] + }, + "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": "A form parser for Amp's HTTP parser.", + "homepage": "https://github.com/amphp/http-server-form-parser", + "keywords": [ + "amp", + "amphp", + "async", + "form", + "http", + "non-blocking" + ], + "time": "2019-12-13T15:52:33+00:00" + }, { "name": "amphp/http-server-router", "version": "v1.0.2", @@ -1513,12 +1581,12 @@ "source": { "type": "git", "url": "https://github.com/danog/MadelineProto.git", - "reference": "17bacd1389f090fd5a83cf04ff9018dda1bbaf5e" + "reference": "bdd86d4efbefe8a0ed840f25d7608df60ed9261a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/danog/MadelineProto/zipball/17bacd1389f090fd5a83cf04ff9018dda1bbaf5e", - "reference": "17bacd1389f090fd5a83cf04ff9018dda1bbaf5e", + "url": "https://api.github.com/repos/danog/MadelineProto/zipball/bdd86d4efbefe8a0ed840f25d7608df60ed9261a", + "reference": "bdd86d4efbefe8a0ed840f25d7608df60ed9261a", "shasum": "" }, "require": { @@ -1603,7 +1671,7 @@ "telegram", "video" ], - "time": "2020-02-06T01:44:00+00:00" + "time": "2020-02-07T20:23:29+00:00" }, { "name": "danog/magicalserializer", diff --git a/src/Controllers/AbstractApiController.php b/src/Controllers/AbstractApiController.php index 34b4c15..cac1667 100644 --- a/src/Controllers/AbstractApiController.php +++ b/src/Controllers/AbstractApiController.php @@ -3,6 +3,9 @@ namespace TelegramApiServer\Controllers; use Amp\ByteStream\ResourceInputStream; +use Amp\Http\Server\FormParser\BufferingParser; +use Amp\Http\Server\FormParser\File; +use Amp\Http\Server\FormParser\Form; use Amp\Http\Server\Request; use Amp\Http\Server\RequestHandler\CallableRequestHandler; use Amp\Http\Server\Response; @@ -21,6 +24,7 @@ abstract class AbstractApiController protected Client $client; protected Request $request; + protected ?File $file = null; protected $extensionClass; @@ -68,7 +72,7 @@ abstract class AbstractApiController public function process() { $this->resolvePath($this->request->getAttribute(Router::class)); - yield from $this->resolveRequest($this->request); + yield from $this->resolveRequest(); yield from $this->generateResponse(); return $this->getResponse(); @@ -77,26 +81,32 @@ abstract class AbstractApiController /** * Получаем параметры из GET и POST * - * @param Request $request - * * @return AbstractApiController */ - private function resolveRequest(Request $request) + private function resolveRequest() { - $query = $request->getUri()->getQuery(); - $body = ''; - while ($chunk = yield $request->getBody()->read()) { - $body .= $chunk; - } - $contentType = $request->getHeader('Content-Type'); + $query = $this->request->getUri()->getQuery(); + $contentType = $this->request->getHeader('Content-Type'); parse_str($query, $get); - switch ($contentType) { - case 'application/json': + switch (true) { + case $contentType === 'application/x-www-form-urlencoded': + case mb_strpos($contentType, 'multipart/form-data') !== false: + /** @var Form $form */ + $form = yield (new BufferingParser())->parseForm($this->request); + $post = $form->getValues(); + $fileName = array_key_first($form->getFiles()); + if ($fileName) { + $this->file = $form->getFile($fileName); + } + break; + case $contentType === 'application/json': + $body = yield $this->request->getBody()->buffer(); $post = json_decode($body, 1); break; default: + $body = yield $this->request->getBody()->buffer(); parse_str($body, $post); } @@ -146,7 +156,7 @@ abstract class AbstractApiController $pathCount = count($this->api); if ($pathCount === 1 && $this->extensionClass && is_callable([$this->extensionClass,$this->api[0]])) { /** @var ApiExtensions|SystemApiExtensions $madelineProtoExtensions */ - $madelineProtoExtensions = new $this->extensionClass($madelineProto, $this->request); + $madelineProtoExtensions = new $this->extensionClass($madelineProto, $this->request, $this->file); $result = $madelineProtoExtensions->{$this->api[0]}(...$this->parameters); } else { //Проверяем нет ли в MadilineProto такого метода. diff --git a/src/MadelineProtoExtensions/ApiExtensions.php b/src/MadelineProtoExtensions/ApiExtensions.php index 4c9cbd0..e3d0cf1 100644 --- a/src/MadelineProtoExtensions/ApiExtensions.php +++ b/src/MadelineProtoExtensions/ApiExtensions.php @@ -4,16 +4,18 @@ namespace TelegramApiServer\MadelineProtoExtensions; +use Amp\ByteStream\InMemoryStream; use Amp\ByteStream\IteratorStream; +use Amp\Http\Server\FormParser\File; use Amp\Http\Server\Request; use Amp\Producer; use Amp\Promise; +use danog\MadelineProto; use danog\MadelineProto\TL\Conversion\BotAPI; use OutOfRangeException; use TelegramApiServer\EventObservers\EventHandler; use UnexpectedValueException; use function Amp\call; -use \danog\MadelineProto; class ApiExtensions { @@ -21,11 +23,13 @@ class ApiExtensions private MadelineProto\Api $madelineProto; private Request $request; + private ?File $file; - public function __construct(MadelineProto\Api $madelineProto, Request $request) + public function __construct(MadelineProto\Api $madelineProto, Request $request, ?File $file) { $this->madelineProto = $madelineProto; $this->request = $request; + $this->file = $file; } /** @@ -475,6 +479,39 @@ class ApiExtensions }); } + /** + * Upload file from POST request. + * Response can be passed to 'media' field in messages.sendMedia. + * + * @return Promise + */ + public function uploadMediaForm(): Promise + { + return call(function() { + $media = []; + if ($this->file !== null) { + $inputFile = yield $this->madelineProto->uploadFromStream( + new InMemoryStream($this->file->getContents()), + strlen($this->file->getContents()), + $this->file->getMimeType(), + $this->file->getName() + ); + $inputFile['id'] = unpack('P', $inputFile['id'])['1']; + $media = [ + 'media' => [ + '_' => 'inputMediaUploadedDocument', + 'file' => $inputFile, + 'attributes' => [ + ['_' => 'documentAttributeFilename', 'file_name' => $this->file->getName()] + ] + ] + ]; + } + + return $media; + }); + } + public function setEventHandler(): void { $this->madelineProto->setEventHandler(EventHandler::class); diff --git a/src/Server/Server.php b/src/Server/Server.php index 98eb429..9c7186e 100644 --- a/src/Server/Server.php +++ b/src/Server/Server.php @@ -40,7 +40,7 @@ class Server try { Amp\Loop::run(); } catch (\Throwable $e) { - Logger::getInstance()->critical($e->getMessage(), [ + Logger::getInstance()->alert($e->getMessage(), [ 'exception' => [ 'message' => $e->getMessage(), 'code' => $e->getCode(), @@ -48,6 +48,7 @@ class Server 'line' => $e->getLine(), ], ]); + exit; } } @@ -75,6 +76,7 @@ class Server Logger::getInstance()->emergency('Got SIGINT'); Amp\Loop::cancel($watcherId); yield $server->stop(); + exit; }); } }