1
0
mirror of https://github.com/danog/telerpc.git synced 2024-11-26 12:04:47 +01:00
This commit is contained in:
Daniil Gentili 2024-07-20 19:42:46 +02:00
parent 2d297398fc
commit 3a3bbb63ca
7 changed files with 186 additions and 136 deletions

19
Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM danog/madelineproto:latest
WORKDIR /app
ADD src /app
ADD composer.json /app
ADD server.php /app
ADD db.php /app
ADD entrypoint.sh /
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php composer-setup.php && \
php -r "unlink('composer-setup.php');" && \
mv composer.phar /usr/local/bin/composer && \
\
apk add procps git unzip github-cli openssh && \
composer update
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -3,7 +3,10 @@
"description": "RPC Error reporting API", "description": "RPC Error reporting API",
"type": "project", "type": "project",
"require": { "require": {
"danog/madelineproto": "^8" "danog/madelineproto": "^8",
"amphp/http-server": "3.x-dev",
"amphp/mysql": "3.x-dev",
"amphp/http-server-form-parser": "2.x-dev"
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"require-dev": { "require-dev": {
@ -17,6 +20,11 @@
"email": "daniil.gentili.dg@gmail.com" "email": "daniil.gentili.dg@gmail.com"
} }
], ],
"autoload": {
"files": [
"src/Main.php"
]
},
"scripts": { "scripts": {
"cs-fix": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix -v --diff" "cs-fix": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix -v --diff"
} }

View File

@ -1,3 +1,10 @@
<?php <?php
return new PDO('mysql:unix_socket=/var/run/mysqld/mysqld.sock;dbname=rpc;charset=utf8mb4', 'user', 'pass'); use Amp\Mysql\MysqlConfig;
return new MysqlConfig(
host: 'unix_socket=/var/run/mysqld/mysqld.sock',
user: 'user',
password: 'pass',
database: 'rpc',
);

5
entrypoint.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
composer update
php server.php server

View File

@ -1,9 +0,0 @@
<?php
chdir(__DIR__);
require 'vendor/autoload.php';
include 'src/Main.php';
(new Main())->run();

7
server.php Normal file
View File

@ -0,0 +1,7 @@
<?php
chdir(__DIR__);
require __DIR__.'/vendor/autoload.php';
(new Main)->run($argv[1] === 'serve');

View File

