From 8bf140ca7437d36947a12f4a10c1c8a67da5e2cf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Mar 2018 14:04:25 +0100 Subject: [PATCH] Document asyncio better (#456) --- readthedocs/extra/basic/creating-a-client.rst | 33 +++++++++++++------ readthedocs/extra/basic/entities.rst | 22 ++++++------- readthedocs/extra/basic/getting-started.rst | 25 +++++++------- readthedocs/extra/basic/telegram-client.rst | 12 +++---- .../extra/basic/working-with-updates.rst | 33 ++++++++++--------- readthedocs/extra/developing/test-servers.rst | 2 +- readthedocs/extra/examples/bots.rst | 6 ++-- .../extra/examples/chats-and-channels.rst | 16 ++++----- .../extra/examples/working-with-messages.rst | 10 +++--- readthedocs/index.rst | 12 +++++++ 10 files changed, 99 insertions(+), 72 deletions(-) diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index 384ebd47..03e9f932 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -42,7 +42,7 @@ your disk. This is by default a database file using Python's ``sqlite3``. Before using the client, you must be connected to Telegram. Doing so is very easy: - ``client.connect() # Must return True, otherwise, try again`` + ``await client.connect() # Must return True, otherwise, try again`` You may or may not be authorized yet. You must be authorized before you're able to send any request: @@ -54,8 +54,8 @@ If you're not authorized, you need to ``.sign_in()``: .. code-block:: python phone_number = '+34600000000' - client.send_code_request(phone_number) - myself = client.sign_in(phone_number, input('Enter code: ')) + await client.send_code_request(phone_number) + myself = await 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. @@ -78,10 +78,10 @@ As a full example: .. code-block:: python client = TelegramClient('anon', api_id, api_hash) - assert client.connect() + assert await client.connect() if not client.is_user_authorized(): - client.send_code_request(phone_number) - me = client.sign_in(phone_number, input('Enter code: ')) + await client.send_code_request(phone_number) + me = await client.sign_in(phone_number, input('Enter code: ')) All of this, however, can be done through a call to ``.start()``: @@ -89,7 +89,7 @@ All of this, however, can be done through a call to ``.start()``: .. code-block:: python client = TelegramClient('anon', api_id, api_hash) - client.start() + await client.start() The code shown is just what ``.start()`` will be doing behind the scenes @@ -103,6 +103,19 @@ is just a matter of taste, and how much control you need. Remember that you can get yourself at any time with ``client.get_me()``. +Assuming you've written all of this in a ``async def main():``, you can +run it with: + + .. code-block:: python + + import asyncio + + async def main(): + ... + + asyncio.get_event_loop().run_until_complete(main()) + + .. warning:: Please note that if you fail to login around 5 times (or change the first parameter of the ``TelegramClient``, which is the session name) you will @@ -141,9 +154,9 @@ account, calling :meth:`telethon.TelegramClient.sign_in` will raise a client.sign_in(phone) try: - client.sign_in(code=input('Enter code: ')) + await client.sign_in(code=input('Enter code: ')) except SessionPasswordNeededError: - client.sign_in(password=getpass.getpass()) + await client.sign_in(password=getpass.getpass()) The mentioned ``.start()`` method will handle this for you as well, but @@ -168,7 +181,7 @@ take as example the following code snippet: pw_salted = salt + pw + salt pw_hash = sha256(pw_salted).digest() - result = client(account.UpdatePasswordSettingsRequest( + result = await client(account.UpdatePasswordSettingsRequest( current_password_hash=salt, new_settings=PasswordInputSettings( new_salt=salt, diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index c7e55524..30ccc90c 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -32,17 +32,17 @@ you're able to just do this: # 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(limit=200) + dialogs = await client.get_dialogs(limit=200) # 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') + lonami = await client.get_entity('lonami') + lonami = await client.get_entity('t.me/lonami') + lonami = await client.get_entity('https://telegram.dog/lonami') # 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) + channel = await client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg') + contact = await client.get_entity('+34xxxxxxxxx') + friend = await client.get_entity(friend_id) # Using Peer/InputPeer (note that the API may return these) # users, chats and channels may all have the same ID, so it's @@ -51,9 +51,9 @@ you're able to just do this: # 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)) + 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)) .. warning:: @@ -122,7 +122,7 @@ library, the raw requests you make to the API are also able to call .. code-block:: python - client(SendMessageRequest('username', 'hello')) + await 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 e40bae44..5cbf9b62 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -27,7 +27,7 @@ Creating a client api_hash = '0123456789abcdef0123456789abcdef' client = TelegramClient('session_name', api_id, api_hash) - client.start() + await client.start() **More details**: :ref:`creating-a-client` @@ -38,30 +38,30 @@ Basic Usage .. code-block:: python # Getting information about yourself - print(client.get_me().stringify()) + print((await client.get_me()).stringify()) # Sending a message (you can use 'me' or 'self' to message yourself) - client.send_message('username', 'Hello World from Telethon!') + await client.send_message('username', 'Hello World from Telethon!') # Sending a file - client.send_file('username', '/home/myself/Pictures/holidays.jpg') + await client.send_file('username', '/home/myself/Pictures/holidays.jpg') # Retrieving messages from a chat from telethon import utils - for message in client.get_message_history('username', limit=10): + async for message in client.get_message_history('username', limit=10): print(utils.get_display_name(message.sender), message.message) # Listing all the dialogs (conversations you have open) - for dialog in client.get_dialogs(limit=10): + async for dialog in client.get_dialogs(limit=10): print(utils.get_display_name(dialog.entity), dialog.draft.message) # Downloading profile photos (default path is the working directory) - client.download_profile_photo('username') + await client.download_profile_photo('username') # Once you have a message with .media (if message.media) # you can download it using client.download_media(): - messages = client.get_message_history('username') - client.download_media(messages[0]) + messages = await client.get_message_history('username') + await client.download_media(messages[0]) **More details**: :ref:`telegram-client` @@ -71,17 +71,18 @@ Handling Updates .. code-block:: python + import asyncio from telethon import events # We need to have some worker running client.updates.workers = 1 @client.on(events.NewMessage(incoming=True, pattern='(?i)hi')) - def handler(event): - event.reply('Hello!') + async def handler(event): + await event.reply('Hello!') # If you want to handle updates you can't let the script end. - input('Press enter to exit.') + asyncio.get_event_loop().run_forever() **More details**: :ref:`working-with-updates` diff --git a/readthedocs/extra/basic/telegram-client.rst b/readthedocs/extra/basic/telegram-client.rst index decb3765..6468541e 100644 --- a/readthedocs/extra/basic/telegram-client.rst +++ b/readthedocs/extra/basic/telegram-client.rst @@ -45,7 +45,7 @@ how the library refers to either of these: # 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') + lonami = await 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 @@ -56,21 +56,21 @@ Many other common methods for quick scripts are also available: .. code-block:: python # Note that you can use 'me' or 'self' to message yourself - client.send_message('username', 'Hello World from Telethon!') + await client.send_message('username', 'Hello World from Telethon!') - client.send_file('username', '/home/myself/Pictures/holidays.jpg') + await client.send_file('username', '/home/myself/Pictures/holidays.jpg') # The utils package has some goodies, like .get_display_name() from telethon import utils - for message in client.get_message_history('username', limit=10): + async for message in client.get_message_history('username', limit=10): print(utils.get_display_name(message.sender), message.message) # Dialogs are the conversations you have open - for dialog in client.get_dialogs(limit=10): + async for dialog in client.get_dialogs(limit=10): print(utils.get_display_name(dialog.entity), dialog.draft.message) # Default path is the working directory - client.download_profile_photo('username') + await client.download_profile_photo('username') # Call .disconnect() when you're done client.disconnect() diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index 308c3a79..858a472d 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -30,17 +30,18 @@ Getting Started .. code-block:: python + import asyncio from telethon import TelegramClient, events client = TelegramClient(..., update_workers=1, spawn_read_thread=False) - client.start() + await client.start() @client.on(events.NewMessage) - def my_event_handler(event): + async def my_event_handler(event): if 'hello' in event.raw_text: - event.reply('hi!') + await event.reply('hi!') - client.idle() + asyncio.get_event_loop().run_forever() Not much, but there might be some things unclear. What does this code do? @@ -50,7 +51,7 @@ Not much, but there might be some things unclear. What does this code do? from telethon import TelegramClient, events client = TelegramClient(..., update_workers=1, spawn_read_thread=False) - client.start() + await client.start() This is normal initialization (of course, pass session name, API ID and hash). @@ -67,9 +68,9 @@ the callback function you're about to define will be called: .. code-block:: python - def my_event_handler(event): + async def my_event_handler(event): if 'hello' in event.raw_text: - event.reply('hi!') + await event.reply('hi!') If a ``NewMessage`` event occurs, and ``'hello'`` is in the text of the @@ -77,10 +78,10 @@ message, we ``reply`` to the event with a ``'hi!'`` message. .. code-block:: python - client.idle() + asyncio.get_event_loop().run_forever() -Finally, this tells the client that we're done with our code, and want +Finally, this tells the script that we're done with our code, and want to listen for all these events to occur. Of course, you might want to do other things instead idling. For this refer to :ref:`update-modes`. @@ -119,17 +120,17 @@ for example: # Either a single item or a list of them will work for the chats. # You can also use the IDs, Peers, or even User/Chat/Channel objects. @client.on(events.NewMessage(chats=('TelethonChat', 'TelethonOffTopic'))) - def normal_handler(event): + async def normal_handler(event): if 'roll' in event.raw_text: - event.reply(str(random.randint(1, 6))) + await event.reply(str(random.randint(1, 6))) # Similarly, you can use incoming=True for messages that you receive @client.on(events.NewMessage(chats='TelethonOffTopic', outgoing=True)) - def admin_handler(event): + async def admin_handler(event): if event.raw_text.startswith('eval'): expression = event.raw_text.replace('eval', '').strip() - event.reply(str(ast.literal_eval(expression))) + await event.reply(str(ast.literal_eval(expression))) You can pass one or more chats to the ``chats`` parameter (as a list or tuple), @@ -167,15 +168,15 @@ propagation of the update through your handlers to stop: from telethon.events import StopPropagation @client.on(events.NewMessage) - def _(event): + async def _(event): # ... some conditions - event.delete() + await event.delete() # Other handlers won't have an event to work with raise StopPropagation @client.on(events.NewMessage) - def _(event): + async def _(event): # Will never be reached, because it is the second handler # in the chain. pass diff --git a/readthedocs/extra/developing/test-servers.rst b/readthedocs/extra/developing/test-servers.rst index a3288a25..4805937b 100644 --- a/readthedocs/extra/developing/test-servers.rst +++ b/readthedocs/extra/developing/test-servers.rst @@ -32,4 +32,4 @@ times, in this case, ``22222`` so we can hardcode that: client = TelegramClient(None, api_id, api_hash) client.session.set_dc(2, '149.154.167.40', 80) - client.start(phone='9996621234', code_callback=lambda: '22222') + await client.start(phone='9996621234', code_callback=lambda: '22222') diff --git a/readthedocs/extra/examples/bots.rst b/readthedocs/extra/examples/bots.rst index fd4d54de..9648097d 100644 --- a/readthedocs/extra/examples/bots.rst +++ b/readthedocs/extra/examples/bots.rst @@ -19,7 +19,7 @@ not *interact* with a voting message), by making use of the from telethon.tl.functions.messages import GetInlineBotResultsRequest - bot_results = client(GetInlineBotResultsRequest( + bot_results = await client(GetInlineBotResultsRequest( bot, user_or_chat, 'query', '' )) @@ -30,7 +30,7 @@ And you can select any of their results by using from telethon.tl.functions.messages import SendInlineBotResultRequest - client(SendInlineBotResultRequest( + await client(SendInlineBotResultRequest( get_input_peer(user_or_chat), obtained_query_id, obtained_str_id @@ -47,7 +47,7 @@ To interact with a message that has a special reply markup, such as from telethon.tl.functions.messages import GetBotCallbackAnswerRequest - client(GetBotCallbackAnswerRequest( + await client(GetBotCallbackAnswerRequest( user_or_chat, msg.id, data=msg.reply_markup.rows[wanted_row].buttons[wanted_button].data diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index f38519c6..cdaed4cd 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -26,11 +26,11 @@ to, you can make use of the `JoinChannelRequest`__ to join such channel: .. code-block:: python from telethon.tl.functions.channels import JoinChannelRequest - client(JoinChannelRequest(channel)) + await client(JoinChannelRequest(channel)) # In the same way, you can also leave such channel from telethon.tl.functions.channels import LeaveChannelRequest - client(LeaveChannelRequest(input_channel)) + await client(LeaveChannelRequest(input_channel)) For more on channels, check the `channels namespace`__. @@ -49,7 +49,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 = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) + updates = await client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) Adding someone else to such chat or channel @@ -64,7 +64,7 @@ use is very straightforward, or `InviteToChannelRequest`__ for channels: # For normal chats from telethon.tl.functions.messages import AddChatUserRequest - client(AddChatUserRequest( + await client(AddChatUserRequest( chat_id, user_to_add, fwd_limit=10 # Allow the user to see the 10 last messages @@ -73,7 +73,7 @@ use is very straightforward, or `InviteToChannelRequest`__ for channels: # For channels from telethon.tl.functions.channels import InviteToChannelRequest - client(InviteToChannelRequest( + await client(InviteToChannelRequest( channel, [users_to_add] )) @@ -122,7 +122,7 @@ a fixed limit: all_participants = [] while True: - participants = client(GetParticipantsRequest( + participants = await client(GetParticipantsRequest( channel, ChannelParticipantsSearch(''), offset, limit, hash=0 )) @@ -197,7 +197,7 @@ Giving or revoking admin permissions can be done with the `EditAdminRequest`__: # ) # Once you have a ChannelAdminRights, invoke it - client(EditAdminRequest(channel, user, rights)) + await client(EditAdminRequest(channel, user, rights)) # User will now be able to change group info, delete other people's # messages and pin messages. @@ -230,7 +230,7 @@ use `GetMessagesViewsRequest`__, setting ``increment=True``: # Obtain `channel' through dialogs or through client.get_entity() or anyhow. # Obtain `msg_ids' through `.get_message_history()` or anyhow. Must be a list. - client(GetMessagesViewsRequest( + await client(GetMessagesViewsRequest( peer=channel, id=msg_ids, increment=True diff --git a/readthedocs/extra/examples/working-with-messages.rst b/readthedocs/extra/examples/working-with-messages.rst index e2471a25..dbe1ed93 100644 --- a/readthedocs/extra/examples/working-with-messages.rst +++ b/readthedocs/extra/examples/working-with-messages.rst @@ -28,7 +28,7 @@ Message*s*, plural) *always*, since it is more powerful, as follows: from_entity = bar() to_entity = baz() - client(ForwardMessagesRequest( + 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? @@ -51,7 +51,7 @@ into issues_. A valid example would be: from telethon.tl.types import InputMessagesFilterEmpty filter = InputMessagesFilterEmpty() - result = client(SearchRequest( + result = await client(SearchRequest( peer=peer, # On which chat/conversation q='query', # What to search for filter=filter, # Filter to use (maybe filter for media) @@ -95,20 +95,20 @@ send yourself the very first sticker you have: .. code-block:: python # Get all the sticker sets this user has - sticker_sets = client(GetAllStickersRequest(0)) + sticker_sets = await client(GetAllStickersRequest(0)) # Choose a sticker set sticker_set = sticker_sets.sets[0] # Get the stickers for this sticker set - stickers = client(GetStickerSetRequest( + stickers = await client(GetStickerSetRequest( stickerset=InputStickerSetID( id=sticker_set.id, access_hash=sticker_set.access_hash ) )) # Stickers are nothing more than files, so send that - client(SendMediaRequest( + await client(SendMediaRequest( peer=client.get_me(), media=InputMediaDocument( id=InputDocument( diff --git a/readthedocs/index.rst b/readthedocs/index.rst index a3982d86..136a6824 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -19,6 +19,18 @@ when you upgrade! looking for the method reference, you should check :ref:`telethon-package`. +.. note:: + + We assume that you have some experience working with ``asyncio``, + if you don't you should probably use the threaded version of the + library, or either learn how to use ``asyncio``. All the code + here assumes you're writing the code inside an ``async def`` so + we can use ``await`` across the examples. + + Then you can ``import asyncio`` and run + ``asyncio.get_event_loop().run_until_complete(my_method())`` + + What is this? *************