Begin unification of event builders and events

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import asyncio
import time import time
import weakref import weakref
from .common import EventBuilder, EventCommon, name_inner_event from .base import EventBuilder
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
from ..types import _custom from ..types import _custom
@ -64,13 +64,16 @@ class AlbumHack:
await asyncio.sleep(diff) await asyncio.sleep(diff)
@name_inner_event class Album(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
class Album(EventBuilder):
""" """
Occurs whenever you receive an album. This event only exists Occurs whenever you receive an album. This event only exists
to ease dealing with an unknown amount of messages that belong to ease dealing with an unknown amount of messages that belong
to the same album. to the same album.
Members:
messages (Sequence[`Message <telethon.tl._custom.message.Message>`]):
The list of messages belonging to the same album.
Example Example
.. code-block:: python .. code-block:: python
@ -91,12 +94,20 @@ class Album(EventBuilder):
await event.messages[4].reply('Cool!') await event.messages[4].reply('Cool!')
""" """
def __init__( def __init__(self, messages):
self, chats=None, *, blacklist_chats=False, func=None): message = messages[0]
super().__init__(chats, blacklist_chats=blacklist_chats, func=func) if not message.out and isinstance(message.peer_id, _tl.PeerUser):
# Incoming message (e.g. from a bot) has peer_id=us, and
# from_id=bot (the actual "chat" from a user's perspective).
chat_peer = message.from_id
else:
chat_peer = message.peer_id
@classmethod _custom.chatgetter.ChatGetter.__init__(self, chat_peer=chat_peer, broadcast=bool(message.post))
def build(cls, update, others=None, self_id=None, *todo, **todo2): _custom.sendergetter.SenderGetter.__init__(self, message.sender_id)
self.messages = messages
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if not others: if not others:
return # We only care about albums which come inside the same Updates return # We only care about albums which come inside the same Updates
@ -135,34 +146,6 @@ class Album(EventBuilder):
and u.message.grouped_id == group) and u.message.grouped_id == group)
]) ])
def filter(self, event):
# Albums with less than two messages require a few hacks to work.
if len(event.messages) > 1:
return super().filter(event)
class Event(EventCommon, _custom.sendergetter.SenderGetter):
"""
Represents the event of a new album.
Members:
messages (Sequence[`Message <telethon.tl._custom.message.Message>`]):
The list of messages belonging to the same album.
"""
def __init__(self, messages):
message = messages[0]
if not message.out and isinstance(message.peer_id, _tl.PeerUser):
# Incoming message (e.g. from a bot) has peer_id=us, and
# from_id=bot (the actual "chat" from a user's perspective).
chat_peer = message.from_id
else:
chat_peer = message.peer_id
super().__init__(chat_peer=chat_peer,
msg_id=message.id, broadcast=bool(message.post))
_custom.sendergetter.SenderGetter.__init__(self, message.sender_id)
self.messages = messages
def _set_client(self, client): def _set_client(self, client):
super()._set_client(client) super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)

View File

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

View File

@ -3,7 +3,7 @@ import struct
import asyncio import asyncio
import functools import functools
from .common import EventBuilder, EventCommon, name_inner_event from .base import EventBuilder
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
from ..types import _custom from ..types import _custom
@ -23,8 +23,7 @@ def auto_answer(func):
return wrapped return wrapped
@name_inner_event class CallbackQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
class CallbackQuery(EventBuilder):
""" """
Occurs whenever you sign in as a bot and a user Occurs whenever you sign in as a bot and a user
clicks one of the inline buttons on your messages. clicks one of the inline buttons on your messages.
@ -34,18 +33,17 @@ class CallbackQuery(EventBuilder):
message. The `chats` parameter also supports checking against the message. The `chats` parameter also supports checking against the
`chat_instance` which should be used for inline callbacks. `chat_instance` which should be used for inline callbacks.
Args: Members:
data (`bytes`, `str`, `callable`, optional): query (:tl:`UpdateBotCallbackQuery`):
If set, the inline button payload data must match this data. The original :tl:`UpdateBotCallbackQuery`.
A UTF-8 string can also be given, a regex or a callable. For
instance, to check against ``'data_1'`` and ``'data_2'`` you
can use ``re.compile(b'data_')``.
pattern (`bytes`, `str`, `callable`, `Pattern`, optional): data_match (`obj`, optional):
If set, only buttons with payload matching this pattern will be handled. The object returned by the ``data=`` parameter
You can specify a regex-like string which will be matched when creating the event builder, if any. Similar
against the payload data, a callable function that returns `True` to ``pattern_match`` for the new message event.
if a the payload data is acceptable, or a compiled regex pattern.
pattern_match (`obj`, optional):
Alias for ``data_match``.
Example Example
.. code-block:: python .. code-block:: python
@ -71,39 +69,17 @@ class CallbackQuery(EventBuilder):
Button.inline('Nope', b'no') Button.inline('Nope', b'no')
]) ])
""" """
def __init__( def __init__(self, query, peer, msg_id):
self, chats=None, *, blacklist_chats=False, func=None, data=None, pattern=None): _custom.chatgetter.ChatGetter.__init__(self, peer)
super().__init__(chats, blacklist_chats=blacklist_chats, func=func) _custom.sendergetter.SenderGetter.__init__(self, query.user_id)
self.query = query
if data and pattern: self.data_match = None
raise ValueError("Only pass either data or pattern not both.") self.pattern_match = None
self._message = None
if isinstance(data, str): self._answered = False
data = data.encode('utf-8')
if isinstance(pattern, str):
pattern = pattern.encode('utf-8')
match = data if data else pattern
if isinstance(match, bytes):
self.match = data if data else re.compile(pattern).match
elif not match or callable(match):
self.match = match
elif hasattr(match, 'match') and callable(match.match):
if not isinstance(getattr(match, 'pattern', b''), bytes):
match = re.compile(match.pattern.encode('utf-8'),
match.flags & (~re.UNICODE))
self.match = match.match
else:
raise TypeError('Invalid data or pattern type given')
self._no_check = all(x is None for x in (
self.chats, self.func, self.match,
))
@classmethod @classmethod
def build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, _tl.UpdateBotCallbackQuery): if isinstance(update, _tl.UpdateBotCallbackQuery):
return cls.Event(update, update.peer, update.msg_id) return cls.Event(update, update.peer, update.msg_id)
elif isinstance(update, _tl.UpdateInlineBotCallbackQuery): elif isinstance(update, _tl.UpdateInlineBotCallbackQuery):
@ -113,57 +89,6 @@ 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):
# We can't call super().filter(...) because it ignores chat_instance
if self._no_check:
return event
if self.chats is not None:
inside = event.query.chat_instance in self.chats
if event.chat_id:
inside |= event.chat_id in self.chats
if inside == self.blacklist_chats:
return
if self.match:
if callable(self.match):
event.data_match = event.pattern_match = self.match(event.query.data)
if not event.data_match:
return
elif event.query.data != self.match:
return
if self.func:
# Return the result of func directly as it may need to be awaited
return self.func(event)
return True
class Event(EventCommon, _custom.sendergetter.SenderGetter):
"""
Represents the event of a new callback query.
Members:
query (:tl:`UpdateBotCallbackQuery`):
The original :tl:`UpdateBotCallbackQuery`.
data_match (`obj`, optional):
The object returned by the ``data=`` parameter
when creating the event builder, if any. Similar
to ``pattern_match`` for the new message event.
pattern_match (`obj`, optional):
Alias for ``data_match``.
"""
def __init__(self, query, peer, msg_id):
super().__init__(peer, msg_id=msg_id)
_custom.sendergetter.SenderGetter.__init__(self, query.user_id)
self.query = query
self.data_match = None
self.pattern_match = None
self._message = None
self._answered = False
def _set_client(self, client): def _set_client(self, client):
super()._set_client(client) super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)

