1
0
mirror of https://gitlab.com/MoonTestUse1/AdministrationItDepartmens.git synced 2025-08-14 00:25:46 +02:00

Все подряд

This commit is contained in:
MoonTestUse1
2024-12-31 02:37:57 +06:00
parent 8e53bb6cb2
commit d5780b2eab
3258 changed files with 1087440 additions and 268 deletions

View File

@@ -0,0 +1,140 @@
from .raw import Raw
from .album import Album
from .chataction import ChatAction
from .messagedeleted import MessageDeleted
from .messageedited import MessageEdited
from .messageread import MessageRead
from .newmessage import NewMessage
from .userupdate import UserUpdate
from .callbackquery import CallbackQuery
from .inlinequery import InlineQuery
_HANDLERS_ATTRIBUTE = '__tl.handlers'
class StopPropagation(Exception):
"""
If this exception is raised in any of the handlers for a given event,
it will stop the execution of all other registered event handlers.
It can be seen as the ``StopIteration`` in a for loop but for events.
Example usage:
>>> from telethon import TelegramClient, events
>>> client = TelegramClient(...)
>>>
>>> @client.on(events.NewMessage)
... async def delete(event):
... await event.delete()
... # No other event handler will have a chance to handle this event
... raise StopPropagation
...
>>> @client.on(events.NewMessage)
... async def _(event):
... # Will never be reached, because it is the second handler
... pass
"""
# For some reason Sphinx wants the silly >>> or
# it will show warnings and look bad when generated.
pass
def register(event=None):
"""
Decorator method to *register* event handlers. This is the client-less
`add_event_handler()
<telethon.client.updates.UpdateMethods.add_event_handler>` variant.
Note that this method only registers callbacks as handlers,
and does not attach them to any client. This is useful for
external modules that don't have access to the client, but
still want to define themselves as a handler. Example:
>>> from telethon import events
>>> @events.register(events.NewMessage)
... async def handler(event):
... ...
...
>>> # (somewhere else)
...
>>> from telethon import TelegramClient
>>> client = TelegramClient(...)
>>> client.add_event_handler(handler)
Remember that you can use this as a non-decorator
through ``register(event)(callback)``.
Args:
event (`_EventBuilder` | `type`):
The event builder class or instance to be used,
for instance ``events.NewMessage``.
"""
if isinstance(event, type):
event = event()
elif not event:
event = Raw()
def decorator(callback):
handlers = getattr(callback, _HANDLERS_ATTRIBUTE, [])
handlers.append(event)
setattr(callback, _HANDLERS_ATTRIBUTE, handlers)
return callback
return decorator
def unregister(callback, event=None):
"""
Inverse operation of `register` (though not a decorator). Client-less
`remove_event_handler
<telethon.client.updates.UpdateMethods.remove_event_handler>`
variant. **Note that this won't remove handlers from the client**,
because it simply can't, so you would generally use this before
adding the handlers to the client.
This method is here for symmetry. You will rarely need to
unregister events, since you can simply just not add them
to any client.
If no event is given, all events for this callback are removed.
Returns how many callbacks were removed.
"""
found = 0
if event and not isinstance(event, type):
event = type(event)
handlers = getattr(callback, _HANDLERS_ATTRIBUTE, [])
handlers.append((event, callback))
i = len(handlers)
while i:
i -= 1
ev = handlers[i]
if not event or isinstance(ev, event):
del handlers[i]
found += 1
return found
def is_handler(callback):
"""
Returns `True` if the given callback is an
event handler (i.e. you used `register` on it).
"""
return hasattr(callback, _HANDLERS_ATTRIBUTE)
def list(callback):
"""
Returns a list containing the registered event
builders inside the specified callback handler.
"""
return getattr(callback, _HANDLERS_ATTRIBUTE, [])[:]
def _get_handlers(callback):
"""
Like ``list`` but returns `None` if the callback was never registered.
"""
return getattr(callback, _HANDLERS_ATTRIBUTE, None)

View File

