diff --git a/readthedocs/telethon.tl.custom.rst b/readthedocs/telethon.tl.custom.rst index 74828cb4..1099fa5e 100644 --- a/readthedocs/telethon.tl.custom.rst +++ b/readthedocs/telethon.tl.custom.rst @@ -53,3 +53,19 @@ telethon\.tl\.custom\.button module :members: :undoc-members: :show-inheritance: + +telethon\.tl\.custom\.chatgetter module +--------------------------------------- + +.. automodule:: telethon.tl.custom.chatgetter + :members: + :undoc-members: + :show-inheritance: + +telethon\.tl\.custom\.sendergetter module +----------------------------------------- + +.. automodule:: telethon.tl.custom.sendergetter + :members: + :undoc-members: + :show-inheritance: diff --git a/telethon/events/common.py b/telethon/events/common.py index 47235a9d..2e7c2d26 100644 --- a/telethon/events/common.py +++ b/telethon/events/common.py @@ -3,6 +3,7 @@ import warnings from .. import utils from ..tl import TLObject, types +from ..tl.custom.chatgetter import ChatGetter async def _into_id_set(client, chats): @@ -79,13 +80,16 @@ class EventBuilder(abc.ABC): return event -class EventCommon(abc.ABC): +class EventCommon(ChatGetter, abc.ABC): """ Intermediate class with common things to all events. - All events (except `Raw`) have ``is_private``, ``is_group`` - and ``is_channel`` boolean properties, as well as an - ``original_update`` field containing the original :tl:`Update`. + 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' @@ -96,64 +100,27 @@ class EventCommon(abc.ABC): self._message_id = msg_id self._input_chat = None self._chat = None + self._broadcast = broadcast self.original_update = None - self.is_private = isinstance(chat_peer, types.PeerUser) - self.is_group = ( - isinstance(chat_peer, (types.PeerChat, types.PeerChannel)) - and not broadcast - ) - self.is_channel = isinstance(chat_peer, types.PeerChannel) - def _set_client(self, client): """ Setter so subclasses can act accordingly when the client is set. """ self._client = client + self._chat = self._entities.get(self.chat_id) + if not self._chat: + return - @property - def input_chat(self): - """ - This (:tl:`InputPeer`) is the input version of the chat where the - event occurred. This doesn't have things like username or similar, - but is still useful in some cases. - - Note that this might not be available if the library doesn't have - enough information available. - """ - if self._input_chat is None and self._chat_peer is not None: + self._input_chat = utils.get_input_peer(self._chat) + if not getattr(self._input_chat, 'access_hash', True): + # getattr with True to handle the InputPeerSelf() case try: - self._input_chat =\ - self._client.session.get_input_entity(self._chat_peer) + self._input_chat = self._client.session.get_input_entity( + self._chat_peer + ) except ValueError: - pass - - return self._input_chat - - async def get_input_chat(self): - """ - Returns `input_chat`, but will make an API call to find the - input chat unless it's already cached. - """ - if self.input_chat is None and self._chat_peer is not None: - ch = isinstance(self._chat_peer, types.PeerChannel) - if not ch and self._message_id is not None: - msg = await self._client.get_messages( - None, ids=self._message_id) - self._chat = msg._chat - self._input_chat = msg._input_chat - else: - target = utils.get_peer_id(self._chat_peer) - async for d in self._client.iter_dialogs(100): - if d.id == target: - self._chat = d.entity - self._input_chat = d.input_entity - # TODO Don't break, exhaust the iterator, otherwise - # async_generator raises RuntimeError: partially- - # exhausted async_generator 'xyz' garbage collected - # break - - return self._input_chat + self._input_chat = None @property def client(self): @@ -162,44 +129,6 @@ class EventCommon(abc.ABC): """ return self._client - @property - def chat(self): - """ - The :tl:`User`, :tl:`Chat` or :tl:`Channel` on which - the event occurred. This property may make an API call the first time - to get the most up to date version of the chat (mostly when the event - doesn't belong to a channel), so keep that in mind. You should use - `get_chat` instead, unless you want to avoid an API call. - """ - if not self.input_chat: - return None - - if self._chat is None: - self._chat = self._entities.get(utils.get_peer_id(self._chat_peer)) - - return self._chat - - async def get_chat(self): - """ - Returns `chat`, but will make an API call to find the - chat unless it's already cached. - """ - if self.chat is None and await self.get_input_chat(): - try: - self._chat =\ - await self._client.get_entity(self._input_chat) - except ValueError: - pass - return self._chat - - @property - def chat_id(self): - """ - Returns the marked integer ID of the chat, if any. - """ - if self._chat_peer: - return utils.get_peer_id(self._chat_peer) - def __str__(self): return TLObject.pretty_format(self.to_dict()) diff --git a/telethon/sync.py b/telethon/sync.py index 51a41f27..9e79a112 100644 --- a/telethon/sync.py +++ b/telethon/sync.py @@ -18,6 +18,8 @@ from async_generator import isasyncgenfunction from .client.telegramclient import TelegramClient from .tl.custom import Draft, Dialog, MessageButton, Forward, Message +from .tl.custom.chatgetter import ChatGetter +from .tl.custom.sendergetter import SenderGetter def _syncify_coro(t, method_name): @@ -78,4 +80,5 @@ def syncify(*types): _syncify_gen(t, method_name) -syncify(TelegramClient, Draft, Dialog, MessageButton, Forward, Message) +syncify(TelegramClient, Draft, Dialog, MessageButton, + ChatGetter, SenderGetter, Forward, Message) diff --git a/telethon/tl/custom/chatgetter.py b/telethon/tl/custom/chatgetter.py new file mode 100644 index 00000000..fb733d9a --- /dev/null +++ b/telethon/tl/custom/chatgetter.py @@ -0,0 +1,114 @@ +import abc + +from ... import errors, utils +from ...tl import types + + +class ChatGetter(abc.ABC): + """ + Helper base class that introduces the `chat`, `input_chat` + and `chat_id` properties and `get_chat` and `get_input_chat` + methods. + + Subclasses **must** have the following private members: `_chat`, + `_input_chat`, `_chat_peer`, `_broadcast` and `_client`. As an end + user, you should not worry about this. + """ + @property + def chat(self): + """ + Returns the :tl:`User`, :tl:`Chat` or :tl:`Channel` where this object + belongs to. It may be ``None`` if Telegram didn't send the chat. + + If you're using `telethon.events`, use `get_chat` instead. + """ + return self._chat + + async def get_chat(self): + """ + Returns `chat`, but will make an API call to find the + chat unless it's already cached. + """ + if self._chat is None and await self.get_input_chat(): + try: + self._chat =\ + await self._client.get_entity(self._input_chat) + except ValueError: + await self._refetch_chat() + return self._chat + + @property + def input_chat(self): + """ + This :tl:`InputPeer` is the input version of the chat where the + message was sent. Similarly to `input_sender`, this doesn't have + things like username or similar, but still useful in some cases. + + Note that this might not be available if the library doesn't + have enough information available. + """ + if self._input_chat is None and self._chat_peer: + try: + self._input_chat =\ + self._client.session.get_input_entity(self._chat_peer) + except ValueError: + pass + + return self._input_chat + + async def get_input_chat(self): + """ + Returns `input_chat`, but will make an API call to find the + input chat unless it's already cached. + """ + if self.input_chat is None and self.chat_id: + try: + # The chat may be recent, look in dialogs + target = self.chat_id + async for d in self._client.iter_dialogs(100): + if d.id == target: + self._chat = d.entity + self._input_chat = d.input_entity + break + except errors.RPCError: + pass + + return self._input_chat + + @property + def chat_id(self): + """ + Returns the marked chat integer ID. Note that this value **will + be different** from `to_id` for incoming private messages, since + the chat *to* which the messages go is to your own person, but + the *chat* itself is with the one who sent the message. + + TL;DR; this gets the ID that you expect. + """ + return utils.get_peer_id(self._chat_peer) if self._chat_peer else None + + @property + def is_private(self): + """True if the message was sent as a private message.""" + return isinstance(self._chat_peer, types.PeerUser) + + @property + def is_group(self): + """True if the message was sent on a group or megagroup.""" + if self._broadcast is None and self.chat: + self._broadcast = getattr(self.chat, 'broadcast', None) + + return ( + isinstance(self._chat_peer, (types.PeerChat, types.PeerChannel)) + and not self._broadcast + ) + + @property + def is_channel(self): + """True if the message was sent on a megagroup or channel.""" + return isinstance(self._chat_peer, types.PeerChannel) + + async def _refetch_chat(self): + """ + Re-fetches chat information through other means. + """ diff --git a/telethon/tl/custom/forward.py b/telethon/tl/custom/forward.py index 737b2119..52603f9b 100644 --- a/telethon/tl/custom/forward.py +++ b/telethon/tl/custom/forward.py @@ -1,11 +1,19 @@ -from ...utils import get_input_peer +from .chatgetter import ChatGetter +from .sendergetter import SenderGetter +from ... import utils +from ...tl import types -class Forward: +class Forward(ChatGetter, SenderGetter): """ Custom class that encapsulates a :tl:`MessageFwdHeader` providing an abstraction to easily access information like the original sender. + Remember that this class implements `ChatGetter + ` and `SenderGetter + ` which means you + have access to all their sender and chat properties and methods. + Attributes: original_fwd (:tl:`MessageFwdHeader`): @@ -19,105 +27,21 @@ class Forward: self.__dict__ = original.__dict__ self._client = client self.original_fwd = original + + self._sender_id = original.from_id self._sender = entities.get(original.from_id) - self._chat = entities.get(original.channel_id) - self._input_sender =\ - get_input_peer(self._sender) if self._sender else None - self._input_chat =\ - get_input_peer(self._chat) if self._chat else None + utils.get_input_peer(self._sender) if self._sender else None - # TODO The pattern to get sender and chat is very similar - # and copy pasted in/to several places. Reuse the code. - # - # It could be an ABC with some ``resolve_sender`` abstract, - # so every subclass knew what tricks it can make to get - # the sender. + self._broadcast = None + if original.channel_id: + self._chat_peer = types.PeerChannel(original.channel_id) + self._chat = entities.get(utils.get_peer_id(self._chat_peer)) + else: + self._chat_peer = None + self._chat = None - @property - def sender(self): - """ - The :tl:`User` that sent the original message. This may be ``None`` - if it couldn't be found or the message wasn't forwarded from an user - but instead was forwarded from e.g. a channel. - """ - return self._sender + self._input_chat = \ + utils.get_input_peer(self._chat) if self._chat else None - async def get_sender(self): - """ - Returns `sender` but will make an API if necessary. - """ - if not self.sender and self.original_fwd.from_id: - try: - self._sender = await self._client.get_entity( - await self.get_input_sender()) - except ValueError: - # TODO We could reload the message - pass - - return self._sender - - @property - def input_sender(self): - """ - Returns the input version of `user`. - """ - if not self._input_sender and self.original_fwd.from_id: - try: - self._input_sender = self._client.session.get_input_entity( - self.original_fwd.from_id) - except ValueError: - pass - - return self._input_sender - - async def get_input_sender(self): - """ - Returns `input_sender` but will make an API call if necessary. - """ - # TODO We could reload the message - return self.input_sender - - @property - def chat(self): - """ - The :tl:`Channel` where the original message was sent. This may be - ``None`` if it couldn't be found or the message wasn't forwarded - from a channel but instead was forwarded from e.g. an user. - """ - return self._chat - - async def get_chat(self): - """ - Returns `chat` but will make an API if necessary. - """ - if not self.chat and self.original_fwd.channel_id: - try: - self._chat = await self._client.get_entity( - await self.get_input_chat()) - except ValueError: - # TODO We could reload the message - pass - - return self._chat - - @property - def input_chat(self): - """ - Returns the input version of `chat`. - """ - if not self._input_chat and self.original_fwd.channel_id: - try: - self._input_chat = self._client.session.get_input_entity( - self.original_fwd.channel_id) - except ValueError: - pass - - return self._input_chat - - async def get_input_chat(self): - """ - Returns `input_chat` but will make an API call if necessary. - """ - # TODO We could reload the message - return self.input_chat + # TODO We could reload the message diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index 40236c6c..2436876b 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -1,15 +1,22 @@ from .. import types -from ...utils import get_input_peer, get_peer_id, get_inner_text +from ...utils import get_input_peer, get_inner_text +from .chatgetter import ChatGetter +from .sendergetter import SenderGetter from .messagebutton import MessageButton from .forward import Forward -class Message: +class Message(ChatGetter, SenderGetter): """ Custom class that encapsulates a message providing an abstraction to easily access some commonly needed features (such as the markdown text or the text for a given message entity). + Remember that this class implements `ChatGetter + ` and `SenderGetter + ` which means you + have access to all their sender and chat properties and methods. + Attributes: original_message (:tl:`Message`): @@ -34,7 +41,8 @@ class Message: self._buttons_flat = None self._buttons_count = None - self._sender = entities.get(self.original_message.from_id) + self._sender_id = self.original_message.from_id + self._sender = entities.get(self._sender_id) if self._sender: self._input_sender = get_input_peer(self._sender) if not getattr(self._input_sender, 'access_hash', None): @@ -46,10 +54,11 @@ class Message: # was sent, not *to which ID* it was sent. if not self.original_message.out \ and isinstance(self.original_message.to_id, types.PeerUser): - self._chat_peer = types.PeerUser(self.original_message.from_id) + self._chat_peer = types.PeerUser(self._sender_id) else: self._chat_peer = self.original_message.to_id + self._broadcast = bool(self.original_message.post) self._chat = entities.get(self.chat_id) self._input_chat = input_chat if not self._input_chat and self._chat: @@ -171,158 +180,8 @@ class Message: self._chat = msg._chat self._input_chat = msg._input_chat - @property - def sender(self): - """ - Returns the :tl:`User` that sent this message. It may be ``None`` - if the message has no sender or if Telegram didn't send the sender - inside message events. - - If you're using `telethon.events`, use `get_sender` instead. - """ - return self._sender - - async def get_sender(self): - """ - Returns `sender`, but will make an API call to find the - sender unless it's already cached. - """ - if self._sender is None and await self.get_input_sender(): - try: - self._sender =\ - await self._client.get_entity(self._input_sender) - except ValueError: - await self._reload_message() - return self._sender - - @property - def chat(self): - """ - Returns the :tl:`User`, :tl:`Chat` or :tl:`Channel` where this message - was sent. It may be ``None`` if Telegram didn't send the chat inside - message events. - - If you're using `telethon.events`, use `get_chat` instead. - """ - return self._chat - - async def get_chat(self): - """ - Returns `chat`, but will make an API call to find the - chat unless it's already cached. - """ - if self._chat is None and await self.get_input_chat(): - try: - self._chat =\ - await self._client.get_entity(self._input_chat) - except ValueError: - await self._reload_message() - return self._chat - - @property - def input_sender(self): - """ - This (:tl:`InputPeer`) is the input version of the user who - sent the message. Similarly to `input_chat`, this doesn't have - things like username or similar, but still useful in some cases. - - Note that this might not be available if the library can't - find the input chat, or if the message a broadcast on a channel. - """ - if self._input_sender is None: - if self.is_channel and not self.is_group: - return None - try: - self._input_sender = self._client.session\ - .get_input_entity(self.original_message.from_id) - except ValueError: - pass - return self._input_sender - - async def get_input_sender(self): - """ - Returns `input_sender`, but will make an API call to find the - input sender unless it's already cached. - """ - if self.input_sender is None\ - and not self.is_channel and not self.is_group: - await self._reload_message() - return self._input_sender - - @property - def input_chat(self): - """ - This (:tl:`InputPeer`) is the input version of the chat where the - message was sent. Similarly to `input_sender`, this doesn't have - things like username or similar, but still useful in some cases. - - Note that this might not be available if the library doesn't know - where the message came from. - """ - if self._input_chat is None: - try: - self._input_chat =\ - self._client.session.get_input_entity(self._chat_peer) - except ValueError: - pass - - return self._input_chat - - async def get_input_chat(self): - """ - Returns `input_chat`, but will make an API call to find the - input chat unless it's already cached. - """ - if self.input_chat is None: - # There's a chance that the chat is a recent new dialog. - # The input chat cannot rely on ._reload_message() because - # said method may need the input chat. - target = self.chat_id - async for d in self._client.iter_dialogs(100): - if d.id == target: - self._chat = d.entity - self._input_chat = d.input_entity - break - - return self._input_chat - - @property - def sender_id(self): - """ - Returns the marked sender integer ID, if present. - """ - return self.original_message.from_id - - @property - def chat_id(self): - """ - Returns the marked chat integer ID. Note that this value **will - be different** from `to_id` for incoming private messages, since - the chat *to* which the messages go is to your own person, but - the *chat* itself is with the one who sent the message. - - TL;DR; this gets the ID that you expect. - """ - return get_peer_id(self._chat_peer) - - @property - def is_private(self): - """True if the message was sent as a private message.""" - return isinstance(self.original_message.to_id, types.PeerUser) - - @property - def is_group(self): - """True if the message was sent on a group or megagroup.""" - return ( - isinstance(self.original_message.to_id, (types.PeerChat, - types.PeerChannel)) - and not self.original_message.post - ) - - @property - def is_channel(self): - """True if the message was sent on a megagroup or channel.""" - return isinstance(self.original_message.to_id, types.PeerChannel) + async def _refetch_sender(self): + await self._reload_message() @property def is_reply(self): @@ -602,10 +461,10 @@ class Message: if self.original_message.fwd_from: return None if not self.original_message.out: - if not isinstance(self.original_message.to_id, types.PeerUser): + if not isinstance(self._chat_peer, types.PeerUser): return None me = await self._client.get_me(input_peer=True) - if self.original_message.to_id.user_id != me.user_id: + if self._chat_peer.user_id != me.user_id: return None return await self._client.edit_message( diff --git a/telethon/tl/custom/sendergetter.py b/telethon/tl/custom/sendergetter.py new file mode 100644 index 00000000..a0359550 --- /dev/null +++ b/telethon/tl/custom/sendergetter.py @@ -0,0 +1,74 @@ +import abc + + +class SenderGetter(abc.ABC): + """ + Helper base class that introduces the `sender`, `input_sender` + and `sender_id` properties and `get_sender` and `get_input_sender` + methods. + + Subclasses **must** have the following private members: `_sender`, + `_input_sender`, `_sender_id` and `_client`. As an end user, you + should not worry about this. + """ + @property + def sender(self): + """ + Returns the :tl:`User` that created this object. It may be ``None`` + if the object has no sender or if Telegram didn't send the sender. + + If you're using `telethon.events`, use `get_sender` instead. + """ + return self._sender + + async def get_sender(self): + """ + Returns `sender`, but will make an API call to find the + sender unless it's already cached. + """ + if self._sender is None and await self.get_input_sender(): + try: + self._sender =\ + await self._client.get_entity(self._input_sender) + except ValueError: + await self._reload_message() + return self._sender + + @property + def input_sender(self): + """ + This :tl:`InputPeer` is the input version of the user who + sent the message. Similarly to `input_chat`, this doesn't have + things like username or similar, but still useful in some cases. + + Note that this might not be available if the library can't + find the input chat, or if the message a broadcast on a channel. + """ + if self._input_sender is None and self._sender_id: + try: + self._input_sender = self._client.session\ + .get_input_entity(self._sender_id) + except ValueError: + pass + return self._input_sender + + async def get_input_sender(self): + """ + Returns `input_sender`, but will make an API call to find the + input sender unless it's already cached. + """ + if self.input_sender is None and self._sender_id: + await self._refetch_sender() + return self._input_sender + + @property + def sender_id(self): + """ + Returns the marked sender integer ID, if present. + """ + return self._sender_id + + async def _refetch_sender(self): + """ + Re-fetches sender information through other means. + """