From 0f72aa8f94f5b834af5c934a671449286ce5e2e1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 17:08:49 +0100 Subject: [PATCH 01/12] Fix set union --- telethon/sessions/memory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 42af7ad9..4d7e6778 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -125,7 +125,7 @@ class MemorySession(Session): return rows def process_entities(self, tlo): - self._entities += set(self._entities_to_rows(tlo)) + self._entities |= set(self._entities_to_rows(tlo)) def get_entity_rows_by_phone(self, phone): try: From 854c42b7ef25763bb5f14290e32c714164d0ace4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 23:12:05 +0100 Subject: [PATCH 02/12] Add a file= parameter to client.send_message() --- .../extra/basic/working-with-updates.rst | 4 ++++ telethon/telegram_client.py | 21 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index 652f6000..3c57b792 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -99,6 +99,10 @@ done! The event that will be passed always is of type ``XYZ.Event`` (for instance, ``NewMessage.Event``), except for the ``Raw`` event which just passes the ``Update`` object. +Note that ``.reply()`` and ``.respond()`` are just wrappers around the +``client.send_message()`` method which supports the ``file=`` parameter. +This means you can reply with a photo if you do ``client.reply(file=photo)``. + You can put the same event on many handlers, and even different events on the same handler. You can also have a handler work on only specific chats, for example: diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index fbb57d63..a86b257e 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -667,8 +667,8 @@ class TelegramClient(TelegramBareClient): return message, msg_entities - def send_message(self, entity, message, reply_to=None, parse_mode='md', - link_preview=True): + def send_message(self, entity, message='', reply_to=None, parse_mode='md', + link_preview=True, file=None, force_document=False): """ Sends the given message to the specified entity (user/chat/channel). @@ -692,9 +692,26 @@ class TelegramClient(TelegramBareClient): link_preview (:obj:`bool`, optional): Should the link preview be shown? + file (:obj:`file`, optional): + Sends a message with a file attached (e.g. a photo, + video, audio or document). The ``message`` may be empty. + + force_document (:obj:`bool`, optional): + Whether to send the given file as a document or not. + Returns: the sent message """ + if file is not None: + return self.send_file( + entity, file, caption=message, reply_to=reply_to, + parse_mode=parse_mode, force_document=force_document + ) + elif not message: + raise ValueError( + 'The message cannot be empty unless a file is provided' + ) + entity = self.get_input_entity(entity) if isinstance(message, Message): if (message.media From c40a3ca77ca8e844203a45d9a51cd21cc6e12a5d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 23:23:14 +0100 Subject: [PATCH 03/12] Split MessageChanged into MessageEdited and MessageDeleted --- telethon/events/__init__.py | 57 ++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 5966a120..233fc94a 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -829,21 +829,32 @@ class UserUpdate(_EventBuilder): return self.chat -class MessageChanged(_EventBuilder): +class MessageEdited(NewMessage): """ - Represents a message changed (edited or deleted). + Event fired when a message has been edited. """ def build(self, update): if isinstance(update, (types.UpdateEditMessage, types.UpdateEditChannelMessage)): - event = MessageChanged.Event(edit_msg=update.message) - elif isinstance(update, types.UpdateDeleteMessages): - event = MessageChanged.Event( + event = MessageEdited.Event(update.message) + else: + return + + return self._filter_event(event) + + +class MessageDeleted(_EventBuilder): + """ + Event fired when one or more messages are deleted. + """ + def build(self, update): + if isinstance(update, types.UpdateDeleteMessages): + event = MessageDeleted.Event( deleted_ids=update.messages, peer=None ) elif isinstance(update, types.UpdateDeleteChannelMessages): - event = MessageChanged.Event( + event = MessageDeleted.Event( deleted_ids=update.messages, peer=types.PeerChannel(update.channel_id) ) @@ -852,33 +863,13 @@ class MessageChanged(_EventBuilder): return self._filter_event(event) - 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. - - 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. - """ - def __init__(self, edit_msg=None, deleted_ids=None, peer=None): - 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.deleted = bool(deleted_ids) - self.deleted_ids = deleted_ids or [] + class Event(_EventCommon): + def __init__(self, deleted_ids, peer): + super().__init__( + types.Message((deleted_ids or [0])[0], peer, None, '') + ) + self.deleted_id = None if not deleted_ids else deleted_ids[0] + self.deleted_ids = self.deleted_ids class StopPropagation(Exception): From 1c8bf4471308ddd9df7f003523f4123287b61df1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 23:31:06 +0100 Subject: [PATCH 04/12] Add input user versions to events.ChatAction --- telethon/events/__init__.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 233fc94a..821930a6 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -607,6 +607,7 @@ class ChatAction(_EventBuilder): self.created = bool(created) self._user_peers = users if isinstance(users, list) else [users] self._users = None + self._input_users = None self.new_title = new_title @property @@ -665,6 +666,16 @@ class ChatAction(_EventBuilder): except (StopIteration, TypeError): return None + @property + def input_user(self): + """ + Input version of the self.user property. + """ + try: + return next(self.input_users) + except (StopIteration, TypeError): + return None + @property def users(self): """ @@ -681,6 +692,22 @@ class ChatAction(_EventBuilder): return self._users + @property + def input_users(self): + """ + Input version of the self.users property. + """ + if self._input_users is None and self._user_peers: + self._input_users = [] + for peer in self._user_peers: + try: + self._input_users.append(self._client.get_input_entity( + peer + )) + except (TypeError, ValueError): + pass + return self._input_users + class UserUpdate(_EventBuilder): """ From 458d220af9643321442baa605aa07a1a34403b68 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 23:41:27 +0100 Subject: [PATCH 05/12] Fix users not being set for some events.ChatAction and properties --- telethon/events/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 821930a6..fe9fc821 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -525,15 +525,19 @@ class ChatAction(_EventBuilder): elif isinstance(action, types.MessageActionChannelCreate): event = ChatAction.Event(msg.to_id, created=True, + users=msg.from_id, new_title=action.title) elif isinstance(action, types.MessageActionChatEditTitle): event = ChatAction.Event(msg.to_id, + users=msg.from_id, new_title=action.title) elif isinstance(action, types.MessageActionChatEditPhoto): event = ChatAction.Event(msg.to_id, + users=msg.from_id, new_photo=action.photo) elif isinstance(action, types.MessageActionChatDeletePhoto): event = ChatAction.Event(msg.to_id, + users=msg.from_id, new_photo=True) else: return @@ -661,20 +665,16 @@ class ChatAction(_EventBuilder): Might be ``None`` if the information can't be retrieved or there is no user taking part. """ - try: - return next(self.users) - except (StopIteration, TypeError): - return None + if self.users: + return self._users[0] @property def input_user(self): """ Input version of the self.user property. """ - try: - return next(self.input_users) - except (StopIteration, TypeError): - return None + if self.input_users: + return self._input_users[0] @property def users(self): From 393f505dc8eba464d392187a487449818ead1e88 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 23:51:35 +0100 Subject: [PATCH 06/12] Put more emphasis on common mistakes when getting entities by IDs --- .../extra/advanced-usage/accessing-the-full-api.rst | 9 +++++++++ readthedocs/extra/basic/entities.rst | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst index 7276aa43..edbe821d 100644 --- a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst +++ b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst @@ -112,6 +112,15 @@ as you wish. Remember to use the right types! To sum up: )) +This can further be simplified to: + + .. code-block:: python + + result = client(SendMessageRequest('username', 'Hello there!')) + # Or even + result = client(SendMessageRequest(PeerChannel(id), 'Hello there!')) + + .. note:: Note that some requests have a "hash" parameter. This is **not** diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index b68a74d7..84be3250 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -37,12 +37,24 @@ you're able to just do this: # Using Peer/InputPeer (note that the API may return these) # users, chats and channels may all have the same ID, so it's # necessary to wrap (at least) chat and channels inside Peer. + # + # NOTICE how the IDs *must* be wrapped inside a Peer() so the + # library knows their type. from telethon.tl.types import PeerUser, PeerChat, PeerChannel my_user = client.get_entity(PeerUser(some_id)) my_chat = client.get_entity(PeerChat(some_id)) my_channel = client.get_entity(PeerChannel(some_id)) +.. warning:: + + As it has been mentioned already, getting the entity of a channel + through e.g. ``client.get_entity(channel id)`` will **not** work. + You would use ``client.get_entity(types.PeerChannel(channel id))``. + Remember that supergroups are channels and normal groups are chats. + This is a common mistake! + + All methods in the :ref:`telegram-client` call ``.get_input_entity()`` prior to sending the requst to save you from the hassle of doing so manually. That way, convenience calls such as ``client.send_message('lonami', 'hi!')`` From 4de811b8cb6d7e3dddef0bf32ea2066061d68656 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 23:55:35 +0100 Subject: [PATCH 07/12] Expose the client on events as a public property --- telethon/events/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index fe9fc821..942fc342 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -140,6 +140,10 @@ class _EventCommon(abc.ABC): ) return self._input_chat + @property + def client(self): + return self._client + @property def chat(self): """ From 363e751f4896e4e29669723af84a8a175e8ee6ad Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 4 Mar 2018 00:23:13 +0100 Subject: [PATCH 08/12] Fix UserList not being considered a list --- telethon/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/telethon/utils.py b/telethon/utils.py index faf69649..a9311521 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -6,6 +6,7 @@ import math import mimetypes import re import types +from collections import UserList from mimetypes import add_type, guess_extension from .tl.types import ( @@ -342,7 +343,8 @@ def is_list_like(obj): 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)) + return isinstance(obj, (list, tuple, set, dict, + UserList, types.GeneratorType)) def parse_phone(phone): From 3a13f5f02faea49b72a0917cad8042365fd0d0fa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 4 Mar 2018 00:27:21 +0100 Subject: [PATCH 09/12] Implement a forward_messages convenience method --- telethon/telegram_client.py | 56 ++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index a86b257e..f79721a6 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -56,7 +56,8 @@ from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, SendMediaRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest, - UploadMediaRequest, EditMessageRequest, GetFullChatRequest + UploadMediaRequest, EditMessageRequest, GetFullChatRequest, + ForwardMessagesRequest ) from .tl.functions import channels @@ -756,6 +757,59 @@ class TelegramClient(TelegramBareClient): return self._get_response_message(request, result) + def forward_messages(self, entity, messages, from_peer=None): + """ + Forwards the given message(s) to the specified entity. + + Args: + entity (:obj:`entity`): + To which entity the message(s) will be forwarded. + + messages (:obj:`list` | :obj:`int` | :obj:`Message`): + The message(s) to forward, or their integer IDs. + + from_peer (:obj:`entity`): + If the given messages are integer IDs and not instances + of the ``Message`` class, this *must* be specified in + order for the forward to work. + + Returns: + The forwarded messages. + """ + if not utils.is_list_like(messages): + messages = (messages,) + + if not from_peer: + try: + # On private chats (to_id = PeerUser), if the message is + # not outgoing, we actually need to use "from_id" to get + # the conversation on which the message was sent. + from_peer = next( + m.from_id if not m.out and isinstance(m.to_id, PeerUser) + else m.to_id for m in messages if isinstance(m, Message) + ) + except StopIteration: + raise ValueError( + 'from_chat must be given if integer IDs are used' + ) + + req = ForwardMessagesRequest( + from_peer=from_peer, + id=[m if isinstance(m, int) else m.id for m in messages], + to_peer=entity + ) + result = self(req) + random_to_id = {} + id_to_message = {} + for update in result.updates: + if isinstance(update, UpdateMessageID): + random_to_id[update.random_id] = update.id + elif isinstance(update, UpdateNewMessage): + id_to_message[update.message.id] = update.message + + return [id_to_message[random_to_id[rnd]] for rnd in req.random_id] + + def edit_message(self, entity, message_id, message=None, parse_mode='md', link_preview=True): """ From 82c034dc5610ccfec8bdb8fa384641dd2cbb8daa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 4 Mar 2018 00:32:26 +0100 Subject: [PATCH 10/12] Add forward_to on events.NewMessage --- telethon/events/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 942fc342..5c0f2d07 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -320,9 +320,17 @@ class NewMessage(_EventBuilder): Replies to the message (as a reply). This is a shorthand for ``client.send_message(event.chat, ..., reply_to=event.message.id)``. """ - return self._client.send_message(self.input_chat, - reply_to=self.message.id, - *args, **kwargs) + kwargs['reply_to'] = self.message.id + return self._client.send_message(self.input_chat, *args, **kwargs) + + def forward_to(self, *args, **kwargs): + """ + Forwards the message. This is a shorthand for + ``client.forward_messages(entity, event.message, event.chat)``. + """ + kwargs['messages'] = [self.message.id] + kwargs['from_peer'] = self.input_chat + return self._client.forward_messages(*args, **kwargs) def edit(self, *args, **kwargs): """ From e8a21dc3b9b92ffe970f7752ef7cd8fbfca2fb72 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 4 Mar 2018 11:23:18 +0100 Subject: [PATCH 11/12] Fix telethon_generator/ package not being excluded from PyPi --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0e052d31..143ca0cb 100755 --- a/setup.py +++ b/setup.py @@ -147,7 +147,11 @@ def main(): keywords='telegram api chat client library messaging mtproto', packages=find_packages(exclude=[ 'telethon_generator', 'telethon_tests', 'run_tests.py', - 'try_telethon.py' + 'try_telethon.py', + 'telethon_generator/parser/__init__.py', + 'telethon_generator/parser/source_builder.py', + 'telethon_generator/parser/tl_object.py', + 'telethon_generator/parser/tl_parser.py', ]), install_requires=['pyaes', 'rsa'], extras_require={ From fe627d197003ca3774cfaf76b85ca1a4866cb630 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 4 Mar 2018 12:03:09 +0100 Subject: [PATCH 12/12] Update to v0.18 --- readthedocs/extra/changelog.rst | 65 +++++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index ad027361..e8876d5c 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,71 @@ it can take advantage of new goodies! .. contents:: List of All Versions +Sessions overhaul (v0.18) +========================= + +*Published at 2018/03/04* + ++-----------------------+ +| Scheme layer used: 75 | ++-----------------------+ + +The ``Session``'s have been revisited thanks to the work of **@tulir** and +they now use an `ABC `__ so you +can easily implement your own! + +The default will still be a ``SQLiteSession``, but you might want to use +the new ``AlchemySessionContainer`` if you need. Refer to the section of +the documentation on :ref:`sessions` for more. + +Breaking changes +~~~~~~~~~~~~~~~~ + +- ``events.MessageChanged`` doesn't exist anymore. Use the new + ``events.MessageEdited`` and ``events.MessageDeleted`` instead. + +Additions +~~~~~~~~~ + +- The mentioned addition of new session types. +- You can omit the event type on ``client.add_event_handler`` to use ``Raw``. +- You can ``raise StopPropagation`` of events if you added several of them. +- ``.get_participants()`` can now get up to 90,000 members from groups with + 100,000 if when ``aggressive=True``, "bypassing" Telegram's limit. +- You now can access ``NewMessage.Event.pattern_match``. +- Multiple captions are now supported when sending albums. +- ``client.send_message()`` has an optional ``file=`` parameter, so + you can do ``events.reply(file='/path/to/photo.jpg')`` and similar. +- Added ``.input_`` versions to ``events.ChatAction``. +- You can now access the public ``.client`` property on ``events``. +- New ``client.forward_messages``, with its own wrapper on ``events``, + called ``event.forward_to(...)``. + + +Bug fixes +~~~~~~~~~ + +- Silly bug regarding ``client.get_me(input_peer=True)``. +- ``client.send_voice_note()`` was missing some parameters. +- ``client.send_file()`` plays better with streams now. +- Incoming messages from bots weren't working with whitelists. +- Markdown's URL regex was not accepting newlines. +- Better attempt at joining background update threads. +- Use the right peer type when a marked integer ID is provided. + + +Internal changes +~~~~~~~~~~~~~~~~ + +- Resolving ``events.Raw`` is now a no-op. +- Logging calls in the ``TcpClient`` to spot errors. +- ``events`` resolution is postponed until you are successfully connected, + so you can attach them before starting the client. +- When an entity is not found, it is searched in *all* dialogs. This might + not always be desirable but it's more comfortable for legitimate uses. +- Some non-persisting properties from the ``Session`` have been moved out. + + Further easing library usage (v0.17.4) ====================================== diff --git a/telethon/version.py b/telethon/version.py index 8cc14b33..90e8bfe4 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.4' +__version__ = '0.18'