mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-07-03 11:53:24 +03:00
Begin unification of event builders and events
This commit is contained in:
parent
f2ef0bfceb
commit
9726169a8c
|
@ -779,3 +779,12 @@ input_peer removed from get_me; input peers should remain mostly an impl detail
|
||||||
raw api types and fns are now immutable. this can enable optimizations in the future.
|
raw api types and fns are now immutable. this can enable optimizations in the future.
|
||||||
|
|
||||||
upload_file has been removed from the public methods. it's a low-level method users should not need to use.
|
upload_file has been removed from the public methods. it's a low-level method users should not need to use.
|
||||||
|
|
||||||
|
events have changed. rather than differentiating between "event builder" and "event instance", instead there is only the instance, and you register the class.
|
||||||
|
where you had
|
||||||
|
@client.on(events.NewMessage(chats=...))
|
||||||
|
it's now
|
||||||
|
@client.on(events.NewMessage, chats=...)
|
||||||
|
this also means filters are unified, although not all have an effect on all events. from_users renamed to senders. messageread inbox is gone in favor of outgoing/incoming.
|
||||||
|
events.register, unregister, is_handler and list are gone. now you can typehint instead.
|
||||||
|
def handler(event: events.NewMessage)
|
||||||
|
|
|
@ -10,7 +10,7 @@ from . import (
|
||||||
)
|
)
|
||||||
from .. import version, _tl
|
from .. import version, _tl
|
||||||
from ..types import _custom
|
from ..types import _custom
|
||||||
from .._events.common import EventBuilder, EventCommon
|
from .._events.base import EventBuilder
|
||||||
from .._misc import enums
|
from .._misc import enums
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import logging
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
from ..errors._rpcbase import RpcError
|
from ..errors._rpcbase import RpcError
|
||||||
from .._events.common import EventBuilder, EventCommon
|
from .._events.base import EventBuilder
|
||||||
from .._events.raw import Raw
|
from .._events.raw import Raw
|
||||||
from .._events.base import StopPropagation, _get_handlers
|
from .._events.base import StopPropagation, _get_handlers
|
||||||
from .._misc import utils
|
from .._misc import utils
|
||||||
|
|
|
@ -2,7 +2,7 @@ import asyncio
|
||||||
import time
|
import time
|
||||||
import weakref
|
import weakref
|
||||||
|
|
||||||
from .common import EventBuilder, EventCommon, name_inner_event
|
from .base import EventBuilder
|
||||||
from .._misc import utils
|
from .._misc import utils
|
||||||
from .. import _tl
|
from .. import _tl
|
||||||
from ..types import _custom
|
from ..types import _custom
|
||||||
|
@ -64,13 +64,16 @@ class AlbumHack:
|
||||||
await asyncio.sleep(diff)
|
await asyncio.sleep(diff)
|
||||||
|
|
||||||
|
|
||||||
@name_inner_event
|
class Album(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
|
||||||
class Album(EventBuilder):
|
|
||||||
"""
|
"""
|
||||||
Occurs whenever you receive an album. This event only exists
|
Occurs whenever you receive an album. This event only exists
|
||||||
to ease dealing with an unknown amount of messages that belong
|
to ease dealing with an unknown amount of messages that belong
|
||||||
to the same album.
|
to the same album.
|
||||||
|
|
||||||
|
Members:
|
||||||
|
messages (Sequence[`Message <telethon.tl._custom.message.Message>`]):
|
||||||
|
The list of messages belonging to the same album.
|
||||||
|
|
||||||
Example
|
Example
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -91,12 +94,20 @@ class Album(EventBuilder):
|
||||||
await event.messages[4].reply('Cool!')
|
await event.messages[4].reply('Cool!')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, messages):
|
||||||
self, chats=None, *, blacklist_chats=False, func=None):
|
message = messages[0]
|
||||||
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
|
if not message.out and isinstance(message.peer_id, _tl.PeerUser):
|
||||||
|
# Incoming message (e.g. from a bot) has peer_id=us, and
|
||||||
|
# from_id=bot (the actual "chat" from a user's perspective).
|
||||||
|
chat_peer = message.from_id
|
||||||
|
else:
|
||||||
|
chat_peer = message.peer_id
|
||||||
|
|
||||||
@classmethod
|
_custom.chatgetter.ChatGetter.__init__(self, chat_peer=chat_peer, broadcast=bool(message.post))
|
||||||
def build(cls, update, others=None, self_id=None, *todo, **todo2):
|
_custom.sendergetter.SenderGetter.__init__(self, message.sender_id)
|
||||||
|
self.messages = messages
|
||||||
|
|
||||||
|
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
|
||||||
if not others:
|
if not others:
|
||||||
return # We only care about albums which come inside the same Updates
|
return # We only care about albums which come inside the same Updates
|
||||||
|
|
||||||
|
@ -135,216 +146,188 @@ class Album(EventBuilder):
|
||||||
and u.message.grouped_id == group)
|
and u.message.grouped_id == group)
|
||||||
])
|
])
|
||||||
|
|
||||||
def filter(self, event):
|
def _set_client(self, client):
|
||||||
# Albums with less than two messages require a few hacks to work.
|
super()._set_client(client)
|
||||||
if len(event.messages) > 1:
|
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
|
||||||
return super().filter(event)
|
|
||||||
|
|
||||||
class Event(EventCommon, _custom.sendergetter.SenderGetter):
|
self.messages = [
|
||||||
"""
|
_custom.Message._new(client, m, self._entities, None)
|
||||||
Represents the event of a new album.
|
for m in self.messages
|
||||||
|
]
|
||||||
|
|
||||||
Members:
|
if len(self.messages) == 1:
|
||||||
messages (Sequence[`Message <telethon.tl._custom.message.Message>`]):
|
# This will require hacks to be a proper album event
|
||||||
The list of messages belonging to the same album.
|
hack = client._albums.get(self.grouped_id)
|
||||||
"""
|
if hack is None:
|
||||||
def __init__(self, messages):
|
client._albums[self.grouped_id] = AlbumHack(client, self)
|
||||||
message = messages[0]
|
|
||||||
if not message.out and isinstance(message.peer_id, _tl.PeerUser):
|
|
||||||
# Incoming message (e.g. from a bot) has peer_id=us, and
|
|
||||||
# from_id=bot (the actual "chat" from a user's perspective).
|
|
||||||
chat_peer = message.from_id
|
|
||||||
else:
|
else:
|
||||||
chat_peer = message.peer_id
|
hack.extend(self.messages)
|
||||||
|
|
||||||
super().__init__(chat_peer=chat_peer,
|
@property
|
||||||
msg_id=message.id, broadcast=bool(message.post))
|
def grouped_id(self):
|
||||||
|
"""
|
||||||
|
The shared ``grouped_id`` between all the messages.
|
||||||
|
"""
|
||||||
|
return self.messages[0].grouped_id
|
||||||
|
|
||||||
_custom.sendergetter.SenderGetter.__init__(self, message.sender_id)
|
@property
|
||||||
self.messages = messages
|
def text(self):
|
||||||
|
"""
|
||||||
|
The message text of the first photo with a caption,
|
||||||
|
formatted using the client's default parse mode.
|
||||||
|
"""
|
||||||
|
return next((m.text for m in self.messages if m.text), '')
|
||||||
|
|
||||||
def _set_client(self, client):
|
@property
|
||||||
super()._set_client(client)
|
def raw_text(self):
|
||||||
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
|
"""
|
||||||
|
The raw message text of the first photo
|
||||||
|
with a caption, ignoring any formatting.
|
||||||
|
"""
|
||||||
|
return next((m.raw_text for m in self.messages if m.raw_text), '')
|
||||||
|
|
||||||
self.messages = [
|
@property
|
||||||
_custom.Message._new(client, m, self._entities, None)
|
def is_reply(self):
|
||||||
for m in self.messages
|
"""
|
||||||
]
|
`True` if the album is a reply to some other message.
|
||||||
|
|
||||||
if len(self.messages) == 1:
|
Remember that you can access the ID of the message
|
||||||
# This will require hacks to be a proper album event
|
this one is replying to through `reply_to_msg_id`,
|
||||||
hack = client._albums.get(self.grouped_id)
|
and the `Message` object with `get_reply_message()`.
|
||||||
if hack is None:
|
"""
|
||||||
client._albums[self.grouped_id] = AlbumHack(client, self)
|
# Each individual message in an album all reply to the same message
|
||||||
else:
|
return self.messages[0].is_reply
|
||||||
hack.extend(self.messages)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def grouped_id(self):
|
def forward(self):
|
||||||
"""
|
"""
|
||||||
The shared ``grouped_id`` between all the messages.
|
The `Forward <telethon.tl._custom.forward.Forward>`
|
||||||
"""
|
information for the first message in the album if it was forwarded.
|
||||||
return self.messages[0].grouped_id
|
"""
|
||||||
|
# Each individual message in an album all reply to the same message
|
||||||
|
return self.messages[0].forward
|
||||||
|
|
||||||
@property
|
# endregion Public Properties
|
||||||
def text(self):
|
|
||||||
"""
|
|
||||||
The message text of the first photo with a caption,
|
|
||||||
formatted using the client's default parse mode.
|
|
||||||
"""
|
|
||||||
return next((m.text for m in self.messages if m.text), '')
|
|
||||||
|
|
||||||
@property
|
# region Public Methods
|
||||||
def raw_text(self):
|
|
||||||
"""
|
|
||||||
The raw message text of the first photo
|
|
||||||
with a caption, ignoring any formatting.
|
|
||||||
"""
|
|
||||||
return next((m.raw_text for m in self.messages if m.raw_text), '')
|
|
||||||
|
|
||||||
@property
|
async def get_reply_message(self):
|
||||||
def is_reply(self):
|
"""
|
||||||
"""
|
The `Message <telethon.tl._custom.message.Message>`
|
||||||
`True` if the album is a reply to some other message.
|
that this album is replying to, or `None`.
|
||||||
|
|
||||||
Remember that you can access the ID of the message
|
The result will be cached after its first use.
|
||||||
this one is replying to through `reply_to_msg_id`,
|
"""
|
||||||
and the `Message` object with `get_reply_message()`.
|
return await self.messages[0].get_reply_message()
|
||||||
"""
|
|
||||||
# Each individual message in an album all reply to the same message
|
|
||||||
return self.messages[0].is_reply
|
|
||||||
|
|
||||||
@property
|
async def respond(self, *args, **kwargs):
|
||||||
def forward(self):
|
"""
|
||||||
"""
|
Responds to the album (not as a reply). Shorthand for
|
||||||
The `Forward <telethon.tl._custom.forward.Forward>`
|
`telethon.client.messages.MessageMethods.send_message`
|
||||||
information for the first message in the album if it was forwarded.
|
with ``entity`` already set.
|
||||||
"""
|
"""
|
||||||
# Each individual message in an album all reply to the same message
|
return await self.messages[0].respond(*args, **kwargs)
|
||||||
return self.messages[0].forward
|
|
||||||
|
|
||||||
# endregion Public Properties
|
async def reply(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Replies to the first photo in the album (as a reply). Shorthand
|
||||||
|
for `telethon.client.messages.MessageMethods.send_message`
|
||||||
|
with both ``entity`` and ``reply_to`` already set.
|
||||||
|
"""
|
||||||
|
return await self.messages[0].reply(*args, **kwargs)
|
||||||
|
|
||||||
# region Public Methods
|
async def forward_to(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Forwards the entire album. Shorthand for
|
||||||
|
`telethon.client.messages.MessageMethods.forward_messages`
|
||||||
|
with both ``messages`` and ``from_peer`` already set.
|
||||||
|
"""
|
||||||
|
if self._client:
|
||||||
|
kwargs['messages'] = self.messages
|
||||||
|
kwargs['from_peer'] = await self.get_input_chat()
|
||||||
|
return await self._client.forward_messages(*args, **kwargs)
|
||||||
|
|
||||||
async def get_reply_message(self):
|
async def edit(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
The `Message <telethon.tl._custom.message.Message>`
|
Edits the first caption or the message, or the first messages'
|
||||||
that this album is replying to, or `None`.
|
caption if no caption is set, iff it's outgoing. Shorthand for
|
||||||
|
`telethon.client.messages.MessageMethods.edit_message`
|
||||||
|
with both ``entity`` and ``message`` already set.
|
||||||
|
|
||||||
The result will be cached after its first use.
|
Returns `None` if the message was incoming,
|
||||||
"""
|
or the edited `Message` otherwise.
|
||||||
return await self.messages[0].get_reply_message()
|
|
||||||
|
|
||||||
async def respond(self, *args, **kwargs):
|
.. note::
|
||||||
"""
|
|
||||||
Responds to the album (not as a reply). Shorthand for
|
|
||||||
`telethon.client.messages.MessageMethods.send_message`
|
|
||||||
with ``entity`` already set.
|
|
||||||
"""
|
|
||||||
return await self.messages[0].respond(*args, **kwargs)
|
|
||||||
|
|
||||||
async def reply(self, *args, **kwargs):
|
This is different from `client.edit_message
|
||||||
"""
|
<telethon.client.messages.MessageMethods.edit_message>`
|
||||||
Replies to the first photo in the album (as a reply). Shorthand
|
and **will respect** the previous state of the message.
|
||||||
for `telethon.client.messages.MessageMethods.send_message`
|
For example, if the message didn't have a link preview,
|
||||||
with both ``entity`` and ``reply_to`` already set.
|
the edit won't add one by default, and you should force
|
||||||
"""
|
it by setting it to `True` if you want it.
|
||||||
return await self.messages[0].reply(*args, **kwargs)
|
|
||||||
|
|
||||||
async def forward_to(self, *args, **kwargs):
|
This is generally the most desired and convenient behaviour,
|
||||||
"""
|
and will work for link previews and message buttons.
|
||||||
Forwards the entire album. Shorthand for
|
"""
|
||||||
`telethon.client.messages.MessageMethods.forward_messages`
|
for msg in self.messages:
|
||||||
with both ``messages`` and ``from_peer`` already set.
|
if msg.raw_text:
|
||||||
"""
|
return await msg.edit(*args, **kwargs)
|
||||||
if self._client:
|
|
||||||
kwargs['messages'] = self.messages
|
|
||||||
kwargs['from_peer'] = await self.get_input_chat()
|
|
||||||
return await self._client.forward_messages(*args, **kwargs)
|
|
||||||
|
|
||||||
async def edit(self, *args, **kwargs):
|
return await self.messages[0].edit(*args, **kwargs)
|
||||||
"""
|
|
||||||
Edits the first caption or the message, or the first messages'
|
|
||||||
caption if no caption is set, iff it's outgoing. Shorthand for
|
|
||||||
`telethon.client.messages.MessageMethods.edit_message`
|
|
||||||
with both ``entity`` and ``message`` already set.
|
|
||||||
|
|
||||||
Returns `None` if the message was incoming,
|
async def delete(self, *args, **kwargs):
|
||||||
or the edited `Message` otherwise.
|
"""
|
||||||
|
Deletes the entire album. You're responsible for checking whether
|
||||||
|
you have the permission to do so, or to except the error otherwise.
|
||||||
|
Shorthand for
|
||||||
|
`telethon.client.messages.MessageMethods.delete_messages` with
|
||||||
|
``entity`` and ``message_ids`` already set.
|
||||||
|
"""
|
||||||
|
if self._client:
|
||||||
|
return await self._client.delete_messages(
|
||||||
|
await self.get_input_chat(), self.messages,
|
||||||
|
*args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
.. note::
|
async def mark_read(self):
|
||||||
|
"""
|
||||||
|
Marks the entire album as read. Shorthand for
|
||||||
|
`client.mark_read()
|
||||||
|
<telethon.client.messages.MessageMethods.mark_read>`
|
||||||
|
with both ``entity`` and ``message`` already set.
|
||||||
|
"""
|
||||||
|
if self._client:
|
||||||
|
await self._client.mark_read(
|
||||||
|
await self.get_input_chat(), max_id=self.messages[-1].id)
|
||||||
|
|
||||||
This is different from `client.edit_message
|
async def pin(self, *, notify=False):
|
||||||
<telethon.client.messages.MessageMethods.edit_message>`
|
"""
|
||||||
and **will respect** the previous state of the message.
|
Pins the first photo in the album. Shorthand for
|
||||||
For example, if the message didn't have a link preview,
|
`telethon.client.messages.MessageMethods.pin_message`
|
||||||
the edit won't add one by default, and you should force
|
with both ``entity`` and ``message`` already set.
|
||||||
it by setting it to `True` if you want it.
|
"""
|
||||||
|
return await self.messages[0].pin(notify=notify)
|
||||||
|
|
||||||
This is generally the most desired and convenient behaviour,
|
def __len__(self):
|
||||||
and will work for link previews and message buttons.
|
"""
|
||||||
"""
|
Return the amount of messages in the album.
|
||||||
for msg in self.messages:
|
|
||||||
if msg.raw_text:
|
|
||||||
return await msg.edit(*args, **kwargs)
|
|
||||||
|
|
||||||
return await self.messages[0].edit(*args, **kwargs)
|
Equivalent to ``len(self.messages)``.
|
||||||
|
"""
|
||||||
|
return len(self.messages)
|
||||||
|
|
||||||
async def delete(self, *args, **kwargs):
|
def __iter__(self):
|
||||||
"""
|
"""
|
||||||
Deletes the entire album. You're responsible for checking whether
|
Iterate over the messages in the album.
|
||||||
you have the permission to do so, or to except the error otherwise.
|
|
||||||
Shorthand for
|
|
||||||
`telethon.client.messages.MessageMethods.delete_messages` with
|
|
||||||
``entity`` and ``message_ids`` already set.
|
|
||||||
"""
|
|
||||||
if self._client:
|
|
||||||
return await self._client.delete_messages(
|
|
||||||
await self.get_input_chat(), self.messages,
|
|
||||||
*args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
async def mark_read(self):
|
Equivalent to ``iter(self.messages)``.
|
||||||
"""
|
"""
|
||||||
Marks the entire album as read. Shorthand for
|
return iter(self.messages)
|
||||||
`client.mark_read()
|
|
||||||
<telethon.client.messages.MessageMethods.mark_read>`
|
|
||||||
with both ``entity`` and ``message`` already set.
|
|
||||||
"""
|
|
||||||
if self._client:
|
|
||||||
await self._client.mark_read(
|
|
||||||
await self.get_input_chat(), max_id=self.messages[-1].id)
|
|
||||||
|
|
||||||
async def pin(self, *, notify=False):
|
def __getitem__(self, n):
|
||||||
"""
|
"""
|
||||||
Pins the first photo in the album. Shorthand for
|
Access the n'th message in the album.
|
||||||
`telethon.client.messages.MessageMethods.pin_message`
|
|
||||||
with both ``entity`` and ``message`` already set.
|
|
||||||
"""
|
|
||||||
return await self.messages[0].pin(notify=notify)
|
|
||||||
|
|
||||||
def __len__(self):
|
Equivalent to ``event.messages[n]``.
|
||||||
"""
|
"""
|
||||||
Return the amount of messages in the album.
|
return self.messages[n]
|
||||||
|
|
||||||
Equivalent to ``len(self.messages)``.
|
|
||||||
"""
|
|
||||||
return len(self.messages)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""
|
|
||||||
Iterate over the messages in the album.
|
|
||||||
|
|
||||||
Equivalent to ``iter(self.messages)``.
|
|
||||||
"""
|
|
||||||
return iter(self.messages)
|
|
||||||
|
|
||||||
def __getitem__(self, n):
|
|
||||||
"""
|
|
||||||
Access the n'th message in the album.
|
|
||||||
|
|
||||||
Equivalent to ``event.messages[n]``.
|
|
||||||
"""
|
|
||||||
return self.messages[n]
|
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
from .raw import Raw
|
import abc
|
||||||
|
|
||||||
|
|
||||||
_HANDLERS_ATTRIBUTE = '__tl.handlers'
|
|
||||||
|
|
||||||
|
|
||||||
class StopPropagation(Exception):
|
class StopPropagation(Exception):
|
||||||
|
@ -31,101 +28,16 @@ class StopPropagation(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def register(event=None):
|
class EventBuilder(abc.ABC):
|
||||||
"""
|
@classmethod
|
||||||
Decorator method to *register* event handlers. This is the client-less
|
@abc.abstractmethod
|
||||||
`add_event_handler()
|
def _build(cls, update, others, self_id, entities, client):
|
||||||
<telethon.client.updates.UpdateMethods.add_event_handler>` variant.
|
"""
|
||||||
|
Builds an event for the given update if possible, or returns None.
|
||||||
|
|
||||||
Note that this method only registers callbacks as handlers,
|
`others` are the rest of updates that came in the same container
|
||||||
and does not attach them to any client. This is useful for
|
as the current `update`.
|
||||||
external modules that don't have access to the client, but
|
|
||||||
still want to define themselves as a handler. Example:
|
|
||||||
|
|
||||||
>>> from telethon import events
|
`self_id` should be the current user's ID, since it is required
|
||||||
>>> @events.register(events.NewMessage)
|
for some events which lack this information but still need it.
|
||||||
... async def handler(event):
|
"""
|
||||||
... ...
|
|
||||||
...
|
|
||||||
>>> # (somewhere else)
|
|
||||||
...
|
|
||||||
>>> from telethon import TelegramClient
|
|
||||||
>>> client = TelegramClient(...)
|
|
||||||
>>> client.add_event_handler(handler)
|
|
||||||
|
|
||||||
Remember that you can use this as a non-decorator
|
|
||||||
through ``register(event)(callback)``.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event (`_EventBuilder` | `type`):
|
|
||||||
The event builder class or instance to be used,
|
|
||||||
for instance ``events.NewMessage``.
|
|
||||||
"""
|
|
||||||
if isinstance(event, type):
|
|
||||||
event = event()
|
|
||||||
elif not event:
|
|
||||||
event = Raw()
|
|
||||||
|
|
||||||
def decorator(callback):
|
|
||||||
handlers = getattr(callback, _HANDLERS_ATTRIBUTE, [])
|
|
||||||
handlers.append(event)
|
|
||||||
setattr(callback, _HANDLERS_ATTRIBUTE, handlers)
|
|
||||||
return callback
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def unregister(callback, event=None):
|
|
||||||
"""
|
|
||||||
Inverse operation of `register` (though not a decorator). Client-less
|
|
||||||
`remove_event_handler
|
|
||||||
<telethon.client.updates.UpdateMethods.remove_event_handler>`
|
|
||||||
variant. **Note that this won't remove handlers from the client**,
|
|
||||||
because it simply can't, so you would generally use this before
|
|
||||||
adding the handlers to the client.
|
|
||||||
|
|
||||||
This method is here for symmetry. You will rarely need to
|
|
||||||
unregister events, since you can simply just not add them
|
|
||||||
to any client.
|
|
||||||
|
|
||||||
If no event is given, all events for this callback are removed.
|
|
||||||
Returns how many callbacks were removed.
|
|
||||||
"""
|
|
||||||
found = 0
|
|
||||||
if event and not isinstance(event, type):
|
|
||||||
event = type(event)
|
|
||||||
|
|
||||||
handlers = getattr(callback, _HANDLERS_ATTRIBUTE, [])
|
|
||||||
handlers.append((event, callback))
|
|
||||||
i = len(handlers)
|
|
||||||
while i:
|
|
||||||
i -= 1
|
|
||||||
ev = handlers[i]
|
|
||||||
if not event or isinstance(ev, event):
|
|
||||||
del handlers[i]
|
|
||||||
found += 1
|
|
||||||
|
|
||||||
return found
|
|
||||||
|
|
||||||
|
|
||||||
def is_handler(callback):
|
|
||||||
"""
|
|
||||||
Returns `True` if the given callback is an
|
|
||||||
event handler (i.e. you used `register` on it).
|
|
||||||
"""
|
|
||||||
return hasattr(callback, _HANDLERS_ATTRIBUTE)
|
|
||||||
|
|
||||||
|
|
||||||
def list(callback):
|
|
||||||
"""
|
|
||||||
Returns a list containing the registered event
|
|
||||||
builders inside the specified callback handler.
|
|
||||||
"""
|
|
||||||
return getattr(callback, _HANDLERS_ATTRIBUTE, [])[:]
|
|
||||||
|
|
||||||
|
|
||||||
def _get_handlers(callback):
|
|
||||||
"""
|
|
||||||
Like ``list`` but returns `None` if the callback was never registered.
|
|
||||||
"""
|
|
||||||
return getattr(callback, _HANDLERS_ATTRIBUTE, None)
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import struct
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
from .common import EventBuilder, EventCommon, name_inner_event
|
from .base import EventBuilder
|
||||||
from .._misc import utils
|
from .._misc import utils
|
||||||
from .. import _tl
|
from .. import _tl
|
||||||
from ..types import _custom
|
from ..types import _custom
|
||||||
|
@ -23,8 +23,7 @@ def auto_answer(func):
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
@name_inner_event
|
class CallbackQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
|
||||||
class CallbackQuery(EventBuilder):
|
|
||||||
"""
|
"""
|
||||||
Occurs whenever you sign in as a bot and a user
|
Occurs whenever you sign in as a bot and a user
|
||||||
clicks one of the inline buttons on your messages.
|
clicks one of the inline buttons on your messages.
|
||||||
|
@ -34,18 +33,17 @@ class CallbackQuery(EventBuilder):
|
||||||
message. The `chats` parameter also supports checking against the
|
message. The `chats` parameter also supports checking against the
|
||||||
`chat_instance` which should be used for inline callbacks.
|
`chat_instance` which should be used for inline callbacks.
|
||||||
|
|
||||||
Args:
|
Members:
|
||||||
data (`bytes`, `str`, `callable`, optional):
|
query (:tl:`UpdateBotCallbackQuery`):
|
||||||
If set, the inline button payload data must match this data.
|
The original :tl:`UpdateBotCallbackQuery`.
|
||||||
A UTF-8 string can also be given, a regex or a callable. For
|
|
||||||
instance, to check against ``'data_1'`` and ``'data_2'`` you
|
|
||||||
can use ``re.compile(b'data_')``.
|
|
||||||
|
|
||||||
pattern (`bytes`, `str`, `callable`, `Pattern`, optional):
|
data_match (`obj`, optional):
|
||||||
If set, only buttons with payload matching this pattern will be handled.
|
The object returned by the ``data=`` parameter
|
||||||
You can specify a regex-like string which will be matched
|
when creating the event builder, if any. Similar
|
||||||
against the payload data, a callable function that returns `True`
|
to ``pattern_match`` for the new message event.
|
||||||
if a the payload data is acceptable, or a compiled regex pattern.
|
|
||||||
|
pattern_match (`obj`, optional):
|
||||||
|
Alias for ``data_match``.
|
||||||
|
|
||||||
Example
|
Example
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
@ -71,39 +69,17 @@ class CallbackQuery(EventBuilder):
|
||||||
Button.inline('Nope', b'no')
|
Button.inline('Nope', b'no')
|
||||||
])
|
])
|
||||||
"""
|
"""
|
||||||
def __init__(
|
def __init__(self, query, peer, msg_id):
|
||||||
self, chats=None, *, blacklist_chats=False, func=None, data=None, pattern=None):
|
_custom.chatgetter.ChatGetter.__init__(self, peer)
|
||||||
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
|
_custom.sendergetter.SenderGetter.__init__(self, query.user_id)
|
||||||
|
self.query = query
|
||||||
if data and pattern:
|
self.data_match = None
|
||||||
raise ValueError("Only pass either data or pattern not both.")
|
self.pattern_match = None
|
||||||
|
self._message = None
|
||||||
if isinstance(data, str):
|
self._answered = False
|
||||||
data = data.encode('utf-8')
|
|
||||||
if isinstance(pattern, str):
|
|
||||||
pattern = pattern.encode('utf-8')
|
|
||||||
|
|
||||||
match = data if data else pattern
|
|
||||||
|
|
||||||
if isinstance(match, bytes):
|
|
||||||
self.match = data if data else re.compile(pattern).match
|
|
||||||
elif not match or callable(match):
|
|
||||||
self.match = match
|
|
||||||
elif hasattr(match, 'match') and callable(match.match):
|
|
||||||
if not isinstance(getattr(match, 'pattern', b''), bytes):
|
|
||||||
match = re.compile(match.pattern.encode('utf-8'),
|
|
||||||
match.flags & (~re.UNICODE))
|
|
||||||
|
|
||||||
self.match = match.match
|
|
||||||
else:
|
|
||||||
raise TypeError('Invalid data or pattern type given')
|
|
||||||
|
|
||||||
self._no_check = all(x is None for x in (
|
|
||||||
self.chats, self.func, self.match,
|
|
||||||
))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, update, others=None, self_id=None, *todo, **todo2):
|
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
|
||||||
if isinstance(update, _tl.UpdateBotCallbackQuery):
|
if isinstance(update, _tl.UpdateBotCallbackQuery):
|
||||||
return cls.Event(update, update.peer, update.msg_id)
|
return cls.Event(update, update.peer, update.msg_id)
|
||||||
elif isinstance(update, _tl.UpdateInlineBotCallbackQuery):
|
elif isinstance(update, _tl.UpdateInlineBotCallbackQuery):
|
||||||
|
@ -113,242 +89,191 @@ class CallbackQuery(EventBuilder):
|
||||||
peer = _tl.PeerChannel(-pid) if pid < 0 else _tl.PeerUser(pid)
|
peer = _tl.PeerChannel(-pid) if pid < 0 else _tl.PeerUser(pid)
|
||||||
return cls.Event(update, peer, mid)
|
return cls.Event(update, peer, mid)
|
||||||
|
|
||||||
def filter(self, event):
|
def _set_client(self, client):
|
||||||
# We can't call super().filter(...) because it ignores chat_instance
|
super()._set_client(client)
|
||||||
if self._no_check:
|
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
|
||||||
return event
|
|
||||||
|
|
||||||
if self.chats is not None:
|
@property
|
||||||
inside = event.query.chat_instance in self.chats
|
def id(self):
|
||||||
if event.chat_id:
|
|
||||||
inside |= event.chat_id in self.chats
|
|
||||||
|
|
||||||
if inside == self.blacklist_chats:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.match:
|
|
||||||
if callable(self.match):
|
|
||||||
event.data_match = event.pattern_match = self.match(event.query.data)
|
|
||||||
if not event.data_match:
|
|
||||||
return
|
|
||||||
elif event.query.data != self.match:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.func:
|
|
||||||
# Return the result of func directly as it may need to be awaited
|
|
||||||
return self.func(event)
|
|
||||||
return True
|
|
||||||
|
|
||||||
class Event(EventCommon, _custom.sendergetter.SenderGetter):
|
|
||||||
"""
|
"""
|
||||||
Represents the event of a new callback query.
|
Returns the query ID. The user clicking the inline
|
||||||
|
button is the one who generated this random ID.
|
||||||
Members:
|
|
||||||
query (:tl:`UpdateBotCallbackQuery`):
|
|
||||||
The original :tl:`UpdateBotCallbackQuery`.
|
|
||||||
|
|
||||||
data_match (`obj`, optional):
|
|
||||||
The object returned by the ``data=`` parameter
|
|
||||||
when creating the event builder, if any. Similar
|
|
||||||
to ``pattern_match`` for the new message event.
|
|
||||||
|
|
||||||
pattern_match (`obj`, optional):
|
|
||||||
Alias for ``data_match``.
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, query, peer, msg_id):
|
return self.query.query_id
|
||||||
super().__init__(peer, msg_id=msg_id)
|
|
||||||
_custom.sendergetter.SenderGetter.__init__(self, query.user_id)
|
|
||||||
self.query = query
|
|
||||||
self.data_match = None
|
|
||||||
self.pattern_match = None
|
|
||||||
self._message = None
|
|
||||||
self._answered = False
|
|
||||||
|
|
||||||
def _set_client(self, client):
|
@property
|
||||||
super()._set_client(client)
|
def message_id(self):
|
||||||
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
|
"""
|
||||||
|
Returns the message ID to which the clicked inline button belongs.
|
||||||
|
"""
|
||||||
|
return self._message_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def data(self):
|
||||||
"""
|
"""
|
||||||
Returns the query ID. The user clicking the inline
|
Returns the data payload from the original inline button.
|
||||||
button is the one who generated this random ID.
|
"""
|
||||||
"""
|
return self.query.data
|
||||||
return self.query.query_id
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def message_id(self):
|
def chat_instance(self):
|
||||||
"""
|
"""
|
||||||
Returns the message ID to which the clicked inline button belongs.
|
Unique identifier for the chat where the callback occurred.
|
||||||
"""
|
Useful for high scores in games.
|
||||||
return self._message_id
|
"""
|
||||||
|
return self.query.chat_instance
|
||||||
@property
|
|
||||||
def data(self):
|
|
||||||
"""
|
|
||||||
Returns the data payload from the original inline button.
|
|
||||||
"""
|
|
||||||
return self.query.data
|
|
||||||
|
|
||||||
@property
|
|
||||||
def chat_instance(self):
|
|
||||||
"""
|
|
||||||
Unique identifier for the chat where the callback occurred.
|
|
||||||
Useful for high scores in games.
|
|
||||||
"""
|
|
||||||
return self.query.chat_instance
|
|
||||||
|
|
||||||
async def get_message(self):
|
|
||||||
"""
|
|
||||||
Returns the message to which the clicked inline button belongs.
|
|
||||||
"""
|
|
||||||
if self._message is not None:
|
|
||||||
return self._message
|
|
||||||
|
|
||||||
try:
|
|
||||||
chat = await self.get_input_chat() if self.is_channel else None
|
|
||||||
self._message = await self._client.get_messages(
|
|
||||||
chat, ids=self._message_id)
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
|
|
||||||
|
async def get_message(self):
|
||||||
|
"""
|
||||||
|
Returns the message to which the clicked inline button belongs.
|
||||||
|
"""
|
||||||
|
if self._message is not None:
|
||||||
return self._message
|
return self._message
|
||||||
|
|
||||||
async def _refetch_sender(self):
|
try:
|
||||||
self._sender = self._entities.get(self.sender_id)
|
chat = await self.get_input_chat() if self.is_channel else None
|
||||||
if not self._sender:
|
self._message = await self._client.get_messages(
|
||||||
return
|
chat, ids=self._message_id)
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
|
||||||
self._input_sender = utils.get_input_peer(self._chat)
|
return self._message
|
||||||
if not getattr(self._input_sender, 'access_hash', True):
|
|
||||||
# getattr with True to handle the InputPeerSelf() case
|
|
||||||
m = await self.get_message()
|
|
||||||
if m:
|
|
||||||
self._sender = m._sender
|
|
||||||
self._input_sender = m._input_sender
|
|
||||||
|
|
||||||
async def answer(
|
async def _refetch_sender(self):
|
||||||
self, message=None, cache_time=0, *, url=None, alert=False):
|
self._sender = self._entities.get(self.sender_id)
|
||||||
"""
|
if not self._sender:
|
||||||
Answers the callback query (and stops the loading circle).
|
return
|
||||||
|
|
||||||
Args:
|
self._input_sender = utils.get_input_peer(self._chat)
|
||||||
message (`str`, optional):
|
if not getattr(self._input_sender, 'access_hash', True):
|
||||||
The toast message to show feedback to the user.
|
# getattr with True to handle the InputPeerSelf() case
|
||||||
|
m = await self.get_message()
|
||||||
|
if m:
|
||||||
|
self._sender = m._sender
|
||||||
|
self._input_sender = m._input_sender
|
||||||
|
|
||||||
cache_time (`int`, optional):
|
async def answer(
|
||||||
For how long this result should be cached on
|
self, message=None, cache_time=0, *, url=None, alert=False):
|
||||||
the user's client. Defaults to 0 for no cache.
|
"""
|
||||||
|
Answers the callback query (and stops the loading circle).
|
||||||
|
|
||||||
url (`str`, optional):
|
Args:
|
||||||
The URL to be opened in the user's client. Note that
|
message (`str`, optional):
|
||||||
the only valid URLs are those of games your bot has,
|
The toast message to show feedback to the user.
|
||||||
or alternatively a 't.me/your_bot?start=xyz' parameter.
|
|
||||||
|
|
||||||
alert (`bool`, optional):
|
cache_time (`int`, optional):
|
||||||
Whether an alert (a pop-up dialog) should be used
|
For how long this result should be cached on
|
||||||
instead of showing a toast. Defaults to `False`.
|
the user's client. Defaults to 0 for no cache.
|
||||||
"""
|
|
||||||
if self._answered:
|
|
||||||
return
|
|
||||||
|
|
||||||
res = await self._client(_tl.fn.messages.SetBotCallbackAnswer(
|
url (`str`, optional):
|
||||||
query_id=self.query.query_id,
|
The URL to be opened in the user's client. Note that
|
||||||
cache_time=cache_time,
|
the only valid URLs are those of games your bot has,
|
||||||
alert=alert,
|
or alternatively a 't.me/your_bot?start=xyz' parameter.
|
||||||
message=message,
|
|
||||||
url=url,
|
|
||||||
))
|
|
||||||
self._answered = True
|
|
||||||
return res
|
|
||||||
|
|
||||||
@property
|
alert (`bool`, optional):
|
||||||
def via_inline(self):
|
Whether an alert (a pop-up dialog) should be used
|
||||||
"""
|
instead of showing a toast. Defaults to `False`.
|
||||||
Whether this callback was generated from an inline button sent
|
"""
|
||||||
via an inline query or not. If the bot sent the message itself
|
if self._answered:
|
||||||
with buttons, and one of those is clicked, this will be `False`.
|
return
|
||||||
If a user sent the message coming from an inline query to the
|
|
||||||
bot, and one of those is clicked, this will be `True`.
|
|
||||||
|
|
||||||
If it's `True`, it's likely that the bot is **not** in the
|
res = await self._client(_tl.fn.messages.SetBotCallbackAnswer(
|
||||||
chat, so methods like `respond` or `delete` won't work (but
|
query_id=self.query.query_id,
|
||||||
`edit` will always work).
|
cache_time=cache_time,
|
||||||
"""
|
alert=alert,
|
||||||
return isinstance(self.query, _tl.UpdateInlineBotCallbackQuery)
|
message=message,
|
||||||
|
url=url,
|
||||||
|
))
|
||||||
|
self._answered = True
|
||||||
|
return res
|
||||||
|
|
||||||
@auto_answer
|
@property
|
||||||
async def respond(self, *args, **kwargs):
|
def via_inline(self):
|
||||||
"""
|
"""
|
||||||
Responds to the message (not as a reply). Shorthand for
|
Whether this callback was generated from an inline button sent
|
||||||
`telethon.client.messages.MessageMethods.send_message` with
|
via an inline query or not. If the bot sent the message itself
|
||||||
``entity`` already set.
|
with buttons, and one of those is clicked, this will be `False`.
|
||||||
|
If a user sent the message coming from an inline query to the
|
||||||
|
bot, and one of those is clicked, this will be `True`.
|
||||||
|
|
||||||
This method will also `answer` the callback if necessary.
|
If it's `True`, it's likely that the bot is **not** in the
|
||||||
|
chat, so methods like `respond` or `delete` won't work (but
|
||||||
|
`edit` will always work).
|
||||||
|
"""
|
||||||
|
return isinstance(self.query, _tl.UpdateInlineBotCallbackQuery)
|
||||||
|
|
||||||
This method will likely fail if `via_inline` is `True`.
|
@auto_answer
|
||||||
"""
|
async def respond(self, *args, **kwargs):
|
||||||
return await self._client.send_message(
|
"""
|
||||||
await self.get_input_chat(), *args, **kwargs)
|
Responds to the message (not as a reply). Shorthand for
|
||||||
|
`telethon.client.messages.MessageMethods.send_message` with
|
||||||
|
``entity`` already set.
|
||||||
|
|
||||||
@auto_answer
|
This method will also `answer` the callback if necessary.
|
||||||
async def reply(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Replies to the message (as a reply). Shorthand for
|
|
||||||
`telethon.client.messages.MessageMethods.send_message` with
|
|
||||||
both ``entity`` and ``reply_to`` already set.
|
|
||||||
|
|
||||||
This method will also `answer` the callback if necessary.
|
This method will likely fail if `via_inline` is `True`.
|
||||||
|
"""
|
||||||
|
return await self._client.send_message(
|
||||||
|
await self.get_input_chat(), *args, **kwargs)
|
||||||
|
|
||||||
This method will likely fail if `via_inline` is `True`.
|
@auto_answer
|
||||||
"""
|
async def reply(self, *args, **kwargs):
|
||||||
kwargs['reply_to'] = self.query.msg_id
|
"""
|
||||||
return await self._client.send_message(
|
Replies to the message (as a reply). Shorthand for
|
||||||
await self.get_input_chat(), *args, **kwargs)
|
`telethon.client.messages.MessageMethods.send_message` with
|
||||||
|
both ``entity`` and ``reply_to`` already set.
|
||||||
|
|
||||||
@auto_answer
|
This method will also `answer` the callback if necessary.
|
||||||
async def edit(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Edits the message. Shorthand for
|
|
||||||
`telethon.client.messages.MessageMethods.edit_message` with
|
|
||||||
the ``entity`` set to the correct :tl:`InputBotInlineMessageID`.
|
|
||||||
|
|
||||||
Returns `True` if the edit was successful.
|
This method will likely fail if `via_inline` is `True`.
|
||||||
|
"""
|
||||||
|
kwargs['reply_to'] = self.query.msg_id
|
||||||
|
return await self._client.send_message(
|
||||||
|
await self.get_input_chat(), *args, **kwargs)
|
||||||
|
|
||||||
This method will also `answer` the callback if necessary.
|
@auto_answer
|
||||||
|
async def edit(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Edits the message. Shorthand for
|
||||||
|
`telethon.client.messages.MessageMethods.edit_message` with
|
||||||
|
the ``entity`` set to the correct :tl:`InputBotInlineMessageID`.
|
||||||
|
|
||||||
.. note::
|
Returns `True` if the edit was successful.
|
||||||
|
|
||||||
This method won't respect the previous message unlike
|
This method will also `answer` the callback if necessary.
|
||||||
`Message.edit <telethon.tl._custom.message.Message.edit>`,
|
|
||||||
since the message object is normally not present.
|
|
||||||
"""
|
|
||||||
if isinstance(self.query.msg_id, _tl.InputBotInlineMessageID):
|
|
||||||
return await self._client.edit_message(
|
|
||||||
None, self.query.msg_id, *args, **kwargs
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await self._client.edit_message(
|
|
||||||
await self.get_input_chat(), self.query.msg_id,
|
|
||||||
*args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
@auto_answer
|
.. note::
|
||||||
async def delete(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Deletes the message. Shorthand for
|
|
||||||
`telethon.client.messages.MessageMethods.delete_messages` with
|
|
||||||
``entity`` and ``message_ids`` already set.
|
|
||||||
|
|
||||||
If you need to delete more than one message at once, don't use
|
This method won't respect the previous message unlike
|
||||||
this `delete` method. Use a
|
`Message.edit <telethon.tl._custom.message.Message.edit>`,
|
||||||
`telethon.client.telegramclient.TelegramClient` instance directly.
|
since the message object is normally not present.
|
||||||
|
"""
|
||||||
This method will also `answer` the callback if necessary.
|
if isinstance(self.query.msg_id, _tl.InputBotInlineMessageID):
|
||||||
|
return await self._client.edit_message(
|
||||||
This method will likely fail if `via_inline` is `True`.
|
None, self.query.msg_id, *args, **kwargs
|
||||||
"""
|
)
|
||||||
return await self._client.delete_messages(
|
else:
|
||||||
await self.get_input_chat(), [self.query.msg_id],
|
return await self._client.edit_message(
|
||||||
|
await self.get_input_chat(), self.query.msg_id,
|
||||||
*args, **kwargs
|
*args, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@auto_answer
|
||||||
|
async def delete(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Deletes the message. Shorthand for
|
||||||
|
`telethon.client.messages.MessageMethods.delete_messages` with
|
||||||
|
``entity`` and ``message_ids`` already set.
|
||||||
|
|
||||||
|
If you need to delete more than one message at once, don't use
|
||||||
|
this `delete` method. Use a
|
||||||
|
`telethon.client.telegramclient.TelegramClient` instance directly.
|
||||||
|
|
||||||
|
This method will also `answer` the callback if necessary.
|
||||||
|
|
||||||
|
This method will likely fail if `via_inline` is `True`.
|
||||||
|
"""
|
||||||
|
return await self._client.delete_messages(
|
||||||
|
await self.get_input_chat(), [self.query.msg_id],
|
||||||
|
*args, **kwargs
|
||||||
|
)
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
from .common import EventBuilder, EventCommon, name_inner_event
|
from .base import EventBuilder
|
||||||
from .._misc import utils
|
from .._misc import utils
|
||||||
from .. import _tl
|
from .. import _tl
|
||||||
from ..types import _custom
|
from ..types import _custom
|
||||||
|
|
||||||
|
|
||||||
@name_inner_event
|
|
||||||
class ChatAction(EventBuilder):
|
class ChatAction(EventBuilder):
|
||||||
"""
|
"""
|
||||||
Occurs on certain chat actions:
|
Occurs on certain chat actions:
|
||||||
|
@ -20,6 +19,47 @@ class ChatAction(EventBuilder):
|
||||||
Note that "chat" refers to "small group, megagroup and broadcast
|
Note that "chat" refers to "small group, megagroup and broadcast
|
||||||
channel", whereas "group" refers to "small group and megagroup" only.
|
channel", whereas "group" refers to "small group and megagroup" only.
|
||||||
|
|
||||||
|
Members:
|
||||||
|
action_message (`MessageAction <https://tl.telethon.dev/types/message_action.html>`_):
|
||||||
|
The message invoked by this Chat Action.
|
||||||
|
|
||||||
|
new_pin (`bool`):
|
||||||
|
`True` if there is a new pin.
|
||||||
|
|
||||||
|
new_photo (`bool`):
|
||||||
|
`True` if there's a new chat photo (or it was removed).
|
||||||
|
|
||||||
|
photo (:tl:`Photo`, optional):
|
||||||
|
The new photo (or `None` if it was removed).
|
||||||
|
|
||||||
|
user_added (`bool`):
|
||||||
|
`True` if the user was added by some other.
|
||||||
|
|
||||||
|
user_joined (`bool`):
|
||||||
|
`True` if the user joined on their own.
|
||||||
|
|
||||||
|
user_left (`bool`):
|
||||||
|
`True` if the user left on their own.
|
||||||
|
|
||||||
|
user_kicked (`bool`):
|
||||||
|
`True` if the user was kicked by some other.
|
||||||
|
|
||||||
|
user_approved (`bool`):
|
||||||
|
`True` if the user's join request was approved.
|
||||||
|
along with `user_joined` will be also True.
|
||||||
|
|
||||||
|
created (`bool`, optional):
|
||||||
|
`True` if this chat was just created.
|
||||||
|
|
||||||
|
new_title (`str`, optional):
|
||||||
|
The new title string for the chat, if applicable.
|
||||||
|
|
||||||
|
new_score (`str`, optional):
|
||||||
|
The new score string for the game, if applicable.
|
||||||
|
|
||||||
|
unpin (`bool`):
|
||||||
|
`True` if the existing pin gets unpinned.
|
||||||
|
|
||||||
Example
|
Example
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -32,8 +72,64 @@ class ChatAction(EventBuilder):
|
||||||
await event.reply('Welcome to the group!')
|
await event.reply('Welcome to the group!')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, where, new_photo=None,
|
||||||
|
added_by=None, kicked_by=None, created=None, from_approval=None,
|
||||||
|
users=None, new_title=None, pin_ids=None, pin=None, new_score=None):
|
||||||
|
if isinstance(where, _tl.MessageService):
|
||||||
|
self.action_message = where
|
||||||
|
where = where.peer_id
|
||||||
|
else:
|
||||||
|
self.action_message = None
|
||||||
|
|
||||||
|
# TODO needs some testing (can there be more than one id, and do they follow pin order?)
|
||||||
|
# same in get_pinned_message
|
||||||
|
super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None)
|
||||||
|
|
||||||
|
self.new_pin = pin_ids is not None
|
||||||
|
self._pin_ids = pin_ids
|
||||||
|
self._pinned_messages = None
|
||||||
|
|
||||||
|
self.new_photo = new_photo is not None
|
||||||
|
self.photo = \
|
||||||
|
new_photo if isinstance(new_photo, _tl.Photo) else None
|
||||||
|
|
||||||
|
self._added_by = None
|
||||||
|
self._kicked_by = None
|
||||||
|
self.user_added = self.user_joined = self.user_left = \
|
||||||
|
self.user_kicked = self.unpin = False
|
||||||
|
|
||||||
|
if added_by is True or from_approval is True:
|
||||||
|
self.user_joined = True
|
||||||
|
elif added_by:
|
||||||
|
self.user_added = True
|
||||||
|
self._added_by = added_by
|
||||||
|
self.user_approved = from_approval
|
||||||
|
|
||||||
|
# If `from_id` was not present (it's `True`) or the affected
|
||||||
|
# user was "kicked by itself", then it left. Else it was kicked.
|
||||||
|
if kicked_by is True or (users is not None and kicked_by == users):
|
||||||
|
self.user_left = True
|
||||||
|
elif kicked_by:
|
||||||
|
self.user_kicked = True
|
||||||
|
self._kicked_by = kicked_by
|
||||||
|
|
||||||
|
self.created = bool(created)
|
||||||
|
|
||||||
|
if isinstance(users, list):
|
||||||
|
self._user_ids = [utils.get_peer_id(u) for u in users]
|
||||||
|
elif users:
|
||||||
|
self._user_ids = [utils.get_peer_id(users)]
|
||||||
|
else:
|
||||||
|
self._user_ids = []
|
||||||
|
|
||||||
|
self._users = None
|
||||||
|
self._input_users = None
|
||||||
|
self.new_title = new_title
|
||||||
|
self.new_score = new_score
|
||||||
|
self.unpin = not pin
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, update, others=None, self_id=None, *todo, **todo2):
|
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
|
||||||
# Rely on specific pin updates for unpins, but otherwise ignore them
|
# Rely on specific pin updates for unpins, but otherwise ignore them
|
||||||
# for new pins (we'd rather handle the new service message with pin,
|
# for new pins (we'd rather handle the new service message with pin,
|
||||||
# so that we can act on that message').
|
# so that we can act on that message').
|
||||||
|
@ -114,332 +210,230 @@ class ChatAction(EventBuilder):
|
||||||
return cls.Event(msg,
|
return cls.Event(msg,
|
||||||
new_score=action.score)
|
new_score=action.score)
|
||||||
|
|
||||||
class Event(EventCommon):
|
async def respond(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Represents the event of a new chat action.
|
Responds to the chat action message (not as a reply). Shorthand for
|
||||||
|
`telethon.client.messages.MessageMethods.send_message` with
|
||||||
Members:
|
``entity`` already set.
|
||||||
action_message (`MessageAction <https://tl.telethon.dev/types/message_action.html>`_):
|
|
||||||
The message invoked by this Chat Action.
|
|
||||||
|
|
||||||
new_pin (`bool`):
|
|
||||||
`True` if there is a new pin.
|
|
||||||
|
|
||||||
new_photo (`bool`):
|
|
||||||
`True` if there's a new chat photo (or it was removed).
|
|
||||||
|
|
||||||
photo (:tl:`Photo`, optional):
|
|
||||||
The new photo (or `None` if it was removed).
|
|
||||||
|
|
||||||
user_added (`bool`):
|
|
||||||
`True` if the user was added by some other.
|
|
||||||
|
|
||||||
user_joined (`bool`):
|
|
||||||
`True` if the user joined on their own.
|
|
||||||
|
|
||||||
user_left (`bool`):
|
|
||||||
`True` if the user left on their own.
|
|
||||||
|
|
||||||
user_kicked (`bool`):
|
|
||||||
`True` if the user was kicked by some other.
|
|
||||||
|
|
||||||
user_approved (`bool`):
|
|
||||||
`True` if the user's join request was approved.
|
|
||||||
along with `user_joined` will be also True.
|
|
||||||
|
|
||||||
created (`bool`, optional):
|
|
||||||
`True` if this chat was just created.
|
|
||||||
|
|
||||||
new_title (`str`, optional):
|
|
||||||
The new title string for the chat, if applicable.
|
|
||||||
|
|
||||||
new_score (`str`, optional):
|
|
||||||
The new score string for the game, if applicable.
|
|
||||||
|
|
||||||
unpin (`bool`):
|
|
||||||
`True` if the existing pin gets unpinned.
|
|
||||||
"""
|
"""
|
||||||
|
return await self._client.send_message(
|
||||||
|
await self.get_input_chat(), *args, **kwargs)
|
||||||
|
|
||||||
def __init__(self, where, new_photo=None,
|
async def reply(self, *args, **kwargs):
|
||||||
added_by=None, kicked_by=None, created=None, from_approval=None,
|
"""
|
||||||
users=None, new_title=None, pin_ids=None, pin=None, new_score=None):
|
Replies to the chat action message (as a reply). Shorthand for
|
||||||
if isinstance(where, _tl.MessageService):
|
`telethon.client.messages.MessageMethods.send_message` with
|
||||||
self.action_message = where
|
both ``entity`` and ``reply_to`` already set.
|
||||||
where = where.peer_id
|
|
||||||
else:
|
|
||||||
self.action_message = None
|
|
||||||
|
|
||||||
# TODO needs some testing (can there be more than one id, and do they follow pin order?)
|
Has the same effect as `respond` if there is no message.
|
||||||
# same in get_pinned_message
|
"""
|
||||||
super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None)
|
if not self.action_message:
|
||||||
|
return await self.respond(*args, **kwargs)
|
||||||
|
|
||||||
self.new_pin = pin_ids is not None
|
kwargs['reply_to'] = self.action_message.id
|
||||||
self._pin_ids = pin_ids
|
return await self._client.send_message(
|
||||||
self._pinned_messages = None
|
await self.get_input_chat(), *args, **kwargs)
|
||||||
|
|
||||||
self.new_photo = new_photo is not None
|
async def delete(self, *args, **kwargs):
|
||||||
self.photo = \
|
"""
|
||||||
new_photo if isinstance(new_photo, _tl.Photo) else None
|
Deletes the chat action message. You're responsible for checking
|
||||||
|
whether you have the permission to do so, or to except the error
|
||||||
|
otherwise. Shorthand for
|
||||||
|
`telethon.client.messages.MessageMethods.delete_messages` with
|
||||||
|
``entity`` and ``message_ids`` already set.
|
||||||
|
|
||||||
self._added_by = None
|
Does nothing if no message action triggered this event.
|
||||||
self._kicked_by = None
|
"""
|
||||||
self.user_added = self.user_joined = self.user_left = \
|
if not self.action_message:
|
||||||
self.user_kicked = self.unpin = False
|
return
|
||||||
|
|
||||||
if added_by is True or from_approval is True:
|
return await self._client.delete_messages(
|
||||||
self.user_joined = True
|
await self.get_input_chat(), [self.action_message],
|
||||||
elif added_by:
|
*args, **kwargs
|
||||||
self.user_added = True
|
)
|
||||||
self._added_by = added_by
|
|
||||||
self.user_approved = from_approval
|
|
||||||
|
|
||||||
# If `from_id` was not present (it's `True`) or the affected
|
async def get_pinned_message(self):
|
||||||
# user was "kicked by itself", then it left. Else it was kicked.
|
"""
|
||||||
if kicked_by is True or (users is not None and kicked_by == users):
|
If ``new_pin`` is `True`, this returns the `Message
|
||||||
self.user_left = True
|
<telethon.tl.custom.message.Message>` object that was pinned.
|
||||||
elif kicked_by:
|
"""
|
||||||
self.user_kicked = True
|
if self._pinned_messages is None:
|
||||||
self._kicked_by = kicked_by
|
await self.get_pinned_messages()
|
||||||
|
|
||||||
self.created = bool(created)
|
if self._pinned_messages:
|
||||||
|
return self._pinned_messages[0]
|
||||||
|
|
||||||
if isinstance(users, list):
|
async def get_pinned_messages(self):
|
||||||
self._user_ids = [utils.get_peer_id(u) for u in users]
|
"""
|
||||||
elif users:
|
If ``new_pin`` is `True`, this returns a `list` of `Message
|
||||||
self._user_ids = [utils.get_peer_id(users)]
|
<telethon.tl.custom.message.Message>` objects that were pinned.
|
||||||
else:
|
"""
|
||||||
self._user_ids = []
|
if not self._pin_ids:
|
||||||
|
return self._pin_ids # either None or empty list
|
||||||
|
|
||||||
self._users = None
|
chat = await self.get_input_chat()
|
||||||
self._input_users = None
|
if chat:
|
||||||
self.new_title = new_title
|
self._pinned_messages = await self._client.get_messages(
|
||||||
self.new_score = new_score
|
self._input_chat, ids=self._pin_ids)
|
||||||
self.unpin = not pin
|
|
||||||
|
|
||||||
async def respond(self, *args, **kwargs):
|
return self._pinned_messages
|
||||||
"""
|
|
||||||
Responds to the chat action message (not as a reply). Shorthand for
|
|
||||||
`telethon.client.messages.MessageMethods.send_message` with
|
|
||||||
``entity`` already set.
|
|
||||||
"""
|
|
||||||
return await self._client.send_message(
|
|
||||||
await self.get_input_chat(), *args, **kwargs)
|
|
||||||
|
|
||||||
async def reply(self, *args, **kwargs):
|
@property
|
||||||
"""
|
def added_by(self):
|
||||||
Replies to the chat action message (as a reply). Shorthand for
|
"""
|
||||||
`telethon.client.messages.MessageMethods.send_message` with
|
The user who added ``users``, if applicable (`None` otherwise).
|
||||||
both ``entity`` and ``reply_to`` already set.
|
"""
|
||||||
|
if self._added_by and not isinstance(self._added_by, _tl.User):
|
||||||
|
aby = self._entities.get(utils.get_peer_id(self._added_by))
|
||||||
|
if aby:
|
||||||
|
self._added_by = aby
|
||||||
|
|
||||||
Has the same effect as `respond` if there is no message.
|
return self._added_by
|
||||||
"""
|
|
||||||
if not self.action_message:
|
|
||||||
return await self.respond(*args, **kwargs)
|
|
||||||
|
|
||||||
kwargs['reply_to'] = self.action_message.id
|
async def get_added_by(self):
|
||||||
return await self._client.send_message(
|
"""
|
||||||
await self.get_input_chat(), *args, **kwargs)
|
Returns `added_by` but will make an API call if necessary.
|
||||||
|
"""
|
||||||
|
if not self.added_by and self._added_by:
|
||||||
|
self._added_by = await self._client.get_entity(self._added_by)
|
||||||
|
|
||||||
async def delete(self, *args, **kwargs):
|
return self._added_by
|
||||||
"""
|
|
||||||
Deletes the chat action message. You're responsible for checking
|
|
||||||
whether you have the permission to do so, or to except the error
|
|
||||||
otherwise. Shorthand for
|
|
||||||
`telethon.client.messages.MessageMethods.delete_messages` with
|
|
||||||
``entity`` and ``message_ids`` already set.
|
|
||||||
|
|
||||||
Does nothing if no message action triggered this event.
|
@property
|
||||||
"""
|
def kicked_by(self):
|
||||||
if not self.action_message:
|
"""
|
||||||
return
|
The user who kicked ``users``, if applicable (`None` otherwise).
|
||||||
|
"""
|
||||||
|
if self._kicked_by and not isinstance(self._kicked_by, _tl.User):
|
||||||
|
kby = self._entities.get(utils.get_peer_id(self._kicked_by))
|
||||||
|
if kby:
|
||||||
|
self._kicked_by = kby
|
||||||
|
|
||||||
return await self._client.delete_messages(
|
return self._kicked_by
|
||||||
await self.get_input_chat(), [self.action_message],
|
|
||||||
*args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_pinned_message(self):
|
async def get_kicked_by(self):
|
||||||
"""
|
"""
|
||||||
If ``new_pin`` is `True`, this returns the `Message
|
Returns `kicked_by` but will make an API call if necessary.
|
||||||
<telethon.tl.custom.message.Message>` object that was pinned.
|
"""
|
||||||
"""
|
if not self.kicked_by and self._kicked_by:
|
||||||
if self._pinned_messages is None:
|
self._kicked_by = await self._client.get_entity(self._kicked_by)
|
||||||
await self.get_pinned_messages()
|
|
||||||
|
|
||||||
if self._pinned_messages:
|
return self._kicked_by
|
||||||
return self._pinned_messages[0]
|
|
||||||
|
|
||||||
async def get_pinned_messages(self):
|
@property
|
||||||
"""
|
def user(self):
|
||||||
If ``new_pin`` is `True`, this returns a `list` of `Message
|
"""
|
||||||
<telethon.tl.custom.message.Message>` objects that were pinned.
|
The first user that takes part in this action. For example, who joined.
|
||||||
"""
|
|
||||||
if not self._pin_ids:
|
|
||||||
return self._pin_ids # either None or empty list
|
|
||||||
|
|
||||||
chat = await self.get_input_chat()
|
Might be `None` if the information can't be retrieved or
|
||||||
if chat:
|
there is no user taking part.
|
||||||
self._pinned_messages = await self._client.get_messages(
|
"""
|
||||||
self._input_chat, ids=self._pin_ids)
|
if self.users:
|
||||||
|
return self._users[0]
|
||||||
|
|
||||||
return self._pinned_messages
|
async def get_user(self):
|
||||||
|
"""
|
||||||
|
Returns `user` but will make an API call if necessary.
|
||||||
|
"""
|
||||||
|
if self.users or await self.get_users():
|
||||||
|
return self._users[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def added_by(self):
|
def input_user(self):
|
||||||
"""
|
"""
|
||||||
The user who added ``users``, if applicable (`None` otherwise).
|
Input version of the ``self.user`` property.
|
||||||
"""
|
"""
|
||||||
if self._added_by and not isinstance(self._added_by, _tl.User):
|
if self.input_users:
|
||||||
aby = self._entities.get(utils.get_peer_id(self._added_by))
|
return self._input_users[0]
|
||||||
if aby:
|
|
||||||
self._added_by = aby
|
|
||||||
|
|
||||||
return self._added_by
|
async def get_input_user(self):
|
||||||
|
"""
|
||||||
|
Returns `input_user` but will make an API call if necessary.
|
||||||
|
"""
|
||||||
|
if self.input_users or await self.get_input_users():
|
||||||
|
return self._input_users[0]
|
||||||
|
|
||||||
async def get_added_by(self):
|
@property
|
||||||
"""
|
def user_id(self):
|
||||||
Returns `added_by` but will make an API call if necessary.
|
"""
|
||||||
"""
|
Returns the marked signed ID of the first user, if any.
|
||||||
if not self.added_by and self._added_by:
|
"""
|
||||||
self._added_by = await self._client.get_entity(self._added_by)
|
if self._user_ids:
|
||||||
|
return self._user_ids[0]
|
||||||
|
|
||||||
return self._added_by
|
@property
|
||||||
|
def users(self):
|
||||||
|
"""
|
||||||
|
A list of users that take part in this action. For example, who joined.
|
||||||
|
|
||||||
@property
|
Might be empty if the information can't be retrieved or there
|
||||||
def kicked_by(self):
|
are no users taking part.
|
||||||
"""
|
"""
|
||||||
The user who kicked ``users``, if applicable (`None` otherwise).
|
if not self._user_ids:
|
||||||
"""
|
return []
|
||||||
if self._kicked_by and not isinstance(self._kicked_by, _tl.User):
|
|
||||||
kby = self._entities.get(utils.get_peer_id(self._kicked_by))
|
|
||||||
if kby:
|
|
||||||
self._kicked_by = kby
|
|
||||||
|
|
||||||
return self._kicked_by
|
if self._users is None:
|
||||||
|
self._users = [
|
||||||
|
self._entities[user_id]
|
||||||
|
for user_id in self._user_ids
|
||||||
|
if user_id in self._entities
|
||||||
|
]
|
||||||
|
|
||||||
async def get_kicked_by(self):
|
return self._users
|
||||||
"""
|
|
||||||
Returns `kicked_by` but will make an API call if necessary.
|
|
||||||
"""
|
|
||||||
if not self.kicked_by and self._kicked_by:
|
|
||||||
self._kicked_by = await self._client.get_entity(self._kicked_by)
|
|
||||||
|
|
||||||
return self._kicked_by
|
async def get_users(self):
|
||||||
|
"""
|
||||||
|
Returns `users` but will make an API call if necessary.
|
||||||
|
"""
|
||||||
|
if not self._user_ids:
|
||||||
|
return []
|
||||||
|
|
||||||
@property
|
# Note: we access the property first so that it fills if needed
|
||||||
def user(self):
|
if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message:
|
||||||
"""
|
await self.action_message._reload_message()
|
||||||
The first user that takes part in this action. For example, who joined.
|
self._users = [
|
||||||
|
u for u in self.action_message.action_entities
|
||||||
|
if isinstance(u, (_tl.User, _tl.UserEmpty))]
|
||||||
|
|
||||||
Might be `None` if the information can't be retrieved or
|
return self._users
|
||||||
there is no user taking part.
|
|
||||||
"""
|
|
||||||
if self.users:
|
|
||||||
return self._users[0]
|
|
||||||
|
|
||||||
async def get_user(self):
|
@property
|
||||||
"""
|
def input_users(self):
|
||||||
Returns `user` but will make an API call if necessary.
|
"""
|
||||||
"""
|
Input version of the ``self.users`` property.
|
||||||
if self.users or await self.get_users():
|
"""
|
||||||
return self._users[0]
|
if self._input_users is None and self._user_ids:
|
||||||
|
self._input_users = []
|
||||||
|
for user_id in self._user_ids:
|
||||||
|
# Try to get it from our entities
|
||||||
|
try:
|
||||||
|
self._input_users.append(utils.get_input_peer(self._entities[user_id]))
|
||||||
|
continue
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
@property
|
return self._input_users or []
|
||||||
def input_user(self):
|
|
||||||
"""
|
|
||||||
Input version of the ``self.user`` property.
|
|
||||||
"""
|
|
||||||
if self.input_users:
|
|
||||||
return self._input_users[0]
|
|
||||||
|
|
||||||
async def get_input_user(self):
|
async def get_input_users(self):
|
||||||
"""
|
"""
|
||||||
Returns `input_user` but will make an API call if necessary.
|
Returns `input_users` but will make an API call if necessary.
|
||||||
"""
|
"""
|
||||||
if self.input_users or await self.get_input_users():
|
if not self._user_ids:
|
||||||
return self._input_users[0]
|
return []
|
||||||
|
|
||||||
@property
|
# Note: we access the property first so that it fills if needed
|
||||||
def user_id(self):
|
if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message:
|
||||||
"""
|
self._input_users = [
|
||||||
Returns the marked signed ID of the first user, if any.
|
utils.get_input_peer(u)
|
||||||
"""
|
for u in self.action_message.action_entities
|
||||||
if self._user_ids:
|
if isinstance(u, (_tl.User, _tl.UserEmpty))]
|
||||||
return self._user_ids[0]
|
|
||||||
|
|
||||||
@property
|
return self._input_users or []
|
||||||
def users(self):
|
|
||||||
"""
|
|
||||||
A list of users that take part in this action. For example, who joined.
|
|
||||||
|
|
||||||
Might be empty if the information can't be retrieved or there
|
@property
|
||||||
are no users taking part.
|
def user_ids(self):
|
||||||
"""
|
"""
|
||||||
if not self._user_ids:
|
Returns the marked signed ID of the users, if any.
|
||||||
return []
|
"""
|
||||||
|
if self._user_ids:
|
||||||
if self._users is None:
|
return self._user_ids[:]
|
||||||
self._users = [
|
|
||||||
self._entities[user_id]
|
|
||||||
for user_id in self._user_ids
|
|
||||||
if user_id in self._entities
|
|
||||||
]
|
|
||||||
|
|
||||||
return self._users
|
|
||||||
|
|
||||||
async def get_users(self):
|
|
||||||
"""
|
|
||||||
Returns `users` but will make an API call if necessary.
|
|
||||||
"""
|
|
||||||
if not self._user_ids:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Note: we access the property first so that it fills if needed
|
|
||||||
if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message:
|
|
||||||
await self.action_message._reload_message()
|
|
||||||
self._users = [
|
|
||||||
u for u in self.action_message.action_entities
|
|
||||||
if isinstance(u, (_tl.User, _tl.UserEmpty))]
|
|
||||||
|
|
||||||
return self._users
|
|
||||||
|
|
||||||
@property
|
|
||||||
def input_users(self):
|
|
||||||
"""
|
|
||||||
Input version of the ``self.users`` property.
|
|
||||||
"""
|
|
||||||
if self._input_users is None and self._user_ids:
|
|
||||||
self._input_users = []
|
|
||||||
for user_id in self._user_ids:
|
|
||||||
# Try to get it from our entities
|
|
||||||
try:
|
|
||||||
self._input_users.append(utils.get_input_peer(self._entities[user_id]))
|
|
||||||
continue
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return self._input_users or []
|
|
||||||
|
|
||||||
async def get_input_users(self):
|
|
||||||
"""
|
|
||||||
Returns `input_users` but will make an API call if necessary.
|
|
||||||
"""
|
|
||||||
if not self._user_ids:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Note: we access the property first so that it fills if needed
|
|
||||||
if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message:
|
|
||||||
self._input_users = [
|
|
||||||
utils.get_input_peer(u)
|
|
||||||
for u in self.action_message.action_entities
|
|
||||||
if isinstance(u, (_tl.User, _tl.UserEmpty))]
|
|
||||||
|
|
||||||
return self._input_users or []
|
|
||||||
|
|
||||||
@property
|
|
||||||
def user_ids(self):
|
|
||||||
"""
|
|
||||||
Returns the marked signed ID of the users, if any.
|
|
||||||
"""
|
|
||||||
if self._user_ids:
|
|
||||||
return self._user_ids[:]
|
|
||||||
|
|
|
@ -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
|
|
|
@ -3,34 +3,27 @@ import re
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from .common import EventBuilder, EventCommon, name_inner_event
|
from .base import EventBuilder
|
||||||
from .._misc import utils
|
from .._misc import utils
|
||||||
from .. import _tl
|
from .. import _tl
|
||||||
from ..types import _custom
|
from ..types import _custom
|
||||||
|
|
||||||
|
|
||||||
@name_inner_event
|
class InlineQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
|
||||||
class InlineQuery(EventBuilder):
|
|
||||||
"""
|
"""
|
||||||
Occurs whenever you sign in as a bot and a user
|
Occurs whenever you sign in as a bot and a user
|
||||||
sends an inline query such as ``@bot query``.
|
sends an inline query such as ``@bot query``.
|
||||||
|
|
||||||
Args:
|
Members:
|
||||||
users (`entity`, optional):
|
query (:tl:`UpdateBotInlineQuery`):
|
||||||
May be one or more entities (username/peer/etc.), preferably IDs.
|
The original :tl:`UpdateBotInlineQuery`.
|
||||||
By default, only inline queries from these users will be handled.
|
|
||||||
|
|
||||||
blacklist_users (`bool`, optional):
|
Make sure to access the `text` property of the query if
|
||||||
Whether to treat the users as a blacklist instead of
|
you want the text rather than the actual query object.
|
||||||
as a whitelist (default). This means that every chat
|
|
||||||
will be handled *except* those specified in ``users``
|
|
||||||
which will be ignored if ``blacklist_users=True``.
|
|
||||||
|
|
||||||
pattern (`str`, `callable`, `Pattern`, optional):
|
pattern_match (`obj`, optional):
|
||||||
If set, only queries matching this pattern will be handled.
|
The resulting object from calling the passed ``pattern``
|
||||||
You can specify a regex-like string which will be matched
|
function, which is ``re.compile(...).match`` by default.
|
||||||
against the message, a callable function that returns `True`
|
|
||||||
if a message is acceptable, or a compiled regex pattern.
|
|
||||||
|
|
||||||
Example
|
Example
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
@ -47,200 +40,163 @@ class InlineQuery(EventBuilder):
|
||||||
builder.article('lowercase', text=event.text.lower()),
|
builder.article('lowercase', text=event.text.lower()),
|
||||||
])
|
])
|
||||||
"""
|
"""
|
||||||
def __init__(
|
def __init__(self, query):
|
||||||
self, users=None, *, blacklist_users=False, func=None, pattern=None):
|
_custom.chatgetter.ChatGetter.__init__(self, _tl.PeerUser(query.user_id))
|
||||||
super().__init__(users, blacklist_chats=blacklist_users, func=func)
|
_custom.sendergetter.SenderGetter.__init__(self, query.user_id)
|
||||||
|
self.query = query
|
||||||
if isinstance(pattern, str):
|
self.pattern_match = None
|
||||||
self.pattern = re.compile(pattern).match
|
self._answered = False
|
||||||
elif not pattern or callable(pattern):
|
|
||||||
self.pattern = pattern
|
|
||||||
elif hasattr(pattern, 'match') and callable(pattern.match):
|
|
||||||
self.pattern = pattern.match
|
|
||||||
else:
|
|
||||||
raise TypeError('Invalid pattern type given')
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, update, others=None, self_id=None, *todo, **todo2):
|
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
|
||||||
if isinstance(update, _tl.UpdateBotInlineQuery):
|
if isinstance(update, _tl.UpdateBotInlineQuery):
|
||||||
return cls.Event(update)
|
return cls.Event(update)
|
||||||
|
|
||||||
def filter(self, event):
|
def _set_client(self, client):
|
||||||
if self.pattern:
|
super()._set_client(client)
|
||||||
match = self.pattern(event.text)
|
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
|
||||||
if not match:
|
|
||||||
return
|
|
||||||
event.pattern_match = match
|
|
||||||
|
|
||||||
return super().filter(event)
|
@property
|
||||||
|
def id(self):
|
||||||
class Event(EventCommon, _custom.sendergetter.SenderGetter):
|
|
||||||
"""
|
"""
|
||||||
Represents the event of a new callback query.
|
Returns the unique identifier for the query ID.
|
||||||
|
|
||||||
Members:
|
|
||||||
query (:tl:`UpdateBotInlineQuery`):
|
|
||||||
The original :tl:`UpdateBotInlineQuery`.
|
|
||||||
|
|
||||||
Make sure to access the `text` property of the query if
|
|
||||||
you want the text rather than the actual query object.
|
|
||||||
|
|
||||||
pattern_match (`obj`, optional):
|
|
||||||
The resulting object from calling the passed ``pattern``
|
|
||||||
function, which is ``re.compile(...).match`` by default.
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, query):
|
return self.query.query_id
|
||||||
super().__init__(chat_peer=_tl.PeerUser(query.user_id))
|
|
||||||
_custom.sendergetter.SenderGetter.__init__(self, query.user_id)
|
|
||||||
self.query = query
|
|
||||||
self.pattern_match = None
|
|
||||||
self._answered = False
|
|
||||||
|
|
||||||
def _set_client(self, client):
|
@property
|
||||||
super()._set_client(client)
|
def text(self):
|
||||||
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
|
"""
|
||||||
|
Returns the text the user used to make the inline query.
|
||||||
|
"""
|
||||||
|
return self.query.query
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def offset(self):
|
||||||
"""
|
"""
|
||||||
Returns the unique identifier for the query ID.
|
The string the user's client used as an offset for the query.
|
||||||
"""
|
This will either be empty or equal to offsets passed to `answer`.
|
||||||
return self.query.query_id
|
"""
|
||||||
|
return self.query.offset
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text(self):
|
def geo(self):
|
||||||
"""
|
"""
|
||||||
Returns the text the user used to make the inline query.
|
If the user location is requested when using inline mode
|
||||||
"""
|
and the user's device is able to send it, this will return
|
||||||
return self.query.query
|
the :tl:`GeoPoint` with the position of the user.
|
||||||
|
"""
|
||||||
|
return self.query.geo
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def offset(self):
|
def builder(self):
|
||||||
"""
|
"""
|
||||||
The string the user's client used as an offset for the query.
|
Returns a new `InlineBuilder
|
||||||
This will either be empty or equal to offsets passed to `answer`.
|
<telethon.tl.custom.inlinebuilder.InlineBuilder>` instance.
|
||||||
"""
|
|
||||||
return self.query.offset
|
|
||||||
|
|
||||||
@property
|
See the documentation for `builder` to know what kind of answers
|
||||||
def geo(self):
|
can be given.
|
||||||
"""
|
"""
|
||||||
If the user location is requested when using inline mode
|
return _custom.InlineBuilder(self._client)
|
||||||
and the user's device is able to send it, this will return
|
|
||||||
the :tl:`GeoPoint` with the position of the user.
|
|
||||||
"""
|
|
||||||
return self.query.geo
|
|
||||||
|
|
||||||
@property
|
async def answer(
|
||||||
def builder(self):
|
self, results=None, cache_time=0, *,
|
||||||
"""
|
gallery=False, next_offset=None, private=False,
|
||||||
Returns a new `InlineBuilder
|
switch_pm=None, switch_pm_param=''):
|
||||||
<telethon.tl.custom.inlinebuilder.InlineBuilder>` instance.
|
"""
|
||||||
"""
|
Answers the inline query with the given results.
|
||||||
return _custom.InlineBuilder(self._client)
|
|
||||||
|
|
||||||
async def answer(
|
Args:
|
||||||
self, results=None, cache_time=0, *,
|
results (`list`, optional):
|
||||||
gallery=False, next_offset=None, private=False,
|
A list of :tl:`InputBotInlineResult` to use.
|
||||||
switch_pm=None, switch_pm_param=''):
|
You should use `builder` to create these:
|
||||||
"""
|
|
||||||
Answers the inline query with the given results.
|
|
||||||
|
|
||||||
See the documentation for `builder` to know what kind of answers
|
|
||||||
can be given.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
results (`list`, optional):
|
|
||||||
A list of :tl:`InputBotInlineResult` to use.
|
|
||||||
You should use `builder` to create these:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
builder = inline.builder
|
|
||||||
r1 = builder.article('Be nice', text='Have a nice day')
|
|
||||||
r2 = builder.article('Be bad', text="I don't like you")
|
|
||||||
await inline.answer([r1, r2])
|
|
||||||
|
|
||||||
You can send up to 50 results as documented in
|
|
||||||
https://core.telegram.org/bots/api#answerinlinequery.
|
|
||||||
Sending more will raise ``ResultsTooMuchError``,
|
|
||||||
and you should consider using `next_offset` to
|
|
||||||
paginate them.
|
|
||||||
|
|
||||||
cache_time (`int`, optional):
|
|
||||||
For how long this result should be cached on
|
|
||||||
the user's client. Defaults to 0 for no cache.
|
|
||||||
|
|
||||||
gallery (`bool`, optional):
|
|
||||||
Whether the results should show as a gallery (grid) or not.
|
|
||||||
|
|
||||||
next_offset (`str`, optional):
|
|
||||||
The offset the client will send when the user scrolls the
|
|
||||||
results and it repeats the request.
|
|
||||||
|
|
||||||
private (`bool`, optional):
|
|
||||||
Whether the results should be cached by Telegram
|
|
||||||
(not private) or by the user's client (private).
|
|
||||||
|
|
||||||
switch_pm (`str`, optional):
|
|
||||||
If set, this text will be shown in the results
|
|
||||||
to allow the user to switch to private messages.
|
|
||||||
|
|
||||||
switch_pm_param (`str`, optional):
|
|
||||||
Optional parameter to start the bot with if
|
|
||||||
`switch_pm` was used.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@bot.on(events.InlineQuery)
|
builder = inline.builder
|
||||||
async def handler(event):
|
r1 = builder.article('Be nice', text='Have a nice day')
|
||||||
builder = event.builder
|
r2 = builder.article('Be bad', text="I don't like you")
|
||||||
|
await inline.answer([r1, r2])
|
||||||
|
|
||||||
rev_text = event.text[::-1]
|
You can send up to 50 results as documented in
|
||||||
await event.answer([
|
https://core.telegram.org/bots/api#answerinlinequery.
|
||||||
builder.article('Reverse text', text=rev_text),
|
Sending more will raise ``ResultsTooMuchError``,
|
||||||
builder.photo('/path/to/photo.jpg')
|
and you should consider using `next_offset` to
|
||||||
])
|
paginate them.
|
||||||
"""
|
|
||||||
if self._answered:
|
|
||||||
return
|
|
||||||
|
|
||||||
if results:
|
cache_time (`int`, optional):
|
||||||
futures = [self._as_future(x) for x in results]
|
For how long this result should be cached on
|
||||||
|
the user's client. Defaults to 0 for no cache.
|
||||||
|
|
||||||
await asyncio.wait(futures)
|
gallery (`bool`, optional):
|
||||||
|
Whether the results should show as a gallery (grid) or not.
|
||||||
|
|
||||||
# All futures will be in the `done` *set* that `wait` returns.
|
next_offset (`str`, optional):
|
||||||
#
|
The offset the client will send when the user scrolls the
|
||||||
# Precisely because it's a `set` and not a `list`, it
|
results and it repeats the request.
|
||||||
# will not preserve the order, but since all futures
|
|
||||||
# completed we can use our original, ordered `list`.
|
|
||||||
results = [x.result() for x in futures]
|
|
||||||
else:
|
|
||||||
results = []
|
|
||||||
|
|
||||||
if switch_pm:
|
private (`bool`, optional):
|
||||||
switch_pm = _tl.InlineBotSwitchPM(switch_pm, switch_pm_param)
|
Whether the results should be cached by Telegram
|
||||||
|
(not private) or by the user's client (private).
|
||||||
|
|
||||||
return await self._client(
|
switch_pm (`str`, optional):
|
||||||
_tl.fn.messages.SetInlineBotResults(
|
If set, this text will be shown in the results
|
||||||
query_id=self.query.query_id,
|
to allow the user to switch to private messages.
|
||||||
results=results,
|
|
||||||
cache_time=cache_time,
|
switch_pm_param (`str`, optional):
|
||||||
gallery=gallery,
|
Optional parameter to start the bot with if
|
||||||
next_offset=next_offset,
|
`switch_pm` was used.
|
||||||
private=private,
|
|
||||||
switch_pm=switch_pm
|
Example:
|
||||||
)
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@bot.on(events.InlineQuery)
|
||||||
|
async def handler(event):
|
||||||
|
builder = event.builder
|
||||||
|
|
||||||
|
rev_text = event.text[::-1]
|
||||||
|
await event.answer([
|
||||||
|
builder.article('Reverse text', text=rev_text),
|
||||||
|
builder.photo('/path/to/photo.jpg')
|
||||||
|
])
|
||||||
|
"""
|
||||||
|
if self._answered:
|
||||||
|
return
|
||||||
|
|
||||||
|
if results:
|
||||||
|
futures = [self._as_future(x) for x in results]
|
||||||
|
|
||||||
|
await asyncio.wait(futures)
|
||||||
|
|
||||||
|
# All futures will be in the `done` *set* that `wait` returns.
|
||||||
|
#
|
||||||
|
# Precisely because it's a `set` and not a `list`, it
|
||||||
|
# will not preserve the order, but since all futures
|
||||||
|
# completed we can use our original, ordered `list`.
|
||||||
|
results = [x.result() for x in futures]
|
||||||
|
else:
|
||||||
|
results = []
|
||||||
|
|
||||||
|
if switch_pm:
|
||||||
|
switch_pm = _tl.InlineBotSwitchPM(switch_pm, switch_pm_param)
|
||||||
|
|
||||||
|
return await self._client(
|
||||||
|
_tl.fn.messages.SetInlineBotResults(
|
||||||
|
query_id=self.query.query_id,
|
||||||
|
results=results,
|
||||||
|
cache_time=cache_time,
|
||||||
|
gallery=gallery,
|
||||||
|
next_offset=next_offset,
|
||||||
|
private=private,
|
||||||
|
switch_pm=switch_pm
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _as_future(obj):
|
def _as_future(obj):
|
||||||
if inspect.isawaitable(obj):
|
if inspect.isawaitable(obj):
|
||||||
return asyncio.ensure_future(obj)
|
return asyncio.ensure_future(obj)
|
||||||
|
|
||||||
f = asyncio.get_running_loop().create_future()
|
f = asyncio.get_running_loop().create_future()
|
||||||
f.set_result(obj)
|
f.set_result(obj)
|
||||||
return f
|
return f
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
from .common import EventBuilder, EventCommon, name_inner_event
|
from .base import EventBuilder
|
||||||
from .. import _tl
|
from .. import _tl
|
||||||
|
from ..types import _custom
|
||||||
|
|
||||||
|
|
||||||
@name_inner_event
|
class MessageDeleted(EventBuilder, _custom.chatgetter.ChatGetter):
|
||||||
class MessageDeleted(EventBuilder):
|
|
||||||
"""
|
"""
|
||||||
Occurs whenever a message is deleted. Note that this event isn't 100%
|
Occurs whenever a message is deleted. Note that this event isn't 100%
|
||||||
reliable, since Telegram doesn't always notify the clients that a message
|
reliable, since Telegram doesn't always notify the clients that a message
|
||||||
|
@ -35,8 +35,13 @@ class MessageDeleted(EventBuilder):
|
||||||
for msg_id in event.deleted_ids:
|
for msg_id in event.deleted_ids:
|
||||||
print('Message', msg_id, 'was deleted in', event.chat_id)
|
print('Message', msg_id, 'was deleted in', event.chat_id)
|
||||||
"""
|
"""
|
||||||
|
def __init__(self, deleted_ids, peer):
|
||||||
|
_custom.chatgetter.ChatGetter.__init__(self, chat_peer=peer)
|
||||||
|
self.deleted_id = None if not deleted_ids else deleted_ids[0]
|
||||||
|
self.deleted_ids = deleted_ids
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, update, others=None, self_id=None, *todo, **todo2):
|
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
|
||||||
if isinstance(update, _tl.UpdateDeleteMessages):
|
if isinstance(update, _tl.UpdateDeleteMessages):
|
||||||
return cls.Event(
|
return cls.Event(
|
||||||
deleted_ids=update.messages,
|
deleted_ids=update.messages,
|
||||||
|
@ -47,11 +52,3 @@ class MessageDeleted(EventBuilder):
|
||||||
deleted_ids=update.messages,
|
deleted_ids=update.messages,
|
||||||
peer=_tl.PeerChannel(update.channel_id)
|
peer=_tl.PeerChannel(update.channel_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
class Event(EventCommon):
|
|
||||||
def __init__(self, deleted_ids, peer):
|
|
||||||
super().__init__(
|
|
||||||
chat_peer=peer, msg_id=(deleted_ids or [0])[0]
|
|
||||||
)
|
|
||||||
self.deleted_id = None if not deleted_ids else deleted_ids[0]
|
|
||||||
self.deleted_ids = deleted_ids
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
from .common import name_inner_event
|
from .base import EventBuilder
|
||||||
from .newmessage import NewMessage
|
|
||||||
from .. import _tl
|
from .. import _tl
|
||||||
|
|
||||||
|
|
||||||
@name_inner_event
|
class MessageEdited(EventBuilder):
|
||||||
class MessageEdited(NewMessage):
|
|
||||||
"""
|
"""
|
||||||
Occurs whenever a message is edited. Just like `NewMessage
|
Occurs whenever a message is edited. Just like `NewMessage
|
||||||
<telethon.events.newmessage.NewMessage>`, you should treat
|
<telethon.events.newmessage.NewMessage>`, you should treat
|
||||||
|
@ -43,10 +41,7 @@ class MessageEdited(NewMessage):
|
||||||
print('Message', event.id, 'changed at', event.date)
|
print('Message', event.id, 'changed at', event.date)
|
||||||
"""
|
"""
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, update, others=None, self_id=None, *todo, **todo2):
|
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
|
||||||
if isinstance(update, (_tl.UpdateEditMessage,
|
if isinstance(update, (_tl.UpdateEditMessage,
|
||||||
_tl.UpdateEditChannelMessage)):
|
_tl.UpdateEditChannelMessage)):
|
||||||
return cls.Event(update.message)
|
return cls.Event(update.message)
|
||||||
|
|
||||||
class Event(NewMessage.Event):
|
|
||||||
pass # Required if we want a different name for it
|
|
||||||
|
|
|
@ -1,18 +1,24 @@
|
||||||
from .common import EventBuilder, EventCommon, name_inner_event
|
from .base import EventBuilder
|
||||||
from .._misc import utils
|
from .._misc import utils
|
||||||
from .. import _tl
|
from .. import _tl
|
||||||
|
|
||||||
|
|
||||||
@name_inner_event
|
|
||||||
class MessageRead(EventBuilder):
|
class MessageRead(EventBuilder):
|
||||||
"""
|
"""
|
||||||
Occurs whenever one or more messages are read in a chat.
|
Occurs whenever one or more messages are read in a chat.
|
||||||
|
|
||||||
Args:
|
Members:
|
||||||
inbox (`bool`, optional):
|
max_id (`int`):
|
||||||
If this argument is `True`, then when you read someone else's
|
Up to which message ID has been read. Every message
|
||||||
messages the event will be fired. By default (`False`) only
|
with an ID equal or lower to it have been read.
|
||||||
when messages you sent are read by someone else will fire it.
|
|
||||||
|
outbox (`bool`):
|
||||||
|
`True` if someone else has read your messages.
|
||||||
|
|
||||||
|
contents (`bool`):
|
||||||
|
`True` if what was read were the contents of a message.
|
||||||
|
This will be the case when e.g. you play a voice note.
|
||||||
|
It may only be set on ``inbox`` events.
|
||||||
|
|
||||||
Example
|
Example
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
@ -29,13 +35,17 @@ class MessageRead(EventBuilder):
|
||||||
# Log when you read message in a chat (from your "inbox")
|
# Log when you read message in a chat (from your "inbox")
|
||||||
print('You have read messages until', event.max_id)
|
print('You have read messages until', event.max_id)
|
||||||
"""
|
"""
|
||||||
def __init__(
|
def __init__(self, peer=None, max_id=None, out=False, contents=False,
|
||||||
self, chats=None, *, blacklist_chats=False, func=None, inbox=False):
|
message_ids=None):
|
||||||
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
|
self.outbox = out
|
||||||
self.inbox = inbox
|
self.contents = contents
|
||||||
|
self._message_ids = message_ids or []
|
||||||
|
self._messages = None
|
||||||
|
self.max_id = max_id or max(message_ids or [], default=None)
|
||||||
|
super().__init__(peer, self.max_id)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, update, others=None, self_id=None, *todo, **todo2):
|
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
|
||||||
if isinstance(update, _tl.UpdateReadHistoryInbox):
|
if isinstance(update, _tl.UpdateReadHistoryInbox):
|
||||||
return cls.Event(update.peer, update.max_id, False)
|
return cls.Event(update.peer, update.max_id, False)
|
||||||
elif isinstance(update, _tl.UpdateReadHistoryOutbox):
|
elif isinstance(update, _tl.UpdateReadHistoryOutbox):
|
||||||
|
@ -54,90 +64,58 @@ class MessageRead(EventBuilder):
|
||||||
message_ids=update.messages,
|
message_ids=update.messages,
|
||||||
contents=True)
|
contents=True)
|
||||||
|
|
||||||
def filter(self, event):
|
@property
|
||||||
if self.inbox == event.outbox:
|
def inbox(self):
|
||||||
return
|
|
||||||
|
|
||||||
return super().filter(event)
|
|
||||||
|
|
||||||
class Event(EventCommon):
|
|
||||||
"""
|
"""
|
||||||
Represents the event of one or more messages being read.
|
`True` if you have read someone else's messages.
|
||||||
|
|
||||||
Members:
|
|
||||||
max_id (`int`):
|
|
||||||
Up to which message ID has been read. Every message
|
|
||||||
with an ID equal or lower to it have been read.
|
|
||||||
|
|
||||||
outbox (`bool`):
|
|
||||||
`True` if someone else has read your messages.
|
|
||||||
|
|
||||||
contents (`bool`):
|
|
||||||
`True` if what was read were the contents of a message.
|
|
||||||
This will be the case when e.g. you play a voice note.
|
|
||||||
It may only be set on ``inbox`` events.
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, peer=None, max_id=None, out=False, contents=False,
|
return not self.outbox
|
||||||
message_ids=None):
|
|
||||||
self.outbox = out
|
|
||||||
self.contents = contents
|
|
||||||
self._message_ids = message_ids or []
|
|
||||||
self._messages = None
|
|
||||||
self.max_id = max_id or max(message_ids or [], default=None)
|
|
||||||
super().__init__(peer, self.max_id)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def inbox(self):
|
def message_ids(self):
|
||||||
"""
|
"""
|
||||||
`True` if you have read someone else's messages.
|
The IDs of the messages **which contents'** were read.
|
||||||
"""
|
|
||||||
return not self.outbox
|
|
||||||
|
|
||||||
@property
|
Use :meth:`is_read` if you need to check whether a message
|
||||||
def message_ids(self):
|
was read instead checking if it's in here.
|
||||||
"""
|
"""
|
||||||
The IDs of the messages **which contents'** were read.
|
return self._message_ids
|
||||||
|
|
||||||
Use :meth:`is_read` if you need to check whether a message
|
async def get_messages(self):
|
||||||
was read instead checking if it's in here.
|
"""
|
||||||
"""
|
Returns the list of `Message <telethon.tl.custom.message.Message>`
|
||||||
return self._message_ids
|
**which contents'** were read.
|
||||||
|
|
||||||
async def get_messages(self):
|
Use :meth:`is_read` if you need to check whether a message
|
||||||
"""
|
was read instead checking if it's in here.
|
||||||
Returns the list of `Message <telethon.tl.custom.message.Message>`
|
"""
|
||||||
**which contents'** were read.
|
if self._messages is None:
|
||||||
|
chat = await self.get_input_chat()
|
||||||
Use :meth:`is_read` if you need to check whether a message
|
if not chat:
|
||||||
was read instead checking if it's in here.
|
self._messages = []
|
||||||
"""
|
|
||||||
if self._messages is None:
|
|
||||||
chat = await self.get_input_chat()
|
|
||||||
if not chat:
|
|
||||||
self._messages = []
|
|
||||||
else:
|
|
||||||
self._messages = await self._client.get_messages(
|
|
||||||
chat, ids=self._message_ids)
|
|
||||||
|
|
||||||
return self._messages
|
|
||||||
|
|
||||||
def is_read(self, message):
|
|
||||||
"""
|
|
||||||
Returns `True` if the given message (or its ID) has been read.
|
|
||||||
|
|
||||||
If a list-like argument is provided, this method will return a
|
|
||||||
list of booleans indicating which messages have been read.
|
|
||||||
"""
|
|
||||||
if utils.is_list_like(message):
|
|
||||||
return [(m if isinstance(m, int) else m.id) <= self.max_id
|
|
||||||
for m in message]
|
|
||||||
else:
|
else:
|
||||||
return (message if isinstance(message, int)
|
self._messages = await self._client.get_messages(
|
||||||
else message.id) <= self.max_id
|
chat, ids=self._message_ids)
|
||||||
|
|
||||||
def __contains__(self, message):
|
return self._messages
|
||||||
"""`True` if the message(s) are read message."""
|
|
||||||
if utils.is_list_like(message):
|
def is_read(self, message):
|
||||||
return all(self.is_read(message))
|
"""
|
||||||
else:
|
Returns `True` if the given message (or its ID) has been read.
|
||||||
return self.is_read(message)
|
|
||||||
|
If a list-like argument is provided, this method will return a
|
||||||
|
list of booleans indicating which messages have been read.
|
||||||
|
"""
|
||||||
|
if utils.is_list_like(message):
|
||||||
|
return [(m if isinstance(m, int) else m.id) <= self.max_id
|
||||||
|
for m in message]
|
||||||
|
else:
|
||||||
|
return (message if isinstance(message, int)
|
||||||
|
else message.id) <= self.max_id
|
||||||
|
|
||||||
|
def __contains__(self, message):
|
||||||
|
"""`True` if the message(s) are read message."""
|
||||||
|
if utils.is_list_like(message):
|
||||||
|
return all(self.is_read(message))
|
||||||
|
else:
|
||||||
|
return self.is_read(message)
|
||||||
|
|
|
@ -1,43 +1,43 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set
|
from .base import EventBuilder
|
||||||
from .._misc import utils
|
from .._misc import utils
|
||||||
from .. import _tl
|
from .. import _tl
|
||||||
from ..types import _custom
|
from ..types import _custom
|
||||||
|
|
||||||
|
|
||||||
@name_inner_event
|
class NewMessageEvent(EventBuilder, Message):
|
||||||
class NewMessage(EventBuilder):
|
|
||||||
"""
|
"""
|
||||||
Occurs whenever a new text message or a message with media arrives.
|
Represents the event of a new message. This event can be treated
|
||||||
|
to all effects as a `Message <telethon.tl.custom.message.Message>`,
|
||||||
|
so please **refer to its documentation** to know what you can do
|
||||||
|
with this event.
|
||||||
|
|
||||||
Args:
|
Members:
|
||||||
incoming (`bool`, optional):
|
message (`Message <telethon.tl.custom.message.Message>`):
|
||||||
If set to `True`, only **incoming** messages will be handled.
|
This is the only difference with the received
|
||||||
Mutually exclusive with ``outgoing`` (can only set one of either).
|
`Message <telethon.tl.custom.message.Message>`, and will
|
||||||
|
return the `telethon.tl.custom.message.Message` itself,
|
||||||
|
not the text.
|
||||||
|
|
||||||
outgoing (`bool`, optional):
|
See `Message <telethon.tl.custom.message.Message>` for
|
||||||
If set to `True`, only **outgoing** messages will be handled.
|
the rest of available members and methods.
|
||||||
Mutually exclusive with ``incoming`` (can only set one of either).
|
|
||||||
|
|
||||||
from_users (`entity`, optional):
|
pattern_match (`obj`):
|
||||||
Unlike `chats`, this parameter filters the *senders* of the
|
The resulting object from calling the passed ``pattern`` function.
|
||||||
message. That is, only messages *sent by these users* will be
|
Here's an example using a string (defaults to regex match):
|
||||||
handled. Use `chats` if you want private messages with this/these
|
|
||||||
users. `from_users` lets you filter by messages sent by *one or
|
|
||||||
more* users across the desired chats (doesn't need a list).
|
|
||||||
|
|
||||||
forwards (`bool`, optional):
|
>>> from telethon import TelegramClient, events
|
||||||
Whether forwarded messages should be handled or not. By default,
|
>>> client = TelegramClient(...)
|
||||||
both forwarded and normal messages are included. If it's `True`
|
>>>
|
||||||
*only* forwards will be handled. If it's `False` only messages
|
>>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!'))
|
||||||
that are *not* forwards will be handled.
|
... async def handler(event):
|
||||||
|
... # In this case, the result is a ``Match`` object
|
||||||
pattern (`str`, `callable`, `Pattern`, optional):
|
... # since the `str` pattern was converted into
|
||||||
If set, only messages matching this pattern will be handled.
|
... # the ``re.compile(pattern).match`` function.
|
||||||
You can specify a regex-like string which will be matched
|
... print('Welcomed', event.pattern_match.group(1))
|
||||||
against the message, a callable function that returns `True`
|
...
|
||||||
if a message is acceptable, or a compiled regex pattern.
|
>>>
|
||||||
|
|
||||||
Example
|
Example
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
@ -57,45 +57,16 @@ class NewMessage(EventBuilder):
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
await client.delete_messages(event.chat_id, [event.id, m.id])
|
await client.delete_messages(event.chat_id, [event.id, m.id])
|
||||||
"""
|
"""
|
||||||
def __init__(self, chats=None, *, blacklist_chats=False, func=None,
|
def __init__(self, message):
|
||||||
incoming=None, outgoing=None,
|
self.__dict__['_init'] = False
|
||||||
from_users=None, forwards=None, pattern=None):
|
super().__init__(chat_peer=message.peer_id,
|
||||||
if incoming and outgoing:
|
msg_id=message.id, broadcast=bool(message.post))
|
||||||
incoming = outgoing = None # Same as no filter
|
|
||||||
elif incoming is not None and outgoing is None:
|
|
||||||
outgoing = not incoming
|
|
||||||
elif outgoing is not None and incoming is None:
|
|
||||||
incoming = not outgoing
|
|
||||||
elif all(x is not None and not x for x in (incoming, outgoing)):
|
|
||||||
raise ValueError("Don't create an event handler if you "
|
|
||||||
"don't want neither incoming nor outgoing!")
|
|
||||||
|
|
||||||
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
|
self.pattern_match = None
|
||||||
self.incoming = incoming
|
self.message = message
|
||||||
self.outgoing = outgoing
|
|
||||||
self.from_users = from_users
|
|
||||||
self.forwards = forwards
|
|
||||||
if isinstance(pattern, str):
|
|
||||||
self.pattern = re.compile(pattern).match
|
|
||||||
elif not pattern or callable(pattern):
|
|
||||||
self.pattern = pattern
|
|
||||||
elif hasattr(pattern, 'match') and callable(pattern.match):
|
|
||||||
self.pattern = pattern.match
|
|
||||||
else:
|
|
||||||
raise TypeError('Invalid pattern type given')
|
|
||||||
|
|
||||||
# Should we short-circuit? E.g. perform no check at all
|
|
||||||
self._no_check = all(x is None for x in (
|
|
||||||
self.chats, self.incoming, self.outgoing, self.pattern,
|
|
||||||
self.from_users, self.forwards, self.from_users, self.func
|
|
||||||
))
|
|
||||||
|
|
||||||
async def _resolve(self, client):
|
|
||||||
await super()._resolve(client)
|
|
||||||
self.from_users = await _into_id_set(client, self.from_users)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, update, others, self_id, entities, client):
|
def _build(cls, update, others, self_id, entities, client):
|
||||||
if isinstance(update,
|
if isinstance(update,
|
||||||
(_tl.UpdateNewMessage, _tl.UpdateNewChannelMessage)):
|
(_tl.UpdateNewMessage, _tl.UpdateNewChannelMessage)):
|
||||||
if not isinstance(update.message, _tl.Message):
|
if not isinstance(update.message, _tl.Message):
|
||||||
|
@ -139,85 +110,3 @@ class NewMessage(EventBuilder):
|
||||||
return
|
return
|
||||||
|
|
||||||
return cls.Event(_custom.Message._new(client, msg, entities, None))
|
return cls.Event(_custom.Message._new(client, msg, entities, None))
|
||||||
|
|
||||||
def filter(self, event):
|
|
||||||
if self._no_check:
|
|
||||||
return event
|
|
||||||
|
|
||||||
if self.incoming and event.message.out:
|
|
||||||
return
|
|
||||||
if self.outgoing and not event.message.out:
|
|
||||||
return
|
|
||||||
if self.forwards is not None:
|
|
||||||
if bool(self.forwards) != bool(event.message.fwd_from):
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.from_users is not None:
|
|
||||||
if event.message.sender_id not in self.from_users:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.pattern:
|
|
||||||
match = self.pattern(event.message.message or '')
|
|
||||||
if not match:
|
|
||||||
return
|
|
||||||
event.pattern_match = match
|
|
||||||
|
|
||||||
return super().filter(event)
|
|
||||||
|
|
||||||
class Event(EventCommon):
|
|
||||||
"""
|
|
||||||
Represents the event of a new message. This event can be treated
|
|
||||||
to all effects as a `Message <telethon.tl.custom.message.Message>`,
|
|
||||||
so please **refer to its documentation** to know what you can do
|
|
||||||
with this event.
|
|
||||||
|
|
||||||
Members:
|
|
||||||
message (`Message <telethon.tl.custom.message.Message>`):
|
|
||||||
This is the only difference with the received
|
|
||||||
`Message <telethon.tl.custom.message.Message>`, and will
|
|
||||||
return the `telethon.tl.custom.message.Message` itself,
|
|
||||||
not the text.
|
|
||||||
|
|
||||||
See `Message <telethon.tl.custom.message.Message>` for
|
|
||||||
the rest of available members and methods.
|
|
||||||
|
|
||||||
pattern_match (`obj`):
|
|
||||||
The resulting object from calling the passed ``pattern`` function.
|
|
||||||
Here's an example using a string (defaults to regex match):
|
|
||||||
|
|
||||||
>>> from telethon import TelegramClient, events
|
|
||||||
>>> client = TelegramClient(...)
|
|
||||||
>>>
|
|
||||||
>>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!'))
|
|
||||||
... async def handler(event):
|
|
||||||
... # In this case, the result is a ``Match`` object
|
|
||||||
... # since the `str` pattern was converted into
|
|
||||||
... # the ``re.compile(pattern).match`` function.
|
|
||||||
... print('Welcomed', event.pattern_match.group(1))
|
|
||||||
...
|
|
||||||
>>>
|
|
||||||
"""
|
|
||||||
def __init__(self, message):
|
|
||||||
self.__dict__['_init'] = False
|
|
||||||
super().__init__(chat_peer=message.peer_id,
|
|
||||||
msg_id=message.id, broadcast=bool(message.post))
|
|
||||||
|
|
||||||
self.pattern_match = None
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
def _set_client(self, client):
|
|
||||||
super()._set_client(client)
|
|
||||||
m = self.message
|
|
||||||
self.__dict__['_init'] = True # No new attributes can be set
|
|
||||||
|
|
||||||
def __getattr__(self, item):
|
|
||||||
if item in self.__dict__:
|
|
||||||
return self.__dict__[item]
|
|
||||||
else:
|
|
||||||
return getattr(self.message, item)
|
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
|
||||||
if not self.__dict__['_init'] or name in self.__dict__:
|
|
||||||
self.__dict__[name] = value
|
|
||||||
else:
|
|
||||||
setattr(self.message, name, value)
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from .common import EventBuilder
|
from .base import EventBuilder
|
||||||
from .._misc import utils
|
from .._misc import utils
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,11 +8,6 @@ class Raw(EventBuilder):
|
||||||
:tl:`Update` object that Telegram sends. You normally shouldn't
|
:tl:`Update` object that Telegram sends. You normally shouldn't
|
||||||
need these.
|
need these.
|
||||||
|
|
||||||
Args:
|
|
||||||
types (`list` | `tuple` | `type`, optional):
|
|
||||||
The type or types that the :tl:`Update` instance must be.
|
|
||||||
Equivalent to ``if not isinstance(update, types): return``.
|
|
||||||
|
|
||||||
Example
|
Example
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -23,31 +18,6 @@ class Raw(EventBuilder):
|
||||||
# Print all incoming updates
|
# Print all incoming updates
|
||||||
print(update.stringify())
|
print(update.stringify())
|
||||||
"""
|
"""
|
||||||
def __init__(self, types=None, *, func=None):
|
|
||||||
super().__init__(func=func)
|
|
||||||
if not types:
|
|
||||||
self.types = None
|
|
||||||
elif not utils.is_list_like(types):
|
|
||||||
if not isinstance(types, type):
|
|
||||||
raise TypeError('Invalid input type given: {}'.format(types))
|
|
||||||
|
|
||||||
self.types = types
|
|
||||||
else:
|
|
||||||
if not all(isinstance(x, type) for x in types):
|
|
||||||
raise TypeError('Invalid input types given: {}'.format(types))
|
|
||||||
|
|
||||||
self.types = tuple(types)
|
|
||||||
|
|
||||||
async def resolve(self, client):
|
|
||||||
self.resolved = True
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, update, others=None, self_id=None, *todo, **todo2):
|
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
|
||||||
return update
|
return update
|
||||||
|
|
||||||
def filter(self, event):
|
|
||||||
if not self.types or isinstance(event, self.types):
|
|
||||||
if self.func:
|
|
||||||
# Return the result of func directly as it may need to be awaited
|
|
||||||
return self.func(event)
|
|
||||||
return event
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
from .common import EventBuilder, EventCommon, name_inner_event
|
from .base import EventBuilder
|
||||||
from .._misc import utils
|
from .._misc import utils
|
||||||
from .. import _tl
|
from .. import _tl
|
||||||
from ..types import _custom
|
from ..types import _custom
|
||||||
|
@ -32,11 +32,25 @@ def _requires_status(function):
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
@name_inner_event
|
class UserUpdateEvent(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
|
||||||
class UserUpdate(EventBuilder):
|
|
||||||
"""
|
"""
|
||||||
Occurs whenever a user goes online, starts typing, etc.
|
Occurs whenever a user goes online, starts typing, etc.
|
||||||
|
|
||||||
|
Members:
|
||||||
|
status (:tl:`UserStatus`, optional):
|
||||||
|
The user status if the update is about going online or offline.
|
||||||
|
|
||||||
|
You should check this attribute first before checking any
|
||||||
|
of the seen within properties, since they will all be `None`
|
||||||
|
if the status is not set.
|
||||||
|
|
||||||
|
action (:tl:`SendMessageAction`, optional):
|
||||||
|
The "typing" action if any the user is performing if any.
|
||||||
|
|
||||||
|
You should check this attribute first before checking any
|
||||||
|
of the typing properties, since they will all be `None`
|
||||||
|
if the action is not set.
|
||||||
|
|
||||||
Example
|
Example
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -48,262 +62,242 @@ class UserUpdate(EventBuilder):
|
||||||
if event.uploading:
|
if event.uploading:
|
||||||
await client.send_message(event.user_id, 'What are you sending?')
|
await client.send_message(event.user_id, 'What are you sending?')
|
||||||
"""
|
"""
|
||||||
|
def __init__(self, peer, *, status=None, chat_peer=None, typing=None):
|
||||||
|
_custom.chatgetter.ChatGetter.__init__(self, chat_peer or peer)
|
||||||
|
_custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer))
|
||||||
|
|
||||||
|
self.status = status
|
||||||
|
self.action = typing
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, update, others=None, self_id=None, *todo, **todo2):
|
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
|
||||||
if isinstance(update, _tl.UpdateUserStatus):
|
if isinstance(update, _tl.UpdateUserStatus):
|
||||||
return cls.Event(_tl.PeerUser(update.user_id),
|
return UserUpdateEvent(_tl.PeerUser(update.user_id),
|
||||||
status=update.status)
|
status=update.status)
|
||||||
elif isinstance(update, _tl.UpdateChannelUserTyping):
|
elif isinstance(update, _tl.UpdateChannelUserTyping):
|
||||||
return cls.Event(update.from_id,
|
return UserUpdateEvent(update.from_id,
|
||||||
chat_peer=_tl.PeerChannel(update.channel_id),
|
chat_peer=_tl.PeerChannel(update.channel_id),
|
||||||
typing=update.action)
|
typing=update.action)
|
||||||
elif isinstance(update, _tl.UpdateChatUserTyping):
|
elif isinstance(update, _tl.UpdateChatUserTyping):
|
||||||
return cls.Event(update.from_id,
|
return UserUpdateEvent(update.from_id,
|
||||||
chat_peer=_tl.PeerChat(update.chat_id),
|
chat_peer=_tl.PeerChat(update.chat_id),
|
||||||
typing=update.action)
|
typing=update.action)
|
||||||
elif isinstance(update, _tl.UpdateUserTyping):
|
elif isinstance(update, _tl.UpdateUserTyping):
|
||||||
return cls.Event(update.user_id,
|
return UserUpdateEvent(update.user_id,
|
||||||
typing=update.action)
|
typing=update.action)
|
||||||
|
|
||||||
class Event(EventCommon, _custom.sendergetter.SenderGetter):
|
def _set_client(self, client):
|
||||||
|
super()._set_client(client)
|
||||||
|
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user(self):
|
||||||
|
"""Alias for `sender <telethon.tl.custom.sendergetter.SenderGetter.sender>`."""
|
||||||
|
return self.sender
|
||||||
|
|
||||||
|
async def get_user(self):
|
||||||
|
"""Alias for `get_sender <telethon.tl.custom.sendergetter.SenderGetter.get_sender>`."""
|
||||||
|
return await self.get_sender()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def input_user(self):
|
||||||
|
"""Alias for `input_sender <telethon.tl.custom.sendergetter.SenderGetter.input_sender>`."""
|
||||||
|
return self.input_sender
|
||||||
|
|
||||||
|
async def get_input_user(self):
|
||||||
|
"""Alias for `get_input_sender <telethon.tl.custom.sendergetter.SenderGetter.get_input_sender>`."""
|
||||||
|
return await self.get_input_sender()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_id(self):
|
||||||
|
"""Alias for `sender_id <telethon.tl.custom.sendergetter.SenderGetter.sender_id>`."""
|
||||||
|
return self.sender_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
@_requires_action
|
||||||
|
def typing(self):
|
||||||
"""
|
"""
|
||||||
Represents the event of a user update
|
`True` if the action is typing a message.
|
||||||
such as gone online, started typing, etc.
|
|
||||||
|
|
||||||
Members:
|
|
||||||
status (:tl:`UserStatus`, optional):
|
|
||||||
The user status if the update is about going online or offline.
|
|
||||||
|
|
||||||
You should check this attribute first before checking any
|
|
||||||
of the seen within properties, since they will all be `None`
|
|
||||||
if the status is not set.
|
|
||||||
|
|
||||||
action (:tl:`SendMessageAction`, optional):
|
|
||||||
The "typing" action if any the user is performing if any.
|
|
||||||
|
|
||||||
You should check this attribute first before checking any
|
|
||||||
of the typing properties, since they will all be `None`
|
|
||||||
if the action is not set.
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, peer, *, status=None, chat_peer=None, typing=None):
|
return isinstance(self.action, _tl.SendMessageTypingAction)
|
||||||
super().__init__(chat_peer or peer)
|
|
||||||
_custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer))
|
|
||||||
|
|
||||||
self.status = status
|
@property
|
||||||
self.action = typing
|
@_requires_action
|
||||||
|
def uploading(self):
|
||||||
|
"""
|
||||||
|
`True` if the action is uploading something.
|
||||||
|
"""
|
||||||
|
return isinstance(self.action, (
|
||||||
|
_tl.SendMessageChooseContactAction,
|
||||||
|
_tl.SendMessageChooseStickerAction,
|
||||||
|
_tl.SendMessageUploadAudioAction,
|
||||||
|
_tl.SendMessageUploadDocumentAction,
|
||||||
|
_tl.SendMessageUploadPhotoAction,
|
||||||
|
_tl.SendMessageUploadRoundAction,
|
||||||
|
_tl.SendMessageUploadVideoAction
|
||||||
|
))
|
||||||
|
|
||||||
def _set_client(self, client):
|
@property
|
||||||
super()._set_client(client)
|
@_requires_action
|
||||||
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
|
def recording(self):
|
||||||
|
"""
|
||||||
|
`True` if the action is recording something.
|
||||||
|
"""
|
||||||
|
return isinstance(self.action, (
|
||||||
|
_tl.SendMessageRecordAudioAction,
|
||||||
|
_tl.SendMessageRecordRoundAction,
|
||||||
|
_tl.SendMessageRecordVideoAction
|
||||||
|
))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user(self):
|
@_requires_action
|
||||||
"""Alias for `sender <telethon.tl.custom.sendergetter.SenderGetter.sender>`."""
|
def playing(self):
|
||||||
return self.sender
|
"""
|
||||||
|
`True` if the action is playing a game.
|
||||||
|
"""
|
||||||
|
return isinstance(self.action, _tl.SendMessageGamePlayAction)
|
||||||
|
|
||||||
async def get_user(self):
|
@property
|
||||||
"""Alias for `get_sender <telethon.tl.custom.sendergetter.SenderGetter.get_sender>`."""
|
@_requires_action
|
||||||
return await self.get_sender()
|
def cancel(self):
|
||||||
|
"""
|
||||||
|
`True` if the action was cancelling other actions.
|
||||||
|
"""
|
||||||
|
return isinstance(self.action, _tl.SendMessageCancelAction)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def input_user(self):
|
@_requires_action
|
||||||
"""Alias for `input_sender <telethon.tl.custom.sendergetter.SenderGetter.input_sender>`."""
|
def geo(self):
|
||||||
return self.input_sender
|
"""
|
||||||
|
`True` if what's being uploaded is a geo.
|
||||||
|
"""
|
||||||
|
return isinstance(self.action, _tl.SendMessageGeoLocationAction)
|
||||||
|
|
||||||
async def get_input_user(self):
|
@property
|
||||||
"""Alias for `get_input_sender <telethon.tl.custom.sendergetter.SenderGetter.get_input_sender>`."""
|
@_requires_action
|
||||||
return await self.get_input_sender()
|
def audio(self):
|
||||||
|
"""
|
||||||
|
`True` if what's being recorded/uploaded is an audio.
|
||||||
|
"""
|
||||||
|
return isinstance(self.action, (
|
||||||
|
_tl.SendMessageRecordAudioAction,
|
||||||
|
_tl.SendMessageUploadAudioAction
|
||||||
|
))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user_id(self):
|
@_requires_action
|
||||||
"""Alias for `sender_id <telethon.tl.custom.sendergetter.SenderGetter.sender_id>`."""
|
def round(self):
|
||||||
return self.sender_id
|
"""
|
||||||
|
`True` if what's being recorded/uploaded is a round video.
|
||||||
|
"""
|
||||||
|
return isinstance(self.action, (
|
||||||
|
_tl.SendMessageRecordRoundAction,
|
||||||
|
_tl.SendMessageUploadRoundAction
|
||||||
|
))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@_requires_action
|
@_requires_action
|
||||||
def typing(self):
|
def video(self):
|
||||||
"""
|
"""
|
||||||
`True` if the action is typing a message.
|
`True` if what's being recorded/uploaded is an video.
|
||||||
"""
|
"""
|
||||||
return isinstance(self.action, _tl.SendMessageTypingAction)
|
return isinstance(self.action, (
|
||||||
|
_tl.SendMessageRecordVideoAction,
|
||||||
|
_tl.SendMessageUploadVideoAction
|
||||||
|
))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@_requires_action
|
@_requires_action
|
||||||
def uploading(self):
|
def contact(self):
|
||||||
"""
|
"""
|
||||||
`True` if the action is uploading something.
|
`True` if what's being uploaded (selected) is a contact.
|
||||||
"""
|
"""
|
||||||
return isinstance(self.action, (
|
return isinstance(self.action, _tl.SendMessageChooseContactAction)
|
||||||
_tl.SendMessageChooseContactAction,
|
|
||||||
_tl.SendMessageChooseStickerAction,
|
|
||||||
_tl.SendMessageUploadAudioAction,
|
|
||||||
_tl.SendMessageUploadDocumentAction,
|
|
||||||
_tl.SendMessageUploadPhotoAction,
|
|
||||||
_tl.SendMessageUploadRoundAction,
|
|
||||||
_tl.SendMessageUploadVideoAction
|
|
||||||
))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@_requires_action
|
@_requires_action
|
||||||
def recording(self):
|
def document(self):
|
||||||
"""
|
"""
|
||||||
`True` if the action is recording something.
|
`True` if what's being uploaded is document.
|
||||||
"""
|
"""
|
||||||
return isinstance(self.action, (
|
return isinstance(self.action, _tl.SendMessageUploadDocumentAction)
|
||||||
_tl.SendMessageRecordAudioAction,
|
|
||||||
_tl.SendMessageRecordRoundAction,
|
|
||||||
_tl.SendMessageRecordVideoAction
|
|
||||||
))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@_requires_action
|
@_requires_action
|
||||||
def playing(self):
|
def sticker(self):
|
||||||
"""
|
"""
|
||||||
`True` if the action is playing a game.
|
`True` if what's being uploaded is a sticker.
|
||||||
"""
|
"""
|
||||||
return isinstance(self.action, _tl.SendMessageGamePlayAction)
|
return isinstance(self.action, _tl.SendMessageChooseStickerAction)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@_requires_action
|
@_requires_action
|
||||||
def cancel(self):
|
def photo(self):
|
||||||
"""
|
"""
|
||||||
`True` if the action was cancelling other actions.
|
`True` if what's being uploaded is a photo.
|
||||||
"""
|
"""
|
||||||
return isinstance(self.action, _tl.SendMessageCancelAction)
|
return isinstance(self.action, _tl.SendMessageUploadPhotoAction)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@_requires_action
|
@_requires_action
|
||||||
def geo(self):
|
def last_seen(self):
|
||||||
"""
|
"""
|
||||||
`True` if what's being uploaded is a geo.
|
Exact `datetime.datetime` when the user was last seen if known.
|
||||||
"""
|
"""
|
||||||
return isinstance(self.action, _tl.SendMessageGeoLocationAction)
|
if isinstance(self.status, _tl.UserStatusOffline):
|
||||||
|
return self.status.was_online
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@_requires_action
|
@_requires_status
|
||||||
def audio(self):
|
def until(self):
|
||||||
"""
|
"""
|
||||||
`True` if what's being recorded/uploaded is an audio.
|
The `datetime.datetime` until when the user should appear online.
|
||||||
"""
|
"""
|
||||||
return isinstance(self.action, (
|
if isinstance(self.status, _tl.UserStatusOnline):
|
||||||
_tl.SendMessageRecordAudioAction,
|
return self.status.expires
|
||||||
_tl.SendMessageUploadAudioAction
|
|
||||||
))
|
|
||||||
|
|
||||||
@property
|
def _last_seen_delta(self):
|
||||||
@_requires_action
|
if isinstance(self.status, _tl.UserStatusOffline):
|
||||||
def round(self):
|
return datetime.datetime.now(tz=datetime.timezone.utc) - self.status.was_online
|
||||||
"""
|
elif isinstance(self.status, _tl.UserStatusOnline):
|
||||||
`True` if what's being recorded/uploaded is a round video.
|
return datetime.timedelta(days=0)
|
||||||
"""
|
elif isinstance(self.status, _tl.UserStatusRecently):
|
||||||
return isinstance(self.action, (
|
return datetime.timedelta(days=1)
|
||||||
_tl.SendMessageRecordRoundAction,
|
elif isinstance(self.status, _tl.UserStatusLastWeek):
|
||||||
_tl.SendMessageUploadRoundAction
|
return datetime.timedelta(days=7)
|
||||||
))
|
elif isinstance(self.status, _tl.UserStatusLastMonth):
|
||||||
|
return datetime.timedelta(days=30)
|
||||||
|
else:
|
||||||
|
return datetime.timedelta(days=365)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@_requires_action
|
@_requires_status
|
||||||
def video(self):
|
def online(self):
|
||||||
"""
|
"""
|
||||||
`True` if what's being recorded/uploaded is an video.
|
`True` if the user is currently online,
|
||||||
"""
|
"""
|
||||||
return isinstance(self.action, (
|
return self._last_seen_delta() <= datetime.timedelta(days=0)
|
||||||
_tl.SendMessageRecordVideoAction,
|
|
||||||
_tl.SendMessageUploadVideoAction
|
|
||||||
))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@_requires_action
|
@_requires_status
|
||||||
def contact(self):
|
def recently(self):
|
||||||
"""
|
"""
|
||||||
`True` if what's being uploaded (selected) is a contact.
|
`True` if the user was seen within a day.
|
||||||
"""
|
"""
|
||||||
return isinstance(self.action, _tl.SendMessageChooseContactAction)
|
return self._last_seen_delta() <= datetime.timedelta(days=1)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@_requires_action
|
@_requires_status
|
||||||
def document(self):
|
def within_weeks(self):
|
||||||
"""
|
"""
|
||||||
`True` if what's being uploaded is document.
|
`True` if the user was seen within 7 days.
|
||||||
"""
|
"""
|
||||||
return isinstance(self.action, _tl.SendMessageUploadDocumentAction)
|
return self._last_seen_delta() <= datetime.timedelta(days=7)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@_requires_action
|
@_requires_status
|
||||||
def sticker(self):
|
def within_months(self):
|
||||||
"""
|
"""
|
||||||
`True` if what's being uploaded is a sticker.
|
`True` if the user was seen within 30 days.
|
||||||
"""
|
"""
|
||||||
return isinstance(self.action, _tl.SendMessageChooseStickerAction)
|
return self._last_seen_delta() <= datetime.timedelta(days=30)
|
||||||
|
|
||||||
@property
|
|
||||||
@_requires_action
|
|
||||||
def photo(self):
|
|
||||||
"""
|
|
||||||
`True` if what's being uploaded is a photo.
|
|
||||||
"""
|
|
||||||
return isinstance(self.action, _tl.SendMessageUploadPhotoAction)
|
|
||||||
|
|
||||||
@property
|
|
||||||
@_requires_action
|
|
||||||
def last_seen(self):
|
|
||||||
"""
|
|
||||||
Exact `datetime.datetime` when the user was last seen if known.
|
|
||||||
"""
|
|
||||||
if isinstance(self.status, _tl.UserStatusOffline):
|
|
||||||
return self.status.was_online
|
|
||||||
|
|
||||||
@property
|
|
||||||
@_requires_status
|
|
||||||
def until(self):
|
|
||||||
"""
|
|
||||||
The `datetime.datetime` until when the user should appear online.
|
|
||||||
"""
|
|
||||||
if isinstance(self.status, _tl.UserStatusOnline):
|
|
||||||
return self.status.expires
|
|
||||||
|
|
||||||
def _last_seen_delta(self):
|
|
||||||
if isinstance(self.status, _tl.UserStatusOffline):
|
|
||||||
return datetime.datetime.now(tz=datetime.timezone.utc) - self.status.was_online
|
|
||||||
elif isinstance(self.status, _tl.UserStatusOnline):
|
|
||||||
return datetime.timedelta(days=0)
|
|
||||||
elif isinstance(self.status, _tl.UserStatusRecently):
|
|
||||||
return datetime.timedelta(days=1)
|
|
||||||
elif isinstance(self.status, _tl.UserStatusLastWeek):
|
|
||||||
return datetime.timedelta(days=7)
|
|
||||||
elif isinstance(self.status, _tl.UserStatusLastMonth):
|
|
||||||
return datetime.timedelta(days=30)
|
|
||||||
else:
|
|
||||||
return datetime.timedelta(days=365)
|
|
||||||
|
|
||||||
@property
|
|
||||||
@_requires_status
|
|
||||||
def online(self):
|
|
||||||
"""
|
|
||||||
`True` if the user is currently online,
|
|
||||||
"""
|
|
||||||
return self._last_seen_delta() <= datetime.timedelta(days=0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
@_requires_status
|
|
||||||
def recently(self):
|
|
||||||
"""
|
|
||||||
`True` if the user was seen within a day.
|
|
||||||
"""
|
|
||||||
return self._last_seen_delta() <= datetime.timedelta(days=1)
|
|
||||||
|
|
||||||
@property
|
|
||||||
@_requires_status
|
|
||||||
def within_weeks(self):
|
|
||||||
"""
|
|
||||||
`True` if the user was seen within 7 days.
|
|
||||||
"""
|
|
||||||
return self._last_seen_delta() <= datetime.timedelta(days=7)
|
|
||||||
|
|
||||||
@property
|
|
||||||
@_requires_status
|
|
||||||
def within_months(self):
|
|
||||||
"""
|
|
||||||
`True` if the user was seen within 30 days.
|
|
||||||
"""
|
|
||||||
return self._last_seen_delta() <= datetime.timedelta(days=30)
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user