Merge remote-tracking branch 'remotes/upstream/master'

This commit is contained in:
JosXa 2017-10-21 00:04:55 +02:00
commit b7c72f18fd
31 changed files with 823 additions and 957 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
telethon/tl/functions/ telethon/tl/functions/
telethon/tl/types/ telethon/tl/types/
telethon/tl/all_tlobjects.py telethon/tl/all_tlobjects.py
telethon/errors/rpc_error_list.py
# User session # User session
*.session *.session

View File

@ -42,17 +42,26 @@ class TempWorkDir:
os.chdir(self.original) os.chdir(self.original)
ERROR_LIST = 'telethon/errors/rpc_error_list.py'
ERRORS_JSON = 'telethon_generator/errors.json'
ERRORS_DESC = 'telethon_generator/error_descriptions'
SCHEME_TL = 'telethon_generator/scheme.tl'
GENERATOR_DIR = 'telethon/tl'
IMPORT_DEPTH = 2
def gen_tl(): def gen_tl():
from telethon_generator.tl_generator import TLGenerator from telethon_generator.tl_generator import TLGenerator
generator = TLGenerator('telethon/tl') from telethon_generator.error_generator import generate_code
generator = TLGenerator(GENERATOR_DIR)
if generator.tlobjects_exist(): if generator.tlobjects_exist():
print('Detected previous TLObjects. Cleaning...') print('Detected previous TLObjects. Cleaning...')
generator.clean_tlobjects() generator.clean_tlobjects()
print('Generating TLObjects...') print('Generating TLObjects...')
generator.generate_tlobjects( generator.generate_tlobjects(SCHEME_TL, import_depth=IMPORT_DEPTH)
'telethon_generator/scheme.tl', import_depth=2 print('Generating errors...')
) generate_code(ERROR_LIST, json_file=ERRORS_JSON, errors_desc=ERRORS_DESC)
print('Done.') print('Done.')
@ -63,7 +72,7 @@ def main():
elif len(argv) >= 2 and argv[1] == 'clean_tl': elif len(argv) >= 2 and argv[1] == 'clean_tl':
from telethon_generator.tl_generator import TLGenerator from telethon_generator.tl_generator import TLGenerator
print('Cleaning...') print('Cleaning...')
TLGenerator('telethon/tl').clean_tlobjects() TLGenerator(GENERATOR_DIR).clean_tlobjects()
print('Done.') print('Done.')
elif len(argv) >= 2 and argv[1] == 'pypi': elif len(argv) >= 2 and argv[1] == 'pypi':
@ -80,6 +89,10 @@ def main():
for x in ('build', 'dist', 'Telethon.egg-info'): for x in ('build', 'dist', 'Telethon.egg-info'):
rmtree(x, ignore_errors=True) rmtree(x, ignore_errors=True)
if len(argv) >= 2 and argv[1] == 'fetch_errors':
from telethon_generator.error_generator import fetch_errors
fetch_errors(ERRORS_JSON)
else: else:
if not TelegramClient: if not TelegramClient:
gen_tl() gen_tl()

View File

@ -8,15 +8,8 @@ from .common import (
CdnFileTamperedError CdnFileTamperedError
) )
from .rpc_errors import ( # This imports the base errors too, as they're imported there
RPCError, InvalidDCError, BadRequestError, UnauthorizedError, from .rpc_error_list import *
ForbiddenError, NotFoundError, FloodError, ServerError, BadMessageError
)
from .rpc_errors_303 import *
from .rpc_errors_400 import *
from .rpc_errors_401 import *
from .rpc_errors_420 import *
def report_error(code, message, report_method): def report_error(code, message, report_method):
@ -43,27 +36,31 @@ def rpc_message_to_error(code, message, report_method=None):
args=(code, message, report_method) args=(code, message, report_method)
).start() ).start()
errors = { # Try to get the error by direct look-up, otherwise regex
303: rpc_errors_303_all, # TODO Maybe regexes could live in a separate dictionary?
400: rpc_errors_400_all, cls = rpc_errors_all.get(message, None)
401: rpc_errors_401_all, if cls:
420: rpc_errors_420_all return cls()
}.get(code, None)
if errors is not None: for msg_regex, cls in rpc_errors_all.items():
for msg, cls in errors.items(): m = re.match(msg_regex, message)
m = re.match(msg, message)
if m: if m:
extra = int(m.group(1)) if m.groups() else None capture = int(m.group(1)) if m.groups() else None
return cls(extra=extra) return cls(capture=capture)
elif code == 403: if code == 400:
return BadRequestError(message)
if code == 401:
return UnauthorizedError(message)
if code == 403:
return ForbiddenError(message) return ForbiddenError(message)
elif code == 404: if code == 404:
return NotFoundError(message) return NotFoundError(message)
elif code == 500: if code == 500:
return ServerError(message) return ServerError(message)
return RPCError('{} (code {})'.format(message, code)) return RPCError('{} (code {})'.format(message, code))

View File

@ -1,51 +0,0 @@
from . import InvalidDCError
class FileMigrateError(InvalidDCError):
def __init__(self, **kwargs):
self.new_dc = kwargs['extra']
super(Exception, self).__init__(
self,
'The file to be accessed is currently stored in DC {}.'
.format(self.new_dc)
)
class PhoneMigrateError(InvalidDCError):
def __init__(self, **kwargs):
self.new_dc = kwargs['extra']
super(Exception, self).__init__(
self,
'The phone number a user is trying to use for authorization is '
'associated with DC {}.'
.format(self.new_dc)
)
class NetworkMigrateError(InvalidDCError):
def __init__(self, **kwargs):
self.new_dc = kwargs['extra']
super(Exception, self).__init__(
self,
'The source IP address is associated with DC {}.'
.format(self.new_dc)
)
class UserMigrateError(InvalidDCError):
def __init__(self, **kwargs):
self.new_dc = kwargs['extra']
super(Exception, self).__init__(
self,
'The user whose identity is being used to execute queries is '
'associated with DC {}.'
.format(self.new_dc)
)
rpc_errors_303_all = {
'FILE_MIGRATE_(\d+)': FileMigrateError,
'PHONE_MIGRATE_(\d+)': PhoneMigrateError,
'NETWORK_MIGRATE_(\d+)': NetworkMigrateError,
'USER_MIGRATE_(\d+)': UserMigrateError
}

View File

@ -1,453 +0,0 @@
from . import BadRequestError
class ApiIdInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The api_id/api_hash combination is invalid.'
)
class BotMethodInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The API access for bot users is restricted. The method you '
'tried to invoke cannot be executed as a bot.'
)
class CdnMethodInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'This method cannot be invoked on a CDN server. Refer to '
'https://core.telegram.org/cdn#schema for available methods.'
)
class ChannelInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Invalid channel object. Make sure to pass the right types,'
' for instance making sure that the request is designed for '
'channels or otherwise look for a different one more suited.'
)
class ChannelPrivateError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The channel specified is private and you lack permission to '
'access it. Another reason may be that you were banned from it.'
)
class ChatAdminRequiredError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Chat admin privileges are required to do that in the specified '
'chat (for example, to send a message in a channel which is not '
'yours).'
)
class ChatIdInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Invalid object ID for a chat. Make sure to pass the right types,'
' for instance making sure that the request is designed for chats'
' (not channels/megagroups) or otherwise look for a different one'
' more suited.\nAn example working with a megagroup and'
' AddChatUserRequest, it will fail because megagroups are channels'
'. Use InviteToChannelRequest instead.'
)
class ConnectionLangPackInvalid(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The specified language pack is not valid. This is meant to be '
'used by official applications only so far, leave it empty.'
)
class ConnectionLayerInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The very first request must always be InvokeWithLayerRequest.'
)
class DcIdInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'This occurs when an authorization is tried to be exported for '
'the same data center one is currently connected to.'
)
class FieldNameEmptyError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The field with the name FIELD_NAME is missing.'
)
class FieldNameInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The field with the name FIELD_NAME is invalid.'
)
class FilePartsInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The number of file parts is invalid.'
)
class FilePartMissingError(BadRequestError):
def __init__(self, **kwargs):
self.which = kwargs['extra']
super(Exception, self).__init__(
self,
'Part {} of the file is missing from storage.'.format(self.which)
)
class FilePartInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The file part number is invalid.'
)
class FirstNameInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The first name is invalid.'
)
class InputMethodInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The invoked method does not exist anymore or has never existed.'
)
class InputRequestTooLongError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The input request was too long. This may be a bug in the library '
'as it can occur when serializing more bytes than it should (like'
'appending the vector constructor code at the end of a message).'
)
class LastNameInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The last name is invalid.'
)
class LimitInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'An invalid limit was provided. See '
'https://core.telegram.org/api/files#downloading-files'
)
class LocationInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The location given for a file was invalid. See '
'https://core.telegram.org/api/files#downloading-files'
)
class Md5ChecksumInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The MD5 check-sums do not match.'
)
class MessageEmptyError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Empty or invalid UTF-8 message was sent.'
)
class MessageIdInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The specified message ID is invalid.'
)
class MessageTooLongError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Message was too long. Current maximum length is 4096 UTF-8 '
'characters.'
)
class MessageNotModifiedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Content of the message was not modified.'
)
class MsgWaitFailedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'A waiting call returned an error.'
)
class OffsetInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The given offset was invalid, it must be divisible by 1KB. '
'See https://core.telegram.org/api/files#downloading-files'
)
class PasswordHashInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The password (and thus its hash value) you entered is invalid.'
)
class PeerIdInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'An invalid Peer was used. Make sure to pass the right peer type.'
)
class PhoneCodeEmptyError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The phone code is missing.'
)
class PhoneCodeExpiredError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The confirmation code has expired.'
)
class PhoneCodeHashEmptyError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The phone code hash is missing.'
)
class PhoneCodeInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The phone code entered was invalid.'
)
class PhoneNumberBannedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The used phone number has been banned from Telegram and cannot '
'be used anymore. Maybe check https://www.telegram.org/faq_spam.'
)
class PhoneNumberInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The phone number is invalid.'
)
class PhoneNumberOccupiedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The phone number is already in use.'
)
class PhoneNumberUnoccupiedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The phone number is not yet being used.'
)
class PhotoInvalidDimensionsError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The photo dimensions are invalid.'
)
class TypeConstructorInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The type constructor is invalid.'
)
class UsernameInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Unacceptable username. Must match r"[a-zA-Z][\w\d]{4,31}".'
)
class UsernameNotModifiedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The username is not different from the current username.'
)
class UsernameNotOccupiedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The username is not in use by anyone else yet.'
)
class UsernameOccupiedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The username is already taken.'
)
class UsersTooFewError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Not enough users (to create a chat, for example).'
)
class UsersTooMuchError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The maximum number of users has been exceeded (to create a '
'chat, for example).'
)
class UserIdInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Invalid object ID for an user. Make sure to pass the right types,'
'for instance making sure that the request is designed for users'
'or otherwise look for a different one more suited.'
)
rpc_errors_400_all = {
'API_ID_INVALID': ApiIdInvalidError,
'BOT_METHOD_INVALID': BotMethodInvalidError,
'CDN_METHOD_INVALID': CdnMethodInvalidError,
'CHANNEL_INVALID': ChannelInvalidError,
'CHANNEL_PRIVATE': ChannelPrivateError,
'CHAT_ADMIN_REQUIRED': ChatAdminRequiredError,
'CHAT_ID_INVALID': ChatIdInvalidError,
'CONNECTION_LAYER_INVALID': ConnectionLayerInvalidError,
'DC_ID_INVALID': DcIdInvalidError,
'FIELD_NAME_EMPTY': FieldNameEmptyError,
'FIELD_NAME_INVALID': FieldNameInvalidError,
'FILE_PARTS_INVALID': FilePartsInvalidError,
'FILE_PART_(\d+)_MISSING': FilePartMissingError,
'FILE_PART_INVALID': FilePartInvalidError,
'FIRSTNAME_INVALID': FirstNameInvalidError,
'INPUT_METHOD_INVALID': InputMethodInvalidError,
'INPUT_REQUEST_TOO_LONG': InputRequestTooLongError,
'LASTNAME_INVALID': LastNameInvalidError,
'LIMIT_INVALID': LimitInvalidError,
'LOCATION_INVALID': LocationInvalidError,
'MD5_CHECKSUM_INVALID': Md5ChecksumInvalidError,
'MESSAGE_EMPTY': MessageEmptyError,
'MESSAGE_ID_INVALID': MessageIdInvalidError,
'MESSAGE_TOO_LONG': MessageTooLongError,
'MESSAGE_NOT_MODIFIED': MessageNotModifiedError,
'MSG_WAIT_FAILED': MsgWaitFailedError,
'OFFSET_INVALID': OffsetInvalidError,
'PASSWORD_HASH_INVALID': PasswordHashInvalidError,
'PEER_ID_INVALID': PeerIdInvalidError,
'PHONE_CODE_EMPTY': PhoneCodeEmptyError,
'PHONE_CODE_EXPIRED': PhoneCodeExpiredError,
'PHONE_CODE_HASH_EMPTY': PhoneCodeHashEmptyError,
'PHONE_CODE_INVALID': PhoneCodeInvalidError,
'PHONE_NUMBER_BANNED': PhoneNumberBannedError,
'PHONE_NUMBER_INVALID': PhoneNumberInvalidError,
'PHONE_NUMBER_OCCUPIED': PhoneNumberOccupiedError,
'PHONE_NUMBER_UNOCCUPIED': PhoneNumberUnoccupiedError,
'PHOTO_INVALID_DIMENSIONS': PhotoInvalidDimensionsError,
'TYPE_CONSTRUCTOR_INVALID': TypeConstructorInvalidError,
'USERNAME_INVALID': UsernameInvalidError,
'USERNAME_NOT_MODIFIED': UsernameNotModifiedError,
'USERNAME_NOT_OCCUPIED': UsernameNotOccupiedError,
'USERNAME_OCCUPIED': UsernameOccupiedError,
'USERS_TOO_FEW': UsersTooFewError,
'USERS_TOO_MUCH': UsersTooMuchError,
'USER_ID_INVALID': UserIdInvalidError,
}

