From 7285b156f48feb4367e682742181484bd28979c8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 30 Jun 2019 16:34:07 +0200 Subject: [PATCH] Create events.Album (#1216) --- readthedocs/modules/events.rst | 5 + .../quick-references/events-reference.rst | 28 ++ telethon/events/__init__.py | 1 + telethon/events/album.py | 267 ++++++++++++++++++ 4 files changed, 301 insertions(+) create mode 100644 telethon/events/album.py diff --git a/readthedocs/modules/events.rst b/readthedocs/modules/events.rst index 961885a0..4344156a 100644 --- a/readthedocs/modules/events.rst +++ b/readthedocs/modules/events.rst @@ -54,6 +54,11 @@ so all the methods in it can be used from any event builder/event instance. :undoc-members: :show-inheritance: +.. automodule:: telethon.events.album + :members: + :undoc-members: + :show-inheritance: + .. automodule:: telethon.events.raw :members: :undoc-members: diff --git a/readthedocs/quick-references/events-reference.rst b/readthedocs/quick-references/events-reference.rst index cba224cd..a960efae 100644 --- a/readthedocs/quick-references/events-reference.rst +++ b/readthedocs/quick-references/events-reference.rst @@ -194,6 +194,34 @@ Full documentation for the `InlineQuery answer +Album +===== + +Occurs whenever you receive an entire album. + +Full documentation for the `Album +`. + +.. currentmodule:: telethon.events.album.Album.Event + +.. autosummary:: + :nosignatures: + + grouped_id + text + raw_text + is_reply + forward + + get_reply_message + respond + reply + forward_to + edit + delete + mark_read + pin + Raw === diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 0b1c16c6..ffbbecdf 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -1,4 +1,5 @@ from .raw import Raw +from .album import Album from .chataction import ChatAction from .messagedeleted import MessageDeleted from .messageedited import MessageEdited diff --git a/telethon/events/album.py b/telethon/events/album.py new file mode 100644 index 00000000..a9be9434 --- /dev/null +++ b/telethon/events/album.py @@ -0,0 +1,267 @@ +import time + +from .common import EventBuilder, EventCommon, name_inner_event +from .. import utils +from ..tl import types +from ..tl.custom.sendergetter import SenderGetter + +_IGNORE_MAX_SIZE = 100 # len() +_IGNORE_MAX_AGE = 5 # seconds + +# IDs to ignore, and when they were added. If it grows too large, we will +# remove old entries. Although it should generally not be bigger than 10, +# it may be possible some updates are not processed and thus not removed. +_IGNORE_DICT = {} + + +@name_inner_event +class Album(EventBuilder): + """ + Occurs whenever you receive an album. This event only exists + to ease dealing with an unknown amount of messages that belong + to the same album. + """ + + def __init__( + self, chats=None, *, blacklist_chats=False, func=None): + super().__init__(chats, blacklist_chats=blacklist_chats, func=func) + + @classmethod + def build(cls, update, others=None): + if not others: + return # We only care about albums which come inside the same Updates + + if isinstance(update, + (types.UpdateNewMessage, types.UpdateNewChannelMessage)): + if not isinstance(update.message, types.Message): + return # We don't care about MessageService's here + + group = update.message.grouped_id + if group is None: + return # It must be grouped + + # Check whether we are supposed to skip this update, and + # if we do also remove it from the ignore list since we + # won't need to check against it again. + if _IGNORE_DICT.pop(id(update), None): + return + + # Check if the ignore list is too big, and if it is clean it + now = time.time() + if len(_IGNORE_DICT) > _IGNORE_MAX_SIZE: + for i in [i for i, t in _IGNORE_DICT.items() if now - t > _IGNORE_MAX_AGE]: + del _IGNORE_DICT[i] + + # Add the other updates to the ignore list + for u in others: + if u is not update: + _IGNORE_DICT[id(u)] = now + + # Figure out which updates share the same group and use those + return cls.Event([ + u.message for u in others + if (isinstance(u, (types.UpdateNewMessage, types.UpdateNewChannelMessage)) + and isinstance(u.message, types.Message) + and u.message.grouped_id == group) + ]) + + class Event(EventCommon, SenderGetter): + """ + Represents the event of a new album. + + Members: + messages (Sequence[`Message `]): + The list of messages belonging to the same album. + """ + def __init__(self, messages): + message = messages[0] + 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 a 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)) + + SenderGetter.__init__(self, message.sender_id) + self.messages = messages + + def _set_client(self, client): + super()._set_client(client) + self._sender, self._input_sender = utils._get_entity_pair( + self.sender_id, self._entities, client._entity_cache) + + for msg in self.messages: + msg._finish_init(client, self._entities, None) + + @property + def grouped_id(self): + """ + The shared ``grouped_id`` between all the messages. + """ + return self.messages[0].grouped_id + + @property + def text(self): + """ + The message text of the first photo with a caption, + formatted using the client's default parse mode. + """ + return next((m.text for m in self.messages if m.text), '') + + @property + def raw_text(self): + """ + The raw message text of the first photo + with a caption, ignoring any formatting. + """ + return next((m.raw_text for m in self.messages if m.raw_text), '') + + @property + def is_reply(self): + """ + ``True`` if the album is a reply to some other message. + + Remember that you can access the ID of the message + this one is replying to through `reply_to_msg_id`, + and the `Message` object with `get_reply_message()`. + """ + # Each individual message in an album all reply to the same message + return self.messages[0].is_reply + + @property + def forward(self): + """ + The `Forward ` + information for the first message in the album if it was forwarded. + """ + # Each individual message in an album all reply to the same message + return self.messages[0].forward + + # endregion Public Properties + + # region Public Methods + + async def get_reply_message(self): + """ + The `Message ` + that this album is replying to, or ``None``. + + The result will be cached after its first use. + """ + return await self.messages[0].get_reply_message() + + async def respond(self, *args, **kwargs): + """ + Responds to the album (not as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` + with ``entity`` already set. + """ + return await self.messages[0].respond(*args, **kwargs) + + async def reply(self, *args, **kwargs): + """ + Replies to the first photo in the album (as a reply). Shorthand + for `telethon.client.messages.MessageMethods.send_message` + with both ``entity`` and ``reply_to`` already set. + """ + return await self.messages[0].reply(*args, **kwargs) + + async def forward_to(self, *args, **kwargs): + """ + Forwards the entire album. Shorthand for + `telethon.client.messages.MessageMethods.forward_messages` + with both ``messages`` and ``from_peer`` already set. + """ + if self._client: + kwargs['messages'] = self.messages + kwargs['as_album'] = True + kwargs['from_peer'] = await self.get_input_chat() + return await self._client.forward_messages(*args, **kwargs) + + async def edit(self, *args, **kwargs): + """ + Edits the first caption or the message, or the first messages' + caption if no caption is set, iff it's outgoing. Shorthand for + `telethon.client.messages.MessageMethods.edit_message` + with both ``entity`` and ``message`` already set. + + Returns ``None`` if the message was incoming, + or the edited `Message` otherwise. + + .. note:: + + This is different from `client.edit_message + ` + and **will respect** the previous state of the message. + For example, if the message didn't have a link preview, + the edit won't add one by default, and you should force + it by setting it to ``True`` if you want it. + + This is generally the most desired and convenient behaviour, + and will work for link previews and message buttons. + """ + for msg in self.messages: + if msg.raw_text: + return await msg.edit(*args, **kwargs) + + return await self.messages[0].edit(*args, **kwargs) + + async def delete(self, *args, **kwargs): + """ + Deletes the entire album. You're responsible for checking whether + you have the permission to do so, or to except the error otherwise. + Shorthand for + `telethon.client.messages.MessageMethods.delete_messages` with + ``entity`` and ``message_ids`` already set. + """ + if self._client: + return await self._client.delete_messages( + await self.get_input_chat(), self.messages, + *args, **kwargs + ) + + async def mark_read(self): + """ + Marks the entire album as read. Shorthand for + `client.send_read_acknowledge() + ` + with both ``entity`` and ``message`` already set. + """ + if self._client: + await self._client.send_read_acknowledge( + await self.get_input_chat(), max_id=self.messages[-1].id) + + async def pin(self, *, notify=False): + """ + Pins the first photo in the album. Shorthand for + `telethon.client.messages.MessageMethods.pin_message` + with both ``entity`` and ``message`` already set. + """ + await self.messages[0].pin(notify=notify) + + def __len__(self): + """ + Return the amount of messages in the album. + + Equivalent to ``len(self.messages)``. + """ + return len(self.messages) + + def __iter__(self): + """ + Iterate over the messages in the album. + + Equivalent to ``iter(self.messages)``. + """ + return iter(self.messages) + + def __getitem__(self, n): + """ + Access the n'th message in the album. + + Equivalent to ``event.messages[n]``. + """ + return self.messages[n]