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 // 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 The Conversation API has been removed
------------------------------------- -------------------------------------

View File

@ -291,16 +291,16 @@ class _AdminLogIter(requestiter.RequestIter):
for ev in r.events: for ev in r.events:
if isinstance(ev.action, if isinstance(ev.action,
_tl.ChannelAdminLogEventActionEditMessage): _tl.ChannelAdminLogEventActionEditMessage):
ev.action.prev_message._finish_init( ev.action.prev_message = _custom.Message._new(
self.client, entities, self.entity) self.client, ev.action.prev_message, entities, self.entity)
ev.action.new_message._finish_init( ev.action.new_message = _custom.Message._new(
self.client, entities, self.entity) self.client, ev.action.new_message, entities, self.entity)
elif isinstance(ev.action, elif isinstance(ev.action,
_tl.ChannelAdminLogEventActionDeleteMessage): _tl.ChannelAdminLogEventActionDeleteMessage):
ev.action.message._finish_init( ev.action.message = _custom.Message._new(
self.client, entities, self.entity) self.client, ev.action.message, entities, self.entity)
self.buffer.append(_custom.AdminLogEvent(ev, entities)) 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) for x in itertools.chain(r.users, r.chats)
if not isinstance(x, (_tl.UserEmpty, _tl.ChatEmpty))} if not isinstance(x, (_tl.UserEmpty, _tl.ChatEmpty))}
messages = {} messages = {
for m in r.messages: _dialog_message_key(m.peer_id, m.id): _custom.Message._new(self.client, m, entities, None)
m._finish_init(self.client, entities, None) for m in r.messages
messages[_dialog_message_key(m.peer_id, m.id)] = m }
for d in r.dialogs: for d in r.dialogs:
# We check the offset date here because Telegram may ignore it # We check the offset date here because Telegram may ignore it

View File

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

View File

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

View File

@ -3783,6 +3783,9 @@ class TelegramClient:
async def _handle_auto_reconnect(self: 'TelegramClient'): async def _handle_auto_reconnect(self: 'TelegramClient'):
return await updates._handle_auto_reconnect(**locals()) return await updates._handle_auto_reconnect(**locals())
def _self_id(self: 'TelegramClient') -> typing.Optional[int]:
return users._self_id(**locals())
# endregion Private # endregion Private
# TODO re-patch everything to remove the intermediate calls # 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, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._entity_cache) self.sender_id, self._entities, client._entity_cache)
for msg in self.messages: self.messages = [
msg._finish_init(client, self._entities, None) _custom.Message._new(client, m, self._entities, None)
for m in self.messages
]
if len(self.messages) == 1: if len(self.messages) == 1:
# This will require hacks to be a proper album event # 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 .common import EventBuilder, EventCommon, name_inner_event
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
from ..types import _custom
@name_inner_event @name_inner_event

View File

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

View File

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