2018-02-07 12:41:58 +03:00
|
|
|
import abc
|
2018-02-09 14:42:04 +03:00
|
|
|
import datetime
|
2018-02-07 15:45:17 +03:00
|
|
|
import itertools
|
2018-02-20 17:55:02 +03:00
|
|
|
import re
|
2018-02-07 15:45:17 +03:00
|
|
|
|
2018-02-07 12:41:58 +03:00
|
|
|
from .. import utils
|
2018-02-07 15:45:17 +03:00
|
|
|
from ..errors import RPCError
|
|
|
|
from ..extensions import markdown
|
|
|
|
from ..tl import types, functions
|
2018-02-07 12:41:58 +03:00
|
|
|
|
|
|
|
|
2018-02-17 12:40:01 +03:00
|
|
|
def _into_id_set(client, chats):
|
|
|
|
"""Helper util to turn the input chat or chats into a set of IDs."""
|
|
|
|
if chats is None:
|
|
|
|
return None
|
|
|
|
|
2018-02-26 16:12:21 +03:00
|
|
|
if not utils.is_list_like(chats):
|
2018-02-17 12:40:01 +03:00
|
|
|
chats = (chats,)
|
|
|
|
|
|
|
|
result = set()
|
|
|
|
for chat in chats:
|
|
|
|
chat = client.get_input_entity(chat)
|
|
|
|
if isinstance(chat, types.InputPeerSelf):
|
2018-02-22 23:01:18 +03:00
|
|
|
chat = client.get_me(input_peer=True)
|
2018-02-17 12:40:01 +03:00
|
|
|
result.add(utils.get_peer_id(chat))
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
2018-02-07 12:41:58 +03:00
|
|
|
class _EventBuilder(abc.ABC):
|
2018-02-17 13:29:16 +03:00
|
|
|
"""
|
|
|
|
The common event builder, with builtin support to filter per chat.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
chats (:obj:`entity`, optional):
|
|
|
|
May be one or more entities (username/peer/etc.). By default,
|
|
|
|
only matching chats will be handled.
|
|
|
|
|
|
|
|
blacklist_chats (:obj:`bool`, optional):
|
|
|
|
Whether to treat the the list of chats as a blacklist (if
|
|
|
|
it matches it will NOT be handled) or a whitelist (default).
|
|
|
|
"""
|
|
|
|
def __init__(self, chats=None, blacklist_chats=False):
|
|
|
|
self.chats = chats
|
|
|
|
self.blacklist_chats = blacklist_chats
|
2018-02-22 23:01:18 +03:00
|
|
|
self._self_id = None
|
2018-02-17 13:29:16 +03:00
|
|
|
|
2018-02-07 12:41:58 +03:00
|
|
|
@abc.abstractmethod
|
|
|
|
def build(self, update):
|
|
|
|
"""Builds an event for the given update if possible, or returns None"""
|
|
|
|
|
|
|
|
def resolve(self, client):
|
|
|
|
"""Helper method to allow event builders to be resolved before usage"""
|
2018-02-17 13:29:16 +03:00
|
|
|
self.chats = _into_id_set(client, self.chats)
|
2018-02-22 23:01:18 +03:00
|
|
|
self._self_id = client.get_me(input_peer=True).user_id
|
2018-02-17 13:29:16 +03:00
|
|
|
|
|
|
|
def _filter_event(self, event):
|
|
|
|
"""
|
|
|
|
If the ID of ``event._chat_peer`` isn't in the chats set (or it is
|
|
|
|
but the set is a blacklist) returns ``None``, otherwise the event.
|
|
|
|
"""
|
|
|
|
if self.chats is not None:
|
|
|
|
inside = utils.get_peer_id(event._chat_peer) in self.chats
|
|
|
|
if inside == self.blacklist_chats:
|
|
|
|
# If this chat matches but it's a blacklist ignore.
|
|
|
|
# If it doesn't match but it's a whitelist ignore.
|
|
|
|
return None
|
|
|
|
return event
|
2018-02-07 12:42:40 +03:00
|
|
|
|
|
|
|
|
2018-02-09 13:36:41 +03:00
|
|
|
class _EventCommon(abc.ABC):
|
|
|
|
"""Intermediate class with common things to all events"""
|
|
|
|
|
|
|
|
def __init__(self, chat_peer=None, msg_id=None, broadcast=False):
|
|
|
|
self._client = None
|
|
|
|
self._chat_peer = chat_peer
|
|
|
|
self._message_id = msg_id
|
|
|
|
self._input_chat = None
|
|
|
|
self._chat = None
|
|
|
|
|
2018-03-01 02:15:30 +03:00
|
|
|
self.pattern_match = None
|
|
|
|
|
2018-02-09 13:36:41 +03:00
|
|
|
self.is_private = isinstance(chat_peer, types.PeerUser)
|
|
|
|
self.is_group = (
|
|
|
|
isinstance(chat_peer, (types.PeerChat, types.PeerChannel))
|
|
|
|
and not broadcast
|
|
|
|
)
|
|
|
|
self.is_channel = isinstance(chat_peer, types.PeerChannel)
|
|
|
|
|
|
|
|
def _get_input_entity(self, msg_id, entity_id, chat=None):
|
|
|
|
"""
|
|
|
|
Helper function to call GetMessages on the give msg_id and
|
|
|
|
return the input entity whose ID is the given entity ID.
|
|
|
|
|
|
|
|
If ``chat`` is present it must be an InputPeer.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
if isinstance(chat, types.InputPeerChannel):
|
|
|
|
result = self._client(
|
|
|
|
functions.channels.GetMessagesRequest(chat, [msg_id])
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
result = self._client(
|
|
|
|
functions.messages.GetMessagesRequest([msg_id])
|
|
|
|
)
|
|
|
|
except RPCError:
|
|
|
|
return
|
|
|
|
entity = {
|
|
|
|
utils.get_peer_id(x): x for x in itertools.chain(
|
|
|
|
getattr(result, 'chats', []),
|
|
|
|
getattr(result, 'users', []))
|
|
|
|
}.get(entity_id)
|
|
|
|
if entity:
|
|
|
|
return utils.get_input_peer(entity)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def input_chat(self):
|
|
|
|
"""
|
|
|
|
The (:obj:`InputPeer`) (group, megagroup or channel) on which
|
|
|
|
the event occurred. This doesn't have the title or anything,
|
|
|
|
but is useful if you don't need those to avoid further
|
|
|
|
requests.
|
|
|
|
|
|
|
|
Note that this might be ``None`` if the library can't find it.
|
|
|
|
"""
|
|
|
|
|
|
|
|
if self._input_chat is None and self._chat_peer is not None:
|
|
|
|
try:
|
|
|
|
self._input_chat = self._client.get_input_entity(
|
|
|
|
self._chat_peer
|
|
|
|
)
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
# The library hasn't seen this chat, get the message
|
|
|
|
if not isinstance(self._chat_peer, types.PeerChannel):
|
|
|
|
# TODO For channels, getDifference? Maybe looking
|
|
|
|
# in the dialogs (which is already done) is enough.
|
|
|
|
if self._message_id is not None:
|
|
|
|
self._input_chat = self._get_input_entity(
|
|
|
|
self._message_id,
|
|
|
|
utils.get_peer_id(self._chat_peer)
|
|
|
|
)
|
|
|
|
return self._input_chat
|
|
|
|
|
|
|
|
@property
|
|
|
|
def chat(self):
|
|
|
|
"""
|
|
|
|
The (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional) on which
|
|
|
|
the event occurred. This property will make an API call the first time
|
|
|
|
to get the most up to date version of the chat, so use with care as
|
|
|
|
there is no caching besides local caching yet.
|
|
|
|
"""
|
|
|
|
if self._chat is None and self.input_chat:
|
|
|
|
self._chat = self._client.get_entity(self._input_chat)
|
|
|
|
return self._chat
|
|
|
|
|
|
|
|
|
2018-02-09 15:08:09 +03:00
|
|
|
class Raw(_EventBuilder):
|
|
|
|
"""
|
|
|
|
Represents a raw event. The event is the update itself.
|
|
|
|
"""
|
2018-02-25 12:36:53 +03:00
|
|
|
def resolve(self, client):
|
|
|
|
pass
|
|
|
|
|
2018-02-09 15:08:09 +03:00
|
|
|
def build(self, update):
|
|
|
|
return update
|
|
|
|
|
|
|
|
|
2018-02-07 12:42:40 +03:00
|
|
|
# Classes defined here are actually Event builders
|
|
|
|
# for their inner Event classes. Inner ._client is
|
|
|
|
# set later by the creator TelegramClient.
|
|
|
|
class NewMessage(_EventBuilder):
|
|
|
|
"""
|
|
|
|
Represents a new message event builder.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
incoming (:obj:`bool`, optional):
|
|
|
|
If set to ``True``, only **incoming** messages will be handled.
|
|
|
|
Mutually exclusive with ``outgoing`` (can only set one of either).
|
|
|
|
|
|
|
|
outgoing (:obj:`bool`, optional):
|
|
|
|
If set to ``True``, only **outgoing** messages will be handled.
|
|
|
|
Mutually exclusive with ``incoming`` (can only set one of either).
|
|
|
|
|
2018-02-20 17:55:02 +03:00
|
|
|
pattern (:obj:`str`, :obj:`callable`, :obj:`Pattern`, optional):
|
|
|
|
If set, only messages matching this pattern will be handled.
|
|
|
|
You can specify a regex-like string which will be matched
|
|
|
|
against the message, a callable function that returns ``True``
|
|
|
|
if a message is acceptable, or a compiled regex pattern.
|
2018-02-07 12:42:40 +03:00
|
|
|
"""
|
|
|
|
def __init__(self, incoming=None, outgoing=None,
|
2018-02-20 17:55:02 +03:00
|
|
|
chats=None, blacklist_chats=False, pattern=None):
|
2018-02-07 12:42:40 +03:00
|
|
|
if incoming and outgoing:
|
|
|
|
raise ValueError('Can only set either incoming or outgoing')
|
|
|
|
|
2018-02-17 13:29:16 +03:00
|
|
|
super().__init__(chats=chats, blacklist_chats=blacklist_chats)
|
2018-02-07 12:42:40 +03:00
|
|
|
self.incoming = incoming
|
|
|
|
self.outgoing = outgoing
|
2018-02-20 17:55:02 +03:00
|
|
|
if isinstance(pattern, str):
|
|
|
|
self.pattern = re.compile(pattern).match
|
|
|
|
elif not pattern or callable(pattern):
|
|
|
|
self.pattern = pattern
|
|
|
|
elif hasattr(pattern, 'match') and callable(pattern.match):
|
|
|
|
self.pattern = pattern.match
|
|
|
|
else:
|
|
|
|
raise TypeError('Invalid pattern type given')
|
2018-02-07 12:42:40 +03:00
|
|
|
|
|
|
|
def build(self, update):
|
|
|
|
if isinstance(update,
|
|
|
|
(types.UpdateNewMessage, types.UpdateNewChannelMessage)):
|
2018-02-07 16:06:36 +03:00
|
|
|
if not isinstance(update.message, types.Message):
|
|
|
|
return # We don't care about MessageService's here
|
2018-02-07 12:42:40 +03:00
|
|
|
event = NewMessage.Event(update.message)
|
|
|
|
elif isinstance(update, types.UpdateShortMessage):
|
|
|
|
event = NewMessage.Event(types.Message(
|
|
|
|
out=update.out,
|
|
|
|
mentioned=update.mentioned,
|
|
|
|
media_unread=update.media_unread,
|
|
|
|
silent=update.silent,
|
|
|
|
id=update.id,
|
|
|
|
to_id=types.PeerUser(update.user_id),
|
2018-02-22 23:01:18 +03:00
|
|
|
from_id=self._self_id if update.out else update.user_id,
|
2018-02-07 12:42:40 +03:00
|
|
|
message=update.message,
|
|
|
|
date=update.date,
|
|
|
|
fwd_from=update.fwd_from,
|
|
|
|
via_bot_id=update.via_bot_id,
|
|
|
|
reply_to_msg_id=update.reply_to_msg_id,
|
|
|
|
entities=update.entities
|
|
|
|
))
|
2018-02-17 13:40:38 +03:00
|
|
|
elif isinstance(update, types.UpdateShortChatMessage):
|
|
|
|
event = NewMessage.Event(types.Message(
|
|
|
|
out=update.out,
|
|
|
|
mentioned=update.mentioned,
|
|
|
|
media_unread=update.media_unread,
|
|
|
|
silent=update.silent,
|
|
|
|
id=update.id,
|
|
|
|
from_id=update.from_id,
|
|
|
|
to_id=types.PeerChat(update.chat_id),
|
|
|
|
message=update.message,
|
|
|
|
date=update.date,
|
|
|
|
fwd_from=update.fwd_from,
|
|
|
|
via_bot_id=update.via_bot_id,
|
|
|
|
reply_to_msg_id=update.reply_to_msg_id,
|
|
|
|
entities=update.entities
|
|
|
|
))
|
2018-02-07 12:42:40 +03:00
|
|
|
else:
|
|
|
|
return
|
|
|
|
|
|
|
|
# Short-circuit if we let pass all events
|
2018-02-20 17:55:02 +03:00
|
|
|
if all(x is None for x in (self.incoming, self.outgoing, self.chats,
|
|
|
|
self.pattern)):
|
2018-02-07 12:42:40 +03:00
|
|
|
return event
|
|
|
|
|
|
|
|
if self.incoming and event.message.out:
|
|
|
|
return
|
|
|
|
if self.outgoing and not event.message.out:
|
|
|
|
return
|
2018-03-01 02:15:30 +03:00
|
|
|
|
|
|
|
if self.pattern:
|
|
|
|
match = self.pattern(event.message.message or '')
|
|
|
|
if not match:
|
|
|
|
return
|
|
|
|
event.pattern_match = match
|
2018-02-07 12:42:40 +03:00
|
|
|
|
2018-02-17 13:29:16 +03:00
|
|
|
return self._filter_event(event)
|
2018-02-07 12:42:40 +03:00
|
|
|
|
2018-02-09 13:36:41 +03:00
|
|
|
class Event(_EventCommon):
|
2018-02-07 12:42:40 +03:00
|
|
|
"""
|
|
|
|
Represents the event of a new message.
|
|
|
|
|
|
|
|
Members:
|
|
|
|
message (:obj:`Message`):
|
|
|
|
This is the original ``Message`` object.
|
|
|
|
|
|
|
|
is_private (:obj:`bool`):
|
|
|
|
True if the message was sent as a private message.
|
|
|
|
|
|
|
|
is_group (:obj:`bool`):
|
|
|
|
True if the message was sent on a group or megagroup.
|
|
|
|
|
|
|
|
is_channel (:obj:`bool`):
|
|
|
|
True if the message was sent on a megagroup or channel.
|
|
|
|
|
|
|
|
is_reply (:obj:`str`):
|
|
|
|
Whether the message is a reply to some other or not.
|
|
|
|
"""
|
|
|
|
def __init__(self, message):
|
2018-02-26 14:14:21 +03:00
|
|
|
if not message.out and isinstance(message.to_id, types.PeerUser):
|
|
|
|
# Incoming message (e.g. from a bot) has to_id=us, and
|
|
|
|
# from_id=bot (the actual "chat" from an user's perspective).
|
|
|
|
chat_peer = types.PeerUser(message.from_id)
|
|
|
|
else:
|
|
|
|
chat_peer = message.to_id
|
|
|
|
|
|
|
|
super().__init__(chat_peer=chat_peer,
|
2018-02-09 13:36:41 +03:00
|
|
|
msg_id=message.id, broadcast=bool(message.post))
|
|
|
|
|
2018-02-07 12:42:40 +03:00
|
|
|
self.message = message
|
|
|
|
self._text = None
|
|
|
|
|
2018-02-07 15:45:17 +03:00
|
|
|
self._input_chat = None
|
2018-02-07 12:42:40 +03:00
|
|
|
self._chat = None
|
2018-02-07 15:45:17 +03:00
|
|
|
self._input_sender = None
|
2018-02-07 12:42:40 +03:00
|
|
|
self._sender = None
|
|
|
|
|
|
|
|
self.is_reply = bool(message.reply_to_msg_id)
|
|
|
|
self._reply_message = None
|
|
|
|
|
2018-02-07 15:55:41 +03:00
|
|
|
def respond(self, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Responds to the message (not as a reply). This is a shorthand for
|
|
|
|
``client.send_message(event.chat, ...)``.
|
|
|
|
"""
|
|
|
|
return self._client.send_message(self.input_chat, *args, **kwargs)
|
|
|
|
|
|
|
|
def reply(self, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Replies to the message (as a reply). This is a shorthand for
|
|
|
|
``client.send_message(event.chat, ..., reply_to=event.message.id)``.
|
|
|
|
"""
|
|
|
|
return self._client.send_message(self.input_chat,
|
|
|
|
reply_to=self.message.id,
|
|
|
|
*args, **kwargs)
|
2018-02-07 12:42:40 +03:00
|
|
|
|
2018-02-15 13:35:12 +03:00
|
|
|
def edit(self, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Edits the message iff it's outgoing. This is a shorthand for
|
|
|
|
``client.edit_message(event.chat, event.message, ...)``.
|
|
|
|
|
|
|
|
Returns ``None`` if the message was incoming,
|
|
|
|
or the edited message otherwise.
|
|
|
|
"""
|
|
|
|
if not self.message.out:
|
2018-02-22 23:01:18 +03:00
|
|
|
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
|
2018-02-15 13:35:12 +03:00
|
|
|
|
|
|
|
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.
|
|
|
|
This is a shorthand for
|
|
|
|
``client.delete_messages(event.chat, event.message, ...)``.
|
|
|
|
"""
|
|
|
|
return self._client.delete_messages(self.input_chat,
|
|
|
|
[self.message],
|
|
|
|
*args, **kwargs)
|
|
|
|
|
2018-02-07 12:42:40 +03:00
|
|
|
@property
|
|
|
|
def input_sender(self):
|
2018-02-09 17:56:42 +03:00
|
|
|
"""
|
|
|
|
This (:obj:`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
|
2018-02-13 12:24:35 +03:00
|
|
|
find the input chat, or if the message a broadcast on a channel.
|
2018-02-09 17:56:42 +03:00
|
|
|
"""
|
2018-02-07 15:45:17 +03:00
|
|
|
if self._input_sender is None:
|
2018-02-13 12:24:35 +03:00
|
|
|
if self.is_channel and not self.is_group:
|
|
|
|
return None
|
|
|
|
|
2018-02-07 15:45:17 +03:00
|
|
|
try:
|
|
|
|
self._input_sender = self._client.get_input_entity(
|
|
|
|
self.message.from_id
|
|
|
|
)
|
|
|
|
except (ValueError, TypeError):
|
2018-02-13 12:24:35 +03:00
|
|
|
# We can rely on self.input_chat for this
|
|
|
|
self._input_sender = self._get_input_entity(
|
|
|
|
self.message.id,
|
|
|
|
self.message.from_id,
|
|
|
|
chat=self.input_chat
|
|
|
|
)
|
2018-02-07 15:45:17 +03:00
|
|
|
|
2018-02-09 15:10:02 +03:00
|
|
|
return self._input_sender
|
2018-02-07 12:42:40 +03:00
|
|
|
|
|
|
|
@property
|
|
|
|
def sender(self):
|
2018-02-09 17:56:42 +03:00
|
|
|
"""
|
|
|
|
This (:obj:`User`) will make an API call the first time to get
|
|
|
|
the most up to date version of the sender, so use with care as
|
|
|
|
there is no caching besides local caching yet.
|
|
|
|
|
|
|
|
``input_sender`` needs to be available (often the case).
|
|
|
|
"""
|
2018-02-07 15:45:17 +03:00
|
|
|
if self._sender is None and self.input_sender:
|
|
|
|
self._sender = self._client.get_entity(self._input_sender)
|
2018-02-07 12:42:40 +03:00
|
|
|
return self._sender
|
|
|
|
|
|
|
|
@property
|
|
|
|
def text(self):
|
2018-02-09 17:56:42 +03:00
|
|
|
"""
|
|
|
|
The message text, markdown-formatted.
|
|
|
|
"""
|
2018-02-07 12:42:40 +03:00
|
|
|
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):
|
2018-02-09 17:56:42 +03:00
|
|
|
"""
|
|
|
|
The raw message text, ignoring any formatting.
|
|
|
|
"""
|
2018-02-07 12:42:40 +03:00
|
|
|
return self.message.message
|
|
|
|
|
|
|
|
@property
|
|
|
|
def reply_message(self):
|
2018-02-09 17:56:42 +03:00
|
|
|
"""
|
|
|
|
This (:obj:`Message`, optional) will make an API call the first
|
|
|
|
time to get the full ``Message`` object that one was replying to,
|
|
|
|
so use with care as there is no caching besides local caching yet.
|
|
|
|
"""
|
2018-02-07 12:42:40 +03:00
|
|
|
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):
|
2018-02-09 17:56:42 +03:00
|
|
|
"""
|
|
|
|
The unmodified (:obj:`MessageFwdHeader`, optional).
|
|
|
|
"""
|
2018-02-07 12:42:40 +03:00
|
|
|
return self.message.fwd_from
|
|
|
|
|
2018-02-08 21:43:15 +03:00
|
|
|
@property
|
|
|
|
def media(self):
|
2018-02-09 17:56:42 +03:00
|
|
|
"""
|
|
|
|
The unmodified (:obj:`MessageMedia`, optional).
|
|
|
|
"""
|
2018-02-08 21:43:15 +03:00
|
|
|
return self.message.media
|
|
|
|
|
|
|
|
@property
|
|
|
|
def photo(self):
|
2018-02-09 17:56:42 +03:00
|
|
|
"""
|
|
|
|
If the message media is a photo,
|
|
|
|
this returns the (:obj:`Photo`) object.
|
|
|
|
"""
|
2018-02-08 21:43:15 +03:00
|
|
|
if isinstance(self.message.media, types.MessageMediaPhoto):
|
|
|
|
photo = self.message.media.photo
|
|
|
|
if isinstance(photo, types.Photo):
|
|
|
|
return photo
|
|
|
|
|
|
|
|
@property
|
|
|
|
def document(self):
|
2018-02-09 17:56:42 +03:00
|
|
|
"""
|
|
|
|
If the message media is a document,
|
|
|
|
this returns the (:obj:`Document`) object.
|
|
|
|
"""
|
2018-02-08 21:43:15 +03:00
|
|
|
if isinstance(self.message.media, types.MessageMediaDocument):
|
|
|
|
doc = self.message.media.document
|
|
|
|
if isinstance(doc, types.Document):
|
|
|
|
return doc
|
|
|
|
|
2018-02-07 12:42:40 +03:00
|
|
|
@property
|
|
|
|
def out(self):
|
2018-02-09 17:56:42 +03:00
|
|
|
"""
|
|
|
|
Whether the message is outgoing (i.e. you sent it from
|
|
|
|
another session) or incoming (i.e. someone else sent it).
|
|
|
|
"""
|
2018-02-07 12:42:40 +03:00
|
|
|
return self.message.out
|
2018-02-09 13:37:17 +03:00
|
|
|
|
|
|
|
|
|
|
|
class ChatAction(_EventBuilder):
|
|
|
|
"""
|
|
|
|
Represents an action in a chat (such as user joined, left, or new pin).
|
|
|
|
"""
|
|
|
|
def build(self, update):
|
|
|
|
if isinstance(update, types.UpdateChannelPinnedMessage):
|
|
|
|
# Telegram sends UpdateChannelPinnedMessage and then
|
|
|
|
# UpdateNewChannelMessage with MessageActionPinMessage.
|
|
|
|
event = ChatAction.Event(types.PeerChannel(update.channel_id),
|
|
|
|
new_pin=update.id)
|
|
|
|
|
|
|
|
elif isinstance(update, types.UpdateChatParticipantAdd):
|
|
|
|
event = ChatAction.Event(types.PeerChat(update.chat_id),
|
|
|
|
added_by=update.inviter_id or True,
|
|
|
|
users=update.user_id)
|
|
|
|
|
|
|
|
elif isinstance(update, types.UpdateChatParticipantDelete):
|
|
|
|
event = ChatAction.Event(types.PeerChat(update.chat_id),
|
|
|
|
kicked_by=True,
|
|
|
|
users=update.user_id)
|
|
|
|
|
|
|
|
elif (isinstance(update, (
|
|
|
|
types.UpdateNewMessage, types.UpdateNewChannelMessage))
|
|
|
|
and isinstance(update.message, types.MessageService)):
|
|
|
|
msg = update.message
|
|
|
|
action = update.message.action
|
|
|
|
if isinstance(action, types.MessageActionChatJoinedByLink):
|
|
|
|
event = ChatAction.Event(msg.to_id,
|
|
|
|
added_by=True,
|
|
|
|
users=msg.from_id)
|
|
|
|
elif isinstance(action, types.MessageActionChatAddUser):
|
|
|
|
event = ChatAction.Event(msg.to_id,
|
|
|
|
added_by=msg.from_id or True,
|
|
|
|
users=action.users)
|
|
|
|
elif isinstance(action, types.MessageActionChatDeleteUser):
|
|
|
|
event = ChatAction.Event(msg.to_id,
|
|
|
|
kicked_by=msg.from_id or True,
|
|
|
|
users=action.user_id)
|
|
|
|
elif isinstance(action, types.MessageActionChatCreate):
|
|
|
|
event = ChatAction.Event(msg.to_id,
|
|
|
|
users=action.users,
|
|
|
|
created=True,
|
|
|
|
new_title=action.title)
|
|
|
|
elif isinstance(action, types.MessageActionChannelCreate):
|
|
|
|
event = ChatAction.Event(msg.to_id,
|
|
|
|
created=True,
|
|
|
|
new_title=action.title)
|
|
|
|
elif isinstance(action, types.MessageActionChatEditTitle):
|
|
|
|
event = ChatAction.Event(msg.to_id,
|
|
|
|
new_title=action.title)
|
|
|
|
elif isinstance(action, types.MessageActionChatEditPhoto):
|
|
|
|
event = ChatAction.Event(msg.to_id,
|
|
|
|
new_photo=action.photo)
|
|
|
|
elif isinstance(action, types.MessageActionChatDeletePhoto):
|
|
|
|
event = ChatAction.Event(msg.to_id,
|
|
|
|
new_photo=True)
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
|
2018-02-17 13:29:16 +03:00
|
|
|
return self._filter_event(event)
|
2018-02-09 13:37:17 +03:00
|
|
|
|
|
|
|
class Event(_EventCommon):
|
|
|
|
"""
|
|
|
|
Represents the event of a new chat action.
|
|
|
|
|
|
|
|
Members:
|
|
|
|
new_pin (:obj:`bool`):
|
|
|
|
``True`` if the pin has changed (new pin or removed).
|
|
|
|
|
|
|
|
new_photo (:obj:`bool`):
|
|
|
|
``True`` if there's a new chat photo (or it was removed).
|
|
|
|
|
|
|
|
photo (:obj:`Photo`, optional):
|
|
|
|
The new photo (or ``None`` if it was removed).
|
|
|
|
|
|
|
|
|
|
|
|
user_added (:obj:`bool`):
|
|
|
|
``True`` if the user was added by some other.
|
|
|
|
|
|
|
|
user_joined (:obj:`bool`):
|
|
|
|
``True`` if the user joined on their own.
|
|
|
|
|
|
|
|
user_left (:obj:`bool`):
|
|
|
|
``True`` if the user left on their own.
|
|
|
|
|
|
|
|
user_kicked (:obj:`bool`):
|
|
|
|
``True`` if the user was kicked by some other.
|
|
|
|
|
|
|
|
created (:obj:`bool`, optional):
|
|
|
|
``True`` if this chat was just created.
|
|
|
|
|
|
|
|
new_title (:obj:`bool`, optional):
|
|
|
|
The new title string for the chat, if applicable.
|
|
|
|
"""
|
|
|
|
def __init__(self, chat_peer, new_pin=None, new_photo=None,
|
|
|
|
added_by=None, kicked_by=None, created=None,
|
|
|
|
users=None, new_title=None):
|
|
|
|
super().__init__(chat_peer=chat_peer, msg_id=new_pin)
|
|
|
|
|
|
|
|
self.new_pin = isinstance(new_pin, int)
|
|
|
|
self._pinned_message = new_pin
|
|
|
|
|
|
|
|
self.new_photo = new_photo is not None
|
|
|
|
self.photo = \
|
|
|
|
new_photo if isinstance(new_photo, types.Photo) else None
|
|
|
|
|
|
|
|
self._added_by = None
|
|
|
|
self._kicked_by = None
|
|
|
|
self.user_added, self.user_joined, self.user_left,\
|
|
|
|
self.user_kicked = (False, False, False, False)
|
|
|
|
|
|
|
|
if added_by is True:
|
|
|
|
self.user_joined = True
|
|
|
|
elif added_by:
|
|
|
|
self.user_added = True
|
|
|
|
self._added_by = added_by
|
|
|
|
|
|
|
|
if kicked_by is True:
|
|
|
|
self.user_left = True
|
|
|
|
elif kicked_by:
|
|
|
|
self.user_kicked = True
|
|
|
|
self._kicked_by = kicked_by
|
|
|
|
|
|
|
|
self.created = bool(created)
|
|
|
|
self._user_peers = users if isinstance(users, list) else [users]
|
|
|
|
self._users = None
|
|
|
|
self.new_title = new_title
|
|
|
|
|
|
|
|
@property
|
|
|
|
def pinned_message(self):
|
|
|
|
"""
|
|
|
|
If ``new_pin`` is ``True``, this returns the (:obj:`Message`)
|
|
|
|
object that was pinned.
|
|
|
|
"""
|
|
|
|
if self._pinned_message == 0:
|
|
|
|
return None
|
|
|
|
|
|
|
|
if isinstance(self._pinned_message, int) and self.input_chat:
|
|
|
|
r = self._client(functions.channels.GetMessagesRequest(
|
|
|
|
self._input_chat, [self._pinned_message]
|
|
|
|
))
|
|
|
|
try:
|
|
|
|
self._pinned_message = next(
|
|
|
|
x for x in r.messages
|
|
|
|
if isinstance(x, types.Message)
|
|
|
|
and x.id == self._pinned_message
|
|
|
|
)
|
|
|
|
except StopIteration:
|
|
|
|
pass
|
|
|
|
|
|
|
|
if isinstance(self._pinned_message, types.Message):
|
|
|
|
return self._pinned_message
|
|
|
|
|
|
|
|
@property
|
|
|
|
def added_by(self):
|
|
|
|
"""
|
|
|
|
The user who added ``users``, if applicable (``None`` otherwise).
|
|
|
|
"""
|
|
|
|
if self._added_by and not isinstance(self._added_by, types.User):
|
|
|
|
self._added_by = self._client.get_entity(self._added_by)
|
|
|
|
return self._added_by
|
|
|
|
|
|
|
|
@property
|
|
|
|
def kicked_by(self):
|
|
|
|
"""
|
|
|
|
The user who kicked ``users``, if applicable (``None`` otherwise).
|
|
|
|
"""
|
|
|
|
if self._kicked_by and not isinstance(self._kicked_by, types.User):
|
|
|
|
self._kicked_by = self._client.get_entity(self._kicked_by)
|
|
|
|
return self._kicked_by
|
|
|
|
|
|
|
|
@property
|
|
|
|
def user(self):
|
|
|
|
"""
|
|
|
|
The single user that takes part in this action (e.g. joined).
|
|
|
|
|
|
|
|
Might be ``None`` if the information can't be retrieved or
|
|
|
|
there is no user taking part.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
return next(self.users)
|
|
|
|
except (StopIteration, TypeError):
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def users(self):
|
|
|
|
"""
|
|
|
|
A list of users that take part in this action (e.g. joined).
|
|
|
|
|
|
|
|
Might be empty if the information can't be retrieved or there
|
|
|
|
are no users taking part.
|
|
|
|
"""
|
|
|
|
if self._users is None and self._user_peers:
|
|
|
|
try:
|
|
|
|
self._users = self._client.get_entity(self._user_peers)
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
self._users = []
|
|
|
|
|
|
|
|
return self._users
|
2018-02-09 14:42:04 +03:00
|
|
|
|
|
|
|
|
|
|
|
class UserUpdate(_EventBuilder):
|
|
|
|
"""
|
|
|
|
Represents an user update (gone online, offline, joined Telegram).
|
|
|
|
"""
|
|
|
|
def build(self, update):
|
|
|
|
if isinstance(update, types.UpdateUserStatus):
|
|
|
|
event = UserUpdate.Event(update.user_id,
|
|
|
|
status=update.status)
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
|
2018-02-17 13:29:16 +03:00
|
|
|
return self._filter_event(event)
|
2018-02-09 14:42:04 +03:00
|
|
|
|
|
|
|
class Event(_EventCommon):
|
|
|
|
"""
|
|
|
|
Represents the event of an user status update (last seen, joined).
|
|
|
|
|
|
|
|
Members:
|
|
|
|
online (:obj:`bool`, optional):
|
|
|
|
``True`` if the user is currently online, ``False`` otherwise.
|
|
|
|
Might be ``None`` if this information is not present.
|
|
|
|
|
|
|
|
last_seen (:obj:`datetime`, optional):
|
|
|
|
Exact date when the user was last seen if known.
|
|
|
|
|
|
|
|
until (:obj:`datetime`, optional):
|
|
|
|
Until when will the user remain online.
|
|
|
|
|
|
|
|
within_months (:obj:`bool`):
|
|
|
|
``True`` if the user was seen within 30 days.
|
|
|
|
|
|
|
|
within_weeks (:obj:`bool`):
|
|
|
|
``True`` if the user was seen within 7 days.
|
|
|
|
|
|
|
|
recently (:obj:`bool`):
|
|
|
|
``True`` if the user was seen within a day.
|
|
|
|
|
|
|
|
action (:obj:`SendMessageAction`, optional):
|
|
|
|
The "typing" action if any the user is performing if any.
|
|
|
|
|
|
|
|
cancel (:obj:`bool`):
|
|
|
|
``True`` if the action was cancelling other actions.
|
|
|
|
|
|
|
|
typing (:obj:`bool`):
|
|
|
|
``True`` if the action is typing a message.
|
|
|
|
|
|
|
|
recording (:obj:`bool`):
|
|
|
|
``True`` if the action is recording something.
|
|
|
|
|
|
|
|
uploading (:obj:`bool`):
|
|
|
|
``True`` if the action is uploading something.
|
|
|
|
|
|
|
|
playing (:obj:`bool`):
|
|
|
|
``True`` if the action is playing a game.
|
|
|
|
|
|
|
|
audio (:obj:`bool`):
|
|
|
|
``True`` if what's being recorded/uploaded is an audio.
|
|
|
|
|
|
|
|
round (:obj:`bool`):
|
|
|
|
``True`` if what's being recorded/uploaded is a round video.
|
|
|
|
|
|
|
|
video (:obj:`bool`):
|
|
|
|
``True`` if what's being recorded/uploaded is an video.
|
|
|
|
|
|
|
|
document (:obj:`bool`):
|
|
|
|
``True`` if what's being uploaded is document.
|
|
|
|
|
|
|
|
geo (:obj:`bool`):
|
|
|
|
``True`` if what's being uploaded is a geo.
|
|
|
|
|
|
|
|
photo (:obj:`bool`):
|
|
|
|
``True`` if what's being uploaded is a photo.
|
|
|
|
|
|
|
|
contact (:obj:`bool`):
|
|
|
|
``True`` if what's being uploaded (selected) is a contact.
|
|
|
|
"""
|
|
|
|
def __init__(self, user_id, status=None, typing=None):
|
|
|
|
super().__init__(types.PeerUser(user_id))
|
|
|
|
|
|
|
|
self.online = None if status is None else \
|
|
|
|
isinstance(status, types.UserStatusOnline)
|
|
|
|
|
|
|
|
self.last_seen = status.was_online if \
|
|
|
|
isinstance(status, types.UserStatusOffline) else None
|
|
|
|
|
|
|
|
self.until = status.expires if \
|
|
|
|
isinstance(status, types.UserStatusOnline) else None
|
|
|
|
|
|
|
|
if self.last_seen:
|
|
|
|
diff = datetime.datetime.now() - self.last_seen
|
|
|
|
if diff < datetime.timedelta(days=30):
|
|
|
|
self.within_months = True
|
|
|
|
if diff < datetime.timedelta(days=7):
|
|
|
|
self.within_weeks = True
|
|
|
|
if diff < datetime.timedelta(days=1):
|
|
|
|
self.recently = True
|
|
|
|
else:
|
|
|
|
self.within_months = self.within_weeks = self.recently = False
|
|
|
|
if isinstance(status, (types.UserStatusOnline,
|
|
|
|
types.UserStatusRecently)):
|
|
|
|
self.within_months = self.within_weeks = True
|
|
|
|
self.recently = True
|
|
|
|
elif isinstance(status, types.UserStatusLastWeek):
|
|
|
|
self.within_months = self.within_weeks = True
|
|
|
|
elif isinstance(status, types.UserStatusLastMonth):
|
|
|
|
self.within_months = True
|
|
|
|
|
|
|
|
self.action = typing
|
|
|
|
if typing:
|
|
|
|
self.cancel = self.typing = self.recording = self.uploading = \
|
|
|
|
self.playing = False
|
|
|
|
self.audio = self.round = self.video = self.document = \
|
|
|
|
self.geo = self.photo = self.contact = False
|
|
|
|
|
|
|
|
if isinstance(typing, types.SendMessageCancelAction):
|
|
|
|
self.cancel = True
|
|
|
|
elif isinstance(typing, types.SendMessageTypingAction):
|
|
|
|
self.typing = True
|
|
|
|
elif isinstance(typing, types.SendMessageGamePlayAction):
|
|
|
|
self.playing = True
|
|
|
|
elif isinstance(typing, types.SendMessageGeoLocationAction):
|
|
|
|
self.geo = True
|
|
|
|
elif isinstance(typing, types.SendMessageRecordAudioAction):
|
|
|
|
self.recording = self.audio = True
|
|
|
|
elif isinstance(typing, types.SendMessageRecordRoundAction):
|
|
|
|
self.recording = self.round = True
|
|
|
|
elif isinstance(typing, types.SendMessageRecordVideoAction):
|
|
|
|
self.recording = self.video = True
|
|
|
|
elif isinstance(typing, types.SendMessageChooseContactAction):
|
|
|
|
self.uploading = self.contact = True
|
|
|
|
elif isinstance(typing, types.SendMessageUploadAudioAction):
|
|
|
|
self.uploading = self.audio = True
|
|
|
|
elif isinstance(typing, types.SendMessageUploadDocumentAction):
|
|
|
|
self.uploading = self.document = True
|
|
|
|
elif isinstance(typing, types.SendMessageUploadPhotoAction):
|
|
|
|
self.uploading = self.photo = True
|
|
|
|
elif isinstance(typing, types.SendMessageUploadRoundAction):
|
|
|
|
self.uploading = self.round = True
|
|
|
|
elif isinstance(typing, types.SendMessageUploadVideoAction):
|
|
|
|
self.uploading = self.video = True
|
|
|
|
|
|
|
|
@property
|
|
|
|
def user(self):
|
2018-02-09 17:56:42 +03:00
|
|
|
"""Alias around the chat (conversation)."""
|
2018-02-09 14:42:04 +03:00
|
|
|
return self.chat
|
2018-02-09 15:05:34 +03:00
|
|
|
|
|
|
|
|
|
|
|
class MessageChanged(_EventBuilder):
|
|
|
|
"""
|
|
|
|
Represents a message changed (edited or deleted).
|
|
|
|
"""
|
|
|
|
def build(self, update):
|
|
|
|
if isinstance(update, (types.UpdateEditMessage,
|
|
|
|
types.UpdateEditChannelMessage)):
|
|
|
|
event = MessageChanged.Event(edit_msg=update.message)
|
2018-02-19 22:23:52 +03:00
|
|
|
elif isinstance(update, types.UpdateDeleteMessages):
|
|
|
|
event = MessageChanged.Event(
|
|
|
|
deleted_ids=update.messages,
|
|
|
|
peer=None
|
|
|
|
)
|
|
|
|
elif isinstance(update, types.UpdateDeleteChannelMessages):
|
2018-02-09 15:05:34 +03:00
|
|
|
event = MessageChanged.Event(
|
|
|
|
deleted_ids=update.messages,
|
|
|
|
peer=types.PeerChannel(update.channel_id)
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
|
2018-02-17 13:29:16 +03:00
|
|
|
return self._filter_event(event)
|
2018-02-09 15:05:34 +03:00
|
|
|
|
2018-02-18 16:07:13 +03:00
|
|
|
class Event(NewMessage.Event):
|
2018-02-09 15:05:34 +03:00
|
|
|
"""
|
|
|
|
Represents the event of an user status update (last seen, joined).
|
|
|
|
|
2018-02-18 16:07:13 +03:00
|
|
|
Please note that the ``message`` member will be ``None`` if the
|
|
|
|
action was a deletion and not an edit.
|
|
|
|
|
2018-02-09 15:05:34 +03:00
|
|
|
Members:
|
|
|
|
edited (:obj:`bool`):
|
|
|
|
``True`` if the message was edited.
|
|
|
|
|
|
|
|
deleted (:obj:`bool`):
|
|
|
|
``True`` if the message IDs were deleted.
|
|
|
|
|
|
|
|
deleted_ids (:obj:`List[int]`):
|
|
|
|
A list containing the IDs of the messages that were deleted.
|
|
|
|
"""
|
|
|
|
def __init__(self, edit_msg=None, deleted_ids=None, peer=None):
|
2018-02-18 16:07:13 +03:00
|
|
|
if edit_msg is None:
|
|
|
|
msg = types.Message((deleted_ids or [0])[0], peer, None, '')
|
|
|
|
else:
|
|
|
|
msg = edit_msg
|
|
|
|
super().__init__(msg)
|
2018-02-09 15:05:34 +03:00
|
|
|
|
|
|
|
self.edited = bool(edit_msg)
|
|
|
|
self.deleted = bool(deleted_ids)
|
|
|
|
self.deleted_ids = deleted_ids or []
|
2018-02-27 13:30:42 +03:00
|
|
|
|
|
|
|
|
|
|
|
class StopPropagation(Exception):
|
|
|
|
"""
|
|
|
|
If this Exception is found to be raised in any of the handlers for a
|
|
|
|
given update, it will stop the execution of all other registered
|
|
|
|
event handlers in the chain.
|
|
|
|
Think of it like a ``StopIteration`` exception in a for loop.
|
|
|
|
|
|
|
|
Example usage:
|
|
|
|
```
|
|
|
|
@client.on(events.NewMessage)
|
|
|
|
def delete(event):
|
|
|
|
event.delete()
|
|
|
|
# Other handlers won't have an event to work with
|
|
|
|
raise StopPropagation
|
|
|
|
|
|
|
|
@client.on(events.NewMessage)
|
|
|
|
def _(event):
|
|
|
|
# Will never be reached, because it is the second handler in the chain.
|
|
|
|
pass
|
|
|
|
```
|
|
|
|
"""
|