Merge branch 'master' of https://github.com/fdhadzh/Telethon into fdhadzh-master

This commit is contained in:
Lonami Exo 2016-11-30 15:56:30 +01:00
commit 4862ef1dce
31 changed files with 767 additions and 526 deletions

22
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,22 @@
- repo: git://github.com/pre-commit/pre-commit-hooks
sha: 7539d8bd1a00a3c1bfd34cdb606d3a6372e83469
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: double-quote-string-fixer
- id: end-of-file-fixer
- id: name-tests-test
- id: trailing-whitespace
- repo: git://github.com/pre-commit/mirrors-yapf
sha: v0.11.1
hooks:
- id: yapf
- repo: git://github.com/FalconSocial/pre-commit-python-sorter
sha: 1.0.4
hooks:
- id: python-import-sorter
args:
- --silent-overwrite

View File

@ -1,6 +1,5 @@
import unittest import unittest
if __name__ == '__main__': if __name__ == '__main__':
from telethon_tests import CryptoTests, ParserTests, TLTests, UtilsTests, NetworkTests from telethon_tests import CryptoTests, ParserTests, TLTests, UtilsTests, NetworkTests
test_classes = [CryptoTests, ParserTests, TLTests, UtilsTests] test_classes = [CryptoTests, ParserTests, TLTests, UtilsTests]

View File

@ -5,14 +5,15 @@ https://packaging.python.org/en/latest/distributing.html
https://github.com/pypa/sampleproject https://github.com/pypa/sampleproject
""" """
from telethon import TelegramClient
# Always prefer setuptools over distutils
from setuptools import setup, find_packages
# To use a consistent encoding # To use a consistent encoding
from codecs import open from codecs import open
from os import path from os import path
# Always prefer setuptools over distutils
from setuptools import find_packages, setup
from telethon import TelegramClient
here = path.abspath(path.dirname(__file__)) here = path.abspath(path.dirname(__file__))
# Get the long description from the README file # Get the long description from the README file
@ -24,7 +25,6 @@ setup(
# Versions should comply with PEP440. # Versions should comply with PEP440.
version=TelegramClient.__version__, version=TelegramClient.__version__,
description="Python3 Telegram's client implementation with full access to its API", description="Python3 Telegram's client implementation with full access to its API",
long_description=long_description, long_description=long_description,
@ -63,15 +63,14 @@ setup(
], ],
# What does your project relate to? # What does your project relate to?
keywords='telegram api chat client mtproto', keywords='Telegram API chat client MTProto',
# You can just specify the packages manually here if your project is # You can just specify the packages manually here if your project is
# simple. Or you can use find_packages(). # simple. Or you can use find_packages().
packages=find_packages(exclude=[ packages=find_packages(exclude=[
'telethon_generator', 'telethon_generator', 'telethon_tests', 'run_tests.py',
'telethon_tests', 'try_telethon.py'
'run_tests.py', ]),
'try_telethon.py']),
# List run-time dependencies here. These will be installed by pip when # List run-time dependencies here. These will be installed by pip when
# your project is installed. # your project is installed.
@ -84,5 +83,4 @@ setup(
'console_scripts': [ 'console_scripts': [
'gen_tl = tl_generator:clean_and_generate', 'gen_tl = tl_generator:clean_and_generate',
], ],
} })
)

View File