View File

@ -1,98 +0,0 @@
from . import UnauthorizedError
class ActiveUserRequiredError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The method is only available to already activated users.'
)
class AuthKeyInvalidError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The key is invalid.'
)
class AuthKeyPermEmptyError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The method is unavailable for temporary authorization key, not '
'bound to permanent.'
)
class AuthKeyUnregisteredError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The key is not registered in the system.'
)
class InviteHashExpiredError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The chat the user tried to join has expired and is not valid '
'anymore.'
)
class SessionExpiredError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The authorization has expired.'
)
class SessionPasswordNeededError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Two-steps verification is enabled and a password is required.'
)
class SessionRevokedError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The authorization has been invalidated, because of the user '
'terminating all sessions.'
)
class UserAlreadyParticipantError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The authenticated user is already a participant of the chat.'
)
class UserDeactivatedError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The user has been deleted/deactivated.'
)
rpc_errors_401_all = {
'ACTIVE_USER_REQUIRED': ActiveUserRequiredError,
'AUTH_KEY_INVALID': AuthKeyInvalidError,
'AUTH_KEY_PERM_EMPTY': AuthKeyPermEmptyError,
'AUTH_KEY_UNREGISTERED': AuthKeyUnregisteredError,
'INVITE_HASH_EXPIRED': InviteHashExpiredError,
'SESSION_EXPIRED': SessionExpiredError,
'SESSION_PASSWORD_NEEDED': SessionPasswordNeededError,
'SESSION_REVOKED': SessionRevokedError,
'USER_ALREADY_PARTICIPANT': UserAlreadyParticipantError,
'USER_DEACTIVATED': UserDeactivatedError,
}

View File

@ -1,16 +0,0 @@
from . import FloodError
class FloodWaitError(FloodError):
def __init__(self, **kwargs):
self.seconds = kwargs['extra']
super(Exception, self).__init__(
self,
'A wait of {} seconds is required.'
.format(self.seconds)
)
rpc_errors_420_all = {
'FLOOD_WAIT_(\d+)': FloodWaitError
}

View File

@ -129,13 +129,10 @@ class BinaryReader:
return False return False
# If there was still no luck, give up # If there was still no luck, give up
self.seek(-4) # Go back
raise TypeNotFoundError(constructor_id) raise TypeNotFoundError(constructor_id)
# Create an empty instance of the class and return clazz.from_reader(self)
# fill it with the read attributes
result = clazz.empty()
result.on_response(self)
return result
def tgread_vector(self): def tgread_vector(self):
"""Reads a vector (a list) of Telegram objects""" """Reads a vector (a list) of Telegram objects"""

View File

@ -14,7 +14,7 @@ class TcpClient:
if isinstance(timeout, timedelta): if isinstance(timeout, timedelta):
self.timeout = timeout.seconds self.timeout = timeout.seconds
elif isinstance(timeout, int) or isinstance(timeout, float): elif isinstance(timeout, (int, float)):
self.timeout = float(timeout) self.timeout = float(timeout)
else: else:
raise ValueError('Invalid timeout type', type(timeout)) raise ValueError('Invalid timeout type', type(timeout))

View File

@ -42,7 +42,7 @@ def _do_authentication(connection):
req_pq_request = ReqPqRequest( req_pq_request = ReqPqRequest(
nonce=int.from_bytes(os.urandom(16), 'big', signed=True) nonce=int.from_bytes(os.urandom(16), 'big', signed=True)
) )
sender.send(req_pq_request.to_bytes()) sender.send(bytes(req_pq_request))
with BinaryReader(sender.receive()) as reader: with BinaryReader(sender.receive()) as reader:
req_pq_request.on_response(reader) req_pq_request.on_response(reader)
@ -60,12 +60,12 @@ def _do_authentication(connection):
p, q = rsa.get_byte_array(min(p, q)), rsa.get_byte_array(max(p, q)) p, q = rsa.get_byte_array(min(p, q)), rsa.get_byte_array(max(p, q))
new_nonce = int.from_bytes(os.urandom(32), 'little', signed=True) new_nonce = int.from_bytes(os.urandom(32), 'little', signed=True)
pq_inner_data = PQInnerData( pq_inner_data = bytes(PQInnerData(
pq=rsa.get_byte_array(pq), p=p, q=q, pq=rsa.get_byte_array(pq), p=p, q=q,
nonce=res_pq.nonce, nonce=res_pq.nonce,
server_nonce=res_pq.server_nonce, server_nonce=res_pq.server_nonce,
new_nonce=new_nonce new_nonce=new_nonce
).to_bytes() ))
# sha_digest + data + random_bytes # sha_digest + data + random_bytes
cipher_text, target_fingerprint = None, None cipher_text, target_fingerprint = None, None
@ -90,7 +90,7 @@ def _do_authentication(connection):
public_key_fingerprint=target_fingerprint, public_key_fingerprint=target_fingerprint,
encrypted_data=cipher_text encrypted_data=cipher_text
) )
sender.send(req_dh_params.to_bytes()) sender.send(bytes(req_dh_params))
# Step 2 response: DH Exchange # Step 2 response: DH Exchange
with BinaryReader(sender.receive()) as reader: with BinaryReader(sender.receive()) as reader:
@ -124,7 +124,6 @@ def _do_authentication(connection):
raise AssertionError(server_dh_inner) raise AssertionError(server_dh_inner)
if server_dh_inner.nonce != res_pq.nonce: if server_dh_inner.nonce != res_pq.nonce:
print(server_dh_inner.nonce, res_pq.nonce)
raise SecurityError('Invalid nonce in encrypted answer') raise SecurityError('Invalid nonce in encrypted answer')
if server_dh_inner.server_nonce != res_pq.server_nonce: if server_dh_inner.server_nonce != res_pq.server_nonce:
@ -139,12 +138,12 @@ def _do_authentication(connection):
gab = pow(g_a, b, dh_prime) gab = pow(g_a, b, dh_prime)
# Prepare client DH Inner Data # Prepare client DH Inner Data
client_dh_inner = ClientDHInnerData( client_dh_inner = bytes(ClientDHInnerData(
nonce=res_pq.nonce, nonce=res_pq.nonce,
server_nonce=res_pq.server_nonce, server_nonce=res_pq.server_nonce,
retry_id=0, # TODO Actual retry ID retry_id=0, # TODO Actual retry ID
g_b=rsa.get_byte_array(gb) g_b=rsa.get_byte_array(gb)
).to_bytes() ))
client_dh_inner_hashed = sha1(client_dh_inner).digest() + client_dh_inner client_dh_inner_hashed = sha1(client_dh_inner).digest() + client_dh_inner
@ -157,7 +156,7 @@ def _do_authentication(connection):
server_nonce=res_pq.server_nonce, server_nonce=res_pq.server_nonce,
encrypted_data=client_dh_encrypted, encrypted_data=client_dh_encrypted,
) )
sender.send(set_client_dh.to_bytes()) sender.send(bytes(set_client_dh))
# Step 3 response: Complete DH Exchange # Step 3 response: Complete DH Exchange
with BinaryReader(sender.receive()) as reader: with BinaryReader(sender.receive()) as reader:

View File

@ -141,28 +141,25 @@ 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):
packet_length_bytes = self.read(4) packet_len_seq = self.read(8) # 4 and 4
packet_length = int.from_bytes(packet_length_bytes, 'little') packet_len, seq = struct.unpack('<ii', packet_len_seq)
seq_bytes = self.read(4) body = self.read(packet_len - 12)
seq = int.from_bytes(seq_bytes, 'little') checksum = struct.unpack('<I', self.read(4))[0]
body = self.read(packet_length - 12) valid_checksum = crc32(packet_len_seq + body)
checksum = int.from_bytes(self.read(4), 'little')
valid_checksum = crc32(packet_length_bytes + seq_bytes + body)
if checksum != valid_checksum: if checksum != valid_checksum:
raise InvalidChecksumError(checksum, valid_checksum) raise InvalidChecksumError(checksum, valid_checksum)
return body return body
def _recv_intermediate(self): def _recv_intermediate(self):
return self.read(int.from_bytes(self.read(4), 'little')) return self.read(struct.unpack('<i', self.read(4))[0])
def _recv_abridged(self): def _recv_abridged(self):
length = int.from_bytes(self.read(1), 'little') length = struct.unpack('<B', self.read(1))[0]
if length >= 127: if length >= 127:
length = int.from_bytes(self.read(3) + b'\0', 'little') length = struct.unpack('<i', self.read(3) + b'\0')[0]
return self.read(length << 2) return self.read(length << 2)

View File

@ -11,7 +11,10 @@ from ..errors import (
from ..extensions import BinaryReader from ..extensions import BinaryReader
from ..tl import TLMessage, MessageContainer, GzipPacked from ..tl import TLMessage, MessageContainer, GzipPacked
from ..tl.all_tlobjects import tlobjects from ..tl.all_tlobjects import tlobjects
from ..tl.types import MsgsAck from ..tl.types import (
MsgsAck, Pong, BadServerSalt, BadMsgNotification,
MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo
)
from ..tl.functions.auth import LogOutRequest from ..tl.functions.auth import LogOutRequest
logging.getLogger(__name__).addHandler(logging.NullHandler()) logging.getLogger(__name__).addHandler(logging.NullHandler())
@ -36,7 +39,7 @@ class MtProtoSender:
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
# Message IDs that need confirmation # Message IDs that need confirmation
self._need_confirmation = [] self._need_confirmation = set()
# Requests (as msg_id: Message) sent waiting to be received # Requests (as msg_id: Message) sent waiting to be received
self._pending_receive = {} self._pending_receive = {}
@ -71,7 +74,7 @@ class MtProtoSender:
# Pack everything in the same container if we need to send AckRequests # Pack everything in the same container if we need to send AckRequests
if self._need_confirmation: if self._need_confirmation:
messages.append( messages.append(
TLMessage(self.session, MsgsAck(self._need_confirmation)) TLMessage(self.session, MsgsAck(list(self._need_confirmation)))
) )
self._need_confirmation.clear() self._need_confirmation.clear()
@ -122,7 +125,7 @@ class MtProtoSender:
plain_text = \ plain_text = \
struct.pack('<QQ', self.session.salt, self.session.id) \ struct.pack('<QQ', self.session.salt, self.session.id) \
+ message.to_bytes() + bytes(message)
msg_key = utils.calc_msg_key(plain_text) msg_key = utils.calc_msg_key(plain_text)
key_id = struct.pack('<Q', self.session.auth_key.key_id) key_id = struct.pack('<Q', self.session.auth_key.key_id)
@ -171,7 +174,7 @@ class MtProtoSender:
""" """
# TODO Check salt, session_id and sequence_number # TODO Check salt, session_id and sequence_number
self._need_confirmation.append(msg_id) self._need_confirmation.add(msg_id)
code = reader.read_int(signed=False) code = reader.read_int(signed=False)
reader.seek(-4) reader.seek(-4)
@ -180,24 +183,33 @@ class MtProtoSender:
if code == 0xf35c6d01: # rpc_result, (response of an RPC call) if code == 0xf35c6d01: # rpc_result, (response of an RPC call)
return self._handle_rpc_result(msg_id, sequence, reader) return self._handle_rpc_result(msg_id, sequence, reader)
if code == 0x347773c5: # pong if code == Pong.CONSTRUCTOR_ID:
return self._handle_pong(msg_id, sequence, reader) return self._handle_pong(msg_id, sequence, reader)
if code == 0x73f1f8dc: # msg_container if code == MessageContainer.CONSTRUCTOR_ID:
return self._handle_container(msg_id, sequence, reader, state) return self._handle_container(msg_id, sequence, reader, state)
if code == 0x3072cfa1: # gzip_packed if code == GzipPacked.CONSTRUCTOR_ID:
return self._handle_gzip_packed(msg_id, sequence, reader, state) return self._handle_gzip_packed(msg_id, sequence, reader, state)
if code == 0xedab447b: # bad_server_salt if code == BadServerSalt.CONSTRUCTOR_ID:
return self._handle_bad_server_salt(msg_id, sequence, reader) return self._handle_bad_server_salt(msg_id, sequence, reader)
if code == 0xa7eff811: # bad_msg_notification if code == BadMsgNotification.CONSTRUCTOR_ID:
return self._handle_bad_msg_notification(msg_id, sequence, reader) return self._handle_bad_msg_notification(msg_id, sequence, reader)
# msgs_ack, it may handle the request we wanted if code == MsgDetailedInfo.CONSTRUCTOR_ID:
if code == 0x62d6b459: return self._handle_msg_detailed_info(msg_id, sequence, reader)
if code == MsgNewDetailedInfo.CONSTRUCTOR_ID:
return self._handle_msg_new_detailed_info(msg_id, sequence, reader)
if code == NewSessionCreated.CONSTRUCTOR_ID:
return self._handle_new_session_created(msg_id, sequence, reader)
if code == MsgsAck.CONSTRUCTOR_ID: # may handle the request we wanted
ack = reader.tgread_object() ack = reader.tgread_object()
assert isinstance(ack, MsgsAck)
# Ignore every ack request *unless* when logging out, when it's # Ignore every ack request *unless* when logging out, when it's
# when it seems to only make sense. We also need to set a non-None # when it seems to only make sense. We also need to set a non-None
# result since Telegram doesn't send the response for these. # result since Telegram doesn't send the response for these.
@ -205,7 +217,7 @@ class MtProtoSender:
r = self._pop_request_of_type(msg_id, LogOutRequest) r = self._pop_request_of_type(msg_id, LogOutRequest)
if r: if r:
r.result = True # Telegram won't send this value r.result = True # Telegram won't send this value
r.confirm_received() r.confirm_received.set()
self._logger.debug('Message ack confirmed', r) self._logger.debug('Message ack confirmed', r)
return True return True
@ -219,7 +231,12 @@ class MtProtoSender:
return True return True
self._logger.debug('Unknown message: {}'.format(hex(code))) self._logger.debug(
'[WARN] Unknown message: {}, data left in the buffer: {}'
.format(
hex(code), repr(reader.get_bytes()[reader.tell_position():])
)
)
return False return False
# endregion # endregion
@ -239,22 +256,23 @@ class MtProtoSender:
the given type, or returns None if it's not found/doesn't match. the given type, or returns None if it's not found/doesn't match.
""" """
message = self._pending_receive.get(msg_id, None) message = self._pending_receive.get(msg_id, None)
if 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 _clear_all_pending(self): def _clear_all_pending(self):
for r in self._pending_receive.values(): for r in self._pending_receive.values():
r.confirm_received.set() r.request.confirm_received.set()
self._pending_receive.clear() self._pending_receive.clear()
def _handle_pong(self, msg_id, sequence, reader): def _handle_pong(self, msg_id, sequence, reader):
self._logger.debug('Handling pong') self._logger.debug('Handling pong')
reader.read_int(signed=False) # code pong = reader.tgread_object()
received_msg_id = reader.read_long() assert isinstance(pong, Pong)
request = self._pop_request(received_msg_id) request = self._pop_request(pong.msg_id)
if request: if request:
self._logger.debug('Pong confirmed a request') self._logger.debug('Pong confirmed a request')
request.result = pong
request.confirm_received.set() request.confirm_received.set()
return True return True
@ -278,14 +296,16 @@ class MtProtoSender:
def _handle_bad_server_salt(self, msg_id, sequence, reader): def _handle_bad_server_salt(self, msg_id, sequence, reader):
self._logger.debug('Handling bad server salt') self._logger.debug('Handling bad server salt')
reader.read_int(signed=False) # code bad_salt = reader.tgread_object()
bad_msg_id = reader.read_long() assert isinstance(bad_salt, BadServerSalt)
reader.read_int() # bad_msg_seq_no
reader.read_int() # error_code
new_salt = reader.read_long(signed=False)
self.session.salt = new_salt
request = self._pop_request(bad_msg_id) # Our salt is unsigned, but the objects work with signed salts
self.session.salt = struct.unpack(
'<Q', struct.pack('<q', bad_salt.new_server_salt)
)[0]
self.session.save()
request = self._pop_request(bad_salt.bad_msg_id)
if request: if request:
self.send(request) self.send(request)
@ -293,31 +313,53 @@ class MtProtoSender:
def _handle_bad_msg_notification(self, msg_id, sequence, reader): def _handle_bad_msg_notification(self, msg_id, sequence, reader):
self._logger.debug('Handling bad message notification') self._logger.debug('Handling bad message notification')
reader.read_int(signed=False) # code bad_msg = reader.tgread_object()
reader.read_long() # request_id assert isinstance(bad_msg, BadMsgNotification)
reader.read_int() # request_sequence
error_code = reader.read_int() error = BadMessageError(bad_msg.error_code)
error = BadMessageError(error_code) if bad_msg.error_code in (16, 17):
if error_code in (16, 17):
# sent msg_id too low or too high (respectively). # sent msg_id too low or too high (respectively).
# Use the current msg_id to determine the right time offset. # Use the current msg_id to determine the right time offset.
self.session.update_time_offset(correct_msg_id=msg_id) self.session.update_time_offset(correct_msg_id=msg_id)
self._logger.debug('Read Bad Message error: ' + str(error)) self._logger.debug('Read Bad Message error: ' + str(error))
self._logger.debug('Attempting to use the correct time offset.') self._logger.debug('Attempting to use the correct time offset.')
return True return True
elif error_code == 32: elif bad_msg.error_code == 32:
# msg_seqno too low, so just pump it up by some "large" amount # msg_seqno too low, so just pump it up by some "large" amount
# TODO A better fix would be to start with a new fresh session ID # TODO A better fix would be to start with a new fresh session ID
self.session._sequence += 64 self.session._sequence += 64
return True return True
elif error_code == 33: elif bad_msg.error_code == 33:
# msg_seqno too high never seems to happen but just in case # msg_seqno too high never seems to happen but just in case
self.session._sequence -= 16 self.session._sequence -= 16
return True return True
else: else:
raise error raise error
def _handle_msg_detailed_info(self, msg_id, sequence, reader):
msg_new = reader.tgread_object()
assert isinstance(msg_new, MsgDetailedInfo)
# TODO For now, simply ack msg_new.answer_msg_id
# Relevant tdesktop source code: https://goo.gl/VvpCC6
self._send_acknowledge(msg_new.answer_msg_id)
return True
def _handle_msg_new_detailed_info(self, msg_id, sequence, reader):
msg_new = reader.tgread_object()
assert isinstance(msg_new, MsgNewDetailedInfo)
# TODO For now, simply ack msg_new.answer_msg_id
# Relevant tdesktop source code: https://goo.gl/G7DPsR
self._send_acknowledge(msg_new.answer_msg_id)
return True
def _handle_new_session_created(self, msg_id, sequence, reader):
new_session = reader.tgread_object()
assert isinstance(new_session, NewSessionCreated)
# TODO https://goo.gl/LMyN7A
return True
def _handle_rpc_result(self, msg_id, sequence, reader): def _handle_rpc_result(self, msg_id, sequence, reader):
self._logger.debug('Handling RPC result') self._logger.debug('Handling RPC result')
reader.read_int(signed=False) # code reader.read_int(signed=False) # code
@ -346,8 +388,9 @@ class MtProtoSender:
# else TODO Where should this error be reported? # else TODO Where should this error be reported?
# Read may be async. Can an error not-belong to a request? # Read may be async. Can an error not-belong to a request?
self._logger.debug('Read RPC error: %s', str(error)) self._logger.debug('Read RPC error: %s', str(error))
else: return True # All contents were read okay
if request:
elif request:
self._logger.debug('Reading request response') self._logger.debug('Reading request response')
if inner_code == 0x3072cfa1: # GZip packed if inner_code == 0x3072cfa1: # GZip packed
unpacked_data = gzip.decompress(reader.tgread_bytes()) unpacked_data = gzip.decompress(reader.tgread_bytes())
@ -360,7 +403,7 @@ class MtProtoSender:
self.session.process_entities(request.result) self.session.process_entities(request.result)
request.confirm_received.set() request.confirm_received.set()
return True return True
else:
# If it's really a result for RPC from previous connection # If it's really a result for RPC from previous connection
# session, it will be skipped by the handle_container() # session, it will be skipped by the handle_container()
self._logger.debug('Lost request will be skipped.') self._logger.debug('Lost request will be skipped.')
@ -369,6 +412,11 @@ class MtProtoSender:
def _handle_gzip_packed(self, msg_id, sequence, reader, state): def _handle_gzip_packed(self, msg_id, sequence, reader, state):
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
# to the self._need_confirmation set. Remove it from there first
# to avoid any future conflicts (i.e. if we "ignore" messages
# that we are already aware of, see 1a91c02 and old 63dfb1e)
self._need_confirmation -= {msg_id}
return self._process_msg(msg_id, sequence, compressed_reader, state) return self._process_msg(msg_id, sequence, compressed_reader, state)
# endregion # endregion

View File

@ -1,6 +1,7 @@
import logging import logging
import os import os
import threading import threading
import warnings
from datetime import timedelta, datetime from datetime import timedelta, datetime
from hashlib import md5 from hashlib import md5
from io import BytesIO from io import BytesIO
@ -57,7 +58,7 @@ class TelegramBareClient:
""" """
# Current TelegramClient version # Current TelegramClient version
__version__ = '0.15.1' __version__ = '0.15.3'
# TODO Make this thread-safe, all connections share the same DC # TODO Make this thread-safe, all connections share the same DC
_dc_options = None _dc_options = None
@ -131,7 +132,7 @@ class TelegramBareClient:
self._user_connected = False self._user_connected = False
# Save whether the user is authorized here (a.k.a. logged in) # Save whether the user is authorized here (a.k.a. logged in)
self._authorized = False self._authorized = None # None = We don't know yet
# Uploaded files cache so subsequent calls are instant # Uploaded files cache so subsequent calls are instant
self._upload_cache = {} self._upload_cache = {}
@ -222,12 +223,14 @@ class TelegramBareClient:
# another data center and this would raise UserMigrateError) # another data center and this would raise UserMigrateError)
# to also assert whether the user is logged in or not. # to also assert whether the user is logged in or not.
self._user_connected = True self._user_connected = True
if _sync_updates and not _cdn: if self._authorized is None and _sync_updates and not _cdn:
try: try:
self.sync_updates() self.sync_updates()
self._set_connected_and_authorized() self._set_connected_and_authorized()
except UnauthorizedError: except UnauthorizedError:
self._authorized = False self._authorized = False
elif self._authorized:
self._set_connected_and_authorized()
return True return True
@ -270,18 +273,16 @@ class TelegramBareClient:
def disconnect(self): def disconnect(self):
"""Disconnects from the Telegram server """Disconnects from the Telegram server
and stops all the spawned threads""" and stops all the spawned threads"""
self._user_connected = False self._user_connected = False # This will stop recv_thread's loop
self._recv_thread = None
# Stop the workers from the background thread
self.updates.stop_workers() self.updates.stop_workers()
# This will trigger a "ConnectionResetError", for subsequent calls # This will trigger a "ConnectionResetError" on the recv_thread,
# to read or send (from another thread) and usually, the background # which won't attempt reconnecting as ._user_connected is False.
# thread would try restarting the connection but since the
# ._recv_thread = None, it knows it doesn't have to.
self._sender.disconnect() self._sender.disconnect()
if self._recv_thread:
self._recv_thread.join()
# TODO Shall we clear the _exported_sessions, or may be reused? # TODO Shall we clear the _exported_sessions, or may be reused?
pass pass
@ -298,10 +299,13 @@ class TelegramBareClient:
# Assume we are disconnected due to some error, so connect again # Assume we are disconnected due to some error, so connect again
with self._reconnect_lock: with self._reconnect_lock:
# Another thread may have connected again, so check that first # Another thread may have connected again, so check that first
if not self.is_connected(): if self.is_connected():
return self.connect()
else:
return True return True
try:
return self.connect()
except ConnectionResetError:
return False
else: else:
self.disconnect() self.disconnect()
self.session.auth_key = None # Force creating new auth_key self.session.auth_key = None # Force creating new auth_key
@ -428,9 +432,18 @@ class TelegramBareClient:
on_main_thread = threading.get_ident() == self._main_thread_ident on_main_thread = threading.get_ident() == self._main_thread_ident
if on_main_thread or self._on_read_thread(): if on_main_thread or self._on_read_thread():
sender = self._sender sender = self._sender
update_state = self.updates
else: else:
sender = self._sender.clone() sender = self._sender.clone()
sender.connect() sender.connect()
# We're on another connection, Telegram will resend all the
# updates that we haven't acknowledged (potentially entering
# an infinite loop if we're calling this in response to an
# update event, as it would be received again and again). So
# to avoid this we will simply not process updates on these
# new temporary connections, as they will be sent and later
# acknowledged over the main connection.
update_state = None
# We should call receive from this thread if there's no background # We should call receive from this thread if there's no background
# thread reading or if the server disconnected us and we're trying # thread reading or if the server disconnected us and we're trying
@ -443,8 +456,10 @@ class TelegramBareClient:
if self._background_error and on_main_thread: if self._background_error and on_main_thread:
raise self._background_error raise self._background_error
result = self._invoke(sender, call_receive, *requests) result = self._invoke(
if result: sender, call_receive, update_state, *requests
)
if result is not None:
return result return result
raise ValueError('Number of retries reached 0.') raise ValueError('Number of retries reached 0.')
@ -455,7 +470,7 @@ class TelegramBareClient:
# Let people use client.invoke(SomeRequest()) instead client(...) # Let people use client.invoke(SomeRequest()) instead client(...)
invoke = __call__ invoke = __call__
def _invoke(self, sender, call_receive, *requests): def _invoke(self, sender, call_receive, update_state, *requests):
try: try:
# Ensure that we start with no previous errors (i.e. resending) # Ensure that we start with no previous errors (i.e. resending)
for x in requests: for x in requests:
@ -475,14 +490,14 @@ class TelegramBareClient:
) )
else: else:
while not all(x.confirm_received.is_set() for x in requests): while not all(x.confirm_received.is_set() for x in requests):
sender.receive(update_state=self.updates) sender.receive(update_state=update_state)
except TimeoutError: except TimeoutError:
pass # We will just retry pass # We will just retry
except ConnectionResetError: except ConnectionResetError:
if not self._authorized or self._reconnect_lock.locked(): if not self._user_connected or self._reconnect_lock.locked():
# Only attempt reconnecting if we're authorized and not # Only attempt reconnecting if the user called connect and not
# reconnecting already. # reconnecting already.
raise raise
@ -495,10 +510,7 @@ class TelegramBareClient:
else: else:
while self._user_connected and not self._reconnect(): while self._user_connected and not self._reconnect():
sleep(0.1) # Retry forever until we can send the request sleep(0.1) # Retry forever until we can send the request
return None
finally:
if sender != self._sender:
sender.disconnect()
try: try:
raise next(x.rpc_error for x in requests if x.rpc_error) raise next(x.rpc_error for x in requests if x.rpc_error)
@ -525,7 +537,7 @@ class TelegramBareClient:
# be on the very first connection (not authorized, not running), # be on the very first connection (not authorized, not running),
# but may be an issue for people who actually travel? # but may be an issue for people who actually travel?
self._reconnect(new_dc=e.new_dc) self._reconnect(new_dc=e.new_dc)
return self._invoke(sender, call_receive, *requests) return self._invoke(sender, call_receive, update_state, *requests)
except ServerError as e: except ServerError as e:
# Telegram is having some issues, just retry # Telegram is having some issues, just retry
@ -533,11 +545,15 @@ class TelegramBareClient:
'[ERROR] Telegram is having some internal issues', e '[ERROR] Telegram is having some internal issues', e
) )
except FloodWaitError: except FloodWaitError as e:
sender.disconnect() if e.seconds > self.session.flood_sleep_threshold | 0:
self.disconnect()
raise raise
self._logger.debug(
'Sleep of %d seconds below threshold, sleeping' % e.seconds
)
sleep(e.seconds)
# Some really basic functionality # Some really basic functionality
def is_user_authorized(self): def is_user_authorized(self):
@ -683,10 +699,8 @@ class TelegramBareClient:
cdn_decrypter = None cdn_decrypter = None
try: try:
offset_index = 0 offset = 0
while True: while True:
offset = offset_index * part_size
try: try:
if cdn_decrypter: if cdn_decrypter:
result = cdn_decrypter.get_file() result = cdn_decrypter.get_file()
@ -705,7 +719,7 @@ class TelegramBareClient:
client = self._get_exported_client(e.new_dc) client = self._get_exported_client(e.new_dc)
continue continue
offset_index += 1 offset += part_size
# If we have received no data (0 bytes), the file is over # If we have received no data (0 bytes), the file is over
# So there is nothing left to download and write # So there is nothing left to download and write
@ -742,10 +756,10 @@ class TelegramBareClient:
def add_update_handler(self, handler): def add_update_handler(self, handler):
"""Adds an update handler (a function which takes a TLObject, """Adds an update handler (a function which takes a TLObject,
an update, as its parameter) and listens for updates""" an update, as its parameter) and listens for updates"""
sync = not self.updates.handlers if not self.updates.get_workers:
warnings.warn("There are no update workers running, so adding an update handler will have no effect.")
self.updates.handlers.append(handler) self.updates.handlers.append(handler)
if sync:
self.sync_updates()
def remove_update_handler(self, handler): def remove_update_handler(self, handler):
self.updates.handlers.remove(handler) self.updates.handlers.remove(handler)
@ -801,7 +815,9 @@ class TelegramBareClient:
try: try:
import socks import socks
if isinstance(error, socks.GeneralProxyError): if isinstance(error, (
socks.GeneralProxyError, socks.ProxyConnectionError
)):
# This is a known error, and it's not related to # This is a known error, and it's not related to
# Telegram but rather to the proxy. Disconnect and # Telegram but rather to the proxy. Disconnect and
# hand it over to the main thread. # hand it over to the main thread.

View File

@ -15,6 +15,7 @@ from .errors import (
) )
from .network import ConnectionMode from .network import ConnectionMode
from .tl import TLObject from .tl import TLObject
from .tl.custom import Draft
from .tl.entity_database import EntityDatabase from .tl.entity_database import EntityDatabase
from .tl.functions.account import ( from .tl.functions.account import (
GetPasswordRequest GetPasswordRequest
@ -28,8 +29,8 @@ from .tl.functions.contacts import (
) )
from .tl.functions.messages import ( from .tl.functions.messages import (
GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest, GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest,
SendMessageRequest, GetChatsRequest SendMessageRequest, GetChatsRequest,
) GetAllDraftsRequest)
from .tl.functions import channels from .tl.functions import channels
from .tl.functions import messages from .tl.functions import messages
@ -46,7 +47,7 @@ from .tl.types import (
InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty, InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty,
Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto, Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto,
InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID, InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID,
UpdateNewMessage, UpdateShortSentMessage, UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage,
PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel) PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel)
from .tl.types.messages import DialogsSlice from .tl.types.messages import DialogsSlice
@ -252,7 +253,7 @@ class TelegramClient(TelegramBareClient):
if limit is None: if limit is None:
limit = float('inf') limit = float('inf')
dialogs = {} # Use Dialog.top_message as identifier to avoid dupes dialogs = {} # Use peer id as identifier to avoid dupes
messages = {} # Used later for sorting TODO also return these? messages = {} # Used later for sorting TODO also return these?
entities = {} entities = {}
while len(dialogs) < limit: while len(dialogs) < limit:
@ -267,7 +268,7 @@ class TelegramClient(TelegramBareClient):
break break
for d in r.dialogs: for d in r.dialogs:
dialogs[d.top_message] = d dialogs[utils.get_peer_id(d.peer, True)] = d
for m in r.messages: for m in r.messages:
messages[m.id] = m messages[m.id] = m
@ -302,9 +303,20 @@ class TelegramClient(TelegramBareClient):
[utils.find_user_or_chat(d.peer, entities, entities) for d in ds] [utils.find_user_or_chat(d.peer, entities, entities) for d in ds]
) )
# endregion def get_drafts(self): # TODO: Ability to provide a `filter`
"""
Gets all open draft messages.
# region Message requests Returns a list of custom `Draft` objects that are easy to work with: You can call
`draft.set_message('text')` to change the message, or delete it through `draft.delete()`.
:return List[telethon.tl.custom.Draft]: A list of open drafts
"""
response = self(GetAllDraftsRequest())
self.session.process_entities(response)
self.session.generate_sequence(response.seq)
drafts = [Draft._from_update(self, u) for u in response.updates]
return drafts
def send_message(self, def send_message(self,
entity, entity,
@ -347,7 +359,7 @@ class TelegramClient(TelegramBareClient):
break break
for update in result.updates: for update in result.updates:
if isinstance(update, UpdateNewMessage): if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)):
if update.message.id == msg_id: if update.message.id == msg_id:
return update.message return update.message
@ -488,9 +500,13 @@ class TelegramClient(TelegramBareClient):
def send_file(self, entity, file, caption='', def send_file(self, entity, file, caption='',
force_document=False, progress_callback=None, force_document=False, progress_callback=None,
reply_to=None, reply_to=None,
attributes=None,
**kwargs): **kwargs):
"""Sends a file to the specified entity. """Sends a file to the specified entity.
The file may either be a path, a byte array, or a stream. The file may either be a path, a byte array, or a stream.
Note that if a byte array or a stream is given, a filename
or its type won't be inferred, and it will be sent as an
"unnamed application/octet-stream".
An optional caption can also be specified for said file. An optional caption can also be specified for said file.
@ -507,6 +523,10 @@ class TelegramClient(TelegramBareClient):
The "reply_to" parameter works exactly as the one on .send_message. The "reply_to" parameter works exactly as the one on .send_message.
If "attributes" is set to be a list of DocumentAttribute's, these
will override the automatically inferred ones (so that you can
modify the file name of the file sent for instance).
If "is_voice_note" in kwargs, despite its value, and the file is If "is_voice_note" in kwargs, despite its value, and the file is
sent as a document, it will be sent as a voice note. sent as a document, it will be sent as a voice note.
@ -537,16 +557,28 @@ class TelegramClient(TelegramBareClient):
# Determine mime-type and attributes # Determine mime-type and attributes
# Take the first element by using [0] since it returns a tuple # Take the first element by using [0] since it returns a tuple
mime_type = guess_type(file)[0] mime_type = guess_type(file)[0]
attributes = [ attr_dict = {
DocumentAttributeFilename:
DocumentAttributeFilename(os.path.basename(file)) DocumentAttributeFilename(os.path.basename(file))
# TODO If the input file is an audio, find out: # TODO If the input file is an audio, find out:
# Performer and song title and add DocumentAttributeAudio # Performer and song title and add DocumentAttributeAudio
] }
else: else:
attributes = [DocumentAttributeFilename('unnamed')] attr_dict = {
DocumentAttributeFilename:
DocumentAttributeFilename('unnamed')
}
if 'is_voice_note' in kwargs: if 'is_voice_note' in kwargs:
attributes.append(DocumentAttributeAudio(0, voice=True)) attr_dict[DocumentAttributeAudio] = \
DocumentAttributeAudio(0, voice=True)
# Now override the attributes if any. As we have a dict of
# {cls: instance}, we can override any class with the list
# of attributes provided by the user easily.
if attributes:
for a in attributes:
attr_dict[type(a)] = a
# Ensure we have a mime type, any; but it cannot be None # Ensure we have a mime type, any; but it cannot be None
# 'The "octet-stream" subtype is used to indicate that a body # 'The "octet-stream" subtype is used to indicate that a body
@ -557,7 +589,7 @@ class TelegramClient(TelegramBareClient):
media = InputMediaUploadedDocument( media = InputMediaUploadedDocument(
file=file_handle, file=file_handle,
mime_type=mime_type, mime_type=mime_type,
attributes=attributes, attributes=list(attr_dict.values()),
caption=caption caption=caption
) )
@ -877,17 +909,16 @@ class TelegramClient(TelegramBareClient):
# crc32(b'InputPeer') and crc32(b'Peer') # crc32(b'InputPeer') and crc32(b'Peer')
type(entity).SUBCLASS_OF_ID in (0xc91c90b6, 0x2d45687)): type(entity).SUBCLASS_OF_ID in (0xc91c90b6, 0x2d45687)):
ie = self.get_input_entity(entity) ie = self.get_input_entity(entity)
result = None
if isinstance(ie, InputPeerUser): if isinstance(ie, InputPeerUser):
result = self(GetUsersRequest([ie])) self(GetUsersRequest([ie]))
elif isinstance(ie, InputPeerChat): elif isinstance(ie, InputPeerChat):
result = self(GetChatsRequest([ie.chat_id])) self(GetChatsRequest([ie.chat_id]))
elif isinstance(ie, InputPeerChannel): elif isinstance(ie, InputPeerChannel):
result = self(GetChannelsRequest([ie])) self(GetChannelsRequest([ie]))
if result:
self.session.process_entities(result)
try: try:
# session.process_entities has been called in the MtProtoSender
# with the result of these calls, so they should now be on the
# entities database.
return self.session.entities[ie] return self.session.entities[ie]
except KeyError: except KeyError:
pass pass
@ -906,11 +937,11 @@ class TelegramClient(TelegramBareClient):
phone = EntityDatabase.parse_phone(string) phone = EntityDatabase.parse_phone(string)
if phone: if phone:
entity = phone entity = phone
self.session.process_entities(self(GetContactsRequest(0))) self(GetContactsRequest(0))
else: else:
entity = string.strip('@').lower() entity = string.strip('@').lower()
self.session.process_entities(self(ResolveUsernameRequest(entity))) self(ResolveUsernameRequest(entity))
# MtProtoSender will call .process_entities on the requests made
try: try:
return self.session.entities[entity] return self.session.entities[entity]
except KeyError: except KeyError:
@ -956,9 +987,17 @@ class TelegramClient(TelegramBareClient):
) )
if self.session.save_entities: if self.session.save_entities:
# Not found, look in the dialogs (this will save the users) # Not found, look in the latest dialogs.
self.get_dialogs(limit=None) # This is useful if for instance someone just sent a message but
# the updates didn't specify who, as this person or chat should
# be in the latest dialogs.
self(GetDialogsRequest(
offset_date=None,
offset_id=0,
offset_peer=InputPeerEmpty(),
limit=0,
exclude_pinned=True
))
try: try:
return self.session.entities.get_input_entity(peer) return self.session.entities.get_input_entity(peer)
except KeyError: except KeyError:

View File

@ -0,0 +1 @@
from .draft import Draft

View File

@ -0,0 +1,80 @@
from ..functions.messages import SaveDraftRequest
from ..types import UpdateDraftMessage
class Draft:
"""
Custom class that encapsulates a draft on the Telegram servers, providing
an abstraction to change the message conveniently. The library will return
instances of this class when calling `client.get_drafts()`.
"""
def __init__(self, client, peer, draft):
self._client = client
self._peer = peer
self.text = draft.message
self.date = draft.date
self.no_webpage = draft.no_webpage
self.reply_to_msg_id = draft.reply_to_msg_id
self.entities = draft.entities
@classmethod
def _from_update(cls, client, update):
if not isinstance(update, UpdateDraftMessage):
raise ValueError(
'You can only create a new `Draft` from a corresponding '
'`UpdateDraftMessage` object.'
)
return cls(client=client, peer=update.peer, draft=update.draft)
@property
def entity(self):
return self._client.get_entity(self._peer)
@property
def input_entity(self):
return self._client.get_input_entity(self._peer)
def set_message(self, text, no_webpage=None, reply_to_msg_id=None, entities=None):
"""
Changes the draft message on the Telegram servers. The changes are
reflected in this object. Changing only individual attributes like for
example the `reply_to_msg_id` should be done by providing the current
values of this object, like so:
draft.set_message(
draft.text,
no_webpage=draft.no_webpage,
reply_to_msg_id=NEW_VALUE,
entities=draft.entities
)
:param str text: New text of the draft
:param bool no_webpage: Whether to attach a web page preview
:param int reply_to_msg_id: Message id to reply to
:param list entities: A list of formatting entities
:return bool: `True` on success
"""
result = self._client(SaveDraftRequest(
peer=self._peer,
message=text,
no_webpage=no_webpage,
reply_to_msg_id=reply_to_msg_id,
entities=entities
))
if result:
self.text = text
self.no_webpage = no_webpage
self.reply_to_msg_id = reply_to_msg_id
self.entities = entities
return result
def delete(self):
"""
Deletes this draft
:return bool: `True` on success
"""
return self.set_message(text='')

View File

@ -74,9 +74,7 @@ class EntityDatabase:
getattr(p, 'access_hash', 0) # chats won't have hash getattr(p, 'access_hash', 0) # chats won't have hash
if self.enabled_full: if self.enabled_full:
if isinstance(e, User) \ if isinstance(e, (User, Chat, Channel)):
or isinstance(e, Chat) \
or isinstance(e, Channel):
new.append(e) new.append(e)
except ValueError: except ValueError:
pass pass
@ -123,47 +121,64 @@ class EntityDatabase:
if phone: if phone:
self._username_id[phone] = marked_id self._username_id[phone] = marked_id
def __getitem__(self, key): def _parse_key(self, key):
"""Accepts a digit only string as phone number, """Parses the given string, integer or TLObject key into a
otherwise it's treated as an username. marked user ID ready for use on self._entities.
If an integer is given, it's treated as the ID of the desired User. If a callable key is given, the entity will be passed to the
The ID given won't try to be guessed as the ID of a chat or channel, function, and if it returns a true-like value, the marked ID
as there may be an user with that ID, and it would be unreliable. for such entity will be returned.
If a Peer is given (PeerUser, PeerChat, PeerChannel), Raises ValueError if it cannot be parsed.
its specific entity is retrieved as User, Chat or Channel.
Note that megagroups are channels with .megagroup = True.
""" """
if isinstance(key, str): if isinstance(key, str):
phone = EntityDatabase.parse_phone(key) phone = EntityDatabase.parse_phone(key)
try:
if phone: if phone:
return self._phone_id[phone] return self._phone_id[phone]
else: else:
key = key.lstrip('@').lower() return self._username_id[key.lstrip('@').lower()]
return self._entities[self._username_id[key]] except KeyError as e:
raise ValueError() from e
if isinstance(key, int): if isinstance(key, int):
return self._entities[key] # normal IDs are assumed users return key # normal IDs are assumed users
if isinstance(key, TLObject): if isinstance(key, TLObject):
sc = type(key).SUBCLASS_OF_ID return utils.get_peer_id(key, add_mark=True)
if sc == 0x2d45687:
# Subclass of "Peer"
return self._entities[utils.get_peer_id(key, add_mark=True)]
elif sc in {0x2da17977, 0xc5af5d94, 0x6d44b7db}:
# Subclass of "User", "Chat" or "Channel"
return key
raise KeyError(key) if callable(key):
for k, v in self._entities.items():
if key(v):
return k
raise ValueError()
def __getitem__(self, key):
"""See the ._parse_key() docstring for possible values of the key"""
try:
return self._entities[self._parse_key(key)]
except (ValueError, KeyError) as e:
raise KeyError(key) from e
def __delitem__(self, key): def __delitem__(self, key):
target = self[key] try:
del self._entities[key] old = self._entities.pop(self._parse_key(key))
if getattr(target, 'username'): # Try removing the username and phone (if pop didn't fail),
del self._username_id[target.username] # since the entity may have no username or phone, just ignore
# errors. It should be there if we popped the entity correctly.
try:
del self._username_id[getattr(old, 'username', None)]
except KeyError:
pass
# TODO Allow search by name by tokenizing the input and return a list try:
del self._phone_id[getattr(old, 'phone', None)]
except KeyError:
pass
except (ValueError, KeyError) as e:
raise KeyError(key) from e
@staticmethod @staticmethod
def parse_phone(phone): def parse_phone(phone):
@ -177,8 +192,10 @@ class EntityDatabase:
def get_input_entity(self, peer): def get_input_entity(self, peer):
try: try:
i, k = utils.get_peer_id(peer, add_mark=True, get_kind=True) i = utils.get_peer_id(peer, add_mark=True)
h = self._input_entities[i] h = self._input_entities[i] # we store the IDs marked
i, k = utils.resolve_id(i) # removes the mark and returns kind
if k == PeerUser: if k == PeerUser:
return InputPeerUser(i, h) return InputPeerUser(i, h)
elif k == PeerChat: elif k == PeerChat:

View File

@ -13,26 +13,26 @@ class GzipPacked(TLObject):
@staticmethod @staticmethod
def gzip_if_smaller(request): def gzip_if_smaller(request):
"""Calls request.to_bytes(), and based on a certain threshold, """Calls bytes(request), and based on a certain threshold,
optionally gzips the resulting data. If the gzipped data is optionally gzips the resulting data. If the gzipped data is
smaller than the original byte array, this is returned instead. smaller than the original byte array, this is returned instead.
Note that this only applies to content related requests. Note that this only applies to content related requests.
""" """
data = request.to_bytes() data = bytes(request)
# TODO This threshold could be configurable # TODO This threshold could be configurable
if request.content_related and len(data) > 512: if request.content_related and len(data) > 512:
gzipped = GzipPacked(data).to_bytes() gzipped = bytes(GzipPacked(data))
return gzipped if len(gzipped) < len(data) else data return gzipped if len(gzipped) < len(data) else data
else: else:
return data return data
def to_bytes(self): def __bytes__(self):
# TODO Maybe compress level could be an option # TODO Maybe compress level could be an option
return struct.pack('<I', GzipPacked.CONSTRUCTOR_ID) + \ return struct.pack('<I', GzipPacked.CONSTRUCTOR_ID) + \
TLObject.serialize_bytes(gzip.compress(self.data)) TLObject.serialize_bytes(gzip.compress(self.data))
@staticmethod @staticmethod
def read(reader): def read(reader):
reader.read_int(signed=False) # code assert reader.read_int(signed=False) == GzipPacked.CONSTRUCTOR_ID
return gzip.decompress(reader.tgread_bytes()) return gzip.decompress(reader.tgread_bytes())

View File

@ -11,10 +11,10 @@ class MessageContainer(TLObject):
self.content_related = False self.content_related = False
self.messages = messages self.messages = messages
def to_bytes(self): def __bytes__(self):
return struct.pack( return struct.pack(
'<Ii', MessageContainer.CONSTRUCTOR_ID, len(self.messages) '<Ii', MessageContainer.CONSTRUCTOR_ID, len(self.messages)
) + b''.join(m.to_bytes() for m in self.messages) ) + b''.join(bytes(m) for m in self.messages)
@staticmethod @staticmethod
def iter_read(reader): def iter_read(reader):

View File

@ -37,6 +37,7 @@ class Session:
self.lang_pack = session.lang_pack self.lang_pack = session.lang_pack
self.report_errors = session.report_errors self.report_errors = session.report_errors
self.save_entities = session.save_entities self.save_entities = session.save_entities
self.flood_sleep_threshold = session.flood_sleep_threshold
else: # str / None else: # str / None
self.session_user_id = session_user_id self.session_user_id = session_user_id
@ -50,6 +51,7 @@ class Session:
self.lang_pack = '' self.lang_pack = ''
self.report_errors = True self.report_errors = True
self.save_entities = True self.save_entities = True
self.flood_sleep_threshold = 60
# Cross-thread safety # Cross-thread safety
self._seq_no_lock = Lock() self._seq_no_lock = Lock()

View File

@ -12,6 +12,6 @@ class TLMessage(TLObject):
self.seq_no = session.generate_sequence(request.content_related) self.seq_no = session.generate_sequence(request.content_related)
self.request = request self.request = request
def to_bytes(self): def __bytes__(self):
body = GzipPacked.gzip_if_smaller(self.request) body = GzipPacked.gzip_if_smaller(self.request)
return struct.pack('<qii', self.msg_id, self.seq_no, len(body)) + body return struct.pack('<qii', self.msg_id, self.seq_no, len(body)) + body

View File

