Upload media from POST request

This commit is contained in:
Alexander Pankratov 2020-02-08 03:53:17 +03:00
parent 320fd42e7d
commit dd53cc98bb
6 changed files with 249 additions and 64 deletions

151
README.md
View File

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

View File

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

78
composer.lock generated
View File

@ -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",

View File

@ -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 такого метода.

View File

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

View File

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