Merge remote-tracking branch 'remotes/upstream/master'

This commit is contained in:
JosXa 2017-10-21 00:04:55 +02:00
commit b7c72f18fd
31 changed files with 823 additions and 957 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
telethon/tl/functions/
telethon/tl/types/
telethon/tl/all_tlobjects.py
telethon/errors/rpc_error_list.py
# User session
*.session

View File

@ -42,17 +42,26 @@ class TempWorkDir:
os.chdir(self.original)
ERROR_LIST = 'telethon/errors/rpc_error_list.py'
ERRORS_JSON = 'telethon_generator/errors.json'
ERRORS_DESC = 'telethon_generator/error_descriptions'
SCHEME_TL = 'telethon_generator/scheme.tl'
GENERATOR_DIR = 'telethon/tl'
IMPORT_DEPTH = 2
def gen_tl():
from telethon_generator.tl_generator import TLGenerator
generator = TLGenerator('telethon/tl')
from telethon_generator.error_generator import generate_code
generator = TLGenerator(GENERATOR_DIR)
if generator.tlobjects_exist():
print('Detected previous TLObjects. Cleaning...')
generator.clean_tlobjects()
print('Generating TLObjects...')
generator.generate_tlobjects(
'telethon_generator/scheme.tl', import_depth=2
)
generator.generate_tlobjects(SCHEME_TL, import_depth=IMPORT_DEPTH)
print('Generating errors...')
generate_code(ERROR_LIST, json_file=ERRORS_JSON, errors_desc=ERRORS_DESC)
print('Done.')
@ -63,7 +72,7 @@ def main():
elif len(argv) >= 2 and argv[1] == 'clean_tl':
from telethon_generator.tl_generator import TLGenerator
print('Cleaning...')
TLGenerator('telethon/tl').clean_tlobjects()
TLGenerator(GENERATOR_DIR).clean_tlobjects()
print('Done.')
elif len(argv) >= 2 and argv[1] == 'pypi':
@ -80,6 +89,10 @@ def main():
for x in ('build', 'dist', 'Telethon.egg-info'):
rmtree(x, ignore_errors=True)
if len(argv) >= 2 and argv[1] == 'fetch_errors':
from telethon_generator.error_generator import fetch_errors
fetch_errors(ERRORS_JSON)
else:
if not TelegramClient:
gen_tl()

View File

@ -8,15 +8,8 @@ from .common import (
CdnFileTamperedError
)
from .rpc_errors import (
RPCError, InvalidDCError, BadRequestError, UnauthorizedError,
ForbiddenError, NotFoundError, FloodError, ServerError, BadMessageError
)
from .rpc_errors_303 import *
from .rpc_errors_400 import *
from .rpc_errors_401 import *
from .rpc_errors_420 import *
# This imports the base errors too, as they're imported there
from .rpc_error_list import *
def report_error(code, message, report_method):
@ -43,27 +36,31 @@ def rpc_message_to_error(code, message, report_method=None):
args=(code, message, report_method)
).start()
errors = {
303: rpc_errors_303_all,
400: rpc_errors_400_all,
401: rpc_errors_401_all,
420: rpc_errors_420_all
}.get(code, None)
# Try to get the error by direct look-up, otherwise regex
# TODO Maybe regexes could live in a separate dictionary?
cls = rpc_errors_all.get(message, None)
if cls:
return cls()
if errors is not None:
for msg, cls in errors.items():
m = re.match(msg, message)
if m:
extra = int(m.group(1)) if m.groups() else None
return cls(extra=extra)
for msg_regex, cls in rpc_errors_all.items():
m = re.match(msg_regex, message)
if m:
capture = int(m.group(1)) if m.groups() else None
return cls(capture=capture)
elif code == 403:
if code == 400:
return BadRequestError(message)
if code == 401:
return UnauthorizedError(message)
if code == 403:
return ForbiddenError(message)
elif code == 404:
if code == 404:
return NotFoundError(message)
elif code == 500:
if code == 500:
return ServerError(message)
return RPCError('{} (code {})'.format(message, code))

View File

@ -1,51 +0,0 @@
from . import InvalidDCError
class FileMigrateError(InvalidDCError):
def __init__(self, **kwargs):
self.new_dc = kwargs['extra']
super(Exception, self).__init__(
self,
'The file to be accessed is currently stored in DC {}.'
.format(self.new_dc)
)
class PhoneMigrateError(InvalidDCError):
def __init__(self, **kwargs):
self.new_dc = kwargs['extra']
super(Exception, self).__init__(
self,
'The phone number a user is trying to use for authorization is '
'associated with DC {}.'
.format(self.new_dc)
)
class NetworkMigrateError(InvalidDCError):
def __init__(self, **kwargs):
self.new_dc = kwargs['extra']
super(Exception, self).__init__(
self,
'The source IP address is associated with DC {}.'
.format(self.new_dc)
)
class UserMigrateError(InvalidDCError):
def __init__(self, **kwargs):
self.new_dc = kwargs['extra']
super(Exception, self).__init__(
self,
'The user whose identity is being used to execute queries is '
'associated with DC {}.'
.format(self.new_dc)
)
rpc_errors_303_all = {
'FILE_MIGRATE_(\d+)': FileMigrateError,
'PHONE_MIGRATE_(\d+)': PhoneMigrateError,
'NETWORK_MIGRATE_(\d+)': NetworkMigrateError,
'USER_MIGRATE_(\d+)': UserMigrateError
}

View File

@ -1,453 +0,0 @@
from . import BadRequestError
class ApiIdInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The api_id/api_hash combination is invalid.'
)
class BotMethodInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The API access for bot users is restricted. The method you '
'tried to invoke cannot be executed as a bot.'
)
class CdnMethodInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'This method cannot be invoked on a CDN server. Refer to '
'https://core.telegram.org/cdn#schema for available methods.'
)
class ChannelInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Invalid channel object. Make sure to pass the right types,'
' for instance making sure that the request is designed for '
'channels or otherwise look for a different one more suited.'
)
class ChannelPrivateError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The channel specified is private and you lack permission to '
'access it. Another reason may be that you were banned from it.'
)
class ChatAdminRequiredError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'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).'
)
class ChatIdInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Invalid object ID for a chat. Make sure to pass the right types,'
' for instance making sure that the request is designed for chats'
' (not channels/megagroups) or otherwise look for a different one'
' more suited.\nAn example working with a megagroup and'
' AddChatUserRequest, it will fail because megagroups are channels'
'. Use InviteToChannelRequest instead.'
)
class ConnectionLangPackInvalid(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The specified language pack is not valid. This is meant to be '
'used by official applications only so far, leave it empty.'
)
class ConnectionLayerInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The very first request must always be InvokeWithLayerRequest.'
)
class DcIdInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'This occurs when an authorization is tried to be exported for '
'the same data center one is currently connected to.'
)
class FieldNameEmptyError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The field with the name FIELD_NAME is missing.'
)
class FieldNameInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The field with the name FIELD_NAME is invalid.'
)
class FilePartsInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The number of file parts is invalid.'
)
class FilePartMissingError(BadRequestError):
def __init__(self, **kwargs):
self.which = kwargs['extra']
super(Exception, self).__init__(
self,
'Part {} of the file is missing from storage.'.format(self.which)
)
class FilePartInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The file part number is invalid.'
)
class FirstNameInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The first name is invalid.'
)
class InputMethodInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The invoked method does not exist anymore or has never existed.'
)
class InputRequestTooLongError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The input request was too long. This may be a bug in the library '
'as it can occur when serializing more bytes than it should (like'
'appending the vector constructor code at the end of a message).'
)
class LastNameInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The last name is invalid.'
)
class LimitInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'An invalid limit was provided. See '
'https://core.telegram.org/api/files#downloading-files'
)
class LocationInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The location given for a file was invalid. See '
'https://core.telegram.org/api/files#downloading-files'
)
class Md5ChecksumInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The MD5 check-sums do not match.'
)
class MessageEmptyError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Empty or invalid UTF-8 message was sent.'
)
class MessageIdInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The specified message ID is invalid.'
)
class MessageTooLongError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Message was too long. Current maximum length is 4096 UTF-8 '
'characters.'
)
class MessageNotModifiedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Content of the message was not modified.'
)
class MsgWaitFailedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'A waiting call returned an error.'
)
class OffsetInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The given offset was invalid, it must be divisible by 1KB. '
'See https://core.telegram.org/api/files#downloading-files'
)
class PasswordHashInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The password (and thus its hash value) you entered is invalid.'
)
class PeerIdInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'An invalid Peer was used. Make sure to pass the right peer type.'
)
class PhoneCodeEmptyError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The phone code is missing.'
)
class PhoneCodeExpiredError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The confirmation code has expired.'
)
class PhoneCodeHashEmptyError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The phone code hash is missing.'
)
class PhoneCodeInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The phone code entered was invalid.'
)
class PhoneNumberBannedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The used phone number has been banned from Telegram and cannot '
'be used anymore. Maybe check https://www.telegram.org/faq_spam.'
)
class PhoneNumberInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The phone number is invalid.'
)
class PhoneNumberOccupiedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The phone number is already in use.'
)
class PhoneNumberUnoccupiedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The phone number is not yet being used.'
)
class PhotoInvalidDimensionsError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The photo dimensions are invalid.'
)
class TypeConstructorInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The type constructor is invalid.'
)
class UsernameInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Unacceptable username. Must match r"[a-zA-Z][\w\d]{4,31}".'
)
class UsernameNotModifiedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The username is not different from the current username.'
)
class UsernameNotOccupiedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The username is not in use by anyone else yet.'
)
class UsernameOccupiedError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The username is already taken.'
)
class UsersTooFewError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Not enough users (to create a chat, for example).'
)
class UsersTooMuchError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The maximum number of users has been exceeded (to create a '
'chat, for example).'
)
class UserIdInvalidError(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Invalid object ID for an user. Make sure to pass the right types,'
'for instance making sure that the request is designed for users'
'or otherwise look for a different one more suited.'
)
rpc_errors_400_all = {
'API_ID_INVALID': ApiIdInvalidError,
'BOT_METHOD_INVALID': BotMethodInvalidError,
'CDN_METHOD_INVALID': CdnMethodInvalidError,
'CHANNEL_INVALID': ChannelInvalidError,
'CHANNEL_PRIVATE': ChannelPrivateError,
'CHAT_ADMIN_REQUIRED': ChatAdminRequiredError,
'CHAT_ID_INVALID': ChatIdInvalidError,
'CONNECTION_LAYER_INVALID': ConnectionLayerInvalidError,
'DC_ID_INVALID': DcIdInvalidError,
'FIELD_NAME_EMPTY': FieldNameEmptyError,
'FIELD_NAME_INVALID': FieldNameInvalidError,
'FILE_PARTS_INVALID': FilePartsInvalidError,
'FILE_PART_(\d+)_MISSING': FilePartMissingError,
'FILE_PART_INVALID': FilePartInvalidError,
'FIRSTNAME_INVALID': FirstNameInvalidError,
'INPUT_METHOD_INVALID': InputMethodInvalidError,
'INPUT_REQUEST_TOO_LONG': InputRequestTooLongError,
'LASTNAME_INVALID': LastNameInvalidError,
'LIMIT_INVALID': LimitInvalidError,
'LOCATION_INVALID': LocationInvalidError,
'MD5_CHECKSUM_INVALID': Md5ChecksumInvalidError,
'MESSAGE_EMPTY': MessageEmptyError,
'MESSAGE_ID_INVALID': MessageIdInvalidError,
'MESSAGE_TOO_LONG': MessageTooLongError,
'MESSAGE_NOT_MODIFIED': MessageNotModifiedError,
'MSG_WAIT_FAILED': MsgWaitFailedError,
'OFFSET_INVALID': OffsetInvalidError,
'PASSWORD_HASH_INVALID': PasswordHashInvalidError,
'PEER_ID_INVALID': PeerIdInvalidError,
'PHONE_CODE_EMPTY': PhoneCodeEmptyError,
'PHONE_CODE_EXPIRED': PhoneCodeExpiredError,
'PHONE_CODE_HASH_EMPTY': PhoneCodeHashEmptyError,
'PHONE_CODE_INVALID': PhoneCodeInvalidError,
'PHONE_NUMBER_BANNED': PhoneNumberBannedError,
'PHONE_NUMBER_INVALID': PhoneNumberInvalidError,
'PHONE_NUMBER_OCCUPIED': PhoneNumberOccupiedError,
'PHONE_NUMBER_UNOCCUPIED': PhoneNumberUnoccupiedError,
'PHOTO_INVALID_DIMENSIONS': PhotoInvalidDimensionsError,
'TYPE_CONSTRUCTOR_INVALID': TypeConstructorInvalidError,
'USERNAME_INVALID': UsernameInvalidError,
'USERNAME_NOT_MODIFIED': UsernameNotModifiedError,
'USERNAME_NOT_OCCUPIED': UsernameNotOccupiedError,
'USERNAME_OCCUPIED': UsernameOccupiedError,
'USERS_TOO_FEW': UsersTooFewError,
'USERS_TOO_MUCH': UsersTooMuchError,
'USER_ID_INVALID': UserIdInvalidError,
}