@ -1,3 +1,4 @@
from datetime import datetime
from threading import Event from threading import Event
@ -19,18 +20,14 @@ class TLObject:
""" """
if indent is None: if indent is None:
if isinstance(obj, TLObject): if isinstance(obj, TLObject):
children = obj.to_dict(recursive=False) return '{}({})'.format(type(obj).__name__, ', '.join(
if children: '{}={}'.format(k, TLObject.pretty_format(v))
return '{}: {}'.format( for k, v in obj.to_dict(recursive=False).items()
type(obj).__name__, TLObject.pretty_format(children) ))
)
else:
return type(obj).__name__
if isinstance(obj, dict): if isinstance(obj, dict):
return '{{{}}}'.format(', '.join( return '{{{}}}'.format(', '.join(
'{}: {}'.format( '{}: {}'.format(k, TLObject.pretty_format(v))
k, TLObject.pretty_format(v) for k, v in obj.items()
) for k, v in obj.items()
)) ))
elif isinstance(obj, str) or isinstance(obj, bytes): elif isinstance(obj, str) or isinstance(obj, bytes):
return repr(obj) return repr(obj)
@ -38,31 +35,36 @@ class TLObject:
return '[{}]'.format( return '[{}]'.format(
', '.join(TLObject.pretty_format(x) for x in obj) ', '.join(TLObject.pretty_format(x) for x in obj)
) )
elif isinstance(obj, datetime):
return 'datetime.fromtimestamp({})'.format(obj.timestamp())
else: else:
return str(obj) return repr(obj)
else: else:
result = [] result = []
if isinstance(obj, TLObject): if isinstance(obj, TLObject) or isinstance(obj, dict):
if isinstance(obj, dict):
d = obj
start, end, sep = '{', '}', ': '
else:
d = obj.to_dict(recursive=False)
start, end, sep = '(', ')', '='
result.append(type(obj).__name__) result.append(type(obj).__name__)
children = obj.to_dict(recursive=False)
if children:
result.append(': ')
result.append(TLObject.pretty_format(
obj.to_dict(recursive=False), indent
))
elif isinstance(obj, dict): result.append(start)
result.append('{\n') if d:
result.append('\n')
indent += 1 indent += 1
for k, v in obj.items(): for k, v in d.items():
result.append('\t' * indent) result.append('\t' * indent)
result.append(k) result.append(k)
result.append(': ') result.append(sep)
result.append(TLObject.pretty_format(v, indent)) result.append(TLObject.pretty_format(v, indent))
result.append(',\n') result.append(',\n')
result.pop() # last ',\n'
indent -= 1 indent -= 1
result.append('\n')
result.append('\t' * indent) result.append('\t' * indent)
result.append('}') result.append(end)
elif isinstance(obj, str) or isinstance(obj, bytes): elif isinstance(obj, str) or isinstance(obj, bytes):
result.append(repr(obj)) result.append(repr(obj))
@ -78,8 +80,13 @@ class TLObject:
result.append('\t' * indent) result.append('\t' * indent)
result.append(']') result.append(']')
elif isinstance(obj, datetime):
result.append('datetime.fromtimestamp(')
result.append(repr(obj.timestamp()))
result.append(')')
else: else:
result.append(str(obj)) result.append(repr(obj))
return ''.join(result) return ''.join(result)
@ -118,8 +125,9 @@ class TLObject:
def to_dict(self, recursive=True): def to_dict(self, recursive=True):
return {} return {}
def to_bytes(self): def __bytes__(self):
return b'' return b''
def on_response(self, reader): @staticmethod
pass def from_reader(reader):
return TLObject()

View File

@ -1,4 +1,5 @@
import logging import logging
import pickle
from collections import deque from collections import deque
from datetime import datetime from datetime import datetime
from threading import RLock, Event, Thread from threading import RLock, Event, Thread
@ -27,6 +28,7 @@ class UpdateState:
self._updates_lock = RLock() self._updates_lock = RLock()
self._updates_available = Event() self._updates_available = Event()
self._updates = deque() self._updates = deque()
self._latest_updates = deque(maxlen=10)
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
@ -141,6 +143,26 @@ class UpdateState:
self._state.pts = pts self._state.pts = pts
# TODO There must be a better way to handle updates rather than
# keeping a queue with the latest updates only, and handling
# the 'pts' correctly should be enough. However some updates
# like UpdateUserStatus (even inside UpdateShort) will be called
# repeatedly very often if invoking anything inside an update
# handler. TODO Figure out why.
"""
client = TelegramClient('anon', api_id, api_hash, update_workers=1)
client.connect()
def handle(u):
client.get_me()
client.add_update_handler(handle)
input('Enter to exit.')
"""
data = pickle.dumps(update.to_dict())
if data in self._latest_updates:
return # Duplicated too
self._latest_updates.append(data)
if type(update).SUBCLASS_OF_ID == 0x8af52aac: # crc32(b'Updates') if type(update).SUBCLASS_OF_ID == 0x8af52aac: # crc32(b'Updates')
# Expand "Updates" into "Update", and pass these to callbacks. # Expand "Updates" into "Update", and pass these to callbacks.
# Since .users and .chats have already been processed, we # Since .users and .chats have already been processed, we
@ -149,8 +171,7 @@ class UpdateState:
self._updates.append(update.update) self._updates.append(update.update)
self._updates_available.set() self._updates_available.set()
elif isinstance(update, tl.Updates) or \ elif isinstance(update, (tl.Updates, tl.UpdatesCombined)):
isinstance(update, tl.UpdatesCombined):
self._updates.extend(update.updates) self._updates.extend(update.updates)
self._updates_available.set() self._updates_available.set()

View File

@ -20,8 +20,8 @@ from .tl.types import (
GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty, GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty,
InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty, InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty,
FileLocationUnavailable, InputMediaUploadedDocument, FileLocationUnavailable, InputMediaUploadedDocument,
InputMediaUploadedPhoto, InputMediaUploadedPhoto, DocumentAttributeFilename, photos
DocumentAttributeFilename) )
def get_display_name(entity): def get_display_name(entity):
@ -37,7 +37,7 @@ def get_display_name(entity):
else: else:
return '(No name)' return '(No name)'
if isinstance(entity, Chat) or isinstance(entity, Channel): if isinstance(entity, (Chat, Channel)):
return entity.title return entity.title
return '(unknown)' return '(unknown)'
@ -51,8 +51,7 @@ def get_extension(media):
"""Gets the corresponding extension for any Telegram media""" """Gets the corresponding extension for any Telegram media"""
# Photos are always compressed as .jpg by Telegram # Photos are always compressed as .jpg by Telegram
if (isinstance(media, UserProfilePhoto) or isinstance(media, ChatPhoto) or if isinstance(media, (UserProfilePhoto, ChatPhoto, MessageMediaPhoto)):
isinstance(media, MessageMediaPhoto)):
return '.jpg' return '.jpg'
# Documents will come with a mime type # Documents will come with a mime type
@ -88,12 +87,10 @@ def get_input_peer(entity, allow_self=True):
else: else:
return InputPeerUser(entity.id, entity.access_hash) return InputPeerUser(entity.id, entity.access_hash)
if any(isinstance(entity, c) for c in ( if isinstance(entity, (Chat, ChatEmpty, ChatForbidden)):
Chat, ChatEmpty, ChatForbidden)):
return InputPeerChat(entity.id) return InputPeerChat(entity.id)
if any(isinstance(entity, c) for c in ( if isinstance(entity, (Channel, ChannelForbidden)):
Channel, ChannelForbidden)):
return InputPeerChannel(entity.id, entity.access_hash) return InputPeerChannel(entity.id, entity.access_hash)
# Less common cases # Less common cases
@ -123,7 +120,7 @@ def get_input_channel(entity):
if type(entity).SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel') if type(entity).SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel')
return entity return entity
if isinstance(entity, Channel) or isinstance(entity, ChannelForbidden): if isinstance(entity, (Channel, ChannelForbidden)):
return InputChannel(entity.id, entity.access_hash) return InputChannel(entity.id, entity.access_hash)
if isinstance(entity, InputPeerChannel): if isinstance(entity, InputPeerChannel):
@ -189,6 +186,9 @@ def get_input_photo(photo):
if type(photo).SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto') if type(photo).SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto')
return photo return photo
if isinstance(photo, photos.Photo):
photo = photo.photo
if isinstance(photo, Photo): if isinstance(photo, Photo):
return InputPhoto(id=photo.id, access_hash=photo.access_hash) return InputPhoto(id=photo.id, access_hash=photo.access_hash)
@ -264,7 +264,7 @@ def get_input_media(media, user_caption=None, is_photo=False):
if isinstance(media, MessageMediaGame): if isinstance(media, MessageMediaGame):
return InputMediaGame(id=media.game.id) return InputMediaGame(id=media.game.id)
if isinstance(media, ChatPhoto) or isinstance(media, UserProfilePhoto): if isinstance(media, (ChatPhoto, UserProfilePhoto)):
if isinstance(media.photo_big, FileLocationUnavailable): if isinstance(media.photo_big, FileLocationUnavailable):
return get_input_media(media.photo_small, is_photo=True) return get_input_media(media.photo_small, is_photo=True)
else: else:
@ -289,10 +289,9 @@ def get_input_media(media, user_caption=None, is_photo=False):
venue_id=media.venue_id venue_id=media.venue_id
) )
if any(isinstance(media, t) for t in ( if isinstance(media, (
MessageMediaEmpty, MessageMediaUnsupported, MessageMediaEmpty, MessageMediaUnsupported,
FileLocationUnavailable, ChatPhotoEmpty, ChatPhotoEmpty, UserProfilePhotoEmpty, FileLocationUnavailable)):
UserProfilePhotoEmpty)):
return InputMediaEmpty() return InputMediaEmpty()
if isinstance(media, Message): if isinstance(media, Message):
@ -301,16 +300,14 @@ def get_input_media(media, user_caption=None, is_photo=False):
_raise_cast_fail(media, 'InputMedia') _raise_cast_fail(media, 'InputMedia')
def get_peer_id(peer, add_mark=False, get_kind=False): def get_peer_id(peer, add_mark=False):
"""Finds the ID of the given peer, and optionally converts it to """Finds the ID of the given peer, and optionally converts it to
the "bot api" format if 'add_mark' is set to True. the "bot api" format if 'add_mark' is set to True.
If 'get_kind', the kind will be returned as a second value.
""" """
# First we assert it's a Peer TLObject, or early return for integers # First we assert it's a Peer TLObject, or early return for integers
if not isinstance(peer, TLObject): if not isinstance(peer, TLObject):
if isinstance(peer, int): if isinstance(peer, int):
return (peer, PeerUser) if get_kind else peer return peer
else: else:
_raise_cast_fail(peer, 'int') _raise_cast_fail(peer, 'int')
@ -319,25 +316,20 @@ def get_peer_id(peer, add_mark=False, get_kind=False):
peer = get_input_peer(peer, allow_self=False) peer = get_input_peer(peer, allow_self=False)
# Set the right ID/kind, or raise if the TLObject is not recognised # Set the right ID/kind, or raise if the TLObject is not recognised
i, k = None, None if isinstance(peer, (PeerUser, InputPeerUser)):
if isinstance(peer, PeerUser) or isinstance(peer, InputPeerUser): return peer.user_id
i, k = peer.user_id, PeerUser elif isinstance(peer, (PeerChat, InputPeerChat)):
elif isinstance(peer, PeerChat) or isinstance(peer, InputPeerChat): return -peer.chat_id if add_mark else peer.chat_id
i, k = peer.chat_id, PeerChat elif isinstance(peer, (PeerChannel, InputPeerChannel)):
elif isinstance(peer, PeerChannel) or isinstance(peer, InputPeerChannel): i = peer.channel_id
i, k = peer.channel_id, PeerChannel
else:
_raise_cast_fail(peer, 'int')
if add_mark: if add_mark:
if k == PeerChat:
i = -i
elif k == PeerChannel:
# Concat -100 through math tricks, .to_supergroup() on Madeline # Concat -100 through math tricks, .to_supergroup() on Madeline
# IDs will be strictly positive -> log works # IDs will be strictly positive -> log works
i = -(i + pow(10, math.floor(math.log10(i) + 3))) return -(i + pow(10, math.floor(math.log10(i) + 3)))
else:
return i
return (i, k) if get_kind else i # return kind only if get_kind _raise_cast_fail(peer, 'int')
def resolve_id(marked_id): def resolve_id(marked_id):
@ -384,11 +376,7 @@ def find_message(update):
def get_appropriated_part_size(file_size): def get_appropriated_part_size(file_size):
"""Gets the appropriated part size when uploading or downloading files, """Gets the appropriated part size when uploading or downloading files,
given an initial file size""" given an initial file size"""
if file_size <= 1048576: # 1MB if file_size <= 104857600: # 100MB
return 32
if file_size <= 10485760: # 10MB
return 64
if file_size <= 393216000: # 375MB
return 128 return 128
if file_size <= 786432000: # 750MB if file_size <= 786432000: # 750MB
return 256 return 256

View File

@ -0,0 +1,65 @@
# These are comments. Spaces around the = are optional. Empty lines ignored.
#CODE=Human readable description
FILE_MIGRATE_X=The file to be accessed is currently stored in DC {}
PHONE_MIGRATE_X=The phone number a user is trying to use for authorization is associated with DC {}
NETWORK_MIGRATE_X=The source IP address is associated with DC {}
USER_MIGRATE_X=The user whose identity is being used to execute queries is associated with DC {}
API_ID_INVALID=The api_id/api_hash combination is invalid
BOT_METHOD_INVALID=The API access for bot users is restricted. The method you tried to invoke cannot be executed as a bot
CDN_METHOD_INVALID=This method cannot be invoked on a CDN server. Refer to https://core.telegram.org/cdn#schema for available methods
CHANNEL_INVALID=Invalid channel object. Make sure to pass the right types, for instance making sure that the request is designed for channels or otherwise look for a different one more suited
CHANNEL_PRIVATE=The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it
CHAT_ADMIN_REQUIRED=Chat admin privileges are required to do that in the specified chat (for example, to send a message in a channel which is not yours)
CHAT_ID_INVALID=Invalid object ID for a chat. Make sure to pass the right types, for instance making sure that the request is designed for chats (not channels/megagroups) or otherwise look for a different one more suited\nAn example working with a megagroup and AddChatUserRequest, it will fail because megagroups are channels. Use InviteToChannelRequest instead
CONNECTION_LANG_PACK_INVALID=The specified language pack is not valid. This is meant to be used by official applications only so far, leave it empty
CONNECTION_LAYER_INVALID=The very first request must always be InvokeWithLayerRequest
DC_ID_INVALID=This occurs when an authorization is tried to be exported for the same data center one is currently connected to
FIELD_NAME_EMPTY=The field with the name FIELD_NAME is missing
FIELD_NAME_INVALID=The field with the name FIELD_NAME is invalid
FILE_PARTS_INVALID=The number of file parts is invalid
FILE_PART_X_MISSING=Part {} of the file is missing from storage
FILE_PART_INVALID=The file part number is invalid
FIRSTNAME_INVALID=The first name is invalid
INPUT_METHOD_INVALID=The invoked method does not exist anymore or has never existed
INPUT_REQUEST_TOO_LONG=The input request was too long. This may be a bug in the library as it can occur when serializing more bytes than it should (likeappending the vector constructor code at the end of a message)
LASTNAME_INVALID=The last name is invalid
LIMIT_INVALID=An invalid limit was provided. See https://core.telegram.org/api/files#downloading-files
LOCATION_INVALID=The location given for a file was invalid. See https://core.telegram.org/api/files#downloading-files
MD5_CHECKSUM_INVALID=The MD5 check-sums do not match
MESSAGE_EMPTY=Empty or invalid UTF-8 message was sent
MESSAGE_ID_INVALID=The specified message ID is invalid
MESSAGE_TOO_LONG=Message was too long. Current maximum length is 4096 UTF-8 characters
MESSAGE_NOT_MODIFIED=Content of the message was not modified
MSG_WAIT_FAILED=A waiting call returned an error
OFFSET_INVALID=The given offset was invalid, it must be divisible by 1KB. See https://core.telegram.org/api/files#downloading-files
PASSWORD_HASH_INVALID=The password (and thus its hash value) you entered is invalid
PEER_ID_INVALID=An invalid Peer was used. Make sure to pass the right peer type
PHONE_CODE_EMPTY=The phone code is missing
PHONE_CODE_EXPIRED=The confirmation code has expired
PHONE_CODE_HASH_EMPTY=The phone code hash is missing
PHONE_CODE_INVALID=The phone code entered was invalid
PHONE_NUMBER_BANNED=The used phone number has been banned from Telegram and cannot be used anymore. Maybe check https://www.telegram.org/faq_spam
PHONE_NUMBER_INVALID=The phone number is invalid
PHONE_NUMBER_OCCUPIED=The phone number is already in use
PHONE_NUMBER_UNOCCUPIED=The phone number is not yet being used
PHOTO_INVALID_DIMENSIONS=The photo dimensions are invalid
TYPE_CONSTRUCTOR_INVALID=The type constructor is invalid
USERNAME_INVALID=Unacceptable username. Must match r"[a-zA-Z][\w\d]{4,31}"
USERNAME_NOT_MODIFIED=The username is not different from the current username
USERNAME_NOT_OCCUPIED=The username is not in use by anyone else yet
USERNAME_OCCUPIED=The username is already taken
USERS_TOO_FEW=Not enough users (to create a chat, for example)
USERS_TOO_MUCH=The maximum number of users has been exceeded (to create a chat, for example)
USER_ID_INVALID=Invalid object ID for an user. Make sure to pass the right types, for instance making sure that the request is designed for users or otherwise look for a different one more suited
ACTIVE_USER_REQUIRED=The method is only available to already activated users
AUTH_KEY_INVALID=The key is invalid
AUTH_KEY_PERM_EMPTY=The method is unavailable for temporary authorization key, not bound to permanent
AUTH_KEY_UNREGISTERED=The key is not registered in the system
INVITE_HASH_EXPIRED=The chat the user tried to join has expired and is not valid anymore
SESSION_EXPIRED=The authorization has expired
SESSION_PASSWORD_NEEDED=Two-steps verification is enabled and a password is required
SESSION_REVOKED=The authorization has been invalidated, because of the user terminating all sessions
USER_ALREADY_PARTICIPANT=The authenticated user is already a participant of the chat
USER_DEACTIVATED=The user has been deleted/deactivated
FLOOD_WAIT_X=A wait of {} seconds is required

View File

@ -0,0 +1,170 @@
import json
import re
import urllib.request
from collections import defaultdict
URL = 'https://rpc.pwrtelegram.xyz/?all'
known_base_classes = {
303: 'InvalidDCError',
400: 'BadRequestError',
401: 'UnauthorizedError',
403: 'ForbiddenError',
404: 'NotFoundError',
420: 'FloodError',
500: 'ServerError',
}
# The API doesn't return the code for some (vital) errors. They are
# all assumed to be 400, except these well-known ones that aren't.
known_codes = {
'ACTIVE_USER_REQUIRED': 401,
'AUTH_KEY_UNREGISTERED': 401,
'USER_DEACTIVATED': 401
}
def fetch_errors(output, url=URL):
print('Opening a connection to', url, '...')
r = urllib.request.urlopen(url)
print('Checking response...')
data = json.loads(
r.read().decode(r.info().get_param('charset') or 'utf-8')
)
if data.get('ok'):
print('Response was okay, saving data')
with open(output, 'w', encoding='utf-8') as f:
json.dump(data, f)
return True
else:
print('The data received was not okay:')
print(json.dumps(data, indent=4))
return False
def get_class_name(error_code):
if isinstance(error_code, int):
return known_base_classes.get(
error_code, 'RPCError' + str(error_code).replace('-', 'Neg')
)
if 'FIRSTNAME' in error_code:
error_code = error_code.replace('FIRSTNAME', 'FIRST_NAME')
result = re.sub(
r'_([a-z])', lambda m: m.group(1).upper(), error_code.lower()
)
return result[:1].upper() + result[1:].replace('_', '') + 'Error'
def write_error(f, code, name, desc, capture_name):
f.write(
f'\n'
f'\n'
f'class {name}({get_class_name(code)}):\n'
f' def __init__(self, **kwargs):\n'
f' '
)
if capture_name:
f.write(
f"self.{capture_name} = int(kwargs.get('capture', 0))\n"
f" "
)
f.write(f'super(Exception, self).__init__(self, {repr(desc)}')
if capture_name:
f.write(f'.format(self.{capture_name})')
f.write(')\n')
def generate_code(output, json_file, errors_desc):
with open(json_file, encoding='utf-8') as f:
data = json.load(f)
errors = defaultdict(set)
# PWRTelegram's API doesn't return all errors, which we do need here.
# Add some special known-cases manually first.
errors[420].add('FLOOD_WAIT_X')
errors[401].update((
'AUTH_KEY_INVALID', 'SESSION_EXPIRED', 'SESSION_REVOKED'
))
errors[303].update((
'FILE_MIGRATE_X', 'PHONE_MIGRATE_X',
'NETWORK_MIGRATE_X', 'USER_MIGRATE_X'
))
for error_code, method_errors in data['result'].items():
for error_list in method_errors.values():
for error in error_list:
errors[int(error_code)].add(re.sub('_\d+', '_X', error).upper())
# Some errors are in the human result, but not with a code. Assume code 400
for error in data['human_result']:
if error[0] != '-' and not error.isdigit():
error = re.sub('_\d+', '_X', error).upper()
if not any(error in es for es in errors.values()):
errors[known_codes.get(error, 400)].add(error)
# Some error codes are not known, so create custom base classes if needed
needed_base_classes = [
(e, get_class_name(e)) for e in errors if e not in known_base_classes
]
# Prefer the descriptions that are related with Telethon way of coding to
# those that PWRTelegram's API provides.
telethon_descriptions = {}
with open(errors_desc, encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
equal = line.index('=')
message, description = line[:equal], line[equal + 1:]
telethon_descriptions[message.rstrip()] = description.lstrip()
# Names for the captures, or 'x' if unknown
capture_names = {
'FloodWaitError': 'seconds',
'FileMigrateError': 'new_dc',
'NetworkMigrateError': 'new_dc',
'PhoneMigrateError': 'new_dc',
'UserMigrateError': 'new_dc',
'FilePartMissingError': 'which'
}
# Everything ready, generate the code
with open(output, 'w', encoding='utf-8') as f:
f.write(
f'from .rpc_base_errors import RPCError, BadMessageError, '
f'{", ".join(known_base_classes.values())}\n'
)
for code, cls in needed_base_classes:
f.write(
f'\n'
f'\n'
f'class {cls}(RPCError):\n'
f' code = {code}\n'
)
patterns = [] # Save this dictionary later in the generated code
for error_code, error_set in errors.items():
for error in sorted(error_set):
description = telethon_descriptions.get(
error, '\n'.join(data['human_result'].get(
error, ['No description known.']
))
)
has_captures = '_X' in error
if has_captures:
name = get_class_name(error.replace('_X', ''))
pattern = error.replace('_X', r'_(\d+)')
else:
name, pattern = get_class_name(error), error
patterns.append((pattern, name))
capture = capture_names.get(name, 'x') if has_captures else None
# TODO Some errors have the same name but different code,
# split this accross different files?
write_error(f, error_code, name, description, capture)
f.write('\n\nrpc_errors_all = {\n')
for pattern, name in patterns:
f.write(f' {repr(pattern)}: {name},\n')
f.write('}\n')

File diff suppressed because one or more lines are too long

View File

@ -98,12 +98,17 @@ class TLObject:
def class_name(self): def class_name(self):
"""Gets the class name following the Python style guidelines""" """Gets the class name following the Python style guidelines"""
return self.class_name_for(self.name, self.is_function)
@staticmethod
def class_name_for(typename, is_function=False):
"""Gets the class name following the Python style guidelines"""
# Courtesy of http://stackoverflow.com/a/31531797/4759433 # Courtesy of http://stackoverflow.com/a/31531797/4759433
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), self.name) result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(),
typename)
result = result[:1].upper() + result[1:].replace('_', '') result = result[:1].upper() + result[1:].replace('_', '')
# If it's a function, let it end with "Request" to identify them # If it's a function, let it end with "Request" to identify them
if self.is_function: if is_function:
result += 'Request' result += 'Request'
return result return result
@ -192,6 +197,7 @@ class TLArg:
# Default values # Default values
self.is_vector = False self.is_vector = False
self.is_flag = False self.is_flag = False
self.skip_constructor_id = False
self.flag_index = -1 self.flag_index = -1
# Special case: some types can be inferred, which makes it # Special case: some types can be inferred, which makes it
@ -222,7 +228,7 @@ class TLArg:
self.type = flag_match.group(2) self.type = flag_match.group(2)
# Then check if the type is a Vector<REAL_TYPE> # Then check if the type is a Vector<REAL_TYPE>
vector_match = re.match(r'vector<(\w+)>', self.type, re.IGNORECASE) vector_match = re.match(r'[Vv]ector<([\w\d.]+)>', self.type)
if vector_match: if vector_match:
self.is_vector = True self.is_vector = True
@ -234,6 +240,11 @@ class TLArg:
# Update the type to match the one inside the vector # Update the type to match the one inside the vector
self.type = vector_match.group(1) self.type = vector_match.group(1)
# See use_vector_id. An example of such case is ipPort in
# help.configSpecial
if self.type.split('.')[-1][0].islower():
self.skip_constructor_id = True
# The name may contain "date" in it, if this is the case and the type is "int", # The name may contain "date" in it, if this is the case and the type is "int",
# we can safely assume that this should be treated as a "date" object. # we can safely assume that this should be treated as a "date" object.
# Note that this is not a valid Telegram object, but it's easier to work with # Note that this is not a valid Telegram object, but it's easier to work with

