diff --git a/examples/secret_bot.php b/examples/secret_bot.php index 4527c5c3..c616832b 100755 --- a/examples/secret_bot.php +++ b/examples/secret_bot.php @@ -134,7 +134,7 @@ if (\file_exists('.env')) { echo 'Loading settings...'.PHP_EOL; $settings = \json_decode(\getenv('MTPROTO_SETTINGS'), true) ?: []; -$MadelineProto = new \danog\MadelineProto\API('s.madeline', $settings); +$MadelineProto = new \danog\MadelineProto\API('secret.madeline', $settings); // Reduce boilerplate with new wrapper method $MadelineProto->startAndLoop(SecretHandler::class); diff --git a/src/danog/MadelineProto/Connection.php b/src/danog/MadelineProto/Connection.php index b966be95..c84caeaf 100644 --- a/src/danog/MadelineProto/Connection.php +++ b/src/danog/MadelineProto/Connection.php @@ -360,35 +360,94 @@ class Connection $this->pinger->start(); } } + /** + * Apply method abstractions. + * + * @param string $method Method name + * @param array $arguments Arguments + * + * @return \Generator Whether we need to resolve a queue promise + */ + private function methodAbstractions(string &$method, array &$arguments): \Generator + { + if ($method === 'messages.importChatInvite' && isset($arguments['hash']) && \is_string($arguments['hash']) && \preg_match('@(?:t|telegram)\\.(?:me|dog)/(joinchat/)?([a-z0-9_-]*)@i', $arguments['hash'], $matches)) { + if ($matches[1] === '') { + $method = 'channels.joinChannel'; + $arguments['channel'] = $matches[2]; + } else { + $arguments['hash'] = $matches[2]; + } + } elseif ($method === 'messages.checkChatInvite' && isset($arguments['hash']) && \is_string($arguments['hash']) && \preg_match('@(?:t|telegram)\\.(?:me|dog)/joinchat/([a-z0-9_-]*)@i', $arguments['hash'], $matches)) { + $arguments['hash'] = $matches[1]; + } elseif ($method === 'channels.joinChannel' && isset($arguments['channel']) && \is_string($arguments['channel']) && \preg_match('@(?:t|telegram)\\.(?:me|dog)/(joinchat/)?([a-z0-9_-]*)@i', $arguments['channel'], $matches)) { + if ($matches[1] !== '') { + $method = 'messages.importChatInvite'; + $arguments['hash'] = $matches[2]; + } + } elseif ($method === 'messages.sendMessage' && isset($arguments['peer']['_']) && \in_array($arguments['peer']['_'], ['inputEncryptedChat', 'updateEncryption', 'updateEncryptedChatTyping', 'updateEncryptedMessagesRead', 'updateNewEncryptedMessage', 'encryptedMessage', 'encryptedMessageService'])) { + $method = 'messages.sendEncrypted'; + $arguments = ['peer' => $arguments['peer'], 'message' => $arguments]; + if (!isset($arguments['message']['_'])) { + $arguments['message']['_'] = 'decryptedMessage'; + } + if (!isset($arguments['message']['ttl'])) { + $arguments['message']['ttl'] = 0; + } + if (isset($arguments['message']['reply_to_msg_id'])) { + $arguments['message']['reply_to_random_id'] = $arguments['message']['reply_to_msg_id']; + } + } elseif ($method === 'messages.sendEncryptedFile') { + if (isset($arguments['file'])) { + if ((!\is_array($arguments['file']) || !(isset($arguments['file']['_']) && $this->API->getTL()->getConstructors()->findByPredicate($arguments['file']['_']) === 'InputEncryptedFile')) && $this->API->getSettings()->getFiles()->getAllowAutomaticUpload()) { + $arguments['file'] = (yield from $this->API->uploadEncrypted($arguments['file'])); + } + if (isset($arguments['file']['key'])) { + $arguments['message']['media']['key'] = $arguments['file']['key']; + } + if (isset($arguments['file']['iv'])) { + $arguments['message']['media']['iv'] = $arguments['file']['iv']; + } + } + $arguments['queuePromise'] = new Deferred; + return $arguments['queuePromise']; + } elseif (\in_array($method, ['messages.addChatUser', 'messages.deleteChatUser', 'messages.editChatAdmin', 'messages.editChatPhoto', 'messages.editChatTitle', 'messages.getFullChat', 'messages.exportChatInvite', 'messages.editChatAdmin', 'messages.migrateChat']) && isset($arguments['chat_id']) && (!\is_numeric($arguments['chat_id']) || $arguments['chat_id'] < 0)) { + $res = (yield from $this->API->getInfo($arguments['chat_id'])); + if ($res['type'] !== 'chat') { + throw new \danog\MadelineProto\Exception('chat_id is not a chat id (only normal groups allowed, not supergroups)!'); + } + $arguments['chat_id'] = $res['chat_id']; + } elseif ($method === 'photos.updateProfilePhoto') { + if (isset($arguments['id'])) { + if (!\is_array($arguments['id'])) { + $method = 'photos.uploadProfilePhoto'; + $arguments['file'] = $arguments['id']; + } + } elseif (isset($arguments['file'])) { + $method = 'photos.uploadProfilePhoto'; + } + } elseif ($method === 'photos.uploadProfilePhoto') { + if (isset($arguments['file'])) { + if (\is_array($arguments['file']) && !\in_array($arguments['file']['_'], ['inputFile', 'inputFileBig'])) { + $method = 'photos.uploadProfilePhoto'; + $arguments['id'] = $arguments['file']; + } + } elseif (isset($arguments['id'])) { + $method = 'photos.updateProfilePhoto'; + } + } elseif ($method === 'messages.uploadMedia') { + if (!isset($arguments['peer']) && !$this->API->getSelf()['bot']) { + $arguments['peer'] = 'me'; + } + } + if ($method === 'messages.sendEncrypted' || $method === 'messages.sendEncryptedService') { + $arguments['queuePromise'] = new Deferred; + return $arguments['queuePromise']; + } + return null; + } /** * Send an MTProto message. * - * Structure of message array: - * [ - * // only in outgoing messages - * 'body' => deserialized body, (optional if container) - * 'serialized_body' => 'serialized body', (optional if container) - * 'contentRelated' => bool, - * '_' => 'predicate', - * 'promise' => deferred promise that gets resolved when a response to the message is received (optional), - * 'send_promise' => deferred promise that gets resolved when the message is sent (optional), - * 'file' => bool (optional), - * 'type' => 'type' (optional), - * 'queue' => queue ID (optional), - * 'container' => [message ids] (optional), - * - * // only in incoming messages - * 'content' => deserialized body, - * 'seq_no' => number (optional), - * 'from_container' => bool (optional), - * - * // can be present in both - * 'response' => message id (optional), - * 'msg_id' => message id (optional), - * 'sent' => timestamp, - * 'tries' => number - * ] - * * @param OutgoingMessage $message The message to send * @param boolean $flush Whether to flush the message right away * @@ -404,7 +463,9 @@ class Connection $this->API->referenceDatabase->refreshNext(true); } if ($message->isMethod()) { - $body = yield from $this->API->getTL()->serializeMethod($message->getConstructor(), $body); + $method = $message->getConstructor(); + $queuePromise = yield from $this->methodAbstractions($method, $body); + $body = yield from $this->API->getTL()->serializeMethod($method, $body); } else { $body['_'] = $message->getConstructor(); $body = yield from $this->API->getTL()->serializeObject(['type' => ''], $body, $message->getConstructor()); @@ -416,6 +477,9 @@ class Connection unset($body); } $this->pendingOutgoing[$this->pendingOutgoingKey++] = $message; + if (isset($queuePromise)) { + $queuePromise->resolve(); + } if ($flush && isset($this->writer)) { $this->writer->resume(); } diff --git a/src/danog/MadelineProto/SecretChats/MessageHandler.php b/src/danog/MadelineProto/SecretChats/MessageHandler.php index 017a709e..afd26d3d 100644 --- a/src/danog/MadelineProto/SecretChats/MessageHandler.php +++ b/src/danog/MadelineProto/SecretChats/MessageHandler.php @@ -19,6 +19,8 @@ namespace danog\MadelineProto\SecretChats; +use Amp\Deferred; +use Amp\Promise; use danog\MadelineProto\MTProtoTools\Crypt; /** @@ -26,17 +28,24 @@ use danog\MadelineProto\MTProtoTools\Crypt; */ trait MessageHandler { + /** + * Secret queue. + * + * @var Promise[] + */ + private $secretQueue = []; /** * Encrypt secret chat message. * - * @param integer $chat_id Chat ID - * @param array $message Message to encrypt + * @param integer $chat_id Chat ID + * @param array $message Message to encrypt + * @param Deferred $queuePromise Queue promise * * @internal * * @return \Generator */ - public function encryptSecretMessage(int $chat_id, array $message): \Generator + public function encryptSecretMessage(int $chat_id, array $message, Deferred $queuePromise): \Generator { if (!isset($this->secret_chats[$chat_id])) { $this->logger->logger(\sprintf(\danog\MadelineProto\Lang::$current_lang['secret_chat_skipping'], $chat_id)); @@ -48,6 +57,13 @@ trait MessageHandler if (($this->secret_chats[$chat_id]['ttr'] <= 0 || \time() - $this->secret_chats[$chat_id]['updated'] > 7 * 24 * 60 * 60) && $this->secret_chats[$chat_id]['rekeying'][0] === 0) { yield from $this->rekey($chat_id); } + if (isset($this->secretQueue[$chat_id])) { + $promise = $this->secretQueue[$chat_id]; + $this->secretQueue[$chat_id] = $queuePromise->promise(); + yield $promise; + } else { + $this->secretQueue[$chat_id] = $queuePromise->promise(); + } $message = ['_' => 'decryptedMessageLayer', 'layer' => $this->secret_chats[$chat_id]['layer'], 'in_seq_no' => $this->generateSecretInSeqNo($chat_id), 'out_seq_no' => $this->generateSecretOutSeqNo($chat_id), 'message' => $message]; $this->secret_chats[$chat_id]['out_seq_no']++; } diff --git a/src/danog/MadelineProto/TL/TL.php b/src/danog/MadelineProto/TL/TL.php index 4de07d49..8dfc20c2 100644 --- a/src/danog/MadelineProto/TL/TL.php +++ b/src/danog/MadelineProto/TL/TL.php @@ -520,7 +520,7 @@ class TL $concat = $this->constructors->findByPredicate('vector')['id']; $concat .= \danog\MadelineProto\Tools::packUnsignedInt(\count($object)); foreach ($object as $k => $current_object) { - $concat .= (yield from $this->serializeObject(['type' => $type['subtype']], $current_object, $k)); + $concat .= (yield from $this->serializeObject(['type' => $type['subtype']], $current_object, $k, $layer)); } return $concat; case 'vector': @@ -529,7 +529,7 @@ class TL } $concat = \danog\MadelineProto\Tools::packUnsignedInt(\count($object)); foreach ($object as $k => $current_object) { - $concat .= (yield from $this->serializeObject(['type' => $type['subtype']], $current_object, $k)); + $concat .= (yield from $this->serializeObject(['type' => $type['subtype']], $current_object, $k, $layer)); } return $concat; case 'Object': @@ -574,7 +574,7 @@ class TL $constructorData = $this->constructors->findByPredicate('inputMessageEntityMentionName'); } $concat = $bare ? '' : $constructorData['id']; - return $concat.(yield from $this->serializeParams($constructorData, $object, '', $layer)); + return $concat.(yield from $this->serializeParams($constructorData, $object, '', $layer, null)); } /** * Serialize method. @@ -588,78 +588,11 @@ class TL */ public function serializeMethod(string $method, $arguments): \Generator { - if ($method === 'messages.importChatInvite' && isset($arguments['hash']) && \is_string($arguments['hash']) && \preg_match('@(?:t|telegram)\\.(?:me|dog)/(joinchat/)?([a-z0-9_-]*)@i', $arguments['hash'], $matches)) { - if ($matches[1] === '') { - $method = 'channels.joinChannel'; - $arguments['channel'] = $matches[2]; - } else { - $arguments['hash'] = $matches[2]; - } - } elseif ($method === 'messages.checkChatInvite' && isset($arguments['hash']) && \is_string($arguments['hash']) && \preg_match('@(?:t|telegram)\\.(?:me|dog)/joinchat/([a-z0-9_-]*)@i', $arguments['hash'], $matches)) { - $arguments['hash'] = $matches[1]; - } elseif ($method === 'channels.joinChannel' && isset($arguments['channel']) && \is_string($arguments['channel']) && \preg_match('@(?:t|telegram)\\.(?:me|dog)/(joinchat/)?([a-z0-9_-]*)@i', $arguments['channel'], $matches)) { - if ($matches[1] !== '') { - $method = 'messages.importChatInvite'; - $arguments['hash'] = $matches[2]; - } - } elseif ($method === 'messages.sendMessage' && isset($arguments['peer']['_']) && \in_array($arguments['peer']['_'], ['inputEncryptedChat', 'updateEncryption', 'updateEncryptedChatTyping', 'updateEncryptedMessagesRead', 'updateNewEncryptedMessage', 'encryptedMessage', 'encryptedMessageService'])) { - $method = 'messages.sendEncrypted'; - $arguments = ['peer' => $arguments['peer'], 'message' => $arguments]; - if (!isset($arguments['message']['_'])) { - $arguments['message']['_'] = 'decryptedMessage'; - } - if (!isset($arguments['message']['ttl'])) { - $arguments['message']['ttl'] = 0; - } - if (isset($arguments['message']['reply_to_msg_id'])) { - $arguments['message']['reply_to_random_id'] = $arguments['message']['reply_to_msg_id']; - } - } elseif ($method === 'messages.sendEncryptedFile') { - if (isset($arguments['file'])) { - if ((!\is_array($arguments['file']) || !(isset($arguments['file']['_']) && $this->constructors->findByPredicate($arguments['file']['_']) === 'InputEncryptedFile')) && $this->API->getSettings()->getFiles()->getAllowAutomaticUpload()) { - $arguments['file'] = (yield from $this->API->uploadEncrypted($arguments['file'])); - } - if (isset($arguments['file']['key'])) { - $arguments['message']['media']['key'] = $arguments['file']['key']; - } - if (isset($arguments['file']['iv'])) { - $arguments['message']['media']['iv'] = $arguments['file']['iv']; - } - } - } elseif (\in_array($method, ['messages.addChatUser', 'messages.deleteChatUser', 'messages.editChatAdmin', 'messages.editChatPhoto', 'messages.editChatTitle', 'messages.getFullChat', 'messages.exportChatInvite', 'messages.editChatAdmin', 'messages.migrateChat']) && isset($arguments['chat_id']) && (!\is_numeric($arguments['chat_id']) || $arguments['chat_id'] < 0)) { - $res = (yield from $this->API->getInfo($arguments['chat_id'])); - if ($res['type'] !== 'chat') { - throw new \danog\MadelineProto\Exception('chat_id is not a chat id (only normal groups allowed, not supergroups)!'); - } - $arguments['chat_id'] = $res['chat_id']; - } elseif ($method === 'photos.updateProfilePhoto') { - if (isset($arguments['id'])) { - if (!\is_array($arguments['id'])) { - $method = 'photos.uploadProfilePhoto'; - $arguments['file'] = $arguments['id']; - } - } elseif (isset($arguments['file'])) { - $method = 'photos.uploadProfilePhoto'; - } - } elseif ($method === 'photos.uploadProfilePhoto') { - if (isset($arguments['file'])) { - if (\is_array($arguments['file']) && !\in_array($arguments['file']['_'], ['inputFile', 'inputFileBig'])) { - $method = 'photos.uploadProfilePhoto'; - $arguments['id'] = $arguments['file']; - } - } elseif (isset($arguments['id'])) { - $method = 'photos.updateProfilePhoto'; - } - } elseif ($method === 'messages.uploadMedia') { - if (!isset($arguments['peer']) && !$this->API->getSelf()['bot']) { - $arguments['peer'] = 'me'; - } - } $tl = $this->methods->findByMethod($method); if ($tl === false) { throw new Exception(\danog\MadelineProto\Lang::$current_lang['method_not_found'].$method); } - return $tl['id'].(yield from $this->serializeParams($tl, $arguments, $method)); + return $tl['id'].(yield from $this->serializeParams($tl, $arguments, $method, -1, $arguments['queuePromise'] ?? null)); } /** * Serialize parameters. @@ -673,7 +606,7 @@ class TL * * @psalm-return \Generator|Promise<\Amp\Ipc\Sync\ChannelledSocket>|Promise|Promise|Promise|\danog\MadelineProto\Stream\StreamInterface|array|int|mixed, mixed, string> */ - private function serializeParams(array $tl, $arguments, $ctx, int $layer = -1): \Generator + private function serializeParams(array $tl, $arguments, $ctx, int $layer, $promise): \Generator { $serialized = ''; $arguments = (yield from $this->API->botAPIToMTProto($arguments)); @@ -706,11 +639,11 @@ class TL continue; } if ($current_argument['name'] === 'random_bytes') { - $serialized .= (yield from $this->serializeObject(['type' => 'bytes'], \danog\MadelineProto\Tools::random(15 + 4 * \danog\MadelineProto\Tools::randomInt($modulus = 3)), 'random_bytes')); + $serialized .= yield from $this->serializeObject(['type' => 'bytes'], \danog\MadelineProto\Tools::random(15 + 4 * \danog\MadelineProto\Tools::randomInt($modulus = 3)), 'random_bytes'); continue; } if ($current_argument['name'] === 'data' && isset($tl['method']) && \in_array($tl['method'], ['messages.sendEncrypted', 'messages.sendEncryptedFile', 'messages.sendEncryptedService']) && isset($arguments['message'])) { - $serialized .= (yield from $this->serializeObject($current_argument, yield from $this->API->encryptSecretMessage($arguments['peer']['chat_id'], $arguments['message']), 'data')); + $serialized .= yield from $this->serializeObject($current_argument, yield from $this->API->encryptSecretMessage($arguments['peer']['chat_id'], $arguments['message'], $promise), 'data'); continue; } if ($current_argument['name'] === 'random_id') {