Begin unification of event builders and events

This commit is contained in:
Lonami Exo 2022-01-28 11:34:16 +01:00
parent f2ef0bfceb
commit 9726169a8c
15 changed files with 1142 additions and 1719 deletions

View File

@ -779,3 +779,12 @@ input_peer removed from get_me; input peers should remain mostly an impl detail
raw api types and fns are now immutable. this can enable optimizations in the future. raw api types and fns are now immutable. this can enable optimizations in the future.
upload_file has been removed from the public methods. it's a low-level method users should not need to use. upload_file has been removed from the public methods. it's a low-level method users should not need to use.
events have changed. rather than differentiating between "event builder" and "event instance", instead there is only the instance, and you register the class.
where you had
@client.on(events.NewMessage(chats=...))
it's now
@client.on(events.NewMessage, chats=...)
this also means filters are unified, although not all have an effect on all events. from_users renamed to senders. messageread inbox is gone in favor of outgoing/incoming.
events.register, unregister, is_handler and list are gone. now you can typehint instead.
def handler(event: events.NewMessage)

View File

@ -10,7 +10,7 @@ from . import (
) )
from .. import version, _tl from .. import version, _tl
from ..types import _custom from ..types import _custom
from .._events.common import EventBuilder, EventCommon from .._events.base import EventBuilder
from .._misc import enums from .._misc import enums

View File

@ -10,7 +10,7 @@ import logging
from collections import deque from collections import deque
from ..errors._rpcbase import RpcError from ..errors._rpcbase import RpcError
from .._events.common import EventBuilder, EventCommon from .._events.base import EventBuilder
from .._events.raw import Raw from .._events.raw import Raw
from .._events.base import StopPropagation, _get_handlers from .._events.base import StopPropagation, _get_handlers
from .._misc import utils from .._misc import utils

View File

