mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-03 03:30:09 +03:00
Merge remote-tracking branch 'remotes/upstream/master'
This commit is contained in:
commit
b7c72f18fd
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,6 +2,7 @@
|
|||
telethon/tl/functions/
|
||||
telethon/tl/types/
|
||||
telethon/tl/all_tlobjects.py
|
||||
telethon/errors/rpc_error_list.py
|
||||
|
||||
# User session
|
||||
*.session
|
||||
|
|
23
setup.py
23
setup.py
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"""
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
1
telethon/tl/custom/__init__.py
Normal file
1
telethon/tl/custom/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .draft import Draft
|
80
telethon/tl/custom/draft.py
Normal file
80
telethon/tl/custom/draft.py
Normal 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='')
|
|
@ -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:
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
65
telethon_generator/error_descriptions
Normal file
65
telethon_generator/error_descriptions
Normal 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
|
170
telethon_generator/error_generator.py
Normal file
170
telethon_generator/error_generator.py
Normal 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')
|
1
telethon_generator/errors.json
Normal file
1
telethon_generator/errors.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
Loading…
Reference in New Issue
Block a user