Many code-style improvements

This commit is contained in:
Fadi Hadzh
2016-11-30 00:29:42 +03:00
parent ef264ae83f
commit d087941bd0
25 changed files with 698 additions and 499 deletions

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
from telethon.utils import BinaryWriter
import telethon.helpers as utils
import os
import telethon.helpers as utils
from telethon.utils import BinaryWriter
class RSAServerKey:
def __init__(self, fingerprint, m, e):
@@ -18,9 +19,9 @@ class RSAServerKey:
with BinaryWriter() as writer:
# Write SHA
writer.write(utils.sha1(data[offset:offset+length]))
writer.write(utils.sha1(data[offset:offset + length]))
# Write data
writer.write(data[offset:offset+length])
writer.write(data[offset:offset + length])
# Add padding if required
if length < 235:
writer.write(os.urandom(235 - length))
@@ -31,21 +32,22 @@ 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'
'1F8F8B2A0EC9BA9720CE352EFCF6C5680FFC424BD634864902DE0B4BD6D49F4E'
'580230E3AE97D95C8B19442B3C0A10D8F5633FECEDD6926A7F6DAB0DDB7D457F'
'9EA81B8465FCD6FFFEED114011DF91C059CAEDAF97625F6C96ECC74725556934'
'EF781D866B34F011FCE4D835A090196E9A5F0E4449AF7EB697DDB9076494CA5F'
'81104A305B6DD27665722C46B60E5DF680FB16B210607EF217652E60236C255F'
'6A28315F4083A96791D7214BF64C1DF4FD0DB1944FB26A2A57031B32EEE64AD1'
'5A8BA68885CDE74A5BFC920F6ABF59BA5C75506373E7130F9042DA922179251F',
16), int('010001', 16))
'216be86c022bb4c3': RSAServerKey('216be86c022bb4c3', int(
'C150023E2F70DB7985DED064759CFECF0AF328E69A41DAF4D6F01B538135A6F9'
'1F8F8B2A0EC9BA9720CE352EFCF6C5680FFC424BD634864902DE0B4BD6D49F4E'
'580230E3AE97D95C8B19442B3C0A10D8F5633FECEDD6926A7F6DAB0DDB7D457F'
'9EA81B8465FCD6FFFEED114011DF91C059CAEDAF97625F6C96ECC74725556934'
'EF781D866B34F011FCE4D835A090196E9A5F0E4449AF7EB697DDB9076494CA5F'
'81104A305B6DD27665722C46B60E5DF680FB16B210607EF217652E60236C255F'
'6A28315F4083A96791D7214BF64C1DF4FD0DB1944FB26A2A57031B32EEE64AD1'
'5A8BA68885CDE74A5BFC920F6ABF59BA5C75506373E7130F9042DA922179251F',
16), int('010001', 16))
}
@staticmethod

View File

@@ -3,6 +3,7 @@ import re
class ReadCancelledError(Exception):
"""Occurs when a read operation was cancelled"""
def __init__(self):
super().__init__(self, 'The read operation was cancelled.')
@@ -15,28 +16,33 @@ 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 '
'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)))
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)))
self.invalid_constructor_id = invalid_constructor_id
class InvalidDCError(Exception):
def __init__(self, new_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))
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))
self.new_dc = new_dc
class InvalidChecksumError(Exception):
def __init__(self, checksum, valid_checksum):
super().__init__(self, 'Invalid checksum ({} when {} was expected). This packet should be skipped.'
.format(checksum, valid_checksum))
super().__init__(
self,
'Invalid checksum ({} when {} was expected). This packet should be skipped.'
.format(checksum, valid_checksum))
self.checksum = checksum
self.valid_checksum = valid_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 '
'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 '
'authorized users.'),
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 '
'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.')
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 '
'authorized users.'),
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 '
'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.'
)
}
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 '
'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 '
'a different data center (#{} for registration).',
'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 (#{}, '
'for registration).',
'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 '
'(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.',
'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.',
# 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 '
'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 '
'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 '
'to be divisible by 4).',
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 '
'message with this msg_id or not.',
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 '
'either a lower or an equal and odd seqno).',
34: 'An even msg_seqno expected (irrelevant message), but odd received.',
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 '
'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 '
'to be divisible by 4).',
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 '
'message with this msg_id or not.',
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 '
'either a lower or an equal and odd seqno).',
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 '
'the correct salt, and the message is to be re-sent with it).',
48:
'Incorrect server salt (in this case, the bad_server_salt response is received with '
'the correct salt, and the message is to be re-sent with it).',
64: 'Invalid container.'
}
def __init__(self, code):
super().__init__(self, BadMessageError
.ErrorMessages.get(code,'Unknown error code (this should not happen): {}.'.format(code)))
super().__init__(self, BadMessageError.ErrorMessages.get(
code,
'Unknown error code (this should not happen): {}.'.format(code)))
self.code = code

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,47 +1,40 @@
import platform
from datetime import datetime, timedelta
from hashlib import md5
from os import path, listdir
from mimetypes import guess_type
# For sending and receiving requests
from telethon.tl import MTProtoRequest
from telethon.tl import Session
# The Requests and types that we'll be using
from telethon.tl.functions.upload import SaveBigFilePartRequest
from telethon.tl.functions import InvokeWithLayerRequest, InitConnectionRequest
from telethon.tl.functions.help import GetConfigRequest
from telethon.tl.functions.upload import SaveFilePartRequest, GetFileRequest
from telethon.tl.functions.messages import \
GetDialogsRequest, GetHistoryRequest, \
SendMessageRequest, SendMediaRequest, \
ReadHistoryRequest
from telethon.tl.functions.auth import \
SendCodeRequest, CheckPasswordRequest, \
SignInRequest, SignUpRequest, LogOutRequest
# The following is required to get the password salt
from telethon.tl.functions.account import GetPasswordRequest
# All the types we need to work with
from telethon.tl.types import \
InputPeerEmpty, \
UserProfilePhotoEmpty, ChatPhotoEmpty, \
InputFile, InputFileLocation, InputMediaUploadedPhoto, InputMediaUploadedDocument, \
MessageMediaContact, MessageMediaDocument, MessageMediaPhoto, \
DocumentAttributeAudio, DocumentAttributeFilename, InputDocumentFileLocation
from os import listdir, path
# Import some externalized utilities to work with the Telegram types and more
import telethon.helpers as utils
import telethon.network.authenticator as authenticator
from telethon.utils import find_user_or_chat, get_appropiate_part_size, get_extension
from telethon.errors import *
from telethon.network import MtProtoSender, TcpTransport
from telethon.parser.markdown_parser import parse_message_entities
# For sending and receiving requests
from telethon.tl import MTProtoRequest, Session
from telethon.tl.all_tlobjects import layer
from telethon.tl.functions import InitConnectionRequest, InvokeWithLayerRequest
# The following is required to get the password salt
from telethon.tl.functions.account import GetPasswordRequest
from telethon.tl.functions.auth import (CheckPasswordRequest, LogOutRequest,
SendCodeRequest, SignInRequest,
SignUpRequest)
from telethon.tl.functions.help import GetConfigRequest
from telethon.tl.functions.messages import (
GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest,
SendMessageRequest)
# The Requests and types that we'll be using
from telethon.tl.functions.upload import (
GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest)
# All the types we need to work with
from telethon.tl.types import (
ChatPhotoEmpty, DocumentAttributeAudio, DocumentAttributeFilename,
InputDocumentFileLocation, InputFile, InputFileLocation,
InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty,
MessageMediaContact, MessageMediaDocument, MessageMediaPhoto,
UserProfilePhotoEmpty)
from telethon.utils import (find_user_or_chat, get_appropiate_part_size,
get_extension)
class TelegramClient:
@@ -53,13 +46,15 @@ class TelegramClient:
def __init__(self, session_user_id, api_id, api_hash):
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
self.session = Session.try_load_or_create_new(session_user_id)
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
@@ -88,14 +83,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,
device_model=platform.node(),
system_version=platform.system(),
app_version=self.__version__,
lang_code='en',
query=GetConfigRequest())
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!
@@ -114,7 +112,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)
@@ -175,11 +174,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_'):
@@ -189,10 +190,12 @@ 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, '
'and a password only if an RPCError was raised before.')
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
self.session.user = result.user
@@ -205,11 +208,13 @@ 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,
phone_code_hash=self.phone_code_hashes[phone_number],
phone_code=code,
first_name=first_name,
last_name=last_name))
result = self.invoke(
SignUpRequest(
phone_number=phone_number,
phone_code_hash=self.phone_code_hashes[phone_number],
phone_code=code,
first_name=first_name,
last_name=last_name))
self.session.user = result.user
self.session.save()
@@ -229,29 +234,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,
offset_id=offset_id,
offset_peer=offset_peer,
limit=count))
return (r.dialogs,
[find_user_or_chat(d.peer, r.users, r.chats) for d in r.dialogs])
r = self.invoke(
GetDialogsRequest(
offset_date=offset_date,
offset_id=offset_id,
offset_peer=offset_peer,
limit=count))
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)
@@ -259,15 +276,23 @@ class TelegramClient:
msg, entities = message, []
msg_id = utils.generate_random_long()
self.invoke(SendMessageRequest(peer=input_peer,
message=msg,
random_id=msg_id,
entities=entities,
no_webpage=no_web_page))
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
@@ -282,13 +307,15 @@ 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,
limit=limit,
offset_date=offset_date,
offset_id=offset_id,
max_id=max_id,
min_id=min_id,
add_offset=add_offset))
result = self.invoke(
GetHistoryRequest(
input_peer,
limit=limit,
offset_date=offset_date,
offset_id=offset_id,
max_id=max_id,
min_id=min_id,
add_offset=add_offset))
# The result may be a messages slice (not all messages were retrieved) or
# simply a messages TLObject. In the later case, no "count" attribute is specified:
@@ -315,7 +342,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)
@@ -331,7 +359,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
@@ -359,7 +391,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:
@@ -370,7 +402,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)
@@ -381,17 +414,19 @@ 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,
parts=part_count,
name=file_name,
md5_checksum=hash_md5.hexdigest())
return InputFile(
id=file_id,
parts=part_count,
name=file_name,
md5_checksum=hash_md5.hexdigest())
def send_photo_file(self, input_file, input_peer, caption=''):
"""Sends a previously uploaded input_file
@@ -415,28 +450,36 @@ 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,
mime_type=mime_type,
attributes=attributes,
caption=caption), input_peer)
self.send_media_file(
InputMediaUploadedDocument(
file=input_file,
mime_type=mime_type,
attributes=attributes,
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,
media=input_media,
random_id=utils.generate_random_long()))
self.invoke(
SendMediaRequest(
peer=input_peer,
media=input_media,
random_id=utils.generate_random_long()))
# endregion
# 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"""
if (not profile_photo or
isinstance(profile_photo, UserProfilePhotoEmpty) or
isinstance(profile_photo, UserProfilePhotoEmpty) or
isinstance(profile_photo, ChatPhotoEmpty)):
return False
@@ -449,28 +492,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,
local_id=photo_location.local_id,
secret=photo_location.secret),
file_path)
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
@@ -488,13 +543,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,
local_id=largest_size.local_id,
secret=largest_size.secret),
file_path, file_size=file_size, progress_callback=progress_callback)
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)
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.
@@ -521,10 +583,14 @@ class TelegramClient:
if add_extension:
file_path += get_extension(document.mime_type)
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)
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)
return file_path
@staticmethod
@@ -546,15 +612,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)"""
@@ -578,7 +650,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
@@ -600,7 +673,8 @@ class TelegramClient:
"""Adds an update handler (a function which takes a TLObject,
an update, as its parameter) and listens for updates"""
if not self.signed_in:
raise ValueError("You cannot add update handlers until you've signed in.")
raise ValueError(
"You cannot add update handlers until you've signed in.")
self.sender.add_update_handler(handler)

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
from datetime import datetime
from io import BytesIO, BufferedReader
from telethon.tl.all_tlobjects import tlobjects
from struct import unpack
from telethon.errors import *
import inspect
import os
from datetime import datetime
from io import BufferedReader, BytesIO
from struct import unpack
from telethon.errors import *
from telethon.tl.all_tlobjects import tlobjects
class BinaryReader:
@@ -12,13 +12,15 @@ class BinaryReader:
Small utility class to read binary data.
Also creates a "Memory Stream" if necessary
"""
def __init__(self, data=None, stream=None):
if data:
self.stream = BytesIO(data)
elif stream:
self.stream = stream
else:
raise InvalidParameterError("Either bytes or a stream must be provided")
raise InvalidParameterError(
'Either bytes or a stream must be provided')
self.reader = BufferedReader(self.stream)
@@ -47,14 +49,16 @@ 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
def get_bytes(self):
@@ -69,7 +73,8 @@ class BinaryReader:
"""Reads a Telegram-encoded byte array, without the need of specifying its length"""
first_byte = self.read_byte()
if first_byte == 254:
length = self.read_byte() | (self.read_byte() << 8) | (self.read_byte() << 16)
length = self.read_byte() | (self.read_byte() << 8) | (
self.read_byte() << 16)
padding = length % 4
else:
length = first_byte

View File

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

View File

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