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.
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 ..types import _custom
from .._events.common import EventBuilder, EventCommon
from .._events.base import EventBuilder
from .._misc import enums

View File

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

View File

@ -2,7 +2,7 @@ import asyncio
import time
import weakref
from .common import EventBuilder, EventCommon, name_inner_event
from .base import EventBuilder
from .._misc import utils
from .. import _tl
from ..types import _custom
@ -64,13 +64,16 @@ class AlbumHack:
await asyncio.sleep(diff)
@name_inner_event
class Album(EventBuilder):
class Album(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
"""
Occurs whenever you receive an album. This event only exists
to ease dealing with an unknown amount of messages that belong
to the same album.
Members:
messages (Sequence[`Message <telethon.tl._custom.message.Message>`]):
The list of messages belonging to the same album.
Example
.. code-block:: python
@ -91,12 +94,20 @@ class Album(EventBuilder):
await event.messages[4].reply('Cool!')
"""
def __init__(
self, chats=None, *, blacklist_chats=False, func=None):
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
def __init__(self, messages):
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:
chat_peer = message.peer_id
@classmethod
def build(cls, update, others=None, self_id=None, *todo, **todo2):
_custom.chatgetter.ChatGetter.__init__(self, chat_peer=chat_peer, broadcast=bool(message.post))
_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:
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)
])
def filter(self, event):
# Albums with less than two messages require a few hacks to work.
if len(event.messages) > 1:
return super().filter(event)
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
class Event(EventCommon, _custom.sendergetter.SenderGetter):
"""
Represents the event of a new album.
self.messages = [
_custom.Message._new(client, m, self._entities, None)
for m in self.messages
]
Members:
messages (Sequence[`Message <telethon.tl._custom.message.Message>`]):
The list of messages belonging to the same album.
"""
def __init__(self, messages):
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
if len(self.messages) == 1:
# This will require hacks to be a proper album event
hack = client._albums.get(self.grouped_id)
if hack is None:
client._albums[self.grouped_id] = AlbumHack(client, self)
else:
chat_peer = message.peer_id
hack.extend(self.messages)
super().__init__(chat_peer=chat_peer,
msg_id=message.id, broadcast=bool(message.post))
@property
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)
self.messages = messages
@property
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):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
@property
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), '')
self.messages = [
_custom.Message._new(client, m, self._entities, None)
for m in self.messages
]
@property
def is_reply(self):
"""
`True` if the album is a reply to some other message.
if len(self.messages) == 1:
# This will require hacks to be a proper album event
hack = client._albums.get(self.grouped_id)
if hack is None:
client._albums[self.grouped_id] = AlbumHack(client, self)
else:
hack.extend(self.messages)
Remember that you can access the ID of the message
this one is replying to through `reply_to_msg_id`,
and the `Message` object with `get_reply_message()`.
"""
# Each individual message in an album all reply to the same message
return self.messages[0].is_reply
@property
def grouped_id(self):
"""
The shared ``grouped_id`` between all the messages.
"""
return self.messages[0].grouped_id
@property
def forward(self):
"""
The `Forward <telethon.tl._custom.forward.Forward>`
information for the first message in the album if it was forwarded.
"""
# Each individual message in an album all reply to the same message
return self.messages[0].forward
@property
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), '')
# endregion Public Properties
@property
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), '')
# region Public Methods
@property
def is_reply(self):
"""
`True` if the album is a reply to some other message.
async def get_reply_message(self):
"""
The `Message <telethon.tl._custom.message.Message>`
that this album is replying to, or `None`.
Remember that you can access the ID of the message
this one is replying to through `reply_to_msg_id`,
and the `Message` object with `get_reply_message()`.
"""
# Each individual message in an album all reply to the same message
return self.messages[0].is_reply
The result will be cached after its first use.
"""
return await self.messages[0].get_reply_message()
@property
def forward(self):
"""
The `Forward <telethon.tl._custom.forward.Forward>`
information for the first message in the album if it was forwarded.
"""
# Each individual message in an album all reply to the same message
return self.messages[0].forward
async def respond(self, *args, **kwargs):
"""
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)
# 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):
"""
The `Message <telethon.tl._custom.message.Message>`
that this album is replying to, or `None`.
async def edit(self, *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.
The result will be cached after its first use.
"""
return await self.messages[0].get_reply_message()
Returns `None` if the message was incoming,
or the edited `Message` otherwise.
async def respond(self, *args, **kwargs):
"""
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)
.. note::
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)
This is different from `client.edit_message
<telethon.client.messages.MessageMethods.edit_message>`
and **will respect** the previous state of the message.
For example, if the message didn't have a link preview,
the edit won't add one by default, and you should force
it by setting it to `True` if you want it.
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)
This is generally the most desired and convenient behaviour,
and will work for link previews and message buttons.
"""
for msg in self.messages:
if msg.raw_text:
return await msg.edit(*args, **kwargs)
async def edit(self, *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.
return await self.messages[0].edit(*args, **kwargs)
Returns `None` if the message was incoming,
or the edited `Message` otherwise.
async def delete(self, *args, **kwargs):
"""
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
<telethon.client.messages.MessageMethods.edit_message>`
and **will respect** the previous state of the message.
For example, if the message didn't have a link preview,
the edit won't add one by default, and you should force
it by setting it to `True` if you want it.
async def pin(self, *, notify=False):
"""
Pins the first photo in the album. Shorthand for
`telethon.client.messages.MessageMethods.pin_message`
with both ``entity`` and ``message`` already set.
"""
return await self.messages[0].pin(notify=notify)
This is generally the most desired and convenient behaviour,
and will work for link previews and message buttons.
"""
for msg in self.messages:
if msg.raw_text:
return await msg.edit(*args, **kwargs)
def __len__(self):
"""
Return the amount of messages in the album.
return await self.messages[0].edit(*args, **kwargs)
Equivalent to ``len(self.messages)``.
"""
return len(self.messages)
async def delete(self, *args, **kwargs):
"""
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
)
def __iter__(self):
"""
Iterate over the messages in the album.
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)
Equivalent to ``iter(self.messages)``.
"""
return iter(self.messages)
async def pin(self, *, notify=False):
"""
Pins the first photo in the album. Shorthand for
`telethon.client.messages.MessageMethods.pin_message`
with both ``entity`` and ``message`` already set.
"""
return await self.messages[0].pin(notify=notify)
def __getitem__(self, n):
"""
Access the n'th message in the album.
def __len__(self):
"""
Return the amount of messages in the album.
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]
Equivalent to ``event.messages[n]``.
"""
return self.messages[n]

View File

@ -1,7 +1,4 @@
from .raw import Raw
_HANDLERS_ATTRIBUTE = '__tl.handlers'
import abc
class StopPropagation(Exception):
@ -31,101 +28,16 @@ class StopPropagation(Exception):
pass
def register(event=None):
"""
Decorator method to *register* event handlers. This is the client-less
`add_event_handler()
<telethon.client.updates.UpdateMethods.add_event_handler>` variant.
class EventBuilder(abc.ABC):
@classmethod
@abc.abstractmethod
def _build(cls, update, others, self_id, entities, client):
"""
Builds an event for the given update if possible, or returns None.
Note that this method only registers callbacks as handlers,
and does not attach them to any client. This is useful for
external modules that don't have access to the client, but
still want to define themselves as a handler. Example:
`others` are the rest of updates that came in the same container
as the current `update`.
>>> from telethon import events
>>> @events.register(events.NewMessage)
... 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)
`self_id` should be the current user's ID, since it is required
for some events which lack this information but still need it.
"""

View File

@ -3,7 +3,7 @@ import struct
import asyncio
import functools
from .common import EventBuilder, EventCommon, name_inner_event
from .base import EventBuilder
from .._misc import utils
from .. import _tl
from ..types import _custom
@ -23,8 +23,7 @@ def auto_answer(func):
return wrapped
@name_inner_event
class CallbackQuery(EventBuilder):
class CallbackQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
"""
Occurs whenever you sign in as a bot and a user
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
`chat_instance` which should be used for inline callbacks.
Args:
data (`bytes`, `str`, `callable`, optional):
If set, the inline button payload data must match this data.
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_')``.
Members:
query (:tl:`UpdateBotCallbackQuery`):
The original :tl:`UpdateBotCallbackQuery`.
pattern (`bytes`, `str`, `callable`, `Pattern`, optional):
If set, only buttons with payload matching this pattern will be handled.
You can specify a regex-like string which will be matched
against the payload data, a callable function that returns `True`
if a the payload data is acceptable, or a compiled regex pattern.
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``.
Example
.. code-block:: python
@ -71,39 +69,17 @@ class CallbackQuery(EventBuilder):
Button.inline('Nope', b'no')
])
"""
def __init__(
self, chats=None, *, blacklist_chats=False, func=None, data=None, pattern=None):
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
if data and pattern:
raise ValueError("Only pass either data or pattern not both.")
if isinstance(data, str):
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,
))
def __init__(self, query, peer, msg_id):
_custom.chatgetter.ChatGetter.__init__(self, peer)
_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
@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):
return cls.Event(update, update.peer, update.msg_id)
elif isinstance(update, _tl.UpdateInlineBotCallbackQuery):
@ -113,242 +89,191 @@ class CallbackQuery(EventBuilder):
peer = _tl.PeerChannel(-pid) if pid < 0 else _tl.PeerUser(pid)
return cls.Event(update, peer, mid)
def filter(self, event):
# We can't call super().filter(...) because it ignores chat_instance
if self._no_check:
return event
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
if self.chats is not None:
inside = event.query.chat_instance in self.chats
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):
@property
def id(self):
"""
Represents the event of a new callback query.
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``.
Returns the query ID. The user clicking the inline
button is the one who generated this random ID.
"""
def __init__(self, query, peer, msg_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
return self.query.query_id
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 message_id(self):
"""
Returns the message ID to which the clicked inline button belongs.
"""
return self._message_id
@property
def id(self):
"""
Returns the query ID. The user clicking the inline
button is the one who generated this random ID.
"""
return self.query.query_id
@property
def data(self):
"""
Returns the data payload from the original inline button.
"""
return self.query.data
@property
def message_id(self):
"""
Returns the message ID to which the clicked inline button belongs.
"""
return self._message_id
@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
@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
async def _refetch_sender(self):
self._sender = self._entities.get(self.sender_id)
if not self._sender:
return
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
self._input_sender = utils.get_input_peer(self._chat)
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
return self._message
async def answer(
self, message=None, cache_time=0, *, url=None, alert=False):
"""
Answers the callback query (and stops the loading circle).
async def _refetch_sender(self):
self._sender = self._entities.get(self.sender_id)
if not self._sender:
return
Args:
message (`str`, optional):
The toast message to show feedback to the user.
self._input_sender = utils.get_input_peer(self._chat)
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
cache_time (`int`, optional):
For how long this result should be cached on
the user's client. Defaults to 0 for no cache.
async def answer(
self, message=None, cache_time=0, *, url=None, alert=False):
"""
Answers the callback query (and stops the loading circle).
url (`str`, optional):
The URL to be opened in the user's client. Note that
the only valid URLs are those of games your bot has,
or alternatively a 't.me/your_bot?start=xyz' parameter.
Args:
message (`str`, optional):
The toast message to show feedback to the user.
alert (`bool`, optional):
Whether an alert (a pop-up dialog) should be used
instead of showing a toast. Defaults to `False`.
"""
if self._answered:
return
cache_time (`int`, optional):
For how long this result should be cached on
the user's client. Defaults to 0 for no cache.
res = await self._client(_tl.fn.messages.SetBotCallbackAnswer(
query_id=self.query.query_id,
cache_time=cache_time,
alert=alert,
message=message,
url=url,
))
self._answered = True
return res
url (`str`, optional):
The URL to be opened in the user's client. Note that
the only valid URLs are those of games your bot has,
or alternatively a 't.me/your_bot?start=xyz' parameter.
@property
def via_inline(self):
"""
Whether this callback was generated from an inline button sent
via an inline query or not. If the bot sent the message itself
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`.
alert (`bool`, optional):
Whether an alert (a pop-up dialog) should be used
instead of showing a toast. Defaults to `False`.
"""
if self._answered:
return
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)
res = await self._client(_tl.fn.messages.SetBotCallbackAnswer(
query_id=self.query.query_id,
cache_time=cache_time,
alert=alert,
message=message,
url=url,
))
self._answered = True
return res
@auto_answer
async def respond(self, *args, **kwargs):
"""
Responds to the message (not as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
``entity`` already set.
@property
def via_inline(self):
"""
Whether this callback was generated from an inline button sent
via an inline query or not. If the bot sent the message itself
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`.
"""
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
@auto_answer
async def respond(self, *args, **kwargs):
"""
Responds to the message (not as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
``entity`` already set.
@auto_answer
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 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`.
"""
kwargs['reply_to'] = self.query.msg_id
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
@auto_answer
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.
@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`.
This method will also `answer` the callback if necessary.
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
`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
)
This method will also `answer` the callback if necessary.
@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.
.. note::
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],
This method won't respect the previous message unlike
`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
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 .. import _tl
from ..types import _custom
@name_inner_event
class ChatAction(EventBuilder):
"""
Occurs on certain chat actions:
@ -20,6 +19,47 @@ class ChatAction(EventBuilder):
Note that "chat" refers to "small group, megagroup and broadcast
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
.. code-block:: python
@ -32,8 +72,64 @@ class ChatAction(EventBuilder):
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
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
# for new pins (we'd rather handle the new service message with pin,
# so that we can act on that message').
@ -114,332 +210,230 @@ class ChatAction(EventBuilder):
return cls.Event(msg,
new_score=action.score)
class Event(EventCommon):
async def respond(self, *args, **kwargs):
"""
Represents the event of a new chat action.
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.
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)
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
async def reply(self, *args, **kwargs):
"""
Replies to the chat action message (as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
both ``entity`` and ``reply_to`` already set.
# 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)
Has the same effect as `respond` if there is no message.
"""
if not self.action_message:
return await self.respond(*args, **kwargs)
self.new_pin = pin_ids is not None
self._pin_ids = pin_ids
self._pinned_messages = None
kwargs['reply_to'] = self.action_message.id
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
self.new_photo = new_photo is not None
self.photo = \
new_photo if isinstance(new_photo, _tl.Photo) else None
async def delete(self, *args, **kwargs):
"""
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
self._kicked_by = None
self.user_added = self.user_joined = self.user_left = \
self.user_kicked = self.unpin = False
Does nothing if no message action triggered this event.
"""
if not self.action_message:
return
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
return await self._client.delete_messages(
await self.get_input_chat(), [self.action_message],
*args, **kwargs
)
# 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
async def get_pinned_message(self):
"""
If ``new_pin`` is `True`, this returns the `Message
<telethon.tl.custom.message.Message>` object that was pinned.
"""
if self._pinned_messages is None:
await self.get_pinned_messages()
self.created = bool(created)
if self._pinned_messages:
return self._pinned_messages[0]
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 = []
async def get_pinned_messages(self):
"""
If ``new_pin`` is `True`, this returns a `list` of `Message
<telethon.tl.custom.message.Message>` objects that were pinned.
"""
if not self._pin_ids:
return self._pin_ids # either None or empty list
self._users = None
self._input_users = None
self.new_title = new_title
self.new_score = new_score
self.unpin = not pin
chat = await self.get_input_chat()
if chat:
self._pinned_messages = await self._client.get_messages(
self._input_chat, ids=self._pin_ids)
async def respond(self, *args, **kwargs):
"""
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)
return self._pinned_messages
async def reply(self, *args, **kwargs):
"""
Replies to the chat action message (as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
both ``entity`` and ``reply_to`` already set.
@property
def added_by(self):
"""
The user who added ``users``, if applicable (`None` otherwise).
"""
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.
"""
if not self.action_message:
return await self.respond(*args, **kwargs)
return self._added_by
kwargs['reply_to'] = self.action_message.id
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
async def get_added_by(self):
"""
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):
"""
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.
return self._added_by
Does nothing if no message action triggered this event.
"""
if not self.action_message:
return
@property
def kicked_by(self):
"""
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(
await self.get_input_chat(), [self.action_message],
*args, **kwargs
)
return self._kicked_by
async def get_pinned_message(self):
"""
If ``new_pin`` is `True`, this returns the `Message
<telethon.tl.custom.message.Message>` object that was pinned.
"""
if self._pinned_messages is None:
await self.get_pinned_messages()
async def get_kicked_by(self):
"""
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)
if self._pinned_messages:
return self._pinned_messages[0]
return self._kicked_by
async def get_pinned_messages(self):
"""
If ``new_pin`` is `True`, this returns a `list` of `Message
<telethon.tl.custom.message.Message>` objects that were pinned.
"""
if not self._pin_ids:
return self._pin_ids # either None or empty list
@property
def user(self):
"""
The first user that takes part in this action. For example, who joined.
chat = await self.get_input_chat()
if chat:
self._pinned_messages = await self._client.get_messages(
self._input_chat, ids=self._pin_ids)
Might be `None` if the information can't be retrieved or
there is no user taking part.
"""
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
def added_by(self):
"""
The user who added ``users``, if applicable (`None` otherwise).
"""
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
@property
def input_user(self):
"""
Input version of the ``self.user`` property.
"""
if self.input_users:
return self._input_users[0]
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):
"""
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)
@property
def user_id(self):
"""
Returns the marked signed ID of the first user, if any.
"""
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
def kicked_by(self):
"""
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
Might be empty if the information can't be retrieved or there
are no users taking part.
"""
if not self._user_ids:
return []
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):
"""
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._users
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
def user(self):
"""
The first user that takes part in this action. For example, who joined.
# 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))]
Might be `None` if the information can't be retrieved or
there is no user taking part.
"""
if self.users:
return self._users[0]
return self._users
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
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
@property
def input_user(self):
"""
Input version of the ``self.user`` property.
"""
if self.input_users:
return self._input_users[0]
return self._input_users or []
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_input_users(self):
"""
Returns `input_users` but will make an API call if necessary.
"""
if not self._user_ids:
return []
@property
def user_id(self):
"""
Returns the marked signed ID of the first user, if any.
"""
if self._user_ids:
return self._user_ids[0]
# 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))]
@property
def users(self):
"""
A list of users that take part in this action. For example, who joined.
return self._input_users or []
Might be empty if the information can't be retrieved or there
are no users taking part.
"""
if not self._user_ids:
return []
if self._users is None:
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[:]
@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
from .common import EventBuilder, EventCommon, name_inner_event
from .base import EventBuilder
from .._misc import utils
from .. import _tl
from ..types import _custom
@name_inner_event
class InlineQuery(EventBuilder):
class InlineQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
"""
Occurs whenever you sign in as a bot and a user
sends an inline query such as ``@bot query``.
Args:
users (`entity`, optional):
May be one or more entities (username/peer/etc.), preferably IDs.
By default, only inline queries from these users will be handled.
Members:
query (:tl:`UpdateBotInlineQuery`):
The original :tl:`UpdateBotInlineQuery`.
blacklist_users (`bool`, optional):
Whether to treat the users as a blacklist instead of
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``.
Make sure to access the `text` property of the query if
you want the text rather than the actual query object.
pattern (`str`, `callable`, `Pattern`, optional):
If set, only queries matching this pattern will be handled.
You can specify a regex-like string which will be matched
against the message, a callable function that returns `True`
if a message is acceptable, or a compiled regex pattern.
pattern_match (`obj`, optional):
The resulting object from calling the passed ``pattern``
function, which is ``re.compile(...).match`` by default.
Example
.. code-block:: python
@ -47,200 +40,163 @@ class InlineQuery(EventBuilder):
builder.article('lowercase', text=event.text.lower()),
])
"""
def __init__(
self, users=None, *, blacklist_users=False, func=None, pattern=None):
super().__init__(users, blacklist_chats=blacklist_users, func=func)
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')
def __init__(self, query):
_custom.chatgetter.ChatGetter.__init__(self, _tl.PeerUser(query.user_id))
_custom.sendergetter.SenderGetter.__init__(self, query.user_id)
self.query = query
self.pattern_match = None
self._answered = False
@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):
return cls.Event(update)
def filter(self, event):
if self.pattern:
match = self.pattern(event.text)
if not match:
return
event.pattern_match = match
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
return super().filter(event)
class Event(EventCommon, _custom.sendergetter.SenderGetter):
@property
def id(self):
"""
Represents the event of a new callback query.
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.
Returns the unique identifier for the query ID.
"""
def __init__(self, query):
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
return self.query.query_id
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 text(self):
"""
Returns the text the user used to make the inline query.
"""
return self.query.query
@property
def id(self):
"""
Returns the unique identifier for the query ID.
"""
return self.query.query_id
@property
def offset(self):
"""
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.offset
@property
def text(self):
"""
Returns the text the user used to make the inline query.
"""
return self.query.query
@property
def geo(self):
"""
If the user location is requested when using inline mode
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
def offset(self):
"""
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.offset
@property
def builder(self):
"""
Returns a new `InlineBuilder
<telethon.tl.custom.inlinebuilder.InlineBuilder>` instance.
@property
def geo(self):
"""
If the user location is requested when using inline mode
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
See the documentation for `builder` to know what kind of answers
can be given.
"""
return _custom.InlineBuilder(self._client)
@property
def builder(self):
"""
Returns a new `InlineBuilder
<telethon.tl.custom.inlinebuilder.InlineBuilder>` instance.
"""
return _custom.InlineBuilder(self._client)
async def answer(
self, results=None, cache_time=0, *,
gallery=False, next_offset=None, private=False,
switch_pm=None, switch_pm_param=''):
"""
Answers the inline query with the given results.
async def answer(
self, results=None, cache_time=0, *,
gallery=False, next_offset=None, private=False,
switch_pm=None, switch_pm_param=''):
"""
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:
Args:
results (`list`, optional):
A list of :tl:`InputBotInlineResult` to use.
You should use `builder` to create these:
.. code-block:: python
@bot.on(events.InlineQuery)
async def handler(event):
builder = event.builder
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])
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
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.
if results:
futures = [self._as_future(x) for x in results]
cache_time (`int`, optional):
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.
#
# 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 = []
next_offset (`str`, optional):
The offset the client will send when the user scrolls the
results and it repeats the request.
if switch_pm:
switch_pm = _tl.InlineBotSwitchPM(switch_pm, switch_pm_param)
private (`bool`, optional):
Whether the results should be cached by Telegram
(not private) or by the user's client (private).
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
)
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
@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
def _as_future(obj):
if inspect.isawaitable(obj):
return asyncio.ensure_future(obj)
@staticmethod
def _as_future(obj):
if inspect.isawaitable(obj):
return asyncio.ensure_future(obj)
f = asyncio.get_running_loop().create_future()
f.set_result(obj)
return f
f = asyncio.get_running_loop().create_future()
f.set_result(obj)
return f

View File

@ -1,9 +1,9 @@
from .common import EventBuilder, EventCommon, name_inner_event
from .base import EventBuilder
from .. import _tl
from ..types import _custom
@name_inner_event
class MessageDeleted(EventBuilder):
class MessageDeleted(EventBuilder, _custom.chatgetter.ChatGetter):
"""
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
@ -35,8 +35,13 @@ class MessageDeleted(EventBuilder):
for msg_id in event.deleted_ids:
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
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):
return cls.Event(
deleted_ids=update.messages,
@ -47,11 +52,3 @@ class MessageDeleted(EventBuilder):
deleted_ids=update.messages,
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 .newmessage import NewMessage
from .base import EventBuilder
from .. import _tl
@name_inner_event
class MessageEdited(NewMessage):
class MessageEdited(EventBuilder):
"""
Occurs whenever a message is edited. Just like `NewMessage
<telethon.events.newmessage.NewMessage>`, you should treat
@ -43,10 +41,7 @@ class MessageEdited(NewMessage):
print('Message', event.id, 'changed at', event.date)
"""
@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,
_tl.UpdateEditChannelMessage)):
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 .. import _tl
@name_inner_event
class MessageRead(EventBuilder):
"""
Occurs whenever one or more messages are read in a chat.
Args:
inbox (`bool`, optional):
If this argument is `True`, then when you read someone else's
messages the event will be fired. By default (`False`) only
when messages you sent are read by someone else will fire it.
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.
Example
.. code-block:: python
@ -29,13 +35,17 @@ class MessageRead(EventBuilder):
# Log when you read message in a chat (from your "inbox")
print('You have read messages until', event.max_id)
"""
def __init__(
self, chats=None, *, blacklist_chats=False, func=None, inbox=False):
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
self.inbox = inbox
def __init__(self, peer=None, max_id=None, out=False, contents=False,
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)
@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):
return cls.Event(update.peer, update.max_id, False)
elif isinstance(update, _tl.UpdateReadHistoryOutbox):
@ -54,90 +64,58 @@ class MessageRead(EventBuilder):
message_ids=update.messages,
contents=True)
def filter(self, event):
if self.inbox == event.outbox:
return
return super().filter(event)
class Event(EventCommon):
@property
def inbox(self):
"""
Represents the event of one or more messages being read.
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.
`True` if you have read someone else's messages.
"""
def __init__(self, peer=None, max_id=None, out=False, contents=False,
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)
return not self.outbox
@property
def inbox(self):
"""
`True` if you have read someone else's messages.
"""
return not self.outbox
@property
def message_ids(self):
"""
The IDs of the messages **which contents'** were read.
@property
def message_ids(self):
"""
The IDs of the messages **which contents'** were read.
Use :meth:`is_read` if you need to check whether a message
was read instead checking if it's in here.
"""
return self._message_ids
Use :meth:`is_read` if you need to check whether a message
was read instead checking if it's in here.
"""
return self._message_ids
async def get_messages(self):
"""
Returns the list of `Message <telethon.tl.custom.message.Message>`
**which contents'** were read.
async def get_messages(self):
"""
Returns the list of `Message <telethon.tl.custom.message.Message>`
**which contents'** were read.
Use :meth:`is_read` if you need to check whether a message
was read instead checking if it's in here.
"""
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]
Use :meth:`is_read` if you need to check whether a message
was read instead checking if it's in here.
"""
if self._messages is None:
chat = await self.get_input_chat()
if not chat:
self._messages = []
else:
return (message if isinstance(message, int)
else message.id) <= self.max_id
self._messages = await self._client.get_messages(
chat, ids=self._message_ids)
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)
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:
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
from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set
from .base import EventBuilder
from .._misc import utils
from .. import _tl
from ..types import _custom
@name_inner_event
class NewMessage(EventBuilder):
class NewMessageEvent(EventBuilder, Message):
"""
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:
incoming (`bool`, optional):
If set to `True`, only **incoming** messages will be handled.
Mutually exclusive with ``outgoing`` (can only set one of either).
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.
outgoing (`bool`, optional):
If set to `True`, only **outgoing** messages will be handled.
Mutually exclusive with ``incoming`` (can only set one of either).
See `Message <telethon.tl.custom.message.Message>` for
the rest of available members and methods.
from_users (`entity`, optional):
Unlike `chats`, this parameter filters the *senders* of the
message. That is, only messages *sent by these users* will be
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).
pattern_match (`obj`):
The resulting object from calling the passed ``pattern`` function.
Here's an example using a string (defaults to regex match):
forwards (`bool`, optional):
Whether forwarded messages should be handled or not. By default,
both forwarded and normal messages are included. If it's `True`
*only* forwards will be handled. If it's `False` only messages
that are *not* forwards will be handled.
pattern (`str`, `callable`, `Pattern`, optional):
If set, only messages matching this pattern will be handled.
You can specify a regex-like string which will be matched
against the message, a callable function that returns `True`
if a message is acceptable, or a compiled regex pattern.
>>> 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))
...
>>>
Example
.. code-block:: python
@ -57,45 +57,16 @@ class NewMessage(EventBuilder):
await asyncio.sleep(5)
await client.delete_messages(event.chat_id, [event.id, m.id])
"""
def __init__(self, chats=None, *, blacklist_chats=False, func=None,
incoming=None, outgoing=None,
from_users=None, forwards=None, pattern=None):
if incoming and outgoing:
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!")
def __init__(self, message):
self.__dict__['_init'] = False
super().__init__(chat_peer=message.peer_id,
msg_id=message.id, broadcast=bool(message.post))
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
self.incoming = incoming
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)
self.pattern_match = None
self.message = message
@classmethod
def build(cls, update, others, self_id, entities, client):
def _build(cls, update, others, self_id, entities, client):
if isinstance(update,
(_tl.UpdateNewMessage, _tl.UpdateNewChannelMessage)):
if not isinstance(update.message, _tl.Message):
@ -139,85 +110,3 @@ class NewMessage(EventBuilder):
return
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
@ -8,11 +8,6 @@ class Raw(EventBuilder):
:tl:`Update` object that Telegram sends. You normally shouldn't
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
.. code-block:: python
@ -23,31 +18,6 @@ class Raw(EventBuilder):
# Print all incoming updates
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
def build(cls, update, others=None, self_id=None, *todo, **todo2):
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
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 functools
from .common import EventBuilder, EventCommon, name_inner_event
from .base import EventBuilder
from .._misc import utils
from .. import _tl
from ..types import _custom
@ -32,11 +32,25 @@ def _requires_status(function):
return wrapped
@name_inner_event
class UserUpdate(EventBuilder):
class UserUpdateEvent(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
"""
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
.. code-block:: python
@ -48,262 +62,242 @@ class UserUpdate(EventBuilder):
if event.uploading:
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
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):
return cls.Event(_tl.PeerUser(update.user_id),
return UserUpdateEvent(_tl.PeerUser(update.user_id),
status=update.status)
elif isinstance(update, _tl.UpdateChannelUserTyping):
return cls.Event(update.from_id,
return UserUpdateEvent(update.from_id,
chat_peer=_tl.PeerChannel(update.channel_id),
typing=update.action)
elif isinstance(update, _tl.UpdateChatUserTyping):
return cls.Event(update.from_id,
return UserUpdateEvent(update.from_id,
chat_peer=_tl.PeerChat(update.chat_id),
typing=update.action)
elif isinstance(update, _tl.UpdateUserTyping):
return cls.Event(update.user_id,
return UserUpdateEvent(update.user_id,
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
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.
`True` if the action is typing a message.
"""
def __init__(self, peer, *, status=None, chat_peer=None, typing=None):
super().__init__(chat_peer or peer)
_custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer))
return isinstance(self.action, _tl.SendMessageTypingAction)
self.status = status
self.action = typing
@property
@_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):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
@property
@_requires_action
def recording(self):
"""
`True` if the action is recording something.
"""
return isinstance(self.action, (
_tl.SendMessageRecordAudioAction,
_tl.SendMessageRecordRoundAction,
_tl.SendMessageRecordVideoAction
))
@property
def user(self):
"""Alias for `sender <telethon.tl.custom.sendergetter.SenderGetter.sender>`."""
return self.sender
@property
@_requires_action
def playing(self):
"""
`True` if the action is playing a game.
"""
return isinstance(self.action, _tl.SendMessageGamePlayAction)
async def get_user(self):
"""Alias for `get_sender <telethon.tl.custom.sendergetter.SenderGetter.get_sender>`."""
return await self.get_sender()
@property
@_requires_action
def cancel(self):
"""
`True` if the action was cancelling other actions.
"""
return isinstance(self.action, _tl.SendMessageCancelAction)
@property
def input_user(self):
"""Alias for `input_sender <telethon.tl.custom.sendergetter.SenderGetter.input_sender>`."""
return self.input_sender
@property
@_requires_action
def geo(self):
"""
`True` if what's being uploaded is a geo.
"""
return isinstance(self.action, _tl.SendMessageGeoLocationAction)
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
@_requires_action
def audio(self):
"""
`True` if what's being recorded/uploaded is an audio.
"""
return isinstance(self.action, (
_tl.SendMessageRecordAudioAction,
_tl.SendMessageUploadAudioAction
))
@property
def user_id(self):
"""Alias for `sender_id <telethon.tl.custom.sendergetter.SenderGetter.sender_id>`."""
return self.sender_id
@property
@_requires_action
def round(self):
"""
`True` if what's being recorded/uploaded is a round video.
"""
return isinstance(self.action, (
_tl.SendMessageRecordRoundAction,
_tl.SendMessageUploadRoundAction
))
@property
@_requires_action
def typing(self):
"""
`True` if the action is typing a message.
"""
return isinstance(self.action, _tl.SendMessageTypingAction)
@property
@_requires_action
def video(self):
"""
`True` if what's being recorded/uploaded is an video.
"""
return isinstance(self.action, (
_tl.SendMessageRecordVideoAction,
_tl.SendMessageUploadVideoAction
))
@property
@_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
))
@property
@_requires_action
def contact(self):
"""
`True` if what's being uploaded (selected) is a contact.
"""
return isinstance(self.action, _tl.SendMessageChooseContactAction)
@property
@_requires_action
def recording(self):
"""
`True` if the action is recording something.
"""
return isinstance(self.action, (
_tl.SendMessageRecordAudioAction,
_tl.SendMessageRecordRoundAction,
_tl.SendMessageRecordVideoAction
))
@property
@_requires_action
def document(self):
"""
`True` if what's being uploaded is document.
"""
return isinstance(self.action, _tl.SendMessageUploadDocumentAction)
@property
@_requires_action
def playing(self):
"""
`True` if the action is playing a game.
"""
return isinstance(self.action, _tl.SendMessageGamePlayAction)
@property
@_requires_action
def sticker(self):
"""
`True` if what's being uploaded is a sticker.
"""
return isinstance(self.action, _tl.SendMessageChooseStickerAction)
@property
@_requires_action
def cancel(self):
"""
`True` if the action was cancelling other actions.
"""
return isinstance(self.action, _tl.SendMessageCancelAction)
@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 geo(self):
"""
`True` if what's being uploaded is a geo.
"""
return isinstance(self.action, _tl.SendMessageGeoLocationAction)
@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_action
def audio(self):
"""
`True` if what's being recorded/uploaded is an audio.
"""
return isinstance(self.action, (
_tl.SendMessageRecordAudioAction,
_tl.SendMessageUploadAudioAction
))
@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
@property
@_requires_action
def round(self):
"""
`True` if what's being recorded/uploaded is a round video.
"""
return isinstance(self.action, (
_tl.SendMessageRecordRoundAction,
_tl.SendMessageUploadRoundAction
))
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_action
def video(self):
"""
`True` if what's being recorded/uploaded is an video.
"""
return isinstance(self.action, (
_tl.SendMessageRecordVideoAction,
_tl.SendMessageUploadVideoAction
))
@property
@_requires_status
def online(self):
"""
`True` if the user is currently online,
"""
return self._last_seen_delta() <= datetime.timedelta(days=0)
@property
@_requires_action
def contact(self):
"""
`True` if what's being uploaded (selected) is a contact.
"""
return isinstance(self.action, _tl.SendMessageChooseContactAction)
@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_action
def document(self):
"""
`True` if what's being uploaded is document.
"""
return isinstance(self.action, _tl.SendMessageUploadDocumentAction)
@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_action
def sticker(self):
"""
`True` if what's being uploaded is a sticker.
"""
return isinstance(self.action, _tl.SendMessageChooseStickerAction)
@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)
@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)