Make custom.Message functional

This commit is contained in:
Lonami Exo 2021-09-13 20:37:29 +02:00
parent 499fc9f603
commit 334a847de7
10 changed files with 261 additions and 250 deletions

View File

@ -119,6 +119,32 @@ The following modules have been moved inside ``_misc``:
// TODO review telethon/__init__.py isn't exposing more than it should
The custom.Message class and the way it is used has changed
-----------------------------------------------------------
It no longer inherits ``TLObject``, and rather than trying to mimick Telegram's ``Message``
constructor, it now takes two parameters: a ``TelegramClient`` instance and a ``_tl.Message``.
As a benefit, you can now more easily reconstruct instances of this type from a previously-stored
``_tl.Message`` instance.
There are no public attributes. Instead, they are now properties which forward the values into and
from the private ``_message`` field. As a benefit, the documentation will now be easier to follow.
However, you can no longer use ``del`` on these.
The ``_tl.Message.media`` attribute will no longer be ``None`` when using raw API if the media was
``messageMediaEmpty``. As a benefit, you can now actually distinguish between no media and empty
media. The ``Message.media`` property as returned by friendly methods will still be ``None`` on
empty media.
The ``telethon.tl.patched`` hack has been removed.
In order to avoid breaking more code than strictly necessary, ``.raw_text`` will remain a synonym
of ``.message``, and ``.text`` will still be the text formatted through the ``client.parse_mode``.
However, you're encouraged to change uses of ``.raw_text`` with ``.message``, and ``.text`` with
either ``.md_text`` or ``.html_text`` as needed. This is because both ``.text`` and ``.raw_text``
may disappear in future versions, and their behaviour is not immediately obvious.
The Conversation API has been removed
-------------------------------------

View File

@ -291,16 +291,16 @@ class _AdminLogIter(requestiter.RequestIter):
for ev in r.events:
if isinstance(ev.action,
_tl.ChannelAdminLogEventActionEditMessage):
ev.action.prev_message._finish_init(
self.client, entities, self.entity)
ev.action.prev_message = _custom.Message._new(
self.client, ev.action.prev_message, entities, self.entity)
ev.action.new_message._finish_init(
self.client, entities, self.entity)
ev.action.new_message = _custom.Message._new(
self.client, ev.action.new_message, entities, self.entity)
elif isinstance(ev.action,
_tl.ChannelAdminLogEventActionDeleteMessage):
ev.action.message._finish_init(
self.client, entities, self.entity)
ev.action.message = _custom.Message._new(
self.client, ev.action.message, entities, self.entity)
self.buffer.append(_custom.AdminLogEvent(ev, entities))

View File

@ -58,10 +58,10 @@ class _DialogsIter(requestiter.RequestIter):
for x in itertools.chain(r.users, r.chats)
if not isinstance(x, (_tl.UserEmpty, _tl.ChatEmpty))}
messages = {}
for m in r.messages:
m._finish_init(self.client, entities, None)
messages[_dialog_message_key(m.peer_id, m.id)] = m
messages = {
_dialog_message_key(m.peer_id, m.id): _custom.Message._new(self.client, m, entities, None)
for m in r.messages
}
for d in r.dialogs:
# We check the offset date here because Telegram may ignore it

View File

@ -3,6 +3,7 @@ import re
import typing
from .. import helpers, utils, _tl
from ..types import _custom
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
@ -94,7 +95,7 @@ def _get_response_message(self: 'TelegramClient', request, result, input_chat):
elif isinstance(update, (
_tl.UpdateNewChannelMessage, _tl.UpdateNewMessage)):
update.message._finish_init(self, entities, input_chat)
update.message = _custom.Message._new(self, update.message, entities, input_chat)
# Pinning a message with `updatePinnedMessage` seems to
# always produce a service message we can't map so return
@ -110,7 +111,7 @@ def _get_response_message(self: 'TelegramClient', request, result, input_chat):
elif (isinstance(update, _tl.UpdateEditMessage)
and helpers._entity_type(request.peer) != helpers._EntityType.CHANNEL):
update.message._finish_init(self, entities, input_chat)
update.message = _custom.Message._new(self, update.message, entities, input_chat)
# Live locations use `sendMedia` but Telegram responds with
# `updateEditMessage`, which means we won't have `id` field.
@ -123,28 +124,24 @@ def _get_response_message(self: 'TelegramClient', request, result, input_chat):
and utils.get_peer_id(request.peer) ==
utils.get_peer_id(update.message.peer_id)):
if request.id == update.message.id:
update.message._finish_init(self, entities, input_chat)
return update.message
return _custom.Message._new(self, update.message, entities, input_chat)
elif isinstance(update, _tl.UpdateNewScheduledMessage):
update.message._finish_init(self, entities, input_chat)
# Scheduled IDs may collide with normal IDs. However, for a
# single request there *shouldn't* be a mix between "some
# scheduled and some not".
id_to_message[update.message.id] = update.message
id_to_message[update.message.id] = _custom.Message._new(self, update.message, entities, input_chat)
elif isinstance(update, _tl.UpdateMessagePoll):
if request.media.poll.id == update.poll_id:
m = _tl.Message(
return _custom.Message._new(self, _tl.Message(
id=request.id,
peer_id=utils.get_peer(request.peer),
media=_tl.MessageMediaPoll(
poll=update.poll,
results=update.results
)
)
m._finish_init(self, entities, input_chat)
return m
), entities, input_chat)
if request is None:
return id_to_message

