Merge branch 'master' into asyncio

This commit is contained in:
Lonami Exo 2018-03-04 12:14:20 +01:00
commit 69970b5b20
10 changed files with 242 additions and 46 deletions

View File

@ -112,6 +112,15 @@ as you wish. Remember to use the right types! To sum up:
)) ))
This can further be simplified to:
.. code-block:: python
result = client(SendMessageRequest('username', 'Hello there!'))
# Or even
result = client(SendMessageRequest(PeerChannel(id), 'Hello there!'))
.. note:: .. note::
Note that some requests have a "hash" parameter. This is **not** Note that some requests have a "hash" parameter. This is **not**

View File

@ -37,12 +37,24 @@ you're able to just do this:
# Using Peer/InputPeer (note that the API may return these) # Using Peer/InputPeer (note that the API may return these)
# users, chats and channels may all have the same ID, so it's # users, chats and channels may all have the same ID, so it's
# necessary to wrap (at least) chat and channels inside Peer. # necessary to wrap (at least) chat and channels inside Peer.
#
# NOTICE how the IDs *must* be wrapped inside a Peer() so the
# library knows their type.
from telethon.tl.types import PeerUser, PeerChat, PeerChannel from telethon.tl.types import PeerUser, PeerChat, PeerChannel
my_user = client.get_entity(PeerUser(some_id)) my_user = client.get_entity(PeerUser(some_id))
my_chat = client.get_entity(PeerChat(some_id)) my_chat = client.get_entity(PeerChat(some_id))
my_channel = client.get_entity(PeerChannel(some_id)) my_channel = client.get_entity(PeerChannel(some_id))
.. warning::
As it has been mentioned already, getting the entity of a channel
through e.g. ``client.get_entity(channel id)`` will **not** work.
You would use ``client.get_entity(types.PeerChannel(channel id))``.
Remember that supergroups are channels and normal groups are chats.
This is a common mistake!
All methods in the :ref:`telegram-client` call ``.get_input_entity()`` prior All methods in the :ref:`telegram-client` call ``.get_input_entity()`` prior
to sending the requst to save you from the hassle of doing so manually. to sending the requst to save you from the hassle of doing so manually.
That way, convenience calls such as ``client.send_message('lonami', 'hi!')`` That way, convenience calls such as ``client.send_message('lonami', 'hi!')``

View File

@ -99,6 +99,10 @@ done! The event that will be passed always is of type ``XYZ.Event`` (for
instance, ``NewMessage.Event``), except for the ``Raw`` event which just instance, ``NewMessage.Event``), except for the ``Raw`` event which just
passes the ``Update`` object. passes the ``Update`` object.
Note that ``.reply()`` and ``.respond()`` are just wrappers around the
``client.send_message()`` method which supports the ``file=`` parameter.
This means you can reply with a photo if you do ``client.reply(file=photo)``.
You can put the same event on many handlers, and even different events on 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, the same handler. You can also have a handler work on only specific chats,
for example: for example:

View File

@ -14,6 +14,71 @@ it can take advantage of new goodies!
.. contents:: List of All Versions .. contents:: List of All Versions
Sessions overhaul (v0.18)
=========================
*Published at 2018/03/04*
+-----------------------+
| Scheme layer used: 75 |
+-----------------------+
The ``Session``'s have been revisited thanks to the work of **@tulir** and
they now use an `ABC <https://docs.python.org/3/library/abc.html>`__ so you
can easily implement your own!
The default will still be a ``SQLiteSession``, but you might want to use
the new ``AlchemySessionContainer`` if you need. Refer to the section of
the documentation on :ref:`sessions` for more.
Breaking changes
~~~~~~~~~~~~~~~~
- ``events.MessageChanged`` doesn't exist anymore. Use the new
``events.MessageEdited`` and ``events.MessageDeleted`` instead.
Additions
~~~~~~~~~
- The mentioned addition of new session types.
- You can omit the event type on ``client.add_event_handler`` to use ``Raw``.
- You can ``raise StopPropagation`` of events if you added several of them.
- ``.get_participants()`` can now get up to 90,000 members from groups with
100,000 if when ``aggressive=True``, "bypassing" Telegram's limit.
- You now can access ``NewMessage.Event.pattern_match``.
- Multiple captions are now supported when sending albums.
- ``client.send_message()`` has an optional ``file=`` parameter, so
you can do ``events.reply(file='/path/to/photo.jpg')`` and similar.
- Added ``.input_`` versions to ``events.ChatAction``.
- You can now access the public ``.client`` property on ``events``.
- New ``client.forward_messages``, with its own wrapper on ``events``,
called ``event.forward_to(...)``.
Bug fixes
~~~~~~~~~
- Silly bug regarding ``client.get_me(input_peer=True)``.
- ``client.send_voice_note()`` was missing some parameters.
- ``client.send_file()`` plays better with streams now.
- Incoming messages from bots weren't working with whitelists.
- Markdown's URL regex was not accepting newlines.
- Better attempt at joining background update threads.
- Use the right peer type when a marked integer ID is provided.
Internal changes
~~~~~~~~~~~~~~~~
- Resolving ``events.Raw`` is now a no-op.
- Logging calls in the ``TcpClient`` to spot errors.
- ``events`` resolution is postponed until you are successfully connected,
so you can attach them before starting the client.
- When an entity is not found, it is searched in *all* dialogs. This might
not always be desirable but it's more comfortable for legitimate uses.
- Some non-persisting properties from the ``Session`` have been moved out.
Further easing library usage (v0.17.4) Further easing library usage (v0.17.4)
====================================== ======================================

View File

