Complete overhaul of MTProto message system, parallelized getPwrChat

This commit is contained in:
Daniil Gentili 2020-10-18 14:46:34 +02:00
parent a376f0bf82
commit eca8542be4
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
43 changed files with 1978 additions and 767 deletions

View File

@ -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 <daniil@daniil.it>
*/
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.

View File

@ -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, []]];
}

View File

@ -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));
}
}

View File

@ -11,7 +11,7 @@ use function Amp\Mysql\Pool;
/**
* MySQL driver wrapper.
*
*
* @internal
*/
class Mysql

View File

@ -11,7 +11,7 @@ use function Amp\Postgres\Pool;
/**
* Postgres driver wrapper.
*
*
* @internal
*/
class Postgres

View File

@ -9,7 +9,7 @@ use danog\MadelineProto\Settings\Database\Redis as DatabaseRedis;
/**
* Redis driver wrapper.
*
*
* @internal
*/
class Redis

View File

@ -6,7 +6,7 @@ use danog\MadelineProto\Db\MysqlArray as DbMysqlArray;
/**
* MySQL database backend, no caching.
*
*
* @internal
*/
class MysqlArray extends DbMysqlArray

View File

@ -6,7 +6,7 @@ use danog\MadelineProto\Db\PostgresArray as DbPostgresArray;
/**
* Postgres database backend, no caching.
*
*
* @internal
*/
class PostgresArray extends DbPostgresArray

View File

@ -6,7 +6,7 @@ use danog\MadelineProto\Db\RedisArray as DbRedisArray;
/**
* Redis database backend, no caching.
*
*
* @internal
*/
class RedisArray extends DbRedisArray

View File

@ -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))]);
}
});
}

View File

@ -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();

View File

@ -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()}");
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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;

View File

@ -0,0 +1,54 @@
<?php
/**
* Container message.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @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;
}
}

View File

@ -0,0 +1,237 @@
<?php
/**
* Outgoing message.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @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;
}
}

View File

@ -0,0 +1,160 @@
<?php
/**
* Message.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @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;
}

View File

@ -0,0 +1,617 @@
<?php
/**
* Outgoing message.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @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;
}
}

View File

@ -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;

View File

@ -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']);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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]));
}
}

View File

@ -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());
}
}
}

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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;

View File

@ -21,7 +21,7 @@ namespace danog\MadelineProto\MTProtoTools;
/**
* Stores multiple update states.
*
*
* @internal
*/
class CombinedUpdatesState

View File

@ -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) {

View File

@ -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<int|mixed, \Amp\Promise|\Amp\Promise<string>|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<array> 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') {

View File

@ -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:

View File

@ -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;

View File

@ -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']);

View File

@ -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;

View File

@ -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<mixed>
* @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']);
}
}

View File

@ -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]);
}
}
})());

View File

@ -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);

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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}");
}

View File

@ -116,4 +116,3 @@ PhpDocBuilder::fromNamespace()
->setOutput(__DIR__.'/../docs/docs/PHP/')
->setImage("https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png")
->run();