@ -1,8 +1,23 @@
<?php <?php
use Amp\Http\HttpStatus;
use Amp\Http\Server\DefaultErrorHandler;
use Amp\Http\Server\FormParser\Form;
use Amp\Http\Server\Request;
use Amp\Http\Server\RequestHandler;
use Amp\Http\Server\Response;
use Amp\Http\Server\SocketHttpServer;
use Amp\Log\ConsoleFormatter;
use Amp\Log\StreamHandler;
use Amp\Mysql\MysqlConfig;
use Amp\Mysql\MysqlConnectionPool;
use danog\MadelineProto\RPCErrorException; use danog\MadelineProto\RPCErrorException;
use Monolog\Logger;
use Monolog\Processor\PsrLogMessageProcessor;
final class Main use function Amp\ByteStream\getStdout;
final class Main implements RequestHandler
{ {
private const GLOBAL_CODES = [ private const GLOBAL_CODES = [
'FLOOD_WAIT_%d' => 420, 'FLOOD_WAIT_%d' => 420,
@ -67,28 +82,35 @@ final class Main
'MSG_WAIT_FAILED' => -500, 'MSG_WAIT_FAILED' => -500,
]; ];
private ?\PDO $pdo = null; private MysqlConnectionPool $pool;
public function __construct()
private function connect(): \PDO
{ {
if ($this->pdo !== null) { $this->pool = new MysqlConnectionPool(include __DIR__.'/../db.php');
return $this->pdo;
}
$this->pdo = include __DIR__.'/../db.php';
$this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $this->pdo;
} }
private static function error(string $message): string private const HEADERS = [
'Content-Type' => 'application/json',
'access-control-allow-origin' => '*',
'access-control-allow-methods' => 'GET, POST, OPTIONS',
'access-control-expose-headers' => 'Content-Length,Content-Type,Date,Server,Connection'
];
private static function error(string $message, int $code = HttpStatus::BAD_REQUEST): Response
{ {
return \json_encode(['ok' => false, 'description' => $message]); return new Response(
$code,
self::HEADERS,
json_encode(['ok' => false, 'description' => $message])
);
} }
private static function ok(mixed $result): string private static function ok(mixed $result): Response
{ {
return \json_encode(['ok' => true, 'result' => $result]); return new Response(
HttpStatus::OK,
self::HEADERS,
json_encode(['ok' => false, 'result' => $result])
);
} }
private static function sanitize(string $error): string private static function sanitize(string $error): string
@ -104,69 +126,53 @@ final class Main
private function v2(): array private function v2(): array
{ {
$this->connect();
$desc = []; $desc = [];
$q = $this->pdo->prepare('SELECT error, description FROM error_descriptions'); $q = $this->pool->prepare('SELECT error, description FROM error_descriptions');
$q->execute(); foreach ($q->execute() as ['error' => $error, 'description' => $description]) {
$q->fetchAll(PDO::FETCH_FUNC, function ($error, $description) use (&$desc) {
$desc[$error] = $description; $desc[$error] = $description;
}); }
$q = $this->pdo->prepare('SELECT method, code, error FROM errors'); $q = $this->pool->prepare('SELECT method, code, error FROM errors WHERE code != 500');
$q->execute();
$r = []; $r = [];
$q->fetchAll(PDO::FETCH_FUNC, function ($method, $code, $error) use (&$r, &$desc) { foreach ($q->execute() as ['method' => $method, 'code' => $code, 'error' => $error]) {
$code = (int) $code; $code = (int) $code;
if ($code === 500) {
return;
}
$r[$method][] = [ $r[$method][] = [
'error_code' => $code, 'error_code' => $code,
'error_message' => $error, 'error_message' => $error,
'error_description' => $desc[$error] ?? '', 'error_description' => $desc[$error] ?? '',
]; ];
}); }
return $r; return $r;
} }
private function v3(): array private function v3(): array
{ {
$this->connect(); $q = $this->pool->prepare('SELECT method, code, error FROM errors');
$q = $this->pdo->prepare('SELECT method, code, error FROM errors');
$q->execute();
$r = []; $r = [];
$q->fetchAll(PDO::FETCH_FUNC, function ($method, $code, $error) use (&$r) { foreach ($q->execute() as ['method' => $method, 'code' => $code, 'error' => $error]) {
$error = self::sanitize($error); $error = self::sanitize($error);
$r[(int) $code][$method][$error] = $error; $r[(int) $code][$method][$error] = $error;
}); }
$hr = []; $hr = [];
$q = $this->pdo->prepare('SELECT error, description FROM error_descriptions'); $q = $this->pool->prepare('SELECT error, description FROM error_descriptions');
$q->execute(); foreach ($q->execute() as ['error' => $error, 'description' => $description]) {
$q->fetchAll(PDO::FETCH_FUNC, function ($error, $description) use (&$hr) {
$error = self::sanitize($error); $error = self::sanitize($error);
$description = \str_replace(' X ', ' %d ', $description); $description = \str_replace(' X ', ' %d ', $description);
$hr[$error] = $description; $hr[$error] = $description;
}); }
return [$r, $hr]; return [$r, $hr];
} }
private function v4(bool $core = false): array private function v4(bool $core = false): array
{ {
$this->connect(); $q = $this->pool->prepare('SELECT method, code, error FROM errors');
$q = $this->pdo->prepare('SELECT method, code, error FROM errors');
$q->execute();
$r = []; $r = [];
$errors = []; $errors = [];
$bot_only = []; $bot_only = [];
$q->fetchAll(PDO::FETCH_FUNC, function ($method, $code, $error) use (&$r, &$bot_only, &$errors, $core) { foreach ($q->execute() as ['method' => $method, 'code' => $code, 'error' => $error]) {
if ($core && ($error === 'UPDATE_APP_TO_LOGIN' || $error === 'UPDATE_APP_REQUIRED')) {
return;
}
$code = (int) $code; $code = (int) $code;
$error = self::sanitize($error); $error = self::sanitize($error);
if (!\in_array($method, $r[$code][$error] ?? [])) { if (!\in_array($method, $r[$code][$error] ?? [])) {
@ -176,21 +182,17 @@ final class Main
if (\in_array($error, ['USER_BOT_REQUIRED', 'USER_BOT_INVALID']) && !\in_array($method, $bot_only) && !\in_array($method, ['bots.setBotInfo', 'bots.getBotInfo'])) { if (\in_array($error, ['USER_BOT_REQUIRED', 'USER_BOT_INVALID']) && !\in_array($method, $bot_only) && !\in_array($method, ['bots.setBotInfo', 'bots.getBotInfo'])) {
$bot_only[] = $method; $bot_only[] = $method;
} }
}); }
$hr = []; $hr = [];
$q = $this->pdo->prepare('SELECT error, description FROM error_descriptions'); $q = $this->pool->prepare('SELECT error, description FROM error_descriptions');
$q->execute(); foreach ($q->execute() as ['error' => $error, 'description' => $description]) {
$q->fetchAll(PDO::FETCH_FUNC, function ($error, $description) use (&$hr, $core) {
if ($core && ($error === 'UPDATE_APP_TO_LOGIN' || $error === 'UPDATE_APP_REQUIRED')) {
return;
}
$error = self::sanitize($error); $error = self::sanitize($error);
$description = \str_replace(' X ', ' %d ', $description); $description = \str_replace(' X ', ' %d ', $description);
if ($description !== '' && !\in_array($description[\strlen($description) - 1], ['?', '.', '!'])) { if ($description !== '' && !\in_array($description[\strlen($description) - 1], ['?', '.', '!'])) {
$description .= '.'; $description .= '.';
} }
$hr[$error] = $description; $hr[$error] = $description;
}); }
$hr['FLOOD_WAIT_%d'] = 'Please wait %d seconds before repeating the action.'; $hr['FLOOD_WAIT_%d'] = 'Please wait %d seconds before repeating the action.';
@ -210,35 +212,35 @@ final class Main
private function bot(): array private function bot(): array
{ {
$this->connect(); $q = $this->pool->prepare('SELECT method FROM bot_method_invalid');
$r = [];
$q = $this->pdo->prepare('SELECT method FROM bot_method_invalid'); foreach ($q->execute() as $result) {
$q->execute(); $r []= $result['method'];
$r = $q->fetchAll(PDO::FETCH_COLUMN); }
return $r; return $r;
} }
private function cli(): void private function cli(): void
{ {
$this->connect();
$bot_only = []; $bot_only = [];
$q = $this->pdo->prepare('SELECT error, method FROM errors'); $q = $this->pool->prepare('SELECT error, method FROM errors');
$q->execute(); $r = [];
$r = $q->fetchAll(PDO::FETCH_COLUMN | PDO::FETCH_GROUP); foreach ($q->execute() as $result) {
$r[$result['error']][]= $result['method'];
}
foreach ($r as $error => $methods) { foreach ($r as $error => $methods) {
$fixed = self::sanitize($error); $fixed = self::sanitize($error);
if ($fixed !== $error) { if ($fixed !== $error) {
foreach ($methods as $method) { foreach ($methods as $method) {
$q = $this->pdo->prepare('UPDATE errors SET error=? WHERE error=? AND method=?'); $q = $this->pool->prepare('UPDATE errors SET error=? WHERE error=? AND method=?');
$q->execute([$fixed, $error, $method]); $q->execute([$fixed, $error, $method]);
$q = $this->pdo->prepare('UPDATE error_descriptions SET error=? WHERE error=?'); $q = $this->pool->prepare('UPDATE error_descriptions SET error=? WHERE error=?');
$q->execute([$fixed, $error]); $q->execute([$fixed, $error]);
if (self::sanitize($method) === $fixed) { if (self::sanitize($method) === $fixed) {
$q = $this->pdo->prepare('DELETE FROM errors WHERE error=? AND method=?'); $q = $this->pool->prepare('DELETE FROM errors WHERE error=? AND method=?');
$q->execute([$fixed, $fixed]); $q->execute([$fixed, $fixed]);
echo 'Delete strange '.$error."\n"; echo 'Delete strange '.$error."\n";
} }
@ -247,7 +249,7 @@ final class Main
} }
foreach ($methods as $method) { foreach ($methods as $method) {
if (self::sanitize($method) === $fixed) { if (self::sanitize($method) === $fixed) {
$q = $this->pdo->prepare('DELETE FROM errors WHERE error=? AND method=?'); $q = $this->pool->prepare('DELETE FROM errors WHERE error=? AND method=?');
$q->execute([$fixed, $method]); $q->execute([$fixed, $method]);
echo 'Delete strange '.$error."\n"; echo 'Delete strange '.$error."\n";
} }
@ -256,9 +258,11 @@ final class Main
$allowed = []; $allowed = [];
$q = $this->pdo->prepare('SELECT error, method FROM errors'); $q = $this->pool->prepare('SELECT error, method FROM errors');
$q->execute(); $r = [];
$r = $q->fetchAll(PDO::FETCH_COLUMN | PDO::FETCH_GROUP); foreach ($q->execute() as $result) {
$r[$result['error']][]= $result['method'];
}
foreach (\array_merge($r, self::GLOBAL_CODES) as $error => $methods) { foreach (\array_merge($r, self::GLOBAL_CODES) as $error => $methods) {
if (\is_int($methods)) { if (\is_int($methods)) {
$allowed[$error] = true; $allowed[$error] = true;
@ -267,7 +271,7 @@ final class Main
$anyok = false; $anyok = false;
foreach ($methods as $method) { foreach ($methods as $method) {
if (RPCErrorException::isBad($error, 0, $method)) { if (RPCErrorException::isBad($error, 0, $method)) {
$q = $this->pdo->prepare('DELETE FROM errors WHERE error=? AND method=?'); $q = $this->pool->prepare('DELETE FROM errors WHERE error=? AND method=?');
$q->execute([$error, $method]); $q->execute([$error, $method]);
echo "Delete $error for $method\n"; echo "Delete $error for $method\n";
continue; continue;
@ -279,33 +283,31 @@ final class Main
} }
$allowed[$error] = true; $allowed[$error] = true;
$q = $this->pdo->prepare('SELECT description FROM error_descriptions WHERE error=?'); $q = $this->pool->prepare('SELECT description FROM error_descriptions WHERE error=?');
$q->execute([$error]); $res = $q->execute([$error])->fetchRow();
if (!$q->rowCount() || !($er = $q->fetchColumn())) { if (!($res['description'] ?? null)) {
$methods = \implode(', ', $methods); $methods = \implode(', ', $methods);
$description = \readline('Insert description for '.$error.' ('.$methods.'): '); $description = \readline('Insert description for '.$error.' ('.$methods.'): ');
if (\strpos(\readline($error.' - '.$description.' OK? '), 'n') !== false) { if (\strpos(\readline($error.' - '.$description.' OK? '), 'n') !== false) {
continue; continue;
} }
if ($description === 'drop' || $description === 'delete' || $description === 'd') { if ($description === 'drop' || $description === 'delete' || $description === 'd') {
$q = $this->pdo->prepare('DELETE FROM errors WHERE error=?'); $q = $this->pool->prepare('DELETE FROM errors WHERE error=?');
$q->execute([$error]); $q->execute([$error]);
echo 'Delete '.$error."\n"; echo 'Delete '.$error."\n";
} else { } else {
$q = $this->pdo->prepare('REPLACE INTO error_descriptions VALUES (?, ?)'); $q = $this->pool->prepare('REPLACE INTO error_descriptions VALUES (?, ?)');
$q->execute([$error, $description]); $q->execute([$error, $description]);
} }
} }
} }
$q = $this->pdo->prepare('SELECT error FROM error_descriptions'); $q = $this->pool->prepare('SELECT error FROM error_descriptions');
$q->execute(); foreach ($q->execute() as ['error' => $error]) {
$r = $q->fetchAll(PDO::FETCH_COLUMN);
foreach ($r as $error) {
if (!isset($allowed[$error])) { if (!isset($allowed[$error])) {
$q = $this->pdo->prepare('DELETE FROM errors WHERE error=?'); $q = $this->pool->prepare('DELETE FROM errors WHERE error=?');
$q->execute([$error]); $q->execute([$error]);
$q = $this->pdo->prepare('DELETE FROM error_descriptions WHERE error=?'); $q = $this->pool->prepare('DELETE FROM error_descriptions WHERE error=?');
$q->execute([$error]); $q->execute([$error]);
echo 'Delete '.$error."\n"; echo 'Delete '.$error."\n";
} }
@ -326,54 +328,65 @@ final class Main
\file_put_contents('data/core.json', \json_encode(['errors' => $r, 'descriptions' => $hr, 'user_only' => $bot, 'bot_only' => $bot_only])); \file_put_contents('data/core.json', \json_encode(['errors' => $r, 'descriptions' => $hr, 'user_only' => $bot, 'bot_only' => $bot_only]));
} }
public function run(): void public function handleRequest(Request $request): Response {
$form = Form::fromRequest($request);
$error = $request->getQueryParameter('error') ?? $form->getValue('error');
$code = $request->getQueryParameter('code') ?? $form->getValue('code');
$method = $request->getQueryParameter('method') ?? $form->getValue('method');
if ($error && $code && $method
&& \is_numeric($_REQUEST['code'])
&& !RPCErrorException::isBad(
$error,
(int) $code,
$method
)
) {
$error = self::sanitize($error);
try {
if ($error === $code) {
$this->pool->prepare('REPLACE INTO code_errors VALUES (?);')->execute([$code]);
} elseif ($error === 'BOT_METHOD_INVALID') {
$this->pool->prepare('REPLACE INTO bot_method_invalid VALUES (?);')->execute([$method]);
} else {
$this->pool->prepare('REPLACE INTO errors VALUES (?, ?, ?);')->execute([$error, $method, $code]);
$q = $this->pool->prepare('SELECT description FROM error_descriptions WHERE error=?');
$result = $q->execute([$error]);
if ($row = $result->fetchRow()) {
return self::ok($row['description']);
}
return self::error('No description', 404);
}
} catch (\Throwable $e) {
return self::error($e->getMessage(), 500);
}
return self::ok(true);
}
return self::error('API for reporting Telegram RPC errors. For localized errors see https://rpc.madelineproto.xyz, to report a new error use the `code`, `method` and `error` GET/POST parameters. Source code at https://github.com/danog/telerpc.');
}
public function run(bool $serve): void
{ {
if (PHP_SAPI === 'cli') { if (!$serve) {
$this->cli(); $this->cli();
exit; exit;
} }
\ini_set('log_errors', 1); $logHandler = new StreamHandler(getStdout());
\ini_set('error_log', '/tmp/rpc.log'); $logHandler->pushProcessor(new PsrLogMessageProcessor());
\header('Content-Type: application/json'); $logHandler->setFormatter(new ConsoleFormatter());
\header('access-control-allow-origin: *');
\header('access-control-allow-methods: GET, POST, OPTIONS');
\header('access-control-expose-headers: Content-Length,Content-Type,Date,Server,Connection');
if (isset($_REQUEST['error'], $_REQUEST['code'], $_REQUEST['method'])
&& $_REQUEST['error'] !== ''
&& $_REQUEST['method'] !== ''
&& \is_numeric($_REQUEST['code'])
&& !RPCErrorException::isBad(
$_REQUEST['error'],
(int) $_REQUEST['code'],
$_REQUEST['method']
)
) {
$error = self::sanitize($_REQUEST['error']);
$method = $_REQUEST['method'];
$code = $_REQUEST['code'];
try { $logger = new Logger('server');
$this->connect(); $logger->pushHandler($logHandler);
if ($error === $code) { $errorHandler = new DefaultErrorHandler();
$this->pdo->prepare('REPLACE INTO code_errors VALUES (?);')->execute([$code]);
} elseif ($error === 'BOT_METHOD_INVALID') {
$this->pdo->prepare('REPLACE INTO bot_method_invalid VALUES (?);')->execute([$method]);
} else {
$this->pdo->prepare('REPLACE INTO errors VALUES (?, ?, ?);')->execute([$error, $method, $code]);
$q = $this->pdo->prepare('SELECT description FROM error_descriptions WHERE error=?'); $server = SocketHttpServer::createForDirectAccess($logger);
$q->execute([$error]); $server->expose('127.0.0.1:1337');
if ($q->rowCount()) { $server->start($this, $errorHandler);
exit(self::ok($q->fetchColumn()));
} // Serve requests until SIGINT or SIGTERM is received by the process.
exit(self::error('No description')); Amp\trapSignal([SIGINT, SIGTERM]);
}
} catch (\Throwable $e) { $server->stop();
exit(self::error($e->getMessage()));
}
exit(self::ok(true));
}
exit(self::error('API for reporting Telegram RPC errors. For localized errors see https://rpc.madelineproto.xyz, to report a new error use the `code`, `method` and `error` GET/POST parameters. Source code at https://github.com/danog/telerpc.'));
} }
} }