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 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)
# users, chats and channels may all have the same ID, so it's
# 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
my_user = client.get_entity(PeerUser(some_id))
my_chat = client.get_entity(PeerChat(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
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!')``

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
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
the same handler. You can also have a handler work on only specific chats,
for example:

View File

@ -14,6 +14,71 @@ it can take advantage of new goodies!
.. 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)
======================================

View File

@ -148,7 +148,11 @@ def main():
keywords='telegram api chat client library messaging mtproto',
packages=find_packages(exclude=[
'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'],
extras_require={

View File

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

View File

@ -125,7 +125,7 @@ class MemorySession(Session):
return rows
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):
try:

View File

@ -56,7 +56,8 @@ from .tl.functions.messages import (
GetDialogsRequest, GetHistoryRequest, SendMediaRequest,
SendMessageRequest, GetChatsRequest, GetAllDraftsRequest,
CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest,
UploadMediaRequest, EditMessageRequest, GetFullChatRequest
UploadMediaRequest, EditMessageRequest, GetFullChatRequest,
ForwardMessagesRequest
)
from .tl.functions import channels
@ -665,8 +666,9 @@ class TelegramClient(TelegramBareClient):
return message, msg_entities
async def send_message(self, entity, message, reply_to=None,
parse_mode='md', link_preview=True):
async def send_message(self, entity, message='', reply_to=None,
parse_mode='md', link_preview=True, file=None,
force_document=False):
"""
Sends the given message to the specified entity (user/chat/channel).
@ -690,9 +692,25 @@ class TelegramClient(TelegramBareClient):
link_preview (:obj:`bool`, optional):
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:
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)
if isinstance(message, Message):
@ -739,6 +757,58 @@ class TelegramClient(TelegramBareClient):
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,
parse_mode='md', link_preview=True):
"""

View File

@ -6,6 +6,7 @@ import math
import mimetypes
import re
import types
from collections import UserList
from mimetypes import add_type, guess_extension
from .tl.types import (
@ -342,7 +343,8 @@ def is_list_like(obj):
enough. Things like open() are also iterable (and probably many
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):

View File

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