View File

@ -5,6 +5,7 @@ import warnings
from .. import errors, hints, _tl
from .._misc import helpers, utils, requestiter
from ..types import _custom
_MAX_CHUNK_SIZE = 100
@ -200,8 +201,7 @@ class _MessagesIter(requestiter.RequestIter):
# is an attempt to avoid these duplicates, since the message
# IDs are returned in descending order (or asc if reverse).
self.last_id = message.id
message._finish_init(self.client, entities, self.entity)
self.buffer.append(message)
self.buffer.append(_custom.Message._new(self.client, message, entities, self.entity))
if len(r.messages) < self.request.limit:
return True
@ -315,8 +315,7 @@ class _IDsIter(requestiter.RequestIter):
from_id and message.peer_id != from_id):
self.buffer.append(None)
else:
message._finish_init(self.client, entities, self._entity)
self.buffer.append(message)
self.buffer.append(_custom.Message._new(self.client, message, entities, self._entity))
def iter_messages(
@ -498,7 +497,7 @@ async def send_message(
result = await self(request)
if isinstance(result, _tl.UpdateShortSentMessage):
message = _tl.Message(
return _custom.Message._new(self, _tl.Message(
id=result.id,
peer_id=await self._get_peer(entity),
message=message,
@ -508,9 +507,7 @@ async def send_message(
entities=result.entities,
reply_markup=request.reply_markup,
ttl_period=result.ttl_period
)
message._finish_init(self, {}, entity)
return message
), {}, entity)
return self._get_response_message(request, result, entity)

View File

@ -3783,6 +3783,9 @@ class TelegramClient:
async def _handle_auto_reconnect(self: 'TelegramClient'):
return await updates._handle_auto_reconnect(**locals())
def _self_id(self: 'TelegramClient') -> typing.Optional[int]:
return users._self_id(**locals())
# endregion Private
# TODO re-patch everything to remove the intermediate calls

View File

@ -168,8 +168,10 @@ class Album(EventBuilder):
self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._entity_cache)
for msg in self.messages:
msg._finish_init(client, self._entities, None)
self.messages = [
_custom.Message._new(client, m, self._entities, None)
for m in self.messages
]
if len(self.messages) == 1:
# This will require hacks to be a proper album event

View File

@ -1,6 +1,7 @@
from .common import EventBuilder, EventCommon, name_inner_event
from .._misc import utils
from .. import _tl
from ..types import _custom
@name_inner_event

View File

@ -3,6 +3,7 @@ import re
from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set
from .._misc import utils
from .. import _tl
from ..types import _custom
@name_inner_event

View File

@ -9,9 +9,22 @@ from ..._misc import utils, tlobject
from ... import errors, _tl
def _fwd(field, doc):
def fget(self):
try:
return self._message.__dict__[field]
except KeyError:
return None
def fset(self, value):
self._message.__dict__[field] = value
return property(fget, fset, None, doc)
# TODO Figure out a way to have the code generator error on missing fields
# Maybe parsing the init function alone if that's possible.
class Message(ChatGetter, SenderGetter, tlobject.TLObject):
class Message(ChatGetter, SenderGetter):
"""
This custom class aggregates both :tl:`Message` and
:tl:`MessageService` to ease accessing their members.
@ -20,9 +33,11 @@ class Message(ChatGetter, SenderGetter, tlobject.TLObject):
<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.
"""
Members:
out (`bool`):
# region Forwarded properties
out = _fwd('out', """
Whether the message is outgoing (i.e. you sent it from
another session) or incoming (i.e. someone else sent it).
@ -31,76 +46,95 @@ class Message(ChatGetter, SenderGetter, tlobject.TLObject):
to your own chat. Messages you forward to your chat are
*not* considered outgoing, just like official clients
display them.
""")
mentioned (`bool`):
mentioned = _fwd('mentioned', """
Whether you were mentioned in this message or not.
Note that replies to your own messages also count
as mentions.
""")
media_unread (`bool`):
media_unread = _fwd('media_unread', """
Whether you have read the media in this message
or not, e.g. listened to the voice note media.
""")
silent (`bool`):
silent = _fwd('silent', """
Whether the message should notify people with sound or not.
Previously used in channels, but since 9 August 2019, it can
also be `used in private chats
<https://telegram.org/blog/silent-messages-slow-mode>`_.
""")
post (`bool`):
post = _fwd('post', """
Whether this message is a post in a broadcast
channel or not.
""")
from_scheduled (`bool`):
from_scheduled = _fwd('from_scheduled', """
Whether this message was originated from a previously-scheduled
message or not.
""")
legacy (`bool`):
legacy = _fwd('legacy', """
Whether this is a legacy message or not.
""")
edit_hide (`bool`):
edit_hide = _fwd('edit_hide', """
Whether the edited mark of this message is edited
should be hidden (e.g. in GUI clients) or shown.
""")
pinned (`bool`):
pinned = _fwd('pinned', """
Whether this message is currently pinned or not.
""")
id (`int`):
id = _fwd('id', """
The ID of this message. This field is *always* present.
Any other member is optional and may be `None`.
""")
from_id (:tl:`Peer`):
from_id = _fwd('from_id', """
The peer who sent this message, which is either
:tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`.
This value will be `None` for anonymous messages.
""")
peer_id (:tl:`Peer`):
peer_id = _fwd('peer_id', """
The peer to which this message was sent, which is either
:tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`. This
will always be present except for empty messages.
""")
fwd_from (:tl:`MessageFwdHeader`):
fwd_from = _fwd('fwd_from', """
The original forward header if this message is a forward.
You should probably use the `forward` property instead.
""")
via_bot_id (`int`):
via_bot_id = _fwd('via_bot_id', """
The ID of the bot used to send this message
through its inline mode (e.g. "via @like").
""")
reply_to (:tl:`MessageReplyHeader`):
reply_to = _fwd('reply_to', """
The original reply header if this message is replying to another.
""")
date (`datetime`):
date = _fwd('date', """
The UTC+0 `datetime` object indicating when this message
was sent. This will always be present except for empty
messages.
""")
message (`str`):
message = _fwd('message', """
The string text of the message for `Message
<telethon.tl.custom.message.Message>` instances,
which will be `None` for other types of messages.
""")
media (:tl:`MessageMedia`):
@property
def media(self):
"""
The media sent with this message if any (such as
photos, videos, documents, gifs, stickers, etc.).
@ -109,34 +143,52 @@ class Message(ChatGetter, SenderGetter, tlobject.TLObject):
If the media was not present or it was :tl:`MessageMediaEmpty`,
this member will instead be `None` for convenience.
"""
try:
media = self._message.media
except AttributeError:
return None
reply_markup (:tl:`ReplyMarkup`):
return None if media.CONSTRUCTOR_ID == 0x3ded6320 else media
@media.setter
def media(self, value):
self._message.media = value
reply_markup = _fwd('reply_markup', """
The reply markup for this message (which was sent
either via a bot or by a bot). You probably want
to access `buttons` instead.
""")
entities (List[:tl:`MessageEntity`]):
entities = _fwd('entities', """
The list of markup entities in this message,
such as bold, italics, code, hyperlinks, etc.
""")
views (`int`):
views = _fwd('views', """
The number of views this message from a broadcast
channel has. This is also present in forwards.
""")
forwards (`int`):
forwards = _fwd('forwards', """
The number of times this message has been forwarded.
""")
replies (`int`):
replies = _fwd('replies', """
The number of times another message has replied to this message.
""")
edit_date (`datetime`):
edit_date = _fwd('edit_date', """
The date when this message was last edited.
""")
post_author (`str`):
post_author = _fwd('post_author', """
The display name of the message sender to
show in messages sent to broadcast channels.
""")
grouped_id (`int`):
grouped_id = _fwd('grouped_id', """
If this message belongs to a group of messages
(photo albums or video albums), all of them will
have the same value here.
@ -144,95 +196,29 @@ class Message(ChatGetter, SenderGetter, tlobject.TLObject):
restriction_reason (List[:tl:`RestrictionReason`])
An optional list of reasons why this message was restricted.
If the list is `None`, this message has not been restricted.
""")
ttl_period (`int`):
ttl_period = _fwd('ttl_period', """
The Time To Live period configured for this message.
The message should be erased from wherever it's stored (memory, a
local database, etc.) when
``datetime.now() > message.date + timedelta(seconds=message.ttl_period)``.
""")
action (:tl:`MessageAction`):
action = _fwd('action', """
The message action object of the message for :tl:`MessageService`
instances, which will be `None` for other types of messages.
"""
""")
# endregion
# region Initialization
def __init__(
# Common to all
self, id: int,
# Common to Message and MessageService (mandatory)
peer_id: _tl.TypePeer = None,
date: Optional[datetime] = None,
# Common to Message and MessageService (flags)
out: Optional[bool] = None,
mentioned: Optional[bool] = None,
media_unread: Optional[bool] = None,
silent: Optional[bool] = None,
post: Optional[bool] = None,
from_id: Optional[_tl.TypePeer] = None,
reply_to: Optional[_tl.TypeMessageReplyHeader] = None,
ttl_period: Optional[int] = None,
# For Message (mandatory)
message: Optional[str] = None,
# For Message (flags)
fwd_from: Optional[_tl.TypeMessageFwdHeader] = None,
via_bot_id: Optional[int] = None,
media: Optional[_tl.TypeMessageMedia] = None,
reply_markup: Optional[_tl.TypeReplyMarkup] = None,
entities: Optional[List[_tl.TypeMessageEntity]] = None,
views: Optional[int] = None,
edit_date: Optional[datetime] = None,
post_author: Optional[str] = None,
grouped_id: Optional[int] = None,
from_scheduled: Optional[bool] = None,
legacy: Optional[bool] = None,
edit_hide: Optional[bool] = None,
pinned: Optional[bool] = None,
restriction_reason: Optional[_tl.TypeRestrictionReason] = None,
forwards: Optional[int] = None,
replies: Optional[_tl.TypeMessageReplies] = None,
# For MessageAction (mandatory)
action: Optional[_tl.TypeMessageAction] = None
):
# Common properties to messages, then to service (in the order they're defined in the `.tl`)
self.out = bool(out)
self.mentioned = mentioned
self.media_unread = media_unread
self.silent = silent
self.post = post
self.from_scheduled = from_scheduled
self.legacy = legacy
self.edit_hide = edit_hide
self.id = id
self.from_id = from_id
self.peer_id = peer_id
self.fwd_from = fwd_from
self.via_bot_id = via_bot_id
self.reply_to = reply_to
self.date = date
self.message = message
self.media = None if isinstance(media, _tl.MessageMediaEmpty) else media
self.reply_markup = reply_markup
self.entities = entities
self.views = views
self.forwards = forwards
self.replies = replies
self.edit_date = edit_date
self.pinned = pinned
self.post_author = post_author
self.grouped_id = grouped_id
self.restriction_reason = restriction_reason
self.ttl_period = ttl_period
self.action = action
def __init__(self, client, message):
self._client = client
self._message = message
# Convenient storage for custom functions
# TODO This is becoming a bit of bloat
self._client = None
self._text = None
self._file = None
@ -246,28 +232,25 @@ class Message(ChatGetter, SenderGetter, tlobject.TLObject):
self._linked_chat = None
sender_id = None
if from_id is not None:
sender_id = utils.get_peer_id(from_id)
elif peer_id:
if self.from_id is not None:
sender_id = utils.get_peer_id(self.from_id)
elif self.peer_id:
# If the message comes from a Channel, let the sender be it
# ...or...
# incoming messages in private conversations no longer have from_id
# (layer 119+), but the sender can only be the chat we're in.
if post or (not out and isinstance(peer_id, _tl.PeerUser)):
sender_id = utils.get_peer_id(peer_id)
if self.post or (not self.out and isinstance(self.peer_id, _tl.PeerUser)):
sender_id = utils.get_peer_id(self.peer_id)
# Note that these calls would reset the client
ChatGetter.__init__(self, peer_id, broadcast=post)
ChatGetter.__init__(self, self.peer_id, broadcast=self.post)
SenderGetter.__init__(self, sender_id)
self._forward = None
def _finish_init(self, client, entities, input_chat):
"""
Finishes the initialization of this message by setting
the client that sent the message and making use of the
known entities.
"""
@classmethod
def _new(cls, client, message, entities, input_chat):
self = cls(client, message)
self._client = client
# Make messages sent to ourselves outgoing unless they're forwarded.
@ -314,6 +297,7 @@ class Message(ChatGetter, SenderGetter, tlobject.TLObject):
self._linked_chat = entities.get(utils.get_peer_id(
_tl.PeerChannel(self.replies.channel_id)))
return self
# endregion Initialization