View File

@ -1,98 +0,0 @@
from . import UnauthorizedError
class ActiveUserRequiredError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The method is only available to already activated users.'
)
class AuthKeyInvalidError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The key is invalid.'
)
class AuthKeyPermEmptyError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The method is unavailable for temporary authorization key, not '
'bound to permanent.'
)
class AuthKeyUnregisteredError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The key is not registered in the system.'
)
class InviteHashExpiredError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The chat the user tried to join has expired and is not valid '
'anymore.'
)
class SessionExpiredError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The authorization has expired.'
)
class SessionPasswordNeededError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'Two-steps verification is enabled and a password is required.'
)
class SessionRevokedError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The authorization has been invalidated, because of the user '
'terminating all sessions.'
)
class UserAlreadyParticipantError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The authenticated user is already a participant of the chat.'
)
class UserDeactivatedError(UnauthorizedError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The user has been deleted/deactivated.'
)
rpc_errors_401_all = {
'ACTIVE_USER_REQUIRED': ActiveUserRequiredError,
'AUTH_KEY_INVALID': AuthKeyInvalidError,
'AUTH_KEY_PERM_EMPTY': AuthKeyPermEmptyError,
'AUTH_KEY_UNREGISTERED': AuthKeyUnregisteredError,
'INVITE_HASH_EXPIRED': InviteHashExpiredError,
'SESSION_EXPIRED': SessionExpiredError,
'SESSION_PASSWORD_NEEDED': SessionPasswordNeededError,
'SESSION_REVOKED': SessionRevokedError,
'USER_ALREADY_PARTICIPANT': UserAlreadyParticipantError,
'USER_DEACTIVATED': UserDeactivatedError,
}

View File

@ -1,16 +0,0 @@
from . import FloodError
class FloodWaitError(FloodError):
def __init__(self, **kwargs):
self.seconds = kwargs['extra']
super(Exception, self).__init__(
self,
'A wait of {} seconds is required.'
.format(self.seconds)
)
rpc_errors_420_all = {
'FLOOD_WAIT_(\d+)': FloodWaitError
}

View File

@ -129,13 +129,10 @@ class BinaryReader:
return False
# If there was still no luck, give up
self.seek(-4) # Go back
raise TypeNotFoundError(constructor_id)
# Create an empty instance of the class and
# fill it with the read attributes
result = clazz.empty()
result.on_response(self)
return result
return clazz.from_reader(self)
def tgread_vector(self):
"""Reads a vector (a list) of Telegram objects"""

View File

@ -14,7 +14,7 @@ class TcpClient:
if isinstance(timeout, timedelta):
self.timeout = timeout.seconds
elif isinstance(timeout, int) or isinstance(timeout, float):
elif isinstance(timeout, (int, float)):
self.timeout = float(timeout)
else:
raise ValueError('Invalid timeout type', type(timeout))

View File

@ -42,7 +42,7 @@ def _do_authentication(connection):
req_pq_request = ReqPqRequest(
nonce=int.from_bytes(os.urandom(16), 'big', signed=True)
)
sender.send(req_pq_request.to_bytes())
sender.send(bytes(req_pq_request))
with BinaryReader(sender.receive()) as reader:
req_pq_request.on_response(reader)
@ -60,12 +60,12 @@ def _do_authentication(connection):
p, q = rsa.get_byte_array(min(p, q)), rsa.get_byte_array(max(p, q))
new_nonce = int.from_bytes(os.urandom(32), 'little', signed=True)
pq_inner_data = PQInnerData(
pq_inner_data = bytes(PQInnerData(
pq=rsa.get_byte_array(pq), p=p, q=q,
nonce=res_pq.nonce,
server_nonce=res_pq.server_nonce,
new_nonce=new_nonce
).to_bytes()
))
# sha_digest + data + random_bytes
cipher_text, target_fingerprint = None, None
@ -90,7 +90,7 @@ def _do_authentication(connection):
public_key_fingerprint=target_fingerprint,
encrypted_data=cipher_text
)
sender.send(req_dh_params.to_bytes())
sender.send(bytes(req_dh_params))
# Step 2 response: DH Exchange
with BinaryReader(sender.receive()) as reader:
@ -124,7 +124,6 @@ def _do_authentication(connection):
raise AssertionError(server_dh_inner)
if server_dh_inner.nonce != res_pq.nonce:
print(server_dh_inner.nonce, res_pq.nonce)
raise SecurityError('Invalid nonce in encrypted answer')
if server_dh_inner.server_nonce != res_pq.server_nonce:
@ -139,12 +138,12 @@ def _do_authentication(connection):
gab = pow(g_a, b, dh_prime)
# Prepare client DH Inner Data
client_dh_inner = ClientDHInnerData(
client_dh_inner = bytes(ClientDHInnerData(
nonce=res_pq.nonce,
server_nonce=res_pq.server_nonce,
retry_id=0, # TODO Actual retry ID
g_b=rsa.get_byte_array(gb)
).to_bytes()
))
client_dh_inner_hashed = sha1(client_dh_inner).digest() + client_dh_inner
@ -157,7 +156,7 @@ def _do_authentication(connection):
server_nonce=res_pq.server_nonce,
encrypted_data=client_dh_encrypted,
)
sender.send(set_client_dh.to_bytes())
sender.send(bytes(set_client_dh))
# Step 3 response: Complete DH Exchange
with BinaryReader(sender.receive()) as reader:

View File

@ -141,28 +141,25 @@ class Connection:
raise ValueError('Invalid connection mode specified: ' + str(self._mode))
def _recv_tcp_full(self):
packet_length_bytes = self.read(4)
packet_length = int.from_bytes(packet_length_bytes, 'little')
packet_len_seq = self.read(8) # 4 and 4
packet_len, seq = struct.unpack('<ii', packet_len_seq)
seq_bytes = self.read(4)
seq = int.from_bytes(seq_bytes, 'little')
body = self.read(packet_len - 12)
checksum = struct.unpack('<I', self.read(4))[0]
body = self.read(packet_length - 12)
checksum = int.from_bytes(self.read(4), 'little')
valid_checksum = crc32(packet_length_bytes + seq_bytes + body)
valid_checksum = crc32(packet_len_seq + body)
if checksum != valid_checksum:
raise InvalidChecksumError(checksum, valid_checksum)
return body
def _recv_intermediate(self):
return self.read(int.from_bytes(self.read(4), 'little'))
return self.read(struct.unpack('<i', self.read(4))[0])
def _recv_abridged(self):
length = int.from_bytes(self.read(1), 'little')
length = struct.unpack('<B', self.read(1))[0]
if length >= 127:
length = int.from_bytes(self.read(3) + b'\0', 'little')
length = struct.unpack('<i', self.read(3) + b'\0')[0]
return self.read(length << 2)

View File