@ -2,7 +2,7 @@ import asyncio
import time import time
import weakref import weakref
from .common import EventBuilder, EventCommon, name_inner_event from .base import EventBuilder
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
from ..types import _custom from ..types import _custom
@ -64,13 +64,16 @@ class AlbumHack:
await asyncio.sleep(diff) await asyncio.sleep(diff)
@name_inner_event class Album(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
class Album(EventBuilder):
""" """
Occurs whenever you receive an album. This event only exists Occurs whenever you receive an album. This event only exists
to ease dealing with an unknown amount of messages that belong to ease dealing with an unknown amount of messages that belong
to the same album. to the same album.
Members:
messages (Sequence[`Message <telethon.tl._custom.message.Message>`]):
The list of messages belonging to the same album.
Example Example
.. code-block:: python .. code-block:: python
@ -91,12 +94,20 @@ class Album(EventBuilder):
await event.messages[4].reply('Cool!') await event.messages[4].reply('Cool!')
""" """
def __init__( def __init__(self, messages):
self, chats=None, *, blacklist_chats=False, func=None): message = messages[0]
super().__init__(chats, blacklist_chats=blacklist_chats, func=func) if not message.out and isinstance(message.peer_id, _tl.PeerUser):
# Incoming message (e.g. from a bot) has peer_id=us, and
# from_id=bot (the actual "chat" from a user's perspective).
chat_peer = message.from_id
else:
chat_peer = message.peer_id
@classmethod _custom.chatgetter.ChatGetter.__init__(self, chat_peer=chat_peer, broadcast=bool(message.post))
def build(cls, update, others=None, self_id=None, *todo, **todo2): _custom.sendergetter.SenderGetter.__init__(self, message.sender_id)
self.messages = messages
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if not others: if not others:
return # We only care about albums which come inside the same Updates return # We only care about albums which come inside the same Updates
@ -135,216 +146,188 @@ class Album(EventBuilder):
and u.message.grouped_id == group) and u.message.grouped_id == group)
]) ])
def filter(self, event): def _set_client(self, client):
# Albums with less than two messages require a few hacks to work. super()._set_client(client)
if len(event.messages) > 1: self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
return super().filter(event)
class Event(EventCommon, _custom.sendergetter.SenderGetter): self.messages = [
""" _custom.Message._new(client, m, self._entities, None)
Represents the event of a new album. for m in self.messages
]
Members: if len(self.messages) == 1:
messages (Sequence[`Message <telethon.tl._custom.message.Message>`]): # This will require hacks to be a proper album event
The list of messages belonging to the same album. hack = client._albums.get(self.grouped_id)
""" if hack is None:
def __init__(self, messages): client._albums[self.grouped_id] = AlbumHack(client, self)
message = messages[0]
if not message.out and isinstance(message.peer_id, _tl.PeerUser):
# Incoming message (e.g. from a bot) has peer_id=us, and
# from_id=bot (the actual "chat" from a user's perspective).
chat_peer = message.from_id
else: else:
chat_peer = message.peer_id hack.extend(self.messages)
super().__init__(chat_peer=chat_peer, @property
msg_id=message.id, broadcast=bool(message.post)) def grouped_id(self):
"""
The shared ``grouped_id`` between all the messages.
"""
return self.messages[0].grouped_id
_custom.sendergetter.SenderGetter.__init__(self, message.sender_id) @property
self.messages = messages def text(self):
"""
The message text of the first photo with a caption,
formatted using the client's default parse mode.
"""
return next((m.text for m in self.messages if m.text), '')
def _set_client(self, client): @property
super()._set_client(client) def raw_text(self):
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) """
The raw message text of the first photo
with a caption, ignoring any formatting.
"""
return next((m.raw_text for m in self.messages if m.raw_text), '')
self.messages = [ @property
_custom.Message._new(client, m, self._entities, None) def is_reply(self):
for m in self.messages """
] `True` if the album is a reply to some other message.
if len(self.messages) == 1: Remember that you can access the ID of the message
# This will require hacks to be a proper album event this one is replying to through `reply_to_msg_id`,
hack = client._albums.get(self.grouped_id) and the `Message` object with `get_reply_message()`.
if hack is None: """
client._albums[self.grouped_id] = AlbumHack(client, self) # Each individual message in an album all reply to the same message
else: return self.messages[0].is_reply
hack.extend(self.messages)
@property @property
def grouped_id(self): def forward(self):
""" """
The shared ``grouped_id`` between all the messages. The `Forward <telethon.tl._custom.forward.Forward>`
""" information for the first message in the album if it was forwarded.
return self.messages[0].grouped_id """
# Each individual message in an album all reply to the same message
return self.messages[0].forward
@property # endregion Public Properties
def text(self):
"""
The message text of the first photo with a caption,
formatted using the client's default parse mode.
"""
return next((m.text for m in self.messages if m.text), '')
@property # region Public Methods
def raw_text(self):
"""
The raw message text of the first photo
with a caption, ignoring any formatting.
"""
return next((m.raw_text for m in self.messages if m.raw_text), '')
@property async def get_reply_message(self):
def is_reply(self): """
""" The `Message <telethon.tl._custom.message.Message>`
`True` if the album is a reply to some other message. that this album is replying to, or `None`.
Remember that you can access the ID of the message The result will be cached after its first use.
this one is replying to through `reply_to_msg_id`, """
and the `Message` object with `get_reply_message()`. return await self.messages[0].get_reply_message()
"""
# Each individual message in an album all reply to the same message
return self.messages[0].is_reply
@property async def respond(self, *args, **kwargs):
def forward(self): """
""" Responds to the album (not as a reply). Shorthand for
The `Forward <telethon.tl._custom.forward.Forward>` `telethon.client.messages.MessageMethods.send_message`
information for the first message in the album if it was forwarded. with ``entity`` already set.
""" """
# Each individual message in an album all reply to the same message return await self.messages[0].respond(*args, **kwargs)
return self.messages[0].forward
# endregion Public Properties async def reply(self, *args, **kwargs):
"""
Replies to the first photo in the album (as a reply). Shorthand
for `telethon.client.messages.MessageMethods.send_message`
with both ``entity`` and ``reply_to`` already set.
"""
return await self.messages[0].reply(*args, **kwargs)
# region Public Methods async def forward_to(self, *args, **kwargs):
"""
Forwards the entire album. Shorthand for
`telethon.client.messages.MessageMethods.forward_messages`
with both ``messages`` and ``from_peer`` already set.
"""
if self._client:
kwargs['messages'] = self.messages
kwargs['from_peer'] = await self.get_input_chat()
return await self._client.forward_messages(*args, **kwargs)
async def get_reply_message(self): async def edit(self, *args, **kwargs):
""" """
The `Message <telethon.tl._custom.message.Message>` Edits the first caption or the message, or the first messages'
that this album is replying to, or `None`. caption if no caption is set, iff it's outgoing. Shorthand for
`telethon.client.messages.MessageMethods.edit_message`
with both ``entity`` and ``message`` already set.
The result will be cached after its first use. Returns `None` if the message was incoming,
""" or the edited `Message` otherwise.
return await self.messages[0].get_reply_message()
async def respond(self, *args, **kwargs): .. note::
"""
Responds to the album (not as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message`
with ``entity`` already set.
"""
return await self.messages[0].respond(*args, **kwargs)
async def reply(self, *args, **kwargs): This is different from `client.edit_message
""" <telethon.client.messages.MessageMethods.edit_message>`
Replies to the first photo in the album (as a reply). Shorthand and **will respect** the previous state of the message.
for `telethon.client.messages.MessageMethods.send_message` For example, if the message didn't have a link preview,
with both ``entity`` and ``reply_to`` already set. the edit won't add one by default, and you should force
""" it by setting it to `True` if you want it.
return await self.messages[0].reply(*args, **kwargs)
async def forward_to(self, *args, **kwargs): This is generally the most desired and convenient behaviour,
""" and will work for link previews and message buttons.
Forwards the entire album. Shorthand for """
`telethon.client.messages.MessageMethods.forward_messages` for msg in self.messages:
with both ``messages`` and ``from_peer`` already set. if msg.raw_text:
""" return await msg.edit(*args, **kwargs)
if self._client:
kwargs['messages'] = self.messages
kwargs['from_peer'] = await self.get_input_chat()
return await self._client.forward_messages(*args, **kwargs)
async def edit(self, *args, **kwargs): return await self.messages[0].edit(*args, **kwargs)
"""
Edits the first caption or the message, or the first messages'
caption if no caption is set, iff it's outgoing. Shorthand for
`telethon.client.messages.MessageMethods.edit_message`
with both ``entity`` and ``message`` already set.
Returns `None` if the message was incoming, async def delete(self, *args, **kwargs):
or the edited `Message` otherwise. """
Deletes the entire album. You're responsible for checking whether
you have the permission to do so, or to except the error otherwise.
Shorthand for
`telethon.client.messages.MessageMethods.delete_messages` with
``entity`` and ``message_ids`` already set.
"""
if self._client:
return await self._client.delete_messages(
await self.get_input_chat(), self.messages,
*args, **kwargs
)
.. note:: async def mark_read(self):
"""
Marks the entire album as read. Shorthand for
`client.mark_read()
<telethon.client.messages.MessageMethods.mark_read>`
with both ``entity`` and ``message`` already set.
"""
if self._client:
await self._client.mark_read(
await self.get_input_chat(), max_id=self.messages[-1].id)
This is different from `client.edit_message async def pin(self, *, notify=False):
<telethon.client.messages.MessageMethods.edit_message>` """
and **will respect** the previous state of the message. Pins the first photo in the album. Shorthand for
For example, if the message didn't have a link preview, `telethon.client.messages.MessageMethods.pin_message`
the edit won't add one by default, and you should force with both ``entity`` and ``message`` already set.
it by setting it to `True` if you want it. """
return await self.messages[0].pin(notify=notify)
This is generally the most desired and convenient behaviour, def __len__(self):
and will work for link previews and message buttons. """
""" Return the amount of messages in the album.
for msg in self.messages:
if msg.raw_text:
return await msg.edit(*args, **kwargs)
return await self.messages[0].edit(*args, **kwargs) Equivalent to ``len(self.messages)``.
"""
return len(self.messages)
async def delete(self, *args, **kwargs): def __iter__(self):
""" """
Deletes the entire album. You're responsible for checking whether Iterate over the messages in the album.
you have the permission to do so, or to except the error otherwise.
Shorthand for
`telethon.client.messages.MessageMethods.delete_messages` with
``entity`` and ``message_ids`` already set.
"""
if self._client:
return await self._client.delete_messages(
await self.get_input_chat(), self.messages,
*args, **kwargs
)
async def mark_read(self): Equivalent to ``iter(self.messages)``.
""" """
Marks the entire album as read. Shorthand for return iter(self.messages)
`client.mark_read()
<telethon.client.messages.MessageMethods.mark_read>`
with both ``entity`` and ``message`` already set.
"""
if self._client:
await self._client.mark_read(
await self.get_input_chat(), max_id=self.messages[-1].id)
async def pin(self, *, notify=False): def __getitem__(self, n):
""" """
Pins the first photo in the album. Shorthand for Access the n'th message in the album.
`telethon.client.messages.MessageMethods.pin_message`
with both ``entity`` and ``message`` already set.
"""
return await self.messages[0].pin(notify=notify)
def __len__(self): Equivalent to ``event.messages[n]``.
""" """
Return the amount of messages in the album. return self.messages[n]
Equivalent to ``len(self.messages)``.
"""
return len(self.messages)
def __iter__(self):
"""
Iterate over the messages in the album.
Equivalent to ``iter(self.messages)``.
"""
return iter(self.messages)
def __getitem__(self, n):
"""
Access the n'th message in the album.
Equivalent to ``event.messages[n]``.
"""
return self.messages[n]

View File

@ -1,7 +1,4 @@
from .raw import Raw import abc
_HANDLERS_ATTRIBUTE = '__tl.handlers'
class StopPropagation(Exception): class StopPropagation(Exception):
@ -31,101 +28,16 @@ class StopPropagation(Exception):
pass pass
def register(event=None): class EventBuilder(abc.ABC):
""" @classmethod
Decorator method to *register* event handlers. This is the client-less @abc.abstractmethod
`add_event_handler() def _build(cls, update, others, self_id, entities, client):
<telethon.client.updates.UpdateMethods.add_event_handler>` variant. """
Builds an event for the given update if possible, or returns None.
Note that this method only registers callbacks as handlers, `others` are the rest of updates that came in the same container
and does not attach them to any client. This is useful for as the current `update`.
external modules that don't have access to the client, but
still want to define themselves as a handler. Example:
>>> from telethon import events `self_id` should be the current user's ID, since it is required
>>> @events.register(events.NewMessage) for some events which lack this information but still need it.
... async def handler(event): """
... ...
...
>>> # (somewhere else)
...
>>> from telethon import TelegramClient
>>> client = TelegramClient(...)
>>> client.add_event_handler(handler)
Remember that you can use this as a non-decorator
through ``register(event)(callback)``.
Args:
event (`_EventBuilder` | `type`):
The event builder class or instance to be used,
for instance ``events.NewMessage``.
"""
if isinstance(event, type):
event = event()
elif not event:
event = Raw()
def decorator(callback):
handlers = getattr(callback, _HANDLERS_ATTRIBUTE, [])
handlers.append(event)
setattr(callback, _HANDLERS_ATTRIBUTE, handlers)
return callback
return decorator
def unregister(callback, event=None):
"""
Inverse operation of `register` (though not a decorator). Client-less
`remove_event_handler
<telethon.client.updates.UpdateMethods.remove_event_handler>`
variant. **Note that this won't remove handlers from the client**,
because it simply can't, so you would generally use this before
adding the handlers to the client.
This method is here for symmetry. You will rarely need to
unregister events, since you can simply just not add them
to any client.
If no event is given, all events for this callback are removed.
Returns how many callbacks were removed.
"""
found = 0
if event and not isinstance(event, type):
event = type(event)
handlers = getattr(callback, _HANDLERS_ATTRIBUTE, [])
handlers.append((event, callback))
i = len(handlers)
while i:
i -= 1
ev = handlers[i]
if not event or isinstance(ev, event):
del handlers[i]
found += 1
return found
def is_handler(callback):
"""
Returns `True` if the given callback is an
event handler (i.e. you used `register` on it).
"""
return hasattr(callback, _HANDLERS_ATTRIBUTE)
def list(callback):
"""
Returns a list containing the registered event
builders inside the specified callback handler.
"""
return getattr(callback, _HANDLERS_ATTRIBUTE, [])[:]
def _get_handlers(callback):
"""
Like ``list`` but returns `None` if the callback was never registered.
"""
return getattr(callback, _HANDLERS_ATTRIBUTE, None)

View File

@ -3,7 +3,7 @@ import struct
import asyncio import asyncio
import functools import functools
from .common import EventBuilder, EventCommon, name_inner_event from .base import EventBuilder
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
from ..types import _custom from ..types import _custom
@ -23,8 +23,7 @@ def auto_answer(func):
return wrapped return wrapped
@name_inner_event class CallbackQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
class CallbackQuery(EventBuilder):
""" """
Occurs whenever you sign in as a bot and a user Occurs whenever you sign in as a bot and a user
clicks one of the inline buttons on your messages. clicks one of the inline buttons on your messages.
@ -34,18 +33,17 @@ class CallbackQuery(EventBuilder):
message. The `chats` parameter also supports checking against the message. The `chats` parameter also supports checking against the
`chat_instance` which should be used for inline callbacks. `chat_instance` which should be used for inline callbacks.
Args: Members:
data (`bytes`, `str`, `callable`, optional): query (:tl:`UpdateBotCallbackQuery`):
If set, the inline button payload data must match this data. The original :tl:`UpdateBotCallbackQuery`.
A UTF-8 string can also be given, a regex or a callable. For
instance, to check against ``'data_1'`` and ``'data_2'`` you
can use ``re.compile(b'data_')``.
pattern (`bytes`, `str`, `callable`, `Pattern`, optional): data_match (`obj`, optional):
If set, only buttons with payload matching this pattern will be handled. The object returned by the ``data=`` parameter
You can specify a regex-like string which will be matched when creating the event builder, if any. Similar
against the payload data, a callable function that returns `True` to ``pattern_match`` for the new message event.
if a the payload data is acceptable, or a compiled regex pattern.
pattern_match (`obj`, optional):
Alias for ``data_match``.
Example Example
.. code-block:: python .. code-block:: python
@ -71,39 +69,17 @@ class CallbackQuery(EventBuilder):
Button.inline('Nope', b'no') Button.inline('Nope', b'no')
]) ])
""" """
def __init__( def __init__(self, query, peer, msg_id):
self, chats=None, *, blacklist_chats=False, func=None, data=None, pattern=None): _custom.chatgetter.ChatGetter.__init__(self, peer)
super().__init__(chats, blacklist_chats=blacklist_chats, func=func) _custom.sendergetter.SenderGetter.__init__(self, query.user_id)
self.query = query
if data and pattern: self.data_match = None
raise ValueError("Only pass either data or pattern not both.") self.pattern_match = None
self._message = None
if isinstance(data, str): self._answered = False
data = data.encode('utf-8')
if isinstance(pattern, str):
pattern = pattern.encode('utf-8')
match = data if data else pattern
if isinstance(match, bytes):
self.match = data if data else re.compile(pattern).match
elif not match or callable(match):
self.match = match
elif hasattr(match, 'match') and callable(match.match):
if not isinstance(getattr(match, 'pattern', b''), bytes):
match = re.compile(match.pattern.encode('utf-8'),
match.flags & (~re.UNICODE))
self.match = match.match
else:
raise TypeError('Invalid data or pattern type given')
self._no_check = all(x is None for x in (
self.chats, self.func, self.match,
))
@classmethod @classmethod
def build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, _tl.UpdateBotCallbackQuery): if isinstance(update, _tl.UpdateBotCallbackQuery):
return cls.Event(update, update.peer, update.msg_id) return cls.Event(update, update.peer, update.msg_id)
elif isinstance(update, _tl.UpdateInlineBotCallbackQuery): elif isinstance(update, _tl.UpdateInlineBotCallbackQuery):
@ -113,242 +89,191 @@ class CallbackQuery(EventBuilder):
peer = _tl.PeerChannel(-pid) if pid < 0 else _tl.PeerUser(pid) peer = _tl.PeerChannel(-pid) if pid < 0 else _tl.PeerUser(pid)
return cls.Event(update, peer, mid) return cls.Event(update, peer, mid)
def filter(self, event): def _set_client(self, client):
# We can't call super().filter(...) because it ignores chat_instance super()._set_client(client)
if self._no_check: self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
return event
if self.chats is not None: @property
inside = event.query.chat_instance in self.chats def id(self):
if event.chat_id:
inside |= event.chat_id in self.chats
if inside == self.blacklist_chats:
return
if self.match:
if callable(self.match):
event.data_match = event.pattern_match = self.match(event.query.data)
if not event.data_match:
return
elif event.query.data != self.match:
return
if self.func:
# Return the result of func directly as it may need to be awaited
return self.func(event)
return True
class Event(EventCommon, _custom.sendergetter.SenderGetter):
""" """
Represents the event of a new callback query. Returns the query ID. The user clicking the inline
button is the one who generated this random ID.
Members:
query (:tl:`UpdateBotCallbackQuery`):
The original :tl:`UpdateBotCallbackQuery`.
data_match (`obj`, optional):
The object returned by the ``data=`` parameter
when creating the event builder, if any. Similar
to ``pattern_match`` for the new message event.
pattern_match (`obj`, optional):
Alias for ``data_match``.
""" """
def __init__(self, query, peer, msg_id): return self.query.query_id
super().__init__(peer, msg_id=msg_id)
_custom.sendergetter.SenderGetter.__init__(self, query.user_id)
self.query = query
self.data_match = None
self.pattern_match = None
self._message = None
self._answered = False
def _set_client(self, client): @property
super()._set_client(client) def message_id(self):
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) """
Returns the message ID to which the clicked inline button belongs.
"""
return self._message_id
@property @property
def id(self): def data(self):
""" """
Returns the query ID. The user clicking the inline Returns the data payload from the original inline button.
button is the one who generated this random ID. """
""" return self.query.data
return self.query.query_id
@property @property
def message_id(self): def chat_instance(self):
""" """
Returns the message ID to which the clicked inline button belongs. Unique identifier for the chat where the callback occurred.
""" Useful for high scores in games.
return self._message_id """
return self.query.chat_instance
@property
def data(self):
"""
Returns the data payload from the original inline button.
"""
return self.query.data
@property
def chat_instance(self):
"""
Unique identifier for the chat where the callback occurred.
Useful for high scores in games.
"""
return self.query.chat_instance
async def get_message(self):
"""
Returns the message to which the clicked inline button belongs.
"""
if self._message is not None:
return self._message
try:
chat = await self.get_input_chat() if self.is_channel else None
self._message = await self._client.get_messages(
chat, ids=self._message_id)
except ValueError:
return
async def get_message(self):
"""
Returns the message to which the clicked inline button belongs.
"""
if self._message is not None:
return self._message return self._message
async def _refetch_sender(self): try:
self._sender = self._entities.get(self.sender_id) chat = await self.get_input_chat() if self.is_channel else None
if not self._sender: self._message = await self._client.get_messages(
return chat, ids=self._message_id)
except ValueError:
return
self._input_sender = utils.get_input_peer(self._chat) return self._message
if not getattr(self._input_sender, 'access_hash', True):
# getattr with True to handle the InputPeerSelf() case
m = await self.get_message()
if m:
self._sender = m._sender
self._input_sender = m._input_sender
async def answer( async def _refetch_sender(self):
self, message=None, cache_time=0, *, url=None, alert=False): self._sender = self._entities.get(self.sender_id)
""" if not self._sender:
Answers the callback query (and stops the loading circle). return
Args: self._input_sender = utils.get_input_peer(self._chat)
message (`str`, optional): if not getattr(self._input_sender, 'access_hash', True):
The toast message to show feedback to the user. # getattr with True to handle the InputPeerSelf() case
m = await self.get_message()
if m:
self._sender = m._sender
self._input_sender = m._input_sender
cache_time (`int`, optional): async def answer(
For how long this result should be cached on self, message=None, cache_time=0, *, url=None, alert=False):
the user's client. Defaults to 0 for no cache. """
Answers the callback query (and stops the loading circle).
url (`str`, optional): Args:
The URL to be opened in the user's client. Note that message (`str`, optional):
the only valid URLs are those of games your bot has, The toast message to show feedback to the user.
or alternatively a 't.me/your_bot?start=xyz' parameter.
alert (`bool`, optional): cache_time (`int`, optional):
Whether an alert (a pop-up dialog) should be used For how long this result should be cached on
instead of showing a toast. Defaults to `False`. the user's client. Defaults to 0 for no cache.
"""
if self._answered:
return
res = await self._client(_tl.fn.messages.SetBotCallbackAnswer( url (`str`, optional):
query_id=self.query.query_id, The URL to be opened in the user's client. Note that
cache_time=cache_time, the only valid URLs are those of games your bot has,
alert=alert, or alternatively a 't.me/your_bot?start=xyz' parameter.
message=message,
url=url,
))
self._answered = True
return res
@property alert (`bool`, optional):
def via_inline(self): Whether an alert (a pop-up dialog) should be used
""" instead of showing a toast. Defaults to `False`.
Whether this callback was generated from an inline button sent """
via an inline query or not. If the bot sent the message itself if self._answered:
with buttons, and one of those is clicked, this will be `False`. return
If a user sent the message coming from an inline query to the
bot, and one of those is clicked, this will be `True`.
If it's `True`, it's likely that the bot is **not** in the res = await self._client(_tl.fn.messages.SetBotCallbackAnswer(
chat, so methods like `respond` or `delete` won't work (but query_id=self.query.query_id,
`edit` will always work). cache_time=cache_time,
""" alert=alert,
return isinstance(self.query, _tl.UpdateInlineBotCallbackQuery) message=message,
url=url,
))
self._answered = True
return res
@auto_answer @property
async def respond(self, *args, **kwargs): def via_inline(self):
""" """
Responds to the message (not as a reply). Shorthand for Whether this callback was generated from an inline button sent
`telethon.client.messages.MessageMethods.send_message` with via an inline query or not. If the bot sent the message itself
``entity`` already set. with buttons, and one of those is clicked, this will be `False`.
If a user sent the message coming from an inline query to the
bot, and one of those is clicked, this will be `True`.
This method will also `answer` the callback if necessary. If it's `True`, it's likely that the bot is **not** in the
chat, so methods like `respond` or `delete` won't work (but
`edit` will always work).
"""
return isinstance(self.query, _tl.UpdateInlineBotCallbackQuery)
This method will likely fail if `via_inline` is `True`. @auto_answer
""" async def respond(self, *args, **kwargs):
return await self._client.send_message( """
await self.get_input_chat(), *args, **kwargs) Responds to the message (not as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
``entity`` already set.
@auto_answer This method will also `answer` the callback if necessary.
async def reply(self, *args, **kwargs):
"""
Replies to the message (as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
both ``entity`` and ``reply_to`` already set.
This method will also `answer` the callback if necessary. This method will likely fail if `via_inline` is `True`.
"""
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
This method will likely fail if `via_inline` is `True`. @auto_answer
""" async def reply(self, *args, **kwargs):
kwargs['reply_to'] = self.query.msg_id """
return await self._client.send_message( Replies to the message (as a reply). Shorthand for
await self.get_input_chat(), *args, **kwargs) `telethon.client.messages.MessageMethods.send_message` with
both ``entity`` and ``reply_to`` already set.
@auto_answer This method will also `answer` the callback if necessary.
async def edit(self, *args, **kwargs):
"""
Edits the message. Shorthand for
`telethon.client.messages.MessageMethods.edit_message` with
the ``entity`` set to the correct :tl:`InputBotInlineMessageID`.
Returns `True` if the edit was successful. This method will likely fail if `via_inline` is `True`.
"""
kwargs['reply_to'] = self.query.msg_id
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
This method will also `answer` the callback if necessary. @auto_answer
async def edit(self, *args, **kwargs):
"""
Edits the message. Shorthand for
`telethon.client.messages.MessageMethods.edit_message` with
the ``entity`` set to the correct :tl:`InputBotInlineMessageID`.
.. note:: Returns `True` if the edit was successful.
This method won't respect the previous message unlike This method will also `answer` the callback if necessary.
`Message.edit <telethon.tl._custom.message.Message.edit>`,
since the message object is normally not present.
"""
if isinstance(self.query.msg_id, _tl.InputBotInlineMessageID):
return await self._client.edit_message(
None, self.query.msg_id, *args, **kwargs
)
else:
return await self._client.edit_message(
await self.get_input_chat(), self.query.msg_id,
*args, **kwargs
)
@auto_answer .. note::
async def delete(self, *args, **kwargs):
"""
Deletes the message. Shorthand for
`telethon.client.messages.MessageMethods.delete_messages` with
``entity`` and ``message_ids`` already set.
If you need to delete more than one message at once, don't use This method won't respect the previous message unlike
this `delete` method. Use a `Message.edit <telethon.tl._custom.message.Message.edit>`,
`telethon.client.telegramclient.TelegramClient` instance directly. since the message object is normally not present.
"""
This method will also `answer` the callback if necessary. if isinstance(self.query.msg_id, _tl.InputBotInlineMessageID):
return await self._client.edit_message(
This method will likely fail if `via_inline` is `True`. None, self.query.msg_id, *args, **kwargs
""" )
return await self._client.delete_messages( else:
await self.get_input_chat(), [self.query.msg_id], return await self._client.edit_message(
await self.get_input_chat(), self.query.msg_id,
*args, **kwargs *args, **kwargs
) )
@auto_answer
async def delete(self, *args, **kwargs):
"""
Deletes the message. Shorthand for
`telethon.client.messages.MessageMethods.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.client.telegramclient.TelegramClient` instance directly.
This method will also `answer` the callback if necessary.
This method will likely fail if `via_inline` is `True`.
"""
return await self._client.delete_messages(
await self.get_input_chat(), [self.query.msg_id],
*args, **kwargs
)

View File

@ -1,10 +1,9 @@
from .common import EventBuilder, EventCommon, name_inner_event from .base import EventBuilder
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
from ..types import _custom from ..types import _custom
@name_inner_event
class ChatAction(EventBuilder): class ChatAction(EventBuilder):
""" """
Occurs on certain chat actions: Occurs on certain chat actions:
@ -20,6 +19,47 @@ class ChatAction(EventBuilder):
Note that "chat" refers to "small group, megagroup and broadcast Note that "chat" refers to "small group, megagroup and broadcast
channel", whereas "group" refers to "small group and megagroup" only. channel", whereas "group" refers to "small group and megagroup" only.
Members:
action_message (`MessageAction <https://tl.telethon.dev/types/message_action.html>`_):
The message invoked by this Chat Action.
new_pin (`bool`):
`True` if there is a new pin.
new_photo (`bool`):
`True` if there's a new chat photo (or it was removed).
photo (:tl:`Photo`, optional):
The new photo (or `None` if it was removed).
user_added (`bool`):
`True` if the user was added by some other.
user_joined (`bool`):
`True` if the user joined on their own.
user_left (`bool`):
`True` if the user left on their own.
user_kicked (`bool`):
`True` if the user was kicked by some other.
user_approved (`bool`):
`True` if the user's join request was approved.
along with `user_joined` will be also True.
created (`bool`, optional):
`True` if this chat was just created.
new_title (`str`, optional):
The new title string for the chat, if applicable.
new_score (`str`, optional):
The new score string for the game, if applicable.
unpin (`bool`):
`True` if the existing pin gets unpinned.
Example Example
.. code-block:: python .. code-block:: python
@ -32,8 +72,64 @@ class ChatAction(EventBuilder):
await event.reply('Welcome to the group!') await event.reply('Welcome to the group!')
""" """
def __init__(self, where, new_photo=None,
added_by=None, kicked_by=None, created=None, from_approval=None,
users=None, new_title=None, pin_ids=None, pin=None, new_score=None):
if isinstance(where, _tl.MessageService):
self.action_message = where
where = where.peer_id
else:
self.action_message = None
# TODO needs some testing (can there be more than one id, and do they follow pin order?)
# same in get_pinned_message
super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None)
self.new_pin = pin_ids is not None
self._pin_ids = pin_ids
self._pinned_messages = None
self.new_photo = new_photo is not None
self.photo = \
new_photo if isinstance(new_photo, _tl.Photo) else None
self._added_by = None
self._kicked_by = None
self.user_added = self.user_joined = self.user_left = \
self.user_kicked = self.unpin = False
if added_by is True or from_approval is True:
self.user_joined = True
elif added_by:
self.user_added = True
self._added_by = added_by
self.user_approved = from_approval
# If `from_id` was not present (it's `True`) or the affected
# user was "kicked by itself", then it left. Else it was kicked.
if kicked_by is True or (users is not None and kicked_by == users):
self.user_left = True
elif kicked_by:
self.user_kicked = True
self._kicked_by = kicked_by
self.created = bool(created)
if isinstance(users, list):
self._user_ids = [utils.get_peer_id(u) for u in users]
elif users:
self._user_ids = [utils.get_peer_id(users)]
else:
self._user_ids = []
self._users = None
self._input_users = None
self.new_title = new_title
self.new_score = new_score
self.unpin = not pin
@classmethod @classmethod
def build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, update, others=None, self_id=None, *todo, **todo2):
# Rely on specific pin updates for unpins, but otherwise ignore them # Rely on specific pin updates for unpins, but otherwise ignore them
# for new pins (we'd rather handle the new service message with pin, # for new pins (we'd rather handle the new service message with pin,
# so that we can act on that message'). # so that we can act on that message').
@ -114,332 +210,230 @@ class ChatAction(EventBuilder):
return cls.Event(msg, return cls.Event(msg,
new_score=action.score) new_score=action.score)
class Event(EventCommon): async def respond(self, *args, **kwargs):
""" """
Represents the event of a new chat action. Responds to the chat action message (not as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
Members: ``entity`` already set.
action_message (`MessageAction <https://tl.telethon.dev/types/message_action.html>`_):
The message invoked by this Chat Action.
new_pin (`bool`):
`True` if there is a new pin.
new_photo (`bool`):
`True` if there's a new chat photo (or it was removed).
photo (:tl:`Photo`, optional):
The new photo (or `None` if it was removed).
user_added (`bool`):
`True` if the user was added by some other.
user_joined (`bool`):
`True` if the user joined on their own.
user_left (`bool`):
`True` if the user left on their own.
user_kicked (`bool`):
`True` if the user was kicked by some other.
user_approved (`bool`):
`True` if the user's join request was approved.
along with `user_joined` will be also True.
created (`bool`, optional):
`True` if this chat was just created.
new_title (`str`, optional):
The new title string for the chat, if applicable.
new_score (`str`, optional):
The new score string for the game, if applicable.
unpin (`bool`):
`True` if the existing pin gets unpinned.
""" """
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
def __init__(self, where, new_photo=None, async def reply(self, *args, **kwargs):
added_by=None, kicked_by=None, created=None, from_approval=None, """
users=None, new_title=None, pin_ids=None, pin=None, new_score=None): Replies to the chat action message (as a reply). Shorthand for
if isinstance(where, _tl.MessageService): `telethon.client.messages.MessageMethods.send_message` with
self.action_message = where both ``entity`` and ``reply_to`` already set.
where = where.peer_id
else:
self.action_message = None
# TODO needs some testing (can there be more than one id, and do they follow pin order?) Has the same effect as `respond` if there is no message.
# same in get_pinned_message """
super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None) if not self.action_message:
return await self.respond(*args, **kwargs)
self.new_pin = pin_ids is not None kwargs['reply_to'] = self.action_message.id
self._pin_ids = pin_ids return await self._client.send_message(
self._pinned_messages = None await self.get_input_chat(), *args, **kwargs)
self.new_photo = new_photo is not None async def delete(self, *args, **kwargs):
self.photo = \ """
new_photo if isinstance(new_photo, _tl.Photo) else None Deletes the chat action message. You're responsible for checking
whether you have the permission to do so, or to except the error
otherwise. Shorthand for
`telethon.client.messages.MessageMethods.delete_messages` with
``entity`` and ``message_ids`` already set.
self._added_by = None Does nothing if no message action triggered this event.
self._kicked_by = None """
self.user_added = self.user_joined = self.user_left = \ if not self.action_message:
self.user_kicked = self.unpin = False return
if added_by is True or from_approval is True: return await self._client.delete_messages(
self.user_joined = True await self.get_input_chat(), [self.action_message],
elif added_by: *args, **kwargs
self.user_added = True )
self._added_by = added_by
self.user_approved = from_approval
# If `from_id` was not present (it's `True`) or the affected async def get_pinned_message(self):
# user was "kicked by itself", then it left. Else it was kicked. """
if kicked_by is True or (users is not None and kicked_by == users): If ``new_pin`` is `True`, this returns the `Message
self.user_left = True <telethon.tl.custom.message.Message>` object that was pinned.
elif kicked_by: """
self.user_kicked = True if self._pinned_messages is None:
self._kicked_by = kicked_by await self.get_pinned_messages()
self.created = bool(created) if self._pinned_messages:
return self._pinned_messages[0]
if isinstance(users, list): async def get_pinned_messages(self):
self._user_ids = [utils.get_peer_id(u) for u in users] """
elif users: If ``new_pin`` is `True`, this returns a `list` of `Message
self._user_ids = [utils.get_peer_id(users)] <telethon.tl.custom.message.Message>` objects that were pinned.
else: """
self._user_ids = [] if not self._pin_ids:
return self._pin_ids # either None or empty list
self._users = None chat = await self.get_input_chat()
self._input_users = None if chat:
self.new_title = new_title self._pinned_messages = await self._client.get_messages(
self.new_score = new_score self._input_chat, ids=self._pin_ids)
self.unpin = not pin
async def respond(self, *args, **kwargs): return self._pinned_messages
"""
Responds to the chat action message (not as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
``entity`` already set.
"""
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
async def reply(self, *args, **kwargs): @property
""" def added_by(self):
Replies to the chat action message (as a reply). Shorthand for """
`telethon.client.messages.MessageMethods.send_message` with The user who added ``users``, if applicable (`None` otherwise).
both ``entity`` and ``reply_to`` already set. """
if self._added_by and not isinstance(self._added_by, _tl.User):
aby = self._entities.get(utils.get_peer_id(self._added_by))
if aby:
self._added_by = aby
Has the same effect as `respond` if there is no message. return self._added_by
"""
if not self.action_message:
return await self.respond(*args, **kwargs)
kwargs['reply_to'] = self.action_message.id async def get_added_by(self):
return await self._client.send_message( """
await self.get_input_chat(), *args, **kwargs) Returns `added_by` but will make an API call if necessary.
"""
if not self.added_by and self._added_by:
self._added_by = await self._client.get_entity(self._added_by)
async def delete(self, *args, **kwargs): return self._added_by
"""
Deletes the chat action message. You're responsible for checking
whether you have the permission to do so, or to except the error
otherwise. Shorthand for
`telethon.client.messages.MessageMethods.delete_messages` with
``entity`` and ``message_ids`` already set.
Does nothing if no message action triggered this event. @property
""" def kicked_by(self):
if not self.action_message: """
return The user who kicked ``users``, if applicable (`None` otherwise).
"""
if self._kicked_by and not isinstance(self._kicked_by, _tl.User):
kby = self._entities.get(utils.get_peer_id(self._kicked_by))
if kby:
self._kicked_by = kby
return await self._client.delete_messages( return self._kicked_by
await self.get_input_chat(), [self.action_message],
*args, **kwargs
)
async def get_pinned_message(self): async def get_kicked_by(self):
""" """
If ``new_pin`` is `True`, this returns the `Message Returns `kicked_by` but will make an API call if necessary.
<telethon.tl.custom.message.Message>` object that was pinned. """
""" if not self.kicked_by and self._kicked_by:
if self._pinned_messages is None: self._kicked_by = await self._client.get_entity(self._kicked_by)
await self.get_pinned_messages()
if self._pinned_messages: return self._kicked_by
return self._pinned_messages[0]
async def get_pinned_messages(self): @property
""" def user(self):
If ``new_pin`` is `True`, this returns a `list` of `Message """
<telethon.tl.custom.message.Message>` objects that were pinned. The first user that takes part in this action. For example, who joined.
"""
if not self._pin_ids:
return self._pin_ids # either None or empty list
chat = await self.get_input_chat() Might be `None` if the information can't be retrieved or
if chat: there is no user taking part.
self._pinned_messages = await self._client.get_messages( """
self._input_chat, ids=self._pin_ids) if self.users:
return self._users[0]
return self._pinned_messages async def get_user(self):
"""
Returns `user` but will make an API call if necessary.
"""
if self.users or await self.get_users():
return self._users[0]
@property @property
def added_by(self): def input_user(self):
""" """
The user who added ``users``, if applicable (`None` otherwise). Input version of the ``self.user`` property.
""" """
if self._added_by and not isinstance(self._added_by, _tl.User): if self.input_users:
aby = self._entities.get(utils.get_peer_id(self._added_by)) return self._input_users[0]
if aby:
self._added_by = aby
return self._added_by async def get_input_user(self):
"""
Returns `input_user` but will make an API call if necessary.
"""
if self.input_users or await self.get_input_users():
return self._input_users[0]
async def get_added_by(self): @property
""" def user_id(self):
Returns `added_by` but will make an API call if necessary. """
""" Returns the marked signed ID of the first user, if any.
if not self.added_by and self._added_by: """
self._added_by = await self._client.get_entity(self._added_by) if self._user_ids:
return self._user_ids[0]
return self._added_by @property
def users(self):
"""
A list of users that take part in this action. For example, who joined.
@property Might be empty if the information can't be retrieved or there
def kicked_by(self): are no users taking part.
""" """
The user who kicked ``users``, if applicable (`None` otherwise). if not self._user_ids:
""" return []
if self._kicked_by and not isinstance(self._kicked_by, _tl.User):
kby = self._entities.get(utils.get_peer_id(self._kicked_by))
if kby:
self._kicked_by = kby
return self._kicked_by if self._users is None:
self._users = [
self._entities[user_id]
for user_id in self._user_ids
if user_id in self._entities
]
async def get_kicked_by(self): return self._users
"""
Returns `kicked_by` but will make an API call if necessary.
"""
if not self.kicked_by and self._kicked_by:
self._kicked_by = await self._client.get_entity(self._kicked_by)
return self._kicked_by async def get_users(self):
"""
Returns `users` but will make an API call if necessary.
"""
if not self._user_ids:
return []
@property # Note: we access the property first so that it fills if needed
def user(self): if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message:
""" await self.action_message._reload_message()
The first user that takes part in this action. For example, who joined. self._users = [
u for u in self.action_message.action_entities
if isinstance(u, (_tl.User, _tl.UserEmpty))]
Might be `None` if the information can't be retrieved or return self._users
there is no user taking part.
"""
if self.users:
return self._users[0]
async def get_user(self): @property
""" def input_users(self):
Returns `user` but will make an API call if necessary. """
""" Input version of the ``self.users`` property.
if self.users or await self.get_users(): """
return self._users[0] if self._input_users is None and self._user_ids:
self._input_users = []
for user_id in self._user_ids:
# Try to get it from our entities
try:
self._input_users.append(utils.get_input_peer(self._entities[user_id]))
continue
except (KeyError, TypeError):
pass
@property return self._input_users or []
def input_user(self):
"""
Input version of the ``self.user`` property.
"""
if self.input_users:
return self._input_users[0]
async def get_input_user(self): async def get_input_users(self):
""" """
Returns `input_user` but will make an API call if necessary. Returns `input_users` but will make an API call if necessary.
""" """
if self.input_users or await self.get_input_users(): if not self._user_ids:
return self._input_users[0] return []
@property # Note: we access the property first so that it fills if needed
def user_id(self): if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message:
""" self._input_users = [
Returns the marked signed ID of the first user, if any. utils.get_input_peer(u)
""" for u in self.action_message.action_entities
if self._user_ids: if isinstance(u, (_tl.User, _tl.UserEmpty))]
return self._user_ids[0]
@property return self._input_users or []
def users(self):
"""
A list of users that take part in this action. For example, who joined.
Might be empty if the information can't be retrieved or there @property
are no users taking part. def user_ids(self):
""" """
if not self._user_ids: Returns the marked signed ID of the users, if any.
return [] """
if self._user_ids:
if self._users is None: return self._user_ids[:]
self._users = [
self._entities[user_id]
for user_id in self._user_ids
if user_id in self._entities
]
return self._users
async def get_users(self):
"""
Returns `users` but will make an API call if necessary.
"""
if not self._user_ids:
return []
# Note: we access the property first so that it fills if needed
if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message:
await self.action_message._reload_message()
self._users = [
u for u in self.action_message.action_entities
if isinstance(u, (_tl.User, _tl.UserEmpty))]
return self._users
@property
def input_users(self):
"""
Input version of the ``self.users`` property.
"""
if self._input_users is None and self._user_ids:
self._input_users = []
for user_id in self._user_ids:
# Try to get it from our entities
try:
self._input_users.append(utils.get_input_peer(self._entities[user_id]))
continue
except (KeyError, TypeError):
pass
return self._input_users or []
async def get_input_users(self):
"""
Returns `input_users` but will make an API call if necessary.
"""
if not self._user_ids:
return []
# Note: we access the property first so that it fills if needed
if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message:
self._input_users = [
utils.get_input_peer(u)
for u in self.action_message.action_entities
if isinstance(u, (_tl.User, _tl.UserEmpty))]
return self._input_users or []
@property
def user_ids(self):
"""
Returns the marked signed ID of the users, if any.
"""
if self._user_ids:
return self._user_ids[:]

