. * * @author Daniil Gentili * @copyright 2016-2020 Daniil Gentili * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 * * @link https://docs.madelineproto.xyz MadelineProto documentation */ namespace danog\MadelineProto\MTProtoSession; use Amp\Loop; use danog\MadelineProto\Logger; use danog\MadelineProto\Loop\Update\UpdateLoop; use danog\MadelineProto\MTProto; /** * Manages responses. */ trait ResponseHandler { public $n = 0; public function handleMessages(): bool { $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; } $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']['_']) { 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); // Acknowledge that the server received my message } 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; 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); 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 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) { $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']; } \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 } 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']; } unset($this->incoming_messages[$current_msg_id]['content']); 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']); 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']); 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); } 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])); } } 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])); } 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'])); } 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])); } } 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; } break; } } if ($this->pending_outgoing) { $this->writer->resume(); } return $only_updates; } /** * Reject request with exception. * * @param array $request Request * @param \Throwable $data Exception * * @return void */ public function handleReject(&$request, \Throwable $data) { 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 '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 ($response['error_message'] === 'USER_DEACTIVATED') { $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->authorization['user']['phone']) ? '+'.$this->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->authorization['user']['phone']) ? '+'.$this->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); 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)); } unset($request); $this->gotResponseForOutgoingMessageId($request_id); $r = isset($response['_']) ? $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); } } })()); } }