diff --git a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst index 7276aa43..edbe821d 100644 --- a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst +++ b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst @@ -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** diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index b68a74d7..84be3250 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -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!')`` diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index 652f6000..3c57b792 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -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: diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index ad027361..e8876d5c 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -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 `__ 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) ====================================== diff --git a/setup.py b/setup.py index 003d7c6d..8e5d5bcd 100755 --- a/setup.py +++ b/setup.py @@ -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={ diff --git a/telethon_aio/events/__init__.py b/telethon_aio/events/__init__.py index 6c909180..39f852fd 100644 --- a/telethon_aio/events/__init__.py +++ b/telethon_aio/events/__init__.py @@ -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): diff --git a/telethon_aio/sessions/memory.py b/telethon_aio/sessions/memory.py index 42af7ad9..4d7e6778 100644 --- a/telethon_aio/sessions/memory.py +++ b/telethon_aio/sessions/memory.py @@ -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: diff --git a/telethon_aio/telegram_client.py b/telethon_aio/telegram_client.py index bddecdf5..f8a26105 100644 --- a/telethon_aio/telegram_client.py +++ b/telethon_aio/telegram_client.py @@ -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): """ diff --git a/telethon_aio/utils.py b/telethon_aio/utils.py index faf69649..a9311521 100644 --- a/telethon_aio/utils.py +++ b/telethon_aio/utils.py @@ -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): diff --git a/telethon_aio/version.py b/telethon_aio/version.py index 8cc14b33..90e8bfe4 100644 --- a/telethon_aio/version.py +++ b/telethon_aio/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.17.4' +__version__ = '0.18'