diff --git a/setup.py b/setup.py index 6e11c079..679d068b 100755 --- a/setup.py +++ b/setup.py @@ -8,9 +8,12 @@ https://github.com/pypa/sampleproject Extra supported commands are: * gen_tl, to generate the classes required for Telethon to run * clean_tl, to clean these generated classes +* pypi, to generate sdist, bdist_wheel, and push to PyPi """ # To use a consistent encoding +from subprocess import run +from shutil import rmtree from codecs import open from sys import argv from os import path @@ -44,6 +47,15 @@ if __name__ == '__main__': TLGenerator('telethon/tl').clean_tlobjects() print('Done.') + elif len(argv) >= 2 and argv[1] == 'pypi': + for x in ('build', 'dist', 'Telethon.egg-info'): + rmtree(x, ignore_errors=True) + run('python3 setup.py sdist', shell=True) + run('python3 setup.py bdist_wheel', shell=True) + run('twine upload dist/*', shell=True) + for x in ('build', 'dist', 'Telethon.egg-info'): + rmtree(x, ignore_errors=True) + else: if not TelegramClient: print('Run `python3', argv[0], 'gen_tl` first.') diff --git a/telethon/errors/rpc_errors_400.py b/telethon/errors/rpc_errors_400.py index 216910c8..68874199 100644 --- a/telethon/errors/rpc_errors_400.py +++ b/telethon/errors/rpc_errors_400.py @@ -22,7 +22,9 @@ class ChannelInvalidError(BadRequestError): def __init__(self, **kwargs): super(Exception, self).__init__( self, - 'Invalid channel object. Make sure to pass the right types.' + '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.' ) @@ -40,7 +42,12 @@ 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.' + '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.' ) @@ -48,7 +55,8 @@ class ConnectionLangPackInvalid(BadRequestError): def __init__(self, **kwargs): super(Exception, self).__init__( self, - 'The specified language pack is not valid.' + 'The specified language pack is not valid. This is meant to be ' + 'used by official applications only so far, leave it empty.' ) @@ -284,7 +292,7 @@ class UsernameInvalidError(BadRequestError): def __init__(self, **kwargs): super(Exception, self).__init__( self, - 'Unacceptable username. Must match r"[a-zA-Z][\w\d]{4,31}"' + 'Unacceptable username. Must match r"[a-zA-Z][\w\d]{4,31}".' ) @@ -292,7 +300,7 @@ class UsernameNotModifiedError(BadRequestError): def __init__(self, **kwargs): super(Exception, self).__init__( self, - 'The username is not different from the current username' + 'The username is not different from the current username.' ) @@ -300,7 +308,7 @@ class UsernameNotOccupiedError(BadRequestError): def __init__(self, **kwargs): super(Exception, self).__init__( self, - 'See issue #96 for Telethon - try upgrading the library.' + 'The username is not in use by anyone else yet.' ) @@ -333,7 +341,9 @@ 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.' + '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.' ) diff --git a/telethon/extensions/binary_reader.py b/telethon/extensions/binary_reader.py index 6770115c..43232b0b 100644 --- a/telethon/extensions/binary_reader.py +++ b/telethon/extensions/binary_reader.py @@ -23,6 +23,7 @@ class BinaryReader: 'Either bytes or a stream must be provided') self.reader = BufferedReader(self.stream) + self._last = None # Should come in handy to spot -404 errors # region Reading @@ -57,8 +58,12 @@ class BinaryReader: """Read the given amount of bytes""" result = self.reader.read(length) if len(result) != length: - raise BufferError('No more data left to read') + raise BufferError( + 'No more data left to read (need {}, got {}: {}); last read {}' + .format(length, len(result), repr(result), repr(self._last)) + ) + self._last = result return result def get_bytes(self): diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index f4ecf490..3a8b27dc 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -52,7 +52,7 @@ class TelegramBareClient: """ # Current TelegramClient version - __version__ = '0.13.3' + __version__ = '0.13.4' # TODO Make this thread-safe, all connections share the same DC _dc_options = None diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 5fc6b1f6..5d3f5ae2 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -41,7 +41,7 @@ from .tl.types import ( InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty, Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto, InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID, - UpdateNewMessage + UpdateNewMessage, UpdateShortSentMessage ) from .utils import find_user_or_chat, get_extension @@ -136,18 +136,18 @@ class TelegramClient(TelegramBareClient): # region Connecting - def connect(self, *args): + def connect(self, exported_auth=None): """Connects to the Telegram servers, executing authentication if required. Note that authenticating to the Telegram servers is not the same as authenticating the desired user itself, which may require a call (or several) to 'sign_in' for the first time. - *args will be ignored. + exported_auth is meant for internal purposes and can be ignored. """ if self._sender and self._sender.is_connected(): return - ok = super().connect() + ok = super().connect(exported_auth=exported_auth) # The main TelegramClient is the only one that will have # constant_read, since it's also the only one who receives # updates and need to be processed as soon as they occur. @@ -230,6 +230,8 @@ class TelegramClient(TelegramBareClient): threading.get_ident() == self._recv_thread.ident: raise AssertionError('Cannot invoke requests from the ReadThread') + self.updates.check_error() + try: # Users may call this method from within some update handler. # If this is the case, then the thread invoking the request @@ -292,8 +294,7 @@ class TelegramClient(TelegramBareClient): If no phone or code is provided, then the sole password will be used. The password should be used after a normal authorization attempt - has happened and an RPCError with `.password_required = True` was - raised. + has happened and an SessionPasswordNeededError was raised. To login as a bot, only `bot_token` should be provided. This should equal to the bot access hash provided by @@ -417,14 +418,26 @@ class TelegramClient(TelegramBareClient): If 'reply_to' is set to either a message or a message ID, the sent message will be replying to such message. """ + entity = self.get_entity(entity) request = SendMessageRequest( - peer=self.get_entity(entity), + peer=entity, message=message, entities=[], no_webpage=not link_preview, reply_to_msg_id=self._get_reply_to(reply_to) ) result = self(request) + if isinstance(request, UpdateShortSentMessage): + return Message( + id=result.id, + to_id=entity, + message=message, + date=result.date, + out=result.out, + media=result.media, + entities=result.entities + ) + # Telegram seems to send updateMessageID first, then updateNewMessage, # however let's not rely on that just in case. msg_id = None @@ -1026,5 +1039,10 @@ class TelegramClient(TelegramBareClient): self._recv_thread = None # Not running anymore self.reconnect() return + except Exception as e: + # Unknown exception, pass it to the main thread + self.updates.set_error(e) + self._recv_thread = None + return # endregion diff --git a/telethon/update_state.py b/telethon/update_state.py index f9303258..2f313dea 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -34,7 +34,10 @@ class UpdateState: if not self._updates: self._updates_available.clear() - return update + if isinstance(update, Exception): + raise update # Some error was set through .set_error() + + return update def get_polling(self): return self._polling @@ -47,6 +50,21 @@ class UpdateState: polling = property(fget=get_polling, fset=set_polling) + def set_error(self, error): + """Sets an error, so that the next call to .poll() will raise it. + Can be (and is) used to pass exceptions between threads. + """ + with self._updates_lock: + # Insert at the beginning so the very next poll causes an error + # TODO Should this reset the pts and such? + self._updates.insert(0, error) + self._updates_available.set() + + def check_error(self): + with self._updates_lock: + if self._updates and isinstance(self._updates[0], Exception): + raise self._updates.pop() + def process(self, update): """Processes an update object. This method is normally called by the library itself. @@ -59,9 +77,10 @@ class UpdateState: self._state = update elif not hasattr(update, 'pts') or update.pts > self._state.pts: self._state.pts = getattr(update, 'pts', self._state.pts) - for handler in self.handlers: - handler(update) if self._polling: self._updates.append(update) self._updates_available.set() + + for handler in self.handlers: + handler(update)