Merge branch 'master' into asyncio

This commit is contained in:
Lonami Exo 2017-10-21 15:45:56 +02:00
commit 335bc6a789
20 changed files with 343 additions and 694 deletions

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@ class MtProtoSender:
self._logger = logging.getLogger(__name__)
# Message IDs that need confirmation
self._need_confirmation = []
self._need_confirmation = set()
# Requests (as msg_id: Message) sent waiting to be received
self._pending_receive = {}
@ -74,7 +74,7 @@ class MtProtoSender:
# Pack everything in the same container if we need to send AckRequests
if self._need_confirmation:
messages.append(
TLMessage(self.session, MsgsAck(self._need_confirmation))
TLMessage(self.session, MsgsAck(list(self._need_confirmation)))
)
self._need_confirmation.clear()
@ -125,7 +125,7 @@ class MtProtoSender:
plain_text = \
struct.pack('<QQ', self.session.salt, self.session.id) \
+ message.to_bytes()
+ bytes(message)
msg_key = utils.calc_msg_key(plain_text)
key_id = struct.pack('<Q', self.session.auth_key.key_id)
@ -174,7 +174,7 @@ class MtProtoSender:
"""
# TODO Check salt, session_id and sequence_number
self._need_confirmation.append(msg_id)
self._need_confirmation.add(msg_id)
code = reader.read_int(signed=False)
reader.seek(-4)
@ -217,7 +217,7 @@ class MtProtoSender:
r = self._pop_request_of_type(msg_id, LogOutRequest)
if r:
r.result = True # Telegram won't send this value
r.confirm_received()
r.confirm_received.set()
self._logger.debug('Message ack confirmed', r)
return True
@ -261,7 +261,7 @@ class MtProtoSender:
def _clear_all_pending(self):
for r in self._pending_receive.values():
r.confirm_received.set()
r.request.confirm_received.set()
self._pending_receive.clear()
async def _handle_pong(self, msg_id, sequence, reader):
@ -303,6 +303,7 @@ class MtProtoSender:
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:
@ -411,6 +412,11 @@ class MtProtoSender:
async def _handle_gzip_packed(self, msg_id, sequence, reader, state):
self._logger.debug('Handling gzip packed data')
with BinaryReader(GzipPacked.read(reader)) as compressed_reader:
# We are reentering process_msg, which seemingly the same msg_id
# to the self._need_confirmation set. Remove it from there first
# to avoid any future conflicts (i.e. if we "ignore" messages
# that we are already aware of, see 1a91c02 and old 63dfb1e)
self._need_confirmation -= {msg_id}
return await self._process_msg(msg_id, sequence, compressed_reader, state)
# endregion

View File

@ -56,7 +56,7 @@ class TelegramBareClient:
"""
# Current TelegramClient version
__version__ = '0.15.2'
__version__ = '0.15.3'
# TODO Make this thread-safe, all connections share the same DC
_dc_options = None
@ -124,7 +124,7 @@ class TelegramBareClient:
self._user_connected = False
# Save whether the user is authorized here (a.k.a. logged in)
self._authorized = False
self._authorized = None # None = We don't know yet
# Uploaded files cache so subsequent calls are instant
self._upload_cache = {}
@ -198,12 +198,14 @@ class TelegramBareClient:
# another data center and this would raise UserMigrateError)
# to also assert whether the user is logged in or not.
self._user_connected = True
if _sync_updates and not _cdn:
if self._authorized is None and _sync_updates and not _cdn:
try:
await self.sync_updates()
self._set_connected_and_authorized()
except UnauthorizedError:
self._authorized = False
elif self._authorized:
self._set_connected_and_authorized()
return True
@ -383,6 +385,8 @@ class TelegramBareClient:
# TODO Determine the sender to be used (main or a new connection)
sender = self._sender # .clone(), .connect()
# We're on the same connection so no need to pass update_state=None
# to avoid getting messages that we haven't acknowledged yet.
try:
for _ in range(retries):
@ -426,10 +430,7 @@ class TelegramBareClient:
else:
while self._user_connected and not await self._reconnect():
sleep(0.1) # Retry forever until we can send the request
finally:
if sender != self._sender:
sender.disconnect()
return None
try:
raise next(x.rpc_error for x in requests if x.rpc_error)
@ -673,13 +674,14 @@ class TelegramBareClient:
def add_update_handler(self, handler):
"""Adds an update handler (a function which takes a TLObject,
an update, as its parameter) and listens for updates"""
if not self.updates.get_workers:
warnings.warn("There are no update workers running, so adding an update handler will have no effect.")
if self.updates.workers is None:
warnings.warn(
"You have not setup any workers, so you won't receive updates."
" Pass update_workers=4 when creating the TelegramClient,"
" or set client.self.updates.workers = 4"
)
sync = not self.updates.handlers
self.updates.handlers.append(handler)
if sync:
self.sync_updates()
def remove_update_handler(self, handler):
self.updates.handlers.remove(handler)

View File

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

View File

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

View File

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

View File

@ -125,7 +125,7 @@ class TLObject:
def to_dict(self, recursive=True):
return {}
def to_bytes(self):
def __bytes__(self):
return b''
@staticmethod

View File

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

View File

@ -0,0 +1,164 @@
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(
'\n\nclass {}({}):\n def __init__(self, **kwargs):\n '
''.format(name, get_class_name(code))
)
if capture_name:
f.write(
"self.{} = int(kwargs.get('capture', 0))\n ".format(capture_name)
)
f.write('super(Exception, self).__init__(self, {}'.format(repr(desc)))
if capture_name:
f.write('.format(self.{})'.format(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(
'from .rpc_base_errors import RPCError, BadMessageError, {}\n'.format(
", ".join(known_base_classes.values()))
)
for code, cls in needed_base_classes:
f.write(
'\n\nclass {}(RPCError):\n code = {}\n'.format(cls, code)
)
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(' {}: {},\n'.format(repr(pattern), name))
f.write('}\n')

File diff suppressed because one or more lines are too long

View File

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

View File

@ -5,7 +5,7 @@ import struct
from zlib import crc32
from collections import defaultdict
from .parser import SourceBuilder, TLParser
from .parser import SourceBuilder, TLParser, TLObject
AUTO_GEN_NOTICE = \
'"""File generated by TLObjects\' generator. All changes will be ERASED"""'
@ -129,6 +129,9 @@ class TLGenerator:
builder.writeln(
'from {}.tl.tlobject import TLObject'.format('.' * depth)
)
builder.writeln(
'from {}.tl import types'.format('.' * depth)
)
# Add the relative imports to the namespaces,
# unless we already are in a namespace.
@ -151,7 +154,7 @@ class TLGenerator:
# for all those TLObjects with arg.can_be_inferred.
builder.writeln('import os')
# Import struct for the .to_bytes(self) serialization
# Import struct for the .__bytes__(self) serialization
builder.writeln('import struct')
# Generate the class for every TLObject
@ -299,8 +302,8 @@ class TLGenerator:
builder.end_block()
# Write the .to_bytes() function
builder.writeln('def to_bytes(self):')
# Write the .__bytes__() function
builder.writeln('def __bytes__(self):')
# Some objects require more than one flag parameter to be set
# at the same time. In this case, add an assertion.
@ -311,11 +314,11 @@ class TLGenerator:
for ra in repeated_args.values():
if len(ra) > 1:
cnd1 = ('self.{} is None'.format(a.name) for a in ra)
cnd2 = ('self.{} is not None'.format(a.name) for a in ra)
cnd1 = ('self.{}'.format(a.name) for a in ra)
cnd2 = ('not self.{}'.format(a.name) for a in ra)
builder.writeln(
"assert ({}) or ({}), '{} parameters must all "
"be None or neither be None'".format(
"be False-y (like None) or all me True-y'".format(
' and '.join(cnd1), ' and '.join(cnd2),
', '.join(a.name for a in ra)
)
@ -438,10 +441,10 @@ class TLGenerator:
@staticmethod
def write_to_bytes(builder, arg, args, name=None):
"""
Writes the .to_bytes() code for the given argument
Writes the .__bytes__() code for the given argument
:param builder: The source code builder
:param arg: The argument to write
:param args: All the other arguments in TLObject same to_bytes.
:param args: All the other arguments in TLObject same __bytes__.
This is required to determine the flags value
:param name: The name of the argument. Defaults to "self.argname"
This argument is an option because it's required when
@ -537,7 +540,7 @@ class TLGenerator:
else:
# Else it may be a custom type
builder.write('{}.to_bytes()'.format(name))
builder.write('bytes({})'.format(name))
if arg.is_flag:
builder.write(')')
@ -638,7 +641,11 @@ class TLGenerator:
else:
# Else it may be a custom type
builder.writeln('{} = reader.tgread_object()'.format(name))
if not arg.skip_constructor_id:
builder.writeln('{} = reader.tgread_object()'.format(name))
else:
builder.writeln('{} = types.{}.from_reader(reader)'.format(
name, TLObject.class_name_for(arg.type)))
# End vector and flag blocks if required (if we opened them before)
if arg.is_vector: