diff --git a/README.rst b/README.rst index 6dee1cdd..98a7a177 100755 --- a/README.rst +++ b/README.rst @@ -4,8 +4,11 @@ Telethon ⭐️ Thanks **everyone** who has starred the project, it means a lot! -**Telethon** is Telegram client implementation in **Python 3** which uses -the latest available API of Telegram. +**Telethon** is an `asyncio `_ +**Python 3** library to interact with Telegram's API. + +If you don't like ``asyncio``, you can still use `a simpler version +`_ that uses threads instead. What is this? @@ -22,7 +25,7 @@ Installing .. code:: sh - pip3 install telethon + pip3 install telethon-aio Creating a client @@ -30,15 +33,18 @@ Creating a client .. code:: python - from telethon import TelegramClient + import asyncio + loop = asyncio.get_event_loop() - # 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' + from telethon import TelegramClient - client = TelegramClient('session_name', api_id, api_hash) - client.start() + # 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()) Doing stuff @@ -46,20 +52,23 @@ Doing stuff .. code:: python - print(client.get_me().stringify()) + async def main(): + me = await client.get_me() + print(me.stringify()) - client.send_message('username', 'Hello! Talking to you from Telethon') - client.send_file('username', '/home/myself/Pictures/holidays.jpg') + await client.send_message('username', 'Hello! Talking to you from Telethon') + await client.send_file('username', '/home/myself/Pictures/holidays.jpg') - client.download_profile_photo('me') - messages = client.get_messages('username') - client.download_media(messages[0]) + await client.download_profile_photo('me') + messages = await client.get_messages('username') + await messages[0].download_media() + + loop.run_until_complete(main()) Next steps ---------- -Do you like how Telethon looks? Check out -`Read The Docs `_ -for a more in-depth explanation, with examples, -troubleshooting issues, and more useful information. +Do you like how Telethon looks? Check out `Read The Docs +`_ for a more in-depth explanation, +with examples, troubleshooting issues, and more useful information. diff --git a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst index 016395ac..8cc34479 100644 --- a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst +++ b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst @@ -69,7 +69,9 @@ Or we call `client.get_input_entity .. code-block:: python - peer = client.get_input_entity('someone') + import asyncio + loop = asyncio.get_event_loop() + peer = loop.run_until_complete(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 @@ -81,7 +83,7 @@ instead: .. code-block:: python - entity = client.get_entity('someone') + entity = loop.run_until_complete(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 @@ -110,7 +112,9 @@ request we do: .. code-block:: python - result = client(SendMessageRequest(peer, 'Hello there!')) + result = loop.run_until_complete( + 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 nearly 250 @@ -119,19 +123,21 @@ as you wish. Remember to use the right types! To sum up: .. code-block:: python - result = client(SendMessageRequest( + result = loop.run_until_complete(client(SendMessageRequest( client.get_input_entity('username'), 'Hello there!' - )) + ))) This can further be simplified to: .. code-block:: python - result = client(SendMessageRequest('username', 'Hello there!')) - # Or even - result = client(SendMessageRequest(PeerChannel(id), 'Hello there!')) + 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()) .. note:: diff --git a/readthedocs/extra/advanced-usage/update-modes.rst b/readthedocs/extra/advanced-usage/update-modes.rst index 59f7035a..6ee6fd46 100644 --- a/readthedocs/extra/advanced-usage/update-modes.rst +++ b/readthedocs/extra/advanced-usage/update-modes.rst @@ -4,141 +4,46 @@ Update Modes ============ +With ``asyncio``, the library has several tasks running in the background. +One task is used for sending requests, another task is used to receive them, +and a third one is used to handle updates. -The library can run in four distinguishable modes: +To handle updates, you must keep your script running. You can do this in +several ways. For instance, if you are *not* running ``asyncio``'s event +loop, you should use `client.run_until_disconnected +`: -- With no extra threads at all. -- With an extra thread that receives everything as soon as possible (default). -- With several worker threads that run your update handlers. -- A mix of the above. +.. code-block:: python -Since this section is about updates, we'll describe the simplest way to -work with them. + import asyncio + from telethon import TelegramClient + + client = TelegramClient(...) + ... + client.run_until_disconnected() -Using multiple workers -********************** - -When you create your client, simply pass a number to the -``update_workers`` parameter: - - ``client = TelegramClient('session', api_id, api_hash, update_workers=2)`` - -You can set any amount of workers you want. The more you put, the more -update handlers that can be called "at the same time". One or two should -suffice most of the time, since setting more will not make things run -faster most of the times (actually, it could slow things down). - -The next thing you want to do is to add a method that will be called when -an `Update`__ arrives: - - .. code-block:: python - - def callback(update): - print('I received', update) - - client.add_event_handler(callback) - # do more work here, or simply sleep! - -That's it! This is the old way to listen for raw updates, with no further -processing. If this feels annoying for you, remember that you can always -use :ref:`working-with-updates` but maybe use this for some other cases. - -Now let's do something more interesting. Every time an user talks to us, -let's reply to them with the same text reversed: - - .. code-block:: python - - from telethon.tl.types import UpdateShortMessage, PeerUser - - def replier(update): - if isinstance(update, UpdateShortMessage) and not update.out: - client.send_message(PeerUser(update.user_id), update.message[::-1]) +Behind the scenes, this method is ``await``'ing on the `client.disconnected +` property, so the code above +and the following are equivalent: - client.add_event_handler(replier) - input('Press enter to stop this!') - client.disconnect() +.. code-block:: python -We only ask you one thing: don't keep this running for too long, or your -contacts will go mad. + import asyncio + from telethon import TelegramClient + + client = TelegramClient(...) + + async def main(): + await client.disconnected + + asyncio.get_event_loop().run_until_complete(main()) -Spawning no worker at all -************************* +You could also run `client.disconnected +` until it completed. -All the workers do is loop forever and poll updates from a queue that is -filled from the ``ReadThread``, responsible for reading every item off -the network. If you only need a worker and the ``MainThread`` would be -doing no other job, this is the preferred way. You can easily do the same -as the workers like so: - - .. code-block:: python - - while True: - try: - update = client.updates.poll() - if not update: - continue - - print('I received', update) - except KeyboardInterrupt: - break - - client.disconnect() - -Note that ``poll`` accepts a ``timeout=`` parameter, and it will return -``None`` if other thread got the update before you could or if the timeout -expired, so it's important to check ``if not update``. - -This can coexist with the rest of ``N`` workers, or you can set it to ``0`` -additional workers: - - ``client = TelegramClient('session', api_id, api_hash, update_workers=0)`` - -You **must** set it to ``0`` (or higher), as it defaults to ``None`` and that -has a different meaning. ``None`` workers means updates won't be processed -*at all*, so you must set it to some integer value if you want -``client.updates.poll()`` to work. - - -Using the main thread instead the ``ReadThread`` -************************************************ - -If you have no work to do on the ``MainThread`` and you were planning to have -a ``while True: sleep(1)``, don't do that. Instead, don't spawn the secondary -``ReadThread`` at all like so: - - .. code-block:: python - - client = TelegramClient( - ... - spawn_read_thread=False - ) - -And then ``.idle()`` from the ``MainThread``: - - ``client.idle()`` - -You can stop it with :kbd:`Control+C`, and you can configure the signals -to be used in a similar fashion to `Python Telegram Bot`__. - -As a complete example: - - .. code-block:: python - - def callback(update): - print('I received', update) - - client = TelegramClient('session', api_id, api_hash, - update_workers=1, spawn_read_thread=False) - - client.connect() - client.add_event_handler(callback) - client.idle() # ends with Ctrl+C - - -This is the preferred way to use if you're simply going to listen for updates. - -__ https://lonamiwebs.github.io/Telethon/types/update.html -__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460 +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. diff --git a/readthedocs/extra/basic/asyncio-crash-course.rst b/readthedocs/extra/basic/asyncio-crash-course.rst new file mode 100644 index 00000000..8a5861b0 --- /dev/null +++ b/readthedocs/extra/basic/asyncio-crash-course.rst @@ -0,0 +1,123 @@ +.. _asyncio-crash-course: + +=========================== +A Crash Course into asyncio +=========================== + + +Why asyncio? +************ + +Python's `asyncio `_ is the +standard way to run asynchronous code from within Python. Since Python 3.5, +using ``async def`` and ``await`` became possible, and Python 3.6 further +improves what you can do with asynchronous code, although it's not the only +way (other projects like `Trio `_ also exist). + +Telegram is a service where all API calls are executed in an asynchronous +way. You send your request, and eventually, Telegram will process it and +respond to it. It feels natural to make a library that also behaves this +way: you send a request, and you can ``await`` for its result. + +Having understood that Telegram's API follows an asynchronous model and +developing a library that does the same greatly simplifies the internal +code and eases working with the API. + +Using ``asyncio`` keeps a cleaner library that will be easier to understand, +develop, and that will be faster than using threads, which are harder to get +right and can cause issues. It also enables to use the powerful ``asyncio`` +system such as futures, timeouts, cancellation, etc. in a natural way. + +If you're still not convinced or you're just not ready for using ``asyncio``, +the library offers a synchronous interface without the need for all the +``async`` and ``await`` you would otherwise see. `Follow this link +`_ to find out more. + + +How do I get started? +********************* + +To get started with ``asyncio``, all you need is to setup your main +``async def`` like so: + +.. code-block:: python + + import asyncio + + async def main(): + pass # Your code goes here + + if __name__ == '__main__': + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) + + +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 coroutine: + +.. code-block:: python + + async def main(): + client = TelegramClient(...) + + # client.start() is a coroutine (async def), it needs an await + await client.start() + + # Sending a message also interacts with the API, and needs an await + await client.send_message('me', 'Hello myself!') + + +If you don't know anything else about ``asyncio``, this will be enough +to get you started. Once you're ready to learn more about it, you will +be able to use that power and everything you've learnt with Telethon. +Just remember that if you use ``await``, you need to be inside of an +``async def``. + +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 ` +which is a property that you can wait on until you call +`await client.disconnect() `: + + +.. code-block:: python + + client = TelegramClient(...) + + @client.on(events.NewMessage) + async def handler(event): + print(event) + + async def main(): + await client.start() + await client.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: + +.. code-block:: python + + client = TelegramClient(...) + + @client.on(events.NewMessage) + async def handler(event): + print(event) + + if __name__ == '__main__': + loop = asyncio.get_event_loop() + loop.run_until_complete(client.start()) + client.run_until_disconnected() + + +More resources to learn asyncio +******************************* + +If you would like to learn a bit more about why ``asyncio`` is something +you should learn, `check out my blog post +`_ that goes into more detail. diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index 8b231ded..dccc2031 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -26,6 +26,14 @@ 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 # Use your own values here @@ -54,7 +62,7 @@ your disk. This is by default a database file using Python's ``sqlite3``. .. code-block:: python - client.start() + rc(client.start()) This is explained after going through the manual process. @@ -64,14 +72,14 @@ Doing so is very easy: .. code-block:: python - client.connect() # Must return True, otherwise, try again + rc(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: .. code-block:: python - client.is_user_authorized() # Returns True if you can send requests + rc(client.is_user_authorized()) # Returns True if you can send requests If you're not authorized, you need to `.sign_in `: @@ -79,8 +87,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: ')) + rc(client.send_code_request(phone_number)) + myself = rc(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. @@ -103,10 +111,14 @@ As a full example: .. code-block:: python client = TelegramClient('anon', api_id, api_hash) - assert client.connect() - if not client.is_user_authorized(): - client.send_code_request(phone_number) - me = client.sign_in(phone_number, input('Enter code: ')) + + 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()) All of this, however, can be done through a call to `.start() @@ -115,7 +127,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() + loop.run_until_complete(client.start()) The code shown is just what `.start() @@ -169,11 +181,12 @@ again with a ``password=``: import getpass from telethon.errors import SessionPasswordNeededError - client.sign_in(phone) - try: - client.sign_in(code=input('Enter code: ')) - except SessionPasswordNeededError: - client.sign_in(password=getpass.getpass()) + 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()) The mentioned `.start() @@ -196,32 +209,33 @@ See the examples below: from telethon.errors import EmailUnconfirmedError - # Sets 2FA password for first time: - client.edit_2fa(new_password='supersecurepassword') + async def main(): + # Sets 2FA password for first time: + await client.edit_2fa(new_password='supersecurepassword') - # Changes password: - client.edit_2fa(current_password='supersecurepassword', - new_password='changedmymind') + # Changes password: + await client.edit_2fa(current_password='supersecurepassword', + new_password='changedmymind') - # Clears current password (i.e. removes 2FA): - client.edit_2fa(current_password='changedmymind', new_password=None) + # Clears current password (i.e. removes 2FA): + await client.edit_2fa(current_password='changedmymind', new_password=None) - # 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 + # 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 - # 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: - client.edit_2fa(current_password='memes and dreams', - new_password='memes and dreams', - hint='It keeps you alive') + # 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') __ 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 3a160880..9ed5da16 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -40,33 +40,34 @@ Through the use of the :ref:`sessions`, the library will automatically remember the ID and hash pair, along with some extra information, so you're able to just do this: - .. code-block:: python +.. 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 = client.get_dialogs() + dialogs = await client.get_dialogs() # 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) # Getting entities through their ID (User, Chat or Channel) - entity = client.get_entity(some_id) + entity = await 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 - 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)) All methods in the :ref:`telegram-client` call `.get_input_entity() @@ -134,9 +135,10 @@ library, the raw requests you make to the API are also able to call `client.get_input_entity() ` wherever needed, so you can even do things like: - .. code-block:: python +.. code-block:: python - client(SendMessageRequest('username', 'hello')) + async def main(): + 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 9f14aa0a..6d6d2a6e 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -21,6 +21,9 @@ Creating a client .. code-block:: python + import asyncio + loop = asyncio.get_event_loop() + from telethon import TelegramClient # These example values won't work. You must get your own api_id and @@ -29,7 +32,7 @@ Creating a client api_hash = '0123456789abcdef0123456789abcdef' client = TelegramClient('session_name', api_id, api_hash) - client.start() + loop.run_until_complete(client.start()) **More details**: :ref:`creating-a-client` @@ -39,31 +42,36 @@ Basic Usage .. code-block:: python - # Getting information about yourself - print(client.get_me().stringify()) + async def main(): + # Getting information about yourself + me = await client.get_me() + print(me.stringify()) - # Sending a message (you can use 'me' or 'self' to message yourself) - client.send_message('username', 'Hello World from Telethon!') + # Sending a message (you can use 'me' or 'self' to message yourself) + await client.send_message('username', 'Hello World from Telethon!') - # Sending a file - client.send_file('username', '/home/myself/Pictures/holidays.jpg') + # Sending a file + await client.send_file('username', '/home/myself/Pictures/holidays.jpg') - # 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) + # 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) - # Listing all the dialogs (conversations you have open) - for dialog in client.get_dialogs(limit=10): - print(utils.get_display_name(dialog.entity), dialog.draft.text) + # Listing all the dialogs (conversations you have open) + async for dialog in client.get_dialogs(limit=10): + print(dialog.name, dialog.draft.text) - # Downloading profile photos (default path is the working directory) - client.download_profile_photo('username') + # Downloading profile photos (default path is the working directory) + 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_messages('username') - client.download_media(messages[0]) + # 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()) **More details**: :ref:`telegram-client` @@ -77,15 +85,11 @@ Handling Updates 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.') + client.run_until_disconnected() **More details**: :ref:`working-with-updates` diff --git a/readthedocs/extra/basic/telegram-client.rst b/readthedocs/extra/basic/telegram-client.rst index 17ff3bb4..9a308f56 100644 --- a/readthedocs/extra/basic/telegram-client.rst +++ b/readthedocs/extra/basic/telegram-client.rst @@ -34,7 +34,14 @@ For instance, retrieving your own user can be done in a single line: .. code-block:: python - myself = client.get_me() + import asyncio + + async def main(): + myself = await client.get_me() + + if __name__ == '__main__': + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) Internally, this method has sent a request to Telegram, who replied with the information about your own user, and then the desired information @@ -46,10 +53,11 @@ how the library refers to either of these: .. code-block:: python - # 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') + 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 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 @@ -59,30 +67,31 @@ 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!') + async def main(): + # Note that you can use 'me' or 'self' to message yourself + await 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) - 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) + await client.send_message('username', '**Using** __markdown__ `too`!') - 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.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 + async 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 - for dialog in client.get_dialogs(limit=10): - print(utils.get_display_name(dialog.entity), dialog.draft.text) + # Dialogs are the conversations you have open + async for dialog in client.get_dialogs(limit=10): + print(dialog.name, dialog.draft.text) - # Default path is the working directory - client.download_profile_photo('username') + # Default path is the working directory + await client.download_profile_photo('username') - # Call .disconnect() when you're done - client.disconnect() + # Call .disconnect() when you're done + await 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 8d1b3e50..386722c8 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -34,37 +34,38 @@ let's dive in! Getting Started *************** - .. code-block:: python +.. code-block:: python - from telethon import TelegramClient, events + import asyncio + from telethon import TelegramClient, events - client = TelegramClient(..., update_workers=1, spawn_read_thread=False) - client.start() + client = TelegramClient('name', api_id, api_hash) - @client.on(events.NewMessage) - def my_event_handler(event): - if 'hello' in event.raw_text: - event.reply('hi!') + @client.on(events.NewMessage) + async def my_event_handler(event): + if 'hello' in event.raw_text: + await event.reply('hi!') - client.idle() + asyncio.get_event_loop().run_until_complete(client.start()) + client.run_until_disconnected() Not much, but there might be some things unclear. What does this code do? - .. code-block:: python +.. code-block:: python - from telethon import TelegramClient, events + import asyncio + from telethon import TelegramClient, events - client = TelegramClient(..., update_workers=1, spawn_read_thread=False) - client.start() + client = TelegramClient('name', api_id, api_hash) -This is normal initialization (of course, pass session name, API ID and hash). +This is normal creation (of course, pass session name, API ID and hash). Nothing we don't know already. - .. code-block:: python +.. code-block:: python - @client.on(events.NewMessage) + @client.on(events.NewMessage) This Python decorator will attach itself to the ``my_event_handler`` @@ -72,11 +73,11 @@ definition, and basically means that *on* a `NewMessage ` *event*, the callback function you're about to define will be called: - .. code-block:: python +.. code-block:: python - def my_event_handler(event): - if 'hello' in event.raw_text: - event.reply('hi!') + async def my_event_handler(event): + if 'hello' in event.raw_text: + await event.reply('hi!') If a `NewMessage @@ -84,14 +85,16 @@ If a `NewMessage and ``'hello'`` is in the text of the message, we ``reply`` to the event with a ``'hi!'`` message. - .. code-block:: python +.. code-block:: python - client.idle() + asyncio.get_event_loop().run_until_complete(client.start()) + client.run_until_disconnected() -Finally, this tells the client 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`. +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 +until disconnected. For this refer to :ref:`update-modes`. More on events @@ -130,17 +133,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): - if event.raw_text.startswith('eval'): - expression = event.raw_text.replace('eval', '').strip() - event.reply(str(ast.literal_eval(expression))) + @client.on(events.NewMessage(chats='TelethonOffTopic', outgoing=True, + pattern='eval (.+)')) + async def admin_handler(event): + expression = event.pattern_match.group(1) + await event.reply(str(ast.literal_eval(expression))) You can pass one or more chats to the ``chats`` parameter (as a list or tuple), @@ -178,23 +181,23 @@ it makes no sense to process any other handlers in the chain. For this case, it is possible to raise a `telethon.events.StopPropagation` exception which will cause the propagation of the update through your handlers to stop: - .. code-block:: python +.. code-block:: python - from telethon.events import StopPropagation + from telethon.events import StopPropagation - @client.on(events.NewMessage) - def _(event): - # ... some conditions - event.delete() + @client.on(events.NewMessage) + async def _(event): + # ... some conditions + await event.delete() - # Other handlers won't have an event to work with - raise StopPropagation + # Other handlers won't have an event to work with + raise StopPropagation - @client.on(events.NewMessage) - def _(event): - # Will never be reached, because it is the second handler - # in the chain. - pass + @client.on(events.NewMessage) + async def _(event): + # Will never be reached, because it is the second handler + # in the chain. + pass Remember to check :ref:`telethon-events-package` if you're looking for diff --git a/readthedocs/index.rst b/readthedocs/index.rst index 994ea2e5..886c8b06 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -22,6 +22,17 @@ when you upgrade! 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). + + 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. + + What is this? ************* @@ -39,6 +50,7 @@ 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 diff --git a/telethon/events/newmessage.py b/telethon/events/newmessage.py index 6881c8cf..30b4ef30 100644 --- a/telethon/events/newmessage.py +++ b/telethon/events/newmessage.py @@ -174,8 +174,8 @@ class NewMessage(EventBuilder): >>> from telethon import TelegramClient, events >>> client = TelegramClient(...) >>> - >>> @client.on(events.NewMessage(pattern=r'hi (\w+)!')) - ... def handler(event): + >>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!')) + ... async def handler(event): ... # In this case, the result is a ``Match`` object ... # since the ``str`` pattern was converted into ... # the ``re.compile(pattern).match`` function.