Merge pull request #830 from LonamiWebs/new-message

This commit is contained in:
Lonami 2018-06-02 12:53:05 +02:00 committed by GitHub
commit 12812ea542
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 749 additions and 366 deletions

View File

@ -19,3 +19,21 @@ telethon\.tl\.custom\.dialog module
:members:
:undoc-members:
:show-inheritance:
telethon\.tl\.custom\.message module
------------------------------------
.. automodule:: telethon.tl.custom.message
:members:
:undoc-members:
:show-inheritance:
telethon\.tl\.custom\.messagebutton module
------------------------------------------
.. automodule:: telethon.tl.custom.messagebutton
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,6 +1,6 @@
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types, functions
from ..tl import types, functions, custom
@name_inner_event
@ -158,6 +158,12 @@ class ChatAction(EventBuilder):
self.new_title = new_title
self.unpin = unpin
def _set_client(self, client):
super()._set_client(client)
if self.action_message:
self.action_message = custom.Message(
client, self.action_message, self._entities, None)
def respond(self, *args, **kwargs):
"""
Responds to the chat action message (not as a reply). Shorthand for
@ -198,8 +204,8 @@ class ChatAction(EventBuilder):
@property
def pinned_message(self):
"""
If ``new_pin`` is ``True``, this returns the (:tl:`Message`)
object that was pinned.
If ``new_pin`` is ``True``, this returns the
`telethon.tl.custom.message.Message` object that was pinned.
"""
if self._pinned_message == 0:
return None

View File

@ -103,6 +103,12 @@ class EventCommon(abc.ABC):
)
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
def _get_entity(self, msg_id, entity_id, chat=None):
"""
Helper function to call :tl:`GetMessages` on the give msg_id and

View File

