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
if __name__ == '__main__':
from telethon_tests import CryptoTests, ParserTests, TLTests, UtilsTests, NetworkTests
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
"""
from telethon import TelegramClient
# Always prefer setuptools over distutils
from setuptools import setup, find_packages
# To use a consistent encoding
from codecs import open
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__))
# Get the long description from the README file
@ -24,7 +25,6 @@ setup(
# Versions should comply with PEP440.
version=TelegramClient.__version__,
description="Python3 Telegram's client implementation with full access to its API",
long_description=long_description,
@ -63,15 +63,14 @@ setup(
],
# 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
# simple. Or you can use find_packages().
packages=find_packages(exclude=[
'telethon_generator',
'telethon_tests',
'run_tests.py',
'try_telethon.py']),
'telethon_generator', 'telethon_tests', 'run_tests.py',
'try_telethon.py'
]),
# List run-time dependencies here. These will be installed by pip when
# your project is installed.
@ -84,5 +83,4 @@ setup(
'console_scripts': [
'gen_tl = tl_generator:clean_and_generate',
],
}
)
})

View File

@ -6,8 +6,8 @@ class AES:
@staticmethod
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"""
iv1 = iv[:len(iv)//2]
iv2 = iv[len(iv)//2:]
iv1 = iv[:len(iv) // 2]
iv2 = iv[len(iv) // 2:]
aes = pyaes.AES(key)
@ -17,7 +17,8 @@ class AES:
cipher_text_block = [0] * 16
for block_index in range(blocks_count):
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)
@ -40,8 +41,8 @@ class AES:
padding_count = 16 - len(plain_text) % 16
plain_text += os.urandom(padding_count)
iv1 = iv[:len(iv)//2]
iv2 = iv[len(iv)//2:]
iv1 = iv[:len(iv) // 2]
iv2 = iv[len(iv) // 2:]
aes = pyaes.AES(key)
@ -49,7 +50,8 @@ class AES:
blocks_count = len(plain_text) // 16
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):
plain_text_block[i] ^= iv1[i]

View File

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

View File

@ -1,7 +1,8 @@
from telethon.utils import BinaryWriter
import telethon.helpers as utils
import os
import telethon.helpers as utils
from telethon.utils import BinaryWriter
class RSAServerKey:
def __init__(self, fingerprint, m, e):
@ -18,9 +19,9 @@ class RSAServerKey:
with BinaryWriter() as writer:
# Write SHA
writer.write(utils.sha1(data[offset:offset+length]))
writer.write(utils.sha1(data[offset:offset + length]))
# Write data
writer.write(data[offset:offset+length])
writer.write(data[offset:offset + length])
# Add padding if required
if length < 235:
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,
# the non-used bytes on the left will be 0 and act as padding,
# 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:
_server_keys = {
'216be86c022bb4c3':
RSAServerKey('216be86c022bb4c3', int('C150023E2F70DB7985DED064759CFECF0AF328E69A41DAF4D6F01B538135A6F9'
'216be86c022bb4c3': RSAServerKey('216be86c022bb4c3', int(
'C150023E2F70DB7985DED064759CFECF0AF328E69A41DAF4D6F01B538135A6F9'
'1F8F8B2A0EC9BA9720CE352EFCF6C5680FFC424BD634864902DE0B4BD6D49F4E'
'580230E3AE97D95C8B19442B3C0A10D8F5633FECEDD6926A7F6DAB0DDB7D457F'
'9EA81B8465FCD6FFFEED114011DF91C059CAEDAF97625F6C96ECC74725556934'

View File

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

View File

@ -1,5 +1,5 @@
import os
import hashlib
import os
# region Multiple utilities
@ -15,7 +15,6 @@ def ensure_parent_dir_exists(file_path):
if parent:
os.makedirs(parent, exist_ok=True)
# endregion
# region Cryptographic related utils
@ -26,7 +25,8 @@ def calc_key(shared_key, msg_key, client):
x = 0 if client else 8
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)
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
data = pw.encode('utf-8')
pw_hash = current_salt+data+current_salt
pw_hash = current_salt + data + current_salt
return sha256(pw_hash)
# 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
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
cols, rows = shutil.get_terminal_size()
@ -27,7 +25,8 @@ def bytes_to_string(byte_count):
byte_count /= 1024
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):
@ -58,7 +57,8 @@ class InteractiveTelegramClient(TelegramClient):
# Two-step verification may be enabled
except RPCError as e:
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)
else:
raise e
@ -117,10 +117,14 @@ class InteractiveTelegramClient(TelegramClient):
print('Available commands:')
print(' !q: Quits the current chat.')
print(' !Q: Quits the current chat and exits.')
print(' !h: prints the latest messages (message History) of the chat.')
print(' !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(
' !h: prints the latest messages (message History) of the chat.')
print(
' !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()
@ -136,10 +140,12 @@ class InteractiveTelegramClient(TelegramClient):
# History
elif msg == '!h':
# 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)
# 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
name = sender.first_name if sender else '???'
@ -147,13 +153,15 @@ class InteractiveTelegramClient(TelegramClient):
if msg.media:
self.found_media.add(msg)
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:
content = msg.message
# And print it to the user
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
elif msg.startswith('!up '):
@ -176,18 +184,21 @@ class InteractiveTelegramClient(TelegramClient):
print('Downloading profile picture...')
success = self.download_profile_photo(entity.photo, output)
if success:
print('Profile picture downloaded to {}'.format(output))
print('Profile picture downloaded to {}'.format(
output))
else:
print('"{}" does not seem to have a profile picture.'
.format(get_display_name(entity)))
# Send chat message (if any)
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):
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
self.send_photo_file(input_file, peer)
@ -195,7 +206,8 @@ class InteractiveTelegramClient(TelegramClient):
def send_document(self, path, peer):
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
self.send_document_file(input_file, peer)
@ -212,7 +224,8 @@ class InteractiveTelegramClient(TelegramClient):
# Let the output be the message ID
output = str('usermedia/{}'.format(msg_media_id))
print('Downloading media with name {}...'.format(output))
output = self.download_msg_media(msg.media,
output = self.download_msg_media(
msg.media,
file_path=output,
progress_callback=self.download_progress_callback)
print('Media downloaded to {}!'.format(output))
@ -222,32 +235,35 @@ class InteractiveTelegramClient(TelegramClient):
@staticmethod
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
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
def print_progress(progress_type, downloaded_bytes, total_bytes):
print('{} {} out of {} ({:.2%})'.format(
progress_type,
bytes_to_string(downloaded_bytes),
bytes_to_string(total_bytes),
downloaded_bytes / total_bytes))
print('{} {} out of {} ({:.2%})'.format(progress_type, bytes_to_string(
downloaded_bytes), bytes_to_string(total_bytes), downloaded_bytes /
total_bytes))
@staticmethod
def update_handler(update_object):
if type(update_object) is UpdateShortMessage:
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:
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:
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:
print('[Chat #{}, user #{} sent {}]'.format(update_object.chat_id,
update_object.from_id,
print('[Chat #{}, user #{} sent {}]'.format(
update_object.chat_id, update_object.from_id,
update_object.message))

View File

@ -1,9 +1,10 @@
import os
import time
import telethon.helpers as utils
from telethon.utils import BinaryWriter, BinaryReader
from telethon.crypto import AES, AuthKey, Factorizator, RSA
from telethon.crypto import AES, RSA, AuthKey, Factorizator
from telethon.network import MtProtoPlainSender
from telethon.utils import BinaryReader, BinaryWriter
def do_authentication(transport):
@ -23,7 +24,8 @@ def do_authentication(transport):
with BinaryReader(sender.receive()) as reader:
response_code = reader.read_int(signed=False)
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)
if nonce_from_server != nonce:
@ -36,7 +38,8 @@ def do_authentication(transport):
vector_id = reader.read_int()
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 = []
fingerprint_count = reader.read_int()
@ -47,32 +50,46 @@ def do_authentication(transport):
new_nonce = os.urandom(32)
p, q = Factorizator.factorize(pq)
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(min(p, q), signed=False))
pq_inner_data_writer.tgwrite_bytes(get_byte_array(max(p, q), signed=False))
pq_inner_data_writer.tgwrite_bytes(
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(server_nonce)
pq_inner_data_writer.write(new_nonce)
cipher_text, target_fingerprint = None, None
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:
target_fingerprint = fingerprint
break
if cipher_text is None:
raise AssertionError('Could not find a valid key for fingerprints: {}'
.format(', '.join([get_fingerprint_text(f) for f in fingerprints])))
raise AssertionError(
'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:
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(server_nonce)
req_dh_params_writer.tgwrite_bytes(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.tgwrite_bytes(
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.tgwrite_bytes(cipher_text)
@ -88,7 +105,8 @@ def do_authentication(transport):
raise AssertionError('Server DH params fail: TODO')
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)
if nonce_from_server != nonce:
@ -106,7 +124,6 @@ def do_authentication(transport):
g, dh_prime, ga, time_offset = None, None, None, None
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)
if code != 0xb5890dba:
raise AssertionError('Invalid DH Inner Data code: {}'.format(code))
@ -132,26 +149,34 @@ def do_authentication(transport):
# Prepare client DH Inner Data
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(server_nonce)
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:
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_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(
utils.sha1(client_dh_inner_data_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
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
with BinaryWriter() as set_client_dh_params_writer:
set_client_dh_params_writer.write_int(0xf5045f1f, signed=False)
set_client_dh_params_writer.write(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()
sender.send(set_client_dh_params_bytes)
@ -171,7 +196,8 @@ def do_authentication(transport):
new_nonce_hash1 = reader.read(16)
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:
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"""
bits = integer.bit_length()
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):

View File

@ -1,10 +1,12 @@
import time
import random
from telethon.utils import BinaryWriter, BinaryReader
import time
from telethon.utils import BinaryReader, BinaryWriter
class MtProtoPlainSender:
"""MTProto Mobile Protocol plain sender (https://core.telegram.org/mtproto/description#unencrypted-messages)"""
def __init__(self, transport):
self._sequence = 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"""
# See https://core.telegram.org/mtproto/description#message-identifier-msg-id
ms_time = int(time.time() * 1000)
new_msg_id = (((ms_time // 1000) << 32) | # "must approximately equal unixtime*2^32"
((ms_time % 1000) << 22) | # "approximate moment in time the message was created"
random.randint(0, 524288) << 2) # "message identifiers are divisible by 4"
new_msg_id = (((ms_time // 1000) << 32)
| # "must approximately equal unixtime*2^32"
((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
if self._last_msg_id >= new_msg_id:

View File

@ -1,18 +1,19 @@
import gzip
from telethon.errors import *
from time import sleep
from datetime import timedelta
from threading import Thread, RLock
from threading import RLock, Thread
from time import sleep
import telethon.helpers as utils
from telethon.crypto import AES
from telethon.utils import BinaryWriter, BinaryReader
from telethon.tl.types import MsgsAck
from telethon.errors import *
from telethon.tl.all_tlobjects import tlobjects
from telethon.tl.types import MsgsAck
from telethon.utils import BinaryReader, BinaryWriter
class MtProtoSender:
"""MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)"""
def __init__(self, transport, session):
self.transport = transport
self.session = session
@ -27,7 +28,8 @@ class MtProtoSender:
# We need this to avoid using the updates thread if we're waiting to read
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_receiving = False
@ -118,7 +120,8 @@ class MtProtoSender:
message, remote_msg_id, remote_sequence = self.decode_msg(body)
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
self.waiting_receive = False
@ -148,7 +151,8 @@ class MtProtoSender:
# And then finally send the encrypted packet
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(cipher_text)
self.transport.send(cipher_writer.get_bytes())
@ -168,7 +172,8 @@ class MtProtoSender:
msg_key = reader.read(16)
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:
remote_salt = plain_text_reader.read_long()
@ -198,7 +203,8 @@ class MtProtoSender:
if code == 0x3072cfa1: # gzip_packed
return self.handle_gzip_packed(msg_id, sequence, reader, request)
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
return self.handle_bad_msg_notification(msg_id, sequence, reader)
@ -253,7 +259,8 @@ class MtProtoSender:
self.session.salt = new_salt
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
self.send(request)
@ -277,15 +284,18 @@ class MtProtoSender:
request.confirm_received = True
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 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).')
request.confirm_received = False
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)
elif error.message.startswith('PHONE_MIGRATE_'):
@ -295,7 +305,8 @@ class MtProtoSender:
raise error
else:
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
unpacked_data = gzip.decompress(reader.tgread_bytes())
@ -311,7 +322,8 @@ class MtProtoSender:
unpacked_data = gzip.decompress(packed_data)
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
@ -340,10 +352,12 @@ class MtProtoSender:
try:
self.updates_thread_receiving = True
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:
self.process_msg(remote_msg_id, remote_sequence, reader)
self.process_msg(remote_msg_id, remote_sequence,
reader)
except (ReadCancelledError, TimeoutError):
pass

View File

@ -78,7 +78,8 @@ class TcpClient:
if timeout:
time_passed = datetime.now() - start_time
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
return writer.get_bytes()

View File

@ -1,8 +1,8 @@
from binascii import crc32
from datetime import timedelta
from telethon.network import TcpClient
from telethon.errors import *
from telethon.network import TcpClient
from telethon.utils import BinaryWriter
@ -45,9 +45,8 @@ class TcpTransport:
body = self.tcp_client.read(packet_length - 12, timeout)
checksum = int.from_bytes(self.tcp_client.read(4, timeout),
byteorder='little',
signed=False)
checksum = int.from_bytes(
self.tcp_client.read(4, timeout), byteorder='little', signed=False)
# Then perform the checks
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):
@ -44,13 +45,13 @@ def parse_message_entities(msg):
# 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
link_text = ''.join(msg[vui[0]+1:vui[1]])
link_url = ''.join(msg[vui[2]+1:vui[3]])
link_text = ''.join(msg[vui[0] + 1:vui[1]])
link_url = ''.join(msg[vui[2] + 1:vui[3]])
# 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!
del msg[vui[2]:vui[3]+1]
msg[vui[0]:vui[1]+1] = link_text
del msg[vui[2]:vui[3] + 1]
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
# 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)
# 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
indicator_flags = {
'*': None,
'_': None,
'`': None
}
indicator_flags = {'*': None, '_': None, '`': None}
# Iterate over the list to find the indicators of entities
for i, c in enumerate(msg):
@ -88,13 +87,19 @@ def parse_message_entities(msg):
# Add the corresponding entity
if c == '*':
entities.append(MessageEntityBold(offset=offset, length=length))
entities.append(
MessageEntityBold(
offset=offset, length=length))
elif c == '_':
entities.append(MessageEntityItalic(offset=offset, length=length))
entities.append(
MessageEntityItalic(
offset=offset, length=length))
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
indicator_flags[c] = None
@ -116,15 +121,16 @@ def parse_message_entities(msg):
# In this case, the current entity length is decreased by two,
# and all the subentities offset decreases 1
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
subentity.offset -= 1
# Second case, both inside: so*me_th*in_g.
# In this case, the current entity length is decreased by one,
# and all the subentities offset and length decrease 1
elif (entity.offset < subentity.offset < entity.offset + entity.length and
subentity.offset + subentity.length > entity.offset + entity.length):
elif (entity.offset < subentity.offset < entity.offset +
entity.length < subentity.offset + subentity.length):
entity.length -= 1
subentity.offset -= 1
subentity.length -= 1

View File

@ -1,47 +1,40 @@
import platform
from datetime import datetime, timedelta
from hashlib import md5
from os import path, listdir
from mimetypes import guess_type
# 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
from os import listdir, path
# Import some externalized utilities to work with the Telegram types and more
import telethon.helpers as utils
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.network import MtProtoSender, TcpTransport
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.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:
@ -62,7 +55,8 @@ class TelegramClient:
.save() and .load() implementations to suit your needs."""
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_hash = api_hash
@ -73,9 +67,11 @@ class TelegramClient:
elif isinstance(session, Session):
self.session = session
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
self.dc_options = None
@ -104,14 +100,17 @@ class TelegramClient:
# Now it's time to send an InitConnectionRequest
# 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(),
system_version=platform.system(),
app_version=self.__version__,
lang_code='en',
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,
# although many other options are available!
@ -130,7 +129,8 @@ class TelegramClient:
def reconnect_to_dc(self, dc_id):
"""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:
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)
@ -191,11 +191,13 @@ class TelegramClient:
with `.password_required = True` was raised"""
if phone_number and code:
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:
result = self.invoke(SignInRequest(
phone_number, self.phone_code_hashes[phone_number], code))
result = self.invoke(
SignInRequest(phone_number, self.phone_code_hashes[
phone_number], code))
except RPCError as error:
if error.message.startswith('PHONE_CODE_'):
@ -205,9 +207,11 @@ class TelegramClient:
raise error
elif password:
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:
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.')
# Result is an Auth.Authorization TLObject
@ -221,7 +225,9 @@ class TelegramClient:
def sign_up(self, phone_number, code, first_name, last_name=''):
"""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=code,
first_name=first_name,
@ -245,29 +251,41 @@ class TelegramClient:
def list_sessions():
"""Lists all the sessions of the users who have ever connected
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')]
# endregion
# 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.
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_peer=offset_peer,
limit=count))
return (r.dialogs,
return (
r.dialogs,
[find_user_or_chat(d.peer, r.users, r.chats) for d in r.dialogs])
# endregion
# 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"""
if markdown:
msg, entities = parse_message_entities(message)
@ -275,15 +293,23 @@ class TelegramClient:
msg, entities = message, []
msg_id = utils.generate_random_long()
self.invoke(SendMessageRequest(peer=input_peer,
self.invoke(
SendMessageRequest(
peer=input_peer,
message=msg,
random_id=msg_id,
entities=entities,
no_webpage=no_web_page))
return msg_id
def get_message_history(self, input_peer, limit=20,
offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0):
def get_message_history(self,
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
@ -298,7 +324,9 @@ class TelegramClient:
: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!
"""
result = self.invoke(GetHistoryRequest(input_peer,
result = self.invoke(
GetHistoryRequest(
input_peer,
limit=limit,
offset_date=offset_date,
offset_id=offset_id,
@ -331,7 +359,8 @@ class TelegramClient:
Returns an AffectedMessages TLObject"""
if max_id is None:
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):
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"
# 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
: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
# 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()
with open(file_path, 'rb') as file:
@ -386,7 +419,8 @@ class TelegramClient:
# The SavePartRequest is different depending on whether
# the file is too large or not (over or less than 10MB)
if is_large:
request = SaveBigFilePartRequest(file_id, part_index, part_count, part)
request = SaveBigFilePartRequest(file_id, part_index,
part_count, part)
else:
request = SaveFilePartRequest(file_id, part_index, part)
@ -397,14 +431,16 @@ class TelegramClient:
if progress_callback:
progress_callback(file.tell(), file_size)
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
if not file_name:
file_name = path.basename(file_path)
# 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,
name=file_name,
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.»
if not mime_type:
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,
attributes=attributes,
caption=caption), input_peer)
caption=caption),
input_peer)
def send_media_file(self, input_media, 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,
random_id=utils.generate_random_long()))
@ -446,8 +487,11 @@ class TelegramClient:
# region Downloading media requests
def download_profile_photo(self, profile_photo, file_path,
add_extension=True, download_big=True):
def download_profile_photo(self,
profile_photo,
file_path,
add_extension=True,
download_big=True):
"""Downloads the profile photo for an user or a chat (including channels).
Returns False if no photo was providen, or if it was Empty"""
@ -465,28 +509,40 @@ class TelegramClient:
photo_location = profile_photo.photo_small
# 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,
secret=photo_location.secret),
file_path)
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)
into the desired file_path, optionally finding its extension automatically
The progress_callback should be a callback function which takes two parameters,
uploaded size (in bytes) and total file size (in bytes).
This will be called every time a part is downloaded"""
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:
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:
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):
"""Downloads MessageMediaPhoto's largest size into the desired
file_path, optionally finding its extension automatically
@ -504,13 +560,20 @@ class TelegramClient:
file_path += get_extension(message_media_photo)
# 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,
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
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):
"""Downloads the given MessageMediaDocument into the desired
file_path, optionally finding its extension automatically.
@ -537,10 +600,14 @@ class TelegramClient:
if add_extension:
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,
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
@staticmethod
@ -562,15 +629,21 @@ class TelegramClient:
with open(file_path, 'w', encoding='utf-8') as file:
file.write('BEGIN:VCARD\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('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')
return file_path
def download_file_loc(self, input_location, file_path, part_size_kb=64,
file_size=None, progress_callback=None):
def download_file_loc(self,
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.
If a progress_callback function is given, it will be called taking two
arguments (downloaded bytes count and total file size)"""
@ -594,7 +667,8 @@ class TelegramClient:
while True:
# The current offset equals the offset_index multiplied by the 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
# 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,
an update, as its parameter) and listens for updates"""
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)

View File

@ -26,7 +26,8 @@ class MTProtoRequest:
self.confirm_received = True
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))
# These should be overrode

View File

@ -1,8 +1,9 @@
from os.path import isfile as file_exists
import os
import time
import pickle
import random
import time
from os.path import isfile as file_exists
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 session_user_id is None:
return Session(None)
else:
filepath = '{}.session'.format(session_user_id)
path = '{}.session'.format(session_user_id)
if file_exists(filepath):
with open(filepath, 'rb') as file:
if file_exists(path):
with open(path, 'rb') as file:
return pickle.load(file)
else:
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"""
# Refer to mtproto_plain_sender.py for the original method, this is a simple copy
ms_time = int(time.time() * 1000)
new_msg_id = (((ms_time // 1000 + self.time_offset) << 32) | # "must approximately equal unixtime*2^32"
((ms_time % 1000) << 22) | # "approximate moment in time the message was created"
random.randint(0, 524288) << 2) # "message identifiers are divisible by 4"
new_msg_id = (((ms_time // 1000 + self.time_offset) << 32)
| # "must approximately equal unixtime*2^32"
((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:
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
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:
@ -12,13 +12,15 @@ class BinaryReader:
Small utility class to read binary data.
Also creates a "Memory Stream" if necessary
"""
def __init__(self, data=None, stream=None):
if data:
self.stream = BytesIO(data)
elif stream:
self.stream = stream
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)
@ -47,13 +49,15 @@ class BinaryReader:
def read_large_int(self, bits, signed=True):
"""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):
"""Read the given amount of bytes"""
result = self.reader.read(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
@ -69,7 +73,8 @@ class BinaryReader:
"""Reads a Telegram-encoded byte array, without the need of specifying its length"""
first_byte = self.read_byte()
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
else:
length = first_byte

View File

@ -1,4 +1,4 @@
from io import BytesIO, BufferedWriter
from io import BufferedWriter, BytesIO
from struct import pack
@ -26,12 +26,16 @@ class BinaryWriter:
def write_int(self, value, signed=True):
"""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
def write_long(self, value, signed=True):
"""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
def write_float(self, value):
@ -46,7 +50,9 @@ class BinaryWriter:
def write_large_int(self, value, bits, signed=True):
"""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
def write(self, data):

View File

@ -4,12 +4,10 @@
after all, both are the same attribute, IDs."""
from mimetypes import add_type, guess_extension
from telethon.tl.types import \
User, Chat, Channel, \
PeerUser, PeerChat, PeerChannel, \
InputPeerUser, InputPeerChat, InputPeerChannel, \
UserProfilePhoto, ChatPhoto, \
MessageMediaPhoto, MessageMediaDocument
from telethon.tl.types import (
Channel, Chat, ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser,
MessageMediaDocument, MessageMediaPhoto, PeerChannel, PeerChat, PeerUser,
User, UserProfilePhoto)
def get_display_name(entity):
@ -31,8 +29,7 @@ def get_extension(media):
"""Gets the corresponding extension for any Telegram media"""
# Photos are always compressed as .jpg by Telegram
if (isinstance(media, UserProfilePhoto) or
isinstance(media, ChatPhoto) or
if (isinstance(media, UserProfilePhoto) or isinstance(media, ChatPhoto) or
isinstance(media, MessageMediaPhoto)):
return '.jpg'

View File

@ -18,7 +18,8 @@ class SourceBuilder:
"""Writes a string into the source code, applying indentation if required"""
if self.on_new_line:
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.out_stream.write(string)

View File

@ -5,13 +5,13 @@ class TLObject:
""".tl core types IDs (such as vector, booleans, etc.)"""
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.
Usually, this will be called from `from_tl` instead
:param fullname: The fullname of the TL object (namespace.name)
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 result: The result type of the TL object
:param is_function: Is the object a function or a type?
@ -25,7 +25,7 @@ class TLObject:
self.name = fullname
# The ID should be an hexadecimal string
self.id = int(id, base=16)
self.id = int(object_id, base=16)
self.args = args
self.result = result
self.is_function = is_function
@ -67,11 +67,13 @@ class TLObject:
''', tl, re.IGNORECASE | re.VERBOSE)
# 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
return TLObject(fullname=match.group(1),
id=match.group(2),
return TLObject(
fullname=match.group(1),
object_id=match.group(2),
args=args,
result=match.group(3),
is_function=is_function)
@ -82,19 +84,19 @@ class TLObject:
return self.id in TLObject.CORE_TYPES
def __repr__(self):
fullname = ('{}.{}'.format(self.namespace, self.name) if self.namespace is not None
else self.name)
fullname = ('{}.{}'.format(self.namespace, 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,
hex_id,
' '.join([str(arg) for arg in self.args]),
return '{}#{} {} = {}'.format(
fullname, hex_id, ' '.join([str(arg) for arg in self.args]),
self.result)
def __str__(self):
fullname = ('{}.{}'.format(self.namespace, self.name) if self.namespace is not None
else self.name)
fullname = ('{}.{}'.format(self.namespace, 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
# (these have no explicit values until used)
@ -104,20 +106,21 @@ class TLObject:
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
args_format = ', '.join(['str(self.{})'.format(arg.name) if not arg.is_vector else
'None if not self.{0} else [str(_) for _ in self.{0}]'.format(arg.name)
for arg in valid_args])
args_format = ', '.join(
['str(self.{})'.format(arg.name) if not arg.is_vector else
'None if not self.{0} else [str(_) for _ in self.{0}]'.format(
arg.name) for arg in valid_args])
return ("'({} (ID: {}) = ({}))'.format({})"
.format(fullname, hex(self.id), args, args_format))
class TLArg:
def __init__(self, name, type, generic_definition):
def __init__(self, name, arg_type, generic_definition):
"""
Initializes a new .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?
(i.e. {X:Type})
"""
@ -132,14 +135,15 @@ class TLArg:
self.flag_index = -1
# The type can be an indicator that other arguments will be flags
if type == '#':
if arg_type == '#':
self.flag_indicator = True
self.type = None
self.is_generic = False
else:
self.flag_indicator = False
self.is_generic = type.startswith('!')
self.type = type.lstrip('!') # Strip the exclamation mark always to have only the name
self.is_generic = arg_type.startswith('!')
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)
# Note that «flags» is NOT the flags name; this is determined by a previous argument
@ -148,13 +152,15 @@ class TLArg:
if flag_match:
self.is_flag = True
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>
vector_match = re.match(r'vector<(\w+)>', self.type, re.IGNORECASE)
if vector_match:
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",
# we can safely assume that this should be treated as a "date" object.

View File

@ -2,7 +2,7 @@ import os
import re
import shutil
from parser import SourceBuilder, TLParser
from .parser import SourceBuilder, TLParser
def get_output_path(normal_path):
@ -60,8 +60,8 @@ class TLGenerator:
continue
# Determine the output directory and create it
out_dir = get_output_path('functions' if tlobject.is_function
else 'types')
out_dir = get_output_path('functions'
if tlobject.is_function else 'types')
if tlobject.namespace:
out_dir = os.path.join(out_dir, tlobject.namespace)
@ -77,39 +77,51 @@ class TLGenerator:
TLGenerator.get_class_name(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:
# Let's build the source code!
with SourceBuilder(file) as builder:
# 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('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
builder.writeln('"""Class generated by TLObjects\' generator. '
builder.writeln(
'"""Class generated by TLObjects\' generator. '
'All changes will be ERASED. Original .tl definition below.')
builder.writeln('{}"""'.format(repr(tlobject)))
builder.writeln()
# 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('constructor_id = {}'.format(hex(tlobject.id)))
builder.writeln(
"# Telegram's constructor ID (and unique identifier) for this class")
builder.writeln('constructor_id = {}'.format(
hex(tlobject.id)))
builder.writeln()
# 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)
# Then convert the args to string parameters, the flags having =None
args = [(arg.name if not arg.is_flag
else '{}=None'.format(arg.name)) for arg in args
if not arg.flag_indicator and not arg.generic_definition]
args = [(arg.name if not arg.is_flag else
'{}=None'.format(arg.name)) for arg in args
if not arg.flag_indicator and
not arg.generic_definition]
# Write the __init__ function
if args:
builder.writeln('def __init__(self, {}):'.format(', '.join(args)))
builder.writeln('def __init__(self, {}):'.format(
', '.join(args)))
else:
builder.writeln('def __init__(self):')
@ -117,18 +129,23 @@ class TLGenerator:
# 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
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:
# Write the docstring, so we know the type of the arguments
builder.writeln('"""')
for arg in args:
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:
builder.write(' Must be a list.'.format(arg.name))
builder.write(' Must be a list.'.format(
arg.name))
if arg.is_generic:
builder.write(' This should be another MTProtoRequest.')
builder.write(
' This should be another MTProtoRequest.')
builder.writeln()
builder.writeln('"""')
@ -136,7 +153,8 @@ class TLGenerator:
# Functions have a result object and are confirmed by default
if tlobject.is_function:
builder.writeln('self.result = None')
builder.writeln('self.confirmed = True # Confirmed by default')
builder.writeln(
'self.confirmed = True # Confirmed by default')
# Set the arguments
if args:
@ -148,22 +166,24 @@ class TLGenerator:
# Write the on_send(self, writer) function
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)))
for arg in tlobject.args:
TLGenerator.write_onsend_code(builder, arg, tlobject.args)
TLGenerator.write_onsend_code(builder, arg,
tlobject.args)
builder.end_block()
# Write the empty() function, which returns an "empty"
# instance, in which all attributes are set to None
builder.writeln('@staticmethod')
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(
TLGenerator.get_class_name(tlobject),
', '.join('None' for _ in range(len(args)))
))
TLGenerator.get_class_name(tlobject), ', '.join(
'None' for _ in range(len(args)))))
builder.end_block()
# Write the on_response(self, reader) function
@ -174,7 +194,8 @@ class TLGenerator:
else:
if tlobject.args:
for arg in tlobject.args:
TLGenerator.write_onresponse_code(builder, arg, tlobject.args)
TLGenerator.write_onresponse_code(
builder, arg, tlobject.args)
else:
# If there were no arguments, we still need an on_response method, and hence "pass" if empty
builder.writeln('pass')
@ -186,23 +207,26 @@ class TLGenerator:
builder.end_block()
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
# 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'))
with open(filename, 'w', encoding='utf-8') as file:
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()
# First add imports
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()
# 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()
# Then create the dictionary containing constructor_id: class
@ -211,10 +235,9 @@ class TLGenerator:
# Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class)
for tlobject in tlobjects:
builder.writeln('{}: {}.{},'
.format(hex(tlobject.id),
TLGenerator.get_full_file_name(tlobject),
TLGenerator.get_class_name(tlobject)))
builder.writeln('{}: {}.{},'.format(
hex(tlobject.id), TLGenerator.get_full_file_name(
tlobject), TLGenerator.get_class_name(tlobject)))
builder.current_indent -= 1
builder.writeln('}')
@ -225,8 +248,10 @@ class TLGenerator:
# Courtesy of http://stackoverflow.com/a/31531797/4759433
# 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 = result[:1].upper() + result[1:].replace('_', '') # Replace again to fully ensure!
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(),
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 tlobject.is_function:
result += 'Request'
@ -283,22 +308,25 @@ class TLGenerator:
builder.writeln('if {}:'.format(name))
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('for {}_item in {}:'.format(arg.name, name))
# Temporary disable .is_vector, not to enter this if again
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
elif arg.flag_indicator:
# 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')
for flag in args:
if flag.is_flag:
builder.writeln('flags |= (1 << {}) if {} else 0'
.format(flag.flag_index, 'self.{}'.format(flag.name)))
builder.writeln('flags |= (1 << {}) if {} else 0'.format(
flag.flag_index, 'self.{}'.format(flag.name)))
builder.writeln('writer.write_int(flags)')
builder.writeln()
@ -310,10 +338,12 @@ class TLGenerator:
builder.writeln('writer.write_long({})'.format(name))
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:
builder.writeln('writer.write_large_int({}, bits=256)'.format(name))
builder.writeln('writer.write_large_int({}, bits=256)'.format(
name))
elif 'double' == arg.type:
builder.writeln('writer.write_double({})'.format(name))
@ -366,7 +396,8 @@ class TLGenerator:
was_flag = False
if arg.is_flag:
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
arg.is_flag = False
@ -377,7 +408,8 @@ class TLGenerator:
builder.writeln('for _ in range({}_len):'.format(arg.name))
# Temporary disable .is_vector, not to enter this if again
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))
arg.is_vector = True
@ -393,10 +425,12 @@ class TLGenerator:
builder.writeln('{} = reader.read_long()'.format(name))
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:
builder.writeln('{} = reader.read_large_int(bits=256)'.format(name))
builder.writeln('{} = reader.read_large_int(bits=256)'.format(
name))
elif 'double' == arg.type:
builder.writeln('{} = reader.read_double()'.format(name))
@ -408,7 +442,9 @@ class TLGenerator:
builder.writeln('{} = reader.tgread_bool()'.format(name))
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:
builder.writeln('{} = reader.tgread_bytes()'.format(name))
@ -429,6 +465,7 @@ class TLGenerator:
# Restore .is_flag
arg.is_flag = True
if __name__ == '__main__':
if TLGenerator.tlobjects_exist():
print('Detected previous TLObjects. Cleaning...')

View File

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

View File

@ -1,8 +1,7 @@
import unittest
from telethon.crypto import AES
import telethon.helpers as utils
from telethon.crypto import Factorizator
from telethon.crypto import AES, Factorizator
class CryptoTests(unittest.TestCase):
@ -38,13 +37,15 @@ class CryptoTests(unittest.TestCase):
.format(value[:take], self.cipher_text[:take]))
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))
def test_aes_decrypt(self):
# The ciphered text must always be padded
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))
@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_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 iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(expected_iv, iv)
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(
expected_key, key)
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(
expected_iv, iv)
# Calculate key being the server
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_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 iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(expected_iv, iv)
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(
expected_key, key)
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(
expected_iv, iv)
@staticmethod
def test_calc_msg_key():
value = utils.calc_msg_key(b'Some random message')
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
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_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 iv == expected_iv, 'Key ("{}") does not equal expected ("{}")'.format(key, expected_iv)
assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format(
key, expected_key)
assert iv == expected_iv, 'Key ("{}") does not equal expected ("{}")'.format(
key, expected_iv)
@staticmethod
def test_factorizator():

View File

@ -3,8 +3,8 @@ import socket
import threading
import unittest
from telethon.network import TcpTransport, TcpClient
import telethon.network.authenticator as authenticator
from telethon.network import TcpClient, TcpTransport
def run_server_echo_thread(port):
@ -31,7 +31,8 @@ class NetworkTests(unittest.TestCase):
client = TcpClient()
client.connect('localhost', port)
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()
@staticmethod

View File

@ -14,7 +14,7 @@ class UtilsTests(unittest.TestCase):
writer.write_float(17.0)
writer.write_double(25.0)
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()
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:
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()
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()
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()
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()
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)
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)
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
with BinaryWriter() as writer:
@ -51,12 +57,14 @@ class UtilsTests(unittest.TestCase):
buffer = writer.get_bytes()
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
def test_binary_tgwriter_tgreader():
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_padded = os.urandom(1024)
@ -74,7 +82,9 @@ class UtilsTests(unittest.TestCase):
# And then try reading it without errors (it should be unharmed!)
for datum in data:
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()
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
from telethon.interactive_telegram_client import \
InteractiveTelegramClient, print_title
from telethon.interactive_telegram_client import (InteractiveTelegramClient,
print_title)
def load_settings(path='api/settings'):
@ -34,7 +35,8 @@ if __name__ == '__main__':
client.run()
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:
print_title('Exit')