mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2024-11-26 03:13:45 +03:00
Merge branch 'master' of https://github.com/fdhadzh/Telethon into fdhadzh-master
This commit is contained in:
commit
4862ef1dce
22
.pre-commit-config.yaml
Normal file
22
.pre-commit-config.yaml
Normal 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
|
|
@ -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]
|
||||||
|
|
22
setup.py
22
setup.py
|
@ -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',
|
||||||
],
|
],
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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...')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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():
|
|
@ -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
|
|
@ -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)
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in New Issue
Block a user