View File

@ -5,7 +5,7 @@ import struct
from zlib import crc32 from zlib import crc32
from collections import defaultdict from collections import defaultdict
from .parser import SourceBuilder, TLParser from .parser import SourceBuilder, TLParser, TLObject
AUTO_GEN_NOTICE = \ AUTO_GEN_NOTICE = \
'"""File generated by TLObjects\' generator. All changes will be ERASED"""' '"""File generated by TLObjects\' generator. All changes will be ERASED"""'
@ -129,6 +129,9 @@ class TLGenerator:
builder.writeln( builder.writeln(
'from {}.tl.tlobject import TLObject'.format('.' * depth) 'from {}.tl.tlobject import TLObject'.format('.' * depth)
) )
builder.writeln(
'from {}.tl import types'.format('.' * depth)
)
# Add the relative imports to the namespaces, # Add the relative imports to the namespaces,
# unless we already are in a namespace. # unless we already are in a namespace.
@ -143,7 +146,7 @@ class TLGenerator:
builder.writeln( builder.writeln(
'from {}.utils import get_input_peer, ' 'from {}.utils import get_input_peer, '
'get_input_channel, get_input_user, ' 'get_input_channel, get_input_user, '
'get_input_media'.format('.' * depth) 'get_input_media, get_input_photo'.format('.' * depth)
) )
# Import 'os' for those needing access to 'os.urandom()' # Import 'os' for those needing access to 'os.urandom()'
@ -151,7 +154,7 @@ class TLGenerator:
# for all those TLObjects with arg.can_be_inferred. # for all those TLObjects with arg.can_be_inferred.
builder.writeln('import os') builder.writeln('import os')
# Import struct for the .to_bytes(self) serialization # Import struct for the .__bytes__(self) serialization
builder.writeln('import struct') builder.writeln('import struct')
# Generate the class for every TLObject # Generate the class for every TLObject
@ -299,8 +302,8 @@ class TLGenerator:
builder.end_block() builder.end_block()
# Write the .to_bytes() function # Write the .__bytes__() function
builder.writeln('def to_bytes(self):') builder.writeln('def __bytes__(self):')
# Some objects require more than one flag parameter to be set # Some objects require more than one flag parameter to be set
# at the same time. In this case, add an assertion. # at the same time. In this case, add an assertion.
@ -311,11 +314,11 @@ class TLGenerator:
for ra in repeated_args.values(): for ra in repeated_args.values():
if len(ra) > 1: if len(ra) > 1:
cnd1 = ('self.{} is None'.format(a.name) for a in ra) cnd1 = ('self.{}'.format(a.name) for a in ra)
cnd2 = ('self.{} is not None'.format(a.name) for a in ra) cnd2 = ('not self.{}'.format(a.name) for a in ra)
builder.writeln( builder.writeln(
"assert ({}) or ({}), '{} parameters must all " "assert ({}) or ({}), '{} parameters must all "
"be None or neither be None'".format( "be False-y (like None) or all me True-y'".format(
' and '.join(cnd1), ' and '.join(cnd2), ' and '.join(cnd1), ' and '.join(cnd2),
', '.join(a.name for a in ra) ', '.join(a.name for a in ra)
) )
@ -335,31 +338,27 @@ class TLGenerator:
builder.writeln('))') builder.writeln('))')
builder.end_block() builder.end_block()
# Write the empty() function, which returns an "empty" # Write the static from_reader(reader) function
# instance, in which all attributes are set to None
builder.writeln('@staticmethod') builder.writeln('@staticmethod')
builder.writeln('def empty():') builder.writeln('def from_reader(reader):')
for arg in tlobject.args:
TLGenerator.write_read_code(
builder, arg, tlobject.args, name='_' + arg.name
)
builder.writeln('return {}({})'.format( builder.writeln('return {}({})'.format(
tlobject.class_name(), ', '.join('None' for _ in range(len(args))) tlobject.class_name(), ', '.join(
'{0}=_{0}'.format(a.name) for a in tlobject.sorted_args()
if not a.flag_indicator and not a.generic_definition
)
)) ))
builder.end_block() builder.end_block()
# Write the on_response(self, reader) function # Only requests can have a different response that's not their
builder.writeln('def on_response(self, reader):') # serialized body, that is, we'll be setting their .result.
# Do not read constructor's ID, since
# that's already been read somewhere else
if tlobject.is_function: if tlobject.is_function:
builder.writeln('def on_response(self, reader):')
TLGenerator.write_request_result_code(builder, tlobject) TLGenerator.write_request_result_code(builder, tlobject)
else:
if tlobject.args:
for arg in tlobject.args:
TLGenerator.write_onresponse_code(
builder, arg, tlobject.args
)
else:
# If there were no arguments, we still need an
# on_response method, and hence "pass" if empty
builder.writeln('pass')
builder.end_block() builder.end_block()
# Write the __str__(self) and stringify(self) functions # Write the __str__(self) and stringify(self) functions
@ -406,6 +405,8 @@ class TLGenerator:
TLGenerator.write_get_input(builder, arg, 'get_input_user') TLGenerator.write_get_input(builder, arg, 'get_input_user')
elif arg.type == 'InputMedia' and tlobject.is_function: elif arg.type == 'InputMedia' and tlobject.is_function:
TLGenerator.write_get_input(builder, arg, 'get_input_media') TLGenerator.write_get_input(builder, arg, 'get_input_media')
elif arg.type == 'InputPhoto' and tlobject.is_function:
TLGenerator.write_get_input(builder, arg, 'get_input_photo')
else: else:
builder.writeln('self.{0} = {0}'.format(arg.name)) builder.writeln('self.{0} = {0}'.format(arg.name))
@ -440,10 +441,10 @@ class TLGenerator:
@staticmethod @staticmethod
def write_to_bytes(builder, arg, args, name=None): def write_to_bytes(builder, arg, args, name=None):
""" """
Writes the .to_bytes() code for the given argument Writes the .__bytes__() code for the given argument
:param builder: The source code builder :param builder: The source code builder
:param arg: The argument to write :param arg: The argument to write
:param args: All the other arguments in TLObject same to_bytes. :param args: All the other arguments in TLObject same __bytes__.
This is required to determine the flags value This is required to determine the flags value
:param name: The name of the argument. Defaults to "self.argname" :param name: The name of the argument. Defaults to "self.argname"
This argument is an option because it's required when This argument is an option because it's required when
@ -539,7 +540,7 @@ class TLGenerator:
else: else:
# Else it may be a custom type # Else it may be a custom type
builder.write('{}.to_bytes()'.format(name)) builder.write('bytes({})'.format(name))
if arg.is_flag: if arg.is_flag:
builder.write(')') builder.write(')')
@ -549,9 +550,10 @@ class TLGenerator:
return True # Something was written return True # Something was written
@staticmethod @staticmethod
def write_onresponse_code(builder, arg, args, name=None): def write_read_code(builder, arg, args, name):
""" """
Writes the receive code for the given argument Writes the read code for the given argument, setting the
arg.name variable to its read value.
:param builder: The source code builder :param builder: The source code builder
:param arg: The argument to write :param arg: The argument to write
@ -565,12 +567,17 @@ class TLGenerator:
if arg.generic_definition: if arg.generic_definition:
return # Do nothing, this only specifies a later type return # Do nothing, this only specifies a later type
if name is None:
name = 'self.{}'.format(arg.name)
# The argument may be a flag, only write that flag was given! # The argument may be a flag, only write that flag was given!
was_flag = False was_flag = False
if arg.is_flag: if arg.is_flag:
# Treat 'true' flags as a special case, since they're true if
# they're set, and nothing else needs to actually be read.
if 'true' == arg.type:
builder.writeln(
'{} = bool(flags & {})'.format(name, 1 << arg.flag_index)
)
return
was_flag = True was_flag = True
builder.writeln('if flags & {}:'.format( builder.writeln('if flags & {}:'.format(
1 << arg.flag_index 1 << arg.flag_index
@ -585,11 +592,10 @@ class TLGenerator:
builder.writeln("reader.read_int()") builder.writeln("reader.read_int()")
builder.writeln('{} = []'.format(name)) builder.writeln('{} = []'.format(name))
builder.writeln('_len = reader.read_int()') builder.writeln('for _ in range(reader.read_int()):')
builder.writeln('for _ in range(_len):')
# Temporary disable .is_vector, not to enter this if again # Temporary disable .is_vector, not to enter this if again
arg.is_vector = False arg.is_vector = False
TLGenerator.write_onresponse_code(builder, arg, args, name='_x') TLGenerator.write_read_code(builder, arg, args, name='_x')
builder.writeln('{}.append(_x)'.format(name)) builder.writeln('{}.append(_x)'.format(name))
arg.is_vector = True arg.is_vector = True
@ -635,14 +641,21 @@ class TLGenerator:
else: else:
# Else it may be a custom type # Else it may be a custom type
if not arg.skip_constructor_id:
builder.writeln('{} = reader.tgread_object()'.format(name)) builder.writeln('{} = reader.tgread_object()'.format(name))
else:
builder.writeln('{} = types.{}.from_reader(reader)'.format(
name, TLObject.class_name_for(arg.type)))
# End vector and flag blocks if required (if we opened them before) # End vector and flag blocks if required (if we opened them before)
if arg.is_vector: if arg.is_vector:
builder.end_block() builder.end_block()
if was_flag: if was_flag:
builder.end_block() builder.current_indent -= 1
builder.writeln('else:')
builder.writeln('{} = None'.format(name))
builder.current_indent -= 1
# Restore .is_flag # Restore .is_flag
arg.is_flag = True arg.is_flag = True

View File

@ -107,17 +107,17 @@ class CryptoTests(unittest.TestCase):
@staticmethod @staticmethod
def test_generate_key_data_from_nonce(): def test_generate_key_data_from_nonce():
server_nonce = b'I am the server nonce.' server_nonce = int.from_bytes(b'The 16-bit nonce', byteorder='little')
new_nonce = b'I am a new calculated nonce.' new_nonce = int.from_bytes(b'The new, calculated 32-bit nonce', byteorder='little')
key, iv = utils.generate_key_data_from_nonce(server_nonce, new_nonce) key, iv = utils.generate_key_data_from_nonce(server_nonce, new_nonce)
expected_key = b'?\xc4\xbd\xdf\rWU\x8a\xf5\x0f+V\xdc\x96up\x1d\xeeG\x00\x81|\x1eg\x8a\x8f{\xf0y\x80\xda\xde' expected_key = b'/\xaa\x7f\xa1\xfcs\xef\xa0\x99zh\x03M\xa4\x8e\xb4\xab\x0eE]b\x95|\xfe\xc0\xf8\x1f\xd4\xa0\xd4\xec\x91'
expected_iv = b'Q\x9dpZ\xb7\xdd\xcb\x82_\xfa\xf4\x90\xecn\x10\x9cD\xd2\x01\x8d\x83\xa0\xa4^\xb8\x91,\x7fI am' expected_iv = b'\xf7\xae\xe3\xc8+=\xc2\xb8\xd1\xe1\x1b\x0e\x10\x07\x9fn\x9e\xdc\x960\x05\xf9\xea\xee\x8b\xa1h The '
assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format( assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format(
key, expected_key) key, expected_key)
assert iv == expected_iv, 'Key ("{}") does not equal expected ("{}")'.format( assert iv == expected_iv, 'IV ("{}") does not equal expected ("{}")'.format(
key, expected_iv) iv, expected_iv)
@staticmethod @staticmethod
def test_fingerprint_from_key(): def test_fingerprint_from_key():