Merge pull request #595 from LonamiWebs/events

Friendlier update handling through Events
This commit is contained in:
Lonami 2018-02-09 16:42:17 +01:00 committed by GitHub
commit f91b76b063
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1157 additions and 109 deletions

View File

@ -0,0 +1,144 @@
.. _update-modes:
============
Update Modes
============
The library can run in four distinguishable modes:
- With no extra threads at all.
- With an extra thread that receives everything as soon as possible (default).
- With several worker threads that run your update handlers.
- A mix of the above.
Since this section is about updates, we'll describe the simplest way to
work with them.
Using multiple workers
**********************
When you create your client, simply pass a number to the
``update_workers`` parameter:
``client = TelegramClient('session', api_id, api_hash, update_workers=2)``
You can set any amount of workers you want. The more you put, the more
update handlers that can be called "at the same time". One or two should
suffice most of the time, since setting more will not make things run
faster most of the times (actually, it could slow things down).
The next thing you want to do is to add a method that will be called when
an `Update`__ arrives:
.. code-block:: python
def callback(update):
print('I received', update)
client.add_update_handler(callback)
# do more work here, or simply sleep!
That's it! This is the old way to listen for raw updates, with no further
processing. If this feels annoying for you, remember that you can always
use :ref:`working-with-updates` but maybe use this for some other cases.
Now let's do something more interesting. Every time an user talks to use,
let's reply to them with the same text reversed:
.. code-block:: python
from telethon.tl.types import UpdateShortMessage, PeerUser
def replier(update):
if isinstance(update, UpdateShortMessage) and not update.out:
client.send_message(PeerUser(update.user_id), update.message[::-1])
client.add_update_handler(replier)
input('Press enter to stop this!')
client.disconnect()
We only ask you one thing: don't keep this running for too long, or your
contacts will go mad.
Spawning no worker at all
*************************
All the workers do is loop forever and poll updates from a queue that is
filled from the ``ReadThread``, responsible for reading every item off
the network. If you only need a worker and the ``MainThread`` would be
doing no other job, this is the preferred way. You can easily do the same
as the workers like so:
.. code-block:: python
while True:
try:
update = client.updates.poll()
if not update:
continue
print('I received', update)
except KeyboardInterrupt:
break
client.disconnect()
Note that ``poll`` accepts a ``timeout=`` parameter, and it will return
``None`` if other thread got the update before you could or if the timeout
expired, so it's important to check ``if not update``.
This can coexist with the rest of ``N`` workers, or you can set it to ``0``
additional workers:
``client = TelegramClient('session', api_id, api_hash, update_workers=0)``
You **must** set it to ``0`` (or other number), as it defaults to ``None``
and there is a different. ``None`` workers means updates won't be processed
*at all*, so you must set it to some value (``0`` or greater) if you want
``client.updates.poll()`` to work.
Using the main thread instead the ``ReadThread``
************************************************
If you have no work to do on the ``MainThread`` and you were planning to have
a ``while True: sleep(1)``, don't do that. Instead, don't spawn the secondary
``ReadThread`` at all like so:
.. code-block:: python
client = TelegramClient(
...
spawn_read_thread=False
)
And then ``.idle()`` from the ``MainThread``:
``client.idle()``
You can stop it with :kbd:`Control+C`, and you can configure the signals
to be used in a similar fashion to `Python Telegram Bot`__.
As a complete example:
.. code-block:: python
def callback(update):
print('I received', update)
client = TelegramClient('session', api_id, api_hash,
update_workers=1, spawn_read_thread=False)
client.connect()
client.add_update_handler(callback)
client.idle() # ends with Ctrl+C
This is the preferred way to use if you're simply going to listen for updates.
__ https://lonamiwebs.github.io/Telethon/types/update.html
__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460

View File