@ -11,7 +11,10 @@ from ..errors import (
from ..extensions import BinaryReader
from ..tl import TLMessage, MessageContainer, GzipPacked
from ..tl.all_tlobjects import tlobjects
from ..tl.types import MsgsAck
from ..tl.types import (
MsgsAck, Pong, BadServerSalt, BadMsgNotification,
MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo
)
from ..tl.functions.auth import LogOutRequest
logging.getLogger(__name__).addHandler(logging.NullHandler())
@ -36,7 +39,7 @@ class MtProtoSender:
self._logger = logging.getLogger(__name__)
# Message IDs that need confirmation
self._need_confirmation = []
self._need_confirmation = set()
# Requests (as msg_id: Message) sent waiting to be received
self._pending_receive = {}
@ -71,7 +74,7 @@ class MtProtoSender:
# Pack everything in the same container if we need to send AckRequests
if self._need_confirmation:
messages.append(
TLMessage(self.session, MsgsAck(self._need_confirmation))
TLMessage(self.session, MsgsAck(list(self._need_confirmation)))
)
self._need_confirmation.clear()
@ -122,7 +125,7 @@ class MtProtoSender:
plain_text = \
struct.pack('<QQ', self.session.salt, self.session.id) \
+ message.to_bytes()
+ bytes(message)
msg_key = utils.calc_msg_key(plain_text)
key_id = struct.pack('<Q', self.session.auth_key.key_id)
@ -171,7 +174,7 @@ class MtProtoSender:
"""
# TODO Check salt, session_id and sequence_number
self._need_confirmation.append(msg_id)
self._need_confirmation.add(msg_id)
code = reader.read_int(signed=False)
reader.seek(-4)
@ -180,24 +183,33 @@ class MtProtoSender:
if code == 0xf35c6d01: # rpc_result, (response of an RPC call)
return self._handle_rpc_result(msg_id, sequence, reader)
if code == 0x347773c5: # pong
if code == Pong.CONSTRUCTOR_ID:
return self._handle_pong(msg_id, sequence, reader)
if code == 0x73f1f8dc: # msg_container
if code == MessageContainer.CONSTRUCTOR_ID:
return self._handle_container(msg_id, sequence, reader, state)
if code == 0x3072cfa1: # gzip_packed
if code == GzipPacked.CONSTRUCTOR_ID:
return self._handle_gzip_packed(msg_id, sequence, reader, state)
if code == 0xedab447b: # bad_server_salt
if code == BadServerSalt.CONSTRUCTOR_ID:
return self._handle_bad_server_salt(msg_id, sequence, reader)
if code == 0xa7eff811: # bad_msg_notification
if code == BadMsgNotification.CONSTRUCTOR_ID:
return self._handle_bad_msg_notification(msg_id, sequence, reader)
# msgs_ack, it may handle the request we wanted
if code == 0x62d6b459:
if code == MsgDetailedInfo.CONSTRUCTOR_ID:
return self._handle_msg_detailed_info(msg_id, sequence, reader)
if code == MsgNewDetailedInfo.CONSTRUCTOR_ID:
return self._handle_msg_new_detailed_info(msg_id, sequence, reader)
if code == NewSessionCreated.CONSTRUCTOR_ID:
return self._handle_new_session_created(msg_id, sequence, reader)
if code == MsgsAck.CONSTRUCTOR_ID: # may handle the request we wanted
ack = reader.tgread_object()
assert isinstance(ack, MsgsAck)
# Ignore every ack request *unless* when logging out, when it's
# when it seems to only make sense. We also need to set a non-None
# result since Telegram doesn't send the response for these.
@ -205,7 +217,7 @@ class MtProtoSender:
r = self._pop_request_of_type(msg_id, LogOutRequest)
if r:
r.result = True # Telegram won't send this value
r.confirm_received()
r.confirm_received.set()
self._logger.debug('Message ack confirmed', r)
return True
@ -219,7 +231,12 @@ class MtProtoSender:
return True
self._logger.debug('Unknown message: {}'.format(hex(code)))
self._logger.debug(
'[WARN] Unknown message: {}, data left in the buffer: {}'
.format(
hex(code), repr(reader.get_bytes()[reader.tell_position():])
)
)
return False
# endregion
@ -239,22 +256,23 @@ class MtProtoSender:
the given type, or returns None if it's not found/doesn't match.
"""
message = self._pending_receive.get(msg_id, None)
if isinstance(message.request, t):
if message and isinstance(message.request, t):
return self._pending_receive.pop(msg_id).request
def _clear_all_pending(self):
for r in self._pending_receive.values():
r.confirm_received.set()
r.request.confirm_received.set()
self._pending_receive.clear()
def _handle_pong(self, msg_id, sequence, reader):
self._logger.debug('Handling pong')
reader.read_int(signed=False) # code
received_msg_id = reader.read_long()
pong = reader.tgread_object()
assert isinstance(pong, Pong)
request = self._pop_request(received_msg_id)
request = self._pop_request(pong.msg_id)
if request:
self._logger.debug('Pong confirmed a request')
request.result = pong
request.confirm_received.set()
return True
@ -278,14 +296,16 @@ class MtProtoSender:
def _handle_bad_server_salt(self, msg_id, sequence, reader):
self._logger.debug('Handling bad server salt')
reader.read_int(signed=False) # code
bad_msg_id = reader.read_long()
reader.read_int() # bad_msg_seq_no
reader.read_int() # error_code
new_salt = reader.read_long(signed=False)
self.session.salt = new_salt
bad_salt = reader.tgread_object()
assert isinstance(bad_salt, BadServerSalt)
request = self._pop_request(bad_msg_id)
# Our salt is unsigned, but the objects work with signed salts
self.session.salt = struct.unpack(
'<Q', struct.pack('<q', bad_salt.new_server_salt)
)[0]
self.session.save()
request = self._pop_request(bad_salt.bad_msg_id)
if request:
self.send(request)
@ -293,31 +313,53 @@ class MtProtoSender:
def _handle_bad_msg_notification(self, msg_id, sequence, reader):
self._logger.debug('Handling bad message notification')
reader.read_int(signed=False) # code
reader.read_long() # request_id
reader.read_int() # request_sequence
bad_msg = reader.tgread_object()
assert isinstance(bad_msg, BadMsgNotification)
error_code = reader.read_int()
error = BadMessageError(error_code)
if error_code in (16, 17):
error = BadMessageError(bad_msg.error_code)
if bad_msg.error_code in (16, 17):
# sent msg_id too low or too high (respectively).
# Use the current msg_id to determine the right time offset.
self.session.update_time_offset(correct_msg_id=msg_id)
self._logger.debug('Read Bad Message error: ' + str(error))
self._logger.debug('Attempting to use the correct time offset.')
return True
elif error_code == 32:
elif bad_msg.error_code == 32:
# msg_seqno too low, so just pump it up by some "large" amount
# TODO A better fix would be to start with a new fresh session ID
self.session._sequence += 64
return True
elif error_code == 33:
elif bad_msg.error_code == 33:
# msg_seqno too high never seems to happen but just in case
self.session._sequence -= 16
return True
else:
raise error
def _handle_msg_detailed_info(self, msg_id, sequence, reader):
msg_new = reader.tgread_object()
assert isinstance(msg_new, MsgDetailedInfo)
# TODO For now, simply ack msg_new.answer_msg_id
# Relevant tdesktop source code: https://goo.gl/VvpCC6
self._send_acknowledge(msg_new.answer_msg_id)
return True
def _handle_msg_new_detailed_info(self, msg_id, sequence, reader):
msg_new = reader.tgread_object()
assert isinstance(msg_new, MsgNewDetailedInfo)
# TODO For now, simply ack msg_new.answer_msg_id
# Relevant tdesktop source code: https://goo.gl/G7DPsR
self._send_acknowledge(msg_new.answer_msg_id)
return True
def _handle_new_session_created(self, msg_id, sequence, reader):
new_session = reader.tgread_object()
assert isinstance(new_session, NewSessionCreated)
# TODO https://goo.gl/LMyN7A
return True
def _handle_rpc_result(self, msg_id, sequence, reader):
self._logger.debug('Handling RPC result')
reader.read_int(signed=False) # code
@ -346,29 +388,35 @@ class MtProtoSender:
# else TODO Where should this error be reported?
# Read may be async. Can an error not-belong to a request?
self._logger.debug('Read RPC error: %s', str(error))
else:
if request:
self._logger.debug('Reading request response')
if inner_code == 0x3072cfa1: # GZip packed
unpacked_data = gzip.decompress(reader.tgread_bytes())
with BinaryReader(unpacked_data) as compressed_reader:
request.on_response(compressed_reader)
else:
reader.seek(-4)
request.on_response(reader)
return True # All contents were read okay
self.session.process_entities(request.result)
request.confirm_received.set()
return True
elif request:
self._logger.debug('Reading request response')
if inner_code == 0x3072cfa1: # GZip packed
unpacked_data = gzip.decompress(reader.tgread_bytes())
with BinaryReader(unpacked_data) as compressed_reader:
request.on_response(compressed_reader)
else:
# If it's really a result for RPC from previous connection
# session, it will be skipped by the handle_container()
self._logger.debug('Lost request will be skipped.')
return False
reader.seek(-4)
request.on_response(reader)
self.session.process_entities(request.result)
request.confirm_received.set()
return True
# If it's really a result for RPC from previous connection
# session, it will be skipped by the handle_container()
self._logger.debug('Lost request will be skipped.')
return False
def _handle_gzip_packed(self, msg_id, sequence, reader, state):
self._logger.debug('Handling gzip packed data')
with BinaryReader(GzipPacked.read(reader)) as compressed_reader:
# We are reentering process_msg, which seemingly the same msg_id
# to the self._need_confirmation set. Remove it from there first
# to avoid any future conflicts (i.e. if we "ignore" messages
# that we are already aware of, see 1a91c02 and old 63dfb1e)
self._need_confirmation -= {msg_id}
return self._process_msg(msg_id, sequence, compressed_reader, state)
# endregion

View File

@ -1,6 +1,7 @@
import logging
import os
import threading
import warnings
from datetime import timedelta, datetime
from hashlib import md5
from io import BytesIO
@ -57,7 +58,7 @@ class TelegramBareClient:
"""
# Current TelegramClient version
__version__ = '0.15.1'
__version__ = '0.15.3'
# TODO Make this thread-safe, all connections share the same DC
_dc_options = None
@ -131,7 +132,7 @@ class TelegramBareClient:
self._user_connected = False
# Save whether the user is authorized here (a.k.a. logged in)
self._authorized = False
self._authorized = None # None = We don't know yet
# Uploaded files cache so subsequent calls are instant
self._upload_cache = {}
@ -222,12 +223,14 @@ class TelegramBareClient:
# another data center and this would raise UserMigrateError)
# to also assert whether the user is logged in or not.
self._user_connected = True
if _sync_updates and not _cdn:
if self._authorized is None and _sync_updates and not _cdn:
try:
self.sync_updates()
self._set_connected_and_authorized()
except UnauthorizedError:
self._authorized = False
elif self._authorized:
self._set_connected_and_authorized()
return True
@ -270,18 +273,16 @@ class TelegramBareClient:
def disconnect(self):
"""Disconnects from the Telegram server
and stops all the spawned threads"""
self._user_connected = False
self._recv_thread = None
# Stop the workers from the background thread
self._user_connected = False # This will stop recv_thread's loop
self.updates.stop_workers()
# This will trigger a "ConnectionResetError", for subsequent calls
# to read or send (from another thread) and usually, the background
# thread would try restarting the connection but since the
# ._recv_thread = None, it knows it doesn't have to.
# This will trigger a "ConnectionResetError" on the recv_thread,
# which won't attempt reconnecting as ._user_connected is False.
self._sender.disconnect()
if self._recv_thread:
self._recv_thread.join()
# TODO Shall we clear the _exported_sessions, or may be reused?
pass
@ -298,10 +299,13 @@ class TelegramBareClient:
# Assume we are disconnected due to some error, so connect again
with self._reconnect_lock:
# Another thread may have connected again, so check that first
if not self.is_connected():
return self.connect()
else:
if self.is_connected():
return True
try:
return self.connect()
except ConnectionResetError:
return False
else:
self.disconnect()
self.session.auth_key = None # Force creating new auth_key
@ -428,9 +432,18 @@ class TelegramBareClient:
on_main_thread = threading.get_ident() == self._main_thread_ident
if on_main_thread or self._on_read_thread():
sender = self._sender
update_state = self.updates
else:
sender = self._sender.clone()
sender.connect()
# We're on another connection, Telegram will resend all the
# updates that we haven't acknowledged (potentially entering
# an infinite loop if we're calling this in response to an
# update event, as it would be received again and again). So
# to avoid this we will simply not process updates on these
# new temporary connections, as they will be sent and later
# acknowledged over the main connection.
update_state = None
# We should call receive from this thread if there's no background
# thread reading or if the server disconnected us and we're trying
@ -443,8 +456,10 @@ class TelegramBareClient:
if self._background_error and on_main_thread:
raise self._background_error
result = self._invoke(sender, call_receive, *requests)
if result:
result = self._invoke(
sender, call_receive, update_state, *requests
)
if result is not None:
return result
raise ValueError('Number of retries reached 0.')
@ -455,7 +470,7 @@ class TelegramBareClient:
# Let people use client.invoke(SomeRequest()) instead client(...)
invoke = __call__
def _invoke(self, sender, call_receive, *requests):
def _invoke(self, sender, call_receive, update_state, *requests):
try:
# Ensure that we start with no previous errors (i.e. resending)
for x in requests:
@ -475,14 +490,14 @@ class TelegramBareClient:
)
else:
while not all(x.confirm_received.is_set() for x in requests):
sender.receive(update_state=self.updates)
sender.receive(update_state=update_state)
except TimeoutError:
pass # We will just retry
except ConnectionResetError:
if not self._authorized or self._reconnect_lock.locked():
# Only attempt reconnecting if we're authorized and not
if not self._user_connected or self._reconnect_lock.locked():
# Only attempt reconnecting if the user called connect and not
# reconnecting already.
raise
@ -495,10 +510,7 @@ class TelegramBareClient:
else:
while self._user_connected and not self._reconnect():
sleep(0.1) # Retry forever until we can send the request
finally:
if sender != self._sender:
sender.disconnect()
return None
try:
raise next(x.rpc_error for x in requests if x.rpc_error)
@ -525,7 +537,7 @@ class TelegramBareClient:
# be on the very first connection (not authorized, not running),
# but may be an issue for people who actually travel?
self._reconnect(new_dc=e.new_dc)
return self._invoke(sender, call_receive, *requests)
return self._invoke(sender, call_receive, update_state, *requests)
except ServerError as e:
# Telegram is having some issues, just retry
@ -533,10 +545,14 @@ class TelegramBareClient:
'[ERROR] Telegram is having some internal issues', e
)
except FloodWaitError:
sender.disconnect()
self.disconnect()
raise
except FloodWaitError as e:
if e.seconds > self.session.flood_sleep_threshold | 0:
raise
self._logger.debug(
'Sleep of %d seconds below threshold, sleeping' % e.seconds
)
sleep(e.seconds)
# Some really basic functionality
@ -683,10 +699,8 @@ class TelegramBareClient:
cdn_decrypter = None
try:
offset_index = 0
offset = 0
while True:
offset = offset_index * part_size
try:
if cdn_decrypter:
result = cdn_decrypter.get_file()
@ -705,7 +719,7 @@ class TelegramBareClient:
client = self._get_exported_client(e.new_dc)
continue
offset_index += 1
offset += part_size
# If we have received no data (0 bytes), the file is over
# So there is nothing left to download and write
@ -742,10 +756,10 @@ class TelegramBareClient:
def add_update_handler(self, handler):
"""Adds an update handler (a function which takes a TLObject,
an update, as its parameter) and listens for updates"""
sync = not self.updates.handlers
if not self.updates.get_workers:
warnings.warn("There are no update workers running, so adding an update handler will have no effect.")
self.updates.handlers.append(handler)
if sync:
self.sync_updates()
def remove_update_handler(self, handler):
self.updates.handlers.remove(handler)
@ -801,7 +815,9 @@ class TelegramBareClient:
try:
import socks
if isinstance(error, socks.GeneralProxyError):
if isinstance(error, (
socks.GeneralProxyError, socks.ProxyConnectionError
)):
# This is a known error, and it's not related to
# Telegram but rather to the proxy. Disconnect and
# hand it over to the main thread.

View File

@ -15,6 +15,7 @@ from .errors import (
)
from .network import ConnectionMode
from .tl import TLObject
from .tl.custom import Draft
from .tl.entity_database import EntityDatabase
from .tl.functions.account import (
GetPasswordRequest
@ -28,8 +29,8 @@ from .tl.functions.contacts import (
)
from .tl.functions.messages import (
GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest,
SendMessageRequest, GetChatsRequest
)
SendMessageRequest, GetChatsRequest,
GetAllDraftsRequest)
from .tl.functions import channels
from .tl.functions import messages
@ -46,7 +47,7 @@ from .tl.types import (
InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty,
Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto,
InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID,
UpdateNewMessage, UpdateShortSentMessage,
UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage,
PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel)
from .tl.types.messages import DialogsSlice
@ -252,7 +253,7 @@ class TelegramClient(TelegramBareClient):
if limit is None:
limit = float('inf')
dialogs = {} # Use Dialog.top_message as identifier to avoid dupes
dialogs = {} # Use peer id as identifier to avoid dupes
messages = {} # Used later for sorting TODO also return these?
entities = {}
while len(dialogs) < limit:
@ -267,7 +268,7 @@ class TelegramClient(TelegramBareClient):
break
for d in r.dialogs:
dialogs[d.top_message] = d
dialogs[utils.get_peer_id(d.peer, True)] = d
for m in r.messages:
messages[m.id] = m
@ -302,9 +303,20 @@ class TelegramClient(TelegramBareClient):
[utils.find_user_or_chat(d.peer, entities, entities) for d in ds]
)
# endregion
def get_drafts(self): # TODO: Ability to provide a `filter`
"""
Gets all open draft messages.
# region Message requests
Returns a list of custom `Draft` objects that are easy to work with: You can call
`draft.set_message('text')` to change the message, or delete it through `draft.delete()`.
:return List[telethon.tl.custom.Draft]: A list of open drafts
"""
response = self(GetAllDraftsRequest())
self.session.process_entities(response)
self.session.generate_sequence(response.seq)
drafts = [Draft._from_update(self, u) for u in response.updates]
return drafts
def send_message(self,
entity,
@ -347,7 +359,7 @@ class TelegramClient(TelegramBareClient):
break
for update in result.updates:
if isinstance(update, UpdateNewMessage):
if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)):
if update.message.id == msg_id:
return update.message
@ -488,9 +500,13 @@ class TelegramClient(TelegramBareClient):
def send_file(self, entity, file, caption='',
force_document=False, progress_callback=None,
reply_to=None,
attributes=None,
**kwargs):
"""Sends a file to the specified entity.
The file may either be a path, a byte array, or a stream.
Note that if a byte array or a stream is given, a filename
or its type won't be inferred, and it will be sent as an
"unnamed application/octet-stream".
An optional caption can also be specified for said file.
@ -507,6 +523,10 @@ class TelegramClient(TelegramBareClient):
The "reply_to" parameter works exactly as the one on .send_message.
If "attributes" is set to be a list of DocumentAttribute's, these
will override the automatically inferred ones (so that you can
modify the file name of the file sent for instance).
If "is_voice_note" in kwargs, despite its value, and the file is
sent as a document, it will be sent as a voice note.
@ -537,16 +557,28 @@ class TelegramClient(TelegramBareClient):
# Determine mime-type and attributes
# Take the first element by using [0] since it returns a tuple
mime_type = guess_type(file)[0]
attributes = [
attr_dict = {
DocumentAttributeFilename:
DocumentAttributeFilename(os.path.basename(file))
# TODO If the input file is an audio, find out:
# Performer and song title and add DocumentAttributeAudio
]
}
else:
attributes = [DocumentAttributeFilename('unnamed')]
attr_dict = {
DocumentAttributeFilename:
DocumentAttributeFilename('unnamed')
}
if 'is_voice_note' in kwargs:
attributes.append(DocumentAttributeAudio(0, voice=True))
attr_dict[DocumentAttributeAudio] = \
DocumentAttributeAudio(0, voice=True)
# Now override the attributes if any. As we have a dict of
# {cls: instance}, we can override any class with the list
# of attributes provided by the user easily.
if attributes:
for a in attributes:
attr_dict[type(a)] = a
# Ensure we have a mime type, any; but it cannot be None
# 'The "octet-stream" subtype is used to indicate that a body
@ -557,7 +589,7 @@ class TelegramClient(TelegramBareClient):
media = InputMediaUploadedDocument(
file=file_handle,
mime_type=mime_type,
attributes=attributes,
attributes=list(attr_dict.values()),
caption=caption
)
@ -873,24 +905,23 @@ class TelegramClient(TelegramBareClient):
pass
if isinstance(entity, int) or (
isinstance(entity, TLObject) and
isinstance(entity, TLObject) and
# crc32(b'InputPeer') and crc32(b'Peer')
type(entity).SUBCLASS_OF_ID in (0xc91c90b6, 0x2d45687)):
type(entity).SUBCLASS_OF_ID in (0xc91c90b6, 0x2d45687)):
ie = self.get_input_entity(entity)
result = None
if isinstance(ie, InputPeerUser):
result = self(GetUsersRequest([ie]))
self(GetUsersRequest([ie]))
elif isinstance(ie, InputPeerChat):
result = self(GetChatsRequest([ie.chat_id]))
self(GetChatsRequest([ie.chat_id]))
elif isinstance(ie, InputPeerChannel):
result = self(GetChannelsRequest([ie]))
if result:
self.session.process_entities(result)
try:
return self.session.entities[ie]
except KeyError:
pass
self(GetChannelsRequest([ie]))
try:
# session.process_entities has been called in the MtProtoSender
# with the result of these calls, so they should now be on the
# entities database.
return self.session.entities[ie]
except KeyError:
pass
if isinstance(entity, str):
return self._get_entity_from_string(entity)
@ -906,11 +937,11 @@ class TelegramClient(TelegramBareClient):
phone = EntityDatabase.parse_phone(string)
if phone:
entity = phone
self.session.process_entities(self(GetContactsRequest(0)))
self(GetContactsRequest(0))
else:
entity = string.strip('@').lower()
self.session.process_entities(self(ResolveUsernameRequest(entity)))
self(ResolveUsernameRequest(entity))
# MtProtoSender will call .process_entities on the requests made
try:
return self.session.entities[entity]
except KeyError:
@ -956,9 +987,17 @@ class TelegramClient(TelegramBareClient):
)
if self.session.save_entities:
# Not found, look in the dialogs (this will save the users)
self.get_dialogs(limit=None)
# Not found, look in the latest dialogs.
# This is useful if for instance someone just sent a message but
# the updates didn't specify who, as this person or chat should
# be in the latest dialogs.
self(GetDialogsRequest(
offset_date=None,
offset_id=0,
offset_peer=InputPeerEmpty(),
limit=0,
exclude_pinned=True
))
try:
return self.session.entities.get_input_entity(peer)
except KeyError:

View File

@ -0,0 +1 @@
from .draft import Draft

View File

@ -0,0 +1,80 @@
from ..functions.messages import SaveDraftRequest
from ..types import UpdateDraftMessage
class Draft:
"""
Custom class that encapsulates a draft on the Telegram servers, providing
an abstraction to change the message conveniently. The library will return
instances of this class when calling `client.get_drafts()`.
"""
def __init__(self, client, peer, draft):
self._client = client
self._peer = peer
self.text = draft.message
self.date = draft.date
self.no_webpage = draft.no_webpage
self.reply_to_msg_id = draft.reply_to_msg_id
self.entities = draft.entities
@classmethod
def _from_update(cls, client, update):
if not isinstance(update, UpdateDraftMessage):
raise ValueError(
'You can only create a new `Draft` from a corresponding '
'`UpdateDraftMessage` object.'
)
return cls(client=client, peer=update.peer, draft=update.draft)
@property
def entity(self):
return self._client.get_entity(self._peer)
@property
def input_entity(self):
return self._client.get_input_entity(self._peer)
def set_message(self, text, no_webpage=None, reply_to_msg_id=None, entities=None):
"""
Changes the draft message on the Telegram servers. The changes are
reflected in this object. Changing only individual attributes like for
example the `reply_to_msg_id` should be done by providing the current
values of this object, like so:
draft.set_message(
draft.text,
no_webpage=draft.no_webpage,
reply_to_msg_id=NEW_VALUE,
entities=draft.entities
)
:param str text: New text of the draft
:param bool no_webpage: Whether to attach a web page preview
:param int reply_to_msg_id: Message id to reply to
:param list entities: A list of formatting entities
:return bool: `True` on success
"""
result = self._client(SaveDraftRequest(
peer=self._peer,
message=text,
no_webpage=no_webpage,
reply_to_msg_id=reply_to_msg_id,
entities=entities
))
if result:
self.text = text
self.no_webpage = no_webpage
self.reply_to_msg_id = reply_to_msg_id
self.entities = entities
return result
def delete(self):
"""
Deletes this draft
:return bool: `True` on success
"""
return self.set_message(text='')

