Merge branch 'master' into asyncio

This commit is contained in:
Lonami Exo 2018-04-08 15:47:17 +02:00
commit 06af73ed65
19 changed files with 1602 additions and 1359 deletions

View File

@ -65,7 +65,7 @@ if you're new with ``asyncio``.
await client.send_file('username', '/home/myself/Pictures/holidays.jpg')
await client.download_profile_photo('me')
messages = await client.get_message_history('username')
messages = await client.get_messages('username')
await client.download_media(messages[0])

View File

@ -20,6 +20,13 @@ in response to certain methods, such as :tl:`GetUsersRequest`.
or even entire :tl:`User`, :tl:`Chat` and :tl:`Channel` objects and even
phone numbers from people you have in your contacts.
To "encounter" an ID, you would have to "find it" like you would in the
normal app. If the peer is in your dialogs, you would need to
`client.get_dialogs() <telethon.telegram_client.TelegramClient.get_dialogs>`.
If the peer is someone in a group, you would similarly
`client.get_participants(group) <telethon.telegram_client.TelegramClient.get_participants>`.
Getting entities
****************

View File

@ -58,6 +58,11 @@ Many other common methods for quick scripts are also available:
# Note that you can use 'me' or 'self' to message yourself
await client.send_message('username', 'Hello World from Telethon!')
# .send_message's parse mode defaults to markdown, so you
# can use **bold**, __italics__, [links](https://example.com), `code`,
# and even [mentions](@username)/[mentions](tg://user?id=123456789)
await client.send_message('username', '**Using** __markdown__ `too`!')
await client.send_file('username', '/home/myself/Pictures/holidays.jpg')
# The utils package has some goodies, like .get_display_name()
@ -83,15 +88,16 @@ a single line.
Available methods
*****************
This page lists all the "handy" methods available for you to use in the
``TelegramClient`` class. These are simply wrappers around the "raw"
Telegram API, making it much more manageable and easier to work with.
The :ref:`reference <telethon-package>` lists all the "handy" methods
available for you to use in the ``TelegramClient`` class. These are simply
wrappers around the "raw" Telegram API, making it much more manageable and
easier to work with.
Please refer to :ref:`accessing-the-full-api` if these aren't enough,
and don't be afraid to read the source code of the InteractiveTelegramClient_
or even the TelegramClient_ itself to learn how it works.
To see the methods available in the client, see :ref:`telethon-package`.
See the mentioned :ref:`telethon-package` to find the available methods.
.. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py
.. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py

View File

@ -13,7 +13,7 @@ Talking to Inline Bots
You can query an inline bot, such as `@VoteBot`__ (note, *query*,
not *interact* with a voting message), by making use of the
`GetInlineBotResultsRequest`__ request:
:tl:`GetInlineBotResultsRequest` request:
.. code-block:: python
@ -24,7 +24,7 @@ not *interact* with a voting message), by making use of the
))
And you can select any of their results by using
`SendInlineBotResultRequest`__:
:tl:`SendInlineBotResultRequest`:
.. code-block:: python
@ -41,7 +41,7 @@ Talking to Bots with special reply markup
*****************************************
To interact with a message that has a special reply markup, such as
`@VoteBot`__ polls, you would use `GetBotCallbackAnswerRequest`__:
`@VoteBot`__ polls, you would use :tl:`GetBotCallbackAnswerRequest`:
.. code-block:: python
@ -58,7 +58,4 @@ show it visually (button rows, and buttons within each row, each with
its own data).
__ https://t.me/vote
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_inline_bot_results.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/send_inline_bot_result.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_bot_callback_answer.html
__ https://t.me/vote

View File