View File

@ -1,10 +1,9 @@
from .common import EventBuilder, EventCommon, name_inner_event from .base import EventBuilder
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
from ..types import _custom from ..types import _custom
@name_inner_event
class ChatAction(EventBuilder): class ChatAction(EventBuilder):
""" """
Occurs on certain chat actions: Occurs on certain chat actions:
@ -20,6 +19,47 @@ class ChatAction(EventBuilder):
Note that "chat" refers to "small group, megagroup and broadcast Note that "chat" refers to "small group, megagroup and broadcast
channel", whereas "group" refers to "small group and megagroup" only. channel", whereas "group" refers to "small group and megagroup" only.
Members:
action_message (`MessageAction <https://tl.telethon.dev/types/message_action.html>`_):
The message invoked by this Chat Action.
new_pin (`bool`):
`True` if there is a new pin.
new_photo (`bool`):
`True` if there's a new chat photo (or it was removed).
photo (:tl:`Photo`, optional):
The new photo (or `None` if it was removed).
user_added (`bool`):
`True` if the user was added by some other.
user_joined (`bool`):
`True` if the user joined on their own.
user_left (`bool`):
`True` if the user left on their own.
user_kicked (`bool`):
`True` if the user was kicked by some other.
user_approved (`bool`):
`True` if the user's join request was approved.
along with `user_joined` will be also True.
created (`bool`, optional):
`True` if this chat was just created.
new_title (`str`, optional):
The new title string for the chat, if applicable.
new_score (`str`, optional):
The new score string for the game, if applicable.
unpin (`bool`):
`True` if the existing pin gets unpinned.
Example Example
.. code-block:: python .. code-block:: python
@ -32,8 +72,64 @@ class ChatAction(EventBuilder):
await event.reply('Welcome to the group!') await event.reply('Welcome to the group!')
""" """
def __init__(self, where, new_photo=None,
added_by=None, kicked_by=None, created=None, from_approval=None,
users=None, new_title=None, pin_ids=None, pin=None, new_score=None):
if isinstance(where, _tl.MessageService):
self.action_message = where
where = where.peer_id
else:
self.action_message = None
# TODO needs some testing (can there be more than one id, and do they follow pin order?)
# same in get_pinned_message
super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None)
self.new_pin = pin_ids is not None
self._pin_ids = pin_ids
self._pinned_messages = None
self.new_photo = new_photo is not None
self.photo = \
new_photo if isinstance(new_photo, _tl.Photo) else None
self._added_by = None
self._kicked_by = None
self.user_added = self.user_joined = self.user_left = \
self.user_kicked = self.unpin = False
if added_by is True or from_approval is True:
self.user_joined = True
elif added_by:
self.user_added = True
self._added_by = added_by
self.user_approved = from_approval
# If `from_id` was not present (it's `True`) or the affected
# user was "kicked by itself", then it left. Else it was kicked.
if kicked_by is True or (users is not None and kicked_by == users):
self.user_left = True
elif kicked_by:
self.user_kicked = True
self._kicked_by = kicked_by
self.created = bool(created)
if isinstance(users, list):
self._user_ids = [utils.get_peer_id(u) for u in users]
elif users:
self._user_ids = [utils.get_peer_id(users)]
else:
self._user_ids = []
self._users = None
self._input_users = None
self.new_title = new_title
self.new_score = new_score
self.unpin = not pin
@classmethod @classmethod
def build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, update, others=None, self_id=None, *todo, **todo2):
# Rely on specific pin updates for unpins, but otherwise ignore them # Rely on specific pin updates for unpins, but otherwise ignore them
# for new pins (we'd rather handle the new service message with pin, # for new pins (we'd rather handle the new service message with pin,
# so that we can act on that message'). # so that we can act on that message').
@ -114,108 +210,6 @@ class ChatAction(EventBuilder):
return cls.Event(msg, return cls.Event(msg,
new_score=action.score) new_score=action.score)
class Event(EventCommon):
"""
Represents the event of a new chat action.
Members:
action_message (`MessageAction <https://tl.telethon.dev/types/message_action.html>`_):
The message invoked by this Chat Action.
new_pin (`bool`):
`True` if there is a new pin.
new_photo (`bool`):
`True` if there's a new chat photo (or it was removed).
photo (:tl:`Photo`, optional):
The new photo (or `None` if it was removed).
user_added (`bool`):
`True` if the user was added by some other.
user_joined (`bool`):
`True` if the user joined on their own.
user_left (`bool`):
`True` if the user left on their own.
user_kicked (`bool`):
`True` if the user was kicked by some other.
user_approved (`bool`):
`True` if the user's join request was approved.
along with `user_joined` will be also True.
created (`bool`, optional):
`True` if this chat was just created.
new_title (`str`, optional):
The new title string for the chat, if applicable.
new_score (`str`, optional):
The new score string for the game, if applicable.
unpin (`bool`):
`True` if the existing pin gets unpinned.
"""
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
async def respond(self, *args, **kwargs): async def respond(self, *args, **kwargs):
""" """
Responds to the chat action message (not as a reply). Shorthand for Responds to the chat action message (not as a reply). Shorthand for

View File

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

View File

@ -3,34 +3,27 @@ import re
import asyncio import asyncio
from .common import EventBuilder, EventCommon, name_inner_event from .base import EventBuilder
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
from ..types import _custom from ..types import _custom
@name_inner_event class InlineQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
class InlineQuery(EventBuilder):
""" """
Occurs whenever you sign in as a bot and a user Occurs whenever you sign in as a bot and a user
sends an inline query such as ``@bot query``. sends an inline query such as ``@bot query``.
Args: Members:
users (`entity`, optional): query (:tl:`UpdateBotInlineQuery`):
May be one or more entities (username/peer/etc.), preferably IDs. The original :tl:`UpdateBotInlineQuery`.
By default, only inline queries from these users will be handled.
blacklist_users (`bool`, optional): Make sure to access the `text` property of the query if
Whether to treat the users as a blacklist instead of you want the text rather than the actual query object.
as a whitelist (default). This means that every chat
will be handled *except* those specified in ``users``
which will be ignored if ``blacklist_users=True``.
pattern (`str`, `callable`, `Pattern`, optional): pattern_match (`obj`, optional):
If set, only queries matching this pattern will be handled. The resulting object from calling the passed ``pattern``
You can specify a regex-like string which will be matched function, which is ``re.compile(...).match`` by default.
against the message, a callable function that returns `True`
if a message is acceptable, or a compiled regex pattern.
Example Example
.. code-block:: python .. code-block:: python
@ -47,55 +40,18 @@ class InlineQuery(EventBuilder):
builder.article('lowercase', text=event.text.lower()), builder.article('lowercase', text=event.text.lower()),
]) ])
""" """
def __init__(
self, users=None, *, blacklist_users=False, func=None, pattern=None):
super().__init__(users, blacklist_chats=blacklist_users, func=func)
if isinstance(pattern, str):
self.pattern = re.compile(pattern).match
elif not pattern or callable(pattern):
self.pattern = pattern
elif hasattr(pattern, 'match') and callable(pattern.match):
self.pattern = pattern.match
else:
raise TypeError('Invalid pattern type given')
@classmethod
def build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, _tl.UpdateBotInlineQuery):
return cls.Event(update)
def filter(self, event):
if self.pattern:
match = self.pattern(event.text)
if not match:
return
event.pattern_match = match
return super().filter(event)
class Event(EventCommon, _custom.sendergetter.SenderGetter):
"""
Represents the event of a new callback query.
Members:
query (:tl:`UpdateBotInlineQuery`):
The original :tl:`UpdateBotInlineQuery`.
Make sure to access the `text` property of the query if
you want the text rather than the actual query object.
pattern_match (`obj`, optional):
The resulting object from calling the passed ``pattern``
function, which is ``re.compile(...).match`` by default.
"""
def __init__(self, query): def __init__(self, query):
super().__init__(chat_peer=_tl.PeerUser(query.user_id)) _custom.chatgetter.ChatGetter.__init__(self, _tl.PeerUser(query.user_id))
_custom.sendergetter.SenderGetter.__init__(self, query.user_id) _custom.sendergetter.SenderGetter.__init__(self, query.user_id)
self.query = query self.query = query
self.pattern_match = None self.pattern_match = None
self._answered = False self._answered = False
@classmethod
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, _tl.UpdateBotInlineQuery):
return cls.Event(update)
def _set_client(self, client): def _set_client(self, client):
super()._set_client(client) super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
@ -136,6 +92,9 @@ class InlineQuery(EventBuilder):
""" """
Returns a new `InlineBuilder Returns a new `InlineBuilder
<telethon.tl.custom.inlinebuilder.InlineBuilder>` instance. <telethon.tl.custom.inlinebuilder.InlineBuilder>` instance.
See the documentation for `builder` to know what kind of answers
can be given.
""" """
return _custom.InlineBuilder(self._client) return _custom.InlineBuilder(self._client)
@ -146,9 +105,6 @@ class InlineQuery(EventBuilder):
""" """
Answers the inline query with the given results. Answers the inline query with the given results.
See the documentation for `builder` to know what kind of answers
can be given.
Args: Args:
results (`list`, optional): results (`list`, optional):
A list of :tl:`InputBotInlineResult` to use. A list of :tl:`InputBotInlineResult` to use.

