Secret chat improvements

This commit is contained in:
Daniil Gentili 2020-10-19 18:48:59 +02:00
parent bfc8009778
commit fb1df72a21
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
4 changed files with 118 additions and 105 deletions

View File

@ -134,7 +134,7 @@ if (\file_exists('.env')) {
echo 'Loading settings...'.PHP_EOL; echo 'Loading settings...'.PHP_EOL;
$settings = \json_decode(\getenv('MTPROTO_SETTINGS'), true) ?: []; $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 // Reduce boilerplate with new wrapper method
$MadelineProto->startAndLoop(SecretHandler::class); $MadelineProto->startAndLoop(SecretHandler::class);

View File

@ -360,35 +360,94 @@ class Connection
$this->pinger->start(); $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. * 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 OutgoingMessage $message The message to send
* @param boolean $flush Whether to flush the message right away * @param boolean $flush Whether to flush the message right away
* *
@ -404,7 +463,9 @@ class Connection
$this->API->referenceDatabase->refreshNext(true); $this->API->referenceDatabase->refreshNext(true);
} }
if ($message->isMethod()) { 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 { } else {
$body['_'] = $message->getConstructor(); $body['_'] = $message->getConstructor();
$body = yield from $this->API->getTL()->serializeObject(['type' => ''], $body, $message->getConstructor()); $body = yield from $this->API->getTL()->serializeObject(['type' => ''], $body, $message->getConstructor());
@ -416,6 +477,9 @@ class Connection
unset($body); unset($body);
} }
$this->pendingOutgoing[$this->pendingOutgoingKey++] = $message; $this->pendingOutgoing[$this->pendingOutgoingKey++] = $message;
if (isset($queuePromise)) {
$queuePromise->resolve();
}
if ($flush && isset($this->writer)) { if ($flush && isset($this->writer)) {
$this->writer->resume(); $this->writer->resume();
} }

View File

@ -19,6 +19,8 @@
namespace danog\MadelineProto\SecretChats; namespace danog\MadelineProto\SecretChats;
use Amp\Deferred;
use Amp\Promise;
use danog\MadelineProto\MTProtoTools\Crypt; use danog\MadelineProto\MTProtoTools\Crypt;
/** /**
@ -26,17 +28,24 @@ use danog\MadelineProto\MTProtoTools\Crypt;
*/ */
trait MessageHandler trait MessageHandler
{ {
/**
* Secret queue.
*
* @var Promise[]
*/
private $secretQueue = [];
/** /**
* Encrypt secret chat message. * Encrypt secret chat message.
* *
* @param integer $chat_id Chat ID * @param integer $chat_id Chat ID
* @param array $message Message to encrypt * @param array $message Message to encrypt
* @param Deferred $queuePromise Queue promise
* *
* @internal * @internal
* *
* @return \Generator * @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])) { if (!isset($this->secret_chats[$chat_id])) {
$this->logger->logger(\sprintf(\danog\MadelineProto\Lang::$current_lang['secret_chat_skipping'], $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) { 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); 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]; $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']++; $this->secret_chats[$chat_id]['out_seq_no']++;
} }

View File

@ -520,7 +520,7 @@ class TL
$concat = $this->constructors->findByPredicate('vector')['id']; $concat = $this->constructors->findByPredicate('vector')['id'];
$concat .= \danog\MadelineProto\Tools::packUnsignedInt(\count($object)); $concat .= \danog\MadelineProto\Tools::packUnsignedInt(\count($object));
foreach ($object as $k => $current_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; return $concat;
case 'vector': case 'vector':
@ -529,7 +529,7 @@ class TL
} }
$concat = \danog\MadelineProto\Tools::packUnsignedInt(\count($object)); $concat = \danog\MadelineProto\Tools::packUnsignedInt(\count($object));
foreach ($object as $k => $current_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; return $concat;
case 'Object': case 'Object':
@ -574,7 +574,7 @@ class TL
$constructorData = $this->constructors->findByPredicate('inputMessageEntityMentionName'); $constructorData = $this->constructors->findByPredicate('inputMessageEntityMentionName');
} }
$concat = $bare ? '' : $constructorData['id']; $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. * Serialize method.
@ -588,78 +588,11 @@ class TL
*/ */
public function serializeMethod(string $method, $arguments): \Generator 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); $tl = $this->methods->findByMethod($method);
if ($tl === false) { if ($tl === false) {
throw new Exception(\danog\MadelineProto\Lang::$current_lang['method_not_found'].$method); 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. * Serialize parameters.
@ -673,7 +606,7 @@ class TL
* *
* @psalm-return \Generator<int|mixed, Promise|Promise<\Amp\File\File>|Promise<\Amp\Ipc\Sync\ChannelledSocket>|Promise<int>|Promise<mixed>|Promise<null|string>|\danog\MadelineProto\Stream\StreamInterface|array|int|mixed, mixed, string> * @psalm-return \Generator<int|mixed, Promise|Promise<\Amp\File\File>|Promise<\Amp\Ipc\Sync\ChannelledSocket>|Promise<int>|Promise<mixed>|Promise<null|string>|\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 = ''; $serialized = '';
$arguments = (yield from $this->API->botAPIToMTProto($arguments)); $arguments = (yield from $this->API->botAPIToMTProto($arguments));
@ -706,11 +639,11 @@ class TL
continue; continue;
} }
if ($current_argument['name'] === 'random_bytes') { 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; continue;
} }
if ($current_argument['name'] === 'data' && isset($tl['method']) && \in_array($tl['method'], ['messages.sendEncrypted', 'messages.sendEncryptedFile', 'messages.sendEncryptedService']) && isset($arguments['message'])) { 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; continue;
} }
if ($current_argument['name'] === 'random_id') { if ($current_argument['name'] === 'random_id') {