View File

@ -74,9 +74,7 @@ class EntityDatabase:
getattr(p, 'access_hash', 0) # chats won't have hash
if self.enabled_full:
if isinstance(e, User) \
or isinstance(e, Chat) \
or isinstance(e, Channel):
if isinstance(e, (User, Chat, Channel)):
new.append(e)
except ValueError:
pass
@ -123,47 +121,64 @@ class EntityDatabase:
if phone:
self._username_id[phone] = marked_id
def __getitem__(self, key):
"""Accepts a digit only string as phone number,
otherwise it's treated as an username.
def _parse_key(self, key):
"""Parses the given string, integer or TLObject key into a
marked user ID ready for use on self._entities.
If an integer is given, it's treated as the ID of the desired User.
The ID given won't try to be guessed as the ID of a chat or channel,
as there may be an user with that ID, and it would be unreliable.
If a callable key is given, the entity will be passed to the
function, and if it returns a true-like value, the marked ID
for such entity will be returned.
If a Peer is given (PeerUser, PeerChat, PeerChannel),
its specific entity is retrieved as User, Chat or Channel.
Note that megagroups are channels with .megagroup = True.
Raises ValueError if it cannot be parsed.
"""
if isinstance(key, str):
phone = EntityDatabase.parse_phone(key)
if phone:
return self._phone_id[phone]
else:
key = key.lstrip('@').lower()
return self._entities[self._username_id[key]]
try:
if phone:
return self._phone_id[phone]
else:
return self._username_id[key.lstrip('@').lower()]
except KeyError as e:
raise ValueError() from e
if isinstance(key, int):
return self._entities[key] # normal IDs are assumed users
return key # normal IDs are assumed users
if isinstance(key, TLObject):
sc = type(key).SUBCLASS_OF_ID
if sc == 0x2d45687:
# Subclass of "Peer"
return self._entities[utils.get_peer_id(key, add_mark=True)]
elif sc in {0x2da17977, 0xc5af5d94, 0x6d44b7db}:
# Subclass of "User", "Chat" or "Channel"
return key
return utils.get_peer_id(key, add_mark=True)
raise KeyError(key)
if callable(key):
for k, v in self._entities.items():
if key(v):
return k
raise ValueError()
def __getitem__(self, key):
"""See the ._parse_key() docstring for possible values of the key"""
try:
return self._entities[self._parse_key(key)]
except (ValueError, KeyError) as e:
raise KeyError(key) from e
def __delitem__(self, key):
target = self[key]
del self._entities[key]
if getattr(target, 'username'):
del self._username_id[target.username]
try:
old = self._entities.pop(self._parse_key(key))
# Try removing the username and phone (if pop didn't fail),
# since the entity may have no username or phone, just ignore
# errors. It should be there if we popped the entity correctly.
try:
del self._username_id[getattr(old, 'username', None)]
except KeyError:
pass
# TODO Allow search by name by tokenizing the input and return a list
try:
del self._phone_id[getattr(old, 'phone', None)]
except KeyError:
pass
except (ValueError, KeyError) as e:
raise KeyError(key) from e
@staticmethod
def parse_phone(phone):
@ -177,8 +192,10 @@ class EntityDatabase:
def get_input_entity(self, peer):
try:
i, k = utils.get_peer_id(peer, add_mark=True, get_kind=True)
h = self._input_entities[i]
i = utils.get_peer_id(peer, add_mark=True)
h = self._input_entities[i] # we store the IDs marked
i, k = utils.resolve_id(i) # removes the mark and returns kind
if k == PeerUser:
return InputPeerUser(i, h)
elif k == PeerChat:

View File

@ -13,26 +13,26 @@ class GzipPacked(TLObject):
@staticmethod
def gzip_if_smaller(request):
"""Calls request.to_bytes(), and based on a certain threshold,
"""Calls bytes(request), and based on a certain threshold,
optionally gzips the resulting data. If the gzipped data is
smaller than the original byte array, this is returned instead.
Note that this only applies to content related requests.
"""
data = request.to_bytes()
data = bytes(request)
# TODO This threshold could be configurable
if request.content_related and len(data) > 512:
gzipped = GzipPacked(data).to_bytes()
gzipped = bytes(GzipPacked(data))
return gzipped if len(gzipped) < len(data) else data
else:
return data
def to_bytes(self):
def __bytes__(self):
# TODO Maybe compress level could be an option
return struct.pack('<I', GzipPacked.CONSTRUCTOR_ID) + \
TLObject.serialize_bytes(gzip.compress(self.data))
@staticmethod
def read(reader):
reader.read_int(signed=False) # code
assert reader.read_int(signed=False) == GzipPacked.CONSTRUCTOR_ID
return gzip.decompress(reader.tgread_bytes())

View File

@ -11,10 +11,10 @@ class MessageContainer(TLObject):
self.content_related = False
self.messages = messages
def to_bytes(self):
def __bytes__(self):
return struct.pack(
'<Ii', MessageContainer.CONSTRUCTOR_ID, len(self.messages)
) + b''.join(m.to_bytes() for m in self.messages)
) + b''.join(bytes(m) for m in self.messages)
@staticmethod
def iter_read(reader):

View File

@ -37,6 +37,7 @@ class Session:
self.lang_pack = session.lang_pack
self.report_errors = session.report_errors
self.save_entities = session.save_entities
self.flood_sleep_threshold = session.flood_sleep_threshold
else: # str / None
self.session_user_id = session_user_id
@ -50,6 +51,7 @@ class Session:
self.lang_pack = ''
self.report_errors = True
self.save_entities = True
self.flood_sleep_threshold = 60
# Cross-thread safety
self._seq_no_lock = Lock()

View File

@ -12,6 +12,6 @@ class TLMessage(TLObject):
self.seq_no = session.generate_sequence(request.content_related)
self.request = request
def to_bytes(self):
def __bytes__(self):
body = GzipPacked.gzip_if_smaller(self.request)
return struct.pack('<qii', self.msg_id, self.seq_no, len(body)) + body

View File

@ -1,3 +1,4 @@
from datetime import datetime
from threading import Event
@ -19,18 +20,14 @@ class TLObject:
"""
if indent is None:
if isinstance(obj, TLObject):
children = obj.to_dict(recursive=False)
if children:
return '{}: {}'.format(
type(obj).__name__, TLObject.pretty_format(children)
)
else:
return type(obj).__name__
return '{}({})'.format(type(obj).__name__, ', '.join(
'{}={}'.format(k, TLObject.pretty_format(v))
for k, v in obj.to_dict(recursive=False).items()
))
if isinstance(obj, dict):
return '{{{}}}'.format(', '.join(
'{}: {}'.format(
k, TLObject.pretty_format(v)
) for k, v in obj.items()
'{}: {}'.format(k, TLObject.pretty_format(v))
for k, v in obj.items()
))
elif isinstance(obj, str) or isinstance(obj, bytes):
return repr(obj)
@ -38,31 +35,36 @@ class TLObject:
return '[{}]'.format(
', '.join(TLObject.pretty_format(x) for x in obj)
)
elif isinstance(obj, datetime):
return 'datetime.fromtimestamp({})'.format(obj.timestamp())
else:
return str(obj)
return repr(obj)
else:
result = []
if isinstance(obj, TLObject):
result.append(type(obj).__name__)
children = obj.to_dict(recursive=False)
if children:
result.append(': ')
result.append(TLObject.pretty_format(
obj.to_dict(recursive=False), indent
))
if isinstance(obj, TLObject) or isinstance(obj, dict):
if isinstance(obj, dict):
d = obj
start, end, sep = '{', '}', ': '
else:
d = obj.to_dict(recursive=False)
start, end, sep = '(', ')', '='
result.append(type(obj).__name__)
elif isinstance(obj, dict):
result.append('{\n')
indent += 1
for k, v in obj.items():
result.append(start)
if d:
result.append('\n')
indent += 1
for k, v in d.items():
result.append('\t' * indent)
result.append(k)
result.append(sep)
result.append(TLObject.pretty_format(v, indent))
result.append(',\n')
result.pop() # last ',\n'
indent -= 1
result.append('\n')
result.append('\t' * indent)
result.append(k)
result.append(': ')
result.append(TLObject.pretty_format(v, indent))
result.append(',\n')
indent -= 1
result.append('\t' * indent)
result.append('}')
result.append(end)
elif isinstance(obj, str) or isinstance(obj, bytes):
result.append(repr(obj))
@ -78,8 +80,13 @@ class TLObject:
result.append('\t' * indent)
result.append(']')
elif isinstance(obj, datetime):
result.append('datetime.fromtimestamp(')
result.append(repr(obj.timestamp()))
result.append(')')
else:
result.append(str(obj))
result.append(repr(obj))
return ''.join(result)
@ -118,8 +125,9 @@ class TLObject:
def to_dict(self, recursive=True):
return {}
def to_bytes(self):
def __bytes__(self):
return b''
def on_response(self, reader):
pass
@staticmethod
def from_reader(reader):
return TLObject()

View File

@ -1,4 +1,5 @@
import logging
import pickle
from collections import deque
from datetime import datetime
from threading import RLock, Event, Thread
@ -27,6 +28,7 @@ class UpdateState:
self._updates_lock = RLock()
self._updates_available = Event()
self._updates = deque()
self._latest_updates = deque(maxlen=10)
self._logger = logging.getLogger(__name__)
@ -141,6 +143,26 @@ class UpdateState:
self._state.pts = pts
# TODO There must be a better way to handle updates rather than
# keeping a queue with the latest updates only, and handling
# the 'pts' correctly should be enough. However some updates
# like UpdateUserStatus (even inside UpdateShort) will be called
# repeatedly very often if invoking anything inside an update
# handler. TODO Figure out why.
"""
client = TelegramClient('anon', api_id, api_hash, update_workers=1)
client.connect()
def handle(u):
client.get_me()
client.add_update_handler(handle)
input('Enter to exit.')
"""
data = pickle.dumps(update.to_dict())
if data in self._latest_updates:
return # Duplicated too
self._latest_updates.append(data)
if type(update).SUBCLASS_OF_ID == 0x8af52aac: # crc32(b'Updates')
# Expand "Updates" into "Update", and pass these to callbacks.
# Since .users and .chats have already been processed, we
@ -149,8 +171,7 @@ class UpdateState:
self._updates.append(update.update)
self._updates_available.set()
elif isinstance(update, tl.Updates) or \
isinstance(update, tl.UpdatesCombined):
elif isinstance(update, (tl.Updates, tl.UpdatesCombined)):
self._updates.extend(update.updates)
self._updates_available.set()

View File

@ -20,8 +20,8 @@ from .tl.types import (
GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty,
InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty,
FileLocationUnavailable, InputMediaUploadedDocument,
InputMediaUploadedPhoto,
DocumentAttributeFilename)
InputMediaUploadedPhoto, DocumentAttributeFilename, photos
)
def get_display_name(entity):
@ -37,7 +37,7 @@ def get_display_name(entity):
else:
return '(No name)'
if isinstance(entity, Chat) or isinstance(entity, Channel):
if isinstance(entity, (Chat, Channel)):
return entity.title
return '(unknown)'
@ -51,8 +51,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
isinstance(media, MessageMediaPhoto)):
if isinstance(media, (UserProfilePhoto, ChatPhoto, MessageMediaPhoto)):
return '.jpg'
# Documents will come with a mime type
@ -88,12 +87,10 @@ def get_input_peer(entity, allow_self=True):
else:
return InputPeerUser(entity.id, entity.access_hash)
if any(isinstance(entity, c) for c in (
Chat, ChatEmpty, ChatForbidden)):
if isinstance(entity, (Chat, ChatEmpty, ChatForbidden)):
return InputPeerChat(entity.id)
if any(isinstance(entity, c) for c in (
Channel, ChannelForbidden)):
if isinstance(entity, (Channel, ChannelForbidden)):
return InputPeerChannel(entity.id, entity.access_hash)
# Less common cases
@ -123,7 +120,7 @@ def get_input_channel(entity):
if type(entity).SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel')
return entity
if isinstance(entity, Channel) or isinstance(entity, ChannelForbidden):
if isinstance(entity, (Channel, ChannelForbidden)):
return InputChannel(entity.id, entity.access_hash)
if isinstance(entity, InputPeerChannel):
@ -189,6 +186,9 @@ def get_input_photo(photo):
if type(photo).SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto')
return photo
if isinstance(photo, photos.Photo):
photo = photo.photo
if isinstance(photo, Photo):
return InputPhoto(id=photo.id, access_hash=photo.access_hash)
@ -264,7 +264,7 @@ def get_input_media(media, user_caption=None, is_photo=False):
if isinstance(media, MessageMediaGame):
return InputMediaGame(id=media.game.id)
if isinstance(media, ChatPhoto) or isinstance(media, UserProfilePhoto):
if isinstance(media, (ChatPhoto, UserProfilePhoto)):
if isinstance(media.photo_big, FileLocationUnavailable):
return get_input_media(media.photo_small, is_photo=True)
else:
@ -289,10 +289,9 @@ def get_input_media(media, user_caption=None, is_photo=False):
venue_id=media.venue_id
)
if any(isinstance(media, t) for t in (
if isinstance(media, (
MessageMediaEmpty, MessageMediaUnsupported,
FileLocationUnavailable, ChatPhotoEmpty,
UserProfilePhotoEmpty)):
ChatPhotoEmpty, UserProfilePhotoEmpty, FileLocationUnavailable)):
return InputMediaEmpty()
if isinstance(media, Message):
@ -301,16 +300,14 @@ def get_input_media(media, user_caption=None, is_photo=False):
_raise_cast_fail(media, 'InputMedia')
def get_peer_id(peer, add_mark=False, get_kind=False):
def get_peer_id(peer, add_mark=False):
"""Finds the ID of the given peer, and optionally converts it to
the "bot api" format if 'add_mark' is set to True.
If 'get_kind', the kind will be returned as a second value.
"""
# First we assert it's a Peer TLObject, or early return for integers
if not isinstance(peer, TLObject):
if isinstance(peer, int):
return (peer, PeerUser) if get_kind else peer
return peer
else:
_raise_cast_fail(peer, 'int')
@ -319,25 +316,20 @@ def get_peer_id(peer, add_mark=False, get_kind=False):
peer = get_input_peer(peer, allow_self=False)
# Set the right ID/kind, or raise if the TLObject is not recognised
i, k = None, None
if isinstance(peer, PeerUser) or isinstance(peer, InputPeerUser):
i, k = peer.user_id, PeerUser
elif isinstance(peer, PeerChat) or isinstance(peer, InputPeerChat):
i, k = peer.chat_id, PeerChat
elif isinstance(peer, PeerChannel) or isinstance(peer, InputPeerChannel):
i, k = peer.channel_id, PeerChannel
else:
_raise_cast_fail(peer, 'int')
if add_mark:
if k == PeerChat:
i = -i
elif k == PeerChannel:
if isinstance(peer, (PeerUser, InputPeerUser)):
return peer.user_id
elif isinstance(peer, (PeerChat, InputPeerChat)):
return -peer.chat_id if add_mark else peer.chat_id
elif isinstance(peer, (PeerChannel, InputPeerChannel)):
i = peer.channel_id
if add_mark:
# Concat -100 through math tricks, .to_supergroup() on Madeline
# IDs will be strictly positive -> log works
i = -(i + pow(10, math.floor(math.log10(i) + 3)))
return -(i + pow(10, math.floor(math.log10(i) + 3)))
else:
return i
return (i, k) if get_kind else i # return kind only if get_kind
_raise_cast_fail(peer, 'int')
def resolve_id(marked_id):
@ -384,11 +376,7 @@ def find_message(update):
def get_appropriated_part_size(file_size):
"""Gets the appropriated part size when uploading or downloading files,
given an initial file size"""
if file_size <= 1048576: # 1MB
return 32
if file_size <= 10485760: # 10MB
return 64
if file_size <= 393216000: # 375MB
if file_size <= 104857600: # 100MB
return 128
if file_size <= 786432000: # 750MB
return 256

View File

@ -0,0 +1,65 @@
# These are comments. Spaces around the = are optional. Empty lines ignored.
#CODE=Human readable description
FILE_MIGRATE_X=The file to be accessed is currently stored in DC {}
PHONE_MIGRATE_X=The phone number a user is trying to use for authorization is associated with DC {}
NETWORK_MIGRATE_X=The source IP address is associated with DC {}
USER_MIGRATE_X=The user whose identity is being used to execute queries is associated with DC {}
API_ID_INVALID=The api_id/api_hash combination is invalid
BOT_METHOD_INVALID=The API access for bot users is restricted. The method you tried to invoke cannot be executed as a bot
CDN_METHOD_INVALID=This method cannot be invoked on a CDN server. Refer to https://core.telegram.org/cdn#schema for available methods
CHANNEL_INVALID=Invalid channel object. Make sure to pass the right types, for instance making sure that the request is designed for channels or otherwise look for a different one more suited
CHANNEL_PRIVATE=The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it
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)
CHAT_ID_INVALID=Invalid object ID for a chat. Make sure to pass the right types, for instance making sure that the request is designed for chats (not channels/megagroups) or otherwise look for a different one more suited\nAn example working with a megagroup and AddChatUserRequest, it will fail because megagroups are channels. Use InviteToChannelRequest instead
CONNECTION_LANG_PACK_INVALID=The specified language pack is not valid. This is meant to be used by official applications only so far, leave it empty
CONNECTION_LAYER_INVALID=The very first request must always be InvokeWithLayerRequest
DC_ID_INVALID=This occurs when an authorization is tried to be exported for the same data center one is currently connected to
FIELD_NAME_EMPTY=The field with the name FIELD_NAME is missing
FIELD_NAME_INVALID=The field with the name FIELD_NAME is invalid
FILE_PARTS_INVALID=The number of file parts is invalid
FILE_PART_X_MISSING=Part {} of the file is missing from storage
FILE_PART_INVALID=The file part number is invalid
FIRSTNAME_INVALID=The first name is invalid
INPUT_METHOD_INVALID=The invoked method does not exist anymore or has never existed
INPUT_REQUEST_TOO_LONG=The input request was too long. This may be a bug in the library as it can occur when serializing more bytes than it should (likeappending the vector constructor code at the end of a message)
LASTNAME_INVALID=The last name is invalid
LIMIT_INVALID=An invalid limit was provided. See https://core.telegram.org/api/files#downloading-files
LOCATION_INVALID=The location given for a file was invalid. See https://core.telegram.org/api/files#downloading-files
MD5_CHECKSUM_INVALID=The MD5 check-sums do not match
MESSAGE_EMPTY=Empty or invalid UTF-8 message was sent
MESSAGE_ID_INVALID=The specified message ID is invalid
MESSAGE_TOO_LONG=Message was too long. Current maximum length is 4096 UTF-8 characters
MESSAGE_NOT_MODIFIED=Content of the message was not modified
MSG_WAIT_FAILED=A waiting call returned an error
OFFSET_INVALID=The given offset was invalid, it must be divisible by 1KB. See https://core.telegram.org/api/files#downloading-files
PASSWORD_HASH_INVALID=The password (and thus its hash value) you entered is invalid
PEER_ID_INVALID=An invalid Peer was used. Make sure to pass the right peer type
PHONE_CODE_EMPTY=The phone code is missing
PHONE_CODE_EXPIRED=The confirmation code has expired
PHONE_CODE_HASH_EMPTY=The phone code hash is missing
PHONE_CODE_INVALID=The phone code entered was invalid
PHONE_NUMBER_BANNED=The used phone number has been banned from Telegram and cannot be used anymore. Maybe check https://www.telegram.org/faq_spam
PHONE_NUMBER_INVALID=The phone number is invalid
PHONE_NUMBER_OCCUPIED=The phone number is already in use
PHONE_NUMBER_UNOCCUPIED=The phone number is not yet being used
PHOTO_INVALID_DIMENSIONS=The photo dimensions are invalid
TYPE_CONSTRUCTOR_INVALID=The type constructor is invalid
USERNAME_INVALID=Unacceptable username. Must match r"[a-zA-Z][\w\d]{4,31}"
USERNAME_NOT_MODIFIED=The username is not different from the current username
USERNAME_NOT_OCCUPIED=The username is not in use by anyone else yet
USERNAME_OCCUPIED=The username is already taken
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)
USER_ID_INVALID=Invalid object ID for an user. Make sure to pass the right types, for instance making sure that the request is designed for users or otherwise look for a different one more suited
ACTIVE_USER_REQUIRED=The method is only available to already activated users
AUTH_KEY_INVALID=The key is invalid
AUTH_KEY_PERM_EMPTY=The method is unavailable for temporary authorization key, not bound to permanent
AUTH_KEY_UNREGISTERED=The key is not registered in the system
INVITE_HASH_EXPIRED=The chat the user tried to join has expired and is not valid anymore
SESSION_EXPIRED=The authorization has expired
SESSION_PASSWORD_NEEDED=Two-steps verification is enabled and a password is required
SESSION_REVOKED=The authorization has been invalidated, because of the user terminating all sessions
USER_ALREADY_PARTICIPANT=The authenticated user is already a participant of the chat
USER_DEACTIVATED=The user has been deleted/deactivated
FLOOD_WAIT_X=A wait of {} seconds is required

View File

@ -0,0 +1,170 @@
import json
import re
import urllib.request
from collections import defaultdict
URL = 'https://rpc.pwrtelegram.xyz/?all'
known_base_classes = {
303: 'InvalidDCError',
400: 'BadRequestError',
401: 'UnauthorizedError',
403: 'ForbiddenError',
404: 'NotFoundError',
420: 'FloodError',
500: 'ServerError',
}
# The API doesn't return the code for some (vital) errors. They are
# all assumed to be 400, except these well-known ones that aren't.
known_codes = {
'ACTIVE_USER_REQUIRED': 401,
'AUTH_KEY_UNREGISTERED': 401,
'USER_DEACTIVATED': 401
}
def fetch_errors(output, url=URL):
print('Opening a connection to', url, '...')
r = urllib.request.urlopen(url)
print('Checking response...')
data = json.loads(
r.read().decode(r.info().get_param('charset') or 'utf-8')
)
if data.get('ok'):
print('Response was okay, saving data')
with open(output, 'w', encoding='utf-8') as f:
json.dump(data, f)
return True
else:
print('The data received was not okay:')
print(json.dumps(data, indent=4))
return False
def get_class_name(error_code):
if isinstance(error_code, int):
return known_base_classes.get(
error_code, 'RPCError' + str(error_code).replace('-', 'Neg')
)
if 'FIRSTNAME' in error_code:
error_code = error_code.replace('FIRSTNAME', 'FIRST_NAME')
result = re.sub(
r'_([a-z])', lambda m: m.group(1).upper(), error_code.lower()
)
return result[:1].upper() + result[1:].replace('_', '') + 'Error'
def write_error(f, code, name, desc, capture_name):
f.write(
f'\n'
f'\n'
f'class {name}({get_class_name(code)}):\n'
f' def __init__(self, **kwargs):\n'
f' '
)
if capture_name:
f.write(
f"self.{capture_name} = int(kwargs.get('capture', 0))\n"
f" "
)
f.write(f'super(Exception, self).__init__(self, {repr(desc)}')
if capture_name:
f.write(f'.format(self.{capture_name})')
f.write(')\n')
def generate_code(output, json_file, errors_desc):
with open(json_file, encoding='utf-8') as f:
data = json.load(f)
errors = defaultdict(set)
# PWRTelegram's API doesn't return all errors, which we do need here.
# Add some special known-cases manually first.
errors[420].add('FLOOD_WAIT_X')
errors[401].update((
'AUTH_KEY_INVALID', 'SESSION_EXPIRED', 'SESSION_REVOKED'
))
errors[303].update((
'FILE_MIGRATE_X', 'PHONE_MIGRATE_X',
'NETWORK_MIGRATE_X', 'USER_MIGRATE_X'
))
for error_code, method_errors in data['result'].items():
for error_list in method_errors.values():
for error in error_list:
errors[int(error_code)].add(re.sub('_\d+', '_X', error).upper())
# Some errors are in the human result, but not with a code. Assume code 400
for error in data['human_result']:
if error[0] != '-' and not error.isdigit():
error = re.sub('_\d+', '_X', error).upper()
if not any(error in es for es in errors.values()):
errors[known_codes.get(error, 400)].add(error)
# Some error codes are not known, so create custom base classes if needed
needed_base_classes = [
(e, get_class_name(e)) for e in errors if e not in known_base_classes
]
# Prefer the descriptions that are related with Telethon way of coding to
# those that PWRTelegram's API provides.
telethon_descriptions = {}
with open(errors_desc, encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
equal = line.index('=')
message, description = line[:equal], line[equal + 1:]
telethon_descriptions[message.rstrip()] = description.lstrip()
# Names for the captures, or 'x' if unknown
capture_names = {
'FloodWaitError': 'seconds',
'FileMigrateError': 'new_dc',
'NetworkMigrateError': 'new_dc',
'PhoneMigrateError': 'new_dc',
'UserMigrateError': 'new_dc',
'FilePartMissingError': 'which'
}
# Everything ready, generate the code
with open(output, 'w', encoding='utf-8') as f:
f.write(
f'from .rpc_base_errors import RPCError, BadMessageError, '
f'{", ".join(known_base_classes.values())}\n'
)
for code, cls in needed_base_classes:
f.write(
f'\n'
f'\n'
f'class {cls}(RPCError):\n'
f' code = {code}\n'
)
patterns = [] # Save this dictionary later in the generated code
for error_code, error_set in errors.items():
for error in sorted(error_set):
description = telethon_descriptions.get(
error, '\n'.join(data['human_result'].get(
error, ['No description known.']
))
)
has_captures = '_X' in error
if has_captures:
name = get_class_name(error.replace('_X', ''))
pattern = error.replace('_X', r'_(\d+)')
else:
name, pattern = get_class_name(error), error
patterns.append((pattern, name))
capture = capture_names.get(name, 'x') if has_captures else None
# TODO Some errors have the same name but different code,
# split this accross different files?
write_error(f, error_code, name, description, capture)
f.write('\n\nrpc_errors_all = {\n')
for pattern, name in patterns:
f.write(f' {repr(pattern)}: {name},\n')
f.write('}\n')

File diff suppressed because one or more lines are too long

View File

@ -98,12 +98,17 @@ class TLObject:
def class_name(self):
"""Gets the class name following the Python style guidelines"""
return self.class_name_for(self.name, self.is_function)
@staticmethod
def class_name_for(typename, is_function=False):
"""Gets the class name following the Python style guidelines"""
# Courtesy of http://stackoverflow.com/a/31531797/4759433
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), self.name)
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(),
typename)
result = result[:1].upper() + result[1:].replace('_', '')
# If it's a function, let it end with "Request" to identify them
if self.is_function:
if is_function:
result += 'Request'
return result
@ -192,6 +197,7 @@ class TLArg:
# Default values
self.is_vector = False
self.is_flag = False
self.skip_constructor_id = False
self.flag_index = -1
# Special case: some types can be inferred, which makes it
@ -222,7 +228,7 @@ class TLArg:
self.type = flag_match.group(2)
# Then check if the type is a Vector<REAL_TYPE>
vector_match = re.match(r'vector<(\w+)>', self.type, re.IGNORECASE)
vector_match = re.match(r'[Vv]ector<([\w\d.]+)>', self.type)
if vector_match:
self.is_vector = True
@ -234,6 +240,11 @@ class TLArg:
# Update the type to match the one inside the vector
self.type = vector_match.group(1)
# See use_vector_id. An example of such case is ipPort in
# help.configSpecial
if self.type.split('.')[-1][0].islower():
self.skip_constructor_id = True
# 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.
# Note that this is not a valid Telegram object, but it's easier to work with

View File

@ -5,7 +5,7 @@ import struct
from zlib import crc32
from collections import defaultdict
from .parser import SourceBuilder, TLParser
from .parser import SourceBuilder, TLParser, TLObject
AUTO_GEN_NOTICE = \
'"""File generated by TLObjects\' generator. All changes will be ERASED"""'
@ -129,6 +129,9 @@ class TLGenerator:
builder.writeln(
'from {}.tl.tlobject import TLObject'.format('.' * depth)
)
builder.writeln(
'from {}.tl import types'.format('.' * depth)
)
# Add the relative imports to the namespaces,
# unless we already are in a namespace.
@ -143,7 +146,7 @@ class TLGenerator:
builder.writeln(
'from {}.utils import get_input_peer, '
'get_input_channel, get_input_user, '
'get_input_media'.format('.' * depth)
'get_input_media, get_input_photo'.format('.' * depth)
)
# Import 'os' for those needing access to 'os.urandom()'
@ -151,7 +154,7 @@ class TLGenerator:
# for all those TLObjects with arg.can_be_inferred.
builder.writeln('import os')
# Import struct for the .to_bytes(self) serialization
# Import struct for the .__bytes__(self) serialization
builder.writeln('import struct')
# Generate the class for every TLObject
@ -299,8 +302,8 @@ class TLGenerator:
builder.end_block()
# Write the .to_bytes() function
builder.writeln('def to_bytes(self):')
# Write the .__bytes__() function
builder.writeln('def __bytes__(self):')
# Some objects require more than one flag parameter to be set
# at the same time. In this case, add an assertion.
@ -311,11 +314,11 @@ class TLGenerator:
for ra in repeated_args.values():
if len(ra) > 1:
cnd1 = ('self.{} is None'.format(a.name) for a in ra)
cnd2 = ('self.{} is not None'.format(a.name) for a in ra)
cnd1 = ('self.{}'.format(a.name) for a in ra)
cnd2 = ('not self.{}'.format(a.name) for a in ra)
builder.writeln(
"assert ({}) or ({}), '{} parameters must all "
"be None or neither be None'".format(
"be False-y (like None) or all me True-y'".format(
' and '.join(cnd1), ' and '.join(cnd2),
', '.join(a.name for a in ra)
)
@ -335,32 +338,28 @@ class TLGenerator:
builder.writeln('))')
builder.end_block()
# Write the empty() function, which returns an "empty"
# instance, in which all attributes are set to None
# Write the static from_reader(reader) function
builder.writeln('@staticmethod')
builder.writeln('def empty():')
builder.writeln('def from_reader(reader):')
for arg in tlobject.args:
TLGenerator.write_read_code(
builder, arg, tlobject.args, name='_' + arg.name
)
builder.writeln('return {}({})'.format(
tlobject.class_name(), ', '.join('None' for _ in range(len(args)))
tlobject.class_name(), ', '.join(
'{0}=_{0}'.format(a.name) for a in tlobject.sorted_args()
if not a.flag_indicator and not a.generic_definition
)
))
builder.end_block()
# Write the on_response(self, reader) function
builder.writeln('def on_response(self, reader):')
# Do not read constructor's ID, since
# that's already been read somewhere else
# Only requests can have a different response that's not their
# serialized body, that is, we'll be setting their .result.
if tlobject.is_function:
builder.writeln('def on_response(self, reader):')
TLGenerator.write_request_result_code(builder, tlobject)
else:
if tlobject.args:
for arg in 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')
builder.end_block()
builder.end_block()
# Write the __str__(self) and stringify(self) functions
builder.writeln('def __str__(self):')
@ -406,6 +405,8 @@ class TLGenerator:
TLGenerator.write_get_input(builder, arg, 'get_input_user')
elif arg.type == 'InputMedia' and tlobject.is_function:
TLGenerator.write_get_input(builder, arg, 'get_input_media')
elif arg.type == 'InputPhoto' and tlobject.is_function:
TLGenerator.write_get_input(builder, arg, 'get_input_photo')
else:
builder.writeln('self.{0} = {0}'.format(arg.name))
@ -440,10 +441,10 @@ class TLGenerator:
@staticmethod
def write_to_bytes(builder, arg, args, name=None):
"""
Writes the .to_bytes() code for the given argument
Writes the .__bytes__() code for the given argument
:param builder: The source code builder
:param arg: The argument to write
:param args: All the other arguments in TLObject same to_bytes.
:param args: All the other arguments in TLObject same __bytes__.
This is required to determine the flags value
:param name: The name of the argument. Defaults to "self.argname"
This argument is an option because it's required when
@ -539,7 +540,7 @@ class TLGenerator:
else:
# Else it may be a custom type
builder.write('{}.to_bytes()'.format(name))
builder.write('bytes({})'.format(name))
if arg.is_flag:
builder.write(')')
@ -549,9 +550,10 @@ class TLGenerator:
return True # Something was written
@staticmethod
def write_onresponse_code(builder, arg, args, name=None):
def write_read_code(builder, arg, args, name):
"""
Writes the receive code for the given argument
Writes the read code for the given argument, setting the
arg.name variable to its read value.
:param builder: The source code builder
:param arg: The argument to write
@ -565,12 +567,17 @@ class TLGenerator:
if arg.generic_definition:
return # Do nothing, this only specifies a later type
if name is None:
name = 'self.{}'.format(arg.name)
# The argument may be a flag, only write that flag was given!
was_flag = False
if arg.is_flag:
# Treat 'true' flags as a special case, since they're true if
# they're set, and nothing else needs to actually be read.
if 'true' == arg.type:
builder.writeln(
'{} = bool(flags & {})'.format(name, 1 << arg.flag_index)
)
return
was_flag = True
builder.writeln('if flags & {}:'.format(
1 << arg.flag_index
@ -585,11 +592,10 @@ class TLGenerator:
builder.writeln("reader.read_int()")
builder.writeln('{} = []'.format(name))
builder.writeln('_len = reader.read_int()')
builder.writeln('for _ in range(_len):')
builder.writeln('for _ in range(reader.read_int()):')
# Temporary disable .is_vector, not to enter this if again
arg.is_vector = False
TLGenerator.write_onresponse_code(builder, arg, args, name='_x')
TLGenerator.write_read_code(builder, arg, args, name='_x')
builder.writeln('{}.append(_x)'.format(name))
arg.is_vector = True
@ -635,14 +641,21 @@ class TLGenerator:
else:
# Else it may be a custom type
builder.writeln('{} = reader.tgread_object()'.format(name))
if not arg.skip_constructor_id:
builder.writeln('{} = reader.tgread_object()'.format(name))
else:
builder.writeln('{} = types.{}.from_reader(reader)'.format(
name, TLObject.class_name_for(arg.type)))
# End vector and flag blocks if required (if we opened them before)
if arg.is_vector:
builder.end_block()
if was_flag:
builder.end_block()
builder.current_indent -= 1
builder.writeln('else:')
builder.writeln('{} = None'.format(name))
builder.current_indent -= 1
# Restore .is_flag
arg.is_flag = True

View File

@ -107,17 +107,17 @@ class CryptoTests(unittest.TestCase):
@staticmethod
def test_generate_key_data_from_nonce():
server_nonce = b'I am the server nonce.'
new_nonce = b'I am a new calculated nonce.'
server_nonce = int.from_bytes(b'The 16-bit nonce', byteorder='little')
new_nonce = int.from_bytes(b'The new, calculated 32-bit nonce', byteorder='little')
key, iv = utils.generate_key_data_from_nonce(server_nonce, new_nonce)
expected_key = b'?\xc4\xbd\xdf\rWU\x8a\xf5\x0f+V\xdc\x96up\x1d\xeeG\x00\x81|\x1eg\x8a\x8f{\xf0y\x80\xda\xde'
expected_iv = b'Q\x9dpZ\xb7\xdd\xcb\x82_\xfa\xf4\x90\xecn\x10\x9cD\xd2\x01\x8d\x83\xa0\xa4^\xb8\x91,\x7fI am'
expected_key = b'/\xaa\x7f\xa1\xfcs\xef\xa0\x99zh\x03M\xa4\x8e\xb4\xab\x0eE]b\x95|\xfe\xc0\xf8\x1f\xd4\xa0\xd4\xec\x91'
expected_iv = b'\xf7\xae\xe3\xc8+=\xc2\xb8\xd1\xe1\x1b\x0e\x10\x07\x9fn\x9e\xdc\x960\x05\xf9\xea\xee\x8b\xa1h The '
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 iv == expected_iv, 'IV ("{}") does not equal expected ("{}")'.format(
iv, expected_iv)
@staticmethod
def test_fingerprint_from_key():