Reuse code to get chat and sender

This commit is contained in:
Lonami Exo 2018-07-10 15:15:22 +02:00
parent 531a02a2a1
commit 8eecd9c226
7 changed files with 267 additions and 348 deletions

View File

@ -53,3 +53,19 @@ telethon\.tl\.custom\.button module
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :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:

View File

@ -3,6 +3,7 @@ import warnings
from .. import utils from .. import utils
from ..tl import TLObject, types from ..tl import TLObject, types
from ..tl.custom.chatgetter import ChatGetter
async def _into_id_set(client, chats): async def _into_id_set(client, chats):
@ -79,13 +80,16 @@ class EventBuilder(abc.ABC):
return event return event
class EventCommon(abc.ABC): class EventCommon(ChatGetter, abc.ABC):
""" """
Intermediate class with common things to all events. Intermediate class with common things to all events.
All events (except `Raw`) have ``is_private``, ``is_group`` Remember that this class implements `ChatGetter
and ``is_channel`` boolean properties, as well as an <telethon.tl.custom.chatgetter.ChatGetter>` which
``original_update`` field containing the original :tl:`Update`. 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' _event_name = 'Event'
@ -96,64 +100,27 @@ class EventCommon(abc.ABC):
self._message_id = msg_id self._message_id = msg_id
self._input_chat = None self._input_chat = None
self._chat = None self._chat = None
self._broadcast = broadcast
self.original_update = None 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): def _set_client(self, client):
""" """
Setter so subclasses can act accordingly when the client is set. Setter so subclasses can act accordingly when the client is set.
""" """
self._client = client self._client = client
self._chat = self._entities.get(self.chat_id)
if not self._chat:
return
@property self._input_chat = utils.get_input_peer(self._chat)
def input_chat(self): if not getattr(self._input_chat, 'access_hash', True):
""" # getattr with True to handle the InputPeerSelf() case
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:
try: try:
self._input_chat =\ self._input_chat = self._client.session.get_input_entity(
self._client.session.get_input_entity(self._chat_peer) self._chat_peer
)
except ValueError: except ValueError:
pass self._input_chat = None
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
@property @property
def client(self): def client(self):
@ -162,44 +129,6 @@ class EventCommon(abc.ABC):
""" """
return self._client 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): def __str__(self):
return TLObject.pretty_format(self.to_dict()) return TLObject.pretty_format(self.to_dict())

View File

@ -18,6 +18,8 @@ from async_generator import isasyncgenfunction
from .client.telegramclient import TelegramClient from .client.telegramclient import TelegramClient
from .tl.custom import Draft, Dialog, MessageButton, Forward, Message 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): def _syncify_coro(t, method_name):
@ -78,4 +80,5 @@ def syncify(*types):
_syncify_gen(t, method_name) _syncify_gen(t, method_name)
syncify(TelegramClient, Draft, Dialog, MessageButton, Forward, Message) syncify(TelegramClient, Draft, Dialog, MessageButton,
ChatGetter, SenderGetter, Forward, Message)

View File

@ -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.
"""

View File

@ -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 Custom class that encapsulates a :tl:`MessageFwdHeader` providing an
abstraction to easily access information like the original sender. abstraction to easily access information like the original sender.
Remember that this class implements `ChatGetter
<telethon.tl.custom.chatgetter.ChatGetter>` and `SenderGetter
<telethon.tl.custom.sendergetter.SenderGetter>` which means you
have access to all their sender and chat properties and methods.
Attributes: Attributes:
original_fwd (:tl:`MessageFwdHeader`): original_fwd (:tl:`MessageFwdHeader`):
@ -19,105 +27,21 @@ class Forward:
self.__dict__ = original.__dict__ self.__dict__ = original.__dict__
self._client = client self._client = client
self.original_fwd = original self.original_fwd = original
self._sender_id = original.from_id
self._sender = entities.get(original.from_id) self._sender = entities.get(original.from_id)
self._chat = entities.get(original.channel_id)
self._input_sender =\ self._input_sender =\
get_input_peer(self._sender) if self._sender else None utils.get_input_peer(self._sender) if self._sender else None
self._input_chat =\
get_input_peer(self._chat) if self._chat else None
# TODO The pattern to get sender and chat is very similar self._broadcast = None
# and copy pasted in/to several places. Reuse the code. if original.channel_id:
# self._chat_peer = types.PeerChannel(original.channel_id)
# It could be an ABC with some ``resolve_sender`` abstract, self._chat = entities.get(utils.get_peer_id(self._chat_peer))
# so every subclass knew what tricks it can make to get else:
# the sender. self._chat_peer = None
self._chat = None
@property self._input_chat = \
def sender(self): utils.get_input_peer(self._chat) if self._chat else None
"""
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
async def get_sender(self): # TODO We could reload the message
"""
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

View File

@ -1,15 +1,22 @@
from .. import types 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 .messagebutton import MessageButton
from .forward import Forward from .forward import Forward
class Message: class Message(ChatGetter, SenderGetter):
""" """
Custom class that encapsulates a message providing an abstraction to Custom class that encapsulates a message providing an abstraction to
easily access some commonly needed features (such as the markdown text easily access some commonly needed features (such as the markdown text
or the text for a given message entity). or the text for a given message entity).
Remember that this class implements `ChatGetter
<telethon.tl.custom.chatgetter.ChatGetter>` and `SenderGetter
<telethon.tl.custom.sendergetter.SenderGetter>` which means you
have access to all their sender and chat properties and methods.
Attributes: Attributes:
original_message (:tl:`Message`): original_message (:tl:`Message`):
@ -34,7 +41,8 @@ class Message:
self._buttons_flat = None self._buttons_flat = None
self._buttons_count = 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: if self._sender:
self._input_sender = get_input_peer(self._sender) self._input_sender = get_input_peer(self._sender)
if not getattr(self._input_sender, 'access_hash', None): if not getattr(self._input_sender, 'access_hash', None):
@ -46,10 +54,11 @@ class Message:
# was sent, not *to which ID* it was sent. # was sent, not *to which ID* it was sent.
if not self.original_message.out \ if not self.original_message.out \
and isinstance(self.original_message.to_id, types.PeerUser): 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: else:
self._chat_peer = self.original_message.to_id self._chat_peer = self.original_message.to_id
self._broadcast = bool(self.original_message.post)
self._chat = entities.get(self.chat_id) self._chat = entities.get(self.chat_id)
self._input_chat = input_chat self._input_chat = input_chat
if not self._input_chat and self._chat: if not self._input_chat and self._chat:
@ -171,158 +180,8 @@ class Message:
self._chat = msg._chat self._chat = msg._chat
self._input_chat = msg._input_chat self._input_chat = msg._input_chat
@property async def _refetch_sender(self):
def sender(self): await self._reload_message()
"""
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)
@property @property
def is_reply(self): def is_reply(self):
@ -602,10 +461,10 @@ class Message:
if self.original_message.fwd_from: if self.original_message.fwd_from:
return None return None
if not self.original_message.out: 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 return None
me = await self._client.get_me(input_peer=True) 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 None
return await self._client.edit_message( return await self._client.edit_message(

View File

@ -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.
"""