From d65f8ecc0d6e6126de9dbfd9c388555124acdede Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 25 Jun 2018 21:14:58 +0200 Subject: [PATCH] Document the magic sync module --- README.rst | 23 +-- .../advanced-usage/accessing-the-full-api.rst | 34 ++-- .../extra/advanced-usage/update-modes.rst | 17 +- ...cio-crash-course.rst => asyncio-magic.rst} | 148 ++++++++++++++++-- readthedocs/extra/basic/creating-a-client.rst | 87 +++++----- readthedocs/extra/basic/entities.rst | 44 +++--- readthedocs/extra/basic/getting-started.rst | 57 +++---- readthedocs/extra/basic/telegram-client.rst | 58 +++---- .../extra/basic/working-with-updates.rst | 26 ++- readthedocs/extra/examples/bots.rst | 17 +- .../extra/examples/chats-and-channels.rst | 34 ++-- readthedocs/extra/examples/users.rst | 17 +- .../extra/examples/working-with-messages.rst | 78 +++++---- readthedocs/index.rst | 27 ++-- telethon/{syncify.py => sync.py} | 0 15 files changed, 389 insertions(+), 278 deletions(-) rename readthedocs/extra/basic/{asyncio-crash-course.rst => asyncio-magic.rst} (52%) rename telethon/{syncify.py => sync.py} (100%) diff --git a/README.rst b/README.rst index 8dc69e7c..11198330 100755 --- a/README.rst +++ b/README.rst @@ -34,10 +34,7 @@ Creating a client .. code:: python - import asyncio - loop = asyncio.get_event_loop() - - from telethon import TelegramClient + from telethon import TelegramClient, sync # These example values won't work. You must get your own api_id and # api_hash from https://my.telegram.org, under API Development. @@ -45,7 +42,7 @@ Creating a client api_hash = '0123456789abcdef0123456789abcdef' client = TelegramClient('session_name', api_id, api_hash) - loop.run_until_complete(client.start()) + client.start() Doing stuff @@ -53,18 +50,14 @@ Doing stuff .. code:: python - async def main(): - me = await client.get_me() - print(me.stringify()) + print(client.get_me().stringify()) - await client.send_message('username', 'Hello! Talking to you from Telethon') - await client.send_file('username', '/home/myself/Pictures/holidays.jpg') + client.send_message('username', 'Hello! Talking to you from Telethon') + client.send_file('username', '/home/myself/Pictures/holidays.jpg') - await client.download_profile_photo('me') - messages = await client.get_messages('username') - await messages[0].download_media() - - loop.run_until_complete(main()) + client.download_profile_photo('me') + messages = client.get_messages('username') + messages[0].download_media() Next steps diff --git a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst index ca435dfb..6a2e87f5 100644 --- a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst +++ b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst @@ -26,6 +26,15 @@ can go through a sorted list of everything you can do. can also do ``from telethon.tl import types, functions``. Both work. +.. important:: + + All the examples in this documentation assume that you have + ``from telethon import sync`` or ``import telethon.sync`` + for the sake of simplicity and that you understand what + it does (see :ref:`asyncio-magic` for more). Simply add + either line at the beginning of your project and it will work. + + You should also refer to the documentation to see what the objects (constructors) Telegram returns look like. Every constructor inherits from a common type, and that's the reason for this distinction. @@ -69,9 +78,9 @@ Or we call `client.get_input_entity .. code-block:: python - import asyncio - loop = asyncio.get_event_loop() - peer = loop.run_until_complete(client.get_input_entity('someone')) + import telethon.sync + peer = client.get_input_entity('someone') + When you're going to invoke an API method, most require you to pass an :tl:`InputUser`, :tl:`InputChat`, or so on, this is why using @@ -83,7 +92,7 @@ instead: .. code-block:: python - entity = loop.run_until_complete(client.get_entity('someone')) + entity = client.get_entity('someone') In the later case, when you use the entity, the library will cast it to its "input" version for you. If you already have the complete user and @@ -112,9 +121,7 @@ request we do: .. code-block:: python - result = loop.run_until_complete( - client(SendMessageRequest(peer, 'Hello there!')) - ) + result = client(SendMessageRequest(peer, 'Hello there!')) # __call__ is an alias for client.invoke(request). Both will work Message sent! Of course, this is only an example. There are over 250 @@ -123,21 +130,18 @@ as you wish. Remember to use the right types! To sum up: .. code-block:: python - result = loop.run_until_complete(client(SendMessageRequest( + result = client(SendMessageRequest( client.get_input_entity('username'), 'Hello there!' - ))) + )) This can further be simplified to: .. code-block:: python - async def main(): - result = await client(SendMessageRequest('username', 'Hello there!')) - # Or even - result = await client(SendMessageRequest(PeerChannel(id), 'Hello there!')) - - loop.run_until_complete(main()) + result = client(SendMessageRequest('username', 'Hello there!')) + # Or even + result = client(SendMessageRequest(PeerChannel(id), 'Hello there!')) .. note:: diff --git a/readthedocs/extra/advanced-usage/update-modes.rst b/readthedocs/extra/advanced-usage/update-modes.rst index 717b7ec3..c3391499 100644 --- a/readthedocs/extra/advanced-usage/update-modes.rst +++ b/readthedocs/extra/advanced-usage/update-modes.rst @@ -38,7 +38,8 @@ so the code above and the following are equivalent: async def main(): await client.disconnected - asyncio.get_event_loop().run_until_complete(main()) + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) You could also run `client.disconnected @@ -48,3 +49,17 @@ until it completed. But if you don't want to ``await``, then you should know what you want to be doing instead! What matters is that you shouldn't let your script die. If you don't care about updates, you don't need any of this. + +Notice that unlike `client.disconnected +`, +`client.run_until_disconnected +` will +handle ``KeyboardInterrupt`` with you. This method is special and can +also be ran while the loop is running, so you can do this: + +.. code-block:: python + + async def main(): + await client.run_until_disconnected() + + loop.run_until_complete(main()) diff --git a/readthedocs/extra/basic/asyncio-crash-course.rst b/readthedocs/extra/basic/asyncio-magic.rst similarity index 52% rename from readthedocs/extra/basic/asyncio-crash-course.rst rename to readthedocs/extra/basic/asyncio-magic.rst index a9c99785..69a38bba 100644 --- a/readthedocs/extra/basic/asyncio-crash-course.rst +++ b/readthedocs/extra/basic/asyncio-magic.rst @@ -1,8 +1,117 @@ -.. _asyncio-crash-course: +.. _asyncio-magic: -=========================== -A Crash Course into asyncio -=========================== +================== +Magic with asyncio +================== + +The sync module +*************** + +It's time to tell you the truth. The library has been doing magic behind +the scenes. We're sorry to tell you this, but at least it wasn't dark magic! + +You may have noticed one of these lines across the documentation: + +.. code-block:: python + + from telethon import sync + # or + import telethon.sync + +Either of these lines will import the *magic* ``sync`` module. When you +import this module, you can suddenly use all the methods defined in the +:ref:`TelegramClient ` like so: + +.. code-block:: python + + client.send_message('me', 'Hello!') + + for dialog in client.iter_dialogs(): + print(dialog.title) + + +What happened behind the scenes is that all those methods, called *coroutines*, +were rewritten to be normal methods that will block (with some exceptions). +This means you can use the library without worrying about ``asyncio`` and +event loops. + +However, this only works until you run the event loop yourself explicitly: + +.. code-block:: python + + import asyncio + + async def coro(): + client.send_message('me', 'Hello!') # <- no longer works! + + loop = asyncio.get_event_loop() + loop.run_until_complete(coro()) + + +What things will work and when? +******************************* + +You can use all the methods in the :ref:`TelegramClient ` +in a synchronous, blocking way without trouble, as long as you're not running +the loop as we saw above (the ``loop.run_until_complete(...)`` line runs "the +loop"). If you're running the loop, then *you* are the one responsible to +``await`` everything. So to fix the code above: + +.. code-block:: python + + import asyncio + + async def coro(): + await client.send_message('me', 'Hello!') + # ^ notice this new await + + loop = asyncio.get_event_loop() + loop.run_until_complete(coro()) + +The library can only run the loop until the method completes if the loop +isn't already running, which is why the magic can't work if you run the +loop yourself. + +**When you work with updates or events**, the loop needs to be +running one way or another (using `client.run_until_disconnected() +` runs the loop), +so your event handlers must be ``async def``. + +.. important:: + + Turning your event handlers into ``async def`` is the biggest change + between Telethon pre-1.0 and 1.0, but updating will likely cause a + noticeable speed-up in your programs. Keep reading! + + +So in short, you can use **all** methods in the client with ``await`` or +without it if the loop isn't running: + +.. code-block:: python + + client.send_message('me', 'Hello!') # works + + async def main(): + await client.send_message('me', 'Hello!') # also works + + loop.run_until_complete(main()) + + +When you work with updates, you should stick using the ``async def main`` +way, since your event handlers will be ``async def`` too. + +.. note:: + + There are two exceptions. Both `client.run_until_disconnected() + ` and + `client.start() ` work in + and outside of ``async def`` for convenience without importing the + magic module. The rest of methods remain ``async`` unless you import it. + +You can skip the rest if you already know how ``asyncio`` works and you +already understand what the magic does and how it works. Just remember +to ``await`` all your methods if you're inside an ``async def`` or are +using updates and you will be good. Why asyncio? @@ -51,8 +160,11 @@ To get started with ``asyncio``, all you need is to setup your main loop = asyncio.get_event_loop() loop.run_until_complete(main()) +You don't need to ``import telethon.sync`` if you're going to work this +way. This is the best way to work in real programs since the loop won't +be starting and ending all the time, but is a bit more annoying to setup. -Inside ``async def main():``, you can use the ``await`` keyword. Most +Inside ``async def main()``, you can use the ``await`` keyword. Most methods in the :ref:`TelegramClient ` are ``async def``. You must ``await`` all ``async def``, also known as a *coroutines*: @@ -78,9 +190,11 @@ Another way to use ``async def`` is to use ``loop.run_until_complete(f())``, but the loop must not be running before. If you want to handle updates (and don't let the script die), you must -`await client.disconnected ` +`await client.run_until_disconnected() +` which is a property that you can wait on until you call -`await client.disconnect() `: +`await client.disconnect() +`: .. code-block:: python @@ -93,13 +207,18 @@ which is a property that you can wait on until you call async def main(): await client.start() - await client.disconnected + await client.run_until_disconnected() if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(main()) -This is the same as using the ``run_until_disconnected()`` method: +`client.run_until_disconnected() +` and +`client.start() +` are special-cased and work +inside or outside ``async def`` for convenience, even without importing +the ``sync`` module, so you can also do this: .. code-block:: python @@ -110,8 +229,7 @@ This is the same as using the ``run_until_disconnected()`` method: print(event) if __name__ == '__main__': - loop = asyncio.get_event_loop() - loop.run_until_complete(client.start()) + client.start() client.run_until_disconnected() @@ -172,9 +290,11 @@ a lot less. You can also rename the run method to something shorter: rc(asyncio.sleep(1)) rc(message.delete()) -The documentation will use all these three styles so you can get used -to them. Which one to use is up to you, but generally you should work -inside an ``async def main()`` and just run the loop there. +The documentation generally runs the loop until complete behind the +scenes if you've imported the magic ``sync`` module, but if you haven't, +you need to run the loop yourself. We recommend that you use the +``async def main()`` method to do all your work with ``await``. +It's the easiest and most performant thing to do. More resources to learn asyncio diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index dccc2031..bda905e4 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -26,15 +26,7 @@ one is very simple: .. code-block:: python - import asyncio - loop = asyncio.get_event_loop() - - # Rename loop.run_until_complete(...) as rc(...), we will use it a lot. - # This basically lets us run the event loop (necessary in asyncio) to - # execute all the requests we need. - rc = loop.run_until_complete - - from telethon import TelegramClient + from telethon import TelegramClient, sync # Use your own values here api_id = 12345 @@ -62,7 +54,7 @@ your disk. This is by default a database file using Python's ``sqlite3``. .. code-block:: python - rc(client.start()) + client.start() This is explained after going through the manual process. @@ -72,14 +64,14 @@ Doing so is very easy: .. code-block:: python - rc(client.connect()) # Must return True, otherwise, try again + client.connect() You may or may not be authorized yet. You must be authorized before you're able to send any request: .. code-block:: python - rc(client.is_user_authorized()) # Returns True if you can send requests + client.is_user_authorized() # Returns True if you can send requests If you're not authorized, you need to `.sign_in `: @@ -87,8 +79,8 @@ If you're not authorized, you need to `.sign_in .. code-block:: python phone_number = '+34600000000' - rc(client.send_code_request(phone_number)) - myself = rc(client.sign_in(phone_number, input('Enter code: '))) + client.send_code_request(phone_number) + myself = client.sign_in(phone_number, input('Enter code: ')) # If .sign_in raises PhoneNumberUnoccupiedError, use .sign_up instead # If .sign_in raises SessionPasswordNeeded error, call .sign_in(password=...) # You can import both exceptions from telethon.errors. @@ -112,13 +104,10 @@ As a full example: client = TelegramClient('anon', api_id, api_hash) - async def main(): - assert await client.connect() - if not await client.is_user_authorized(): - await client.send_code_request(phone_number) - me = await client.sign_in(phone_number, input('Enter code: ')) - - loop.run_until_complete(main()) + client.connect() + if not client.is_user_authorized(): + client.send_code_request(phone_number) + me = client.sign_in(phone_number, input('Enter code: ')) All of this, however, can be done through a call to `.start() @@ -127,7 +116,7 @@ All of this, however, can be done through a call to `.start() .. code-block:: python client = TelegramClient('anon', api_id, api_hash) - loop.run_until_complete(client.start()) + client.start() The code shown is just what `.start() @@ -181,12 +170,11 @@ again with a ``password=``: import getpass from telethon.errors import SessionPasswordNeededError - async def main(): - await client.sign_in(phone) - try: - await client.sign_in(code=input('Enter code: ')) - except SessionPasswordNeededError: - await client.sign_in(password=getpass.getpass()) + client.sign_in(phone) + try: + client.sign_in(code=input('Enter code: ')) + except SessionPasswordNeededError: + client.sign_in(password=getpass.getpass()) The mentioned `.start() @@ -209,33 +197,32 @@ See the examples below: from telethon.errors import EmailUnconfirmedError - async def main(): - # Sets 2FA password for first time: - await client.edit_2fa(new_password='supersecurepassword') + # Sets 2FA password for first time: + client.edit_2fa(new_password='supersecurepassword') - # Changes password: - await client.edit_2fa(current_password='supersecurepassword', - new_password='changedmymind') + # Changes password: + client.edit_2fa(current_password='supersecurepassword', + new_password='changedmymind') - # Clears current password (i.e. removes 2FA): - await client.edit_2fa(current_password='changedmymind', new_password=None) + # Clears current password (i.e. removes 2FA): + client.edit_2fa(current_password='changedmymind', new_password=None) - # Sets new password with recovery email: - try: - await client.edit_2fa(new_password='memes and dreams', - email='JohnSmith@example.com') - # Raises error (you need to check your email to complete 2FA setup.) - except EmailUnconfirmedError: - # You can put email checking code here if desired. - pass + # Sets new password with recovery email: + try: + client.edit_2fa(new_password='memes and dreams', + email='JohnSmith@example.com') + # Raises error (you need to check your email to complete 2FA setup.) + except EmailUnconfirmedError: + # You can put email checking code here if desired. + pass - # Also take note that unless you remove 2FA or explicitly - # give email parameter again it will keep the last used setting + # Also take note that unless you remove 2FA or explicitly + # give email parameter again it will keep the last used setting - # Set hint after already setting password: - await client.edit_2fa(current_password='memes and dreams', - new_password='memes and dreams', - hint='It keeps you alive') + # Set hint after already setting password: + client.edit_2fa(current_password='memes and dreams', + new_password='memes and dreams', + hint='It keeps you alive') __ https://github.com/Anorov/PySocks#installation __ https://github.com/Anorov/PySocks#usage-1 diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index 9ed5da16..f1578465 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -42,32 +42,31 @@ you're able to just do this: .. code-block:: python - async def main(): - # Dialogs are the "conversations you have open". - # This method returns a list of Dialog, which - # has the .entity attribute and other information. - dialogs = await client.get_dialogs() + # Dialogs are the "conversations you have open". + # This method returns a list of Dialog, which + # has the .entity attribute and other information. + dialogs = client.get_dialogs() - # All of these work and do the same. - lonami = await client.get_entity('lonami') - lonami = await client.get_entity('t.me/lonami') - lonami = await client.get_entity('https://telegram.dog/lonami') + # All of these work and do the same. + lonami = client.get_entity('lonami') + lonami = client.get_entity('t.me/lonami') + lonami = client.get_entity('https://telegram.dog/lonami') - # Other kind of entities. - channel = await client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg') - contact = await client.get_entity('+34xxxxxxxxx') - friend = await client.get_entity(friend_id) + # Other kind of entities. + channel = client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg') + contact = client.get_entity('+34xxxxxxxxx') + friend = client.get_entity(friend_id) - # Getting entities through their ID (User, Chat or Channel) - entity = await client.get_entity(some_id) + # Getting entities through their ID (User, Chat or Channel) + entity = client.get_entity(some_id) - # You can be more explicit about the type for said ID by wrapping - # it inside a Peer instance. This is recommended but not necessary. - from telethon.tl.types import PeerUser, PeerChat, PeerChannel + # You can be more explicit about the type for said ID by wrapping + # it inside a Peer instance. This is recommended but not necessary. + from telethon.tl.types import PeerUser, PeerChat, PeerChannel - my_user = await client.get_entity(PeerUser(some_id)) - my_chat = await client.get_entity(PeerChat(some_id)) - my_channel = await client.get_entity(PeerChannel(some_id)) + my_user = client.get_entity(PeerUser(some_id)) + my_chat = client.get_entity(PeerChat(some_id)) + my_channel = client.get_entity(PeerChannel(some_id)) All methods in the :ref:`telegram-client` call `.get_input_entity() @@ -137,8 +136,7 @@ wherever needed, so you can even do things like: .. code-block:: python - async def main(): - await client(SendMessageRequest('username', 'hello')) + client(SendMessageRequest('username', 'hello')) The library will call the ``.resolve()`` method of the request, which will resolve ``'username'`` with the appropriated :tl:`InputPeer`. Don't worry if diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst index 6d6d2a6e..a3dec33d 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -21,18 +21,14 @@ Creating a client .. code-block:: python - import asyncio - loop = asyncio.get_event_loop() - - from telethon import TelegramClient + from telethon import TelegramClient, sync # These example values won't work. You must get your own api_id and # api_hash from https://my.telegram.org, under API Development. api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' - client = TelegramClient('session_name', api_id, api_hash) - loop.run_until_complete(client.start()) + client = TelegramClient('session_name', api_id, api_hash).start() **More details**: :ref:`creating-a-client` @@ -42,36 +38,33 @@ Basic Usage .. code-block:: python - async def main(): - # Getting information about yourself - me = await client.get_me() - print(me.stringify()) + # Getting information about yourself + me = client.get_me() + print(me.stringify()) - # Sending a message (you can use 'me' or 'self' to message yourself) - await client.send_message('username', 'Hello World from Telethon!') + # Sending a message (you can use 'me' or 'self' to message yourself) + client.send_message('username', 'Hello World from Telethon!') - # Sending a file - await client.send_file('username', '/home/myself/Pictures/holidays.jpg') + # Sending a file + client.send_file('username', '/home/myself/Pictures/holidays.jpg') - # Retrieving messages from a chat - from telethon import utils - async for message in client.iter_messages('username', limit=10): - print(utils.get_display_name(message.sender), message.message) + # Retrieving messages from a chat + from telethon import utils + for message in client.iter_messages('username', limit=10): + print(utils.get_display_name(message.sender), message.message) - # Listing all the dialogs (conversations you have open) - async for dialog in client.get_dialogs(limit=10): - print(dialog.name, dialog.draft.text) + # Listing all the dialogs (conversations you have open) + for dialog in client.get_dialogs(limit=10): + print(dialog.name, dialog.draft.text) - # Downloading profile photos (default path is the working directory) - await client.download_profile_photo('username') + # Downloading profile photos (default path is the working directory) + client.download_profile_photo('username') - # Once you have a message with .media (if message.media) - # you can download it using client.download_media(), - # or even using message.download_media(): - messages = await client.get_messages('username') - await messages[0].download_media() - - loop.run_until_complete(main()) + # Once you have a message with .media (if message.media) + # you can download it using client.download_media(), + # or even using message.download_media(): + messages = client.get_messages('username') + messages[0].download_media() **More details**: :ref:`telegram-client` @@ -86,8 +79,8 @@ Handling Updates from telethon import events @client.on(events.NewMessage(incoming=True, pattern='(?i)hi')) - async def handler(event): - await event.reply('Hello!') + def handler(event): + event.reply('Hello!') client.run_until_disconnected() diff --git a/readthedocs/extra/basic/telegram-client.rst b/readthedocs/extra/basic/telegram-client.rst index 0002bfe1..2e1e7904 100644 --- a/readthedocs/extra/basic/telegram-client.rst +++ b/readthedocs/extra/basic/telegram-client.rst @@ -31,19 +31,11 @@ growing!) on the :ref:`TelegramClient ` class that abstract you from the need of manually importing the requests you need. For instance, retrieving your own user can be done in a single line -(if we ignore the boilerplate needed to setup ``asyncio``, which only -needs to be done once for your entire program): +(assuming you have ``from telethon import sync`` or ``import telethon.sync``): .. code-block:: python - import asyncio - - async def main(): - myself = await client.get_me() # <- a single line! - - if __name__ == '__main__': - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) + myself = client.get_me() Internally, this method has sent a request to Telegram, who replied with the information about your own user, and then the desired information @@ -55,11 +47,10 @@ how the library refers to either of these: .. code-block:: python - async def main(): - # The method will infer that you've passed an username - # It also accepts phone numbers, and will get the user - # from your contact list. - lonami = await client.get_entity('lonami') + # The method will infer that you've passed an username + # It also accepts phone numbers, and will get the user + # from your contact list. + lonami = client.get_entity('lonami') The so called "entities" are another important whole concept on its own, but for now you don't need to worry about it. Simply know that they are @@ -69,31 +60,30 @@ Many other common methods for quick scripts are also available: .. code-block:: python - async def main(): - # Note that you can use 'me' or 'self' to message yourself - await client.send_message('username', 'Hello World from Telethon!') + # Note that you can use 'me' or 'self' to message yourself + 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`!') + # .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) + client.send_message('username', '**Using** __markdown__ `too`!') - await client.send_file('username', '/home/myself/Pictures/holidays.jpg') + client.send_file('username', '/home/myself/Pictures/holidays.jpg') - # The utils package has some goodies, like .get_display_name() - from telethon import utils - async for message in client.iter_messages('username', limit=10): - print(utils.get_display_name(message.sender), message.message) + # The utils package has some goodies, like .get_display_name() + from telethon import utils + for message in client.iter_messages('username', limit=10): + print(utils.get_display_name(message.sender), message.message) - # Dialogs are the conversations you have open - async for dialog in client.get_dialogs(limit=10): - print(dialog.name, dialog.draft.text) + # Dialogs are the conversations you have open + for dialog in client.get_dialogs(limit=10): + print(dialog.name, dialog.draft.text) - # Default path is the working directory - await client.download_profile_photo('username') + # Default path is the working directory + client.download_profile_photo('username') - # Call .disconnect() when you're done - await client.disconnect() + # Call .disconnect() when you're done + client.disconnect() Remember that you can call ``.stringify()`` to any object Telegram returns to pretty print it. Calling ``str(result)`` does the same operation, but on diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index 34681db2..6aa9341e 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -4,6 +4,16 @@ Working with Updates ==================== +.. important:: + + Make sure you have read at least the first part of :ref:`asyncio-magic` + before working with updates. **This is a big change from Telethon pre-1.0 + and 1.0, and your old handlers won't work with this version**. + + To port your code to the new version, you should just prefix all your + event handlers with ``async`` and ``await`` everything that makes an + API call, such as replying, deleting messages, etc. + The library comes with the `telethon.events` module. *Events* are an abstraction over what Telegram calls `updates`__, and are meant to ease simple and common @@ -36,7 +46,6 @@ Getting Started .. code-block:: python - import asyncio from telethon import TelegramClient, events client = TelegramClient('name', api_id, api_hash) @@ -46,7 +55,7 @@ Getting Started if 'hello' in event.raw_text: await event.reply('hi!') - asyncio.get_event_loop().run_until_complete(client.start()) + client.start() client.run_until_disconnected() @@ -54,7 +63,6 @@ Not much, but there might be some things unclear. What does this code do? .. code-block:: python - import asyncio from telethon import TelegramClient, events client = TelegramClient('name', api_id, api_hash) @@ -86,15 +94,21 @@ and ``'hello'`` is in the text of the message, we `.reply() ` to the event with a ``'hi!'`` message. +Do you notice anything different? Yes! Event handlers **must** be ``async`` +for them to work, and **every method using the network** needs to have an +``await``, otherwise, Python's ``asyncio`` will tell you that you forgot +to do so, so you can easily add it. + .. code-block:: python - asyncio.get_event_loop().run_until_complete(client.start()) + client.start() client.run_until_disconnected() Finally, this tells the client that we're done with our code. We run the -``asyncio`` loop until the client starts, and then we run it again until -we are disconnected. Of course, you can do other things instead of running +``asyncio`` loop until the client starts (this is done behind the scenes, +since the method is so common), and then we run it again until we are +disconnected. Of course, you can do other things instead of running until disconnected. For this refer to :ref:`update-modes`. diff --git a/readthedocs/extra/examples/bots.rst b/readthedocs/extra/examples/bots.rst index 76851e88..20bb26d3 100644 --- a/readthedocs/extra/examples/bots.rst +++ b/readthedocs/extra/examples/bots.rst @@ -19,9 +19,9 @@ not *interact* with a voting message), by making use of the from telethon.tl.functions.messages import GetInlineBotResultsRequest - bot_results = loop.run_until_complete(client(GetInlineBotResultsRequest( + bot_results = client(GetInlineBotResultsRequest( bot, user_or_chat, 'query', '' - ))) + )) And you can select any of their results by using :tl:`SendInlineBotResultRequest`: @@ -30,11 +30,11 @@ And you can select any of their results by using from telethon.tl.functions.messages import SendInlineBotResultRequest - loop.run_until_complete(client(SendInlineBotResultRequest( + client(SendInlineBotResultRequest( get_input_peer(user_or_chat), obtained_query_id, obtained_str_id - ))) + )) Talking to Bots with special reply markup @@ -45,9 +45,8 @@ Generally, you just use the `message.click() .. code-block:: python - async def main(): - messages = await client.get_messages('somebot') - await messages[0].click(0) + messages = client.get_messages('somebot') + messages[0].click(0) You can also do it manually. @@ -58,11 +57,11 @@ To interact with a message that has a special reply markup, such as from telethon.tl.functions.messages import GetBotCallbackAnswerRequest - loop.run_until_complete(client(GetBotCallbackAnswerRequest( + client(GetBotCallbackAnswerRequest( user_or_chat, msg.id, data=msg.reply_markup.rows[wanted_row].buttons[wanted_button].data - ))) + )) It's a bit verbose, but it has all the information you would need to show it visually (button rows, and buttons within each row, each with diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index ba4838e2..3b928e8c 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -25,11 +25,11 @@ to, you can make use of the :tl:`JoinChannelRequest` to join such channel: .. code-block:: python from telethon.tl.functions.channels import JoinChannelRequest - loop.run_until_complete(client(JoinChannelRequest(channel))) + client(JoinChannelRequest(channel)) # In the same way, you can also leave such channel from telethon.tl.functions.channels import LeaveChannelRequest - loop.run_until_complete(client(LeaveChannelRequest(input_channel))) + client(LeaveChannelRequest(input_channel)) For more on channels, check the `channels namespace`__. @@ -51,9 +51,7 @@ example, is the ``hash`` of the chat or channel. Now you can use .. code-block:: python from telethon.tl.functions.messages import ImportChatInviteRequest - updates = loop.run_until_complete( - client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) - ) + updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) Adding someone else to such chat or channel @@ -70,19 +68,19 @@ use is very straightforward, or :tl:`InviteToChannelRequest` for channels: # 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``). - loop.run_until_complete(client(AddChatUserRequest( + client(AddChatUserRequest( chat_id, user_to_add, fwd_limit=10 # Allow the user to see the 10 last messages - ))) + )) # For channels (which includes megagroups) from telethon.tl.functions.channels import InviteToChannelRequest - loop.run_until_complete(client(InviteToChannelRequest( + client(InviteToChannelRequest( channel, [users_to_add] - ))) + )) Checking a link without joining @@ -108,7 +106,7 @@ Here is the easy way to do it: .. code-block:: python - participants = loop.run_until_complete(client.get_participants(group)) + participants = client.get_participants(group) Now we will show how the method works internally. @@ -133,9 +131,9 @@ a fixed limit: all_participants = [] while True: - participants = loop.run_until_complete(client(GetParticipantsRequest( + participants = client(GetParticipantsRequest( channel, ChannelParticipantsSearch(''), offset, limit, hash=0 - ))) + )) if not participants.users: break all_participants.extend(participants.users) @@ -202,7 +200,7 @@ Giving or revoking admin permissions can be done with the :tl:`EditAdminRequest` # ) # Once you have a ChannelAdminRights, invoke it - loop.run_until_complete(client(EditAdminRequest(channel, user, rights))) + client(EditAdminRequest(channel, user, rights)) # User will now be able to change group info, delete other people's # messages and pin messages. @@ -261,7 +259,7 @@ banned rights of an user through :tl:`EditBannedRequest` and its parameter embed_links=True ) - loop.run_until_complete(client(EditBannedRequest(channel, user, rights))) + client(EditBannedRequest(channel, user, rights)) Kicking a member @@ -276,12 +274,12 @@ is enough: from telethon.tl.functions.channels import EditBannedRequest from telethon.tl.types import ChannelBannedRights - loop.run_until_complete(client(EditBannedRequest( + client(EditBannedRequest( channel, user, ChannelBannedRights( until_date=None, view_messages=True ) - ))) + )) __ https://github.com/Kyle2142 @@ -302,11 +300,11 @@ use :tl:`GetMessagesViewsRequest`, setting ``increment=True``: # Obtain `channel' through dialogs or through client.get_entity() or anyhow. # Obtain `msg_ids' through `.get_messages()` or anyhow. Must be a list. - loop.run_until_complete(client(GetMessagesViewsRequest( + client(GetMessagesViewsRequest( peer=channel, id=msg_ids, increment=True - ))) + )) Note that you can only do this **once or twice a day** per account, diff --git a/readthedocs/extra/examples/users.rst b/readthedocs/extra/examples/users.rst index ad594a81..e029596e 100644 --- a/readthedocs/extra/examples/users.rst +++ b/readthedocs/extra/examples/users.rst @@ -19,12 +19,11 @@ you should use :tl:`GetFullUser`: from telethon.tl.functions.users import GetFullUserRequest - async def main(): - full = await client(GetFullUserRequest(user)) - # or even - full = await client(GetFullUserRequest('username')) + full = client(GetFullUserRequest(user)) + # or even + full = client(GetFullUserRequest('username')) - bio = full.about + bio = full.about See :tl:`UserFull` to know what other fields you can access. @@ -40,9 +39,9 @@ request. Omitted fields won't change after invoking :tl:`UpdateProfile`: from telethon.tl.functions.account import UpdateProfileRequest - loop.run_until_complete(client(UpdateProfileRequest(a + client(UpdateProfileRequest(a bout='This is a test from Telethon' - ))) + )) Updating your username @@ -54,7 +53,7 @@ You need to use :tl:`account.UpdateUsername`: from telethon.tl.functions.account import UpdateUsernameRequest - loop.run_until_complete(client(UpdateUsernameRequest('new_username'))) + client(UpdateUsernameRequest('new_username')) Updating your profile photo @@ -68,6 +67,6 @@ through :tl:`UploadProfilePhoto`: from telethon.tl.functions.photos import UploadProfilePhotoRequest - loop.run_until_complete(client(UploadProfilePhotoRequest( + client(UploadProfilePhotoRequest( client.upload_file('/path/to/some/file') ))) diff --git a/readthedocs/extra/examples/working-with-messages.rst b/readthedocs/extra/examples/working-with-messages.rst index 231f1076..4f741425 100644 --- a/readthedocs/extra/examples/working-with-messages.rst +++ b/readthedocs/extra/examples/working-with-messages.rst @@ -20,32 +20,31 @@ Forwarding messages .. code-block:: python - async def main(): - # If you only have the message IDs - await client.forward_messages( - entity, # to which entity you are forwarding the messages - message_ids, # the IDs of the messages (or message) to forward - from_entity # who sent the messages? - ) + # If you only have the message IDs + client.forward_messages( + entity, # to which entity you are forwarding the messages + message_ids, # the IDs of the messages (or message) to forward + from_entity # who sent the messages? + ) - # If you have ``Message`` objects - await client.forward_messages( - entity, # to which entity you are forwarding the messages - messages # the messages (or message) to forward - ) + # If you have ``Message`` objects + client.forward_messages( + entity, # to which entity you are forwarding the messages + messages # the messages (or message) to forward + ) - # You can also do it manually if you prefer - from telethon.tl.functions.messages import ForwardMessagesRequest + # You can also do it manually if you prefer + from telethon.tl.functions.messages import ForwardMessagesRequest - messages = foo() # retrieve a few messages (or even one, in a list) - from_entity = bar() - to_entity = baz() + messages = foo() # retrieve a few messages (or even one, in a list) + from_entity = bar() + to_entity = baz() - await client(ForwardMessagesRequest( - from_peer=from_entity, # who sent these messages? - id=[msg.id for msg in messages], # which are the messages? - to_peer=to_entity # who are we forwarding them to? - )) + client(ForwardMessagesRequest( + from_peer=from_entity, # who sent these messages? + id=[msg.id for msg in messages], # which are the messages? + to_peer=to_entity # who are we forwarding them to? + )) The named arguments are there for clarity, although they're not needed because they appear in order. You can obviously just wrap a single message on the list @@ -72,7 +71,7 @@ into issues_. A valid example would be: from telethon.tl.types import InputMessagesFilterEmpty filter = InputMessagesFilterEmpty() - result = loop.run_until_complete(client(SearchRequest( + result = client(SearchRequest( peer=peer, # On which chat/conversation q='query', # What to search for filter=filter, # Filter to use (maybe filter for media) @@ -85,7 +84,7 @@ into issues_. A valid example would be: min_id=0, # Minimum message ID from_id=None, # Who must have sent the message (peer) hash=0 # Special number to return nothing on no-change - ))) + )) It's important to note that the optional parameter ``from_id`` could have been omitted (defaulting to ``None``). Changing it to :tl:`InputUserEmpty`, as one @@ -116,25 +115,24 @@ send yourself the very first sticker you have: .. code-block:: python - async def main(): - # Get all the sticker sets this user has - from telethon.tl.functions.messages import GetAllStickersRequest - sticker_sets = await client(GetAllStickersRequest(0)) + # Get all the sticker sets this user has + from telethon.tl.functions.messages import GetAllStickersRequest + sticker_sets = client(GetAllStickersRequest(0)) - # Choose a sticker set - from telethon.tl.functions.messages import GetStickerSetRequest - from telethon.tl.types import InputStickerSetID - sticker_set = sticker_sets.sets[0] + # Choose a sticker set + from telethon.tl.functions.messages import GetStickerSetRequest + from telethon.tl.types import InputStickerSetID + sticker_set = sticker_sets.sets[0] - # Get the stickers for this sticker set - stickers = await client(GetStickerSetRequest( - stickerset=InputStickerSetID( - id=sticker_set.id, access_hash=sticker_set.access_hash - ) - )) + # Get the stickers for this sticker set + stickers = client(GetStickerSetRequest( + stickerset=InputStickerSetID( + id=sticker_set.id, access_hash=sticker_set.access_hash + ) + )) - # Stickers are nothing more than files, so send that - await client.send_file('me', stickers.documents[0]) + # Stickers are nothing more than files, so send that + client.send_file('me', stickers.documents[0]) .. _issues: https://github.com/LonamiWebs/Telethon/issues/215 diff --git a/readthedocs/index.rst b/readthedocs/index.rst index 886c8b06..090b1c69 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -15,22 +15,25 @@ or use the menu on the left. Remember to read the :ref:`changelog` when you upgrade! .. important:: - If you're new here, you want to read :ref:`getting-started`. If you're - looking for the method reference, you should check :ref:`telethon-client`. + If you're new here, you want to read :ref:`getting-started`. If you're + looking for the method reference, you should check :ref:`telethon-client`. - The mentioned :ref:`telethon-client` is an important section and it - contains the friendly methods that **you should use** most of the time. + The mentioned :ref:`telethon-client` is an important section and it + contains the friendly methods that **you should use** most of the time. .. note:: - The library uses `asyncio `_ - by default, but you if you don't know how to use ``asyncio`` you can use - `a simpler version `_ - (select the "sync" version in ``readthedocs``' bottom left corner). + The library uses `asyncio `_ + under the hood, but you don't need to know anything about it unless you're + going to work with updates! If you're an user of Telethon pre-1.0 and you + aren't ready to convert your event handlers into ``async``, you can use + `a simpler version `_ + (select the "sync" version in ``readthedocs``' bottom left corner). - However, **you are encouraged to use asyncio**, it will make your scripts - faster and more powerful. :ref:`asyncio-crash-course` will teach you why - ``asyncio`` is good and how to use it. + If you used Telethon pre-1.0 but your scripts don't use updates or threads, + running ``import telethon.sync`` should make them Just Work. Otherwise, + we have :ref:`asyncio-magic` to teach you why ``asyncio`` is good and + how to use it. What is this? @@ -50,10 +53,10 @@ heavy job for you, so you can focus on developing an application. extra/basic/getting-started extra/basic/installation - extra/basic/asyncio-crash-course extra/basic/creating-a-client extra/basic/telegram-client extra/basic/entities + extra/basic/asyncio-magic extra/basic/working-with-updates diff --git a/telethon/syncify.py b/telethon/sync.py similarity index 100% rename from telethon/syncify.py rename to telethon/sync.py