diff --git a/src/danog/MadelineProto/Connection.php b/src/danog/MadelineProto/Connection.php index c08cbe15..1dd322db 100644 --- a/src/danog/MadelineProto/Connection.php +++ b/src/danog/MadelineProto/Connection.php @@ -21,11 +21,13 @@ namespace danog\MadelineProto; use Amp\ByteStream\ClosedException; use Amp\Deferred; +use Amp\Failure; use danog\MadelineProto\Loop\Connection\CheckLoop; use danog\MadelineProto\Loop\Connection\HttpWaitLoop; use danog\MadelineProto\Loop\Connection\PingLoop; use danog\MadelineProto\Loop\Connection\ReadLoop; use danog\MadelineProto\Loop\Connection\WriteLoop; +use danog\MadelineProto\MTProto\OutgoingMessage; use danog\MadelineProto\MTProtoSession\Session; use danog\MadelineProto\Stream\ConnectionContext; use danog\MadelineProto\Stream\MTProtoTransport\HttpsStream; @@ -39,6 +41,8 @@ use danog\MadelineProto\Stream\Transport\WsStream; * * Manages connection to Telegram datacenters * + * @internal + * * @author Daniil Gentili */ class Connection @@ -336,11 +340,12 @@ class Connection if (!isset($this->pinger) && ($this->ctx->hasStreamName(WssStream::class) || $this->ctx->hasStreamName(WsStream::class))) { $this->pinger = new PingLoop($this); } - foreach ($this->new_outgoing as $message_id) { - if ($this->outgoing_messages[$message_id]['unencrypted']) { - $promise = $this->outgoing_messages[$message_id]['promise']; - \Amp\Loop::defer(function () use ($promise) { - $promise->fail(new Exception('Restart because we were reconnected')); + foreach ($this->new_outgoing as $message_id => $message) { + if ($message->isUnencrypted()) { + \Amp\Loop::defer(function () use ($message) { + if (!($message->getState() & OutgoingMessage::STATE_REPLIED)) { + $message->reply(new Failure(new Exception('Restart because we were reconnected'))); + } }); unset($this->new_outgoing[$message_id], $this->outgoing_messages[$message_id]); } @@ -384,40 +389,37 @@ class Connection * 'tries' => number * ] * - * @param array $message The message to send - * @param boolean $flush Whether to flush the message right away + * @param OutgoingMessage $message The message to send + * @param boolean $flush Whether to flush the message right away * * @return \Generator */ - public function sendMessage(array $message, bool $flush = true): \Generator + public function sendMessage(OutgoingMessage $message, bool $flush = true): \Generator { - $deferred = new Deferred(); - if (!isset($message['serialized_body'])) { - $body = $message['body'] instanceof \Generator - ? yield from $message['body'] - : $message['body']; - $refreshNext = $message['refreshReferences'] ?? false; - if ($refreshNext) { + $message->trySend(); + $promise = $message->getSendPromise(); + if (!$message->hasSerializedBody() || $message->shouldRefreshReferences()) { + $body = yield from $message->getBody(); + if ($message->shouldRefreshReferences()) { $this->API->referenceDatabase->refreshNext(true); } - if ($message['method']) { - $body = (yield from $this->API->getTL()->serializeMethod($message['_'], $body)); + if ($message->isMethod()) { + $body = yield from $this->API->getTL()->serializeMethod($message->getConstructor(), $body); } else { - $body['_'] = $message['_']; - $body = (yield from $this->API->getTL()->serializeObject(['type' => ''], $body, $message['_'])); + $body['_'] = $message->getConstructor(); + $body = yield from $this->API->getTL()->serializeObject(['type' => ''], $body, $message->getConstructor()); } - if ($refreshNext) { + if ($message->shouldRefreshReferences()) { $this->API->referenceDatabase->refreshNext(false); } - $message['serialized_body'] = $body; + $message->setSerializedBody($body); unset($body); } - $message['send_promise'] = $deferred; - $this->pending_outgoing[$this->pending_outgoing_key++] = $message; + $this->pendingOutgoing[$this->pendingOutgoingKey++] = $message; if ($flush && isset($this->writer)) { $this->writer->resume(); } - return yield $deferred->promise(); + return yield $promise; } /** * Flush pending packets. diff --git a/src/danog/MadelineProto/DataCenter.php b/src/danog/MadelineProto/DataCenter.php index 7821d55f..3d5bfaf7 100644 --- a/src/danog/MadelineProto/DataCenter.php +++ b/src/danog/MadelineProto/DataCenter.php @@ -401,7 +401,7 @@ class DataCenter } } } - $combos = array_merge($proxyCombos, $combos); + $combos = \array_merge($proxyCombos, $combos); if ($dc_number) { $combos[] = [[DefaultStream::class, []], [BufferedRawStream::class, []], [HttpsStream::class, []]]; } diff --git a/src/danog/MadelineProto/DataCenterConnection.php b/src/danog/MadelineProto/DataCenterConnection.php index 157618ff..a3b6b8e3 100644 --- a/src/danog/MadelineProto/DataCenterConnection.php +++ b/src/danog/MadelineProto/DataCenterConnection.php @@ -24,6 +24,7 @@ use Amp\Promise; use Amp\Success; use danog\MadelineProto\Loop\Generic\PeriodicLoopInternal; use danog\MadelineProto\MTProto\AuthKey; +use danog\MadelineProto\MTProto\OutgoingMessage; use danog\MadelineProto\MTProto\PermAuthKey; use danog\MadelineProto\MTProto\TempAuthKey; use danog\MadelineProto\Settings\Connection as ConnectionSettings; @@ -327,7 +328,7 @@ class DataCenterConnection implements JsonSerializable * * @return void */ - public function flush() + public function flush(): void { foreach ($this->connections as $socket) { $socket->flush(); @@ -424,11 +425,11 @@ class DataCenterConnection implements JsonSerializable $backup = $this->connections[$id]->backupSession(); $list = ''; foreach ($backup as $k => $message) { - if (($message['_'] ?? '') === 'msgs_state_req') { + if ($message->getConstructor() === 'msgs_state_req') { unset($backup[$k]); continue; } - $list .= $message['_'] ?? '-'; + $list .= $message->getConstructor(); $list .= ', '; } $this->API->logger->logger("Backed up {$list} from DC {$this->datacenter}.{$id}"); @@ -480,14 +481,15 @@ class DataCenterConnection implements JsonSerializable $this->backup = []; $count = \count($backup); $this->API->logger->logger("Restoring {$count} messages to DC {$this->datacenter}"); + /** @var OutgoingMessage */ foreach ($backup as $message) { - if (isset($message['seqno'])) { - unset($message['seqno']); + if ($message->hasSeqno()) { + $message->setSeqno(null); } - if (isset($message['msg_id'])) { - unset($message['msg_id']); + if ($message->hasMsgId()) { + $message->setMsgId(null); } - if (isset($message['body'])) { + if (!($message->getState() & OutgoingMessage::STATE_REPLIED)) { Tools::callFork($this->getConnection()->sendMessage($message, false)); } } diff --git a/src/danog/MadelineProto/Db/Driver/Mysql.php b/src/danog/MadelineProto/Db/Driver/Mysql.php index 2c888826..2d407116 100644 --- a/src/danog/MadelineProto/Db/Driver/Mysql.php +++ b/src/danog/MadelineProto/Db/Driver/Mysql.php @@ -11,7 +11,7 @@ use function Amp\Mysql\Pool; /** * MySQL driver wrapper. - * + * * @internal */ class Mysql diff --git a/src/danog/MadelineProto/Db/Driver/Postgres.php b/src/danog/MadelineProto/Db/Driver/Postgres.php index af86f6e1..0af529a0 100644 --- a/src/danog/MadelineProto/Db/Driver/Postgres.php +++ b/src/danog/MadelineProto/Db/Driver/Postgres.php @@ -11,7 +11,7 @@ use function Amp\Postgres\Pool; /** * Postgres driver wrapper. - * + * * @internal */ class Postgres diff --git a/src/danog/MadelineProto/Db/Driver/Redis.php b/src/danog/MadelineProto/Db/Driver/Redis.php index a73b27b5..d231ddbe 100644 --- a/src/danog/MadelineProto/Db/Driver/Redis.php +++ b/src/danog/MadelineProto/Db/Driver/Redis.php @@ -9,7 +9,7 @@ use danog\MadelineProto\Settings\Database\Redis as DatabaseRedis; /** * Redis driver wrapper. - * + * * @internal */ class Redis diff --git a/src/danog/MadelineProto/Db/NullCache/MysqlArray.php b/src/danog/MadelineProto/Db/NullCache/MysqlArray.php index b932de6f..48b639b7 100644 --- a/src/danog/MadelineProto/Db/NullCache/MysqlArray.php +++ b/src/danog/MadelineProto/Db/NullCache/MysqlArray.php @@ -6,7 +6,7 @@ use danog\MadelineProto\Db\MysqlArray as DbMysqlArray; /** * MySQL database backend, no caching. - * + * * @internal */ class MysqlArray extends DbMysqlArray diff --git a/src/danog/MadelineProto/Db/NullCache/PostgresArray.php b/src/danog/MadelineProto/Db/NullCache/PostgresArray.php index a16c5626..e293be6b 100644 --- a/src/danog/MadelineProto/Db/NullCache/PostgresArray.php +++ b/src/danog/MadelineProto/Db/NullCache/PostgresArray.php @@ -6,7 +6,7 @@ use danog\MadelineProto\Db\PostgresArray as DbPostgresArray; /** * Postgres database backend, no caching. - * + * * @internal */ class PostgresArray extends DbPostgresArray diff --git a/src/danog/MadelineProto/Db/NullCache/RedisArray.php b/src/danog/MadelineProto/Db/NullCache/RedisArray.php index 318a1ec0..9ab33d32 100644 --- a/src/danog/MadelineProto/Db/NullCache/RedisArray.php +++ b/src/danog/MadelineProto/Db/NullCache/RedisArray.php @@ -6,7 +6,7 @@ use danog\MadelineProto\Db\RedisArray as DbRedisArray; /** * Redis database backend, no caching. - * + * * @internal */ class RedisArray extends DbRedisArray diff --git a/src/danog/MadelineProto/Db/RedisArray.php b/src/danog/MadelineProto/Db/RedisArray.php index 0470cb16..52ca2ece 100644 --- a/src/danog/MadelineProto/Db/RedisArray.php +++ b/src/danog/MadelineProto/Db/RedisArray.php @@ -208,10 +208,8 @@ class RedisArray extends SqlArray return call(function () { $iterator = $this->getIterator(); $result = []; - $len = \strlen($this->rKey('')); while (yield $iterator->advance()) { [$key, $value] = $iterator->getCurrent(); - $key = \substr($key, $len); $result[$key] = $value; } return $result; @@ -223,8 +221,10 @@ class RedisArray extends SqlArray return new Producer(function (callable $emit) { $request = $this->db->scan($this->itKey()); + $len = \strlen($this->rKey('')); while (yield $request->advance()) { - yield $emit([$key = $request->getCurrent(), \unserialize(yield $this->db->get($key))]); + $key = $request->getCurrent(); + yield $emit([\substr($key, $len), \unserialize(yield $this->db->get($key))]); } }); } diff --git a/src/danog/MadelineProto/Loop/Connection/CheckLoop.php b/src/danog/MadelineProto/Loop/Connection/CheckLoop.php index 5a86b5f6..9affd5d9 100644 --- a/src/danog/MadelineProto/Loop/Connection/CheckLoop.php +++ b/src/danog/MadelineProto/Loop/Connection/CheckLoop.php @@ -79,40 +79,41 @@ class CheckLoop extends ResumableSignalLoop continue; } if (!isset($connection->new_outgoing[$message_id])) { - $API->logger->logger('Already got response for '.$connection->outgoing_messages[$message_id]['_'].' with message ID '.$message_id); + $API->logger->logger('Already got response for '.$connection->outgoing_messages[$message_id]); continue; } + $message = $connection->new_outgoing[$message_id]; $chr = \ord($chr); switch ($chr & 7) { case 0: - $API->logger->logger('Wrong message status 0 for '.$connection->outgoing_messages[$message_id]['_'], \danog\MadelineProto\Logger::FATAL_ERROR); + $API->logger->logger("Wrong message status 0 for $message", \danog\MadelineProto\Logger::FATAL_ERROR); break; case 1: case 2: case 3: - if ($connection->outgoing_messages[$message_id]['_'] === 'msgs_state_req') { - $connection->gotResponseForOutgoingMessageId($message_id); + if ($message->getConstructor() === 'msgs_state_req') { + $connection->gotResponseForOutgoingMessage($message); break; } - $API->logger->logger('Message '.$connection->outgoing_messages[$message_id]['_'].' with message ID '.$message_id.' not received by server, resending...', \danog\MadelineProto\Logger::ERROR); + $API->logger->logger("Message $message not received by server, resending...", \danog\MadelineProto\Logger::ERROR); $connection->methodRecall('watcherId', ['message_id' => $message_id, 'postpone' => true]); break; case 4: if ($chr & 32) { - if ($connection->outgoing_messages[$message_id]['sent'] + $timeoutResend < \time()) { - $API->logger->logger('Message '.$connection->outgoing_messages[$message_id]['_'].' with message ID '.$message_id.' received by server and is being processed for way too long, resending request...', \danog\MadelineProto\Logger::ERROR); + if ($message->getSent() + $timeoutResend < \time()) { + $API->logger->logger("Message $message received by server and is being processed for way too long, resending request...", \danog\MadelineProto\Logger::ERROR); $connection->methodRecall('', ['message_id' => $message_id, 'postpone' => true]); } else { - $API->logger->logger('Message '.$connection->outgoing_messages[$message_id]['_'].' with message ID '.$message_id.' received by server and is being processed, waiting...', \danog\MadelineProto\Logger::ERROR); + $API->logger->logger("Message $message received by server and is being processed, waiting...", \danog\MadelineProto\Logger::ERROR); } } elseif ($chr & 64) { - $API->logger->logger('Message '.$connection->outgoing_messages[$message_id]['_'].' with message ID '.$message_id.' received by server and was already processed, requesting reply...', \danog\MadelineProto\Logger::ERROR); + $API->logger->logger("Message $message received by server and was already processed, requesting reply...", \danog\MadelineProto\Logger::ERROR); $reply[] = $message_id; } elseif ($chr & 128) { - $API->logger->logger('Message '.$connection->outgoing_messages[$message_id]['_'].' with message ID '.$message_id.' received by server and was already sent, requesting reply...', \danog\MadelineProto\Logger::ERROR); + $API->logger->logger("Message $message received by server and was already sent, requesting reply...", \danog\MadelineProto\Logger::ERROR); $reply[] = $message_id; } else { - $API->logger->logger('Message '.$connection->outgoing_messages[$message_id]['_'].' with message ID '.$message_id.' received by server, waiting...', \danog\MadelineProto\Logger::ERROR); + $API->logger->logger("Message $message received by server, waiting...", \danog\MadelineProto\Logger::ERROR); $reply[] = $message_id; } } @@ -128,16 +129,19 @@ class CheckLoop extends ResumableSignalLoop $list = ''; // Don't edit this here pls foreach ($message_ids as $message_id) { - $list .= $connection->outgoing_messages[$message_id]['_'].', '; + $list .= $connection->outgoing_messages[$message_id]->getConstructor().', '; } $API->logger->logger("Still missing {$list} on DC {$datacenter}, sending state request", \danog\MadelineProto\Logger::ERROR); yield from $connection->objectCall('msgs_state_req', ['msg_ids' => $message_ids], ['promise' => $deferred]); } } else { - foreach ($connection->new_outgoing as $message_id) { - if (isset($connection->outgoing_messages[$message_id]['sent']) && $connection->outgoing_messages[$message_id]['sent'] + $timeout < \time() && $connection->outgoing_messages[$message_id]['unencrypted']) { - $API->logger->logger('Still missing '.$connection->outgoing_messages[$message_id]['_'].' with message id '.$message_id." on DC {$datacenter}, resending", \danog\MadelineProto\Logger::ERROR); - $connection->methodRecall('', ['message_id' => $message_id, 'postpone' => true]); + foreach ($connection->new_outgoing as $message_id => $message) { + if ($message->wasSent() + && $message->getSent() + $timeout < \time() + && $message->isUnencrypted() + ) { + $API->logger->logger("Still missing $message on DC $datacenter, resending", \danog\MadelineProto\Logger::ERROR); + $connection->methodRecall('', ['message_id' => $message->getMsgId(), 'postpone' => true]); } } $connection->flush(); diff --git a/src/danog/MadelineProto/Loop/Connection/HttpWaitLoop.php b/src/danog/MadelineProto/Loop/Connection/HttpWaitLoop.php index 56413014..b5c4b4e8 100644 --- a/src/danog/MadelineProto/Loop/Connection/HttpWaitLoop.php +++ b/src/danog/MadelineProto/Loop/Connection/HttpWaitLoop.php @@ -20,6 +20,7 @@ namespace danog\MadelineProto\Loop\Connection; use danog\Loop\ResumableSignalLoop; +use danog\MadelineProto\MTProto\OutgoingMessage; /** * HttpWait loop. @@ -56,8 +57,16 @@ class HttpWaitLoop extends ResumableSignalLoop } } $API->logger->logger("DC {$datacenter}: request {$connection->countHttpSent()}, response {$connection->countHttpReceived()}"); - if ($connection->countHttpSent() === $connection->countHttpReceived() && (!empty($connection->pending_outgoing) || !empty($connection->new_outgoing) && !$connection->hasPendingCalls())) { - yield from $connection->sendMessage(['_' => 'http_wait', 'body' => ['max_wait' => 30000, 'wait_after' => 0, 'max_delay' => 0], 'contentRelated' => true, 'unencrypted' => false, 'method' => false]); + if ($connection->countHttpSent() === $connection->countHttpReceived() && (!empty($connection->pendingOutgoing) || !empty($connection->new_outgoing) && !$connection->hasPendingCalls())) { + yield from $connection->sendMessage( + new OutgoingMessage( + ['max_wait' => 30000, 'wait_after' => 0, 'max_delay' => 0], + 'http_wait', + '', + false, + false + ) + ); } $API->logger->logger("DC {$datacenter}: request {$connection->countHttpSent()}, response {$connection->countHttpReceived()}"); } diff --git a/src/danog/MadelineProto/Loop/Connection/ReadLoop.php b/src/danog/MadelineProto/Loop/Connection/ReadLoop.php index dc1ccea7..3dcad77a 100644 --- a/src/danog/MadelineProto/Loop/Connection/ReadLoop.php +++ b/src/danog/MadelineProto/Loop/Connection/ReadLoop.php @@ -25,6 +25,7 @@ use Amp\Loop; use Amp\Websocket\ClosedException; use danog\Loop\SignalLoop; use danog\MadelineProto\Logger; +use danog\MadelineProto\MTProto\IncomingMessage; use danog\MadelineProto\MTProtoTools\Crypt; use danog\MadelineProto\NothingInTheSocketException; use danog\MadelineProto\Tools; @@ -70,8 +71,8 @@ class ReadLoop extends SignalLoop $API->logger->logger("WARNING: Resetting auth key in DC {$datacenter}...", Logger::WARNING); $shared->setTempAuthKey(null); $shared->resetSession(); - foreach ($connection->new_outgoing as $message_id) { - $connection->outgoing_messages[$message_id]['sent'] = 0; + foreach ($connection->new_outgoing as $message) { + $message->resetSent(); } yield from $shared->reconnect(); yield from $API->initAuthorization(); @@ -146,7 +147,6 @@ class ReadLoop extends SignalLoop } yield $buffer->bufferRead($left); } - $connection->incoming_messages[$message_id] = []; } elseif ($auth_key_id === $shared->getTempAuthKey()->getID()) { $message_key = yield $buffer->bufferRead(16); list($aes_key, $aes_iv) = Crypt::aesCalculate($message_key, $shared->getTempAuthKey()->getAuthKey(), false); @@ -191,18 +191,22 @@ class ReadLoop extends SignalLoop if ($message_key != \substr(\hash('sha256', \substr($shared->getTempAuthKey()->getAuthKey(), 96, 32).$decrypted_data, true), 8, 16)) { throw new \danog\MadelineProto\SecurityException('msg_key mismatch'); } - $connection->incoming_messages[$message_id] = ['seq_no' => $seq_no]; } else { $API->logger->logger('Got unknown auth_key id', Logger::ERROR); return -404; } - $deserialized = yield from $API->getTL()->deserialize($message_data, ['type' => '', 'connection' => $connection]); + [$deserialized, $sideEffects] = $API->getTL()->deserialize($message_data, ['type' => '', 'connection' => $connection]); if (isset($API->referenceDatabase)) { $API->referenceDatabase->reset(); } - $connection->incoming_messages[$message_id]['content'] = $deserialized; - $connection->incoming_messages[$message_id]['response'] = -1; - $connection->new_incoming[$message_id] = $message_id; + $message = new IncomingMessage($deserialized, $message_id); + if (isset($seq_no)) { + $message->setSeqNo($seq_no); + } + if ($sideEffects) { + $message->setSideEffects($sideEffects); + } + $connection->new_incoming[$message_id] = $connection->incoming_messages[$message_id] = $message; $API->logger->logger('Received payload from DC '.$datacenter, Logger::ULTRA_VERBOSE); } finally { $connection->reading(false); diff --git a/src/danog/MadelineProto/Loop/Connection/WriteLoop.php b/src/danog/MadelineProto/Loop/Connection/WriteLoop.php index 99151d03..a1113663 100644 --- a/src/danog/MadelineProto/Loop/Connection/WriteLoop.php +++ b/src/danog/MadelineProto/Loop/Connection/WriteLoop.php @@ -23,6 +23,7 @@ use Amp\ByteStream\StreamException; use Amp\Loop; use danog\Loop\ResumableSignalLoop; use danog\MadelineProto\Logger; +use danog\MadelineProto\MTProto\Container; use danog\MadelineProto\MTProtoTools\Crypt; use danog\MadelineProto\Tools; @@ -51,7 +52,7 @@ class WriteLoop extends ResumableSignalLoop $datacenter = $this->datacenter; $please_wait = false; while (true) { - while (empty($connection->pending_outgoing) || $please_wait) { + while (empty($connection->pendingOutgoing) || $please_wait) { if ($connection->shouldReconnect()) { $API->logger->logger('Not writing because connection is old'); return; @@ -93,35 +94,34 @@ class WriteLoop extends ResumableSignalLoop $datacenter = $this->datacenter; $connection = $this->connection; $shared = $this->datacenterConnection; - while ($connection->pending_outgoing) { + while ($connection->pendingOutgoing) { $skipped_all = true; - foreach ($connection->pending_outgoing as $k => $message) { + foreach ($connection->pendingOutgoing as $k => $message) { if ($shared->hasTempAuthKey()) { return; } - if (!$message['unencrypted']) { + if ($message->isEncrypted()) { continue; } $skipped_all = false; - $API->logger->logger("Sending {$message['_']} as unencrypted message to DC {$datacenter}", \danog\MadelineProto\Logger::ULTRA_VERBOSE); - $message_id = isset($message['msg_id']) ? $message['msg_id'] : $connection->msgIdHandler->generateMessageId(); - $length = \strlen($message['serialized_body']); + $API->logger->logger("Sending $message as unencrypted message to DC $datacenter", \danog\MadelineProto\Logger::ULTRA_VERBOSE); + $message_id = $message->getMsgId() ?? $connection->msgIdHandler->generateMessageId(); + $length = \strlen($message->getSerializedBody()); $pad_length = -$length & 15; $pad_length += 16 * \danog\MadelineProto\Tools::randomInt($modulus = 16); $pad = \danog\MadelineProto\Tools::random($pad_length); $buffer = yield $connection->stream->getWriteBuffer(8 + 8 + 4 + $pad_length + $length); - yield $buffer->bufferWrite("\0\0\0\0\0\0\0\0".$message_id.\danog\MadelineProto\Tools::packUnsignedInt($length).$message['serialized_body'].$pad); + yield $buffer->bufferWrite("\0\0\0\0\0\0\0\0".$message_id.\danog\MadelineProto\Tools::packUnsignedInt($length).$message->getSerializedBody().$pad); //var_dump("plain ".bin2hex($message_id)); $connection->httpSent(); + + $API->logger->logger("Sent $message as unencrypted message to DC $datacenter!", \danog\MadelineProto\Logger::ULTRA_VERBOSE); + + unset($connection->pendingOutgoing[$k]); $connection->outgoing_messages[$message_id] = $message; - $connection->outgoing_messages[$message_id]['sent'] = \time(); - $connection->outgoing_messages[$message_id]['tries'] = 0; - $connection->outgoing_messages[$message_id]['unencrypted'] = true; - $connection->new_outgoing[$message_id] = $message_id; - unset($connection->pending_outgoing[$k]); - $API->logger->logger("Sent {$message['_']} as unencrypted message to DC {$datacenter}!", \danog\MadelineProto\Logger::ULTRA_VERBOSE); - $message['send_promise']->resolve(isset($message['promise']) ? $message['promise'] : true); - unset($message['send_promise']); + $connection->new_outgoing[$message_id] = $message; + + $message->sent(); } if ($skipped_all) { return true; @@ -138,11 +138,11 @@ class WriteLoop extends ResumableSignalLoop if (!$shared->hasTempAuthKey()) { return; } - if ($shared->isHttp() && empty($connection->pending_outgoing)) { + if ($shared->isHttp() && empty($connection->pendingOutgoing)) { return; } - \ksort($connection->pending_outgoing); + \ksort($connection->pendingOutgoing); $messages = []; $keys = []; @@ -157,71 +157,72 @@ class WriteLoop extends ResumableSignalLoop $has_state = false; $has_resend = false; $has_http_wait = false; - foreach ($connection->pending_outgoing as $k => $message) { - if ($message['unencrypted']) { + foreach ($connection->pendingOutgoing as $k => $message) { + if ($message->isUnencrypted()) { continue; } - if (isset($message['container'])) { - unset($connection->pending_outgoing[$k]); + if ($message instanceof Container) { + unset($connection->pendingOutgoing[$k]); continue; } - if ($shared->getGenericSettings()->getAuth()->getPfs() && !$shared->isBound() && !$connection->isCDN() && !\in_array($message['_'], ['http_wait', 'auth.bindTempAuthKey']) && $message['method']) { - $API->logger->logger("Skipping {$message['_']} due to unbound keys in DC {$datacenter}"); + $constructor = $message->getConstructor(); + if ($shared->getGenericSettings()->getAuth()->getPfs() && !$shared->isBound() && !$connection->isCDN() && $message->isMethod() && !\in_array($constructor, ['http_wait', 'auth.bindTempAuthKey'])) { + $API->logger->logger("Skipping $message due to unbound keys in DC $datacenter"); $skipped = true; continue; } - if ($message['_'] === 'http_wait') { + if ($constructor === 'http_wait') { $has_http_wait = true; } - if ($message['_'] === 'msgs_state_req') { + if ($constructor === 'msgs_state_req') { if ($has_state) { $API->logger->logger("Already have a state request queued for the current container in DC {$datacenter}"); continue; } $has_state = true; } - if ($message['_'] === 'msg_resend_req') { + if ($constructor === 'msg_resend_req') { if ($has_resend) { - $API->logger->logger("Already have a resend request queued for the current container in DC {$datacenter}"); continue; } $has_resend = true; } - $body_length = \strlen($message['serialized_body']); + $body_length = \strlen($message->getSerializedBody()); $actual_length = $body_length + 32; if ($total_length && $total_length + $actual_length > 32760 || $count >= 1020) { $API->logger->logger('Length overflow, postponing part of payload', \danog\MadelineProto\Logger::ULTRA_VERBOSE); break; } - if (isset($message['seqno'])) { + if ($message->hasSeqNo()) { $has_seq = true; } - $message_id = $message['msg_id'] ?? $connection->msgIdHandler->generateMessageId(); - $API->logger->logger("Sending {$message['_']} as encrypted message to DC {$datacenter}", \danog\MadelineProto\Logger::ULTRA_VERBOSE); + $message_id = $message->getMsgId() ?? $connection->msgIdHandler->generateMessageId(); + $API->logger->logger("Sending $message as encrypted message to DC $datacenter", \danog\MadelineProto\Logger::ULTRA_VERBOSE); $MTmessage = [ '_' => 'MTmessage', 'msg_id' => $message_id, - 'body' => $message['serialized_body'], - 'seqno' => $message['seqno'] ?? $connection->generateOutSeqNo($message['contentRelated']) + 'body' => $message->getSerializedBody(), + 'seqno' => $message->getSeqNo() ?? $connection->generateOutSeqNo($message->isContentRelated()) ]; - if (isset($message['method']) && $message['method'] && $message['_'] !== 'http_wait') { - if (!$shared->getTempAuthKey()->isInited() && $message['_'] !== 'auth.bindTempAuthKey' && !$inited) { + if ($message->isMethod() && $constructor !== 'http_wait') { + if (!$shared->getTempAuthKey()->isInited() && $constructor !== 'auth.bindTempAuthKey' && !$inited) { $inited = true; - $API->logger->logger(\sprintf(\danog\MadelineProto\Lang::$current_lang['write_client_info'], $message['_']), \danog\MadelineProto\Logger::NOTICE); + $API->logger->logger(\sprintf(\danog\MadelineProto\Lang::$current_lang['write_client_info'], $constructor), \danog\MadelineProto\Logger::NOTICE); $MTmessage['body'] = (yield from $API->getTL()->serializeMethod('invokeWithLayer', ['layer' => $API->settings->getSchema()->getLayer(), 'query' => yield from $API->getTL()->serializeMethod('initConnection', ['api_id' => $API->settings->getAppInfo()->getApiId(), 'api_hash' => $API->settings->getAppInfo()->getApiHash(), 'device_model' => !$connection->isCDN() ? $API->settings->getAppInfo()->getDeviceModel() : 'n/a', 'system_version' => !$connection->isCDN() ? $API->settings->getAppInfo()->getSystemVersion() : 'n/a', 'app_version' => $API->settings->getAppInfo()->getAppVersion(), 'system_lang_code' => $API->settings->getAppInfo()->getLangCode(), 'lang_code' => $API->settings->getAppInfo()->getLangCode(), 'lang_pack' => $API->settings->getAppInfo()->getLangPack(), 'proxy' => $connection->getCtx()->getInputClientProxy(), 'query' => $MTmessage['body']])])); } else { - if (isset($message['queue'])) { - if (!isset($connection->call_queue[$message['queue']])) { - $connection->call_queue[$message['queue']] = []; + if ($message->hasQueue()) { + $queueId = $message->getQueueId(); + if (!isset($connection->call_queue[$queueId])) { + $connection->call_queue[$queueId] = []; } - $MTmessage['body'] = (yield from $API->getTL()->serializeMethod('invokeAfterMsgs', ['msg_ids' => $connection->call_queue[$message['queue']], 'query' => $MTmessage['body']])); - $connection->call_queue[$message['queue']][$message_id] = $message_id; - if (\count($connection->call_queue[$message['queue']]) > $API->settings->getRpc()->getLimitCallQueue()) { - \reset($connection->call_queue[$message['queue']]); - $key = \key($connection->call_queue[$message['queue']]); - unset($connection->call_queue[$message['queue']][$key]); + $MTmessage['body'] = (yield from $API->getTL()->serializeMethod('invokeAfterMsgs', ['msg_ids' => $connection->call_queue[$queueId], 'query' => $MTmessage['body']])); + $connection->call_queue[$queueId][$message_id] = $message_id; + if (\count($connection->call_queue[$queueId]) > $API->settings->getRpc()->getLimitCallQueue()) { + \reset($connection->call_queue[$queueId]); + $key = \key($connection->call_queue[$queueId]); + unset($connection->call_queue[$queueId][$key]); } } // TODO @@ -229,7 +230,7 @@ class WriteLoop extends ResumableSignalLoop if ($API->settings['requests']['gzip_encode_if_gt'] !== -1 && ($l = strlen($MTmessage['body'])) > $API->settings['requests']['gzip_encode_if_gt']) { if (($g = strlen($gzipped = gzencode($MTmessage['body']))) < $l) { $MTmessage['body'] = yield $API->getTL()->serializeObject(['type' => ''], ['_' => 'gzip_packed', 'packed_data' => $gzipped], 'gzipped data'); - $API->logger->logger('Using GZIP compression for ' . $message['_'] . ', saved ' . ($l - $g) . ' bytes of data, reduced call size by ' . $g * 100 / $l . '%', \danog\MadelineProto\Logger::ULTRA_VERBOSE); + $API->logger->logger('Using GZIP compression for ' . $constructor . ', saved ' . ($l - $g) . ' bytes of data, reduced call size by ' . $g * 100 / $l . '%', \danog\MadelineProto\Logger::ULTRA_VERBOSE); } unset($gzipped); }*/ @@ -247,8 +248,8 @@ class WriteLoop extends ResumableSignalLoop $messages[] = $MTmessage; $keys[$k] = $message_id; - $connection->pending_outgoing[$k]['seqno'] = $MTmessage['seqno']; - $connection->pending_outgoing[$k]['msg_id'] = $MTmessage['msg_id']; + $message->setSeqNo($MTmessage['seqno']) + ->setMsgId($MTmessage['msg_id']); } $MTmessage = null; @@ -284,9 +285,9 @@ class WriteLoop extends ResumableSignalLoop if ($count > 1 || $has_seq) { $API->logger->logger("Wrapping in msg_container ({$count} messages of total size {$total_length}) as encrypted message for DC {$datacenter}", \danog\MadelineProto\Logger::ULTRA_VERBOSE); $message_id = $connection->msgIdHandler->generateMessageId(); - $connection->pending_outgoing[$connection->pending_outgoing_key] = ['_' => 'msg_container', 'container' => \array_values($keys), 'contentRelated' => false, 'method' => false, 'unencrypted' => false]; - $keys[$connection->pending_outgoing_key++] = $message_id; - $message_data = (yield from $API->getTL()->serializeObject(['type' => ''], ['_' => 'msg_container', 'messages' => $messages], 'container')); + $connection->pendingOutgoing[$connection->pendingOutgoingKey] = new Container(\array_values($keys)); + $keys[$connection->pendingOutgoingKey++] = $message_id; + $message_data = yield from $API->getTL()->serializeObject(['type' => ''], ['_' => 'msg_container', 'messages' => $messages], 'container'); $message_data_length = \strlen($message_data); $seq_no = $connection->generateOutSeqNo(false); } elseif ($count) { @@ -320,20 +321,17 @@ class WriteLoop extends ResumableSignalLoop } foreach ($keys as $key => $message_id) { - $connection->outgoing_messages[$message_id] =& $connection->pending_outgoing[$key]; - if (isset($connection->outgoing_messages[$message_id]['promise'])) { - $connection->new_outgoing[$message_id] = $message_id; - $connection->outgoing_messages[$message_id]['sent'] = $sent; + $message = $connection->pendingOutgoing[$key]; + unset($connection->pendingOutgoing[$key]); + $connection->outgoing_messages[$message_id] = $message; + if ($message->hasPromise()) { + $connection->new_outgoing[$message_id] = $message; } - if (isset($connection->outgoing_messages[$message_id]['send_promise'])) { - $connection->outgoing_messages[$message_id]['send_promise']->resolve(isset($connection->outgoing_messages[$message_id]['promise']) ? $connection->outgoing_messages[$message_id]['promise'] : true); - unset($connection->outgoing_messages[$message_id]['send_promise']); - } - unset($connection->pending_outgoing[$key]); + $message->sent(); } - } while ($connection->pending_outgoing && !$skipped); - if (empty($connection->pending_outgoing)) { - $connection->pending_outgoing_key = 'a'; + } while ($connection->pendingOutgoing && !$skipped); + if (empty($connection->pendingOutgoing)) { + $connection->pendingOutgoingKey = 'a'; } return $skipped; } diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index 6182dddd..18f20882 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -52,7 +52,7 @@ use function Amp\File\size; /** * Manages all of the mtproto stuff. - * + * * @internal */ class MTProto extends AsyncConstruct implements TLCallback @@ -101,7 +101,7 @@ class MTProto extends AsyncConstruct implements TLCallback * * @var int */ - const V = 147; + const V = 148; /** * Release version. * @@ -301,7 +301,7 @@ class MTProto extends AsyncConstruct implements TLCallback * * @var DbArray|Promise[] */ - public $channel_participants; + public $channelParticipants; /** * When we last stored data in remote peer database (now doesn't exist anymore). * @@ -491,7 +491,7 @@ class MTProto extends AsyncConstruct implements TLCallback protected static array $dbProperties = [ 'chats' => 'array', 'full_chats' => 'array', - 'channel_participants' => 'array', + 'channelParticipants' => 'array', 'usernames' => 'array', 'session' => [ 'type' => 'array', @@ -574,7 +574,7 @@ class MTProto extends AsyncConstruct implements TLCallback $this->datacenter->curdc = 2; if ((!isset($this->authorization['user']['bot']) || !$this->authorization['user']['bot']) && $this->datacenter->getDataCenterConnection($this->datacenter->curdc)->hasTempAuthKey()) { try { - $nearest_dc = yield from $this->methodCallAsyncRead('help.getNearestDc', [], ['datacenter' => $this->datacenter->curdc]); + $nearest_dc = yield from $this->methodCallAsyncRead('help.getNearestDc', []); $this->logger->logger(\sprintf(Lang::$current_lang['nearest_dc'], $nearest_dc['country'], $nearest_dc['nearest_dc']), Logger::NOTICE); if ($nearest_dc['nearest_dc'] != $nearest_dc['this_dc']) { $this->settings->setDefaultDc($this->datacenter->curdc = (int) $nearest_dc['nearest_dc']); @@ -585,7 +585,7 @@ class MTProto extends AsyncConstruct implements TLCallback } } } - yield from $this->getConfig([], ['datacenter' => $this->datacenter->curdc]); + yield from $this->getConfig([]); $this->startUpdateSystem(true); $this->v = self::V; @@ -627,7 +627,7 @@ class MTProto extends AsyncConstruct implements TLCallback 'full_chats', 'referenceDatabase', 'minDatabase', - 'channel_participants', + 'channelParticipants', 'usernames', // Misc caching @@ -1029,6 +1029,39 @@ class MTProto extends AsyncConstruct implements TLCallback } } + if (isset($this->channel_participants)) { + if (\is_array($this->channel_participants)) { + foreach ($this->channel_participants as $channelId => $filters) { + foreach ($filters as $filter => $qs) { + foreach ($qs as $q => $offsets) { + foreach ($offsets as $offset => $limits) { + foreach ($limits as $limit => $data) { + $this->channelParticipants[$this->participantsKey($channelId, $filter, $q, $offset, $limit)] = $data; + } + } + } + } + unset($this->channel_participants[$channelId]); + } + } else { + self::$dbProperties['channel_participants'] = 'array'; + yield from $this->initDb($this); + $iterator = $this->channel_participants->getIterator(); + while (yield $iterator->advance()) { + [$channelId, $filters] = $iterator->getCurrent(); + foreach ($filters as $filter => $qs) { + foreach ($qs as $q => $offsets) { + foreach ($offsets as $offset => $limits) { + foreach ($limits as $limit => $data) { + $this->channelParticipants[$this->participantsKey($channelId, $filter, $q, $offset, $limit)] = $data; + } + } + } + } + unset($this->channel_participants[$channelId]); + } + } + } foreach ($this->secret_chats as $key => &$chat) { if (!\is_array($chat)) { unset($this->secret_chats[$key]); @@ -1157,6 +1190,9 @@ class MTProto extends AsyncConstruct implements TLCallback // onStart event handler if ($this->event_handler && \class_exists($this->event_handler) && \is_subclass_of($this->event_handler, EventHandler::class)) { yield from $this->setEventHandler($this->event_handler); + } else { + $this->event_handler = null; + $this->event_handler_instance = null; } $this->startUpdateSystem(true); if ($this->authorized === self::LOGGED_IN && !$this->authorization['user']['bot'] && $this->settings->getPeer()->getCacheAllPeersOnStartup()) { @@ -1550,13 +1586,22 @@ class MTProto extends AsyncConstruct implements TLCallback $this->updaters[$channelId]->resume(); } } - foreach ($this->datacenter->getDataCenterConnections() as $datacenter) { - $datacenter->flush(); - } + $this->flushAll(); if ($this->seqUpdater->start()) { $this->seqUpdater->resume(); } } + /** + * Flush all datacenter connections. + * + * @return void + */ + private function flushAll(): void + { + foreach ($this->datacenter->getDataCenterConnections() as $datacenter) { + $datacenter->flush(); + } + } /** * Store shared phone config. * @@ -1688,7 +1733,7 @@ class MTProto extends AsyncConstruct implements TLCallback public function fullGetSelf(): \Generator { try { - $this->authorization = ['user' => (yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [['_' => 'inputUserSelf']]], ['datacenter' => $this->datacenter->curdc]))[0]]; + $this->authorization = ['user' => (yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [['_' => 'inputUserSelf']]]))[0]]; } catch (RPCErrorException $e) { $this->logger->logger($e->getMessage()); return false; diff --git a/src/danog/MadelineProto/MTProto/Container.php b/src/danog/MadelineProto/MTProto/Container.php new file mode 100644 index 00000000..526ef857 --- /dev/null +++ b/src/danog/MadelineProto/MTProto/Container.php @@ -0,0 +1,54 @@ +. + * + * @author Daniil Gentili + * @copyright 2016-2020 Daniil Gentili + * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 + * + * @link https://docs.madelineproto.xyz MadelineProto documentation + */ + +namespace danog\MadelineProto\MTProto; + +/** + * Outgoing container message. + * + * @internal + */ +class Container extends OutgoingMessage +{ + /** + * Message IDs. + * + */ + private array $ids = []; + + /** + * Constructor. + * + * @param array $ids + */ + public function __construct(array $ids) + { + $this->ids = $ids; + parent::__construct([], 'msg_container', '', false, false); + } + /** + * Get message IDs. + * + * @return array + */ + public function getIds(): array + { + return $this->ids; + } +} diff --git a/src/danog/MadelineProto/MTProto/IncomingMessage.php b/src/danog/MadelineProto/MTProto/IncomingMessage.php new file mode 100644 index 00000000..4d1b8d80 --- /dev/null +++ b/src/danog/MadelineProto/MTProto/IncomingMessage.php @@ -0,0 +1,237 @@ +. + * + * @author Daniil Gentili + * @copyright 2016-2020 Daniil Gentili + * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 + * + * @link https://docs.madelineproto.xyz MadelineProto documentation + */ + +namespace danog\MadelineProto\MTProto; + +use Amp\Promise; + +/** + * Incoming message. + * + * @internal + */ +class IncomingMessage extends Message +{ + /** + * We have received this message. + */ + const STATE_RECEIVED = 4; + /** + * We have acknowledged this message. + */ + const STATE_ACKED = 8; + /** + * We have read the contents of this message. + */ + const STATE_READ = 128; + + /** + * Response field map. + */ + private const RESPONSE_ID_MAP = [ + 'rpc_result' => 'req_msg_id', + 'future_salts' => 'req_msg_id', + 'msgs_state_info' => 'req_msg_id', + 'bad_server_salt' => 'bad_msg_id', + 'bad_message_notification' => 'bad_msg_id', + 'pong' => 'msg_id', + ]; + /** + * State. + */ + private int $state = self::STATE_RECEIVED; + /** + * Receive date. + */ + private int $received; + /** + * Deserialized response content. + */ + private array $content; + /** + * Was present in container. + */ + private bool $fromContainer; + + /** + * DB side effects to be resolved before using the content. + */ + private ?Promise $sideEffects = null; + + /** + * Constructor. + * + * @param array $content Content + * @param boolean $fromContainer Whether this message was in a container + */ + public function __construct(array $content, string $msgId, bool $fromContainer = false) + { + $this->content = $content; + $this->fromContainer = $fromContainer; + $this->msgId = $msgId; + + $this->received = \time(); + + $this->contentRelated = !isset(Message::NOT_CONTENT_RELATED[$content['_']]); + if (!$this->contentRelated) { + $this->state |= 16; // message not requiring acknowledgment + } + } + /** + * Get deserialized response content. + * + * @return array + */ + public function getContent(): array + { + return $this->content; + } + + /** + * Get was present in container. + * + * @return bool + */ + public function isFromContainer(): bool + { + return $this->fromContainer; + } + + /** + * Get log line. + * + * @param int|string $dc DC ID + * + * @return string + */ + public function log($dc): string + { + if ($this->fromContainer) { + return "Inside of container, received {$this->content['_']} from DC $dc"; + } + return "Received {$this->content['_']} from DC $dc"; + } + + /** + * Get message type. + * + * @return string + */ + public function getType(): string + { + return $this->content['_']; + } + + /** + * Get message type. + * + * @return string + */ + public function __toString(): string + { + return $this->content['_']; + } + + /** + * We have acked this message. + * + * @return void + */ + public function ack(): void + { + $this->state |= self::STATE_ACKED; + } + /** + * Read this message, clearing its contents. + * + * @return array + */ + public function read(): array + { + $this->state |= self::STATE_READ; + $content = $this->content; + $this->content = ['_' => $content['_']]; + return $content; + } + + /** + * Check if this message can be garbage collected. + * + * @return boolean + */ + public function canGarbageCollect(): bool + { + return (bool) ($this->state & self::STATE_READ); + } + + /** + * Get ID of message to which this message replies. + * + * @return string + */ + public function getRequestId(): string + { + return $this->content[self::RESPONSE_ID_MAP[$this->content['_']]]; + } + /** + * Get state. + * + * @return int + */ + public function getState(): int + { + return $this->state; + } + + /** + * Set DB side effects to be resolved before using the content. + * + * @param ?Promise $sideEffects DB side effects to be resolved before using the content + * + * @return self + */ + public function setSideEffects(?Promise $sideEffects): self + { + $this->sideEffects = $sideEffects; + + return $this; + } + + /** + * yield DB side effects to be resolved before using the content. + * + * @return \Generator + */ + public function yieldSideEffects(): \Generator + { + if ($this->sideEffects) { + yield $this->sideEffects; + } + } + + /** + * Get receive date. + * + * @return int + */ + public function getReceived(): int + { + return $this->received; + } +} diff --git a/src/danog/MadelineProto/MTProto/Message.php b/src/danog/MadelineProto/MTProto/Message.php new file mode 100644 index 00000000..ccc0697c --- /dev/null +++ b/src/danog/MadelineProto/MTProto/Message.php @@ -0,0 +1,160 @@ +. + * + * @author Daniil Gentili + * @copyright 2016-2020 Daniil Gentili + * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 + * + * @link https://docs.madelineproto.xyz MadelineProto documentation + */ + +namespace danog\MadelineProto\MTProto; + +/** + * MTProto message. + * + * @internal + */ +abstract class Message +{ + protected const NOT_CONTENT_RELATED = [ + //'rpc_result' => true, + //'rpc_error' => true, + 'rpc_drop_answer' => true, + 'rpc_answer_unknown' => true, + 'rpc_answer_dropped_running' => true, + 'rpc_answer_dropped' => true, + 'get_future_salts' => true, + 'future_salt' => true, + 'future_salts' => true, + 'ping' => true, + 'pong' => true, + 'ping_delay_disconnect' => true, + 'destroy_session' => true, + 'destroy_session_ok' => true, + 'destroy_session_none' => true, + //'new_session_created' => true, + 'msg_container' => true, + 'msg_copy' => true, + 'gzip_packed' => true, + 'http_wait' => true, + 'msgs_ack' => true, + 'bad_msg_notification' => true, + 'bad_server_salt' => true, + 'msgs_state_req' => true, + 'msgs_state_info' => true, + 'msgs_all_info' => true, + 'msg_detailed_info' => true, + 'msg_new_detailed_info' => true, + 'msg_resend_req' => true, + 'msg_resend_ans_req' => true, + ]; + /** + * My message ID. + */ + protected $msgId = null; + + /** + * Sequence number. + */ + protected ?int $seqNo = null; + + /** + * Whether constructor is content related. + */ + protected bool $contentRelated; + + /** + * Get whether constructor is content related. + * + * @return bool + */ + public function isContentRelated(): bool + { + return $this->contentRelated; + } + + /** + * Get my message ID. + * + * @return mixed + */ + public function getMsgId() + { + return $this->msgId; + } + + /** + * Set my message ID. + * + * @param mixed $msgId My message ID + * + * @return self + */ + public function setMsgId($msgId): self + { + $this->msgId = $msgId; + + return $this; + } + + /** + * Check if we have a message ID. + * + * @return boolean + */ + public function hasMsgId(): bool + { + return $this->msgId !== null; + } + + /** + * Get sequence number. + * + * @return ?int + */ + public function getSeqNo(): ?int + { + return $this->seqNo; + } + + /** + * Has sequence number. + * + * @return bool + */ + public function hasSeqNo(): bool + { + return isset($this->seqNo); + } + + /** + * Set sequence number. + * + * @param ?int $seqNo Sequence number + * + * @return self + */ + public function setSeqNo(?int $seqNo): self + { + $this->seqNo = $seqNo; + + return $this; + } + + /** + * Check whether this message can be garbage collected. + * + * @return boolean + */ + abstract public function canGarbageCollect(): bool; +} diff --git a/src/danog/MadelineProto/MTProto/OutgoingMessage.php b/src/danog/MadelineProto/MTProto/OutgoingMessage.php new file mode 100644 index 00000000..07975131 --- /dev/null +++ b/src/danog/MadelineProto/MTProto/OutgoingMessage.php @@ -0,0 +1,617 @@ +. + * + * @author Daniil Gentili + * @copyright 2016-2020 Daniil Gentili + * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 + * + * @link https://docs.madelineproto.xyz MadelineProto documentation + */ + +namespace danog\MadelineProto\MTProto; + +use Amp\Deferred; +use Amp\Promise; +use danog\MadelineProto\Exception; +use danog\MadelineProto\MTProtoSession\MsgIdHandler; + +/** + * Outgoing message. + * + * @internal + */ +class OutgoingMessage extends Message +{ + /** + * The message was created. + */ + const STATE_PENDING = 0; + /** + * The message was sent. + */ + const STATE_SENT = 1; + /** + * The message was acked. + */ + const STATE_ACKED = 2; + /** + * We got a reply to the message. + */ + const STATE_REPLIED = self::STATE_ACKED | 4; + + /** + * State of message. + * + * @var int + * @psalm-var self::STATE_* + */ + private int $state = self::STATE_PENDING; + /** + * Constructor name. + */ + private string $constructor; + /** + * Constructor type. + */ + private string $type; + + /** + * Whether this is a method. + */ + private bool $method; + /** + * Resolution deferred. + */ + private ?Deferred $promise = null; + /** + * Send deferred. + */ + private ?Deferred $sendPromise = null; + + + /** + * Whether this is an unencrypted message. + */ + private bool $unencrypted; + + /** + * Message body. + * + * @var \Generator|array|null + */ + private $body; + + /** + * Serialized body. + */ + private ?string $serializedBody = null; + + /** + * Whether this message is related to a user, as in getting a successful reply means we have auth. + */ + private bool $userRelated = false; + /** + * Whether this message is related to a file upload, as in getting a redirect should redirect to a media server. + */ + private bool $fileRelated = false; + + /** + * Custom flood wait limit for this bot + */ + private ?int $floodWaitLimit = null; + + /** + * Whether we should try converting the result to a bot API object. + */ + private bool $botAPI = false; + + /** + * Whether we should refresh references upon serialization of this message. + */ + private bool $refreshReferences = false; + + /** + * Queue ID. + */ + private ?string $queueId = null; + + /** + * When was this message sent. + */ + private int $sent = 0; + + /** + * Number of times this message was sent. + */ + private int $tries = 0; + + /** + * Create outgoing message. + * + * @param \Generator|array $body Body + * @param string $constructor Constructor name + * @param string $type Constructor type + * @param boolean $method Is this a method? + * @param boolean $unencrypted Is this an unencrypted message? + */ + public function __construct($body, string $constructor, string $type, bool $method, bool $unencrypted) + { + $this->body = $body; + $this->constructor = $constructor; + $this->type = $type; + $this->method = $method; + $this->unencrypted = $unencrypted; + if ($method) { + $this->promise = new Deferred; + } + + $this->contentRelated = !isset(Message::NOT_CONTENT_RELATED[$constructor]); + } + + /** + * Signal that we're trying to send the message. + * + * @return void + */ + public function trySend(): void + { + if (!isset($this->sendPromise)) { + $this->sendPromise = new Deferred; + } + $this->tries++; + } + /** + * Signal that the message was sent. + * + * @return void + */ + public function sent(): void + { + if ($this->state & self::STATE_REPLIED) { + throw new Exception("Trying to resend already replied message $this!"); + } + $this->state |= self::STATE_SENT; + $this->sent = \time(); + if (isset($this->sendPromise)) { + $sendPromise = $this->sendPromise; + $this->sendPromise = null; + $sendPromise->resolve($this->promise ?? true); + } + } + /** + * Set reply to message. + * + * @param mixed $result + * @return void + */ + public function reply($result): void + { + if ($this->state & self::STATE_REPLIED) { + throw new Exception("Trying to double reply to message $this!"); + } + $this->serializedBody = null; + $this->body = null; + + $this->state |= self::STATE_REPLIED; + if ($this->promise) { // Sometimes can get an RPC error for constructors + $promise = $this->promise; + $this->promise = null; + $promise->resolve($result); + } + } + + /** + * ACK message. + * + * @return void + */ + public function ack(): void + { + $this->state |= self::STATE_ACKED; + } + /** + * Get state of message. + * + * @return int + * @psalm-return self::STATE_* + */ + public function getState(): int + { + return $this->state; + } + + + /** + * Get message body. + * + * @return \Generator + */ + public function getBody(): \Generator + { + return $this->body instanceof \Generator ? yield from $this->body : $this->body; + } + + /** + * Get message body or empty array. + * + * @return array + */ + public function getBodyOrEmpty(): array + { + return \is_array($this->body) ? $this->body : []; + } + /** + * Check if we have a body. + * + * @return boolean + */ + public function hasBody(): bool + { + return $this->body !== null; + } + + /** + * Get serialized body. + * + * @return ?string + */ + public function getSerializedBody(): ?string + { + return $this->serializedBody; + } + /** + * Check if we have a serialized body. + * + * @return boolean + */ + public function hasSerializedBody(): bool + { + return $this->serializedBody !== null; + } + + /** + * Get number of times this message was sent. + * + * @return int + */ + public function getTries(): int + { + return $this->tries; + } + + /** + * Get constructor name. + * + * @return string + */ + public function getConstructor(): string + { + return $this->constructor; + } + + /** + * Get constructor type. + * + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * Get whether this is a method. + * + * @return bool + */ + public function isMethod(): bool + { + return $this->method; + } + + /** + * Get whether this is an unencrypted message. + * + * @return bool + */ + public function isUnencrypted(): bool + { + return $this->unencrypted; + } + /** + * Get whether this is an encrypted message. + * + * @return bool + */ + public function isEncrypted(): bool + { + return !$this->unencrypted; + } + + /** + * Get whether this message is related to a user, as in getting a successful reply means we have auth. + * + * @return bool + */ + public function isUserRelated(): bool + { + return $this->userRelated; + } + + /** + * Get whether we should refresh references upon serialization of this message. + * + * @return bool + */ + public function shouldRefreshReferences(): bool + { + return $this->refreshReferences; + } + + /** + * Get queue ID. + * + * @return ?string + */ + public function getQueueId(): ?string + { + return $this->queueId; + } + /** + * Get whether we have a queue ID. + * + * @return bool + */ + public function hasQueue(): bool + { + return $this->queueId !== null; + } + + /** + * Set serialized body. + * + * @param string $serializedBody Serialized body. + * + * @return self + */ + public function setSerializedBody(string $serializedBody): self + { + $this->serializedBody = $serializedBody; + + return $this; + } + + /** + * Set whether this message is related to a user, as in getting a successful reply means we have auth. + * + * @param bool $userRelated Whether this message is related to a user, as in getting a successful reply means we have auth. + * + * @return self + */ + public function setUserRelated(bool $userRelated): self + { + $this->userRelated = $userRelated; + + return $this; + } + + /** + * Set whether we should refresh references upon serialization of this message. + * + * @param bool $refreshReferences Whether we should refresh references upon serialization of this message. + * + * @return self + */ + public function setRefreshReferences(bool $refreshReferences): self + { + $this->refreshReferences = $refreshReferences; + + return $this; + } + + /** + * Set queue ID. + * + * @param ?string $queueId Queue ID. + * + * @return self + */ + public function setQueueId(?string $queueId): self + { + $this->queueId = $queueId; + + return $this; + } + + /** + * Get when was this message sent. + * + * @return int + */ + public function getSent(): int + { + return $this->sent; + } + + /** + * Check if the message was sent. + * + * @return boolean + */ + public function wasSent(): bool + { + return (bool) ($this->state & self::STATE_SENT); + } + /** + * Check if can garbage collect this message. + * + * @return boolean + */ + public function canGarbageCollect(): bool + { + if ($this->state & self::STATE_REPLIED) { + return true; + } + if (!$this->hasPromise()) { + return true; + } + return false; + } + /** + * For logging. + * + * @return string + */ + public function __toString() + { + if ($this->msgId) { + $msgId = MsgIdHandler::toString($this->msgId); + return "{$this->constructor} with message ID $msgId"; + } + return $this->constructor; + } + + /** + * Set resolution deferred. + * + * @param Deferred $promise Resolution deferred. + * + * @return self + */ + public function setPromise(Deferred $promise): self + { + $this->promise = $promise; + + return $this; + } + + /** + * Wait for message to be sent. + * + * @return Promise + */ + public function getSendPromise(): Promise + { + if (!$this->sendPromise) { + throw new Exception("Message was already sent, can't get send promise!"); + } + return $this->sendPromise->promise(); + } + + /** + * Check if we have a promise. + * + * @return bool + */ + public function hasPromise(): bool + { + return $this->promise !== null; + } + + /** + * Reset sent time to trigger resending. + * + * @return self + */ + public function resetSent(): self + { + $this->sent = 0; + + return $this; + } + + /** + * Get whether we should try converting the result to a bot API object. + * + * @return bool + */ + public function getBotAPI(): bool + { + return $this->botAPI; + } + + /** + * Set whether we should try converting the result to a bot API object. + * + * @param bool $botAPI Whether we should try converting the result to a bot API object + * + * @return self + */ + public function setBotAPI(bool $botAPI): self + { + $this->botAPI = $botAPI; + + return $this; + } + + /** + * Get whether this message is related to a file upload, as in getting a redirect should redirect to a media server. + * + * @return bool + */ + public function isFileRelated(): bool + { + return $this->fileRelated; + } + + /** + * Set whether this message is related to a file upload, as in getting a redirect should redirect to a media server. + * + * @param bool $fileRelated Whether this message is related to a file upload, as in getting a redirect should redirect to a media server. + * + * @return self + */ + public function setFileRelated(bool $fileRelated): self + { + $this->fileRelated = $fileRelated; + + return $this; + } + + /** + * Get custom flood wait limit for this bot + * + * @return ?int + */ + public function getFloodWaitLimit(): ?int + { + return $this->floodWaitLimit; + } + + /** + * Set custom flood wait limit for this bot + * + * @param ?int $floodWaitLimit Custom flood wait limit for this bot + * + * @return self + */ + public function setFloodWaitLimit(?int $floodWaitLimit): self + { + $this->floodWaitLimit = $floodWaitLimit; + + return $this; + } + + /** + * Set when was this message sent. + * + * @param int $sent When was this message sent. + * + * @return self + */ + public function setSent(int $sent): self + { + $this->sent = $sent; + + return $this; + } +} diff --git a/src/danog/MadelineProto/MTProtoSession/AckHandler.php b/src/danog/MadelineProto/MTProtoSession/AckHandler.php index 088459b5..f00bdda0 100644 --- a/src/danog/MadelineProto/MTProtoSession/AckHandler.php +++ b/src/danog/MadelineProto/MTProtoSession/AckHandler.php @@ -20,6 +20,8 @@ namespace danog\MadelineProto\MTProtoSession; use danog\MadelineProto\DataCenterConnection; +use danog\MadelineProto\MTProto\IncomingMessage; +use danog\MadelineProto\MTProto\OutgoingMessage; /** * Manages acknowledgement of messages. @@ -45,45 +47,33 @@ trait AckHandler return true; } /** - * We have gotten response for outgoing message ID. + * We have gotten a response for an outgoing message. * - * @param string|int $message_id Message ID + * @param OutgoingMessage $message Message * - * @return boolean + * @return void */ - public function gotResponseForOutgoingMessageId($message_id): bool + public function gotResponseForOutgoingMessage(OutgoingMessage $outgoingMessage): void { // The server acknowledges that it received my message - if (isset($this->new_outgoing[$message_id])) { - unset($this->new_outgoing[$message_id]); + if (isset($this->new_outgoing[$outgoingMessage->getMsgId()])) { + unset($this->new_outgoing[$outgoingMessage->getMsgId()]); } - if (!isset($this->outgoing_messages[$message_id])) { - $this->logger->logger("WARNING: Couldn't find message id ".$message_id.' in the array of outgoing messages. Maybe try to increase its size?', \danog\MadelineProto\Logger::WARNING); - return false; - } - if (isset($this->outgoing_messages[$message_id]['body'])) { - unset($this->outgoing_messages[$message_id]['body']); - } - if (isset($this->outgoing_messages[$message_id]['serialized_body'])) { - unset($this->outgoing_messages[$message_id]['serialized_body']); - } - return true; } /** * Acknowledge incoming message ID. * - * @param string|int $message_id Message ID + * @param IncomingMessage $message Message * - * @return boolean + * @return void */ - public function ackIncomingMessageId($message_id): bool + public function ackIncomingMessage(IncomingMessage $message): void { + // Not exactly true, but we don't care + $message->ack(); + $message_id = $message->getMsgId(); // I let the server know that I received its message - if (!isset($this->incoming_messages[$message_id])) { - $this->logger->logger("WARNING: Couldn't find message id ".$message_id.' in the array of incoming messages. Maybe try to increase its size?', \danog\MadelineProto\Logger::WARNING); - } $this->ack_queue[$message_id] = $message_id; - return true; } /** @@ -98,9 +88,13 @@ trait AckHandler $unencrypted = !$this->shared->hasTempAuthKey(); $notBound = !$this->shared->isBound(); $pfsNotBound = $pfs && $notBound; - foreach ($this->new_outgoing as $message_id) { - if (isset($this->outgoing_messages[$message_id]['sent']) && $this->outgoing_messages[$message_id]['sent'] + $timeout < \time() && $unencrypted === $this->outgoing_messages[$message_id]['unencrypted'] && $this->outgoing_messages[$message_id]['_'] !== 'msgs_state_req') { - if ($pfsNotBound && $this->outgoing_messages[$message_id]['_'] !== 'auth.bindTempAuthKey') { + /** @var OutgoingMessage */ + foreach ($this->new_outgoing as $message) { + if ($message->wasSent() + && $message->getSent() + $timeout < \time() + && $message->isUnencrypted() === $unencrypted + && $message->getConstructor() !== 'msgs_state_req') { + if ($pfsNotBound && $message->getConstructor() !== 'auth.bindTempAuthKey') { continue; } return true; @@ -124,18 +118,25 @@ trait AckHandler $notBound = !$this->shared->isBound(); $pfsNotBound = $pfs && $notBound; $result = []; - foreach ($this->new_outgoing as $k => $message_id) { - if (isset($this->outgoing_messages[$message_id]['sent']) && $this->outgoing_messages[$message_id]['sent'] + $timeout < \time() && $unencrypted === $this->outgoing_messages[$message_id]['unencrypted']) { - if ($pfsNotBound && $this->outgoing_messages[$message_id]['_'] !== 'auth.bindTempAuthKey') { + /** @var OutgoingMessage $message */ + foreach ($this->new_outgoing as $message_id => $message) { + if ($message->wasSent() + && $message->getSent() + $timeout < \time() + && $message->isUnencrypted() === $unencrypted + ) { + if ($pfsNotBound && $message->getConstructor() !== 'auth.bindTempAuthKey') { continue; } - if ($this->outgoing_messages[$message_id]['_'] === 'msgs_state_req') { - unset($this->new_outgoing[$k], $this->outgoing_messages[$message_id]); + if ($message->getConstructor() === 'msgs_state_req') { + unset($this->new_outgoing[$message_id], $this->outgoing_messages[$message_id]); continue; } - if ($this->outgoing_messages[$message_id]['sent'] + $dropTimeout < \time()) { - $this->gotResponseForOutgoingMessageId($message_id); - $this->handleReject($this->outgoing_messages[$message_id], new \danog\MadelineProto\Exception("Request timeout")); + if ($message->getSent() + $dropTimeout < \time()) { + $this->handleReject($message, new \danog\MadelineProto\Exception("Request timeout")); + continue; + } + if ($message->getState() & OutgoingMessage::STATE_REPLIED) { + $this->logger->logger("Already replied to message $message, but still in new_outgoing"); continue; } $result[] = $message_id; diff --git a/src/danog/MadelineProto/MTProtoSession/CallHandler.php b/src/danog/MadelineProto/MTProtoSession/CallHandler.php index 3e212ef8..68631c02 100644 --- a/src/danog/MadelineProto/MTProtoSession/CallHandler.php +++ b/src/danog/MadelineProto/MTProtoSession/CallHandler.php @@ -20,6 +20,8 @@ namespace danog\MadelineProto\MTProtoSession; use Amp\Deferred; +use danog\MadelineProto\MTProto\Container; +use danog\MadelineProto\MTProto\OutgoingMessage; use danog\MadelineProto\TL\Exception; use danog\MadelineProto\Tools; @@ -44,25 +46,31 @@ trait CallHandler if ($datacenter === $this->datacenter) { $datacenter = false; } - $message_ids = $this->outgoing_messages[$message_id]['container'] ?? [$message_id]; + $message_ids = $this->outgoing_messages[$message_id] instanceof Container + ? $this->outgoing_messages[$message_id]->getIds() + : [$message_id]; foreach ($message_ids as $message_id) { - if (isset($this->outgoing_messages[$message_id]['body'])) { + if (isset($this->outgoing_messages[$message_id]) + && $this->outgoing_messages[$message_id]->canGarbageCollect()) { if ($datacenter) { - unset($this->outgoing_messages[$message_id]['msg_id'], $this->outgoing_messages[$message_id]['seqno']); - Tools::call($this->API->datacenter->waitGetConnection($datacenter))->onResolve(function ($e, $r) use ($message_id) { - Tools::callFork($r->sendMessage($this->outgoing_messages[$message_id], false)); + /** @var OutgoingMessage */ + $message = $this->outgoing_messages[$message_id]; + $message->setMsgId(null); + $message->setSeqNo(null); + Tools::call($this->API->datacenter->waitGetConnection($datacenter))->onResolve(function ($e, $r) use ($message) { + Tools::callFork($r->sendMessage($message, false)); }); - $this->ackOutgoingMessageId($message_id); - $this->gotResponseForOutgoingMessageId($message_id); + $this->gotResponseForOutgoingMessage($message); } else { - Tools::callFork($this->sendMessage($this->outgoing_messages[$message_id], false)); - if (!isset($this->outgoing_messages[$message_id]['seqno'])) { - $this->ackOutgoingMessageId($message_id); - $this->gotResponseForOutgoingMessageId($message_id); + /** @var OutgoingMessage */ + $message = $this->outgoing_messages[$message_id]; + Tools::callFork($this->sendMessage($message, false)); + if (!$message->hasSeqNo()) { + $this->gotResponseForOutgoingMessage($message); } } } else { - $this->logger->logger('Could not resend '.(isset($this->outgoing_messages[$message_id]['_']) ? $this->outgoing_messages[$message_id]['_'] : $message_id)); + $this->logger->logger('Could not resend '.($this->outgoing_messages[$message_id] ?? $message_id)); } } if (!$postpone) { @@ -154,25 +162,28 @@ trait CallHandler $args['ping_id'] = Tools::packSignedLong($args['ping_id']); } } - $deferred = new Deferred(); $methodInfo = $this->API->getTL()->getMethods()->findByMethod($method); if (!$methodInfo) { throw new Exception("Could not find method $method!"); } - $message = \array_merge( - $aargs, - [ - '_' => $method, - 'body' => $args, - 'type' => $methodInfo['type'], - 'contentRelated' => $this->contentRelated($method), - 'promise' => $deferred, - 'method' => true, - 'unencrypted' => !$this->shared->hasTempAuthKey() && \strpos($method, '.') === false - ] + $message = new OutgoingMessage( + $args, + $method, + $methodInfo['type'], + true, + !$this->shared->hasTempAuthKey() && \strpos($method, '.') === false ); if ($method === 'users.getUsers' && $args === ['id' => [['_' => 'inputUserSelf']]] || $method === 'auth.exportAuthorization' || $method === 'updates.getDifference') { - $message['user_related'] = true; + $message->setUserRelated(true); + } + if (isset($aargs['msg_id'])) { + $message->setMsgId($aargs['msg_id']); + } + if ($aargs['file'] ?? false) { + $message->setFileRelated(true); + } + if (isset($aargs['FloodWaitLimit'])) { + $message->setFloodWaitLimit($aargs['FloodWaitLimit']); } $aargs['postpone'] = $aargs['postpone'] ?? false; $deferred = yield from $this->sendMessage($message, !$aargs['postpone']); @@ -190,11 +201,17 @@ trait CallHandler */ public function objectCall(string $object, $args = [], array $aargs = ['msg_id' => null]): \Generator { - $message = ['_' => $object, 'body' => $args, 'contentRelated' => $this->contentRelated($object), 'unencrypted' => !$this->shared->hasTempAuthKey(), 'method' => false]; + $message = new OutgoingMessage( + $args, + $object, + '', + false, + !$this->shared->hasTempAuthKey() + ); if (isset($aargs['promise'])) { - $message['promise'] = $aargs['promise']; + $message->setPromise($aargs['promise']); } $aargs['postpone'] = $aargs['postpone'] ?? false; - return $this->sendMessage($message, !$aargs['postpone']); + return yield from $this->sendMessage($message, !$aargs['postpone']); } } diff --git a/src/danog/MadelineProto/MTProtoSession/MsgIdHandler.php b/src/danog/MadelineProto/MTProtoSession/MsgIdHandler.php index bff727a2..3d7bb698 100644 --- a/src/danog/MadelineProto/MTProtoSession/MsgIdHandler.php +++ b/src/danog/MadelineProto/MTProtoSession/MsgIdHandler.php @@ -80,4 +80,50 @@ abstract class MsgIdHandler * @return mixed */ abstract public function getMaxId(bool $incoming); + + /** + * Get readable representation of message ID. + * + * @param string $messageId + * @return string + */ + abstract protected static function toStringInternal(string $messageId): string; + + /** + * Cleanup incoming or outgoing messages. + * + * @param boolean $incoming + * @return void + */ + protected function cleanup(bool $incoming): void + { + if ($incoming) { + $array = &$this->session->incoming_messages; + } else { + $array = &$this->session->outgoing_messages; + } + if (\count($array) > $this->session->API->settings->getRpc()->getLimitOutgoing()) { + \reset($array); + $key = \key($array); + foreach ($array as $key => $message) { + if ($message->canGarbageCollect()) { + unset($array[$key]); + break; + } + $this->session->API->logger->logger("Can't garbage collect $message", \danog\MadelineProto\Logger::VERBOSE); + } + } + } + /** + * Get readable representation of message ID. + * + * @param string $messageId + * @return string + */ + public static function toString(string $messageId): string + { + return PHP_INT_SIZE === 8 + ? MsgIdHandler64::toStringInternal($messageId) + : MsgIdHandler32::toStringInternal($messageId); + } } diff --git a/src/danog/MadelineProto/MTProtoSession/MsgIdHandler/MsgIdHandler32.php b/src/danog/MadelineProto/MTProtoSession/MsgIdHandler/MsgIdHandler32.php index 00da6ed3..a0a1f2cb 100644 --- a/src/danog/MadelineProto/MTProtoSession/MsgIdHandler/MsgIdHandler32.php +++ b/src/danog/MadelineProto/MTProtoSession/MsgIdHandler/MsgIdHandler32.php @@ -66,15 +66,8 @@ class MsgIdHandler32 extends MsgIdHandler if ($newMessageId->compare($key = $this->getMaxId($incoming = false)) <= 0) { throw new \danog\MadelineProto\Exception('Given message id ('.$newMessageId.') is lower than or equal to the current limit ('.$key.'). Consider syncing your date.', 1); } - if (\count($this->session->outgoing_messages) > $this->session->API->settings->getRpc()->getLimitOutgoing()) { - \reset($this->session->outgoing_messages); - $key = \key($this->session->outgoing_messages); - if (!isset($this->session->outgoing_messages[$key]['promise'])) { - unset($this->session->outgoing_messages[$key]); - } - } + $this->cleanup(false); $this->maxOutgoingId = $newMessageId; - $this->session->outgoing_messages[\strrev($newMessageId->toBytes())] = []; } else { if (!$newMessageId->divide(\danog\MadelineProto\Magic::$four)[1]->equals(\danog\MadelineProto\Magic::$one) && !$newMessageId->divide(\danog\MadelineProto\Magic::$four)[1]->equals(\danog\MadelineProto\Magic::$three)) { throw new \danog\MadelineProto\Exception('message id mod 4 != 1 or 3'); @@ -89,15 +82,8 @@ class MsgIdHandler32 extends MsgIdHandler $this->session->API->logger->logger('WARNING: Given message id ('.$newMessageId.') is lower than or equal to the current limit ('.$key.'). Consider syncing your date.', \danog\MadelineProto\Logger::WARNING); } } - if (\count($this->session->incoming_messages) > $this->session->API->settings->getRpc()->getLimitIncoming()) { - \reset($this->session->incoming_messages); - $key = \key($this->session->incoming_messages); - if (!isset($this->session->incoming_messages[$key]['promise'])) { - unset($this->session->incoming_messages[$key]); - } - } + $this->cleanup(true); $this->maxIncomingId = $newMessageId; - $this->session->incoming_messages[\strrev($newMessageId->toBytes())] = []; } } /** @@ -140,4 +126,15 @@ class MsgIdHandler32 extends MsgIdHandler $this->maxIncomingId = null; $this->maxOutgoingId = null; } + + /** + * Get readable representation of message ID. + * + * @param string $messageId + * @return string + */ + protected static function toStringInternal(string $messageId): string + { + return new BigInteger(\strrev($messageId), 256); + } } diff --git a/src/danog/MadelineProto/MTProtoSession/MsgIdHandler/MsgIdHandler64.php b/src/danog/MadelineProto/MTProtoSession/MsgIdHandler/MsgIdHandler64.php index bfd1ed41..b0540e83 100644 --- a/src/danog/MadelineProto/MTProtoSession/MsgIdHandler/MsgIdHandler64.php +++ b/src/danog/MadelineProto/MTProtoSession/MsgIdHandler/MsgIdHandler64.php @@ -65,15 +65,8 @@ class MsgIdHandler64 extends MsgIdHandler if ($newMessageId <= $this->maxOutgoingId) { throw new \danog\MadelineProto\Exception('Given message id ('.$newMessageId.') is lower than or equal to the current limit ('.$this->maxOutgoingId.'). Consider syncing your date.'); } - if (\count($this->session->outgoing_messages) > $this->session->API->settings->getRpc()->getLimitOutgoing()) { - \reset($this->session->outgoing_messages); - $key = \key($this->session->outgoing_messages); - if (!isset($this->session->outgoing_messages[$key]['promise'])) { - unset($this->session->outgoing_messages[$key]); - } - } + $this->cleanup(false); $this->maxOutgoingId = $newMessageId; - $this->session->outgoing_messages[Tools::packSignedLong($newMessageId)] = []; } else { if (!($newMessageId % 2)) { throw new \danog\MadelineProto\Exception('message id mod 4 != 1 or 3'); @@ -88,15 +81,8 @@ class MsgIdHandler64 extends MsgIdHandler $this->session->API->logger->logger('WARNING: Given message id ('.$newMessageId.') is lower than or equal to the current limit ('.$key.'). Consider syncing your date.', \danog\MadelineProto\Logger::WARNING); } } - if (\count($this->session->incoming_messages) > $this->session->API->settings->getRpc()->getLimitIncoming()) { - \reset($this->session->incoming_messages); - $key = \key($this->session->incoming_messages); - if (!isset($this->session->incoming_messages[$key]['promise'])) { - unset($this->session->incoming_messages[$key]); - } - } + $this->cleanup(true); $this->maxIncomingId = $newMessageId; - $this->session->incoming_messages[Tools::packSignedLong($newMessageId)] = []; } } /** @@ -124,4 +110,15 @@ class MsgIdHandler64 extends MsgIdHandler { return $this->{$incoming ? 'maxIncomingId' : 'maxOutgoingId'}; } + + /** + * Get readable representation of message ID. + * + * @param string $messageId + * @return string + */ + protected static function toStringInternal(string $messageId): string + { + return (string) Tools::unpackSignedLong($messageId); + } } diff --git a/src/danog/MadelineProto/MTProtoSession/Reliable.php b/src/danog/MadelineProto/MTProtoSession/Reliable.php index 6d923b79..dac2f516 100644 --- a/src/danog/MadelineProto/MTProtoSession/Reliable.php +++ b/src/danog/MadelineProto/MTProtoSession/Reliable.php @@ -19,20 +19,108 @@ namespace danog\MadelineProto\MTProtoSession; +use danog\MadelineProto\MTProto; +use danog\MadelineProto\Tools; + /** * Manages responses. */ trait Reliable { + /** + * Called when receiving a new_msg_detailed_info. + * + * @param array $content + * @return void + */ + public function onNewMsgDetailedInfo(array $content): void + { + if (isset($this->incoming_messages[$content['answer_msg_id']])) { + $this->ackIncomingMessage($this->incoming_messages[$content['answer_msg_id']]); + } else { + Tools::callFork($this->objectCall('msg_resend_req', ['msg_ids' => [$content['answer_msg_id']]], ['postpone' => true])); + } + } + /** + * Called when receiving a msg_detailed_info. + * + * @param array $content + * @return void + */ + public function onMsgDetailedInfo(array $content): void + { + if (isset($this->outgoing_messages[$content['msg_id']])) { + $this->onNewMsgDetailedInfo($content); + } + } + /** + * Called when receiving a msg_resend_req. + * + * @param array $content + * @param string $current_msg_id + * @return void + */ + public function onMsgResendReq(array $content, $current_msg_id): void + { + $ok = true; + foreach ($content['msg_ids'] as $msg_id) { + if (!isset($this->outgoing_messages[$msg_id]) || isset($this->incoming_messages[$msg_id])) { + $ok = false; + } + } + if ($ok) { + foreach ($content['msg_ids'] as $msg_id) { + $this->methodRecall('', ['message_id' => $msg_id, 'postpone' => true]); + } + } else { + $this->sendMsgsStateInfo($content['msg_ids'], $current_msg_id); + } + } + /** + * Called when receiving a msg_resend_ans_req. + * + * @param array $content + * @param string $current_msg_id + * @return void + */ + public function onMsgResendAnsReq(array $content, $current_msg_id): void + { + $this->sendMsgsStateInfo($content['msg_ids'], $current_msg_id); + } + + /** + * Called when receiving a msgs_all_info. + * + * @param array $content + * @return void + */ + public function onMsgsAllInfo(array $content): void + { + foreach ($content['msg_ids'] as $key => $msg_id) { + $info = \ord($content['info'][$key]); + $msg_id = MsgIdHandler::toString($msg_id); + $status = 'Status for message id '.$msg_id.': '; + /*if ($info & 4) { + *$this->gotResponseForOutgoingMessageId($msg_id); + *} + */ + foreach (MTProto::MSGS_INFO_FLAGS as $flag => $description) { + if (($info & $flag) !== 0) { + $status .= $description; + } + } + $this->logger->logger($status, \danog\MadelineProto\Logger::NOTICE); + } + } /** * Send state info for message IDs. * - * @param string|int $req_msg_id Message ID of msgs_state_req that initiated this * @param array $msg_ids Message IDs to send info about + * @param string|int $req_msg_id Message ID of msgs_state_req that initiated this * - * @return \Generator + * @return void */ - public function sendMsgsStateInfo($req_msg_id, array $msg_ids): \Generator + public function sendMsgsStateInfo(array $msg_ids, $req_msg_id): void { $this->logger->logger('Sending state info for '.\count($msg_ids).' message IDs'); $info = ''; @@ -52,10 +140,10 @@ trait Reliable } } else { $this->logger->logger("Know about {$msg_id}"); - $cur_info |= 4; + $cur_info = $this->incoming_messages[$msg_id]->getState(); } $info .= \chr($cur_info); } - $this->outgoing_messages[yield from $this->objectCall('msgs_state_info', ['req_msg_id' => $req_msg_id, 'info' => $info], ['postpone' => true])]['response'] = $req_msg_id; + Tools::callFork($this->objectCall('msgs_state_info', ['req_msg_id' => $req_msg_id, 'info' => $info], ['postpone' => true])); } } diff --git a/src/danog/MadelineProto/MTProtoSession/ResponseHandler.php b/src/danog/MadelineProto/MTProtoSession/ResponseHandler.php index 0a8e3cc7..5233fc3b 100644 --- a/src/danog/MadelineProto/MTProtoSession/ResponseHandler.php +++ b/src/danog/MadelineProto/MTProtoSession/ResponseHandler.php @@ -19,10 +19,14 @@ namespace danog\MadelineProto\MTProtoSession; +use Amp\Failure; use Amp\Loop; use danog\MadelineProto\Logger; use danog\MadelineProto\Loop\Update\UpdateLoop; use danog\MadelineProto\MTProto; +use danog\MadelineProto\MTProto\IncomingMessage; +use danog\MadelineProto\MTProto\OutgoingMessage; +use danog\MadelineProto\Tools; /** * Manages responses. @@ -32,492 +36,360 @@ use danog\MadelineProto\MTProto; trait ResponseHandler { public $n = 0; - public function handleMessages(): bool + public function handleMessages(): void { - $only_updates = true; while ($this->new_incoming) { \reset($this->new_incoming); $current_msg_id = \key($this->new_incoming); - if (!isset($this->incoming_messages[$current_msg_id])) { - unset($this->new_incoming[$current_msg_id]); - continue; + + /** @var IncomingMessage */ + $message = $this->new_incoming[$current_msg_id]; + unset($this->new_incoming[$current_msg_id]); + + + $this->logger->logger($message->log($this->datacenter), Logger::ULTRA_VERBOSE); + + $type = $message->getType(); + if ($type !== 'msg_container') { + $this->checkInSeqNo($message); } - $this->logger->logger((isset($this->incoming_messages[$current_msg_id]['from_container']) ? 'Inside of container, received ' : 'Received ').$this->incoming_messages[$current_msg_id]['content']['_'].' from DC '.$this->datacenter, \danog\MadelineProto\Logger::ULTRA_VERBOSE); - switch ($this->incoming_messages[$current_msg_id]['content']['_']) { + switch ($type) { case 'msgs_ack': - unset($this->new_incoming[$current_msg_id]); - $this->checkInSeqNo($current_msg_id); - $only_updates = false; - foreach ($this->incoming_messages[$current_msg_id]['content']['msg_ids'] as $msg_id) { - $this->ackOutgoingMessageId($msg_id); + foreach ($message->read()['msg_ids'] as $msg_id) { // Acknowledge that the server received my message + $this->ackOutgoingMessageId($msg_id); } - unset($this->incoming_messages[$current_msg_id]['content']); break; case 'rpc_result': - unset($this->new_incoming[$current_msg_id]); - $this->ackIncomingMessageId($current_msg_id); - $only_updates = false; - // Acknowledge that the server received my request - $req_msg_id = $this->incoming_messages[$current_msg_id]['content']['req_msg_id']; - $this->incoming_messages[$current_msg_id]['content'] = $this->incoming_messages[$current_msg_id]['content']['result']; - $this->checkInSeqNo($current_msg_id); - $this->handleResponse($req_msg_id, $current_msg_id); - break; + $this->ackIncomingMessage($message); + // no break case 'future_salts': case 'msgs_state_info': - $msg_id_type = 'req_msg_id'; - // no break case 'bad_server_salt': case 'bad_msg_notification': - $msg_id_type = isset($msg_id_type) ? $msg_id_type : 'bad_msg_id'; - // no break case 'pong': - $msg_id_type = isset($msg_id_type) ? $msg_id_type : 'msg_id'; - unset($this->new_incoming[$current_msg_id]); - $this->checkInSeqNo($current_msg_id); - $only_updates = false; - $this->handleResponse($this->incoming_messages[$current_msg_id]['content'][$msg_id_type], $current_msg_id); - unset($msg_id_type); + $this->handleResponse($message); break; case 'new_session_created': - unset($this->new_incoming[$current_msg_id]); - $this->checkInSeqNo($current_msg_id); - $only_updates = false; - $this->shared->getTempAuthKey()->setServerSalt($this->incoming_messages[$current_msg_id]['content']['server_salt']); - $this->ackIncomingMessageId($current_msg_id); - // Acknowledge that I received the server's response + $this->ackIncomingMessage($message); + $this->shared->getTempAuthKey()->setServerSalt($message->read()['server_salt']); if ($this->API->authorized === MTProto::LOGGED_IN && !$this->API->isInitingAuthorization() && $this->API->datacenter->getDataCenterConnection($this->API->datacenter->curdc)->hasTempAuthKey() && isset($this->API->updaters[UpdateLoop::GENERIC])) { $this->API->updaters[UpdateLoop::GENERIC]->resumeDefer(); } - unset($this->incoming_messages[$current_msg_id]['content']); break; case 'msg_container': - unset($this->new_incoming[$current_msg_id]); - $only_updates = false; - foreach ($this->incoming_messages[$current_msg_id]['content']['messages'] as $message) { + foreach ($message->read()['messages'] as $message) { $this->msgIdHandler->checkMessageId($message['msg_id'], ['outgoing' => false, 'container' => true]); - $this->incoming_messages[$message['msg_id']] = ['seq_no' => $message['seqno'], 'content' => $message['body'], 'from_container' => true]; - $this->new_incoming[$message['msg_id']] = $message['msg_id']; + $newMessage = new IncomingMessage($message['body'], $message['msg_id'], true); + $newMessage->setSeqNo($message['seqno']); + $this->new_incoming[$message['msg_id']] = $this->incoming_messages[$message['msg_id']] = $newMessage; } + unset($newMessage, $message); \ksort($this->new_incoming); - //$this->handleMessages(); - //$this->checkInSeqNo($current_msg_id); - unset($this->incoming_messages[$current_msg_id]['content']); break; case 'msg_copy': - unset($this->new_incoming[$current_msg_id]); - $this->checkInSeqNo($current_msg_id); - $only_updates = false; - $this->ackIncomingMessageId($current_msg_id); - // Acknowledge that I received the server's response - if (isset($this->incoming_messages[$this->incoming_messages[$current_msg_id]['content']['orig_message']['msg_id']])) { - $this->ackIncomingMessageId($this->incoming_messages[$current_msg_id]['content']['orig_message']['msg_id']); - // Acknowledge that I received the server's response + $this->ackIncomingMessage($message); + $content = $message->read(); + $referencedMsgId = $content['msg_id']; + if (isset($this->incoming_messages[$referencedMsgId])) { + $this->ackIncomingMessage($this->incoming_messages[$referencedMsgId]); } else { - $message = $this->incoming_messages[$current_msg_id]['content']; - $this->msgIdHandler->checkMessageId($message['orig_message']['msg_id'], ['outgoing' => false, 'container' => true]); - $this->incoming_messages[$message['orig_message']['msg_id']] = ['content' => $this->incoming_messages[$current_msg_id]['content']['orig_message']]; - $this->new_incoming[$message['orig_message']['msg_id']] = $message['orig_message']['msg_id']; + $this->msgIdHandler->checkMessageId($referencedMsgId, ['outgoing' => false, 'container' => true]); + $message = new IncomingMessage($content['orig_message'], $referencedMsgId); + $this->new_incoming[$referencedMsgId] = $this->incoming_messages[$referencedMsgId] = $message; + unset($message); } - unset($this->incoming_messages[$current_msg_id]['content']); + unset($content, $referencedMsgId); break; case 'http_wait': - unset($this->new_incoming[$current_msg_id]); - $this->checkInSeqNo($current_msg_id); - $only_updates = false; - $this->logger->logger($this->incoming_messages[$current_msg_id]['content'], \danog\MadelineProto\Logger::NOTICE); - unset($this->incoming_messages[$current_msg_id]['content']); + $this->logger->logger($message->read(), Logger::NOTICE); break; case 'msgs_state_req': - $this->checkInSeqNo($current_msg_id); - $only_updates = false; - unset($this->new_incoming[$current_msg_id]); - \danog\MadelineProto\Tools::callFork($this->sendMsgsStateInfo($current_msg_id, $this->incoming_messages[$current_msg_id]['content']['msg_ids'])); - unset($this->incoming_messages[$current_msg_id]['content']); + $this->sendMsgsStateInfo($message->read()['msg_ids'], $current_msg_id); break; case 'msgs_all_info': - $this->checkInSeqNo($current_msg_id); - $only_updates = false; - unset($this->new_incoming[$current_msg_id]); - foreach ($this->incoming_messages[$current_msg_id]['content']['msg_ids'] as $key => $msg_id) { - $info = \ord($this->incoming_messages[$current_msg_id]['content']['info'][$key]); - $msg_id = new \tgseclib\Math\BigInteger(\strrev($msg_id), 256); - $status = 'Status for message id '.$msg_id.': '; - /*if ($info & 4) { - *$this->gotResponseForOutgoingMessageId($msg_id); - *} - */ - foreach (MTProto::MSGS_INFO_FLAGS as $flag => $description) { - if (($info & $flag) !== 0) { - $status .= $description; - } - } - $this->logger->logger($status, \danog\MadelineProto\Logger::NOTICE); - } + $this->onMsgsAllInfo($message->read()); break; case 'msg_detailed_info': - $this->checkInSeqNo($current_msg_id); - unset($this->new_incoming[$current_msg_id]); - $only_updates = false; - if (isset($this->outgoing_messages[$this->incoming_messages[$current_msg_id]['content']['msg_id']])) { - if (isset($this->incoming_messages[$this->incoming_messages[$current_msg_id]['content']['answer_msg_id']])) { - $this->handleResponse($this->incoming_messages[$current_msg_id]['content']['msg_id'], $this->incoming_messages[$current_msg_id]['content']['answer_msg_id']); - } else { - \danog\MadelineProto\Tools::callFork($this->objectCall('msg_resend_req', ['msg_ids' => [$this->incoming_messages[$current_msg_id]['content']['answer_msg_id']]], ['postpone' => true])); - } - } + $this->onMsgDetailedInfo($message->read()); break; case 'msg_new_detailed_info': - $this->checkInSeqNo($current_msg_id); - $only_updates = false; - unset($this->new_incoming[$current_msg_id]); - if (isset($this->incoming_messages[$this->incoming_messages[$current_msg_id]['content']['answer_msg_id']])) { - $this->ackIncomingMessageId($this->incoming_messages[$current_msg_id]['content']['answer_msg_id']); - } else { - \danog\MadelineProto\Tools::callFork($this->objectCall('msg_resend_req', ['msg_ids' => [$this->incoming_messages[$current_msg_id]['content']['answer_msg_id']]], ['postpone' => true])); - } + $this->onNewMsgDetailedInfo($message->read()); break; case 'msg_resend_req': - $this->checkInSeqNo($current_msg_id); - $only_updates = false; - unset($this->new_incoming[$current_msg_id]); - $ok = true; - foreach ($this->incoming_messages[$current_msg_id]['content']['msg_ids'] as $msg_id) { - if (!isset($this->outgoing_messages[$msg_id]) || isset($this->incoming_messages[$msg_id])) { - $ok = false; - } - } - if ($ok) { - foreach ($this->incoming_messages[$current_msg_id]['content']['msg_ids'] as $msg_id) { - $this->methodRecall('', ['message_id' => $msg_id, 'postpone' => true]); - } - } else { - \danog\MadelineProto\Tools::callFork($this->sendMsgsStateInfo($current_msg_id, $this->incoming_messages[$current_msg_id]['content']['msg_ids'])); - } + $this->onMsgResendReq($message->read(), $current_msg_id); break; case 'msg_resend_ans_req': - $this->checkInSeqNo($current_msg_id); - $only_updates = false; - unset($this->new_incoming[$current_msg_id]); - \danog\MadelineProto\Tools::callFork($this->sendMsgsStateInfo($current_msg_id, $this->incoming_messages[$current_msg_id]['content']['msg_ids'])); - foreach ($this->incoming_messages[$current_msg_id]['content']['msg_ids'] as $msg_id) { - if (isset($this->incoming_messages[$msg_id]['response']) && isset($this->outgoing_messages[$this->incoming_messages[$msg_id]['response']])) { - \danog\MadelineProto\Tools::callFork($this->objectCall($this->outgoing_messages[$this->incoming_messages[$msg_id]['response']]['_'], $this->outgoing_messages[$this->incoming_messages[$msg_id]['response']]['body'], ['postpone' => true])); - } - } + $this->onMsgResendAnsReq($message->read(), $current_msg_id); break; default: - $this->checkInSeqNo($current_msg_id); - $this->ackIncomingMessageId($current_msg_id); - // Acknowledge that I received the server's response - $response_type = $this->API->getTL()->getConstructors()->findByPredicate($this->incoming_messages[$current_msg_id]['content']['_'])['type']; - switch ($response_type) { - case 'Updates': - unset($this->new_incoming[$current_msg_id]); - if (!$this->isCdn()) { - \danog\MadelineProto\Tools::callForkDefer($this->API->handleUpdates($this->incoming_messages[$current_msg_id]['content'])); - } - unset($this->incoming_messages[$current_msg_id]['content']); - $only_updates = true && $only_updates; - break; - default: - $only_updates = false; - $this->logger->logger('Trying to assign a response of type '.$response_type.' to its request...', \danog\MadelineProto\Logger::VERBOSE); - foreach ($this->new_outgoing as $key => $expecting_msg_id) { - $expecting = $this->outgoing_messages[$expecting_msg_id]; - if (!isset($expecting['type'])) { - continue; - } - $this->logger->logger('Does the request of return type '.$expecting['type'].' match?', \danog\MadelineProto\Logger::VERBOSE); - if ($response_type === $expecting['type']) { - $this->logger->logger('Yes', \danog\MadelineProto\Logger::VERBOSE); - unset($this->new_incoming[$current_msg_id]); - $this->handleResponse($expecting_msg_id, $current_msg_id); - break 2; - } - $this->logger->logger('No', \danog\MadelineProto\Logger::VERBOSE); - } - $this->logger->logger('Dunno how to handle '.PHP_EOL.\var_export($this->incoming_messages[$current_msg_id]['content'], true), \danog\MadelineProto\Logger::FATAL_ERROR); - unset($this->new_incoming[$current_msg_id]); - break; + $this->ackIncomingMessage($message); + $response_type = $this->API->getTL()->getConstructors()->findByPredicate($message->getContent()['_'])['type']; + if ($response_type == 'Updates') { + if (!$this->isCdn()) { + Tools::callForkDefer($this->API->handleUpdates($message->read())); + } + break; } + + $this->logger->logger('Trying to assign a response of type '.$response_type.' to its request...', Logger::VERBOSE); + foreach ($this->new_outgoing as $expecting_msg_id => $expecting) { + if (!$type = $expecting->getType()) { + continue; + } + $this->logger->logger("Does the request of return type $type match?", Logger::VERBOSE); + if ($response_type === $type) { + $this->logger->logger('Yes', Logger::VERBOSE); + $this->handleResponse($message, $expecting_msg_id); + break 2; + } + $this->logger->logger('No', Logger::VERBOSE); + } + $this->logger->logger('Dunno how to handle '.PHP_EOL.\var_export($message->read(), true), Logger::FATAL_ERROR); break; } } - if ($this->pending_outgoing) { + if ($this->pendingOutgoing) { $this->writer->resume(); } - return $only_updates; } + public function handleReject(OutgoingMessage $message, \Throwable $data): void + { + $this->gotResponseForOutgoingMessage($message); + Loop::defer(fn () => $message->reply(new Failure($data))); + } + /** - * Reject request with exception. + * Handle RPC response. * - * @param array $request Request - * @param \Throwable $data Exception + * @param IncomingMessage $message Incoming message + * @param string $requestId Request ID * * @return void */ - public function handleReject(&$request, \Throwable $data) + private function handleResponse(IncomingMessage $message, $requestId = null): void { - if (isset($request['promise']) && \is_object($request['promise'])) { - Loop::defer(function () use (&$request, $data) { - if (isset($request['promise'])) { - $this->logger->logger('Rejecting: '.(isset($request['_']) ? $request['_'] : '-')); - $this->logger->logger("Rejecting: {$data}"); - $promise = $request['promise']; - unset($request['promise']); - try { - $promise->fail($data); - } catch (\Error $e) { - if (\strpos($e->getMessage(), "Promise has already been resolved") !== 0) { - throw $e; - } - $this->logger->logger("Got promise already resolved error", \danog\MadelineProto\Logger::FATAL_ERROR); - } - } else { - $this->logger->logger('Rejecting: already got response for '.(isset($request['_']) ? $request['_'] : '-')); - $this->logger->logger("Rejecting: {$data}"); - } - }); - } elseif (isset($request['container'])) { - foreach ($request['container'] as $message_id) { - $this->handleReject($this->outgoing_messages[$message_id], $data); - } - } else { - $this->logger->logger('Rejecting: already got response for '.(isset($request['_']) ? $request['_'] : '-')); - $this->logger->logger("Rejecting: {$data}"); - } - } - /** - * @return void - */ - public function handleResponse($request_id, $response_id) - { - $response =& $this->incoming_messages[$response_id]['content']; - unset($this->incoming_messages[$response_id]['content']); - $request =& $this->outgoing_messages[$request_id]; - if (isset($response['_'])) { - switch ($response['_']) { - case 'rpc_error': - if (($request['method'] ?? false) && $request['_'] !== 'auth.bindTempAuthKey' && $this->shared->hasTempAuthKey() && !$this->shared->getTempAuthKey()->isInited()) { - $this->shared->getTempAuthKey()->init(true); - } - if (\in_array($response['error_message'], ['PERSISTENT_TIMESTAMP_EMPTY', 'PERSISTENT_TIMESTAMP_INVALID'])) { - $this->gotResponseForOutgoingMessageId($request_id); - $this->handleReject($request, new \danog\MadelineProto\PTSException($response['error_message'])); - return; - } - if ($response['error_message'] === 'PERSISTENT_TIMESTAMP_OUTDATED') { - $response['error_code'] = 500; - } - if (\strpos($response['error_message'], 'FILE_REFERENCE_') === 0) { - $this->logger->logger("Got {$response['error_message']}, refreshing file reference and repeating method call..."); - $request['refreshReferences'] = true; - if (isset($request['serialized_body'])) { - unset($request['serialized_body']); - } - $this->methodRecall('', ['message_id' => $request_id, 'postpone' => true]); - return; - } - switch ($response['error_code']) { - case 500: - case -500: - if ($response['error_message'] === 'MSG_WAIT_FAILED') { - $this->call_queue[$request['queue']] = []; - $this->methodRecall('', ['message_id' => $request_id, 'postpone' => true]); - return; - } - if (\in_array($response['error_message'], ['MSGID_DECREASE_RETRY', 'HISTORY_GET_FAILED', 'RPC_CONNECT_FAILED', 'RPC_CALL_FAIL', 'PERSISTENT_TIMESTAMP_OUTDATED', 'RPC_MCGET_FAIL', 'no workers running', 'No workers running'])) { - Loop::delay(1 * 1000, [$this, 'methodRecall'], ['message_id' => $request_id]); - return; - } - $this->gotResponseForOutgoingMessageId($request_id); - $this->handleReject($request, new \danog\MadelineProto\RPCErrorException($response['error_message'], $response['error_code'], $request['_'] ?? '')); - return; - case 303: - $this->API->datacenter->curdc = $datacenter = (int) \preg_replace('/[^0-9]+/', '', $response['error_message']); - if (isset($request['file']) && $request['file'] && $this->API->datacenter->has($datacenter.'_media')) { - $datacenter .= '_media'; - } - if (isset($request['user_related']) && $request['user_related']) { - $this->API->settings->setDefaultDc($this->API->authorized_dc = $this->API->datacenter->curdc); - } - Loop::defer([$this, 'methodRecall'], ['message_id' => $request_id, 'datacenter' => $datacenter]); - //$this->API->methodRecall('', ['message_id' => $request_id, 'datacenter' => $datacenter, 'postpone' => true]); - return; - case 401: - switch ($response['error_message']) { - case 'USER_DEACTIVATED': - case 'USER_DEACTIVATED_BAN': - case 'SESSION_REVOKED': - case 'SESSION_EXPIRED': - $this->gotResponseForOutgoingMessageId($request_id); - $this->logger->logger($response['error_message'], \danog\MadelineProto\Logger::FATAL_ERROR); - foreach ($this->API->datacenter->getDataCenterConnections() as $socket) { - $socket->setTempAuthKey(null); - $socket->setPermAuthKey(null); - $socket->resetSession(); - } - if (\in_array($response['error_message'], ['USER_DEACTIVATED', 'USER_DEACTIVATED_BAN'], true)) { - $this->logger->logger('!!!!!!! WARNING !!!!!!!', \danog\MadelineProto\Logger::FATAL_ERROR); - $this->logger->logger("Telegram's flood prevention system suspended this account.", \danog\MadelineProto\Logger::ERROR); - $this->logger->logger('To continue, manual verification is required.', \danog\MadelineProto\Logger::FATAL_ERROR); - $phone = isset($this->API->authorization['user']['phone']) ? '+'.$this->API->authorization['user']['phone'] : 'you are currently using'; - $this->logger->logger('Send an email to recover@telegram.org, asking to unban the phone number '.$phone.', and shortly describe what will you do with this phone number.', \danog\MadelineProto\Logger::FATAL_ERROR); - $this->logger->logger('Then login again.', \danog\MadelineProto\Logger::FATAL_ERROR); - $this->logger->logger('If you intentionally deleted this account, ignore this message.', \danog\MadelineProto\Logger::FATAL_ERROR); - } - $this->API->resetSession(); - \danog\MadelineProto\Tools::callFork((function () use ($request, &$response): \Generator { - yield from $this->API->initAuthorization(); - $this->handleReject($request, new \danog\MadelineProto\RPCErrorException($response['error_message'], $response['error_code'], $request['_'] ?? '')); - })()); - return; - case 'AUTH_KEY_UNREGISTERED': - case 'AUTH_KEY_INVALID': - if ($this->API->authorized !== MTProto::LOGGED_IN) { - $this->gotResponseForOutgoingMessageId($request_id); - \danog\MadelineProto\Tools::callFork((function () use ($request, &$response): \Generator { - yield from $this->API->initAuthorization(); - $this->handleReject($request, new \danog\MadelineProto\RPCErrorException($response['error_message'], $response['error_code'], $request['_'] ?? '')); - })()); - return; - } - $this->session_id = null; - $this->shared->setTempAuthKey(null); - $this->shared->setPermAuthKey(null); - $this->logger->logger('Auth key not registered, resetting temporary and permanent auth keys...', \danog\MadelineProto\Logger::ERROR); - if ($this->API->authorized_dc === $this->datacenter && $this->API->authorized === MTProto::LOGGED_IN) { - $this->gotResponseForOutgoingMessageId($request_id); - $this->logger->logger('Permanent auth key was main authorized key, logging out...', \danog\MadelineProto\Logger::FATAL_ERROR); - foreach ($this->API->datacenter->getDataCenterConnections() as $socket) { - $socket->setTempAuthKey(null); - $socket->setPermAuthKey(null); - } - $this->logger->logger('!!!!!!! WARNING !!!!!!!', \danog\MadelineProto\Logger::FATAL_ERROR); - $this->logger->logger("Telegram's flood prevention system suspended this account.", \danog\MadelineProto\Logger::ERROR); - $this->logger->logger('To continue, manual verification is required.', \danog\MadelineProto\Logger::FATAL_ERROR); - $phone = isset($this->API->authorization['user']['phone']) ? '+'.$this->API->authorization['user']['phone'] : 'you are currently using'; - $this->logger->logger('Send an email to recover@telegram.org, asking to unban the phone number '.$phone.', and quickly describe what will you do with this phone number.', \danog\MadelineProto\Logger::FATAL_ERROR); - $this->logger->logger('Then login again.', \danog\MadelineProto\Logger::FATAL_ERROR); - $this->logger->logger('If you intentionally deleted this account, ignore this message.', \danog\MadelineProto\Logger::FATAL_ERROR); - $this->API->resetSession(); - \danog\MadelineProto\Tools::callFork((function () use ($request, &$response): \Generator { - yield from $this->API->initAuthorization(); - $this->handleReject($request, new \danog\MadelineProto\RPCErrorException($response['error_message'], $response['error_code'], $request['_'] ?? '')); - })()); - return; - } - \danog\MadelineProto\Tools::callFork((function () use ($request_id): \Generator { - yield from $this->API->initAuthorization(); - $this->methodRecall('', ['message_id' => $request_id]); - })()); - return; - case 'AUTH_KEY_PERM_EMPTY': - $this->logger->logger('Temporary auth key not bound, resetting temporary auth key...', \danog\MadelineProto\Logger::ERROR); - $this->shared->setTempAuthKey(null); - \danog\MadelineProto\Tools::callFork((function () use ($request_id): \Generator { - yield from $this->API->initAuthorization(); - $this->methodRecall('', ['message_id' => $request_id]); - })()); - return; - } - $this->gotResponseForOutgoingMessageId($request_id); - $this->handleReject($request, new \danog\MadelineProto\RPCErrorException($response['error_message'], $response['error_code'], $request['_'] ?? '')); - return; - case 420: - $seconds = \preg_replace('/[^0-9]+/', '', $response['error_message']); - $limit = $request['FloodWaitLimit'] ?? $this->API->settings->getRPC()->getFloodTimeout(); - if (\is_numeric($seconds) && $seconds < $limit) { - //$this->gotResponseForOutgoingMessageId($request_id); - $this->logger->logger('Flood, waiting '.$seconds.' seconds before repeating async call of '.($request['_'] ?? '').'...', \danog\MadelineProto\Logger::NOTICE); - $request['sent'] = ($request['sent'] ?? \time()) + $seconds; - Loop::delay($seconds * 1000, [$this, 'methodRecall'], ['message_id' => $request_id]); - return; - } - // no break - default: - $this->gotResponseForOutgoingMessageId($request_id); - $this->handleReject($request, new \danog\MadelineProto\RPCErrorException($response['error_message'], $response['error_code'], $request['_'] ?? '')); - return; - } - return; - case 'boolTrue': - case 'boolFalse': - $response = $response['_'] === 'boolTrue'; - break; - case 'bad_server_salt': - case 'bad_msg_notification': - $this->logger->logger('Received bad_msg_notification: '.MTProto::BAD_MSG_ERROR_CODES[$response['error_code']], \danog\MadelineProto\Logger::WARNING); - switch ($response['error_code']) { - case 48: - $this->shared->getTempAuthKey()->setServerSalt($response['new_server_salt']); - $this->methodRecall('', ['message_id' => $request_id, 'postpone' => true]); - return; - case 20: - $this->methodRecall('', ['message_id' => $request_id, 'postpone' => true]); - return; - case 16: - case 17: - $this->time_delta = (int) (new \tgseclib\Math\BigInteger(\strrev($response_id), 256))->bitwise_rightShift(32)->subtract(new \tgseclib\Math\BigInteger(\time()))->toString(); - $this->logger->logger('Set time delta to '.$this->time_delta, \danog\MadelineProto\Logger::WARNING); - $this->API->resetMTProtoSession(); - $this->shared->setTempAuthKey(null); - \danog\MadelineProto\Tools::callFork((function () use ($request_id): \Generator { - yield from $this->API->initAuthorization(); - $this->methodRecall('', ['message_id' => $request_id]); - })()); - return; - } - $this->gotResponseForOutgoingMessageId($request_id); - $this->handleReject($request, new \danog\MadelineProto\RPCErrorException('Received bad_msg_notification: '.MTProto::BAD_MSG_ERROR_CODES[$response['error_code']], $response['error_code'], $request['_'] ?? '')); - return; - } - } - if (($request['method'] ?? false) && $request['_'] !== 'auth.bindTempAuthKey' && $this->shared->hasTempAuthKey() && !$this->shared->getTempAuthKey()->isInited()) { - $this->shared->getTempAuthKey()->init(true); - } - if (!isset($request['promise'])) { - $this->gotResponseForOutgoingMessageId($request_id); - $this->logger->logger('Response: already got response for '.(isset($request['_']) ? $request['_'] : '-').' with message ID '.$request_id); + $requestId ??= $message->getRequestId(); + $response = $message->read(); + if (!isset($this->outgoing_messages[$requestId])) { + $requestId = MsgIdHandler::toString($requestId); + $this->logger->logger("Got a reponse $message with message ID $requestId, but there is no request!", Logger::FATAL_ERROR); return; } - $botAPI = isset($request['botAPI']) && $request['botAPI']; - if (isset($response['_']) && !$this->isCdn() && $this->API->getTL()->getConstructors()->findByPredicate($response['_'])['type'] === 'Updates') { - $body = []; - if (isset($request['body']['peer'])) { - $body['peer'] = \is_string($request['body']['peer']) ? $request['body']['peer'] : $this->API->getId($request['body']['peer']); - } - if (isset($request['body']['message'])) { - $body['message'] = (string) $request['body']['message']; - } - $response['request'] = ['_' => $request['_'], 'body' => $body]; - \danog\MadelineProto\Tools::callForkDefer($this->API->handleUpdates($response)); + /** @var OutgoingMessage */ + $request = $this->outgoing_messages[$requestId]; + if ($request->getState() & OutgoingMessage::STATE_REPLIED) { + $this->logger->logger("Already got a reponse to $request, but there is another reply $message with message ID $requestId!", Logger::FATAL_ERROR); + return; } - unset($request); - $this->gotResponseForOutgoingMessageId($request_id); - $r = isset($response['_']) ? $response['_'] : \json_encode($response); + if ($response['_'] === 'rpc_result') { + $response = $response['result']; + } + $constructor = $response['_'] ?? ''; + if ($constructor === 'rpc_error') { + try { + $exception = $this->handleRpcError($request, $response); + } catch (\Throwable $e) { + $exception = $e; + } + if ($exception) { + $this->handleReject($request, $exception); + } + return; + } + if ($constructor === 'bad_server_salt' || $constructor === 'bad_msg_notification') { + $this->logger->logger('Received bad_msg_notification: '.MTProto::BAD_MSG_ERROR_CODES[$response['error_code']], Logger::WARNING); + switch ($response['error_code']) { + case 48: + $this->shared->getTempAuthKey()->setServerSalt($response['new_server_salt']); + $this->methodRecall('', ['message_id' => $requestId, 'postpone' => true]); + return; + case 20: + $this->methodRecall('', ['message_id' => $requestId, 'postpone' => true]); + return; + case 16: + case 17: + $this->time_delta = (int) (new \tgseclib\Math\BigInteger(\strrev($message->getMsgId()), 256))->bitwise_rightShift(32)->subtract(new \tgseclib\Math\BigInteger(\time()))->toString(); + $this->logger->logger('Set time delta to '.$this->time_delta, Logger::WARNING); + $this->API->resetMTProtoSession(); + $this->shared->setTempAuthKey(null); + Tools::callFork((function () use ($requestId): \Generator { + yield from $this->API->initAuthorization(); + $this->methodRecall('', ['message_id' => $requestId]); + })()); + return; + } + $this->handleReject($request, new \danog\MadelineProto\RPCErrorException('Received bad_msg_notification: '.MTProto::BAD_MSG_ERROR_CODES[$response['error_code']], $response['error_code'], $request->getConstructor())); + return; + } + + if ($request->isMethod() && $request->getConstructor() !== 'auth.bindTempAuthKey' && $this->shared->hasTempAuthKey() && !$this->shared->getTempAuthKey()->isInited()) { + $this->shared->getTempAuthKey()->init(true); + } + $botAPI = $request->getBotAPI(); + if (isset($response['_']) && !$this->isCdn() && $this->API->getTL()->getConstructors()->findByPredicate($response['_'])['type'] === 'Updates') { + $body = $request->getBodyOrEmpty(); + $trimmed = []; + if (isset($body['peer'])) { + $trimmed['peer'] = \is_string($body['peer']) ? $body['peer'] : $this->API->getId($body['peer']); + } + if (isset($body['message'])) { + $trimmed['message'] = (string) $body['message']; + } + $response['request'] = ['_' => $request->getConstructor(), 'body' => $trimmed]; + unset($body); + Tools::callForkDefer($this->API->handleUpdates($response)); + } + $this->gotResponseForOutgoingMessage($request); + + $r = $response['_'] ?? \json_encode($response); $this->logger->logger("Defer sending {$r} to deferred", Logger::ULTRA_VERBOSE); - \danog\MadelineProto\Tools::callFork((function () use ($request_id, $response, $botAPI): \Generator { - $r = isset($response['_']) ? $response['_'] : \json_encode($response); - $this->logger->logger("Deferred: sent {$r} to deferred", Logger::ULTRA_VERBOSE); - if ($botAPI) { - $response = (yield from $this->API->MTProtoToBotAPI($response)); - } - if (isset($this->outgoing_messages[$request_id]['promise'])) { - // This should not happen but happens, should debug - $promise = $this->outgoing_messages[$request_id]['promise']; - unset($this->outgoing_messages[$request_id]['promise']); - try { - $promise->resolve($response); - } catch (\Error $e) { - if (\strpos($e->getMessage(), "Promise has already been resolved") !== 0) { - throw $e; - } - $this->logger->logger("Got promise already resolved error", \danog\MadelineProto\Logger::FATAL_ERROR); + + if (!$botAPI) { + Tools::callForkDefer((function () use ($request, $message, $response): \Generator { + yield from $message->yieldSideEffects(); + $request->reply($response); + })()); + } else { + Tools::callForkDefer((function () use ($request, $message, $response): \Generator { + yield from $message->yieldSideEffects(); + $response = yield from $this->API->MTProtoToBotAPI($response); + $request->reply($response); + })()); + } + } + public function handleRpcError(OutgoingMessage $request, array $response): ?\Throwable + { + if ($request->isMethod() && $request->getConstructor() !== 'auth.bindTempAuthKey' && $this->shared->hasTempAuthKey() && !$this->shared->getTempAuthKey()->isInited()) { + $this->shared->getTempAuthKey()->init(true); + } + if (\in_array($response['error_message'], ['PERSISTENT_TIMESTAMP_EMPTY', 'PERSISTENT_TIMESTAMP_INVALID'])) { + return new \danog\MadelineProto\PTSException($response['error_message']); + } + if ($response['error_message'] === 'PERSISTENT_TIMESTAMP_OUTDATED') { + $response['error_code'] = 500; + } + if (\strpos($response['error_message'], 'FILE_REFERENCE_') === 0) { + $this->logger->logger("Got {$response['error_message']}, refreshing file reference and repeating method call..."); + $request->setRefreshReferences(true); + $this->methodRecall('', ['message_id' => $request->getMsgId(), 'postpone' => true]); + return null; + } + + switch ($response['error_code']) { + case 500: + case -500: + if ($response['error_message'] === 'MSG_WAIT_FAILED') { + $this->call_queue[$request->getQueueId()] = []; + $this->methodRecall('', ['message_id' => $request->getMsgId(), 'postpone' => true]); + return null; } - } - })()); + if (\in_array($response['error_message'], ['MSGID_DECREASE_RETRY', 'HISTORY_GET_FAILED', 'RPC_CONNECT_FAILED', 'RPC_CALL_FAIL', 'PERSISTENT_TIMESTAMP_OUTDATED', 'RPC_MCGET_FAIL', 'no workers running', 'No workers running'])) { + Loop::delay(1 * 1000, [$this, 'methodRecall'], ['message_id' => $request->getMsgId()]); + return null; + } + return new \danog\MadelineProto\RPCErrorException($response['error_message'], $response['error_code'], $request->getConstructor()); + case 303: + $this->API->datacenter->curdc = $datacenter = (int) \preg_replace('/[^0-9]+/', '', $response['error_message']); + if ($request->isFileRelated() && $this->API->datacenter->has($datacenter.'_media')) { + $datacenter .= '_media'; + } + if ($request->isUserRelated()) { + $this->API->settings->setDefaultDc($this->API->authorized_dc = $this->API->datacenter->curdc); + } + Loop::defer([$this, 'methodRecall'], ['message_id' => $request->getMsgId(), 'datacenter' => $datacenter]); + //$this->API->methodRecall('', ['message_id' => $requestId, 'datacenter' => $datacenter, 'postpone' => true]); + return null; + case 401: + switch ($response['error_message']) { + case 'USER_DEACTIVATED': + case 'USER_DEACTIVATED_BAN': + case 'SESSION_REVOKED': + case 'SESSION_EXPIRED': + $this->logger->logger($response['error_message'], Logger::FATAL_ERROR); + foreach ($this->API->datacenter->getDataCenterConnections() as $socket) { + $socket->setTempAuthKey(null); + $socket->setPermAuthKey(null); + $socket->resetSession(); + } + if (\in_array($response['error_message'], ['USER_DEACTIVATED', 'USER_DEACTIVATED_BAN'], true)) { + $this->logger->logger('!!!!!!! WARNING !!!!!!!', Logger::FATAL_ERROR); + $this->logger->logger("Telegram's flood prevention system suspended this account.", Logger::ERROR); + $this->logger->logger('To continue, manual verification is required.', Logger::FATAL_ERROR); + $phone = isset($this->API->authorization['user']['phone']) ? '+'.$this->API->authorization['user']['phone'] : 'you are currently using'; + $this->logger->logger('Send an email to recover@telegram.org, asking to unban the phone number '.$phone.', and shortly describe what will you do with this phone number.', Logger::FATAL_ERROR); + $this->logger->logger('Then login again.', Logger::FATAL_ERROR); + $this->logger->logger('If you intentionally deleted this account, ignore this message.', Logger::FATAL_ERROR); + } + $this->API->resetSession(); + $this->gotResponseForOutgoingMessage($request); + Tools::callFork((function () use ($request, $response): \Generator { + yield from $this->API->initAuthorization(); + $this->handleReject($request, new \danog\MadelineProto\RPCErrorException($response['error_message'], $response['error_code'], $request->getConstructor())); + })()); + return null; + case 'AUTH_KEY_UNREGISTERED': + case 'AUTH_KEY_INVALID': + if ($this->API->authorized !== MTProto::LOGGED_IN) { + $this->gotResponseForOutgoingMessage($request); + Tools::callFork((function () use ($request, $response): \Generator { + yield from $this->API->initAuthorization(); + $this->handleReject($request, new \danog\MadelineProto\RPCErrorException($response['error_message'], $response['error_code'], $request->getConstructor())); + })()); + return null; + } + $this->session_id = null; + $this->shared->setTempAuthKey(null); + $this->shared->setPermAuthKey(null); + $this->logger->logger('Auth key not registered, resetting temporary and permanent auth keys...', Logger::ERROR); + if ($this->API->authorized_dc === $this->datacenter && $this->API->authorized === MTProto::LOGGED_IN) { + $this->logger->logger('Permanent auth key was main authorized key, logging out...', Logger::FATAL_ERROR); + foreach ($this->API->datacenter->getDataCenterConnections() as $socket) { + $socket->setTempAuthKey(null); + $socket->setPermAuthKey(null); + } + $this->logger->logger('!!!!!!! WARNING !!!!!!!', Logger::FATAL_ERROR); + $this->logger->logger("Telegram's flood prevention system suspended this account.", Logger::ERROR); + $this->logger->logger('To continue, manual verification is required.', Logger::FATAL_ERROR); + $phone = isset($this->API->authorization['user']['phone']) ? '+'.$this->API->authorization['user']['phone'] : 'you are currently using'; + $this->logger->logger('Send an email to recover@telegram.org, asking to unban the phone number '.$phone.', and quickly describe what will you do with this phone number.', Logger::FATAL_ERROR); + $this->logger->logger('Then login again.', Logger::FATAL_ERROR); + $this->logger->logger('If you intentionally deleted this account, ignore this message.', Logger::FATAL_ERROR); + $this->API->resetSession(); + $this->gotResponseForOutgoingMessage($request); + Tools::callFork((function () use ($request, &$response): \Generator { + yield from $this->API->initAuthorization(); + $this->handleReject($request, new \danog\MadelineProto\RPCErrorException($response['error_message'], $response['error_code'], $request->getConstructor())); + })()); + return null; + } + Tools::callFork((function () use ($request): \Generator { + yield from $this->API->initAuthorization(); + $this->methodRecall('', ['message_id' => $request->getMsgId()]); + })()); + return null; + case 'AUTH_KEY_PERM_EMPTY': + $this->logger->logger('Temporary auth key not bound, resetting temporary auth key...', Logger::ERROR); + $this->shared->setTempAuthKey(null); + Tools::callFork((function () use ($request): \Generator { + yield from $this->API->initAuthorization(); + $this->methodRecall('', ['message_id' => $request->getMsgId()]); + })()); + return null; + } + return new \danog\MadelineProto\RPCErrorException($response['error_message'], $response['error_code'], $request->getConstructor()); + case 420: + $seconds = \preg_replace('/[^0-9]+/', '', $response['error_message']); + $limit = $request->getFloodWaitLimit() ?? $this->API->settings->getRPC()->getFloodTimeout(); + if (\is_numeric($seconds) && $seconds < $limit) { + $this->logger->logger("Flood, waiting '.$seconds.' seconds before repeating async call of $request...", Logger::NOTICE); + $request->setSent(($request->getSent() ?? \time()) + $seconds); + Loop::delay($seconds * 1000, [$this, 'methodRecall'], ['message_id' => $request->getMsgId()]); + return null; + } + // no break + default: + return new \danog\MadelineProto\RPCErrorException($response['error_message'], $response['error_code'], $request->getConstructor()); + } } } diff --git a/src/danog/MadelineProto/MTProtoSession/SeqNoHandler.php b/src/danog/MadelineProto/MTProtoSession/SeqNoHandler.php index f929160c..21021bf2 100644 --- a/src/danog/MadelineProto/MTProtoSession/SeqNoHandler.php +++ b/src/danog/MadelineProto/MTProtoSession/SeqNoHandler.php @@ -19,6 +19,8 @@ namespace danog\MadelineProto\MTProtoSession; +use danog\MadelineProto\MTProto\IncomingMessage; + /** * Manages sequence number. */ @@ -35,11 +37,16 @@ trait SeqNoHandler //$this->API->logger->logger("OUT: $value + $in = ".$this->session_out_seq_no); return $value * 2 + $in; } - public function checkInSeqNo($current_msg_id): void + public function checkInSeqNo(IncomingMessage $message): void { - $type = isset($this->incoming_messages[$current_msg_id]['content']['_']) ? $this->incoming_messages[$current_msg_id]['content']['_'] : '-'; - if (isset($this->incoming_messages[$current_msg_id]['seq_no']) && ($seq_no = $this->generateInSeqNo($this->contentRelated($this->incoming_messages[$current_msg_id]['content']))) !== $this->incoming_messages[$current_msg_id]['seq_no']) { - $this->API->logger->logger('SECURITY WARNING: Seqno mismatch (should be '.$seq_no.', is '.$this->incoming_messages[$current_msg_id]['seq_no'].', '.$type.')', \danog\MadelineProto\Logger::ULTRA_VERBOSE); + if ($message->hasSeqNo()) { + $seq_no = $this->generateInSeqNo($message->isContentRelated()); + if ($seq_no !== $message->getSeqNo()) { + if ($message->isContentRelated()) { + $this->session_in_seq_no -= 1; + } + $this->API->logger->logger('SECURITY WARNING: Seqno mismatch (should be '.$seq_no.', is '.$message->getSeqNo().", $message)", \danog\MadelineProto\Logger::ULTRA_VERBOSE); + } } } public function generateInSeqNo($contentRelated) diff --git a/src/danog/MadelineProto/MTProtoSession/Session.php b/src/danog/MadelineProto/MTProtoSession/Session.php index 3698f539..283618a5 100644 --- a/src/danog/MadelineProto/MTProtoSession/Session.php +++ b/src/danog/MadelineProto/MTProtoSession/Session.php @@ -21,6 +21,8 @@ namespace danog\MadelineProto\MTProtoSession; use danog\MadelineProto\Connection; use danog\MadelineProto\MTProto; +use danog\MadelineProto\MTProto\IncomingMessage; +use danog\MadelineProto\MTProto\OutgoingMessage; /** * Manages MTProto session-specific data. @@ -37,39 +39,39 @@ trait Session /** * Incoming message array. * - * @var array + * @var IncomingMessage[] */ public $incoming_messages = []; /** * Outgoing message array. * - * @var array + * @var OutgoingMessage[] */ public $outgoing_messages = []; /** * New incoming message ID array. * - * @var array + * @var IncomingMessage[] */ public $new_incoming = []; /** - * New outgoing message ID array. + * New outgoing message array. * - * @var array + * @var OutgoingMessage[] */ public $new_outgoing = []; /** * Pending outgoing messages. * - * @var array + * @var OutgoingMessage[] */ - public $pending_outgoing = []; + public $pendingOutgoing = []; /** * Pending outgoing key. * * @var string */ - public $pending_outgoing_key = 'a'; + public $pendingOutgoingKey = 'a'; /** * Time delta with server. * @@ -113,16 +115,19 @@ trait Session */ public function resetSession(): void { + $this->API->logger->logger("Resetting session in DC {$this->datacenterId}...", \danog\MadelineProto\Logger::WARNING); $this->session_id = \danog\MadelineProto\Tools::random(8); $this->session_in_seq_no = 0; $this->session_out_seq_no = 0; - $this->msgIdHandler = MsgIdHandler::createInstance($this); + if (!isset($this->msgIdHandler)) { + $this->msgIdHandler = MsgIdHandler::createInstance($this); + } foreach ($this->outgoing_messages as &$msg) { - if (isset($msg['msg_id'])) { - unset($msg['msg_id']); + if ($msg->hasMsgId()) { + $msg->setMsgId(null); } - if (isset($msg['seqno'])) { - unset($msg['seqno']); + if ($msg->hasSeqNo()) { + $msg->setSeqNo(null); } } } @@ -144,10 +149,7 @@ trait Session */ public function backupSession(): array { - $pending = \array_values($this->pending_outgoing); - foreach ($this->new_outgoing as $id) { - $pending[] = $this->outgoing_messages[$id]; - } - return $pending; + $pending = \array_values($this->pendingOutgoing); + return \array_merge($pending, $this->new_outgoing); } } diff --git a/src/danog/MadelineProto/MTProtoTools/AuthKeyHandler.php b/src/danog/MadelineProto/MTProtoTools/AuthKeyHandler.php index 9a5ff1b1..16bd99dd 100644 --- a/src/danog/MadelineProto/MTProtoTools/AuthKeyHandler.php +++ b/src/danog/MadelineProto/MTProtoTools/AuthKeyHandler.php @@ -243,7 +243,7 @@ trait AuthKeyHandler * int $server_time * ] */ - $server_DH_inner_data = yield from $this->TL->deserialize($answer, ['type' => '']); + [$server_DH_inner_data] = $this->TL->deserialize($answer, ['type' => '']); /* * *********************************************************************** * Do some checks @@ -411,7 +411,7 @@ trait AuthKeyHandler */ public function getDhConfig(): \Generator { - $dh_config = yield from $this->methodCallAsyncRead('messages.getDhConfig', ['version' => $this->dh_config['version'], 'random_length' => 0], ['datacenter' => $this->datacenter->curdc]); + $dh_config = yield from $this->methodCallAsyncRead('messages.getDhConfig', ['version' => $this->dh_config['version'], 'random_length' => 0]); if ($dh_config['_'] === 'messages.dhConfigNotModified') { $this->logger->logger('DH configuration not modified', \danog\MadelineProto\Logger::VERBOSE); return $this->dh_config; diff --git a/src/danog/MadelineProto/MTProtoTools/CombinedUpdatesState.php b/src/danog/MadelineProto/MTProtoTools/CombinedUpdatesState.php index 08371e23..ca83754d 100644 --- a/src/danog/MadelineProto/MTProtoTools/CombinedUpdatesState.php +++ b/src/danog/MadelineProto/MTProtoTools/CombinedUpdatesState.php @@ -21,7 +21,7 @@ namespace danog\MadelineProto\MTProtoTools; /** * Stores multiple update states. - * + * * @internal */ class CombinedUpdatesState diff --git a/src/danog/MadelineProto/MTProtoTools/Files.php b/src/danog/MadelineProto/MTProtoTools/Files.php index f4a7d7ef..6512bc00 100644 --- a/src/danog/MadelineProto/MTProtoTools/Files.php +++ b/src/danog/MadelineProto/MTProtoTools/Files.php @@ -1015,12 +1015,12 @@ trait Files $datacenter = $res['dc_id'].'_cdn'; if (!$this->datacenter->has($datacenter)) { $this->config['expires'] = -1; - yield from $this->getConfig([], ['datacenter' => $this->datacenter->curdc]); + yield from $this->getConfig([]); } $this->logger->logger(\danog\MadelineProto\Lang::$current_lang['stored_on_cdn'], \danog\MadelineProto\Logger::NOTICE); } elseif ($res['_'] === 'upload.cdnFileReuploadNeeded') { $this->logger->logger(\danog\MadelineProto\Lang::$current_lang['cdn_reupload'], \danog\MadelineProto\Logger::NOTICE); - yield from $this->getConfig([], ['datacenter' => $this->datacenter->curdc]); + yield from $this->getConfig([]); try { $this->addCdnHashes($messageMedia['file_token'], yield from $this->methodCallAsyncRead('upload.reuploadCdnFile', ['file_token' => $messageMedia['file_token'], 'request_token' => $res['request_token']], ['heavy' => true, 'datacenter' => $old_dc])); } catch (\danog\MadelineProto\RPCErrorException $e) { diff --git a/src/danog/MadelineProto/MTProtoTools/PeerHandler.php b/src/danog/MadelineProto/MTProtoTools/PeerHandler.php index eb708660..57e33bd8 100644 --- a/src/danog/MadelineProto/MTProtoTools/PeerHandler.php +++ b/src/danog/MadelineProto/MTProtoTools/PeerHandler.php @@ -20,6 +20,7 @@ namespace danog\MadelineProto\MTProtoTools; use Amp\Http\Client\Request; +use Amp\Promise; use danog\Decoder\FileId; use danog\Decoder\PhotoSizeSource\PhotoSizeSourceDialogPhoto; use danog\MadelineProto\Db\DbArray; @@ -478,7 +479,13 @@ trait PeerHandler * * @return \Generator Info object * + * @template TConstructor + * @psalm-param $id array{_: TConstructor}|mixed + * + * @return (((mixed|string)[]|mixed|string)[]|int|mixed|string)[] + * * @psalm-return \Generator|array, mixed, array{ + * TConstructor: array * InputPeer: array{_: string, user_id?: mixed, access_hash?: mixed, min?: mixed, chat_id?: mixed, channel_id?: mixed}, * Peer: array{_: string, user_id?: mixed, chat_id?: mixed, channel_id?: mixed}, * DialogPeer: array{_: string, peer: array{_: string, user_id?: mixed, chat_id?: mixed, channel_id?: mixed}}, @@ -529,12 +536,12 @@ trait PeerHandler $this->caching_simple[$id] = true; if ($id < 0) { if ($this->isSupergroup($id)) { - yield from $this->methodCallAsyncRead('channels.getChannels', ['id' => [['access_hash' => 0, 'channel_id' => $this->fromSupergroup($id), '_' => 'inputChannel']]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('channels.getChannels', ['id' => [['access_hash' => 0, 'channel_id' => $this->fromSupergroup($id), '_' => 'inputChannel']]]); } else { - yield from $this->methodCallAsyncRead('messages.getFullChat', ['chat_id' => -$id], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('messages.getFullChat', ['chat_id' => -$id]); } } else { - yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [['access_hash' => 0, 'user_id' => $id, '_' => 'inputUser']]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [['access_hash' => 0, 'user_id' => $id, '_' => 'inputUser']]]); } } catch (\danog\MadelineProto\Exception $e) { $this->logger->logger($e->getMessage(), \danog\MadelineProto\Logger::WARNING); @@ -553,9 +560,9 @@ trait PeerHandler $this->logger->logger("Only have min peer for {$id} in database, trying to fetch full info"); try { if ($id < 0) { - yield from $this->methodCallAsyncRead('channels.getChannels', ['id' => [$this->genAll(yield $this->chats[$id], $folder_id)['InputChannel']]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('channels.getChannels', ['id' => [$this->genAll(yield $this->chats[$id], $folder_id)['InputChannel']]]); } else { - yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [$this->genAll(yield $this->chats[$id], $folder_id)['InputUser']]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [$this->genAll(yield $this->chats[$id], $folder_id)['InputUser']]]); } } catch (\danog\MadelineProto\Exception $e) { $this->logger->logger($e->getMessage(), \danog\MadelineProto\Logger::WARNING); @@ -599,7 +606,7 @@ trait PeerHandler if ($matches[1] === '') { $id = $matches[2]; } else { - $invite = yield from $this->methodCallAsyncRead('messages.checkChatInvite', ['hash' => $matches[2]], ['datacenter' => $this->datacenter->curdc]); + $invite = yield from $this->methodCallAsyncRead('messages.checkChatInvite', ['hash' => $matches[2]]); if (isset($invite['chat'])) { return yield from $this->getInfo($invite['chat']); } @@ -628,9 +635,9 @@ trait PeerHandler $this->logger->logger("Only have min peer for {$bot_api_id} in database, trying to fetch full info"); try { if ($bot_api_id < 0) { - yield from $this->methodCallAsyncRead('channels.getChannels', ['id' => [$this->genAll(yield $this->chats[$bot_api_id], $folder_id)['InputChannel']]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('channels.getChannels', ['id' => [$this->genAll(yield $this->chats[$bot_api_id], $folder_id)['InputChannel']]]); } else { - yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [$this->genAll(yield $this->chats[$bot_api_id], $folder_id)['InputUser']]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [$this->genAll(yield $this->chats[$bot_api_id], $folder_id)['InputUser']]]); } } catch (\danog\MadelineProto\Exception $e) { $this->logger->logger($e->getMessage(), \danog\MadelineProto\Logger::WARNING); @@ -651,9 +658,13 @@ trait PeerHandler throw new \danog\MadelineProto\Exception('This peer is not present in the internal peer database'); } /** + * @template TConstructor + * @psalm-param $constructor array{_: TConstructor} + * * @return (((mixed|string)[]|mixed|string)[]|int|mixed|string)[] * * @psalm-return array{ + * TConstructor: array * InputPeer: array{_: string, user_id?: mixed, access_hash?: mixed, min?: mixed, chat_id?: mixed, channel_id?: mixed}, * Peer: array{_: string, user_id?: mixed, chat_id?: mixed, channel_id?: mixed}, * DialogPeer: array{_: string, peer: array{_: string, user_id?: mixed, chat_id?: mixed, channel_id?: mixed}}, @@ -762,14 +773,14 @@ trait PeerHandler switch ($partial['type']) { case 'user': case 'bot': - $full = yield from $this->methodCallAsyncRead('users.getFullUser', ['id' => $partial['InputUser']], ['datacenter' => $this->datacenter->curdc]); + $full = yield from $this->methodCallAsyncRead('users.getFullUser', ['id' => $partial['InputUser']]); break; case 'chat': - $full = (yield from $this->methodCallAsyncRead('messages.getFullChat', $partial, ['datacenter' => $this->datacenter->curdc]))['full_chat']; + $full = (yield from $this->methodCallAsyncRead('messages.getFullChat', $partial))['full_chat']; break; case 'channel': case 'supergroup': - $full = (yield from $this->methodCallAsyncRead('channels.getFullChannel', ['channel' => $partial['InputChannel']], ['datacenter' => $this->datacenter->curdc]))['full_chat']; + $full = (yield from $this->methodCallAsyncRead('channels.getFullChannel', ['channel' => $partial['InputChannel']]))['full_chat']; break; } $res = []; @@ -786,7 +797,7 @@ trait PeerHandler * * @see https://docs.madelineproto.xyz/Chat.html * - * @return \Generator Chat object + * @return \Generator Chat object */ public function getPwrChat($id, bool $fullfetch = true, bool $send = true): \Generator { @@ -901,16 +912,21 @@ trait PeerHandler if (!isset($res['participants']) && $fullfetch && \in_array($res['type'], ['supergroup', 'channel'])) { $total_count = (isset($res['participants_count']) ? $res['participants_count'] : 0) + (isset($res['admins_count']) ? $res['admins_count'] : 0) + (isset($res['kicked_count']) ? $res['kicked_count'] : 0) + (isset($res['banned_count']) ? $res['banned_count'] : 0); $res['participants'] = []; - $limit = 200; $filters = ['channelParticipantsAdmins', 'channelParticipantsBots']; + $promises = []; foreach ($filters as $filter) { - yield from $this->fetchParticipants($full['InputChannel'], $filter, '', $total_count, $res); + $promises []= $this->fetchParticipants($full['InputChannel'], $filter, '', $total_count, $res); } + yield Tools::all($promises); + $q = ''; $filters = ['channelParticipantsSearch', 'channelParticipantsKicked', 'channelParticipantsBanned']; + $promises = []; foreach ($filters as $filter) { - yield from $this->recurseAlphabetSearchParticipants($full['InputChannel'], $filter, $q, $total_count, $res); + $promises []= $this->recurseAlphabetSearchParticipants($full['InputChannel'], $filter, $q, $total_count, $res, 0); } + yield Tools::all($promises); + $this->logger->logger('Fetched '.\count($res['participants'])." out of {$total_count}"); $res['participants'] = \array_values($res['participants']); } @@ -950,14 +966,32 @@ trait PeerHandler } return $res; } - private function recurseAlphabetSearchParticipants($channel, $filter, $q, $total_count, &$res): \Generator + private function recurseAlphabetSearchParticipants($channel, $filter, $q, $total_count, &$res, int $depth): \Generator { if (!(yield from $this->fetchParticipants($channel, $filter, $q, $total_count, $res))) { - return false; + return []; } + $promises = []; for ($x = 'a'; $x !== 'aa' && $total_count > \count($res['participants']); $x++) { - yield from $this->recurseAlphabetSearchParticipants($channel, $filter, $q.$x, $total_count, $res); + $promises []= $this->recurseAlphabetSearchParticipants($channel, $filter, $q.$x, $total_count, $res, $depth + 1); } + + if ($depth > 2) { + return $promises; + } + + $yielded = yield Tools::all($promises); + while ($yielded) { + $newYielded = []; + + foreach (\array_chunk([...$yielded], 10) as $promises) { + $newYielded = \array_merge(yield Tools::all($promises), $newYielded); + } + + $yielded = $newYielded; + } + + return []; } private function fetchParticipants($channel, $filter, $q, $total_count, &$res): \Generator { @@ -977,9 +1011,9 @@ trait PeerHandler throw $e; } if ($cached = ($gres['_'] === 'channels.channelParticipantsNotModified')) { - $gres = yield from $this->fetchParticipantsCache($channel, $filter, $q, $offset, $limit); + $gres = yield $this->fetchParticipantsCache($channel, $filter, $q, $offset, $limit); } else { - yield from $this->storeParticipantsCache($gres, $channel, $filter, $q, $offset, $limit); + $this->storeParticipantsCache($gres, $channel, $filter, $q, $offset, $limit); } if ($last_count !== -1 && $last_count !== $gres['count']) { $has_more = true; @@ -1037,11 +1071,25 @@ trait PeerHandler } return $has_more; } - private function fetchParticipantsCache($channel, $filter, $q, $offset, $limit): \Generator + /** + * Key for participatns cache. + * + * @param integer $channelId + * @param string $filter + * @param string $q + * @param integer $offset + * @param integer $limit + * @return string + */ + private function participantsKey(int $channelId, string $filter, string $q, int $offset, int $limit): string { - return (yield $this->channel_participants[$channel['channel_id']])[$filter][$q][$offset][$limit]; + return "$channelId'$filter'$q'$offset'$limit"; } - private function storeParticipantsCache($gres, $channel, $filter, $q, $offset, $limit): \Generator + private function fetchParticipantsCache($channel, $filter, $q, $offset, $limit): Promise + { + return $this->channelParticipants[$this->participantsKey($channel['channel_id'], $filter, $q, $offset, $limit)]; + } + private function storeParticipantsCache($gres, $channel, $filter, $q, $offset, $limit) { unset($gres['users']); $ids = []; @@ -1050,13 +1098,11 @@ trait PeerHandler } \sort($ids, SORT_NUMERIC); $gres['hash'] = \danog\MadelineProto\Tools::genVectorHash($ids); - $participant = yield $this->channel_participants[$channel['channel_id']]; - $participant[$filter][$q][$offset][$limit] = $gres; - $this->channel_participants[$channel['channel_id']] = $participant; + $this->channelParticipants[$this->participantsKey($channel['channel_id'], $filter, $q, $offset, $limit)] = $gres; } private function getParticipantsHash($channel, $filter, $q, $offset, $limit): \Generator { - return (yield $this->channel_participants[$channel['channel_id']])[$filter][$q][$offset][$limit]['hash'] ?? 0; + return (yield $this->channelParticipants[$this->participantsKey($channel['channel_id'], $filter, $q, $offset, $limit)])['hash'] ?? 0; } private function storeDb($res, $force = false): \Generator { @@ -1107,7 +1153,7 @@ trait PeerHandler } try { $this->caching_simple_username[$username] = true; - $res = yield from $this->methodCallAsyncRead('contacts.resolveUsername', ['username' => $username], ['datacenter' => $this->datacenter->curdc]); + $res = yield from $this->methodCallAsyncRead('contacts.resolveUsername', ['username' => $username]); } catch (\danog\MadelineProto\RPCErrorException $e) { $this->logger->logger('Username resolution failed with error '.$e->getMessage(), \danog\MadelineProto\Logger::ERROR); if (\strpos($e->rpc, 'FLOOD_WAIT_') === 0 || $e->rpc === 'AUTH_KEY_UNREGISTERED' || $e->rpc === 'USERNAME_INVALID') { diff --git a/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php b/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php index a4fb7b17..f6632113 100644 --- a/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php +++ b/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php @@ -200,7 +200,7 @@ class ReferenceDatabase implements TLCallback throw new \danog\MadelineProto\Exception("Unknown origin type provided: {$type}"); } $originContext = self::CONSTRUCTOR_CONTEXT[$type]; - $this->API->logger->logger("Adding origin context {$originContext} for {$type}!", \danog\MadelineProto\Logger::ULTRA_VERBOSE); + //$this->API->logger->logger("Adding origin context {$originContext} for {$type}!", \danog\MadelineProto\Logger::ULTRA_VERBOSE); $this->cacheContexts[] = $originContext; } public function addOrigin(array $data = []): \Generator @@ -211,7 +211,7 @@ class ReferenceDatabase implements TLCallback } $originType = \array_pop($this->cacheContexts); if (!isset($this->cache[$key])) { - $this->API->logger->logger("Removing origin context {$originType} for {$data['_']}, nothing in the reference cache!", \danog\MadelineProto\Logger::ULTRA_VERBOSE); + //$this->API->logger->logger("Removing origin context {$originType} for {$data['_']}, nothing in the reference cache!", \danog\MadelineProto\Logger::ULTRA_VERBOSE); return; } $cache = $this->cache[$key]; @@ -412,7 +412,7 @@ class ReferenceDatabase implements TLCallback \ksort($locationValue['origins']); $this->db[$location] = $locationValue; $count = 0; - foreach ((yield $this->db[$location]['origins']) as $originType => &$origin) { + foreach ((yield $this->db[$location])['origins'] as $originType => &$origin) { $count++; $this->API->logger->logger("Try {$count} refreshing file reference with origin type {$originType}", \danog\MadelineProto\Logger::VERBOSE); switch ($originType) { @@ -492,7 +492,7 @@ class ReferenceDatabase implements TLCallback } return (yield $this->db[$locationString])['reference']; } - private function serializeLocation(int $locationType, array $location): string + private static function serializeLocation(int $locationType, array $location): string { switch ($locationType) { case self::DOCUMENT_LOCATION: diff --git a/src/danog/MadelineProto/SecretChats/AuthKeyHandler.php b/src/danog/MadelineProto/SecretChats/AuthKeyHandler.php index 50187e3b..bea983b8 100644 --- a/src/danog/MadelineProto/SecretChats/AuthKeyHandler.php +++ b/src/danog/MadelineProto/SecretChats/AuthKeyHandler.php @@ -70,7 +70,7 @@ trait AuthKeyHandler $this->secret_chats[$params['id']] = ['key' => $key, 'admin' => false, 'user_id' => $params['admin_id'], 'InputEncryptedChat' => ['_' => 'inputEncryptedChat', 'chat_id' => $params['id'], 'access_hash' => $params['access_hash']], 'in_seq_no_x' => 1, 'out_seq_no_x' => 0, 'in_seq_no' => 0, 'out_seq_no' => 0, 'layer' => 8, 'ttl' => 0, 'ttr' => 100, 'updated' => \time(), 'incoming' => [], 'outgoing' => [], 'created' => \time(), 'rekeying' => [0], 'key_x' => 'from server', 'mtproto' => 1]; $g_b = $dh_config['g']->powMod($b, $dh_config['p']); Crypt::checkG($g_b, $dh_config['p']); - yield from $this->methodCallAsyncRead('messages.acceptEncryption', ['peer' => $params['id'], 'g_b' => $g_b->toBytes(), 'key_fingerprint' => $key['fingerprint']], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('messages.acceptEncryption', ['peer' => $params['id'], 'g_b' => $g_b->toBytes(), 'key_fingerprint' => $key['fingerprint']]); yield from $this->notifyLayer($params['id']); $this->logger->logger('Secret chat '.$params['id'].' accepted successfully!', \danog\MadelineProto\Logger::NOTICE); } @@ -95,7 +95,7 @@ trait AuthKeyHandler $this->logger->logger('Generating g_a...', \danog\MadelineProto\Logger::VERBOSE); $g_a = $dh_config['g']->powMod($a, $dh_config['p']); Crypt::checkG($g_a, $dh_config['p']); - $res = yield from $this->methodCallAsyncRead('messages.requestEncryption', ['user_id' => $user, 'g_a' => $g_a->toBytes()], ['datacenter' => $this->datacenter->curdc]); + $res = yield from $this->methodCallAsyncRead('messages.requestEncryption', ['user_id' => $user, 'g_a' => $g_a->toBytes()]); $this->temp_requested_secret_chats[$res['id']] = $a; $this->updaters[UpdateLoop::GENERIC]->resume(); $this->logger->logger('Secret chat '.$res['id'].' requested successfully!', \danog\MadelineProto\Logger::NOTICE); @@ -134,7 +134,7 @@ trait AuthKeyHandler } private function notifyLayer($chat): \Generator { - yield from $this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionNotifyLayer', 'layer' => $this->TL->getSecretLayer()]]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionNotifyLayer', 'layer' => $this->TL->getSecretLayer()]]]); } /** * Temporary rekeyed secret chats. @@ -164,7 +164,7 @@ trait AuthKeyHandler $e = \danog\MadelineProto\Tools::random(8); $this->temp_rekeyed_secret_chats[$e] = $a; $this->secret_chats[$chat]['rekeying'] = [1, $e]; - yield from $this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionRequestKey', 'g_a' => $g_a->toBytes(), 'exchange_id' => $e]]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionRequestKey', 'g_a' => $g_a->toBytes(), 'exchange_id' => $e]]]); $this->updaters[UpdateLoop::GENERIC]->resume(); return $e; } @@ -204,7 +204,7 @@ trait AuthKeyHandler $this->secret_chats[$chat]['rekeying'] = [2, $params['exchange_id']]; $g_b = $dh_config['g']->powMod($b, $dh_config['p']); Crypt::checkG($g_b, $dh_config['p']); - yield from $this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionAcceptKey', 'g_b' => $g_b->toBytes(), 'exchange_id' => $params['exchange_id'], 'key_fingerprint' => $key['fingerprint']]]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionAcceptKey', 'g_b' => $g_b->toBytes(), 'exchange_id' => $params['exchange_id'], 'key_fingerprint' => $key['fingerprint']]]]); $this->updaters[UpdateLoop::GENERIC]->resume(); } /** @@ -230,10 +230,10 @@ trait AuthKeyHandler $key['visualization_orig'] = $this->secret_chats[$chat]['key']['visualization_orig']; $key['visualization_46'] = \substr(\hash('sha256', $key['auth_key'], true), 20); if ($key['fingerprint'] !== $params['key_fingerprint']) { - yield from $this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionAbortKey', 'exchange_id' => $params['exchange_id']]]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionAbortKey', 'exchange_id' => $params['exchange_id']]]]); throw new \danog\MadelineProto\SecurityException('Invalid key fingerprint!'); } - yield from $this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionCommitKey', 'exchange_id' => $params['exchange_id'], 'key_fingerprint' => $key['fingerprint']]]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionCommitKey', 'exchange_id' => $params['exchange_id'], 'key_fingerprint' => $key['fingerprint']]]]); unset($this->temp_rekeyed_secret_chats[$params['exchange_id']]); $this->secret_chats[$chat]['rekeying'] = [0]; $this->secret_chats[$chat]['old_key'] = $this->secret_chats[$chat]['key']; @@ -256,7 +256,7 @@ trait AuthKeyHandler return; } if ($this->temp_rekeyed_secret_chats[$params['exchange_id']]['fingerprint'] !== $params['key_fingerprint']) { - yield from $this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionAbortKey', 'exchange_id' => $params['exchange_id']]]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionAbortKey', 'exchange_id' => $params['exchange_id']]]]); throw new \danog\MadelineProto\SecurityException('Invalid key fingerprint!'); } $this->logger->logger('Completing rekeying of secret chat '.$chat.'...', \danog\MadelineProto\Logger::VERBOSE); @@ -266,7 +266,7 @@ trait AuthKeyHandler $this->secret_chats[$chat]['ttr'] = 100; $this->secret_chats[$chat]['updated'] = \time(); unset($this->temp_rekeyed_secret_chats[$params['exchange_id']]); - yield from $this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionNoop']]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionNoop']]]); $this->logger->logger('Secret chat '.$chat.' rekeyed successfully!', \danog\MadelineProto\Logger::VERBOSE); return true; } @@ -326,7 +326,7 @@ trait AuthKeyHandler unset($this->temp_requested_secret_chats[$chat]); } try { - yield from $this->methodCallAsyncRead('messages.discardEncryption', ['chat_id' => $chat], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('messages.discardEncryption', ['chat_id' => $chat]); } catch (\danog\MadelineProto\RPCErrorException $e) { if ($e->rpc !== 'ENCRYPTION_ALREADY_DECLINED') { throw $e; diff --git a/src/danog/MadelineProto/SecretChats/MessageHandler.php b/src/danog/MadelineProto/SecretChats/MessageHandler.php index 712a1047..1f0e74a1 100644 --- a/src/danog/MadelineProto/SecretChats/MessageHandler.php +++ b/src/danog/MadelineProto/SecretChats/MessageHandler.php @@ -115,7 +115,10 @@ trait MessageHandler $this->secret_chats[$message['message']['chat_id']]['mtproto'] = 2; } } - $deserialized = yield from $this->TL->deserialize($message_data, ['type' => '']); + [$deserialized, $sideEffects] = $this->TL->deserialize($message_data, ['type' => '']); + if ($sideEffects) { + yield $sideEffects; + } $this->secret_chats[$message['message']['chat_id']]['ttr']--; if (($this->secret_chats[$message['message']['chat_id']]['ttr'] <= 0 || \time() - $this->secret_chats[$message['message']['chat_id']]['updated'] > 7 * 24 * 60 * 60) && $this->secret_chats[$message['message']['chat_id']]['rekeying'][0] === 0) { yield from $this->rekey($message['message']['chat_id']); diff --git a/src/danog/MadelineProto/SecretChats/ResponseHandler.php b/src/danog/MadelineProto/SecretChats/ResponseHandler.php index a844da29..e5d3866d 100644 --- a/src/danog/MadelineProto/SecretChats/ResponseHandler.php +++ b/src/danog/MadelineProto/SecretChats/ResponseHandler.php @@ -63,7 +63,7 @@ trait ResponseHandler foreach ($this->secret_chats[$update['message']['chat_id']]['outgoing'] as $seq => $message) { if ($seq >= $update['message']['decrypted_message']['action']['start_seq_no'] && $seq <= $update['message']['decrypted_message']['action']['end_seq_no']) { //throw new \danog\MadelineProto\ResponseException(\danog\MadelineProto\Lang::$current_lang['resending_unsupported']); - yield from $this->methodCallAsyncRead('messages.sendEncrypted', ['peer' => $update['message']['chat_id'], 'message' => $update['message']['decrypted_message']], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('messages.sendEncrypted', ['peer' => $update['message']['chat_id'], 'message' => $update['message']['decrypted_message']]); } } return; diff --git a/src/danog/MadelineProto/TL/TL.php b/src/danog/MadelineProto/TL/TL.php index 4e2c4db5..330f54c3 100644 --- a/src/danog/MadelineProto/TL/TL.php +++ b/src/danog/MadelineProto/TL/TL.php @@ -21,6 +21,7 @@ namespace danog\MadelineProto\TL; use Amp\Promise; use danog\MadelineProto\MTProto; +use danog\MadelineProto\MTProto\OutgoingMessage; use danog\MadelineProto\Settings\TLSchema; use danog\MadelineProto\Tools; @@ -812,16 +813,17 @@ class TL * @param string|resource $stream Stream * @param array $type Type identifier * - * @return \Generator + * @return array + * @psalm-return array{0: mixed, 1: \Amp\Promise} */ - public function deserialize($stream, $type = ['type' => '']): \Generator + public function deserialize($stream, $type = ['type' => '']): array { $promises = []; $result = $this->deserializeInternal($stream, $promises, $type); - if ($promises) { - yield $promises; - } - return $result; + return [ + $result, + $promises ? Tools::all($promises) : null + ]; } /** @@ -1009,14 +1011,14 @@ class TL if (\in_array($arg['name'], ['peer_tag', 'file_token', 'cdn_key', 'cdn_iv'])) { $arg['type'] = 'string'; } - if ($x['_'] === 'rpc_result' && $arg['name'] === 'result') { - if (isset($type['connection']->outgoing_messages[$x['req_msg_id']]['_']) && isset($this->callbacks[TLCallback::METHOD_BEFORE_CALLBACK][$type['connection']->outgoing_messages[$x['req_msg_id']]['_']])) { - foreach ($this->callbacks[TLCallback::METHOD_BEFORE_CALLBACK][$type['connection']->outgoing_messages[$x['req_msg_id']]['_']] as $callback) { - $callback($type['connection']->outgoing_messages[$x['req_msg_id']]['_']); - } + if ($x['_'] === 'rpc_result' && $arg['name'] === 'result' && isset($type['connection']->outgoing_messages[$x['req_msg_id']])) { + /** @var OutgoingMessage */ + $message = $type['connection']->outgoing_messages[$x['req_msg_id']]; + foreach ($this->callbacks[TLCallback::METHOD_BEFORE_CALLBACK][$message->getConstructor()] ?? [] as $callback) { + $callback($type['connection']->outgoing_messages[$x['req_msg_id']]['_']); } - if (isset($type['connection']->outgoing_messages[$x['req_msg_id']]['type']) && \stripos($type['connection']->outgoing_messages[$x['req_msg_id']]['type'], '<') !== false) { - $arg['subtype'] = \str_replace(['Vector<', '>'], '', $type['connection']->outgoing_messages[$x['req_msg_id']]['type']); + if ($message->getType() && \stripos($message->getType(), '<') !== false) { + $arg['subtype'] = \str_replace(['Vector<', '>'], '', $message->getType()); } } if (isset($type['connection'])) { @@ -1059,8 +1061,10 @@ class TL $promises []= $promise; } } - } elseif ($x['_'] === 'rpc_result' && isset($type['connection']->outgoing_messages[$x['req_msg_id']]['_']) && isset($this->callbacks[TLCallback::METHOD_CALLBACK][$type['connection']->outgoing_messages[$x['req_msg_id']]['_']])) { - foreach ($this->callbacks[TLCallback::METHOD_CALLBACK][$type['connection']->outgoing_messages[$x['req_msg_id']]['_']] as $callback) { + } elseif ($x['_'] === 'rpc_result' + && isset($type['connection']->outgoing_messages[$x['req_msg_id']]) + && isset($this->callbacks[TLCallback::METHOD_CALLBACK][$type['connection']->outgoing_messages[$x['req_msg_id']]->getConstructor()])) { + foreach ($this->callbacks[TLCallback::METHOD_CALLBACK][$type['connection']->outgoing_messages[$x['req_msg_id']]->getConstructor()] as $callback) { $callback($type['connection']->outgoing_messages[$x['req_msg_id']], $x['result']); } } diff --git a/src/danog/MadelineProto/TON/ADNLConnection.php b/src/danog/MadelineProto/TON/ADNLConnection.php index b5d07652..ea70719d 100644 --- a/src/danog/MadelineProto/TON/ADNLConnection.php +++ b/src/danog/MadelineProto/TON/ADNLConnection.php @@ -132,11 +132,11 @@ class ADNLConnection $buffer = yield $this->stream->getReadBuffer($length); if ($length) { $data = yield $buffer->bufferRead($length); - $data = yield from $this->TL->deserialize($data); + $data = $this->TL->deserialize($data)[0]; if ($data['_'] !== 'adnl.message.answer') { throw new Exception('Wrong answer type: '.$data['_']); } - $this->requests[$data['query_id']]->resolve(yield from $this->TL->deserialize((string) $data['answer'])); + $this->requests[$data['query_id']]->resolve($this->TL->deserialize((string) $data['answer'])[0]); } } })()); diff --git a/src/danog/MadelineProto/TON/Lite.php b/src/danog/MadelineProto/TON/Lite.php index 8261e348..748cc1a6 100644 --- a/src/danog/MadelineProto/TON/Lite.php +++ b/src/danog/MadelineProto/TON/Lite.php @@ -89,7 +89,7 @@ class Lite $config['_'] = 'liteclient.config.global'; $config = Tools::convertJsonTL($config); $config['validator']['init_block'] = $config['validator']['init_block'] ?? $config['validator']['zero_state']; - $this->config = yield from $this->TL->deserialize(yield from $this->TL->serializeObject(['type' => ''], $config, 'cleanup')); + [$this->config] = $this->TL->deserialize(yield from $this->TL->serializeObject(['type' => ''], $config, 'cleanup')); foreach ($this->config['liteservers'] as $lite) { $this->connections[] = $connection = new ADNLConnection($this->TL); yield from $connection->connect($lite); diff --git a/src/danog/MadelineProto/VoIP/AuthKeyHandler.php b/src/danog/MadelineProto/VoIP/AuthKeyHandler.php index 831ed870..ffb8887f 100644 --- a/src/danog/MadelineProto/VoIP/AuthKeyHandler.php +++ b/src/danog/MadelineProto/VoIP/AuthKeyHandler.php @@ -108,7 +108,7 @@ trait AuthKeyHandler Crypt::checkG($g_a, $dh_config['p']); $controller = new \danog\MadelineProto\VoIP(true, $user['user_id'], $this, \danog\MadelineProto\VoIP::CALL_STATE_REQUESTED); $controller->storage = ['a' => $a, 'g_a' => \str_pad($g_a->toBytes(), 256, \chr(0), \STR_PAD_LEFT)]; - $res = yield from $this->methodCallAsyncRead('phone.requestCall', ['user_id' => $user, 'g_a_hash' => \hash('sha256', $g_a->toBytes(), true), 'protocol' => ['_' => 'phoneCallProtocol', 'udp_p2p' => true, 'udp_reflector' => true, 'min_layer' => 65, 'max_layer' => \danog\MadelineProto\VoIP::getConnectionMaxLayer()]], ['datacenter' => $this->datacenter->curdc]); + $res = yield from $this->methodCallAsyncRead('phone.requestCall', ['user_id' => $user, 'g_a_hash' => \hash('sha256', $g_a->toBytes(), true), 'protocol' => ['_' => 'phoneCallProtocol', 'udp_p2p' => true, 'udp_reflector' => true, 'min_layer' => 65, 'max_layer' => \danog\MadelineProto\VoIP::getConnectionMaxLayer()]]); $controller->setCall($res['phone_call']); $this->calls[$res['phone_call']['id']] = $controller; yield $this->updaters[UpdateLoop::GENERIC]->resume(); @@ -137,7 +137,7 @@ trait AuthKeyHandler $g_b = $dh_config['g']->powMod($b, $dh_config['p']); Crypt::checkG($g_b, $dh_config['p']); try { - $res = yield from $this->methodCallAsyncRead('phone.acceptCall', ['peer' => $call, 'g_b' => $g_b->toBytes(), 'protocol' => ['_' => 'phoneCallProtocol', 'udp_reflector' => true, 'udp_p2p' => true, 'min_layer' => 65, 'max_layer' => \danog\MadelineProto\VoIP::getConnectionMaxLayer()]], ['datacenter' => $this->datacenter->curdc]); + $res = yield from $this->methodCallAsyncRead('phone.acceptCall', ['peer' => $call, 'g_b' => $g_b->toBytes(), 'protocol' => ['_' => 'phoneCallProtocol', 'udp_reflector' => true, 'udp_p2p' => true, 'min_layer' => 65, 'max_layer' => \danog\MadelineProto\VoIP::getConnectionMaxLayer()]]); } catch (\danog\MadelineProto\RPCErrorException $e) { if ($e->rpc === 'CALL_ALREADY_ACCEPTED') { $this->logger->logger(\sprintf(\danog\MadelineProto\Lang::$current_lang['call_already_accepted'], $call['id'])); @@ -176,7 +176,7 @@ trait AuthKeyHandler Crypt::checkG($params['g_b'], $dh_config['p']); $key = \str_pad($params['g_b']->powMod($this->calls[$params['id']]->storage['a'], $dh_config['p'])->toBytes(), 256, \chr(0), \STR_PAD_LEFT); try { - $res = (yield from $this->methodCallAsyncRead('phone.confirmCall', ['key_fingerprint' => \substr(\sha1($key, true), -8), 'peer' => ['id' => $params['id'], 'access_hash' => $params['access_hash'], '_' => 'inputPhoneCall'], 'g_a' => $this->calls[$params['id']]->storage['g_a'], 'protocol' => ['_' => 'phoneCallProtocol', 'udp_reflector' => true, 'min_layer' => 65, 'max_layer' => \danog\MadelineProto\VoIP::getConnectionMaxLayer()]], ['datacenter' => $this->datacenter->curdc]))['phone_call']; + $res = (yield from $this->methodCallAsyncRead('phone.confirmCall', ['key_fingerprint' => \substr(\sha1($key, true), -8), 'peer' => ['id' => $params['id'], 'access_hash' => $params['access_hash'], '_' => 'inputPhoneCall'], 'g_a' => $this->calls[$params['id']]->storage['g_a'], 'protocol' => ['_' => 'phoneCallProtocol', 'udp_reflector' => true, 'min_layer' => 65, 'max_layer' => \danog\MadelineProto\VoIP::getConnectionMaxLayer()]]))['phone_call']; } catch (\danog\MadelineProto\RPCErrorException $e) { if ($e->rpc === 'CALL_ALREADY_ACCEPTED') { $this->logger->logger(\sprintf(\danog\MadelineProto\Lang::$current_lang['call_already_accepted'], $params['id'])); @@ -293,7 +293,7 @@ trait AuthKeyHandler } $this->logger->logger(\sprintf(\danog\MadelineProto\Lang::$current_lang['call_discarding'], $call['id']), \danog\MadelineProto\Logger::VERBOSE); try { - $res = yield from $this->methodCallAsyncRead('phone.discardCall', ['peer' => $call, 'duration' => \time() - $this->calls[$call['id']]->whenCreated(), 'connection_id' => $this->calls[$call['id']]->getPreferredRelayID(), 'reason' => $reason], ['datacenter' => $this->datacenter->curdc]); + $res = yield from $this->methodCallAsyncRead('phone.discardCall', ['peer' => $call, 'duration' => \time() - $this->calls[$call['id']]->whenCreated(), 'connection_id' => $this->calls[$call['id']]->getPreferredRelayID(), 'reason' => $reason]); } catch (\danog\MadelineProto\RPCErrorException $e) { if (!\in_array($e->rpc, ['CALL_ALREADY_DECLINED', 'CALL_ALREADY_ACCEPTED'])) { throw $e; @@ -301,11 +301,11 @@ trait AuthKeyHandler } if (!empty($rating)) { $this->logger->logger(\sprintf(\danog\MadelineProto\Lang::$current_lang['call_set_rating'], $call['id']), \danog\MadelineProto\Logger::VERBOSE); - yield from $this->methodCallAsyncRead('phone.setCallRating', ['peer' => $call, 'rating' => $rating['rating'], 'comment' => $rating['comment']], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('phone.setCallRating', ['peer' => $call, 'rating' => $rating['rating'], 'comment' => $rating['comment']]); } if ($need_debug && isset($this->calls[$call['id']])) { $this->logger->logger(\sprintf(\danog\MadelineProto\Lang::$current_lang['call_debug_saving'], $call['id']), \danog\MadelineProto\Logger::VERBOSE); - yield from $this->methodCallAsyncRead('phone.saveCallDebug', ['peer' => $call, 'debug' => $this->calls[$call['id']]->getDebugLog()], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('phone.saveCallDebug', ['peer' => $call, 'debug' => $this->calls[$call['id']]->getDebugLog()]); } $update = ['_' => 'updatePhoneCall', 'phone_call' => $this->calls[$call['id']]]; $this->updates[$this->updates_key++] = $update; diff --git a/src/danog/MadelineProto/Wrappers/TOS.php b/src/danog/MadelineProto/Wrappers/TOS.php index 02b0d1fe..a34dfbfa 100644 --- a/src/danog/MadelineProto/Wrappers/TOS.php +++ b/src/danog/MadelineProto/Wrappers/TOS.php @@ -34,7 +34,7 @@ trait TOS if ($this->authorized === self::LOGGED_IN && !$this->authorization['user']['bot']) { if ($this->tos['expires'] < \time()) { $this->logger->logger('Fetching TOS...'); - $this->tos = yield from $this->methodCallAsyncRead('help.getTermsOfServiceUpdate', [], ['datacenter' => $this->datacenter->curdc]); + $this->tos = yield from $this->methodCallAsyncRead('help.getTermsOfServiceUpdate', []); $this->tos['accepted'] = $this->tos['_'] === 'help.termsOfServiceUpdateEmpty'; } if (!$this->tos['accepted']) { @@ -55,7 +55,7 @@ trait TOS */ public function acceptTos(): \Generator { - $this->tos['accepted'] = yield from $this->methodCallAsyncRead('help.acceptTermsOfService', ['id' => $this->tos['terms_of_service']['id']], ['datacenter' => $this->datacenter->curdc]); + $this->tos['accepted'] = yield from $this->methodCallAsyncRead('help.acceptTermsOfService', ['id' => $this->tos['terms_of_service']['id']]); if ($this->tos['accepted']) { $this->logger->logger('TOS accepted successfully'); } else { @@ -71,7 +71,7 @@ trait TOS */ public function declineTos(): \Generator { - yield from $this->methodCallAsyncRead('account.deleteAccount', ['reason' => 'Decline ToS update'], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('account.deleteAccount', ['reason' => 'Decline ToS update']); yield from $this->logout(); } } diff --git a/src/danog/MadelineProto/Wrappers/Webhook.php b/src/danog/MadelineProto/Wrappers/Webhook.php index df576fe6..9b92dc1b 100644 --- a/src/danog/MadelineProto/Wrappers/Webhook.php +++ b/src/danog/MadelineProto/Wrappers/Webhook.php @@ -68,7 +68,7 @@ trait Webhook $result = \json_decode($result, true); if (\is_array($result) && isset($result['method']) && $result['method'] != '' && \is_string($result['method'])) { try { - $this->logger->logger('Reverse webhook command returned', yield from $this->methodCallAsyncRead($result['method'], $result, ['datacenter' => $this->datacenter->curdc])); + $this->logger->logger('Reverse webhook command returned', yield from $this->methodCallAsyncRead($result['method'], $result)); } catch (\Throwable $e) { $this->logger->logger("Reverse webhook command returned: {$e}"); } diff --git a/tools/phpdoc.php b/tools/phpdoc.php index 2ed5641e..85a846dd 100644 --- a/tools/phpdoc.php +++ b/tools/phpdoc.php @@ -116,4 +116,3 @@ PhpDocBuilder::fromNamespace() ->setOutput(__DIR__.'/../docs/docs/PHP/') ->setImage("https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png") ->run(); -