@ -20,7 +20,7 @@ Joining a public channel
************************
Once you have the :ref:`entity <entities>` of the channel you want to join
to, you can make use of the `JoinChannelRequest`__ to join such channel:
to, you can make use of the :tl:`JoinChannelRequest` to join such channel:
.. code-block:: python
@ -35,6 +35,9 @@ to, you can make use of the `JoinChannelRequest`__ to join such channel:
For more on channels, check the `channels namespace`__.
__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html
Joining a private chat or channel
*********************************
@ -43,7 +46,7 @@ If all you have is a link like this one:
enough information to join! The part after the
``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this
example, is the ``hash`` of the chat or channel. Now you can use
`ImportChatInviteRequest`__ as follows:
:tl:`ImportChatInviteRequest` as follows:
.. code-block:: python
@ -55,21 +58,23 @@ Adding someone else to such chat or channel
*******************************************
If you don't want to add yourself, maybe because you're already in,
you can always add someone else with the `AddChatUserRequest`__, which
use is very straightforward, or `InviteToChannelRequest`__ for channels:
you can always add someone else with the :tl:`AddChatUserRequest`, which
use is very straightforward, or :tl:`InviteToChannelRequest` for channels:
.. code-block:: python
# For normal chats
from telethon.tl.functions.messages import AddChatUserRequest
# Note that ``user_to_add`` is NOT the name of the parameter.
# It's the user you want to add (``user_id=user_to_add``).
await client(AddChatUserRequest(
chat_id,
user_to_add,
fwd_limit=10 # Allow the user to see the 10 last messages
))
# For channels
# For channels (which includes megagroups)
from telethon.tl.functions.channels import InviteToChannelRequest
await client(InviteToChannelRequest(
@ -78,24 +83,13 @@ use is very straightforward, or `InviteToChannelRequest`__ for channels:
))
Checking a link without joining
*******************************
If you don't need to join but rather check whether it's a group or a
channel, you can use the `CheckChatInviteRequest`__, which takes in
channel, you can use the :tl:`CheckChatInviteRequest`, which takes in
the hash of said channel or group.
__ https://lonamiwebs.github.io/Telethon/constructors/chat.html
__ https://lonamiwebs.github.io/Telethon/constructors/channel.html
__ https://lonamiwebs.github.io/Telethon/types/chat.html
__ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html
__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html
__ https://lonamiwebs.github.io/Telethon/methods/channels/invite_to_channel.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html
Retrieving all chat members (channels too)
******************************************
@ -108,11 +102,11 @@ Most of the time you will just need ``client.get_participants(entity)``.
This is what said method is doing behind the scenes as an example.
In order to get all the members from a mega-group or channel, you need
to use `GetParticipantsRequest`__. As we can see it needs an
`InputChannel`__, (passing the mega-group or channel you're going to
use will work), and a mandatory `ChannelParticipantsFilter`__. The
to use :tl:`GetParticipantsRequest`. As we can see it needs an
:tl:`InputChannel`, (passing the mega-group or channel you're going to
use will work), and a mandatory :tl:`ChannelParticipantsFilter`. The
closest thing to "no filter" is to simply use
`ChannelParticipantsSearch`__ with an empty ``'q'`` string.
:tl:`ChannelParticipantsSearch` with an empty ``'q'`` string.
If we want to get *all* the members, we need to use a moving offset and
a fixed limit:
@ -146,34 +140,28 @@ a fixed limit:
Refer to `issue 573`__ for more on this.
Note that ``GetParticipantsRequest`` returns `ChannelParticipants`__,
Note that :tl:`GetParticipantsRequest` returns :tl:`ChannelParticipants`,
which may have more information you need (like the role of the
participants, total count of members, etc.)
__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html
__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html
__ https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html
__ https://lonamiwebs.github.io/Telethon/constructors/channel_participants_search.html
__ https://github.com/LonamiWebs/Telethon/issues/573
__ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html
Recent Actions
**************
"Recent actions" is simply the name official applications have given to
the "admin log". Simply use `GetAdminLogRequest`__ for that, and
the "admin log". Simply use :tl:`GetAdminLogRequest` for that, and
you'll get AdminLogResults.events in return which in turn has the final
`.action`__.
__ https://lonamiwebs.github.io/Telethon/methods/channels/get_admin_log.html
__ https://lonamiwebs.github.io/Telethon/types/channel_admin_log_event_action.html
Admin Permissions
*****************
Giving or revoking admin permissions can be done with the `EditAdminRequest`__:
Giving or revoking admin permissions can be done with the :tl:`EditAdminRequest`:
.. code-block:: python
@ -208,17 +196,82 @@ Giving or revoking admin permissions can be done with the `EditAdminRequest`__:
# User will now be able to change group info, delete other people's
# messages and pin messages.
| Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set all
| parameters to ``True`` to give a user full permissions, as not all
| permissions are related to both broadcast channels/megagroups.
|
| E.g. trying to set ``post_messages=True`` in a megagroup will raise an
| error. It is recommended to always use keyword arguments, and to set only
| the permissions the user needs. If you don't need to change a permission,
| it can be omitted (full list `here`__).
__ https://lonamiwebs.github.io/Telethon/methods/channels/edit_admin.html
.. note::
Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set all
parameters to ``True`` to give a user full permissions, as not all
permissions are related to both broadcast channels/megagroups.
E.g. trying to set ``post_messages=True`` in a megagroup will raise an
error. It is recommended to always use keyword arguments, and to set only
the permissions the user needs. If you don't need to change a permission,
it can be omitted (full list `here`__).
Restricting Users
*****************
Similar to how you give or revoke admin permissions, you can edit the
banned rights of an user through :tl:`EditAdminRequest` and its parameter
:tl:`ChannelBannedRights`:
.. code-block:: python
from telethon.tl.functions.channels import EditBannedRequest
from telethon.tl.types import ChannelBannedRights
from datetime import datetime, timedelta
# Restricting an user for 7 days, only allowing view/send messages.
#
# Note that it's "reversed". You must set to ``True`` the permissions
# you want to REMOVE, and leave as ``None`` those you want to KEEP.
rights = ChannelBannedRights(
until_date=datetime.now() + timedelta(days=7),
view_messages=None,
send_messages=None,
send_media=True,
send_stickers=True,
send_gifs=True,
send_games=True,
send_inline=True,
embed_links=True
)
# The above is equivalent to
rights = ChannelBannedRights(
until_date=datetime.now() + timedelta(days=7),
send_media=True,
send_stickers=True,
send_gifs=True,
send_games=True,
send_inline=True,
embed_links=True
)
await client(EditBannedRequest(channel, user, rights))
Kicking a member
****************
Telegram doesn't actually have a request to kick an user from a group.
Instead, you need to restrict them so they can't see messages. Any date
is enough:
.. code-block:: python
from telethon.tl.functions.channels import EditBannedRequest
from telethon.tl.types import ChannelBannedRights
await client(EditBannedRequest(channel, user, ChannelBannedRights(
until_date=None,
view_messages=True
)))
__ https://github.com/Kyle2142
__ https://github.com/LonamiWebs/Telethon/issues/490
__ https://lonamiwebs.github.io/Telethon/constructors/channel_admin_rights.html
@ -229,7 +282,7 @@ Increasing View Count in a Channel
It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and
while I don't understand why so many people ask this, the solution is to
use `GetMessagesViewsRequest`__, setting ``increment=True``:
use :tl:`GetMessagesViewsRequest`, setting ``increment=True``:
.. code-block:: python
@ -253,4 +306,3 @@ __ https://github.com/LonamiWebs/Telethon/issues/233
__ https://github.com/LonamiWebs/Telethon/issues/305
__ https://github.com/LonamiWebs/Telethon/issues/409
__ https://github.com/LonamiWebs/Telethon/issues/447
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages_views.html