@ -6,8 +6,8 @@ class AES:
@staticmethod @staticmethod
def decrypt_ige(cipher_text, key, iv): def decrypt_ige(cipher_text, key, iv):
"""Decrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector""" """Decrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector"""
iv1 = iv[:len(iv)//2] iv1 = iv[:len(iv) // 2]
iv2 = iv[len(iv)//2:] iv2 = iv[len(iv) // 2:]
aes = pyaes.AES(key) aes = pyaes.AES(key)
@ -17,7 +17,8 @@ class AES:
cipher_text_block = [0] * 16 cipher_text_block = [0] * 16
for block_index in range(blocks_count): for block_index in range(blocks_count):
for i in range(16): for i in range(16):
cipher_text_block[i] = cipher_text[block_index * 16 + i] ^ iv2[i] cipher_text_block[i] = cipher_text[block_index * 16 + i] ^ iv2[
i]
plain_text_block = aes.decrypt(cipher_text_block) plain_text_block = aes.decrypt(cipher_text_block)
@ -40,8 +41,8 @@ class AES:
padding_count = 16 - len(plain_text) % 16 padding_count = 16 - len(plain_text) % 16
plain_text += os.urandom(padding_count) plain_text += os.urandom(padding_count)
iv1 = iv[:len(iv)//2] iv1 = iv[:len(iv) // 2]
iv2 = iv[len(iv)//2:] iv2 = iv[len(iv) // 2:]
aes = pyaes.AES(key) aes = pyaes.AES(key)
@ -49,7 +50,8 @@ class AES:
blocks_count = len(plain_text) // 16 blocks_count = len(plain_text) // 16
for block_index in range(blocks_count): for block_index in range(blocks_count):
plain_text_block = list(plain_text[block_index * 16:block_index * 16 + 16]) plain_text_block = list(plain_text[block_index * 16:block_index *
16 + 16])
for i in range(16): for i in range(16):
plain_text_block[i] ^= iv1[i] plain_text_block[i] ^= iv1[i]

View File

@ -1,5 +1,5 @@
from telethon.utils import BinaryWriter, BinaryReader
import telethon.helpers as utils import telethon.helpers as utils
from telethon.utils import BinaryReader, BinaryWriter
class AuthKey: class AuthKey:

View File

@ -1,7 +1,8 @@
from telethon.utils import BinaryWriter
import telethon.helpers as utils
import os import os
import telethon.helpers as utils
from telethon.utils import BinaryWriter
class RSAServerKey: class RSAServerKey:
def __init__(self, fingerprint, m, e): def __init__(self, fingerprint, m, e):
@ -18,9 +19,9 @@ class RSAServerKey:
with BinaryWriter() as writer: with BinaryWriter() as writer:
# Write SHA # Write SHA
writer.write(utils.sha1(data[offset:offset+length])) writer.write(utils.sha1(data[offset:offset + length]))
# Write data # Write data
writer.write(data[offset:offset+length]) writer.write(data[offset:offset + length])
# Add padding if required # Add padding if required
if length < 235: if length < 235:
writer.write(os.urandom(235 - length)) writer.write(os.urandom(235 - length))
@ -31,13 +32,14 @@ class RSAServerKey:
# If the result byte count is less than 256, since the byte order is big, # If the result byte count is less than 256, since the byte order is big,
# the non-used bytes on the left will be 0 and act as padding, # the non-used bytes on the left will be 0 and act as padding,
# without need of any additional checks # without need of any additional checks
return int.to_bytes(result, length=256, byteorder='big', signed=False) return int.to_bytes(
result, length=256, byteorder='big', signed=False)
class RSA: class RSA:
_server_keys = { _server_keys = {
'216be86c022bb4c3': '216be86c022bb4c3': RSAServerKey('216be86c022bb4c3', int(
RSAServerKey('216be86c022bb4c3', int('C150023E2F70DB7985DED064759CFECF0AF328E69A41DAF4D6F01B538135A6F9' 'C150023E2F70DB7985DED064759CFECF0AF328E69A41DAF4D6F01B538135A6F9'
'1F8F8B2A0EC9BA9720CE352EFCF6C5680FFC424BD634864902DE0B4BD6D49F4E' '1F8F8B2A0EC9BA9720CE352EFCF6C5680FFC424BD634864902DE0B4BD6D49F4E'
'580230E3AE97D95C8B19442B3C0A10D8F5633FECEDD6926A7F6DAB0DDB7D457F' '580230E3AE97D95C8B19442B3C0A10D8F5633FECEDD6926A7F6DAB0DDB7D457F'
'9EA81B8465FCD6FFFEED114011DF91C059CAEDAF97625F6C96ECC74725556934' '9EA81B8465FCD6FFFEED114011DF91C059CAEDAF97625F6C96ECC74725556934'

View File

@ -3,6 +3,7 @@ import re
class ReadCancelledError(Exception): class ReadCancelledError(Exception):
"""Occurs when a read operation was cancelled""" """Occurs when a read operation was cancelled"""
def __init__(self): def __init__(self):
super().__init__(self, 'The read operation was cancelled.') super().__init__(self, 'The read operation was cancelled.')
@ -15,8 +16,10 @@ class InvalidParameterError(Exception):
class TypeNotFoundError(Exception): class TypeNotFoundError(Exception):
"""Occurs when a type is not found, for example, """Occurs when a type is not found, for example,
when trying to read a TLObject with an invalid constructor code""" when trying to read a TLObject with an invalid constructor code"""
def __init__(self, invalid_constructor_id): def __init__(self, invalid_constructor_id):
super().__init__(self, 'Could not find a matching Constructor ID for the TLObject ' super().__init__(
self, 'Could not find a matching Constructor ID for the TLObject '
'that was supposed to be read with ID {}. Most likely, a TLObject ' 'that was supposed to be read with ID {}. Most likely, a TLObject '
'was trying to be read when it should not be read.' 'was trying to be read when it should not be read.'
.format(hex(invalid_constructor_id))) .format(hex(invalid_constructor_id)))
@ -26,7 +29,8 @@ class TypeNotFoundError(Exception):
class InvalidDCError(Exception): class InvalidDCError(Exception):
def __init__(self, new_dc): def __init__(self, new_dc):
super().__init__(self, 'Your phone number is registered to #{} DC. ' super().__init__(
self, 'Your phone number is registered to #{} DC. '
'This should have been handled automatically; ' 'This should have been handled automatically; '
'if it has not, please restart the app.'.format(new_dc)) 'if it has not, please restart the app.'.format(new_dc))
@ -35,7 +39,9 @@ class InvalidDCError(Exception):
class InvalidChecksumError(Exception): class InvalidChecksumError(Exception):
def __init__(self, checksum, valid_checksum): def __init__(self, checksum, valid_checksum):
super().__init__(self, 'Invalid checksum ({} when {} was expected). This packet should be skipped.' super().__init__(
self,
'Invalid checksum ({} when {} was expected). This packet should be skipped.'
.format(checksum, valid_checksum)) .format(checksum, valid_checksum))
self.checksum = checksum self.checksum = checksum
@ -45,105 +51,95 @@ class InvalidChecksumError(Exception):
class RPCError(Exception): class RPCError(Exception):
CodeMessages = { CodeMessages = {
303: ('ERROR_SEE_OTHER', 'The request must be repeated, but directed to a different data center.'), 303:
('ERROR_SEE_OTHER',
400: ('BAD_REQUEST', 'The query contains errors. In the event that a request was created using a ' 'The request must be repeated, but directed to a different data center.'
),
400:
('BAD_REQUEST',
'The query contains errors. In the event that a request was created using a '
'form and contains user generated data, the user should be notified that the ' 'form and contains user generated data, the user should be notified that the '
'data must be corrected before the query is repeated.'), 'data must be corrected before the query is repeated.'),
401:
401: ('UNAUTHORIZED', 'There was an unauthorized attempt to use functionality available only to ' ('UNAUTHORIZED',
'There was an unauthorized attempt to use functionality available only to '
'authorized users.'), 'authorized users.'),
403:
403: ('FORBIDDEN', 'Privacy violation. For example, an attempt to write a message to someone who ' ('FORBIDDEN',
'Privacy violation. For example, an attempt to write a message to someone who '
'has blacklisted the current user.'), 'has blacklisted the current user.'),
404: ('NOT_FOUND',
404: ('NOT_FOUND', 'An attempt to invoke a non-existent object, such as a method.'), 'An attempt to invoke a non-existent object, such as a method.'),
420:
420: ('FLOOD', 'The maximum allowed number of attempts to invoke the given method with ' ('FLOOD',
'The maximum allowed number of attempts to invoke the given method with '
'the given input parameters has been exceeded. For example, in an attempt ' 'the given input parameters has been exceeded. For example, in an attempt '
'to request a large number of text messages (SMS) for the same phone number.'), 'to request a large number of text messages (SMS) for the same phone number.'
),
500: ('INTERNAL', 'An internal server error occurred while a request was being processed; ' 500:
'for example, there was a disruption while accessing a database or file storage.') ('INTERNAL',
'An internal server error occurred while a request was being processed; '
'for example, there was a disruption while accessing a database or file storage.'
)
} }
ErrorMessages = { ErrorMessages = {
# 303 ERROR_SEE_OTHER # 303 ERROR_SEE_OTHER
'FILE_MIGRATE_(\d+)': 'The file to be accessed is currently stored in a different data center (#{}).', 'FILE_MIGRATE_(\d+)':
'The file to be accessed is currently stored in a different data center (#{}).',
'PHONE_MIGRATE_(\d+)': 'The phone number a user is trying to use for authorization is associated ' 'PHONE_MIGRATE_(\d+)':
'The phone number a user is trying to use for authorization is associated '
'with a different data center (#{}).', 'with a different data center (#{}).',
'NETWORK_MIGRATE_(\d+)':
'NETWORK_MIGRATE_(\d+)': 'The source IP address is associated with a different data center (#{}, ' 'The source IP address is associated with a different data center (#{}, '
'for registration).', 'for registration).',
'USER_MIGRATE_(\d+)':
'USER_MIGRATE_(\d+)': 'The user whose identity is being used to execute queries is associated with ' 'The user whose identity is being used to execute queries is associated with '
'a different data center (#{} for registration).', 'a different data center (#{} for registration).',
# 400 BAD_REQUEST # 400 BAD_REQUEST
'FIRSTNAME_INVALID': 'The first name is invalid.', 'FIRSTNAME_INVALID': 'The first name is invalid.',
'LASTNAME_INVALID': 'The last name is invalid.', 'LASTNAME_INVALID': 'The last name is invalid.',
'PHONE_NUMBER_INVALID': 'The phone number is invalid.', 'PHONE_NUMBER_INVALID': 'The phone number is invalid.',
'PHONE_CODE_HASH_EMPTY': 'The phone code hash is missing.', 'PHONE_CODE_HASH_EMPTY': 'The phone code hash is missing.',
'PHONE_CODE_EMPTY': 'The phone code is missing.', 'PHONE_CODE_EMPTY': 'The phone code is missing.',
'PHONE_CODE_INVALID': 'The phone code entered was invalid.', 'PHONE_CODE_INVALID': 'The phone code entered was invalid.',
'PHONE_CODE_EXPIRED': 'The confirmation code has expired.', 'PHONE_CODE_EXPIRED': 'The confirmation code has expired.',
'API_ID_INVALID': 'The api_id/api_hash combination is invalid.', 'API_ID_INVALID': 'The api_id/api_hash combination is invalid.',
'PHONE_NUMBER_OCCUPIED': 'The phone number is already in use.', 'PHONE_NUMBER_OCCUPIED': 'The phone number is already in use.',
'PHONE_NUMBER_UNOCCUPIED': 'The phone number is not yet being used.', 'PHONE_NUMBER_UNOCCUPIED': 'The phone number is not yet being used.',
'USERS_TOO_FEW': 'Not enough users (to create a chat, for example).', 'USERS_TOO_FEW': 'Not enough users (to create a chat, for example).',
'USERS_TOO_MUCH':
'USERS_TOO_MUCH': 'The maximum number of users has been exceeded (to create a chat, for example).', 'The maximum number of users has been exceeded (to create a chat, for example).',
'TYPE_CONSTRUCTOR_INVALID': 'The type constructor is invalid.', 'TYPE_CONSTRUCTOR_INVALID': 'The type constructor is invalid.',
'FILE_PART_INVALID': 'The file part number is invalid.', 'FILE_PART_INVALID': 'The file part number is invalid.',
'FILE_PARTS_INVALID': 'The number of file parts is invalid.', 'FILE_PARTS_INVALID': 'The number of file parts is invalid.',
'FILE_PART_(\d+)_MISSING':
'FILE_PART_(\d+)_MISSING': 'Part {} of the file is missing from storage.', 'Part {} of the file is missing from storage.',
'MD5_CHECKSUM_INVALID': 'The MD5 checksums do not match.', 'MD5_CHECKSUM_INVALID': 'The MD5 checksums do not match.',
'PHOTO_INVALID_DIMENSIONS': 'The photo dimensions are invalid.', 'PHOTO_INVALID_DIMENSIONS': 'The photo dimensions are invalid.',
'FIELD_NAME_INVALID': 'The field with the name FIELD_NAME is invalid.', 'FIELD_NAME_INVALID': 'The field with the name FIELD_NAME is invalid.',
'FIELD_NAME_EMPTY': 'The field with the name FIELD_NAME is missing.', 'FIELD_NAME_EMPTY': 'The field with the name FIELD_NAME is missing.',
'MSG_WAIT_FAILED': 'A waiting call returned an error.', 'MSG_WAIT_FAILED': 'A waiting call returned an error.',
'CHAT_ADMIN_REQUIRED':
'CHAT_ADMIN_REQUIRED': 'Chat admin privileges are required to do that in the specified chat ' '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).', '(for example, to send a message in a channel which is not yours).',
'PASSWORD_HASH_INVALID':
'PASSWORD_HASH_INVALID': 'The password (and thus its hash value) you entered is invalid.', 'The password (and thus its hash value) you entered is invalid.',
# 401 UNAUTHORIZED # 401 UNAUTHORIZED
'AUTH_KEY_UNREGISTERED': 'The key is not registered in the system.', 'AUTH_KEY_UNREGISTERED': 'The key is not registered in the system.',
'AUTH_KEY_INVALID': 'The key is invalid.', 'AUTH_KEY_INVALID': 'The key is invalid.',
'USER_DEACTIVATED': 'The user has been deleted/deactivated.', 'USER_DEACTIVATED': 'The user has been deleted/deactivated.',
'SESSION_REVOKED':
'SESSION_REVOKED': 'The authorization has been invalidated, because of the user terminating all sessions.', 'The authorization has been invalidated, because of the user terminating all sessions.',
'SESSION_EXPIRED': 'The authorization has expired.', 'SESSION_EXPIRED': 'The authorization has expired.',
'ACTIVE_USER_REQUIRED':
'ACTIVE_USER_REQUIRED': 'The method is only available to already activated users.', 'The method is only available to already activated users.',
'AUTH_KEY_PERM_EMPTY':
'AUTH_KEY_PERM_EMPTY': 'The method is unavailable for temporary authorization key, not bound to permanent.', 'The method is unavailable for temporary authorization key, not bound to permanent.',
'SESSION_PASSWORD_NEEDED':
'SESSION_PASSWORD_NEEDED': 'Two-steps verification is enabled and a password is required.', 'Two-steps verification is enabled and a password is required.',
# 420 FLOOD # 420 FLOOD
'FLOOD_WAIT_(\d+)': 'A wait of {} seconds is required.' 'FLOOD_WAIT_(\d+)': 'A wait of {} seconds is required.'
@ -163,7 +159,8 @@ class RPCError(Exception):
# Get additional_data, if any # Get additional_data, if any
if match.groups(): if match.groups():
self.additional_data = int(match.group(1)) self.additional_data = int(match.group(1))
super().__init__(self, error_msg.format(self.additional_data)) super().__init__(self,
error_msg.format(self.additional_data))
else: else:
self.additional_data = None self.additional_data = None
super().__init__(self, error_msg) super().__init__(self, error_msg)
@ -176,47 +173,49 @@ class RPCError(Exception):
break break
if not called_super: if not called_super:
super().__init__(self, 'Unknown error message with code {}: {}'.format(code, message)) super().__init__(
self, 'Unknown error message with code {}: {}'.format(code,
message))
class BadMessageError(Exception): class BadMessageError(Exception):
"""Occurs when handling a bad_message_notification""" """Occurs when handling a bad_message_notification"""
ErrorMessages = { ErrorMessages = {
16: 'msg_id too low (most likely, client time is wrong it would be worthwhile to ' 16:
'msg_id too low (most likely, client time is wrong it would be worthwhile to '
'synchronize it using msg_id notifications and re-send the original message ' 'synchronize it using msg_id notifications and re-send the original message '
'with the "correct" msg_id or wrap it in a container with a new msg_id if the ' 'with the "correct" msg_id or wrap it in a container with a new msg_id if the '
'original message had waited too long on the client to be transmitted).', 'original message had waited too long on the client to be transmitted).',
17:
17: 'msg_id too high (similar to the previous case, the client time has to be ' 'msg_id too high (similar to the previous case, the client time has to be '
'synchronized, and the message re-sent with the correct msg_id).', 'synchronized, and the message re-sent with the correct msg_id).',
18:
18: 'Incorrect two lower order msg_id bits (the server expects client message msg_id ' 'Incorrect two lower order msg_id bits (the server expects client message msg_id '
'to be divisible by 4).', 'to be divisible by 4).',
19:
19: 'Container msg_id is the same as msg_id of a previously received message ' 'Container msg_id is the same as msg_id of a previously received message '
'(this must never happen).', '(this must never happen).',
20:
20: 'Message too old, and it cannot be verified whether the server has received a ' 'Message too old, and it cannot be verified whether the server has received a '
'message with this msg_id or not.', 'message with this msg_id or not.',
32:
32: 'msg_seqno too low (the server has already received a message with a lower ' 'msg_seqno too low (the server has already received a message with a lower '
'msg_id but with either a higher or an equal and odd seqno).', 'msg_id but with either a higher or an equal and odd seqno).',
33:
33: 'msg_seqno too high (similarly, there is a message with a higher msg_id but with ' 'msg_seqno too high (similarly, there is a message with a higher msg_id but with '
'either a lower or an equal and odd seqno).', 'either a lower or an equal and odd seqno).',
34:
34: 'An even msg_seqno expected (irrelevant message), but odd received.', 'An even msg_seqno expected (irrelevant message), but odd received.',
35: 'Odd msg_seqno expected (relevant message), but even received.', 35: 'Odd msg_seqno expected (relevant message), but even received.',
48:
48: 'Incorrect server salt (in this case, the bad_server_salt response is received with ' 'Incorrect server salt (in this case, the bad_server_salt response is received with '
'the correct salt, and the message is to be re-sent with it).', 'the correct salt, and the message is to be re-sent with it).',
64: 'Invalid container.' 64: 'Invalid container.'
} }
def __init__(self, code): def __init__(self, code):
super().__init__(self, BadMessageError super().__init__(self, BadMessageError.ErrorMessages.get(
.ErrorMessages.get(code,'Unknown error code (this should not happen): {}.'.format(code))) code,
'Unknown error code (this should not happen): {}.'.format(code)))
self.code = code self.code = code

View File

@ -1,5 +1,5 @@
import os
import hashlib import hashlib
import os
# region Multiple utilities # region Multiple utilities
@ -15,7 +15,6 @@ def ensure_parent_dir_exists(file_path):
if parent: if parent:
os.makedirs(parent, exist_ok=True) os.makedirs(parent, exist_ok=True)
# endregion # endregion
# region Cryptographic related utils # region Cryptographic related utils
@ -26,7 +25,8 @@ def calc_key(shared_key, msg_key, client):
x = 0 if client else 8 x = 0 if client else 8
sha1a = sha1(msg_key + shared_key[x:x + 32]) sha1a = sha1(msg_key + shared_key[x:x + 32])
sha1b = sha1(shared_key[x + 32:x + 48] + msg_key + shared_key[x + 48:x + 64]) sha1b = sha1(shared_key[x + 32:x + 48] + msg_key + shared_key[x + 48:x +
64])
sha1c = sha1(shared_key[x + 64:x + 96] + msg_key) sha1c = sha1(shared_key[x + 64:x + 96] + msg_key)
sha1d = sha1(msg_key + shared_key[x + 96:x + 128]) sha1d = sha1(msg_key + shared_key[x + 96:x + 128])
@ -74,8 +74,7 @@ def get_password_hash(pw, current_salt):
# https://github.com/DrKLO/Telegram/blob/e31388/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java#L2003 # https://github.com/DrKLO/Telegram/blob/e31388/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java#L2003
data = pw.encode('utf-8') data = pw.encode('utf-8')
pw_hash = current_salt+data+current_salt pw_hash = current_salt + data + current_salt
return sha256(pw_hash) return sha256(pw_hash)
# endregion # endregion

View File

@ -1,12 +1,10 @@
from telethon.tl.types import UpdateShortChatMessage
from telethon.tl.types import UpdateShortMessage
from telethon import TelegramClient, RPCError
from telethon.utils import get_display_name, get_input_peer
import shutil import shutil
from getpass import getpass from getpass import getpass
from telethon import RPCError, TelegramClient
from telethon.tl.types import UpdateShortChatMessage, UpdateShortMessage
from telethon.utils import get_display_name, get_input_peer
# Get the (current) number of lines in the terminal # Get the (current) number of lines in the terminal
cols, rows = shutil.get_terminal_size() cols, rows = shutil.get_terminal_size()
@ -27,7 +25,8 @@ def bytes_to_string(byte_count):
byte_count /= 1024 byte_count /= 1024
suffix_index += 1 suffix_index += 1
return '{:.2f}{}'.format(byte_count, [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index]) return '{:.2f}{}'.format(byte_count,
[' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index])
class InteractiveTelegramClient(TelegramClient): class InteractiveTelegramClient(TelegramClient):
@ -58,7 +57,8 @@ class InteractiveTelegramClient(TelegramClient):
# Two-step verification may be enabled # Two-step verification may be enabled
except RPCError as e: except RPCError as e:
if e.password_required: if e.password_required:
pw = getpass('Two step verification is enabled. Please enter your password: ') pw = getpass(
'Two step verification is enabled. Please enter your password: ')
code_ok = self.sign_in(password=pw) code_ok = self.sign_in(password=pw)
else: else:
raise e raise e
@ -117,10 +117,14 @@ class InteractiveTelegramClient(TelegramClient):
print('Available commands:') print('Available commands:')
print(' !q: Quits the current chat.') print(' !q: Quits the current chat.')
print(' !Q: Quits the current chat and exits.') print(' !Q: Quits the current chat and exits.')
print(' !h: prints the latest messages (message History) of the chat.') print(
print(' !up <path>: Uploads and sends a Photo located at the given path.') ' !h: prints the latest messages (message History) of the chat.')
print(' !uf <path>: Uploads and sends a File document located at the given path.') print(
print(' !dm <msg-id>: Downloads the given message Media (if any).') ' !up <path>: Uploads and sends a Photo located at the given path.')
print(
' !uf <path>: Uploads and sends a File document located at the given path.')
print(
' !dm <msg-id>: Downloads the given message Media (if any).')
print(' !dp: Downloads the current dialog Profile picture.') print(' !dp: Downloads the current dialog Profile picture.')
print() print()
@ -136,10 +140,12 @@ class InteractiveTelegramClient(TelegramClient):
# History # History
elif msg == '!h': elif msg == '!h':
# First retrieve the messages and some information # First retrieve the messages and some information
total_count, messages, senders = self.get_message_history(input_peer, limit=10) total_count, messages, senders = self.get_message_history(
input_peer, limit=10)
# Iterate over all (in reverse order so the latest appears the last in the console) # Iterate over all (in reverse order so the latest appears the last in the console)
# and print them in "[hh:mm] Sender: Message" text format # and print them in "[hh:mm] Sender: Message" text format
for msg, sender in zip(reversed(messages), reversed(senders)): for msg, sender in zip(
reversed(messages), reversed(senders)):
# Get the name of the sender if any # Get the name of the sender if any
name = sender.first_name if sender else '???' name = sender.first_name if sender else '???'
@ -147,13 +153,15 @@ class InteractiveTelegramClient(TelegramClient):
if msg.media: if msg.media:
self.found_media.add(msg) self.found_media.add(msg)
content = '<{}> {}'.format( # The media may or may not have a caption content = '<{}> {}'.format( # The media may or may not have a caption
msg.media.__class__.__name__, getattr(msg.media, 'caption', '')) msg.media.__class__.__name__,
getattr(msg.media, 'caption', ''))
else: else:
content = msg.message content = msg.message
# And print it to the user # And print it to the user
print('[{}:{}] (ID={}) {}: {}'.format( print('[{}:{}] (ID={}) {}: {}'.format(
msg.date.hour, msg.date.minute, msg.id, name, content)) msg.date.hour, msg.date.minute, msg.id, name,
content))
# Send photo # Send photo
elif msg.startswith('!up '): elif msg.startswith('!up '):
@ -176,18 +184,21 @@ class InteractiveTelegramClient(TelegramClient):
print('Downloading profile picture...') print('Downloading profile picture...')
success = self.download_profile_photo(entity.photo, output) success = self.download_profile_photo(entity.photo, output)
if success: if success:
print('Profile picture downloaded to {}'.format(output)) print('Profile picture downloaded to {}'.format(
output))
else: else:
print('"{}" does not seem to have a profile picture.' print('"{}" does not seem to have a profile picture.'
.format(get_display_name(entity))) .format(get_display_name(entity)))
# Send chat message (if any) # Send chat message (if any)
elif msg: elif msg:
self.send_message(input_peer, msg, markdown=True, no_web_page=True) self.send_message(
input_peer, msg, markdown=True, no_web_page=True)
def send_photo(self, path, peer): def send_photo(self, path, peer):
print('Uploading {}...'.format(path)) print('Uploading {}...'.format(path))
input_file = self.upload_file(path, progress_callback=self.upload_progress_callback) input_file = self.upload_file(
path, progress_callback=self.upload_progress_callback)
# After we have the handle to the uploaded file, send it to our peer # After we have the handle to the uploaded file, send it to our peer
self.send_photo_file(input_file, peer) self.send_photo_file(input_file, peer)
@ -195,7 +206,8 @@ class InteractiveTelegramClient(TelegramClient):
def send_document(self, path, peer): def send_document(self, path, peer):
print('Uploading {}...'.format(path)) print('Uploading {}...'.format(path))
input_file = self.upload_file(path, progress_callback=self.upload_progress_callback) input_file = self.upload_file(
path, progress_callback=self.upload_progress_callback)
# After we have the handle to the uploaded file, send it to our peer # After we have the handle to the uploaded file, send it to our peer
self.send_document_file(input_file, peer) self.send_document_file(input_file, peer)
@ -212,7 +224,8 @@ class InteractiveTelegramClient(TelegramClient):
# Let the output be the message ID # Let the output be the message ID
output = str('usermedia/{}'.format(msg_media_id)) output = str('usermedia/{}'.format(msg_media_id))
print('Downloading media with name {}...'.format(output)) print('Downloading media with name {}...'.format(output))
output = self.download_msg_media(msg.media, output = self.download_msg_media(
msg.media,
file_path=output, file_path=output,
progress_callback=self.download_progress_callback) progress_callback=self.download_progress_callback)
print('Media downloaded to {}!'.format(output)) print('Media downloaded to {}!'.format(output))
@ -222,32 +235,35 @@ class InteractiveTelegramClient(TelegramClient):
@staticmethod @staticmethod
def download_progress_callback(downloaded_bytes, total_bytes): def download_progress_callback(downloaded_bytes, total_bytes):
InteractiveTelegramClient.print_progress('Downloaded', downloaded_bytes, total_bytes) InteractiveTelegramClient.print_progress('Downloaded',
downloaded_bytes, total_bytes)
@staticmethod @staticmethod
def upload_progress_callback(uploaded_bytes, total_bytes): def upload_progress_callback(uploaded_bytes, total_bytes):
InteractiveTelegramClient.print_progress('Uploaded', uploaded_bytes, total_bytes) InteractiveTelegramClient.print_progress('Uploaded', uploaded_bytes,
total_bytes)
@staticmethod @staticmethod
def print_progress(progress_type, downloaded_bytes, total_bytes): def print_progress(progress_type, downloaded_bytes, total_bytes):
print('{} {} out of {} ({:.2%})'.format( print('{} {} out of {} ({:.2%})'.format(progress_type, bytes_to_string(
progress_type, downloaded_bytes), bytes_to_string(total_bytes), downloaded_bytes /
bytes_to_string(downloaded_bytes), total_bytes))
bytes_to_string(total_bytes),
downloaded_bytes / total_bytes))
@staticmethod @staticmethod
def update_handler(update_object): def update_handler(update_object):
if type(update_object) is UpdateShortMessage: if type(update_object) is UpdateShortMessage:
if update_object.out: if update_object.out:
print('You sent {} to user #{}'.format(update_object.message, update_object.user_id)) print('You sent {} to user #{}'.format(update_object.message,
update_object.user_id))
else: else:
print('[User #{} sent {}]'.format(update_object.user_id, update_object.message)) print('[User #{} sent {}]'.format(update_object.user_id,
update_object.message))
elif type(update_object) is UpdateShortChatMessage: elif type(update_object) is UpdateShortChatMessage:
if update_object.out: if update_object.out:
print('You sent {} to chat #{}'.format(update_object.message, update_object.chat_id)) print('You sent {} to chat #{}'.format(update_object.message,
update_object.chat_id))
else: else:
print('[Chat #{}, user #{} sent {}]'.format(update_object.chat_id, print('[Chat #{}, user #{} sent {}]'.format(
update_object.from_id, update_object.chat_id, update_object.from_id,
update_object.message)) update_object.message))

View File

@ -1,9 +1,10 @@
import os import os
import time import time
import telethon.helpers as utils import telethon.helpers as utils
from telethon.utils import BinaryWriter, BinaryReader from telethon.crypto import AES, RSA, AuthKey, Factorizator
from telethon.crypto import AES, AuthKey, Factorizator, RSA
from telethon.network import MtProtoPlainSender from telethon.network import MtProtoPlainSender
from telethon.utils import BinaryReader, BinaryWriter
def do_authentication(transport): def do_authentication(transport):
@ -23,7 +24,8 @@ def do_authentication(transport):
with BinaryReader(sender.receive()) as reader: with BinaryReader(sender.receive()) as reader:
response_code = reader.read_int(signed=False) response_code = reader.read_int(signed=False)
if response_code != 0x05162463: if response_code != 0x05162463:
raise AssertionError('Invalid response code: {}'.format(hex(response_code))) raise AssertionError('Invalid response code: {}'.format(
hex(response_code)))
nonce_from_server = reader.read(16) nonce_from_server = reader.read(16)
if nonce_from_server != nonce: if nonce_from_server != nonce:
@ -36,7 +38,8 @@ def do_authentication(transport):
vector_id = reader.read_int() vector_id = reader.read_int()
if vector_id != 0x1cb5c415: if vector_id != 0x1cb5c415:
raise AssertionError('Invalid vector constructor ID: {}'.format(hex(response_code))) raise AssertionError('Invalid vector constructor ID: {}'.format(
hex(response_code)))
fingerprints = [] fingerprints = []
fingerprint_count = reader.read_int() fingerprint_count = reader.read_int()
@ -47,32 +50,46 @@ def do_authentication(transport):
new_nonce = os.urandom(32) new_nonce = os.urandom(32)
p, q = Factorizator.factorize(pq) p, q = Factorizator.factorize(pq)
with BinaryWriter() as pq_inner_data_writer: with BinaryWriter() as pq_inner_data_writer:
pq_inner_data_writer.write_int(0x83c95aec, signed=False) # PQ Inner Data pq_inner_data_writer.write_int(
0x83c95aec, signed=False) # PQ Inner Data
pq_inner_data_writer.tgwrite_bytes(get_byte_array(pq, signed=False)) pq_inner_data_writer.tgwrite_bytes(get_byte_array(pq, signed=False))
pq_inner_data_writer.tgwrite_bytes(get_byte_array(min(p, q), signed=False)) pq_inner_data_writer.tgwrite_bytes(
pq_inner_data_writer.tgwrite_bytes(get_byte_array(max(p, q), signed=False)) get_byte_array(
min(p, q), signed=False))
pq_inner_data_writer.tgwrite_bytes(
get_byte_array(
max(p, q), signed=False))
pq_inner_data_writer.write(nonce) pq_inner_data_writer.write(nonce)
pq_inner_data_writer.write(server_nonce) pq_inner_data_writer.write(server_nonce)
pq_inner_data_writer.write(new_nonce) pq_inner_data_writer.write(new_nonce)
cipher_text, target_fingerprint = None, None cipher_text, target_fingerprint = None, None
for fingerprint in fingerprints: for fingerprint in fingerprints:
cipher_text = RSA.encrypt(get_fingerprint_text(fingerprint), pq_inner_data_writer.get_bytes()) cipher_text = RSA.encrypt(
get_fingerprint_text(fingerprint),
pq_inner_data_writer.get_bytes())
if cipher_text is not None: if cipher_text is not None:
target_fingerprint = fingerprint target_fingerprint = fingerprint
break break
if cipher_text is None: if cipher_text is None:
raise AssertionError('Could not find a valid key for fingerprints: {}' raise AssertionError(
.format(', '.join([get_fingerprint_text(f) for f in fingerprints]))) 'Could not find a valid key for fingerprints: {}'
.format(', '.join([get_fingerprint_text(f)
for f in fingerprints])))
with BinaryWriter() as req_dh_params_writer: with BinaryWriter() as req_dh_params_writer:
req_dh_params_writer.write_int(0xd712e4be, signed=False) # Req DH Params req_dh_params_writer.write_int(
0xd712e4be, signed=False) # Req DH Params
req_dh_params_writer.write(nonce) req_dh_params_writer.write(nonce)
req_dh_params_writer.write(server_nonce) req_dh_params_writer.write(server_nonce)
req_dh_params_writer.tgwrite_bytes(get_byte_array(min(p, q), signed=False)) req_dh_params_writer.tgwrite_bytes(
req_dh_params_writer.tgwrite_bytes(get_byte_array(max(p, q), signed=False)) get_byte_array(
min(p, q), signed=False))
req_dh_params_writer.tgwrite_bytes(
get_byte_array(
max(p, q), signed=False))
req_dh_params_writer.write(target_fingerprint) req_dh_params_writer.write(target_fingerprint)
req_dh_params_writer.tgwrite_bytes(cipher_text) req_dh_params_writer.tgwrite_bytes(cipher_text)
@ -88,7 +105,8 @@ def do_authentication(transport):
raise AssertionError('Server DH params fail: TODO') raise AssertionError('Server DH params fail: TODO')
if response_code != 0xd0e8075c: if response_code != 0xd0e8075c:
raise AssertionError('Invalid response code: {}'.format(hex(response_code))) raise AssertionError('Invalid response code: {}'.format(
hex(response_code)))
nonce_from_server = reader.read(16) nonce_from_server = reader.read(16)
if nonce_from_server != nonce: if nonce_from_server != nonce:
@ -106,7 +124,6 @@ def do_authentication(transport):
g, dh_prime, ga, time_offset = None, None, None, None g, dh_prime, ga, time_offset = None, None, None, None
with BinaryReader(plain_text_answer) as dh_inner_data_reader: with BinaryReader(plain_text_answer) as dh_inner_data_reader:
hashsum = dh_inner_data_reader.read(20)
code = dh_inner_data_reader.read_int(signed=False) code = dh_inner_data_reader.read_int(signed=False)
if code != 0xb5890dba: if code != 0xb5890dba:
raise AssertionError('Invalid DH Inner Data code: {}'.format(code)) raise AssertionError('Invalid DH Inner Data code: {}'.format(code))
@ -132,26 +149,34 @@ def do_authentication(transport):
# Prepare client DH Inner Data # Prepare client DH Inner Data
with BinaryWriter() as client_dh_inner_data_writer: with BinaryWriter() as client_dh_inner_data_writer:
client_dh_inner_data_writer.write_int(0x6643b654, signed=False) # Client DH Inner Data client_dh_inner_data_writer.write_int(
0x6643b654, signed=False) # Client DH Inner Data
client_dh_inner_data_writer.write(nonce) client_dh_inner_data_writer.write(nonce)
client_dh_inner_data_writer.write(server_nonce) client_dh_inner_data_writer.write(server_nonce)
client_dh_inner_data_writer.write_long(0) # TODO retry_id client_dh_inner_data_writer.write_long(0) # TODO retry_id
client_dh_inner_data_writer.tgwrite_bytes(get_byte_array(gb, signed=False)) client_dh_inner_data_writer.tgwrite_bytes(
get_byte_array(
gb, signed=False))
with BinaryWriter() as client_dh_inner_data_with_hash_writer: with BinaryWriter() as client_dh_inner_data_with_hash_writer:
client_dh_inner_data_with_hash_writer.write(utils.sha1(client_dh_inner_data_writer.get_bytes())) client_dh_inner_data_with_hash_writer.write(
client_dh_inner_data_with_hash_writer.write(client_dh_inner_data_writer.get_bytes()) utils.sha1(client_dh_inner_data_writer.get_bytes()))
client_dh_inner_data_bytes = client_dh_inner_data_with_hash_writer.get_bytes() client_dh_inner_data_with_hash_writer.write(
client_dh_inner_data_writer.get_bytes())
client_dh_inner_data_bytes = client_dh_inner_data_with_hash_writer.get_bytes(
)
# Encryption # Encryption
client_dh_inner_data_encrypted_bytes = AES.encrypt_ige(client_dh_inner_data_bytes, key, iv) client_dh_inner_data_encrypted_bytes = AES.encrypt_ige(
client_dh_inner_data_bytes, key, iv)
# Prepare Set client DH params # Prepare Set client DH params
with BinaryWriter() as set_client_dh_params_writer: with BinaryWriter() as set_client_dh_params_writer:
set_client_dh_params_writer.write_int(0xf5045f1f, signed=False) set_client_dh_params_writer.write_int(0xf5045f1f, signed=False)
set_client_dh_params_writer.write(nonce) set_client_dh_params_writer.write(nonce)
set_client_dh_params_writer.write(server_nonce) set_client_dh_params_writer.write(server_nonce)
set_client_dh_params_writer.tgwrite_bytes(client_dh_inner_data_encrypted_bytes) set_client_dh_params_writer.tgwrite_bytes(
client_dh_inner_data_encrypted_bytes)
set_client_dh_params_bytes = set_client_dh_params_writer.get_bytes() set_client_dh_params_bytes = set_client_dh_params_writer.get_bytes()
sender.send(set_client_dh_params_bytes) sender.send(set_client_dh_params_bytes)
@ -171,7 +196,8 @@ def do_authentication(transport):
new_nonce_hash1 = reader.read(16) new_nonce_hash1 = reader.read(16)
auth_key = AuthKey(get_byte_array(gab, signed=False)) auth_key = AuthKey(get_byte_array(gab, signed=False))
new_nonce_hash_calculated = auth_key.calc_new_nonce_hash(new_nonce, 1) new_nonce_hash_calculated = auth_key.calc_new_nonce_hash(new_nonce,
1)
if new_nonce_hash1 != new_nonce_hash_calculated: if new_nonce_hash1 != new_nonce_hash_calculated:
raise AssertionError('Invalid new nonce hash') raise AssertionError('Invalid new nonce hash')
@ -200,7 +226,8 @@ def get_byte_array(integer, signed):
"""Gets the arbitrary-length byte array corresponding to the given integer""" """Gets the arbitrary-length byte array corresponding to the given integer"""
bits = integer.bit_length() bits = integer.bit_length()
byte_length = (bits + 8 - 1) // 8 # 8 bits per byte byte_length = (bits + 8 - 1) // 8 # 8 bits per byte
return int.to_bytes(integer, length=byte_length, byteorder='big', signed=signed) return int.to_bytes(
integer, length=byte_length, byteorder='big', signed=signed)
def get_int(byte_array, signed=True): def get_int(byte_array, signed=True):

View File

@ -1,10 +1,12 @@
import time
import random import random
from telethon.utils import BinaryWriter, BinaryReader import time
from telethon.utils import BinaryReader, BinaryWriter
class MtProtoPlainSender: class MtProtoPlainSender:
"""MTProto Mobile Protocol plain sender (https://core.telegram.org/mtproto/description#unencrypted-messages)""" """MTProto Mobile Protocol plain sender (https://core.telegram.org/mtproto/description#unencrypted-messages)"""
def __init__(self, transport): def __init__(self, transport):
self._sequence = 0 self._sequence = 0
self._time_offset = 0 self._time_offset = 0
@ -37,9 +39,12 @@ class MtProtoPlainSender:
"""Generates a new message ID based on the current time (in ms) since epoch""" """Generates a new message ID based on the current time (in ms) since epoch"""
# See https://core.telegram.org/mtproto/description#message-identifier-msg-id # See https://core.telegram.org/mtproto/description#message-identifier-msg-id
ms_time = int(time.time() * 1000) ms_time = int(time.time() * 1000)
new_msg_id = (((ms_time // 1000) << 32) | # "must approximately equal unixtime*2^32" new_msg_id = (((ms_time // 1000) << 32)
((ms_time % 1000) << 22) | # "approximate moment in time the message was created" | # "must approximately equal unixtime*2^32"
random.randint(0, 524288) << 2) # "message identifiers are divisible by 4" ((ms_time % 1000) << 22)
| # "approximate moment in time the message was created"
random.randint(0, 524288)
<< 2) # "message identifiers are divisible by 4"
# Ensure that we always return a message ID which is higher than the previous one # Ensure that we always return a message ID which is higher than the previous one
if self._last_msg_id >= new_msg_id: if self._last_msg_id >= new_msg_id:

View File

@ -1,18 +1,19 @@
import gzip import gzip
from telethon.errors import *
from time import sleep
from datetime import timedelta from datetime import timedelta
from threading import Thread, RLock from threading import RLock, Thread
from time import sleep
import telethon.helpers as utils import telethon.helpers as utils
from telethon.crypto import AES from telethon.crypto import AES
from telethon.utils import BinaryWriter, BinaryReader from telethon.errors import *
from telethon.tl.types import MsgsAck
from telethon.tl.all_tlobjects import tlobjects from telethon.tl.all_tlobjects import tlobjects
from telethon.tl.types import MsgsAck
from telethon.utils import BinaryReader, BinaryWriter
class MtProtoSender: class MtProtoSender:
"""MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)""" """MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)"""
def __init__(self, transport, session): def __init__(self, transport, session):
self.transport = transport self.transport = transport
self.session = session self.session = session
@ -27,7 +28,8 @@ class MtProtoSender:
# We need this to avoid using the updates thread if we're waiting to read # We need this to avoid using the updates thread if we're waiting to read
self.waiting_receive = False self.waiting_receive = False
self.updates_thread = Thread(target=self.updates_thread_method, name='Updates thread') self.updates_thread = Thread(
target=self.updates_thread_method, name='Updates thread')
self.updates_thread_running = False self.updates_thread_running = False
self.updates_thread_receiving = False self.updates_thread_receiving = False
@ -118,7 +120,8 @@ class MtProtoSender:
message, remote_msg_id, remote_sequence = self.decode_msg(body) message, remote_msg_id, remote_sequence = self.decode_msg(body)
with BinaryReader(message) as reader: with BinaryReader(message) as reader:
self.process_msg(remote_msg_id, remote_sequence, reader, request) self.process_msg(remote_msg_id, remote_sequence, reader,
request)
# We can now set the flag to False thus resuming the updates thread # We can now set the flag to False thus resuming the updates thread
self.waiting_receive = False self.waiting_receive = False
@ -148,7 +151,8 @@ class MtProtoSender:
# And then finally send the encrypted packet # And then finally send the encrypted packet
with BinaryWriter() as cipher_writer: with BinaryWriter() as cipher_writer:
cipher_writer.write_long(self.session.auth_key.key_id, signed=False) cipher_writer.write_long(
self.session.auth_key.key_id, signed=False)
cipher_writer.write(msg_key) cipher_writer.write(msg_key)
cipher_writer.write(cipher_text) cipher_writer.write(cipher_text)
self.transport.send(cipher_writer.get_bytes()) self.transport.send(cipher_writer.get_bytes())
@ -168,7 +172,8 @@ class MtProtoSender:
msg_key = reader.read(16) msg_key = reader.read(16)
key, iv = utils.calc_key(self.session.auth_key.key, msg_key, False) key, iv = utils.calc_key(self.session.auth_key.key, msg_key, False)
plain_text = AES.decrypt_ige(reader.read(len(body) - reader.tell_position()), key, iv) plain_text = AES.decrypt_ige(
reader.read(len(body) - reader.tell_position()), key, iv)
with BinaryReader(plain_text) as plain_text_reader: with BinaryReader(plain_text) as plain_text_reader:
remote_salt = plain_text_reader.read_long() remote_salt = plain_text_reader.read_long()
@ -198,7 +203,8 @@ class MtProtoSender:
if code == 0x3072cfa1: # gzip_packed if code == 0x3072cfa1: # gzip_packed
return self.handle_gzip_packed(msg_id, sequence, reader, request) return self.handle_gzip_packed(msg_id, sequence, reader, request)
if code == 0xedab447b: # bad_server_salt if code == 0xedab447b: # bad_server_salt
return self.handle_bad_server_salt(msg_id, sequence, reader, request) return self.handle_bad_server_salt(msg_id, sequence, reader,
request)
if code == 0xa7eff811: # bad_msg_notification if code == 0xa7eff811: # bad_msg_notification
return self.handle_bad_msg_notification(msg_id, sequence, reader) return self.handle_bad_msg_notification(msg_id, sequence, reader)
@ -253,7 +259,8 @@ class MtProtoSender:
self.session.salt = new_salt self.session.salt = new_salt
if request is None: if request is None:
raise ValueError('Tried to handle a bad server salt with no request specified') raise ValueError(
'Tried to handle a bad server salt with no request specified')
# Resend # Resend
self.send(request) self.send(request)
@ -277,15 +284,18 @@ class MtProtoSender:
request.confirm_received = True request.confirm_received = True
if inner_code == 0x2144ca19: # RPC Error if inner_code == 0x2144ca19: # RPC Error
error = RPCError(code=reader.read_int(), message=reader.tgread_string()) error = RPCError(
code=reader.read_int(), message=reader.tgread_string())
if error.must_resend: if error.must_resend:
if not request: if not request:
raise ValueError('The previously sent request must be resent. ' raise ValueError(
'The previously sent request must be resent. '
'However, no request was previously sent (called from updates thread).') 'However, no request was previously sent (called from updates thread).')
request.confirm_received = False request.confirm_received = False
if error.message.startswith('FLOOD_WAIT_'): if error.message.startswith('FLOOD_WAIT_'):
print('Should wait {}s. Sleeping until then.'.format(error.additional_data)) print('Should wait {}s. Sleeping until then.'.format(
error.additional_data))
sleep(error.additional_data) sleep(error.additional_data)
elif error.message.startswith('PHONE_MIGRATE_'): elif error.message.startswith('PHONE_MIGRATE_'):
@ -295,7 +305,8 @@ class MtProtoSender:
raise error raise error
else: else:
if not request: if not request:
raise ValueError('Cannot receive a request from inside an RPC result from the updates thread.') raise ValueError(
'Cannot receive a request from inside an RPC result from the updates thread.')
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())
@ -311,7 +322,8 @@ class MtProtoSender:
unpacked_data = gzip.decompress(packed_data) unpacked_data = gzip.decompress(packed_data)
with BinaryReader(unpacked_data) as compressed_reader: with BinaryReader(unpacked_data) as compressed_reader:
return self.process_msg(msg_id, sequence, compressed_reader, request) return self.process_msg(msg_id, sequence, compressed_reader,
request)
# endregion # endregion
@ -340,10 +352,12 @@ class MtProtoSender:
try: try:
self.updates_thread_receiving = True self.updates_thread_receiving = True
seq, body = self.transport.receive(timeout) seq, body = self.transport.receive(timeout)
message, remote_msg_id, remote_sequence = self.decode_msg(body) message, remote_msg_id, remote_sequence = self.decode_msg(
body)
with BinaryReader(message) as reader: with BinaryReader(message) as reader:
self.process_msg(remote_msg_id, remote_sequence, reader) self.process_msg(remote_msg_id, remote_sequence,
reader)
except (ReadCancelledError, TimeoutError): except (ReadCancelledError, TimeoutError):
pass pass

View File

@ -78,7 +78,8 @@ class TcpClient:
if timeout: if timeout:
time_passed = datetime.now() - start_time time_passed = datetime.now() - start_time
if time_passed > timeout: if time_passed > timeout:
raise TimeoutError('The read operation exceeded the timeout.') raise TimeoutError(
'The read operation exceeded the timeout.')
# If everything went fine, return the read bytes # If everything went fine, return the read bytes
return writer.get_bytes() return writer.get_bytes()

View File

@ -1,8 +1,8 @@
from binascii import crc32 from binascii import crc32
from datetime import timedelta from datetime import timedelta
from telethon.network import TcpClient
from telethon.errors import * from telethon.errors import *
from telethon.network import TcpClient
from telethon.utils import BinaryWriter from telethon.utils import BinaryWriter
@ -45,9 +45,8 @@ class TcpTransport:
body = self.tcp_client.read(packet_length - 12, timeout) body = self.tcp_client.read(packet_length - 12, timeout)
checksum = int.from_bytes(self.tcp_client.read(4, timeout), checksum = int.from_bytes(
byteorder='little', self.tcp_client.read(4, timeout), byteorder='little', signed=False)
signed=False)
# Then perform the checks # Then perform the checks
rv = packet_length_bytes + seq_bytes + body rv = packet_length_bytes + seq_bytes + body

View File

@ -1,4 +1,5 @@
from telethon.tl.types import MessageEntityBold, MessageEntityItalic, MessageEntityCode, MessageEntityTextUrl from telethon.tl.types import (MessageEntityBold, MessageEntityCode,
MessageEntityItalic, MessageEntityTextUrl)
def parse_message_entities(msg): def parse_message_entities(msg):
@ -44,13 +45,13 @@ def parse_message_entities(msg):
# Add 1 when slicing the message not to include the [] nor () # Add 1 when slicing the message not to include the [] nor ()
# There is no need to subtract 1 on the later part because that index is already excluded # There is no need to subtract 1 on the later part because that index is already excluded
link_text = ''.join(msg[vui[0]+1:vui[1]]) link_text = ''.join(msg[vui[0] + 1:vui[1]])
link_url = ''.join(msg[vui[2]+1:vui[3]]) link_url = ''.join(msg[vui[2] + 1:vui[3]])
# After we have retrieved both the link text and url, replace them in the message # After we have retrieved both the link text and url, replace them in the message
# Now we do have to add 1 to include the [] and () when deleting and replacing! # Now we do have to add 1 to include the [] and () when deleting and replacing!
del msg[vui[2]:vui[3]+1] del msg[vui[2]:vui[3] + 1]
msg[vui[0]:vui[1]+1] = link_text msg[vui[0]:vui[1] + 1] = link_text
# Finally, update the current valid index url to reflect that all the previous VUI's will be removed # Finally, update the current valid index url to reflect that all the previous VUI's will be removed
# This is because, after the previous VUI's get done, their part of the message is removed too, # This is because, after the previous VUI's get done, their part of the message is removed too,
@ -63,14 +64,12 @@ def parse_message_entities(msg):
# No need to subtract the displacement from the URL part (indices 2 and 3) # No need to subtract the displacement from the URL part (indices 2 and 3)
# When calculating the length, subtract 1 again not to include the previously called ']' # When calculating the length, subtract 1 again not to include the previously called ']'
entities.append(MessageEntityTextUrl(offset=vui[0], length=vui[1] - vui[0] - 1, url=link_url)) entities.append(
MessageEntityTextUrl(
offset=vui[0], length=vui[1] - vui[0] - 1, url=link_url))
# After the message is clean from links, handle all the indicator flags # After the message is clean from links, handle all the indicator flags
indicator_flags = { indicator_flags = {'*': None, '_': None, '`': None}
'*': None,
'_': None,
'`': None
}
# Iterate over the list to find the indicators of entities # Iterate over the list to find the indicators of entities
for i, c in enumerate(msg): for i, c in enumerate(msg):
@ -88,13 +87,19 @@ def parse_message_entities(msg):
# Add the corresponding entity # Add the corresponding entity
if c == '*': if c == '*':
entities.append(MessageEntityBold(offset=offset, length=length)) entities.append(
MessageEntityBold(
offset=offset, length=length))
elif c == '_': elif c == '_':
entities.append(MessageEntityItalic(offset=offset, length=length)) entities.append(
MessageEntityItalic(
offset=offset, length=length))
elif c == '`': elif c == '`':
entities.append(MessageEntityCode(offset=offset, length=length)) entities.append(
MessageEntityCode(
offset=offset, length=length))
# Clear the flag to start over with this indicator # Clear the flag to start over with this indicator
indicator_flags[c] = None indicator_flags[c] = None
@ -116,15 +121,16 @@ def parse_message_entities(msg):
# In this case, the current entity length is decreased by two, # In this case, the current entity length is decreased by two,
# and all the subentities offset decreases 1 # and all the subentities offset decreases 1
if (subentity.offset > entity.offset and if (subentity.offset > entity.offset and
subentity.offset + subentity.length < entity.offset + entity.length): subentity.offset + subentity.length <
entity.offset + entity.length):
entity.length -= 2 entity.length -= 2
subentity.offset -= 1 subentity.offset -= 1
# Second case, both inside: so*me_th*in_g. # Second case, both inside: so*me_th*in_g.
# In this case, the current entity length is decreased by one, # In this case, the current entity length is decreased by one,
# and all the subentities offset and length decrease 1 # and all the subentities offset and length decrease 1
elif (entity.offset < subentity.offset < entity.offset + entity.length and elif (entity.offset < subentity.offset < entity.offset +
subentity.offset + subentity.length > entity.offset + entity.length): entity.length < subentity.offset + subentity.length):
entity.length -= 1 entity.length -= 1
subentity.offset -= 1 subentity.offset -= 1
subentity.length -= 1 subentity.length -= 1

View File

@ -1,47 +1,40 @@
import platform import platform
from datetime import datetime, timedelta from datetime import datetime, timedelta
from hashlib import md5 from hashlib import md5
from os import path, listdir
from mimetypes import guess_type from mimetypes import guess_type
from os import listdir, path
# For sending and receiving requests
from telethon.tl import MTProtoRequest
from telethon.tl import Session
# The Requests and types that we'll be using
from telethon.tl.functions.upload import SaveBigFilePartRequest
from telethon.tl.functions import InvokeWithLayerRequest, InitConnectionRequest
from telethon.tl.functions.help import GetConfigRequest
from telethon.tl.functions.upload import SaveFilePartRequest, GetFileRequest
from telethon.tl.functions.messages import \
GetDialogsRequest, GetHistoryRequest, \
SendMessageRequest, SendMediaRequest, \
ReadHistoryRequest
from telethon.tl.functions.auth import \
SendCodeRequest, CheckPasswordRequest, \
SignInRequest, SignUpRequest, LogOutRequest
# The following is required to get the password salt
from telethon.tl.functions.account import GetPasswordRequest
# All the types we need to work with
from telethon.tl.types import \
InputPeerEmpty, \
UserProfilePhotoEmpty, ChatPhotoEmpty, \
InputFile, InputFileLocation, InputMediaUploadedPhoto, InputMediaUploadedDocument, \
MessageMediaContact, MessageMediaDocument, MessageMediaPhoto, \
DocumentAttributeAudio, DocumentAttributeFilename, InputDocumentFileLocation
# Import some externalized utilities to work with the Telegram types and more # Import some externalized utilities to work with the Telegram types and more
import telethon.helpers as utils import telethon.helpers as utils
import telethon.network.authenticator as authenticator import telethon.network.authenticator as authenticator
from telethon.utils import find_user_or_chat, get_appropiate_part_size, get_extension
from telethon.errors import * from telethon.errors import *
from telethon.network import MtProtoSender, TcpTransport from telethon.network import MtProtoSender, TcpTransport
from telethon.parser.markdown_parser import parse_message_entities from telethon.parser.markdown_parser import parse_message_entities
# For sending and receiving requests
from telethon.tl import MTProtoRequest, Session
from telethon.tl.all_tlobjects import layer from telethon.tl.all_tlobjects import layer
from telethon.tl.functions import InitConnectionRequest, InvokeWithLayerRequest
# The following is required to get the password salt
from telethon.tl.functions.account import GetPasswordRequest
from telethon.tl.functions.auth import (CheckPasswordRequest, LogOutRequest,
SendCodeRequest, SignInRequest,
SignUpRequest)
from telethon.tl.functions.help import GetConfigRequest
from telethon.tl.functions.messages import (
GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest,
SendMessageRequest)
# The Requests and types that we'll be using
from telethon.tl.functions.upload import (
GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest)
# All the types we need to work with
from telethon.tl.types import (
ChatPhotoEmpty, DocumentAttributeAudio, DocumentAttributeFilename,
InputDocumentFileLocation, InputFile, InputFileLocation,
InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty,
MessageMediaContact, MessageMediaDocument, MessageMediaPhoto,
UserProfilePhotoEmpty)
from telethon.utils import (find_user_or_chat, get_appropiate_part_size,
get_extension)
class TelegramClient: class TelegramClient:
@ -62,7 +55,8 @@ class TelegramClient:
.save() and .load() implementations to suit your needs.""" .save() and .load() implementations to suit your needs."""
if api_id is None or api_hash is None: if api_id is None or api_hash is None:
raise PermissionError('Your API ID or Hash are invalid. Please read "Requirements" on README.rst') raise PermissionError(
'Your API ID or Hash are invalid. Please read "Requirements" on README.rst')
self.api_id = api_id self.api_id = api_id
self.api_hash = api_hash self.api_hash = api_hash
@ -73,9 +67,11 @@ class TelegramClient:
elif isinstance(session, Session): elif isinstance(session, Session):
self.session = session self.session = session
else: else:
raise ValueError('The given session must either be a string or a Session instance.') raise ValueError(
'The given session must either be a string or a Session instance.')
self.transport = TcpTransport(self.session.server_address, self.session.port) self.transport = TcpTransport(self.session.server_address,
self.session.port)
# These will be set later # These will be set later
self.dc_options = None self.dc_options = None
@ -104,14 +100,17 @@ class TelegramClient:
# Now it's time to send an InitConnectionRequest # Now it's time to send an InitConnectionRequest
# This must always be invoked with the layer we'll be using # This must always be invoked with the layer we'll be using
query = InitConnectionRequest(api_id=self.api_id, query = InitConnectionRequest(
api_id=self.api_id,
device_model=platform.node(), device_model=platform.node(),
system_version=platform.system(), system_version=platform.system(),
app_version=self.__version__, app_version=self.__version__,
lang_code='en', lang_code='en',
query=GetConfigRequest()) query=GetConfigRequest())
result = self.invoke(InvokeWithLayerRequest(layer=layer, query=query)) result = self.invoke(
InvokeWithLayerRequest(
layer=layer, query=query))
# We're only interested in the DC options, # We're only interested in the DC options,
# although many other options are available! # although many other options are available!
@ -130,7 +129,8 @@ class TelegramClient:
def reconnect_to_dc(self, dc_id): def reconnect_to_dc(self, dc_id):
"""Reconnects to the specified DC ID. This is automatically called after an InvalidDCError is raised""" """Reconnects to the specified DC ID. This is automatically called after an InvalidDCError is raised"""
if self.dc_options is None or not self.dc_options: if self.dc_options is None or not self.dc_options:
raise ConnectionError("Can't reconnect. Stabilise an initial connection first.") raise ConnectionError(
"Can't reconnect. Stabilise an initial connection first.")
dc = next(dc for dc in self.dc_options if dc.id == dc_id) dc = next(dc for dc in self.dc_options if dc.id == dc_id)
@ -191,11 +191,13 @@ class TelegramClient:
with `.password_required = True` was raised""" with `.password_required = True` was raised"""
if phone_number and code: if phone_number and code:
if phone_number not in self.phone_code_hashes: if phone_number not in self.phone_code_hashes:
raise ValueError('Please make sure you have called send_code_request first.') raise ValueError(
'Please make sure you have called send_code_request first.')
try: try:
result = self.invoke(SignInRequest( result = self.invoke(
phone_number, self.phone_code_hashes[phone_number], code)) SignInRequest(phone_number, self.phone_code_hashes[
phone_number], code))
except RPCError as error: except RPCError as error:
if error.message.startswith('PHONE_CODE_'): if error.message.startswith('PHONE_CODE_'):
@ -205,9 +207,11 @@ class TelegramClient:
raise error raise error
elif password: elif password:
salt = self.invoke(GetPasswordRequest()).current_salt salt = self.invoke(GetPasswordRequest()).current_salt
result = self.invoke(CheckPasswordRequest(utils.get_password_hash(password, salt))) result = self.invoke(
CheckPasswordRequest(utils.get_password_hash(password, salt)))
else: else:
raise ValueError('You must provide a phone_number and a code for the first time, ' raise ValueError(
'You must provide a phone_number and a code for the first time, '
'and a password only if an RPCError was raised before.') 'and a password only if an RPCError was raised before.')
# Result is an Auth.Authorization TLObject # Result is an Auth.Authorization TLObject
@ -221,7 +225,9 @@ class TelegramClient:
def sign_up(self, phone_number, code, first_name, last_name=''): def sign_up(self, phone_number, code, first_name, last_name=''):
"""Signs up to Telegram. Make sure you sent a code request first!""" """Signs up to Telegram. Make sure you sent a code request first!"""
result = self.invoke(SignUpRequest(phone_number=phone_number, result = self.invoke(
SignUpRequest(
phone_number=phone_number,
phone_code_hash=self.phone_code_hashes[phone_number], phone_code_hash=self.phone_code_hashes[phone_number],
phone_code=code, phone_code=code,
first_name=first_name, first_name=first_name,
@ -245,29 +251,41 @@ class TelegramClient:
def list_sessions(): def list_sessions():
"""Lists all the sessions of the users who have ever connected """Lists all the sessions of the users who have ever connected
using this client and never logged out""" using this client and never logged out"""
return [path.splitext(path.basename(f))[0] # splitext = split ext (not spli text!) return [path.splitext(path.basename(f))[
0] # splitext = split ext (not spli text!)
for f in listdir('.') if f.endswith('.session')] for f in listdir('.') if f.endswith('.session')]
# endregion # endregion
# region Dialogs ("chats") requests # region Dialogs ("chats") requests
def get_dialogs(self, count=10, offset_date=None, offset_id=0, offset_peer=InputPeerEmpty()): def get_dialogs(self,
count=10,
offset_date=None,
offset_id=0,
offset_peer=InputPeerEmpty()):
"""Returns a tuple of lists ([dialogs], [entities]) with 'count' items each. """Returns a tuple of lists ([dialogs], [entities]) with 'count' items each.
The `entity` represents the user, chat or channel corresponding to that dialog""" The `entity` represents the user, chat or channel corresponding to that dialog"""
r = self.invoke(GetDialogsRequest(offset_date=offset_date, r = self.invoke(
GetDialogsRequest(
offset_date=offset_date,
offset_id=offset_id, offset_id=offset_id,
offset_peer=offset_peer, offset_peer=offset_peer,
limit=count)) limit=count))
return (r.dialogs, return (
r.dialogs,
[find_user_or_chat(d.peer, r.users, r.chats) for d in r.dialogs]) [find_user_or_chat(d.peer, r.users, r.chats) for d in r.dialogs])
# endregion # endregion
# region Message requests # region Message requests
def send_message(self, input_peer, message, markdown=False, no_web_page=False): def send_message(self,
input_peer,
message,
markdown=False,
no_web_page=False):
"""Sends a message to the given input peer and returns the sent message ID""" """Sends a message to the given input peer and returns the sent message ID"""
if markdown: if markdown:
msg, entities = parse_message_entities(message) msg, entities = parse_message_entities(message)
@ -275,15 +293,23 @@ class TelegramClient:
msg, entities = message, [] msg, entities = message, []
msg_id = utils.generate_random_long() msg_id = utils.generate_random_long()
self.invoke(SendMessageRequest(peer=input_peer, self.invoke(
SendMessageRequest(
peer=input_peer,
message=msg, message=msg,
random_id=msg_id, random_id=msg_id,
entities=entities, entities=entities,
no_webpage=no_web_page)) no_webpage=no_web_page))
return msg_id return msg_id
def get_message_history(self, input_peer, limit=20, def get_message_history(self,
offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0): input_peer,
limit=20,
offset_date=None,
offset_id=0,
max_id=0,
min_id=0,
add_offset=0):
""" """
Gets the message history for the specified InputPeer Gets the message history for the specified InputPeer
@ -298,7 +324,9 @@ class TelegramClient:
:return: A tuple containing total message count and two more lists ([messages], [senders]). :return: A tuple containing total message count and two more lists ([messages], [senders]).
Note that the sender can be null if it was not found! Note that the sender can be null if it was not found!
""" """
result = self.invoke(GetHistoryRequest(input_peer, result = self.invoke(
GetHistoryRequest(
input_peer,
limit=limit, limit=limit,
offset_date=offset_date, offset_date=offset_date,
offset_id=offset_id, offset_id=offset_id,
@ -331,7 +359,8 @@ class TelegramClient:
Returns an AffectedMessages TLObject""" Returns an AffectedMessages TLObject"""
if max_id is None: if max_id is None:
if not messages: if not messages:
raise InvalidParameterError('Either a message list or a max_id must be provided.') raise InvalidParameterError(
'Either a message list or a max_id must be provided.')
if isinstance(messages, list): if isinstance(messages, list):
max_id = max(msg.id for msg in messages) max_id = max(msg.id for msg in messages)
@ -347,7 +376,11 @@ class TelegramClient:
# be handled through a separate session and a separate connection" # be handled through a separate session and a separate connection"
# region Uploading media requests # region Uploading media requests
def upload_file(self, file_path, part_size_kb=None, file_name=None, progress_callback=None): def upload_file(self,
file_path,
part_size_kb=None,
file_name=None,
progress_callback=None):
"""Uploads the specified file_path and returns a handle which can be later used """Uploads the specified file_path and returns a handle which can be later used
:param file_path: The file path of the file that will be uploaded :param file_path: The file path of the file that will be uploaded
@ -375,7 +408,7 @@ class TelegramClient:
# Multiply the datetime timestamp by 10^6 to get the ticks # Multiply the datetime timestamp by 10^6 to get the ticks
# This is high likely going to be unique # This is high likely going to be unique
file_id = int(datetime.now().timestamp() * (10 ** 6)) file_id = int(datetime.now().timestamp() * (10**6))
hash_md5 = md5() hash_md5 = md5()
with open(file_path, 'rb') as file: with open(file_path, 'rb') as file:
@ -386,7 +419,8 @@ class TelegramClient:
# The SavePartRequest is different depending on whether # The SavePartRequest is different depending on whether
# the file is too large or not (over or less than 10MB) # the file is too large or not (over or less than 10MB)
if is_large: if is_large:
request = SaveBigFilePartRequest(file_id, part_index, part_count, part) request = SaveBigFilePartRequest(file_id, part_index,
part_count, part)
else: else:
request = SaveFilePartRequest(file_id, part_index, part) request = SaveFilePartRequest(file_id, part_index, part)
@ -397,14 +431,16 @@ class TelegramClient:
if progress_callback: if progress_callback:
progress_callback(file.tell(), file_size) progress_callback(file.tell(), file_size)
else: else:
raise ValueError('Could not upload file part #{}'.format(part_index)) raise ValueError('Could not upload file part #{}'.format(
part_index))
# Set a default file name if None was specified # Set a default file name if None was specified
if not file_name: if not file_name:
file_name = path.basename(file_path) file_name = path.basename(file_path)
# After the file has been uploaded, we can return a handle pointing to it # After the file has been uploaded, we can return a handle pointing to it
return InputFile(id=file_id, return InputFile(
id=file_id,
parts=part_count, parts=part_count,
name=file_name, name=file_name,
md5_checksum=hash_md5.hexdigest()) md5_checksum=hash_md5.hexdigest())
@ -431,14 +467,19 @@ class TelegramClient:
# «The "octet-stream" subtype is used to indicate that a body contains arbitrary binary data.» # «The "octet-stream" subtype is used to indicate that a body contains arbitrary binary data.»
if not mime_type: if not mime_type:
mime_type = 'application/octet-stream' mime_type = 'application/octet-stream'
self.send_media_file(InputMediaUploadedDocument(file=input_file, self.send_media_file(
InputMediaUploadedDocument(
file=input_file,
mime_type=mime_type, mime_type=mime_type,
attributes=attributes, attributes=attributes,
caption=caption), input_peer) caption=caption),
input_peer)
def send_media_file(self, input_media, input_peer): def send_media_file(self, input_media, input_peer):
"""Sends any input_media (contact, document, photo...) to an input_peer""" """Sends any input_media (contact, document, photo...) to an input_peer"""
self.invoke(SendMediaRequest(peer=input_peer, self.invoke(
SendMediaRequest(
peer=input_peer,
media=input_media, media=input_media,
random_id=utils.generate_random_long())) random_id=utils.generate_random_long()))
@ -446,8 +487,11 @@ class TelegramClient:
# region Downloading media requests # region Downloading media requests
def download_profile_photo(self, profile_photo, file_path, def download_profile_photo(self,
add_extension=True, download_big=True): profile_photo,
file_path,
add_extension=True,
download_big=True):
"""Downloads the profile photo for an user or a chat (including channels). """Downloads the profile photo for an user or a chat (including channels).
Returns False if no photo was providen, or if it was Empty""" Returns False if no photo was providen, or if it was Empty"""
@ -465,28 +509,40 @@ class TelegramClient:
photo_location = profile_photo.photo_small photo_location = profile_photo.photo_small
# Download the media with the largest size input file location # Download the media with the largest size input file location
self.download_file_loc(InputFileLocation(volume_id=photo_location.volume_id, self.download_file_loc(
InputFileLocation(
volume_id=photo_location.volume_id,
local_id=photo_location.local_id, local_id=photo_location.local_id,
secret=photo_location.secret), secret=photo_location.secret),
file_path) file_path)
return True return True
def download_msg_media(self, message_media, file_path, add_extension=True, progress_callback=None): def download_msg_media(self,
message_media,
file_path,
add_extension=True,
progress_callback=None):
"""Downloads the given MessageMedia (Photo, Document or Contact) """Downloads the given MessageMedia (Photo, Document or Contact)
into the desired file_path, optionally finding its extension automatically into the desired file_path, optionally finding its extension automatically
The progress_callback should be a callback function which takes two parameters, The progress_callback should be a callback function which takes two parameters,
uploaded size (in bytes) and total file size (in bytes). uploaded size (in bytes) and total file size (in bytes).
This will be called every time a part is downloaded""" This will be called every time a part is downloaded"""
if type(message_media) == MessageMediaPhoto: if type(message_media) == MessageMediaPhoto:
return self.download_photo(message_media, file_path, add_extension, progress_callback) return self.download_photo(message_media, file_path, add_extension,
progress_callback)
elif type(message_media) == MessageMediaDocument: elif type(message_media) == MessageMediaDocument:
return self.download_document(message_media, file_path, add_extension, progress_callback) return self.download_document(message_media, file_path,
add_extension, progress_callback)
elif type(message_media) == MessageMediaContact: elif type(message_media) == MessageMediaContact:
return self.download_contact(message_media, file_path, add_extension) return self.download_contact(message_media, file_path,
add_extension)
def download_photo(self, message_media_photo, file_path, add_extension=False, def download_photo(self,
message_media_photo,
file_path,
add_extension=False,
progress_callback=None): progress_callback=None):
"""Downloads MessageMediaPhoto's largest size into the desired """Downloads MessageMediaPhoto's largest size into the desired
file_path, optionally finding its extension automatically file_path, optionally finding its extension automatically
@ -504,13 +560,20 @@ class TelegramClient:
file_path += get_extension(message_media_photo) file_path += get_extension(message_media_photo)
# Download the media with the largest size input file location # Download the media with the largest size input file location
self.download_file_loc(InputFileLocation(volume_id=largest_size.volume_id, self.download_file_loc(
InputFileLocation(
volume_id=largest_size.volume_id,
local_id=largest_size.local_id, local_id=largest_size.local_id,
secret=largest_size.secret), secret=largest_size.secret),
file_path, file_size=file_size, progress_callback=progress_callback) file_path,
file_size=file_size,
progress_callback=progress_callback)
return file_path return file_path
def download_document(self, message_media_document, file_path=None, add_extension=True, def download_document(self,
message_media_document,
file_path=None,
add_extension=True,
progress_callback=None): progress_callback=None):
"""Downloads the given MessageMediaDocument into the desired """Downloads the given MessageMediaDocument into the desired
file_path, optionally finding its extension automatically. file_path, optionally finding its extension automatically.
@ -537,10 +600,14 @@ class TelegramClient:
if add_extension: if add_extension:
file_path += get_extension(document.mime_type) file_path += get_extension(document.mime_type)
self.download_file_loc(InputDocumentFileLocation(id=document.id, self.download_file_loc(
InputDocumentFileLocation(
id=document.id,
access_hash=document.access_hash, access_hash=document.access_hash,
version=document.version), version=document.version),
file_path, file_size=file_size, progress_callback=progress_callback) file_path,
file_size=file_size,
progress_callback=progress_callback)
return file_path return file_path
@staticmethod @staticmethod
@ -562,15 +629,21 @@ class TelegramClient:
with open(file_path, 'w', encoding='utf-8') as file: with open(file_path, 'w', encoding='utf-8') as file:
file.write('BEGIN:VCARD\n') file.write('BEGIN:VCARD\n')
file.write('VERSION:4.0\n') file.write('VERSION:4.0\n')
file.write('N:{};{};;;\n'.format(first_name, last_name if last_name else '')) file.write('N:{};{};;;\n'.format(first_name, last_name
if last_name else ''))
file.write('FN:{}\n'.format(' '.join((first_name, last_name)))) file.write('FN:{}\n'.format(' '.join((first_name, last_name))))
file.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(phone_number)) file.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(
phone_number))
file.write('END:VCARD\n') file.write('END:VCARD\n')
return file_path return file_path
def download_file_loc(self, input_location, file_path, part_size_kb=64, def download_file_loc(self,
file_size=None, progress_callback=None): input_location,
file_path,
part_size_kb=64,
file_size=None,
progress_callback=None):
"""Downloads media from the given input_file_location to the specified file_path. """Downloads media from the given input_file_location to the specified file_path.
If a progress_callback function is given, it will be called taking two If a progress_callback function is given, it will be called taking two
arguments (downloaded bytes count and total file size)""" arguments (downloaded bytes count and total file size)"""
@ -594,7 +667,8 @@ class TelegramClient:
while True: while True:
# The current offset equals the offset_index multiplied by the part size # The current offset equals the offset_index multiplied by the part size
offset = offset_index * part_size offset = offset_index * part_size
result = self.invoke(GetFileRequest(input_location, offset, part_size)) result = self.invoke(
GetFileRequest(input_location, offset, part_size))
offset_index += 1 offset_index += 1
# If we have received no data (0 bytes), the file is over # If we have received no data (0 bytes), the file is over
@ -616,7 +690,8 @@ class TelegramClient:
"""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"""
if not self.signed_in: if not self.signed_in:
raise ValueError("You cannot add update handlers until you've signed in.") raise ValueError(
"You cannot add update handlers until you've signed in.")
self.sender.add_update_handler(handler) self.sender.add_update_handler(handler)

View File

@ -26,7 +26,8 @@ class MTProtoRequest:
self.confirm_received = True self.confirm_received = True
def need_resend(self): def need_resend(self):
return self.dirty or (self.confirmed and not self.confirm_received and return self.dirty or (
self.confirmed and not self.confirm_received and
datetime.now() - self.send_time > timedelta(seconds=3)) datetime.now() - self.send_time > timedelta(seconds=3))
# These should be overrode # These should be overrode

View File

@ -1,8 +1,9 @@
from os.path import isfile as file_exists
import os import os
import time
import pickle import pickle
import random import random
import time
from os.path import isfile as file_exists
import telethon.helpers as utils import telethon.helpers as utils
@ -39,12 +40,11 @@ class Session:
If the given session_user_id is None, we assume that it is for testing purposes""" If the given session_user_id is None, we assume that it is for testing purposes"""
if session_user_id is None: if session_user_id is None:
return Session(None) return Session(None)
else: else:
filepath = '{}.session'.format(session_user_id) path = '{}.session'.format(session_user_id)
if file_exists(filepath): if file_exists(path):
with open(filepath, 'rb') as file: with open(path, 'rb') as file:
return pickle.load(file) return pickle.load(file)
else: else:
return Session(session_user_id) return Session(session_user_id)
@ -53,9 +53,12 @@ class Session:
"""Generates a new message ID based on the current time (in ms) since epoch""" """Generates a new message ID based on the current time (in ms) since epoch"""
# Refer to mtproto_plain_sender.py for the original method, this is a simple copy # Refer to mtproto_plain_sender.py for the original method, this is a simple copy
ms_time = int(time.time() * 1000) ms_time = int(time.time() * 1000)
new_msg_id = (((ms_time // 1000 + self.time_offset) << 32) | # "must approximately equal unixtime*2^32" new_msg_id = (((ms_time // 1000 + self.time_offset) << 32)
((ms_time % 1000) << 22) | # "approximate moment in time the message was created" | # "must approximately equal unixtime*2^32"
random.randint(0, 524288) << 2) # "message identifiers are divisible by 4" ((ms_time % 1000) << 22)
| # "approximate moment in time the message was created"
random.randint(0, 524288)
<< 2) # "message identifiers are divisible by 4"
if self.last_message_id >= new_msg_id: if self.last_message_id >= new_msg_id:
new_msg_id = self.last_message_id + 4 new_msg_id = self.last_message_id + 4

View File

@ -1,10 +1,10 @@
from datetime import datetime
from io import BytesIO, BufferedReader
from telethon.tl.all_tlobjects import tlobjects
from struct import unpack
from telethon.errors import *
import inspect
import os import os
from datetime import datetime
from io import BufferedReader, BytesIO
from struct import unpack
from telethon.errors import *
from telethon.tl.all_tlobjects import tlobjects
class BinaryReader: class BinaryReader:
@ -12,13 +12,15 @@ class BinaryReader:
Small utility class to read binary data. Small utility class to read binary data.
Also creates a "Memory Stream" if necessary Also creates a "Memory Stream" if necessary
""" """
def __init__(self, data=None, stream=None): def __init__(self, data=None, stream=None):
if data: if data:
self.stream = BytesIO(data) self.stream = BytesIO(data)
elif stream: elif stream:
self.stream = stream self.stream = stream
else: else:
raise InvalidParameterError("Either bytes or a stream must be provided") raise InvalidParameterError(
'Either bytes or a stream must be provided')
self.reader = BufferedReader(self.stream) self.reader = BufferedReader(self.stream)
@ -47,13 +49,15 @@ class BinaryReader:
def read_large_int(self, bits, signed=True): def read_large_int(self, bits, signed=True):
"""Reads a n-bits long integer value""" """Reads a n-bits long integer value"""
return int.from_bytes(self.read(bits // 8), byteorder='little', signed=signed) return int.from_bytes(
self.read(bits // 8), byteorder='little', signed=signed)
def read(self, length): def read(self, length):
"""Read the given amount of bytes""" """Read the given amount of bytes"""
result = self.reader.read(length) result = self.reader.read(length)
if len(result) != length: if len(result) != length:
raise BufferError('Trying to read outside the data bounds (no more data left to read)') raise BufferError(
'Trying to read outside the data bounds (no more data left to read)')
return result return result
@ -69,7 +73,8 @@ class BinaryReader:
"""Reads a Telegram-encoded byte array, without the need of specifying its length""" """Reads a Telegram-encoded byte array, without the need of specifying its length"""
first_byte = self.read_byte() first_byte = self.read_byte()
if first_byte == 254: if first_byte == 254:
length = self.read_byte() | (self.read_byte() << 8) | (self.read_byte() << 16) length = self.read_byte() | (self.read_byte() << 8) | (
self.read_byte() << 16)
padding = length % 4 padding = length % 4
else: else:
length = first_byte length = first_byte

View File

@ -1,4 +1,4 @@
from io import BytesIO, BufferedWriter from io import BufferedWriter, BytesIO
from struct import pack from struct import pack
@ -26,12 +26,16 @@ class BinaryWriter:
def write_int(self, value, signed=True): def write_int(self, value, signed=True):
"""Writes an integer value (4 bytes), which can or cannot be signed""" """Writes an integer value (4 bytes), which can or cannot be signed"""
self.writer.write(int.to_bytes(value, length=4, byteorder='little', signed=signed)) self.writer.write(
int.to_bytes(
value, length=4, byteorder='little', signed=signed))
self.written_count += 4 self.written_count += 4
def write_long(self, value, signed=True): def write_long(self, value, signed=True):
"""Writes a long integer value (8 bytes), which can or cannot be signed""" """Writes a long integer value (8 bytes), which can or cannot be signed"""
self.writer.write(int.to_bytes(value, length=8, byteorder='little', signed=signed)) self.writer.write(
int.to_bytes(
value, length=8, byteorder='little', signed=signed))
self.written_count += 8 self.written_count += 8
def write_float(self, value): def write_float(self, value):
@ -46,7 +50,9 @@ class BinaryWriter:
def write_large_int(self, value, bits, signed=True): def write_large_int(self, value, bits, signed=True):
"""Writes a n-bits long integer value""" """Writes a n-bits long integer value"""
self.writer.write(int.to_bytes(value, length=bits // 8, byteorder='little', signed=signed)) self.writer.write(
int.to_bytes(
value, length=bits // 8, byteorder='little', signed=signed))
self.written_count += bits // 8 self.written_count += bits // 8
def write(self, data): def write(self, data):

View File

@ -4,12 +4,10 @@
after all, both are the same attribute, IDs.""" after all, both are the same attribute, IDs."""
from mimetypes import add_type, guess_extension from mimetypes import add_type, guess_extension
from telethon.tl.types import \ from telethon.tl.types import (
User, Chat, Channel, \ Channel, Chat, ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser,
PeerUser, PeerChat, PeerChannel, \ MessageMediaDocument, MessageMediaPhoto, PeerChannel, PeerChat, PeerUser,
InputPeerUser, InputPeerChat, InputPeerChannel, \ User, UserProfilePhoto)
UserProfilePhoto, ChatPhoto, \
MessageMediaPhoto, MessageMediaDocument
def get_display_name(entity): def get_display_name(entity):
@ -31,8 +29,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 if (isinstance(media, UserProfilePhoto) or isinstance(media, ChatPhoto) or
isinstance(media, ChatPhoto) or
isinstance(media, MessageMediaPhoto)): isinstance(media, MessageMediaPhoto)):
return '.jpg' return '.jpg'

View File

@ -18,7 +18,8 @@ class SourceBuilder:
"""Writes a string into the source code, applying indentation if required""" """Writes a string into the source code, applying indentation if required"""
if self.on_new_line: if self.on_new_line:
self.on_new_line = False # We're not on a new line anymore self.on_new_line = False # We're not on a new line anymore
if string.strip(): # If the string was not empty, indent; Else it probably was a new line if string.strip(
): # If the string was not empty, indent; Else it probably was a new line
self.indent() self.indent()
self.out_stream.write(string) self.out_stream.write(string)

View File

@ -5,13 +5,13 @@ class TLObject:
""".tl core types IDs (such as vector, booleans, etc.)""" """.tl core types IDs (such as vector, booleans, etc.)"""
CORE_TYPES = (0x1cb5c415, 0xbc799737, 0x997275b5, 0x3fedd339) CORE_TYPES = (0x1cb5c415, 0xbc799737, 0x997275b5, 0x3fedd339)
def __init__(self, fullname, id, args, result, is_function): def __init__(self, fullname, object_id, args, result, is_function):
""" """
Initializes a new TLObject, given its properties. Initializes a new TLObject, given its properties.
Usually, this will be called from `from_tl` instead Usually, this will be called from `from_tl` instead
:param fullname: The fullname of the TL object (namespace.name) :param fullname: The fullname of the TL object (namespace.name)
The namespace can be omitted The namespace can be omitted
:param id: The hexadecimal string representing the object ID :param object_id: The hexadecimal string representing the object ID
:param args: The arguments, if any, of the TL object :param args: The arguments, if any, of the TL object
:param result: The result type of the TL object :param result: The result type of the TL object
:param is_function: Is the object a function or a type? :param is_function: Is the object a function or a type?
@ -25,7 +25,7 @@ class TLObject:
self.name = fullname self.name = fullname
# The ID should be an hexadecimal string # The ID should be an hexadecimal string
self.id = int(id, base=16) self.id = int(object_id, base=16)
self.args = args self.args = args
self.result = result self.result = result
self.is_function = is_function self.is_function = is_function
@ -67,11 +67,13 @@ class TLObject:
''', tl, re.IGNORECASE | re.VERBOSE) ''', tl, re.IGNORECASE | re.VERBOSE)
# Retrieve the matched arguments # Retrieve the matched arguments
args = [TLArg(name, type, brace != '') for brace, name, type, _ in args_match] args = [TLArg(name, arg_type, brace != '')
for brace, name, arg_type, _ in args_match]
# And initialize the TLObject # And initialize the TLObject
return TLObject(fullname=match.group(1), return TLObject(
id=match.group(2), fullname=match.group(1),
object_id=match.group(2),
args=args, args=args,
result=match.group(3), result=match.group(3),
is_function=is_function) is_function=is_function)
@ -82,19 +84,19 @@ class TLObject:
return self.id in TLObject.CORE_TYPES return self.id in TLObject.CORE_TYPES
def __repr__(self): def __repr__(self):
fullname = ('{}.{}'.format(self.namespace, self.name) if self.namespace is not None fullname = ('{}.{}'.format(self.namespace, self.name)
else self.name) if self.namespace is not None else self.name)
hex_id = hex(self.id)[2:].rjust(8, '0') # Skip 0x and add 0's for padding hex_id = hex(self.id)[2:].rjust(8,
'0') # Skip 0x and add 0's for padding
return '{}#{} {} = {}'.format(fullname, return '{}#{} {} = {}'.format(
hex_id, fullname, hex_id, ' '.join([str(arg) for arg in self.args]),
' '.join([str(arg) for arg in self.args]),
self.result) self.result)
def __str__(self): def __str__(self):
fullname = ('{}.{}'.format(self.namespace, self.name) if self.namespace is not None fullname = ('{}.{}'.format(self.namespace, self.name)
else self.name) if self.namespace is not None else self.name)
# Some arguments are not valid for being represented, such as the flag indicator or generic definition # Some arguments are not valid for being represented, such as the flag indicator or generic definition
# (these have no explicit values until used) # (these have no explicit values until used)
@ -104,20 +106,21 @@ class TLObject:
args = ', '.join(['{}={{}}'.format(arg.name) for arg in valid_args]) args = ', '.join(['{}={{}}'.format(arg.name) for arg in valid_args])
# Since Python's default representation for lists is using repr(), we need to str() manually on every item # Since Python's default representation for lists is using repr(), we need to str() manually on every item
args_format = ', '.join(['str(self.{})'.format(arg.name) if not arg.is_vector else args_format = ', '.join(
'None if not self.{0} else [str(_) for _ in self.{0}]'.format(arg.name) ['str(self.{})'.format(arg.name) if not arg.is_vector else
for arg in valid_args]) 'None if not self.{0} else [str(_) for _ in self.{0}]'.format(
arg.name) for arg in valid_args])
return ("'({} (ID: {}) = ({}))'.format({})" return ("'({} (ID: {}) = ({}))'.format({})"
.format(fullname, hex(self.id), args, args_format)) .format(fullname, hex(self.id), args, args_format))
class TLArg: class TLArg:
def __init__(self, name, type, generic_definition): def __init__(self, name, arg_type, generic_definition):
""" """
Initializes a new .tl argument Initializes a new .tl argument
:param name: The name of the .tl argument :param name: The name of the .tl argument
:param type: The type of the .tl argument :param arg_type: The type of the .tl argument
:param generic_definition: Is the argument a generic definition? :param generic_definition: Is the argument a generic definition?
(i.e. {X:Type}) (i.e. {X:Type})
""" """
@ -132,14 +135,15 @@ class TLArg:
self.flag_index = -1 self.flag_index = -1
# The type can be an indicator that other arguments will be flags # The type can be an indicator that other arguments will be flags
if type == '#': if arg_type == '#':
self.flag_indicator = True self.flag_indicator = True
self.type = None self.type = None
self.is_generic = False self.is_generic = False
else: else:
self.flag_indicator = False self.flag_indicator = False
self.is_generic = type.startswith('!') self.is_generic = arg_type.startswith('!')
self.type = type.lstrip('!') # Strip the exclamation mark always to have only the name self.type = arg_type.lstrip(
'!') # Strip the exclamation mark always to have only the name
# The type may be a flag (flags.IDX?REAL_TYPE) # The type may be a flag (flags.IDX?REAL_TYPE)
# Note that «flags» is NOT the flags name; this is determined by a previous argument # Note that «flags» is NOT the flags name; this is determined by a previous argument
@ -148,13 +152,15 @@ class TLArg:
if flag_match: if flag_match:
self.is_flag = True self.is_flag = True
self.flag_index = int(flag_match.group(1)) self.flag_index = int(flag_match.group(1))
self.type = flag_match.group(2) # Update the type to match the exact type, not the "flagged" one self.type = flag_match.group(
2) # Update the type to match the exact type, not the "flagged" one
# 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'vector<(\w+)>', self.type, re.IGNORECASE)
if vector_match: if vector_match:
self.is_vector = True self.is_vector = True
self.type = vector_match.group(1) # Update the type to match the one inside the vector self.type = vector_match.group(
1) # Update the type to match the one inside the vector
# 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.

View File

@ -2,7 +2,7 @@ import os
import re import re
import shutil import shutil
from parser import SourceBuilder, TLParser from .parser import SourceBuilder, TLParser
def get_output_path(normal_path): def get_output_path(normal_path):
@ -60,8 +60,8 @@ class TLGenerator:
continue continue
# Determine the output directory and create it # Determine the output directory and create it
out_dir = get_output_path('functions' if tlobject.is_function out_dir = get_output_path('functions'
else 'types') if tlobject.is_function else 'types')
if tlobject.namespace: if tlobject.namespace:
out_dir = os.path.join(out_dir, tlobject.namespace) out_dir = os.path.join(out_dir, tlobject.namespace)
@ -77,39 +77,51 @@ class TLGenerator:
TLGenerator.get_class_name(tlobject))) TLGenerator.get_class_name(tlobject)))
# Create the file for this TLObject # Create the file for this TLObject
filename = os.path.join(out_dir, TLGenerator.get_file_name(tlobject, add_extension=True)) filename = os.path.join(
out_dir,
TLGenerator.get_file_name(
tlobject, add_extension=True))
with open(filename, 'w', encoding='utf-8') as file: with open(filename, 'w', encoding='utf-8') as file:
# Let's build the source code! # Let's build the source code!
with SourceBuilder(file) as builder: with SourceBuilder(file) as builder:
# Both types and functions inherit from MTProtoRequest so they all can be sent # Both types and functions inherit from MTProtoRequest so they all can be sent
builder.writeln('from telethon.tl.mtproto_request import MTProtoRequest') builder.writeln(
'from telethon.tl.mtproto_request import MTProtoRequest')
builder.writeln() builder.writeln()
builder.writeln() builder.writeln()
builder.writeln('class {}(MTProtoRequest):'.format(TLGenerator.get_class_name(tlobject))) builder.writeln('class {}(MTProtoRequest):'.format(
TLGenerator.get_class_name(tlobject)))
# Write the original .tl definition, along with a "generated automatically" message # Write the original .tl definition, along with a "generated automatically" message
builder.writeln('"""Class generated by TLObjects\' generator. ' builder.writeln(
'"""Class generated by TLObjects\' generator. '
'All changes will be ERASED. Original .tl definition below.') 'All changes will be ERASED. Original .tl definition below.')
builder.writeln('{}"""'.format(repr(tlobject))) builder.writeln('{}"""'.format(repr(tlobject)))
builder.writeln() builder.writeln()
# Create an class-level variable that stores the TLObject's constructor ID # Create an class-level variable that stores the TLObject's constructor ID
builder.writeln("# Telegram's constructor ID (and unique identifier) for this class") builder.writeln(
builder.writeln('constructor_id = {}'.format(hex(tlobject.id))) "# Telegram's constructor ID (and unique identifier) for this class")
builder.writeln('constructor_id = {}'.format(
hex(tlobject.id)))
builder.writeln() builder.writeln()
# First sort the arguments so that those not being a flag come first # First sort the arguments so that those not being a flag come first
args = sorted([arg for arg in tlobject.args if not arg.flag_indicator], args = sorted(
[arg for arg in tlobject.args
if not arg.flag_indicator],
key=lambda x: x.is_flag) key=lambda x: x.is_flag)
# Then convert the args to string parameters, the flags having =None # Then convert the args to string parameters, the flags having =None
args = [(arg.name if not arg.is_flag args = [(arg.name if not arg.is_flag else
else '{}=None'.format(arg.name)) for arg in args '{}=None'.format(arg.name)) for arg in args
if not arg.flag_indicator and not arg.generic_definition] if not arg.flag_indicator and
not arg.generic_definition]
# Write the __init__ function # Write the __init__ function
if args: if args:
builder.writeln('def __init__(self, {}):'.format(', '.join(args))) builder.writeln('def __init__(self, {}):'.format(
', '.join(args)))
else: else:
builder.writeln('def __init__(self):') builder.writeln('def __init__(self):')
@ -117,18 +129,23 @@ class TLGenerator:
# those which are generated automatically: flag indicator and generic definitions. # those which are generated automatically: flag indicator and generic definitions.
# We don't need the generic definitions in Python because arguments can be any type # We don't need the generic definitions in Python because arguments can be any type
args = [arg for arg in tlobject.args args = [arg for arg in tlobject.args
if not arg.flag_indicator and not arg.generic_definition] if not arg.flag_indicator and
not arg.generic_definition]
if args: if args:
# Write the docstring, so we know the type of the arguments # Write the docstring, so we know the type of the arguments
builder.writeln('"""') builder.writeln('"""')
for arg in args: for arg in args:
if not arg.flag_indicator: if not arg.flag_indicator:
builder.write(':param {}: Telegram type: «{}».'.format(arg.name, arg.type)) builder.write(
':param {}: Telegram type: «{}».'.format(
arg.name, arg.type))
if arg.is_vector: if arg.is_vector:
builder.write(' Must be a list.'.format(arg.name)) builder.write(' Must be a list.'.format(
arg.name))
if arg.is_generic: if arg.is_generic:
builder.write(' This should be another MTProtoRequest.') builder.write(
' This should be another MTProtoRequest.')
builder.writeln() builder.writeln()
builder.writeln('"""') builder.writeln('"""')
@ -136,7 +153,8 @@ class TLGenerator:
# Functions have a result object and are confirmed by default # Functions have a result object and are confirmed by default
if tlobject.is_function: if tlobject.is_function:
builder.writeln('self.result = None') builder.writeln('self.result = None')
builder.writeln('self.confirmed = True # Confirmed by default') builder.writeln(
'self.confirmed = True # Confirmed by default')
# Set the arguments # Set the arguments
if args: if args:
@ -148,22 +166,24 @@ class TLGenerator:
# Write the on_send(self, writer) function # Write the on_send(self, writer) function
builder.writeln('def on_send(self, writer):') builder.writeln('def on_send(self, writer):')
builder.writeln('writer.write_int({}.constructor_id, signed=False)' builder.writeln(
'writer.write_int({}.constructor_id, signed=False)'
.format(TLGenerator.get_class_name(tlobject))) .format(TLGenerator.get_class_name(tlobject)))
for arg in tlobject.args: for arg in tlobject.args:
TLGenerator.write_onsend_code(builder, arg, tlobject.args) TLGenerator.write_onsend_code(builder, arg,
tlobject.args)
builder.end_block() builder.end_block()
# Write the empty() function, which returns an "empty" # Write the empty() function, which returns an "empty"
# instance, in which all attributes are set to None # instance, in which all attributes are set to None
builder.writeln('@staticmethod') builder.writeln('@staticmethod')
builder.writeln('def empty():') builder.writeln('def empty():')
builder.writeln('"""Returns an "empty" instance (all attributes are None)"""') builder.writeln(
'"""Returns an "empty" instance (all attributes are None)"""')
builder.writeln('return {}({})'.format( builder.writeln('return {}({})'.format(
TLGenerator.get_class_name(tlobject), TLGenerator.get_class_name(tlobject), ', '.join(
', '.join('None' for _ in range(len(args))) 'None' for _ in range(len(args)))))
))
builder.end_block() builder.end_block()
# Write the on_response(self, reader) function # Write the on_response(self, reader) function
@ -174,7 +194,8 @@ class TLGenerator:
else: else:
if tlobject.args: if tlobject.args:
for arg in tlobject.args: for arg in tlobject.args:
TLGenerator.write_onresponse_code(builder, arg, tlobject.args) TLGenerator.write_onresponse_code(
builder, arg, tlobject.args)
else: else:
# If there were no arguments, we still need an on_response method, and hence "pass" if empty # If there were no arguments, we still need an on_response method, and hence "pass" if empty
builder.writeln('pass') builder.writeln('pass')
@ -186,23 +207,26 @@ class TLGenerator:
builder.end_block() builder.end_block()
builder.writeln('def __str__(self):') builder.writeln('def __str__(self):')
builder.writeln("return {}".format(str(tlobject))) builder.writeln('return {}'.format(str(tlobject)))
# builder.end_block() # There is no need to end the last block # builder.end_block() # There is no need to end the last block
# Step 3: Once all the objects have been generated, we can now group them in a single file # Step 3: Once all the objects have been generated, we can now group them in a single file
filename = os.path.join(get_output_path('all_tlobjects.py')) filename = os.path.join(get_output_path('all_tlobjects.py'))
with open(filename, 'w', encoding='utf-8') as file: with open(filename, 'w', encoding='utf-8') as file:
with SourceBuilder(file) as builder: with SourceBuilder(file) as builder:
builder.writeln('"""File generated by TLObjects\' generator. All changes will be ERASED"""') builder.writeln(
'"""File generated by TLObjects\' generator. All changes will be ERASED"""')
builder.writeln() builder.writeln()
# First add imports # First add imports
for tlobject in tlobjects: for tlobject in tlobjects:
builder.writeln('import {}'.format(TLGenerator.get_full_file_name(tlobject))) builder.writeln('import {}'.format(
TLGenerator.get_full_file_name(tlobject)))
builder.writeln() builder.writeln()
# Create a variable to indicate which layer this is # Create a variable to indicate which layer this is
builder.writeln('layer = {} # Current generated layer'.format(TLParser.find_layer(scheme_file))) builder.writeln('layer = {} # Current generated layer'.format(
TLParser.find_layer(scheme_file)))
builder.writeln() builder.writeln()
# Then create the dictionary containing constructor_id: class # Then create the dictionary containing constructor_id: class
@ -211,10 +235,9 @@ class TLGenerator:
# Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class) # Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class)
for tlobject in tlobjects: for tlobject in tlobjects:
builder.writeln('{}: {}.{},' builder.writeln('{}: {}.{},'.format(
.format(hex(tlobject.id), hex(tlobject.id), TLGenerator.get_full_file_name(
TLGenerator.get_full_file_name(tlobject), tlobject), TLGenerator.get_class_name(tlobject)))
TLGenerator.get_class_name(tlobject)))
builder.current_indent -= 1 builder.current_indent -= 1
builder.writeln('}') builder.writeln('}')
@ -225,8 +248,10 @@ class TLGenerator:
# Courtesy of http://stackoverflow.com/a/31531797/4759433 # Courtesy of http://stackoverflow.com/a/31531797/4759433
# Also, '_' could be replaced for ' ', then use .title(), and then remove ' ' # Also, '_' could be replaced for ' ', then use .title(), and then remove ' '
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), tlobject.name) result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(),
result = result[:1].upper() + result[1:].replace('_', '') # Replace again to fully ensure! tlobject.name)
result = result[:1].upper() + result[1:].replace(
'_', '') # Replace again to fully ensure!
# If it's a function, let it end with "Request" to identify them more easily # If it's a function, let it end with "Request" to identify them more easily
if tlobject.is_function: if tlobject.is_function:
result += 'Request' result += 'Request'
@ -283,22 +308,25 @@ class TLGenerator:
builder.writeln('if {}:'.format(name)) builder.writeln('if {}:'.format(name))
if arg.is_vector: if arg.is_vector:
builder.writeln("writer.write_int(0x1cb5c415, signed=False) # Vector's constructor ID") builder.writeln(
"writer.write_int(0x1cb5c415, signed=False) # Vector's constructor ID")
builder.writeln('writer.write_int(len({}))'.format(name)) builder.writeln('writer.write_int(len({}))'.format(name))
builder.writeln('for {}_item in {}:'.format(arg.name, name)) builder.writeln('for {}_item in {}:'.format(arg.name, name))
# 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_onsend_code(builder, arg, args, name='{}_item'.format(arg.name)) TLGenerator.write_onsend_code(
builder, arg, args, name='{}_item'.format(arg.name))
arg.is_vector = True arg.is_vector = True
elif arg.flag_indicator: elif arg.flag_indicator:
# Calculate the flags with those items which are not None # Calculate the flags with those items which are not None
builder.writeln('# Calculate the flags. This equals to those flag arguments which are NOT None') builder.writeln(
'# Calculate the flags. This equals to those flag arguments which are NOT None')
builder.writeln('flags = 0') builder.writeln('flags = 0')
for flag in args: for flag in args:
if flag.is_flag: if flag.is_flag:
builder.writeln('flags |= (1 << {}) if {} else 0' builder.writeln('flags |= (1 << {}) if {} else 0'.format(
.format(flag.flag_index, 'self.{}'.format(flag.name))) flag.flag_index, 'self.{}'.format(flag.name)))
builder.writeln('writer.write_int(flags)') builder.writeln('writer.write_int(flags)')
builder.writeln() builder.writeln()
@ -310,10 +338,12 @@ class TLGenerator:
builder.writeln('writer.write_long({})'.format(name)) builder.writeln('writer.write_long({})'.format(name))
elif 'int128' == arg.type: elif 'int128' == arg.type:
builder.writeln('writer.write_large_int({}, bits=128)'.format(name)) builder.writeln('writer.write_large_int({}, bits=128)'.format(
name))
elif 'int256' == arg.type: elif 'int256' == arg.type:
builder.writeln('writer.write_large_int({}, bits=256)'.format(name)) builder.writeln('writer.write_large_int({}, bits=256)'.format(
name))
elif 'double' == arg.type: elif 'double' == arg.type:
builder.writeln('writer.write_double({})'.format(name)) builder.writeln('writer.write_double({})'.format(name))
@ -366,7 +396,8 @@ class TLGenerator:
was_flag = False was_flag = False
if arg.is_flag: if arg.is_flag:
was_flag = True was_flag = True
builder.writeln('if (flags & (1 << {})) != 0:'.format(arg.flag_index)) builder.writeln('if (flags & (1 << {})) != 0:'.format(
arg.flag_index))
# Temporary disable .is_flag not to enter this if again when calling the method recursively # Temporary disable .is_flag not to enter this if again when calling the method recursively
arg.is_flag = False arg.is_flag = False
@ -377,7 +408,8 @@ class TLGenerator:
builder.writeln('for _ in range({}_len):'.format(arg.name)) builder.writeln('for _ in range({}_len):'.format(arg.name))
# 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='{}_item'.format(arg.name)) TLGenerator.write_onresponse_code(
builder, arg, args, name='{}_item'.format(arg.name))
builder.writeln('{}.append({}_item)'.format(name, arg.name)) builder.writeln('{}.append({}_item)'.format(name, arg.name))
arg.is_vector = True arg.is_vector = True
@ -393,10 +425,12 @@ class TLGenerator:
builder.writeln('{} = reader.read_long()'.format(name)) builder.writeln('{} = reader.read_long()'.format(name))
elif 'int128' == arg.type: elif 'int128' == arg.type:
builder.writeln('{} = reader.read_large_int(bits=128)'.format(name)) builder.writeln('{} = reader.read_large_int(bits=128)'.format(
name))
elif 'int256' == arg.type: elif 'int256' == arg.type:
builder.writeln('{} = reader.read_large_int(bits=256)'.format(name)) builder.writeln('{} = reader.read_large_int(bits=256)'.format(
name))
elif 'double' == arg.type: elif 'double' == arg.type:
builder.writeln('{} = reader.read_double()'.format(name)) builder.writeln('{} = reader.read_double()'.format(name))
@ -408,7 +442,9 @@ class TLGenerator:
builder.writeln('{} = reader.tgread_bool()'.format(name)) builder.writeln('{} = reader.tgread_bool()'.format(name))
elif 'true' == arg.type: # Awkwardly enough, Telegram has both bool and "true", used in flags elif 'true' == arg.type: # Awkwardly enough, Telegram has both bool and "true", used in flags
builder.writeln('{} = True # Arbitrary not-None value, no need to read since it is a flag'.format(name)) builder.writeln(
'{} = True # Arbitrary not-None value, no need to read since it is a flag'.
format(name))
elif 'bytes' == arg.type: elif 'bytes' == arg.type:
builder.writeln('{} = reader.tgread_bytes()'.format(name)) builder.writeln('{} = reader.tgread_bytes()'.format(name))
@ -429,6 +465,7 @@ class TLGenerator:
# Restore .is_flag # Restore .is_flag
arg.is_flag = True arg.is_flag = True
if __name__ == '__main__': if __name__ == '__main__':
if TLGenerator.tlobjects_exist(): if TLGenerator.tlobjects_exist():
print('Detected previous TLObjects. Cleaning...') print('Detected previous TLObjects. Cleaning...')

View File

@ -1,5 +1,5 @@
from .crypto_tests import CryptoTests from .crypto_test import CryptoTests
from .network_tests import NetworkTests from .network_test import NetworkTests
from .parser_tests import ParserTests from .parser_test import ParserTests
from .tl_tests import TLTests from .tl_test import TLTests
from .utils_tests import UtilsTests from .utils_test import UtilsTests

View File

@ -1,8 +1,7 @@
import unittest import unittest
from telethon.crypto import AES
import telethon.helpers as utils import telethon.helpers as utils
from telethon.crypto import Factorizator from telethon.crypto import AES, Factorizator
class CryptoTests(unittest.TestCase): class CryptoTests(unittest.TestCase):
@ -38,13 +37,15 @@ class CryptoTests(unittest.TestCase):
.format(value[:take], self.cipher_text[:take])) .format(value[:take], self.cipher_text[:take]))
value = AES.encrypt_ige(self.plain_text_padded, self.key, self.iv) value = AES.encrypt_ige(self.plain_text_padded, self.key, self.iv)
assert value == self.cipher_text_padded, ('Ciphered text ("{}") does not equal expected ("{}")' assert value == self.cipher_text_padded, (
'Ciphered text ("{}") does not equal expected ("{}")'
.format(value, self.cipher_text_padded)) .format(value, self.cipher_text_padded))
def test_aes_decrypt(self): def test_aes_decrypt(self):
# The ciphered text must always be padded # The ciphered text must always be padded
value = AES.decrypt_ige(self.cipher_text_padded, self.key, self.iv) value = AES.decrypt_ige(self.cipher_text_padded, self.key, self.iv)
assert value == self.plain_text_padded, ('Decrypted text ("{}") does not equal expected ("{}")' assert value == self.plain_text_padded, (
'Decrypted text ("{}") does not equal expected ("{}")'
.format(value, self.plain_text_padded)) .format(value, self.plain_text_padded))
@staticmethod @staticmethod
@ -69,8 +70,10 @@ class CryptoTests(unittest.TestCase):
expected_key = b"\xaf\xe3\x84Qm\xe0!\x0c\xd91\xe4\x9a\xa0v_gcx\xa1\xb0\xc9\xbc\x16'v\xcf,\x9dM\xae\xc6\xa5" expected_key = b"\xaf\xe3\x84Qm\xe0!\x0c\xd91\xe4\x9a\xa0v_gcx\xa1\xb0\xc9\xbc\x16'v\xcf,\x9dM\xae\xc6\xa5"
expected_iv = b'\xb8Q\xf3\xc5\xa3]\xc6\xdf\x9e\xe0Q\xbd"\x8d\x13\t\x0e\x9a\x9d^8\xa2\xf8\xe7\x00w\xd9\xc1\xa7\xa0\xf7\x0f' expected_iv = b'\xb8Q\xf3\xc5\xa3]\xc6\xdf\x9e\xe0Q\xbd"\x8d\x13\t\x0e\x9a\x9d^8\xa2\xf8\xe7\x00w\xd9\xc1\xa7\xa0\xf7\x0f'
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(expected_key, key) assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(expected_iv, iv) expected_key, key)
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(
expected_iv, iv)
# Calculate key being the server # Calculate key being the server
msg_key = b'\x86m\x92i\xcf\x8b\x93\xaa\x86K\x1fi\xd04\x83]' msg_key = b'\x86m\x92i\xcf\x8b\x93\xaa\x86K\x1fi\xd04\x83]'
@ -79,14 +82,17 @@ class CryptoTests(unittest.TestCase):
expected_key = b'\xdd0X\xb6\x93\x8e\xc9y\xef\x83\xf8\x8cj\xa7h\x03\xe2\xc6\xb16\xc5\xbb\xfc\xe7\xdf\xd6\xb1g\xf7u\xcfk' expected_key = b'\xdd0X\xb6\x93\x8e\xc9y\xef\x83\xf8\x8cj\xa7h\x03\xe2\xc6\xb16\xc5\xbb\xfc\xe7\xdf\xd6\xb1g\xf7u\xcfk'
expected_iv = b'\xdcL\xc2\x18\x01J"X\x86lb\xb6\xb547\xfd\xe2a4\xb6\xaf}FS\xd7[\xe0N\r\x19\xfb\xbc' expected_iv = b'\xdcL\xc2\x18\x01J"X\x86lb\xb6\xb547\xfd\xe2a4\xb6\xaf}FS\xd7[\xe0N\r\x19\xfb\xbc'
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(expected_key, key) assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(expected_iv, iv) expected_key, key)
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(
expected_iv, iv)
@staticmethod @staticmethod
def test_calc_msg_key(): def test_calc_msg_key():
value = utils.calc_msg_key(b'Some random message') value = utils.calc_msg_key(b'Some random message')
expected = b'\xdfAa\xfc\x10\xab\x89\xd2\xfe\x19C\xf1\xdd~\xbf\x81' expected = b'\xdfAa\xfc\x10\xab\x89\xd2\xfe\x19C\xf1\xdd~\xbf\x81'
assert value == expected, 'Value ("{}") does not equal expected ("{}")'.format(value, expected) assert value == expected, 'Value ("{}") does not equal expected ("{}")'.format(
value, expected)
@staticmethod @staticmethod
def test_generate_key_data_from_nonces(): def test_generate_key_data_from_nonces():
@ -97,8 +103,10 @@ class CryptoTests(unittest.TestCase):
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'?\xc4\xbd\xdf\rWU\x8a\xf5\x0f+V\xdc\x96up\x1d\xeeG\x00\x81|\x1eg\x8a\x8f{\xf0y\x80\xda\xde'
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'Q\x9dpZ\xb7\xdd\xcb\x82_\xfa\xf4\x90\xecn\x10\x9cD\xd2\x01\x8d\x83\xa0\xa4^\xb8\x91,\x7fI am'
assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format(key, expected_key) assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format(
assert iv == expected_iv, 'Key ("{}") does not equal expected ("{}")'.format(key, expected_iv) key, expected_key)
assert iv == expected_iv, 'Key ("{}") does not equal expected ("{}")'.format(
key, expected_iv)
@staticmethod @staticmethod
def test_factorizator(): def test_factorizator():

View File

@ -3,8 +3,8 @@ import socket
import threading import threading
import unittest import unittest
from telethon.network import TcpTransport, TcpClient
import telethon.network.authenticator as authenticator import telethon.network.authenticator as authenticator
from telethon.network import TcpClient, TcpTransport
def run_server_echo_thread(port): def run_server_echo_thread(port):
@ -31,7 +31,8 @@ class NetworkTests(unittest.TestCase):
client = TcpClient() client = TcpClient()
client.connect('localhost', port) client.connect('localhost', port)
client.write(msg) client.write(msg)
assert msg == client.read(16), 'Read message does not equal sent message' assert msg == client.read(
16), 'Read message does not equal sent message'
client.close() client.close()
@staticmethod @staticmethod

View File

@ -14,7 +14,7 @@ class UtilsTests(unittest.TestCase):
writer.write_float(17.0) writer.write_float(17.0)
writer.write_double(25.0) writer.write_double(25.0)
writer.write(bytes([26, 27, 28, 29, 30, 31, 32])) writer.write(bytes([26, 27, 28, 29, 30, 31, 32]))
writer.write_large_int(2 ** 127, 128, signed=False) writer.write_large_int(2**127, 128, signed=False)
data = writer.get_bytes() data = writer.get_bytes()
expected = b'\x01\x05\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x88A\x00\x00\x00\x00\x00\x00' \ expected = b'\x01\x05\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x88A\x00\x00\x00\x00\x00\x00' \
@ -24,26 +24,32 @@ class UtilsTests(unittest.TestCase):
with BinaryReader(data) as reader: with BinaryReader(data) as reader:
value = reader.read_byte() value = reader.read_byte()
assert value == 1, 'Example byte should be 1 but is {}'.format(value) assert value == 1, 'Example byte should be 1 but is {}'.format(
value)
value = reader.read_int() value = reader.read_int()
assert value == 5, 'Example integer should be 5 but is {}'.format(value) assert value == 5, 'Example integer should be 5 but is {}'.format(
value)
value = reader.read_long() value = reader.read_long()
assert value == 13, 'Example long integer should be 13 but is {}'.format(value) assert value == 13, 'Example long integer should be 13 but is {}'.format(
value)
value = reader.read_float() value = reader.read_float()
assert value == 17.0, 'Example float should be 17.0 but is {}'.format(value) assert value == 17.0, 'Example float should be 17.0 but is {}'.format(
value)
value = reader.read_double() value = reader.read_double()
assert value == 25.0, 'Example double should be 25.0 but is {}'.format(value) assert value == 25.0, 'Example double should be 25.0 but is {}'.format(
value)
value = reader.read(7) value = reader.read(7)
assert value == bytes([26, 27, 28, 29, 30, 31, 32]), 'Example bytes should be {} but is {}' \ assert value == bytes([26, 27, 28, 29, 30, 31, 32]), 'Example bytes should be {} but is {}' \
.format(bytes([26, 27, 28, 29, 30, 31, 32]), value) .format(bytes([26, 27, 28, 29, 30, 31, 32]), value)
value = reader.read_large_int(128, signed=False) value = reader.read_large_int(128, signed=False)
assert value == 2 ** 127, 'Example large integer should be {} but is {}'.format(2 ** 127, value) assert value == 2**127, 'Example large integer should be {} but is {}'.format(
2**127, value)
# Test Telegram that types are written right # Test Telegram that types are written right
with BinaryWriter() as writer: with BinaryWriter() as writer:
@ -51,12 +57,14 @@ class UtilsTests(unittest.TestCase):
buffer = writer.get_bytes() buffer = writer.get_bytes()
valid = b'\x78\x97\x46\x60' # Tested written bytes using C#'s MemoryStream valid = b'\x78\x97\x46\x60' # Tested written bytes using C#'s MemoryStream
assert buffer == valid, "Written type should be {} but is {}".format(list(valid), list(buffer)) assert buffer == valid, 'Written type should be {} but is {}'.format(
list(valid), list(buffer))
@staticmethod @staticmethod
def test_binary_tgwriter_tgreader(): def test_binary_tgwriter_tgreader():
small_data = os.urandom(33) small_data = os.urandom(33)
small_data_padded = os.urandom(19) # +1 byte for length = 20 (evenly divisible by 4) small_data_padded = os.urandom(
19) # +1 byte for length = 20 (evenly divisible by 4)
large_data = os.urandom(999) large_data = os.urandom(999)
large_data_padded = os.urandom(1024) large_data_padded = os.urandom(1024)
@ -74,7 +82,9 @@ class UtilsTests(unittest.TestCase):
# And then try reading it without errors (it should be unharmed!) # And then try reading it without errors (it should be unharmed!)
for datum in data: for datum in data:
value = reader.tgread_bytes() value = reader.tgread_bytes()
assert value == datum, 'Example bytes should be {} but is {}'.format(datum, value) assert value == datum, 'Example bytes should be {} but is {}'.format(
datum, value)
value = reader.tgread_string() value = reader.tgread_string()
assert value == string, 'Example string should be {} but is {}'.format(string, value) assert value == string, 'Example string should be {} but is {}'.format(
string, value)

View File

@ -1,6 +1,7 @@
import traceback import traceback
from telethon.interactive_telegram_client import \
InteractiveTelegramClient, print_title from telethon.interactive_telegram_client import (InteractiveTelegramClient,
print_title)
def load_settings(path='api/settings'): def load_settings(path='api/settings'):
@ -34,7 +35,8 @@ if __name__ == '__main__':
client.run() client.run()
except Exception as e: except Exception as e:
print('Unexpected error ({}): {} at\n{}'.format(type(e), e, traceback.format_exc())) print('Unexpected error ({}): {} at\n{}'.format(
type(e), e, traceback.format_exc()))
finally: finally:
print_title('Exit') print_title('Exit')