@ -5,144 +5,130 @@ Working with Updates
====================
.. note::
There are plans to make working with updates more friendly. Stay tuned!
The library comes with the :mod:`events` module. *Events* are an abstraction
over what Telegram calls `updates`__, and are meant to ease simple and common
usage when dealing with them, since there are many updates. Let's dive in!
.. contents::
The library can run in four distinguishable modes:
- With no extra threads at all.
- With an extra thread that receives everything as soon as possible (default).
- With several worker threads that run your update handlers.
- A mix of the above.
Since this section is about updates, we'll describe the simplest way to
work with them.
Using multiple workers
**********************
When you create your client, simply pass a number to the
``update_workers`` parameter:
``client = TelegramClient('session', api_id, api_hash, update_workers=4)``
4 workers should suffice for most cases (this is also the default on
`Python Telegram Bot`__). You can set this value to more, or even less
if you need.
The next thing you want to do is to add a method that will be called when
an `Update`__ arrives:
Getting Started
***************
.. code-block:: python
def callback(update):
print('I received', update)
from telethon import TelegramClient, events
client.add_update_handler(callback)
# do more work here, or simply sleep!
client = TelegramClient(..., update_workers=1, spawn_read_thread=False)
client.start()
That's it! Now let's do something more interesting.
Every time an user talks to use, let's reply to them with the same
text reversed:
@client.on(events.NewMessage)
def my_event_handler(event):
if 'hello' in event.raw_text:
event.reply('hi!')
client.idle()
Not much, but there might be some things unclear. What does this code do?
.. code-block:: python
from telethon.tl.types import UpdateShortMessage, PeerUser
from telethon import TelegramClient, events
def replier(update):
if isinstance(update, UpdateShortMessage) and not update.out:
client.send_message(PeerUser(update.user_id), update.message[::-1])
client = TelegramClient(..., update_workers=1, spawn_read_thread=False)
client.start()
client.add_update_handler(replier)
input('Press enter to stop this!')
client.disconnect()
We only ask you one thing: don't keep this running for too long, or your
contacts will go mad.
Spawning no worker at all
*************************
All the workers do is loop forever and poll updates from a queue that is
filled from the ``ReadThread``, responsible for reading every item off
the network. If you only need a worker and the ``MainThread`` would be
doing no other job, this is the preferred way. You can easily do the same
as the workers like so:
This is normal initialization (of course, pass session name, API ID and hash).
Nothing we don't know already.
.. code-block:: python
while True:
try:
update = client.updates.poll()
if not update:
continue
print('I received', update)
except KeyboardInterrupt:
break
client.disconnect()
Note that ``poll`` accepts a ``timeout=`` parameter, and it will return
``None`` if other thread got the update before you could or if the timeout
expired, so it's important to check ``if not update``.
This can coexist with the rest of ``N`` workers, or you can set it to ``0``
additional workers:
``client = TelegramClient('session', api_id, api_hash, update_workers=0)``
You **must** set it to ``0`` (or other number), as it defaults to ``None``
and there is a different. ``None`` workers means updates won't be processed
*at all*, so you must set it to some value (``0`` or greater) if you want
``client.updates.poll()`` to work.
@client.on(events.NewMessage)
Using the main thread instead the ``ReadThread``
************************************************
If you have no work to do on the ``MainThread`` and you were planning to have
a ``while True: sleep(1)``, don't do that. Instead, don't spawn the secondary
``ReadThread`` at all like so:
This Python decorator will attach itself to the ``my_event_handler``
definition, and basically means that *on* a ``NewMessage`` *event*,
the callback function you're about to define will be called:
.. code-block:: python
client = TelegramClient(
...
spawn_read_thread=False
)
def my_event_handler(event):
if 'hello' in event.raw_text:
event.reply('hi!')
And then ``.idle()`` from the ``MainThread``:
``client.idle()``
You can stop it with :kbd:`Control+C`, and you can configure the signals
to be used in a similar fashion to `Python Telegram Bot`__.
As a complete example:
If a ``NewMessage`` event occurs, and ``'hello'`` is in the text of the
message, we ``reply`` to the event with a ``'hi!'`` message.
.. code-block:: python
def callback(update):
print('I received', update)
client = TelegramClient('session', api_id, api_hash,
update_workers=1, spawn_read_thread=False)
client.connect()
client.add_update_handler(callback)
client.idle() # ends with Ctrl+C
client.disconnect()
client.idle()
Finally, this tells the client that we're done with our code, and want
to listen for all these events to occur. Of course, you might want to
do other things instead idling. For this refer to :ref:`update-modes`.
More on events
**************
The ``NewMessage`` event has much more than what was shown. You can access
the ``.sender`` of the message through that member, or even see if the message
had ``.media``, a ``.photo`` or a ``.document`` (which you could download with
for example ``client.download_media(event.photo)``.
If you don't want to ``.reply`` as a reply, you can use the ``.respond()``
method instead. Of course, there are more events such as ``ChatAction`` or
``UserUpdate``, and they're all used in the same way. Simply add the
``@client.on(events.XYZ)`` decorator on the top of your handler and you're
done! The event that will be passed always is of type ``XYZ.Event`` (for
instance, ``NewMessage.Event``), except for the ``Raw`` event which just
passes the ``Update`` object.
You can put the same event on many handlers, and even different events on
the same handler. You can also have a handler work on only specific chats,
for example:
.. code-block:: python
import ast
import random
@client.on(events.NewMessage(chats='TelethonOffTopic', incoming=True))
def normal_handler(event):
if 'roll' in event.raw_text:
event.reply(str(random.randint(1, 6)))
@client.on(events.NewMessage(chats='TelethonOffTopic', outgoing=True))
def admin_handler(event):
if event.raw_text.startswith('eval'):
expression = event.raw_text.replace('eval', '').strip()
event.reply(str(ast.literal_eval(expression)))
You can pass one or more chats to the ``chats`` parameter (as a list or tuple),
and only events from there will be processed. You can also specify whether you
want to handle incoming or outgoing messages (those you receive or those you
send). In this example, people can say ``'roll'`` and you will reply with a
random number, while if you say ``'eval 4+4'``, you will reply with the
solution. Try it!
Events module
*************
.. automodule:: telethon.events
:members:
:undoc-members:
:show-inheritance:
__ https://python-telegram-bot.org/
__ https://lonamiwebs.github.io/Telethon/types/update.html
__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460

View File

@ -49,6 +49,7 @@ heavy job for you, so you can focus on developing an application.
extra/advanced-usage/accessing-the-full-api
extra/advanced-usage/sessions
extra/advanced-usage/update-modes
.. _Examples:

View File

@ -0,0 +1,4 @@
telethon\.events package
========================

View File

@ -26,6 +26,14 @@ telethon\.telegram\_client module
:undoc-members:
:show-inheritance:
telethon\.events package
------------------------
.. toctree::
telethon.events
telethon\.update\_state module
------------------------------

868
telethon/events/__init__.py Normal file
View File

@ -0,0 +1,868 @@
import abc
import datetime
import itertools
from .. import utils
from ..errors import RPCError
from ..extensions import markdown
from ..tl import types, functions
class _EventBuilder(abc.ABC):
@abc.abstractmethod
def build(self, update):
"""Builds an event for the given update if possible, or returns None"""
@abc.abstractmethod
def resolve(self, client):
"""Helper method to allow event builders to be resolved before usage"""
class _EventCommon(abc.ABC):
"""Intermediate class with common things to all events"""
def __init__(self, chat_peer=None, msg_id=None, broadcast=False):
self._client = None
self._chat_peer = chat_peer
self._message_id = msg_id
self._input_chat = None
self._chat = None
self.is_private = isinstance(chat_peer, types.PeerUser)
self.is_group = (
isinstance(chat_peer, (types.PeerChat, types.PeerChannel))
and not broadcast
)
self.is_channel = isinstance(chat_peer, types.PeerChannel)
def _get_input_entity(self, msg_id, entity_id, chat=None):
"""
Helper function to call GetMessages on the give msg_id and
return the input entity whose ID is the given entity ID.
If ``chat`` is present it must be an InputPeer.
"""
try:
if isinstance(chat, types.InputPeerChannel):
result = self._client(
functions.channels.GetMessagesRequest(chat, [msg_id])
)
else:
result = self._client(
functions.messages.GetMessagesRequest([msg_id])
)
except RPCError:
return
entity = {
utils.get_peer_id(x): x for x in itertools.chain(
getattr(result, 'chats', []),
getattr(result, 'users', []))
}.get(entity_id)
if entity:
return utils.get_input_peer(entity)
@property
def input_chat(self):
"""
The (:obj:`InputPeer`) (group, megagroup or channel) on which
the event occurred. This doesn't have the title or anything,
but is useful if you don't need those to avoid further
requests.
Note that this might be ``None`` if the library can't find it.
"""
if self._input_chat is None and self._chat_peer is not None:
try:
self._input_chat = self._client.get_input_entity(
self._chat_peer
)
except (ValueError, TypeError):
# The library hasn't seen this chat, get the message
if not isinstance(self._chat_peer, types.PeerChannel):
# TODO For channels, getDifference? Maybe looking
# in the dialogs (which is already done) is enough.
if self._message_id is not None:
self._input_chat = self._get_input_entity(
self._message_id,
utils.get_peer_id(self._chat_peer)
)
return self._input_chat
@property
def chat(self):
"""
The (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional) on which
the event occurred. This property will make an API call the first time
to get the most up to date version of the chat, so use with care as
there is no caching besides local caching yet.
"""
if self._chat is None and self.input_chat:
self._chat = self._client.get_entity(self._input_chat)
return self._chat
class Raw(_EventBuilder):
"""
Represents a raw event. The event is the update itself.
"""
def resolve(self, client):
pass
def build(self, update):
return update
# Classes defined here are actually Event builders
# for their inner Event classes. Inner ._client is
# set later by the creator TelegramClient.
class NewMessage(_EventBuilder):
"""
Represents a new message event builder.
Args:
incoming (:obj:`bool`, optional):
If set to ``True``, only **incoming** messages will be handled.
Mutually exclusive with ``outgoing`` (can only set one of either).
outgoing (:obj:`bool`, optional):
If set to ``True``, only **outgoing** messages will be handled.
Mutually exclusive with ``incoming`` (can only set one of either).
chats (:obj:`entity`, optional):
May be one or more entities (username/peer/etc.). By default,
only matching chats will be handled.
blacklist_chats (:obj:`bool`, optional):
Whether to treat the the list of chats as a blacklist (if
it matches it will NOT be handled) or a whitelist (default).
"""
def __init__(self, incoming=None, outgoing=None,
chats=None, blacklist_chats=False):
if incoming and outgoing:
raise ValueError('Can only set either incoming or outgoing')
self.incoming = incoming
self.outgoing = outgoing
self.chats = chats
self.blacklist_chats = blacklist_chats
def resolve(self, client):
if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str):
self.chats = set(utils.get_peer_id(x)
for x in client.get_input_entity(self.chats))
elif self.chats is not None:
self.chats = {utils.get_peer_id(
client.get_input_entity(self.chats))}
def build(self, update):
if isinstance(update,
(types.UpdateNewMessage, types.UpdateNewChannelMessage)):
if not isinstance(update.message, types.Message):
return # We don't care about MessageService's here
event = NewMessage.Event(update.message)
elif isinstance(update, types.UpdateShortMessage):
event = NewMessage.Event(types.Message(
out=update.out,
mentioned=update.mentioned,
media_unread=update.media_unread,
silent=update.silent,
id=update.id,
to_id=types.PeerUser(update.user_id),
message=update.message,
date=update.date,
fwd_from=update.fwd_from,
via_bot_id=update.via_bot_id,
reply_to_msg_id=update.reply_to_msg_id,
entities=update.entities
))
else:
return
# Short-circuit if we let pass all events
if all(x is None for x in (self.incoming, self.outgoing, self.chats)):
return event
if self.incoming and event.message.out:
return
if self.outgoing and not event.message.out:
return
if self.chats is not None:
inside = utils.get_peer_id(event.message.to_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
# Tests passed so return the event
return event
class Event(_EventCommon):
"""
Represents the event of a new message.
Members:
message (:obj:`Message`):
This is the original ``Message`` object.
is_private (:obj:`bool`):
True if the message was sent as a private message.
is_group (:obj:`bool`):
True if the message was sent on a group or megagroup.
is_channel (:obj:`bool`):
True if the message was sent on a megagroup or channel.
is_reply (:obj:`str`):
Whether the message is a reply to some other or not.
"""
def __init__(self, message):
super().__init__(chat_peer=message.to_id,
msg_id=message.id, broadcast=bool(message.post))
self.message = message
self._text = None
self._input_chat = None
self._chat = None
self._input_sender = None
self._sender = None
self.is_reply = bool(message.reply_to_msg_id)
self._reply_message = None
def respond(self, *args, **kwargs):
"""
Responds to the message (not as a reply). This is a shorthand for
``client.send_message(event.chat, ...)``.
"""
return self._client.send_message(self.input_chat, *args, **kwargs)
def reply(self, *args, **kwargs):
"""
Replies to the message (as a reply). This is a shorthand for
``client.send_message(event.chat, ..., reply_to=event.message.id)``.
"""
return self._client.send_message(self.input_chat,
reply_to=self.message.id,
*args, **kwargs)
@property
def input_sender(self):
"""
This (:obj:`InputPeer`) is the input version of the user who
sent the message. Similarly to ``input_chat``, this doesn't have
things like username or similar, but still useful in some cases.
Note that this might not be available if the library can't
find the input chat.
"""
if self._input_sender is None:
try:
self._input_sender = self._client.get_input_entity(
self.message.from_id
)
except (ValueError, TypeError):
if isinstance(self.message.to_id, types.PeerChannel):
# We can rely on self.input_chat for this
self._input_sender = self._get_input_entity(
self.message.id,
self.message.from_id,
chat=self.input_chat
)
return self._input_sender
@property
def sender(self):
"""
This (:obj:`User`) will make an API call the first time to get
the most up to date version of the sender, so use with care as
there is no caching besides local caching yet.
``input_sender`` needs to be available (often the case).
"""
if self._sender is None and self.input_sender:
self._sender = self._client.get_entity(self._input_sender)
return self._sender
@property
def text(self):
"""
The message text, markdown-formatted.
"""
if self._text is None:
if not self.message.entities:
return self.message.message
self._text = markdown.unparse(self.message.message,
self.message.entities or [])
return self._text
@property
def raw_text(self):
"""
The raw message text, ignoring any formatting.
"""
return self.message.message
@property
def reply_message(self):
"""
This (:obj:`Message`, optional) will make an API call the first
time to get the full ``Message`` object that one was replying to,
so use with care as there is no caching besides local caching yet.
"""
if not self.message.reply_to_msg_id:
return None
if self._reply_message is None:
if isinstance(self.input_chat, types.InputPeerChannel):
r = self._client(functions.channels.GetMessagesRequest(
self.input_chat, [self.message.reply_to_msg_id]
))
else:
r = self._client(functions.messages.GetMessagesRequest(
[self.message.reply_to_msg_id]
))
if not isinstance(r, types.messages.MessagesNotModified):
self._reply_message = r.messages[0]
return self._reply_message
@property
def forward(self):
"""
The unmodified (:obj:`MessageFwdHeader`, optional).
"""
return self.message.fwd_from
@property
def media(self):
"""
The unmodified (:obj:`MessageMedia`, optional).
"""
return self.message.media
@property
def photo(self):
"""
If the message media is a photo,
this returns the (:obj:`Photo`) object.
"""
if isinstance(self.message.media, types.MessageMediaPhoto):
photo = self.message.media.photo
if isinstance(photo, types.Photo):
return photo
@property
def document(self):
"""
If the message media is a document,
this returns the (:obj:`Document`) object.
"""
if isinstance(self.message.media, types.MessageMediaDocument):
doc = self.message.media.document
if isinstance(doc, types.Document):
return doc
@property
def out(self):
"""
Whether the message is outgoing (i.e. you sent it from
another session) or incoming (i.e. someone else sent it).
"""
return self.message.out
class ChatAction(_EventBuilder):
"""
Represents an action in a chat (such as user joined, left, or new pin).
Args:
chats (:obj:`entity`, optional):
May be one or more entities (username/peer/etc.). By default,
only matching chats will be handled.
blacklist_chats (:obj:`bool`, optional):
Whether to treat the the list of chats as a blacklist (if
it matches it will NOT be handled) or a whitelist (default).
"""
def __init__(self, chats=None, blacklist_chats=False):
# TODO This can probably be reused in all builders
self.chats = chats
self.blacklist_chats = blacklist_chats
def resolve(self, client):
if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str):
self.chats = set(utils.get_peer_id(x)
for x in client.get_input_entity(self.chats))
elif self.chats is not None:
self.chats = {utils.get_peer_id(
client.get_input_entity(self.chats))}
def build(self, update):
if isinstance(update, types.UpdateChannelPinnedMessage):
# Telegram sends UpdateChannelPinnedMessage and then
# UpdateNewChannelMessage with MessageActionPinMessage.
event = ChatAction.Event(types.PeerChannel(update.channel_id),
new_pin=update.id)
elif isinstance(update, types.UpdateChatParticipantAdd):
event = ChatAction.Event(types.PeerChat(update.chat_id),
added_by=update.inviter_id or True,
users=update.user_id)
elif isinstance(update, types.UpdateChatParticipantDelete):
event = ChatAction.Event(types.PeerChat(update.chat_id),
kicked_by=True,
users=update.user_id)
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):
event = ChatAction.Event(msg.to_id,
added_by=True,
users=msg.from_id)
elif isinstance(action, types.MessageActionChatAddUser):
event = ChatAction.Event(msg.to_id,
added_by=msg.from_id or True,
users=action.users)
elif isinstance(action, types.MessageActionChatDeleteUser):
event = ChatAction.Event(msg.to_id,
kicked_by=msg.from_id or True,
users=action.user_id)
elif isinstance(action, types.MessageActionChatCreate):
event = ChatAction.Event(msg.to_id,
users=action.users,
created=True,
new_title=action.title)
elif isinstance(action, types.MessageActionChannelCreate):
event = ChatAction.Event(msg.to_id,
created=True,
new_title=action.title)
elif isinstance(action, types.MessageActionChatEditTitle):
event = ChatAction.Event(msg.to_id,
new_title=action.title)
elif isinstance(action, types.MessageActionChatEditPhoto):
event = ChatAction.Event(msg.to_id,
new_photo=action.photo)
elif isinstance(action, types.MessageActionChatDeletePhoto):
event = ChatAction.Event(msg.to_id,
new_photo=True)
else:
return
else:
return
if self.chats is None:
return event
else:
inside = utils.get_peer_id(event._chat_peer) 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
return event
class Event(_EventCommon):
"""
Represents the event of a new chat action.
Members:
new_pin (:obj:`bool`):
``True`` if the pin has changed (new pin or removed).
new_photo (:obj:`bool`):
``True`` if there's a new chat photo (or it was removed).
photo (:obj:`Photo`, optional):
The new photo (or ``None`` if it was removed).
user_added (:obj:`bool`):
``True`` if the user was added by some other.
user_joined (:obj:`bool`):
``True`` if the user joined on their own.
user_left (:obj:`bool`):
``True`` if the user left on their own.
user_kicked (:obj:`bool`):
``True`` if the user was kicked by some other.
created (:obj:`bool`, optional):
``True`` if this chat was just created.
new_title (:obj:`bool`, optional):
The new title string for the chat, if applicable.
"""
def __init__(self, chat_peer, new_pin=None, new_photo=None,
added_by=None, kicked_by=None, created=None,
users=None, new_title=None):
super().__init__(chat_peer=chat_peer, msg_id=new_pin)
self.new_pin = isinstance(new_pin, int)
self._pinned_message = new_pin
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 = (False, False, False, False)
if added_by is True:
self.user_joined = True
elif added_by:
self.user_added = True
self._added_by = added_by
if kicked_by is True:
self.user_left = True
elif kicked_by:
self.user_kicked = True
self._kicked_by = kicked_by
self.created = bool(created)
self._user_peers = users if isinstance(users, list) else [users]
self._users = None
self.new_title = new_title
@property
def pinned_message(self):
"""
If ``new_pin`` is ``True``, this returns the (:obj:`Message`)
object that was pinned.
"""
if self._pinned_message == 0:
return None
if isinstance(self._pinned_message, int) and self.input_chat:
r = self._client(functions.channels.GetMessagesRequest(
self._input_chat, [self._pinned_message]
))
try:
self._pinned_message = next(
x for x in r.messages
if isinstance(x, types.Message)
and x.id == self._pinned_message
)
except StopIteration:
pass
if isinstance(self._pinned_message, types.Message):
return self._pinned_message
@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):
self._added_by = 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):
self._kicked_by = self._client.get_entity(self._kicked_by)
return self._kicked_by
@property
def user(self):
"""
The single user that takes part in this action (e.g. joined).
Might be ``None`` if the information can't be retrieved or
there is no user taking part.
"""
try:
return next(self.users)
except (StopIteration, TypeError):
return None
@property
def users(self):
"""
A list of users that take part in this action (e.g. joined).
Might be empty if the information can't be retrieved or there
are no users taking part.
"""
if self._users is None and self._user_peers:
try:
self._users = self._client.get_entity(self._user_peers)
except (TypeError, ValueError):
self._users = []
return self._users
class UserUpdate(_EventBuilder):
"""
Represents an user update (gone online, offline, joined Telegram).
"""
def build(self, update):
if isinstance(update, types.UpdateUserStatus):
event = UserUpdate.Event(update.user_id,
status=update.status)
else:
return
return event
def resolve(self, client):
pass
class Event(_EventCommon):
"""
Represents the event of an user status update (last seen, joined).
Members:
online (:obj:`bool`, optional):
``True`` if the user is currently online, ``False`` otherwise.
Might be ``None`` if this information is not present.
last_seen (:obj:`datetime`, optional):
Exact date when the user was last seen if known.
until (:obj:`datetime`, optional):
Until when will the user remain online.
within_months (:obj:`bool`):
``True`` if the user was seen within 30 days.
within_weeks (:obj:`bool`):
``True`` if the user was seen within 7 days.
recently (:obj:`bool`):
``True`` if the user was seen within a day.
action (:obj:`SendMessageAction`, optional):
The "typing" action if any the user is performing if any.
cancel (:obj:`bool`):
``True`` if the action was cancelling other actions.
typing (:obj:`bool`):
``True`` if the action is typing a message.
recording (:obj:`bool`):
``True`` if the action is recording something.
uploading (:obj:`bool`):
``True`` if the action is uploading something.
playing (:obj:`bool`):
``True`` if the action is playing a game.
audio (:obj:`bool`):
``True`` if what's being recorded/uploaded is an audio.
round (:obj:`bool`):
``True`` if what's being recorded/uploaded is a round video.
video (:obj:`bool`):
``True`` if what's being recorded/uploaded is an video.
document (:obj:`bool`):
``True`` if what's being uploaded is document.
geo (:obj:`bool`):
``True`` if what's being uploaded is a geo.
photo (:obj:`bool`):
``True`` if what's being uploaded is a photo.
contact (:obj:`bool`):
``True`` if what's being uploaded (selected) is a contact.
"""
def __init__(self, user_id, status=None, typing=None):
super().__init__(types.PeerUser(user_id))
self.online = None if status is None else \
isinstance(status, types.UserStatusOnline)
self.last_seen = status.was_online if \
isinstance(status, types.UserStatusOffline) else None
self.until = status.expires if \
isinstance(status, types.UserStatusOnline) else None
if self.last_seen:
diff = datetime.datetime.now() - self.last_seen
if diff < datetime.timedelta(days=30):
self.within_months = True
if diff < datetime.timedelta(days=7):
self.within_weeks = True
if diff < datetime.timedelta(days=1):
self.recently = True
else:
self.within_months = self.within_weeks = self.recently = False
if isinstance(status, (types.UserStatusOnline,
types.UserStatusRecently)):
self.within_months = self.within_weeks = True
self.recently = True
elif isinstance(status, types.UserStatusLastWeek):
self.within_months = self.within_weeks = True
elif isinstance(status, types.UserStatusLastMonth):
self.within_months = True
self.action = typing
if typing:
self.cancel = self.typing = self.recording = self.uploading = \
self.playing = False
self.audio = self.round = self.video = self.document = \
self.geo = self.photo = self.contact = False
if isinstance(typing, types.SendMessageCancelAction):
self.cancel = True
elif isinstance(typing, types.SendMessageTypingAction):
self.typing = True
elif isinstance(typing, types.SendMessageGamePlayAction):
self.playing = True
elif isinstance(typing, types.SendMessageGeoLocationAction):
self.geo = True
elif isinstance(typing, types.SendMessageRecordAudioAction):
self.recording = self.audio = True
elif isinstance(typing, types.SendMessageRecordRoundAction):
self.recording = self.round = True
elif isinstance(typing, types.SendMessageRecordVideoAction):
self.recording = self.video = True
elif isinstance(typing, types.SendMessageChooseContactAction):
self.uploading = self.contact = True
elif isinstance(typing, types.SendMessageUploadAudioAction):
self.uploading = self.audio = True
elif isinstance(typing, types.SendMessageUploadDocumentAction):
self.uploading = self.document = True
elif isinstance(typing, types.SendMessageUploadPhotoAction):
self.uploading = self.photo = True
elif isinstance(typing, types.SendMessageUploadRoundAction):
self.uploading = self.round = True
elif isinstance(typing, types.SendMessageUploadVideoAction):
self.uploading = self.video = True
@property
def user(self):
"""Alias around the chat (conversation)."""
return self.chat
class MessageChanged(_EventBuilder):
"""
Represents a message changed (edited or deleted).
"""
def build(self, update):
if isinstance(update, (types.UpdateEditMessage,
types.UpdateEditChannelMessage)):
event = MessageChanged.Event(edit_msg=update.message)
elif isinstance(update, (types.UpdateDeleteMessages,
types.UpdateDeleteChannelMessages)):
event = MessageChanged.Event(
deleted_ids=update.messages,
peer=types.PeerChannel(update.channel_id)
)
else:
return
return event
def resolve(self, client):
pass
class Event(_EventCommon):
"""
Represents the event of an user status update (last seen, joined).
Members:
edited (:obj:`bool`):
``True`` if the message was edited.
message (:obj:`Message`, optional):
The new edited message, if any.
deleted (:obj:`bool`):
``True`` if the message IDs were deleted.
deleted_ids (:obj:`List[int]`):
A list containing the IDs of the messages that were deleted.
input_sender (:obj:`InputPeer`):
This is the input version of the user who edited the message.
Similarly to ``input_chat``, this doesn't have things like
username or similar, but still useful in some cases.
Note that this might not be available if the library can't
find the input chat.
sender (:obj:`User`):
This property will make an API call the first time to get the
most up to date version of the sender, so use with care as
there is no caching besides local caching yet.
``input_sender`` needs to be available (often the case).
"""
def __init__(self, edit_msg=None, deleted_ids=None, peer=None):
super().__init__(peer if not edit_msg else edit_msg.to_id)
self.edited = bool(edit_msg)
self.message = edit_msg
self.deleted = bool(deleted_ids)
self.deleted_ids = deleted_ids or []
self._input_sender = None
self._sender = None
@property
def input_sender(self):
"""
This (:obj:`InputPeer`) is the input version of the user who
sent the message. Similarly to ``input_chat``, this doesn't have
things like username or similar, but still useful in some cases.
Note that this might not be available if the library can't
find the input chat.
"""
# TODO Code duplication
if self._input_sender is None and self.message:
try:
self._input_sender = self._client.get_input_entity(
self.message.from_id
)
except (ValueError, TypeError):
if isinstance(self.message.to_id, types.PeerChannel):
# We can rely on self.input_chat for this
self._input_sender = self._get_input_entity(
self.message.id,
self.message.from_id,
chat=self.input_chat
)
return self._input_sender
@property
def sender(self):
"""
This (:obj:`User`) will make an API call the first time to get
the most up to date version of the sender, so use with care as
there is no caching besides local caching yet.
``input_sender`` needs to be available (often the case).
"""
if self._sender is None and self.input_sender:
self._sender = self._client.get_entity(self._input_sender)
return self._sender

View File

@ -160,6 +160,8 @@ class TelegramClient(TelegramBareClient):
**kwargs
)
self._event_builders = []
# Some fields to easy signing in. Let {phone: hash} be
# a dictionary because the user may change their mind.
self._phone_code_hash = {}
@ -1623,6 +1625,41 @@ class TelegramClient(TelegramBareClient):
# endregion
# region Event handling
def on(self, event):
"""
Turns the given entity into a valid Telegram user or chat.
Args:
event (:obj:`_EventBuilder` | :obj:`type`):
The event builder class or instance to be used,
for instance ``events.NewMessage``.
"""
if isinstance(event, type):
event = event()
event.resolve(self)
def decorator(f):
self._event_builders.append((event, f))
return f
if self._on_handler not in self.updates.handlers:
self.add_update_handler(self._on_handler)
return decorator
def _on_handler(self, update):
for builder, callback in self._event_builders:
event = builder.build(update)
if event:
event._client = self
callback(event)
# endregion
# region Small utilities to make users' life easier
def get_entity(self, entity):