View File

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

View File

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

View File

@ -1,18 +1,24 @@
from .common import EventBuilder, EventCommon, name_inner_event from .base import EventBuilder
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
@name_inner_event
class MessageRead(EventBuilder): class MessageRead(EventBuilder):
""" """
Occurs whenever one or more messages are read in a chat. Occurs whenever one or more messages are read in a chat.
Args: Members:
inbox (`bool`, optional): max_id (`int`):
If this argument is `True`, then when you read someone else's Up to which message ID has been read. Every message
messages the event will be fired. By default (`False`) only with an ID equal or lower to it have been read.
when messages you sent are read by someone else will fire it.
outbox (`bool`):
`True` if someone else has read your messages.
contents (`bool`):
`True` if what was read were the contents of a message.
This will be the case when e.g. you play a voice note.
It may only be set on ``inbox`` events.
Example Example
.. code-block:: python .. code-block:: python
@ -29,13 +35,17 @@ class MessageRead(EventBuilder):
# Log when you read message in a chat (from your "inbox") # Log when you read message in a chat (from your "inbox")
print('You have read messages until', event.max_id) print('You have read messages until', event.max_id)
""" """
def __init__( def __init__(self, peer=None, max_id=None, out=False, contents=False,
self, chats=None, *, blacklist_chats=False, func=None, inbox=False): message_ids=None):
super().__init__(chats, blacklist_chats=blacklist_chats, func=func) self.outbox = out
self.inbox = inbox self.contents = contents
self._message_ids = message_ids or []
self._messages = None
self.max_id = max_id or max(message_ids or [], default=None)
super().__init__(peer, self.max_id)
@classmethod @classmethod
def build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, _tl.UpdateReadHistoryInbox): if isinstance(update, _tl.UpdateReadHistoryInbox):
return cls.Event(update.peer, update.max_id, False) return cls.Event(update.peer, update.max_id, False)
elif isinstance(update, _tl.UpdateReadHistoryOutbox): elif isinstance(update, _tl.UpdateReadHistoryOutbox):
@ -54,38 +64,6 @@ class MessageRead(EventBuilder):
message_ids=update.messages, message_ids=update.messages,
contents=True) contents=True)
def filter(self, event):
if self.inbox == event.outbox:
return
return super().filter(event)
class Event(EventCommon):
"""
Represents the event of one or more messages being read.
Members:
max_id (`int`):
Up to which message ID has been read. Every message
with an ID equal or lower to it have been read.
outbox (`bool`):
`True` if someone else has read your messages.
contents (`bool`):
`True` if what was read were the contents of a message.
This will be the case when e.g. you play a voice note.
It may only be set on ``inbox`` events.
"""
def __init__(self, peer=None, max_id=None, out=False, contents=False,
message_ids=None):
self.outbox = out
self.contents = contents
self._message_ids = message_ids or []
self._messages = None
self.max_id = max_id or max(message_ids or [], default=None)
super().__init__(peer, self.max_id)
@property @property
def inbox(self): def inbox(self):
""" """

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import datetime import datetime
import functools import functools
from .common import EventBuilder, EventCommon, name_inner_event from .base import EventBuilder
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
from ..types import _custom from ..types import _custom
@ -32,44 +32,10 @@ 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.
Example
.. code-block:: python
from telethon import events
@client.on(events.UserUpdate)
async def handler(event):
# If someone is uploading, say something
if event.uploading:
await client.send_message(event.user_id, 'What are you sending?')
"""
@classmethod
def build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, _tl.UpdateUserStatus):
return cls.Event(_tl.PeerUser(update.user_id),
status=update.status)
elif isinstance(update, _tl.UpdateChannelUserTyping):
return cls.Event(update.from_id,
chat_peer=_tl.PeerChannel(update.channel_id),
typing=update.action)
elif isinstance(update, _tl.UpdateChatUserTyping):
return cls.Event(update.from_id,
chat_peer=_tl.PeerChat(update.chat_id),
typing=update.action)
elif isinstance(update, _tl.UpdateUserTyping):
return cls.Event(update.user_id,
typing=update.action)
class Event(EventCommon, _custom.sendergetter.SenderGetter):
"""
Represents the event of a user update
such as gone online, started typing, etc.
Members: Members:
status (:tl:`UserStatus`, optional): status (:tl:`UserStatus`, optional):
The user status if the update is about going online or offline. The user status if the update is about going online or offline.
@ -84,14 +50,42 @@ class UserUpdate(EventBuilder):
You should check this attribute first before checking any You should check this attribute first before checking any
of the typing properties, since they will all be `None` of the typing properties, since they will all be `None`
if the action is not set. if the action is not set.
Example
.. code-block:: python
from telethon import events
@client.on(events.UserUpdate)
async def handler(event):
# If someone is uploading, say something
if event.uploading:
await client.send_message(event.user_id, 'What are you sending?')
""" """
def __init__(self, peer, *, status=None, chat_peer=None, typing=None): def __init__(self, peer, *, status=None, chat_peer=None, typing=None):
super().__init__(chat_peer or peer) _custom.chatgetter.ChatGetter.__init__(self, chat_peer or peer)
_custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer)) _custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer))
self.status = status self.status = status
self.action = typing self.action = typing
@classmethod
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, _tl.UpdateUserStatus):
return UserUpdateEvent(_tl.PeerUser(update.user_id),
status=update.status)
elif isinstance(update, _tl.UpdateChannelUserTyping):
return UserUpdateEvent(update.from_id,
chat_peer=_tl.PeerChannel(update.channel_id),
typing=update.action)
elif isinstance(update, _tl.UpdateChatUserTyping):
return UserUpdateEvent(update.from_id,
chat_peer=_tl.PeerChat(update.chat_id),
typing=update.action)
elif isinstance(update, _tl.UpdateUserTyping):
return UserUpdateEvent(update.user_id,
typing=update.action)
def _set_client(self, client): def _set_client(self, client):
super()._set_client(client) super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)