Merge pull request #2 from danog/upstream_docker_image

Upstream docker image
This commit is contained in:
Daniil Gentili 2024-07-14 12:26:54 +02:00 committed by GitHub
commit 151c00516f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 2797 additions and 465 deletions

View File

@ -1,31 +1,13 @@
FROM php:8.3-cli FROM danog/madelineproto:latest
RUN apt-get update && apt-get upgrade -y RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
RUN true \
# Install main extension
&& apt-get install procps git zip vim libzip-dev libgmp-dev libuv1-dev libssl-dev libnghttp2-dev libffi-dev libicu-dev libonig-dev libxml2-dev libpng-dev -y \
&& docker-php-ext-install -j$(nproc) sockets bcmath mysqli pdo_mysql pcntl ffi intl gmp zip gd \
# Install additional extension
&& mkdir -p /usr/src/php/ext/ && cd /usr/src/php/ext/ \
&& pecl bundle uv && pecl bundle igbinary \
&& docker-php-ext-install -j$(nproc) uv igbinary \
# Install PrimeModule for AuthKey generation speedup
&& git clone https://github.com/danog/PrimeModule-ext \
&& cd PrimeModule-ext && make -j$(nproc) \
&& make install \
&& cd ../ \
&& rm -rf PrimeModule-ext/ \
# Install composer
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
# Cleanup
&& docker-php-source delete \
&& apt-get autoremove --purge -y && apt-get autoclean -y && apt-get clean -y \
&& rm -rf /usr/src
COPY --from=ghcr.io/ufoscout/docker-compose-wait:latest /wait /usr/local/bin/docker-compose-wait COPY --from=ghcr.io/ufoscout/docker-compose-wait:latest /wait /usr/local/bin/docker-compose-wait
ADD docker/php/conf.d/. "$PHP_INI_DIR/conf.d/" RUN echo 1.0.0 > /tas_version
EXPOSE 9503 EXPOSE 9503
ENTRYPOINT ["./entrypoint.sh"] ENV UV_USE_IO_URING=0
ENTRYPOINT ["./entrypoint.sh"]

View File

@ -1,12 +1,16 @@
FROM xtrime/telegram-api-server:latest FROM xtrime/telegram-api-server:latest
ADD docker/php/conf.d/. "$PHP_INI_DIR/conf.d/" ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN pecl install xdebug \ RUN echo "opcache.jit=disable" >> "$PHP_INI_DIR/conf.d/xdebug.ini"
&& echo "" >> "$PHP_INI_DIR/conf.d/xdebug.ini" \
&& echo "zend_extension=xdebug.so" >> "$PHP_INI_DIR/conf.d/xdebug.ini" RUN chmod +x /usr/local/bin/install-php-extensions && \
install-php-extensions xdebug && \
rm /usr/local/bin/install-php-extensions
ADD dev/. "$PHP_INI_DIR/conf.d/"
EXPOSE 9503 EXPOSE 9503
EXPOSE 9003 EXPOSE 9003
ENTRYPOINT ["./entrypoint.sh"] ENTRYPOINT ["./entrypoint.sh"]

View File

@ -1,5 +1,9 @@
<?php <?php
use Amp\Future\UnhandledFutureError;
use Amp\SignalException;
use Amp\Sql\SqlException;
use danog\MadelineProto\SecurityException;
use Revolt\EventLoop; use Revolt\EventLoop;
use TelegramApiServer\Logger; use TelegramApiServer\Logger;
use TelegramApiServer\Migrations\EnvUpgrade; use TelegramApiServer\Migrations\EnvUpgrade;
@ -113,3 +117,16 @@ if (!function_exists('emergency')) {
Logger::getInstance()->emergency($message, $context); Logger::getInstance()->emergency($message, $context);
} }
} }
EventLoop::setErrorHandler(function (\Throwable $e) {
if ($e instanceof UnhandledFutureError) {
$e = $e->getPrevious();
}
if ($e instanceof SecurityException || $e instanceof SignalException || $e instanceof SqlException) {
throw $e;
}
if (str_starts_with($e->getMessage(), 'Could not connect to DC ')) {
throw $e;
}
emergency((string) $e);
});

View File

@ -24,19 +24,22 @@
} }
], ],
"require": { "require": {
"php": "^8.1", "php": "^8.2",
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"amphp/http-server": "^v3", "amphp/http-server": "^3.3.1",
"amphp/http": "^v2", "amphp/http": "^2.1.1",
"amphp/http-server-router": "^2", "amphp/http-server-router": "^2",
"amphp/http-server-form-parser": "^v2", "amphp/http-server-form-parser": "^v2",
"amphp/websocket-server": "^v3", "amphp/websocket-server": "^3.0.1",
"amphp/websocket-client": "^v2", "amphp/websocket-client": "^v2",
"vlucas/phpdotenv": "^4", "vlucas/phpdotenv": "^4.3",
"danog/madelineproto": "dev-v8_fix_cleanup", "danog/madelineproto": "dev-v8_fix_cleanup",
"amphp/dns": "2.x-dev" "amphp/dns": "2.x-dev"
}, },
"require-dev": {
"amphp/php-cs-fixer-config": "^2.0.1"
},
"suggest": { "suggest": {
"ext-pcntl": "Install pcintl for propper signal handling and healthcheck (enabled in .env)" "ext-pcntl": "Install pcintl for propper signal handling and healthcheck (enabled in .env)"
}, },
@ -57,5 +60,8 @@
"allow-plugins": { "allow-plugins": {
"symfony/thanks": false "symfony/thanks": false
} }
},
"scripts": {
"cs-fix": "PHP_CS_FIXER_IGNORE_ENV=1 php -d pcre.jit=0 vendor/bin/php-cs-fixer fix -v"
} }
} }

