diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 79681ff0..ce077d13 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -779,3 +779,12 @@ input_peer removed from get_me; input peers should remain mostly an impl detail raw api types and fns are now immutable. this can enable optimizations in the future. upload_file has been removed from the public methods. it's a low-level method users should not need to use. + +events have changed. rather than differentiating between "event builder" and "event instance", instead there is only the instance, and you register the class. +where you had +@client.on(events.NewMessage(chats=...)) +it's now +@client.on(events.NewMessage, chats=...) +this also means filters are unified, although not all have an effect on all events. from_users renamed to senders. messageread inbox is gone in favor of outgoing/incoming. +events.register, unregister, is_handler and list are gone. now you can typehint instead. +def handler(event: events.NewMessage) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 67141622..136a6d31 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -10,7 +10,7 @@ from . import ( ) from .. import version, _tl from ..types import _custom -from .._events.common import EventBuilder, EventCommon +from .._events.base import EventBuilder from .._misc import enums diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index cf26e809..a9875bba 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -10,7 +10,7 @@ import logging from collections import deque from ..errors._rpcbase import RpcError -from .._events.common import EventBuilder, EventCommon +from .._events.base import EventBuilder from .._events.raw import Raw from .._events.base import StopPropagation, _get_handlers from .._misc import utils diff --git a/telethon/_events/album.py b/telethon/_events/album.py index 580e5a31..41646acf 100644 --- a/telethon/_events/album.py +++ b/telethon/_events/album.py @@ -2,7 +2,7 @@ import asyncio import time import weakref -from .common import EventBuilder, EventCommon, name_inner_event +from .base import EventBuilder from .._misc import utils from .. import _tl from ..types import _custom @@ -64,13 +64,16 @@ class AlbumHack: await asyncio.sleep(diff) -@name_inner_event -class Album(EventBuilder): +class Album(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter): """ 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. + Members: + messages (Sequence[`Message `]): + The list of messages belonging to the same album. + Example .. code-block:: python @@ -91,12 +94,20 @@ class Album(EventBuilder): await event.messages[4].reply('Cool!') """ - def __init__( - self, chats=None, *, blacklist_chats=False, func=None): - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) + def __init__(self, messages): + message = messages[0] + if not message.out and isinstance(message.peer_id, _tl.PeerUser): + # Incoming message (e.g. from a bot) has peer_id=us, and + # from_id=bot (the actual "chat" from a user's perspective). + chat_peer = message.from_id + else: + chat_peer = message.peer_id - @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + _custom.chatgetter.ChatGetter.__init__(self, chat_peer=chat_peer, broadcast=bool(message.post)) + _custom.sendergetter.SenderGetter.__init__(self, message.sender_id) + self.messages = messages + + def _build(cls, update, others=None, self_id=None, *todo, **todo2): if not others: return # We only care about albums which come inside the same Updates @@ -135,216 +146,188 @@ class Album(EventBuilder): and u.message.grouped_id == group) ]) - def filter(self, event): - # Albums with less than two messages require a few hacks to work. - if len(event.messages) > 1: - return super().filter(event) + def _set_client(self, client): + super()._set_client(client) + self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) - class Event(EventCommon, _custom.sendergetter.SenderGetter): - """ - Represents the event of a new album. + self.messages = [ + _custom.Message._new(client, m, self._entities, None) + for m in self.messages + ] - 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.peer_id, _tl.PeerUser): - # Incoming message (e.g. from a bot) has peer_id=us, and - # from_id=bot (the actual "chat" from a user's perspective). - chat_peer = message.from_id + if len(self.messages) == 1: + # This will require hacks to be a proper album event + hack = client._albums.get(self.grouped_id) + if hack is None: + client._albums[self.grouped_id] = AlbumHack(client, self) else: - chat_peer = message.peer_id + hack.extend(self.messages) - super().__init__(chat_peer=chat_peer, - msg_id=message.id, broadcast=bool(message.post)) + @property + def grouped_id(self): + """ + The shared ``grouped_id`` between all the messages. + """ + return self.messages[0].grouped_id - _custom.sendergetter.SenderGetter.__init__(self, message.sender_id) - self.messages = messages + @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), '') - def _set_client(self, client): - super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) + @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), '') - self.messages = [ - _custom.Message._new(client, m, self._entities, None) - for m in self.messages - ] + @property + def is_reply(self): + """ + `True` if the album is a reply to some other message. - if len(self.messages) == 1: - # This will require hacks to be a proper album event - hack = client._albums.get(self.grouped_id) - if hack is None: - client._albums[self.grouped_id] = AlbumHack(client, self) - else: - hack.extend(self.messages) + 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 grouped_id(self): - """ - The shared ``grouped_id`` between all the messages. - """ - return self.messages[0].grouped_id + @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 - @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), '') + # endregion Public Properties - @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), '') + # region Public Methods - @property - def is_reply(self): - """ - `True` if the album is a reply to some other message. + async def get_reply_message(self): + """ + The `Message ` + that this album is replying to, or `None`. - 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 + The result will be cached after its first use. + """ + return await self.messages[0].get_reply_message() - @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 + 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) - # endregion Public Properties + 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) - # region Public Methods + 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['from_peer'] = await self.get_input_chat() + return await self._client.forward_messages(*args, **kwargs) - async def get_reply_message(self): - """ - The `Message ` - that this album is replying to, or `None`. + 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. - The result will be cached after its first use. - """ - return await self.messages[0].get_reply_message() + Returns `None` if the message was incoming, + or the edited `Message` otherwise. - 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) + .. note:: - 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) + 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. - 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['from_peer'] = await self.get_input_chat() - return await self._client.forward_messages(*args, **kwargs) + 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) - 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. + return await self.messages[0].edit(*args, **kwargs) - Returns `None` if the message was incoming, - or the edited `Message` otherwise. + 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 + ) - .. note:: + async def mark_read(self): + """ + Marks the entire album as read. Shorthand for + `client.mark_read() + ` + with both ``entity`` and ``message`` already set. + """ + if self._client: + await self._client.mark_read( + await self.get_input_chat(), max_id=self.messages[-1].id) - 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. + 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. + """ + return await self.messages[0].pin(notify=notify) - 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) + def __len__(self): + """ + Return the amount of messages in the album. - return await self.messages[0].edit(*args, **kwargs) + Equivalent to ``len(self.messages)``. + """ + return len(self.messages) - 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 - ) + def __iter__(self): + """ + Iterate over the messages in the album. - async def mark_read(self): - """ - Marks the entire album as read. Shorthand for - `client.mark_read() - ` - with both ``entity`` and ``message`` already set. - """ - if self._client: - await self._client.mark_read( - await self.get_input_chat(), max_id=self.messages[-1].id) + Equivalent to ``iter(self.messages)``. + """ + return iter(self.messages) - 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. - """ - return await self.messages[0].pin(notify=notify) + def __getitem__(self, n): + """ + Access the n'th message in the album. - 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] + Equivalent to ``event.messages[n]``. + """ + return self.messages[n] diff --git a/telethon/_events/base.py b/telethon/_events/base.py index 8f913ad7..303c5976 100644 --- a/telethon/_events/base.py +++ b/telethon/_events/base.py @@ -1,7 +1,4 @@ -from .raw import Raw - - -_HANDLERS_ATTRIBUTE = '__tl.handlers' +import abc class StopPropagation(Exception): @@ -31,101 +28,16 @@ class StopPropagation(Exception): pass -def register(event=None): - """ - Decorator method to *register* event handlers. This is the client-less - `add_event_handler() - ` variant. +class EventBuilder(abc.ABC): + @classmethod + @abc.abstractmethod + def _build(cls, update, others, self_id, entities, client): + """ + Builds an event for the given update if possible, or returns None. - Note that this method only registers callbacks as handlers, - and does not attach them to any client. This is useful for - external modules that don't have access to the client, but - still want to define themselves as a handler. Example: + `others` are the rest of updates that came in the same container + as the current `update`. - >>> from telethon import events - >>> @events.register(events.NewMessage) - ... async def handler(event): - ... ... - ... - >>> # (somewhere else) - ... - >>> from telethon import TelegramClient - >>> client = TelegramClient(...) - >>> client.add_event_handler(handler) - - Remember that you can use this as a non-decorator - through ``register(event)(callback)``. - - Args: - event (`_EventBuilder` | `type`): - The event builder class or instance to be used, - for instance ``events.NewMessage``. - """ - if isinstance(event, type): - event = event() - elif not event: - event = Raw() - - def decorator(callback): - handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) - handlers.append(event) - setattr(callback, _HANDLERS_ATTRIBUTE, handlers) - return callback - - return decorator - - -def unregister(callback, event=None): - """ - Inverse operation of `register` (though not a decorator). Client-less - `remove_event_handler - ` - variant. **Note that this won't remove handlers from the client**, - because it simply can't, so you would generally use this before - adding the handlers to the client. - - This method is here for symmetry. You will rarely need to - unregister events, since you can simply just not add them - to any client. - - If no event is given, all events for this callback are removed. - Returns how many callbacks were removed. - """ - found = 0 - if event and not isinstance(event, type): - event = type(event) - - handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) - handlers.append((event, callback)) - i = len(handlers) - while i: - i -= 1 - ev = handlers[i] - if not event or isinstance(ev, event): - del handlers[i] - found += 1 - - return found - - -def is_handler(callback): - """ - Returns `True` if the given callback is an - event handler (i.e. you used `register` on it). - """ - return hasattr(callback, _HANDLERS_ATTRIBUTE) - - -def list(callback): - """ - Returns a list containing the registered event - builders inside the specified callback handler. - """ - return getattr(callback, _HANDLERS_ATTRIBUTE, [])[:] - - -def _get_handlers(callback): - """ - Like ``list`` but returns `None` if the callback was never registered. - """ - return getattr(callback, _HANDLERS_ATTRIBUTE, None) + `self_id` should be the current user's ID, since it is required + for some events which lack this information but still need it. + """ diff --git a/telethon/_events/callbackquery.py b/telethon/_events/callbackquery.py index 0e3e2d67..8d298349 100644 --- a/telethon/_events/callbackquery.py +++ b/telethon/_events/callbackquery.py @@ -3,7 +3,7 @@ import struct import asyncio import functools -from .common import EventBuilder, EventCommon, name_inner_event +from .base import EventBuilder from .._misc import utils from .. import _tl from ..types import _custom @@ -23,8 +23,7 @@ def auto_answer(func): return wrapped -@name_inner_event -class CallbackQuery(EventBuilder): +class CallbackQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter): """ Occurs whenever you sign in as a bot and a user clicks one of the inline buttons on your messages. @@ -34,18 +33,17 @@ class CallbackQuery(EventBuilder): message. The `chats` parameter also supports checking against the `chat_instance` which should be used for inline callbacks. - Args: - data (`bytes`, `str`, `callable`, optional): - If set, the inline button payload data must match this data. - A UTF-8 string can also be given, a regex or a callable. For - instance, to check against ``'data_1'`` and ``'data_2'`` you - can use ``re.compile(b'data_')``. + Members: + query (:tl:`UpdateBotCallbackQuery`): + The original :tl:`UpdateBotCallbackQuery`. - pattern (`bytes`, `str`, `callable`, `Pattern`, optional): - If set, only buttons with payload matching this pattern will be handled. - You can specify a regex-like string which will be matched - against the payload data, a callable function that returns `True` - if a the payload data is acceptable, or a compiled regex pattern. + data_match (`obj`, optional): + The object returned by the ``data=`` parameter + when creating the event builder, if any. Similar + to ``pattern_match`` for the new message event. + + pattern_match (`obj`, optional): + Alias for ``data_match``. Example .. code-block:: python @@ -71,39 +69,17 @@ class CallbackQuery(EventBuilder): Button.inline('Nope', b'no') ]) """ - def __init__( - self, chats=None, *, blacklist_chats=False, func=None, data=None, pattern=None): - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) - - if data and pattern: - raise ValueError("Only pass either data or pattern not both.") - - if isinstance(data, str): - data = data.encode('utf-8') - if isinstance(pattern, str): - pattern = pattern.encode('utf-8') - - match = data if data else pattern - - if isinstance(match, bytes): - self.match = data if data else re.compile(pattern).match - elif not match or callable(match): - self.match = match - elif hasattr(match, 'match') and callable(match.match): - if not isinstance(getattr(match, 'pattern', b''), bytes): - match = re.compile(match.pattern.encode('utf-8'), - match.flags & (~re.UNICODE)) - - self.match = match.match - else: - raise TypeError('Invalid data or pattern type given') - - self._no_check = all(x is None for x in ( - self.chats, self.func, self.match, - )) + def __init__(self, query, peer, msg_id): + _custom.chatgetter.ChatGetter.__init__(self, peer) + _custom.sendergetter.SenderGetter.__init__(self, query.user_id) + self.query = query + self.data_match = None + self.pattern_match = None + self._message = None + self._answered = False @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateBotCallbackQuery): return cls.Event(update, update.peer, update.msg_id) elif isinstance(update, _tl.UpdateInlineBotCallbackQuery): @@ -113,242 +89,191 @@ class CallbackQuery(EventBuilder): peer = _tl.PeerChannel(-pid) if pid < 0 else _tl.PeerUser(pid) return cls.Event(update, peer, mid) - def filter(self, event): - # We can't call super().filter(...) because it ignores chat_instance - if self._no_check: - return event + def _set_client(self, client): + super()._set_client(client) + self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) - if self.chats is not None: - inside = event.query.chat_instance in self.chats - if event.chat_id: - inside |= event.chat_id in self.chats - - if inside == self.blacklist_chats: - return - - if self.match: - if callable(self.match): - event.data_match = event.pattern_match = self.match(event.query.data) - if not event.data_match: - return - elif event.query.data != self.match: - return - - if self.func: - # Return the result of func directly as it may need to be awaited - return self.func(event) - return True - - class Event(EventCommon, _custom.sendergetter.SenderGetter): + @property + def id(self): """ - Represents the event of a new callback query. - - Members: - query (:tl:`UpdateBotCallbackQuery`): - The original :tl:`UpdateBotCallbackQuery`. - - data_match (`obj`, optional): - The object returned by the ``data=`` parameter - when creating the event builder, if any. Similar - to ``pattern_match`` for the new message event. - - pattern_match (`obj`, optional): - Alias for ``data_match``. + Returns the query ID. The user clicking the inline + button is the one who generated this random ID. """ - def __init__(self, query, peer, msg_id): - super().__init__(peer, msg_id=msg_id) - _custom.sendergetter.SenderGetter.__init__(self, query.user_id) - self.query = query - self.data_match = None - self.pattern_match = None - self._message = None - self._answered = False + return self.query.query_id - def _set_client(self, client): - super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) + @property + def message_id(self): + """ + Returns the message ID to which the clicked inline button belongs. + """ + return self._message_id - @property - def id(self): - """ - Returns the query ID. The user clicking the inline - button is the one who generated this random ID. - """ - return self.query.query_id + @property + def data(self): + """ + Returns the data payload from the original inline button. + """ + return self.query.data - @property - def message_id(self): - """ - Returns the message ID to which the clicked inline button belongs. - """ - return self._message_id - - @property - def data(self): - """ - Returns the data payload from the original inline button. - """ - return self.query.data - - @property - def chat_instance(self): - """ - Unique identifier for the chat where the callback occurred. - Useful for high scores in games. - """ - return self.query.chat_instance - - async def get_message(self): - """ - Returns the message to which the clicked inline button belongs. - """ - if self._message is not None: - return self._message - - try: - chat = await self.get_input_chat() if self.is_channel else None - self._message = await self._client.get_messages( - chat, ids=self._message_id) - except ValueError: - return + @property + def chat_instance(self): + """ + Unique identifier for the chat where the callback occurred. + Useful for high scores in games. + """ + return self.query.chat_instance + async def get_message(self): + """ + Returns the message to which the clicked inline button belongs. + """ + if self._message is not None: return self._message - async def _refetch_sender(self): - self._sender = self._entities.get(self.sender_id) - if not self._sender: - return + try: + chat = await self.get_input_chat() if self.is_channel else None + self._message = await self._client.get_messages( + chat, ids=self._message_id) + except ValueError: + return - self._input_sender = utils.get_input_peer(self._chat) - if not getattr(self._input_sender, 'access_hash', True): - # getattr with True to handle the InputPeerSelf() case - m = await self.get_message() - if m: - self._sender = m._sender - self._input_sender = m._input_sender + return self._message - async def answer( - self, message=None, cache_time=0, *, url=None, alert=False): - """ - Answers the callback query (and stops the loading circle). + async def _refetch_sender(self): + self._sender = self._entities.get(self.sender_id) + if not self._sender: + return - Args: - message (`str`, optional): - The toast message to show feedback to the user. + self._input_sender = utils.get_input_peer(self._chat) + if not getattr(self._input_sender, 'access_hash', True): + # getattr with True to handle the InputPeerSelf() case + m = await self.get_message() + if m: + self._sender = m._sender + self._input_sender = m._input_sender - cache_time (`int`, optional): - For how long this result should be cached on - the user's client. Defaults to 0 for no cache. + async def answer( + self, message=None, cache_time=0, *, url=None, alert=False): + """ + Answers the callback query (and stops the loading circle). - url (`str`, optional): - The URL to be opened in the user's client. Note that - the only valid URLs are those of games your bot has, - or alternatively a 't.me/your_bot?start=xyz' parameter. + Args: + message (`str`, optional): + The toast message to show feedback to the user. - alert (`bool`, optional): - Whether an alert (a pop-up dialog) should be used - instead of showing a toast. Defaults to `False`. - """ - if self._answered: - return + cache_time (`int`, optional): + For how long this result should be cached on + the user's client. Defaults to 0 for no cache. - res = await self._client(_tl.fn.messages.SetBotCallbackAnswer( - query_id=self.query.query_id, - cache_time=cache_time, - alert=alert, - message=message, - url=url, - )) - self._answered = True - return res + url (`str`, optional): + The URL to be opened in the user's client. Note that + the only valid URLs are those of games your bot has, + or alternatively a 't.me/your_bot?start=xyz' parameter. - @property - def via_inline(self): - """ - Whether this callback was generated from an inline button sent - via an inline query or not. If the bot sent the message itself - with buttons, and one of those is clicked, this will be `False`. - If a user sent the message coming from an inline query to the - bot, and one of those is clicked, this will be `True`. + alert (`bool`, optional): + Whether an alert (a pop-up dialog) should be used + instead of showing a toast. Defaults to `False`. + """ + if self._answered: + return - If it's `True`, it's likely that the bot is **not** in the - chat, so methods like `respond` or `delete` won't work (but - `edit` will always work). - """ - return isinstance(self.query, _tl.UpdateInlineBotCallbackQuery) + res = await self._client(_tl.fn.messages.SetBotCallbackAnswer( + query_id=self.query.query_id, + cache_time=cache_time, + alert=alert, + message=message, + url=url, + )) + self._answered = True + return res - @auto_answer - async def respond(self, *args, **kwargs): - """ - Responds to the message (not as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` with - ``entity`` already set. + @property + def via_inline(self): + """ + Whether this callback was generated from an inline button sent + via an inline query or not. If the bot sent the message itself + with buttons, and one of those is clicked, this will be `False`. + If a user sent the message coming from an inline query to the + bot, and one of those is clicked, this will be `True`. - This method will also `answer` the callback if necessary. + If it's `True`, it's likely that the bot is **not** in the + chat, so methods like `respond` or `delete` won't work (but + `edit` will always work). + """ + return isinstance(self.query, _tl.UpdateInlineBotCallbackQuery) - This method will likely fail if `via_inline` is `True`. - """ - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) + @auto_answer + async def respond(self, *args, **kwargs): + """ + Responds to the message (not as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` with + ``entity`` already set. - @auto_answer - async def reply(self, *args, **kwargs): - """ - Replies to the message (as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` with - both ``entity`` and ``reply_to`` already set. + This method will also `answer` the callback if necessary. - This method will also `answer` the callback if necessary. + This method will likely fail if `via_inline` is `True`. + """ + return await self._client.send_message( + await self.get_input_chat(), *args, **kwargs) - This method will likely fail if `via_inline` is `True`. - """ - kwargs['reply_to'] = self.query.msg_id - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) + @auto_answer + async def reply(self, *args, **kwargs): + """ + Replies to the message (as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` with + both ``entity`` and ``reply_to`` already set. - @auto_answer - async def edit(self, *args, **kwargs): - """ - Edits the message. Shorthand for - `telethon.client.messages.MessageMethods.edit_message` with - the ``entity`` set to the correct :tl:`InputBotInlineMessageID`. + This method will also `answer` the callback if necessary. - Returns `True` if the edit was successful. + This method will likely fail if `via_inline` is `True`. + """ + kwargs['reply_to'] = self.query.msg_id + return await self._client.send_message( + await self.get_input_chat(), *args, **kwargs) - This method will also `answer` the callback if necessary. + @auto_answer + async def edit(self, *args, **kwargs): + """ + Edits the message. Shorthand for + `telethon.client.messages.MessageMethods.edit_message` with + the ``entity`` set to the correct :tl:`InputBotInlineMessageID`. - .. note:: + Returns `True` if the edit was successful. - This method won't respect the previous message unlike - `Message.edit `, - since the message object is normally not present. - """ - if isinstance(self.query.msg_id, _tl.InputBotInlineMessageID): - return await self._client.edit_message( - None, self.query.msg_id, *args, **kwargs - ) - else: - return await self._client.edit_message( - await self.get_input_chat(), self.query.msg_id, - *args, **kwargs - ) + This method will also `answer` the callback if necessary. - @auto_answer - async def delete(self, *args, **kwargs): - """ - Deletes the message. Shorthand for - `telethon.client.messages.MessageMethods.delete_messages` with - ``entity`` and ``message_ids`` already set. + .. note:: - If you need to delete more than one message at once, don't use - this `delete` method. Use a - `telethon.client.telegramclient.TelegramClient` instance directly. - - This method will also `answer` the callback if necessary. - - This method will likely fail if `via_inline` is `True`. - """ - return await self._client.delete_messages( - await self.get_input_chat(), [self.query.msg_id], + This method won't respect the previous message unlike + `Message.edit `, + since the message object is normally not present. + """ + if isinstance(self.query.msg_id, _tl.InputBotInlineMessageID): + return await self._client.edit_message( + None, self.query.msg_id, *args, **kwargs + ) + else: + return await self._client.edit_message( + await self.get_input_chat(), self.query.msg_id, *args, **kwargs ) + + @auto_answer + async def delete(self, *args, **kwargs): + """ + Deletes the message. Shorthand for + `telethon.client.messages.MessageMethods.delete_messages` with + ``entity`` and ``message_ids`` already set. + + If you need to delete more than one message at once, don't use + this `delete` method. Use a + `telethon.client.telegramclient.TelegramClient` instance directly. + + This method will also `answer` the callback if necessary. + + This method will likely fail if `via_inline` is `True`. + """ + return await self._client.delete_messages( + await self.get_input_chat(), [self.query.msg_id], + *args, **kwargs + ) diff --git a/telethon/_events/chataction.py b/telethon/_events/chataction.py index 0bf83aa1..1e9b7271 100644 --- a/telethon/_events/chataction.py +++ b/telethon/_events/chataction.py @@ -1,10 +1,9 @@ -from .common import EventBuilder, EventCommon, name_inner_event +from .base import EventBuilder from .._misc import utils from .. import _tl from ..types import _custom -@name_inner_event class ChatAction(EventBuilder): """ Occurs on certain chat actions: @@ -20,6 +19,47 @@ class ChatAction(EventBuilder): Note that "chat" refers to "small group, megagroup and broadcast channel", whereas "group" refers to "small group and megagroup" only. + Members: + action_message (`MessageAction `_): + The message invoked by this Chat Action. + + new_pin (`bool`): + `True` if there is a new pin. + + new_photo (`bool`): + `True` if there's a new chat photo (or it was removed). + + photo (:tl:`Photo`, optional): + The new photo (or `None` if it was removed). + + user_added (`bool`): + `True` if the user was added by some other. + + user_joined (`bool`): + `True` if the user joined on their own. + + user_left (`bool`): + `True` if the user left on their own. + + user_kicked (`bool`): + `True` if the user was kicked by some other. + + user_approved (`bool`): + `True` if the user's join request was approved. + along with `user_joined` will be also True. + + created (`bool`, optional): + `True` if this chat was just created. + + new_title (`str`, optional): + The new title string for the chat, if applicable. + + new_score (`str`, optional): + The new score string for the game, if applicable. + + unpin (`bool`): + `True` if the existing pin gets unpinned. + Example .. code-block:: python @@ -32,8 +72,64 @@ class ChatAction(EventBuilder): await event.reply('Welcome to the group!') """ + def __init__(self, where, new_photo=None, + added_by=None, kicked_by=None, created=None, from_approval=None, + users=None, new_title=None, pin_ids=None, pin=None, new_score=None): + if isinstance(where, _tl.MessageService): + self.action_message = where + where = where.peer_id + else: + self.action_message = None + + # TODO needs some testing (can there be more than one id, and do they follow pin order?) + # same in get_pinned_message + super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None) + + self.new_pin = pin_ids is not None + self._pin_ids = pin_ids + self._pinned_messages = None + + self.new_photo = new_photo is not None + self.photo = \ + new_photo if isinstance(new_photo, _tl.Photo) else None + + self._added_by = None + self._kicked_by = None + self.user_added = self.user_joined = self.user_left = \ + self.user_kicked = self.unpin = False + + if added_by is True or from_approval is True: + self.user_joined = True + elif added_by: + self.user_added = True + self._added_by = added_by + self.user_approved = from_approval + + # If `from_id` was not present (it's `True`) or the affected + # user was "kicked by itself", then it left. Else it was kicked. + if kicked_by is True or (users is not None and kicked_by == users): + self.user_left = True + elif kicked_by: + self.user_kicked = True + self._kicked_by = kicked_by + + self.created = bool(created) + + if isinstance(users, list): + self._user_ids = [utils.get_peer_id(u) for u in users] + elif users: + self._user_ids = [utils.get_peer_id(users)] + else: + self._user_ids = [] + + self._users = None + self._input_users = None + self.new_title = new_title + self.new_score = new_score + self.unpin = not pin + @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): # Rely on specific pin updates for unpins, but otherwise ignore them # for new pins (we'd rather handle the new service message with pin, # so that we can act on that message'). @@ -114,332 +210,230 @@ class ChatAction(EventBuilder): return cls.Event(msg, new_score=action.score) - class Event(EventCommon): + async def respond(self, *args, **kwargs): """ - Represents the event of a new chat action. - - Members: - action_message (`MessageAction `_): - The message invoked by this Chat Action. - - new_pin (`bool`): - `True` if there is a new pin. - - new_photo (`bool`): - `True` if there's a new chat photo (or it was removed). - - photo (:tl:`Photo`, optional): - The new photo (or `None` if it was removed). - - user_added (`bool`): - `True` if the user was added by some other. - - user_joined (`bool`): - `True` if the user joined on their own. - - user_left (`bool`): - `True` if the user left on their own. - - user_kicked (`bool`): - `True` if the user was kicked by some other. - - user_approved (`bool`): - `True` if the user's join request was approved. - along with `user_joined` will be also True. - - created (`bool`, optional): - `True` if this chat was just created. - - new_title (`str`, optional): - The new title string for the chat, if applicable. - - new_score (`str`, optional): - The new score string for the game, if applicable. - - unpin (`bool`): - `True` if the existing pin gets unpinned. + Responds to the chat action message (not as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` with + ``entity`` already set. """ + return await self._client.send_message( + await self.get_input_chat(), *args, **kwargs) - def __init__(self, where, new_photo=None, - added_by=None, kicked_by=None, created=None, from_approval=None, - users=None, new_title=None, pin_ids=None, pin=None, new_score=None): - if isinstance(where, _tl.MessageService): - self.action_message = where - where = where.peer_id - else: - self.action_message = None + async def reply(self, *args, **kwargs): + """ + Replies to the chat action message (as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` with + both ``entity`` and ``reply_to`` already set. - # TODO needs some testing (can there be more than one id, and do they follow pin order?) - # same in get_pinned_message - super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None) + Has the same effect as `respond` if there is no message. + """ + if not self.action_message: + return await self.respond(*args, **kwargs) - self.new_pin = pin_ids is not None - self._pin_ids = pin_ids - self._pinned_messages = None + kwargs['reply_to'] = self.action_message.id + return await self._client.send_message( + await self.get_input_chat(), *args, **kwargs) - self.new_photo = new_photo is not None - self.photo = \ - new_photo if isinstance(new_photo, _tl.Photo) else None + async def delete(self, *args, **kwargs): + """ + Deletes the chat action message. 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. - self._added_by = None - self._kicked_by = None - self.user_added = self.user_joined = self.user_left = \ - self.user_kicked = self.unpin = False + Does nothing if no message action triggered this event. + """ + if not self.action_message: + return - if added_by is True or from_approval is True: - self.user_joined = True - elif added_by: - self.user_added = True - self._added_by = added_by - self.user_approved = from_approval + return await self._client.delete_messages( + await self.get_input_chat(), [self.action_message], + *args, **kwargs + ) - # If `from_id` was not present (it's `True`) or the affected - # user was "kicked by itself", then it left. Else it was kicked. - if kicked_by is True or (users is not None and kicked_by == users): - self.user_left = True - elif kicked_by: - self.user_kicked = True - self._kicked_by = kicked_by + async def get_pinned_message(self): + """ + If ``new_pin`` is `True`, this returns the `Message + ` object that was pinned. + """ + if self._pinned_messages is None: + await self.get_pinned_messages() - self.created = bool(created) + if self._pinned_messages: + return self._pinned_messages[0] - if isinstance(users, list): - self._user_ids = [utils.get_peer_id(u) for u in users] - elif users: - self._user_ids = [utils.get_peer_id(users)] - else: - self._user_ids = [] + async def get_pinned_messages(self): + """ + If ``new_pin`` is `True`, this returns a `list` of `Message + ` objects that were pinned. + """ + if not self._pin_ids: + return self._pin_ids # either None or empty list - self._users = None - self._input_users = None - self.new_title = new_title - self.new_score = new_score - self.unpin = not pin + chat = await self.get_input_chat() + if chat: + self._pinned_messages = await self._client.get_messages( + self._input_chat, ids=self._pin_ids) - async def respond(self, *args, **kwargs): - """ - Responds to the chat action message (not as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` with - ``entity`` already set. - """ - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) + return self._pinned_messages - async def reply(self, *args, **kwargs): - """ - Replies to the chat action message (as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` with - both ``entity`` and ``reply_to`` already set. + @property + def added_by(self): + """ + The user who added ``users``, if applicable (`None` otherwise). + """ + if self._added_by and not isinstance(self._added_by, _tl.User): + aby = self._entities.get(utils.get_peer_id(self._added_by)) + if aby: + self._added_by = aby - Has the same effect as `respond` if there is no message. - """ - if not self.action_message: - return await self.respond(*args, **kwargs) + return self._added_by - kwargs['reply_to'] = self.action_message.id - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) + async def get_added_by(self): + """ + Returns `added_by` but will make an API call if necessary. + """ + if not self.added_by and self._added_by: + self._added_by = await self._client.get_entity(self._added_by) - async def delete(self, *args, **kwargs): - """ - Deletes the chat action message. 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. + return self._added_by - Does nothing if no message action triggered this event. - """ - if not self.action_message: - return + @property + def kicked_by(self): + """ + The user who kicked ``users``, if applicable (`None` otherwise). + """ + if self._kicked_by and not isinstance(self._kicked_by, _tl.User): + kby = self._entities.get(utils.get_peer_id(self._kicked_by)) + if kby: + self._kicked_by = kby - return await self._client.delete_messages( - await self.get_input_chat(), [self.action_message], - *args, **kwargs - ) + return self._kicked_by - async def get_pinned_message(self): - """ - If ``new_pin`` is `True`, this returns the `Message - ` object that was pinned. - """ - if self._pinned_messages is None: - await self.get_pinned_messages() + async def get_kicked_by(self): + """ + Returns `kicked_by` but will make an API call if necessary. + """ + if not self.kicked_by and self._kicked_by: + self._kicked_by = await self._client.get_entity(self._kicked_by) - if self._pinned_messages: - return self._pinned_messages[0] + return self._kicked_by - async def get_pinned_messages(self): - """ - If ``new_pin`` is `True`, this returns a `list` of `Message - ` objects that were pinned. - """ - if not self._pin_ids: - return self._pin_ids # either None or empty list + @property + def user(self): + """ + The first user that takes part in this action. For example, who joined. - chat = await self.get_input_chat() - if chat: - self._pinned_messages = await self._client.get_messages( - self._input_chat, ids=self._pin_ids) + Might be `None` if the information can't be retrieved or + there is no user taking part. + """ + if self.users: + return self._users[0] - return self._pinned_messages + async def get_user(self): + """ + Returns `user` but will make an API call if necessary. + """ + if self.users or await self.get_users(): + return self._users[0] - @property - def added_by(self): - """ - The user who added ``users``, if applicable (`None` otherwise). - """ - if self._added_by and not isinstance(self._added_by, _tl.User): - aby = self._entities.get(utils.get_peer_id(self._added_by)) - if aby: - self._added_by = aby + @property + def input_user(self): + """ + Input version of the ``self.user`` property. + """ + if self.input_users: + return self._input_users[0] - return self._added_by + async def get_input_user(self): + """ + Returns `input_user` but will make an API call if necessary. + """ + if self.input_users or await self.get_input_users(): + return self._input_users[0] - async def get_added_by(self): - """ - Returns `added_by` but will make an API call if necessary. - """ - if not self.added_by and self._added_by: - self._added_by = await self._client.get_entity(self._added_by) + @property + def user_id(self): + """ + Returns the marked signed ID of the first user, if any. + """ + if self._user_ids: + return self._user_ids[0] - return self._added_by + @property + def users(self): + """ + A list of users that take part in this action. For example, who joined. - @property - def kicked_by(self): - """ - The user who kicked ``users``, if applicable (`None` otherwise). - """ - if self._kicked_by and not isinstance(self._kicked_by, _tl.User): - kby = self._entities.get(utils.get_peer_id(self._kicked_by)) - if kby: - self._kicked_by = kby + Might be empty if the information can't be retrieved or there + are no users taking part. + """ + if not self._user_ids: + return [] - return self._kicked_by + if self._users is None: + self._users = [ + self._entities[user_id] + for user_id in self._user_ids + if user_id in self._entities + ] - async def get_kicked_by(self): - """ - Returns `kicked_by` but will make an API call if necessary. - """ - if not self.kicked_by and self._kicked_by: - self._kicked_by = await self._client.get_entity(self._kicked_by) + return self._users - return self._kicked_by + async def get_users(self): + """ + Returns `users` but will make an API call if necessary. + """ + if not self._user_ids: + return [] - @property - def user(self): - """ - The first user that takes part in this action. For example, who joined. + # Note: we access the property first so that it fills if needed + if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message: + await self.action_message._reload_message() + self._users = [ + u for u in self.action_message.action_entities + if isinstance(u, (_tl.User, _tl.UserEmpty))] - Might be `None` if the information can't be retrieved or - there is no user taking part. - """ - if self.users: - return self._users[0] + return self._users - async def get_user(self): - """ - Returns `user` but will make an API call if necessary. - """ - if self.users or await self.get_users(): - return self._users[0] + @property + def input_users(self): + """ + Input version of the ``self.users`` property. + """ + if self._input_users is None and self._user_ids: + self._input_users = [] + for user_id in self._user_ids: + # Try to get it from our entities + try: + self._input_users.append(utils.get_input_peer(self._entities[user_id])) + continue + except (KeyError, TypeError): + pass - @property - def input_user(self): - """ - Input version of the ``self.user`` property. - """ - if self.input_users: - return self._input_users[0] + return self._input_users or [] - async def get_input_user(self): - """ - Returns `input_user` but will make an API call if necessary. - """ - if self.input_users or await self.get_input_users(): - return self._input_users[0] + async def get_input_users(self): + """ + Returns `input_users` but will make an API call if necessary. + """ + if not self._user_ids: + return [] - @property - def user_id(self): - """ - Returns the marked signed ID of the first user, if any. - """ - if self._user_ids: - return self._user_ids[0] + # Note: we access the property first so that it fills if needed + if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message: + self._input_users = [ + utils.get_input_peer(u) + for u in self.action_message.action_entities + if isinstance(u, (_tl.User, _tl.UserEmpty))] - @property - def users(self): - """ - A list of users that take part in this action. For example, who joined. + return self._input_users or [] - Might be empty if the information can't be retrieved or there - are no users taking part. - """ - if not self._user_ids: - return [] - - if self._users is None: - self._users = [ - self._entities[user_id] - for user_id in self._user_ids - if user_id in self._entities - ] - - return self._users - - async def get_users(self): - """ - Returns `users` but will make an API call if necessary. - """ - if not self._user_ids: - return [] - - # Note: we access the property first so that it fills if needed - if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message: - await self.action_message._reload_message() - self._users = [ - u for u in self.action_message.action_entities - if isinstance(u, (_tl.User, _tl.UserEmpty))] - - return self._users - - @property - def input_users(self): - """ - Input version of the ``self.users`` property. - """ - if self._input_users is None and self._user_ids: - self._input_users = [] - for user_id in self._user_ids: - # Try to get it from our entities - try: - self._input_users.append(utils.get_input_peer(self._entities[user_id])) - continue - except (KeyError, TypeError): - pass - - return self._input_users or [] - - async def get_input_users(self): - """ - Returns `input_users` but will make an API call if necessary. - """ - if not self._user_ids: - return [] - - # Note: we access the property first so that it fills if needed - if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message: - self._input_users = [ - utils.get_input_peer(u) - for u in self.action_message.action_entities - if isinstance(u, (_tl.User, _tl.UserEmpty))] - - return self._input_users or [] - - @property - def user_ids(self): - """ - Returns the marked signed ID of the users, if any. - """ - if self._user_ids: - return self._user_ids[:] + @property + def user_ids(self): + """ + Returns the marked signed ID of the users, if any. + """ + if self._user_ids: + return self._user_ids[:] diff --git a/telethon/_events/common.py b/telethon/_events/common.py deleted file mode 100644 index c20ac64e..00000000 --- a/telethon/_events/common.py +++ /dev/null @@ -1,179 +0,0 @@ -import abc -import asyncio -import warnings - -from .. import _tl -from .._misc import utils, tlobject -from ..types._custom.chatgetter import ChatGetter - - -async 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 utils.is_list_like(chats): - chats = (chats,) - - result = set() - for chat in chats: - if isinstance(chat, int): - result.add(chat) - elif isinstance(chat, tlobject.TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687: - # 0x2d45687 == crc32(b'Peer') - result.add(utils.get_peer_id(chat)) - else: - chat = await client.get_input_entity(chat) - if isinstance(chat, _tl.InputPeerSelf): - chat = _tl.PeerUser(self._session_state.user_id) - result.add(utils.get_peer_id(chat)) - - return result - - -class EventBuilder(abc.ABC): - """ - The common event builder, with builtin support to filter per chat. - - Args: - chats (`entity`, optional): - May be one or more entities (username/peer/etc.), preferably IDs. - By default, only matching chats will be handled. - - blacklist_chats (`bool`, optional): - Whether to treat the chats as a blacklist instead of - as a whitelist (default). This means that every chat - will be handled *except* those specified in ``chats`` - which will be ignored if ``blacklist_chats=True``. - - func (`callable`, optional): - A callable (async or not) function that should accept the event as input - parameter, and return a value indicating whether the event - should be dispatched or not (any truthy value will do, it - does not need to be a `bool`). It works like a custom filter: - - .. code-block:: python - - @client.on(events.NewMessage(func=lambda e: e.is_private)) - async def handler(event): - pass # code here - """ - def __init__(self, chats=None, *, blacklist_chats=False, func=None): - self.chats = chats - self.blacklist_chats = bool(blacklist_chats) - self.resolved = False - self.func = func - self._resolve_lock = None - - @classmethod - @abc.abstractmethod - def build(cls, update, others, self_id, entities, client): - """ - Builds an event for the given update if possible, or returns None. - - `others` are the rest of updates that came in the same container - as the current `update`. - - `self_id` should be the current user's ID, since it is required - for some events which lack this information but still need it. - """ - # TODO So many parameters specific to only some update types seems dirty - - async def resolve(self, client): - """Helper method to allow event builders to be resolved before usage""" - if self.resolved: - return - - if not self._resolve_lock: - self._resolve_lock = asyncio.Lock() - - async with self._resolve_lock: - if not self.resolved: - await self._resolve(client) - self.resolved = True - - async def _resolve(self, client): - self.chats = await _into_id_set(client, self.chats) - - def filter(self, event): - """ - Returns a truthy value if the event passed the filter and should be - used, or falsy otherwise. The return value may need to be awaited. - - The events must have been resolved before this can be called. - """ - if not self.resolved: - return - - if self.chats is not None: - # Note: the `event.chat_id` property checks if it's `None` for us - inside = event.chat_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 - - if not self.func: - return True - - # Return the result of func directly as it may need to be awaited - return self.func(event) - - -class EventCommon(ChatGetter, abc.ABC): - """ - Intermediate class with common things to all events. - - Remember that this class implements `ChatGetter - ` which - means you have access to all chat properties and methods. - - In addition, you can access the `original_update` - field which contains the original :tl:`Update`. - """ - _event_name = 'Event' - - def __init__(self, chat_peer=None, msg_id=None, broadcast=None): - super().__init__(chat_peer, broadcast=broadcast) - self._entities = {} - self._client = None - self._message_id = msg_id - self.original_update = None - - def _set_client(self, client): - """ - Setter so subclasses can act accordingly when the client is set. - """ - # TODO Nuke - self._client = client - if self._chat_peer: - self._chat, self._input_chat = utils._get_entity_pair(self.chat_id, self._entities) - else: - self._chat = self._input_chat = None - - @property - def client(self): - """ - The `telethon.TelegramClient` that created this event. - """ - return self._client - - def __str__(self): - return _tl.TLObject.pretty_format(self.to_dict()) - - def stringify(self): - return _tl.TLObject.pretty_format(self.to_dict(), indent=0) - - def to_dict(self): - d = {k: v for k, v in self.__dict__.items() if k[0] != '_'} - d['_'] = self._event_name - return d - - -def name_inner_event(cls): - """Decorator to rename cls.Event 'Event' as 'cls.Event'""" - if hasattr(cls, 'Event'): - cls.Event._event_name = '{}.Event'.format(cls.__name__) - else: - warnings.warn('Class {} does not have a inner Event'.format(cls)) - return cls diff --git a/telethon/_events/inlinequery.py b/telethon/_events/inlinequery.py index 59ad6baa..c6c3bba6 100644 --- a/telethon/_events/inlinequery.py +++ b/telethon/_events/inlinequery.py @@ -3,34 +3,27 @@ import re import asyncio -from .common import EventBuilder, EventCommon, name_inner_event +from .base import EventBuilder from .._misc import utils from .. import _tl from ..types import _custom -@name_inner_event -class InlineQuery(EventBuilder): +class InlineQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter): """ Occurs whenever you sign in as a bot and a user sends an inline query such as ``@bot query``. - Args: - users (`entity`, optional): - May be one or more entities (username/peer/etc.), preferably IDs. - By default, only inline queries from these users will be handled. + Members: + query (:tl:`UpdateBotInlineQuery`): + The original :tl:`UpdateBotInlineQuery`. - blacklist_users (`bool`, optional): - Whether to treat the users as a blacklist instead of - as a whitelist (default). This means that every chat - will be handled *except* those specified in ``users`` - which will be ignored if ``blacklist_users=True``. + Make sure to access the `text` property of the query if + you want the text rather than the actual query object. - pattern (`str`, `callable`, `Pattern`, optional): - If set, only queries 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. + pattern_match (`obj`, optional): + The resulting object from calling the passed ``pattern`` + function, which is ``re.compile(...).match`` by default. Example .. code-block:: python @@ -47,200 +40,163 @@ class InlineQuery(EventBuilder): builder.article('lowercase', text=event.text.lower()), ]) """ - def __init__( - self, users=None, *, blacklist_users=False, func=None, pattern=None): - super().__init__(users, blacklist_chats=blacklist_users, func=func) - - 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 __init__(self, query): + _custom.chatgetter.ChatGetter.__init__(self, _tl.PeerUser(query.user_id)) + _custom.sendergetter.SenderGetter.__init__(self, query.user_id) + self.query = query + self.pattern_match = None + self._answered = False @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateBotInlineQuery): return cls.Event(update) - def filter(self, event): - if self.pattern: - match = self.pattern(event.text) - if not match: - return - event.pattern_match = match + def _set_client(self, client): + super()._set_client(client) + self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) - return super().filter(event) - - class Event(EventCommon, _custom.sendergetter.SenderGetter): + @property + def id(self): """ - Represents the event of a new callback query. - - Members: - query (:tl:`UpdateBotInlineQuery`): - The original :tl:`UpdateBotInlineQuery`. - - Make sure to access the `text` property of the query if - you want the text rather than the actual query object. - - pattern_match (`obj`, optional): - The resulting object from calling the passed ``pattern`` - function, which is ``re.compile(...).match`` by default. + Returns the unique identifier for the query ID. """ - def __init__(self, query): - super().__init__(chat_peer=_tl.PeerUser(query.user_id)) - _custom.sendergetter.SenderGetter.__init__(self, query.user_id) - self.query = query - self.pattern_match = None - self._answered = False + return self.query.query_id - def _set_client(self, client): - super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) + @property + def text(self): + """ + Returns the text the user used to make the inline query. + """ + return self.query.query - @property - def id(self): - """ - Returns the unique identifier for the query ID. - """ - return self.query.query_id + @property + def offset(self): + """ + The string the user's client used as an offset for the query. + This will either be empty or equal to offsets passed to `answer`. + """ + return self.query.offset - @property - def text(self): - """ - Returns the text the user used to make the inline query. - """ - return self.query.query + @property + def geo(self): + """ + If the user location is requested when using inline mode + and the user's device is able to send it, this will return + the :tl:`GeoPoint` with the position of the user. + """ + return self.query.geo - @property - def offset(self): - """ - The string the user's client used as an offset for the query. - This will either be empty or equal to offsets passed to `answer`. - """ - return self.query.offset + @property + def builder(self): + """ + Returns a new `InlineBuilder + ` instance. - @property - def geo(self): - """ - If the user location is requested when using inline mode - and the user's device is able to send it, this will return - the :tl:`GeoPoint` with the position of the user. - """ - return self.query.geo + See the documentation for `builder` to know what kind of answers + can be given. + """ + return _custom.InlineBuilder(self._client) - @property - def builder(self): - """ - Returns a new `InlineBuilder - ` instance. - """ - return _custom.InlineBuilder(self._client) + async def answer( + self, results=None, cache_time=0, *, + gallery=False, next_offset=None, private=False, + switch_pm=None, switch_pm_param=''): + """ + Answers the inline query with the given results. - async def answer( - self, results=None, cache_time=0, *, - gallery=False, next_offset=None, private=False, - switch_pm=None, switch_pm_param=''): - """ - Answers the inline query with the given results. - - See the documentation for `builder` to know what kind of answers - can be given. - - Args: - results (`list`, optional): - A list of :tl:`InputBotInlineResult` to use. - You should use `builder` to create these: - - .. code-block:: python - - builder = inline.builder - r1 = builder.article('Be nice', text='Have a nice day') - r2 = builder.article('Be bad', text="I don't like you") - await inline.answer([r1, r2]) - - You can send up to 50 results as documented in - https://core.telegram.org/bots/api#answerinlinequery. - Sending more will raise ``ResultsTooMuchError``, - and you should consider using `next_offset` to - paginate them. - - cache_time (`int`, optional): - For how long this result should be cached on - the user's client. Defaults to 0 for no cache. - - gallery (`bool`, optional): - Whether the results should show as a gallery (grid) or not. - - next_offset (`str`, optional): - The offset the client will send when the user scrolls the - results and it repeats the request. - - private (`bool`, optional): - Whether the results should be cached by Telegram - (not private) or by the user's client (private). - - switch_pm (`str`, optional): - If set, this text will be shown in the results - to allow the user to switch to private messages. - - switch_pm_param (`str`, optional): - Optional parameter to start the bot with if - `switch_pm` was used. - - Example: + Args: + results (`list`, optional): + A list of :tl:`InputBotInlineResult` to use. + You should use `builder` to create these: .. code-block:: python - @bot.on(events.InlineQuery) - async def handler(event): - builder = event.builder + builder = inline.builder + r1 = builder.article('Be nice', text='Have a nice day') + r2 = builder.article('Be bad', text="I don't like you") + await inline.answer([r1, r2]) - rev_text = event.text[::-1] - await event.answer([ - builder.article('Reverse text', text=rev_text), - builder.photo('/path/to/photo.jpg') - ]) - """ - if self._answered: - return + You can send up to 50 results as documented in + https://core.telegram.org/bots/api#answerinlinequery. + Sending more will raise ``ResultsTooMuchError``, + and you should consider using `next_offset` to + paginate them. - if results: - futures = [self._as_future(x) for x in results] + cache_time (`int`, optional): + For how long this result should be cached on + the user's client. Defaults to 0 for no cache. - await asyncio.wait(futures) + gallery (`bool`, optional): + Whether the results should show as a gallery (grid) or not. - # All futures will be in the `done` *set* that `wait` returns. - # - # Precisely because it's a `set` and not a `list`, it - # will not preserve the order, but since all futures - # completed we can use our original, ordered `list`. - results = [x.result() for x in futures] - else: - results = [] + next_offset (`str`, optional): + The offset the client will send when the user scrolls the + results and it repeats the request. - if switch_pm: - switch_pm = _tl.InlineBotSwitchPM(switch_pm, switch_pm_param) + private (`bool`, optional): + Whether the results should be cached by Telegram + (not private) or by the user's client (private). - return await self._client( - _tl.fn.messages.SetInlineBotResults( - query_id=self.query.query_id, - results=results, - cache_time=cache_time, - gallery=gallery, - next_offset=next_offset, - private=private, - switch_pm=switch_pm - ) + switch_pm (`str`, optional): + If set, this text will be shown in the results + to allow the user to switch to private messages. + + switch_pm_param (`str`, optional): + Optional parameter to start the bot with if + `switch_pm` was used. + + Example: + + .. code-block:: python + + @bot.on(events.InlineQuery) + async def handler(event): + builder = event.builder + + rev_text = event.text[::-1] + await event.answer([ + builder.article('Reverse text', text=rev_text), + builder.photo('/path/to/photo.jpg') + ]) + """ + if self._answered: + return + + if results: + futures = [self._as_future(x) for x in results] + + await asyncio.wait(futures) + + # All futures will be in the `done` *set* that `wait` returns. + # + # Precisely because it's a `set` and not a `list`, it + # will not preserve the order, but since all futures + # completed we can use our original, ordered `list`. + results = [x.result() for x in futures] + else: + results = [] + + if switch_pm: + switch_pm = _tl.InlineBotSwitchPM(switch_pm, switch_pm_param) + + return await self._client( + _tl.fn.messages.SetInlineBotResults( + query_id=self.query.query_id, + results=results, + cache_time=cache_time, + gallery=gallery, + next_offset=next_offset, + private=private, + switch_pm=switch_pm ) + ) - @staticmethod - def _as_future(obj): - if inspect.isawaitable(obj): - return asyncio.ensure_future(obj) + @staticmethod + def _as_future(obj): + if inspect.isawaitable(obj): + return asyncio.ensure_future(obj) - f = asyncio.get_running_loop().create_future() - f.set_result(obj) - return f + f = asyncio.get_running_loop().create_future() + f.set_result(obj) + return f diff --git a/telethon/_events/messagedeleted.py b/telethon/_events/messagedeleted.py index 58f9ff5f..6e7603b3 100644 --- a/telethon/_events/messagedeleted.py +++ b/telethon/_events/messagedeleted.py @@ -1,9 +1,9 @@ -from .common import EventBuilder, EventCommon, name_inner_event +from .base import EventBuilder from .. import _tl +from ..types import _custom -@name_inner_event -class MessageDeleted(EventBuilder): +class MessageDeleted(EventBuilder, _custom.chatgetter.ChatGetter): """ Occurs whenever a message is deleted. Note that this event isn't 100% reliable, since Telegram doesn't always notify the clients that a message @@ -35,8 +35,13 @@ class MessageDeleted(EventBuilder): for msg_id in event.deleted_ids: print('Message', msg_id, 'was deleted in', event.chat_id) """ + def __init__(self, deleted_ids, peer): + _custom.chatgetter.ChatGetter.__init__(self, chat_peer=peer) + self.deleted_id = None if not deleted_ids else deleted_ids[0] + self.deleted_ids = deleted_ids + @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateDeleteMessages): return cls.Event( deleted_ids=update.messages, @@ -47,11 +52,3 @@ class MessageDeleted(EventBuilder): deleted_ids=update.messages, peer=_tl.PeerChannel(update.channel_id) ) - - class Event(EventCommon): - def __init__(self, deleted_ids, peer): - super().__init__( - chat_peer=peer, msg_id=(deleted_ids or [0])[0] - ) - self.deleted_id = None if not deleted_ids else deleted_ids[0] - self.deleted_ids = deleted_ids diff --git a/telethon/_events/messageedited.py b/telethon/_events/messageedited.py index 3f430a68..38c512ec 100644 --- a/telethon/_events/messageedited.py +++ b/telethon/_events/messageedited.py @@ -1,10 +1,8 @@ -from .common import name_inner_event -from .newmessage import NewMessage +from .base import EventBuilder from .. import _tl -@name_inner_event -class MessageEdited(NewMessage): +class MessageEdited(EventBuilder): """ Occurs whenever a message is edited. Just like `NewMessage `, you should treat @@ -43,10 +41,7 @@ class MessageEdited(NewMessage): print('Message', event.id, 'changed at', event.date) """ @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, (_tl.UpdateEditMessage, _tl.UpdateEditChannelMessage)): return cls.Event(update.message) - - class Event(NewMessage.Event): - pass # Required if we want a different name for it diff --git a/telethon/_events/messageread.py b/telethon/_events/messageread.py index 0cd50de0..08f145dc 100644 --- a/telethon/_events/messageread.py +++ b/telethon/_events/messageread.py @@ -1,18 +1,24 @@ -from .common import EventBuilder, EventCommon, name_inner_event +from .base import EventBuilder from .._misc import utils from .. import _tl -@name_inner_event class MessageRead(EventBuilder): """ Occurs whenever one or more messages are read in a chat. - Args: - inbox (`bool`, optional): - If this argument is `True`, then when you read someone else's - messages the event will be fired. By default (`False`) only - when messages you sent are read by someone else will fire it. + Members: + max_id (`int`): + Up to which message ID has been read. Every message + with an ID equal or lower to it have been read. + + outbox (`bool`): + `True` if someone else has read your messages. + + contents (`bool`): + `True` if what was read were the contents of a message. + This will be the case when e.g. you play a voice note. + It may only be set on ``inbox`` events. Example .. code-block:: python @@ -29,13 +35,17 @@ class MessageRead(EventBuilder): # Log when you read message in a chat (from your "inbox") print('You have read messages until', event.max_id) """ - def __init__( - self, chats=None, *, blacklist_chats=False, func=None, inbox=False): - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) - self.inbox = inbox + def __init__(self, peer=None, max_id=None, out=False, contents=False, + message_ids=None): + self.outbox = out + self.contents = contents + self._message_ids = message_ids or [] + self._messages = None + self.max_id = max_id or max(message_ids or [], default=None) + super().__init__(peer, self.max_id) @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateReadHistoryInbox): return cls.Event(update.peer, update.max_id, False) elif isinstance(update, _tl.UpdateReadHistoryOutbox): @@ -54,90 +64,58 @@ class MessageRead(EventBuilder): message_ids=update.messages, contents=True) - def filter(self, event): - if self.inbox == event.outbox: - return - - return super().filter(event) - - class Event(EventCommon): + @property + def inbox(self): """ - Represents the event of one or more messages being read. - - Members: - max_id (`int`): - Up to which message ID has been read. Every message - with an ID equal or lower to it have been read. - - outbox (`bool`): - `True` if someone else has read your messages. - - contents (`bool`): - `True` if what was read were the contents of a message. - This will be the case when e.g. you play a voice note. - It may only be set on ``inbox`` events. + `True` if you have read someone else's messages. """ - def __init__(self, peer=None, max_id=None, out=False, contents=False, - message_ids=None): - self.outbox = out - self.contents = contents - self._message_ids = message_ids or [] - self._messages = None - self.max_id = max_id or max(message_ids or [], default=None) - super().__init__(peer, self.max_id) + return not self.outbox - @property - def inbox(self): - """ - `True` if you have read someone else's messages. - """ - return not self.outbox + @property + def message_ids(self): + """ + The IDs of the messages **which contents'** were read. - @property - def message_ids(self): - """ - The IDs of the messages **which contents'** were read. + Use :meth:`is_read` if you need to check whether a message + was read instead checking if it's in here. + """ + return self._message_ids - Use :meth:`is_read` if you need to check whether a message - was read instead checking if it's in here. - """ - return self._message_ids + async def get_messages(self): + """ + Returns the list of `Message ` + **which contents'** were read. - async def get_messages(self): - """ - Returns the list of `Message ` - **which contents'** were read. - - Use :meth:`is_read` if you need to check whether a message - was read instead checking if it's in here. - """ - if self._messages is None: - chat = await self.get_input_chat() - if not chat: - self._messages = [] - else: - self._messages = await self._client.get_messages( - chat, ids=self._message_ids) - - return self._messages - - def is_read(self, message): - """ - Returns `True` if the given message (or its ID) has been read. - - If a list-like argument is provided, this method will return a - list of booleans indicating which messages have been read. - """ - if utils.is_list_like(message): - return [(m if isinstance(m, int) else m.id) <= self.max_id - for m in message] + Use :meth:`is_read` if you need to check whether a message + was read instead checking if it's in here. + """ + if self._messages is None: + chat = await self.get_input_chat() + if not chat: + self._messages = [] else: - return (message if isinstance(message, int) - else message.id) <= self.max_id + self._messages = await self._client.get_messages( + chat, ids=self._message_ids) - def __contains__(self, message): - """`True` if the message(s) are read message.""" - if utils.is_list_like(message): - return all(self.is_read(message)) - else: - return self.is_read(message) + return self._messages + + def is_read(self, message): + """ + Returns `True` if the given message (or its ID) has been read. + + If a list-like argument is provided, this method will return a + list of booleans indicating which messages have been read. + """ + if utils.is_list_like(message): + return [(m if isinstance(m, int) else m.id) <= self.max_id + for m in message] + else: + return (message if isinstance(message, int) + else message.id) <= self.max_id + + def __contains__(self, message): + """`True` if the message(s) are read message.""" + if utils.is_list_like(message): + return all(self.is_read(message)) + else: + return self.is_read(message) diff --git a/telethon/_events/newmessage.py b/telethon/_events/newmessage.py index e4887002..e42aba7b 100644 --- a/telethon/_events/newmessage.py +++ b/telethon/_events/newmessage.py @@ -1,43 +1,43 @@ import re -from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set +from .base import EventBuilder from .._misc import utils from .. import _tl from ..types import _custom -@name_inner_event -class NewMessage(EventBuilder): +class NewMessageEvent(EventBuilder, Message): """ - Occurs whenever a new text message or a message with media arrives. + Represents the event of a new message. This event can be treated + to all effects as a `Message `, + so please **refer to its documentation** to know what you can do + with this event. - Args: - incoming (`bool`, optional): - If set to `True`, only **incoming** messages will be handled. - Mutually exclusive with ``outgoing`` (can only set one of either). + Members: + message (`Message `): + This is the only difference with the received + `Message `, and will + return the `telethon.tl.custom.message.Message` itself, + not the text. - outgoing (`bool`, optional): - If set to `True`, only **outgoing** messages will be handled. - Mutually exclusive with ``incoming`` (can only set one of either). + See `Message ` for + the rest of available members and methods. - from_users (`entity`, optional): - Unlike `chats`, this parameter filters the *senders* of the - message. That is, only messages *sent by these users* will be - handled. Use `chats` if you want private messages with this/these - users. `from_users` lets you filter by messages sent by *one or - more* users across the desired chats (doesn't need a list). + pattern_match (`obj`): + The resulting object from calling the passed ``pattern`` function. + Here's an example using a string (defaults to regex match): - forwards (`bool`, optional): - Whether forwarded messages should be handled or not. By default, - both forwarded and normal messages are included. If it's `True` - *only* forwards will be handled. If it's `False` only messages - that are *not* forwards will be handled. - - pattern (`str`, `callable`, `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. + >>> from telethon import TelegramClient, events + >>> client = TelegramClient(...) + >>> + >>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!')) + ... async def handler(event): + ... # In this case, the result is a ``Match`` object + ... # since the `str` pattern was converted into + ... # the ``re.compile(pattern).match`` function. + ... print('Welcomed', event.pattern_match.group(1)) + ... + >>> Example .. code-block:: python @@ -57,45 +57,16 @@ class NewMessage(EventBuilder): await asyncio.sleep(5) await client.delete_messages(event.chat_id, [event.id, m.id]) """ - def __init__(self, chats=None, *, blacklist_chats=False, func=None, - incoming=None, outgoing=None, - from_users=None, forwards=None, pattern=None): - if incoming and outgoing: - incoming = outgoing = None # Same as no filter - elif incoming is not None and outgoing is None: - outgoing = not incoming - elif outgoing is not None and incoming is None: - incoming = not outgoing - elif all(x is not None and not x for x in (incoming, outgoing)): - raise ValueError("Don't create an event handler if you " - "don't want neither incoming nor outgoing!") + def __init__(self, message): + self.__dict__['_init'] = False + super().__init__(chat_peer=message.peer_id, + msg_id=message.id, broadcast=bool(message.post)) - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) - self.incoming = incoming - self.outgoing = outgoing - self.from_users = from_users - self.forwards = forwards - 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') - - # Should we short-circuit? E.g. perform no check at all - self._no_check = all(x is None for x in ( - self.chats, self.incoming, self.outgoing, self.pattern, - self.from_users, self.forwards, self.from_users, self.func - )) - - async def _resolve(self, client): - await super()._resolve(client) - self.from_users = await _into_id_set(client, self.from_users) + self.pattern_match = None + self.message = message @classmethod - def build(cls, update, others, self_id, entities, client): + def _build(cls, update, others, self_id, entities, client): if isinstance(update, (_tl.UpdateNewMessage, _tl.UpdateNewChannelMessage)): if not isinstance(update.message, _tl.Message): @@ -139,85 +110,3 @@ class NewMessage(EventBuilder): return return cls.Event(_custom.Message._new(client, msg, entities, None)) - - def filter(self, event): - if self._no_check: - return event - - if self.incoming and event.message.out: - return - if self.outgoing and not event.message.out: - return - if self.forwards is not None: - if bool(self.forwards) != bool(event.message.fwd_from): - return - - if self.from_users is not None: - if event.message.sender_id not in self.from_users: - return - - if self.pattern: - match = self.pattern(event.message.message or '') - if not match: - return - event.pattern_match = match - - return super().filter(event) - - class Event(EventCommon): - """ - Represents the event of a new message. This event can be treated - to all effects as a `Message `, - so please **refer to its documentation** to know what you can do - with this event. - - Members: - message (`Message `): - This is the only difference with the received - `Message `, and will - return the `telethon.tl.custom.message.Message` itself, - not the text. - - See `Message ` for - the rest of available members and methods. - - pattern_match (`obj`): - The resulting object from calling the passed ``pattern`` function. - Here's an example using a string (defaults to regex match): - - >>> from telethon import TelegramClient, events - >>> client = TelegramClient(...) - >>> - >>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!')) - ... async def handler(event): - ... # In this case, the result is a ``Match`` object - ... # since the `str` pattern was converted into - ... # the ``re.compile(pattern).match`` function. - ... print('Welcomed', event.pattern_match.group(1)) - ... - >>> - """ - def __init__(self, message): - self.__dict__['_init'] = False - super().__init__(chat_peer=message.peer_id, - msg_id=message.id, broadcast=bool(message.post)) - - self.pattern_match = None - self.message = message - - def _set_client(self, client): - super()._set_client(client) - m = self.message - self.__dict__['_init'] = True # No new attributes can be set - - def __getattr__(self, item): - if item in self.__dict__: - return self.__dict__[item] - else: - return getattr(self.message, item) - - def __setattr__(self, name, value): - if not self.__dict__['_init'] or name in self.__dict__: - self.__dict__[name] = value - else: - setattr(self.message, name, value) diff --git a/telethon/_events/raw.py b/telethon/_events/raw.py index 496f39e5..75b32de6 100644 --- a/telethon/_events/raw.py +++ b/telethon/_events/raw.py @@ -1,4 +1,4 @@ -from .common import EventBuilder +from .base import EventBuilder from .._misc import utils @@ -8,11 +8,6 @@ class Raw(EventBuilder): :tl:`Update` object that Telegram sends. You normally shouldn't need these. - Args: - types (`list` | `tuple` | `type`, optional): - The type or types that the :tl:`Update` instance must be. - Equivalent to ``if not isinstance(update, types): return``. - Example .. code-block:: python @@ -23,31 +18,6 @@ class Raw(EventBuilder): # Print all incoming updates print(update.stringify()) """ - def __init__(self, types=None, *, func=None): - super().__init__(func=func) - if not types: - self.types = None - elif not utils.is_list_like(types): - if not isinstance(types, type): - raise TypeError('Invalid input type given: {}'.format(types)) - - self.types = types - else: - if not all(isinstance(x, type) for x in types): - raise TypeError('Invalid input types given: {}'.format(types)) - - self.types = tuple(types) - - async def resolve(self, client): - self.resolved = True - @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): return update - - def filter(self, event): - if not self.types or isinstance(event, self.types): - if self.func: - # Return the result of func directly as it may need to be awaited - return self.func(event) - return event diff --git a/telethon/_events/userupdate.py b/telethon/_events/userupdate.py index 35e8044c..e5c938e5 100644 --- a/telethon/_events/userupdate.py +++ b/telethon/_events/userupdate.py @@ -1,7 +1,7 @@ import datetime import functools -from .common import EventBuilder, EventCommon, name_inner_event +from .base import EventBuilder from .._misc import utils from .. import _tl from ..types import _custom @@ -32,11 +32,25 @@ def _requires_status(function): return wrapped -@name_inner_event -class UserUpdate(EventBuilder): +class UserUpdateEvent(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter): """ Occurs whenever a user goes online, starts typing, etc. + Members: + status (:tl:`UserStatus`, optional): + The user status if the update is about going online or offline. + + You should check this attribute first before checking any + of the seen within properties, since they will all be `None` + if the status is not set. + + action (:tl:`SendMessageAction`, optional): + The "typing" action if any the user is performing if any. + + You should check this attribute first before checking any + of the typing properties, since they will all be `None` + if the action is not set. + Example .. code-block:: python @@ -48,262 +62,242 @@ class UserUpdate(EventBuilder): if event.uploading: await client.send_message(event.user_id, 'What are you sending?') """ + def __init__(self, peer, *, status=None, chat_peer=None, typing=None): + _custom.chatgetter.ChatGetter.__init__(self, chat_peer or peer) + _custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer)) + + self.status = status + self.action = typing + @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateUserStatus): - return cls.Event(_tl.PeerUser(update.user_id), + return UserUpdateEvent(_tl.PeerUser(update.user_id), status=update.status) elif isinstance(update, _tl.UpdateChannelUserTyping): - return cls.Event(update.from_id, + return UserUpdateEvent(update.from_id, chat_peer=_tl.PeerChannel(update.channel_id), typing=update.action) elif isinstance(update, _tl.UpdateChatUserTyping): - return cls.Event(update.from_id, + return UserUpdateEvent(update.from_id, chat_peer=_tl.PeerChat(update.chat_id), typing=update.action) elif isinstance(update, _tl.UpdateUserTyping): - return cls.Event(update.user_id, + return UserUpdateEvent(update.user_id, typing=update.action) - class Event(EventCommon, _custom.sendergetter.SenderGetter): + def _set_client(self, client): + super()._set_client(client) + self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) + + @property + def user(self): + """Alias for `sender `.""" + return self.sender + + async def get_user(self): + """Alias for `get_sender `.""" + return await self.get_sender() + + @property + def input_user(self): + """Alias for `input_sender `.""" + return self.input_sender + + async def get_input_user(self): + """Alias for `get_input_sender `.""" + return await self.get_input_sender() + + @property + def user_id(self): + """Alias for `sender_id `.""" + return self.sender_id + + @property + @_requires_action + def typing(self): """ - Represents the event of a user update - such as gone online, started typing, etc. - - Members: - status (:tl:`UserStatus`, optional): - The user status if the update is about going online or offline. - - You should check this attribute first before checking any - of the seen within properties, since they will all be `None` - if the status is not set. - - action (:tl:`SendMessageAction`, optional): - The "typing" action if any the user is performing if any. - - You should check this attribute first before checking any - of the typing properties, since they will all be `None` - if the action is not set. + `True` if the action is typing a message. """ - def __init__(self, peer, *, status=None, chat_peer=None, typing=None): - super().__init__(chat_peer or peer) - _custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer)) + return isinstance(self.action, _tl.SendMessageTypingAction) - self.status = status - self.action = typing + @property + @_requires_action + def uploading(self): + """ + `True` if the action is uploading something. + """ + return isinstance(self.action, ( + _tl.SendMessageChooseContactAction, + _tl.SendMessageChooseStickerAction, + _tl.SendMessageUploadAudioAction, + _tl.SendMessageUploadDocumentAction, + _tl.SendMessageUploadPhotoAction, + _tl.SendMessageUploadRoundAction, + _tl.SendMessageUploadVideoAction + )) - def _set_client(self, client): - super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) + @property + @_requires_action + def recording(self): + """ + `True` if the action is recording something. + """ + return isinstance(self.action, ( + _tl.SendMessageRecordAudioAction, + _tl.SendMessageRecordRoundAction, + _tl.SendMessageRecordVideoAction + )) - @property - def user(self): - """Alias for `sender `.""" - return self.sender + @property + @_requires_action + def playing(self): + """ + `True` if the action is playing a game. + """ + return isinstance(self.action, _tl.SendMessageGamePlayAction) - async def get_user(self): - """Alias for `get_sender `.""" - return await self.get_sender() + @property + @_requires_action + def cancel(self): + """ + `True` if the action was cancelling other actions. + """ + return isinstance(self.action, _tl.SendMessageCancelAction) - @property - def input_user(self): - """Alias for `input_sender `.""" - return self.input_sender + @property + @_requires_action + def geo(self): + """ + `True` if what's being uploaded is a geo. + """ + return isinstance(self.action, _tl.SendMessageGeoLocationAction) - async def get_input_user(self): - """Alias for `get_input_sender `.""" - return await self.get_input_sender() + @property + @_requires_action + def audio(self): + """ + `True` if what's being recorded/uploaded is an audio. + """ + return isinstance(self.action, ( + _tl.SendMessageRecordAudioAction, + _tl.SendMessageUploadAudioAction + )) - @property - def user_id(self): - """Alias for `sender_id `.""" - return self.sender_id + @property + @_requires_action + def round(self): + """ + `True` if what's being recorded/uploaded is a round video. + """ + return isinstance(self.action, ( + _tl.SendMessageRecordRoundAction, + _tl.SendMessageUploadRoundAction + )) - @property - @_requires_action - def typing(self): - """ - `True` if the action is typing a message. - """ - return isinstance(self.action, _tl.SendMessageTypingAction) + @property + @_requires_action + def video(self): + """ + `True` if what's being recorded/uploaded is an video. + """ + return isinstance(self.action, ( + _tl.SendMessageRecordVideoAction, + _tl.SendMessageUploadVideoAction + )) - @property - @_requires_action - def uploading(self): - """ - `True` if the action is uploading something. - """ - return isinstance(self.action, ( - _tl.SendMessageChooseContactAction, - _tl.SendMessageChooseStickerAction, - _tl.SendMessageUploadAudioAction, - _tl.SendMessageUploadDocumentAction, - _tl.SendMessageUploadPhotoAction, - _tl.SendMessageUploadRoundAction, - _tl.SendMessageUploadVideoAction - )) + @property + @_requires_action + def contact(self): + """ + `True` if what's being uploaded (selected) is a contact. + """ + return isinstance(self.action, _tl.SendMessageChooseContactAction) - @property - @_requires_action - def recording(self): - """ - `True` if the action is recording something. - """ - return isinstance(self.action, ( - _tl.SendMessageRecordAudioAction, - _tl.SendMessageRecordRoundAction, - _tl.SendMessageRecordVideoAction - )) + @property + @_requires_action + def document(self): + """ + `True` if what's being uploaded is document. + """ + return isinstance(self.action, _tl.SendMessageUploadDocumentAction) - @property - @_requires_action - def playing(self): - """ - `True` if the action is playing a game. - """ - return isinstance(self.action, _tl.SendMessageGamePlayAction) + @property + @_requires_action + def sticker(self): + """ + `True` if what's being uploaded is a sticker. + """ + return isinstance(self.action, _tl.SendMessageChooseStickerAction) - @property - @_requires_action - def cancel(self): - """ - `True` if the action was cancelling other actions. - """ - return isinstance(self.action, _tl.SendMessageCancelAction) + @property + @_requires_action + def photo(self): + """ + `True` if what's being uploaded is a photo. + """ + return isinstance(self.action, _tl.SendMessageUploadPhotoAction) - @property - @_requires_action - def geo(self): - """ - `True` if what's being uploaded is a geo. - """ - return isinstance(self.action, _tl.SendMessageGeoLocationAction) + @property + @_requires_action + def last_seen(self): + """ + Exact `datetime.datetime` when the user was last seen if known. + """ + if isinstance(self.status, _tl.UserStatusOffline): + return self.status.was_online - @property - @_requires_action - def audio(self): - """ - `True` if what's being recorded/uploaded is an audio. - """ - return isinstance(self.action, ( - _tl.SendMessageRecordAudioAction, - _tl.SendMessageUploadAudioAction - )) + @property + @_requires_status + def until(self): + """ + The `datetime.datetime` until when the user should appear online. + """ + if isinstance(self.status, _tl.UserStatusOnline): + return self.status.expires - @property - @_requires_action - def round(self): - """ - `True` if what's being recorded/uploaded is a round video. - """ - return isinstance(self.action, ( - _tl.SendMessageRecordRoundAction, - _tl.SendMessageUploadRoundAction - )) + def _last_seen_delta(self): + if isinstance(self.status, _tl.UserStatusOffline): + return datetime.datetime.now(tz=datetime.timezone.utc) - self.status.was_online + elif isinstance(self.status, _tl.UserStatusOnline): + return datetime.timedelta(days=0) + elif isinstance(self.status, _tl.UserStatusRecently): + return datetime.timedelta(days=1) + elif isinstance(self.status, _tl.UserStatusLastWeek): + return datetime.timedelta(days=7) + elif isinstance(self.status, _tl.UserStatusLastMonth): + return datetime.timedelta(days=30) + else: + return datetime.timedelta(days=365) - @property - @_requires_action - def video(self): - """ - `True` if what's being recorded/uploaded is an video. - """ - return isinstance(self.action, ( - _tl.SendMessageRecordVideoAction, - _tl.SendMessageUploadVideoAction - )) + @property + @_requires_status + def online(self): + """ + `True` if the user is currently online, + """ + return self._last_seen_delta() <= datetime.timedelta(days=0) - @property - @_requires_action - def contact(self): - """ - `True` if what's being uploaded (selected) is a contact. - """ - return isinstance(self.action, _tl.SendMessageChooseContactAction) + @property + @_requires_status + def recently(self): + """ + `True` if the user was seen within a day. + """ + return self._last_seen_delta() <= datetime.timedelta(days=1) - @property - @_requires_action - def document(self): - """ - `True` if what's being uploaded is document. - """ - return isinstance(self.action, _tl.SendMessageUploadDocumentAction) + @property + @_requires_status + def within_weeks(self): + """ + `True` if the user was seen within 7 days. + """ + return self._last_seen_delta() <= datetime.timedelta(days=7) - @property - @_requires_action - def sticker(self): - """ - `True` if what's being uploaded is a sticker. - """ - return isinstance(self.action, _tl.SendMessageChooseStickerAction) - - @property - @_requires_action - def photo(self): - """ - `True` if what's being uploaded is a photo. - """ - return isinstance(self.action, _tl.SendMessageUploadPhotoAction) - - @property - @_requires_action - def last_seen(self): - """ - Exact `datetime.datetime` when the user was last seen if known. - """ - if isinstance(self.status, _tl.UserStatusOffline): - return self.status.was_online - - @property - @_requires_status - def until(self): - """ - The `datetime.datetime` until when the user should appear online. - """ - if isinstance(self.status, _tl.UserStatusOnline): - return self.status.expires - - def _last_seen_delta(self): - if isinstance(self.status, _tl.UserStatusOffline): - return datetime.datetime.now(tz=datetime.timezone.utc) - self.status.was_online - elif isinstance(self.status, _tl.UserStatusOnline): - return datetime.timedelta(days=0) - elif isinstance(self.status, _tl.UserStatusRecently): - return datetime.timedelta(days=1) - elif isinstance(self.status, _tl.UserStatusLastWeek): - return datetime.timedelta(days=7) - elif isinstance(self.status, _tl.UserStatusLastMonth): - return datetime.timedelta(days=30) - else: - return datetime.timedelta(days=365) - - @property - @_requires_status - def online(self): - """ - `True` if the user is currently online, - """ - return self._last_seen_delta() <= datetime.timedelta(days=0) - - @property - @_requires_status - def recently(self): - """ - `True` if the user was seen within a day. - """ - return self._last_seen_delta() <= datetime.timedelta(days=1) - - @property - @_requires_status - def within_weeks(self): - """ - `True` if the user was seen within 7 days. - """ - return self._last_seen_delta() <= datetime.timedelta(days=7) - - @property - @_requires_status - def within_months(self): - """ - `True` if the user was seen within 30 days. - """ - return self._last_seen_delta() <= datetime.timedelta(days=30) + @property + @_requires_status + def within_months(self): + """ + `True` if the user was seen within 30 days. + """ + return self._last_seen_delta() <= datetime.timedelta(days=30)