diff --git a/README.md b/README.md index ac5e09e19..ec0335ad0 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,7 @@ Want to add your own open-source project to this list? [Click here!](https://doc * [FAQ](https://docs.madelineproto.xyz/docs/FAQ.html) - Here's a list of common MadelineProto questions and answers. * [Upgrading from MadelineProto v7 to v8](https://docs.madelineproto.xyz/docs/UPGRADING.html) - MadelineProto v8 is a major MadelineProto update, that removes a large number of long-deprecated APIs: I've created this upgrade checklist, to simplify the upgrade process. * [Using methods](https://docs.madelineproto.xyz/docs/USING_METHODS.html) - There are simplifications for many, if not all of, these methods. - * [Named arguments (PHP 8+)](https://docs.madelineproto.xyz/docs/USING_METHODS.html#named-arguments) + * [Named arguments](https://docs.madelineproto.xyz/docs/USING_METHODS.html#named-arguments) * [Peers](https://docs.madelineproto.xyz/docs/USING_METHODS.html#peers) * [Files](https://docs.madelineproto.xyz/docs/FILES.html) * [Secret chats](https://docs.madelineproto.xyz/docs/USING_METHODS.html#secret-chats) diff --git a/docs b/docs index 61cdff1c4..dbfd462ed 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 61cdff1c445fd53456b63d33a6feac12e4023514 +Subproject commit dbfd462edb6a96c6bb1214b5def5e350a669100e diff --git a/examples/libtgvoipbot.php b/examples/libtgvoipbot.php index ece6daa83..90e177778 100644 --- a/examples/libtgvoipbot.php +++ b/examples/libtgvoipbot.php @@ -47,7 +47,7 @@ class MyEventHandler extends SimpleEventHandler public function convertCmd((Incoming&Message&HasAudio)|(Incoming&Message&HasDocument) $message): void { $reply = $message->reply("Conversion in progress..."); - try { + async(function () use ($message, $reply): void { $pipe = self::getStreamPipe(); $sink = $pipe->getSink(); async( @@ -62,9 +62,7 @@ class MyEventHandler extends SimpleEventHandler fileName: $message->media->fileName.".ogg", replyToMsgId: $message->id ); - } finally { - $reply->delete(); - } + })->finally($reply->delete(...)); } } diff --git a/psalm-baseline.xml b/psalm-baseline.xml index fdd9746c0..b94bfebaf 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -908,6 +908,10 @@ copy unwrap + + $payload[0] + $payload[0] + $data Wrapper @@ -1508,6 +1512,9 @@ seek seek + + $l + diff --git a/src/API.php b/src/API.php index 70f8c4e25..609b7b43e 100644 --- a/src/API.php +++ b/src/API.php @@ -51,7 +51,7 @@ final class API extends AbstractAPI * * @var string */ - public const RELEASE = '8.0.0-beta145'; + public const RELEASE = '8.0.0-beta147'; /** * Secret chat was not found. * diff --git a/src/Broadcast/InternalState.php b/src/Broadcast/InternalState.php index cc3defb85..9b6eb29ce 100644 --- a/src/Broadcast/InternalState.php +++ b/src/Broadcast/InternalState.php @@ -24,6 +24,7 @@ use Amp\CancelledException; use Amp\DeferredCancellation; use danog\MadelineProto\Logger; use danog\MadelineProto\MTProto; +use Revolt\EventLoop; use Throwable; use Webmozart\Assert\Assert; @@ -109,7 +110,7 @@ final class InternalState } private function notifyProgress(): void { - $this->API->saveUpdate(['_' => 'updateBroadcastProgress', 'progress' => $this->getProgress()]); + EventLoop::queue($this->API->saveUpdate(...), ['_' => 'updateBroadcastProgress', 'progress' => $this->getProgress()]); } private function gatherPeers(): void { diff --git a/src/Connection.php b/src/Connection.php index 460407514..4a6489d6d 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -21,6 +21,8 @@ declare(strict_types=1); namespace danog\MadelineProto; use Amp\ByteStream\ClosedException; +use Amp\ByteStream\ReadableBuffer; +use Amp\ByteStream\ReadableStream; use Amp\DeferredFuture; use Amp\Sync\LocalMutex; use AssertionError; @@ -35,10 +37,12 @@ use danog\MadelineProto\MTProtoSession\Session; use danog\MadelineProto\Stream\BufferedStreamInterface; use danog\MadelineProto\Stream\ConnectionContext; use danog\MadelineProto\Stream\MTProtoBufferInterface; -use danog\MadelineProto\TL\Conversion\Extension; use danog\MadelineProto\TL\Exception as TLException; +use Revolt\EventLoop; use Webmozart\Assert\Assert; +use function Amp\ByteStream\buffer; + /** * Connection class. * @@ -105,6 +109,10 @@ final class Connection * Whether we're currently reading an MTProto packet. */ private bool $reading = false; + /** + * Whether we're currently writing an MTProto packet. + */ + private bool $writing = false; /** * Logger instance. * @@ -160,6 +168,7 @@ final class Connection */ public function writing(bool $writing): void { + $this->writing = $writing; $this->shared->writing($writing, $this->id); } /** @@ -177,6 +186,13 @@ final class Connection { return $this->reading; } + /** + * Whether we're currently writing an MTProto packet. + */ + public function isWriting(): bool + { + return $this->writing; + } /** * Indicate a received HTTP response. */ @@ -310,7 +326,7 @@ final class Connection } throw new AssertionError("Could not connect to DC {$this->datacenterId}!"); } finally { - $lock->release(); + EventLoop::queue($lock->release(...)); } } /** @@ -362,16 +378,24 @@ final class Connection } } if (\is_array($arguments['media']) && isset($arguments['media']['_'])) { - if ($arguments['media']['_'] === 'inputMediaPhotoExternal') { - $arguments['media']['_'] = 'inputMediaUploadedPhoto'; - $arguments['media']['file'] = new RemoteUrl($arguments['media']['url']); - } elseif ($arguments['media']['_'] === 'inputMediaDocumentExternal') { - $arguments['media']['_'] = 'inputMediaUploadedDocument'; - $arguments['media']['file'] = new RemoteUrl($arguments['media']['url']); - $arguments['media']['mime_type'] = Extension::getMimeFromExtension( - \pathinfo($arguments['media']['url'], PATHINFO_EXTENSION), - 'application/octet-stream' - ); + $this->API->processMedia($arguments['media']); + if ($arguments['media']['_'] === 'inputMediaUploadedPhoto' + && ( + $arguments['media']['file'] instanceof ReadableStream + || ( + $arguments['media']['file'] instanceof FileCallback + && $arguments['media']['file']->file instanceof ReadableStream + ) + ) + ) { + if ($arguments['media']['file'] instanceof FileCallback) { + $arguments['media']['file'] = new FileCallback( + new ReadableBuffer(buffer($arguments['media']['file']->file)), + $arguments['media']['file']->callback + ); + } else { + $arguments['media']['file'] = new ReadableBuffer(buffer($arguments['media']['file'])); + } } } } elseif ($method === 'messages.sendMultiMedia') { diff --git a/src/DataCenter.php b/src/DataCenter.php index cf4b01a68..27f6a7278 100644 --- a/src/DataCenter.php +++ b/src/DataCenter.php @@ -41,6 +41,7 @@ use danog\MadelineProto\Stream\MTProtoTransport\ObfuscatedStream; use danog\MadelineProto\Stream\Transport\DefaultStream; use danog\MadelineProto\Stream\Transport\WssStream; use danog\MadelineProto\Stream\Transport\WsStream; +use Revolt\EventLoop; /** * @psalm-type TDcOption=array{ @@ -387,7 +388,7 @@ final class DataCenter $this->sockets[$dc]->setExtra($this->API, $dc, $ctxs); $this->sockets[$dc]->connect(); } finally { - $lock->release(); + EventLoop::queue($lock->release(...)); } } return $this->sockets[$dc]; diff --git a/src/DataCenterConnection.php b/src/DataCenterConnection.php index f52b7c8b3..d666a08df 100644 --- a/src/DataCenterConnection.php +++ b/src/DataCenterConnection.php @@ -197,7 +197,7 @@ final class DataCenterConnection implements JsonSerializable $this->syncAuthorization(); } } finally { - $lock->release(); + EventLoop::queue($lock->release(...)); } if ($this->hasTempAuthKey()) { $connection->pingHttpWaiter(); diff --git a/src/Db/CacheContainer.php b/src/Db/CacheContainer.php index a03404db0..945ddf8b3 100644 --- a/src/Db/CacheContainer.php +++ b/src/Db/CacheContainer.php @@ -157,7 +157,7 @@ final class CacheContainer $this->ttl = $newTtl; $this->cache = $newValues; } finally { - $lock->release(); + EventLoop::queue($lock->release(...)); } } } diff --git a/src/Db/Driver/Mysql.php b/src/Db/Driver/Mysql.php index 65ae581fa..a8b5e891f 100644 --- a/src/Db/Driver/Mysql.php +++ b/src/Db/Driver/Mysql.php @@ -24,6 +24,7 @@ use danog\MadelineProto\Logger; use danog\MadelineProto\Settings\Database\Mysql as DatabaseMysql; use PDO; use PDOException; +use Revolt\EventLoop; use Throwable; /** @@ -79,7 +80,7 @@ final class Mysql ]; } } finally { - $lock->release(); + EventLoop::queue($lock->release(...)); } return self::$connections[$dbKey]; diff --git a/src/Db/Driver/Postgres.php b/src/Db/Driver/Postgres.php index 362312d90..082c2e0a1 100644 --- a/src/Db/Driver/Postgres.php +++ b/src/Db/Driver/Postgres.php @@ -21,6 +21,7 @@ use Amp\Postgres\PostgresConnectionPool; use Amp\Sync\LocalKeyedMutex; use danog\MadelineProto\Logger; use danog\MadelineProto\Settings\Database\Postgres as DatabasePostgres; +use Revolt\EventLoop; use Throwable; /** @@ -51,7 +52,7 @@ final class Postgres self::$connections[$dbKey] = new PostgresConnectionPool($config, $settings->getMaxConnections(), $settings->getIdleTimeout()); } } finally { - $lock->release(); + EventLoop::queue($lock->release(...)); } return self::$connections[$dbKey]; diff --git a/src/Db/Driver/Redis.php b/src/Db/Driver/Redis.php index 95574e364..28360a2a1 100644 --- a/src/Db/Driver/Redis.php +++ b/src/Db/Driver/Redis.php @@ -21,6 +21,7 @@ use Amp\Redis\RedisClient; use Amp\Redis\RedisConfig; use Amp\Sync\LocalKeyedMutex; use danog\MadelineProto\Settings\Database\Redis as DatabaseRedis; +use Revolt\EventLoop; use function Amp\Redis\createRedisConnector; @@ -51,7 +52,7 @@ final class Redis self::$connections[$dbKey]->ping(); } } finally { - $lock->release(); + EventLoop::queue($lock->release(...)); } return self::$connections[$dbKey]; diff --git a/src/EventHandler.php b/src/EventHandler.php index 991999857..89664928d 100644 --- a/src/EventHandler.php +++ b/src/EventHandler.php @@ -263,7 +263,7 @@ abstract class EventHandler extends AbstractAPI } finally { $this->startDeferred = null; $startDeferred->complete(); - $lock->release(); + EventLoop::queue($lock->release(...)); } } /** diff --git a/src/EventHandler/Action.php b/src/EventHandler/Action.php index 003d38a1f..8883ebfaa 100644 --- a/src/EventHandler/Action.php +++ b/src/EventHandler/Action.php @@ -16,6 +16,7 @@ namespace danog\MadelineProto\EventHandler; +use danog\MadelineProto\EventHandler\Action\Cancel; use danog\MadelineProto\EventHandler\Action\ChooseContact; use danog\MadelineProto\EventHandler\Action\ChooseSticker; use danog\MadelineProto\EventHandler\Action\EmojiSeen; @@ -56,7 +57,8 @@ abstract class Action implements JsonSerializable } return match ($type) { 'sendMessageTypingAction' => new Typing, - 'sendMessageCancelAction' => new GamePlay, + 'sendMessageCancelAction' => new Cancel, + 'sendMessageGamePlayAction' => new GamePlay, 'sendMessageGeoLocationAction' => new GeoLocation, 'sendMessageChooseContactAction' => new ChooseContact, 'sendMessageChooseStickerAction' => new ChooseSticker, @@ -79,7 +81,8 @@ abstract class Action implements JsonSerializable { return match (true) { $this instanceof Typing => [ '_' => 'sendMessageTypingAction' ], - $this instanceof GamePlay => [ '_' => 'sendMessageCancelAction' ], + $this instanceof Cancel => [ '_' => 'sendMessageCancelAction' ], + $this instanceof GamePlay => [ '_' => 'sendMessageGamePlayAction' ], $this instanceof GeoLocation => [ '_' => 'sendMessageGeoLocationAction' ], $this instanceof ChooseContact => [ '_' => 'sendMessageChooseContactAction' ], $this instanceof ChooseSticker => [ '_' => 'sendMessageChooseStickerAction' ], diff --git a/src/Ipc/Client.php b/src/Ipc/Client.php index c21d11e00..dfa9678fd 100644 --- a/src/Ipc/Client.php +++ b/src/Ipc/Client.php @@ -237,26 +237,19 @@ final class Client extends ClientAbstract */ public function methodCallAsyncRead(string $method, array $args = [], array $aargs = []) { - if (($method === 'messages.editInlineBotMessage' || + if (( + $method === 'messages.editInlineBotMessage' || $method === 'messages.uploadMedia' || $method === 'messages.sendMedia' || - $method === 'messages.editMessage') && - isset($args['media']['file']) && - $args['media']['file'] instanceof FileCallbackInterface - ) { - $params = [$method, &$args, $aargs]; - $wrapper = Wrapper::create($params, $this->session, $this->logger); - $wrapper->wrap($args['media']['file'], true); - return $this->__call('methodCallAsyncRead', $wrapper); + $method === 'messages.editMessage' + ) && isset($args['media']) && \is_array($args['media'])) { + $this->processMedia($args['media'], true); } elseif ($method === 'messages.sendMultiMedia' && isset($args['multi_media'])) { - $params = [$method, &$args, $aargs]; - $wrapper = Wrapper::create($params, $this->session, $this->logger); foreach ($args['multi_media'] as &$media) { - if (isset($media['media']['file']) && $media['media']['file'] instanceof FileCallbackInterface) { - $wrapper->wrap($media['media']['file'], true); + if (\is_array($media['media'])) { + $this->processMedia($media['media'], true); } } - return $this->__call('methodCallAsyncRead', $wrapper); } return $this->__call('methodCallAsyncRead', [$method, $args, $aargs]); } diff --git a/src/Ipc/Server.php b/src/Ipc/Server.php index 694de6be9..894ba9c66 100644 --- a/src/Ipc/Server.php +++ b/src/Ipc/Server.php @@ -214,14 +214,17 @@ class Server extends Loop } catch (Throwable $e) { Logger::log("Exception in IPC connection: $e"); } finally { - try { - $socket->disconnect(); - } catch (Throwable $e) { - } - if ($payload === self::SHUTDOWN) { - Shutdown::removeCallback('restarter'); - $this->stop(); - } + EventLoop::queue(function () use ($socket, $payload): void { + try { + $socket->disconnect(); + } catch (Throwable $e) { + Logger::log("Exception during shutdown in IPC connection: $e"); + } + if ($payload === self::SHUTDOWN) { + Shutdown::removeCallback('restarter'); + $this->stop(); + } + }); } } /** @@ -243,10 +246,13 @@ class Server extends Loop $result = new ExitFailure($e); } finally { if (isset($wrapper)) { - try { - $wrapper->disconnect(); - } catch (Throwable $e) { - } + EventLoop::queue(function () use ($wrapper): void { + try { + $wrapper->disconnect(); + } catch (Throwable $e) { + Logger::log("Exception during shutdown in IPC connection: $e"); + } + }); } } try { diff --git a/src/Ipc/Wrapper.php b/src/Ipc/Wrapper.php index 8a3591882..d990a2a7c 100644 --- a/src/Ipc/Wrapper.php +++ b/src/Ipc/Wrapper.php @@ -20,6 +20,7 @@ use Amp\ByteStream\ReadableStream as ByteStreamReadableStream; use Amp\ByteStream\WritableStream as ByteStreamWritableStream; use Amp\Cancellation; use Amp\Ipc\Sync\ChannelledSocket; +use danog\MadelineProto\FileCallback as MadelineProtoFileCallback; use danog\MadelineProto\FileCallbackInterface; use danog\MadelineProto\Ipc\Wrapper\Cancellation as WrapperCancellation; use danog\MadelineProto\Ipc\Wrapper\FileCallback; @@ -107,6 +108,13 @@ final class Wrapper extends ClientAbstract public function wrap(mixed &$callback, bool $wrapObjects = true): void { if (\is_object($callback) && $wrapObjects) { + if ($callback instanceof FileCallbackInterface) { + $file = $callback->getFile(); + if ($file instanceof ByteStreamReadableStream) { + $this->wrap($file, true); + $callback = new MadelineProtoFileCallback($file, $callback); + } + } $ids = []; foreach (\get_class_methods($callback) as $method) { $id = $this->id++; @@ -115,9 +123,9 @@ final class Wrapper extends ClientAbstract } $class = null; if ($callback instanceof ByteStreamReadableStream) { - $class = \method_exists($callback, 'seek') ? ReadableStream::class : SeekableReadableStream::class; + $class = \method_exists($callback, 'seek') ? SeekableReadableStream::class : ReadableStream::class; } elseif ($callback instanceof ByteStreamWritableStream) { - $class = \method_exists($callback, 'seek') ? WritableStream::class : SeekableWritableStream::class; + $class = \method_exists($callback, 'seek') ? SeekableWritableStream::class : WritableStream::class; } elseif ($callback instanceof FileCallbackInterface) { $class = FileCallback::class; } elseif ($callback instanceof Cancellation) { @@ -154,7 +162,7 @@ final class Wrapper extends ClientAbstract EventLoop::queue($this->clientRequest(...), $id++, $payload); } } finally { - $this->server->disconnect(); + EventLoop::queue($this->server->disconnect(...)); } } @@ -175,11 +183,11 @@ final class Wrapper extends ClientAbstract try { $this->server->send([$id, $result]); } catch (Throwable $e) { - $this->logger->logger("Got error while trying to send result of reverse method: $e", Logger::ERROR); + $this->logger->logger("Got error while trying to send result of reverse method {$payload[0]}: $e", Logger::ERROR); try { $this->server->send([$id, new ExitFailure($e)]); } catch (Throwable $e) { - $this->logger->logger("Got error while trying to send error of error of reverse method: $e", Logger::ERROR); + $this->logger->logger("Got error while trying to send error of error of reverse method {$payload[0]}: $e", Logger::ERROR); } } } diff --git a/src/Ipc/Wrapper/ClosableTrait.php b/src/Ipc/Wrapper/ClosableTrait.php index 70368b69b..90ab82a29 100644 --- a/src/Ipc/Wrapper/ClosableTrait.php +++ b/src/Ipc/Wrapper/ClosableTrait.php @@ -62,6 +62,6 @@ trait ClosableTrait final public function __destruct() { - $this->close(); + EventLoop::queue($this->close(...)); } } diff --git a/src/Loop/Connection/CheckLoop.php b/src/Loop/Connection/CheckLoop.php index b595b900a..5264299aa 100644 --- a/src/Loop/Connection/CheckLoop.php +++ b/src/Loop/Connection/CheckLoop.php @@ -132,9 +132,9 @@ final class CheckLoop extends Loop } } $this->connection->flush(); - } catch (CancelledException) { - $this->logger->logger("We did not receive a response for {$this->timeout} seconds: reconnecting and exiting check loop on DC {$this->datacenter}"); - EventLoop::queue($this->connection->reconnect(...)); + //} catch (CancelledException) { + //$this->logger->logger("We did not receive a response for {$this->timeout} seconds: reconnecting and exiting check loop on DC {$this->datacenter}"); + //EventLoop::queue($this->connection->reconnect(...)); } catch (\Throwable $e) { $this->logger->logger("Got exception in check loop for DC {$this->datacenter}"); $this->logger->logger((string) $e); diff --git a/src/Loop/Connection/ReadLoop.php b/src/Loop/Connection/ReadLoop.php index 01faaa5b6..1e9856eb0 100644 --- a/src/Loop/Connection/ReadLoop.php +++ b/src/Loop/Connection/ReadLoop.php @@ -64,6 +64,7 @@ final class ReadLoop extends Loop if ($e instanceof NothingInTheSocketException && !$this->connection->hasPendingCalls() && $this->connection->isMedia() + && !$this->connection->isWriting() ) { $this->logger->logger("Got NothingInTheSocketException in DC {$this->datacenter}, disconnecting because we have nothing to do...", Logger::ERROR); $this->connection->disconnect(true); diff --git a/src/Loop/VoIP/DjLoop.php b/src/Loop/VoIP/DjLoop.php index c565d2441..3c2c85362 100644 --- a/src/Loop/VoIP/DjLoop.php +++ b/src/Loop/VoIP/DjLoop.php @@ -211,7 +211,7 @@ final class DjLoop extends VoIPLoop Ogg::convert($f, $pipe->getSink(), $cancellation); } catch (CancelledException) { } finally { - $pipe->getSink()->close(); + EventLoop::queue($pipe->getSink()->close(...)); } }); $it = new Ogg($pipe->getSource()); diff --git a/src/MTProto.php b/src/MTProto.php index 96ff1b2d8..43d897e5d 100644 --- a/src/MTProto.php +++ b/src/MTProto.php @@ -1670,7 +1670,7 @@ final class MTProto implements TLCallback, LoggerGetter $this->logger->logger('Reported!'); } } finally { - $lock->release(); + EventLoop::queue($lock->release(...)); } } /** diff --git a/src/MTProtoSession/CallHandler.php b/src/MTProtoSession/CallHandler.php index f900e6004..8592573c3 100644 --- a/src/MTProtoSession/CallHandler.php +++ b/src/MTProtoSession/CallHandler.php @@ -130,7 +130,7 @@ trait CallHandler if (isset($args['message']) && \is_string($args['message']) && \mb_strlen($args['message'], 'UTF-8') > ($this->API->getConfig())['message_length_max'] && \mb_strlen($this->API->parseMode($args)['message'], 'UTF-8') > ($this->API->getConfig())['message_length_max']) { $args = $this->API->splitToChunks($args); $promises = []; - $aargs['queue'] = $method; + $aargs['queue'] = $method.' '.\time(); $aargs['multiple'] = true; } if (isset($aargs['multiple'])) { diff --git a/src/MTProtoSession/ResponseHandler.php b/src/MTProtoSession/ResponseHandler.php index a63befca3..3b8f668c3 100644 --- a/src/MTProtoSession/ResponseHandler.php +++ b/src/MTProtoSession/ResponseHandler.php @@ -312,6 +312,17 @@ trait ResponseHandler } EventLoop::queue(closure: $this->methodRecall(...), message_id: $request->getMsgId(), datacenter: $datacenter); return null; + case 400: + if ($request->hasQueue() && + ( + $response['error_message'] === 'MSG_WAIT_FAILED' + || $response['error_message'] === 'MSG_WAIT_TIMEOUT' + ) + ) { + EventLoop::queue(closure: $this->methodRecall(...), message_id: $request->getMsgId()); + return null; + } + return fn () => new RPCErrorException($response['error_message'], $response['error_code'], $request->getConstructor()); case 401: switch ($response['error_message']) { case 'USER_DEACTIVATED': diff --git a/src/MTProtoTools/FileServer.php b/src/MTProtoTools/FileServer.php index 7f6a38f33..a6229e36f 100644 --- a/src/MTProtoTools/FileServer.php +++ b/src/MTProtoTools/FileServer.php @@ -35,6 +35,7 @@ use danog\MadelineProto\Settings; use danog\MadelineProto\Settings\AppInfo; use danog\MadelineProto\Tools; use Exception; +use Revolt\EventLoop; use Throwable; use function Amp\File\exists; @@ -216,7 +217,7 @@ trait FileServer $this->checkDownloadScript($f); return self::$checkedAutoload[$autoloadPath] = $f; } finally { - $lock->release(); + EventLoop::queue($lock->release(...)); } } @@ -249,7 +250,7 @@ trait FileServer self::$checkedScripts[$scriptUrl] = true; } finally { - $lock->release(); + EventLoop::queue($lock->release(...)); } } } diff --git a/src/MTProtoTools/Files.php b/src/MTProtoTools/Files.php index ddf2122d5..fae3e7ffe 100644 --- a/src/MTProtoTools/Files.php +++ b/src/MTProtoTools/Files.php @@ -23,7 +23,21 @@ namespace danog\MadelineProto\MTProtoTools; use Amp\DeferredFuture; use Amp\Future; use Amp\Http\Client\Request; +use AssertionError; use danog\MadelineProto\EventHandler\Media; +use danog\MadelineProto\EventHandler\Media\AnimatedSticker; +use danog\MadelineProto\EventHandler\Media\Audio; +use danog\MadelineProto\EventHandler\Media\CustomEmoji; +use danog\MadelineProto\EventHandler\Media\Document; +use danog\MadelineProto\EventHandler\Media\DocumentPhoto; +use danog\MadelineProto\EventHandler\Media\Gif; +use danog\MadelineProto\EventHandler\Media\MaskSticker; +use danog\MadelineProto\EventHandler\Media\Photo; +use danog\MadelineProto\EventHandler\Media\RoundVideo; +use danog\MadelineProto\EventHandler\Media\StaticSticker; +use danog\MadelineProto\EventHandler\Media\Video; +use danog\MadelineProto\EventHandler\Media\VideoSticker; +use danog\MadelineProto\EventHandler\Media\Voice; use danog\MadelineProto\EventHandler\Message; use danog\MadelineProto\Exception; use danog\MadelineProto\FileCallbackInterface; @@ -57,8 +71,88 @@ use function Amp\Future\awaitFirst; trait Files { use FilesLogic; - use FilesAbstraction; use FileServer; + /** + * Wrap a media constructor into an abstract Media object. + */ + public function wrapMedia(array $media, bool $protected = false): ?Media + { + if ($media['_'] === 'messageMediaPhoto') { + if (!isset($media['photo'])) { + return null; + } + return new Photo($this, $media, $protected); + } + if ($media['_'] !== 'messageMediaDocument') { + return null; + } + if (!isset($media['document'])) { + return null; + } + $has_video = null; + $has_document_photo = null; + $has_animated = false; + foreach ($media['document']['attributes'] as $attr) { + $t = $attr['_']; + if ($t === 'documentAttributeImageSize') { + $has_document_photo = $attr; + continue; + } + if ($t === 'documentAttributeAnimated') { + $has_animated = true; + continue; + } + if ($t === 'documentAttributeSticker') { + if ($has_video) { + return new VideoSticker($this, $media, $attr, $has_video, $protected); + } + + if ($has_document_photo === null) { + throw new AssertionError("has_document_photo === null: ".\json_encode($media['document'])); + } + + if ($attr['mask']) { + return new MaskSticker($this, $media, $attr, $has_document_photo, $protected); + } + + if ($media['document']['mime_type'] === 'application/x-tgsticker') { + return new AnimatedSticker($this, $media, $attr, $has_document_photo, $protected); + } + + return new StaticSticker($this, $media, $attr, $has_document_photo, $protected); + } + if ($t === 'documentAttributeVideo') { + $has_video = $attr; + continue; + } + if ($t === 'documentAttributeAudio') { + return $attr['voice'] + ? new Voice($this, $media, $attr, $protected) + : new Audio($this, $media, $attr, $protected); + } + if ($t === 'documentAttributeCustomEmoji') { + if ($has_document_photo === null) { + throw new AssertionError("has_document_photo === null: ".\json_encode($media['document'])); + } + return new CustomEmoji($this, $media, $attr, $has_document_photo, $protected); + } + } + if ($has_animated) { + if ($has_video === null) { + throw new AssertionError("has_video === null: ".\json_encode($media['document'])); + } + return new Gif($this, $media, $has_video, $protected); + } + if ($has_video) { + return $has_video['round_message'] + ? new RoundVideo($this, $media, $has_video, $protected) + : new Video($this, $media, $has_video, $protected); + } + if ($has_document_photo) { + return new DocumentPhoto($this, $media, $has_document_photo, $protected); + } + return new Document($this, $media, $protected); + } /** * Upload file from URL. * @@ -859,16 +953,14 @@ trait Files $this->logger->logger('Waiting for lock of file to download...'); $unlock = Tools::flock("$file.lock", LOCK_EX); $this->logger->logger('Got lock of file to download'); - try { - $this->downloadToStream($messageMedia, $stream, $cb, $size, -1); - } finally { + async($this->downloadToStream(...), $messageMedia, $stream, $cb, $size, -1)->finally(function () use ($stream, $unlock, $file): void { + $stream->close(); $unlock(); try { deleteFile("$file.lock"); - } catch (Throwable) { + } catch (\Throwable) { } - $stream->close(); - } + })->await(); return $file; } /** diff --git a/src/MTProtoTools/FilesAbstraction.php b/src/MTProtoTools/FilesAbstraction.php index 14b129974..fae3ce0c4 100644 --- a/src/MTProtoTools/FilesAbstraction.php +++ b/src/MTProtoTools/FilesAbstraction.php @@ -24,19 +24,8 @@ use Amp\ByteStream\ReadableStream; use AssertionError; use danog\MadelineProto\BotApiFileId; use danog\MadelineProto\EventHandler\Media; -use danog\MadelineProto\EventHandler\Media\AnimatedSticker; -use danog\MadelineProto\EventHandler\Media\Audio; -use danog\MadelineProto\EventHandler\Media\CustomEmoji; use danog\MadelineProto\EventHandler\Media\Document; -use danog\MadelineProto\EventHandler\Media\DocumentPhoto; -use danog\MadelineProto\EventHandler\Media\Gif; -use danog\MadelineProto\EventHandler\Media\MaskSticker; use danog\MadelineProto\EventHandler\Media\Photo; -use danog\MadelineProto\EventHandler\Media\RoundVideo; -use danog\MadelineProto\EventHandler\Media\StaticSticker; -use danog\MadelineProto\EventHandler\Media\Video; -use danog\MadelineProto\EventHandler\Media\VideoSticker; -use danog\MadelineProto\EventHandler\Media\Voice; use danog\MadelineProto\EventHandler\Message; use danog\MadelineProto\FileCallback; use danog\MadelineProto\LocalFile; diff --git a/src/MTProtoTools/FilesLogic.php b/src/MTProtoTools/FilesLogic.php index 9fe9beda0..cd56539a7 100644 --- a/src/MTProtoTools/FilesLogic.php +++ b/src/MTProtoTools/FilesLogic.php @@ -17,6 +17,7 @@ namespace danog\MadelineProto\MTProtoTools; use Amp\ByteStream\Pipe; +use Amp\ByteStream\ReadableBuffer; use Amp\ByteStream\ReadableResourceStream; use Amp\ByteStream\ReadableStream; use Amp\ByteStream\StreamException; @@ -34,6 +35,7 @@ use danog\MadelineProto\BotApiFileId; use danog\MadelineProto\EventHandler\Media; use danog\MadelineProto\EventHandler\Message; use danog\MadelineProto\Exception; +use danog\MadelineProto\FileCallback; use danog\MadelineProto\FileCallbackInterface; use danog\MadelineProto\Lang; use danog\MadelineProto\LocalFile; @@ -55,6 +57,7 @@ use Webmozart\Assert\Assert; use const FILTER_VALIDATE_URL; use function Amp\async; +use function Amp\ByteStream\buffer; use function Amp\File\exists; use function Amp\File\getSize; @@ -67,6 +70,7 @@ use function Amp\File\openFile; */ trait FilesLogic { + use FilesAbstraction; /** * Download file to browser. * @@ -139,13 +143,7 @@ trait FilesLogic { $pipe = new Pipe(1024*1024); $sink = $pipe->getSink(); - EventLoop::queue(function () use ($messageMedia, $sink, $cb, $offset, $end): void { - try { - $this->downloadToStream($messageMedia, $sink, $cb, $offset, $end); - } finally { - $sink->close(); - } - }); + async($this->downloadToStream(...), $messageMedia, $sink, $cb, $offset, $end)->finally($sink->close(...)); return $pipe->getSource(); } /** @@ -191,7 +189,7 @@ trait FilesLogic } $stream->write($payload); } finally { - $l->release(); + EventLoop::queue($l->release(...)); } return \strlen($payload); }; @@ -260,6 +258,42 @@ trait FilesLogic return $this->upload($file, $fileName, $cb, true); } + /** + * @internal + */ + public function processMedia(array &$media, bool $upload = false): void + { + if ($media['_'] === 'inputMediaPhotoExternal') { + $media['_'] = 'inputMediaUploadedPhoto'; + if ($media['url'] instanceof FileCallbackInterface) { + $media['file'] = new FileCallback( + new RemoteUrl($media['url']->getFile()), + $media['url'] + ); + } else { + $media['file'] = new RemoteUrl($media['url']); + } + unset($media['url']); + } elseif ($media['_'] === 'inputMediaDocumentExternal') { + $media['_'] = 'inputMediaUploadedDocument'; + if ($media['url'] instanceof FileCallbackInterface) { + $media['file'] = new FileCallback( + new RemoteUrl($url = $media['url']->getFile()), + $media['url'] + ); + } else { + $media['file'] = new RemoteUrl($url = $media['url']); + } + unset($media['url']); + $media['mime_type'] = Extension::getMimeFromExtension( + \pathinfo($url, PATHINFO_EXTENSION), + 'application/octet-stream' + ); + } + if ($upload) { + $media['file'] = $this->upload($media['file']); + } + } /** * Upload file. * @@ -316,11 +350,7 @@ trait FilesLogic } $stream = openFile($file, 'rb'); $mime = Extension::getMimeFromFile($file); - try { - return $this->uploadFromStream($stream, $size, $mime, $fileName, $cb, $encrypted); - } finally { - $stream->close(); - } + return async($this->uploadFromStream(...), $stream, $size, $mime, $fileName, $cb, $encrypted)->finally($stream->close(...))->await(); } /** @@ -356,6 +386,17 @@ trait FilesLogic } } $created = false; + if (!$size) { + if ($seekable && \method_exists($stream, 'tell')) { + $stream->seek(0, Whence::End); + $size = $stream->tell(); + $stream->seek(0); + } elseif ($stream instanceof ReadableBuffer) { + $stream = buffer($stream); + $size = \strlen($stream); + $stream = new ReadableBuffer($stream); + } + } if ($stream instanceof File) { $lock = new LocalMutex; $callable = static function (int $offset, int $size) use ($stream, $seekable, $lock) { @@ -369,7 +410,7 @@ trait FilesLogic } return $stream->read(null, $size); } finally { - $l->release(); + EventLoop::queue($l->release(...)); } }; } else { @@ -392,11 +433,6 @@ trait FilesLogic }; $seekable = false; } - if (!$size && $seekable && \method_exists($stream, 'tell')) { - $stream->seek(0, Whence::End); - $size = $stream->tell(); - $stream->seek(0); - } $res = $this->uploadFromCallable($callable, $size, $mime, $fileName, $cb, $seekable, $encrypted); if ($created) { /** @var StreamInterface $stream */ diff --git a/src/MTProtoTools/MinDatabase.php b/src/MTProtoTools/MinDatabase.php index 6d4e69e03..6b2e006ab 100644 --- a/src/MTProtoTools/MinDatabase.php +++ b/src/MTProtoTools/MinDatabase.php @@ -220,7 +220,7 @@ final class MinDatabase implements TLCallback } } finally { unset($this->pendingDb[$id]); - $lock->release(); + EventLoop::queue($lock->release(...)); } } public function populateFrom(array $object) diff --git a/src/MTProtoTools/PeerDatabase.php b/src/MTProtoTools/PeerDatabase.php index 517e55673..be2d92114 100644 --- a/src/MTProtoTools/PeerDatabase.php +++ b/src/MTProtoTools/PeerDatabase.php @@ -289,7 +289,7 @@ final class PeerDatabase implements TLCallback $this->usernames[$username] = $id; } } finally { - $lock->release(); + EventLoop::queue($lock->release(...)); } } @@ -450,7 +450,7 @@ final class PeerDatabase implements TLCallback if (isset($o) && $this->pendingDb[$id] === $o) { unset($this->pendingDb[$id]); } - $lock->release(); + EventLoop::queue($lock->release(...)); } } public function addChatBlocking(int $chat): void @@ -584,7 +584,7 @@ final class PeerDatabase implements TLCallback if (isset($o) && $this->pendingDb[$id] === $o) { unset($this->pendingDb[$id]); } - $lock->release(); + EventLoop::queue($lock->release(...)); } } /** diff --git a/src/MTProtoTools/ReferenceDatabase.php b/src/MTProtoTools/ReferenceDatabase.php index cde960cb9..e1f32bf4f 100644 --- a/src/MTProtoTools/ReferenceDatabase.php +++ b/src/MTProtoTools/ReferenceDatabase.php @@ -148,7 +148,7 @@ final class ReferenceDatabase implements TLCallback $this->db[$location] = $locationValue; } finally { unset($this->pendingDb[$location]); - $lock->release(); + EventLoop::queue($lock->release(...)); } } public function getMethodAfterResponseDeserializationCallbacks(): array diff --git a/src/Magic.php b/src/Magic.php index fa7d1a72f..badd6426c 100644 --- a/src/Magic.php +++ b/src/Magic.php @@ -308,6 +308,9 @@ final class Magic throw Exception::extension($extension); } } + if (\extension_loaded('psr')) { + throw new Exception("Please uninstall the psr extension to use MadelineProto!"); + } self::$BIG_ENDIAN = \pack('L', 1) === \pack('N', 1); self::$hasOpenssl = \extension_loaded('openssl'); self::$emojis = \json_decode(self::JSON_EMOJIS); diff --git a/src/TL/TL.php b/src/TL/TL.php index a29f941cc..1ef0c085a 100644 --- a/src/TL/TL.php +++ b/src/TL/TL.php @@ -981,12 +981,6 @@ final class TL implements TLInterface case 'true': $x[$arg['name']] = ($x[$arg['flag']] & $arg['pow']) !== 0; continue 2; - case 'Bool': - if (($x[$arg['flag']] & $arg['pow']) === 0) { - $x[$arg['name']] = false; - continue 2; - } - // no break default: if (($x[$arg['flag']] & $arg['pow']) === 0) { continue 2; diff --git a/src/Tools.php b/src/Tools.php index b67317ca7..1287d5ad0 100644 --- a/src/Tools.php +++ b/src/Tools.php @@ -705,6 +705,9 @@ abstract class Tools extends AsyncTools */ public static function validateEventHandlerClass(string $class): array { + if (!\extension_loaded('tokenizer')) { + throw \danog\MadelineProto\Exception::extension('tokenizer'); + } $plugin = \is_subclass_of($class, PluginEventHandler::class); $file = (new ReflectionClass($class))->getFileName(); $code = read($file); @@ -754,6 +757,14 @@ abstract class Tools extends AsyncTools $name = $call->name->toLowerString(); if (isset(self::BLOCKING_FUNCTIONS[$name])) { + if ($name === 'fopen' && + isset($call->args[0]) && + $call->args[0] instanceof Arg && + $call->args[0]->value instanceof String_ && + \str_starts_with($call->args[0]->value->value, 'php://memory') + ) { + continue; + } $explanation = self::BLOCKING_FUNCTIONS[$name]; $issues []= new EventHandlerIssue( message: \sprintf(Lang::$current_lang['do_not_use_blocking_function'], $name, $explanation), diff --git a/src/Wrappers/DialogHandler.php b/src/Wrappers/DialogHandler.php index 668e16a05..3f8c0a881 100644 --- a/src/Wrappers/DialogHandler.php +++ b/src/Wrappers/DialogHandler.php @@ -25,6 +25,7 @@ use danog\MadelineProto\API; use danog\MadelineProto\Exception; use danog\MadelineProto\Settings; use danog\MadelineProto\Tools; +use Revolt\EventLoop; use Throwable; use Webmozart\Assert\Assert; @@ -93,7 +94,7 @@ trait DialogHandler } $this->cachedAllBotUsers = true; } finally { - $lock->release(); + EventLoop::queue($lock->release(...)); } } private function searchRightPts(): void @@ -188,7 +189,7 @@ trait DialogHandler try { $this->getFullDialogsInternal(false); } finally { - $lock->release(); + EventLoop::queue($lock->release(...)); } return true; } diff --git a/tests/testing.php b/tests/testing.php index 696f6225e..37d3fb26c 100755 --- a/tests/testing.php +++ b/tests/testing.php @@ -15,7 +15,9 @@ If not, see . * Various ways to load MadelineProto. */ +use Amp\ByteStream\ReadableBuffer; use danog\MadelineProto\API; +use danog\MadelineProto\FileCallback; use danog\MadelineProto\Logger; use danog\MadelineProto\Settings; use danog\MadelineProto\VoIP; @@ -84,16 +86,6 @@ $MadelineProto = new API(__DIR__.'/../testing.madeline', $settings); $MadelineProto->start(); $MadelineProto->fileGetContents('https://google.com'); -try { - $MadelineProto->getSelf(); -} catch (\danog\MadelineProto\Exception $e) { - if ($e->getMessage() === 'TOS action required, check the logs') { - $MadelineProto->acceptTos(); - } -} - -//var_dump(count($MadelineProto->getPwrChat('@madelineproto')['participants'])); - /* * Test logging */ @@ -260,7 +252,7 @@ $media = []; $media['photo'] = ['_' => 'inputMediaUploadedPhoto', 'file' => __DIR__.'/faust.jpg']; // Image by URL -$media['photo'] = ['_' => 'inputMediaPhotoExternal', 'url' => 'https://github.com/danog/MadelineProto/raw/v8/tests/faust.jpg']; +$media['photo_url'] = ['_' => 'inputMediaPhotoExternal', 'url' => 'https://github.com/danog/MadelineProto/raw/v8/tests/faust.jpg']; // Sticker $media['sticker'] = ['_' => 'inputMediaUploadedDocument', 'file' => __DIR__.'/lel.webp', 'attributes' => [['_' => 'documentAttributeSticker', 'alt' => 'LEL']]]; @@ -278,7 +270,7 @@ $media['voice'] = ['_' => 'inputMediaUploadedDocument', 'file' => __DIR__.'/mosc $media['document'] = ['_' => 'inputMediaUploadedDocument', 'file' => __DIR__.'/60', 'mime_type' => 'magic/magic', 'attributes' => [['_' => 'documentAttributeFilename', 'file_name' => 'magic.magic']]]; // Document by URL -$media['document'] = ['_' => 'inputMediaDocumentExternal', 'url' => 'https://github.com/danog/MadelineProto/raw/v8/tests/60']; +$media['document_url'] = ['_' => 'inputMediaDocumentExternal', 'url' => 'https://github.com/danog/MadelineProto/raw/v8/tests/60']; $message = 'yay '.PHP_VERSION_ID; $mention = $MadelineProto->getInfo(getenv('TEST_USERNAME')); // Returns an array with all of the constructors that can be extracted from a username or an id @@ -288,6 +280,60 @@ $peers = json_decode(getenv('TEST_DESTINATION_GROUPS'), true); if (!$peers) { die("No TEST_DESTINATION_GROUPS array was provided!"); } + +foreach ($media as &$inputMedia) { + $inputMedia['content'] = isset($inputMedia['file']) + ? read($inputMedia['file']) + : $MadelineProto->fileGetContents($inputMedia['url']); +} + +function eq(string $file, string $contents, string $type): void +{ + if ($type !== 'photo' && $type !== 'photo_url') { + Assert::eq(read($file), $contents, "Not equal $type!"); + } +} + +function sendMedia(API $MadelineProto, array $media, string $message, string $mention, mixed $peer, string $type): void +{ + $medias = [ + 'base' => $media + ]; + /*if (isset($media['file']) && is_string($media['file'])) { + $MadelineProto->sendDocument( + peer: $peer, + file: new ReadableBuffer(read($media['file'])), + callback: fn ($v) => $MadelineProto->logger($v), + fileName: basename($media['file']) + ); + $medias['callback'] = array_merge( + $media, + ['file' => new FileCallback($media['file'], fn ($v) => $MadelineProto->logger(...))] + ); + $medias['stream'] = array_merge( + $media, + ['file' => new ReadableBuffer(read($media['file']))] + ); + $medias['callback stream'] = array_merge( + $media, + ['file' => new FileCallback(new ReadableBuffer(read($media['file'])), fn ($v) => $MadelineProto->logger(...))] + ); + } elseif (isset($media['url'])) { + $medias['callback'] = array_merge( + $media, + ['url' => new FileCallback($media['url'], fn ($v) => $MadelineProto->logger(...))] + ); + }*/ + foreach ($medias as $subtype => $m) { + $MadelineProto->logger("Sending $type $subtype"); + $dl = $MadelineProto->extractMessage($MadelineProto->messages->sendMedia(['peer' => $peer, 'media' => $m, 'message' => '['.$message.'](mention:'.$mention.')', 'parse_mode' => 'markdown'])); + + $MadelineProto->logger("Downloading $type $subtype"); + $file = $MadelineProto->downloadToDir($dl, '/tmp'); + eq($file, $m['content'], $type); + } +} + foreach ($peers as $peer) { $sentMessage = $MadelineProto->messages->sendMessage(['peer' => $peer, 'message' => $message, 'entities' => [['_' => 'inputMessageEntityMentionName', 'offset' => 0, 'length' => mb_strlen($message), 'user_id' => $mention]]]); $MadelineProto->logger($sentMessage, Logger::NOTICE); @@ -303,26 +349,15 @@ foreach ($peers as $peer) { ['_' => 'inputSingleMedia', 'media' => $inputMedia, 'message' => '['.$message.'](mention:'.$mention.')', 'parse_mode' => 'markdown'], ]]); } - $fileOrig = isset($inputMedia['file']) - ? read($inputMedia['file']) - : $MadelineProto->fileGetContents($inputMedia['url']); - $MadelineProto->logger("Sending $type"); - $dl = $MadelineProto->extractMessage($MadelineProto->messages->sendMedia(['peer' => $peer, 'media' => $inputMedia, 'message' => '['.$message.'](mention:'.$mention.')', 'parse_mode' => 'markdown'])); - $MadelineProto->logger("Downloading $type"); - $file = $MadelineProto->downloadToDir($dl, '/tmp'); - if ($type !== 'photo') { - Assert::eq(read($file), $fileOrig, "Not equal!"); - } + sendMedia($MadelineProto, $inputMedia, $message, $mention, $peer, $type); $MadelineProto->logger("Uploading $type"); $media = $MadelineProto->messages->uploadMedia(['peer' => '@me', 'media' => $inputMedia]); $MadelineProto->logger("Downloading $type"); $file = $MadelineProto->downloadToDir($media, '/tmp'); - if ($type !== 'photo') { - Assert::eq(read($file), $fileOrig, "Not equal!"); - } + eq($file, $inputMedia['content'], $type); $MadelineProto->logger("Re-sending $type"); $inputMedia['file'] = $media; @@ -331,9 +366,7 @@ foreach ($peers as $peer) { $MadelineProto->logger("Re-downloading $type"); $file = $MadelineProto->downloadToDir($dl, '/tmp'); - if ($type !== 'photo') { - Assert::eq(read($file), $fileOrig, "Not equal!"); - } + eq($file, $inputMedia['content'], $type); } }