2475
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,8 @@
use danog\MadelineProto\Logger; use danog\MadelineProto\Logger;
use TelegramApiServer\EventObservers\LogObserver; use TelegramApiServer\EventObservers\LogObserver;
use function Amp\Socket\SocketAddress\fromString;
$settings = [ $settings = [
'server' => [ 'server' => [
'address' => (string)getenv('SERVER_ADDRESS'), 'address' => (string)getenv('SERVER_ADDRESS'),
@ -58,6 +60,10 @@ $settings = [
'report_broken_media' => false, 'report_broken_media' => false,
'download_parallel_chunks' => 20, 'download_parallel_chunks' => 20,
], ],
'metrics' => [
'enable_prometheus_collection' => true, //(bool)getenv("PROMETHEUS_BIND_TO"),
'metrics_bind_to' => fromString("0.0.0.0:12345")
]
], ],
'api' => [ 'api' => [
'ip_whitelist' => array_filter( 'ip_whitelist' => array_filter(
@ -79,4 +85,4 @@ if (empty($settings['telegram']['app_info']['api_id'])) {
throw new InvalidArgumentException('Need to fill TELEGRAM_API_ID in .env.docker or .env'); throw new InvalidArgumentException('Need to fill TELEGRAM_API_ID in .env.docker or .env');
} }
return $settings; return $settings;

View File

@ -1,5 +1,8 @@
opcache.jit=disable
zend_extension=xdebug.so
xdebug.max_nesting_level=512 xdebug.max_nesting_level=512
xdebug.mode=debug xdebug.mode=debug
xdebug.start_with_request=yes xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal xdebug.client_host=host.docker.internal
xdebug.client_port=9003 xdebug.client_port=9003

View File

@ -19,7 +19,7 @@ services:
max-size: "1024k" max-size: "1024k"
max-file: "2" max-file: "2"
healthcheck: healthcheck:
test: timeout 15 curl -f http://localhost:9503/system/healthcheck || bash -c 'kill -INT -1 && (sleep 5; kill -s 9 -1)' test: timeout 15 curl -f http://localhost:9503/system/healthcheck || sh -c 'kill -INT -1 && (sleep 5; kill -s 9 -1)'
interval: 60s interval: 60s
timeout: 30s timeout: 30s
retries: 1 retries: 1

View File

@ -5,6 +5,7 @@ services:
service: base-api service: base-api
ports: ports:
- "127.0.0.1:9503:9503" - "127.0.0.1:9503:9503"
- "12345:12345"
command: command:
- "-s=session" - "-s=session"
mysql: mysql:
@ -15,4 +16,4 @@ services:
- "127.0.0.1:9507:3306" - "127.0.0.1:9507:3306"
networks: networks:
default: default:
name: telegram-api-server name: telegram-api-server

View File

@ -1 +0,0 @@
zend.assertions=-1

View File

@ -1,11 +0,0 @@
; Extended PHP.ini file to enable JIT.
; ====================================
; Place this file under /usr/local/etc/php/conf.d/
zend_extension=opcache.so
opcache.enable=1
opcache.enable_cli=1
opcache.validate_timestamps=1
opcache.revalidate_freq=0
opcache.huge_code_pages=1
opcache.jit_buffer_size=100M
opcache.jit=function

View File

@ -1,4 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env sh
VERSION=1.0.0
CURRENT_VERSION=$(cat /tas_version)
if [ "$VERSION" != "$CURRENT_VERSION" ]; then
echo "Wrong docker image version, expected $VERSION, got $CURRENT_VERSION, please run docker compose pull!"
exit 1
fi
composer install
docker-compose-wait \ docker-compose-wait \
&& nice -n 20 php server.php -e=.env.docker --docker "$@" && nice -n 20 php server.php -e=.env.docker --docker "$@"

View File

@ -1,7 +1,7 @@
<?php <?php declare(strict_types=1);
/** /**
* Get all updates from MadelineProto EventHandler running inside TelegramApiServer via websocket * Get all updates from MadelineProto EventHandler running inside TelegramApiServer via websocket.
* @see \TelegramApiServer\Controllers\EventsController * @see \TelegramApiServer\Controllers\EventsController
*/ */
@ -13,7 +13,6 @@ use function Amp\Websocket\Client\connect;
require 'vendor/autoload.php'; require 'vendor/autoload.php';
$shortopts = 'u::'; $shortopts = 'u::';
$longopts = [ $longopts = [
'url::', 'url::',
@ -55,4 +54,4 @@ if (defined('SIGINT')) {
$signal = Amp\trapSignal([SIGINT, SIGTERM]); $signal = Amp\trapSignal([SIGINT, SIGTERM]);
} else { } else {
EventLoop::run(); EventLoop::run();
} }

View File

@ -83,9 +83,6 @@ foreach ($options['session'] as $session) {
} }
StartUpFixes::fix(); StartUpFixes::fix();
foreach ($sessions as $session) {
StartUpFixes::removeBrokenIpc($session);
}
new TelegramApiServer\Server\Server( new TelegramApiServer\Server\Server(
$options, $options,

View File

@ -1,7 +1,8 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer; namespace TelegramApiServer;
use Amp\Sync\LocalKeyedMutex;
use danog\MadelineProto\API; use danog\MadelineProto\API;
use danog\MadelineProto\APIWrapper; use danog\MadelineProto\APIWrapper;
use danog\MadelineProto\Settings; use danog\MadelineProto\Settings;
@ -11,10 +12,9 @@ use InvalidArgumentException;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
use ReflectionProperty; use ReflectionProperty;
use RuntimeException; use RuntimeException;
use TelegramApiServer\EventObservers\EventHandler;
use TelegramApiServer\EventObservers\EventObserver; use TelegramApiServer\EventObservers\EventObserver;
class Client final class Client
{ {
public static Client $self; public static Client $self;
/** @var API[] */ /** @var API[] */
@ -22,10 +22,10 @@ class Client
public static function getInstance(): Client public static function getInstance(): Client
{ {
if (empty(static::$self)) { if (empty(self::$self)) {
static::$self = new static(); self::$self = new static();
} }
return static::$self; return self::$self;
} }
public function connect(array $sessionFiles) public function connect(array $sessionFiles)
@ -40,15 +40,19 @@ class Client
$this->startNotLoggedInSessions(); $this->startNotLoggedInSessions();
$sessionsCount = count($sessionFiles); $sessionsCount = \count($sessionFiles);
warning( warning(
"\nTelegramApiServer ready." "\nTelegramApiServer ready."
. "\nNumber of sessions: {$sessionsCount}." . "\nNumber of sessions: {$sessionsCount}."
); );
} }
private static LocalKeyedMutex $mutex;
public function addSession(string $session, array $settings = []): API public function addSession(string $session, array $settings = []): API
{ {
self::$mutex ??= new LocalKeyedMutex;
$lock = self::$mutex->acquire($session);
if (isset($this->instances[$session])) { if (isset($this->instances[$session])) {
throw new InvalidArgumentException('Session already exists'); throw new InvalidArgumentException('Session already exists');
} }
@ -58,8 +62,8 @@ class Client
if ($settings) { if ($settings) {
Files::saveSessionSettings($session, $settings); Files::saveSessionSettings($session, $settings);
} }
$settings = array_replace_recursive( $settings = \array_replace_recursive(
(array)Config::getInstance()->get('telegram'), (array) Config::getInstance()->get('telegram'),
Files::getSessionSettings($session), Files::getSessionSettings($session),
); );
@ -87,14 +91,9 @@ class Client
$instance->unsetEventHandler(); $instance->unsetEventHandler();
} }
unset($instance); unset($instance);
gc_collect_cycles(); \gc_collect_cycles();
} }
/**
* @param string|null $session
*
* @return API
*/
public function getSession(?string $session = null): API public function getSession(?string $session = null): API
{ {
if (!$this->instances) { if (!$this->instances) {
@ -104,8 +103,8 @@ class Client
} }
if (!$session) { if (!$session) {
if (count($this->instances) === 1) { if (\count($this->instances) === 1) {
$session = (string)array_key_first($this->instances); $session = (string) \array_key_first($this->instances);
} else { } else {
throw new InvalidArgumentException( throw new InvalidArgumentException(
'Multiple sessions detected. Specify which session to use. See README for examples.' 'Multiple sessions detected. Specify which session to use. See README for examples.'
@ -160,9 +159,10 @@ class Client
return $wrapper; return $wrapper;
} }
private static function getSettingsFromArray(string $session, array $settings, SettingsAbstract $settingsObject = new Settings()): SettingsAbstract { private static function getSettingsFromArray(string $session, array $settings, SettingsAbstract $settingsObject = new Settings()): SettingsAbstract
{
foreach ($settings as $key => $value) { foreach ($settings as $key => $value) {
if (is_array($value) && $key !== 'proxies') { if (\is_array($value) && $key !== 'proxies') {
if ($key === 'db' && isset($value['type'])) { if ($key === 'db' && isset($value['type'])) {
$type = match ($value['type']) { $type = match ($value['type']) {
'memory' => new Settings\Database\Memory(), 'memory' => new Settings\Database\Memory(),
@ -180,23 +180,22 @@ class Client
} }
unset($value[$value['type']], $value['type'],); unset($value[$value['type']], $value['type'],);
if (count($value) === 0) { if (\count($value) === 0) {
continue; continue;
} }
} }
$method = 'get' . ucfirst(str_replace('_', '', ucwords($key, '_'))); $method = 'get' . \ucfirst(\str_replace('_', '', \ucwords($key, '_')));
self::getSettingsFromArray($session, $value, $settingsObject->$method()); self::getSettingsFromArray($session, $value, $settingsObject->$method());
} else { } else {
if ($key === 'serializer' && is_string($value)) { if ($key === 'serializer' && \is_string($value)) {
$value = SerializerType::from($value); $value = SerializerType::from($value);
} }
$method = 'set' . ucfirst(str_replace('_', '', ucwords($key, '_'))); $method = 'set' . \ucfirst(\str_replace('_', '', \ucwords($key, '_')));
$settingsObject->$method($value); $settingsObject->$method($value);
} }
} }
return $settingsObject; return $settingsObject;
} }
} }

View File

@ -1,26 +1,24 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer; namespace TelegramApiServer;
final class Config
class Config
{ {
private static ?Config $instance = null; private static ?Config $instance = null;
private array $config; private array $config;
public static function getInstance(): Config public static function getInstance(): Config
{ {
if (null === static::$instance) { if (null === self::$instance) {
static::$instance = new static(); self::$instance = new static();
} }
return static::$instance; return self::$instance;
} }
/** /**
* is not allowed to call from outside to prevent from creating multiple instances, * is not allowed to call from outside to prevent from creating multiple instances,
* to use the singleton, you have to obtain the instance from Singleton::getInstance() instead * to use the singleton, you have to obtain the instance from Singleton::getInstance() instead.
*/ */
private function __construct() private function __construct()
{ {
@ -39,12 +37,12 @@ class Config
private function findByKey($key) private function findByKey($key)
{ {
$key = (string)$key; $key = (string) $key;
$path = explode('.', $key); $path = \explode('.', $key);
$value = &$this->config; $value = &$this->config;
foreach ($path as $pathKey) { foreach ($path as $pathKey) {
if (!is_array($value) || !array_key_exists($pathKey, $value)) { if (!\is_array($value) || !\array_key_exists($pathKey, $value)) {
return null; return null;
} }
$value = &$value[$pathKey]; $value = &$value[$pathKey];
@ -53,4 +51,4 @@ class Config
return $value; return $value;
} }
} }

View File

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\Controllers; namespace TelegramApiServer\Controllers;
@ -18,8 +18,6 @@ use TelegramApiServer\MadelineProtoExtensions\ApiExtensions;
use TelegramApiServer\MadelineProtoExtensions\SystemApiExtensions; use TelegramApiServer\MadelineProtoExtensions\SystemApiExtensions;
use Throwable; use Throwable;
use UnexpectedValueException; use UnexpectedValueException;
use function Amp\delay;
use function mb_strpos;
abstract class AbstractApiController abstract class AbstractApiController
{ {
@ -29,7 +27,6 @@ abstract class AbstractApiController
protected ?StreamedField $file = null; protected ?StreamedField $file = null;
protected string $extensionClass; protected string $extensionClass;
public array $page = [ public array $page = [
'headers' => self::JSON_HEADER, 'headers' => self::JSON_HEADER,
'success' => false, 'success' => false,
@ -83,19 +80,19 @@ abstract class AbstractApiController
} }
/** /**
* Получаем параметры из GET и POST * Получаем параметры из GET и POST.
* *
*/ */
private function resolveRequest(): void private function resolveRequest(): void
{ {
$query = $this->request->getUri()->getQuery(); $query = $this->request->getUri()->getQuery();
$contentType = (string)$this->request->getHeader('Content-Type'); $contentType = (string) $this->request->getHeader('Content-Type');
parse_str($query, $get); \parse_str($query, $get);
switch (true) { switch (true) {
case $contentType === 'application/x-www-form-urlencoded': case $contentType === 'application/x-www-form-urlencoded':
case mb_strpos($contentType, 'multipart/form-data') !== false: case \str_contains($contentType, 'multipart/form-data'):
$form = (new StreamingFormParser())->parseForm($this->request); $form = (new StreamingFormParser())->parseForm($this->request);
$post = []; $post = [];
@ -107,27 +104,27 @@ abstract class AbstractApiController
//We need to break loop without getting file //We need to break loop without getting file
//All other post field will be omitted, hope we dont need them :) //All other post field will be omitted, hope we dont need them :)
break; break;
} else {
$post[$field->getName()] = $field->buffer();
} }
$post[$field->getName()] = $field->buffer();
} }
break; break;
case $contentType === 'application/json': case $contentType === 'application/json':
$body = $this->request->getBody()->buffer(); $body = $this->request->getBody()->buffer();
$post = json_decode($body, 1); $post = \json_decode($body, true);
break; break;
default: default:
$body = $this->request->getBody()->buffer(); $body = $this->request->getBody()->buffer();
parse_str($body, $post); \parse_str($body, $post);
} }
$this->parameters = array_merge((array)$post, $get); $this->parameters = \array_merge((array) $post, $get);
$this->parameters = array_values($this->parameters); $this->parameters = \array_values($this->parameters);
} }
/** /**
* Получает посты для формирования ответа * Получает посты для формирования ответа.
* *
*/ */
private function generateResponse(): void private function generateResponse(): void
@ -159,16 +156,16 @@ abstract class AbstractApiController
protected function callApiCommon(API $madelineProto) protected function callApiCommon(API $madelineProto)
{ {
$pathCount = count($this->api); $pathCount = \count($this->api);
if ($pathCount === 1 && method_exists($this->extensionClass, $this->api[0])) { if ($pathCount === 1 && \method_exists($this->extensionClass, $this->api[0])) {
/** @var ApiExtensions|SystemApiExtensions $madelineProtoExtensions */ /** @var ApiExtensions|SystemApiExtensions $madelineProtoExtensions */
$madelineProtoExtensions = new $this->extensionClass($madelineProto, $this->request, $this->file); $madelineProtoExtensions = new $this->extensionClass($madelineProto, $this->request, $this->file);
$result = $madelineProtoExtensions->{$this->api[0]}(...$this->parameters); $result = $madelineProtoExtensions->{$this->api[0]}(...$this->parameters);
} else { } else {
if ($this->api[0] === 'API') { if ($this->api[0] === 'API') {
$madelineProto = Client::getWrapper($madelineProto)->getAPI(); $madelineProto = Client::getWrapper($madelineProto)->getAPI();
array_shift($this->api); \array_shift($this->api);
$pathCount = count($this->api); $pathCount = \count($this->api);
} }
//Проверяем нет ли в MadilineProto такого метода. //Проверяем нет ли в MadilineProto такого метода.
switch ($pathCount) { switch ($pathCount) {
@ -190,9 +187,7 @@ abstract class AbstractApiController
} }
/** /**
* @param Throwable $e
* *
* @return AbstractApiController
* @throws Throwable * @throws Throwable
*/ */
private function setError(Throwable $e): self private function setError(Throwable $e): self
@ -210,9 +205,8 @@ abstract class AbstractApiController
} }
/** /**
* Кодирует ответ в нужный формат: json * Кодирует ответ в нужный формат: json.
* *
* @return Response|string
* @throws JsonException * @throws JsonException
*/ */
private function getResponse(): string|Response private function getResponse(): string|Response
@ -230,7 +224,7 @@ abstract class AbstractApiController
$data['success'] = true; $data['success'] = true;
} }
$result = json_encode( $result = \json_encode(
$data, $data,
JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR |
JSON_INVALID_UTF8_SUBSTITUTE | JSON_INVALID_UTF8_SUBSTITUTE |
@ -244,15 +238,13 @@ abstract class AbstractApiController
} }
/** /**
* Устанавливает http код ответа (200, 400, 404 и тд.) * Устанавливает http код ответа (200, 400, 404 и тд.).
* *
* @param int $code
* *
* @return AbstractApiController
*/ */
private function setPageCode(int $code): self private function setPageCode(int $code): self
{ {
$this->page['code'] = $this->page['code'] === 200 ? $code : $this->page['code']; $this->page['code'] = $this->page['code'] === 200 ? $code : $this->page['code'];
return $this; return $this;
} }
} }

View File

@ -1,35 +1,25 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\Controllers; namespace TelegramApiServer\Controllers;
use Amp\Sync\LocalKeyedMutex;
use Amp\Sync\LocalMutex;
use Amp\Sync\StaticKeyMutex;
use Amp\Sync\SyncException;
use Exception; use Exception;
use Revolt\EventLoop;
use TelegramApiServer\Client; use TelegramApiServer\Client;
use TelegramApiServer\Config; use TelegramApiServer\Config;
use TelegramApiServer\Logger;
use function Amp\async;
use function Amp\delay;
use function Amp\Future\awaitAll;
class ApiController extends AbstractApiController final class ApiController extends AbstractApiController
{ {
private ?string $session = ''; private ?string $session = '';
/** /**
* Получаем параметры из uri * Получаем параметры из uri.
* *
* @param array $path
* *
*/ */
protected function resolvePath(array $path): void protected function resolvePath(array $path): void
{ {
$this->session = $path['session'] ?? null; $this->session = $path['session'] ?? null;
$this->api = explode('.', $path['method'] ?? ''); $this->api = \explode('.', $path['method'] ?? '');
} }
/** /**
@ -44,37 +34,6 @@ class ApiController extends AbstractApiController
return $this->callApiCommon($madelineProto); return $this->callApiCommon($madelineProto);
} }
//GROUP REQUESTS IN BULKS return $this->callApiCommon($madelineProto);
static $futures = [];
$futures[] = $future = async($this->callApiCommon(...), $madelineProto);
delay($this->waitNextTick());
if ($futures) {
awaitAll($futures);
Logger::getInstance()->notice("Executed bulk requests:" . count($futures));
$futures = [];
}
return $future->await();
} }
}
/**
* Sync threads execution via time ticks
* Need to enable madelineProto futures bulk execution
* @param float $tick interval of execution in seconds.
*/
protected function waitNextTick(float $tick = 0.5): float {
$tickMs = $tick * 1000;
$now = (int)(microtime(true) * 1000);
$currentTick = intdiv((int)(microtime(true) * 1000), $tickMs);
$nextTick = ($currentTick + 1);
$nextTickTime = $nextTick * $tickMs;
$wait = round(($nextTickTime - $now)/1000, 3);
Logger::getInstance()->notice("Waiting $wait seconds before tick");
return $wait;
}
}

View File

@ -1,11 +1,11 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\Controllers; namespace TelegramApiServer\Controllers;
use Amp\Http\HttpStatus;
use Amp\Http\Server\Request; use Amp\Http\Server\Request;
use Amp\Http\Server\Response; use Amp\Http\Server\Response;
use Amp\Http\Server\Router; use Amp\Http\Server\Router;
use Amp\Http\HttpStatus;
use Amp\Http\Server\SocketHttpServer; use Amp\Http\Server\SocketHttpServer;
use Amp\Websocket\Server\Rfc6455Acceptor; use Amp\Websocket\Server\Rfc6455Acceptor;
use Amp\Websocket\Server\Websocket as WebsocketServer; use Amp\Websocket\Server\Websocket as WebsocketServer;
@ -20,7 +20,7 @@ use TelegramApiServer\EventObservers\EventObserver;
use TelegramApiServer\Logger; use TelegramApiServer\Logger;
use Throwable; use Throwable;
class EventsController implements WebsocketClientHandler, WebsocketAcceptor final class EventsController implements WebsocketClientHandler, WebsocketAcceptor
{ {
private const PING_INTERVAL_MS = 10_000; private const PING_INTERVAL_MS = 10_000;
private WebsocketClientGateway $gateway; private WebsocketClientGateway $gateway;
@ -82,7 +82,7 @@ class EventsController implements WebsocketClientHandler, WebsocketAcceptor
EventObserver::startEventHandler($requestedSession); EventObserver::startEventHandler($requestedSession);
$pingLoop = EventLoop::repeat(self::PING_INTERVAL_MS, static fn() => $client->ping()); $pingLoop = EventLoop::repeat(self::PING_INTERVAL_MS, static fn () => $client->ping());
$client->onClose(static function () use ($clientId, $requestedSession, $pingLoop) { $client->onClose(static function () use ($clientId, $requestedSession, $pingLoop) {
EventLoop::cancel($pingLoop); EventLoop::cancel($pingLoop);
@ -104,7 +104,7 @@ class EventsController implements WebsocketClientHandler, WebsocketAcceptor
]; ];
$this->gateway->multicastText( $this->gateway->multicastText(
json_encode( \json_encode(
$update, $update,
JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR |
JSON_INVALID_UTF8_SUBSTITUTE | JSON_INVALID_UTF8_SUBSTITUTE |
@ -115,4 +115,4 @@ class EventsController implements WebsocketClientHandler, WebsocketAcceptor
); );
}); });
} }
} }

View File

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\Controllers; namespace TelegramApiServer\Controllers;
@ -17,7 +17,7 @@ use Revolt\EventLoop;
use TelegramApiServer\EventObservers\LogObserver; use TelegramApiServer\EventObservers\LogObserver;
use TelegramApiServer\Logger; use TelegramApiServer\Logger;
class LogController implements WebsocketClientHandler, WebsocketAcceptor final class LogController implements WebsocketClientHandler, WebsocketAcceptor
{ {
private const PING_INTERVAL_MS = 10_000; private const PING_INTERVAL_MS = 10_000;
private WebsocketClientGateway $gateway; private WebsocketClientGateway $gateway;
@ -68,7 +68,7 @@ class LogController implements WebsocketClientHandler, WebsocketAcceptor
{ {
$clientId = $client->getId(); $clientId = $client->getId();
$pingLoop = EventLoop::repeat(self::PING_INTERVAL_MS, static fn() => $client->ping()); $pingLoop = EventLoop::repeat(self::PING_INTERVAL_MS, static fn () => $client->ping());
$client->onClose(static function () use ($clientId, $pingLoop) { $client->onClose(static function () use ($clientId, $pingLoop) {
EventLoop::cancel($pingLoop); EventLoop::cancel($pingLoop);
@ -90,7 +90,7 @@ class LogController implements WebsocketClientHandler, WebsocketAcceptor
]; ];
$this->gateway->multicastText( $this->gateway->multicastText(
json_encode( \json_encode(
$update, $update,
JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR |
JSON_INVALID_UTF8_SUBSTITUTE | JSON_INVALID_UTF8_SUBSTITUTE |
@ -101,4 +101,4 @@ class LogController implements WebsocketClientHandler, WebsocketAcceptor
); );
}); });
} }
} }

View File

@ -1,25 +1,23 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\Controllers; namespace TelegramApiServer\Controllers;
use Exception; use Exception;
use TelegramApiServer\Client; use TelegramApiServer\Client;
class SystemController extends AbstractApiController final class SystemController extends AbstractApiController
{ {
/** /**
* Получаем параметры из uri * Получаем параметры из uri.
* *
* @param array $path
* *
*/ */
protected function resolvePath(array $path): void protected function resolvePath(array $path): void
{ {
$this->api = explode('.', $path['method'] ?? ''); $this->api = \explode('.', $path['method'] ?? '');
} }
/** /**
* @return mixed
* @throws Exception * @throws Exception
*/ */
protected function callApi() protected function callApi()
@ -29,4 +27,4 @@ class SystemController extends AbstractApiController
return $result; return $result;
} }
} }

View File

@ -4,7 +4,7 @@ namespace TelegramApiServer\EventObservers;
use TelegramApiServer\Files; use TelegramApiServer\Files;
class EventHandler extends \danog\MadelineProto\EventHandler final class EventHandler extends \danog\MadelineProto\EventHandler
{ {
public static array $instances = []; public static array $instances = [];
private string $sessionName; private string $sessionName;
@ -12,8 +12,8 @@ class EventHandler extends \danog\MadelineProto\EventHandler
public function onStart() public function onStart()
{ {
$this->sessionName = Files::getSessionName($this->wrapper->getSession()->getSessionPath()); $this->sessionName = Files::getSessionName($this->wrapper->getSession()->getSessionPath());
if (empty(static::$instances[$this->sessionName])) { if (empty(self::$instances[$this->sessionName])) {
static::$instances[$this->sessionName] = true; self::$instances[$this->sessionName] = true;
warning("Event observer CONSTRUCTED: {$this->sessionName}"); warning("Event observer CONSTRUCTED: {$this->sessionName}");
} }
} }
@ -23,7 +23,7 @@ class EventHandler extends \danog\MadelineProto\EventHandler
if (empty($this->sessionName)) { if (empty($this->sessionName)) {
return; return;
} }
unset(static::$instances[$this->sessionName]); unset(self::$instances[$this->sessionName]);
warning("Event observer DESTRUCTED: {$this->sessionName}"); warning("Event observer DESTRUCTED: {$this->sessionName}");
} }
@ -32,4 +32,4 @@ class EventHandler extends \danog\MadelineProto\EventHandler
info("Received update from session: {$this->sessionName}"); info("Received update from session: {$this->sessionName}");
EventObserver::notify($update, $this->sessionName); EventObserver::notify($update, $this->sessionName);
} }
} }

View File

@ -1,15 +1,14 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\EventObservers; namespace TelegramApiServer\EventObservers;
use danog\MadelineProto\APIWrapper; use danog\MadelineProto\APIWrapper;
use ReflectionProperty; use ReflectionProperty;
use TelegramApiServer\Client; use TelegramApiServer\Client;
use TelegramApiServer\Logger; use TelegramApiServer\Logger;
use Throwable; use Throwable;
class EventObserver final class EventObserver
{ {
use ObserverTrait; use ObserverTrait;
@ -18,7 +17,7 @@ class EventObserver
public static function notify(array $update, string $sessionName) public static function notify(array $update, string $sessionName)
{ {
foreach (static::$subscribers as $clientId => $callback) { foreach (self::$subscribers as $clientId => $callback) {
notice("Pass update to callback. ClientId: {$clientId}"); notice("Pass update to callback. ClientId: {$clientId}");
$callback($update, $sessionName); $callback($update, $sessionName);
} }
@ -26,16 +25,16 @@ class EventObserver
private static function addSessionClient(string $session): void private static function addSessionClient(string $session): void
{ {
if (empty(static::$sessionClients[$session])) { if (empty(self::$sessionClients[$session])) {
static::$sessionClients[$session] = 0; self::$sessionClients[$session] = 0;
} }
++static::$sessionClients[$session]; ++self::$sessionClients[$session];
} }
private static function removeSessionClient(string $session): void private static function removeSessionClient(string $session): void
{ {
if (!empty(static::$sessionClients[$session])) { if (!empty(self::$sessionClients[$session])) {
--static::$sessionClients[$session]; --self::$sessionClients[$session];
} }
} }
@ -43,14 +42,14 @@ class EventObserver
{ {
$sessions = []; $sessions = [];
if ($requestedSession === null) { if ($requestedSession === null) {
$sessions = array_keys(Client::getInstance()->instances); $sessions = \array_keys(Client::getInstance()->instances);
} else { } else {
$sessions[] = $requestedSession; $sessions[] = $requestedSession;
} }
foreach ($sessions as $session) { foreach ($sessions as $session) {
static::addSessionClient($session); self::addSessionClient($session);
if (static::$sessionClients[$session] === 1) { if (self::$sessionClients[$session] === 1) {
warning("Start EventHandler: {$session}"); warning("Start EventHandler: {$session}");
try { try {
$instance = Client::getInstance()->getSession($session); $instance = Client::getInstance()->getSession($session);
@ -60,7 +59,7 @@ class EventObserver
EventHandler::cachePlugins(EventHandler::class); EventHandler::cachePlugins(EventHandler::class);
$wrapper->getAPI()->setEventHandler(EventHandler::class); $wrapper->getAPI()->setEventHandler(EventHandler::class);
} catch (Throwable $e) { } catch (Throwable $e) {
static::removeSessionClient($session); self::removeSessionClient($session);
error('Cant set EventHandler', [ error('Cant set EventHandler', [
'session' => $session, 'session' => $session,
'exception' => Logger::getExceptionAsArray($e), 'exception' => Logger::getExceptionAsArray($e),
@ -74,19 +73,19 @@ class EventObserver
{ {
$sessions = []; $sessions = [];
if ($requestedSession === null) { if ($requestedSession === null) {
$sessions = array_keys(Client::getInstance()->instances); $sessions = \array_keys(Client::getInstance()->instances);
} else { } else {
$sessions[] = $requestedSession; $sessions[] = $requestedSession;
} }
foreach ($sessions as $session) { foreach ($sessions as $session) {
static::removeSessionClient($session); self::removeSessionClient($session);
if (empty(static::$sessionClients[$session]) || $force) { if (empty(self::$sessionClients[$session]) || $force) {
warning("Stopping EventHandler: {$session}"); warning("Stopping EventHandler: {$session}");
Client::getInstance()->instances[$session]->unsetEventHandler(); Client::getInstance()->instances[$session]->unsetEventHandler();
unset(EventHandler::$instances[$session], static::$sessionClients[$session]); unset(EventHandler::$instances[$session], self::$sessionClients[$session]);
} }
} }
} }
} }

View File

@ -1,39 +1,39 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\EventObservers; namespace TelegramApiServer\EventObservers;
use TelegramApiServer\Logger; use TelegramApiServer\Logger;
use Throwable; use Throwable;
class LogObserver final class LogObserver
{ {
use ObserverTrait; use ObserverTrait;
public static function notify(string $level, string $message, array $context = []): void public static function notify(string $level, string $message, array $context = []): void
{ {
foreach (static::$subscribers as $clientId => $callback) { foreach (self::$subscribers as $clientId => $callback) {
$callback($level, $message, $context); $callback($level, $message, $context);
} }
} }
/** /**
* @param mixed|array|string $message * @param mixed|array|string $message
* @param int $level
*/ */
public static function log($message, int $level) public static function log($message, int $level)
{ {
if (is_scalar($message)) { if (\is_scalar($message)) {
Logger::getInstance()->log(Logger::$madelineLevels[$level], (string)$message); Logger::getInstance()->log(Logger::$madelineLevels[$level], (string) $message);
} else { } else {
if ($message instanceof Throwable) { if ($message instanceof Throwable) {
$message = Logger::getExceptionAsArray($message); $message = Logger::getExceptionAsArray($message);
} }
if (is_array($message)) { if (\is_array($message)) {
Logger::getInstance()->log(Logger::$madelineLevels[$level], '', $message); Logger::getInstance()->log(Logger::$madelineLevels[$level], '', $message);
} else { } else {
Logger::getInstance()->log( Logger::getInstance()->log(
Logger::$madelineLevels[$level], Logger::$madelineLevels[$level],
json_encode($message, \json_encode(
$message,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_UNICODE |
JSON_PRETTY_PRINT | JSON_PRETTY_PRINT |
JSON_INVALID_UTF8_SUBSTITUTE | JSON_INVALID_UTF8_SUBSTITUTE |
@ -43,4 +43,4 @@ class LogObserver
} }
} }
} }
} }

View File

@ -1,8 +1,7 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\EventObservers; namespace TelegramApiServer\EventObservers;
trait ObserverTrait trait ObserverTrait
{ {
/** @var callable[] */ /** @var callable[] */
@ -11,17 +10,17 @@ trait ObserverTrait
public static function addSubscriber($clientId, callable $callback): void public static function addSubscriber($clientId, callable $callback): void
{ {
notice("Add event listener. ClientId: {$clientId}"); notice("Add event listener. ClientId: {$clientId}");
static::$subscribers[$clientId] = $callback; self::$subscribers[$clientId] = $callback;
} }
public static function removeSubscriber($clientId): void public static function removeSubscriber($clientId): void
{ {
notice("Removing listener: {$clientId}"); notice("Removing listener: {$clientId}");
unset(static::$subscribers[$clientId]); unset(self::$subscribers[$clientId]);
$listenersCount = count(static::$subscribers); $listenersCount = \count(self::$subscribers);
notice("Event listeners left: {$listenersCount}"); notice("Event listeners left: {$listenersCount}");
if ($listenersCount === 0) { if ($listenersCount === 0) {
static::$subscribers = []; self::$subscribers = [];
} }
} }
} }

View File

@ -1,8 +1,8 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\Exceptions; namespace TelegramApiServer\Exceptions;
class NoMediaException extends NoticeException final class NoMediaException extends NoticeException
{ {
} }

View File

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\Exceptions; namespace TelegramApiServer\Exceptions;
@ -7,4 +7,4 @@ use Exception;
class NoticeException extends Exception class NoticeException extends Exception
{ {
} }

View File

@ -1,10 +1,10 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer; namespace TelegramApiServer;
use RuntimeException; use RuntimeException;
class Files final class Files
{ {
public const SESSION_EXTENSION = '.madeline'; public const SESSION_EXTENSION = '.madeline';
@ -13,11 +13,11 @@ class Files
public static function checkOrCreateSessionFolder(string $session): void public static function checkOrCreateSessionFolder(string $session): void
{ {
$directory = dirname($session); $directory = \dirname($session);
if ($directory && $directory !== '.' && !is_dir($directory)) { if ($directory && $directory !== '.' && !\is_dir($directory)) {
$parentDirectoryPermissions = fileperms(ROOT_DIR); $parentDirectoryPermissions = \fileperms(ROOT_DIR);
if (!mkdir($directory, $parentDirectoryPermissions, true) && !is_dir($directory)) { if (!\mkdir($directory, $parentDirectoryPermissions, true) && !\is_dir($directory)) {
throw new RuntimeException(sprintf('Directory "%s" was not created', $directory)); throw new RuntimeException(\sprintf('Directory "%s" was not created', $directory));
} }
} }
} }
@ -28,8 +28,8 @@ class Files
return null; return null;
} }
preg_match( \preg_match(
'~' . static::SESSION_FOLDER . "/(?'sessionName'.*?)" . static::SESSION_EXTENSION . '~', '~' . self::SESSION_FOLDER . "/(?'sessionName'.*?)" . self::SESSION_EXTENSION . '~',
$sessionFile, $sessionFile,
$matches $matches
); );
@ -37,31 +37,24 @@ class Files
return $matches['sessionName'] ?? null; return $matches['sessionName'] ?? null;
} }
/**
* @param string|null $session
*
* @param string $extension
*
* @return string|null
*/
public static function getSessionFile(?string $session, string $extension = self::SESSION_EXTENSION): ?string public static function getSessionFile(?string $session, string $extension = self::SESSION_EXTENSION): ?string
{ {
if (!$session) { if (!$session) {
return null; return null;
} }
$session = trim(trim($session), '/'); $session = \trim(\trim($session), '/');
$session = static::SESSION_FOLDER . '/' . $session . $extension; $session = self::SESSION_FOLDER . '/' . $session . $extension;
$session = str_replace('//', '/', $session); $session = \str_replace('//', '/', $session);
return $session; return $session;
} }
public static function getSessionSettings(string $session): array public static function getSessionSettings(string $session): array
{ {
$settingsFile = static::getSessionFile($session, static::SETTINGS_EXTENSION); $settingsFile = self::getSessionFile($session, self::SETTINGS_EXTENSION);
$settings = []; $settings = [];
if (file_exists($settingsFile)) { if (\file_exists($settingsFile)) {
$settings = json_decode( $settings = \json_decode(
file_get_contents($settingsFile), \file_get_contents($settingsFile),
true, true,
10, 10,
JSON_THROW_ON_ERROR JSON_THROW_ON_ERROR
@ -73,10 +66,10 @@ class Files
public static function saveSessionSettings(string $session, array $settings = []): void public static function saveSessionSettings(string $session, array $settings = []): void
{ {
$settingsFile = static::getSessionFile($session, static::SETTINGS_EXTENSION); $settingsFile = self::getSessionFile($session, self::SETTINGS_EXTENSION);
file_put_contents( \file_put_contents(
$settingsFile, $settingsFile,
json_encode( \json_encode(
$settings, $settings,
JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR |
JSON_INVALID_UTF8_SUBSTITUTE | JSON_INVALID_UTF8_SUBSTITUTE |
@ -89,11 +82,11 @@ class Files
public static function globRecursive($pattern, $flags = 0): array public static function globRecursive($pattern, $flags = 0): array
{ {
$files = glob($pattern, $flags) ?: []; $files = \glob($pattern, $flags) ?: [];
foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) { foreach (\glob(\dirname($pattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) {
$files = [...$files, ...static::globRecursive($dir . '/' . basename($pattern), $flags)]; $files = [...$files, ...self::globRecursive($dir . '/' . \basename($pattern), $flags)];
} }
return $files; return $files;
} }
} }

View File

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/* /*
* This file is part of the Symfony package. * This file is part of the Symfony package.
@ -11,6 +11,8 @@
namespace TelegramApiServer; namespace TelegramApiServer;
use Amp\ByteStream\Pipe;
use Amp\ByteStream\WritableStream;
use danog\MadelineProto; use danog\MadelineProto;
use DateTimeInterface; use DateTimeInterface;
use Psr\Log\AbstractLogger; use Psr\Log\AbstractLogger;
@ -18,17 +20,18 @@ use Psr\Log\InvalidArgumentException;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
use TelegramApiServer\EventObservers\LogObserver; use TelegramApiServer\EventObservers\LogObserver;
use Throwable; use Throwable;
use function get_class;
use function gettype;
use function is_object;
use const PHP_EOL; use const PHP_EOL;
use function Amp\async;
use function Amp\ByteStream\getStdout;
use function Amp\ByteStream\pipe;
/** /**
* Minimalist PSR-3 logger designed to write in stderr or any other stream. * Minimalist PSR-3 logger designed to write in stderr or any other stream.
* *
* @author Kévin Dunglas <dunglas@gmail.com> * @author Kévin Dunglas <dunglas@gmail.com>
*/ */
class Logger extends AbstractLogger final class Logger extends AbstractLogger
{ {
private static ?Logger $instanse = null; private static ?Logger $instanse = null;
@ -54,13 +57,19 @@ class Logger extends AbstractLogger
private static string $dateTimeFormat = 'Y-m-d H:i:s'; private static string $dateTimeFormat = 'Y-m-d H:i:s';
public int $minLevelIndex; public int $minLevelIndex;
private array $formatter; private \Closure $formatter;
protected function __construct(string $minLevel = LogLevel::WARNING, callable $formatter = null) private WritableStream $stdout;
/**
* @var array<int, list{WritableStream, \Amp\Future}>
*/
private static array $closePromises = [];
protected function __construct(string $minLevel = LogLevel::WARNING, ?\Closure $formatter = null)
{ {
if (null === $minLevel) { if (null === $minLevel) {
if (isset($_ENV['SHELL_VERBOSITY']) || isset($_SERVER['SHELL_VERBOSITY'])) { if (isset($_ENV['SHELL_VERBOSITY']) || isset($_SERVER['SHELL_VERBOSITY'])) {
switch ((int)(isset($_ENV['SHELL_VERBOSITY']) ? $_ENV['SHELL_VERBOSITY'] : switch ((int) (isset($_ENV['SHELL_VERBOSITY']) ? $_ENV['SHELL_VERBOSITY'] :
$_SERVER['SHELL_VERBOSITY'])) { $_SERVER['SHELL_VERBOSITY'])) {
case -1: case -1:
$minLevel = LogLevel::ERROR; $minLevel = LogLevel::ERROR;
@ -79,23 +88,34 @@ class Logger extends AbstractLogger
} }
if (!isset(self::$levels[$minLevel])) { if (!isset(self::$levels[$minLevel])) {
throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $minLevel)); throw new InvalidArgumentException(\sprintf('The log level "%s" does not exist.', $minLevel));
} }
$this->minLevelIndex = self::$levels[$minLevel]; $this->minLevelIndex = \min(self::$levels[$minLevel], self::$levels[self::$madelineLevels[MadelineProto\Logger::VERBOSE]]);
$this->formatter = $formatter ?: [$this, 'format']; $this->formatter = $formatter ?: $this->format(...);
$pipe = new Pipe(PHP_INT_MAX);
$this->stdout = $pipe->getSink();
$source = $pipe->getSource();
$promise = async(static function () use ($source, &$promise): void {
try {
pipe($source, getStdout());
} finally {
unset(self::$closePromises[\spl_object_id($promise)]);
}
});
self::$closePromises[\spl_object_id($promise)] = [$this->stdout, $promise];
} }
public static function getInstance(): Logger public static function getInstance(): Logger
{ {
if (!static::$instanse) { if (!self::$instanse) {
$settings = Config::getInstance()->get('telegram'); $settings = Config::getInstance()->get('telegram');
$loggerLevel = static::$madelineLevels[$settings['logger']['level']]; $loggerLevel = self::$madelineLevels[$settings['logger']['level']];
static::$instanse = new static($loggerLevel); self::$instanse = new static($loggerLevel);
} }
return static::$instanse; return self::$instanse;
} }
/** /**
@ -104,7 +124,7 @@ class Logger extends AbstractLogger
public function log($level, $message, array $context = []): void public function log($level, $message, array $context = []): void
{ {
if (!isset(self::$levels[$level])) { if (!isset(self::$levels[$level])) {
throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level)); throw new InvalidArgumentException(\sprintf('The log level "%s" does not exist.', $level));
} }
LogObserver::notify($level, $message, $context); LogObserver::notify($level, $message, $context);
@ -114,61 +134,77 @@ class Logger extends AbstractLogger
} }
$formatter = $this->formatter; $formatter = $this->formatter;
/** @see Logger::format */ $data = $formatter($level, $message, $context);
echo $formatter($level, $message, $context); ;
try {
$this->stdout->write($data);
} catch (\Throwable) {
echo $data;
}
}
/**
* @internal Internal function used to flush the log buffer on shutdown.
*/
public static function finalize(): void
{
foreach (self::$closePromises as [$stdout, $promise]) {
$stdout->close();
$promise->await();
}
} }
private function format(string $level, string $message, array $context): string private function format(string $level, string $message, array $context): string
{ {
if (false !== strpos($message, '{')) { if (false !== \strpos($message, '{')) {
$replacements = []; $replacements = [];
foreach ($context as $key => $val) { foreach ($context as $key => $val) {
if ($val instanceof Throwable) { if ($val instanceof Throwable) {
$context[$key] = self::getExceptionAsArray($val); $context[$key] = self::getExceptionAsArray($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; $replacements["{{$key}}"] = $val;
} else { } else {
if ($val instanceof DateTimeInterface) { if ($val instanceof DateTimeInterface) {
$replacements["{{$key}}"] = $val->format(static::$dateTimeFormat); $replacements["{{$key}}"] = $val->format(self::$dateTimeFormat);
} else { } else {
if (is_object($val)) { if (\is_object($val)) {
$replacements["{{$key}}"] = '[object ' . get_class($val) . ']'; $replacements["{{$key}}"] = '[object ' . \get_class($val) . ']';
} else { } else {
$replacements["{{$key}}"] = '[' . gettype($val) . ']'; $replacements["{{$key}}"] = '[' . \gettype($val) . ']';
} }
} }
} }
} }
$message = strtr($message, $replacements); $message = \strtr($message, $replacements);
} }
return sprintf( return \sprintf(
'[%s] [%s] %s %s', '[%s] [%s] %s %s',
date(static::$dateTimeFormat), \date(self::$dateTimeFormat),
$level, $level,
$message, $message,
$context ? $context ?
"\n" . "\n" .
json_encode( \json_encode(
$context, $context,
JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PRETTY_PRINT | JSON_UNESCAPED_LINE_TERMINATORS | JSON_UNESCAPED_SLASHES JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PRETTY_PRINT | JSON_UNESCAPED_LINE_TERMINATORS | JSON_UNESCAPED_SLASHES
) )
: '' : ''
) . PHP_EOL; ) . PHP_EOL;
} }
public static function getExceptionAsArray(Throwable $exception) public static function getExceptionAsArray(Throwable $exception)
{ {
return [ return [
'exception' => get_class($exception), 'exception' => \get_class($exception),
'message' => $exception->getMessage(), 'message' => $exception->getMessage(),
'file' => $exception->getFile(), 'file' => $exception->getFile(),
'line' => $exception->getLine(), 'line' => $exception->getLine(),
'code' => $exception->getCode(), 'code' => $exception->getCode(),
'backtrace' => array_slice($exception->getTrace(), 0, 3), 'backtrace' => \array_slice($exception->getTrace(), 0, 3),
'previous exception' => $exception->getPrevious(), 'previous exception' => $exception->getPrevious(),
]; ];
} }
} }

View File

@ -1,9 +1,7 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\MadelineProtoExtensions; namespace TelegramApiServer\MadelineProtoExtensions;
use Amp\Http\Server\FormParser\StreamedField; use Amp\Http\Server\FormParser\StreamedField;
use Amp\Http\Server\Request; use Amp\Http\Server\Request;
use Amp\Http\Server\Response; use Amp\Http\Server\Response;
@ -15,7 +13,7 @@ use TelegramApiServer\EventObservers\EventHandler;
use TelegramApiServer\Exceptions\NoMediaException; use TelegramApiServer\Exceptions\NoMediaException;
use function Amp\delay; use function Amp\delay;
class ApiExtensions final class ApiExtensions
{ {
private MadelineProto\Api $madelineProto; private MadelineProto\Api $madelineProto;
@ -43,12 +41,9 @@ class ApiExtensions
} }
/** /**
* Проверяет есть ли подходящие медиа у сообщения * Проверяет есть ли подходящие медиа у сообщения.
* *
* @param array $message
* @param bool $allowWebPage
* *
* @return bool
*/ */
private static function hasMedia(array $message = [], bool $allowWebPage = false): bool private static function hasMedia(array $message = [], bool $allowWebPage = false): bool
{ {
@ -90,13 +85,13 @@ class ApiExtensions
$text = StrTools::mbSubstr($message, $entity['offset'], $entity['length']); $text = StrTools::mbSubstr($message, $entity['offset'], $entity['length']);
$template = $html[$entity['_']]; $template = $html[$entity['_']];
if (in_array($entity['_'], ['messageEntityTextUrl', 'messageEntityMention', 'messageEntityUrl'])) { if (\in_array($entity['_'], ['messageEntityTextUrl', 'messageEntityMention', 'messageEntityUrl'])) {
$textFormated = sprintf($template, strip_tags($entity['url'] ?? $text), $text); $textFormated = \sprintf($template, \strip_tags($entity['url'] ?? $text), $text);
} else { } else {
$textFormated = sprintf($template, $text); $textFormated = \sprintf($template, $text);
} }
$message = static::substringReplace($message, $textFormated, $entity['offset'], $entity['length']); $message = self::substringReplace($message, $textFormated, $entity['offset'], $entity['length']);
//Увеличим оффсеты всех следующих entity //Увеличим оффсеты всех следующих entity
foreach ($entities as $nextKey => &$nextEntity) { foreach ($entities as $nextKey => &$nextEntity) {
@ -105,7 +100,7 @@ class ApiExtensions
} }
if ($nextEntity['offset'] < ($entity['offset'] + $entity['length'])) { if ($nextEntity['offset'] < ($entity['offset'] + $entity['length'])) {
$nextEntity['offset'] += StrTools::mbStrlen( $nextEntity['offset'] += StrTools::mbStrlen(
preg_replace('~(\>).*<\/.*$~', '$1', $textFormated) \preg_replace('~(\>).*<\/.*$~', '$1', $textFormated)
); );
} else { } else {
$nextEntity['offset'] += StrTools::mbStrlen($textFormated) - StrTools::mbStrlen($text); $nextEntity['offset'] += StrTools::mbStrlen($textFormated) - StrTools::mbStrlen($text);
@ -115,7 +110,7 @@ class ApiExtensions
} }
} }
unset($entity); unset($entity);
$message = nl2br($message); $message = \nl2br($message);
return $message; return $message;
} }
@ -127,7 +122,7 @@ class ApiExtensions
} }
/** /**
* Пересылает сообщения без ссылки на оригинал * Пересылает сообщения без ссылки на оригинал.
* *
* @param array $data * @param array $data
* <pre> * <pre>
@ -141,7 +136,7 @@ class ApiExtensions
*/ */
public function copyMessages(array $data) public function copyMessages(array $data)
{ {
$data = array_merge( $data = \array_merge(
[ [
'from_peer' => '', 'from_peer' => '',
'to_peer' => '', 'to_peer' => '',
@ -157,7 +152,7 @@ class ApiExtensions
] ]
); );
$result = []; $result = [];
if (!$response || !is_array($response) || !array_key_exists('messages', $response)) { if (!$response || !\is_array($response) || !\array_key_exists('messages', $response)) {
return $result; return $result;
} }
@ -167,14 +162,14 @@ class ApiExtensions
'peer' => $data['to_peer'], 'peer' => $data['to_peer'],
'entities' => $message['entities'] ?? [], 'entities' => $message['entities'] ?? [],
]; ];
if (static::hasMedia($message, false)) { if (self::hasMedia($message, false)) {
$messageData['media'] = $message; //MadelineProto сама достанет все media из сообщения. $messageData['media'] = $message; //MadelineProto сама достанет все media из сообщения.
$result[] = $this->madelineProto->messages->sendMedia(...$messageData); $result[] = $this->madelineProto->messages->sendMedia(...$messageData);
} else { } else {
$result[] = $this->madelineProto->messages->sendMessage(...$messageData); $result[] = $this->madelineProto->messages->sendMessage(...$messageData);
} }
if ($key > 0) { if ($key > 0) {
delay(random_int(300, 2000) / 1000); delay(\random_int(300, 2000) / 1000);
} }
} }
@ -182,14 +177,13 @@ class ApiExtensions
} }
/** /**
* Загружает медиафайл из указанного сообщения в поток * Загружает медиафайл из указанного сообщения в поток.
* *
* @param array $data
* *
*/ */
public function getMedia(array $data): Response public function getMedia(array $data): Response
{ {
$data = array_merge( $data = \array_merge(
[ [
'peer' => '', 'peer' => '',
'id' => [0], 'id' => [0],
@ -203,7 +197,7 @@ class ApiExtensions
throw new NoMediaException('Empty message'); throw new NoMediaException('Empty message');
} }
if (!static::hasMedia($message, true)) { if (!self::hasMedia($message, true)) {
throw new NoMediaException('Message has no media'); throw new NoMediaException('Message has no media');
} }
@ -222,17 +216,16 @@ class ApiExtensions
} }
} }
return $this->downloadToResponse($info); return $this->downloadToResponse($info);
} }
/** /**
* Загружает превью медиафайла из указанного сообщения в поток * Загружает превью медиафайла из указанного сообщения в поток.
* *
*/ */
public function getMediaPreview(array $data): Response public function getMediaPreview(array $data): Response
{ {
$data = array_merge( $data = \array_merge(
[ [
'peer' => '', 'peer' => '',
'id' => [0], 'id' => [0],
@ -246,11 +239,11 @@ class ApiExtensions
throw new NoMediaException('Empty message'); throw new NoMediaException('Empty message');
} }
if (!static::hasMedia($message, true)) { if (!self::hasMedia($message, true)) {
throw new NoMediaException('Message has no media'); throw new NoMediaException('Message has no media');
} }
$media = $message['media'][array_key_last($message['media'])]; $media = $message['media'][\array_key_last($message['media'])];
$thumb = null; $thumb = null;
switch (true) { switch (true) {
case isset($media['sizes']): case isset($media['sizes']):
@ -308,15 +301,15 @@ class ApiExtensions
public function getMessages(array $data): array public function getMessages(array $data): array
{ {
$peerInfo = $this->madelineProto->getInfo($data['peer']); $peerInfo = $this->madelineProto->getInfo($data['peer']);
if (in_array($peerInfo['type'], ['channel', 'supergroup'])) { if (\in_array($peerInfo['type'], ['channel', 'supergroup'])) {
$response = $this->madelineProto->channels->getMessages( $response = $this->madelineProto->channels->getMessages(
[ [
'channel' => $data['peer'], 'channel' => $data['peer'],
'id' => (array)$data['id'], 'id' => (array) $data['id'],
] ]
); );
} else { } else {
$response = $this->madelineProto->messages->getMessages(['id' => (array)$data['id']]); $response = $this->madelineProto->messages->getMessages(['id' => (array) $data['id']]);
} }
return $response; return $response;
@ -328,7 +321,6 @@ class ApiExtensions
* @param array $info * @param array $info
* Any downloadable array: message, media etc... * Any downloadable array: message, media etc...
* *
* @return Response
*/ */
public function downloadToResponse(array $info): Response public function downloadToResponse(array $info): Response
{ {
@ -336,7 +328,7 @@ class ApiExtensions
} }
/** /**
* Адаптер для стандартного метода * Адаптер для стандартного метода.
* *
*/ */
public function downloadToBrowser(array $info): Response public function downloadToBrowser(array $info): Response
@ -361,7 +353,7 @@ class ApiExtensions
$this->file->getMimeType(), $this->file->getMimeType(),
$this->file->getFilename() $this->file->getFilename()
); );
$inputFile['id'] = unpack('P', $inputFile['id'])['1']; $inputFile['id'] = \unpack('P', $inputFile['id'])['1'];
return [ return [
'media' => [ 'media' => [
'_' => 'inputMediaUploadedDocument', '_' => 'inputMediaUploadedDocument',
@ -384,7 +376,6 @@ class ApiExtensions
Client::getWrapper($this->madelineProto)->serialize(); Client::getWrapper($this->madelineProto)->serialize();
} }
public function getUpdates(array $params): array public function getUpdates(array $params): array
{ {
foreach ($params as $key => $value) { foreach ($params as $key => $value) {
@ -410,12 +401,13 @@ class ApiExtensions
Client::getWrapper($this->madelineProto)->serialize(); Client::getWrapper($this->madelineProto)->serialize();
} }
public function unsubscribeFromUpdates(?string $channel = null): array { public function unsubscribeFromUpdates(?string $channel = null): array
{
$inputChannelId = null; $inputChannelId = null;
if ($channel) { if ($channel) {
$id = (string) $this->madelineProto->getId($channel); $id = (string) $this->madelineProto->getId($channel);
$inputChannelId = (int)str_replace(['-100', '-'], '', $id); $inputChannelId = (int) \str_replace(['-100', '-'], '', $id);
if (!$inputChannelId) { if (!$inputChannelId) {
throw new InvalidArgumentException('Invalid id'); throw new InvalidArgumentException('Invalid id');
} }
@ -438,11 +430,10 @@ class ApiExtensions
$counter++; $counter++;
} }
return [ return [
'disabled_update_loops' => $counter, 'disabled_update_loops' => $counter,
'current_update_loops' => count(Client::getWrapper($this->madelineProto)->getAPI()->feeders), 'current_update_loops' => \count(Client::getWrapper($this->madelineProto)->getAPI()->feeders),
]; ];
} }
} }

View File

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\MadelineProtoExtensions; namespace TelegramApiServer\MadelineProtoExtensions;
@ -13,7 +13,7 @@ use function Amp\async;
use function Amp\File\deleteFile; use function Amp\File\deleteFile;
use function Amp\Future\awaitAll; use function Amp\Future\awaitAll;
class SystemApiExtensions final class SystemApiExtensions
{ {
private Client $client; private Client $client;
@ -41,7 +41,7 @@ class SystemApiExtensions
public function addSession(string $session, array $settings = []): array public function addSession(string $session, array $settings = []): array
{ {
if (!empty($settings['app_info']['api_id'])) { if (!empty($settings['app_info']['api_id'])) {
$settings['app_info']['api_id'] = (int)$settings['app_info']['api_id']; $settings['app_info']['api_id'] = (int) $settings['app_info']['api_id'];
} }
$instance = $this->client->addSession($session, $settings); $instance = $this->client->addSession($session, $settings);
@ -75,7 +75,7 @@ class SystemApiExtensions
foreach ($this->client->instances as $session => $instance) { foreach ($this->client->instances as $session => $instance) {
$authorized = $instance->getAuthorization(); $authorized = $instance->getAuthorization();
switch ($authorized) { switch ($authorized) {
case API::NOT_LOGGED_IN; case API::NOT_LOGGED_IN:
$status = 'NOT_LOGGED_IN'; $status = 'NOT_LOGGED_IN';
break; break;
case API::WAITING_CODE: case API::WAITING_CODE:
@ -107,7 +107,7 @@ class SystemApiExtensions
return [ return [
'sessions' => $sessions, 'sessions' => $sessions,
'memory' => $this->bytesToHuman(memory_get_usage(true)), 'memory' => $this->bytesToHuman(\memory_get_usage(true)),
]; ];
} }
@ -115,10 +115,10 @@ class SystemApiExtensions
{ {
$file = Files::getSessionFile($session); $file = Files::getSessionFile($session);
if (is_file($file)) { if (\is_file($file)) {
$futures = []; $futures = [];
foreach (glob("$file*") as $file) { foreach (\glob("$file*") as $file) {
$futures[] = async(fn() => deleteFile($file)); $futures[] = async(fn () => deleteFile($file));
} }
awaitAll($futures); awaitAll($futures);
} else { } else {
@ -140,7 +140,7 @@ class SystemApiExtensions
public function unlinkSessionSettings($session): string public function unlinkSessionSettings($session): string
{ {
$settings = Files::getSessionFile($session, Files::SETTINGS_EXTENSION); $settings = Files::getSessionFile($session, Files::SETTINGS_EXTENSION);
if (is_file($settings)) { if (\is_file($settings)) {
deleteFile($settings); deleteFile($settings);
} }
@ -149,7 +149,7 @@ class SystemApiExtensions
public function exit(): string public function exit(): string
{ {
EventLoop::defer(static fn() => exit()); EventLoop::defer(static fn () => exit());
return 'ok'; return 'ok';
} }
@ -159,6 +159,6 @@ class SystemApiExtensions
for ($i = 0; $bytes > 1024; $i++) { for ($i = 0; $bytes > 1024; $i++) {
$bytes /= 1024; $bytes /= 1024;
} }
return round($bytes, 2) . ' ' . $units[$i]; return \round($bytes, 2) . ' ' . $units[$i];
} }
} }

View File

@ -1,20 +1,14 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\Migrations; namespace TelegramApiServer\Migrations;
class StartUpFixes use danog\MadelineProto\Magic;
final class StartUpFixes
{ {
public static function fix(): void public static function fix(): void
{ {
define('MADELINE_WORKER_TYPE', 'madeline-ipc'); \define('MADELINE_WORKER_TYPE', 'madeline-ipc');
Magic::$isIpcWorker = true;
} }
}
public static function removeBrokenIpc(string $session): void
{
info('Removing ipc sockets from sessions to fix startup' . PHP_EOL);
foreach (glob(ROOT_DIR . "/$session/*ipc") as $file) {
info("removing: $file");
unlink($file);
}
}
}

View File

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\Server; namespace TelegramApiServer\Server;
@ -9,7 +9,7 @@ use Amp\Http\Server\Response;
use Psr\Log\LoggerInterface as PsrLogger; use Psr\Log\LoggerInterface as PsrLogger;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
class AccessLoggerMiddleware implements Middleware final class AccessLoggerMiddleware implements Middleware
{ {
public function __construct( public function __construct(
private readonly PsrLogger $logger, private readonly PsrLogger $logger,
@ -20,7 +20,7 @@ class AccessLoggerMiddleware implements Middleware
{ {
$method = $request->getMethod(); $method = $request->getMethod();
$uri = (string)$request->getUri(); $uri = (string) $request->getUri();
$protocolVersion = $request->getProtocolVersion(); $protocolVersion = $request->getProtocolVersion();
$remote = Server::getClientIp($request); $remote = Server::getClientIp($request);
@ -58,7 +58,7 @@ class AccessLoggerMiddleware implements Middleware
], ],
]; ];
$level = $status < 400 ? LogLevel::INFO : LogLevel::NOTICE; $level = $status < 400 ? LogLevel::DEBUG : LogLevel::INFO;
$this->logger->log( $this->logger->log(
$level, $level,

View File

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\Server; namespace TelegramApiServer\Server;
@ -9,7 +9,7 @@ use Amp\Http\Server\RequestHandler;
use Amp\Http\Server\Response; use Amp\Http\Server\Response;
use TelegramApiServer\Config; use TelegramApiServer\Config;
class Authorization implements Middleware final class Authorization implements Middleware
{ {
private array $ipWhitelist; private array $ipWhitelist;
private int $selfIp; private int $selfIp;
@ -20,8 +20,8 @@ class Authorization implements Middleware
public function __construct() public function __construct()
{ {
$this->selfIp = ip2long(getHostByName(php_uname('n'))); $this->selfIp = \ip2long(\getHostByName(\php_uname('n')));
$this->ipWhitelist = (array)Config::getInstance()->get('api.ip_whitelist', []); $this->ipWhitelist = (array) Config::getInstance()->get('api.ip_whitelist', []);
$this->passwords = Config::getInstance()->get('api.passwords', []); $this->passwords = Config::getInstance()->get('api.passwords', []);
if (!$this->ipWhitelist && !$this->passwords) { if (!$this->ipWhitelist && !$this->passwords) {
error('API is unprotected! Please specify IP_WHITELIST or PASSWORD in .env.docker'); error('API is unprotected! Please specify IP_WHITELIST or PASSWORD in .env.docker');
@ -37,11 +37,11 @@ class Authorization implements Middleware
} }
if ($this->passwords) { if ($this->passwords) {
$header = (string)$request->getHeader('Authorization'); $header = (string) $request->getHeader('Authorization');
if ($header) { if ($header) {
sscanf($header, "Basic %s", $encodedPassword); \sscanf($header, "Basic %s", $encodedPassword);
[$username, $password] = explode(':', base64_decode($encodedPassword), 2); [$username, $password] = \explode(':', \base64_decode($encodedPassword), 2);
if (array_key_exists($username, $this->passwords) && $this->passwords[$username] === $password) { if (\array_key_exists($username, $this->passwords) && $this->passwords[$username] === $password) {
return $requestHandler->handleRequest($request); return $requestHandler->handleRequest($request);
} }
} }
@ -59,25 +59,25 @@ class Authorization implements Middleware
private function isIpAllowed(string $host): bool private function isIpAllowed(string $host): bool
{ {
if ($this->ipWhitelist && !\in_array($host, $this->ipWhitelist, true)) {
if ($this->ipWhitelist && !in_array($host, $this->ipWhitelist, true)) {
return false; return false;
} }
return true; return true;
} }
private function isLocal(string $host): bool { private function isLocal(string $host): bool
{
if ($host === '127.0.0.1' || $host === 'localhost') { if ($host === '127.0.0.1' || $host === 'localhost') {
return true; return true;
} }
global $options; global $options;
if ($options['docker']) { if ($options['docker']) {
$isSameNetwork = abs(ip2long($host) - $this->selfIp) < 256; $isSameNetwork = \abs(\ip2long($host) - $this->selfIp) < 256;
if ($isSameNetwork) { if ($isSameNetwork) {
return true; return true;
} }
} }
return false; return false;
} }
} }

View File

@ -1,24 +1,22 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\Server; namespace TelegramApiServer\Server;
use Amp\Http\Server\Response; use Amp\Http\Server\Response;
use TelegramApiServer\Controllers\AbstractApiController; use TelegramApiServer\Controllers\AbstractApiController;
class ErrorResponses final class ErrorResponses
{ {
/** /**
* @param int $status
* @param string|array $message * @param string|array $message
* *
* @return Response
*/ */
public static function get(int $status, $message): Response public static function get(int $status, $message): Response
{ {
return new Response( return new Response(
$status, $status,
AbstractApiController::JSON_HEADER, AbstractApiController::JSON_HEADER,
json_encode( \json_encode(
[ [
'success' => false, 'success' => false,
'errors' => [ 'errors' => [
@ -33,4 +31,4 @@ class ErrorResponses
); );
} }
} }

View File

@ -1,12 +1,12 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\Server; namespace TelegramApiServer\Server;
use Amp\Http\HttpStatus;
use Amp\Http\Server\ErrorHandler; use Amp\Http\Server\ErrorHandler;
use Amp\Http\Server\Request; use Amp\Http\Server\Request;
use Amp\Http\Server\RequestHandler\ClosureRequestHandler; use Amp\Http\Server\RequestHandler\ClosureRequestHandler;
use Amp\Http\Server\SocketHttpServer; use Amp\Http\Server\SocketHttpServer;
use Amp\Http\HttpStatus;
use TelegramApiServer\Controllers\ApiController; use TelegramApiServer\Controllers\ApiController;
use TelegramApiServer\Controllers\EventsController; use TelegramApiServer\Controllers\EventsController;
use TelegramApiServer\Controllers\LogController; use TelegramApiServer\Controllers\LogController;
@ -16,7 +16,7 @@ use TelegramApiServer\MadelineProtoExtensions\ApiExtensions;
use TelegramApiServer\MadelineProtoExtensions\SystemApiExtensions; use TelegramApiServer\MadelineProtoExtensions\SystemApiExtensions;
use function Amp\Http\Server\Middleware\stackMiddleware; use function Amp\Http\Server\Middleware\stackMiddleware;
class Router final class Router
{ {
private \Amp\Http\Server\Router $router; private \Amp\Http\Server\Router $router;
private SocketHttpServer $server; private SocketHttpServer $server;
@ -70,5 +70,4 @@ class Router
$this->router->addRoute('GET', '/log/{level:.*?[^/]}[/]', $logHandler); $this->router->addRoute('GET', '/log/{level:.*?[^/]}[/]', $logHandler);
} }
}
}

View File

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
namespace TelegramApiServer\Server; namespace TelegramApiServer\Server;
@ -14,17 +14,14 @@ use Revolt\EventLoop;
use TelegramApiServer\Client; use TelegramApiServer\Client;
use TelegramApiServer\Config; use TelegramApiServer\Config;
use TelegramApiServer\Logger; use TelegramApiServer\Logger;
use function sprintf;
use const SIGINT; use const SIGINT;
use const SIGTERM; use const SIGTERM;
class Server final class Server
{ {
/** /**
* Server constructor. * Server constructor.
* *
* @param array $options
* @param array|null $sessionFiles
*/ */
public function __construct(array $options, ?array $sessionFiles) public function __construct(array $options, ?array $sessionFiles)
{ {
@ -51,7 +48,6 @@ class Server
} }
/** /**
* Stop the server gracefully when SIGINT is received. * Stop the server gracefully when SIGINT is received.
* This is technically optional, but it is best to call Server::stop(). * This is technically optional, but it is best to call Server::stop().
@ -60,29 +56,28 @@ class Server
*/ */
private static function registerShutdown(SocketHttpServer $server) private static function registerShutdown(SocketHttpServer $server)
{ {
if (defined('SIGINT')) { if (\defined('SIGINT')) {
// Await SIGINT or SIGTERM to be received. // Await SIGINT or SIGTERM to be received.
$signal = Amp\trapSignal([SIGINT, SIGTERM]); $signal = Amp\trapSignal([SIGINT, SIGTERM]);
info(sprintf("Received signal %d, stopping HTTP server", $signal)); info(\sprintf("Received signal %d, stopping HTTP server", $signal));
$server->stop(); $server->stop();
} else { } else {
EventLoop::run(); EventLoop::run();
info("Stopping http server"); info("Stopping http server");
$server->stop(); $server->stop();
} }
Logger::finalize();
} }
/** /**
* Установить конфигурацию для http-сервера * Установить конфигурацию для http-сервера.
* *
* @param array $config
* @return array
*/ */
private function getConfig(array $config = []): array private function getConfig(array $config = []): array
{ {
$config = array_filter($config); $config = \array_filter($config);
$config = array_merge( $config = \array_merge(
Config::getInstance()->get('server', []), Config::getInstance()->get('server', []),
$config $config
); );
@ -96,18 +91,18 @@ class Server
if ($realIpHeader) { if ($realIpHeader) {
$remote = $request->getHeader($realIpHeader); $remote = $request->getHeader($realIpHeader);
if (!$remote) { if (!$remote) {
GOTO DIRECT; goto DIRECT;
} }
$tmp = explode(',', $remote); $tmp = \explode(',', $remote);
$remote = trim(end($tmp)); $remote = \trim(\end($tmp));
} else { } else {
DIRECT: DIRECT:
$remote = $request->getClient()->getRemoteAddress()->toString(); $remote = $request->getClient()->getRemoteAddress()->toString();
$hostArray = explode(':', $remote); $hostArray = \explode(':', $remote);
if (count($hostArray) >= 2) { if (\count($hostArray) >= 2) {
$port = (int)array_pop($hostArray); $port = (int) \array_pop($hostArray);
if ($port > 0 && $port <= 65353) { if ($port > 0 && $port <= 65353) {
$remote = implode(':', $hostArray); $remote = \implode(':', $hostArray);
} }
} }
@ -116,4 +111,4 @@ class Server
return $remote; return $remote;
} }
} }