@ -148,7 +148,11 @@ def main():
keywords='telegram api chat client library messaging mtproto', keywords='telegram api chat client library messaging mtproto',
packages=find_packages(exclude=[ packages=find_packages(exclude=[
'telethon_generator', 'telethon_tests', 'run_tests.py', 'telethon_generator', 'telethon_tests', 'run_tests.py',
'try_telethon.py' 'try_telethon.py',
'telethon_generator/parser/__init__.py',
'telethon_generator/parser/source_builder.py',
'telethon_generator/parser/tl_object.py',
'telethon_generator/parser/tl_parser.py',
]), ]),
install_requires=['pyaes', 'rsa'], install_requires=['pyaes', 'rsa'],
extras_require={ extras_require={

View File

@ -140,6 +140,9 @@ class _EventCommon(abc.ABC):
) )
return self._input_chat return self._input_chat
def client(self):
return self._client
@property @property
async def chat(self): async def chat(self):
""" """
@ -316,10 +319,19 @@ class NewMessage(_EventBuilder):
Replies to the message (as a reply). This is a shorthand for Replies to the message (as a reply). This is a shorthand for
``client.send_message(event.chat, ..., reply_to=event.message.id)``. ``client.send_message(event.chat, ..., reply_to=event.message.id)``.
""" """
return await self._client.send_message(await self.input_chat, kwargs['reply_to'] = self.message.id
reply_to=self.message.id, return await self._client.send_message(self.input_chat,
*args, **kwargs) *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): async def edit(self, *args, **kwargs):
""" """
Edits the message iff it's outgoing. This is a shorthand for Edits the message iff it's outgoing. This is a shorthand for
@ -525,15 +537,19 @@ class ChatAction(_EventBuilder):
elif isinstance(action, types.MessageActionChannelCreate): elif isinstance(action, types.MessageActionChannelCreate):
event = ChatAction.Event(msg.to_id, event = ChatAction.Event(msg.to_id,
created=True, created=True,
users=msg.from_id,
new_title=action.title) new_title=action.title)
elif isinstance(action, types.MessageActionChatEditTitle): elif isinstance(action, types.MessageActionChatEditTitle):
event = ChatAction.Event(msg.to_id, event = ChatAction.Event(msg.to_id,
users=msg.from_id,
new_title=action.title) new_title=action.title)
elif isinstance(action, types.MessageActionChatEditPhoto): elif isinstance(action, types.MessageActionChatEditPhoto):
event = ChatAction.Event(msg.to_id, event = ChatAction.Event(msg.to_id,
users=msg.from_id,
new_photo=action.photo) new_photo=action.photo)
elif isinstance(action, types.MessageActionChatDeletePhoto): elif isinstance(action, types.MessageActionChatDeletePhoto):
event = ChatAction.Event(msg.to_id, event = ChatAction.Event(msg.to_id,
users=msg.from_id,
new_photo=True) new_photo=True)
else: else:
return return
@ -607,6 +623,7 @@ class ChatAction(_EventBuilder):
self.created = bool(created) self.created = bool(created)
self._user_peers = users if isinstance(users, list) else [users] self._user_peers = users if isinstance(users, list) else [users]
self._users = None self._users = None
self._input_users = None
self.new_title = new_title self.new_title = new_title
@property @property
@ -660,10 +677,16 @@ class ChatAction(_EventBuilder):
Might be ``None`` if the information can't be retrieved or Might be ``None`` if the information can't be retrieved or
there is no user taking part. there is no user taking part.
""" """
try: if await self.users:
return next(await self.users) return self._users[0]
except (StopIteration, TypeError):
return None @property
async def input_user(self):
"""
Input version of the self.user property.
"""
if await self.input_users:
return self._input_users[0]
@property @property
async def users(self): async def users(self):
@ -681,6 +704,22 @@ class ChatAction(_EventBuilder):
return self._users 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
class UserUpdate(_EventBuilder): class UserUpdate(_EventBuilder):
""" """
@ -829,21 +868,32 @@ class UserUpdate(_EventBuilder):
return self.chat return self.chat
class MessageChanged(_EventBuilder): class MessageEdited(NewMessage):
""" """
Represents a message changed (edited or deleted). Event fired when a message has been edited.
""" """
def build(self, update): def build(self, update):
if isinstance(update, (types.UpdateEditMessage, if isinstance(update, (types.UpdateEditMessage,
types.UpdateEditChannelMessage)): types.UpdateEditChannelMessage)):
event = MessageChanged.Event(edit_msg=update.message) event = MessageEdited.Event(update.message)
elif isinstance(update, types.UpdateDeleteMessages): else:
event = MessageChanged.Event( return
return self._filter_event(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, deleted_ids=update.messages,
peer=None peer=None
) )
elif isinstance(update, types.UpdateDeleteChannelMessages): elif isinstance(update, types.UpdateDeleteChannelMessages):
event = MessageChanged.Event( event = MessageDeleted.Event(
deleted_ids=update.messages, deleted_ids=update.messages,
peer=types.PeerChannel(update.channel_id) peer=types.PeerChannel(update.channel_id)
) )
@ -852,33 +902,13 @@ class MessageChanged(_EventBuilder):
return self._filter_event(event) return self._filter_event(event)
class Event(NewMessage.Event): class Event(_EventCommon):
""" def __init__(self, deleted_ids, peer):
Represents the event of an user status update (last seen, joined). super().__init__(
types.Message((deleted_ids or [0])[0], peer, None, '')
Please note that the ``message`` member will be ``None`` if the )
action was a deletion and not an edit. self.deleted_id = None if not deleted_ids else deleted_ids[0]
self.deleted_ids = self.deleted_ids
Members:
edited (:obj:`bool`):
``True`` if the message was edited.
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.
"""
def __init__(self, edit_msg=None, deleted_ids=None, peer=None):
if edit_msg is None:
msg = types.Message((deleted_ids or [0])[0], peer, None, '')
else:
msg = edit_msg
super().__init__(msg)
self.edited = bool(edit_msg)
self.deleted = bool(deleted_ids)
self.deleted_ids = deleted_ids or []
class StopPropagation(Exception): class StopPropagation(Exception):

View File

@ -125,7 +125,7 @@ class MemorySession(Session):
return rows return rows
def process_entities(self, tlo): def process_entities(self, tlo):
self._entities += set(self._entities_to_rows(tlo)) self._entities |= set(self._entities_to_rows(tlo))
def get_entity_rows_by_phone(self, phone): def get_entity_rows_by_phone(self, phone):
try: try:

View File

