From e0e3947d2f45d1ee483ee558d4337df02541caca Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 18 Sep 2017 09:37:20 +0200 Subject: [PATCH 1/9] Fix some misleading errors/documentation --- telethon/errors/rpc_errors_400.py | 24 +++++++++++++++++------- telethon/telegram_client.py | 3 +-- 2 files changed, 18 insertions(+), 9 deletions(-) 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/telegram_client.py b/telethon/telegram_client.py index 5fc6b1f6..4f123055 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -292,8 +292,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 From e3ab98815d50a70bcaeffd9133142c63960841e1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 18 Sep 2017 10:21:53 +0200 Subject: [PATCH 2/9] Fix reconnect on TypeNotFoundError failing --- telethon/telegram_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 4f123055..38cf4303 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -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. From 143e046cf523a6d342e1804d821345411fdbdf9f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 18 Sep 2017 10:59:54 +0200 Subject: [PATCH 3/9] Attempt at passing errors to the main thread through .updates --- telethon/telegram_client.py | 7 +++++++ telethon/update_state.py | 20 +++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 38cf4303..bf915b04 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -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 @@ -1025,5 +1027,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..a6cf324b 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. From 0235fce99c7bfc39a9293dce3956859febfb0bd0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 18 Sep 2017 11:01:15 +0200 Subject: [PATCH 4/9] Don't hold ._updates_lock while calling .handlers --- telethon/update_state.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/telethon/update_state.py b/telethon/update_state.py index a6cf324b..2f313dea 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -77,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) From 200d1d67be9f95515be1c62586f77ad6601e591d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 18 Sep 2017 11:45:08 +0200 Subject: [PATCH 5/9] Make BufferError message more useful --- telethon/extensions/binary_reader.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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): From 91f44613a8ecb75f8b271ae2ef2479c709564620 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 18 Sep 2017 11:59:55 +0200 Subject: [PATCH 6/9] Fix SendMessageRequest could return UpdateShortSentMessage --- telethon/telegram_client.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index bf915b04..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 @@ -418,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 From e12e82357dc01cb5cdf0530164b6011f3d6ef0ba Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 18 Sep 2017 13:32:16 +0200 Subject: [PATCH 7/9] Update to v0.13.4 --- telethon/telegram_bare_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From aea95a398bb15992601513737591351025d2f001 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 18 Sep 2017 14:03:06 +0200 Subject: [PATCH 8/9] Add a pypi option to setup.py to easily update the library --- setup.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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.') From 879621ab708479b2aa26c72893e442c1d8bfcbda Mon Sep 17 00:00:00 2001 From: Tanuj Date: Mon, 18 Sep 2017 19:09:58 +0100 Subject: [PATCH 9/9] Add new example usage code for auto-replies (#249) --- telethon_examples/auto_reply.py | 113 ++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100755 telethon_examples/auto_reply.py diff --git a/telethon_examples/auto_reply.py b/telethon_examples/auto_reply.py new file mode 100755 index 00000000..67b5d841 --- /dev/null +++ b/telethon_examples/auto_reply.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# disclaimer: you should not actually use this. it can be quite spammy. +from telethon import TelegramClient +from telethon.errors import SessionPasswordNeededError +from getpass import getpass +from telethon.tl.types import InputPeerUser,InputPeerChannel +from telethon.tl.types import Updates +from telethon.tl.types import UpdateNewChannelMessage,UpdateNewMessage +from telethon.tl.functions.messages import SendMessageRequest,EditMessageRequest +from telethon.tl.types import MessageService +from nltk.tokenize import word_tokenize +from os import environ +from time import sleep + +CHANNELS = {} +CHANNELNAMES = {} +USERS = {} +EMACS_BLACKLIST = [1058260578, # si @linux_group + 123456789] +REACTS = {'emacs':'Needs more vim.', + 'chrome':'Needs more firefox.', +} + +class NeedsMore(TelegramClient): + def __init__(self): + settings = {'api_id':int(environ['TG_API_ID']), + 'api_hash':environ['TG_API_HASH'], + 'user_phone':environ['TG_PHONE'], + 'session_name':'needsmore'} + super().__init__( + settings.get('session_name','session1'), + settings['api_id'], + settings['api_hash'], + proxy=None, + process_updates=True) + + user_phone = settings['user_phone'] + + print('INFO: Connecting to Telegram Servers...', end='', flush=True) + self.connect() + print('Done!') + + if not self.is_user_authorized(): + print('INFO: Unauthorized user') + self.send_code_request(user_phone) + code_ok = False + while not code_ok: + code = input('Enter the auth code: ') + try: + code_ok = self.sign_in(user_phone, code) + except SessionPasswordNeededError: + pw = getpass('Two step verification enabled. Please enter your password: ') + self.sign_in(password=pw) + print('INFO: Client initialized succesfully!') + + def run(self): + # Listen for updates + while True: + update = self.updates.poll() # This will block until an update is available + triggers = [] + if isinstance(update, Updates): + for x in update.updates: + if not isinstance(x,UpdateNewChannelMessage): continue + if isinstance(x.message,MessageService): continue + # We're only interested in messages to supergroups + words = word_tokenize(x.message.message.lower()) + # Avoid matching 'emacs' in 'spacemacs' and similar + if 'emacs' in words and x.message.to_id.channel_id not in EMACS_BLACKLIST: + triggers.append(('emacs',x.message)) + if 'chrome' in words: + triggers.append(('chrome',x.message)) + if 'x files theme' == x.message.message.lower() and x.message.out: + # Automatically reply to yourself saying 'x files theme' with the audio + msg = x.message + chan = InputPeerChannel(msg.to_id.channel_id,CHANNELS[msg.to_id.channel_id]) + self.send_voice_note(chan,'xfiles.m4a',reply_to=msg.id) + sleep(1) + if '.shrug' in x.message.message.lower() and x.message.out: + # Automatically replace '.shrug' in any message you + # send to a supergroup with the shrug emoticon + msg = x.message + chan = InputPeerChannel(msg.to_id.channel_id,CHANNELS[msg.to_id.channel_id]) + self(EditMessageRequest(chan,msg.id, + message=msg.message.replace('.shrug','¯\_(ツ)_/¯'))) + sleep(1) + +f for trigger in triggers: + msg = trigger[1] + chan = InputPeerChannel(msg.to_id.channel_id,CHANNELS[msg.to_id.channel_id]) + log_chat = InputPeerUser(user_id=123456789,access_hash=987654321234567890) + self.send_message(log_chat,"{} said {} in {}. Sending react {}".format( + msg.from_id,msg.message,CHANNELNAMES[msg.to_id.channel_id],REACTS[trigger[0]][:20])) + react = '>{}\n{}'.format(trigger[0],REACTS[trigger[0]]) + self.invoke(SendMessageRequest(chan,react,reply_to_msg_id=msg.id)) + sleep(1) + +if __name__ == "__main__": + #TODO: this block could be moved to __init__ + # You can create these text files using https://github.com/LonamiWebs/Telethon/wiki/Retrieving-all-dialogs + with open('channels.txt','r') as f: + # Format: channel_id access_hash #Channel Name + lines = f.readlines() + chans = [l.split(' #',1)[0].split(' ') for l in lines] + CHANNELS = {int(c[0]):int(c[1]) for c in chans} # id:hash + CHANNELNAMES = {int(l.split()[0]):l.split('#',1)[1].strip() for l in lines} #id:name + with open('users','r') as f: + # Format: [user_id, access_hash, 'username', 'Firstname Lastname'] + lines = f.readlines() + uss = [l.strip()[1:-1].split(',') for l in lines] + USERS = {int(user[0]):int(user[1]) for user in uss} # id:hash + + needsmore = NeedsMore() + needsmore.run()