Document the network/ module

This commit is contained in:
Lonami Exo 2017-11-30 13:20:51 +01:00
parent 7509ba9067
commit 9046b46fcd
5 changed files with 300 additions and 46 deletions

View File

@ -1,3 +1,7 @@
"""
This module contains several classes regarding network, low level connection
with Telegram's servers and the protocol used (TCP full, abridged, etc.).
"""
from .mtproto_plain_sender import MtProtoPlainSender from .mtproto_plain_sender import MtProtoPlainSender
from .authenticator import do_authentication from .authenticator import do_authentication
from .mtproto_sender import MtProtoSender from .mtproto_sender import MtProtoSender

View File

@ -1,3 +1,7 @@
"""
This module contains several functions that authenticate the client machine
with Telegram's servers, effectively creating an authorization key.
"""
import os import os
import time import time
from hashlib import sha1 from hashlib import sha1
@ -18,6 +22,14 @@ from ..tl.functions import (
def do_authentication(connection, retries=5): def do_authentication(connection, retries=5):
"""
Performs the authentication steps on the given connection.
Raises an error if all attempts fail.
:param connection: the connection to be used (must be connected).
:param retries: how many times should we retry on failure.
:return:
"""
if not retries or retries < 0: if not retries or retries < 0:
retries = 1 retries = 1
@ -32,9 +44,11 @@ def do_authentication(connection, retries=5):
def _do_authentication(connection): def _do_authentication(connection):
"""Executes the authentication process with the Telegram servers. """
If no error is raised, returns both the authorization key and the Executes the authentication process with the Telegram servers.
time offset.
:param connection: the connection to be used (must be connected).
:return: returns a (authorization key, time offset) tuple.
""" """
sender = MtProtoPlainSender(connection) sender = MtProtoPlainSender(connection)
@ -195,8 +209,12 @@ def _do_authentication(connection):
def get_int(byte_array, signed=True): def get_int(byte_array, signed=True):
"""Gets the specified integer from its byte array. """
This should be used by the authenticator, Gets the specified integer from its byte array.
who requires the data to be in big endian This should be used by this module alone, as it works with big endian.
:param byte_array: the byte array representing th integer.
:param signed: whether the number is signed or not.
:return: the integer representing the given byte array.
""" """
return int.from_bytes(byte_array, byteorder='big', signed=signed) return int.from_bytes(byte_array, byteorder='big', signed=signed)

View File

@ -1,3 +1,7 @@
"""
This module holds both the Connection class and the ConnectionMode enum,
which specifies the protocol to be used by the Connection.
"""
import os import os
import struct import struct
from datetime import timedelta from datetime import timedelta
@ -35,16 +39,24 @@ class ConnectionMode(Enum):
class Connection: class Connection:
"""Represents an abstract connection (TCP, TCP abridged...). """
'mode' must be any of the ConnectionMode enumeration. Represents an abstract connection (TCP, TCP abridged...).
'mode' must be any of the ConnectionMode enumeration.
Note that '.send()' and '.recv()' refer to messages, which Note that '.send()' and '.recv()' refer to messages, which
will be packed accordingly, whereas '.write()' and '.read()' will be packed accordingly, whereas '.write()' and '.read()'
work on plain bytes, with no further additions. work on plain bytes, with no further additions.
""" """
def __init__(self, mode=ConnectionMode.TCP_FULL, def __init__(self, mode=ConnectionMode.TCP_FULL,
proxy=None, timeout=timedelta(seconds=5)): proxy=None, timeout=timedelta(seconds=5)):
"""
Initializes a new connection.
:param mode: the ConnectionMode to be used.
:param proxy: whether to use a proxy or not.
:param timeout: timeout to be used for all operations.
"""
self._mode = mode self._mode = mode
self._send_counter = 0 self._send_counter = 0
self._aes_encrypt, self._aes_decrypt = None, None self._aes_encrypt, self._aes_decrypt = None, None
@ -75,6 +87,12 @@ class Connection:
setattr(self, 'read', self._read_plain) setattr(self, 'read', self._read_plain)
def connect(self, ip, port): def connect(self, ip, port):
"""
Estabilishes a connection to IP:port.
:param ip: the IP to connect to.
:param port: the port to connect to.
"""
try: try:
self.conn.connect(ip, port) self.conn.connect(ip, port)
except OSError as e: except OSError as e:
@ -92,9 +110,13 @@ class Connection:
self._setup_obfuscation() self._setup_obfuscation()
def get_timeout(self): def get_timeout(self):
"""Returns the timeout used by the connection."""
return self.conn.timeout return self.conn.timeout
def _setup_obfuscation(self): def _setup_obfuscation(self):
"""
Sets up the obfuscated protocol.
"""
# Obfuscated messages secrets cannot start with any of these # Obfuscated messages secrets cannot start with any of these
keywords = (b'PVrG', b'GET ', b'POST', b'\xee' * 4) keywords = (b'PVrG', b'GET ', b'POST', b'\xee' * 4)
while True: while True:
@ -122,13 +144,19 @@ class Connection:
self.conn.write(bytes(random)) self.conn.write(bytes(random))
def is_connected(self): def is_connected(self):
"""
Determines whether the connection is alive or not.
:return: true if it's connected.
"""
return self.conn.connected return self.conn.connected
def close(self): def close(self):
"""Closes the connection."""
self.conn.close() self.conn.close()
def clone(self): def clone(self):
"""Creates a copy of this Connection""" """Creates a copy of this Connection."""
return Connection( return Connection(
mode=self._mode, proxy=self.conn.proxy, timeout=self.conn.timeout mode=self._mode, proxy=self.conn.proxy, timeout=self.conn.timeout
) )
@ -141,6 +169,15 @@ class Connection:
raise ValueError('Invalid connection mode specified: ' + str(self._mode)) raise ValueError('Invalid connection mode specified: ' + str(self._mode))
def _recv_tcp_full(self): def _recv_tcp_full(self):
"""
Receives a message from the network,
internally encoded using the TCP full protocol.
May raise InvalidChecksumError if the received data doesn't
match its valid checksum.
:return: the read message payload.
"""
packet_len_seq = self.read(8) # 4 and 4 packet_len_seq = self.read(8) # 4 and 4
packet_len, seq = struct.unpack('<ii', packet_len_seq) packet_len, seq = struct.unpack('<ii', packet_len_seq)
@ -154,9 +191,21 @@ class Connection:
return body return body
def _recv_intermediate(self): def _recv_intermediate(self):
"""
Receives a message from the network,
internally encoded using the TCP intermediate protocol.
:return: the read message payload.
"""
return self.read(struct.unpack('<i', self.read(4))[0]) return self.read(struct.unpack('<i', self.read(4))[0])
def _recv_abridged(self): def _recv_abridged(self):
"""
Receives a message from the network,
internally encoded using the TCP abridged protocol.
:return: the read message payload.
"""
length = struct.unpack('<B', self.read(1))[0] length = struct.unpack('<B', self.read(1))[0]
if length >= 127: if length >= 127:
length = struct.unpack('<i', self.read(3) + b'\0')[0] length = struct.unpack('<i', self.read(3) + b'\0')[0]
@ -173,6 +222,12 @@ class Connection:
raise ValueError('Invalid connection mode specified: ' + str(self._mode)) raise ValueError('Invalid connection mode specified: ' + str(self._mode))
def _send_tcp_full(self, message): def _send_tcp_full(self, message):
"""
Encapsulates and sends the given message payload
using the TCP full mode (length, sequence, message, crc32).
:param message: the message to be sent.
"""
# https://core.telegram.org/mtproto#tcp-transport # https://core.telegram.org/mtproto#tcp-transport
# total length, sequence number, packet and checksum (CRC32) # total length, sequence number, packet and checksum (CRC32)
length = len(message) + 12 length = len(message) + 12
@ -182,9 +237,21 @@ class Connection:
self.write(data + crc) self.write(data + crc)
def _send_intermediate(self, message): def _send_intermediate(self, message):
"""
Encapsulates and sends the given message payload
using the TCP intermediate mode (length, message).
:param message: the message to be sent.
"""
self.write(struct.pack('<i', len(message)) + message) self.write(struct.pack('<i', len(message)) + message)
def _send_abridged(self, message): def _send_abridged(self, message):
"""
Encapsulates and sends the given message payload
using the TCP abridged mode (short length, message).
:param message: the message to be sent.
"""
length = len(message) >> 2 length = len(message) >> 2
if length < 127: if length < 127:
length = struct.pack('B', length) length = struct.pack('B', length)
@ -201,9 +268,21 @@ class Connection:
raise ValueError('Invalid connection mode specified: ' + str(self._mode)) raise ValueError('Invalid connection mode specified: ' + str(self._mode))
def _read_plain(self, length): def _read_plain(self, length):
"""
Reads data from the socket connection.
:param length: how many bytes should be read.
:return: a byte sequence with len(data) == length
"""
return self.conn.read(length) return self.conn.read(length)
def _read_obfuscated(self, length): def _read_obfuscated(self, length):
"""
Reads data and decrypts from the socket connection.
:param length: how many bytes should be read.
:return: the decrypted byte sequence with len(data) == length
"""
return self._aes_decrypt.encrypt( return self._aes_decrypt.encrypt(
self.conn.read(length) self.conn.read(length)
) )
@ -216,9 +295,20 @@ class Connection:
raise ValueError('Invalid connection mode specified: ' + str(self._mode)) raise ValueError('Invalid connection mode specified: ' + str(self._mode))
def _write_plain(self, data): def _write_plain(self, data):
"""
Writes the given data through the socket connection.
:param data: the data in bytes to be written.
"""
self.conn.write(data) self.conn.write(data)
def _write_obfuscated(self, data): def _write_obfuscated(self, data):
"""
Writes the given data through the socket connection,
using the obfuscated mode (AES encryption is applied on top).
:param data: the data in bytes to be written.
"""
self.conn.write(self._aes_encrypt.encrypt(data)) self.conn.write(self._aes_encrypt.encrypt(data))
# endregion # endregion

View File

@ -1,3 +1,7 @@
"""
This module contains the class used to communicate with Telegram's servers
in plain text, when no authorization key has been created yet.
"""
import struct import struct
import time import time
@ -6,32 +10,47 @@ from ..extensions import BinaryReader
class MtProtoPlainSender: class MtProtoPlainSender:
"""MTProto Mobile Protocol plain sender """
(https://core.telegram.org/mtproto/description#unencrypted-messages) MTProto Mobile Protocol plain sender
(https://core.telegram.org/mtproto/description#unencrypted-messages)
""" """
def __init__(self, connection): def __init__(self, connection):
"""
Initializes the MTProto plain sender.
:param connection: the Connection to be used.
"""
self._sequence = 0 self._sequence = 0
self._time_offset = 0 self._time_offset = 0
self._last_msg_id = 0 self._last_msg_id = 0
self._connection = connection self._connection = connection
def connect(self): def connect(self):
"""Connects to Telegram's servers."""
self._connection.connect() self._connection.connect()
def disconnect(self): def disconnect(self):
"""Disconnects from Telegram's servers."""
self._connection.close() self._connection.close()
def send(self, data): def send(self, data):
"""Sends a plain packet (auth_key_id = 0) containing the """
given message body (data) Sends a plain packet (auth_key_id = 0) containing the
given message body (data).
:param data: the data to be sent.
""" """
self._connection.send( self._connection.send(
struct.pack('<QQi', 0, self._get_new_msg_id(), len(data)) + data struct.pack('<QQi', 0, self._get_new_msg_id(), len(data)) + data
) )
def receive(self): def receive(self):
"""Receives a plain packet, returning the body of the response""" """
Receives a plain packet from the network.
:return: the response body.
"""
body = self._connection.recv() body = self._connection.recv()
if body == b'l\xfe\xff\xff': # -404 little endian signed if body == b'l\xfe\xff\xff': # -404 little endian signed
# Broken authorization, must reset the auth key # Broken authorization, must reset the auth key
@ -46,7 +65,7 @@ class MtProtoPlainSender:
return response return response
def _get_new_msg_id(self): def _get_new_msg_id(self):
"""Generates a new message ID based on the current time since epoch""" """Generates a new message ID based on the current time since epoch."""
# See core.telegram.org/mtproto/description#message-identifier-msg-id # See core.telegram.org/mtproto/description#message-identifier-msg-id
now = time.time() now = time.time()
nanoseconds = int((now - int(now)) * 1e+9) nanoseconds = int((now - int(now)) * 1e+9)

View File

@ -1,3 +1,7 @@
"""
This module contains the class used to communicate with Telegram's servers
encrypting every packet, and relies on a valid AuthKey in the used Session.
"""
import gzip import gzip
import logging import logging
import struct import struct
@ -31,8 +35,14 @@ class MtProtoSender:
""" """
def __init__(self, session, connection): def __init__(self, session, connection):
"""Creates a new MtProtoSender configured to send messages through """
'connection' and using the parameters from 'session'. Initializes a new MTProto sender.
:param session:
the Session to be used with this sender. Must contain the IP and
port of the server, salt, ID, and AuthKey,
:param connection:
the Connection to be used.
""" """
self.session = session self.session = session
self.connection = connection self.connection = connection
@ -45,28 +55,36 @@ class MtProtoSender:
self._pending_receive = {} self._pending_receive = {}
def connect(self): def connect(self):
"""Connects to the server""" """Connects to the server."""
self.connection.connect(self.session.server_address, self.session.port) self.connection.connect(self.session.server_address, self.session.port)
def is_connected(self): def is_connected(self):
"""
Determines whether the sender is connected or not.
:return: true if the sender is connected.
"""
return self.connection.is_connected() return self.connection.is_connected()
def disconnect(self): def disconnect(self):
"""Disconnects from the server""" """Disconnects from the server."""
self.connection.close() self.connection.close()
self._need_confirmation.clear() self._need_confirmation.clear()
self._clear_all_pending() self._clear_all_pending()
def clone(self): def clone(self):
"""Creates a copy of this MtProtoSender as a new connection""" """Creates a copy of this MtProtoSender as a new connection."""
return MtProtoSender(self.session, self.connection.clone()) return MtProtoSender(self.session, self.connection.clone())
# region Send and receive # region Send and receive
def send(self, *requests): def send(self, *requests):
"""Sends the specified MTProtoRequest, previously sending any message """
which needed confirmation.""" Sends the specified TLObject(s) (which must be requests),
and acknowledging any message which needed confirmation.
:param requests: the requests to be sent.
"""
# Finally send our packed request(s) # Finally send our packed request(s)
messages = [TLMessage(self.session, r) for r in requests] messages = [TLMessage(self.session, r) for r in requests]
self._pending_receive.update({m.msg_id: m for m in messages}) self._pending_receive.update({m.msg_id: m for m in messages})
@ -91,18 +109,23 @@ class MtProtoSender:
self._send_message(message) self._send_message(message)
def _send_acknowledge(self, msg_id): def _send_acknowledge(self, msg_id):
"""Sends a message acknowledge for the given msg_id""" """Sends a message acknowledge for the given msg_id."""
self._send_message(TLMessage(self.session, MsgsAck([msg_id]))) self._send_message(TLMessage(self.session, MsgsAck([msg_id])))
def receive(self, update_state): def receive(self, update_state):
"""Receives a single message from the connected endpoint. """
Receives a single message from the connected endpoint.
This method returns nothing, and will only affect other parts This method returns nothing, and will only affect other parts
of the MtProtoSender such as the updates callback being fired of the MtProtoSender such as the updates callback being fired
or a pending request being confirmed. or a pending request being confirmed.
Any unhandled object (likely updates) will be passed to Any unhandled object (likely updates) will be passed to
update_state.process(TLObject). update_state.process(TLObject).
:param update_state:
the UpdateState that will process all the received
Update and Updates objects.
""" """
try: try:
body = self.connection.recv() body = self.connection.recv()
@ -126,8 +149,11 @@ class MtProtoSender:
# region Low level processing # region Low level processing
def _send_message(self, message): def _send_message(self, message):
"""Sends the given Message(TLObject) encrypted through the network""" """
Sends the given encrypted through the network.
:param message: the TLMessage to be sent.
"""
plain_text = \ plain_text = \
struct.pack('<QQ', self.session.salt, self.session.id) \ struct.pack('<QQ', self.session.salt, self.session.id) \
+ bytes(message) + bytes(message)
@ -141,7 +167,12 @@ class MtProtoSender:
self.connection.send(result) self.connection.send(result)
def _decode_msg(self, body): def _decode_msg(self, body):
"""Decodes an received encrypted message body bytes""" """
Decodes the body of the payload received from the network.
:param body: the body to be decoded.
:return: a tuple of (decoded message, remote message id, remote seq).
"""
message = None message = None
remote_msg_id = None remote_msg_id = None
remote_sequence = None remote_sequence = None
@ -172,12 +203,15 @@ class MtProtoSender:
return message, remote_msg_id, remote_sequence return message, remote_msg_id, remote_sequence
def _process_msg(self, msg_id, sequence, reader, state): def _process_msg(self, msg_id, sequence, reader, state):
"""Processes and handles a Telegram message.
Returns True if the message was handled correctly and doesn't
need to be skipped. Returns False otherwise.
""" """
Processes the message read from the network inside reader.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the BinaryReader that contains the message.
:param state: the current UpdateState.
:return: true if the message was handled correctly, false otherwise.
"""
# TODO Check salt, session_id and sequence_number # TODO Check salt, session_id and sequence_number
self._need_confirmation.add(msg_id) self._need_confirmation.add(msg_id)
@ -249,24 +283,34 @@ class MtProtoSender:
# region Message handling # region Message handling
def _pop_request(self, msg_id): def _pop_request(self, msg_id):
"""Pops a pending REQUEST from self._pending_receive, or """
returns None if it's not found. Pops a pending **request** from self._pending_receive.
:param msg_id: the ID of the message that belongs to the request.
:return: the request, or None if it wasn't found.
""" """
message = self._pending_receive.pop(msg_id, None) message = self._pending_receive.pop(msg_id, None)
if message: if message:
return message.request return message.request
def _pop_request_of_type(self, msg_id, t): def _pop_request_of_type(self, msg_id, t):
"""Pops a pending REQUEST from self._pending_receive if it matches """
the given type, or returns None if it's not found/doesn't match. Pops a pending **request** from self._pending_receive.
:param msg_id: the ID of the message that belongs to the request.
:param t: the type of the desired request.
:return: the request matching the type t, or None if it wasn't found.
""" """
message = self._pending_receive.get(msg_id, None) message = self._pending_receive.get(msg_id, None)
if message and isinstance(message.request, t): if message and isinstance(message.request, t):
return self._pending_receive.pop(msg_id).request return self._pending_receive.pop(msg_id).request
def _pop_requests_of_container(self, container_msg_id): def _pop_requests_of_container(self, container_msg_id):
"""Pops the pending requests (plural) from self._pending_receive if """
they were sent on a container that matches container_msg_id. Pops pending **requests** from self._pending_receive.
:param container_msg_id: the ID of the container.
:return: the requests that belong to the given container. May be empty.
""" """
msgs = [msg for msg in self._pending_receive.values() msgs = [msg for msg in self._pending_receive.values()
if msg.container_msg_id == container_msg_id] if msg.container_msg_id == container_msg_id]
@ -277,13 +321,19 @@ class MtProtoSender:
return requests return requests
def _clear_all_pending(self): def _clear_all_pending(self):
"""
Clears all pending requests, and flags them all as received.
"""
for r in self._pending_receive.values(): for r in self._pending_receive.values():
r.request.confirm_received.set() r.request.confirm_received.set()
self._pending_receive.clear() self._pending_receive.clear()
def _resend_request(self, msg_id): def _resend_request(self, msg_id):
"""Re-sends the request that belongs to a certain msg_id. This may """
also be the msg_id of a container if they were sent in one. Re-sends the request that belongs to a certain msg_id. This may
also be the msg_id of a container if they were sent in one.
:param msg_id: the ID of the request to be resent.
""" """
request = self._pop_request(msg_id) request = self._pop_request(msg_id)
if request: if request:
@ -293,6 +343,14 @@ class MtProtoSender:
return self.send(*requests) return self.send(*requests)
def _handle_pong(self, msg_id, sequence, reader): def _handle_pong(self, msg_id, sequence, reader):
"""
Handles a Pong response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the Pong.
:return: true, as it always succeeds.
"""
self._logger.debug('Handling pong') self._logger.debug('Handling pong')
pong = reader.tgread_object() pong = reader.tgread_object()
assert isinstance(pong, Pong) assert isinstance(pong, Pong)
@ -306,6 +364,14 @@ class MtProtoSender:
return True return True
def _handle_container(self, msg_id, sequence, reader, state): def _handle_container(self, msg_id, sequence, reader, state):
"""
Handles a MessageContainer response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the MessageContainer.
:return: true, as it always succeeds.
"""
self._logger.debug('Handling container') self._logger.debug('Handling container')
for inner_msg_id, _, inner_len in MessageContainer.iter_read(reader): for inner_msg_id, _, inner_len in MessageContainer.iter_read(reader):
begin_position = reader.tell_position() begin_position = reader.tell_position()
@ -323,6 +389,14 @@ class MtProtoSender:
return True return True
def _handle_bad_server_salt(self, msg_id, sequence, reader): def _handle_bad_server_salt(self, msg_id, sequence, reader):
"""
Handles a BadServerSalt response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the BadServerSalt.
:return: true, as it always succeeds.
"""
self._logger.debug('Handling bad server salt') self._logger.debug('Handling bad server salt')
bad_salt = reader.tgread_object() bad_salt = reader.tgread_object()
assert isinstance(bad_salt, BadServerSalt) assert isinstance(bad_salt, BadServerSalt)
@ -339,6 +413,14 @@ class MtProtoSender:
return True return True
def _handle_bad_msg_notification(self, msg_id, sequence, reader): def _handle_bad_msg_notification(self, msg_id, sequence, reader):
"""
Handles a BadMessageError response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the BadMessageError.
:return: true, as it always succeeds.
"""
self._logger.debug('Handling bad message notification') self._logger.debug('Handling bad message notification')
bad_msg = reader.tgread_object() bad_msg = reader.tgread_object()
assert isinstance(bad_msg, BadMsgNotification) assert isinstance(bad_msg, BadMsgNotification)
@ -367,6 +449,14 @@ class MtProtoSender:
raise error raise error
def _handle_msg_detailed_info(self, msg_id, sequence, reader): def _handle_msg_detailed_info(self, msg_id, sequence, reader):
"""
Handles a MsgDetailedInfo response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the MsgDetailedInfo.
:return: true, as it always succeeds.
"""
msg_new = reader.tgread_object() msg_new = reader.tgread_object()
assert isinstance(msg_new, MsgDetailedInfo) assert isinstance(msg_new, MsgDetailedInfo)
@ -376,6 +466,14 @@ class MtProtoSender:
return True return True
def _handle_msg_new_detailed_info(self, msg_id, sequence, reader): def _handle_msg_new_detailed_info(self, msg_id, sequence, reader):
"""
Handles a MsgNewDetailedInfo response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the MsgNewDetailedInfo.
:return: true, as it always succeeds.
"""
msg_new = reader.tgread_object() msg_new = reader.tgread_object()
assert isinstance(msg_new, MsgNewDetailedInfo) assert isinstance(msg_new, MsgNewDetailedInfo)
@ -385,12 +483,29 @@ class MtProtoSender:
return True return True
def _handle_new_session_created(self, msg_id, sequence, reader): def _handle_new_session_created(self, msg_id, sequence, reader):
"""
Handles a NewSessionCreated response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the NewSessionCreated.
:return: true, as it always succeeds.
"""
new_session = reader.tgread_object() new_session = reader.tgread_object()
assert isinstance(new_session, NewSessionCreated) assert isinstance(new_session, NewSessionCreated)
# TODO https://goo.gl/LMyN7A # TODO https://goo.gl/LMyN7A
return True return True
def _handle_rpc_result(self, msg_id, sequence, reader): def _handle_rpc_result(self, msg_id, sequence, reader):
"""
Handles a RPCResult response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the RPCResult.
:return: true if the request ID to which this result belongs is found,
false otherwise (meaning nothing was read).
"""
self._logger.debug('Handling RPC result') self._logger.debug('Handling RPC result')
reader.read_int(signed=False) # code reader.read_int(signed=False) # code
request_id = reader.read_long() request_id = reader.read_long()
@ -440,6 +555,14 @@ class MtProtoSender:
return False return False
def _handle_gzip_packed(self, msg_id, sequence, reader, state): def _handle_gzip_packed(self, msg_id, sequence, reader, state):
"""
Handles a GzipPacked response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the GzipPacked.
:return: the result of processing the packed message.
"""
self._logger.debug('Handling gzip packed data') self._logger.debug('Handling gzip packed data')
with BinaryReader(GzipPacked.read(reader)) as compressed_reader: with BinaryReader(GzipPacked.read(reader)) as compressed_reader:
# We are reentering process_msg, which seemingly the same msg_id # We are reentering process_msg, which seemingly the same msg_id