mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2024-11-10 19:46:36 +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
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from telethon_tests import CryptoTests, ParserTests, TLTests, UtilsTests, NetworkTests
|
||||
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
|
||||
"""
|
||||
|
||||
from telethon import TelegramClient
|
||||
|
||||
# Always prefer setuptools over distutils
|
||||
from setuptools import setup, find_packages
|
||||
# To use a consistent encoding
|
||||
from codecs import open
|
||||
from os import path
|
||||
|
||||
# Always prefer setuptools over distutils
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
from telethon import TelegramClient
|
||||
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
|
||||
# Get the long description from the README file
|
||||
|
@ -24,7 +25,6 @@ setup(
|
|||
|
||||
# Versions should comply with PEP440.
|
||||
version=TelegramClient.__version__,
|
||||
|
||||
description="Python3 Telegram's client implementation with full access to its API",
|
||||
long_description=long_description,
|
||||
|
||||
|
@ -63,15 +63,14 @@ setup(
|
|||
],
|
||||
|
||||
# What does your project relate to?
|
||||
keywords='telegram api chat client mtproto',
|
||||
keywords='Telegram API chat client MTProto',
|
||||
|
||||
# You can just specify the packages manually here if your project is
|
||||
# simple. Or you can use find_packages().
|
||||
packages=find_packages(exclude=[
|
||||
'telethon_generator',
|
||||
'telethon_tests',
|
||||
'run_tests.py',
|
||||
'try_telethon.py']),
|
||||
'telethon_generator', 'telethon_tests', 'run_tests.py',
|
||||
'try_telethon.py'
|
||||
]),
|
||||
|
||||
# List run-time dependencies here. These will be installed by pip when
|
||||
# your project is installed.
|
||||
|
@ -84,5 +83,4 @@ setup(
|
|||
'console_scripts': [
|
||||
'gen_tl = tl_generator:clean_and_generate',
|
||||
],
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
|
@ -6,8 +6,8 @@ class AES:
|
|||
@staticmethod
|
||||
def decrypt_ige(cipher_text, key, iv):
|
||||
"""Decrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector"""
|
||||
iv1 = iv[:len(iv)//2]
|
||||
iv2 = iv[len(iv)//2:]
|
||||
iv1 = iv[:len(iv) // 2]
|
||||
iv2 = iv[len(iv) // 2:]
|
||||
|
||||
aes = pyaes.AES(key)
|
||||
|
||||
|
@ -17,7 +17,8 @@ class AES:
|
|||
cipher_text_block = [0] * 16
|
||||
for block_index in range(blocks_count):
|
||||
for i in range(16):
|
||||
cipher_text_block[i] = cipher_text[block_index * 16 + i] ^ iv2[i]
|
||||
cipher_text_block[i] = cipher_text[block_index * 16 + i] ^ iv2[
|
||||
i]
|
||||
|
||||
plain_text_block = aes.decrypt(cipher_text_block)
|
||||
|
||||
|
@ -40,8 +41,8 @@ class AES:
|
|||
padding_count = 16 - len(plain_text) % 16
|
||||
plain_text += os.urandom(padding_count)
|
||||
|
||||
iv1 = iv[:len(iv)//2]
|
||||
iv2 = iv[len(iv)//2:]
|
||||
iv1 = iv[:len(iv) // 2]
|
||||
iv2 = iv[len(iv) // 2:]
|
||||
|
||||
aes = pyaes.AES(key)
|
||||
|
||||
|
@ -49,7 +50,8 @@ class AES:
|
|||
blocks_count = len(plain_text) // 16
|
||||
|
||||
for block_index in range(blocks_count):
|
||||
plain_text_block = list(plain_text[block_index * 16:block_index * 16 + 16])
|
||||
plain_text_block = list(plain_text[block_index * 16:block_index *
|
||||
16 + 16])
|
||||
for i in range(16):
|
||||
plain_text_block[i] ^= iv1[i]
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from telethon.utils import BinaryWriter, BinaryReader
|
||||
import telethon.helpers as utils
|
||||
from telethon.utils import BinaryReader, BinaryWriter
|
||||
|
||||
|
||||
class AuthKey:
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from telethon.utils import BinaryWriter
|
||||
import telethon.helpers as utils
|
||||
import os
|
||||
|
||||
import telethon.helpers as utils
|
||||
from telethon.utils import BinaryWriter
|
||||
|
||||
|
||||
class RSAServerKey:
|
||||
def __init__(self, fingerprint, m, e):
|
||||
|
@ -18,9 +19,9 @@ class RSAServerKey:
|
|||
|
||||
with BinaryWriter() as writer:
|
||||
# Write SHA
|
||||
writer.write(utils.sha1(data[offset:offset+length]))
|
||||
writer.write(utils.sha1(data[offset:offset + length]))
|
||||
# Write data
|
||||
writer.write(data[offset:offset+length])
|
||||
writer.write(data[offset:offset + length])
|
||||
# Add padding if required
|
||||
if length < 235:
|
||||
writer.write(os.urandom(235 - length))
|
||||
|
@ -31,13 +32,14 @@ class RSAServerKey:
|
|||
# If the result byte count is less than 256, since the byte order is big,
|
||||
# the non-used bytes on the left will be 0 and act as padding,
|
||||
# without need of any additional checks
|
||||
return int.to_bytes(result, length=256, byteorder='big', signed=False)
|
||||
return int.to_bytes(
|
||||
result, length=256, byteorder='big', signed=False)
|
||||
|
||||
|
||||
class RSA:
|
||||
_server_keys = {
|
||||
'216be86c022bb4c3':
|
||||
RSAServerKey('216be86c022bb4c3', int('C150023E2F70DB7985DED064759CFECF0AF328E69A41DAF4D6F01B538135A6F9'
|
||||
'216be86c022bb4c3': RSAServerKey('216be86c022bb4c3', int(
|
||||
'C150023E2F70DB7985DED064759CFECF0AF328E69A41DAF4D6F01B538135A6F9'
|
||||
'1F8F8B2A0EC9BA9720CE352EFCF6C5680FFC424BD634864902DE0B4BD6D49F4E'
|
||||
'580230E3AE97D95C8B19442B3C0A10D8F5633FECEDD6926A7F6DAB0DDB7D457F'
|
||||
'9EA81B8465FCD6FFFEED114011DF91C059CAEDAF97625F6C96ECC74725556934'
|
||||
|
|
|
@ -3,6 +3,7 @@ import re
|
|||
|
||||
class ReadCancelledError(Exception):
|
||||
"""Occurs when a read operation was cancelled"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(self, 'The read operation was cancelled.')
|
||||
|
||||
|
@ -15,8 +16,10 @@ class InvalidParameterError(Exception):
|
|||
class TypeNotFoundError(Exception):
|
||||
"""Occurs when a type is not found, for example,
|
||||
when trying to read a TLObject with an invalid constructor code"""
|
||||
|
||||
def __init__(self, invalid_constructor_id):
|
||||
super().__init__(self, 'Could not find a matching Constructor ID for the TLObject '
|
||||
super().__init__(
|
||||
self, 'Could not find a matching Constructor ID for the TLObject '
|
||||
'that was supposed to be read with ID {}. Most likely, a TLObject '
|
||||
'was trying to be read when it should not be read.'
|
||||
.format(hex(invalid_constructor_id)))
|
||||
|
@ -26,7 +29,8 @@ class TypeNotFoundError(Exception):
|
|||
|
||||
class InvalidDCError(Exception):
|
||||
def __init__(self, new_dc):
|
||||
super().__init__(self, 'Your phone number is registered to #{} DC. '
|
||||
super().__init__(
|
||||
self, 'Your phone number is registered to #{} DC. '
|
||||
'This should have been handled automatically; '
|
||||
'if it has not, please restart the app.'.format(new_dc))
|
||||
|
||||
|
@ -35,7 +39,9 @@ class InvalidDCError(Exception):
|
|||
|
||||
class InvalidChecksumError(Exception):
|
||||
def __init__(self, checksum, valid_checksum):
|
||||
super().__init__(self, 'Invalid checksum ({} when {} was expected). This packet should be skipped.'
|
||||
super().__init__(
|
||||
self,
|
||||
'Invalid checksum ({} when {} was expected). This packet should be skipped.'
|
||||
.format(checksum, valid_checksum))
|
||||
|
||||
self.checksum = checksum
|
||||
|
@ -45,105 +51,95 @@ class InvalidChecksumError(Exception):
|
|||
class RPCError(Exception):
|
||||
|
||||
CodeMessages = {
|
||||
303: ('ERROR_SEE_OTHER', 'The request must be repeated, but directed to a different data center.'),
|
||||
|
||||
400: ('BAD_REQUEST', 'The query contains errors. In the event that a request was created using a '
|
||||
303:
|
||||
('ERROR_SEE_OTHER',
|
||||
'The request must be repeated, but directed to a different data center.'
|
||||
),
|
||||
400:
|
||||
('BAD_REQUEST',
|
||||
'The query contains errors. In the event that a request was created using a '
|
||||
'form and contains user generated data, the user should be notified that the '
|
||||
'data must be corrected before the query is repeated.'),
|
||||
|
||||
401: ('UNAUTHORIZED', 'There was an unauthorized attempt to use functionality available only to '
|
||||
401:
|
||||
('UNAUTHORIZED',
|
||||
'There was an unauthorized attempt to use functionality available only to '
|
||||
'authorized users.'),
|
||||
|
||||
403: ('FORBIDDEN', 'Privacy violation. For example, an attempt to write a message to someone who '
|
||||
403:
|
||||
('FORBIDDEN',
|
||||
'Privacy violation. For example, an attempt to write a message to someone who '
|
||||
'has blacklisted the current user.'),
|
||||
|
||||
404: ('NOT_FOUND', 'An attempt to invoke a non-existent object, such as a method.'),
|
||||
|
||||
420: ('FLOOD', 'The maximum allowed number of attempts to invoke the given method with '
|
||||
404: ('NOT_FOUND',
|
||||
'An attempt to invoke a non-existent object, such as a method.'),
|
||||
420:
|
||||
('FLOOD',
|
||||
'The maximum allowed number of attempts to invoke the given method with '
|
||||
'the given input parameters has been exceeded. For example, in an attempt '
|
||||
'to request a large number of text messages (SMS) for the same phone number.'),
|
||||
|
||||
500: ('INTERNAL', 'An internal server error occurred while a request was being processed; '
|
||||
'for example, there was a disruption while accessing a database or file storage.')
|
||||
'to request a large number of text messages (SMS) for the same phone number.'
|
||||
),
|
||||
500:
|
||||
('INTERNAL',
|
||||
'An internal server error occurred while a request was being processed; '
|
||||
'for example, there was a disruption while accessing a database or file storage.'
|
||||
)
|
||||
}
|
||||
|
||||
ErrorMessages = {
|
||||
# 303 ERROR_SEE_OTHER
|
||||
'FILE_MIGRATE_(\d+)': 'The file to be accessed is currently stored in a different data center (#{}).',
|
||||
|
||||
'PHONE_MIGRATE_(\d+)': 'The phone number a user is trying to use for authorization is associated '
|
||||
'FILE_MIGRATE_(\d+)':
|
||||
'The file to be accessed is currently stored in a different data center (#{}).',
|
||||
'PHONE_MIGRATE_(\d+)':
|
||||
'The phone number a user is trying to use for authorization is associated '
|
||||
'with a different data center (#{}).',
|
||||
|
||||
'NETWORK_MIGRATE_(\d+)': 'The source IP address is associated with a different data center (#{}, '
|
||||
'NETWORK_MIGRATE_(\d+)':
|
||||
'The source IP address is associated with a different data center (#{}, '
|
||||
'for registration).',
|
||||
|
||||
'USER_MIGRATE_(\d+)': 'The user whose identity is being used to execute queries is associated with '
|
||||
'USER_MIGRATE_(\d+)':
|
||||
'The user whose identity is being used to execute queries is associated with '
|
||||
'a different data center (#{} for registration).',
|
||||
|
||||
# 400 BAD_REQUEST
|
||||
'FIRSTNAME_INVALID': 'The first name is invalid.',
|
||||
|
||||
'LASTNAME_INVALID': 'The last name is invalid.',
|
||||
|
||||
'PHONE_NUMBER_INVALID': 'The phone number is invalid.',
|
||||
|
||||
'PHONE_CODE_HASH_EMPTY': 'The phone code hash is missing.',
|
||||
|
||||
'PHONE_CODE_EMPTY': 'The phone code is missing.',
|
||||
|
||||
'PHONE_CODE_INVALID': 'The phone code entered was invalid.',
|
||||
|
||||
'PHONE_CODE_EXPIRED': 'The confirmation code has expired.',
|
||||
|
||||
'API_ID_INVALID': 'The api_id/api_hash combination is invalid.',
|
||||
|
||||
'PHONE_NUMBER_OCCUPIED': 'The phone number is already in use.',
|
||||
|
||||
'PHONE_NUMBER_UNOCCUPIED': 'The phone number is not yet being used.',
|
||||
|
||||
'USERS_TOO_FEW': 'Not enough users (to create a chat, for example).',
|
||||
|
||||
'USERS_TOO_MUCH': 'The maximum number of users has been exceeded (to create a chat, for example).',
|
||||
|
||||
'USERS_TOO_MUCH':
|
||||
'The maximum number of users has been exceeded (to create a chat, for example).',
|
||||
'TYPE_CONSTRUCTOR_INVALID': 'The type constructor is invalid.',
|
||||
|
||||
'FILE_PART_INVALID': 'The file part number is invalid.',
|
||||
|
||||
'FILE_PARTS_INVALID': 'The number of file parts is invalid.',
|
||||
|
||||
'FILE_PART_(\d+)_MISSING': 'Part {} of the file is missing from storage.',
|
||||
|
||||
'FILE_PART_(\d+)_MISSING':
|
||||
'Part {} of the file is missing from storage.',
|
||||
'MD5_CHECKSUM_INVALID': 'The MD5 checksums do not match.',
|
||||
|
||||
'PHOTO_INVALID_DIMENSIONS': 'The photo dimensions are invalid.',
|
||||
|
||||
'FIELD_NAME_INVALID': 'The field with the name FIELD_NAME is invalid.',
|
||||
|
||||
'FIELD_NAME_EMPTY': 'The field with the name FIELD_NAME is missing.',
|
||||
|
||||
'MSG_WAIT_FAILED': 'A waiting call returned an error.',
|
||||
|
||||
'CHAT_ADMIN_REQUIRED': 'Chat admin privileges are required to do that in the specified chat '
|
||||
'CHAT_ADMIN_REQUIRED':
|
||||
'Chat admin privileges are required to do that in the specified chat '
|
||||
'(for example, to send a message in a channel which is not yours).',
|
||||
|
||||
'PASSWORD_HASH_INVALID': 'The password (and thus its hash value) you entered is invalid.',
|
||||
'PASSWORD_HASH_INVALID':
|
||||
'The password (and thus its hash value) you entered is invalid.',
|
||||
|
||||
# 401 UNAUTHORIZED
|
||||
'AUTH_KEY_UNREGISTERED': 'The key is not registered in the system.',
|
||||
|
||||
'AUTH_KEY_INVALID': 'The key is invalid.',
|
||||
|
||||
'USER_DEACTIVATED': 'The user has been deleted/deactivated.',
|
||||
|
||||
'SESSION_REVOKED': 'The authorization has been invalidated, because of the user terminating all sessions.',
|
||||
|
||||
'SESSION_REVOKED':
|
||||
'The authorization has been invalidated, because of the user terminating all sessions.',
|
||||
'SESSION_EXPIRED': 'The authorization has expired.',
|
||||
|
||||
'ACTIVE_USER_REQUIRED': 'The method is only available to already activated users.',
|
||||
|
||||
'AUTH_KEY_PERM_EMPTY': 'The method is unavailable for temporary authorization key, not bound to permanent.',
|
||||
|
||||
'SESSION_PASSWORD_NEEDED': 'Two-steps verification is enabled and a password is required.',
|
||||
'ACTIVE_USER_REQUIRED':
|
||||
'The method is only available to already activated users.',
|
||||
'AUTH_KEY_PERM_EMPTY':
|
||||
'The method is unavailable for temporary authorization key, not bound to permanent.',
|
||||
'SESSION_PASSWORD_NEEDED':
|
||||
'Two-steps verification is enabled and a password is required.',
|
||||
|
||||
# 420 FLOOD
|
||||
'FLOOD_WAIT_(\d+)': 'A wait of {} seconds is required.'
|
||||
|
@ -163,7 +159,8 @@ class RPCError(Exception):
|
|||
# Get additional_data, if any
|
||||
if match.groups():
|
||||
self.additional_data = int(match.group(1))
|
||||
super().__init__(self, error_msg.format(self.additional_data))
|
||||
super().__init__(self,
|
||||
error_msg.format(self.additional_data))
|
||||
else:
|
||||
self.additional_data = None
|
||||
super().__init__(self, error_msg)
|
||||
|
@ -176,47 +173,49 @@ class RPCError(Exception):
|
|||
break
|
||||
|
||||
if not called_super:
|
||||
super().__init__(self, 'Unknown error message with code {}: {}'.format(code, message))
|
||||
super().__init__(
|
||||
self, 'Unknown error message with code {}: {}'.format(code,
|
||||
message))
|
||||
|
||||
|
||||
class BadMessageError(Exception):
|
||||
"""Occurs when handling a bad_message_notification"""
|
||||
ErrorMessages = {
|
||||
16: 'msg_id too low (most likely, client time is wrong it would be worthwhile to '
|
||||
16:
|
||||
'msg_id too low (most likely, client time is wrong it would be worthwhile to '
|
||||
'synchronize it using msg_id notifications and re-send the original message '
|
||||
'with the "correct" msg_id or wrap it in a container with a new msg_id if the '
|
||||
'original message had waited too long on the client to be transmitted).',
|
||||
|
||||
17: 'msg_id too high (similar to the previous case, the client time has to be '
|
||||
17:
|
||||
'msg_id too high (similar to the previous case, the client time has to be '
|
||||
'synchronized, and the message re-sent with the correct msg_id).',
|
||||
|
||||
18: 'Incorrect two lower order msg_id bits (the server expects client message msg_id '
|
||||
18:
|
||||
'Incorrect two lower order msg_id bits (the server expects client message msg_id '
|
||||
'to be divisible by 4).',
|
||||
|
||||
19: 'Container msg_id is the same as msg_id of a previously received message '
|
||||
19:
|
||||
'Container msg_id is the same as msg_id of a previously received message '
|
||||
'(this must never happen).',
|
||||
|
||||
20: 'Message too old, and it cannot be verified whether the server has received a '
|
||||
20:
|
||||
'Message too old, and it cannot be verified whether the server has received a '
|
||||
'message with this msg_id or not.',
|
||||
|
||||
32: 'msg_seqno too low (the server has already received a message with a lower '
|
||||
32:
|
||||
'msg_seqno too low (the server has already received a message with a lower '
|
||||
'msg_id but with either a higher or an equal and odd seqno).',
|
||||
|
||||
33: 'msg_seqno too high (similarly, there is a message with a higher msg_id but with '
|
||||
33:
|
||||
'msg_seqno too high (similarly, there is a message with a higher msg_id but with '
|
||||
'either a lower or an equal and odd seqno).',
|
||||
|
||||
34: 'An even msg_seqno expected (irrelevant message), but odd received.',
|
||||
|
||||
34:
|
||||
'An even msg_seqno expected (irrelevant message), but odd received.',
|
||||
35: 'Odd msg_seqno expected (relevant message), but even received.',
|
||||
|
||||
48: 'Incorrect server salt (in this case, the bad_server_salt response is received with '
|
||||
48:
|
||||
'Incorrect server salt (in this case, the bad_server_salt response is received with '
|
||||
'the correct salt, and the message is to be re-sent with it).',
|
||||
|
||||
64: 'Invalid container.'
|
||||
}
|
||||
|
||||
def __init__(self, code):
|
||||
super().__init__(self, BadMessageError
|
||||
.ErrorMessages.get(code,'Unknown error code (this should not happen): {}.'.format(code)))
|
||||
super().__init__(self, BadMessageError.ErrorMessages.get(
|
||||
code,
|
||||
'Unknown error code (this should not happen): {}.'.format(code)))
|
||||
|
||||
self.code = code
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
# region Multiple utilities
|
||||
|
||||
|
@ -15,7 +15,6 @@ def ensure_parent_dir_exists(file_path):
|
|||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Cryptographic related utils
|
||||
|
@ -26,7 +25,8 @@ def calc_key(shared_key, msg_key, client):
|
|||
x = 0 if client else 8
|
||||
|
||||
sha1a = sha1(msg_key + shared_key[x:x + 32])
|
||||
sha1b = sha1(shared_key[x + 32:x + 48] + msg_key + shared_key[x + 48:x + 64])
|
||||
sha1b = sha1(shared_key[x + 32:x + 48] + msg_key + shared_key[x + 48:x +
|
||||
64])
|
||||
sha1c = sha1(shared_key[x + 64:x + 96] + msg_key)
|
||||
sha1d = sha1(msg_key + shared_key[x + 96:x + 128])
|
||||
|
||||
|
@ -74,8 +74,7 @@ def get_password_hash(pw, current_salt):
|
|||
# https://github.com/DrKLO/Telegram/blob/e31388/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java#L2003
|
||||
data = pw.encode('utf-8')
|
||||
|
||||
pw_hash = current_salt+data+current_salt
|
||||
pw_hash = current_salt + data + current_salt
|
||||
return sha256(pw_hash)
|
||||
|
||||
|
||||
# endregion
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
from telethon.tl.types import UpdateShortChatMessage
|
||||
from telethon.tl.types import UpdateShortMessage
|
||||
from telethon import TelegramClient, RPCError
|
||||
|
||||
from telethon.utils import get_display_name, get_input_peer
|
||||
|
||||
import shutil
|
||||
from getpass import getpass
|
||||
|
||||
from telethon import RPCError, TelegramClient
|
||||
from telethon.tl.types import UpdateShortChatMessage, UpdateShortMessage
|
||||
from telethon.utils import get_display_name, get_input_peer
|
||||
|
||||
# Get the (current) number of lines in the terminal
|
||||
cols, rows = shutil.get_terminal_size()
|
||||
|
||||
|
@ -27,7 +25,8 @@ def bytes_to_string(byte_count):
|
|||
byte_count /= 1024
|
||||
suffix_index += 1
|
||||
|
||||
return '{:.2f}{}'.format(byte_count, [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index])
|
||||
return '{:.2f}{}'.format(byte_count,
|
||||
[' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index])
|
||||
|
||||
|
||||
class InteractiveTelegramClient(TelegramClient):
|
||||
|
@ -58,7 +57,8 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
# Two-step verification may be enabled
|
||||
except RPCError as e:
|
||||
if e.password_required:
|
||||
pw = getpass('Two step verification is enabled. Please enter your password: ')
|
||||
pw = getpass(
|
||||
'Two step verification is enabled. Please enter your password: ')
|
||||
code_ok = self.sign_in(password=pw)
|
||||
else:
|
||||
raise e
|
||||
|
@ -117,10 +117,14 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
print('Available commands:')
|
||||
print(' !q: Quits the current chat.')
|
||||
print(' !Q: Quits the current chat and exits.')
|
||||
print(' !h: prints the latest messages (message History) of the chat.')
|
||||
print(' !up <path>: Uploads and sends a Photo located at the given path.')
|
||||
print(' !uf <path>: Uploads and sends a File document located at the given path.')
|
||||
print(' !dm <msg-id>: Downloads the given message Media (if any).')
|
||||
print(
|
||||
' !h: prints the latest messages (message History) of the chat.')
|
||||
print(
|
||||
' !up <path>: Uploads and sends a Photo located at the given path.')
|
||||
print(
|
||||
' !uf <path>: Uploads and sends a File document located at the given path.')
|
||||
print(
|
||||
' !dm <msg-id>: Downloads the given message Media (if any).')
|
||||
print(' !dp: Downloads the current dialog Profile picture.')
|
||||
print()
|
||||
|
||||
|
@ -136,10 +140,12 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
# History
|
||||
elif msg == '!h':
|
||||
# First retrieve the messages and some information
|
||||
total_count, messages, senders = self.get_message_history(input_peer, limit=10)
|
||||
total_count, messages, senders = self.get_message_history(
|
||||
input_peer, limit=10)
|
||||
# Iterate over all (in reverse order so the latest appears the last in the console)
|
||||
# and print them in "[hh:mm] Sender: Message" text format
|
||||
for msg, sender in zip(reversed(messages), reversed(senders)):
|
||||
for msg, sender in zip(
|
||||
reversed(messages), reversed(senders)):
|
||||
# Get the name of the sender if any
|
||||
name = sender.first_name if sender else '???'
|
||||
|
||||
|
@ -147,13 +153,15 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
if msg.media:
|
||||
self.found_media.add(msg)
|
||||
content = '<{}> {}'.format( # The media may or may not have a caption
|
||||
msg.media.__class__.__name__, getattr(msg.media, 'caption', ''))
|
||||
msg.media.__class__.__name__,
|
||||
getattr(msg.media, 'caption', ''))
|
||||
else:
|
||||
content = msg.message
|
||||
|
||||
# And print it to the user
|
||||
print('[{}:{}] (ID={}) {}: {}'.format(
|
||||
msg.date.hour, msg.date.minute, msg.id, name, content))
|
||||
msg.date.hour, msg.date.minute, msg.id, name,
|
||||
content))
|
||||
|
||||
# Send photo
|
||||
elif msg.startswith('!up '):
|
||||
|
@ -176,18 +184,21 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
print('Downloading profile picture...')
|
||||
success = self.download_profile_photo(entity.photo, output)
|
||||
if success:
|
||||
print('Profile picture downloaded to {}'.format(output))
|
||||
print('Profile picture downloaded to {}'.format(
|
||||
output))
|
||||
else:
|
||||
print('"{}" does not seem to have a profile picture.'
|
||||
.format(get_display_name(entity)))
|
||||
|
||||
# Send chat message (if any)
|
||||
elif msg:
|
||||
self.send_message(input_peer, msg, markdown=True, no_web_page=True)
|
||||
self.send_message(
|
||||
input_peer, msg, markdown=True, no_web_page=True)
|
||||
|
||||
def send_photo(self, path, peer):
|
||||
print('Uploading {}...'.format(path))
|
||||
input_file = self.upload_file(path, progress_callback=self.upload_progress_callback)
|
||||
input_file = self.upload_file(
|
||||
path, progress_callback=self.upload_progress_callback)
|
||||
|
||||
# After we have the handle to the uploaded file, send it to our peer
|
||||
self.send_photo_file(input_file, peer)
|
||||
|
@ -195,7 +206,8 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
|
||||
def send_document(self, path, peer):
|
||||
print('Uploading {}...'.format(path))
|
||||
input_file = self.upload_file(path, progress_callback=self.upload_progress_callback)
|
||||
input_file = self.upload_file(
|
||||
path, progress_callback=self.upload_progress_callback)
|
||||
|
||||
# After we have the handle to the uploaded file, send it to our peer
|
||||
self.send_document_file(input_file, peer)
|
||||
|
@ -212,7 +224,8 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
# Let the output be the message ID
|
||||
output = str('usermedia/{}'.format(msg_media_id))
|
||||
print('Downloading media with name {}...'.format(output))
|
||||
output = self.download_msg_media(msg.media,
|
||||
output = self.download_msg_media(
|
||||
msg.media,
|
||||
file_path=output,
|
||||
progress_callback=self.download_progress_callback)
|
||||
print('Media downloaded to {}!'.format(output))
|
||||
|
@ -222,32 +235,35 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
|
||||
@staticmethod
|
||||
def download_progress_callback(downloaded_bytes, total_bytes):
|
||||
InteractiveTelegramClient.print_progress('Downloaded', downloaded_bytes, total_bytes)
|
||||
InteractiveTelegramClient.print_progress('Downloaded',
|
||||
downloaded_bytes, total_bytes)
|
||||
|
||||
@staticmethod
|
||||
def upload_progress_callback(uploaded_bytes, total_bytes):
|
||||
InteractiveTelegramClient.print_progress('Uploaded', uploaded_bytes, total_bytes)
|
||||
InteractiveTelegramClient.print_progress('Uploaded', uploaded_bytes,
|
||||
total_bytes)
|
||||
|
||||
@staticmethod
|
||||
def print_progress(progress_type, downloaded_bytes, total_bytes):
|
||||
print('{} {} out of {} ({:.2%})'.format(
|
||||
progress_type,
|
||||
bytes_to_string(downloaded_bytes),
|
||||
bytes_to_string(total_bytes),
|
||||
downloaded_bytes / total_bytes))
|
||||
print('{} {} out of {} ({:.2%})'.format(progress_type, bytes_to_string(
|
||||
downloaded_bytes), bytes_to_string(total_bytes), downloaded_bytes /
|
||||
total_bytes))
|
||||
|
||||
@staticmethod
|
||||
def update_handler(update_object):
|
||||
if type(update_object) is UpdateShortMessage:
|
||||
if update_object.out:
|
||||
print('You sent {} to user #{}'.format(update_object.message, update_object.user_id))
|
||||
print('You sent {} to user #{}'.format(update_object.message,
|
||||
update_object.user_id))
|
||||
else:
|
||||
print('[User #{} sent {}]'.format(update_object.user_id, update_object.message))
|
||||
print('[User #{} sent {}]'.format(update_object.user_id,
|
||||
update_object.message))
|
||||
|
||||
elif type(update_object) is UpdateShortChatMessage:
|
||||
if update_object.out:
|
||||
print('You sent {} to chat #{}'.format(update_object.message, update_object.chat_id))
|
||||
print('You sent {} to chat #{}'.format(update_object.message,
|
||||
update_object.chat_id))
|
||||
else:
|
||||
print('[Chat #{}, user #{} sent {}]'.format(update_object.chat_id,
|
||||
update_object.from_id,
|
||||
print('[Chat #{}, user #{} sent {}]'.format(
|
||||
update_object.chat_id, update_object.from_id,
|
||||
update_object.message))
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import os
|
||||
import time
|
||||
|
||||
import telethon.helpers as utils
|
||||
from telethon.utils import BinaryWriter, BinaryReader
|
||||
from telethon.crypto import AES, AuthKey, Factorizator, RSA
|
||||
from telethon.crypto import AES, RSA, AuthKey, Factorizator
|
||||
from telethon.network import MtProtoPlainSender
|
||||
from telethon.utils import BinaryReader, BinaryWriter
|
||||
|
||||
|
||||
def do_authentication(transport):
|
||||
|
@ -23,7 +24,8 @@ def do_authentication(transport):
|
|||
with BinaryReader(sender.receive()) as reader:
|
||||
response_code = reader.read_int(signed=False)
|
||||
if response_code != 0x05162463:
|
||||
raise AssertionError('Invalid response code: {}'.format(hex(response_code)))
|
||||
raise AssertionError('Invalid response code: {}'.format(
|
||||
hex(response_code)))
|
||||
|
||||
nonce_from_server = reader.read(16)
|
||||
if nonce_from_server != nonce:
|
||||
|
@ -36,7 +38,8 @@ def do_authentication(transport):
|
|||
|
||||
vector_id = reader.read_int()
|
||||
if vector_id != 0x1cb5c415:
|
||||
raise AssertionError('Invalid vector constructor ID: {}'.format(hex(response_code)))
|
||||
raise AssertionError('Invalid vector constructor ID: {}'.format(
|
||||
hex(response_code)))
|
||||
|
||||
fingerprints = []
|
||||
fingerprint_count = reader.read_int()
|
||||
|
@ -47,32 +50,46 @@ def do_authentication(transport):
|
|||
new_nonce = os.urandom(32)
|
||||
p, q = Factorizator.factorize(pq)
|
||||
with BinaryWriter() as pq_inner_data_writer:
|
||||
pq_inner_data_writer.write_int(0x83c95aec, signed=False) # PQ Inner Data
|
||||
pq_inner_data_writer.write_int(
|
||||
0x83c95aec, signed=False) # PQ Inner Data
|
||||
pq_inner_data_writer.tgwrite_bytes(get_byte_array(pq, signed=False))
|
||||
pq_inner_data_writer.tgwrite_bytes(get_byte_array(min(p, q), signed=False))
|
||||
pq_inner_data_writer.tgwrite_bytes(get_byte_array(max(p, q), signed=False))
|
||||
pq_inner_data_writer.tgwrite_bytes(
|
||||
get_byte_array(
|
||||
min(p, q), signed=False))
|
||||
pq_inner_data_writer.tgwrite_bytes(
|
||||
get_byte_array(
|
||||
max(p, q), signed=False))
|
||||
pq_inner_data_writer.write(nonce)
|
||||
pq_inner_data_writer.write(server_nonce)
|
||||
pq_inner_data_writer.write(new_nonce)
|
||||
|
||||
cipher_text, target_fingerprint = None, None
|
||||
for fingerprint in fingerprints:
|
||||
cipher_text = RSA.encrypt(get_fingerprint_text(fingerprint), pq_inner_data_writer.get_bytes())
|
||||
cipher_text = RSA.encrypt(
|
||||
get_fingerprint_text(fingerprint),
|
||||
pq_inner_data_writer.get_bytes())
|
||||
|
||||
if cipher_text is not None:
|
||||
target_fingerprint = fingerprint
|
||||
break
|
||||
|
||||
if cipher_text is None:
|
||||
raise AssertionError('Could not find a valid key for fingerprints: {}'
|
||||
.format(', '.join([get_fingerprint_text(f) for f in fingerprints])))
|
||||
raise AssertionError(
|
||||
'Could not find a valid key for fingerprints: {}'
|
||||
.format(', '.join([get_fingerprint_text(f)
|
||||
for f in fingerprints])))
|
||||
|
||||
with BinaryWriter() as req_dh_params_writer:
|
||||
req_dh_params_writer.write_int(0xd712e4be, signed=False) # Req DH Params
|
||||
req_dh_params_writer.write_int(
|
||||
0xd712e4be, signed=False) # Req DH Params
|
||||
req_dh_params_writer.write(nonce)
|
||||
req_dh_params_writer.write(server_nonce)
|
||||
req_dh_params_writer.tgwrite_bytes(get_byte_array(min(p, q), signed=False))
|
||||
req_dh_params_writer.tgwrite_bytes(get_byte_array(max(p, q), signed=False))
|
||||
req_dh_params_writer.tgwrite_bytes(
|
||||
get_byte_array(
|
||||
min(p, q), signed=False))
|
||||
req_dh_params_writer.tgwrite_bytes(
|
||||
get_byte_array(
|
||||
max(p, q), signed=False))
|
||||
req_dh_params_writer.write(target_fingerprint)
|
||||
req_dh_params_writer.tgwrite_bytes(cipher_text)
|
||||
|
||||
|
@ -88,7 +105,8 @@ def do_authentication(transport):
|
|||
raise AssertionError('Server DH params fail: TODO')
|
||||
|
||||
if response_code != 0xd0e8075c:
|
||||
raise AssertionError('Invalid response code: {}'.format(hex(response_code)))
|
||||
raise AssertionError('Invalid response code: {}'.format(
|
||||
hex(response_code)))
|
||||
|
||||
nonce_from_server = reader.read(16)
|
||||
if nonce_from_server != nonce:
|
||||
|
@ -106,7 +124,6 @@ def do_authentication(transport):
|
|||
|
||||
g, dh_prime, ga, time_offset = None, None, None, None
|
||||
with BinaryReader(plain_text_answer) as dh_inner_data_reader:
|
||||
hashsum = dh_inner_data_reader.read(20)
|
||||
code = dh_inner_data_reader.read_int(signed=False)
|
||||
if code != 0xb5890dba:
|
||||
raise AssertionError('Invalid DH Inner Data code: {}'.format(code))
|
||||
|
@ -132,26 +149,34 @@ def do_authentication(transport):
|
|||
|
||||
# Prepare client DH Inner Data
|
||||
with BinaryWriter() as client_dh_inner_data_writer:
|
||||
client_dh_inner_data_writer.write_int(0x6643b654, signed=False) # Client DH Inner Data
|
||||
client_dh_inner_data_writer.write_int(
|
||||
0x6643b654, signed=False) # Client DH Inner Data
|
||||
client_dh_inner_data_writer.write(nonce)
|
||||
client_dh_inner_data_writer.write(server_nonce)
|
||||
client_dh_inner_data_writer.write_long(0) # TODO retry_id
|
||||
client_dh_inner_data_writer.tgwrite_bytes(get_byte_array(gb, signed=False))
|
||||
client_dh_inner_data_writer.tgwrite_bytes(
|
||||
get_byte_array(
|
||||
gb, signed=False))
|
||||
|
||||
with BinaryWriter() as client_dh_inner_data_with_hash_writer:
|
||||
client_dh_inner_data_with_hash_writer.write(utils.sha1(client_dh_inner_data_writer.get_bytes()))
|
||||
client_dh_inner_data_with_hash_writer.write(client_dh_inner_data_writer.get_bytes())
|
||||
client_dh_inner_data_bytes = client_dh_inner_data_with_hash_writer.get_bytes()
|
||||
client_dh_inner_data_with_hash_writer.write(
|
||||
utils.sha1(client_dh_inner_data_writer.get_bytes()))
|
||||
client_dh_inner_data_with_hash_writer.write(
|
||||
client_dh_inner_data_writer.get_bytes())
|
||||
client_dh_inner_data_bytes = client_dh_inner_data_with_hash_writer.get_bytes(
|
||||
)
|
||||
|
||||
# Encryption
|
||||
client_dh_inner_data_encrypted_bytes = AES.encrypt_ige(client_dh_inner_data_bytes, key, iv)
|
||||
client_dh_inner_data_encrypted_bytes = AES.encrypt_ige(
|
||||
client_dh_inner_data_bytes, key, iv)
|
||||
|
||||
# Prepare Set client DH params
|
||||
with BinaryWriter() as set_client_dh_params_writer:
|
||||
set_client_dh_params_writer.write_int(0xf5045f1f, signed=False)
|
||||
set_client_dh_params_writer.write(nonce)
|
||||
set_client_dh_params_writer.write(server_nonce)
|
||||
set_client_dh_params_writer.tgwrite_bytes(client_dh_inner_data_encrypted_bytes)
|
||||
set_client_dh_params_writer.tgwrite_bytes(
|
||||
client_dh_inner_data_encrypted_bytes)
|
||||
|
||||
set_client_dh_params_bytes = set_client_dh_params_writer.get_bytes()
|
||||
sender.send(set_client_dh_params_bytes)
|
||||
|
@ -171,7 +196,8 @@ def do_authentication(transport):
|
|||
new_nonce_hash1 = reader.read(16)
|
||||
auth_key = AuthKey(get_byte_array(gab, signed=False))
|
||||
|
||||
new_nonce_hash_calculated = auth_key.calc_new_nonce_hash(new_nonce, 1)
|
||||
new_nonce_hash_calculated = auth_key.calc_new_nonce_hash(new_nonce,
|
||||
1)
|
||||
if new_nonce_hash1 != new_nonce_hash_calculated:
|
||||
raise AssertionError('Invalid new nonce hash')
|
||||
|
||||
|
@ -200,7 +226,8 @@ def get_byte_array(integer, signed):
|
|||
"""Gets the arbitrary-length byte array corresponding to the given integer"""
|
||||
bits = integer.bit_length()
|
||||
byte_length = (bits + 8 - 1) // 8 # 8 bits per byte
|
||||
return int.to_bytes(integer, length=byte_length, byteorder='big', signed=signed)
|
||||
return int.to_bytes(
|
||||
integer, length=byte_length, byteorder='big', signed=signed)
|
||||
|
||||
|
||||
def get_int(byte_array, signed=True):
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import time
|
||||
import random
|
||||
from telethon.utils import BinaryWriter, BinaryReader
|
||||
import time
|
||||
|
||||
from telethon.utils import BinaryReader, BinaryWriter
|
||||
|
||||
|
||||
class MtProtoPlainSender:
|
||||
"""MTProto Mobile Protocol plain sender (https://core.telegram.org/mtproto/description#unencrypted-messages)"""
|
||||
|
||||
def __init__(self, transport):
|
||||
self._sequence = 0
|
||||
self._time_offset = 0
|
||||
|
@ -37,9 +39,12 @@ class MtProtoPlainSender:
|
|||
"""Generates a new message ID based on the current time (in ms) since epoch"""
|
||||
# See https://core.telegram.org/mtproto/description#message-identifier-msg-id
|
||||
ms_time = int(time.time() * 1000)
|
||||
new_msg_id = (((ms_time // 1000) << 32) | # "must approximately equal unixtime*2^32"
|
||||
((ms_time % 1000) << 22) | # "approximate moment in time the message was created"
|
||||
random.randint(0, 524288) << 2) # "message identifiers are divisible by 4"
|
||||
new_msg_id = (((ms_time // 1000) << 32)
|
||||
| # "must approximately equal unixtime*2^32"
|
||||
((ms_time % 1000) << 22)
|
||||
| # "approximate moment in time the message was created"
|
||||
random.randint(0, 524288)
|
||||
<< 2) # "message identifiers are divisible by 4"
|
||||
|
||||
# Ensure that we always return a message ID which is higher than the previous one
|
||||
if self._last_msg_id >= new_msg_id:
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import gzip
|
||||
from telethon.errors import *
|
||||
from time import sleep
|
||||
from datetime import timedelta
|
||||
from threading import Thread, RLock
|
||||
from threading import RLock, Thread
|
||||
from time import sleep
|
||||
|
||||
import telethon.helpers as utils
|
||||
from telethon.crypto import AES
|
||||
from telethon.utils import BinaryWriter, BinaryReader
|
||||
from telethon.tl.types import MsgsAck
|
||||
from telethon.errors import *
|
||||
from telethon.tl.all_tlobjects import tlobjects
|
||||
from telethon.tl.types import MsgsAck
|
||||
from telethon.utils import BinaryReader, BinaryWriter
|
||||
|
||||
|
||||
class MtProtoSender:
|
||||
"""MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)"""
|
||||
|
||||
def __init__(self, transport, session):
|
||||
self.transport = transport
|
||||
self.session = session
|
||||
|
@ -27,7 +28,8 @@ class MtProtoSender:
|
|||
# We need this to avoid using the updates thread if we're waiting to read
|
||||
self.waiting_receive = False
|
||||
|
||||
self.updates_thread = Thread(target=self.updates_thread_method, name='Updates thread')
|
||||
self.updates_thread = Thread(
|
||||
target=self.updates_thread_method, name='Updates thread')
|
||||
self.updates_thread_running = False
|
||||
self.updates_thread_receiving = False
|
||||
|
||||
|
@ -118,7 +120,8 @@ class MtProtoSender:
|
|||
message, remote_msg_id, remote_sequence = self.decode_msg(body)
|
||||
|
||||
with BinaryReader(message) as reader:
|
||||
self.process_msg(remote_msg_id, remote_sequence, reader, request)
|
||||
self.process_msg(remote_msg_id, remote_sequence, reader,
|
||||
request)
|
||||
|
||||
# We can now set the flag to False thus resuming the updates thread
|
||||
self.waiting_receive = False
|
||||
|
@ -148,7 +151,8 @@ class MtProtoSender:
|
|||
|
||||
# And then finally send the encrypted packet
|
||||
with BinaryWriter() as cipher_writer:
|
||||
cipher_writer.write_long(self.session.auth_key.key_id, signed=False)
|
||||
cipher_writer.write_long(
|
||||
self.session.auth_key.key_id, signed=False)
|
||||
cipher_writer.write(msg_key)
|
||||
cipher_writer.write(cipher_text)
|
||||
self.transport.send(cipher_writer.get_bytes())
|
||||
|
@ -168,7 +172,8 @@ class MtProtoSender:
|
|||
msg_key = reader.read(16)
|
||||
|
||||
key, iv = utils.calc_key(self.session.auth_key.key, msg_key, False)
|
||||
plain_text = AES.decrypt_ige(reader.read(len(body) - reader.tell_position()), key, iv)
|
||||
plain_text = AES.decrypt_ige(
|
||||
reader.read(len(body) - reader.tell_position()), key, iv)
|
||||
|
||||
with BinaryReader(plain_text) as plain_text_reader:
|
||||
remote_salt = plain_text_reader.read_long()
|
||||
|
@ -198,7 +203,8 @@ class MtProtoSender:
|
|||
if code == 0x3072cfa1: # gzip_packed
|
||||
return self.handle_gzip_packed(msg_id, sequence, reader, request)
|
||||
if code == 0xedab447b: # bad_server_salt
|
||||
return self.handle_bad_server_salt(msg_id, sequence, reader, request)
|
||||
return self.handle_bad_server_salt(msg_id, sequence, reader,
|
||||
request)
|
||||
if code == 0xa7eff811: # bad_msg_notification
|
||||
return self.handle_bad_msg_notification(msg_id, sequence, reader)
|
||||
|
||||
|
@ -253,7 +259,8 @@ class MtProtoSender:
|
|||
self.session.salt = new_salt
|
||||
|
||||
if request is None:
|
||||
raise ValueError('Tried to handle a bad server salt with no request specified')
|
||||
raise ValueError(
|
||||
'Tried to handle a bad server salt with no request specified')
|
||||
|
||||
# Resend
|
||||
self.send(request)
|
||||
|
@ -277,15 +284,18 @@ class MtProtoSender:
|
|||
request.confirm_received = True
|
||||
|
||||
if inner_code == 0x2144ca19: # RPC Error
|
||||
error = RPCError(code=reader.read_int(), message=reader.tgread_string())
|
||||
error = RPCError(
|
||||
code=reader.read_int(), message=reader.tgread_string())
|
||||
if error.must_resend:
|
||||
if not request:
|
||||
raise ValueError('The previously sent request must be resent. '
|
||||
raise ValueError(
|
||||
'The previously sent request must be resent. '
|
||||
'However, no request was previously sent (called from updates thread).')
|
||||
request.confirm_received = False
|
||||
|
||||
if error.message.startswith('FLOOD_WAIT_'):
|
||||
print('Should wait {}s. Sleeping until then.'.format(error.additional_data))
|
||||
print('Should wait {}s. Sleeping until then.'.format(
|
||||
error.additional_data))
|
||||
sleep(error.additional_data)
|
||||
|
||||
elif error.message.startswith('PHONE_MIGRATE_'):
|
||||
|
@ -295,7 +305,8 @@ class MtProtoSender:
|
|||
raise error
|
||||
else:
|
||||
if not request:
|
||||
raise ValueError('Cannot receive a request from inside an RPC result from the updates thread.')
|
||||
raise ValueError(
|
||||
'Cannot receive a request from inside an RPC result from the updates thread.')
|
||||
|
||||
if inner_code == 0x3072cfa1: # GZip packed
|
||||
unpacked_data = gzip.decompress(reader.tgread_bytes())
|
||||
|
@ -311,7 +322,8 @@ class MtProtoSender:
|
|||
unpacked_data = gzip.decompress(packed_data)
|
||||
|
||||
with BinaryReader(unpacked_data) as compressed_reader:
|
||||
return self.process_msg(msg_id, sequence, compressed_reader, request)
|
||||
return self.process_msg(msg_id, sequence, compressed_reader,
|
||||
request)
|
||||
|
||||
# endregion
|
||||
|
||||
|
@ -340,10 +352,12 @@ class MtProtoSender:
|
|||
try:
|
||||
self.updates_thread_receiving = True
|
||||
seq, body = self.transport.receive(timeout)
|
||||
message, remote_msg_id, remote_sequence = self.decode_msg(body)
|
||||
message, remote_msg_id, remote_sequence = self.decode_msg(
|
||||
body)
|
||||
|
||||
with BinaryReader(message) as reader:
|
||||
self.process_msg(remote_msg_id, remote_sequence, reader)
|
||||
self.process_msg(remote_msg_id, remote_sequence,
|
||||
reader)
|
||||
|
||||
except (ReadCancelledError, TimeoutError):
|
||||
pass
|
||||
|
|
|
@ -78,7 +78,8 @@ class TcpClient:
|
|||
if timeout:
|
||||
time_passed = datetime.now() - start_time
|
||||
if time_passed > timeout:
|
||||
raise TimeoutError('The read operation exceeded the timeout.')
|
||||
raise TimeoutError(
|
||||
'The read operation exceeded the timeout.')
|
||||
|
||||
# If everything went fine, return the read bytes
|
||||
return writer.get_bytes()
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from binascii import crc32
|
||||
from datetime import timedelta
|
||||
|
||||
from telethon.network import TcpClient
|
||||
from telethon.errors import *
|
||||
from telethon.network import TcpClient
|
||||
from telethon.utils import BinaryWriter
|
||||
|
||||
|
||||
|
@ -45,9 +45,8 @@ class TcpTransport:
|
|||
|
||||
body = self.tcp_client.read(packet_length - 12, timeout)
|
||||
|
||||
checksum = int.from_bytes(self.tcp_client.read(4, timeout),
|
||||
byteorder='little',
|
||||
signed=False)
|
||||
checksum = int.from_bytes(
|
||||
self.tcp_client.read(4, timeout), byteorder='little', signed=False)
|
||||
|
||||
# Then perform the checks
|
||||
rv = packet_length_bytes + seq_bytes + body
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from telethon.tl.types import MessageEntityBold, MessageEntityItalic, MessageEntityCode, MessageEntityTextUrl
|
||||
from telethon.tl.types import (MessageEntityBold, MessageEntityCode,
|
||||
MessageEntityItalic, MessageEntityTextUrl)
|
||||
|
||||
|
||||
def parse_message_entities(msg):
|
||||
|
@ -44,13 +45,13 @@ def parse_message_entities(msg):
|
|||
|
||||
# Add 1 when slicing the message not to include the [] nor ()
|
||||
# There is no need to subtract 1 on the later part because that index is already excluded
|
||||
link_text = ''.join(msg[vui[0]+1:vui[1]])
|
||||
link_url = ''.join(msg[vui[2]+1:vui[3]])
|
||||
link_text = ''.join(msg[vui[0] + 1:vui[1]])
|
||||
link_url = ''.join(msg[vui[2] + 1:vui[3]])
|
||||
|
||||
# After we have retrieved both the link text and url, replace them in the message
|
||||
# Now we do have to add 1 to include the [] and () when deleting and replacing!
|
||||
del msg[vui[2]:vui[3]+1]
|
||||
msg[vui[0]:vui[1]+1] = link_text
|
||||
del msg[vui[2]:vui[3] + 1]
|
||||
msg[vui[0]:vui[1] + 1] = link_text
|
||||
|
||||
# Finally, update the current valid index url to reflect that all the previous VUI's will be removed
|
||||
# This is because, after the previous VUI's get done, their part of the message is removed too,
|
||||
|
@ -63,14 +64,12 @@ def parse_message_entities(msg):
|
|||
# No need to subtract the displacement from the URL part (indices 2 and 3)
|
||||
|
||||
# When calculating the length, subtract 1 again not to include the previously called ']'
|
||||
entities.append(MessageEntityTextUrl(offset=vui[0], length=vui[1] - vui[0] - 1, url=link_url))
|
||||
entities.append(
|
||||
MessageEntityTextUrl(
|
||||
offset=vui[0], length=vui[1] - vui[0] - 1, url=link_url))
|
||||
|
||||
# After the message is clean from links, handle all the indicator flags
|
||||
indicator_flags = {
|
||||
'*': None,
|
||||
'_': None,
|
||||
'`': None
|
||||
}
|
||||
indicator_flags = {'*': None, '_': None, '`': None}
|
||||
|
||||
# Iterate over the list to find the indicators of entities
|
||||
for i, c in enumerate(msg):
|
||||
|
@ -88,13 +87,19 @@ def parse_message_entities(msg):
|
|||
|
||||
# Add the corresponding entity
|
||||
if c == '*':
|
||||
entities.append(MessageEntityBold(offset=offset, length=length))
|
||||
entities.append(
|
||||
MessageEntityBold(
|
||||
offset=offset, length=length))
|
||||
|
||||
elif c == '_':
|
||||
entities.append(MessageEntityItalic(offset=offset, length=length))
|
||||
entities.append(
|
||||
MessageEntityItalic(
|
||||
offset=offset, length=length))
|
||||
|
||||
elif c == '`':
|
||||
entities.append(MessageEntityCode(offset=offset, length=length))
|
||||
entities.append(
|
||||
MessageEntityCode(
|
||||
offset=offset, length=length))
|
||||
|
||||
# Clear the flag to start over with this indicator
|
||||
indicator_flags[c] = None
|
||||
|
@ -116,15 +121,16 @@ def parse_message_entities(msg):
|
|||
# In this case, the current entity length is decreased by two,
|
||||
# and all the subentities offset decreases 1
|
||||
if (subentity.offset > entity.offset and
|
||||
subentity.offset + subentity.length < entity.offset + entity.length):
|
||||
subentity.offset + subentity.length <
|
||||
entity.offset + entity.length):
|
||||
entity.length -= 2
|
||||
subentity.offset -= 1
|
||||
|
||||
# Second case, both inside: so*me_th*in_g.
|
||||
# In this case, the current entity length is decreased by one,
|
||||
# and all the subentities offset and length decrease 1
|
||||
elif (entity.offset < subentity.offset < entity.offset + entity.length and
|
||||
subentity.offset + subentity.length > entity.offset + entity.length):
|
||||
elif (entity.offset < subentity.offset < entity.offset +
|
||||
entity.length < subentity.offset + subentity.length):
|
||||
entity.length -= 1
|
||||
subentity.offset -= 1
|
||||
subentity.length -= 1
|
||||
|
|
|
@ -1,47 +1,40 @@
|
|||
import platform
|
||||
from datetime import datetime, timedelta
|
||||
from hashlib import md5
|
||||
from os import path, listdir
|
||||
from mimetypes import guess_type
|
||||
|
||||
# For sending and receiving requests
|
||||
from telethon.tl import MTProtoRequest
|
||||
from telethon.tl import Session
|
||||
|
||||
# The Requests and types that we'll be using
|
||||
from telethon.tl.functions.upload import SaveBigFilePartRequest
|
||||
from telethon.tl.functions import InvokeWithLayerRequest, InitConnectionRequest
|
||||
from telethon.tl.functions.help import GetConfigRequest
|
||||
from telethon.tl.functions.upload import SaveFilePartRequest, GetFileRequest
|
||||
from telethon.tl.functions.messages import \
|
||||
GetDialogsRequest, GetHistoryRequest, \
|
||||
SendMessageRequest, SendMediaRequest, \
|
||||
ReadHistoryRequest
|
||||
|
||||
from telethon.tl.functions.auth import \
|
||||
SendCodeRequest, CheckPasswordRequest, \
|
||||
SignInRequest, SignUpRequest, LogOutRequest
|
||||
|
||||
# The following is required to get the password salt
|
||||
from telethon.tl.functions.account import GetPasswordRequest
|
||||
|
||||
# All the types we need to work with
|
||||
from telethon.tl.types import \
|
||||
InputPeerEmpty, \
|
||||
UserProfilePhotoEmpty, ChatPhotoEmpty, \
|
||||
InputFile, InputFileLocation, InputMediaUploadedPhoto, InputMediaUploadedDocument, \
|
||||
MessageMediaContact, MessageMediaDocument, MessageMediaPhoto, \
|
||||
DocumentAttributeAudio, DocumentAttributeFilename, InputDocumentFileLocation
|
||||
from os import listdir, path
|
||||
|
||||
# Import some externalized utilities to work with the Telegram types and more
|
||||
import telethon.helpers as utils
|
||||
import telethon.network.authenticator as authenticator
|
||||
from telethon.utils import find_user_or_chat, get_appropiate_part_size, get_extension
|
||||
|
||||
from telethon.errors import *
|
||||
from telethon.network import MtProtoSender, TcpTransport
|
||||
from telethon.parser.markdown_parser import parse_message_entities
|
||||
# For sending and receiving requests
|
||||
from telethon.tl import MTProtoRequest, Session
|
||||
from telethon.tl.all_tlobjects import layer
|
||||
from telethon.tl.functions import InitConnectionRequest, InvokeWithLayerRequest
|
||||
# The following is required to get the password salt
|
||||
from telethon.tl.functions.account import GetPasswordRequest
|
||||
from telethon.tl.functions.auth import (CheckPasswordRequest, LogOutRequest,
|
||||
SendCodeRequest, SignInRequest,
|
||||
SignUpRequest)
|
||||
from telethon.tl.functions.help import GetConfigRequest
|
||||
from telethon.tl.functions.messages import (
|
||||
GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest,
|
||||
SendMessageRequest)
|
||||
# The Requests and types that we'll be using
|
||||
from telethon.tl.functions.upload import (
|
||||
GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest)
|
||||
# All the types we need to work with
|
||||
from telethon.tl.types import (
|
||||
ChatPhotoEmpty, DocumentAttributeAudio, DocumentAttributeFilename,
|
||||
InputDocumentFileLocation, InputFile, InputFileLocation,
|
||||
InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty,
|
||||
MessageMediaContact, MessageMediaDocument, MessageMediaPhoto,
|
||||
UserProfilePhotoEmpty)
|
||||
from telethon.utils import (find_user_or_chat, get_appropiate_part_size,
|
||||
get_extension)
|
||||
|
||||
|
||||
class TelegramClient:
|
||||
|
@ -62,7 +55,8 @@ class TelegramClient:
|
|||
.save() and .load() implementations to suit your needs."""
|
||||
|
||||
if api_id is None or api_hash is None:
|
||||
raise PermissionError('Your API ID or Hash are invalid. Please read "Requirements" on README.rst')
|
||||
raise PermissionError(
|
||||
'Your API ID or Hash are invalid. Please read "Requirements" on README.rst')
|
||||
|
||||
self.api_id = api_id
|
||||
self.api_hash = api_hash
|
||||
|
@ -73,9 +67,11 @@ class TelegramClient:
|
|||
elif isinstance(session, Session):
|
||||
self.session = session
|
||||
else:
|
||||
raise ValueError('The given session must either be a string or a Session instance.')
|
||||
raise ValueError(
|
||||
'The given session must either be a string or a Session instance.')
|
||||
|
||||
self.transport = TcpTransport(self.session.server_address, self.session.port)
|
||||
self.transport = TcpTransport(self.session.server_address,
|
||||
self.session.port)
|
||||
|
||||
# These will be set later
|
||||
self.dc_options = None
|
||||
|
@ -104,14 +100,17 @@ class TelegramClient:
|
|||
|
||||
# Now it's time to send an InitConnectionRequest
|
||||
# This must always be invoked with the layer we'll be using
|
||||
query = InitConnectionRequest(api_id=self.api_id,
|
||||
query = InitConnectionRequest(
|
||||
api_id=self.api_id,
|
||||
device_model=platform.node(),
|
||||
system_version=platform.system(),
|
||||
app_version=self.__version__,
|
||||
lang_code='en',
|
||||
query=GetConfigRequest())
|
||||
|
||||
result = self.invoke(InvokeWithLayerRequest(layer=layer, query=query))
|
||||
result = self.invoke(
|
||||
InvokeWithLayerRequest(
|
||||
layer=layer, query=query))
|
||||
|
||||
# We're only interested in the DC options,
|
||||
# although many other options are available!
|
||||
|
@ -130,7 +129,8 @@ class TelegramClient:
|
|||
def reconnect_to_dc(self, dc_id):
|
||||
"""Reconnects to the specified DC ID. This is automatically called after an InvalidDCError is raised"""
|
||||
if self.dc_options is None or not self.dc_options:
|
||||
raise ConnectionError("Can't reconnect. Stabilise an initial connection first.")
|
||||
raise ConnectionError(
|
||||
"Can't reconnect. Stabilise an initial connection first.")
|
||||
|
||||
dc = next(dc for dc in self.dc_options if dc.id == dc_id)
|
||||
|
||||
|
@ -191,11 +191,13 @@ class TelegramClient:
|
|||
with `.password_required = True` was raised"""
|
||||
if phone_number and code:
|
||||
if phone_number not in self.phone_code_hashes:
|
||||
raise ValueError('Please make sure you have called send_code_request first.')
|
||||
raise ValueError(
|
||||
'Please make sure you have called send_code_request first.')
|
||||
|
||||
try:
|
||||
result = self.invoke(SignInRequest(
|
||||
phone_number, self.phone_code_hashes[phone_number], code))
|
||||
result = self.invoke(
|
||||
SignInRequest(phone_number, self.phone_code_hashes[
|
||||
phone_number], code))
|
||||
|
||||
except RPCError as error:
|
||||
if error.message.startswith('PHONE_CODE_'):
|
||||
|
@ -205,9 +207,11 @@ class TelegramClient:
|
|||
raise error
|
||||
elif password:
|
||||
salt = self.invoke(GetPasswordRequest()).current_salt
|
||||
result = self.invoke(CheckPasswordRequest(utils.get_password_hash(password, salt)))
|
||||
result = self.invoke(
|
||||
CheckPasswordRequest(utils.get_password_hash(password, salt)))
|
||||
else:
|
||||
raise ValueError('You must provide a phone_number and a code for the first time, '
|
||||
raise ValueError(
|
||||
'You must provide a phone_number and a code for the first time, '
|
||||
'and a password only if an RPCError was raised before.')
|
||||
|
||||
# Result is an Auth.Authorization TLObject
|
||||
|
@ -221,7 +225,9 @@ class TelegramClient:
|
|||
|
||||
def sign_up(self, phone_number, code, first_name, last_name=''):
|
||||
"""Signs up to Telegram. Make sure you sent a code request first!"""
|
||||
result = self.invoke(SignUpRequest(phone_number=phone_number,
|
||||
result = self.invoke(
|
||||
SignUpRequest(
|
||||
phone_number=phone_number,
|
||||
phone_code_hash=self.phone_code_hashes[phone_number],
|
||||
phone_code=code,
|
||||
first_name=first_name,
|
||||
|
@ -245,29 +251,41 @@ class TelegramClient:
|
|||
def list_sessions():
|
||||
"""Lists all the sessions of the users who have ever connected
|
||||
using this client and never logged out"""
|
||||
return [path.splitext(path.basename(f))[0] # splitext = split ext (not spli text!)
|
||||
return [path.splitext(path.basename(f))[
|
||||
0] # splitext = split ext (not spli text!)
|
||||
for f in listdir('.') if f.endswith('.session')]
|
||||
|
||||
# endregion
|
||||
|
||||
# region Dialogs ("chats") requests
|
||||
|
||||
def get_dialogs(self, count=10, offset_date=None, offset_id=0, offset_peer=InputPeerEmpty()):
|
||||
def get_dialogs(self,
|
||||
count=10,
|
||||
offset_date=None,
|
||||
offset_id=0,
|
||||
offset_peer=InputPeerEmpty()):
|
||||
"""Returns a tuple of lists ([dialogs], [entities]) with 'count' items each.
|
||||
The `entity` represents the user, chat or channel corresponding to that dialog"""
|
||||
|
||||
r = self.invoke(GetDialogsRequest(offset_date=offset_date,
|
||||
r = self.invoke(
|
||||
GetDialogsRequest(
|
||||
offset_date=offset_date,
|
||||
offset_id=offset_id,
|
||||
offset_peer=offset_peer,
|
||||
limit=count))
|
||||
return (r.dialogs,
|
||||
return (
|
||||
r.dialogs,
|
||||
[find_user_or_chat(d.peer, r.users, r.chats) for d in r.dialogs])
|
||||
|
||||
# endregion
|
||||
|
||||
# region Message requests
|
||||
|
||||
def send_message(self, input_peer, message, markdown=False, no_web_page=False):
|
||||
def send_message(self,
|
||||
input_peer,
|
||||
message,
|
||||
markdown=False,
|
||||
no_web_page=False):
|
||||
"""Sends a message to the given input peer and returns the sent message ID"""
|
||||
if markdown:
|
||||
msg, entities = parse_message_entities(message)
|
||||
|
@ -275,15 +293,23 @@ class TelegramClient:
|
|||
msg, entities = message, []
|
||||
|
||||
msg_id = utils.generate_random_long()
|
||||
self.invoke(SendMessageRequest(peer=input_peer,
|
||||
self.invoke(
|
||||
SendMessageRequest(
|
||||
peer=input_peer,
|
||||
message=msg,
|
||||
random_id=msg_id,
|
||||
entities=entities,
|
||||
no_webpage=no_web_page))
|
||||
return msg_id
|
||||
|
||||
def get_message_history(self, input_peer, limit=20,
|
||||
offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0):
|
||||
def get_message_history(self,
|
||||
input_peer,
|
||||
limit=20,
|
||||
offset_date=None,
|
||||
offset_id=0,
|
||||
max_id=0,
|
||||
min_id=0,
|
||||
add_offset=0):
|
||||
"""
|
||||
Gets the message history for the specified InputPeer
|
||||
|
||||
|
@ -298,7 +324,9 @@ class TelegramClient:
|
|||
:return: A tuple containing total message count and two more lists ([messages], [senders]).
|
||||
Note that the sender can be null if it was not found!
|
||||
"""
|
||||
result = self.invoke(GetHistoryRequest(input_peer,
|
||||
result = self.invoke(
|
||||
GetHistoryRequest(
|
||||
input_peer,
|
||||
limit=limit,
|
||||
offset_date=offset_date,
|
||||
offset_id=offset_id,
|
||||
|
@ -331,7 +359,8 @@ class TelegramClient:
|
|||
Returns an AffectedMessages TLObject"""
|
||||
if max_id is None:
|
||||
if not messages:
|
||||
raise InvalidParameterError('Either a message list or a max_id must be provided.')
|
||||
raise InvalidParameterError(
|
||||
'Either a message list or a max_id must be provided.')
|
||||
|
||||
if isinstance(messages, list):
|
||||
max_id = max(msg.id for msg in messages)
|
||||
|
@ -347,7 +376,11 @@ class TelegramClient:
|
|||
# be handled through a separate session and a separate connection"
|
||||
# region Uploading media requests
|
||||
|
||||
def upload_file(self, file_path, part_size_kb=None, file_name=None, progress_callback=None):
|
||||
def upload_file(self,
|
||||
file_path,
|
||||
part_size_kb=None,
|
||||
file_name=None,
|
||||
progress_callback=None):
|
||||
"""Uploads the specified file_path and returns a handle which can be later used
|
||||
|
||||
:param file_path: The file path of the file that will be uploaded
|
||||
|
@ -375,7 +408,7 @@ class TelegramClient:
|
|||
|
||||
# Multiply the datetime timestamp by 10^6 to get the ticks
|
||||
# This is high likely going to be unique
|
||||
file_id = int(datetime.now().timestamp() * (10 ** 6))
|
||||
file_id = int(datetime.now().timestamp() * (10**6))
|
||||
hash_md5 = md5()
|
||||
|
||||
with open(file_path, 'rb') as file:
|
||||
|
@ -386,7 +419,8 @@ class TelegramClient:
|
|||
# The SavePartRequest is different depending on whether
|
||||
# the file is too large or not (over or less than 10MB)
|
||||
if is_large:
|
||||
request = SaveBigFilePartRequest(file_id, part_index, part_count, part)
|
||||
request = SaveBigFilePartRequest(file_id, part_index,
|
||||
part_count, part)
|
||||
else:
|
||||
request = SaveFilePartRequest(file_id, part_index, part)
|
||||
|
||||
|
@ -397,14 +431,16 @@ class TelegramClient:
|
|||
if progress_callback:
|
||||
progress_callback(file.tell(), file_size)
|
||||
else:
|
||||
raise ValueError('Could not upload file part #{}'.format(part_index))
|
||||
raise ValueError('Could not upload file part #{}'.format(
|
||||
part_index))
|
||||
|
||||
# Set a default file name if None was specified
|
||||
if not file_name:
|
||||
file_name = path.basename(file_path)
|
||||
|
||||
# After the file has been uploaded, we can return a handle pointing to it
|
||||
return InputFile(id=file_id,
|
||||
return InputFile(
|
||||
id=file_id,
|
||||
parts=part_count,
|
||||
name=file_name,
|
||||
md5_checksum=hash_md5.hexdigest())
|
||||
|
@ -431,14 +467,19 @@ class TelegramClient:
|
|||
# «The "octet-stream" subtype is used to indicate that a body contains arbitrary binary data.»
|
||||
if not mime_type:
|
||||
mime_type = 'application/octet-stream'
|
||||
self.send_media_file(InputMediaUploadedDocument(file=input_file,
|
||||
self.send_media_file(
|
||||
InputMediaUploadedDocument(
|
||||
file=input_file,
|
||||
mime_type=mime_type,
|
||||
attributes=attributes,
|
||||
caption=caption), input_peer)
|
||||
caption=caption),
|
||||
input_peer)
|
||||
|
||||
def send_media_file(self, input_media, input_peer):
|
||||
"""Sends any input_media (contact, document, photo...) to an input_peer"""
|
||||
self.invoke(SendMediaRequest(peer=input_peer,
|
||||
self.invoke(
|
||||
SendMediaRequest(
|
||||
peer=input_peer,
|
||||
media=input_media,
|
||||
random_id=utils.generate_random_long()))
|
||||
|
||||
|
@ -446,8 +487,11 @@ class TelegramClient:
|
|||
|
||||
# region Downloading media requests
|
||||
|
||||
def download_profile_photo(self, profile_photo, file_path,
|
||||
add_extension=True, download_big=True):
|
||||
def download_profile_photo(self,
|
||||
profile_photo,
|
||||
file_path,
|
||||
add_extension=True,
|
||||
download_big=True):
|
||||
"""Downloads the profile photo for an user or a chat (including channels).
|
||||
Returns False if no photo was providen, or if it was Empty"""
|
||||
|
||||
|
@ -465,28 +509,40 @@ class TelegramClient:
|
|||
photo_location = profile_photo.photo_small
|
||||
|
||||
# Download the media with the largest size input file location
|
||||
self.download_file_loc(InputFileLocation(volume_id=photo_location.volume_id,
|
||||
self.download_file_loc(
|
||||
InputFileLocation(
|
||||
volume_id=photo_location.volume_id,
|
||||
local_id=photo_location.local_id,
|
||||
secret=photo_location.secret),
|
||||
file_path)
|
||||
return True
|
||||
|
||||
def download_msg_media(self, message_media, file_path, add_extension=True, progress_callback=None):
|
||||
def download_msg_media(self,
|
||||
message_media,
|
||||
file_path,
|
||||
add_extension=True,
|
||||
progress_callback=None):
|
||||
"""Downloads the given MessageMedia (Photo, Document or Contact)
|
||||
into the desired file_path, optionally finding its extension automatically
|
||||
The progress_callback should be a callback function which takes two parameters,
|
||||
uploaded size (in bytes) and total file size (in bytes).
|
||||
This will be called every time a part is downloaded"""
|
||||
if type(message_media) == MessageMediaPhoto:
|
||||
return self.download_photo(message_media, file_path, add_extension, progress_callback)
|
||||
return self.download_photo(message_media, file_path, add_extension,
|
||||
progress_callback)
|
||||
|
||||
elif type(message_media) == MessageMediaDocument:
|
||||
return self.download_document(message_media, file_path, add_extension, progress_callback)
|
||||
return self.download_document(message_media, file_path,
|
||||
add_extension, progress_callback)
|
||||
|
||||
elif type(message_media) == MessageMediaContact:
|
||||
return self.download_contact(message_media, file_path, add_extension)
|
||||
return self.download_contact(message_media, file_path,
|
||||
add_extension)
|
||||
|
||||
def download_photo(self, message_media_photo, file_path, add_extension=False,
|
||||
def download_photo(self,
|
||||
message_media_photo,
|
||||
file_path,
|
||||
add_extension=False,
|
||||
progress_callback=None):
|
||||
"""Downloads MessageMediaPhoto's largest size into the desired
|
||||
file_path, optionally finding its extension automatically
|
||||
|
@ -504,13 +560,20 @@ class TelegramClient:
|
|||
file_path += get_extension(message_media_photo)
|
||||
|
||||
# Download the media with the largest size input file location
|
||||
self.download_file_loc(InputFileLocation(volume_id=largest_size.volume_id,
|
||||
self.download_file_loc(
|
||||
InputFileLocation(
|
||||
volume_id=largest_size.volume_id,
|
||||
local_id=largest_size.local_id,
|
||||
secret=largest_size.secret),
|
||||
file_path, file_size=file_size, progress_callback=progress_callback)
|
||||
file_path,
|
||||
file_size=file_size,
|
||||
progress_callback=progress_callback)
|
||||
return file_path
|
||||
|
||||
def download_document(self, message_media_document, file_path=None, add_extension=True,
|
||||
def download_document(self,
|
||||
message_media_document,
|
||||
file_path=None,
|
||||
add_extension=True,
|
||||
progress_callback=None):
|
||||
"""Downloads the given MessageMediaDocument into the desired
|
||||
file_path, optionally finding its extension automatically.
|
||||
|
@ -537,10 +600,14 @@ class TelegramClient:
|
|||
if add_extension:
|
||||
file_path += get_extension(document.mime_type)
|
||||
|
||||
self.download_file_loc(InputDocumentFileLocation(id=document.id,
|
||||
self.download_file_loc(
|
||||
InputDocumentFileLocation(
|
||||
id=document.id,
|
||||
access_hash=document.access_hash,
|
||||
version=document.version),
|
||||
file_path, file_size=file_size, progress_callback=progress_callback)
|
||||
file_path,
|
||||
file_size=file_size,
|
||||
progress_callback=progress_callback)
|
||||
return file_path
|
||||
|
||||
@staticmethod
|
||||
|
@ -562,15 +629,21 @@ class TelegramClient:
|
|||
with open(file_path, 'w', encoding='utf-8') as file:
|
||||
file.write('BEGIN:VCARD\n')
|
||||
file.write('VERSION:4.0\n')
|
||||
file.write('N:{};{};;;\n'.format(first_name, last_name if last_name else ''))
|
||||
file.write('N:{};{};;;\n'.format(first_name, last_name
|
||||
if last_name else ''))
|
||||
file.write('FN:{}\n'.format(' '.join((first_name, last_name))))
|
||||
file.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(phone_number))
|
||||
file.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(
|
||||
phone_number))
|
||||
file.write('END:VCARD\n')
|
||||
|
||||
return file_path
|
||||
|
||||
def download_file_loc(self, input_location, file_path, part_size_kb=64,
|
||||
file_size=None, progress_callback=None):
|
||||
def download_file_loc(self,
|
||||
input_location,
|
||||
file_path,
|
||||
part_size_kb=64,
|
||||
file_size=None,
|
||||
progress_callback=None):
|
||||
"""Downloads media from the given input_file_location to the specified file_path.
|
||||
If a progress_callback function is given, it will be called taking two
|
||||
arguments (downloaded bytes count and total file size)"""
|
||||
|
@ -594,7 +667,8 @@ class TelegramClient:
|
|||
while True:
|
||||
# The current offset equals the offset_index multiplied by the part size
|
||||
offset = offset_index * part_size
|
||||
result = self.invoke(GetFileRequest(input_location, offset, part_size))
|
||||
result = self.invoke(
|
||||
GetFileRequest(input_location, offset, part_size))
|
||||
offset_index += 1
|
||||
|
||||
# If we have received no data (0 bytes), the file is over
|
||||
|
@ -616,7 +690,8 @@ class TelegramClient:
|
|||
"""Adds an update handler (a function which takes a TLObject,
|
||||
an update, as its parameter) and listens for updates"""
|
||||
if not self.signed_in:
|
||||
raise ValueError("You cannot add update handlers until you've signed in.")
|
||||
raise ValueError(
|
||||
"You cannot add update handlers until you've signed in.")
|
||||
|
||||
self.sender.add_update_handler(handler)
|
||||
|
||||
|
|
|
@ -26,7 +26,8 @@ class MTProtoRequest:
|
|||
self.confirm_received = True
|
||||
|
||||
def need_resend(self):
|
||||
return self.dirty or (self.confirmed and not self.confirm_received and
|
||||
return self.dirty or (
|
||||
self.confirmed and not self.confirm_received and
|
||||
datetime.now() - self.send_time > timedelta(seconds=3))
|
||||
|
||||
# These should be overrode
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
from os.path import isfile as file_exists
|
||||
import os
|
||||
import time
|
||||
import pickle
|
||||
import random
|
||||
import time
|
||||
from os.path import isfile as file_exists
|
||||
|
||||
import telethon.helpers as utils
|
||||
|
||||
|
||||
|
@ -39,12 +40,11 @@ class Session:
|
|||
If the given session_user_id is None, we assume that it is for testing purposes"""
|
||||
if session_user_id is None:
|
||||
return Session(None)
|
||||
|
||||
else:
|
||||
filepath = '{}.session'.format(session_user_id)
|
||||
path = '{}.session'.format(session_user_id)
|
||||
|
||||
if file_exists(filepath):
|
||||
with open(filepath, 'rb') as file:
|
||||
if file_exists(path):
|
||||
with open(path, 'rb') as file:
|
||||
return pickle.load(file)
|
||||
else:
|
||||
return Session(session_user_id)
|
||||
|
@ -53,9 +53,12 @@ class Session:
|
|||
"""Generates a new message ID based on the current time (in ms) since epoch"""
|
||||
# Refer to mtproto_plain_sender.py for the original method, this is a simple copy
|
||||
ms_time = int(time.time() * 1000)
|
||||
new_msg_id = (((ms_time // 1000 + self.time_offset) << 32) | # "must approximately equal unixtime*2^32"
|
||||
((ms_time % 1000) << 22) | # "approximate moment in time the message was created"
|
||||
random.randint(0, 524288) << 2) # "message identifiers are divisible by 4"
|
||||
new_msg_id = (((ms_time // 1000 + self.time_offset) << 32)
|
||||
| # "must approximately equal unixtime*2^32"
|
||||
((ms_time % 1000) << 22)
|
||||
| # "approximate moment in time the message was created"
|
||||
random.randint(0, 524288)
|
||||
<< 2) # "message identifiers are divisible by 4"
|
||||
|
||||
if self.last_message_id >= new_msg_id:
|
||||
new_msg_id = self.last_message_id + 4
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from datetime import datetime
|
||||
from io import BytesIO, BufferedReader
|
||||
from telethon.tl.all_tlobjects import tlobjects
|
||||
from struct import unpack
|
||||
from telethon.errors import *
|
||||
import inspect
|
||||
import os
|
||||
from datetime import datetime
|
||||
from io import BufferedReader, BytesIO
|
||||
from struct import unpack
|
||||
|
||||
from telethon.errors import *
|
||||
from telethon.tl.all_tlobjects import tlobjects
|
||||
|
||||
|
||||
class BinaryReader:
|
||||
|
@ -12,13 +12,15 @@ class BinaryReader:
|
|||
Small utility class to read binary data.
|
||||
Also creates a "Memory Stream" if necessary
|
||||
"""
|
||||
|
||||
def __init__(self, data=None, stream=None):
|
||||
if data:
|
||||
self.stream = BytesIO(data)
|
||||
elif stream:
|
||||
self.stream = stream
|
||||
else:
|
||||
raise InvalidParameterError("Either bytes or a stream must be provided")
|
||||
raise InvalidParameterError(
|
||||
'Either bytes or a stream must be provided')
|
||||
|
||||
self.reader = BufferedReader(self.stream)
|
||||
|
||||
|
@ -47,13 +49,15 @@ class BinaryReader:
|
|||
|
||||
def read_large_int(self, bits, signed=True):
|
||||
"""Reads a n-bits long integer value"""
|
||||
return int.from_bytes(self.read(bits // 8), byteorder='little', signed=signed)
|
||||
return int.from_bytes(
|
||||
self.read(bits // 8), byteorder='little', signed=signed)
|
||||
|
||||
def read(self, length):
|
||||
"""Read the given amount of bytes"""
|
||||
result = self.reader.read(length)
|
||||
if len(result) != length:
|
||||
raise BufferError('Trying to read outside the data bounds (no more data left to read)')
|
||||
raise BufferError(
|
||||
'Trying to read outside the data bounds (no more data left to read)')
|
||||
|
||||
return result
|
||||
|
||||
|
@ -69,7 +73,8 @@ class BinaryReader:
|
|||
"""Reads a Telegram-encoded byte array, without the need of specifying its length"""
|
||||
first_byte = self.read_byte()
|
||||
if first_byte == 254:
|
||||
length = self.read_byte() | (self.read_byte() << 8) | (self.read_byte() << 16)
|
||||
length = self.read_byte() | (self.read_byte() << 8) | (
|
||||
self.read_byte() << 16)
|
||||
padding = length % 4
|
||||
else:
|
||||
length = first_byte
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from io import BytesIO, BufferedWriter
|
||||
from io import BufferedWriter, BytesIO
|
||||
from struct import pack
|
||||
|
||||
|
||||
|
@ -26,12 +26,16 @@ class BinaryWriter:
|
|||
|
||||
def write_int(self, value, signed=True):
|
||||
"""Writes an integer value (4 bytes), which can or cannot be signed"""
|
||||
self.writer.write(int.to_bytes(value, length=4, byteorder='little', signed=signed))
|
||||
self.writer.write(
|
||||
int.to_bytes(
|
||||
value, length=4, byteorder='little', signed=signed))
|
||||
self.written_count += 4
|
||||
|
||||
def write_long(self, value, signed=True):
|
||||
"""Writes a long integer value (8 bytes), which can or cannot be signed"""
|
||||
self.writer.write(int.to_bytes(value, length=8, byteorder='little', signed=signed))
|
||||
self.writer.write(
|
||||
int.to_bytes(
|
||||
value, length=8, byteorder='little', signed=signed))
|
||||
self.written_count += 8
|
||||
|
||||
def write_float(self, value):
|
||||
|
@ -46,7 +50,9 @@ class BinaryWriter:
|
|||
|
||||
def write_large_int(self, value, bits, signed=True):
|
||||
"""Writes a n-bits long integer value"""
|
||||
self.writer.write(int.to_bytes(value, length=bits // 8, byteorder='little', signed=signed))
|
||||
self.writer.write(
|
||||
int.to_bytes(
|
||||
value, length=bits // 8, byteorder='little', signed=signed))
|
||||
self.written_count += bits // 8
|
||||
|
||||
def write(self, data):
|
||||
|
|
|
@ -4,12 +4,10 @@
|
|||
after all, both are the same attribute, IDs."""
|
||||
from mimetypes import add_type, guess_extension
|
||||
|
||||
from telethon.tl.types import \
|
||||
User, Chat, Channel, \
|
||||
PeerUser, PeerChat, PeerChannel, \
|
||||
InputPeerUser, InputPeerChat, InputPeerChannel, \
|
||||
UserProfilePhoto, ChatPhoto, \
|
||||
MessageMediaPhoto, MessageMediaDocument
|
||||
from telethon.tl.types import (
|
||||
Channel, Chat, ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser,
|
||||
MessageMediaDocument, MessageMediaPhoto, PeerChannel, PeerChat, PeerUser,
|
||||
User, UserProfilePhoto)
|
||||
|
||||
|
||||
def get_display_name(entity):
|
||||
|
@ -31,8 +29,7 @@ def get_extension(media):
|
|||
"""Gets the corresponding extension for any Telegram media"""
|
||||
|
||||
# Photos are always compressed as .jpg by Telegram
|
||||
if (isinstance(media, UserProfilePhoto) or
|
||||
isinstance(media, ChatPhoto) or
|
||||
if (isinstance(media, UserProfilePhoto) or isinstance(media, ChatPhoto) or
|
||||
isinstance(media, MessageMediaPhoto)):
|
||||
return '.jpg'
|
||||
|
||||
|
|
|
@ -18,7 +18,8 @@ class SourceBuilder:
|
|||
"""Writes a string into the source code, applying indentation if required"""
|
||||
if self.on_new_line:
|
||||
self.on_new_line = False # We're not on a new line anymore
|
||||
if string.strip(): # If the string was not empty, indent; Else it probably was a new line
|
||||
if string.strip(
|
||||
): # If the string was not empty, indent; Else it probably was a new line
|
||||
self.indent()
|
||||
|
||||
self.out_stream.write(string)
|
||||
|
|
|
@ -5,13 +5,13 @@ class TLObject:
|
|||
""".tl core types IDs (such as vector, booleans, etc.)"""
|
||||
CORE_TYPES = (0x1cb5c415, 0xbc799737, 0x997275b5, 0x3fedd339)
|
||||
|
||||
def __init__(self, fullname, id, args, result, is_function):
|
||||
def __init__(self, fullname, object_id, args, result, is_function):
|
||||
"""
|
||||
Initializes a new TLObject, given its properties.
|
||||
Usually, this will be called from `from_tl` instead
|
||||
:param fullname: The fullname of the TL object (namespace.name)
|
||||
The namespace can be omitted
|
||||
:param id: The hexadecimal string representing the object ID
|
||||
:param object_id: The hexadecimal string representing the object ID
|
||||
:param args: The arguments, if any, of the TL object
|
||||
:param result: The result type of the TL object
|
||||
:param is_function: Is the object a function or a type?
|
||||
|
@ -25,7 +25,7 @@ class TLObject:
|
|||
self.name = fullname
|
||||
|
||||
# The ID should be an hexadecimal string
|
||||
self.id = int(id, base=16)
|
||||
self.id = int(object_id, base=16)
|
||||
self.args = args
|
||||
self.result = result
|
||||
self.is_function = is_function
|
||||
|
@ -67,11 +67,13 @@ class TLObject:
|
|||
''', tl, re.IGNORECASE | re.VERBOSE)
|
||||
|
||||
# Retrieve the matched arguments
|
||||
args = [TLArg(name, type, brace != '') for brace, name, type, _ in args_match]
|
||||
args = [TLArg(name, arg_type, brace != '')
|
||||
for brace, name, arg_type, _ in args_match]
|
||||
|
||||
# And initialize the TLObject
|
||||
return TLObject(fullname=match.group(1),
|
||||
id=match.group(2),
|
||||
return TLObject(
|
||||
fullname=match.group(1),
|
||||
object_id=match.group(2),
|
||||
args=args,
|
||||
result=match.group(3),
|
||||
is_function=is_function)
|
||||
|
@ -82,19 +84,19 @@ class TLObject:
|
|||
return self.id in TLObject.CORE_TYPES
|
||||
|
||||
def __repr__(self):
|
||||
fullname = ('{}.{}'.format(self.namespace, self.name) if self.namespace is not None
|
||||
else self.name)
|
||||
fullname = ('{}.{}'.format(self.namespace, self.name)
|
||||
if self.namespace is not None else self.name)
|
||||
|
||||
hex_id = hex(self.id)[2:].rjust(8, '0') # Skip 0x and add 0's for padding
|
||||
hex_id = hex(self.id)[2:].rjust(8,
|
||||
'0') # Skip 0x and add 0's for padding
|
||||
|
||||
return '{}#{} {} = {}'.format(fullname,
|
||||
hex_id,
|
||||
' '.join([str(arg) for arg in self.args]),
|
||||
return '{}#{} {} = {}'.format(
|
||||
fullname, hex_id, ' '.join([str(arg) for arg in self.args]),
|
||||
self.result)
|
||||
|
||||
def __str__(self):
|
||||
fullname = ('{}.{}'.format(self.namespace, self.name) if self.namespace is not None
|
||||
else self.name)
|
||||
fullname = ('{}.{}'.format(self.namespace, self.name)
|
||||
if self.namespace is not None else self.name)
|
||||
|
||||
# Some arguments are not valid for being represented, such as the flag indicator or generic definition
|
||||
# (these have no explicit values until used)
|
||||
|
@ -104,20 +106,21 @@ class TLObject:
|
|||
args = ', '.join(['{}={{}}'.format(arg.name) for arg in valid_args])
|
||||
|
||||
# Since Python's default representation for lists is using repr(), we need to str() manually on every item
|
||||
args_format = ', '.join(['str(self.{})'.format(arg.name) if not arg.is_vector else
|
||||
'None if not self.{0} else [str(_) for _ in self.{0}]'.format(arg.name)
|
||||
for arg in valid_args])
|
||||
args_format = ', '.join(
|
||||
['str(self.{})'.format(arg.name) if not arg.is_vector else
|
||||
'None if not self.{0} else [str(_) for _ in self.{0}]'.format(
|
||||
arg.name) for arg in valid_args])
|
||||
|
||||
return ("'({} (ID: {}) = ({}))'.format({})"
|
||||
.format(fullname, hex(self.id), args, args_format))
|
||||
|
||||
|
||||
class TLArg:
|
||||
def __init__(self, name, type, generic_definition):
|
||||
def __init__(self, name, arg_type, generic_definition):
|
||||
"""
|
||||
Initializes a new .tl argument
|
||||
:param name: The name of the .tl argument
|
||||
:param type: The type of the .tl argument
|
||||
:param arg_type: The type of the .tl argument
|
||||
:param generic_definition: Is the argument a generic definition?
|
||||
(i.e. {X:Type})
|
||||
"""
|
||||
|
@ -132,14 +135,15 @@ class TLArg:
|
|||
self.flag_index = -1
|
||||
|
||||
# The type can be an indicator that other arguments will be flags
|
||||
if type == '#':
|
||||
if arg_type == '#':
|
||||
self.flag_indicator = True
|
||||
self.type = None
|
||||
self.is_generic = False
|
||||
else:
|
||||
self.flag_indicator = False
|
||||
self.is_generic = type.startswith('!')
|
||||
self.type = type.lstrip('!') # Strip the exclamation mark always to have only the name
|
||||
self.is_generic = arg_type.startswith('!')
|
||||
self.type = arg_type.lstrip(
|
||||
'!') # Strip the exclamation mark always to have only the name
|
||||
|
||||
# The type may be a flag (flags.IDX?REAL_TYPE)
|
||||
# Note that «flags» is NOT the flags name; this is determined by a previous argument
|
||||
|
@ -148,13 +152,15 @@ class TLArg:
|
|||
if flag_match:
|
||||
self.is_flag = True
|
||||
self.flag_index = int(flag_match.group(1))
|
||||
self.type = flag_match.group(2) # Update the type to match the exact type, not the "flagged" one
|
||||
self.type = flag_match.group(
|
||||
2) # Update the type to match the exact type, not the "flagged" one
|
||||
|
||||
# Then check if the type is a Vector<REAL_TYPE>
|
||||
vector_match = re.match(r'vector<(\w+)>', self.type, re.IGNORECASE)
|
||||
if vector_match:
|
||||
self.is_vector = True
|
||||
self.type = vector_match.group(1) # Update the type to match the one inside the vector
|
||||
self.type = vector_match.group(
|
||||
1) # Update the type to match the one inside the vector
|
||||
|
||||
# The name may contain "date" in it, if this is the case and the type is "int",
|
||||
# we can safely assume that this should be treated as a "date" object.
|
||||
|
|
|
@ -2,7 +2,7 @@ import os
|
|||
import re
|
||||
import shutil
|
||||
|
||||
from parser import SourceBuilder, TLParser
|
||||
from .parser import SourceBuilder, TLParser
|
||||
|
||||
|
||||
def get_output_path(normal_path):
|
||||
|
@ -60,8 +60,8 @@ class TLGenerator:
|
|||
continue
|
||||
|
||||
# Determine the output directory and create it
|
||||
out_dir = get_output_path('functions' if tlobject.is_function
|
||||
else 'types')
|
||||
out_dir = get_output_path('functions'
|
||||
if tlobject.is_function else 'types')
|
||||
|
||||
if tlobject.namespace:
|
||||
out_dir = os.path.join(out_dir, tlobject.namespace)
|
||||
|
@ -77,39 +77,51 @@ class TLGenerator:
|
|||
TLGenerator.get_class_name(tlobject)))
|
||||
|
||||
# Create the file for this TLObject
|
||||
filename = os.path.join(out_dir, TLGenerator.get_file_name(tlobject, add_extension=True))
|
||||
filename = os.path.join(
|
||||
out_dir,
|
||||
TLGenerator.get_file_name(
|
||||
tlobject, add_extension=True))
|
||||
with open(filename, 'w', encoding='utf-8') as file:
|
||||
# Let's build the source code!
|
||||
with SourceBuilder(file) as builder:
|
||||
# Both types and functions inherit from MTProtoRequest so they all can be sent
|
||||
builder.writeln('from telethon.tl.mtproto_request import MTProtoRequest')
|
||||
builder.writeln(
|
||||
'from telethon.tl.mtproto_request import MTProtoRequest')
|
||||
builder.writeln()
|
||||
builder.writeln()
|
||||
builder.writeln('class {}(MTProtoRequest):'.format(TLGenerator.get_class_name(tlobject)))
|
||||
builder.writeln('class {}(MTProtoRequest):'.format(
|
||||
TLGenerator.get_class_name(tlobject)))
|
||||
|
||||
# Write the original .tl definition, along with a "generated automatically" message
|
||||
builder.writeln('"""Class generated by TLObjects\' generator. '
|
||||
builder.writeln(
|
||||
'"""Class generated by TLObjects\' generator. '
|
||||
'All changes will be ERASED. Original .tl definition below.')
|
||||
builder.writeln('{}"""'.format(repr(tlobject)))
|
||||
builder.writeln()
|
||||
|
||||
# Create an class-level variable that stores the TLObject's constructor ID
|
||||
builder.writeln("# Telegram's constructor ID (and unique identifier) for this class")
|
||||
builder.writeln('constructor_id = {}'.format(hex(tlobject.id)))
|
||||
builder.writeln(
|
||||
"# Telegram's constructor ID (and unique identifier) for this class")
|
||||
builder.writeln('constructor_id = {}'.format(
|
||||
hex(tlobject.id)))
|
||||
builder.writeln()
|
||||
|
||||
# First sort the arguments so that those not being a flag come first
|
||||
args = sorted([arg for arg in tlobject.args if not arg.flag_indicator],
|
||||
args = sorted(
|
||||
[arg for arg in tlobject.args
|
||||
if not arg.flag_indicator],
|
||||
key=lambda x: x.is_flag)
|
||||
|
||||
# Then convert the args to string parameters, the flags having =None
|
||||
args = [(arg.name if not arg.is_flag
|
||||
else '{}=None'.format(arg.name)) for arg in args
|
||||
if not arg.flag_indicator and not arg.generic_definition]
|
||||
args = [(arg.name if not arg.is_flag else
|
||||
'{}=None'.format(arg.name)) for arg in args
|
||||
if not arg.flag_indicator and
|
||||
not arg.generic_definition]
|
||||
|
||||
# Write the __init__ function
|
||||
if args:
|
||||
builder.writeln('def __init__(self, {}):'.format(', '.join(args)))
|
||||
builder.writeln('def __init__(self, {}):'.format(
|
||||
', '.join(args)))
|
||||
else:
|
||||
builder.writeln('def __init__(self):')
|
||||
|
||||
|
@ -117,18 +129,23 @@ class TLGenerator:
|
|||
# those which are generated automatically: flag indicator and generic definitions.
|
||||
# We don't need the generic definitions in Python because arguments can be any type
|
||||
args = [arg for arg in tlobject.args
|
||||
if not arg.flag_indicator and not arg.generic_definition]
|
||||
if not arg.flag_indicator and
|
||||
not arg.generic_definition]
|
||||
|
||||
if args:
|
||||
# Write the docstring, so we know the type of the arguments
|
||||
builder.writeln('"""')
|
||||
for arg in args:
|
||||
if not arg.flag_indicator:
|
||||
builder.write(':param {}: Telegram type: «{}».'.format(arg.name, arg.type))
|
||||
builder.write(
|
||||
':param {}: Telegram type: «{}».'.format(
|
||||
arg.name, arg.type))
|
||||
if arg.is_vector:
|
||||
builder.write(' Must be a list.'.format(arg.name))
|
||||
builder.write(' Must be a list.'.format(
|
||||
arg.name))
|
||||
if arg.is_generic:
|
||||
builder.write(' This should be another MTProtoRequest.')
|
||||
builder.write(
|
||||
' This should be another MTProtoRequest.')
|
||||
builder.writeln()
|
||||
builder.writeln('"""')
|
||||
|
||||
|
@ -136,7 +153,8 @@ class TLGenerator:
|
|||
# Functions have a result object and are confirmed by default
|
||||
if tlobject.is_function:
|
||||
builder.writeln('self.result = None')
|
||||
builder.writeln('self.confirmed = True # Confirmed by default')
|
||||
builder.writeln(
|
||||
'self.confirmed = True # Confirmed by default')
|
||||
|
||||
# Set the arguments
|
||||
if args:
|
||||
|
@ -148,22 +166,24 @@ class TLGenerator:
|
|||
|
||||
# Write the on_send(self, writer) function
|
||||
builder.writeln('def on_send(self, writer):')
|
||||
builder.writeln('writer.write_int({}.constructor_id, signed=False)'
|
||||
builder.writeln(
|
||||
'writer.write_int({}.constructor_id, signed=False)'
|
||||
.format(TLGenerator.get_class_name(tlobject)))
|
||||
|
||||
for arg in tlobject.args:
|
||||
TLGenerator.write_onsend_code(builder, arg, tlobject.args)
|
||||
TLGenerator.write_onsend_code(builder, arg,
|
||||
tlobject.args)
|
||||
builder.end_block()
|
||||
|
||||
# Write the empty() function, which returns an "empty"
|
||||
# instance, in which all attributes are set to None
|
||||
builder.writeln('@staticmethod')
|
||||
builder.writeln('def empty():')
|
||||
builder.writeln('"""Returns an "empty" instance (all attributes are None)"""')
|
||||
builder.writeln(
|
||||
'"""Returns an "empty" instance (all attributes are None)"""')
|
||||
builder.writeln('return {}({})'.format(
|
||||
TLGenerator.get_class_name(tlobject),
|
||||
', '.join('None' for _ in range(len(args)))
|
||||
))
|
||||
TLGenerator.get_class_name(tlobject), ', '.join(
|
||||
'None' for _ in range(len(args)))))
|
||||
builder.end_block()
|
||||
|
||||
# Write the on_response(self, reader) function
|
||||
|
@ -174,7 +194,8 @@ class TLGenerator:
|
|||
else:
|
||||
if tlobject.args:
|
||||
for arg in tlobject.args:
|
||||
TLGenerator.write_onresponse_code(builder, arg, tlobject.args)
|
||||
TLGenerator.write_onresponse_code(
|
||||
builder, arg, tlobject.args)
|
||||
else:
|
||||
# If there were no arguments, we still need an on_response method, and hence "pass" if empty
|
||||
builder.writeln('pass')
|
||||
|
@ -186,23 +207,26 @@ class TLGenerator:
|
|||
builder.end_block()
|
||||
|
||||
builder.writeln('def __str__(self):')
|
||||
builder.writeln("return {}".format(str(tlobject)))
|
||||
builder.writeln('return {}'.format(str(tlobject)))
|
||||
# builder.end_block() # There is no need to end the last block
|
||||
|
||||
# Step 3: Once all the objects have been generated, we can now group them in a single file
|
||||
filename = os.path.join(get_output_path('all_tlobjects.py'))
|
||||
with open(filename, 'w', encoding='utf-8') as file:
|
||||
with SourceBuilder(file) as builder:
|
||||
builder.writeln('"""File generated by TLObjects\' generator. All changes will be ERASED"""')
|
||||
builder.writeln(
|
||||
'"""File generated by TLObjects\' generator. All changes will be ERASED"""')
|
||||
builder.writeln()
|
||||
|
||||
# First add imports
|
||||
for tlobject in tlobjects:
|
||||
builder.writeln('import {}'.format(TLGenerator.get_full_file_name(tlobject)))
|
||||
builder.writeln('import {}'.format(
|
||||
TLGenerator.get_full_file_name(tlobject)))
|
||||
builder.writeln()
|
||||
|
||||
# Create a variable to indicate which layer this is
|
||||
builder.writeln('layer = {} # Current generated layer'.format(TLParser.find_layer(scheme_file)))
|
||||
builder.writeln('layer = {} # Current generated layer'.format(
|
||||
TLParser.find_layer(scheme_file)))
|
||||
builder.writeln()
|
||||
|
||||
# Then create the dictionary containing constructor_id: class
|
||||
|
@ -211,10 +235,9 @@ class TLGenerator:
|
|||
|
||||
# Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class)
|
||||
for tlobject in tlobjects:
|
||||
builder.writeln('{}: {}.{},'
|
||||
.format(hex(tlobject.id),
|
||||
TLGenerator.get_full_file_name(tlobject),
|
||||
TLGenerator.get_class_name(tlobject)))
|
||||
builder.writeln('{}: {}.{},'.format(
|
||||
hex(tlobject.id), TLGenerator.get_full_file_name(
|
||||
tlobject), TLGenerator.get_class_name(tlobject)))
|
||||
|
||||
builder.current_indent -= 1
|
||||
builder.writeln('}')
|
||||
|
@ -225,8 +248,10 @@ class TLGenerator:
|
|||
|
||||
# Courtesy of http://stackoverflow.com/a/31531797/4759433
|
||||
# Also, '_' could be replaced for ' ', then use .title(), and then remove ' '
|
||||
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), tlobject.name)
|
||||
result = result[:1].upper() + result[1:].replace('_', '') # Replace again to fully ensure!
|
||||
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(),
|
||||
tlobject.name)
|
||||
result = result[:1].upper() + result[1:].replace(
|
||||
'_', '') # Replace again to fully ensure!
|
||||
# If it's a function, let it end with "Request" to identify them more easily
|
||||
if tlobject.is_function:
|
||||
result += 'Request'
|
||||
|
@ -283,22 +308,25 @@ class TLGenerator:
|
|||
builder.writeln('if {}:'.format(name))
|
||||
|
||||
if arg.is_vector:
|
||||
builder.writeln("writer.write_int(0x1cb5c415, signed=False) # Vector's constructor ID")
|
||||
builder.writeln(
|
||||
"writer.write_int(0x1cb5c415, signed=False) # Vector's constructor ID")
|
||||
builder.writeln('writer.write_int(len({}))'.format(name))
|
||||
builder.writeln('for {}_item in {}:'.format(arg.name, name))
|
||||
# Temporary disable .is_vector, not to enter this if again
|
||||
arg.is_vector = False
|
||||
TLGenerator.write_onsend_code(builder, arg, args, name='{}_item'.format(arg.name))
|
||||
TLGenerator.write_onsend_code(
|
||||
builder, arg, args, name='{}_item'.format(arg.name))
|
||||
arg.is_vector = True
|
||||
|
||||
elif arg.flag_indicator:
|
||||
# Calculate the flags with those items which are not None
|
||||
builder.writeln('# Calculate the flags. This equals to those flag arguments which are NOT None')
|
||||
builder.writeln(
|
||||
'# Calculate the flags. This equals to those flag arguments which are NOT None')
|
||||
builder.writeln('flags = 0')
|
||||
for flag in args:
|
||||
if flag.is_flag:
|
||||
builder.writeln('flags |= (1 << {}) if {} else 0'
|
||||
.format(flag.flag_index, 'self.{}'.format(flag.name)))
|
||||
builder.writeln('flags |= (1 << {}) if {} else 0'.format(
|
||||
flag.flag_index, 'self.{}'.format(flag.name)))
|
||||
|
||||
builder.writeln('writer.write_int(flags)')
|
||||
builder.writeln()
|
||||
|
@ -310,10 +338,12 @@ class TLGenerator:
|
|||
builder.writeln('writer.write_long({})'.format(name))
|
||||
|
||||
elif 'int128' == arg.type:
|
||||
builder.writeln('writer.write_large_int({}, bits=128)'.format(name))
|
||||
builder.writeln('writer.write_large_int({}, bits=128)'.format(
|
||||
name))
|
||||
|
||||
elif 'int256' == arg.type:
|
||||
builder.writeln('writer.write_large_int({}, bits=256)'.format(name))
|
||||
builder.writeln('writer.write_large_int({}, bits=256)'.format(
|
||||
name))
|
||||
|
||||
elif 'double' == arg.type:
|
||||
builder.writeln('writer.write_double({})'.format(name))
|
||||
|
@ -366,7 +396,8 @@ class TLGenerator:
|
|||
was_flag = False
|
||||
if arg.is_flag:
|
||||
was_flag = True
|
||||
builder.writeln('if (flags & (1 << {})) != 0:'.format(arg.flag_index))
|
||||
builder.writeln('if (flags & (1 << {})) != 0:'.format(
|
||||
arg.flag_index))
|
||||
# Temporary disable .is_flag not to enter this if again when calling the method recursively
|
||||
arg.is_flag = False
|
||||
|
||||
|
@ -377,7 +408,8 @@ class TLGenerator:
|
|||
builder.writeln('for _ in range({}_len):'.format(arg.name))
|
||||
# Temporary disable .is_vector, not to enter this if again
|
||||
arg.is_vector = False
|
||||
TLGenerator.write_onresponse_code(builder, arg, args, name='{}_item'.format(arg.name))
|
||||
TLGenerator.write_onresponse_code(
|
||||
builder, arg, args, name='{}_item'.format(arg.name))
|
||||
builder.writeln('{}.append({}_item)'.format(name, arg.name))
|
||||
arg.is_vector = True
|
||||
|
||||
|
@ -393,10 +425,12 @@ class TLGenerator:
|
|||
builder.writeln('{} = reader.read_long()'.format(name))
|
||||
|
||||
elif 'int128' == arg.type:
|
||||
builder.writeln('{} = reader.read_large_int(bits=128)'.format(name))
|
||||
builder.writeln('{} = reader.read_large_int(bits=128)'.format(
|
||||
name))
|
||||
|
||||
elif 'int256' == arg.type:
|
||||
builder.writeln('{} = reader.read_large_int(bits=256)'.format(name))
|
||||
builder.writeln('{} = reader.read_large_int(bits=256)'.format(
|
||||
name))
|
||||
|
||||
elif 'double' == arg.type:
|
||||
builder.writeln('{} = reader.read_double()'.format(name))
|
||||
|
@ -408,7 +442,9 @@ class TLGenerator:
|
|||
builder.writeln('{} = reader.tgread_bool()'.format(name))
|
||||
|
||||
elif 'true' == arg.type: # Awkwardly enough, Telegram has both bool and "true", used in flags
|
||||
builder.writeln('{} = True # Arbitrary not-None value, no need to read since it is a flag'.format(name))
|
||||
builder.writeln(
|
||||
'{} = True # Arbitrary not-None value, no need to read since it is a flag'.
|
||||
format(name))
|
||||
|
||||
elif 'bytes' == arg.type:
|
||||
builder.writeln('{} = reader.tgread_bytes()'.format(name))
|
||||
|
@ -429,6 +465,7 @@ class TLGenerator:
|
|||
# Restore .is_flag
|
||||
arg.is_flag = True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if TLGenerator.tlobjects_exist():
|
||||
print('Detected previous TLObjects. Cleaning...')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from .crypto_tests import CryptoTests
|
||||
from .network_tests import NetworkTests
|
||||
from .parser_tests import ParserTests
|
||||
from .tl_tests import TLTests
|
||||
from .utils_tests import UtilsTests
|
||||
from .crypto_test import CryptoTests
|
||||
from .network_test import NetworkTests
|
||||
from .parser_test import ParserTests
|
||||
from .tl_test import TLTests
|
||||
from .utils_test import UtilsTests
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import unittest
|
||||
|
||||
from telethon.crypto import AES
|
||||
import telethon.helpers as utils
|
||||
from telethon.crypto import Factorizator
|
||||
from telethon.crypto import AES, Factorizator
|
||||
|
||||
|
||||
class CryptoTests(unittest.TestCase):
|
||||
|
@ -38,13 +37,15 @@ class CryptoTests(unittest.TestCase):
|
|||
.format(value[:take], self.cipher_text[:take]))
|
||||
|
||||
value = AES.encrypt_ige(self.plain_text_padded, self.key, self.iv)
|
||||
assert value == self.cipher_text_padded, ('Ciphered text ("{}") does not equal expected ("{}")'
|
||||
assert value == self.cipher_text_padded, (
|
||||
'Ciphered text ("{}") does not equal expected ("{}")'
|
||||
.format(value, self.cipher_text_padded))
|
||||
|
||||
def test_aes_decrypt(self):
|
||||
# The ciphered text must always be padded
|
||||
value = AES.decrypt_ige(self.cipher_text_padded, self.key, self.iv)
|
||||
assert value == self.plain_text_padded, ('Decrypted text ("{}") does not equal expected ("{}")'
|
||||
assert value == self.plain_text_padded, (
|
||||
'Decrypted text ("{}") does not equal expected ("{}")'
|
||||
.format(value, self.plain_text_padded))
|
||||
|
||||
@staticmethod
|
||||
|
@ -69,8 +70,10 @@ class CryptoTests(unittest.TestCase):
|
|||
expected_key = b"\xaf\xe3\x84Qm\xe0!\x0c\xd91\xe4\x9a\xa0v_gcx\xa1\xb0\xc9\xbc\x16'v\xcf,\x9dM\xae\xc6\xa5"
|
||||
expected_iv = b'\xb8Q\xf3\xc5\xa3]\xc6\xdf\x9e\xe0Q\xbd"\x8d\x13\t\x0e\x9a\x9d^8\xa2\xf8\xe7\x00w\xd9\xc1\xa7\xa0\xf7\x0f'
|
||||
|
||||
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(expected_key, key)
|
||||
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(expected_iv, iv)
|
||||
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(
|
||||
expected_key, key)
|
||||
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(
|
||||
expected_iv, iv)
|
||||
|
||||
# Calculate key being the server
|
||||
msg_key = b'\x86m\x92i\xcf\x8b\x93\xaa\x86K\x1fi\xd04\x83]'
|
||||
|
@ -79,14 +82,17 @@ class CryptoTests(unittest.TestCase):
|
|||
expected_key = b'\xdd0X\xb6\x93\x8e\xc9y\xef\x83\xf8\x8cj\xa7h\x03\xe2\xc6\xb16\xc5\xbb\xfc\xe7\xdf\xd6\xb1g\xf7u\xcfk'
|
||||
expected_iv = b'\xdcL\xc2\x18\x01J"X\x86lb\xb6\xb547\xfd\xe2a4\xb6\xaf}FS\xd7[\xe0N\r\x19\xfb\xbc'
|
||||
|
||||
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(expected_key, key)
|
||||
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(expected_iv, iv)
|
||||
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(
|
||||
expected_key, key)
|
||||
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(
|
||||
expected_iv, iv)
|
||||
|
||||
@staticmethod
|
||||
def test_calc_msg_key():
|
||||
value = utils.calc_msg_key(b'Some random message')
|
||||
expected = b'\xdfAa\xfc\x10\xab\x89\xd2\xfe\x19C\xf1\xdd~\xbf\x81'
|
||||
assert value == expected, 'Value ("{}") does not equal expected ("{}")'.format(value, expected)
|
||||
assert value == expected, 'Value ("{}") does not equal expected ("{}")'.format(
|
||||
value, expected)
|
||||
|
||||
@staticmethod
|
||||
def test_generate_key_data_from_nonces():
|
||||
|
@ -97,8 +103,10 @@ class CryptoTests(unittest.TestCase):
|
|||
expected_key = b'?\xc4\xbd\xdf\rWU\x8a\xf5\x0f+V\xdc\x96up\x1d\xeeG\x00\x81|\x1eg\x8a\x8f{\xf0y\x80\xda\xde'
|
||||
expected_iv = b'Q\x9dpZ\xb7\xdd\xcb\x82_\xfa\xf4\x90\xecn\x10\x9cD\xd2\x01\x8d\x83\xa0\xa4^\xb8\x91,\x7fI am'
|
||||
|
||||
assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format(key, expected_key)
|
||||
assert iv == expected_iv, 'Key ("{}") does not equal expected ("{}")'.format(key, expected_iv)
|
||||
assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format(
|
||||
key, expected_key)
|
||||
assert iv == expected_iv, 'Key ("{}") does not equal expected ("{}")'.format(
|
||||
key, expected_iv)
|
||||
|
||||
@staticmethod
|
||||
def test_factorizator():
|
|
@ -3,8 +3,8 @@ import socket
|
|||
import threading
|
||||
import unittest
|
||||
|
||||
from telethon.network import TcpTransport, TcpClient
|
||||
import telethon.network.authenticator as authenticator
|
||||
from telethon.network import TcpClient, TcpTransport
|
||||
|
||||
|
||||
def run_server_echo_thread(port):
|
||||
|
@ -31,7 +31,8 @@ class NetworkTests(unittest.TestCase):
|
|||
client = TcpClient()
|
||||
client.connect('localhost', port)
|
||||
client.write(msg)
|
||||
assert msg == client.read(16), 'Read message does not equal sent message'
|
||||
assert msg == client.read(
|
||||
16), 'Read message does not equal sent message'
|
||||
client.close()
|
||||
|
||||
@staticmethod
|
|
@ -14,7 +14,7 @@ class UtilsTests(unittest.TestCase):
|
|||
writer.write_float(17.0)
|
||||
writer.write_double(25.0)
|
||||
writer.write(bytes([26, 27, 28, 29, 30, 31, 32]))
|
||||
writer.write_large_int(2 ** 127, 128, signed=False)
|
||||
writer.write_large_int(2**127, 128, signed=False)
|
||||
|
||||
data = writer.get_bytes()
|
||||
expected = b'\x01\x05\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x88A\x00\x00\x00\x00\x00\x00' \
|
||||
|
@ -24,26 +24,32 @@ class UtilsTests(unittest.TestCase):
|
|||
|
||||
with BinaryReader(data) as reader:
|
||||
value = reader.read_byte()
|
||||
assert value == 1, 'Example byte should be 1 but is {}'.format(value)
|
||||
assert value == 1, 'Example byte should be 1 but is {}'.format(
|
||||
value)
|
||||
|
||||
value = reader.read_int()
|
||||
assert value == 5, 'Example integer should be 5 but is {}'.format(value)
|
||||
assert value == 5, 'Example integer should be 5 but is {}'.format(
|
||||
value)
|
||||
|
||||
value = reader.read_long()
|
||||
assert value == 13, 'Example long integer should be 13 but is {}'.format(value)
|
||||
assert value == 13, 'Example long integer should be 13 but is {}'.format(
|
||||
value)
|
||||
|
||||
value = reader.read_float()
|
||||
assert value == 17.0, 'Example float should be 17.0 but is {}'.format(value)
|
||||
assert value == 17.0, 'Example float should be 17.0 but is {}'.format(
|
||||
value)
|
||||
|
||||
value = reader.read_double()
|
||||
assert value == 25.0, 'Example double should be 25.0 but is {}'.format(value)
|
||||
assert value == 25.0, 'Example double should be 25.0 but is {}'.format(
|
||||
value)
|
||||
|
||||
value = reader.read(7)
|
||||
assert value == bytes([26, 27, 28, 29, 30, 31, 32]), 'Example bytes should be {} but is {}' \
|
||||
.format(bytes([26, 27, 28, 29, 30, 31, 32]), value)
|
||||
|
||||
value = reader.read_large_int(128, signed=False)
|
||||
assert value == 2 ** 127, 'Example large integer should be {} but is {}'.format(2 ** 127, value)
|
||||
assert value == 2**127, 'Example large integer should be {} but is {}'.format(
|
||||
2**127, value)
|
||||
|
||||
# Test Telegram that types are written right
|
||||
with BinaryWriter() as writer:
|
||||
|
@ -51,12 +57,14 @@ class UtilsTests(unittest.TestCase):
|
|||
buffer = writer.get_bytes()
|
||||
valid = b'\x78\x97\x46\x60' # Tested written bytes using C#'s MemoryStream
|
||||
|
||||
assert buffer == valid, "Written type should be {} but is {}".format(list(valid), list(buffer))
|
||||
assert buffer == valid, 'Written type should be {} but is {}'.format(
|
||||
list(valid), list(buffer))
|
||||
|
||||
@staticmethod
|
||||
def test_binary_tgwriter_tgreader():
|
||||
small_data = os.urandom(33)
|
||||
small_data_padded = os.urandom(19) # +1 byte for length = 20 (evenly divisible by 4)
|
||||
small_data_padded = os.urandom(
|
||||
19) # +1 byte for length = 20 (evenly divisible by 4)
|
||||
|
||||
large_data = os.urandom(999)
|
||||
large_data_padded = os.urandom(1024)
|
||||
|
@ -74,7 +82,9 @@ class UtilsTests(unittest.TestCase):
|
|||
# And then try reading it without errors (it should be unharmed!)
|
||||
for datum in data:
|
||||
value = reader.tgread_bytes()
|
||||
assert value == datum, 'Example bytes should be {} but is {}'.format(datum, value)
|
||||
assert value == datum, 'Example bytes should be {} but is {}'.format(
|
||||
datum, value)
|
||||
|
||||
value = reader.tgread_string()
|
||||
assert value == string, 'Example string should be {} but is {}'.format(string, value)
|
||||
assert value == string, 'Example string should be {} but is {}'.format(
|
||||
string, value)
|
|
@ -1,6 +1,7 @@
|
|||
import traceback
|
||||
from telethon.interactive_telegram_client import \
|
||||
InteractiveTelegramClient, print_title
|
||||
|
||||
from telethon.interactive_telegram_client import (InteractiveTelegramClient,
|
||||
print_title)
|
||||
|
||||
|
||||
def load_settings(path='api/settings'):
|
||||
|
@ -34,7 +35,8 @@ if __name__ == '__main__':
|
|||
client.run()
|
||||
|
||||
except Exception as e:
|
||||
print('Unexpected error ({}): {} at\n{}'.format(type(e), e, traceback.format_exc()))
|
||||
print('Unexpected error ({}): {} at\n{}'.format(
|
||||
type(e), e, traceback.format_exc()))
|
||||
|
||||
finally:
|
||||
print_title('Exit')
|
||||
|
|
Loading…
Reference in New Issue
Block a user