View File

@ -0,0 +1,41 @@
=======================
Projects using Telethon
=======================
This page lists some real world examples showcasing what can be built with
the library.
.. note::
Do you have a project that uses the library or know of any that's not
listed here? Feel free to leave a comment at
`issue 744 <https://github.com/LonamiWebs/Telethon/issues/744>`_
so it can be included in the next revision of the documentation!
telegram-export
***************
`Link <https://github.com/expectocode/telegram-export>`_ /
`Author's website <https://github.com/expectocode>`_
A tool to download Telegram data (users, chats, messages, and media)
into a database (and display the saved data).
mautrix-telegram
****************
`Link <https://github.com/tulir/mautrix-telegram>`_ /
`Author's website <https://maunium.net/>`_
A Matrix-Telegram hybrid puppeting/relaybot bridge.
TelegramTUI
***********
`Link <https://github.com/bad-day/TelegramTUI>`_ /
`Author's website <https://github.com/bad-day>`_
A Telegram client on your terminal.

View File

@ -51,7 +51,7 @@ too, if that's all you have.
Searching Messages
*******************
Messages are searched through the obvious SearchRequest_, but you may run
Messages are searched through the obvious :tl:`SearchRequest`, but you may run
into issues_. A valid example would be:
.. code-block:: python
@ -75,7 +75,7 @@ into issues_. A valid example would be:
))
It's important to note that the optional parameter ``from_id`` could have
been omitted (defaulting to ``None``). Changing it to InputUserEmpty_, as one
been omitted (defaulting to ``None``). Changing it to :tl:`InputUserEmpty`, as one
could think to specify "no user", won't work because this parameter is a flag,
and it being unspecified has a different meaning.
@ -128,7 +128,4 @@ send yourself the very first sticker you have:
))
.. _ForwardMessagesRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_messages.html
.. _SearchRequest: https://lonamiwebs.github.io/Telethon/methods/messages/search.html
.. _issues: https://github.com/LonamiWebs/Telethon/issues/215
.. _InputUserEmpty: https://lonamiwebs.github.io/Telethon/constructors/input_user_empty.html

View File

@ -74,6 +74,7 @@ heavy job for you, so you can focus on developing an application.
extra/examples/working-with-messages
extra/examples/chats-and-channels
extra/examples/bots
extra/examples/projects-using-telethon
.. _Troubleshooting:

View File

