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,219 +33,192 @@ 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`):
Whether the message is outgoing (i.e. you sent it from
another session) or incoming (i.e. someone else sent it).
Note that messages in your own chat are always incoming,
but this member will be `True` if you send a message
to your own chat. Messages you forward to your chat are
*not* considered outgoing, just like official clients
display them.
mentioned (`bool`):
Whether you were mentioned in this message or not.
Note that replies to your own messages also count
as mentions.
media_unread (`bool`):
Whether you have read the media in this message
or not, e.g. listened to the voice note media.
silent (`bool`):
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`):
Whether this message is a post in a broadcast
channel or not.
from_scheduled (`bool`):
Whether this message was originated from a previously-scheduled
message or not.
legacy (`bool`):
Whether this is a legacy message or not.
edit_hide (`bool`):
Whether the edited mark of this message is edited
should be hidden (e.g. in GUI clients) or shown.
pinned (`bool`):
Whether this message is currently pinned or not.
id (`int`):
The ID of this message. This field is *always* present.
Any other member is optional and may be `None`.
from_id (:tl:`Peer`):
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`):
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`):
The original forward header if this message is a forward.
You should probably use the `forward` property instead.
via_bot_id (`int`):
The ID of the bot used to send this message
through its inline mode (e.g. "via @like").
reply_to (:tl:`MessageReplyHeader`):
The original reply header if this message is replying to another.
date (`datetime`):
The UTC+0 `datetime` object indicating when this message
was sent. This will always be present except for empty
messages.
message (`str`):
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`):
The media sent with this message if any (such as
photos, videos, documents, gifs, stickers, etc.).
You may want to access the `photo`, `document`
etc. properties instead.
If the media was not present or it was :tl:`MessageMediaEmpty`,
this member will instead be `None` for convenience.
reply_markup (:tl:`ReplyMarkup`):
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`]):
The list of markup entities in this message,
such as bold, italics, code, hyperlinks, etc.
views (`int`):
The number of views this message from a broadcast
channel has. This is also present in forwards.
forwards (`int`):
The number of times this message has been forwarded.
replies (`int`):
The number of times another message has replied to this message.
edit_date (`datetime`):
The date when this message was last edited.
post_author (`str`):
The display name of the message sender to
show in messages sent to broadcast channels.
grouped_id (`int`):
If this message belongs to a group of messages
(photo albums or video albums), all of them will
have the same value here.
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`):
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`):
The message action object of the message for :tl:`MessageService`
instances, which will be `None` for other types of messages.
"""
# 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).
Note that messages in your own chat are always incoming,
but this member will be `True` if you send a message
to your own chat. Messages you forward to your chat are
*not* considered outgoing, just like official clients
display them.
""")
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 = _fwd('media_unread', """
Whether you have read the media in this message
or not, e.g. listened to the voice note media.
""")
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 = _fwd('post', """
Whether this message is a post in a broadcast
channel or not.
""")
from_scheduled = _fwd('from_scheduled', """
Whether this message was originated from a previously-scheduled
message or not.
""")
legacy = _fwd('legacy', """
Whether this is a legacy message or not.
""")
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 = _fwd('pinned', """
Whether this message is currently pinned or not.
""")
id = _fwd('id', """
The ID of this message. This field is *always* present.
Any other member is optional and may be `None`.
""")
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 = _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 = _fwd('fwd_from', """
The original forward header if this message is a forward.
You should probably use the `forward` property instead.
""")
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 = _fwd('reply_to', """
The original reply header if this message is replying to another.
""")
date = _fwd('date', """
The UTC+0 `datetime` object indicating when this message
was sent. This will always be present except for empty
messages.
""")
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.
""")
@property
def media(self):
"""
The media sent with this message if any (such as
photos, videos, documents, gifs, stickers, etc.).
You may want to access the `photo`, `document`
etc. properties instead.
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
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 = _fwd('entities', """
The list of markup entities in this message,
such as bold, italics, code, hyperlinks, etc.
""")
views = _fwd('views', """
The number of views this message from a broadcast
channel has. This is also present in forwards.
""")
forwards = _fwd('forwards', """
The number of times this message has been forwarded.
""")
replies = _fwd('replies', """
The number of times another message has replied to this message.
""")
edit_date = _fwd('edit_date', """
The date when this message was last edited.
""")
post_author = _fwd('post_author', """
The display name of the message sender to
show in messages sent to broadcast channels.
""")
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.
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 = _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 = _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