@@ -0,0 +1,343 @@
import asyncio
import time
import weakref
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types
from ..tl.custom.sendergetter import SenderGetter
_IGNORE_MAX_SIZE = 100 # len()
_IGNORE_MAX_AGE = 5 # seconds
# IDs to ignore, and when they were added. If it grows too large, we will
# remove old entries. Although it should generally not be bigger than 10,
# it may be possible some updates are not processed and thus not removed.
_IGNORE_DICT = {}
_HACK_DELAY = 0.5
class AlbumHack:
"""
When receiving an album from a different data-center, they will come in
separate `Updates`, so we need to temporarily remember them for a while
and only after produce the event.
Of course events are not designed for this kind of wizardy, so this is
a dirty hack that gets the job done.
When cleaning up the code base we may want to figure out a better way
to do this, or just leave the album problem to the users; the update
handling code is bad enough as it is.
"""
def __init__(self, client, event):
# It's probably silly to use a weakref here because this object is
# very short-lived but might as well try to do "the right thing".
self._client = weakref.ref(client)
self._event = event # parent event
self._due = client.loop.time() + _HACK_DELAY
client.loop.create_task(self.deliver_event())
def extend(self, messages):
client = self._client()
if client: # weakref may be dead
self._event.messages.extend(messages)
self._due = client.loop.time() + _HACK_DELAY
async def deliver_event(self):
while True:
client = self._client()
if client is None:
return # weakref is dead, nothing to deliver
diff = self._due - client.loop.time()
if diff <= 0:
# We've hit our due time, deliver event. It won't respect
# sequential updates but fixing that would just worsen this.
await client._dispatch_event(self._event)
return
del client # Clear ref and sleep until our due time
await asyncio.sleep(diff)
@name_inner_event
class Album(EventBuilder):
"""
Occurs whenever you receive an album. This event only exists
to ease dealing with an unknown amount of messages that belong
to the same album.
Example
.. code-block:: python
from telethon import events
@client.on(events.Album)
async def handler(event):
# Counting how many photos or videos the album has
print('Got an album with', len(event), 'items')
# Forwarding the album as a whole to some chat
event.forward_to(chat)
# Printing the caption
print(event.text)
# Replying to the fifth item in the album
await event.messages[4].reply('Cool!')
"""
def __init__(
self, chats=None, *, blacklist_chats=False, func=None):
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
@classmethod
def build(cls, update, others=None, self_id=None):
# TODO normally we'd only check updates if they come with other updates
# but MessageBox is not designed for this so others will always be None.
# In essence we always rely on AlbumHack rather than returning early if not others.
others = [update]
if isinstance(update,
(types.UpdateNewMessage, types.UpdateNewChannelMessage)):
if not isinstance(update.message, types.Message):
return # We don't care about MessageService's here
group = update.message.grouped_id
if group is None:
return # It must be grouped
# Check whether we are supposed to skip this update, and
# if we do also remove it from the ignore list since we
# won't need to check against it again.
if _IGNORE_DICT.pop(id(update), None):
return
# Check if the ignore list is too big, and if it is clean it
# TODO time could technically go backwards; time is not monotonic
now = time.time()
if len(_IGNORE_DICT) > _IGNORE_MAX_SIZE:
for i in [i for i, t in _IGNORE_DICT.items() if now - t > _IGNORE_MAX_AGE]:
del _IGNORE_DICT[i]
# Add the other updates to the ignore list
for u in others:
if u is not update:
_IGNORE_DICT[id(u)] = now
# Figure out which updates share the same group and use those
return cls.Event([
u.message for u in others
if (isinstance(u, (types.UpdateNewMessage, types.UpdateNewChannelMessage))
and isinstance(u.message, types.Message)
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, 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]
super().__init__(chat_peer=message.peer_id,
msg_id=message.id, broadcast=bool(message.post))
SenderGetter.__init__(self, message.sender_id)
self.messages = messages
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._mb_entity_cache)
for msg in self.messages:
msg._finish_init(client, self._entities, None)
if len(self.messages) == 1:
# This will require hacks to be a proper album event
hack = client._albums.get(self.grouped_id)
if hack is None:
client._albums[self.grouped_id] = AlbumHack(client, self)
else:
hack.extend(self.messages)
@property
def grouped_id(self):
"""
The shared ``grouped_id`` between all the messages.
"""
return self.messages[0].grouped_id
@property
def text(self):
"""
The message text of the first photo with a caption,
formatted using the client's default parse mode.
"""
return next((m.text for m in self.messages if m.text), '')
@property
def raw_text(self):
"""
The raw message text of the first photo
with a caption, ignoring any formatting.
"""
return next((m.raw_text for m in self.messages if m.raw_text), '')
@property
def is_reply(self):
"""
`True` if the album is a reply to some other message.
Remember that you can access the ID of the message
this one is replying to through `reply_to_msg_id`,
and the `Message` object with `get_reply_message()`.
"""
# Each individual message in an album all reply to the same message
return self.messages[0].is_reply
@property
def forward(self):
"""
The `Forward <telethon.tl.custom.forward.Forward>`
information for the first message in the album if it was forwarded.
"""
# Each individual message in an album all reply to the same message
return self.messages[0].forward
# endregion Public Properties
# region Public Methods
async def get_reply_message(self):
"""
The `Message <telethon.tl.custom.message.Message>`
that this album is replying to, or `None`.
The result will be cached after its first use.
"""
return await self.messages[0].get_reply_message()
async def respond(self, *args, **kwargs):
"""
Responds to the album (not as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message`
with ``entity`` already set.
"""
return await self.messages[0].respond(*args, **kwargs)
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)
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 edit(self, *args, **kwargs):
"""
Edits the first caption or the message, or the first messages'
caption if no caption is set, iff it's outgoing. Shorthand for
`telethon.client.messages.MessageMethods.edit_message`
with both ``entity`` and ``message`` already set.
Returns `None` if the message was incoming,
or the edited `Message` otherwise.
.. note::
This is different from `client.edit_message
<telethon.client.messages.MessageMethods.edit_message>`
and **will respect** the previous state of the message.
For example, if the message didn't have a link preview,
the edit won't add one by default, and you should force
it by setting it to `True` if you want it.
This is generally the most desired and convenient behaviour,
and will work for link previews and message buttons.
"""
for msg in self.messages:
if msg.raw_text:
return await msg.edit(*args, **kwargs)
return await self.messages[0].edit(*args, **kwargs)
async def delete(self, *args, **kwargs):
"""
Deletes the entire album. You're responsible for checking whether
you have the permission to do so, or to except the error otherwise.
Shorthand for
`telethon.client.messages.MessageMethods.delete_messages` with
``entity`` and ``message_ids`` already set.
"""
if self._client:
return await self._client.delete_messages(
await self.get_input_chat(), self.messages,
*args, **kwargs
)
async def mark_read(self):
"""
Marks the entire album as read. Shorthand for
`client.send_read_acknowledge()
<telethon.client.messages.MessageMethods.send_read_acknowledge>`
with both ``entity`` and ``message`` already set.
"""
if self._client:
await self._client.send_read_acknowledge(
await self.get_input_chat(), max_id=self.messages[-1].id)
async def pin(self, *, notify=False):
"""
Pins the first photo in the album. Shorthand for
`telethon.client.messages.MessageMethods.pin_message`
with both ``entity`` and ``message`` already set.
"""
return await self.messages[0].pin(notify=notify)
def __len__(self):
"""
Return the amount of messages in the album.
Equivalent to ``len(self.messages)``.
"""
return len(self.messages)
def __iter__(self):
"""
Iterate over the messages in the album.
Equivalent to ``iter(self.messages)``.
"""
return iter(self.messages)
def __getitem__(self, n):
"""
Access the n'th message in the album.
Equivalent to ``event.messages[n]``.
"""
return self.messages[n]

View File

