From 83d9d1d78e536b77834b27b05c5e6789268b2e7a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 16 Feb 2018 20:30:19 +0100 Subject: [PATCH 01/34] Fix markdown parser not inverting delimiters dict --- telethon/extensions/markdown.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 92589a17..e1fd04b7 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -169,6 +169,7 @@ def unparse(text, entities, delimiters=None, url_fmt=None): entities = tuple(sorted(entities, key=lambda e: e.offset, reverse=True)) text = _add_surrogate(text) + delimiters = {v: k for k, v in delimiters.items()} for entity in entities: s = entity.offset e = entity.offset + entity.length From 3c6f34fe6a801f084ebb66dc210d501f3b93aa8e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 16 Feb 2018 21:02:47 +0100 Subject: [PATCH 02/34] Update examples --- telethon_examples/print_updates.py | 34 +++---- telethon_examples/replier.py | 137 +++++++++-------------------- 2 files changed, 55 insertions(+), 116 deletions(-) diff --git a/telethon_examples/print_updates.py b/telethon_examples/print_updates.py index ab7ba1d4..4c676a81 100755 --- a/telethon_examples/print_updates.py +++ b/telethon_examples/print_updates.py @@ -1,46 +1,36 @@ #!/usr/bin/env python3 # A simple script to print all updates received -from getpass import getpass from os import environ + # environ is used to get API information from environment variables # You could also use a config file, pass them as arguments, # or even hardcode them (not recommended) from telethon import TelegramClient -from telethon.errors import SessionPasswordNeededError + def main(): session_name = environ.get('TG_SESSION', 'session') - user_phone = environ['TG_PHONE'] client = TelegramClient(session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'], proxy=None, - update_workers=4) + update_workers=4, + spawn_read_thread=False) - print('INFO: Connecting to Telegram Servers...', end='', flush=True) - client.connect() - print('Done!') - - if not client.is_user_authorized(): - print('INFO: Unauthorized user') - client.send_code_request(user_phone) - code_ok = False - while not code_ok: - code = input('Enter the auth code: ') - try: - code_ok = client.sign_in(user_phone, code) - except SessionPasswordNeededError: - password = getpass('Two step verification enabled. Please enter your password: ') - code_ok = client.sign_in(password=password) - print('INFO: Client initialized succesfully!') + if 'TG_PHONE' in environ: + client.start(phone=environ['TG_PHONE']) + else: + client.start() client.add_update_handler(update_handler) - input('Press Enter to stop this!\n') + print('(Press Ctrl+C to stop this)') + client.idle() + def update_handler(update): print(update) - print('Press Enter to stop this!') + if __name__ == '__main__': main() diff --git a/telethon_examples/replier.py b/telethon_examples/replier.py index 66026363..ed4cc2fa 100755 --- a/telethon_examples/replier.py +++ b/telethon_examples/replier.py @@ -9,17 +9,12 @@ file, including TG_API_ID, TG_API_HASH, TG_PHONE and optionally TG_SESSION. This script assumes that you have certain files on the working directory, such as "xfiles.m4a" or "anytime.png" for some of the automated replies. """ -from getpass import getpass +import re from collections import defaultdict from datetime import datetime, timedelta from os import environ -import re - -from telethon import TelegramClient -from telethon.errors import SessionPasswordNeededError -from telethon.tl.types import UpdateNewChannelMessage, UpdateShortMessage, MessageService -from telethon.tl.functions.messages import EditMessageRequest +from telethon import TelegramClient, events, utils """Uncomment this for debugging import logging @@ -35,103 +30,57 @@ REACTS = {'emacs': 'Needs more vim', recent_reacts = defaultdict(list) -def update_handler(update): - global recent_reacts - try: - msg = update.message - except AttributeError: - # print(update, 'did not have update.message') - return - if isinstance(msg, MessageService): - print(msg, 'was service msg') - return +if __name__ == '__main__': + # TG_API_ID and TG_API_HASH *must* exist or this won't run! + session_name = environ.get('TG_SESSION', 'session') + client = TelegramClient( + session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'], + spawn_read_thread=False, proxy=None, update_workers=4 + ) - # React to messages in supergroups and PMs - if isinstance(update, UpdateNewChannelMessage): - words = re.split('\W+', msg.message) + @client.on(events.NewMessage) + def my_handler(event: events.NewMessage.Event): + global recent_reacts + + # This utils function gets the unique identifier from peers (to_id) + to_id = utils.get_peer_id(event.message.to_id) + + # Through event.raw_text we access the text of messages without format + words = re.split('\W+', event.raw_text) + + # Try to match some reaction for trigger, response in REACTS.items(): - if len(recent_reacts[msg.to_id.channel_id]) > 3: + if len(recent_reacts[to_id]) > 3: # Silently ignore triggers if we've recently sent 3 reactions break if trigger in words: # Remove recent replies older than 10 minutes - recent_reacts[msg.to_id.channel_id] = [ - a for a in recent_reacts[msg.to_id.channel_id] if + recent_reacts[to_id] = [ + a for a in recent_reacts[to_id] if datetime.now() - a < timedelta(minutes=10) ] - # Send a reaction - client.send_message(msg.to_id, response, reply_to=msg.id) + # Send a reaction as a reply (otherwise, event.respond()) + event.reply(response) # Add this reaction to the list of recent actions - recent_reacts[msg.to_id.channel_id].append(datetime.now()) + recent_reacts[to_id].append(datetime.now()) - if isinstance(update, UpdateShortMessage): - words = re.split('\W+', msg) - for trigger, response in REACTS.items(): - if len(recent_reacts[update.user_id]) > 3: - # Silently ignore triggers if we've recently sent 3 reactions - break + # Automatically send relevant media when we say certain things + # When invoking requests, get_input_entity needs to be called manually + if event.out: + if event.raw_text.lower() == 'x files theme': + client.send_voice_note(event.message.to_id, 'xfiles.m4a', + reply_to=event.message.id) + if event.raw_text.lower() == 'anytime': + client.send_file(event.message.to_id, 'anytime.png', + reply_to=event.message.id) + if '.shrug' in event.text: + event.edit(event.text.replace('.shrug', r'¯\_(ツ)_/¯')) - if trigger in words: - # Send a reaction - client.send_message(update.user_id, response, reply_to=update.id) - # Add this reaction to the list of recent reactions - recent_reacts[update.user_id].append(datetime.now()) + if 'TG_PHONE' in environ: + client.start(phone=environ['TG_PHONE']) + else: + client.start() - # Automatically send relevant media when we say certain things - # When invoking requests, get_input_entity needs to be called manually - if isinstance(update, UpdateNewChannelMessage) and msg.out: - if msg.message.lower() == 'x files theme': - client.send_voice_note(msg.to_id, 'xfiles.m4a', reply_to=msg.id) - if msg.message.lower() == 'anytime': - client.send_file(msg.to_id, 'anytime.png', reply_to=msg.id) - if '.shrug' in msg.message: - client(EditMessageRequest( - client.get_input_entity(msg.to_id), msg.id, - message=msg.message.replace('.shrug', r'¯\_(ツ)_/¯') - )) - - if isinstance(update, UpdateShortMessage) and update.out: - if msg.lower() == 'x files theme': - client.send_voice_note(update.user_id, 'xfiles.m4a', reply_to=update.id) - if msg.lower() == 'anytime': - client.send_file(update.user_id, 'anytime.png', reply_to=update.id) - if '.shrug' in msg: - client(EditMessageRequest( - client.get_input_entity(update.user_id), update.id, - message=msg.replace('.shrug', r'¯\_(ツ)_/¯') - )) - - -if __name__ == '__main__': - session_name = environ.get('TG_SESSION', 'session') - user_phone = environ['TG_PHONE'] - client = TelegramClient( - session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'], - proxy=None, update_workers=4 - ) - try: - print('INFO: Connecting to Telegram Servers...', end='', flush=True) - client.connect() - print('Done!') - - if not client.is_user_authorized(): - print('INFO: Unauthorized user') - client.send_code_request(user_phone) - code_ok = False - while not code_ok: - code = input('Enter the auth code: ') - try: - code_ok = client.sign_in(user_phone, code) - except SessionPasswordNeededError: - password = getpass('Two step verification enabled. ' - 'Please enter your password: ') - code_ok = client.sign_in(password=password) - print('INFO: Client initialized successfully!') - - client.add_update_handler(update_handler) - input('Press Enter to stop this!\n') - except KeyboardInterrupt: - pass - finally: - client.disconnect() + print('(Press Ctrl+C to stop this)') + client.idle() From 8718cf0e7e0d47484445ac4ce380ce0d933fdee9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Feb 2018 10:40:01 +0100 Subject: [PATCH 03/34] Reuse turning chats into a set of IDs and handle self case --- telethon/events/__init__.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 3a888200..b422cd26 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -8,6 +8,24 @@ from ..extensions import markdown from ..tl import types, functions +def _into_id_set(client, chats): + """Helper util to turn the input chat or chats into a set of IDs.""" + if chats is None: + return None + + if not hasattr(chats, '__iter__') or isinstance(chats, str): + chats = (chats,) + + result = set() + for chat in chats: + chat = client.get_input_entity(chat) + if isinstance(chat, types.InputPeerSelf): + chat = getattr(_into_id_set, 'me', None) or client.get_me() + _into_id_set.me = chat + result.add(utils.get_peer_id(chat)) + return result + + class _EventBuilder(abc.ABC): @abc.abstractmethod def build(self, update): @@ -153,12 +171,7 @@ class NewMessage(_EventBuilder): self.blacklist_chats = blacklist_chats def resolve(self, client): - if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): - self.chats = set(utils.get_peer_id(client.get_input_entity(x)) - for x in self.chats) - elif self.chats is not None: - self.chats = {utils.get_peer_id( - client.get_input_entity(self.chats))} + self.chats = _into_id_set(client, self.chats) def build(self, update): if isinstance(update, @@ -430,12 +443,7 @@ class ChatAction(_EventBuilder): self.blacklist_chats = blacklist_chats def resolve(self, client): - if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): - self.chats = set(utils.get_peer_id(client.get_input_entity(x)) - for x in self.chats) - elif self.chats is not None: - self.chats = {utils.get_peer_id( - client.get_input_entity(self.chats))} + self.chats = _into_id_set(client, self.chats) def build(self, update): if isinstance(update, types.UpdateChannelPinnedMessage): From 6d993af338384e7bb87ffdd4332a8545a5956211 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Feb 2018 11:29:16 +0100 Subject: [PATCH 04/34] Move events chats and blacklist_chats into the base, reuse code --- telethon/events/__init__.py | 99 +++++++++++++------------------------ 1 file changed, 35 insertions(+), 64 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index b422cd26..06c08db9 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -27,13 +27,42 @@ def _into_id_set(client, chats): class _EventBuilder(abc.ABC): + """ + The common event builder, with builtin support to filter per chat. + + Args: + chats (:obj:`entity`, optional): + May be one or more entities (username/peer/etc.). By default, + only matching chats will be handled. + + blacklist_chats (:obj:`bool`, optional): + Whether to treat the the list of chats as a blacklist (if + it matches it will NOT be handled) or a whitelist (default). + """ + def __init__(self, chats=None, blacklist_chats=False): + self.chats = chats + self.blacklist_chats = blacklist_chats + @abc.abstractmethod def build(self, update): """Builds an event for the given update if possible, or returns None""" - @abc.abstractmethod def resolve(self, client): """Helper method to allow event builders to be resolved before usage""" + self.chats = _into_id_set(client, self.chats) + + def _filter_event(self, event): + """ + If the ID of ``event._chat_peer`` isn't in the chats set (or it is + but the set is a blacklist) returns ``None``, otherwise the event. + """ + if self.chats is not None: + inside = utils.get_peer_id(event._chat_peer) in self.chats + if inside == self.blacklist_chats: + # If this chat matches but it's a blacklist ignore. + # If it doesn't match but it's a whitelist ignore. + return None + return event class _EventCommon(abc.ABC): @@ -124,9 +153,6 @@ class Raw(_EventBuilder): """ Represents a raw event. The event is the update itself. """ - def resolve(self, client): - pass - def build(self, update): return update @@ -147,14 +173,6 @@ class NewMessage(_EventBuilder): If set to ``True``, only **outgoing** messages will be handled. Mutually exclusive with ``incoming`` (can only set one of either). - chats (:obj:`entity`, optional): - May be one or more entities (username/peer/etc.). By default, - only matching chats will be handled. - - blacklist_chats (:obj:`bool`, optional): - Whether to treat the the list of chats as a blacklist (if - it matches it will NOT be handled) or a whitelist (default). - Notes: The ``message.from_id`` might not only be an integer or ``None``, but also ``InputPeerSelf()`` for short private messages (the API @@ -165,13 +183,9 @@ class NewMessage(_EventBuilder): if incoming and outgoing: raise ValueError('Can only set either incoming or outgoing') + super().__init__(chats=chats, blacklist_chats=blacklist_chats) self.incoming = incoming self.outgoing = outgoing - self.chats = chats - self.blacklist_chats = blacklist_chats - - def resolve(self, client): - self.chats = _into_id_set(client, self.chats) def build(self, update): if isinstance(update, @@ -207,15 +221,7 @@ class NewMessage(_EventBuilder): if self.outgoing and not event.message.out: return - if self.chats is not None: - inside = utils.get_peer_id(event.message.to_id) in self.chats - if inside == self.blacklist_chats: - # If this chat matches but it's a blacklist ignore. - # If it doesn't match but it's a whitelist ignore. - return - - # Tests passed so return the event - return event + return self._filter_event(event) class Event(_EventCommon): """ @@ -426,25 +432,7 @@ class NewMessage(_EventBuilder): class ChatAction(_EventBuilder): """ Represents an action in a chat (such as user joined, left, or new pin). - - Args: - chats (:obj:`entity`, optional): - May be one or more entities (username/peer/etc.). By default, - only matching chats will be handled. - - blacklist_chats (:obj:`bool`, optional): - Whether to treat the the list of chats as a blacklist (if - it matches it will NOT be handled) or a whitelist (default). - """ - def __init__(self, chats=None, blacklist_chats=False): - # TODO This can probably be reused in all builders - self.chats = chats - self.blacklist_chats = blacklist_chats - - def resolve(self, client): - self.chats = _into_id_set(client, self.chats) - def build(self, update): if isinstance(update, types.UpdateChannelPinnedMessage): # Telegram sends UpdateChannelPinnedMessage and then @@ -502,16 +490,7 @@ class ChatAction(_EventBuilder): else: return - if self.chats is None: - return event - else: - inside = utils.get_peer_id(event._chat_peer) in self.chats - if inside == self.blacklist_chats: - # If this chat matches but it's a blacklist ignore. - # If it doesn't match but it's a whitelist ignore. - return - - return event + return self._filter_event(event) class Event(_EventCommon): """ @@ -657,7 +636,6 @@ class UserUpdate(_EventBuilder): """ Represents an user update (gone online, offline, joined Telegram). """ - def build(self, update): if isinstance(update, types.UpdateUserStatus): event = UserUpdate.Event(update.user_id, @@ -665,10 +643,7 @@ class UserUpdate(_EventBuilder): else: return - return event - - def resolve(self, client): - pass + return self._filter_event(event) class Event(_EventCommon): """ @@ -808,7 +783,6 @@ class MessageChanged(_EventBuilder): """ Represents a message changed (edited or deleted). """ - def build(self, update): if isinstance(update, (types.UpdateEditMessage, types.UpdateEditChannelMessage)): @@ -822,10 +796,7 @@ class MessageChanged(_EventBuilder): else: return - return event - - def resolve(self, client): - pass + return self._filter_event(event) class Event(_EventCommon): """ From d581589313514ece5ceb1a5ab00576ec34f6dbc9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Feb 2018 11:40:38 +0100 Subject: [PATCH 05/34] Add missing UpdateShortChatMessage case on events.NewMessage --- telethon/events/__init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 06c08db9..91683594 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -209,6 +209,22 @@ class NewMessage(_EventBuilder): reply_to_msg_id=update.reply_to_msg_id, entities=update.entities )) + elif isinstance(update, types.UpdateShortChatMessage): + event = NewMessage.Event(types.Message( + out=update.out, + mentioned=update.mentioned, + media_unread=update.media_unread, + silent=update.silent, + id=update.id, + from_id=update.from_id, + to_id=types.PeerChat(update.chat_id), + message=update.message, + date=update.date, + fwd_from=update.fwd_from, + via_bot_id=update.via_bot_id, + reply_to_msg_id=update.reply_to_msg_id, + entities=update.entities + )) else: return From 7c647b57e396f7bca492f4ff64476428cc9dc7f2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Feb 2018 12:14:23 +0100 Subject: [PATCH 06/34] Mention cryptg as an optional dependency --- optional-requirements.txt | 1 + readthedocs/extra/basic/installation.rst | 7 +++++++ requirements.txt | 2 ++ setup.py | 5 ++++- 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 optional-requirements.txt create mode 100644 requirements.txt diff --git a/optional-requirements.txt b/optional-requirements.txt new file mode 100644 index 00000000..5631ec03 --- /dev/null +++ b/optional-requirements.txt @@ -0,0 +1 @@ +cryptg diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst index e74cdae6..0f812127 100644 --- a/readthedocs/extra/basic/installation.rst +++ b/readthedocs/extra/basic/installation.rst @@ -33,6 +33,13 @@ If you don't have root access, simply pass the ``--user`` flag to the pip command. If you want to install a specific branch, append ``@branch`` to the end of the first install command. +By default the library will use a pure Python implementation for encryption, +which can be really slow when uploading or downloading files. If you don't +mind using a C extension, install `cryptg `__ +via ``pip`` or as an extra: + + ``pip3 install telethon[cryptg]`` + Manual Installation ******************* diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..2b650ec4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyaes +rsa diff --git a/setup.py b/setup.py index 2682e099..85e77a74 100755 --- a/setup.py +++ b/setup.py @@ -149,7 +149,10 @@ def main(): 'telethon_generator', 'telethon_tests', 'run_tests.py', 'try_telethon.py' ]), - install_requires=['pyaes', 'rsa'] + install_requires=['pyaes', 'rsa'], + extras_require={ + 'cryptg': ['cryptg'] + } ) From 2bfe86cda1e2718fb458cb432e9cb3f43d09768b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Feb 2018 12:32:30 +0100 Subject: [PATCH 07/34] Fix bot_token could not be specified alone on .start() --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 7de7b59a..711a438f 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -267,7 +267,7 @@ class TelegramClient(TelegramBareClient): if not phone and not bot_token: raise ValueError('No phone number or bot token provided.') - if phone and bot_token: + if phone and bot_token and not callable(phone): raise ValueError('Both a phone and a bot token provided, ' 'must only provide one of either') From 33fd6895d3909f28610b5ee66a961e731a603c3a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Feb 2018 13:00:58 +0100 Subject: [PATCH 08/34] Use hachoir to determine audio and video metadata if possible Closes #611 --- optional-requirements.txt | 1 + telethon/telegram_client.py | 46 ++++++++++++++++++++++++++++++++----- telethon/utils.py | 5 ++++ 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/optional-requirements.txt b/optional-requirements.txt index 5631ec03..c973d402 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -1 +1,2 @@ cryptg +hachoir3 diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 711a438f..9888b9e5 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -23,6 +23,13 @@ try: except ImportError: socks = None +try: + import hachoir + import hachoir.metadata + import hachoir.parser +except ImportError: + hachoir = None + from . import TelegramBareClient from . import helpers, utils from .errors import ( @@ -1021,6 +1028,10 @@ class TelegramClient(TelegramBareClient): 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. + Notes: + If the ``hachoir3`` package (``hachoir`` module) is installed, + it will be used to determine metadata from audio and video files. + Returns: The message (or messages) containing the sent file. """ @@ -1084,12 +1095,32 @@ class TelegramClient(TelegramBareClient): attr_dict = { DocumentAttributeFilename: DocumentAttributeFilename(os.path.basename(file)) - # TODO If the input file is an audio, find out: - # Performer and song title and add DocumentAttributeAudio } + if utils.is_audio(file) and hachoir: + m = hachoir.metadata.extractMetadata( + hachoir.parser.createParser(file) + ) + attr_dict[DocumentAttributeAudio] = DocumentAttributeAudio( + title=m.get('title') if m.has('title') else None, + performer=m.get('author') if m.has('author') else None, + duration=int(m.get('duration').seconds + if m.has('duration') else 0) + ) + if not force_document and utils.is_video(file): - attr_dict[DocumentAttributeVideo] = \ - DocumentAttributeVideo(0, 0, 0) + if hachoir: + m = hachoir.metadata.extractMetadata( + hachoir.parser.createParser(file) + ) + doc = DocumentAttributeVideo( + w=m.get('width') if m.has('width') else 0, + h=m.get('height') if m.has('height') else 0, + duration=int(m.get('duration').seconds + if m.has('duration') else 0) + ) + else: + doc = DocumentAttributeVideo(0, 0, 0) + attr_dict[DocumentAttributeVideo] = doc else: attr_dict = { DocumentAttributeFilename: @@ -1097,8 +1128,11 @@ class TelegramClient(TelegramBareClient): } if 'is_voice_note' in kwargs: - attr_dict[DocumentAttributeAudio] = \ - DocumentAttributeAudio(0, voice=True) + if DocumentAttributeAudio in attr_dict: + attr_dict[DocumentAttributeAudio].voice = True + else: + 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 diff --git a/telethon/utils.py b/telethon/utils.py index 9460986c..2f27b79e 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -326,6 +326,11 @@ def is_image(file): return (mimetypes.guess_type(file)[0] or '').startswith('image/') +def is_audio(file): + """Returns True if the file extension looks like an audio file""" + return (mimetypes.guess_type(file)[0] or '').startswith('audio/') + + def is_video(file): """Returns True if the file extension looks like a video file""" return (mimetypes.guess_type(file)[0] or '').startswith('video/') From 1179c9e21bab3269f373d2c79b26801dc708cdac Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Feb 2018 12:17:02 +0100 Subject: [PATCH 09/34] Fix start not asking for password if needed --- telethon/telegram_client.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 9888b9e5..e81171d4 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,3 +1,4 @@ +import getpass import hashlib import io import itertools @@ -216,8 +217,8 @@ class TelegramClient(TelegramBareClient): def start(self, phone=lambda: input('Please enter your phone: '), - password=None, bot_token=None, - force_sms=False, code_callback=None, + password=lambda: getpass.getpass('Please enter your password: '), + bot_token=None, force_sms=False, code_callback=None, first_name='New User', last_name=''): """ Convenience method to interactively connect and sign in if required, @@ -331,6 +332,9 @@ class TelegramClient(TelegramBareClient): "Two-step verification is enabled for this account. " "Please provide the 'password' argument to 'start()'." ) + # TODO If callable given make it retry on invalid + if callable(password): + password = password() me = self.sign_in(phone=phone, password=password) # We won't reach here if any step failed (exit by exception) From b93e1b5f5033e7e850917d0cfc88f0ba23e7902d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Feb 2018 13:29:05 +0100 Subject: [PATCH 10/34] Add add_event_handler and deprecate add_update_handler --- telethon/telegram_bare_client.py | 19 ---------- telethon/telegram_client.py | 59 +++++++++++++++++++++++++------- telethon/update_state.py | 10 +++--- 3 files changed, 51 insertions(+), 37 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 31a3f7d9..23fd4ee4 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -1,7 +1,6 @@ import logging import os import threading -import warnings from datetime import timedelta, datetime from signal import signal, SIGINT, SIGTERM, SIGABRT from threading import Lock @@ -589,24 +588,6 @@ class TelegramBareClient: self.updates.process(self(GetStateRequest())) self._last_state = datetime.now() - 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 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" - ) - - self.updates.handlers.append(handler) - - def remove_update_handler(self, handler): - self.updates.handlers.remove(handler) - - def list_update_handlers(self): - return self.updates.handlers[:] - # endregion # region Constant read diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index e81171d4..118f1f53 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -7,6 +7,7 @@ import os import re import sys import time +import warnings from collections import OrderedDict, UserList from datetime import datetime, timedelta from io import BytesIO @@ -32,7 +33,7 @@ except ImportError: hachoir = None from . import TelegramBareClient -from . import helpers, utils +from . import helpers, utils, events from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, @@ -1760,26 +1761,17 @@ class TelegramClient(TelegramBareClient): def on(self, event): """ - - Turns the given entity into a valid Telegram user or chat. + Decorator helper method around add_event_handler(). Args: event (:obj:`_EventBuilder` | :obj:`type`): The event builder class or instance to be used, for instance ``events.NewMessage``. """ - if isinstance(event, type): - event = event() - - event.resolve(self) - def decorator(f): - self._event_builders.append((event, f)) + self.add_event_handler(f, event) return f - if self._on_handler not in self.updates.handlers: - self.add_update_handler(self._on_handler) - return decorator def _on_handler(self, update): @@ -1789,6 +1781,49 @@ class TelegramClient(TelegramBareClient): event._client = self callback(event) + def add_event_handler(self, callback, event): + """ + Registers the given callback to be called on the specified event. + + Args: + callback (:obj:`callable`): + The callable function accepting one parameter to be used. + + event (:obj:`_EventBuilder` | :obj:`type`): + The event builder class or instance to be used, + for instance ``events.NewMessage``. + """ + if self.updates.workers is None: + warnings.warn( + "You have not setup any workers, so you won't receive updates." + " Pass update_workers=1 when creating the TelegramClient," + " or set client.self.updates.workers = 1" + ) + + self.updates.handler = self._on_handler + if isinstance(event, type): + event = event() + + event.resolve(self) + self._event_builders.append((event, callback)) + + 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""" + warnings.warn( + 'add_update_handler is deprecated, use the @client.on syntax ' + 'or add_event_handler(callback, events.Raw) instead (see ' + 'https://telethon.rtfd.io/en/latest/extra/basic/working-' + 'with-updates.html)' + ) + self.add_event_handler(handler, events.Raw) + + def remove_update_handler(self, handler): + pass + + def list_update_handlers(self): + return [] + # endregion # region Small utilities to make users' life easier diff --git a/telethon/update_state.py b/telethon/update_state.py index f98c0c04..6fa0b12a 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -22,12 +22,12 @@ class UpdateState: workers is None: Updates will *not* be stored on self. workers = 0: Another thread is responsible for calling self.poll() workers > 0: 'workers' background threads will be spawned, any - any of them will invoke all the self.handlers. + any of them will invoke the self.handler. """ self._workers = workers self._worker_threads = [] - self.handlers = [] + self.handler = None self._updates_lock = RLock() self._updates = Queue() @@ -106,10 +106,8 @@ class UpdateState: while True: try: update = self.poll(timeout=UpdateState.WORKER_POLL_TIMEOUT) - # TODO Maybe people can add different handlers per update type - if update: - for handler in self.handlers: - handler(update) + if update and self.handler: + self.handler(update) except StopIteration: break except: From 89df481ae4ebba62c1e563729a4f87bc493aaee1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Feb 2018 14:07:13 +0100 Subject: [PATCH 11/34] Make MessageChanged.Event inherit NewMessage.Event --- telethon/events/__init__.py | 74 +++++-------------------------------- 1 file changed, 9 insertions(+), 65 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 91683594..db08a04c 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -814,86 +814,30 @@ class MessageChanged(_EventBuilder): return self._filter_event(event) - class Event(_EventCommon): + class Event(NewMessage.Event): """ Represents the event of an user status update (last seen, joined). + Please note that the ``message`` member will be ``None`` if the + action was a deletion and not an edit. + Members: edited (:obj:`bool`): ``True`` if the message was edited. - message (:obj:`Message`, optional): - The new edited message, if any. - deleted (:obj:`bool`): ``True`` if the message IDs were deleted. deleted_ids (:obj:`List[int]`): A list containing the IDs of the messages that were deleted. - - input_sender (:obj:`InputPeer`): - This is the input version of the user who edited the message. - Similarly to ``input_chat``, this doesn't have things like - username or similar, but still useful in some cases. - - Note that this might not be available if the library can't - find the input chat. - - sender (:obj:`User`): - This property will make an API call the first time to get the - most up to date version of the sender, so use with care as - there is no caching besides local caching yet. - - ``input_sender`` needs to be available (often the case). """ def __init__(self, edit_msg=None, deleted_ids=None, peer=None): - super().__init__(peer if not edit_msg else edit_msg.to_id) + if edit_msg is None: + msg = types.Message((deleted_ids or [0])[0], peer, None, '') + else: + msg = edit_msg + super().__init__(msg) self.edited = bool(edit_msg) - self.message = edit_msg self.deleted = bool(deleted_ids) self.deleted_ids = deleted_ids or [] - self._input_sender = None - self._sender = None - - @property - def input_sender(self): - """ - This (:obj:`InputPeer`) is the input version of the user who - sent the message. Similarly to ``input_chat``, this doesn't have - things like username or similar, but still useful in some cases. - - Note that this might not be available if the library can't - find the input chat, or if the message a broadcast on a channel. - """ - # TODO Code duplication - if self._input_sender is None: - if self.is_channel and not self.is_group: - return None - - try: - self._input_sender = self._client.get_input_entity( - self.message.from_id - ) - except (ValueError, TypeError): - # We can rely on self.input_chat for this - self._input_sender = self._get_input_entity( - self.message.id, - self.message.from_id, - chat=self.input_chat - ) - - return self._input_sender - - @property - def sender(self): - """ - This (:obj:`User`) will make an API call the first time to get - the most up to date version of the sender, so use with care as - there is no caching besides local caching yet. - - ``input_sender`` needs to be available (often the case). - """ - if self._sender is None and self.input_sender: - self._sender = self._client.get_entity(self._input_sender) - return self._sender From b136074340c510af6421551b45ba120f5567352a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Feb 2018 14:23:11 +0100 Subject: [PATCH 12/34] Update to v0.17.3 --- readthedocs/extra/changelog.rst | 31 +++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 34609615..2fe0352f 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,37 @@ it can take advantage of new goodies! .. contents:: List of All Versions +New small convenience functions (v0.17.3) +========================================= + +*Published at 2018/02/18* + +More bug fixes and a few others addition to make events easier to use. + +Additions +~~~~~~~~~ + +- Use ``hachoir`` to extract video and audio metadata before upload. +- New ``.add_event_handler``, ``.add_update_handler`` now deprecated. + +Bug fixes +~~~~~~~~~ + +- ``bot_token`` wouldn't work on ``.start()``, and changes to ``password`` + (now it will ask you for it if you don't provide it, as docstring hinted). +- ``.edit_message()`` was ignoring the formatting (e.g. markdown). +- Added missing case to the ``NewMessage`` event for normal groups. +- Accessing the ``.text`` of the ``NewMessage`` event was failing due + to a bug with the markdown unparser. + +Internal changes +~~~~~~~~~~~~~~~~ + +- ``libssl`` is no longer an optional dependency. Use ``cryptg`` instead, + which you can find on https://github.com/Lonami/cryptg. + + + New small convenience functions (v0.17.2) ========================================= diff --git a/telethon/version.py b/telethon/version.py index 4ead720b..eaeb252f 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.17.2' +__version__ = '0.17.3' From 1eeedc613b406d1c27cc5a0e28c6e23efd5cf552 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 19 Feb 2018 15:29:32 +0100 Subject: [PATCH 13/34] Fix sending byte strings as files not working --- telethon/utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index 2f27b79e..643fa746 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -323,17 +323,19 @@ def get_input_media(media, user_caption=None, is_photo=False): def is_image(file): """Returns True if the file extension looks like an image file""" - return (mimetypes.guess_type(file)[0] or '').startswith('image/') - + return (isinstance(file, str) and + (mimetypes.guess_type(file)[0] or '').startswith('image/')) def is_audio(file): """Returns True if the file extension looks like an audio file""" - return (mimetypes.guess_type(file)[0] or '').startswith('audio/') + return (isinstance(file, str) and + (mimetypes.guess_type(file)[0] or '').startswith('audio/')) def is_video(file): """Returns True if the file extension looks like a video file""" - return (mimetypes.guess_type(file)[0] or '').startswith('video/') + return (isinstance(file, str) and + (mimetypes.guess_type(file)[0] or '').startswith('video/')) def parse_phone(phone): From bf086f3e801fc1bd38cb24004e4ad3fb01db183f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 19 Feb 2018 20:23:52 +0100 Subject: [PATCH 14/34] Fix UpdateDeleteMessages doesn't have .channel_id (#619) --- telethon/events/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index db08a04c..9274813e 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -803,8 +803,12 @@ class MessageChanged(_EventBuilder): if isinstance(update, (types.UpdateEditMessage, types.UpdateEditChannelMessage)): event = MessageChanged.Event(edit_msg=update.message) - elif isinstance(update, (types.UpdateDeleteMessages, - types.UpdateDeleteChannelMessages)): + elif isinstance(update, types.UpdateDeleteMessages): + event = MessageChanged.Event( + deleted_ids=update.messages, + peer=None + ) + elif isinstance(update, types.UpdateDeleteChannelMessages): event = MessageChanged.Event( deleted_ids=update.messages, peer=types.PeerChannel(update.channel_id) From 39621ceae90a5ad0602ed8d74a2a5eabae09d37c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 19 Feb 2018 20:31:47 +0100 Subject: [PATCH 15/34] Use req_pq_multi instead req_pq when creating an auth_key --- telethon/network/authenticator.py | 4 ++-- telethon_generator/scheme.tl | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/telethon/network/authenticator.py b/telethon/network/authenticator.py index a73bae38..32413551 100644 --- a/telethon/network/authenticator.py +++ b/telethon/network/authenticator.py @@ -17,7 +17,7 @@ from ..errors import SecurityError from ..extensions import BinaryReader from ..network import MtProtoPlainSender from ..tl.functions import ( - ReqPqRequest, ReqDHParamsRequest, SetClientDHParamsRequest + ReqPqMultiRequest, ReqDHParamsRequest, SetClientDHParamsRequest ) @@ -53,7 +53,7 @@ def _do_authentication(connection): sender = MtProtoPlainSender(connection) # Step 1 sending: PQ Request, endianness doesn't matter since it's random - req_pq_request = ReqPqRequest( + req_pq_request = ReqPqMultiRequest( nonce=int.from_bytes(os.urandom(16), 'big', signed=True) ) sender.send(bytes(req_pq_request)) diff --git a/telethon_generator/scheme.tl b/telethon_generator/scheme.tl index 1d03c281..491f0c9e 100644 --- a/telethon_generator/scheme.tl +++ b/telethon_generator/scheme.tl @@ -53,7 +53,10 @@ destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes; ---functions--- +// Deprecated since somewhere around February of 2018 +// See https://core.telegram.org/mtproto/auth_key req_pq#60469778 nonce:int128 = ResPQ; +req_pq_multi#be7e8ef1 nonce:int128 = ResPQ; req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:bytes = Server_DH_Params; From c31635cc34e97f9ac1be6f769a4a1060a866503a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 19 Feb 2018 21:03:33 +0100 Subject: [PATCH 16/34] Further validate the username on parse_username --- telethon/utils.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index 643fa746..607897e4 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -30,6 +30,8 @@ USERNAME_RE = re.compile( r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' ) +VALID_USERNAME_RE = re.compile(r'^[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]$') + def get_display_name(entity): """Gets the input peer for the given "entity" (user, chat or channel) @@ -326,6 +328,7 @@ def is_image(file): return (isinstance(file, str) and (mimetypes.guess_type(file)[0] or '').startswith('image/')) + def is_audio(file): """Returns True if the file extension looks like an audio file""" return (isinstance(file, str) and @@ -353,15 +356,21 @@ def parse_username(username): a string, username or URL. Returns a tuple consisting of both the stripped, lowercase username and whether it is a joinchat/ hash (in which case is not lowercase'd). + + Returns None if the username is not valid. """ username = username.strip() m = USERNAME_RE.match(username) if m: - result = username[m.end():] + username = username[m.end():] is_invite = bool(m.group(1)) - return result if is_invite else result.lower(), is_invite - else: + if is_invite: + return username, True + + if VALID_USERNAME_RE.match(username): return username.lower(), False + else: + return None, False def get_peer_id(peer): From 4050d1ca008eeb1cfcd1480eafa1c47a811b6b46 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 19 Feb 2018 21:04:41 +0100 Subject: [PATCH 17/34] Support getting entities by exact name/title match --- telethon/session.py | 10 ++++++++-- telethon/telegram_client.py | 17 +++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/telethon/session.py b/telethon/session.py index e168e559..16232b14 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -438,13 +438,19 @@ class Session: (phone,)) else: username, _ = utils.parse_username(key) - c.execute('select id, hash from entities where username=?', - (username,)) + if username: + c.execute('select id, hash from entities where username=?', + (username,)) if isinstance(key, int): c.execute('select id, hash from entities where id=?', (key,)) result = c.fetchone() + if not result and isinstance(key, str): + # Try exact match by name if phone/username failed + c.execute('select id, hash from entities where name=?', (key,)) + result = c.fetchone() + c.close() if result: i, h = result # unpack resulting tuple diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 118f1f53..10dbb8c5 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1915,9 +1915,9 @@ class TelegramClient(TelegramBareClient): if user.phone == phone: return user else: - string, is_join_chat = utils.parse_username(string) + username, is_join_chat = utils.parse_username(string) if is_join_chat: - invite = self(CheckChatInviteRequest(string)) + invite = self(CheckChatInviteRequest(username)) if isinstance(invite, ChatInvite): raise ValueError( 'Cannot get entity from a channel ' @@ -1925,13 +1925,18 @@ class TelegramClient(TelegramBareClient): ) elif isinstance(invite, ChatInviteAlready): return invite.chat - else: - if string in ('me', 'self'): + elif username: + if username in ('me', 'self'): return self.get_me() - result = self(ResolveUsernameRequest(string)) + result = self(ResolveUsernameRequest(username)) for entity in itertools.chain(result.users, result.chats): - if entity.username.lower() == string: + if entity.username.lower() == username: return entity + try: + # Nobody with this username, maybe it's an exact name/title + return self.get_entity(self.get_input_entity(string)) + except (ValueError, TypeError): + pass raise TypeError( 'Cannot turn "{}" into any entity (user or chat)'.format(string) From ea0da8fc0e58c78c92339af41bba5e660b824fbe Mon Sep 17 00:00:00 2001 From: Jannik <32801117+code1mountain@users.noreply.github.com> Date: Tue, 20 Feb 2018 15:55:02 +0100 Subject: [PATCH 18/34] Add pattern argument on the NewMessage event (#620) --- telethon/events/__init__.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 9274813e..99dd5bda 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -1,6 +1,7 @@ import abc import datetime import itertools +import re from .. import utils from ..errors import RPCError @@ -173,19 +174,33 @@ class NewMessage(_EventBuilder): If set to ``True``, only **outgoing** messages will be handled. Mutually exclusive with ``incoming`` (can only set one of either). + pattern (:obj:`str`, :obj:`callable`, :obj:`Pattern`, optional): + If set, only messages matching this pattern will be handled. + You can specify a regex-like string which will be matched + against the message, a callable function that returns ``True`` + if a message is acceptable, or a compiled regex pattern. + Notes: The ``message.from_id`` might not only be an integer or ``None``, but also ``InputPeerSelf()`` for short private messages (the API would not return such thing, this is a custom modification). """ def __init__(self, incoming=None, outgoing=None, - chats=None, blacklist_chats=False): + chats=None, blacklist_chats=False, pattern=None): if incoming and outgoing: raise ValueError('Can only set either incoming or outgoing') super().__init__(chats=chats, blacklist_chats=blacklist_chats) self.incoming = incoming self.outgoing = outgoing + if isinstance(pattern, str): + self.pattern = re.compile(pattern).match + elif not pattern or callable(pattern): + self.pattern = pattern + elif hasattr(pattern, 'match') and callable(pattern.match): + self.pattern = pattern.match + else: + raise TypeError('Invalid pattern type given') def build(self, update): if isinstance(update, @@ -229,13 +244,16 @@ class NewMessage(_EventBuilder): return # Short-circuit if we let pass all events - if all(x is None for x in (self.incoming, self.outgoing, self.chats)): + if all(x is None for x in (self.incoming, self.outgoing, self.chats, + self.pattern)): return event if self.incoming and event.message.out: return if self.outgoing and not event.message.out: return + if self.pattern and not self.pattern(event.message.message or ''): + return return self._filter_event(event) From 0731a1d6983e663f8cb5cf282641c702027ae21c Mon Sep 17 00:00:00 2001 From: Dmitry Bukhta Date: Tue, 20 Feb 2018 17:58:51 +0300 Subject: [PATCH 19/34] Raise ProxyConnectionError instead looping forever (#621) We shouldn't try reconnecting when using a proxy if what's unavailable is the proxy server (and not Telegram servers). --- telethon/extensions/tcp_client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index a306302a..dd177aa2 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -8,6 +8,11 @@ from datetime import timedelta from io import BytesIO, BufferedWriter from threading import Lock +try: + import socks +except ImportError: + socks = None + MAX_TIMEOUT = 15 # in seconds CONN_RESET_ERRNOS = { errno.EBADF, errno.ENOTSOCK, errno.ENETUNREACH, @@ -70,6 +75,9 @@ class TcpClient: self._socket.connect(address) break # Successful connection, stop retrying to connect except OSError as e: + # Stop retrying to connect if proxy connection error occurred + if socks and isinstance(e, socks.ProxyConnectionError): + raise # There are some errors that we know how to handle, and # the loop will allow us to retry if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL, From 7f35ed59c613f7b283dae639ec93842c4115cb87 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 20 Feb 2018 17:30:01 +0100 Subject: [PATCH 20/34] Fix infinite recursion on .get_entity by exact name --- telethon/telegram_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 10dbb8c5..cd791964 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1934,8 +1934,8 @@ class TelegramClient(TelegramBareClient): return entity try: # Nobody with this username, maybe it's an exact name/title - return self.get_entity(self.get_input_entity(string)) - except (ValueError, TypeError): + return self.get_entity(self.session.get_input_entity(string)) + except ValueError: pass raise TypeError( From 359cdcd77282b73c9a1f844dde2a718e06cc8cad Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Feb 2018 10:27:12 +0100 Subject: [PATCH 21/34] Handle more parsing username cases (closes #630) --- telethon/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/telethon/utils.py b/telethon/utils.py index 607897e4..fdadbd1c 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -27,7 +27,7 @@ from .tl.types import ( from .tl.types.contacts import ResolvedPeer USERNAME_RE = re.compile( - r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' + r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' ) VALID_USERNAME_RE = re.compile(r'^[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]$') @@ -366,6 +366,8 @@ def parse_username(username): is_invite = bool(m.group(1)) if is_invite: return username, True + else: + username = username.rstrip('/') if VALID_USERNAME_RE.match(username): return username.lower(), False From f13a7e4afde5340fd72528b9319d7547127b1ccc Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Feb 2018 20:37:56 +0100 Subject: [PATCH 22/34] Allow getting the input peer for yourself and cache it Warm-up for #632, which needs this information accessible. --- telethon/telegram_client.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index cd791964..4b9bda67 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -179,6 +179,9 @@ class TelegramClient(TelegramBareClient): self._phone_code_hash = {} self._phone = None + # Sometimes we need to know who we are, cache the self peer + self._self_input_peer = None + # endregion # region Telegram requests functions @@ -407,6 +410,9 @@ class TelegramClient(TelegramBareClient): 'and a password only if an RPCError was raised before.' ) + self._self_input_peer = utils.get_input_peer( + result.user, allow_self=False + ) self._set_connected_and_authorized() return result.user @@ -436,6 +442,9 @@ class TelegramClient(TelegramBareClient): last_name=last_name )) + self._self_input_peer = utils.get_input_peer( + result.user, allow_self=False + ) self._set_connected_and_authorized() return result.user @@ -455,16 +464,30 @@ class TelegramClient(TelegramBareClient): self.session.delete() return True - def get_me(self): + def get_me(self, input_peer=False): """ Gets "me" (the self user) which is currently authenticated, or None if the request fails (hence, not authenticated). + Args: + input_peer (:obj:`bool`, optional): + Whether to return the ``InputPeerUser`` version or the normal + ``User``. This can be useful if you just need to know the ID + of yourself. + Returns: :obj:`User`: Your own user. """ + if input_peer and self._self_input_peer: + return self._self_input_peer + try: - return self(GetUsersRequest([InputUserSelf()]))[0] + me = self(GetUsersRequest([InputUserSelf()]))[0] + if not self._self_input_peer: + self._self_input_peer = utils.get_input_peer( + me, allow_self=False + ) + return me except UnauthorizedError: return None From 448a04a7c5be07d28cfe63c8a16f2eab5762c7f0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Feb 2018 21:01:18 +0100 Subject: [PATCH 23/34] Stop using InputPeerSelf() on events and special case edit() Used to fail on the chat with your own (where messages are "incoming" instead outgoing). Now the ID of the chat and sender are compared to achieve the same effect. Fixes #632. --- telethon/events/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 99dd5bda..1d7beab2 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -21,8 +21,7 @@ def _into_id_set(client, chats): for chat in chats: chat = client.get_input_entity(chat) if isinstance(chat, types.InputPeerSelf): - chat = getattr(_into_id_set, 'me', None) or client.get_me() - _into_id_set.me = chat + chat = client.get_me(input_peer=True) result.add(utils.get_peer_id(chat)) return result @@ -43,6 +42,7 @@ class _EventBuilder(abc.ABC): def __init__(self, chats=None, blacklist_chats=False): self.chats = chats self.blacklist_chats = blacklist_chats + self._self_id = None @abc.abstractmethod def build(self, update): @@ -51,6 +51,7 @@ class _EventBuilder(abc.ABC): def resolve(self, client): """Helper method to allow event builders to be resolved before usage""" self.chats = _into_id_set(client, self.chats) + self._self_id = client.get_me(input_peer=True).user_id def _filter_event(self, event): """ @@ -179,11 +180,6 @@ class NewMessage(_EventBuilder): You can specify a regex-like string which will be matched against the message, a callable function that returns ``True`` if a message is acceptable, or a compiled regex pattern. - - Notes: - The ``message.from_id`` might not only be an integer or ``None``, - but also ``InputPeerSelf()`` for short private messages (the API - would not return such thing, this is a custom modification). """ def __init__(self, incoming=None, outgoing=None, chats=None, blacklist_chats=False, pattern=None): @@ -216,7 +212,7 @@ class NewMessage(_EventBuilder): silent=update.silent, id=update.id, to_id=types.PeerUser(update.user_id), - from_id=types.InputPeerSelf() if update.out else update.user_id, + from_id=self._self_id if update.out else update.user_id, message=update.message, date=update.date, fwd_from=update.fwd_from, @@ -317,7 +313,11 @@ class NewMessage(_EventBuilder): or the edited message otherwise. """ if not self.message.out: - return None + if not isinstance(self.message.to_id, types.PeerUser): + return None + me = self._client.get_me(input_peer=True) + if self.message.to_id.user_id != me.user_id: + return None return self._client.edit_message(self.input_chat, self.message, From cda5e59e86c6fd00c83ecd70820baa8172dd1a28 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 23 Feb 2018 12:07:57 +0100 Subject: [PATCH 24/34] Make .send_message() accept another Message as input --- telethon/telegram_client.py | 43 +++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 4b9bda67..a5067e6c 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -80,7 +80,8 @@ from .tl.types import ( InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, InputDocument, InputMediaDocument, Document, MessageEntityTextUrl, InputMessageEntityMentionName, DocumentAttributeVideo, - UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates + UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates, + MessageMediaWebPage ) from .tl.types.messages import DialogsSlice from .extensions import markdown, html @@ -664,8 +665,8 @@ class TelegramClient(TelegramBareClient): entity (:obj:`entity`): To who will it be sent. - message (:obj:`str`): - The message to be sent. + message (:obj:`str` | :obj:`Message`): + The message to be sent, or another message object to resend. reply_to (:obj:`int` | :obj:`Message`, optional): Whether to reply to a message or not. If an integer is provided, @@ -684,15 +685,35 @@ class TelegramClient(TelegramBareClient): the sent message """ entity = self.get_input_entity(entity) - message, msg_entities = self._parse_message_text(message, parse_mode) + if isinstance(message, Message): + if (message.media + and not isinstance(message.media, MessageMediaWebPage)): + return self.send_file(entity, message.media) + + if utils.get_peer_id(entity) == utils.get_peer_id(message.to_id): + reply_id = message.reply_to_msg_id + else: + reply_id = None + request = SendMessageRequest( + peer=entity, + message=message.message or '', + silent=message.silent, + reply_to_msg_id=reply_id, + reply_markup=message.reply_markup, + entities=message.entities, + no_webpage=not isinstance(message.media, MessageMediaWebPage) + ) + message = message.message + else: + message, msg_ent = self._parse_message_text(message, parse_mode) + request = SendMessageRequest( + peer=entity, + message=message, + entities=msg_ent, + no_webpage=not link_preview, + reply_to_msg_id=self._get_message_id(reply_to) + ) - request = SendMessageRequest( - peer=entity, - message=message, - entities=msg_entities, - no_webpage=not link_preview, - reply_to_msg_id=self._get_message_id(reply_to) - ) result = self(request) if isinstance(result, UpdateShortSentMessage): return Message( From 005a8f0a7f61efe2be0396d7bc3c77cc8126e5e6 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 23 Feb 2018 12:10:07 +0100 Subject: [PATCH 25/34] Fix .send_file() not respecting MessageMedia captions --- telethon/telegram_client.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index a5067e6c..84704722 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1018,7 +1018,7 @@ class TelegramClient(TelegramBareClient): # region Uploading files - def send_file(self, entity, file, caption='', + def send_file(self, entity, file, caption=None, force_document=False, progress_callback=None, reply_to=None, attributes=None, @@ -1130,11 +1130,11 @@ class TelegramClient(TelegramBareClient): if isinstance(file_handle, use_cache): # File was cached, so an instance of use_cache was returned if as_image: - media = InputMediaPhoto(file_handle, caption) + media = InputMediaPhoto(file_handle, caption or '') else: - media = InputMediaDocument(file_handle, caption) + media = InputMediaDocument(file_handle, caption or '') elif as_image: - media = InputMediaUploadedPhoto(file_handle, caption) + media = InputMediaUploadedPhoto(file_handle, caption or '') else: mime_type = None if isinstance(file, str): @@ -1204,7 +1204,7 @@ class TelegramClient(TelegramBareClient): file=file_handle, mime_type=mime_type, attributes=list(attr_dict.values()), - caption=caption, + caption=caption or '', **input_kw ) @@ -1224,15 +1224,15 @@ class TelegramClient(TelegramBareClient): return msg - def send_voice_note(self, entity, file, caption='', progress_callback=None, - reply_to=None): + def send_voice_note(self, entity, file, caption=None, + progress_callback=None, reply_to=None): """Wrapper method around .send_file() with is_voice_note=()""" return self.send_file(entity, file, caption, progress_callback=progress_callback, reply_to=reply_to, is_voice_note=()) # empty tuple is enough - def _send_album(self, entity, files, caption='', + def _send_album(self, entity, files, caption=None, progress_callback=None, reply_to=None): """Specialized version of .send_file for albums""" # We don't care if the user wants to avoid cache, we will use it @@ -1241,6 +1241,7 @@ class TelegramClient(TelegramBareClient): # cache only makes a difference for documents where the user may # want the attributes used on them to change. Caption's ignored. entity = self.get_input_entity(entity) + caption = caption or '' reply_to = self._get_message_id(reply_to) # Need to upload the media first, but only if they're not cached yet From a353679796891ef8e0443233bf44c145d1376611 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 23 Feb 2018 13:13:39 +0100 Subject: [PATCH 26/34] Fix downloading from another DC using wrong auth the first time --- telethon/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/session.py b/telethon/session.py index 16232b14..4658df2b 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -227,7 +227,7 @@ class Session: c = self._cursor() c.execute('select auth_key from sessions') tuple_ = c.fetchone() - if tuple_: + if tuple_ and tuple_[0]: self._auth_key = AuthKey(data=tuple_[0]) else: self._auth_key = None From f9cec54c3970aac823ecc96c553012ff059385ea Mon Sep 17 00:00:00 2001 From: Kyle2142 Date: Fri, 23 Feb 2018 22:20:32 +0200 Subject: [PATCH 27/34] Add .get_participants() convenience method (#639) Closes #363 and #380. --- telethon/telegram_client.py | 59 +++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 84704722..3e7918d2 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -56,7 +56,7 @@ from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, SendMediaRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest, - UploadMediaRequest, EditMessageRequest + UploadMediaRequest, EditMessageRequest, GetFullChatRequest ) from .tl.functions import channels @@ -66,7 +66,7 @@ from .tl.functions.users import ( GetUsersRequest ) from .tl.functions.channels import ( - GetChannelsRequest, GetFullChannelRequest + GetChannelsRequest, GetFullChannelRequest, GetParticipantsRequest ) from .tl.types import ( DocumentAttributeAudio, DocumentAttributeFilename, @@ -81,7 +81,7 @@ from .tl.types import ( InputDocument, InputMediaDocument, Document, MessageEntityTextUrl, InputMessageEntityMentionName, DocumentAttributeVideo, UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates, - MessageMediaWebPage + MessageMediaWebPage, ChannelParticipantsSearch ) from .tl.types.messages import DialogsSlice from .extensions import markdown, html @@ -1014,6 +1014,59 @@ class TelegramClient(TelegramBareClient): raise TypeError('Invalid message type: {}'.format(type(message))) + def get_participants(self, entity, limit=None, search=''): + """ + Gets the list of participants from the specified entity + + Args: + entity (:obj:`entity`): + The entity from which to retrieve the participants list. + + limit (:obj: `int`): + Limits amount of participants fetched. + + search (:obj: `str`, optional): + Look for participants with this string in name/username. + + Returns: + A list of participants with an additional .total variable on the list + indicating the total amount of members in this group/channel. + """ + entity = self.get_input_entity(entity) + limit = float('inf') if limit is None else int(limit) + if isinstance(entity, InputPeerChannel): + offset = 0 + all_participants = {} + search = ChannelParticipantsSearch(search) + while True: + loop_limit = min(limit - offset, 200) + participants = self(GetParticipantsRequest( + entity, search, offset, loop_limit, hash=0 + )) + if not participants.users: + break + for user in participants.users: + if len(all_participants) < limit: + all_participants[user.id] = user + offset += len(participants.users) + if offset > limit: + break + + users = UserList(all_participants.values()) + users.total = self(GetFullChannelRequest( + entity)).full_chat.participants_count + + elif isinstance(entity, InputPeerChat): + users = self(GetFullChatRequest(entity.chat_id)).users + if len(users) > limit: + users = users[:limit] + users = UserList(users) + users.total = len(users) + else: + users = UserList([entity]) + users.total = 1 + return users + # endregion # region Uploading files From b7a61510bf7c7903af50d96d6013977e79ef8a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joscha=20G=C3=B6tzer?= Date: Fri, 23 Feb 2018 21:34:15 +0100 Subject: [PATCH 28/34] Add !i for information to the interactive telegram client (#614) --- .../interactive_telegram_client.py | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index d45d2ff1..f6986370 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -1,12 +1,13 @@ import os from getpass import getpass -from telethon import TelegramClient, ConnectionMode +from telethon.utils import get_display_name + +from telethon import ConnectionMode, TelegramClient from telethon.errors import SessionPasswordNeededError from telethon.tl.types import ( - UpdateShortChatMessage, UpdateShortMessage, PeerChat + PeerChat, UpdateShortChatMessage, UpdateShortMessage ) -from telethon.utils import get_display_name def sprint(string, *args, **kwargs): @@ -47,6 +48,7 @@ class InteractiveTelegramClient(TelegramClient): Telegram through Telethon, such as listing dialogs (open chats), talking to people, downloading media, and receiving updates. """ + def __init__(self, session_user_id, user_phone, api_id, api_hash, proxy=None): """ @@ -182,14 +184,15 @@ class InteractiveTelegramClient(TelegramClient): # Show some information print_title('Chat with "{}"'.format(get_display_name(entity))) print('Available commands:') - print(' !q: Quits the current chat.') - print(' !Q: Quits the current chat and exits.') - print(' !h: prints the latest messages (message History).') - print(' !up : Uploads and sends the Photo from path.') - print(' !uf : Uploads and sends the File from path.') - print(' !d : Deletes a message by its id') - print(' !dm : Downloads the given message Media (if any).') + print(' !q: Quits the current chat.') + print(' !Q: Quits the current chat and exits.') + print(' !h: prints the latest messages (message History).') + print(' !up : Uploads and sends the Photo from path.') + print(' !uf : Uploads and sends the File from path.') + print(' !d : Deletes a message by its id') + print(' !dm : Downloads the given message Media (if any).') print(' !dp: Downloads the current dialog Profile picture.') + print(' !i: Prints information about this chat..') print() # And start a while loop to chat @@ -234,8 +237,7 @@ class InteractiveTelegramClient(TelegramClient): # And print it to the user sprint('[{}:{}] (ID={}) {}: {}'.format( - msg.date.hour, msg.date.minute, msg.id, name, - content)) + msg.date.hour, msg.date.minute, msg.id, name, content)) # Send photo elif msg.startswith('!up '): @@ -264,12 +266,16 @@ class InteractiveTelegramClient(TelegramClient): os.makedirs('usermedia', exist_ok=True) output = self.download_profile_photo(entity, 'usermedia') if output: - print( - 'Profile picture downloaded to {}'.format(output) - ) + print('Profile picture downloaded to', output) else: print('No profile picture found for this user!') + elif msg == '!i': + attributes = list(entity.to_dict().items()) + pad = max(len(x) for x, _ in attributes) + for name, val in attributes: + print("{:<{width}} : {}".format(name, val, width=pad)) + # Send chat message (if any) elif msg: self.send_message(entity, msg, link_preview=False) @@ -356,6 +362,5 @@ class InteractiveTelegramClient(TelegramClient): else: who = self.get_entity(update.from_id) sprint('<< {} @ {} sent "{}"'.format( - get_display_name(which), get_display_name(who), - update.message + get_display_name(which), get_display_name(who), update.message )) From 760d84514f5fc727501126de99d2ffb4e2b21052 Mon Sep 17 00:00:00 2001 From: "Dmitry D. Chernov" Date: Sat, 24 Feb 2018 18:25:08 +1000 Subject: [PATCH 29/34] setup: Fix regex failure to match version in case of CRLF line feeds This could happen e.g. in case of using pip3 to install Telethon directly from the git repo. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 85e77a74..00dd7446 100755 --- a/setup.py +++ b/setup.py @@ -110,7 +110,7 @@ def main(): long_description = f.read() with open('telethon/version.py', encoding='utf-8') as f: - version = re.search(r"^__version__\s+=\s+'(.*)'$", + version = re.search(r"^__version__\s*=\s*'(.*)'.*$", f.read(), flags=re.MULTILINE).group(1) setup( name='Telethon', From 7f97997e8def6b5cb5c6045d04ad916110934dd7 Mon Sep 17 00:00:00 2001 From: "Dmitry D. Chernov" Date: Sat, 24 Feb 2018 18:41:53 +1000 Subject: [PATCH 30/34] Add PySocks to the package optional requirements --- optional-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/optional-requirements.txt b/optional-requirements.txt index c973d402..55bfc014 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -1,2 +1,3 @@ cryptg +pysocks hachoir3 From 3301bf3ff6383d51ae8dbab411636428ee9569f9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Feb 2018 17:40:12 +0100 Subject: [PATCH 31/34] Fix voice notes default filename being "None - None.oga" --- telethon/telegram_client.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 3e7918d2..3f341099 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1622,18 +1622,27 @@ class TelegramClient(TelegramBareClient): file_size = document.size + kind = 'document' possible_names = [] for attr in document.attributes: if isinstance(attr, DocumentAttributeFilename): possible_names.insert(0, attr.file_name) elif isinstance(attr, DocumentAttributeAudio): - possible_names.append('{} - {}'.format( - attr.performer, attr.title - )) + kind = 'audio' + if attr.performer and attr.title: + possible_names.append('{} - {}'.format( + attr.performer, attr.title + )) + elif attr.performer: + possible_names.append(attr.performer) + elif attr.title: + possible_names.append(attr.title) + elif attr.voice: + kind = 'voice' file = self._get_proper_filename( - file, 'document', utils.get_extension(document), + file, kind, utils.get_extension(document), date=date, possible_names=possible_names ) From e5aecca79ce77c6643cd87857918cf77ac6fb781 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Feb 2018 18:08:14 +0100 Subject: [PATCH 32/34] Update to v0.17.4 --- readthedocs/extra/changelog.rst | 39 +++++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 2fe0352f..ad027361 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,45 @@ it can take advantage of new goodies! .. contents:: List of All Versions +Further easing library usage (v0.17.4) +====================================== + +*Published at 2018/02/24* + +Some new things and patches that already deserved their own release. + + +Additions +~~~~~~~~~ + +- New ``pattern`` argument to ``NewMessage`` to easily filter messages. +- New ``.get_participants()`` convenience method to get members from chats. +- ``.send_message()`` now accepts a ``Message`` as the ``message`` parameter. +- You can now ``.get_entity()`` through exact name match instead username. +- Raise ``ProxyConnectionError`` instead looping forever so you can + ``except`` it on your own code and behave accordingly. + +Bug fixes +~~~~~~~~~ + +- ``.parse_username`` would fail with ``www.`` or a trailing slash. +- ``events.MessageChanged`` would fail with ``UpdateDeleteMessages``. +- You can now send ``b'byte strings'`` directly as files again. +- ``.send_file()`` was not respecting the original captions when passing + another message (or media) as the file. +- Downloading media from a different data center would always log a warning + for the first time. + +Internal changes +~~~~~~~~~~~~~~~~ + +- Use ``req_pq_multi`` instead ``req_pq`` when generating ``auth_key``. +- You can use ``.get_me(input_peer=True)`` if all you need is your self ID. +- New addition to the interactive client example to show peer information. +- Avoid special casing ``InputPeerSelf`` on some ``NewMessage`` events, so + you can always safely rely on ``.sender`` to get the right ID. + + New small convenience functions (v0.17.3) ========================================= diff --git a/telethon/version.py b/telethon/version.py index eaeb252f..8cc14b33 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.17.3' +__version__ = '0.17.4' From 9ef75e5070bb93dd73fb35d1d33f82688ee6a2db Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Feb 2018 18:25:22 +0100 Subject: [PATCH 33/34] Allow specifying no event type to default to events.Raw --- telethon/telegram_client.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 3f341099..55babafe 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1888,7 +1888,7 @@ class TelegramClient(TelegramBareClient): event._client = self callback(event) - def add_event_handler(self, callback, event): + def add_event_handler(self, callback, event=None): """ Registers the given callback to be called on the specified event. @@ -1896,9 +1896,12 @@ class TelegramClient(TelegramBareClient): callback (:obj:`callable`): The callable function accepting one parameter to be used. - event (:obj:`_EventBuilder` | :obj:`type`): + event (:obj:`_EventBuilder` | :obj:`type`, optional): The event builder class or instance to be used, for instance ``events.NewMessage``. + + If left unspecified, ``events.Raw`` (the ``Update`` objects + with no further processing) will be passed instead. """ if self.updates.workers is None: warnings.warn( @@ -1910,6 +1913,8 @@ class TelegramClient(TelegramBareClient): self.updates.handler = self._on_handler if isinstance(event, type): event = event() + elif not event: + event = events.Raw() event.resolve(self) self._event_builders.append((event, callback)) From 9054a12c1143c67e6b31ad631ad6edf40217f26b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Feb 2018 18:30:09 +0100 Subject: [PATCH 34/34] Fix tiny bug regarding .get_me(input_peer=True) crashing events --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 55babafe..1e086ffe 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -485,7 +485,7 @@ class TelegramClient(TelegramBareClient): try: me = self(GetUsersRequest([InputUserSelf()]))[0] if not self._self_input_peer: - self._self_input_peer = utils.get_input_peer( + self._self_input_peer = me = utils.get_input_peer( me, allow_self=False ) return me