@ -91,7 +91,8 @@ class MessageRead(EventBuilder):
@property
def messages(self):
"""
The list of :tl:`Message` **which contents'** were read.
The list of `telethon.tl.custom.message.Message`
**which contents'** were read.
Use :meth:`is_read` if you need to check whether a message
was read instead checking if it's in here.
@ -100,16 +101,9 @@ class MessageRead(EventBuilder):
chat = self.input_chat
if not chat:
self._messages = []
elif isinstance(chat, types.InputPeerChannel):
self._messages =\
self._client(functions.channels.GetMessagesRequest(
chat, self._message_ids
)).messages
else:
self._messages =\
self._client(functions.messages.GetMessagesRequest(
self._message_ids
)).messages
self._messages = self._client.get_messages(
chat, ids=self._message_ids)
return self._messages

View File

@ -1,9 +1,7 @@
import re
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..extensions import markdown
from ..tl import types, functions
from ..tl import types, custom
@name_inner_event
@ -116,23 +114,19 @@ class NewMessage(EventBuilder):
class Event(EventCommon):
"""
Represents the event of a new message.
Represents the event of a new message. This event can be treated
to all effects as a `telethon.tl.custom.message.Message`, so please
**refer to its documentation** to know what you can do with this event.
Members:
message (:tl:`Message`):
This is the original :tl:`Message` object.
This is the only difference with the received
`telethon.tl.custom.message.Message`, and will
return the `telethon.tl.custom.message.Message` itself,
not the text.
is_private (`bool`):
True if the message was sent as a private message.
is_group (`bool`):
True if the message was sent on a group or megagroup.
is_channel (`bool`):
True if the message was sent on a megagroup or channel.
is_reply (`str`):
Whether the message is a reply to some other or not.
See `telethon.tl.custom.message.Message` for the rest of
available members and methods.
"""
def __init__(self, message):
if not message.out and isinstance(message.to_id, types.PeerUser):
@ -146,277 +140,11 @@ class NewMessage(EventBuilder):
msg_id=message.id, broadcast=bool(message.post))
self.message = message
self._text = None
self._input_sender = None
self._sender = None
def _set_client(self, client):
super()._set_client(client)
self.message = custom.Message(
client, self.message, self._entities, None)
self.is_reply = bool(message.reply_to_msg_id)
self._reply_message = None
def respond(self, *args, **kwargs):
"""
Responds to the message (not as a reply). Shorthand for
`telethon.telegram_client.TelegramClient.send_message` with
``entity`` already set.
"""
return self._client.send_message(self.input_chat, *args, **kwargs)
def reply(self, *args, **kwargs):
"""
Replies to the message (as a reply). Shorthand for
`telethon.telegram_client.TelegramClient.send_message` with
both ``entity`` and ``reply_to`` already set.
"""
kwargs['reply_to'] = self.message.id
return self._client.send_message(self.input_chat, *args, **kwargs)
def forward_to(self, *args, **kwargs):
"""
Forwards the message. Shorthand for
`telethon.telegram_client.TelegramClient.forward_messages` with
both ``messages`` and ``from_peer`` already set.
"""
kwargs['messages'] = self.message.id
kwargs['from_peer'] = self.input_chat
return self._client.forward_messages(*args, **kwargs)
def edit(self, *args, **kwargs):
"""
Edits the message iff it's outgoing. Shorthand for
`telethon.telegram_client.TelegramClient.edit_message` with
both ``entity`` and ``message`` already set.
Returns ``None`` if the message was incoming, or the edited
:tl:`Message` otherwise.
"""
if self.message.fwd_from:
return None
if not self.message.out:
if not isinstance(self.message.to_id, types.PeerUser):
return None
me = self._client.get_me(input_peer=True)
if self.message.to_id.user_id != me.user_id:
return None
return self._client.edit_message(self.input_chat,
self.message,
*args, **kwargs)
def delete(self, *args, **kwargs):
"""
Deletes the message. You're responsible for checking whether you
have the permission to do so, or to except the error otherwise.
Shorthand for
`telethon.telegram_client.TelegramClient.delete_messages` with
``entity`` and ``message_ids`` already set.
"""
return self._client.delete_messages(self.input_chat,
[self.message],
*args, **kwargs)
@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.get_input_entity(
self.message.from_id
)
except (ValueError, TypeError):
# We can rely on self.input_chat for this
self._sender, self._input_sender = self._get_entity(
self.message.id,
self.message.from_id,
chat=self.input_chat
)
return self._input_sender
@property
def sender(self):
"""
This (:tl:`User`) may make an API call the first time to get
the most up to date version of the sender (mostly when the event
doesn't belong to a channel), so keep that in mind.
``input_sender`` needs to be available (often the case).
"""
if not self.input_sender:
return None
if self._sender is None:
self._sender = \
self._entities.get(utils.get_peer_id(self._input_sender))
if self._sender is None:
self._sender = self._client.get_entity(self._input_sender)
return self._sender
@property
def sender_id(self):
"""
Returns the marked sender integer ID, if present.
"""
return self.message.from_id
@property
def text(self):
"""
The message text, markdown-formatted.
"""
if self._text is None:
if not self.message.entities:
return self.message.message
self._text = markdown.unparse(self.message.message,
self.message.entities or [])
return self._text
@property
def raw_text(self):
"""
The raw message text, ignoring any formatting.
"""
return self.message.message
@property
def reply_message(self):
"""
This optional :tl:`Message` will make an API call the first
time to get the full :tl:`Message` object that one was replying to,
so use with care as there is no caching besides local caching yet.
"""
if not self.message.reply_to_msg_id:
return None
if self._reply_message is None:
if isinstance(self.input_chat, types.InputPeerChannel):
r = self._client(functions.channels.GetMessagesRequest(
self.input_chat, [self.message.reply_to_msg_id]
))
else:
r = self._client(functions.messages.GetMessagesRequest(
[self.message.reply_to_msg_id]
))
if not isinstance(r, types.messages.MessagesNotModified):
self._reply_message = r.messages[0]
return self._reply_message
@property
def forward(self):
"""
The unmodified :tl:`MessageFwdHeader`, if present..
"""
return self.message.fwd_from
@property
def media(self):
"""
The unmodified :tl:`MessageMedia`, if present.
"""
return self.message.media
@property
def photo(self):
"""
If the message media is a photo,
this returns the :tl:`Photo` object.
"""
if isinstance(self.message.media, types.MessageMediaPhoto):
photo = self.message.media.photo
if isinstance(photo, types.Photo):
return photo
@property
def document(self):
"""
If the message media is a document,
this returns the :tl:`Document` object.
"""
if isinstance(self.message.media, types.MessageMediaDocument):
doc = self.message.media.document
if isinstance(doc, types.Document):
return doc
def _document_by_attribute(self, kind, condition=None):
"""
Helper method to return the document only if it has an attribute
that's an instance of the given kind, and passes the condition.
"""
doc = self.document
if doc:
for attr in doc.attributes:
if isinstance(attr, kind):
if not condition or condition(doc):
return doc
@property
def audio(self):
"""
If the message media is a document with an Audio attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeAudio,
lambda attr: not attr.voice)
@property
def voice(self):
"""
If the message media is a document with a Voice attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeAudio,
lambda attr: attr.voice)
@property
def video(self):
"""
If the message media is a document with a Video attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeVideo)
@property
def video_note(self):
"""
If the message media is a document with a Video attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeVideo,
lambda attr: attr.round_message)
@property
def gif(self):
"""
If the message media is a document with an Animated attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeAnimated)
@property
def sticker(self):
"""
If the message media is a document with a Sticker attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeSticker)
@property
def out(self):
"""
Whether the message is outgoing (i.e. you sent it from
another session) or incoming (i.e. someone else sent it).
"""
return self.message.out
def __getattr__(self, item):
return getattr(self.message, item)

View File

@ -89,10 +89,11 @@ from .tl.types import (
MessageMediaWebPage, ChannelParticipantsSearch, PhotoSize, PhotoCachedSize,
PhotoSizeEmpty, MessageService, ChatParticipants, User, WebPage,
ChannelParticipantsBanned, ChannelParticipantsKicked,
InputMessagesFilterEmpty
InputMessagesFilterEmpty, UpdatesCombined
)
from .tl.types.messages import DialogsSlice
from .tl.types.account import PasswordInputSettings, NoPassword
from .tl import custom
from .extensions import markdown, html
__log__ = logging.getLogger(__name__)
@ -599,9 +600,10 @@ class TelegramClient(TelegramBareClient):
if _total:
_total[0] = getattr(r, 'count', len(r.dialogs))
messages = {m.id: m for m in r.messages}
entities = {utils.get_peer_id(x): x
for x in itertools.chain(r.users, r.chats)}
messages = {m.id: custom.Message(self, m, entities, None)
for m in r.messages}
# Happens when there are pinned dialogs
if len(r.dialogs) > limit:
@ -652,8 +654,7 @@ class TelegramClient(TelegramBareClient):
"""
return list(self.iter_drafts())
@staticmethod
def _get_response_message(request, result):
def _get_response_message(self, request, result, input_chat):
"""
Extracts the response message known a request and Update result.
The request may also be the ID of the message to match.
@ -672,26 +673,36 @@ class TelegramClient(TelegramBareClient):
if isinstance(result, UpdateShort):
updates = [result.update]
elif isinstance(result, Updates):
entities = {}
elif isinstance(result, (Updates, UpdatesCombined)):
updates = result.updates
entities = {utils.get_peer_id(x): x
for x in itertools.chain(result.users, result.chats)}
else:
return
found = None
for update in updates:
if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)):
if update.message.id == msg_id:
return update.message
found = update.message
break
elif (isinstance(update, UpdateEditMessage) and
not isinstance(request.peer, InputPeerChannel)):
if request.id == update.message.id:
return update.message
found = update.message
break
elif (isinstance(update, UpdateEditChannelMessage) and
utils.get_peer_id(request.peer) ==
utils.get_peer_id(update.message.to_id)):
if request.id == update.message.id:
return update.message
found = update.message
break
if found:
return custom.Message(self, found, entities, input_chat)
def _parse_message_text(self, message, parse_mode):
"""
@ -789,7 +800,7 @@ class TelegramClient(TelegramBareClient):
Has no effect when sending a file.
Returns:
The sent :tl:`Message`.
The sent `telethon.tl.custom.message.Message`.
"""
if file is not None:
return self.send_file(
@ -839,17 +850,18 @@ class TelegramClient(TelegramBareClient):
result = self(request)
if isinstance(result, UpdateShortSentMessage):
return Message(
to_id, cls = utils.resolve_id(utils.get_peer_id(entity))
return custom.Message(self, Message(
id=result.id,
to_id=entity,
to_id=cls(to_id),
message=message,
date=result.date,
out=result.out,
media=result.media,
entities=result.entities
)
), {}, input_chat=entity)
return self._get_response_message(request, result)
return self._get_response_message(request, result, entity)
def forward_messages(self, entity, messages, from_peer=None):
"""
@ -868,8 +880,8 @@ class TelegramClient(TelegramBareClient):
order for the forward to work.
Returns:
The list of forwarded :tl:`Message`, or a single one if a list
wasn't provided as input.
The list of forwarded `telethon.tl.custom.message.Message`,
or a single one if a list wasn't provided as input.
"""
single = not utils.is_list_like(messages)
if single:
@ -895,13 +907,20 @@ class TelegramClient(TelegramBareClient):
to_peer=entity
)
result = self(req)
if isinstance(result, (Updates, UpdatesCombined)):
entities = {utils.get_peer_id(x): x
for x in itertools.chain(result.users, result.chats)}
else:
entities = {}
random_to_id = {}
id_to_message = {}
for update in result.updates:
if isinstance(update, UpdateMessageID):
random_to_id[update.random_id] = update.id
elif isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage)):
id_to_message[update.message.id] = update.message
id_to_message[update.message.id] = custom.Message(
self, update.message, entities, input_chat=entity)
result = [id_to_message[random_to_id[rnd]] for rnd in req.random_id]
return result[0] if single else result
@ -955,23 +974,23 @@ class TelegramClient(TelegramBareClient):
not modified at all.
Returns:
The edited :tl:`Message`.
The edited `telethon.tl.custom.message.Message`.
"""
if isinstance(entity, Message):
text = message # Shift the parameters to the right
message = entity
entity = entity.to_id
entity = self.get_input_entity(entity)
text, msg_entities = self._parse_message_text(text, parse_mode)
request = EditMessageRequest(
peer=self.get_input_entity(entity),
peer=entity,
id=self._get_message_id(message),
message=text,
no_webpage=not link_preview,
entities=msg_entities
)
result = self(request)
return self._get_response_message(request, result)
return self._get_response_message(request, self(request), entity)
def delete_messages(self, entity, message_ids, revoke=True):
"""
@ -1090,12 +1109,7 @@ class TelegramClient(TelegramBareClient):
A single-item list to pass the total parameter by reference.
Yields:
Instances of :tl:`Message` with extra attributes:
* ``.sender`` = entity of the sender.
* ``.fwd_from.sender`` = if fwd_from, who sent it originally.
* ``.fwd_from.channel`` = if fwd_from, original channel.
* ``.to`` = entity to which the message was sent.
Instances of `telethon.tl.custom.message.Message`.
Notes:
Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to
@ -1104,7 +1118,11 @@ class TelegramClient(TelegramBareClient):
an higher limit, so you're free to set the ``batch_size`` that
you think may be good.
"""
entity = self.get_input_entity(entity)
# It's possible to get messages by ID without their entity, so only
# fetch the input version if we're not using IDs or if it was given.
if not ids or entity:
entity = self.get_input_entity(entity)
if ids:
if not utils.is_list_like(ids):
ids = (ids,)
@ -1189,8 +1207,7 @@ class TelegramClient(TelegramBareClient):
# IDs are returned in descending order.
last_id = message.id
self._make_message_friendly(message, entities)
yield message
yield custom.Message(self, message, entities, entity)
have += 1
if len(r.messages) < request.limit:
@ -1204,34 +1221,6 @@ class TelegramClient(TelegramBareClient):
time.sleep(max(wait_time - (time.time() - start), 0))
@staticmethod
def _make_message_friendly(message, entities):
"""
Add a few extra attributes to the :tl:`Message` to be friendlier.
To make messages more friendly, always add message
to service messages, and action to normal messages.
"""
# TODO Create an actual friendlier class
message.message = getattr(message, 'message', None)
message.action = getattr(message, 'action', None)
message.to = entities[utils.get_peer_id(message.to_id)]
message.sender = (
None if not message.from_id else
entities[utils.get_peer_id(message.from_id)]
)
if getattr(message, 'fwd_from', None):
message.fwd_from.sender = (
None if not message.fwd_from.from_id else
entities[utils.get_peer_id(message.fwd_from.from_id)]
)
message.fwd_from.channel = (
None if not message.fwd_from.channel_id else
entities[utils.get_peer_id(
PeerChannel(message.fwd_from.channel_id)
)]
)
def _iter_ids(self, entity, ids, total):
"""
Special case for `iter_messages` when it should only fetch some IDs.
@ -1253,8 +1242,7 @@ class TelegramClient(TelegramBareClient):
if isinstance(message, MessageEmpty):
yield None
else:
self._make_message_friendly(message, entities)
yield message
yield custom.Message(self, message, entities, entity)
def get_messages(self, *args, **kwargs):
"""
@ -1355,6 +1343,9 @@ class TelegramClient(TelegramBareClient):
if isinstance(message, int):
return message
if isinstance(message, custom.Message):
return message.original_message.id
try:
if message.SUBCLASS_OF_ID == 0x790009e3:
# hex(crc32(b'Message')) = 0x790009e3
@ -1614,8 +1605,8 @@ class TelegramClient(TelegramBareClient):
it will be used to determine metadata from audio and video files.
Returns:
The :tl:`Message` (or messages) containing the sent file,
or messages if a list of them was passed.
The `telethon.tl.custom.message.Message` (or messages) containing
the sent file, or messages if a list of them was passed.
"""
# First check if the user passed an iterable, in which case
# we may want to send as an album if all are photo files.
@ -1676,7 +1667,8 @@ class TelegramClient(TelegramBareClient):
reply_to_msg_id=reply_to,
message=caption,
entities=msg_entities)
return self._get_response_message(request, self(request))
return self._get_response_message(request, self(request),
entity)
as_image = utils.is_image(file) and not force_document
use_cache = InputPhoto if as_image else InputDocument
@ -1774,7 +1766,7 @@ class TelegramClient(TelegramBareClient):
# send the media message to the desired entity.
request = SendMediaRequest(entity, media, reply_to_msg_id=reply_to,
message=caption, entities=msg_entities)
msg = self._get_response_message(request, self(request))
msg = self._get_response_message(request, self(request), entity)
if msg and isinstance(file_handle, InputSizedFile):
# There was a response message and we didn't use cached
# version, so cache whatever we just sent to the database.
@ -1840,7 +1832,7 @@ class TelegramClient(TelegramBareClient):
entity, reply_to_msg_id=reply_to, multi_media=media
))
return [
self._get_response_message(update.id, result)
self._get_response_message(update.id, result, entity)
for update in result.updates
if isinstance(update, UpdateMessageID)
]
@ -2423,7 +2415,7 @@ class TelegramClient(TelegramBareClient):
for builder, callback in self._event_builders:
event = builder.build(update)
if event:
event._client = self
event._set_client(self)
event.original_update = update
try:
callback(event)

View File

@ -1,3 +1,5 @@
from .draft import Draft
from .dialog import Dialog
from .input_sized_file import InputSizedFile
from .messagebutton import MessageButton
from .message import Message

View File

@ -0,0 +1,564 @@
from .. import types
from ...extensions import markdown
from ...utils import get_input_peer, get_peer_id
from .messagebutton import MessageButton
class Message:
"""
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).
Attributes:
original_message (:tl:`Message`):
The original :tl:`Message` object.
Any other attribute:
Attributes not described here are the same as those available
in the original :tl:`Message`.
"""
def __init__(self, client, original, entities, input_chat):
self.original_message = original
self.stringify = self.original_message.stringify
self.to_dict = self.original_message.to_dict
self._client = client
self._text = None
self._reply_message = None
self._buttons = None
self._buttons_flat = None
self._sender = entities.get(self.original_message.from_id)
self._chat = entities.get(get_peer_id(self.original_message.to_id))
if self._sender:
self._input_sender = get_input_peer(self._sender)
else:
self._input_sender = None
self._input_chat = input_chat
self._fwd_from_entity = None
if getattr(self.original_message, 'fwd_from', None):
fwd = self.original_message.fwd_from
if fwd.from_id:
self._fwd_from_entity = entities.get(fwd.from_id)
elif fwd.channel_id:
self._fwd_from_entity = entities.get(get_peer_id(
types.PeerChannel(fwd.channel_id)))
def __new__(cls, client, original, entities, input_chat):
if isinstance(original, types.Message):
return super().__new__(_CustomMessage)
elif isinstance(original, types.MessageService):
return super().__new__(_CustomMessageService)
else:
return cls
def __getattr__(self, item):
return getattr(self.original_message, item)
def __str__(self):
return str(self.original_message)
def __repr__(self):
return repr(self.original_message)
def __bytes__(self):
return bytes(self.original_message)
@property
def client(self):
"""
Returns the `telethon.telegram_client.TelegramClient` instance that
created this instance.
"""
return self._client
@property
def text(self):
"""
The message text, markdown-formatted.
Will be ``None`` for :tl:`MessageService`.
"""
if self._text is None\
and isinstance(self.original_message, types.Message):
if not self.original_message.entities:
return self.original_message.message
self._text = markdown.unparse(self.original_message.message,
self.original_message.entities or [])
return self._text
@property
def raw_text(self):
"""
The raw message text, ignoring any formatting.
Will be ``None`` for :tl:`MessageService`.
"""
if isinstance(self.original_message, types.Message):
return self.original_message.message
@property
def message(self):
"""
The raw message text, ignoring any formatting.
Will be ``None`` for :tl:`MessageService`.
"""
return self.raw_text
@property
def action(self):
"""
The :tl:`MessageAction` for the :tl:`MessageService`.
Will be ``None`` for :tl:`Message`.
"""
if isinstance(self.original_message, types.MessageService):
return self.original_message.action
def _reload_message(self):
"""
Re-fetches this message to reload the sender and chat entities,
along with their input versions.
"""
try:
chat = self.input_chat if self.is_channel else None
msg = self._client.get_messages(chat, ids=self.original_message.id)
except ValueError:
return # We may not have the input chat/get message failed
if not msg:
return # The message may be deleted and it will be None
self._sender = msg._sender
self._input_sender = msg._input_sender
self._chat = msg._chat
self._input_chat = msg._input_chat
@property
def sender(self):
"""
This (:tl:`User`) may make an API call the first time to get
the most up to date version of the sender (mostly when the event
doesn't belong to a channel), so keep that in mind.
`input_sender` needs to be available (often the case).
"""
if self._sender is None:
try:
self._sender = self._client.get_entity(self.input_sender)
except ValueError:
self._reload_message()
return self._sender
@property
def chat(self):
if self._chat is None:
try:
self._chat = self._client.get_entity(self.input_chat)
except ValueError:
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
if self._sender is not None:
self._input_sender = get_input_peer(self._sender)
else:
try:
self._input_sender = self._client.get_input_entity(
self.original_message.from_id)
except ValueError:
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, and it may fetch the dialogs to try
to find it in the worst case.
"""
if self._input_chat is None:
if self._chat is None:
try:
self._chat = self._client.get_input_entity(
self.original_message.to_id)
except ValueError:
# 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
for d in self._client.iter_dialogs(100):
if d.id == target:
self._chat = d.entity
break
if self._chat is not None:
self._input_chat = get_input_peer(self._chat)
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.
"""
return get_peer_id(self.original_message.to_id)
@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 not self.original_message.broadcast and isinstance(
self.original_message.to_id, (types.PeerChat, types.PeerChannel))
@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
def is_reply(self):
"""True if the message is a reply to some other or not."""
return bool(self.original_message.reply_to_msg_id)
@property
def buttons(self):
"""
Returns a matrix (list of lists) containing all buttons of the message
as `telethon.tl.custom.messagebutton.MessageButton` instances.
"""
if self._buttons is None and self.original_message.reply_markup:
if isinstance(self.original_message.reply_markup, (
types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)):
self._buttons = [[
MessageButton(self._client, button, self.input_sender,
self.input_chat, self.original_message.id)
for button in row.buttons
] for row in self.original_message.reply_markup.rows]
self._buttons_flat = [x for row in self._buttons for x in row]
return self._buttons
@property
def button_count(self):
"""
Returns the total button count.
"""
return len(self._buttons_flat) if self.buttons else 0
@property
def photo(self):
"""
If the message media is a photo,
this returns the :tl:`Photo` object.
"""
if isinstance(self.original_message.media, types.MessageMediaPhoto):
photo = self.original_message.media.photo
if isinstance(photo, types.Photo):
return photo
@property
def document(self):
"""
If the message media is a document,
this returns the :tl:`Document` object.
"""
if isinstance(self.original_message.media, types.MessageMediaDocument):
doc = self.original_message.media.document
if isinstance(doc, types.Document):
return doc
def _document_by_attribute(self, kind, condition=None):
"""
Helper method to return the document only if it has an attribute
that's an instance of the given kind, and passes the condition.
"""
doc = self.document
if doc:
for attr in doc.attributes:
if isinstance(attr, kind):
if not condition or condition(doc):
return doc
@property
def audio(self):
"""
If the message media is a document with an Audio attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeAudio,
lambda attr: not attr.voice)
@property
def voice(self):
"""
If the message media is a document with a Voice attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeAudio,
lambda attr: attr.voice)
@property
def video(self):
"""
If the message media is a document with a Video attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeVideo)
@property
def video_note(self):
"""
If the message media is a document with a Video attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeVideo,
lambda attr: attr.round_message)
@property
def gif(self):
"""
If the message media is a document with an Animated attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeAnimated)
@property
def sticker(self):
"""
If the message media is a document with a Sticker attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeSticker)
@property
def out(self):
"""
Whether the message is outgoing (i.e. you sent it from
another session) or incoming (i.e. someone else sent it).
"""
return self.original_message.out
@property
def reply_message(self):
"""
The `telethon.tl.custom.message.Message` that this message is replying
to, or ``None``.
Note that this will make a network call to fetch the message and
will later be cached.
"""
if self._reply_message is None:
if not self.original_message.reply_to_msg_id:
return None
self._reply_message = self._client.get_messages(
self.input_chat if self.is_channel else None,
ids=self.original_message.reply_to_msg_id
)
return self._reply_message
@property
def fwd_from_entity(self):
"""
If the :tl:`Message` is a forwarded message, returns the :tl:`User`
or :tl:`Channel` who originally sent the message, or ``None``.
"""
if self._fwd_from_entity is None:
if getattr(self.original_message, 'fwd_from', None):
fwd = self.original_message.fwd_from
if fwd.from_id:
self._fwd_from_entity = self._client.get_entity(
fwd.from_id)
elif fwd.channel_id:
self._fwd_from_entity = self._client.get_entity(
get_peer_id(types.PeerChannel(fwd.channel_id)))
return self._fwd_from_entity
def respond(self, *args, **kwargs):
"""
Responds to the message (not as a reply). Shorthand for
`telethon.telegram_client.TelegramClient.send_message` with
``entity`` already set.
"""
return self._client.send_message(self.input_chat, *args, **kwargs)
def reply(self, *args, **kwargs):
"""
Replies to the message (as a reply). Shorthand for
`telethon.telegram_client.TelegramClient.send_message` with
both ``entity`` and ``reply_to`` already set.
"""
kwargs['reply_to'] = self.original_message.id
return self._client.send_message(self.original_message.to_id,
*args, **kwargs)
def forward_to(self, *args, **kwargs):
"""
Forwards the message. Shorthand for
`telethon.telegram_client.TelegramClient.forward_messages` with
both ``messages`` and ``from_peer`` already set.
If you need to forward more than one message at once, don't use
this `forward_to` method. Use a
`telethon.telegram_client.TelegramClient` instance directly.
"""
kwargs['messages'] = self.original_message.id
kwargs['from_peer'] = self.input_chat
return self._client.forward_messages(*args, **kwargs)
def edit(self, *args, **kwargs):
"""
Edits the message iff it's outgoing. Shorthand for
`telethon.telegram_client.TelegramClient.edit_message` with
both ``entity`` and ``message`` already set.
Returns ``None`` if the message was incoming, or the edited
:tl:`Message` otherwise.
"""
if self.original_message.fwd_from:
return None
if not self.original_message.out:
if not isinstance(self.original_message.to_id, types.PeerUser):
return None
me = self._client.get_me(input_peer=True)
if self.original_message.to_id.user_id != me.user_id:
return None
return self._client.edit_message(
self.input_chat, self.original_message, *args, **kwargs)
def delete(self, *args, **kwargs):
"""
Deletes the message. You're responsible for checking whether you
have the permission to do so, or to except the error otherwise.
Shorthand for
`telethon.telegram_client.TelegramClient.delete_messages` with
``entity`` and ``message_ids`` already set.
If you need to delete more than one message at once, don't use
this `delete` method. Use a
`telethon.telegram_client.TelegramClient` instance directly.
"""
return self._client.delete_messages(
self.input_chat, [self.original_message], *args, **kwargs)
def download_media(self, *args, **kwargs):
"""
Downloads the media contained in the message, if any.
`telethon.telegram_client.TelegramClient.download_media` with
the ``message`` already set.
"""
return self._client.download_media(self.original_message,
*args, **kwargs)
def get_entities_text(self):
"""
Returns a list of tuples [(:tl:`MessageEntity`, `str`)], the string
being the inner text of the message entity (like bold, italics, etc).
"""
texts = markdown.get_inner_text(self.original_message.message,
self.original_message.entities)
return list(zip(self.original_message.entities, texts))
def click(self, i=None, j=None, *, text=None, filter=None):
"""
Clicks the inline keyboard button of the message, if any.
If the message has a non-inline keyboard, clicking it will
send the message, switch to inline, or open its URL.
Does nothing if the message has no buttons.
Args:
i (`int`):
Clicks the i'th button (starting from the index 0).
Will ``raise IndexError`` if out of bounds. Example:
>>> message = Message(...)
>>> # Clicking the 3rd button
>>> # [button1] [button2]
>>> # [ button3 ]
>>> # [button4] [button5]
>>> message.click(2) # index
j (`int`):
Clicks the button at position (i, j), these being the
indices for the (row, column) respectively. Example:
>>> # Clicking the 2nd button on the 1st row.
>>> # [button1] [button2]
>>> # [ button3 ]
>>> # [button4] [button5]
>>> message.click(0, 1) # (row, column)
This is equivalent to ``message.buttons[0][1].click()``.
text (`str` | `callable`):
Clicks the first button with the text "text". This may
also be a callable, like a ``re.compile(...).match``,
and the text will be passed to it.
filter (`callable`):
Clicks the first button for which the callable
returns ``True``. The callable should accept a single
`telethon.tl.custom.messagebutton.MessageButton` argument.
"""
if (i, text, filter).count(None) >= 2:
raise ValueError('You can only set either of i, text or filter')
if not self.buttons:
return # Accessing the property sets self._buttons[_flat]
if text is not None:
if callable(text):
for button in self._buttons_flat:
if text(button.text):
return button.click()
else:
for button in self._buttons_flat:
if button.text == text:
return button.click()
return
if filter is not None:
for button in self._buttons_flat:
if filter(button):
return button.click()
return
if i is None:
i = 0
if j is None:
return self._buttons_flat[i].click()
else:
return self._buttons[i][j].click()
class _CustomMessage(Message, types.Message):
pass
class _CustomMessageService(Message, types.MessageService):
pass

View File

@ -0,0 +1,73 @@
from .. import types, functions
import webbrowser
class MessageButton:
"""
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).
Attributes:
button (:tl:`KeyboardButton`):
The original :tl:`KeyboardButton` object.
"""
def __init__(self, client, original, from_user, chat, msg_id):
self.button = original
self._from = from_user
self._chat = chat
self._msg_id = msg_id
self._client = client
@property
def client(self):
"""
Returns the `telethon.telegram_client.TelegramClient` instance that
created this instance.
"""
return self._client
@property
def text(self):
"""The text string of the button."""
return self.button.text
@property
def data(self):
"""The ``bytes`` data for :tl:`KeyboardButtonCallback` objects."""
if isinstance(self.button, types.KeyboardButtonCallback):
return self.button.data
@property
def inline_query(self):
"""The query ``str`` for :tl:`KeyboardButtonSwitchInline` objects."""
if isinstance(self.button, types.KeyboardButtonSwitchInline):
return self.button.query
@property
def url(self):
"""The url ``str`` for :tl:`KeyboardButtonUrl` objects."""
if isinstance(self.button, types.KeyboardButtonUrl):
return self.button.url
def click(self):
"""
Clicks the inline keyboard button of the message, if any.
If the message has a non-inline keyboard, clicking it will
send the message, switch to inline, or open its URL.
"""
if isinstance(self.button, types.KeyboardButton):
return self._client.send_message(
self._chat, self.button.text, reply_to=self._msg_id)
elif isinstance(self.button, types.KeyboardButtonCallback):
return self._client(functions.messages.GetBotCallbackAnswerRequest(
peer=self._chat, msg_id=self._msg_id, data=self.button.data
), retries=1)
elif isinstance(self.button, types.KeyboardButtonSwitchInline):
return self._client(functions.messages.StartBotRequest(
bot=self._from, peer=self._chat, start_param=self.button.query
), retries=1)
elif isinstance(self.button, types.KeyboardButtonUrl):
return webbrowser.open(self.button.url)