@@ -0,0 +1,346 @@
import re
import struct
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types, functions
from ..tl.custom.sendergetter import SenderGetter
@name_inner_event
class CallbackQuery(EventBuilder):
"""
Occurs whenever you sign in as a bot and a user
clicks one of the inline buttons on your messages.
Note that the `chats` parameter will **not** work with normal
IDs or peers if the clicked inline button comes from a "via bot"
message. The `chats` parameter also supports checking against the
`chat_instance` which should be used for inline callbacks.
Args:
data (`bytes`, `str`, `callable`, optional):
If set, the inline button payload data must match this data.
A UTF-8 string can also be given, a regex or a callable. For
instance, to check against ``'data_1'`` and ``'data_2'`` you
can use ``re.compile(b'data_')``.
pattern (`bytes`, `str`, `callable`, `Pattern`, optional):
If set, only buttons with payload matching this pattern will be handled.
You can specify a regex-like string which will be matched
against the payload data, a callable function that returns `True`
if a the payload data is acceptable, or a compiled regex pattern.
Example
.. code-block:: python
from telethon import events, Button
# Handle all callback queries and check data inside the handler
@client.on(events.CallbackQuery)
async def handler(event):
if event.data == b'yes':
await event.answer('Correct answer!')
# Handle only callback queries with data being b'no'
@client.on(events.CallbackQuery(data=b'no'))
async def handler(event):
# Pop-up message with alert
await event.answer('Wrong answer!', alert=True)
# Send a message with buttons users can click
async def main():
await client.send_message(user, 'Yes or no?', buttons=[
Button.inline('Yes!', b'yes'),
Button.inline('Nope', b'no')
])
"""
def __init__(
self, chats=None, *, blacklist_chats=False, func=None, data=None, pattern=None):
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
if data and pattern:
raise ValueError("Only pass either data or pattern not both.")
if isinstance(data, str):
data = data.encode('utf-8')
if isinstance(pattern, str):
pattern = pattern.encode('utf-8')
match = data if data else pattern
if isinstance(match, bytes):
self.match = data if data else re.compile(pattern).match
elif not match or callable(match):
self.match = match
elif hasattr(match, 'match') and callable(match.match):
if not isinstance(getattr(match, 'pattern', b''), bytes):
match = re.compile(match.pattern.encode('utf-8'),
match.flags & (~re.UNICODE))
self.match = match.match
else:
raise TypeError('Invalid data or pattern type given')
self._no_check = all(x is None for x in (
self.chats, self.func, self.match,
))
@classmethod
def build(cls, update, others=None, self_id=None):
if isinstance(update, types.UpdateBotCallbackQuery):
return cls.Event(update, update.peer, update.msg_id)
elif isinstance(update, types.UpdateInlineBotCallbackQuery):
# See https://github.com/LonamiWebs/Telethon/pull/1005
# The long message ID is actually just msg_id + peer_id
mid, pid = struct.unpack('<ii', struct.pack('<q', update.msg_id.id))
peer = types.PeerChannel(-pid) if pid < 0 else types.PeerUser(pid)
return cls.Event(update, peer, mid)
def filter(self, event):
# We can't call super().filter(...) because it ignores chat_instance
if self._no_check:
return event
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, 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)
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):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._mb_entity_cache)
@property
def id(self):
"""
Returns the query ID. The user clicking the inline
button is the one who generated this random ID.
"""
return self.query.query_id
@property
def message_id(self):
"""
Returns the message ID to which the clicked inline button belongs.
"""
return self._message_id
@property
def data(self):
"""
Returns the data payload from the original inline button.
"""
return self.query.data
@property
def chat_instance(self):
"""
Unique identifier for the chat where the callback occurred.
Useful for high scores in games.
"""
return self.query.chat_instance
async def get_message(self):
"""
Returns the message to which the clicked inline button belongs.
"""
if self._message is not None:
return self._message
try:
chat = await self.get_input_chat() if self.is_channel else None
self._message = await self._client.get_messages(
chat, ids=self._message_id)
except ValueError:
return
return self._message
async def _refetch_sender(self):
self._sender = self._entities.get(self.sender_id)
if not self._sender:
return
self._input_sender = utils.get_input_peer(self._chat)
if not getattr(self._input_sender, 'access_hash', True):
# getattr with True to handle the InputPeerSelf() case
try:
self._input_sender = self._client._mb_entity_cache.get(
utils.resolve_id(self._sender_id)[0])._as_input_peer()
except AttributeError:
m = await self.get_message()
if m:
self._sender = m._sender
self._input_sender = m._input_sender
async def answer(
self, message=None, cache_time=0, *, url=None, alert=False):
"""
Answers the callback query (and stops the loading circle).
Args:
message (`str`, optional):
The toast message to show feedback to the user.
cache_time (`int`, optional):
For how long this result should be cached on
the user's client. Defaults to 0 for no cache.
url (`str`, optional):
The URL to be opened in the user's client. Note that
the only valid URLs are those of games your bot has,
or alternatively a 't.me/your_bot?start=xyz' parameter.
alert (`bool`, optional):
Whether an alert (a pop-up dialog) should be used
instead of showing a toast. Defaults to `False`.
"""
if self._answered:
return
self._answered = True
return await self._client(
functions.messages.SetBotCallbackAnswerRequest(
query_id=self.query.query_id,
cache_time=cache_time,
alert=alert,
message=message,
url=url
)
)
@property
def via_inline(self):
"""
Whether this callback was generated from an inline button sent
via an inline query or not. If the bot sent the message itself
with buttons, and one of those is clicked, this will be `False`.
If a user sent the message coming from an inline query to the
bot, and one of those is clicked, this will be `True`.
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, types.UpdateInlineBotCallbackQuery)
async def respond(self, *args, **kwargs):
"""
Responds to the message (not as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
``entity`` already set.
This method also creates a task to `answer` the callback.
This method will likely fail if `via_inline` is `True`.
"""
self._client.loop.create_task(self.answer())
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
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 also creates a task to `answer` the callback.
This method will likely fail if `via_inline` is `True`.
"""
self._client.loop.create_task(self.answer())
kwargs['reply_to'] = self.query.msg_id
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
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` or :tl:`InputBotInlineMessageID64`.
Returns `True` if the edit was successful.
This method also creates a task to `answer` the callback.
.. note::
This method won't respect the previous message unlike
`Message.edit <telethon.tl.custom.message.Message.edit>`,
since the message object is normally not present.
"""
self._client.loop.create_task(self.answer())
if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):
return await self._client.edit_message(
self.query.msg_id, *args, **kwargs
)
else:
return await self._client.edit_message(
await self.get_input_chat(), self.query.msg_id,
*args, **kwargs
)
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 also creates a task to `answer` the callback.
This method will likely fail if `via_inline` is `True`.
"""
self._client.loop.create_task(self.answer())
if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):
raise TypeError('Inline messages cannot be deleted as there is no API request available to do so')
return await self._client.delete_messages(
await self.get_input_chat(), [self.query.msg_id],
*args, **kwargs
)

View File

@@ -0,0 +1,458 @@
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types
@name_inner_event
class ChatAction(EventBuilder):
"""
Occurs on certain chat actions:
* Whenever a new chat is created.
* Whenever a chat's title or photo is changed or removed.
* Whenever a new message is pinned.
* Whenever a user scores in a game.
* Whenever a user joins or is added to the group.
* Whenever a user is removed or leaves a group if it has
less than 50 members or the removed user was a bot.
Note that "chat" refers to "small group, megagroup and broadcast
channel", whereas "group" refers to "small group and megagroup" only.
Example
.. code-block:: python
from telethon import events
@client.on(events.ChatAction)
async def handler(event):
# Welcome every new user
if event.user_joined:
await event.reply('Welcome to the group!')
"""
@classmethod
def build(cls, update, others=None, self_id=None):
# Rely on specific pin updates for unpins, but otherwise ignore them
# for new pins (we'd rather handle the new service message with pin,
# so that we can act on that message').
if isinstance(update, types.UpdatePinnedChannelMessages) and not update.pinned:
return cls.Event(types.PeerChannel(update.channel_id),
pin_ids=update.messages,
pin=update.pinned)
elif isinstance(update, types.UpdatePinnedMessages) and not update.pinned:
return cls.Event(update.peer,
pin_ids=update.messages,
pin=update.pinned)
elif isinstance(update, types.UpdateChatParticipantAdd):
return cls.Event(types.PeerChat(update.chat_id),
added_by=update.inviter_id or True,
users=update.user_id)
elif isinstance(update, types.UpdateChatParticipantDelete):
return cls.Event(types.PeerChat(update.chat_id),
kicked_by=True,
users=update.user_id)
# UpdateChannel is sent if we leave a channel, and the update._entities
# set by _process_update would let us make some guesses. However it's
# better not to rely on this. Rely only in MessageActionChatDeleteUser.
elif (isinstance(update, (
types.UpdateNewMessage, types.UpdateNewChannelMessage))
and isinstance(update.message, types.MessageService)):
msg = update.message
action = update.message.action
if isinstance(action, types.MessageActionChatJoinedByLink):
return cls.Event(msg,
added_by=True,
users=msg.from_id)
elif isinstance(action, types.MessageActionChatAddUser):
# If a user adds itself, it means they joined via the public chat username
added_by = ([msg.sender_id] == action.users) or msg.from_id
return cls.Event(msg,
added_by=added_by,
users=action.users)
elif isinstance(action, types.MessageActionChatDeleteUser):
return cls.Event(msg,
kicked_by=utils.get_peer_id(msg.from_id) if msg.from_id else True,
users=action.user_id)
elif isinstance(action, types.MessageActionChatCreate):
return cls.Event(msg,
users=action.users,
created=True,
new_title=action.title)
elif isinstance(action, types.MessageActionChannelCreate):
return cls.Event(msg,
created=True,
users=msg.from_id,
new_title=action.title)
elif isinstance(action, types.MessageActionChatEditTitle):
return cls.Event(msg,
users=msg.from_id,
new_title=action.title)
elif isinstance(action, types.MessageActionChatEditPhoto):
return cls.Event(msg,
users=msg.from_id,
new_photo=action.photo)
elif isinstance(action, types.MessageActionChatDeletePhoto):
return cls.Event(msg,
users=msg.from_id,
new_photo=True)
elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to:
return cls.Event(msg,
pin_ids=[msg.reply_to_msg_id])
elif isinstance(action, types.MessageActionGameScore):
return cls.Event(msg,
new_score=action.score)
elif isinstance(update, types.UpdateChannelParticipant) \
and bool(update.new_participant) != bool(update.prev_participant):
# If members are hidden, bots will receive this update instead,
# as there won't be a service message. Promotions and demotions
# seem to have both new and prev participant, which are ignored
# by this event.
return cls.Event(types.PeerChannel(update.channel_id),
users=update.user_id,
added_by=update.actor_id if update.new_participant else None,
kicked_by=update.actor_id if update.prev_participant else None)
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.
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,
users=None, new_title=None, pin_ids=None, pin=None, new_score=None):
if isinstance(where, types.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, types.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:
self.user_joined = True
elif added_by:
self.user_added = True
self._added_by = added_by
# 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
def _set_client(self, client):
super()._set_client(client)
if self.action_message:
self.action_message._finish_init(client, self._entities, None)
async def respond(self, *args, **kwargs):
"""
Responds to the chat action message (not as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
``entity`` already set.
"""
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
async def reply(self, *args, **kwargs):
"""
Replies to the chat action message (as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
both ``entity`` and ``reply_to`` already set.
Has the same effect as `respond` if there is no message.
"""
if not self.action_message:
return await self.respond(*args, **kwargs)
kwargs['reply_to'] = self.action_message.id
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
async def delete(self, *args, **kwargs):
"""
Deletes the chat action message. You're responsible for checking
whether you have the permission to do so, or to except the error
otherwise. Shorthand for
`telethon.client.messages.MessageMethods.delete_messages` with
``entity`` and ``message_ids`` already set.
Does nothing if no message action triggered this event.
"""
if not self.action_message:
return
return await self._client.delete_messages(
await self.get_input_chat(), [self.action_message],
*args, **kwargs
)
async def get_pinned_message(self):
"""
If ``new_pin`` is `True`, this returns the `Message
<telethon.tl.custom.message.Message>` object that was pinned.
"""
if self._pinned_messages is None:
await self.get_pinned_messages()
if self._pinned_messages:
return self._pinned_messages[0]
async def get_pinned_messages(self):
"""
If ``new_pin`` is `True`, this returns a `list` of `Message
<telethon.tl.custom.message.Message>` objects that were pinned.
"""
if not self._pin_ids:
return self._pin_ids # either None or empty list
chat = await self.get_input_chat()
if chat:
self._pinned_messages = await self._client.get_messages(
self._input_chat, ids=self._pin_ids)
return self._pinned_messages
@property
def added_by(self):
"""
The user who added ``users``, if applicable (`None` otherwise).
"""
if self._added_by and not isinstance(self._added_by, types.User):
aby = self._entities.get(utils.get_peer_id(self._added_by))
if aby:
self._added_by = aby
return self._added_by
async def get_added_by(self):
"""
Returns `added_by` but will make an API call if necessary.
"""
if not self.added_by and self._added_by:
self._added_by = await self._client.get_entity(self._added_by)
return self._added_by
@property
def kicked_by(self):
"""
The user who kicked ``users``, if applicable (`None` otherwise).
"""
if self._kicked_by and not isinstance(self._kicked_by, types.User):
kby = self._entities.get(utils.get_peer_id(self._kicked_by))
if kby:
self._kicked_by = kby
return self._kicked_by
async def get_kicked_by(self):
"""
Returns `kicked_by` but will make an API call if necessary.
"""
if not self.kicked_by and self._kicked_by:
self._kicked_by = await self._client.get_entity(self._kicked_by)
return self._kicked_by
@property
def user(self):
"""
The first user that takes part in this action. For example, who joined.
Might be `None` if the information can't be retrieved or
there is no user taking part.
"""
if self.users:
return self._users[0]
async def get_user(self):
"""
Returns `user` but will make an API call if necessary.
"""
if self.users or await self.get_users():
return self._users[0]
@property
def input_user(self):
"""
Input version of the ``self.user`` property.
"""
if self.input_users:
return self._input_users[0]
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]
@property
def user_id(self):
"""
Returns the marked signed ID of the first user, if any.
"""
if self._user_ids:
return self._user_ids[0]
@property
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
are no users taking part.
"""
if not self._user_ids:
return []
if self._users is None:
self._users = [
self._entities[user_id]
for user_id in self._user_ids
if user_id in self._entities
]
return self._users
async def get_users(self):
"""
Returns `users` but will make an API call if necessary.
"""
if not self._user_ids:
return []
# Note: we access the property first so that it fills if needed
if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message:
await self.action_message._reload_message()
self._users = [
u for u in self.action_message.action_entities
if isinstance(u, (types.User, types.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:
# First 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
# If missing, try from the entity cache
try:
self._input_users.append(self._client._mb_entity_cache.get(
utils.resolve_id(user_id)[0])._as_input_peer())
continue
except AttributeError:
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, (types.User, types.UserEmpty))]
return self._input_users or []
@property
def user_ids(self):
"""
Returns the marked signed ID of the users, if any.
"""
if self._user_ids:
return self._user_ids[:]

View File

@@ -0,0 +1,186 @@
import abc
import asyncio
import warnings
from .. import utils
from ..tl import TLObject, types
from ..tl.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):
if chat < 0:
result.add(chat) # Explicitly marked IDs are negative
else:
result.update({ # Support all valid types of peers
utils.get_peer_id(types.PeerUser(chat)),
utils.get_peer_id(types.PeerChat(chat)),
utils.get_peer_id(types.PeerChannel(chat)),
})
elif isinstance(chat, 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, types.InputPeerSelf):
chat = await client.get_me(input_peer=True)
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=None, self_id=None):
"""
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.
"""
self._client = client
if self._chat_peer:
self._chat, self._input_chat = utils._get_entity_pair(
self.chat_id, self._entities, client._mb_entity_cache)
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 TLObject.pretty_format(self.to_dict())
def stringify(self):
return 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

@@ -0,0 +1,247 @@
import inspect
import re
import asyncio
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils, helpers
from ..tl import types, functions, custom
from ..tl.custom.sendergetter import SenderGetter
@name_inner_event
class InlineQuery(EventBuilder):
"""
Occurs whenever you sign in as a bot and a user
sends an inline query such as ``@bot query``.
Args:
users (`entity`, optional):
May be one or more entities (username/peer/etc.), preferably IDs.
By default, only inline queries from these users will be handled.
blacklist_users (`bool`, optional):
Whether to treat the users as a blacklist instead of
as a whitelist (default). This means that every chat
will be handled *except* those specified in ``users``
which will be ignored if ``blacklist_users=True``.
pattern (`str`, `callable`, `Pattern`, optional):
If set, only queries matching this pattern will be handled.
You can specify a regex-like string which will be matched
against the message, a callable function that returns `True`
if a message is acceptable, or a compiled regex pattern.
Example
.. code-block:: python
from telethon import events
@client.on(events.InlineQuery)
async def handler(event):
builder = event.builder
# Two options (convert user text to UPPERCASE or lowercase)
await event.answer([
builder.article('UPPERCASE', text=event.text.upper()),
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):
if isinstance(update, types.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, 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):
super().__init__(chat_peer=types.PeerUser(query.user_id))
SenderGetter.__init__(self, query.user_id)
self.query = query
self.pattern_match = None
self._answered = False
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._mb_entity_cache)
@property
def id(self):
"""
Returns the unique identifier for the query ID.
"""
return self.query.query_id
@property
def text(self):
"""
Returns the text the user used to make the inline query.
"""
return self.query.query
@property
def offset(self):
"""
The string the user's client used as an offset for the query.
This will either be empty or equal to offsets passed to `answer`.
"""
return self.query.offset
@property
def geo(self):
"""
If the user location is requested when using inline mode
and the user's device is able to send it, this will return
the :tl:`GeoPoint` with the position of the user.
"""
return self.query.geo
@property
def builder(self):
"""
Returns a new `InlineBuilder
<telethon.tl.custom.inlinebuilder.InlineBuilder>` instance.
"""
return custom.InlineBuilder(self._client)
async def answer(
self, results=None, cache_time=0, *,
gallery=False, next_offset=None, private=False,
switch_pm=None, switch_pm_param=''):
"""
Answers the inline query with the given results.
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
@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 = types.InlineBotSwitchPM(switch_pm, switch_pm_param)
return await self._client(
functions.messages.SetInlineBotResultsRequest(
query_id=self.query.query_id,
results=results,
cache_time=cache_time,
gallery=gallery,
next_offset=next_offset,
private=private,
switch_pm=switch_pm
)
)
@staticmethod
def _as_future(obj):
if inspect.isawaitable(obj):
return asyncio.ensure_future(obj)
f = helpers.get_running_loop().create_future()
f.set_result(obj)
return f

View File

@@ -0,0 +1,57 @@
from .common import EventBuilder, EventCommon, name_inner_event
from ..tl import types
@name_inner_event
class MessageDeleted(EventBuilder):
"""
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
was deleted.
.. important::
Telegram **does not** send information about *where* a message
was deleted if it occurs in private conversations with other users
or in small group chats, because message IDs are *unique* and you
can identify the chat with the message ID alone if you saved it
previously.
Telethon **does not** save information of where messages occur,
so it cannot know in which chat a message was deleted (this will
only work in channels, where the channel ID *is* present).
This means that the ``chats=`` parameter will not work reliably,
unless you intend on working with channels and super-groups only.
Example
.. code-block:: python
from telethon import events
@client.on(events.MessageDeleted)
async def handler(event):
# Log all deleted message IDs
for msg_id in event.deleted_ids:
print('Message', msg_id, 'was deleted in', event.chat_id)
"""
@classmethod
def build(cls, update, others=None, self_id=None):
if isinstance(update, types.UpdateDeleteMessages):
return cls.Event(
deleted_ids=update.messages,
peer=None
)
elif isinstance(update, types.UpdateDeleteChannelMessages):
return cls.Event(
deleted_ids=update.messages,
peer=types.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

@@ -0,0 +1,52 @@
from .common import name_inner_event
from .newmessage import NewMessage
from ..tl import types
@name_inner_event
class MessageEdited(NewMessage):
"""
Occurs whenever a message is edited. Just like `NewMessage
<telethon.events.newmessage.NewMessage>`, you should treat
this event as a `Message <telethon.tl.custom.message.Message>`.
.. warning::
On channels, `Message.out <telethon.tl.custom.message.Message>`
will be `True` if you sent the message originally, **not if
you edited it**! This can be dangerous if you run outgoing
commands on edits.
Some examples follow:
* You send a message "A", ``out is True``.
* You edit "A" to "B", ``out is True``.
* Someone else edits "B" to "C", ``out is True`` (**be careful!**).
* Someone sends "X", ``out is False``.
* Someone edits "X" to "Y", ``out is False``.
* You edit "Y" to "Z", ``out is False``.
Since there are useful cases where you need the right ``out``
value, the library cannot do anything automatically to help you.
Instead, consider using ``from_users='me'`` (it won't work in
broadcast channels at all since the sender is the channel and
not you).
Example
.. code-block:: python
from telethon import events
@client.on(events.MessageEdited)
async def handler(event):
# Log the date of new edits
print('Message', event.id, 'changed at', event.date)
"""
@classmethod
def build(cls, update, others=None, self_id=None):
if isinstance(update, (types.UpdateEditMessage,
types.UpdateEditChannelMessage)):
return cls.Event(update.message)
class Event(NewMessage.Event):
pass # Required if we want a different name for it

View File

@@ -0,0 +1,143 @@
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types
@name_inner_event
class MessageRead(EventBuilder):
"""
Occurs whenever one or more messages are read in a chat.
Args:
inbox (`bool`, optional):
If this argument is `True`, then when you read someone else's
messages the event will be fired. By default (`False`) only
when messages you sent are read by someone else will fire it.
Example
.. code-block:: python
from telethon import events
@client.on(events.MessageRead)
async def handler(event):
# Log when someone reads your messages
print('Someone has read all your messages until', event.max_id)
@client.on(events.MessageRead(inbox=True))
async def handler(event):
# Log when you read message in a chat (from your "inbox")
print('You have read messages until', event.max_id)
"""
def __init__(
self, chats=None, *, blacklist_chats=False, func=None, inbox=False):
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
self.inbox = inbox
@classmethod
def build(cls, update, others=None, self_id=None):
if isinstance(update, types.UpdateReadHistoryInbox):
return cls.Event(update.peer, update.max_id, False)
elif isinstance(update, types.UpdateReadHistoryOutbox):
return cls.Event(update.peer, update.max_id, True)
elif isinstance(update, types.UpdateReadChannelInbox):
return cls.Event(types.PeerChannel(update.channel_id),
update.max_id, False)
elif isinstance(update, types.UpdateReadChannelOutbox):
return cls.Event(types.PeerChannel(update.channel_id),
update.max_id, True)
elif isinstance(update, types.UpdateReadMessagesContents):
return cls.Event(message_ids=update.messages,
contents=True)
elif isinstance(update, types.UpdateChannelReadMessagesContents):
return cls.Event(types.PeerChannel(update.channel_id),
message_ids=update.messages,
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
def inbox(self):
"""
`True` if you have read someone else's messages.
"""
return not self.outbox
@property
def message_ids(self):
"""
The IDs of the messages **which contents'** were read.
Use :meth:`is_read` if you need to check whether a message
was read instead checking if it's in here.
"""
return self._message_ids
async def get_messages(self):
"""
Returns the list of `Message <telethon.tl.custom.message.Message>`
**which contents'** were read.
Use :meth:`is_read` if you need to check whether a message
was read instead checking if it's in here.
"""
if self._messages is None:
chat = await self.get_input_chat()
if not chat:
self._messages = []
else:
self._messages = await self._client.get_messages(
chat, ids=self._message_ids)
return self._messages
def is_read(self, message):
"""
Returns `True` if the given message (or its ID) has been read.
If a list-like argument is provided, this method will return a
list of booleans indicating which messages have been read.
"""
if utils.is_list_like(message):
return [(m if isinstance(m, int) else m.id) <= self.max_id
for m in message]
else:
return (message if isinstance(message, int)
else message.id) <= self.max_id
def __contains__(self, message):
"""`True` if the message(s) are read message."""
if utils.is_list_like(message):
return all(self.is_read(message))
else:
return self.is_read(message)

View File

@@ -0,0 +1,223 @@
import re
from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set
from .. import utils
from ..tl import types
@name_inner_event
class NewMessage(EventBuilder):
"""
Occurs whenever a new text message or a message with media arrives.
Args:
incoming (`bool`, optional):
If set to `True`, only **incoming** messages will be handled.
Mutually exclusive with ``outgoing`` (can only set one of either).
outgoing (`bool`, optional):
If set to `True`, only **outgoing** messages will be handled.
Mutually exclusive with ``incoming`` (can only set one of either).
from_users (`entity`, optional):
Unlike `chats`, this parameter filters the *senders* of the
message. That is, only messages *sent by these users* will be
handled. Use `chats` if you want private messages with this/these
users. `from_users` lets you filter by messages sent by *one or
more* users across the desired chats (doesn't need a list).
forwards (`bool`, optional):
Whether forwarded messages should be handled or not. By default,
both forwarded and normal messages are included. If it's `True`
*only* forwards will be handled. If it's `False` only messages
that are *not* forwards will be handled.
pattern (`str`, `callable`, `Pattern`, optional):
If set, only messages matching this pattern will be handled.
You can specify a regex-like string which will be matched
against the message, a callable function that returns `True`
if a message is acceptable, or a compiled regex pattern.
Example
.. code-block:: python
import asyncio
from telethon import events
@client.on(events.NewMessage(pattern='(?i)hello.+'))
async def handler(event):
# Respond whenever someone says "Hello" and something else
await event.reply('Hey!')
@client.on(events.NewMessage(outgoing=True, pattern='!ping'))
async def handler(event):
# Say "!pong" whenever you send "!ping", then delete both messages
m = await event.respond('!pong')
await asyncio.sleep(5)
await client.delete_messages(event.chat_id, [event.id, m.id])
"""
def __init__(self, chats=None, *, blacklist_chats=False, func=None,
incoming=None, outgoing=None,
from_users=None, forwards=None, pattern=None):
if incoming and outgoing:
incoming = outgoing = None # Same as no filter
elif incoming is not None and outgoing is None:
outgoing = not incoming
elif outgoing is not None and incoming is None:
incoming = not outgoing
elif all(x is not None and not x for x in (incoming, outgoing)):
raise ValueError("Don't create an event handler if you "
"don't want neither incoming nor outgoing!")
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
self.incoming = incoming
self.outgoing = outgoing
self.from_users = from_users
self.forwards = forwards
if isinstance(pattern, str):
self.pattern = re.compile(pattern).match
elif not pattern or callable(pattern):
self.pattern = pattern
elif hasattr(pattern, 'match') and callable(pattern.match):
self.pattern = pattern.match
else:
raise TypeError('Invalid pattern type given')
# Should we short-circuit? E.g. perform no check at all
self._no_check = all(x is None for x in (
self.chats, self.incoming, self.outgoing, self.pattern,
self.from_users, self.forwards, self.from_users, self.func
))
async def _resolve(self, client):
await super()._resolve(client)
self.from_users = await _into_id_set(client, self.from_users)
@classmethod
def build(cls, update, others=None, self_id=None):
if isinstance(update,
(types.UpdateNewMessage, types.UpdateNewChannelMessage)):
if not isinstance(update.message, types.Message):
return # We don't care about MessageService's here
event = cls.Event(update.message)
elif isinstance(update, types.UpdateShortMessage):
event = cls.Event(types.Message(
out=update.out,
mentioned=update.mentioned,
media_unread=update.media_unread,
silent=update.silent,
id=update.id,
peer_id=types.PeerUser(update.user_id),
from_id=types.PeerUser(self_id if update.out else update.user_id),
message=update.message,
date=update.date,
fwd_from=update.fwd_from,
via_bot_id=update.via_bot_id,
reply_to=update.reply_to,
entities=update.entities,
ttl_period=update.ttl_period
))
elif isinstance(update, types.UpdateShortChatMessage):
event = cls.Event(types.Message(
out=update.out,
mentioned=update.mentioned,
media_unread=update.media_unread,
silent=update.silent,
id=update.id,
from_id=types.PeerUser(self_id if update.out else update.from_id),
peer_id=types.PeerChat(update.chat_id),
message=update.message,
date=update.date,
fwd_from=update.fwd_from,
via_bot_id=update.via_bot_id,
reply_to=update.reply_to,
entities=update.entities,
ttl_period=update.ttl_period
))
else:
return
return event
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
m._finish_init(client, self._entities, None)
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

@@ -0,0 +1,53 @@
from .common import EventBuilder
from .. import utils
class Raw(EventBuilder):
"""
Raw events are not actual events. Instead, they are the raw
:tl:`Update` object that Telegram sends. You normally shouldn't
need these.
Args:
types (`list` | `tuple` | `type`, optional):
The type or types that the :tl:`Update` instance must be.
Equivalent to ``if not isinstance(update, types): return``.
Example
.. code-block:: python
from telethon import events
@client.on(events.Raw)
async def handler(update):
# Print all incoming updates
print(update.stringify())
"""
def __init__(self, types=None, *, func=None):
super().__init__(func=func)
if not types:
self.types = None
elif not utils.is_list_like(types):
if not isinstance(types, type):
raise TypeError('Invalid input type given: {}'.format(types))
self.types = types
else:
if not all(isinstance(x, type) for x in types):
raise TypeError('Invalid input types given: {}'.format(types))
self.types = tuple(types)
async def resolve(self, client):
self.resolved = True
@classmethod
def build(cls, update, others=None, self_id=None):
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

@@ -0,0 +1,310 @@
import datetime
import functools
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types
from ..tl.custom.sendergetter import SenderGetter
# TODO Either the properties are poorly named or they should be
# different events, but that would be a breaking change.
#
# TODO There are more "user updates", but bundling them all up
# in a single place will make it annoying to use (since
# the user needs to check for the existence of `None`).
#
# TODO Handle UpdateUserBlocked, UpdateUserName, UpdateUserPhone, UpdateUser
def _requires_action(function):
@functools.wraps(function)
def wrapped(self):
return None if self.action is None else function(self)
return wrapped
def _requires_status(function):
@functools.wraps(function)
def wrapped(self):
return None if self.status is None else function(self)
return wrapped
@name_inner_event
class UserUpdate(EventBuilder):
"""
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):
if isinstance(update, types.UpdateUserStatus):
return cls.Event(types.PeerUser(update.user_id),
status=update.status)
elif isinstance(update, types.UpdateChannelUserTyping):
return cls.Event(update.from_id,
chat_peer=types.PeerChannel(update.channel_id),
typing=update.action)
elif isinstance(update, types.UpdateChatUserTyping):
return cls.Event(update.from_id,
chat_peer=types.PeerChat(update.chat_id),
typing=update.action)
elif isinstance(update, types.UpdateUserTyping):
return cls.Event(update.user_id,
typing=update.action)
class Event(EventCommon, SenderGetter):
"""
Represents the event of a user update
such as gone online, started typing, etc.
Members:
status (:tl:`UserStatus`, optional):
The user status if the update is about going online or offline.
You should check this attribute first before checking any
of the seen within properties, since they will all be `None`
if the status is not set.
action (:tl:`SendMessageAction`, optional):
The "typing" action if any the user is performing if any.
You should check this attribute first before checking any
of the typing properties, since they will all be `None`
if the action is not set.
"""
def __init__(self, peer, *, status=None, chat_peer=None, typing=None):
super().__init__(chat_peer or peer)
SenderGetter.__init__(self, utils.get_peer_id(peer))
self.status = status
self.action = typing
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._mb_entity_cache)
@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):
"""
`True` if the action is typing a message.
"""
return isinstance(self.action, types.SendMessageTypingAction)
@property
@_requires_action
def uploading(self):
"""
`True` if the action is uploading something.
"""
return isinstance(self.action, (
types.SendMessageChooseContactAction,
types.SendMessageChooseStickerAction,
types.SendMessageUploadAudioAction,
types.SendMessageUploadDocumentAction,
types.SendMessageUploadPhotoAction,
types.SendMessageUploadRoundAction,
types.SendMessageUploadVideoAction
))
@property
@_requires_action
def recording(self):
"""
`True` if the action is recording something.
"""
return isinstance(self.action, (
types.SendMessageRecordAudioAction,
types.SendMessageRecordRoundAction,
types.SendMessageRecordVideoAction
))
@property
@_requires_action
def playing(self):
"""
`True` if the action is playing a game.
"""
return isinstance(self.action, types.SendMessageGamePlayAction)
@property
@_requires_action
def cancel(self):
"""
`True` if the action was cancelling other actions.
"""
return isinstance(self.action, types.SendMessageCancelAction)
@property
@_requires_action
def geo(self):
"""
`True` if what's being uploaded is a geo.
"""
return isinstance(self.action, types.SendMessageGeoLocationAction)
@property
@_requires_action
def audio(self):
"""
`True` if what's being recorded/uploaded is an audio.
"""
return isinstance(self.action, (
types.SendMessageRecordAudioAction,
types.SendMessageUploadAudioAction
))
@property
@_requires_action
def round(self):
"""
`True` if what's being recorded/uploaded is a round video.
"""
return isinstance(self.action, (
types.SendMessageRecordRoundAction,
types.SendMessageUploadRoundAction
))
@property
@_requires_action
def video(self):
"""
`True` if what's being recorded/uploaded is an video.
"""
return isinstance(self.action, (
types.SendMessageRecordVideoAction,
types.SendMessageUploadVideoAction
))
@property
@_requires_action
def contact(self):
"""
`True` if what's being uploaded (selected) is a contact.
"""
return isinstance(self.action, types.SendMessageChooseContactAction)
@property
@_requires_action
def document(self):
"""
`True` if what's being uploaded is document.
"""
return isinstance(self.action, types.SendMessageUploadDocumentAction)
@property
@_requires_action
def sticker(self):
"""
`True` if what's being uploaded is a sticker.
"""
return isinstance(self.action, types.SendMessageChooseStickerAction)
@property
@_requires_action
def photo(self):
"""
`True` if what's being uploaded is a photo.
"""
return isinstance(self.action, types.SendMessageUploadPhotoAction)
@property
@_requires_status
def last_seen(self):
"""
Exact `datetime.datetime` when the user was last seen if known.
"""
if isinstance(self.status, types.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, types.UserStatusOnline):
return self.status.expires
def _last_seen_delta(self):
if isinstance(self.status, types.UserStatusOffline):
return datetime.datetime.now(tz=datetime.timezone.utc) - self.status.was_online
elif isinstance(self.status, types.UserStatusOnline):
return datetime.timedelta(days=0)
elif isinstance(self.status, types.UserStatusRecently):
return datetime.timedelta(days=1)
elif isinstance(self.status, types.UserStatusLastWeek):
return datetime.timedelta(days=7)
elif isinstance(self.status, types.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)