View File

@ -1,179 +0,0 @@
import abc
import asyncio
import warnings
from .. import _tl
from .._misc import utils, tlobject
from ..types._custom.chatgetter import ChatGetter
async 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
if not utils.is_list_like(chats):
chats = (chats,)
result = set()
for chat in chats:
if isinstance(chat, int):
result.add(chat)
elif isinstance(chat, tlobject.TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687:
# 0x2d45687 == crc32(b'Peer')
result.add(utils.get_peer_id(chat))
else:
chat = await client.get_input_entity(chat)
if isinstance(chat, _tl.InputPeerSelf):
chat = _tl.PeerUser(self._session_state.user_id)
result.add(utils.get_peer_id(chat))
return result
class EventBuilder(abc.ABC):
"""
The common event builder, with builtin support to filter per chat.
Args:
chats (`entity`, optional):
May be one or more entities (username/peer/etc.), preferably IDs.
By default, only matching chats will be handled.
blacklist_chats (`bool`, optional):
Whether to treat the chats as a blacklist instead of
as a whitelist (default). This means that every chat
will be handled *except* those specified in ``chats``
which will be ignored if ``blacklist_chats=True``.
func (`callable`, optional):
A callable (async or not) function that should accept the event as input
parameter, and return a value indicating whether the event
should be dispatched or not (any truthy value will do, it
does not need to be a `bool`). It works like a custom filter:
.. code-block:: python
@client.on(events.NewMessage(func=lambda e: e.is_private))
async def handler(event):
pass # code here
"""
def __init__(self, chats=None, *, blacklist_chats=False, func=None):
self.chats = chats
self.blacklist_chats = bool(blacklist_chats)
self.resolved = False
self.func = func
self._resolve_lock = None
@classmethod
@abc.abstractmethod
def build(cls, update, others, self_id, entities, client):
"""
Builds an event for the given update if possible, or returns None.
`others` are the rest of updates that came in the same container
as the current `update`.
`self_id` should be the current user's ID, since it is required
for some events which lack this information but still need it.
"""
# TODO So many parameters specific to only some update types seems dirty
async def resolve(self, client):
"""Helper method to allow event builders to be resolved before usage"""
if self.resolved:
return
if not self._resolve_lock:
self._resolve_lock = asyncio.Lock()
async with self._resolve_lock:
if not self.resolved:
await self._resolve(client)
self.resolved = True
async def _resolve(self, client):
self.chats = await _into_id_set(client, self.chats)
def filter(self, event):
"""
Returns a truthy value if the event passed the filter and should be
used, or falsy otherwise. The return value may need to be awaited.
The events must have been resolved before this can be called.
"""
if not self.resolved:
return
if self.chats is not None:
# Note: the `event.chat_id` property checks if it's `None` for us
inside = event.chat_id 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
if not self.func:
return True
# Return the result of func directly as it may need to be awaited
return self.func(event)
class EventCommon(ChatGetter, abc.ABC):
"""
Intermediate class with common things to all events.
Remember that this class implements `ChatGetter
<telethon.tl.custom.chatgetter.ChatGetter>` which
means you have access to all chat properties and methods.
In addition, you can access the `original_update`
field which contains the original :tl:`Update`.
"""
_event_name = 'Event'
def __init__(self, chat_peer=None, msg_id=None, broadcast=None):
super().__init__(chat_peer, broadcast=broadcast)
self._entities = {}
self._client = None
self._message_id = msg_id
self.original_update = None
def _set_client(self, client):
"""
Setter so subclasses can act accordingly when the client is set.
"""
# TODO Nuke
self._client = client
if self._chat_peer:
self._chat, self._input_chat = utils._get_entity_pair(self.chat_id, self._entities)
else:
self._chat = self._input_chat = None
@property
def client(self):
"""
The `telethon.TelegramClient` that created this event.
"""
return self._client
def __str__(self):
return _tl.TLObject.pretty_format(self.to_dict())
def stringify(self):
return _tl.TLObject.pretty_format(self.to_dict(), indent=0)
def to_dict(self):
d = {k: v for k, v in self.__dict__.items() if k[0] != '_'}
d['_'] = self._event_name
return d
def name_inner_event(cls):
"""Decorator to rename cls.Event 'Event' as 'cls.Event'"""
if hasattr(cls, 'Event'):
cls.Event._event_name = '{}.Event'.format(cls.__name__)
else:
warnings.warn('Class {} does not have a inner Event'.format(cls))
return cls

View File

@ -3,34 +3,27 @@ import re
import asyncio import asyncio
from .common import EventBuilder, EventCommon, name_inner_event from .base import EventBuilder
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
from ..types import _custom from ..types import _custom
@name_inner_event class InlineQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
class InlineQuery(EventBuilder):
""" """
Occurs whenever you sign in as a bot and a user Occurs whenever you sign in as a bot and a user
sends an inline query such as ``@bot query``. sends an inline query such as ``@bot query``.
Args: Members:
users (`entity`, optional): query (:tl:`UpdateBotInlineQuery`):
May be one or more entities (username/peer/etc.), preferably IDs. The original :tl:`UpdateBotInlineQuery`.
By default, only inline queries from these users will be handled.
blacklist_users (`bool`, optional): Make sure to access the `text` property of the query if
Whether to treat the users as a blacklist instead of you want the text rather than the actual query object.
as a whitelist (default). This means that every chat
will be handled *except* those specified in ``users``
which will be ignored if ``blacklist_users=True``.
pattern (`str`, `callable`, `Pattern`, optional): pattern_match (`obj`, optional):
If set, only queries matching this pattern will be handled. The resulting object from calling the passed ``pattern``
You can specify a regex-like string which will be matched function, which is ``re.compile(...).match`` by default.
against the message, a callable function that returns `True`
if a message is acceptable, or a compiled regex pattern.
Example Example
.. code-block:: python .. code-block:: python
@ -47,200 +40,163 @@ class InlineQuery(EventBuilder):
builder.article('lowercase', text=event.text.lower()), builder.article('lowercase', text=event.text.lower()),
]) ])
""" """
def __init__( def __init__(self, query):
self, users=None, *, blacklist_users=False, func=None, pattern=None): _custom.chatgetter.ChatGetter.__init__(self, _tl.PeerUser(query.user_id))
super().__init__(users, blacklist_chats=blacklist_users, func=func) _custom.sendergetter.SenderGetter.__init__(self, query.user_id)
self.query = query
if isinstance(pattern, str): self.pattern_match = None
self.pattern = re.compile(pattern).match self._answered = False
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')
@classmethod @classmethod
def build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, _tl.UpdateBotInlineQuery): if isinstance(update, _tl.UpdateBotInlineQuery):
return cls.Event(update) return cls.Event(update)
def filter(self, event): def _set_client(self, client):
if self.pattern: super()._set_client(client)
match = self.pattern(event.text) self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
if not match:
return
event.pattern_match = match
return super().filter(event) @property
def id(self):
class Event(EventCommon, _custom.sendergetter.SenderGetter):
""" """
Represents the event of a new callback query. Returns the unique identifier for the query ID.
Members:
query (:tl:`UpdateBotInlineQuery`):
The original :tl:`UpdateBotInlineQuery`.
Make sure to access the `text` property of the query if
you want the text rather than the actual query object.
pattern_match (`obj`, optional):
The resulting object from calling the passed ``pattern``
function, which is ``re.compile(...).match`` by default.
""" """
def __init__(self, query): return self.query.query_id
super().__init__(chat_peer=_tl.PeerUser(query.user_id))
_custom.sendergetter.SenderGetter.__init__(self, query.user_id)
self.query = query
self.pattern_match = None
self._answered = False
def _set_client(self, client): @property
super()._set_client(client) def text(self):
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) """
Returns the text the user used to make the inline query.
"""
return self.query.query
@property @property
def id(self): def offset(self):
""" """
Returns the unique identifier for the query ID. The string the user's client used as an offset for the query.
""" This will either be empty or equal to offsets passed to `answer`.
return self.query.query_id """
return self.query.offset
@property @property
def text(self): def geo(self):
""" """
Returns the text the user used to make the inline query. If the user location is requested when using inline mode
""" and the user's device is able to send it, this will return
return self.query.query the :tl:`GeoPoint` with the position of the user.
"""
return self.query.geo
@property @property
def offset(self): def builder(self):
""" """
The string the user's client used as an offset for the query. Returns a new `InlineBuilder
This will either be empty or equal to offsets passed to `answer`. <telethon.tl.custom.inlinebuilder.InlineBuilder>` instance.
"""
return self.query.offset
@property See the documentation for `builder` to know what kind of answers
def geo(self): can be given.
""" """
If the user location is requested when using inline mode return _custom.InlineBuilder(self._client)
and the user's device is able to send it, this will return
the :tl:`GeoPoint` with the position of the user.
"""
return self.query.geo
@property async def answer(
def builder(self): self, results=None, cache_time=0, *,
""" gallery=False, next_offset=None, private=False,
Returns a new `InlineBuilder switch_pm=None, switch_pm_param=''):
<telethon.tl.custom.inlinebuilder.InlineBuilder>` instance. """
""" Answers the inline query with the given results.
return _custom.InlineBuilder(self._client)
async def answer( Args:
self, results=None, cache_time=0, *, results (`list`, optional):
gallery=False, next_offset=None, private=False, A list of :tl:`InputBotInlineResult` to use.
switch_pm=None, switch_pm_param=''): You should use `builder` to create these:
"""
Answers the inline query with the given results.
See the documentation for `builder` to know what kind of answers
can be given.
Args:
results (`list`, optional):
A list of :tl:`InputBotInlineResult` to use.
You should use `builder` to create these:
.. code-block:: python
builder = inline.builder
r1 = builder.article('Be nice', text='Have a nice day')
r2 = builder.article('Be bad', text="I don't like you")
await inline.answer([r1, r2])
You can send up to 50 results as documented in
https://core.telegram.org/bots/api#answerinlinequery.
Sending more will raise ``ResultsTooMuchError``,
and you should consider using `next_offset` to
paginate them.
cache_time (`int`, optional):
For how long this result should be cached on
the user's client. Defaults to 0 for no cache.
gallery (`bool`, optional):
Whether the results should show as a gallery (grid) or not.
next_offset (`str`, optional):
The offset the client will send when the user scrolls the
results and it repeats the request.
private (`bool`, optional):
Whether the results should be cached by Telegram
(not private) or by the user's client (private).
switch_pm (`str`, optional):
If set, this text will be shown in the results
to allow the user to switch to private messages.
switch_pm_param (`str`, optional):
Optional parameter to start the bot with if
`switch_pm` was used.
Example:
.. code-block:: python .. code-block:: python
@bot.on(events.InlineQuery) builder = inline.builder
async def handler(event): r1 = builder.article('Be nice', text='Have a nice day')
builder = event.builder r2 = builder.article('Be bad', text="I don't like you")
await inline.answer([r1, r2])
rev_text = event.text[::-1] You can send up to 50 results as documented in
await event.answer([ https://core.telegram.org/bots/api#answerinlinequery.
builder.article('Reverse text', text=rev_text), Sending more will raise ``ResultsTooMuchError``,
builder.photo('/path/to/photo.jpg') and you should consider using `next_offset` to
]) paginate them.
"""
if self._answered:
return
if results: cache_time (`int`, optional):
futures = [self._as_future(x) for x in results] For how long this result should be cached on
the user's client. Defaults to 0 for no cache.
await asyncio.wait(futures) gallery (`bool`, optional):
Whether the results should show as a gallery (grid) or not.
# All futures will be in the `done` *set* that `wait` returns. next_offset (`str`, optional):
# The offset the client will send when the user scrolls the
# Precisely because it's a `set` and not a `list`, it results and it repeats the request.
# will not preserve the order, but since all futures
# completed we can use our original, ordered `list`.
results = [x.result() for x in futures]
else:
results = []
if switch_pm: private (`bool`, optional):
switch_pm = _tl.InlineBotSwitchPM(switch_pm, switch_pm_param) Whether the results should be cached by Telegram
(not private) or by the user's client (private).
return await self._client( switch_pm (`str`, optional):
_tl.fn.messages.SetInlineBotResults( If set, this text will be shown in the results
query_id=self.query.query_id, to allow the user to switch to private messages.
results=results,
cache_time=cache_time, switch_pm_param (`str`, optional):
gallery=gallery, Optional parameter to start the bot with if
next_offset=next_offset, `switch_pm` was used.
private=private,
switch_pm=switch_pm Example:
)
.. code-block:: python
@bot.on(events.InlineQuery)
async def handler(event):
builder = event.builder
rev_text = event.text[::-1]
await event.answer([
builder.article('Reverse text', text=rev_text),
builder.photo('/path/to/photo.jpg')
])
"""
if self._answered:
return
if results:
futures = [self._as_future(x) for x in results]
await asyncio.wait(futures)
# All futures will be in the `done` *set* that `wait` returns.
#
# Precisely because it's a `set` and not a `list`, it
# will not preserve the order, but since all futures
# completed we can use our original, ordered `list`.
results = [x.result() for x in futures]
else:
results = []
if switch_pm:
switch_pm = _tl.InlineBotSwitchPM(switch_pm, switch_pm_param)
return await self._client(
_tl.fn.messages.SetInlineBotResults(
query_id=self.query.query_id,
results=results,
cache_time=cache_time,
gallery=gallery,
next_offset=next_offset,
private=private,
switch_pm=switch_pm
) )
)
@staticmethod @staticmethod
def _as_future(obj): def _as_future(obj):
if inspect.isawaitable(obj): if inspect.isawaitable(obj):
return asyncio.ensure_future(obj) return asyncio.ensure_future(obj)
f = asyncio.get_running_loop().create_future() f = asyncio.get_running_loop().create_future()
f.set_result(obj) f.set_result(obj)
return f return f

View File

@ -1,9 +1,9 @@
from .common import EventBuilder, EventCommon, name_inner_event from .base import EventBuilder
from .. import _tl from .. import _tl
from ..types import _custom
@name_inner_event class MessageDeleted(EventBuilder, _custom.chatgetter.ChatGetter):
class MessageDeleted(EventBuilder):
""" """
Occurs whenever a message is deleted. Note that this event isn't 100% Occurs whenever a message is deleted. Note that this event isn't 100%
reliable, since Telegram doesn't always notify the clients that a message reliable, since Telegram doesn't always notify the clients that a message
@ -35,8 +35,13 @@ class MessageDeleted(EventBuilder):
for msg_id in event.deleted_ids: for msg_id in event.deleted_ids:
print('Message', msg_id, 'was deleted in', event.chat_id) print('Message', msg_id, 'was deleted in', event.chat_id)
""" """
def __init__(self, deleted_ids, peer):
_custom.chatgetter.ChatGetter.__init__(self, chat_peer=peer)
self.deleted_id = None if not deleted_ids else deleted_ids[0]
self.deleted_ids = deleted_ids
@classmethod @classmethod
def build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, _tl.UpdateDeleteMessages): if isinstance(update, _tl.UpdateDeleteMessages):
return cls.Event( return cls.Event(
deleted_ids=update.messages, deleted_ids=update.messages,
@ -47,11 +52,3 @@ class MessageDeleted(EventBuilder):
deleted_ids=update.messages, deleted_ids=update.messages,
peer=_tl.PeerChannel(update.channel_id) peer=_tl.PeerChannel(update.channel_id)
) )
class Event(EventCommon):
def __init__(self, deleted_ids, peer):
super().__init__(
chat_peer=peer, msg_id=(deleted_ids or [0])[0]
)
self.deleted_id = None if not deleted_ids else deleted_ids[0]
self.deleted_ids = deleted_ids

View File

@ -1,10 +1,8 @@
from .common import name_inner_event from .base import EventBuilder
from .newmessage import NewMessage
from .. import _tl from .. import _tl
@name_inner_event class MessageEdited(EventBuilder):
class MessageEdited(NewMessage):
""" """
Occurs whenever a message is edited. Just like `NewMessage Occurs whenever a message is edited. Just like `NewMessage
<telethon.events.newmessage.NewMessage>`, you should treat <telethon.events.newmessage.NewMessage>`, you should treat
@ -43,10 +41,7 @@ class MessageEdited(NewMessage):
print('Message', event.id, 'changed at', event.date) print('Message', event.id, 'changed at', event.date)
""" """
@classmethod @classmethod
def build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, (_tl.UpdateEditMessage, if isinstance(update, (_tl.UpdateEditMessage,
_tl.UpdateEditChannelMessage)): _tl.UpdateEditChannelMessage)):
return cls.Event(update.message) return cls.Event(update.message)
class Event(NewMessage.Event):
pass # Required if we want a different name for it

View File

@ -1,18 +1,24 @@
from .common import EventBuilder, EventCommon, name_inner_event from .base import EventBuilder
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
@name_inner_event
class MessageRead(EventBuilder): class MessageRead(EventBuilder):
""" """
Occurs whenever one or more messages are read in a chat. Occurs whenever one or more messages are read in a chat.
Args: Members:
inbox (`bool`, optional): max_id (`int`):
If this argument is `True`, then when you read someone else's Up to which message ID has been read. Every message
messages the event will be fired. By default (`False`) only with an ID equal or lower to it have been read.
when messages you sent are read by someone else will fire it.
outbox (`bool`):
`True` if someone else has read your messages.
contents (`bool`):
`True` if what was read were the contents of a message.
This will be the case when e.g. you play a voice note.
It may only be set on ``inbox`` events.
Example Example
.. code-block:: python .. code-block:: python
@ -29,13 +35,17 @@ class MessageRead(EventBuilder):
# Log when you read message in a chat (from your "inbox") # Log when you read message in a chat (from your "inbox")
print('You have read messages until', event.max_id) print('You have read messages until', event.max_id)
""" """
def __init__( def __init__(self, peer=None, max_id=None, out=False, contents=False,
self, chats=None, *, blacklist_chats=False, func=None, inbox=False): message_ids=None):
super().__init__(chats, blacklist_chats=blacklist_chats, func=func) self.outbox = out
self.inbox = inbox self.contents = contents
self._message_ids = message_ids or []
self._messages = None
self.max_id = max_id or max(message_ids or [], default=None)
super().__init__(peer, self.max_id)
@classmethod @classmethod
def build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, _tl.UpdateReadHistoryInbox): if isinstance(update, _tl.UpdateReadHistoryInbox):
return cls.Event(update.peer, update.max_id, False) return cls.Event(update.peer, update.max_id, False)
elif isinstance(update, _tl.UpdateReadHistoryOutbox): elif isinstance(update, _tl.UpdateReadHistoryOutbox):
@ -54,90 +64,58 @@ class MessageRead(EventBuilder):
message_ids=update.messages, message_ids=update.messages,
contents=True) contents=True)
def filter(self, event): @property
if self.inbox == event.outbox: def inbox(self):
return
return super().filter(event)
class Event(EventCommon):
""" """
Represents the event of one or more messages being read. `True` if you have read someone else's messages.
Members:
max_id (`int`):
Up to which message ID has been read. Every message
with an ID equal or lower to it have been read.
outbox (`bool`):
`True` if someone else has read your messages.
contents (`bool`):
`True` if what was read were the contents of a message.
This will be the case when e.g. you play a voice note.
It may only be set on ``inbox`` events.
""" """
def __init__(self, peer=None, max_id=None, out=False, contents=False, return not self.outbox
message_ids=None):
self.outbox = out
self.contents = contents
self._message_ids = message_ids or []
self._messages = None
self.max_id = max_id or max(message_ids or [], default=None)
super().__init__(peer, self.max_id)
@property @property
def inbox(self): def message_ids(self):
""" """
`True` if you have read someone else's messages. The IDs of the messages **which contents'** were read.
"""
return not self.outbox
@property Use :meth:`is_read` if you need to check whether a message
def message_ids(self): was read instead checking if it's in here.
""" """
The IDs of the messages **which contents'** were read. return self._message_ids
Use :meth:`is_read` if you need to check whether a message async def get_messages(self):
was read instead checking if it's in here. """
""" Returns the list of `Message <telethon.tl.custom.message.Message>`
return self._message_ids **which contents'** were read.
async def get_messages(self): Use :meth:`is_read` if you need to check whether a message
""" was read instead checking if it's in here.
Returns the list of `Message <telethon.tl.custom.message.Message>` """
**which contents'** were read. if self._messages is None:
chat = await self.get_input_chat()
Use :meth:`is_read` if you need to check whether a message if not chat:
was read instead checking if it's in here. self._messages = []
"""
if self._messages is None:
chat = await self.get_input_chat()
if not chat:
self._messages = []
else:
self._messages = await self._client.get_messages(
chat, ids=self._message_ids)
return self._messages
def is_read(self, message):
"""
Returns `True` if the given message (or its ID) has been read.
If a list-like argument is provided, this method will return a
list of booleans indicating which messages have been read.
"""
if utils.is_list_like(message):
return [(m if isinstance(m, int) else m.id) <= self.max_id
for m in message]
else: else:
return (message if isinstance(message, int) self._messages = await self._client.get_messages(
else message.id) <= self.max_id chat, ids=self._message_ids)
def __contains__(self, message): return self._messages
"""`True` if the message(s) are read message."""
if utils.is_list_like(message): def is_read(self, message):
return all(self.is_read(message)) """
else: Returns `True` if the given message (or its ID) has been read.
return self.is_read(message)
If a list-like argument is provided, this method will return a
list of booleans indicating which messages have been read.
"""
if utils.is_list_like(message):
return [(m if isinstance(m, int) else m.id) <= self.max_id
for m in message]
else:
return (message if isinstance(message, int)
else message.id) <= self.max_id
def __contains__(self, message):
"""`True` if the message(s) are read message."""
if utils.is_list_like(message):
return all(self.is_read(message))
else:
return self.is_read(message)

View File

@ -1,43 +1,43 @@
import re import re
from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set from .base import EventBuilder
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
from ..types import _custom from ..types import _custom
@name_inner_event class NewMessageEvent(EventBuilder, Message):
class NewMessage(EventBuilder):
""" """
Occurs whenever a new text message or a message with media arrives. Represents the event of a new message. This event can be treated
to all effects as a `Message <telethon.tl.custom.message.Message>`,
so please **refer to its documentation** to know what you can do
with this event.
Args: Members:
incoming (`bool`, optional): message (`Message <telethon.tl.custom.message.Message>`):
If set to `True`, only **incoming** messages will be handled. This is the only difference with the received
Mutually exclusive with ``outgoing`` (can only set one of either). `Message <telethon.tl.custom.message.Message>`, and will
return the `telethon.tl.custom.message.Message` itself,
not the text.
outgoing (`bool`, optional): See `Message <telethon.tl.custom.message.Message>` for
If set to `True`, only **outgoing** messages will be handled. the rest of available members and methods.
Mutually exclusive with ``incoming`` (can only set one of either).
from_users (`entity`, optional): pattern_match (`obj`):
Unlike `chats`, this parameter filters the *senders* of the The resulting object from calling the passed ``pattern`` function.
message. That is, only messages *sent by these users* will be Here's an example using a string (defaults to regex match):
handled. Use `chats` if you want private messages with this/these
users. `from_users` lets you filter by messages sent by *one or
more* users across the desired chats (doesn't need a list).
forwards (`bool`, optional): >>> from telethon import TelegramClient, events
Whether forwarded messages should be handled or not. By default, >>> client = TelegramClient(...)
both forwarded and normal messages are included. If it's `True` >>>
*only* forwards will be handled. If it's `False` only messages >>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!'))
that are *not* forwards will be handled. ... async def handler(event):
... # In this case, the result is a ``Match`` object
pattern (`str`, `callable`, `Pattern`, optional): ... # since the `str` pattern was converted into
If set, only messages matching this pattern will be handled. ... # the ``re.compile(pattern).match`` function.
You can specify a regex-like string which will be matched ... print('Welcomed', event.pattern_match.group(1))
against the message, a callable function that returns `True` ...
if a message is acceptable, or a compiled regex pattern. >>>
Example Example
.. code-block:: python .. code-block:: python
@ -57,45 +57,16 @@ class NewMessage(EventBuilder):
await asyncio.sleep(5) await asyncio.sleep(5)
await client.delete_messages(event.chat_id, [event.id, m.id]) await client.delete_messages(event.chat_id, [event.id, m.id])
""" """
def __init__(self, chats=None, *, blacklist_chats=False, func=None, def __init__(self, message):
incoming=None, outgoing=None, self.__dict__['_init'] = False
from_users=None, forwards=None, pattern=None): super().__init__(chat_peer=message.peer_id,
if incoming and outgoing: msg_id=message.id, broadcast=bool(message.post))
incoming = outgoing = None # Same as no filter
elif incoming is not None and outgoing is None:
outgoing = not incoming
elif outgoing is not None and incoming is None:
incoming = not outgoing
elif all(x is not None and not x for x in (incoming, outgoing)):
raise ValueError("Don't create an event handler if you "
"don't want neither incoming nor outgoing!")
super().__init__(chats, blacklist_chats=blacklist_chats, func=func) self.pattern_match = None
self.incoming = incoming self.message = message
self.outgoing = outgoing
self.from_users = from_users
self.forwards = forwards
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')
# Should we short-circuit? E.g. perform no check at all
self._no_check = all(x is None for x in (
self.chats, self.incoming, self.outgoing, self.pattern,
self.from_users, self.forwards, self.from_users, self.func
))
async def _resolve(self, client):
await super()._resolve(client)
self.from_users = await _into_id_set(client, self.from_users)
@classmethod @classmethod
def build(cls, update, others, self_id, entities, client): def _build(cls, update, others, self_id, entities, client):
if isinstance(update, if isinstance(update,
(_tl.UpdateNewMessage, _tl.UpdateNewChannelMessage)): (_tl.UpdateNewMessage, _tl.UpdateNewChannelMessage)):
if not isinstance(update.message, _tl.Message): if not isinstance(update.message, _tl.Message):
@ -139,85 +110,3 @@ class NewMessage(EventBuilder):
return return
return cls.Event(_custom.Message._new(client, msg, entities, None)) return cls.Event(_custom.Message._new(client, msg, entities, None))
def filter(self, event):
if self._no_check:
return event
if self.incoming and event.message.out:
return
if self.outgoing and not event.message.out:
return
if self.forwards is not None:
if bool(self.forwards) != bool(event.message.fwd_from):
return
if self.from_users is not None:
if event.message.sender_id not in self.from_users:
return
if self.pattern:
match = self.pattern(event.message.message or '')
if not match:
return
event.pattern_match = match
return super().filter(event)
class Event(EventCommon):
"""
Represents the event of a new message. This event can be treated
to all effects as a `Message <telethon.tl.custom.message.Message>`,
so please **refer to its documentation** to know what you can do
with this event.
Members:
message (`Message <telethon.tl.custom.message.Message>`):
This is the only difference with the received
`Message <telethon.tl.custom.message.Message>`, and will
return the `telethon.tl.custom.message.Message` itself,
not the text.
See `Message <telethon.tl.custom.message.Message>` for
the rest of available members and methods.
pattern_match (`obj`):
The resulting object from calling the passed ``pattern`` function.
Here's an example using a string (defaults to regex match):
>>> from telethon import TelegramClient, events
>>> client = TelegramClient(...)
>>>
>>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!'))
... async def handler(event):
... # In this case, the result is a ``Match`` object
... # since the `str` pattern was converted into
... # the ``re.compile(pattern).match`` function.
... print('Welcomed', event.pattern_match.group(1))
...
>>>
"""
def __init__(self, message):
self.__dict__['_init'] = False
super().__init__(chat_peer=message.peer_id,
msg_id=message.id, broadcast=bool(message.post))
self.pattern_match = None
self.message = message
def _set_client(self, client):
super()._set_client(client)
m = self.message
self.__dict__['_init'] = True # No new attributes can be set
def __getattr__(self, item):
if item in self.__dict__:
return self.__dict__[item]
else:
return getattr(self.message, item)
def __setattr__(self, name, value):
if not self.__dict__['_init'] or name in self.__dict__:
self.__dict__[name] = value
else:
setattr(self.message, name, value)

View File

@ -1,4 +1,4 @@
from .common import EventBuilder from .base import EventBuilder
from .._misc import utils from .._misc import utils
@ -8,11 +8,6 @@ class Raw(EventBuilder):
:tl:`Update` object that Telegram sends. You normally shouldn't :tl:`Update` object that Telegram sends. You normally shouldn't
need these. need these.
Args:
types (`list` | `tuple` | `type`, optional):
The type or types that the :tl:`Update` instance must be.
Equivalent to ``if not isinstance(update, types): return``.
Example Example
.. code-block:: python .. code-block:: python
@ -23,31 +18,6 @@ class Raw(EventBuilder):
# Print all incoming updates # Print all incoming updates
print(update.stringify()) print(update.stringify())
""" """
def __init__(self, types=None, *, func=None):
super().__init__(func=func)
if not types:
self.types = None
elif not utils.is_list_like(types):
if not isinstance(types, type):
raise TypeError('Invalid input type given: {}'.format(types))
self.types = types
else:
if not all(isinstance(x, type) for x in types):
raise TypeError('Invalid input types given: {}'.format(types))
self.types = tuple(types)
async def resolve(self, client):
self.resolved = True
@classmethod @classmethod
def build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, update, others=None, self_id=None, *todo, **todo2):
return update return update
def filter(self, event):
if not self.types or isinstance(event, self.types):
if self.func:
# Return the result of func directly as it may need to be awaited
return self.func(event)
return event

View File

@ -1,7 +1,7 @@
import datetime import datetime
import functools import functools
from .common import EventBuilder, EventCommon, name_inner_event from .base import EventBuilder
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
from ..types import _custom from ..types import _custom
@ -32,11 +32,25 @@ def _requires_status(function):
return wrapped return wrapped
@name_inner_event class UserUpdateEvent(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
class UserUpdate(EventBuilder):
""" """
Occurs whenever a user goes online, starts typing, etc. Occurs whenever a user goes online, starts typing, etc.
Members:
status (:tl:`UserStatus`, optional):
The user status if the update is about going online or offline.
You should check this attribute first before checking any
of the seen within properties, since they will all be `None`
if the status is not set.
action (:tl:`SendMessageAction`, optional):
The "typing" action if any the user is performing if any.
You should check this attribute first before checking any
of the typing properties, since they will all be `None`
if the action is not set.
Example Example
.. code-block:: python .. code-block:: python
@ -48,262 +62,242 @@ class UserUpdate(EventBuilder):
if event.uploading: if event.uploading:
await client.send_message(event.user_id, 'What are you sending?') await client.send_message(event.user_id, 'What are you sending?')
""" """
def __init__(self, peer, *, status=None, chat_peer=None, typing=None):
_custom.chatgetter.ChatGetter.__init__(self, chat_peer or peer)
_custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer))
self.status = status
self.action = typing
@classmethod @classmethod
def build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, _tl.UpdateUserStatus): if isinstance(update, _tl.UpdateUserStatus):
return cls.Event(_tl.PeerUser(update.user_id), return UserUpdateEvent(_tl.PeerUser(update.user_id),
status=update.status) status=update.status)
elif isinstance(update, _tl.UpdateChannelUserTyping): elif isinstance(update, _tl.UpdateChannelUserTyping):
return cls.Event(update.from_id, return UserUpdateEvent(update.from_id,
chat_peer=_tl.PeerChannel(update.channel_id), chat_peer=_tl.PeerChannel(update.channel_id),
typing=update.action) typing=update.action)
elif isinstance(update, _tl.UpdateChatUserTyping): elif isinstance(update, _tl.UpdateChatUserTyping):
return cls.Event(update.from_id, return UserUpdateEvent(update.from_id,
chat_peer=_tl.PeerChat(update.chat_id), chat_peer=_tl.PeerChat(update.chat_id),
typing=update.action) typing=update.action)
elif isinstance(update, _tl.UpdateUserTyping): elif isinstance(update, _tl.UpdateUserTyping):
return cls.Event(update.user_id, return UserUpdateEvent(update.user_id,
typing=update.action) typing=update.action)
class Event(EventCommon, _custom.sendergetter.SenderGetter): def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
@property
def user(self):
"""Alias for `sender <telethon.tl.custom.sendergetter.SenderGetter.sender>`."""
return self.sender
async def get_user(self):
"""Alias for `get_sender <telethon.tl.custom.sendergetter.SenderGetter.get_sender>`."""
return await self.get_sender()
@property
def input_user(self):
"""Alias for `input_sender <telethon.tl.custom.sendergetter.SenderGetter.input_sender>`."""
return self.input_sender
async def get_input_user(self):
"""Alias for `get_input_sender <telethon.tl.custom.sendergetter.SenderGetter.get_input_sender>`."""
return await self.get_input_sender()
@property
def user_id(self):
"""Alias for `sender_id <telethon.tl.custom.sendergetter.SenderGetter.sender_id>`."""
return self.sender_id
@property
@_requires_action
def typing(self):
""" """
Represents the event of a user update `True` if the action is typing a message.
such as gone online, started typing, etc.
Members:
status (:tl:`UserStatus`, optional):
The user status if the update is about going online or offline.
You should check this attribute first before checking any
of the seen within properties, since they will all be `None`
if the status is not set.
action (:tl:`SendMessageAction`, optional):
The "typing" action if any the user is performing if any.
You should check this attribute first before checking any
of the typing properties, since they will all be `None`
if the action is not set.
""" """
def __init__(self, peer, *, status=None, chat_peer=None, typing=None): return isinstance(self.action, _tl.SendMessageTypingAction)
super().__init__(chat_peer or peer)
_custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer))
self.status = status @property
self.action = typing @_requires_action
def uploading(self):
"""
`True` if the action is uploading something.
"""
return isinstance(self.action, (
_tl.SendMessageChooseContactAction,
_tl.SendMessageChooseStickerAction,
_tl.SendMessageUploadAudioAction,
_tl.SendMessageUploadDocumentAction,
_tl.SendMessageUploadPhotoAction,
_tl.SendMessageUploadRoundAction,
_tl.SendMessageUploadVideoAction
))
def _set_client(self, client): @property
super()._set_client(client) @_requires_action
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) def recording(self):
"""
`True` if the action is recording something.
"""
return isinstance(self.action, (
_tl.SendMessageRecordAudioAction,
_tl.SendMessageRecordRoundAction,
_tl.SendMessageRecordVideoAction
))
@property @property
def user(self): @_requires_action
"""Alias for `sender <telethon.tl.custom.sendergetter.SenderGetter.sender>`.""" def playing(self):
return self.sender """
`True` if the action is playing a game.
"""
return isinstance(self.action, _tl.SendMessageGamePlayAction)
async def get_user(self): @property
"""Alias for `get_sender <telethon.tl.custom.sendergetter.SenderGetter.get_sender>`.""" @_requires_action
return await self.get_sender() def cancel(self):
"""
`True` if the action was cancelling other actions.
"""
return isinstance(self.action, _tl.SendMessageCancelAction)
@property @property
def input_user(self): @_requires_action
"""Alias for `input_sender <telethon.tl.custom.sendergetter.SenderGetter.input_sender>`.""" def geo(self):
return self.input_sender """
`True` if what's being uploaded is a geo.
"""
return isinstance(self.action, _tl.SendMessageGeoLocationAction)
async def get_input_user(self): @property
"""Alias for `get_input_sender <telethon.tl.custom.sendergetter.SenderGetter.get_input_sender>`.""" @_requires_action
return await self.get_input_sender() def audio(self):
"""
`True` if what's being recorded/uploaded is an audio.
"""
return isinstance(self.action, (
_tl.SendMessageRecordAudioAction,
_tl.SendMessageUploadAudioAction
))
@property @property
def user_id(self): @_requires_action
"""Alias for `sender_id <telethon.tl.custom.sendergetter.SenderGetter.sender_id>`.""" def round(self):
return self.sender_id """
`True` if what's being recorded/uploaded is a round video.
"""
return isinstance(self.action, (
_tl.SendMessageRecordRoundAction,
_tl.SendMessageUploadRoundAction
))
@property @property
@_requires_action @_requires_action
def typing(self): def video(self):
""" """
`True` if the action is typing a message. `True` if what's being recorded/uploaded is an video.
""" """
return isinstance(self.action, _tl.SendMessageTypingAction) return isinstance(self.action, (
_tl.SendMessageRecordVideoAction,
_tl.SendMessageUploadVideoAction
))
@property @property
@_requires_action @_requires_action
def uploading(self): def contact(self):
""" """
`True` if the action is uploading something. `True` if what's being uploaded (selected) is a contact.
""" """
return isinstance(self.action, ( return isinstance(self.action, _tl.SendMessageChooseContactAction)
_tl.SendMessageChooseContactAction,
_tl.SendMessageChooseStickerAction,
_tl.SendMessageUploadAudioAction,
_tl.SendMessageUploadDocumentAction,
_tl.SendMessageUploadPhotoAction,
_tl.SendMessageUploadRoundAction,
_tl.SendMessageUploadVideoAction
))
@property @property
@_requires_action @_requires_action
def recording(self): def document(self):
""" """
`True` if the action is recording something. `True` if what's being uploaded is document.
""" """
return isinstance(self.action, ( return isinstance(self.action, _tl.SendMessageUploadDocumentAction)
_tl.SendMessageRecordAudioAction,
_tl.SendMessageRecordRoundAction,
_tl.SendMessageRecordVideoAction
))
@property @property
@_requires_action @_requires_action
def playing(self): def sticker(self):
""" """
`True` if the action is playing a game. `True` if what's being uploaded is a sticker.
""" """
return isinstance(self.action, _tl.SendMessageGamePlayAction) return isinstance(self.action, _tl.SendMessageChooseStickerAction)
@property @property
@_requires_action @_requires_action
def cancel(self): def photo(self):
""" """
`True` if the action was cancelling other actions. `True` if what's being uploaded is a photo.
""" """
return isinstance(self.action, _tl.SendMessageCancelAction) return isinstance(self.action, _tl.SendMessageUploadPhotoAction)
@property @property
@_requires_action @_requires_action
def geo(self): def last_seen(self):
""" """
`True` if what's being uploaded is a geo. Exact `datetime.datetime` when the user was last seen if known.
""" """
return isinstance(self.action, _tl.SendMessageGeoLocationAction) if isinstance(self.status, _tl.UserStatusOffline):
return self.status.was_online
@property @property
@_requires_action @_requires_status
def audio(self): def until(self):
""" """
`True` if what's being recorded/uploaded is an audio. The `datetime.datetime` until when the user should appear online.
""" """
return isinstance(self.action, ( if isinstance(self.status, _tl.UserStatusOnline):
_tl.SendMessageRecordAudioAction, return self.status.expires
_tl.SendMessageUploadAudioAction
))
@property def _last_seen_delta(self):
@_requires_action if isinstance(self.status, _tl.UserStatusOffline):
def round(self): return datetime.datetime.now(tz=datetime.timezone.utc) - self.status.was_online
""" elif isinstance(self.status, _tl.UserStatusOnline):
`True` if what's being recorded/uploaded is a round video. return datetime.timedelta(days=0)
""" elif isinstance(self.status, _tl.UserStatusRecently):
return isinstance(self.action, ( return datetime.timedelta(days=1)
_tl.SendMessageRecordRoundAction, elif isinstance(self.status, _tl.UserStatusLastWeek):
_tl.SendMessageUploadRoundAction return datetime.timedelta(days=7)
)) elif isinstance(self.status, _tl.UserStatusLastMonth):
return datetime.timedelta(days=30)
else:
return datetime.timedelta(days=365)
@property @property
@_requires_action @_requires_status
def video(self): def online(self):
""" """
`True` if what's being recorded/uploaded is an video. `True` if the user is currently online,
""" """
return isinstance(self.action, ( return self._last_seen_delta() <= datetime.timedelta(days=0)
_tl.SendMessageRecordVideoAction,
_tl.SendMessageUploadVideoAction
))
@property @property
@_requires_action @_requires_status
def contact(self): def recently(self):
""" """
`True` if what's being uploaded (selected) is a contact. `True` if the user was seen within a day.
""" """
return isinstance(self.action, _tl.SendMessageChooseContactAction) return self._last_seen_delta() <= datetime.timedelta(days=1)
@property @property
@_requires_action @_requires_status
def document(self): def within_weeks(self):
""" """
`True` if what's being uploaded is document. `True` if the user was seen within 7 days.
""" """
return isinstance(self.action, _tl.SendMessageUploadDocumentAction) return self._last_seen_delta() <= datetime.timedelta(days=7)
@property @property
@_requires_action @_requires_status
def sticker(self): def within_months(self):
""" """
`True` if what's being uploaded is a sticker. `True` if the user was seen within 30 days.
""" """
return isinstance(self.action, _tl.SendMessageChooseStickerAction) return self._last_seen_delta() <= datetime.timedelta(days=30)
@property
@_requires_action
def photo(self):
"""
`True` if what's being uploaded is a photo.
"""
return isinstance(self.action, _tl.SendMessageUploadPhotoAction)
@property
@_requires_action
def last_seen(self):
"""
Exact `datetime.datetime` when the user was last seen if known.
"""
if isinstance(self.status, _tl.UserStatusOffline):
return self.status.was_online
@property
@_requires_status
def until(self):
"""
The `datetime.datetime` until when the user should appear online.
"""
if isinstance(self.status, _tl.UserStatusOnline):
return self.status.expires
def _last_seen_delta(self):
if isinstance(self.status, _tl.UserStatusOffline):
return datetime.datetime.now(tz=datetime.timezone.utc) - self.status.was_online
elif isinstance(self.status, _tl.UserStatusOnline):
return datetime.timedelta(days=0)
elif isinstance(self.status, _tl.UserStatusRecently):
return datetime.timedelta(days=1)
elif isinstance(self.status, _tl.UserStatusLastWeek):
return datetime.timedelta(days=7)
elif isinstance(self.status, _tl.UserStatusLastMonth):
return datetime.timedelta(days=30)
else:
return datetime.timedelta(days=365)
@property
@_requires_status
def online(self):
"""
`True` if the user is currently online,
"""
return self._last_seen_delta() <= datetime.timedelta(days=0)
@property
@_requires_status
def recently(self):
"""
`True` if the user was seen within a day.
"""
return self._last_seen_delta() <= datetime.timedelta(days=1)
@property
@_requires_status
def within_weeks(self):
"""
`True` if the user was seen within 7 days.
"""
return self._last_seen_delta() <= datetime.timedelta(days=7)
@property
@_requires_status
def within_months(self):
"""
`True` if the user was seen within 30 days.
"""
return self._last_seen_delta() <= datetime.timedelta(days=30)