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 01/24] 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 02/24] 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 03/24] 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 04/24] 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 05/24] 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 06/24] 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 07/24] 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 08/24] 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 09/24] 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 10/24] 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 11/24] 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 12/24] 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 13/24] 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 14/24] 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 15/24] 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 16/24] 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 cfc5ecfdedefdb0331301cd2b652090ee204e695 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Feb 2018 18:30:09 +0100 Subject: [PATCH 17/24] Fix tiny bug regarding .get_me(input_peer=True) crashing events --- telethon/telegram_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 55babafe..1e418904 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -488,7 +488,8 @@ class TelegramClient(TelegramBareClient): self._self_input_peer = utils.get_input_peer( me, allow_self=False ) - return me + + return self._self_input_peer if input_peer else me except UnauthorizedError: return None From 098602ca13edd5013deb1bf9bd1d18b87be5c9ee Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 25 Feb 2018 10:36:53 +0100 Subject: [PATCH 18/24] Let events.Raw.resolve() be a no-op --- telethon/events/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 1d7beab2..204609c0 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -155,6 +155,9 @@ class Raw(_EventBuilder): """ Represents a raw event. The event is the update itself. """ + def resolve(self, client): + pass + def build(self, update): return update From 623c1bd7d178c2876d3dabada4095ff4dd0cfdbc Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 25 Feb 2018 20:34:40 +0100 Subject: [PATCH 19/24] Add missing parameters to TelegramClient.send_voice_note --- telethon/telegram_client.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 1e418904..562a6d7e 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1278,13 +1278,9 @@ class TelegramClient(TelegramBareClient): return msg - 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_voice_note(self, *args, **kwargs): + """Wrapper method around .send_file() with is_voice_note=True""" + return self.send_file(*args, **kwargs, is_voice_note=True) def _send_album(self, entity, files, caption=None, progress_callback=None, reply_to=None): From 3b0ab7794b7399f126a0088d8d3ad9e3b5ec1118 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 25 Feb 2018 20:35:55 +0100 Subject: [PATCH 20/24] Get name attribute from streams instead always 'unnamed' --- 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 562a6d7e..a2bf5e85 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1226,8 +1226,8 @@ class TelegramClient(TelegramBareClient): attr_dict[DocumentAttributeVideo] = doc else: attr_dict = { - DocumentAttributeFilename: - DocumentAttributeFilename('unnamed') + DocumentAttributeFilename: DocumentAttributeFilename( + getattr(file, 'name', None) or 'unnamed') } if 'is_voice_note' in kwargs: From 9604161c91b18169ba6de6e02d349d2091d8c29b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 26 Feb 2018 12:14:21 +0100 Subject: [PATCH 21/24] Fix incoming private messages not working with whitelists For some reason this was only happening with bots and not actual private messages. The fix doesn't seem to affect previous behaviour with actual users in private messages. --- telethon/events/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 204609c0..1f3b15f2 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -277,7 +277,14 @@ class NewMessage(_EventBuilder): Whether the message is a reply to some other or not. """ def __init__(self, message): - super().__init__(chat_peer=message.to_id, + if not message.out and isinstance(message.to_id, types.PeerUser): + # Incoming message (e.g. from a bot) has to_id=us, and + # from_id=bot (the actual "chat" from an user's perspective). + chat_peer = types.PeerUser(message.from_id) + else: + chat_peer = message.to_id + + super().__init__(chat_peer=chat_peer, msg_id=message.id, broadcast=bool(message.post)) self.message = message From 6f16aeb55340ab1442efe9cd2b468a6abc85a69c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 26 Feb 2018 13:41:07 +0100 Subject: [PATCH 22/24] Add logging calls on the TcpClient --- telethon/extensions/tcp_client.py | 35 +++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index dd177aa2..d9cea2f0 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -2,6 +2,7 @@ This module holds a rough implementation of the C# TCP client. """ import errno +import logging import socket import time from datetime import timedelta @@ -19,6 +20,8 @@ CONN_RESET_ERRNOS = { errno.EINVAL, errno.ENOTCONN } +__log__ = logging.getLogger(__name__) + class TcpClient: """A simple TCP client to ease the work with sockets and proxies.""" @@ -75,6 +78,7 @@ class TcpClient: self._socket.connect(address) break # Successful connection, stop retrying to connect except OSError as e: + __log__.info('OSError "%s" raised while connecting', e) # Stop retrying to connect if proxy connection error occurred if socks and isinstance(e, socks.ProxyConnectionError): raise @@ -120,19 +124,22 @@ class TcpClient: :param data: the data to send. """ if self._socket is None: - self._raise_connection_reset() + self._raise_connection_reset(None) # TODO Timeout may be an issue when sending the data, Changed in v3.5: # The socket timeout is now the maximum total duration to send all data. try: self._socket.sendall(data) except socket.timeout as e: + __log__.debug('socket.timeout "%s" while writing data', e) raise TimeoutError() from e - except ConnectionError: - self._raise_connection_reset() + except ConnectionError as e: + __log__.info('ConnectionError "%s" while writing data', e) + self._raise_connection_reset(e) except OSError as e: + __log__.info('OSError "%s" while writing data', e) if e.errno in CONN_RESET_ERRNOS: - self._raise_connection_reset() + self._raise_connection_reset(e) else: raise @@ -144,7 +151,7 @@ class TcpClient: :return: the read data with len(data) == size. """ if self._socket is None: - self._raise_connection_reset() + self._raise_connection_reset(None) # TODO Remove the timeout from this method, always use previous one with BufferedWriter(BytesIO(), buffer_size=size) as buffer: @@ -153,17 +160,22 @@ class TcpClient: try: partial = self._socket.recv(bytes_left) except socket.timeout as e: + # These are somewhat common if the server has nothing + # to send to us, so use a lower logging priority. + __log__.debug('socket.timeout "%s" while reading data', e) raise TimeoutError() from e - except ConnectionError: - self._raise_connection_reset() + except ConnectionError as e: + __log__.info('ConnectionError "%s" while reading data', e) + self._raise_connection_reset(e) except OSError as e: + __log__.info('OSError "%s" while reading data', e) if e.errno in CONN_RESET_ERRNOS: - self._raise_connection_reset() + self._raise_connection_reset(e) else: raise if len(partial) == 0: - self._raise_connection_reset() + self._raise_connection_reset(None) buffer.write(partial) bytes_left -= len(partial) @@ -172,7 +184,8 @@ class TcpClient: buffer.flush() return buffer.raw.getvalue() - def _raise_connection_reset(self): + def _raise_connection_reset(self, original): """Disconnects the client and raises ConnectionResetError.""" self.close() # Connection reset -> flag as socket closed - raise ConnectionResetError('The server has closed the connection.') + raise ConnectionResetError('The server has closed the connection.')\ + from original From 5a54e2279fecc3bbfd44694ad68a8fdb52593a6a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 26 Feb 2018 14:12:21 +0100 Subject: [PATCH 23/24] Avoid relying on .__iter__ to tell iterators apart .send_file() would fail with stream objects (those from open()) since they are iterable, and asserting that they weren't bytes or str was not enough. --- telethon/events/__init__.py | 2 +- telethon/session.py | 6 +++--- telethon/telegram_client.py | 6 +++--- telethon/utils.py | 12 ++++++++++++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 1f3b15f2..48b26004 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -14,7 +14,7 @@ def _into_id_set(client, chats): if chats is None: return None - if not hasattr(chats, '__iter__') or isinstance(chats, str): + if not utils.is_list_like(chats): chats = (chats,) result = set() diff --git a/telethon/session.py b/telethon/session.py index 4658df2b..faa1516f 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -355,14 +355,14 @@ class Session: if not self.save_entities: return - if not isinstance(tlo, TLObject) and hasattr(tlo, '__iter__'): + if not isinstance(tlo, TLObject) and utils.is_list_like(tlo): # This may be a list of users already for instance entities = tlo else: entities = [] - if hasattr(tlo, 'chats') and hasattr(tlo.chats, '__iter__'): + if hasattr(tlo, 'chats') and utils.is_list_like(tlo.chats): entities.extend(tlo.chats) - if hasattr(tlo, 'users') and hasattr(tlo.users, '__iter__'): + if hasattr(tlo, 'users') and utils.is_list_like(tlo.users): entities.extend(tlo.users) if not entities: return diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index a2bf5e85..1dad2716 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -975,7 +975,7 @@ class TelegramClient(TelegramBareClient): """ if max_id is None: if message: - if hasattr(message, '__iter__'): + if utils.is_list_like(message): max_id = max(msg.id for msg in message) else: max_id = message.id @@ -1140,7 +1140,7 @@ class TelegramClient(TelegramBareClient): """ # First check if the user passed an iterable, in which case # we may want to send as an album if all are photo files. - if hasattr(file, '__iter__') and not isinstance(file, (str, bytes)): + if utils.is_list_like(file): # Convert to tuple so we can iterate several times file = tuple(x for x in file) if all(utils.is_image(x) for x in file): @@ -1960,7 +1960,7 @@ class TelegramClient(TelegramBareClient): ``User``, ``Chat`` or ``Channel`` corresponding to the input entity. """ - if hasattr(entity, '__iter__') and not isinstance(entity, str): + if utils.is_list_like(entity): single = False else: single = True diff --git a/telethon/utils.py b/telethon/utils.py index fdadbd1c..8f38563a 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -5,6 +5,7 @@ to convert between an entity like an User, Chat, etc. into its Input version) import math import mimetypes import re +import types from mimetypes import add_type, guess_extension from .tl.types import ( @@ -341,6 +342,17 @@ def is_video(file): (mimetypes.guess_type(file)[0] or '').startswith('video/')) +def is_list_like(obj): + """ + Returns True if the given object looks like a list. + + Checking if hasattr(obj, '__iter__') and ignoring str/bytes is not + enough. Things like open() are also iterable (and probably many + other things), so just support the commonly known list-like objects. + """ + return isinstance(obj, (list, tuple, set, dict, types.GeneratorType)) + + def parse_phone(phone): """Parses the given phone, or returns None if it's invalid""" if isinstance(phone, int): From 8d1b6629cb7e68ac14737ec533aa48475fa71efc Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 26 Feb 2018 14:14:44 +0100 Subject: [PATCH 24/24] Sending open()'ed files would make their name the entire path --- telethon/telegram_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 1dad2716..158855ad 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1227,7 +1227,8 @@ class TelegramClient(TelegramBareClient): else: attr_dict = { DocumentAttributeFilename: DocumentAttributeFilename( - getattr(file, 'name', None) or 'unnamed') + os.path.basename( + getattr(file, 'name', None) or 'unnamed')) } if 'is_voice_note' in kwargs: