mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-04 12:10:21 +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/functions/
|
||||||
telethon/tl/types/
|
telethon/tl/types/
|
||||||
telethon/tl/all_tlobjects.py
|
telethon/tl/all_tlobjects.py
|
||||||
|
telethon/errors/rpc_error_list.py
|
||||||
|
|
||||||
# User session
|
# User session
|
||||||
*.session
|
*.session
|
||||||
|
|
23
setup.py
23
setup.py
|
@ -42,17 +42,26 @@ class TempWorkDir:
|
||||||
os.chdir(self.original)
|
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():
|
def gen_tl():
|
||||||
from telethon_generator.tl_generator import TLGenerator
|
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():
|
if generator.tlobjects_exist():
|
||||||
print('Detected previous TLObjects. Cleaning...')
|
print('Detected previous TLObjects. Cleaning...')
|
||||||
generator.clean_tlobjects()
|
generator.clean_tlobjects()
|
||||||
|
|
||||||
print('Generating TLObjects...')
|
print('Generating TLObjects...')
|
||||||
generator.generate_tlobjects(
|
generator.generate_tlobjects(SCHEME_TL, import_depth=IMPORT_DEPTH)
|
||||||
'telethon_generator/scheme.tl', import_depth=2
|
print('Generating errors...')
|
||||||
)
|
generate_code(ERROR_LIST, json_file=ERRORS_JSON, errors_desc=ERRORS_DESC)
|
||||||
print('Done.')
|
print('Done.')
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,7 +72,7 @@ def main():
|
||||||
elif len(argv) >= 2 and argv[1] == 'clean_tl':
|
elif len(argv) >= 2 and argv[1] == 'clean_tl':
|
||||||
from telethon_generator.tl_generator import TLGenerator
|
from telethon_generator.tl_generator import TLGenerator
|
||||||
print('Cleaning...')
|
print('Cleaning...')
|
||||||
TLGenerator('telethon/tl').clean_tlobjects()
|
TLGenerator(GENERATOR_DIR).clean_tlobjects()
|
||||||
print('Done.')
|
print('Done.')
|
||||||
|
|
||||||
elif len(argv) >= 2 and argv[1] == 'pypi':
|
elif len(argv) >= 2 and argv[1] == 'pypi':
|
||||||
|
@ -80,6 +89,10 @@ def main():
|
||||||
for x in ('build', 'dist', 'Telethon.egg-info'):
|
for x in ('build', 'dist', 'Telethon.egg-info'):
|
||||||
rmtree(x, ignore_errors=True)
|
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:
|
else:
|
||||||
if not TelegramClient:
|
if not TelegramClient:
|
||||||
gen_tl()
|
gen_tl()
|
||||||
|
|
|
@ -8,15 +8,8 @@ from .common import (
|
||||||
CdnFileTamperedError
|
CdnFileTamperedError
|
||||||
)
|
)
|
||||||
|
|
||||||
from .rpc_errors import (
|
# This imports the base errors too, as they're imported there
|
||||||
RPCError, InvalidDCError, BadRequestError, UnauthorizedError,
|
from .rpc_error_list import *
|
||||||
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 *
|
|
||||||
|
|
||||||
|
|
||||||
def report_error(code, message, report_method):
|
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)
|
args=(code, message, report_method)
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
errors = {
|
# Try to get the error by direct look-up, otherwise regex
|
||||||
303: rpc_errors_303_all,
|
# TODO Maybe regexes could live in a separate dictionary?
|
||||||
400: rpc_errors_400_all,
|
cls = rpc_errors_all.get(message, None)
|
||||||
401: rpc_errors_401_all,
|
if cls:
|
||||||
420: rpc_errors_420_all
|
return cls()
|
||||||
}.get(code, None)
|
|
||||||
|
|
||||||
if errors is not None:
|
for msg_regex, cls in rpc_errors_all.items():
|
||||||
for msg, cls in errors.items():
|
m = re.match(msg_regex, message)
|
||||||
m = re.match(msg, message)
|
if m:
|
||||||
if m:
|
capture = int(m.group(1)) if m.groups() else None
|
||||||
extra = int(m.group(1)) if m.groups() else None
|
return cls(capture=capture)
|
||||||
return cls(extra=extra)
|
|
||||||
|
|
||||||
elif code == 403:
|
if code == 400:
|
||||||
|
return BadRequestError(message)
|
||||||
|
|
||||||
|
if code == 401:
|
||||||
|
return UnauthorizedError(message)
|
||||||
|
|
||||||
|
if code == 403:
|
||||||
return ForbiddenError(message)
|
return ForbiddenError(message)
|
||||||
|
|
||||||
elif code == 404:
|
if code == 404:
|
||||||
return NotFoundError(message)
|
return NotFoundError(message)
|
||||||
|
|
||||||
elif code == 500:
|
if code == 500:
|
||||||
return ServerError(message)
|
return ServerError(message)
|
||||||
|
|
||||||
return RPCError('{} (code {})'.format(message, code))
|
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
|
return False
|
||||||
|
|
||||||
# If there was still no luck, give up
|
# If there was still no luck, give up
|
||||||
|
self.seek(-4) # Go back
|
||||||
raise TypeNotFoundError(constructor_id)
|
raise TypeNotFoundError(constructor_id)
|
||||||
|
|
||||||
# Create an empty instance of the class and
|
return clazz.from_reader(self)
|
||||||
# fill it with the read attributes
|
|
||||||
result = clazz.empty()
|
|
||||||
result.on_response(self)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def tgread_vector(self):
|
def tgread_vector(self):
|
||||||
"""Reads a vector (a list) of Telegram objects"""
|
"""Reads a vector (a list) of Telegram objects"""
|
||||||
|
|
|
@ -14,7 +14,7 @@ class TcpClient:
|
||||||
|
|
||||||
if isinstance(timeout, timedelta):
|
if isinstance(timeout, timedelta):
|
||||||
self.timeout = timeout.seconds
|
self.timeout = timeout.seconds
|
||||||
elif isinstance(timeout, int) or isinstance(timeout, float):
|
elif isinstance(timeout, (int, float)):
|
||||||
self.timeout = float(timeout)
|
self.timeout = float(timeout)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Invalid timeout type', type(timeout))
|
raise ValueError('Invalid timeout type', type(timeout))
|
||||||
|
|
|
@ -42,7 +42,7 @@ def _do_authentication(connection):
|
||||||
req_pq_request = ReqPqRequest(
|
req_pq_request = ReqPqRequest(
|
||||||
nonce=int.from_bytes(os.urandom(16), 'big', signed=True)
|
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:
|
with BinaryReader(sender.receive()) as reader:
|
||||||
req_pq_request.on_response(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))
|
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)
|
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,
|
pq=rsa.get_byte_array(pq), p=p, q=q,
|
||||||
nonce=res_pq.nonce,
|
nonce=res_pq.nonce,
|
||||||
server_nonce=res_pq.server_nonce,
|
server_nonce=res_pq.server_nonce,
|
||||||
new_nonce=new_nonce
|
new_nonce=new_nonce
|
||||||
).to_bytes()
|
))
|
||||||
|
|
||||||
# sha_digest + data + random_bytes
|
# sha_digest + data + random_bytes
|
||||||
cipher_text, target_fingerprint = None, None
|
cipher_text, target_fingerprint = None, None
|
||||||
|
@ -90,7 +90,7 @@ def _do_authentication(connection):
|
||||||
public_key_fingerprint=target_fingerprint,
|
public_key_fingerprint=target_fingerprint,
|
||||||
encrypted_data=cipher_text
|
encrypted_data=cipher_text
|
||||||
)
|
)
|
||||||
sender.send(req_dh_params.to_bytes())
|
sender.send(bytes(req_dh_params))
|
||||||
|
|
||||||
# Step 2 response: DH Exchange
|
# Step 2 response: DH Exchange
|
||||||
with BinaryReader(sender.receive()) as reader:
|
with BinaryReader(sender.receive()) as reader:
|
||||||
|
@ -124,7 +124,6 @@ def _do_authentication(connection):
|
||||||
raise AssertionError(server_dh_inner)
|
raise AssertionError(server_dh_inner)
|
||||||
|
|
||||||
if server_dh_inner.nonce != res_pq.nonce:
|
if server_dh_inner.nonce != res_pq.nonce:
|
||||||
print(server_dh_inner.nonce, res_pq.nonce)
|
|
||||||
raise SecurityError('Invalid nonce in encrypted answer')
|
raise SecurityError('Invalid nonce in encrypted answer')
|
||||||
|
|
||||||
if server_dh_inner.server_nonce != res_pq.server_nonce:
|
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)
|
gab = pow(g_a, b, dh_prime)
|
||||||
|
|
||||||
# Prepare client DH Inner Data
|
# Prepare client DH Inner Data
|
||||||
client_dh_inner = ClientDHInnerData(
|
client_dh_inner = bytes(ClientDHInnerData(
|
||||||
nonce=res_pq.nonce,
|
nonce=res_pq.nonce,
|
||||||
server_nonce=res_pq.server_nonce,
|
server_nonce=res_pq.server_nonce,
|
||||||
retry_id=0, # TODO Actual retry ID
|
retry_id=0, # TODO Actual retry ID
|
||||||
g_b=rsa.get_byte_array(gb)
|
g_b=rsa.get_byte_array(gb)
|
||||||
).to_bytes()
|
))
|
||||||
|
|
||||||
client_dh_inner_hashed = sha1(client_dh_inner).digest() + client_dh_inner
|
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,
|
server_nonce=res_pq.server_nonce,
|
||||||
encrypted_data=client_dh_encrypted,
|
encrypted_data=client_dh_encrypted,
|
||||||
)
|
)
|
||||||
sender.send(set_client_dh.to_bytes())
|
sender.send(bytes(set_client_dh))
|
||||||
|
|
||||||
# Step 3 response: Complete DH Exchange
|
# Step 3 response: Complete DH Exchange
|
||||||
with BinaryReader(sender.receive()) as reader:
|
with BinaryReader(sender.receive()) as reader:
|
||||||
|
|
|
@ -141,28 +141,25 @@ class Connection:
|
||||||
raise ValueError('Invalid connection mode specified: ' + str(self._mode))
|
raise ValueError('Invalid connection mode specified: ' + str(self._mode))
|
||||||
|
|
||||||
def _recv_tcp_full(self):
|
def _recv_tcp_full(self):
|
||||||
packet_length_bytes = self.read(4)
|
packet_len_seq = self.read(8) # 4 and 4
|
||||||
packet_length = int.from_bytes(packet_length_bytes, 'little')
|
packet_len, seq = struct.unpack('<ii', packet_len_seq)
|
||||||
|
|
||||||
seq_bytes = self.read(4)
|
body = self.read(packet_len - 12)
|
||||||
seq = int.from_bytes(seq_bytes, 'little')
|
checksum = struct.unpack('<I', self.read(4))[0]
|
||||||
|
|
||||||
body = self.read(packet_length - 12)
|
valid_checksum = crc32(packet_len_seq + body)
|
||||||
checksum = int.from_bytes(self.read(4), 'little')
|
|
||||||
|
|
||||||
valid_checksum = crc32(packet_length_bytes + seq_bytes + body)
|
|
||||||
if checksum != valid_checksum:
|
if checksum != valid_checksum:
|
||||||
raise InvalidChecksumError(checksum, valid_checksum)
|
raise InvalidChecksumError(checksum, valid_checksum)
|
||||||
|
|
||||||
return body
|
return body
|
||||||
|
|
||||||
def _recv_intermediate(self):
|
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):
|
def _recv_abridged(self):
|
||||||
length = int.from_bytes(self.read(1), 'little')
|
length = struct.unpack('<B', self.read(1))[0]
|
||||||
if length >= 127:
|
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)
|
return self.read(length << 2)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,10 @@ from ..errors import (
|
||||||
from ..extensions import BinaryReader
|
from ..extensions import BinaryReader
|
||||||
from ..tl import TLMessage, MessageContainer, GzipPacked
|
from ..tl import TLMessage, MessageContainer, GzipPacked
|
||||||
from ..tl.all_tlobjects import tlobjects
|
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
|
from ..tl.functions.auth import LogOutRequest
|
||||||
|
|
||||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||||
|
@ -36,7 +39,7 @@ class MtProtoSender:
|
||||||
self._logger = logging.getLogger(__name__)
|
self._logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Message IDs that need confirmation
|
# Message IDs that need confirmation
|
||||||
self._need_confirmation = []
|
self._need_confirmation = set()
|
||||||
|
|
||||||
# Requests (as msg_id: Message) sent waiting to be received
|
# Requests (as msg_id: Message) sent waiting to be received
|
||||||
self._pending_receive = {}
|
self._pending_receive = {}
|
||||||
|
@ -71,7 +74,7 @@ class MtProtoSender:
|
||||||
# Pack everything in the same container if we need to send AckRequests
|
# Pack everything in the same container if we need to send AckRequests
|
||||||
if self._need_confirmation:
|
if self._need_confirmation:
|
||||||
messages.append(
|
messages.append(
|
||||||
TLMessage(self.session, MsgsAck(self._need_confirmation))
|
TLMessage(self.session, MsgsAck(list(self._need_confirmation)))
|
||||||
)
|
)
|
||||||
self._need_confirmation.clear()
|
self._need_confirmation.clear()
|
||||||
|
|
||||||
|
@ -122,7 +125,7 @@ class MtProtoSender:
|
||||||
|
|
||||||
plain_text = \
|
plain_text = \
|
||||||
struct.pack('<QQ', self.session.salt, self.session.id) \
|
struct.pack('<QQ', self.session.salt, self.session.id) \
|
||||||
+ message.to_bytes()
|
+ bytes(message)
|
||||||
|
|
||||||
msg_key = utils.calc_msg_key(plain_text)
|
msg_key = utils.calc_msg_key(plain_text)
|
||||||
key_id = struct.pack('<Q', self.session.auth_key.key_id)
|
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
|
# 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)
|
code = reader.read_int(signed=False)
|
||||||
reader.seek(-4)
|
reader.seek(-4)
|
||||||
|
@ -180,24 +183,33 @@ class MtProtoSender:
|
||||||
if code == 0xf35c6d01: # rpc_result, (response of an RPC call)
|
if code == 0xf35c6d01: # rpc_result, (response of an RPC call)
|
||||||
return self._handle_rpc_result(msg_id, sequence, reader)
|
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)
|
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)
|
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)
|
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)
|
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)
|
return self._handle_bad_msg_notification(msg_id, sequence, reader)
|
||||||
|
|
||||||
# msgs_ack, it may handle the request we wanted
|
if code == MsgDetailedInfo.CONSTRUCTOR_ID:
|
||||||
if code == 0x62d6b459:
|
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()
|
ack = reader.tgread_object()
|
||||||
|
assert isinstance(ack, MsgsAck)
|
||||||
# Ignore every ack request *unless* when logging out, when it's
|
# 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
|
# 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.
|
# 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)
|
r = self._pop_request_of_type(msg_id, LogOutRequest)
|
||||||
if r:
|
if r:
|
||||||
r.result = True # Telegram won't send this value
|
r.result = True # Telegram won't send this value
|
||||||
r.confirm_received()
|
r.confirm_received.set()
|
||||||
self._logger.debug('Message ack confirmed', r)
|
self._logger.debug('Message ack confirmed', r)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -219,7 +231,12 @@ class MtProtoSender:
|
||||||
|
|
||||||
return True
|
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
|
return False
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
@ -239,22 +256,23 @@ class MtProtoSender:
|
||||||
the given type, or returns None if it's not found/doesn't match.
|
the given type, or returns None if it's not found/doesn't match.
|
||||||
"""
|
"""
|
||||||
message = self._pending_receive.get(msg_id, None)
|
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
|
return self._pending_receive.pop(msg_id).request
|
||||||
|
|
||||||
def _clear_all_pending(self):
|
def _clear_all_pending(self):
|
||||||
for r in self._pending_receive.values():
|
for r in self._pending_receive.values():
|
||||||
r.confirm_received.set()
|
r.request.confirm_received.set()
|
||||||
self._pending_receive.clear()
|
self._pending_receive.clear()
|
||||||
|
|
||||||
def _handle_pong(self, msg_id, sequence, reader):
|
def _handle_pong(self, msg_id, sequence, reader):
|
||||||
self._logger.debug('Handling pong')
|
self._logger.debug('Handling pong')
|
||||||
reader.read_int(signed=False) # code
|
pong = reader.tgread_object()
|
||||||
received_msg_id = reader.read_long()
|
assert isinstance(pong, Pong)
|
||||||
|
|
||||||
request = self._pop_request(received_msg_id)
|
request = self._pop_request(pong.msg_id)
|
||||||
if request:
|
if request:
|
||||||
self._logger.debug('Pong confirmed a request')
|
self._logger.debug('Pong confirmed a request')
|
||||||
|
request.result = pong
|
||||||
request.confirm_received.set()
|
request.confirm_received.set()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -278,14 +296,16 @@ class MtProtoSender:
|
||||||
|
|
||||||
def _handle_bad_server_salt(self, msg_id, sequence, reader):
|
def _handle_bad_server_salt(self, msg_id, sequence, reader):
|
||||||
self._logger.debug('Handling bad server salt')
|
self._logger.debug('Handling bad server salt')
|
||||||
reader.read_int(signed=False) # code
|
bad_salt = reader.tgread_object()
|
||||||
bad_msg_id = reader.read_long()
|
assert isinstance(bad_salt, BadServerSalt)
|
||||||
reader.read_int() # bad_msg_seq_no
|
|
||||||
reader.read_int() # error_code
|
|
||||||
new_salt = reader.read_long(signed=False)
|
|
||||||
self.session.salt = new_salt
|
|
||||||
|
|
||||||
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:
|
if request:
|
||||||
self.send(request)
|
self.send(request)
|
||||||
|
|
||||||
|
@ -293,31 +313,53 @@ class MtProtoSender:
|
||||||
|
|
||||||
def _handle_bad_msg_notification(self, msg_id, sequence, reader):
|
def _handle_bad_msg_notification(self, msg_id, sequence, reader):
|
||||||
self._logger.debug('Handling bad message notification')
|
self._logger.debug('Handling bad message notification')
|
||||||
reader.read_int(signed=False) # code
|
bad_msg = reader.tgread_object()
|
||||||
reader.read_long() # request_id
|
assert isinstance(bad_msg, BadMsgNotification)
|
||||||
reader.read_int() # request_sequence
|
|
||||||
|
|
||||||
error_code = reader.read_int()
|
error = BadMessageError(bad_msg.error_code)
|
||||||
error = BadMessageError(error_code)
|
if bad_msg.error_code in (16, 17):
|
||||||
if error_code in (16, 17):
|
|
||||||
# sent msg_id too low or too high (respectively).
|
# sent msg_id too low or too high (respectively).
|
||||||
# Use the current msg_id to determine the right time offset.
|
# Use the current msg_id to determine the right time offset.
|
||||||
self.session.update_time_offset(correct_msg_id=msg_id)
|
self.session.update_time_offset(correct_msg_id=msg_id)
|
||||||
self._logger.debug('Read Bad Message error: ' + str(error))
|
self._logger.debug('Read Bad Message error: ' + str(error))
|
||||||
self._logger.debug('Attempting to use the correct time offset.')
|
self._logger.debug('Attempting to use the correct time offset.')
|
||||||
return True
|
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
|
# 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
|
# TODO A better fix would be to start with a new fresh session ID
|
||||||
self.session._sequence += 64
|
self.session._sequence += 64
|
||||||
return True
|
return True
|
||||||
elif error_code == 33:
|
elif bad_msg.error_code == 33:
|
||||||
# msg_seqno too high never seems to happen but just in case
|
# msg_seqno too high never seems to happen but just in case
|
||||||
self.session._sequence -= 16
|
self.session._sequence -= 16
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
raise error
|
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):
|
def _handle_rpc_result(self, msg_id, sequence, reader):
|
||||||
self._logger.debug('Handling RPC result')
|
self._logger.debug('Handling RPC result')
|
||||||
reader.read_int(signed=False) # code
|
reader.read_int(signed=False) # code
|
||||||
|
@ -346,29 +388,35 @@ class MtProtoSender:
|
||||||
# else TODO Where should this error be reported?
|
# else TODO Where should this error be reported?
|
||||||
# Read may be async. Can an error not-belong to a request?
|
# Read may be async. Can an error not-belong to a request?
|
||||||
self._logger.debug('Read RPC error: %s', str(error))
|
self._logger.debug('Read RPC error: %s', str(error))
|
||||||
else:
|
return True # All contents were read okay
|
||||||
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)
|
|
||||||
|
|
||||||
self.session.process_entities(request.result)
|
elif request:
|
||||||
request.confirm_received.set()
|
self._logger.debug('Reading request response')
|
||||||
return True
|
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:
|
else:
|
||||||
# If it's really a result for RPC from previous connection
|
reader.seek(-4)
|
||||||
# session, it will be skipped by the handle_container()
|
request.on_response(reader)
|
||||||
self._logger.debug('Lost request will be skipped.')
|
|
||||||
return False
|
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):
|
def _handle_gzip_packed(self, msg_id, sequence, reader, state):
|
||||||
self._logger.debug('Handling gzip packed data')
|
self._logger.debug('Handling gzip packed data')
|
||||||
with BinaryReader(GzipPacked.read(reader)) as compressed_reader:
|
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)
|
return self._process_msg(msg_id, sequence, compressed_reader, state)
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
|
import warnings
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
@ -57,7 +58,7 @@ class TelegramBareClient:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Current TelegramClient version
|
# Current TelegramClient version
|
||||||
__version__ = '0.15.1'
|
__version__ = '0.15.3'
|
||||||
|
|
||||||
# TODO Make this thread-safe, all connections share the same DC
|
# TODO Make this thread-safe, all connections share the same DC
|
||||||
_dc_options = None
|
_dc_options = None
|
||||||
|
@ -131,7 +132,7 @@ class TelegramBareClient:
|
||||||
self._user_connected = False
|
self._user_connected = False
|
||||||
|
|
||||||
# Save whether the user is authorized here (a.k.a. logged in)
|
# 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
|
# Uploaded files cache so subsequent calls are instant
|
||||||
self._upload_cache = {}
|
self._upload_cache = {}
|
||||||
|
@ -222,12 +223,14 @@ class TelegramBareClient:
|
||||||
# another data center and this would raise UserMigrateError)
|
# another data center and this would raise UserMigrateError)
|
||||||
# to also assert whether the user is logged in or not.
|
# to also assert whether the user is logged in or not.
|
||||||
self._user_connected = True
|
self._user_connected = True
|
||||||
if _sync_updates and not _cdn:
|
if self._authorized is None and _sync_updates and not _cdn:
|
||||||
try:
|
try:
|
||||||
self.sync_updates()
|
self.sync_updates()
|
||||||
self._set_connected_and_authorized()
|
self._set_connected_and_authorized()
|
||||||
except UnauthorizedError:
|
except UnauthorizedError:
|
||||||
self._authorized = False
|
self._authorized = False
|
||||||
|
elif self._authorized:
|
||||||
|
self._set_connected_and_authorized()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -270,18 +273,16 @@ class TelegramBareClient:
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
"""Disconnects from the Telegram server
|
"""Disconnects from the Telegram server
|
||||||
and stops all the spawned threads"""
|
and stops all the spawned threads"""
|
||||||
self._user_connected = False
|
self._user_connected = False # This will stop recv_thread's loop
|
||||||
self._recv_thread = None
|
|
||||||
|
|
||||||
# Stop the workers from the background thread
|
|
||||||
self.updates.stop_workers()
|
self.updates.stop_workers()
|
||||||
|
|
||||||
# This will trigger a "ConnectionResetError", for subsequent calls
|
# This will trigger a "ConnectionResetError" on the recv_thread,
|
||||||
# to read or send (from another thread) and usually, the background
|
# which won't attempt reconnecting as ._user_connected is False.
|
||||||
# thread would try restarting the connection but since the
|
|
||||||
# ._recv_thread = None, it knows it doesn't have to.
|
|
||||||
self._sender.disconnect()
|
self._sender.disconnect()
|
||||||
|
|
||||||
|
if self._recv_thread:
|
||||||
|
self._recv_thread.join()
|
||||||
|
|
||||||
# TODO Shall we clear the _exported_sessions, or may be reused?
|
# TODO Shall we clear the _exported_sessions, or may be reused?
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -298,10 +299,13 @@ class TelegramBareClient:
|
||||||
# Assume we are disconnected due to some error, so connect again
|
# Assume we are disconnected due to some error, so connect again
|
||||||
with self._reconnect_lock:
|
with self._reconnect_lock:
|
||||||
# Another thread may have connected again, so check that first
|
# Another thread may have connected again, so check that first
|
||||||
if not self.is_connected():
|
if self.is_connected():
|
||||||
return self.connect()
|
|
||||||
else:
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.connect()
|
||||||
|
except ConnectionResetError:
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
self.session.auth_key = None # Force creating new auth_key
|
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
|
on_main_thread = threading.get_ident() == self._main_thread_ident
|
||||||
if on_main_thread or self._on_read_thread():
|
if on_main_thread or self._on_read_thread():
|
||||||
sender = self._sender
|
sender = self._sender
|
||||||
|
update_state = self.updates
|
||||||
else:
|
else:
|
||||||
sender = self._sender.clone()
|
sender = self._sender.clone()
|
||||||
sender.connect()
|
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
|
# We should call receive from this thread if there's no background
|
||||||
# thread reading or if the server disconnected us and we're trying
|
# 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:
|
if self._background_error and on_main_thread:
|
||||||
raise self._background_error
|
raise self._background_error
|
||||||
|
|
||||||
result = self._invoke(sender, call_receive, *requests)
|
result = self._invoke(
|
||||||
if result:
|
sender, call_receive, update_state, *requests
|
||||||
|
)
|
||||||
|
if result is not None:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
raise ValueError('Number of retries reached 0.')
|
raise ValueError('Number of retries reached 0.')
|
||||||
|
@ -455,7 +470,7 @@ class TelegramBareClient:
|
||||||
# Let people use client.invoke(SomeRequest()) instead client(...)
|
# Let people use client.invoke(SomeRequest()) instead client(...)
|
||||||
invoke = __call__
|
invoke = __call__
|
||||||
|
|
||||||
def _invoke(self, sender, call_receive, *requests):
|
def _invoke(self, sender, call_receive, update_state, *requests):
|
||||||
try:
|
try:
|
||||||
# Ensure that we start with no previous errors (i.e. resending)
|
# Ensure that we start with no previous errors (i.e. resending)
|
||||||
for x in requests:
|
for x in requests:
|
||||||
|
@ -475,14 +490,14 @@ class TelegramBareClient:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
while not all(x.confirm_received.is_set() for x in requests):
|
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:
|
except TimeoutError:
|
||||||
pass # We will just retry
|
pass # We will just retry
|
||||||
|
|
||||||
except ConnectionResetError:
|
except ConnectionResetError:
|
||||||
if not self._authorized or self._reconnect_lock.locked():
|
if not self._user_connected or self._reconnect_lock.locked():
|
||||||
# Only attempt reconnecting if we're authorized and not
|
# Only attempt reconnecting if the user called connect and not
|
||||||
# reconnecting already.
|
# reconnecting already.
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@ -495,10 +510,7 @@ class TelegramBareClient:
|
||||||
else:
|
else:
|
||||||
while self._user_connected and not self._reconnect():
|
while self._user_connected and not self._reconnect():
|
||||||
sleep(0.1) # Retry forever until we can send the request
|
sleep(0.1) # Retry forever until we can send the request
|
||||||
|
return None
|
||||||
finally:
|
|
||||||
if sender != self._sender:
|
|
||||||
sender.disconnect()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raise next(x.rpc_error for x in requests if x.rpc_error)
|
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),
|
# be on the very first connection (not authorized, not running),
|
||||||
# but may be an issue for people who actually travel?
|
# but may be an issue for people who actually travel?
|
||||||
self._reconnect(new_dc=e.new_dc)
|
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:
|
except ServerError as e:
|
||||||
# Telegram is having some issues, just retry
|
# Telegram is having some issues, just retry
|
||||||
|
@ -533,10 +545,14 @@ class TelegramBareClient:
|
||||||
'[ERROR] Telegram is having some internal issues', e
|
'[ERROR] Telegram is having some internal issues', e
|
||||||
)
|
)
|
||||||
|
|
||||||
except FloodWaitError:
|
except FloodWaitError as e:
|
||||||
sender.disconnect()
|
if e.seconds > self.session.flood_sleep_threshold | 0:
|
||||||
self.disconnect()
|
raise
|
||||||
raise
|
|
||||||
|
self._logger.debug(
|
||||||
|
'Sleep of %d seconds below threshold, sleeping' % e.seconds
|
||||||
|
)
|
||||||
|
sleep(e.seconds)
|
||||||
|
|
||||||
# Some really basic functionality
|
# Some really basic functionality
|
||||||
|
|
||||||
|
@ -683,10 +699,8 @@ class TelegramBareClient:
|
||||||
cdn_decrypter = None
|
cdn_decrypter = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
offset_index = 0
|
offset = 0
|
||||||
while True:
|
while True:
|
||||||
offset = offset_index * part_size
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if cdn_decrypter:
|
if cdn_decrypter:
|
||||||
result = cdn_decrypter.get_file()
|
result = cdn_decrypter.get_file()
|
||||||
|
@ -705,7 +719,7 @@ class TelegramBareClient:
|
||||||
client = self._get_exported_client(e.new_dc)
|
client = self._get_exported_client(e.new_dc)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
offset_index += 1
|
offset += part_size
|
||||||
|
|
||||||
# If we have received no data (0 bytes), the file is over
|
# If we have received no data (0 bytes), the file is over
|
||||||
# So there is nothing left to download and write
|
# So there is nothing left to download and write
|
||||||
|
@ -742,10 +756,10 @@ class TelegramBareClient:
|
||||||
def add_update_handler(self, handler):
|
def add_update_handler(self, handler):
|
||||||
"""Adds an update handler (a function which takes a TLObject,
|
"""Adds an update handler (a function which takes a TLObject,
|
||||||
an update, as its parameter) and listens for updates"""
|
an update, as its parameter) and listens for updates"""
|
||||||
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)
|
self.updates.handlers.append(handler)
|
||||||
if sync:
|
|
||||||
self.sync_updates()
|
|
||||||
|
|
||||||
def remove_update_handler(self, handler):
|
def remove_update_handler(self, handler):
|
||||||
self.updates.handlers.remove(handler)
|
self.updates.handlers.remove(handler)
|
||||||
|
@ -801,7 +815,9 @@ class TelegramBareClient:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import socks
|
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
|
# This is a known error, and it's not related to
|
||||||
# Telegram but rather to the proxy. Disconnect and
|
# Telegram but rather to the proxy. Disconnect and
|
||||||
# hand it over to the main thread.
|
# hand it over to the main thread.
|
||||||
|
|
|
@ -15,6 +15,7 @@ from .errors import (
|
||||||
)
|
)
|
||||||
from .network import ConnectionMode
|
from .network import ConnectionMode
|
||||||
from .tl import TLObject
|
from .tl import TLObject
|
||||||
|
from .tl.custom import Draft
|
||||||
from .tl.entity_database import EntityDatabase
|
from .tl.entity_database import EntityDatabase
|
||||||
from .tl.functions.account import (
|
from .tl.functions.account import (
|
||||||
GetPasswordRequest
|
GetPasswordRequest
|
||||||
|
@ -28,8 +29,8 @@ from .tl.functions.contacts import (
|
||||||
)
|
)
|
||||||
from .tl.functions.messages import (
|
from .tl.functions.messages import (
|
||||||
GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest,
|
GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest,
|
||||||
SendMessageRequest, GetChatsRequest
|
SendMessageRequest, GetChatsRequest,
|
||||||
)
|
GetAllDraftsRequest)
|
||||||
|
|
||||||
from .tl.functions import channels
|
from .tl.functions import channels
|
||||||
from .tl.functions import messages
|
from .tl.functions import messages
|
||||||
|
@ -46,7 +47,7 @@ from .tl.types import (
|
||||||
InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty,
|
InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty,
|
||||||
Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto,
|
Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto,
|
||||||
InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID,
|
InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID,
|
||||||
UpdateNewMessage, UpdateShortSentMessage,
|
UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage,
|
||||||
PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel)
|
PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel)
|
||||||
from .tl.types.messages import DialogsSlice
|
from .tl.types.messages import DialogsSlice
|
||||||
|
|
||||||
|
@ -252,7 +253,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
if limit is None:
|
if limit is None:
|
||||||
limit = float('inf')
|
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?
|
messages = {} # Used later for sorting TODO also return these?
|
||||||
entities = {}
|
entities = {}
|
||||||
while len(dialogs) < limit:
|
while len(dialogs) < limit:
|
||||||
|
@ -267,7 +268,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
break
|
break
|
||||||
|
|
||||||
for d in r.dialogs:
|
for d in r.dialogs:
|
||||||
dialogs[d.top_message] = d
|
dialogs[utils.get_peer_id(d.peer, True)] = d
|
||||||
for m in r.messages:
|
for m in r.messages:
|
||||||
messages[m.id] = m
|
messages[m.id] = m
|
||||||
|
|
||||||
|
@ -302,9 +303,20 @@ class TelegramClient(TelegramBareClient):
|
||||||
[utils.find_user_or_chat(d.peer, entities, entities) for d in ds]
|
[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,
|
def send_message(self,
|
||||||
entity,
|
entity,
|
||||||
|
@ -347,7 +359,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
break
|
break
|
||||||
|
|
||||||
for update in result.updates:
|
for update in result.updates:
|
||||||
if isinstance(update, UpdateNewMessage):
|
if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)):
|
||||||
if update.message.id == msg_id:
|
if update.message.id == msg_id:
|
||||||
return update.message
|
return update.message
|
||||||
|
|
||||||
|
@ -488,9 +500,13 @@ class TelegramClient(TelegramBareClient):
|
||||||
def send_file(self, entity, file, caption='',
|
def send_file(self, entity, file, caption='',
|
||||||
force_document=False, progress_callback=None,
|
force_document=False, progress_callback=None,
|
||||||
reply_to=None,
|
reply_to=None,
|
||||||
|
attributes=None,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""Sends a file to the specified entity.
|
"""Sends a file to the specified entity.
|
||||||
The file may either be a path, a byte array, or a stream.
|
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.
|
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.
|
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
|
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.
|
sent as a document, it will be sent as a voice note.
|
||||||
|
|
||||||
|
@ -537,16 +557,28 @@ class TelegramClient(TelegramBareClient):
|
||||||
# Determine mime-type and attributes
|
# Determine mime-type and attributes
|
||||||
# Take the first element by using [0] since it returns a tuple
|
# Take the first element by using [0] since it returns a tuple
|
||||||
mime_type = guess_type(file)[0]
|
mime_type = guess_type(file)[0]
|
||||||
attributes = [
|
attr_dict = {
|
||||||
|
DocumentAttributeFilename:
|
||||||
DocumentAttributeFilename(os.path.basename(file))
|
DocumentAttributeFilename(os.path.basename(file))
|
||||||
# TODO If the input file is an audio, find out:
|
# TODO If the input file is an audio, find out:
|
||||||
# Performer and song title and add DocumentAttributeAudio
|
# Performer and song title and add DocumentAttributeAudio
|
||||||
]
|
}
|
||||||
else:
|
else:
|
||||||
attributes = [DocumentAttributeFilename('unnamed')]
|
attr_dict = {
|
||||||
|
DocumentAttributeFilename:
|
||||||
|
DocumentAttributeFilename('unnamed')
|
||||||
|
}
|
||||||
|
|
||||||
if 'is_voice_note' in kwargs:
|
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
|
# Ensure we have a mime type, any; but it cannot be None
|
||||||
# 'The "octet-stream" subtype is used to indicate that a body
|
# 'The "octet-stream" subtype is used to indicate that a body
|
||||||
|
@ -557,7 +589,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
media = InputMediaUploadedDocument(
|
media = InputMediaUploadedDocument(
|
||||||
file=file_handle,
|
file=file_handle,
|
||||||
mime_type=mime_type,
|
mime_type=mime_type,
|
||||||
attributes=attributes,
|
attributes=list(attr_dict.values()),
|
||||||
caption=caption
|
caption=caption
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -873,24 +905,23 @@ class TelegramClient(TelegramBareClient):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if isinstance(entity, int) or (
|
if isinstance(entity, int) or (
|
||||||
isinstance(entity, TLObject) and
|
isinstance(entity, TLObject) and
|
||||||
# crc32(b'InputPeer') and crc32(b'Peer')
|
# 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)
|
ie = self.get_input_entity(entity)
|
||||||
result = None
|
|
||||||
if isinstance(ie, InputPeerUser):
|
if isinstance(ie, InputPeerUser):
|
||||||
result = self(GetUsersRequest([ie]))
|
self(GetUsersRequest([ie]))
|
||||||
elif isinstance(ie, InputPeerChat):
|
elif isinstance(ie, InputPeerChat):
|
||||||
result = self(GetChatsRequest([ie.chat_id]))
|
self(GetChatsRequest([ie.chat_id]))
|
||||||
elif isinstance(ie, InputPeerChannel):
|
elif isinstance(ie, InputPeerChannel):
|
||||||
result = self(GetChannelsRequest([ie]))
|
self(GetChannelsRequest([ie]))
|
||||||
|
try:
|
||||||
if result:
|
# session.process_entities has been called in the MtProtoSender
|
||||||
self.session.process_entities(result)
|
# with the result of these calls, so they should now be on the
|
||||||
try:
|
# entities database.
|
||||||
return self.session.entities[ie]
|
return self.session.entities[ie]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if isinstance(entity, str):
|
if isinstance(entity, str):
|
||||||
return self._get_entity_from_string(entity)
|
return self._get_entity_from_string(entity)
|
||||||
|
@ -906,11 +937,11 @@ class TelegramClient(TelegramBareClient):
|
||||||
phone = EntityDatabase.parse_phone(string)
|
phone = EntityDatabase.parse_phone(string)
|
||||||
if phone:
|
if phone:
|
||||||
entity = phone
|
entity = phone
|
||||||
self.session.process_entities(self(GetContactsRequest(0)))
|
self(GetContactsRequest(0))
|
||||||
else:
|
else:
|
||||||
entity = string.strip('@').lower()
|
entity = string.strip('@').lower()
|
||||||
self.session.process_entities(self(ResolveUsernameRequest(entity)))
|
self(ResolveUsernameRequest(entity))
|
||||||
|
# MtProtoSender will call .process_entities on the requests made
|
||||||
try:
|
try:
|
||||||
return self.session.entities[entity]
|
return self.session.entities[entity]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -956,9 +987,17 @@ class TelegramClient(TelegramBareClient):
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.session.save_entities:
|
if self.session.save_entities:
|
||||||
# Not found, look in the dialogs (this will save the users)
|
# Not found, look in the latest dialogs.
|
||||||
self.get_dialogs(limit=None)
|
# 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:
|
try:
|
||||||
return self.session.entities.get_input_entity(peer)
|
return self.session.entities.get_input_entity(peer)
|
||||||
except KeyError:
|
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
|
getattr(p, 'access_hash', 0) # chats won't have hash
|
||||||
|
|
||||||
if self.enabled_full:
|
if self.enabled_full:
|
||||||
if isinstance(e, User) \
|
if isinstance(e, (User, Chat, Channel)):
|
||||||
or isinstance(e, Chat) \
|
|
||||||
or isinstance(e, Channel):
|
|
||||||
new.append(e)
|
new.append(e)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
@ -123,47 +121,64 @@ class EntityDatabase:
|
||||||
if phone:
|
if phone:
|
||||||
self._username_id[phone] = marked_id
|
self._username_id[phone] = marked_id
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def _parse_key(self, key):
|
||||||
"""Accepts a digit only string as phone number,
|
"""Parses the given string, integer or TLObject key into a
|
||||||
otherwise it's treated as an username.
|
marked user ID ready for use on self._entities.
|
||||||
|
|
||||||
If an integer is given, it's treated as the ID of the desired User.
|
If a callable key is given, the entity will be passed to the
|
||||||
The ID given won't try to be guessed as the ID of a chat or channel,
|
function, and if it returns a true-like value, the marked ID
|
||||||
as there may be an user with that ID, and it would be unreliable.
|
for such entity will be returned.
|
||||||
|
|
||||||
If a Peer is given (PeerUser, PeerChat, PeerChannel),
|
Raises ValueError if it cannot be parsed.
|
||||||
its specific entity is retrieved as User, Chat or Channel.
|
|
||||||
Note that megagroups are channels with .megagroup = True.
|
|
||||||
"""
|
"""
|
||||||
if isinstance(key, str):
|
if isinstance(key, str):
|
||||||
phone = EntityDatabase.parse_phone(key)
|
phone = EntityDatabase.parse_phone(key)
|
||||||
if phone:
|
try:
|
||||||
return self._phone_id[phone]
|
if phone:
|
||||||
else:
|
return self._phone_id[phone]
|
||||||
key = key.lstrip('@').lower()
|
else:
|
||||||
return self._entities[self._username_id[key]]
|
return self._username_id[key.lstrip('@').lower()]
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError() from e
|
||||||
|
|
||||||
if isinstance(key, int):
|
if isinstance(key, int):
|
||||||
return self._entities[key] # normal IDs are assumed users
|
return key # normal IDs are assumed users
|
||||||
|
|
||||||
if isinstance(key, TLObject):
|
if isinstance(key, TLObject):
|
||||||
sc = type(key).SUBCLASS_OF_ID
|
return utils.get_peer_id(key, add_mark=True)
|
||||||
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
|
|
||||||
|
|
||||||
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):
|
def __delitem__(self, key):
|
||||||
target = self[key]
|
try:
|
||||||
del self._entities[key]
|
old = self._entities.pop(self._parse_key(key))
|
||||||
if getattr(target, 'username'):
|
# Try removing the username and phone (if pop didn't fail),
|
||||||
del self._username_id[target.username]
|
# 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
|
@staticmethod
|
||||||
def parse_phone(phone):
|
def parse_phone(phone):
|
||||||
|
@ -177,8 +192,10 @@ class EntityDatabase:
|
||||||
|
|
||||||
def get_input_entity(self, peer):
|
def get_input_entity(self, peer):
|
||||||
try:
|
try:
|
||||||
i, k = utils.get_peer_id(peer, add_mark=True, get_kind=True)
|
i = utils.get_peer_id(peer, add_mark=True)
|
||||||
h = self._input_entities[i]
|
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:
|
if k == PeerUser:
|
||||||
return InputPeerUser(i, h)
|
return InputPeerUser(i, h)
|
||||||
elif k == PeerChat:
|
elif k == PeerChat:
|
||||||
|
|
|
@ -13,26 +13,26 @@ class GzipPacked(TLObject):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def gzip_if_smaller(request):
|
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
|
optionally gzips the resulting data. If the gzipped data is
|
||||||
smaller than the original byte array, this is returned instead.
|
smaller than the original byte array, this is returned instead.
|
||||||
|
|
||||||
Note that this only applies to content related requests.
|
Note that this only applies to content related requests.
|
||||||
"""
|
"""
|
||||||
data = request.to_bytes()
|
data = bytes(request)
|
||||||
# TODO This threshold could be configurable
|
# TODO This threshold could be configurable
|
||||||
if request.content_related and len(data) > 512:
|
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
|
return gzipped if len(gzipped) < len(data) else data
|
||||||
else:
|
else:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def to_bytes(self):
|
def __bytes__(self):
|
||||||
# TODO Maybe compress level could be an option
|
# TODO Maybe compress level could be an option
|
||||||
return struct.pack('<I', GzipPacked.CONSTRUCTOR_ID) + \
|
return struct.pack('<I', GzipPacked.CONSTRUCTOR_ID) + \
|
||||||
TLObject.serialize_bytes(gzip.compress(self.data))
|
TLObject.serialize_bytes(gzip.compress(self.data))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def read(reader):
|
def read(reader):
|
||||||
reader.read_int(signed=False) # code
|
assert reader.read_int(signed=False) == GzipPacked.CONSTRUCTOR_ID
|
||||||
return gzip.decompress(reader.tgread_bytes())
|
return gzip.decompress(reader.tgread_bytes())
|
||||||
|
|
|
@ -11,10 +11,10 @@ class MessageContainer(TLObject):
|
||||||
self.content_related = False
|
self.content_related = False
|
||||||
self.messages = messages
|
self.messages = messages
|
||||||
|
|
||||||
def to_bytes(self):
|
def __bytes__(self):
|
||||||
return struct.pack(
|
return struct.pack(
|
||||||
'<Ii', MessageContainer.CONSTRUCTOR_ID, len(self.messages)
|
'<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
|
@staticmethod
|
||||||
def iter_read(reader):
|
def iter_read(reader):
|
||||||
|
|
|
@ -37,6 +37,7 @@ class Session:
|
||||||
self.lang_pack = session.lang_pack
|
self.lang_pack = session.lang_pack
|
||||||
self.report_errors = session.report_errors
|
self.report_errors = session.report_errors
|
||||||
self.save_entities = session.save_entities
|
self.save_entities = session.save_entities
|
||||||
|
self.flood_sleep_threshold = session.flood_sleep_threshold
|
||||||
|
|
||||||
else: # str / None
|
else: # str / None
|
||||||
self.session_user_id = session_user_id
|
self.session_user_id = session_user_id
|
||||||
|
@ -50,6 +51,7 @@ class Session:
|
||||||
self.lang_pack = ''
|
self.lang_pack = ''
|
||||||
self.report_errors = True
|
self.report_errors = True
|
||||||
self.save_entities = True
|
self.save_entities = True
|
||||||
|
self.flood_sleep_threshold = 60
|
||||||
|
|
||||||
# Cross-thread safety
|
# Cross-thread safety
|
||||||
self._seq_no_lock = Lock()
|
self._seq_no_lock = Lock()
|
||||||
|
|
|
@ -12,6 +12,6 @@ class TLMessage(TLObject):
|
||||||
self.seq_no = session.generate_sequence(request.content_related)
|
self.seq_no = session.generate_sequence(request.content_related)
|
||||||
self.request = request
|
self.request = request
|
||||||
|
|
||||||
def to_bytes(self):
|
def __bytes__(self):
|
||||||
body = GzipPacked.gzip_if_smaller(self.request)
|
body = GzipPacked.gzip_if_smaller(self.request)
|
||||||
return struct.pack('<qii', self.msg_id, self.seq_no, len(body)) + body
|
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
|
from threading import Event
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,18 +20,14 @@ class TLObject:
|
||||||
"""
|
"""
|
||||||
if indent is None:
|
if indent is None:
|
||||||
if isinstance(obj, TLObject):
|
if isinstance(obj, TLObject):
|
||||||
children = obj.to_dict(recursive=False)
|
return '{}({})'.format(type(obj).__name__, ', '.join(
|
||||||
if children:
|
'{}={}'.format(k, TLObject.pretty_format(v))
|
||||||
return '{}: {}'.format(
|
for k, v in obj.to_dict(recursive=False).items()
|
||||||
type(obj).__name__, TLObject.pretty_format(children)
|
))
|
||||||
)
|
|
||||||
else:
|
|
||||||
return type(obj).__name__
|
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
return '{{{}}}'.format(', '.join(
|
return '{{{}}}'.format(', '.join(
|
||||||
'{}: {}'.format(
|
'{}: {}'.format(k, TLObject.pretty_format(v))
|
||||||
k, TLObject.pretty_format(v)
|
for k, v in obj.items()
|
||||||
) for k, v in obj.items()
|
|
||||||
))
|
))
|
||||||
elif isinstance(obj, str) or isinstance(obj, bytes):
|
elif isinstance(obj, str) or isinstance(obj, bytes):
|
||||||
return repr(obj)
|
return repr(obj)
|
||||||
|
@ -38,31 +35,36 @@ class TLObject:
|
||||||
return '[{}]'.format(
|
return '[{}]'.format(
|
||||||
', '.join(TLObject.pretty_format(x) for x in obj)
|
', '.join(TLObject.pretty_format(x) for x in obj)
|
||||||
)
|
)
|
||||||
|
elif isinstance(obj, datetime):
|
||||||
|
return 'datetime.fromtimestamp({})'.format(obj.timestamp())
|
||||||
else:
|
else:
|
||||||
return str(obj)
|
return repr(obj)
|
||||||
else:
|
else:
|
||||||
result = []
|
result = []
|
||||||
if isinstance(obj, TLObject):
|
if isinstance(obj, TLObject) or isinstance(obj, dict):
|
||||||
result.append(type(obj).__name__)
|
if isinstance(obj, dict):
|
||||||
children = obj.to_dict(recursive=False)
|
d = obj
|
||||||
if children:
|
start, end, sep = '{', '}', ': '
|
||||||
result.append(': ')
|
else:
|
||||||
result.append(TLObject.pretty_format(
|
d = obj.to_dict(recursive=False)
|
||||||
obj.to_dict(recursive=False), indent
|
start, end, sep = '(', ')', '='
|
||||||
))
|
result.append(type(obj).__name__)
|
||||||
|
|
||||||
elif isinstance(obj, dict):
|
result.append(start)
|
||||||
result.append('{\n')
|
if d:
|
||||||
indent += 1
|
result.append('\n')
|
||||||
for k, v in obj.items():
|
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('\t' * indent)
|
||||||
result.append(k)
|
result.append(end)
|
||||||
result.append(': ')
|
|
||||||
result.append(TLObject.pretty_format(v, indent))
|
|
||||||
result.append(',\n')
|
|
||||||
indent -= 1
|
|
||||||
result.append('\t' * indent)
|
|
||||||
result.append('}')
|
|
||||||
|
|
||||||
elif isinstance(obj, str) or isinstance(obj, bytes):
|
elif isinstance(obj, str) or isinstance(obj, bytes):
|
||||||
result.append(repr(obj))
|
result.append(repr(obj))
|
||||||
|
@ -78,8 +80,13 @@ class TLObject:
|
||||||
result.append('\t' * indent)
|
result.append('\t' * indent)
|
||||||
result.append(']')
|
result.append(']')
|
||||||
|
|
||||||
|
elif isinstance(obj, datetime):
|
||||||
|
result.append('datetime.fromtimestamp(')
|
||||||
|
result.append(repr(obj.timestamp()))
|
||||||
|
result.append(')')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
result.append(str(obj))
|
result.append(repr(obj))
|
||||||
|
|
||||||
return ''.join(result)
|
return ''.join(result)
|
||||||
|
|
||||||
|
@ -118,8 +125,9 @@ class TLObject:
|
||||||
def to_dict(self, recursive=True):
|
def to_dict(self, recursive=True):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def to_bytes(self):
|
def __bytes__(self):
|
||||||
return b''
|
return b''
|
||||||
|
|
||||||
def on_response(self, reader):
|
@staticmethod
|
||||||
pass
|
def from_reader(reader):
|
||||||
|
return TLObject()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
import pickle
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from threading import RLock, Event, Thread
|
from threading import RLock, Event, Thread
|
||||||
|
@ -27,6 +28,7 @@ class UpdateState:
|
||||||
self._updates_lock = RLock()
|
self._updates_lock = RLock()
|
||||||
self._updates_available = Event()
|
self._updates_available = Event()
|
||||||
self._updates = deque()
|
self._updates = deque()
|
||||||
|
self._latest_updates = deque(maxlen=10)
|
||||||
|
|
||||||
self._logger = logging.getLogger(__name__)
|
self._logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -141,6 +143,26 @@ class UpdateState:
|
||||||
|
|
||||||
self._state.pts = pts
|
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')
|
if type(update).SUBCLASS_OF_ID == 0x8af52aac: # crc32(b'Updates')
|
||||||
# Expand "Updates" into "Update", and pass these to callbacks.
|
# Expand "Updates" into "Update", and pass these to callbacks.
|
||||||
# Since .users and .chats have already been processed, we
|
# Since .users and .chats have already been processed, we
|
||||||
|
@ -149,8 +171,7 @@ class UpdateState:
|
||||||
self._updates.append(update.update)
|
self._updates.append(update.update)
|
||||||
self._updates_available.set()
|
self._updates_available.set()
|
||||||
|
|
||||||
elif isinstance(update, tl.Updates) or \
|
elif isinstance(update, (tl.Updates, tl.UpdatesCombined)):
|
||||||
isinstance(update, tl.UpdatesCombined):
|
|
||||||
self._updates.extend(update.updates)
|
self._updates.extend(update.updates)
|
||||||
self._updates_available.set()
|
self._updates_available.set()
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,8 @@ from .tl.types import (
|
||||||
GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty,
|
GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty,
|
||||||
InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty,
|
InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty,
|
||||||
FileLocationUnavailable, InputMediaUploadedDocument,
|
FileLocationUnavailable, InputMediaUploadedDocument,
|
||||||
InputMediaUploadedPhoto,
|
InputMediaUploadedPhoto, DocumentAttributeFilename, photos
|
||||||
DocumentAttributeFilename)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_display_name(entity):
|
def get_display_name(entity):
|
||||||
|
@ -37,7 +37,7 @@ def get_display_name(entity):
|
||||||
else:
|
else:
|
||||||
return '(No name)'
|
return '(No name)'
|
||||||
|
|
||||||
if isinstance(entity, Chat) or isinstance(entity, Channel):
|
if isinstance(entity, (Chat, Channel)):
|
||||||
return entity.title
|
return entity.title
|
||||||
|
|
||||||
return '(unknown)'
|
return '(unknown)'
|
||||||
|
@ -51,8 +51,7 @@ def get_extension(media):
|
||||||
"""Gets the corresponding extension for any Telegram media"""
|
"""Gets the corresponding extension for any Telegram media"""
|
||||||
|
|
||||||
# Photos are always compressed as .jpg by Telegram
|
# Photos are always compressed as .jpg by Telegram
|
||||||
if (isinstance(media, UserProfilePhoto) or isinstance(media, ChatPhoto) or
|
if isinstance(media, (UserProfilePhoto, ChatPhoto, MessageMediaPhoto)):
|
||||||
isinstance(media, MessageMediaPhoto)):
|
|
||||||
return '.jpg'
|
return '.jpg'
|
||||||
|
|
||||||
# Documents will come with a mime type
|
# Documents will come with a mime type
|
||||||
|
@ -88,12 +87,10 @@ def get_input_peer(entity, allow_self=True):
|
||||||
else:
|
else:
|
||||||
return InputPeerUser(entity.id, entity.access_hash)
|
return InputPeerUser(entity.id, entity.access_hash)
|
||||||
|
|
||||||
if any(isinstance(entity, c) for c in (
|
if isinstance(entity, (Chat, ChatEmpty, ChatForbidden)):
|
||||||
Chat, ChatEmpty, ChatForbidden)):
|
|
||||||
return InputPeerChat(entity.id)
|
return InputPeerChat(entity.id)
|
||||||
|
|
||||||
if any(isinstance(entity, c) for c in (
|
if isinstance(entity, (Channel, ChannelForbidden)):
|
||||||
Channel, ChannelForbidden)):
|
|
||||||
return InputPeerChannel(entity.id, entity.access_hash)
|
return InputPeerChannel(entity.id, entity.access_hash)
|
||||||
|
|
||||||
# Less common cases
|
# Less common cases
|
||||||
|
@ -123,7 +120,7 @@ def get_input_channel(entity):
|
||||||
if type(entity).SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel')
|
if type(entity).SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel')
|
||||||
return entity
|
return entity
|
||||||
|
|
||||||
if isinstance(entity, Channel) or isinstance(entity, ChannelForbidden):
|
if isinstance(entity, (Channel, ChannelForbidden)):
|
||||||
return InputChannel(entity.id, entity.access_hash)
|
return InputChannel(entity.id, entity.access_hash)
|
||||||
|
|
||||||
if isinstance(entity, InputPeerChannel):
|
if isinstance(entity, InputPeerChannel):
|
||||||
|
@ -189,6 +186,9 @@ def get_input_photo(photo):
|
||||||
if type(photo).SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto')
|
if type(photo).SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto')
|
||||||
return photo
|
return photo
|
||||||
|
|
||||||
|
if isinstance(photo, photos.Photo):
|
||||||
|
photo = photo.photo
|
||||||
|
|
||||||
if isinstance(photo, Photo):
|
if isinstance(photo, Photo):
|
||||||
return InputPhoto(id=photo.id, access_hash=photo.access_hash)
|
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):
|
if isinstance(media, MessageMediaGame):
|
||||||
return InputMediaGame(id=media.game.id)
|
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):
|
if isinstance(media.photo_big, FileLocationUnavailable):
|
||||||
return get_input_media(media.photo_small, is_photo=True)
|
return get_input_media(media.photo_small, is_photo=True)
|
||||||
else:
|
else:
|
||||||
|
@ -289,10 +289,9 @@ def get_input_media(media, user_caption=None, is_photo=False):
|
||||||
venue_id=media.venue_id
|
venue_id=media.venue_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if any(isinstance(media, t) for t in (
|
if isinstance(media, (
|
||||||
MessageMediaEmpty, MessageMediaUnsupported,
|
MessageMediaEmpty, MessageMediaUnsupported,
|
||||||
FileLocationUnavailable, ChatPhotoEmpty,
|
ChatPhotoEmpty, UserProfilePhotoEmpty, FileLocationUnavailable)):
|
||||||
UserProfilePhotoEmpty)):
|
|
||||||
return InputMediaEmpty()
|
return InputMediaEmpty()
|
||||||
|
|
||||||
if isinstance(media, Message):
|
if isinstance(media, Message):
|
||||||
|
@ -301,16 +300,14 @@ def get_input_media(media, user_caption=None, is_photo=False):
|
||||||
_raise_cast_fail(media, 'InputMedia')
|
_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
|
"""Finds the ID of the given peer, and optionally converts it to
|
||||||
the "bot api" format if 'add_mark' is set to True.
|
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
|
# First we assert it's a Peer TLObject, or early return for integers
|
||||||
if not isinstance(peer, TLObject):
|
if not isinstance(peer, TLObject):
|
||||||
if isinstance(peer, int):
|
if isinstance(peer, int):
|
||||||
return (peer, PeerUser) if get_kind else peer
|
return peer
|
||||||
else:
|
else:
|
||||||
_raise_cast_fail(peer, 'int')
|
_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)
|
peer = get_input_peer(peer, allow_self=False)
|
||||||
|
|
||||||
# Set the right ID/kind, or raise if the TLObject is not recognised
|
# Set the right ID/kind, or raise if the TLObject is not recognised
|
||||||
i, k = None, None
|
if isinstance(peer, (PeerUser, InputPeerUser)):
|
||||||
if isinstance(peer, PeerUser) or isinstance(peer, InputPeerUser):
|
return peer.user_id
|
||||||
i, k = peer.user_id, PeerUser
|
elif isinstance(peer, (PeerChat, InputPeerChat)):
|
||||||
elif isinstance(peer, PeerChat) or isinstance(peer, InputPeerChat):
|
return -peer.chat_id if add_mark else peer.chat_id
|
||||||
i, k = peer.chat_id, PeerChat
|
elif isinstance(peer, (PeerChannel, InputPeerChannel)):
|
||||||
elif isinstance(peer, PeerChannel) or isinstance(peer, InputPeerChannel):
|
i = peer.channel_id
|
||||||
i, k = peer.channel_id, PeerChannel
|
if add_mark:
|
||||||
else:
|
|
||||||
_raise_cast_fail(peer, 'int')
|
|
||||||
|
|
||||||
if add_mark:
|
|
||||||
if k == PeerChat:
|
|
||||||
i = -i
|
|
||||||
elif k == PeerChannel:
|
|
||||||
# Concat -100 through math tricks, .to_supergroup() on Madeline
|
# Concat -100 through math tricks, .to_supergroup() on Madeline
|
||||||
# IDs will be strictly positive -> log works
|
# 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):
|
def resolve_id(marked_id):
|
||||||
|
@ -384,11 +376,7 @@ def find_message(update):
|
||||||
def get_appropriated_part_size(file_size):
|
def get_appropriated_part_size(file_size):
|
||||||
"""Gets the appropriated part size when uploading or downloading files,
|
"""Gets the appropriated part size when uploading or downloading files,
|
||||||
given an initial file size"""
|
given an initial file size"""
|
||||||
if file_size <= 1048576: # 1MB
|
if file_size <= 104857600: # 100MB
|
||||||
return 32
|
|
||||||
if file_size <= 10485760: # 10MB
|
|
||||||
return 64
|
|
||||||
if file_size <= 393216000: # 375MB
|
|
||||||
return 128
|
return 128
|
||||||
if file_size <= 786432000: # 750MB
|
if file_size <= 786432000: # 750MB
|
||||||
return 256
|
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):
|
def class_name(self):
|
||||||
"""Gets the class name following the Python style guidelines"""
|
"""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
|
# 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('_', '')
|
result = result[:1].upper() + result[1:].replace('_', '')
|
||||||
# If it's a function, let it end with "Request" to identify them
|
# If it's a function, let it end with "Request" to identify them
|
||||||
if self.is_function:
|
if is_function:
|
||||||
result += 'Request'
|
result += 'Request'
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -192,6 +197,7 @@ class TLArg:
|
||||||
# Default values
|
# Default values
|
||||||
self.is_vector = False
|
self.is_vector = False
|
||||||
self.is_flag = False
|
self.is_flag = False
|
||||||
|
self.skip_constructor_id = False
|
||||||
self.flag_index = -1
|
self.flag_index = -1
|
||||||
|
|
||||||
# Special case: some types can be inferred, which makes it
|
# Special case: some types can be inferred, which makes it
|
||||||
|
@ -222,7 +228,7 @@ class TLArg:
|
||||||
self.type = flag_match.group(2)
|
self.type = flag_match.group(2)
|
||||||
|
|
||||||
# Then check if the type is a Vector<REAL_TYPE>
|
# Then check if the type is a Vector<REAL_TYPE>
|
||||||
vector_match = re.match(r'vector<(\w+)>', self.type, re.IGNORECASE)
|
vector_match = re.match(r'[Vv]ector<([\w\d.]+)>', self.type)
|
||||||
if vector_match:
|
if vector_match:
|
||||||
self.is_vector = True
|
self.is_vector = True
|
||||||
|
|
||||||
|
@ -234,6 +240,11 @@ class TLArg:
|
||||||
# Update the type to match the one inside the vector
|
# Update the type to match the one inside the vector
|
||||||
self.type = vector_match.group(1)
|
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",
|
# The name may contain "date" in it, if this is the case and the type is "int",
|
||||||
# we can safely assume that this should be treated as a "date" object.
|
# we can safely assume that this should be treated as a "date" object.
|
||||||
# Note that this is not a valid Telegram object, but it's easier to work with
|
# 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 zlib import crc32
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from .parser import SourceBuilder, TLParser
|
from .parser import SourceBuilder, TLParser, TLObject
|
||||||
AUTO_GEN_NOTICE = \
|
AUTO_GEN_NOTICE = \
|
||||||
'"""File generated by TLObjects\' generator. All changes will be ERASED"""'
|
'"""File generated by TLObjects\' generator. All changes will be ERASED"""'
|
||||||
|
|
||||||
|
@ -129,6 +129,9 @@ class TLGenerator:
|
||||||
builder.writeln(
|
builder.writeln(
|
||||||
'from {}.tl.tlobject import TLObject'.format('.' * depth)
|
'from {}.tl.tlobject import TLObject'.format('.' * depth)
|
||||||
)
|
)
|
||||||
|
builder.writeln(
|
||||||
|
'from {}.tl import types'.format('.' * depth)
|
||||||
|
)
|
||||||
|
|
||||||
# Add the relative imports to the namespaces,
|
# Add the relative imports to the namespaces,
|
||||||
# unless we already are in a namespace.
|
# unless we already are in a namespace.
|
||||||
|
@ -143,7 +146,7 @@ class TLGenerator:
|
||||||
builder.writeln(
|
builder.writeln(
|
||||||
'from {}.utils import get_input_peer, '
|
'from {}.utils import get_input_peer, '
|
||||||
'get_input_channel, get_input_user, '
|
'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()'
|
# Import 'os' for those needing access to 'os.urandom()'
|
||||||
|
@ -151,7 +154,7 @@ class TLGenerator:
|
||||||
# for all those TLObjects with arg.can_be_inferred.
|
# for all those TLObjects with arg.can_be_inferred.
|
||||||
builder.writeln('import os')
|
builder.writeln('import os')
|
||||||
|
|
||||||
# Import struct for the .to_bytes(self) serialization
|
# Import struct for the .__bytes__(self) serialization
|
||||||
builder.writeln('import struct')
|
builder.writeln('import struct')
|
||||||
|
|
||||||
# Generate the class for every TLObject
|
# Generate the class for every TLObject
|
||||||
|
@ -299,8 +302,8 @@ class TLGenerator:
|
||||||
|
|
||||||
builder.end_block()
|
builder.end_block()
|
||||||
|
|
||||||
# Write the .to_bytes() function
|
# Write the .__bytes__() function
|
||||||
builder.writeln('def to_bytes(self):')
|
builder.writeln('def __bytes__(self):')
|
||||||
|
|
||||||
# Some objects require more than one flag parameter to be set
|
# Some objects require more than one flag parameter to be set
|
||||||
# at the same time. In this case, add an assertion.
|
# at the same time. In this case, add an assertion.
|
||||||
|
@ -311,11 +314,11 @@ class TLGenerator:
|
||||||
|
|
||||||
for ra in repeated_args.values():
|
for ra in repeated_args.values():
|
||||||
if len(ra) > 1:
|
if len(ra) > 1:
|
||||||
cnd1 = ('self.{} is None'.format(a.name) for a in ra)
|
cnd1 = ('self.{}'.format(a.name) for a in ra)
|
||||||
cnd2 = ('self.{} is not None'.format(a.name) for a in ra)
|
cnd2 = ('not self.{}'.format(a.name) for a in ra)
|
||||||
builder.writeln(
|
builder.writeln(
|
||||||
"assert ({}) or ({}), '{} parameters must all "
|
"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),
|
' and '.join(cnd1), ' and '.join(cnd2),
|
||||||
', '.join(a.name for a in ra)
|
', '.join(a.name for a in ra)
|
||||||
)
|
)
|
||||||
|
@ -335,32 +338,28 @@ class TLGenerator:
|
||||||
builder.writeln('))')
|
builder.writeln('))')
|
||||||
builder.end_block()
|
builder.end_block()
|
||||||
|
|
||||||
# Write the empty() function, which returns an "empty"
|
# Write the static from_reader(reader) function
|
||||||
# instance, in which all attributes are set to None
|
|
||||||
builder.writeln('@staticmethod')
|
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(
|
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()
|
builder.end_block()
|
||||||
|
|
||||||
# Write the on_response(self, reader) function
|
# Only requests can have a different response that's not their
|
||||||
builder.writeln('def on_response(self, reader):')
|
# serialized body, that is, we'll be setting their .result.
|
||||||
# Do not read constructor's ID, since
|
|
||||||
# that's already been read somewhere else
|
|
||||||
if tlobject.is_function:
|
if tlobject.is_function:
|
||||||
|
builder.writeln('def on_response(self, reader):')
|
||||||
TLGenerator.write_request_result_code(builder, tlobject)
|
TLGenerator.write_request_result_code(builder, tlobject)
|
||||||
else:
|
builder.end_block()
|
||||||
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()
|
|
||||||
|
|
||||||
# Write the __str__(self) and stringify(self) functions
|
# Write the __str__(self) and stringify(self) functions
|
||||||
builder.writeln('def __str__(self):')
|
builder.writeln('def __str__(self):')
|
||||||
|
@ -406,6 +405,8 @@ class TLGenerator:
|
||||||
TLGenerator.write_get_input(builder, arg, 'get_input_user')
|
TLGenerator.write_get_input(builder, arg, 'get_input_user')
|
||||||
elif arg.type == 'InputMedia' and tlobject.is_function:
|
elif arg.type == 'InputMedia' and tlobject.is_function:
|
||||||
TLGenerator.write_get_input(builder, arg, 'get_input_media')
|
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:
|
else:
|
||||||
builder.writeln('self.{0} = {0}'.format(arg.name))
|
builder.writeln('self.{0} = {0}'.format(arg.name))
|
||||||
|
@ -440,10 +441,10 @@ class TLGenerator:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def write_to_bytes(builder, arg, args, name=None):
|
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 builder: The source code builder
|
||||||
:param arg: The argument to write
|
: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
|
This is required to determine the flags value
|
||||||
:param name: The name of the argument. Defaults to "self.argname"
|
:param name: The name of the argument. Defaults to "self.argname"
|
||||||
This argument is an option because it's required when
|
This argument is an option because it's required when
|
||||||
|
@ -539,7 +540,7 @@ class TLGenerator:
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Else it may be a custom type
|
# Else it may be a custom type
|
||||||
builder.write('{}.to_bytes()'.format(name))
|
builder.write('bytes({})'.format(name))
|
||||||
|
|
||||||
if arg.is_flag:
|
if arg.is_flag:
|
||||||
builder.write(')')
|
builder.write(')')
|
||||||
|
@ -549,9 +550,10 @@ class TLGenerator:
|
||||||
return True # Something was written
|
return True # Something was written
|
||||||
|
|
||||||
@staticmethod
|
@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 builder: The source code builder
|
||||||
:param arg: The argument to write
|
:param arg: The argument to write
|
||||||
|
@ -565,12 +567,17 @@ class TLGenerator:
|
||||||
if arg.generic_definition:
|
if arg.generic_definition:
|
||||||
return # Do nothing, this only specifies a later type
|
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!
|
# The argument may be a flag, only write that flag was given!
|
||||||
was_flag = False
|
was_flag = False
|
||||||
if arg.is_flag:
|
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
|
was_flag = True
|
||||||
builder.writeln('if flags & {}:'.format(
|
builder.writeln('if flags & {}:'.format(
|
||||||
1 << arg.flag_index
|
1 << arg.flag_index
|
||||||
|
@ -585,11 +592,10 @@ class TLGenerator:
|
||||||
builder.writeln("reader.read_int()")
|
builder.writeln("reader.read_int()")
|
||||||
|
|
||||||
builder.writeln('{} = []'.format(name))
|
builder.writeln('{} = []'.format(name))
|
||||||
builder.writeln('_len = reader.read_int()')
|
builder.writeln('for _ in range(reader.read_int()):')
|
||||||
builder.writeln('for _ in range(_len):')
|
|
||||||
# Temporary disable .is_vector, not to enter this if again
|
# Temporary disable .is_vector, not to enter this if again
|
||||||
arg.is_vector = False
|
arg.is_vector = False
|
||||||
TLGenerator.write_onresponse_code(builder, arg, args, name='_x')
|
TLGenerator.write_read_code(builder, arg, args, name='_x')
|
||||||
builder.writeln('{}.append(_x)'.format(name))
|
builder.writeln('{}.append(_x)'.format(name))
|
||||||
arg.is_vector = True
|
arg.is_vector = True
|
||||||
|
|
||||||
|
@ -635,14 +641,21 @@ class TLGenerator:
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Else it may be a custom type
|
# 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)
|
# End vector and flag blocks if required (if we opened them before)
|
||||||
if arg.is_vector:
|
if arg.is_vector:
|
||||||
builder.end_block()
|
builder.end_block()
|
||||||
|
|
||||||
if was_flag:
|
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
|
# Restore .is_flag
|
||||||
arg.is_flag = True
|
arg.is_flag = True
|
||||||
|
|
||||||
|
|
|
@ -107,17 +107,17 @@ class CryptoTests(unittest.TestCase):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def test_generate_key_data_from_nonce():
|
def test_generate_key_data_from_nonce():
|
||||||
server_nonce = b'I am the server nonce.'
|
server_nonce = int.from_bytes(b'The 16-bit nonce', byteorder='little')
|
||||||
new_nonce = b'I am a new calculated nonce.'
|
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)
|
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_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'Q\x9dpZ\xb7\xdd\xcb\x82_\xfa\xf4\x90\xecn\x10\x9cD\xd2\x01\x8d\x83\xa0\xa4^\xb8\x91,\x7fI am'
|
expected_iv = b'\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(
|
assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format(
|
||||||
key, expected_key)
|
key, expected_key)
|
||||||
assert iv == expected_iv, 'Key ("{}") does not equal expected ("{}")'.format(
|
assert iv == expected_iv, 'IV ("{}") does not equal expected ("{}")'.format(
|
||||||
key, expected_iv)
|
iv, expected_iv)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def test_fingerprint_from_key():
|
def test_fingerprint_from_key():
|
||||||
|
|
Loading…
Reference in New Issue
Block a user