@ -56,7 +56,8 @@ from .tl.functions.messages import (
GetDialogsRequest, GetHistoryRequest, SendMediaRequest, GetDialogsRequest, GetHistoryRequest, SendMediaRequest,
SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest,
CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest, CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest,
UploadMediaRequest, EditMessageRequest, GetFullChatRequest UploadMediaRequest, EditMessageRequest, GetFullChatRequest,
ForwardMessagesRequest
) )
from .tl.functions import channels from .tl.functions import channels
@ -665,8 +666,9 @@ class TelegramClient(TelegramBareClient):
return message, msg_entities return message, msg_entities
async def send_message(self, entity, message, reply_to=None, async def send_message(self, entity, message='', reply_to=None,
parse_mode='md', link_preview=True): parse_mode='md', link_preview=True, file=None,
force_document=False):
""" """
Sends the given message to the specified entity (user/chat/channel). Sends the given message to the specified entity (user/chat/channel).
@ -690,9 +692,25 @@ class TelegramClient(TelegramBareClient):
link_preview (:obj:`bool`, optional): link_preview (:obj:`bool`, optional):
Should the link preview be shown? Should the link preview be shown?
file (:obj:`file`, optional):
Sends a message with a file attached (e.g. a photo,
video, audio or document). The ``message`` may be empty.
force_document (:obj:`bool`, optional):
Whether to send the given file as a document or not.
Returns: Returns:
the sent message the sent message
""" """
if file is not None:
return await self.send_file(
entity, file, caption=message, reply_to=reply_to,
parse_mode=parse_mode, force_document=force_document
)
elif not message:
raise ValueError(
'The message cannot be empty unless a file is provided'
)
entity = await self.get_input_entity(entity) entity = await self.get_input_entity(entity)
if isinstance(message, Message): if isinstance(message, Message):
@ -739,6 +757,58 @@ class TelegramClient(TelegramBareClient):
return self._get_response_message(request, result) return self._get_response_message(request, result)
async def forward_messages(self, entity, messages, from_peer=None):
"""
Forwards the given message(s) to the specified entity.
Args:
entity (:obj:`entity`):
To which entity the message(s) will be forwarded.
messages (:obj:`list` | :obj:`int` | :obj:`Message`):
The message(s) to forward, or their integer IDs.
from_peer (:obj:`entity`):
If the given messages are integer IDs and not instances
of the ``Message`` class, this *must* be specified in
order for the forward to work.
Returns:
The forwarded messages.
"""
if not utils.is_list_like(messages):
messages = (messages,)
if not from_peer:
try:
# On private chats (to_id = PeerUser), if the message is
# not outgoing, we actually need to use "from_id" to get
# the conversation on which the message was sent.
from_peer = next(
m.from_id if not m.out and isinstance(m.to_id, PeerUser)
else m.to_id for m in messages if isinstance(m, Message)
)
except StopIteration:
raise ValueError(
'from_chat must be given if integer IDs are used'
)
req = ForwardMessagesRequest(
from_peer=from_peer,
id=[m if isinstance(m, int) else m.id for m in messages],
to_peer=entity
)
result = await self(req)
random_to_id = {}
id_to_message = {}
for update in result.updates:
if isinstance(update, UpdateMessageID):
random_to_id[update.random_id] = update.id
elif isinstance(update, UpdateNewMessage):
id_to_message[update.message.id] = update.message
return [id_to_message[random_to_id[rnd]] for rnd in req.random_id]
async def edit_message(self, entity, message_id, message=None, async def edit_message(self, entity, message_id, message=None,
parse_mode='md', link_preview=True): parse_mode='md', link_preview=True):
""" """

View File

@ -6,6 +6,7 @@ import math
import mimetypes import mimetypes
import re import re
import types import types
from collections import UserList
from mimetypes import add_type, guess_extension from mimetypes import add_type, guess_extension
from .tl.types import ( from .tl.types import (
@ -342,7 +343,8 @@ def is_list_like(obj):
enough. Things like open() are also iterable (and probably many enough. Things like open() are also iterable (and probably many
other things), so just support the commonly known list-like objects. other things), so just support the commonly known list-like objects.
""" """
return isinstance(obj, (list, tuple, set, dict, types.GeneratorType)) return isinstance(obj, (list, tuple, set, dict,
UserList, types.GeneratorType))
def parse_phone(phone): def parse_phone(phone):

View File

@ -1,3 +1,3 @@
# Versions should comply with PEP440. # Versions should comply with PEP440.
# This line is parsed in setup.py: # This line is parsed in setup.py:
__version__ = '0.17.4' __version__ = '0.18'