@ -7,3 +7,50 @@ telethon\.events package
:members:
:undoc-members:
:show-inheritance:
Every event (builder) subclasses `telethon.events.common.EventBuilder`,
so all the methods in it can be used from any event builder/event instance.
.. automodule:: telethon.events.common
:members:
:undoc-members:
:show-inheritance:
Below all the event types are listed:
.. automodule:: telethon.events.newmessage
:members:
:undoc-members:
:show-inheritance:
.. automodule:: telethon.events.chataction
:members:
:undoc-members:
:show-inheritance:
.. automodule:: telethon.events.userupdate
:members:
:undoc-members:
:show-inheritance:
.. automodule:: telethon.events.messageedited
:members:
:undoc-members:
:show-inheritance:
.. automodule:: telethon.events.messagedeleted
:members:
:undoc-members:
:show-inheritance:
.. automodule:: telethon.events.messageread
:members:
:undoc-members:
:show-inheritance:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,329 @@
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types, functions
@name_inner_event
class ChatAction(EventBuilder):
"""
Represents an action in a chat (such as user joined, left, or new pin).
"""
def build(self, update):
if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0:
# Telegram does not always send
# UpdateChannelPinnedMessage for new pins
# but always for unpin, with update.id = 0
event = ChatAction.Event(types.PeerChannel(update.channel_id),
unpin=True)
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,
added_by=True,
users=msg.from_id)
elif isinstance(action, types.MessageActionChatAddUser):
event = ChatAction.Event(msg,
added_by=msg.from_id or True,
users=action.users)
elif isinstance(action, types.MessageActionChatDeleteUser):
event = ChatAction.Event(msg,
kicked_by=msg.from_id or True,
users=action.user_id)
elif isinstance(action, types.MessageActionChatCreate):
event = ChatAction.Event(msg,
users=action.users,
created=True,
new_title=action.title)
elif isinstance(action, types.MessageActionChannelCreate):
event = ChatAction.Event(msg,
created=True,
users=msg.from_id,
new_title=action.title)
elif isinstance(action, types.MessageActionChatEditTitle):
event = ChatAction.Event(msg,
users=msg.from_id,
new_title=action.title)
elif isinstance(action, types.MessageActionChatEditPhoto):
event = ChatAction.Event(msg,
users=msg.from_id,
new_photo=action.photo)
elif isinstance(action, types.MessageActionChatDeletePhoto):
event = ChatAction.Event(msg,
users=msg.from_id,
new_photo=True)
elif isinstance(action, types.MessageActionPinMessage):
# Telegram always sends this service message for new pins
event = ChatAction.Event(msg,
users=msg.from_id,
new_pin=msg.reply_to_msg_id)
else:
return
else:
return
event._entities = update._entities
return self._filter_event(event)
class Event(EventCommon):
"""
Represents the event of a new chat action.
Members:
action_message (`MessageAction <https://lonamiwebs.github.io/Telethon/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 (`bool`, optional):
The new title string for the chat, if applicable.
unpin (`bool`):
``True`` if the existing pin gets unpinned.
"""
def __init__(self, where, new_pin=None, new_photo=None,
added_by=None, kicked_by=None, created=None,
users=None, new_title=None, unpin=None):
if isinstance(where, types.MessageService):
self.action_message = where
where = where.to_id
else:
self.action_message = None
super().__init__(chat_peer=where, 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, self.unpin = (False, 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._input_users = None
self.new_title = new_title
self.unpin = unpin
async def respond(self, *args, **kwargs):
"""
Responds to the chat action message (not as a reply).
Shorthand for ``client.send_message(event.chat, ...)``.
"""
return await self._client.send_message(self.input_chat, *args, **kwargs)
async def reply(self, *args, **kwargs):
"""
Replies to the chat action message (as a reply). Shorthand for
``client.send_message(event.chat, ..., reply_to=event.message.id)``.
Has the same effect as ``.respond()`` if there is no message.
"""
if not self.action_message:
return self.respond(*args, **kwargs)
kwargs['reply_to'] = self.action_message.id
return await self._client.send_message(self.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. This is a shorthand for
``client.delete_messages(event.chat, event.message, ...)``.
Does nothing if no message action triggered this event.
"""
if self.action_message:
return await self._client.delete_messages(self.input_chat,
[self.action_message],
*args, **kwargs)
@property
async def pinned_message(self):
"""
If ``new_pin`` is ``True``, this returns the (:tl:`Message`)
object that was pinned.
"""
if self._pinned_message == 0:
return None
if isinstance(self._pinned_message, int) and self.input_chat:
r = await self._client(functions.channels.GetMessagesRequest(
self._input_chat, [
types.InputMessageID(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
async 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._entities.get(utils.get_peer_id(self._added_by))
if not self._added_by:
self._added_by = await self._client.get_entity(self._added_by)
return self._added_by
@property
async 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._entities.get(utils.get_peer_id(self._kicked_by))
if not self._kicked_by:
self._kicked_by = await self._client.get_entity(self._kicked_by)
return self._kicked_by
@property
async def user(self):
"""
The first 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.
"""
if await self.users:
return self._users[0]
@property
async def input_user(self):
"""
Input version of the ``self.user`` property.
"""
if await self.input_users:
return self._input_users[0]
@property
async def user_id(self):
"""
Returns the marked signed ID of the first user, if any.
"""
if await self.input_users:
return utils.get_peer_id(self._input_users[0])
@property
async 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 not self._user_peers:
return []
if self._users is None:
have, missing = [], []
for peer in self._user_peers:
user = self._entities.get(utils.get_peer_id(peer))
if user:
have.append(user)
else:
missing.append(peer)
try:
missing = await self._client.get_entity(missing)
except (TypeError, ValueError):
missing = []
self._users = have + missing
return self._users
@property
async def input_users(self):
"""
Input version of the ``self.users`` property.
"""
if self._input_users is None and self._user_peers:
self._input_users = []
for peer in self._user_peers:
try:
self._input_users.append(await self._client.get_input_entity(
peer
))
except (TypeError, ValueError):
pass
return self._input_users
@property
async def user_ids(self):
"""
Returns the marked signed ID of the users, if any.
"""
if await self.input_users:
return [utils.get_peer_id(u) for u in self._input_users]

232
telethon/events/common.py Normal file
View File

@ -0,0 +1,232 @@
import abc
import datetime
import itertools
import re
import warnings
from .. import utils
from ..errors import RPCError
from ..extensions import markdown
from ..tl import TLObject, types, functions
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.). 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``.
"""
def __init__(self, chats=None, blacklist_chats=False):
self.chats = chats
self.blacklist_chats = blacklist_chats
self._self_id = None
@abc.abstractmethod
def build(self, update):
"""Builds an event for the given update if possible, or returns None"""
async def resolve(self, client):
"""Helper method to allow event builders to be resolved before usage"""
self.chats = await _into_id_set(client, self.chats)
self._self_id = await client.get_me(input_peer=True).user_id
def _filter_event(self, event):
"""
If the ID of ``event._chat_peer`` isn't in the chats set (or it is
but the set is a blacklist) returns ``None``, otherwise the event.
"""
if self.chats is not None:
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 None
return event
class EventCommon(abc.ABC):
"""Intermediate class with common things to all events"""
_event_name = 'Event'
def __init__(self, chat_peer=None, msg_id=None, broadcast=False):
self._entities = {}
self._client = None
self._chat_peer = chat_peer
self._message_id = msg_id
self._input_chat = None
self._chat = None
self.pattern_match = 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)
async def _get_entity(self, msg_id, entity_id, chat=None):
"""
Helper function to call :tl:`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 :tl:`InputPeer`.
Returns a tuple of ``(entity, input_peer)`` if it was found, or
a tuple of ``(None, None)`` if it couldn't be.
"""
try:
if isinstance(chat, types.InputPeerChannel):
result = await self._client(
functions.channels.GetMessagesRequest(chat, [
types.InputMessageID(msg_id)
])
)
else:
result = await self._client(
functions.messages.GetMessagesRequest([
types.InputMessageID(msg_id)
])
)
except RPCError:
return None, None
entity = {
utils.get_peer_id(x): x for x in itertools.chain(
getattr(result, 'chats', []),
getattr(result, 'users', []))
}.get(entity_id)
if entity:
return entity, utils.get_input_peer(entity)
else:
return None, None
@property
async def input_chat(self):
"""
The (:tl:`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 = await 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._chat, self._input_chat = await self._get_entity(
self._message_id,
utils.get_peer_id(self._chat_peer)
)
return self._input_chat
@property
def client(self):
return self._client
@property
async def chat(self):
"""
The (:tl:`User` | :tl:`Chat` | :tl:`Channel`, optional) on which
the event occurred. This property may make an API call the first time
to get the most up to date version of the chat (mostly when the event
doesn't belong to a channel), so keep that in mind.
"""
if not await self.input_chat:
return None
if self._chat is None:
self._chat = self._entities.get(utils.get_peer_id(self._input_chat))
if self._chat is None:
self._chat = await self._client.get_entity(self._input_chat)
return self._chat
@property
def chat_id(self):
"""
Returns the marked integer ID of the chat, if any.
"""
if self._chat_peer:
return utils.get_peer_id(self._chat_peer)
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
class Raw(EventBuilder):
"""
Represents a raw event. The event is the update itself.
"""
async def resolve(self, client):
pass
def build(self, update):
return update
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,33 @@
from .common import EventBuilder, EventCommon, name_inner_event
from ..tl import types
@name_inner_event
class MessageDeleted(EventBuilder):
"""
Event fired when one or more messages are deleted.
"""
def build(self, update):
if isinstance(update, types.UpdateDeleteMessages):
event = MessageDeleted.Event(
deleted_ids=update.messages,
peer=None
)
elif isinstance(update, types.UpdateDeleteChannelMessages):
event = MessageDeleted.Event(
deleted_ids=update.messages,
peer=types.PeerChannel(update.channel_id)
)
else:
return
event._entities = update._entities
return self._filter_event(event)
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,22 @@
from .common import name_inner_event
from .newmessage import NewMessage
from ..tl import types
@name_inner_event
class MessageEdited(NewMessage):
"""
Event fired when a message has been edited.
"""
def build(self, update):
if isinstance(update, (types.UpdateEditMessage,
types.UpdateEditChannelMessage)):
event = MessageEdited.Event(update.message)
else:
return
event._entities = update._entities
return self._message_filter_event(event)
class Event(NewMessage.Event):
pass # Required if we want a different name for it

View File

@ -0,0 +1,137 @@
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types, functions
@name_inner_event
class MessageRead(EventBuilder):
"""
Event fired when one or more messages have been read.
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.
"""
def __init__(self, inbox=False, chats=None, blacklist_chats=None):
super().__init__(chats, blacklist_chats)
self.inbox = inbox
def build(self, update):
if isinstance(update, types.UpdateReadHistoryInbox):
event = MessageRead.Event(update.peer, update.max_id, False)
elif isinstance(update, types.UpdateReadHistoryOutbox):
event = MessageRead.Event(update.peer, update.max_id, True)
elif isinstance(update, types.UpdateReadChannelInbox):
event = MessageRead.Event(types.PeerChannel(update.channel_id),
update.max_id, False)
elif isinstance(update, types.UpdateReadChannelOutbox):
event = MessageRead.Event(types.PeerChannel(update.channel_id),
update.max_id, True)
elif isinstance(update, types.UpdateReadMessagesContents):
event = MessageRead.Event(message_ids=update.messages,
contents=True)
elif isinstance(update, types.UpdateChannelReadMessagesContents):
event = MessageRead.Event(types.PeerChannel(update.channel_id),
message_ids=update.messages,
contents=True)
else:
return
if self.inbox == event.outbox:
return
event._entities = update._entities
return self._filter_event(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
@property
async def messages(self):
"""
The list of :tl:`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 = self.input_chat
if not chat:
self._messages = []
elif isinstance(chat, types.InputPeerChannel):
ids = [types.InputMessageID(x) for x in self._message_ids]
self._messages =\
await self._client(functions.channels.GetMessagesRequest(
chat, ids
)).messages
else:
ids = [types.InputMessageID(x) for x in self._message_ids]
self._messages = \
await self._client(functions.messages.GetMessagesRequest(
ids
)).messages
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,419 @@
import abc
import datetime
import itertools
import re
import warnings
from .. import utils
from ..errors import RPCError
from ..extensions import markdown
from ..tl import TLObject, types, functions
from .common import EventBuilder, EventCommon, name_inner_event
@name_inner_event
class NewMessage(EventBuilder):
"""
Represents a new message event builder.
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).
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.
"""
def __init__(self, incoming=None, outgoing=None,
chats=None, blacklist_chats=False, pattern=None):
if incoming and outgoing:
raise ValueError('Can only set either incoming or outgoing')
super().__init__(chats=chats, blacklist_chats=blacklist_chats)
self.incoming = incoming
self.outgoing = outgoing
if isinstance(pattern, str):
self.pattern = re.compile(pattern).match
elif not pattern or callable(pattern):
self.pattern = pattern
elif hasattr(pattern, 'match') and callable(pattern.match):
self.pattern = pattern.match
else:
raise TypeError('Invalid pattern type given')
def 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),
from_id=self._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_msg_id=update.reply_to_msg_id,
entities=update.entities
))
elif isinstance(update, types.UpdateShortChatMessage):
event = NewMessage.Event(types.Message(
out=update.out,
mentioned=update.mentioned,
media_unread=update.media_unread,
silent=update.silent,
id=update.id,
from_id=update.from_id,
to_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_msg_id=update.reply_to_msg_id,
entities=update.entities
))
else:
return
event._entities = update._entities
return self._message_filter_event(event)
def _message_filter_event(self, event):
# Short-circuit if we let pass all events
if all(x is None for x in (self.incoming, self.outgoing, self.chats,
self.pattern)):
return event
if self.incoming and event.message.out:
return
if self.outgoing and not event.message.out:
return
if self.pattern:
match = self.pattern(event.message.message or '')
if not match:
return
event.pattern_match = match
return self._filter_event(event)
class Event(EventCommon):
"""
Represents the event of a new message.
Members:
message (:tl:`Message`):
This is the original :tl:`Message` object.
is_private (`bool`):
True if the message was sent as a private message.
is_group (`bool`):
True if the message was sent on a group or megagroup.
is_channel (`bool`):
True if the message was sent on a megagroup or channel.
is_reply (`str`):
Whether the message is a reply to some other or not.
"""
def __init__(self, message):
if not message.out and isinstance(message.to_id, types.PeerUser):
# Incoming message (e.g. from a bot) has to_id=us, and
# from_id=bot (the actual "chat" from an user's perspective).
chat_peer = types.PeerUser(message.from_id)
else:
chat_peer = message.to_id
super().__init__(chat_peer=chat_peer,
msg_id=message.id, broadcast=bool(message.post))
self.message = message
self._text = None
self._input_sender = None
self._sender = None
self.is_reply = bool(message.reply_to_msg_id)
self._reply_message = None
async def respond(self, *args, **kwargs):
"""
Responds to the message (not as a reply). This is a shorthand for
``client.send_message(event.chat, ...)``.
"""
return await self._client.send_message(self.input_chat, *args, **kwargs)
async 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)``.
"""
kwargs['reply_to'] = self.message.id
return await self._client.send_message(self.input_chat, *args, **kwargs)
async def forward_to(self, *args, **kwargs):
"""
Forwards the message. This is a shorthand for
``client.forward_messages(entity, event.message, event.chat)``.
"""
kwargs['messages'] = self.message.id
kwargs['from_peer'] = self.input_chat
return await self._client.forward_messages(*args, **kwargs)
async def edit(self, *args, **kwargs):
"""
Edits the message iff it's outgoing. This is a shorthand for
``client.edit_message(event.chat, event.message, ...)``.
Returns ``None`` if the message was incoming,
or the edited message otherwise.
"""
if self.message.fwd_from:
return None
if not self.message.out:
if not isinstance(self.message.to_id, types.PeerUser):
return None
me = await self._client.get_me(input_peer=True)
if self.message.to_id.user_id != me.user_id:
return None
return await self._client.edit_message(self.input_chat,
self.message,
*args, **kwargs)
async def delete(self, *args, **kwargs):
"""
Deletes the message. You're responsible for checking whether you
have the permission to do so, or to except the error otherwise.
This is a shorthand for
``client.delete_messages(event.chat, event.message, ...)``.
"""
return await self._client.delete_messages(self.input_chat,
[self.message],
*args, **kwargs)
@property
async def input_sender(self):
"""
This (:tl:`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, or if the message a broadcast on a channel.
"""
if self._input_sender is None:
if self.is_channel and not self.is_group:
return None
try:
self._input_sender = await self._client.get_input_entity(
self.message.from_id
)
except (ValueError, TypeError):
# We can rely on self.input_chat for this
self._sender, self._input_sender = self._get_entity(
self.message.id,
self.message.from_id,
chat=self.input_chat
)
return self._input_sender
@property
async def sender(self):
"""
This (:tl:`User`) may make an API call the first time to get
the most up to date version of the sender (mostly when the event
doesn't belong to a channel), so keep that in mind.
``input_sender`` needs to be available (often the case).
"""
if not await self.input_sender:
return None
if self._sender is None:
self._sender = \
self._entities.get(utils.get_peer_id(self._input_sender))
if self._sender is None:
self._sender = await self._client.get_entity(self._input_sender)
return self._sender
@property
def sender_id(self):
"""
Returns the marked sender integer ID, if present.
"""
if self.input_sender:
return utils.get_peer_id(self._input_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
async def reply_message(self):
"""
This optional :tl:`Message` will make an API call the first
time to get the full :tl:`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 = await self._client(functions.channels.GetMessagesRequest(
self.input_chat, [
types.InputMessageID(self.message.reply_to_msg_id)
]
))
else:
r = await self._client(functions.messages.GetMessagesRequest(
[types.InputMessageID(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 :tl:`MessageFwdHeader`, if present..
"""
return self.message.fwd_from
@property
def media(self):
"""
The unmodified :tl:`MessageMedia`, if present.
"""
return self.message.media
@property
def photo(self):
"""
If the message media is a photo,
this returns the :tl:`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 :tl:`Document` object.
"""
if isinstance(self.message.media, types.MessageMediaDocument):
doc = self.message.media.document
if isinstance(doc, types.Document):
return doc
def _document_by_attribute(self, kind, condition=None):
"""
Helper method to return the document only if it has an attribute
that's an instance of the given kind, and passes the condition.
"""
doc = self.document
if doc:
for attr in doc.attributes:
if isinstance(attr, kind):
if not condition or condition(doc):
return doc
@property
def audio(self):
"""
If the message media is a document with an Audio attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeAudio,
lambda attr: not attr.voice)
@property
def voice(self):
"""
If the message media is a document with a Voice attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeAudio,
lambda attr: attr.voice)
@property
def video(self):
"""
If the message media is a document with a Video attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeVideo)
@property
def video_note(self):
"""
If the message media is a document with a Video attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeVideo,
lambda attr: attr.round_message)
@property
def gif(self):
"""
If the message media is a document with an Animated attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeAnimated)
@property
def sticker(self):
"""
If the message media is a document with a Sticker attribute,
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeSticker)
@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

View File

@ -0,0 +1,163 @@
import datetime
from .common import EventBuilder, EventCommon, name_inner_event
from ..tl import types
@name_inner_event
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
event._entities = update._entities
return self._filter_event(event)
class Event(EventCommon):
"""
Represents the event of an user status update (last seen, joined).
Members:
online (`bool`, optional):
``True`` if the user is currently online, ``False`` otherwise.
Might be ``None`` if this information is not present.
last_seen (`datetime`, optional):
Exact date when the user was last seen if known.
until (`datetime`, optional):
Until when will the user remain online.
within_months (`bool`):
``True`` if the user was seen within 30 days.
within_weeks (`bool`):
``True`` if the user was seen within 7 days.
recently (`bool`):
``True`` if the user was seen within a day.
action (:tl:`SendMessageAction`, optional):
The "typing" action if any the user is performing if any.
cancel (`bool`):
``True`` if the action was cancelling other actions.
typing (`bool`):
``True`` if the action is typing a message.
recording (`bool`):
``True`` if the action is recording something.
uploading (`bool`):
``True`` if the action is uploading something.
playing (`bool`):
``True`` if the action is playing a game.
audio (`bool`):
``True`` if what's being recorded/uploaded is an audio.
round (`bool`):
``True`` if what's being recorded/uploaded is a round video.
video (`bool`):
``True`` if what's being recorded/uploaded is an video.
document (`bool`):
``True`` if what's being uploaded is document.
geo (`bool`):
``True`` if what's being uploaded is a geo.
photo (`bool`):
``True`` if what's being uploaded is a photo.
contact (`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
async def user(self):
"""Alias around the chat (conversation)."""
return await self.chat
@property
async def input_user(self):
"""Alias around the input chat."""
return await self.input_chat
@property
def user_id(self):
"""Alias around `chat_id`."""
return self.chat_id

View File

@ -14,6 +14,7 @@ from io import BytesIO
from mimetypes import guess_type
from .crypto import CdnDecrypter
from .tl import TLObject
from .tl.custom import InputSizedFile
from .tl.functions.upload import (
SaveBigFilePartRequest, SaveFilePartRequest, GetFileRequest
@ -38,8 +39,7 @@ from .errors import (
RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError,
PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError,
SessionPasswordNeededError, FileMigrateError, PhoneNumberUnoccupiedError,
PhoneNumberOccupiedError, EmailUnconfirmedError, PasswordEmptyError,
UsernameNotOccupiedError
PhoneNumberOccupiedError, UsernameNotOccupiedError
)
from .network import ConnectionMode
from .tl.custom import Draft, Dialog
@ -84,7 +84,8 @@ from .tl.types import (
InputMessageEntityMentionName, DocumentAttributeVideo,
UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates,
MessageMediaWebPage, ChannelParticipantsSearch, PhotoSize, PhotoCachedSize,
PhotoSizeEmpty, MessageService, ChatParticipants
PhotoSizeEmpty, MessageService, ChatParticipants,
ChannelParticipantsBanned, ChannelParticipantsKicked
)
from .tl.types.messages import DialogsSlice
from .tl.types.account import PasswordInputSettings, NoPassword
@ -354,7 +355,14 @@ class TelegramClient(TelegramBareClient):
me = await self.sign_in(phone=phone, password=password)
# We won't reach here if any step failed (exit by exception)
print('Signed in successfully as', utils.get_display_name(me))
signed, name = 'Signed in successfully as', utils.get_display_name(me)
try:
print(signed, name)
except UnicodeEncodeError:
# Some terminals don't support certain characters
print(signed, name.encode('utf-8', errors='ignore')
.decode('ascii', errors='ignore'))
await self._check_events_pending_resolve()
return self
@ -712,6 +720,13 @@ class TelegramClient(TelegramBareClient):
"""
Sends the given message to the specified entity (user/chat/channel).
The default parse mode is the same as the official applications
(a custom flavour of markdown). ``**bold**, `code` or __italic__``
are available. In addition you can send ``[links](https://example.com)``
and ``[mentions](@username)`` (or using IDs like in the Bot API:
``[mention](tg://user?id=123456789)``) and ``pre`` blocks with three
backticks.
Args:
entity (`entity`):
To who will it be sent.
@ -820,9 +835,11 @@ class TelegramClient(TelegramBareClient):
order for the forward to work.
Returns:
The list of forwarded :tl:`Message`.
The list of forwarded :tl:`Message`, or a single one if a list
wasn't provided as input.
"""
if not utils.is_list_like(messages):
single = not utils.is_list_like(messages)
if single:
messages = (messages,)
if not from_peer:
@ -853,7 +870,8 @@ class TelegramClient(TelegramBareClient):
elif isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage)):
id_to_message[update.message.id] = update.message
return [id_to_message[random_to_id[rnd]] for rnd in req.random_id]
result = [id_to_message[random_to_id[rnd]] for rnd in req.random_id]
return result[0] if single else result
async def edit_message(self, entity, message_id, message=None,
parse_mode='md', link_preview=True):
@ -1205,7 +1223,12 @@ class TelegramClient(TelegramBareClient):
or :tl:`ChatParticipants` for normal chats.
"""
if isinstance(filter, type):
filter = filter()
if filter in (ChannelParticipantsBanned, ChannelParticipantsKicked,
ChannelParticipantsSearch):
# These require a `q` parameter (support types for convenience)
filter = filter('')
else:
filter = filter()
entity = await self.get_input_entity(entity)
if search and (filter or not isinstance(entity, InputPeerChannel)):
@ -1404,7 +1427,8 @@ class TelegramClient(TelegramBareClient):
it will be used to determine metadata from audio and video files.
Returns:
The :tl:`Message` (or messages) containing the sent file.
The :tl:`Message` (or messages) containing the sent file,
or messages if a list of them was passed.
"""
# First check if the user passed an iterable, in which case
# we may want to send as an album if all are photo files.
@ -2255,8 +2279,10 @@ class TelegramClient(TelegramBareClient):
if event and not isinstance(event, type):
event = type(event)
for i, ec in enumerate(self._event_builders):
ev, cb = ec
i = len(self._event_builders)
while i:
i -= 1
ev, cb = self._event_builders[i]
if cb == callback and (not event or isinstance(ev, event)):
del self._event_builders[i]
found += 1
@ -2313,13 +2339,11 @@ class TelegramClient(TelegramBareClient):
error will be raised.
Returns:
:tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the input
entity.
:tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the
input entity. A list will be returned if more than one was given.
"""
if utils.is_list_like(entity):
single = False
else:
single = True
single = not utils.is_list_like(entity)
if single:
entity = (entity,)
# Group input entities by string (resolve username),
@ -2443,18 +2467,12 @@ class TelegramClient(TelegramBareClient):
if isinstance(peer, str):
return utils.get_input_peer(await self._get_entity_from_string(peer))
original_peer = peer
if not isinstance(peer, int):
try:
if peer.SUBCLASS_OF_ID != 0x2d45687: # crc32(b'Peer')
return utils.get_input_peer(peer)
except (AttributeError, TypeError):
peer = None
if not peer:
raise TypeError(
'Cannot turn "{}" into an input entity.'.format(original_peer)
)
if not isinstance(peer, int) and (not isinstance(peer, TLObject)
or peer.SUBCLASS_OF_ID != 0x2d45687):
# Try casting the object into an input peer. Might TypeError.
# Don't do it if a not-found ID was given (instead ValueError).
# Also ignore Peer (0x2d45687 == crc32(b'Peer'))'s, lacking hash.
return utils.get_input_peer(peer)
raise ValueError(
'Could not find the input entity corresponding to "{}". '

View File

@ -91,7 +91,11 @@ def get_input_peer(entity, allow_self=True):
if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer')
return entity
except AttributeError:
_raise_cast_fail(entity, 'InputPeer')
if hasattr(entity, 'input_entity'):
# e.g. custom.Dialog (can't cyclic import)
return entity.input_entity
else:
_raise_cast_fail(entity, 'InputPeer')
if isinstance(entity, User):
if entity.is_self and allow_self:
@ -105,7 +109,6 @@ def get_input_peer(entity, allow_self=True):
if isinstance(entity, (Channel, ChannelForbidden)):
return InputPeerChannel(entity.id, entity.access_hash or 0)
# Less common cases
if isinstance(entity, InputUser):
return InputPeerUser(entity.user_id, entity.access_hash)