diff --git a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst index 8cc34479..ca435dfb 100644 --- a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst +++ b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst @@ -93,7 +93,7 @@ every time its used, simply call `telethon.utils.get_input_peer`: .. code-block:: python from telethon import utils - peer = utils.get_input_user(entity) + peer = utils.get_input_peer(entity) .. note:: @@ -117,8 +117,8 @@ request we do: ) # __call__ is an alias for client.invoke(request). Both will work -Message sent! Of course, this is only an example. There are nearly 250 -methods available as of layer 73, and you can use every single of them +Message sent! Of course, this is only an example. There are over 250 +methods available as of layer 80, and you can use every single of them as you wish. Remember to use the right types! To sum up: .. code-block:: python diff --git a/readthedocs/extra/advanced-usage/update-modes.rst b/readthedocs/extra/advanced-usage/update-modes.rst index 6ee6fd46..717b7ec3 100644 --- a/readthedocs/extra/advanced-usage/update-modes.rst +++ b/readthedocs/extra/advanced-usage/update-modes.rst @@ -24,8 +24,8 @@ loop, you should use `client.run_until_disconnected Behind the scenes, this method is ``await``'ing on the `client.disconnected -` property, so the code above -and the following are equivalent: +` property, +so the code above and the following are equivalent: .. code-block:: python @@ -42,7 +42,8 @@ and the following are equivalent: You could also run `client.disconnected -` until it completed. +` +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 diff --git a/readthedocs/extra/basic/asyncio-crash-course.rst b/readthedocs/extra/basic/asyncio-crash-course.rst index 8a5861b0..a9c99785 100644 --- a/readthedocs/extra/basic/asyncio-crash-course.rst +++ b/readthedocs/extra/basic/asyncio-crash-course.rst @@ -19,9 +19,9 @@ 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. +Now that we know that Telegram's API follows an asynchronous model, you +should understand the benefits of developing a library that does the same, +it 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 @@ -54,7 +54,7 @@ To get started with ``asyncio``, all you need is to setup your 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: +You must ``await`` all ``async def``, also known as a *coroutines*: .. code-block:: python @@ -78,9 +78,9 @@ 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.disconnected ` which is a property that you can wait on until you call -`await client.disconnect() `: +`await client.disconnect() `: .. code-block:: python @@ -115,6 +115,68 @@ This is the same as using the ``run_until_disconnected()`` method: client.run_until_disconnected() +Which methods should I use and when? +************************************ + +Something to note is that you must always get an event loop if you +want to be able to make any API calls. This is done as follows: + +.. code-block:: python + + import asyncio + loop = asyncio.get_event_loop() + +The loop must be running, or things will never get sent. +Normally, you use ``run_until_complete``: + +.. code-block:: python + + async def coroutine(): + await asyncio.sleep(1) + + loop.run_until_complete(coroutine()) + +Note that ``asyncio.sleep`` is in itself a coroutine, so this will +work too: + +.. code-block:: python + + loop.run_until_complete(asyncio.sleep(1)) + +Generally, you make an ``async def main()`` if you need to ``await`` +a lot of things, instead of typing ``run_until_complete`` all the time: + +.. code-block:: python + + async def main(): + message = await client.send_message('me', 'Hi') + await asyncio.sleep(1) + await message.delete() + + loop.run_until_complete(main()) + + # vs + + message = loop.run_until_complete(client.send_message('me', 'Hi')) + loop.run_until_complete(asyncio.sleep(1)) + loop.run_until_complete(message.delete()) + +You can see that the first version has more lines, but you had to type +a lot less. You can also rename the run method to something shorter: + +.. code-block:: python + + # Note no parenthesis (), we're not running it, just copying the method + rc = loop.run_until_complete + message = rc(client.send_message('me', 'Hi')) + 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. + + More resources to learn asyncio ******************************* diff --git a/readthedocs/extra/basic/telegram-client.rst b/readthedocs/extra/basic/telegram-client.rst index 9a308f56..0002bfe1 100644 --- a/readthedocs/extra/basic/telegram-client.rst +++ b/readthedocs/extra/basic/telegram-client.rst @@ -30,14 +30,16 @@ this is, any "method" listed on the API. There are a few methods (and 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: +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): .. code-block:: python import asyncio async def main(): - myself = await client.get_me() + myself = await client.get_me() # <- a single line! if __name__ == '__main__': loop = asyncio.get_event_loop() diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index 386722c8..34681db2 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -82,7 +82,8 @@ the callback function you're about to define will be called: If a `NewMessage ` event occurs, -and ``'hello'`` is in the text of the message, we ``reply`` to the event +and ``'hello'`` is in the text of the message, we `.reply() +` to the event with a ``'hi!'`` message. .. code-block:: python @@ -101,23 +102,35 @@ More on events ************** The `NewMessage ` event has much -more than what was shown. You can access the ``.sender`` of the message -through that member, or even see if the message had ``.media``, a ``.photo`` -or a ``.document`` (which you could download with for example -`client.download_media(event.photo) `. +more than what was shown. You can access the `.sender +` of the message +through that member, or even see if the message had `.media +`, a `.photo +` or a `.document +` (which you +could download with for example `client.download_media(event.photo) +`. -If you don't want to ``.reply`` as a reply, you can use the ``.respond()`` -method instead. Of course, there are more events such as ``ChatAction`` or -``UserUpdate``, and they're all used in the same way. Simply add the -``@client.on(events.XYZ)`` decorator on the top of your handler and you're -done! The event that will be passed always is of type ``XYZ.Event`` (for -instance, ``NewMessage.Event``), except for the ``Raw`` event which just -passes the ``Update`` object. +If you don't want to `.reply() +` as a reply, +you can use the `.respond() ` +method instead. Of course, there are more events such as `ChatAction +` or `UserUpdate +`, and they're all +used in the same way. Simply add the `@client.on(events.XYZ) +` decorator on the top +of your handler and you're done! The event that will be passed always +is of type ``XYZ.Event`` (for instance, `NewMessage.Event +`), except for the `Raw +` event which just passes the :tl:`Update` object. -Note that ``.reply()`` and ``.respond()`` are just wrappers around the +Note that `.reply() +` and `.respond() +` are just wrappers around the `client.send_message() ` method which supports the ``file=`` parameter. -This means you can reply with a photo if you do ``event.reply(file=photo)``. +This means you can reply with a photo if you do `event.reply(file=photo) +`. You can put the same event on many handlers, and even different events on the same handler. You can also have a handler work on only specific chats, @@ -154,6 +167,45 @@ random number, while if you say ``'eval 4+4'``, you will reply with the solution. Try it! +Properties vs. methods +********************** + +The event shown above acts just like a `custom.Message +`, which means you +can access all the properties it has, like ``.sender``. + +**However** events are different to other methods in the client, like +`client.get_messages `. +Events *may not* send information about the sender or chat, which means it +can be ``None``, but all the methods defined in the client always have this +information so it doesn't need to be re-fetched. For this reason, you have +``get_`` methods, which will make a network call if necessary. + +In short, you should do this: + +.. code-block:: python + + @client.on(events.NewMessage) + async def handler(event): + # event.input_chat may be None, use event.get_input_chat() + chat = await event.get_input_chat() + sender = await event.get_sender() + buttons = await event.get_buttons() + + async def main(): + async for message in client.iter_messages('me', 10): + # Methods from the client always have these properties ready + chat = message.input_chat + sender = message.sender + buttons = message.buttons + +Notice, properties (`message.sender +`) don't need an ``await``, but +methods (`message.get_sender +`) **do** need an ``await``, +and you should use methods in events for these properties that may need network. + + Events without decorators ************************* diff --git a/readthedocs/extra/developing/test-servers.rst b/readthedocs/extra/developing/test-servers.rst index 89a5d734..bf8fd97e 100644 --- a/readthedocs/extra/developing/test-servers.rst +++ b/readthedocs/extra/developing/test-servers.rst @@ -32,4 +32,6 @@ 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') + loop.run_until_complete(client.start( + phone='9996621234', code_callback=lambda: '22222' + )) diff --git a/readthedocs/extra/examples/bots.rst b/readthedocs/extra/examples/bots.rst index 0e1a9122..76851e88 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 = client(GetInlineBotResultsRequest( + bot_results = loop.run_until_complete(client(GetInlineBotResultsRequest( bot, user_or_chat, 'query', '' - )) + ))) And you can select any of their results by using :tl:`SendInlineBotResultRequest`: @@ -30,16 +30,27 @@ And you can select any of their results by using from telethon.tl.functions.messages import SendInlineBotResultRequest - client(SendInlineBotResultRequest( + loop.run_until_complete(client(SendInlineBotResultRequest( get_input_peer(user_or_chat), obtained_query_id, obtained_str_id - )) + ))) Talking to Bots with special reply markup ***************************************** +Generally, you just use the `message.click() +` method: + +.. code-block:: python + + async def main(): + messages = await client.get_messages('somebot') + await messages[0].click(0) + +You can also do it manually. + To interact with a message that has a special reply markup, such as `@VoteBot`__ polls, you would use :tl:`GetBotCallbackAnswerRequest`: @@ -47,11 +58,11 @@ To interact with a message that has a special reply markup, such as from telethon.tl.functions.messages import GetBotCallbackAnswerRequest - client(GetBotCallbackAnswerRequest( + loop.run_until_complete(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 847d8462..ba4838e2 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -12,7 +12,7 @@ Joining a chat or channel ************************* Note that :tl:`Chat` are normal groups, and :tl:`Channel` are a -special form of ``Chat``, which can also be super-groups if +special form of :tl:`Chat`, which can also be super-groups if their ``megagroup`` member is ``True``. @@ -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 - client(JoinChannelRequest(channel)) + loop.run_until_complete(client(JoinChannelRequest(channel))) # In the same way, you can also leave such channel from telethon.tl.functions.channels import LeaveChannelRequest - client(LeaveChannelRequest(input_channel)) + loop.run_until_complete(client(LeaveChannelRequest(input_channel))) For more on channels, check the `channels namespace`__. @@ -51,7 +51,9 @@ 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 = loop.run_until_complete( + client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) + ) Adding someone else to such chat or channel @@ -68,19 +70,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``). - client(AddChatUserRequest( + loop.run_until_complete(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 - client(InviteToChannelRequest( + loop.run_until_complete(client(InviteToChannelRequest( channel, [users_to_add] - )) + ))) Checking a link without joining @@ -102,6 +104,14 @@ Retrieving all chat members (channels too) This method will handle different chat types for you automatically. +Here is the easy way to do it: + +.. code-block:: python + + participants = loop.run_until_complete(client.get_participants(group)) + +Now we will show how the method works internally. + In order to get all the members from a mega-group or channel, you need to use :tl:`GetParticipantsRequest`. As we can see it needs an :tl:`InputChannel`, (passing the mega-group or channel you're going to @@ -123,10 +133,9 @@ a fixed limit: all_participants = [] while True: - participants = client(GetParticipantsRequest( - channel, ChannelParticipantsSearch(''), offset, limit, - hash=0 - )) + participants = loop.run_until_complete(client(GetParticipantsRequest( + channel, ChannelParticipantsSearch(''), offset, limit, hash=0 + ))) if not participants.users: break all_participants.extend(participants.users) @@ -193,7 +202,7 @@ Giving or revoking admin permissions can be done with the :tl:`EditAdminRequest` # ) # Once you have a ChannelAdminRights, invoke it - client(EditAdminRequest(channel, user, rights)) + loop.run_until_complete(client(EditAdminRequest(channel, user, rights))) # User will now be able to change group info, delete other people's # messages and pin messages. @@ -252,7 +261,7 @@ banned rights of an user through :tl:`EditBannedRequest` and its parameter embed_links=True ) - client(EditBannedRequest(channel, user, rights)) + loop.run_until_complete(client(EditBannedRequest(channel, user, rights))) Kicking a member @@ -267,9 +276,11 @@ is enough: from telethon.tl.functions.channels import EditBannedRequest from telethon.tl.types import ChannelBannedRights - client(EditBannedRequest(channel, user, ChannelBannedRights( - until_date=None, - view_messages=True + loop.run_until_complete(client(EditBannedRequest( + channel, user, ChannelBannedRights( + until_date=None, + view_messages=True + ) ))) @@ -291,11 +302,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. - client(GetMessagesViewsRequest( + loop.run_until_complete(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 a327e9a0..ad594a81 100644 --- a/readthedocs/extra/examples/users.rst +++ b/readthedocs/extra/examples/users.rst @@ -19,11 +19,12 @@ you should use :tl:`GetFullUser`: from telethon.tl.functions.users import GetFullUserRequest - full = client(GetFullUserRequest(user)) - # or even - full = client(GetFullUserRequest('username')) + async def main(): + full = await client(GetFullUserRequest(user)) + # or even + full = await client(GetFullUserRequest('username')) - bio = full.about + bio = full.about See :tl:`UserFull` to know what other fields you can access. @@ -39,7 +40,9 @@ request. Omitted fields won't change after invoking :tl:`UpdateProfile`: from telethon.tl.functions.account import UpdateProfileRequest - client(UpdateProfileRequest(about='This is a test from Telethon')) + loop.run_until_complete(client(UpdateProfileRequest(a + bout='This is a test from Telethon' + ))) Updating your username @@ -51,7 +54,7 @@ You need to use :tl:`account.UpdateUsername`: from telethon.tl.functions.account import UpdateUsernameRequest - client(UpdateUsernameRequest('new_username')) + loop.run_until_complete(client(UpdateUsernameRequest('new_username'))) Updating your profile photo @@ -65,6 +68,6 @@ through :tl:`UploadProfilePhoto`: from telethon.tl.functions.photos import UploadProfilePhotoRequest - client(UploadProfilePhotoRequest( + loop.run_until_complete(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 42e6a87d..231f1076 100644 --- a/readthedocs/extra/examples/working-with-messages.rst +++ b/readthedocs/extra/examples/working-with-messages.rst @@ -13,38 +13,39 @@ Forwarding messages .. note:: - Use the `telethon.telegram_client.TelegramClient.forward_messages` + Use the `telethon.client.messages.MessageMethods.forward_messages` friendly method instead unless you have a better reason not to! This method automatically accepts either a single message or many of them. .. code-block:: python - # 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? - ) + 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 have ``Message`` objects - client.forward_messages( - entity, # to which entity you are forwarding the messages - messages # the messages (or message) to forward - ) + # 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 + ) - # 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() - 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? - )) + 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? + )) 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 @@ -56,7 +57,7 @@ Searching Messages .. note:: - Use the `telethon.telegram_client.TelegramClient.iter_messages` + Use the `telethon.client.messages.MessageMethods.iter_messages` friendly method instead unless you have a better reason not to! This method has ``search`` and ``filter`` parameters that will @@ -71,7 +72,7 @@ into issues_. A valid example would be: from telethon.tl.types import InputMessagesFilterEmpty filter = InputMessagesFilterEmpty() - result = client(SearchRequest( + result = loop.run_until_complete(client(SearchRequest( peer=peer, # On which chat/conversation q='query', # What to search for filter=filter, # Filter to use (maybe filter for media) @@ -84,7 +85,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 @@ -99,7 +100,7 @@ you tried setting the ``from_id`` filter, and as the error says, you can't do that. Leave it set to ``None`` and it should work. As with every method, make sure you use the right ID/hash combination for -your ``InputUser`` or ``InputChat``, or you'll likely run into errors like +your :tl:`InputUser` or :tl:`InputChat`, or you'll likely run into errors like ``UserIdInvalidError``. @@ -115,29 +116,25 @@ send yourself the very first sticker you have: .. code-block:: python - # Get all the sticker sets this user has - sticker_sets = client(GetAllStickersRequest(0)) + async def main(): + # Get all the sticker sets this user has + from telethon.tl.functions.messages import GetAllStickersRequest + sticker_sets = await client(GetAllStickersRequest(0)) - # Choose a sticker set - 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 = 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( - peer=client.get_me(), - media=InputMediaDocument( - id=InputDocument( - id=stickers.documents[0].id, - access_hash=stickers.documents[0].access_hash + # Get the stickers for this sticker set + 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 + await client.send_file('me', stickers.documents[0]) .. _issues: https://github.com/LonamiWebs/Telethon/issues/215 diff --git a/readthedocs/telethon.client.rst b/readthedocs/telethon.client.rst index fa9518aa..3803f0d2 100644 --- a/readthedocs/telethon.client.rst +++ b/readthedocs/telethon.client.rst @@ -59,6 +59,11 @@ their methods. :undoc-members: :show-inheritance: +.. automodule:: telethon.client.downloads + :members: + :undoc-members: + :show-inheritance: + .. automodule:: telethon.client.uploads :members: :undoc-members: diff --git a/telethon_generator/data/error_descriptions b/telethon_generator/data/error_descriptions index 2552d4ff..8d7cf7ee 100644 --- a/telethon_generator/data/error_descriptions +++ b/telethon_generator/data/error_descriptions @@ -22,7 +22,7 @@ FILE_PART_X_MISSING=Part {} of the file is missing from storage FILE_PART_INVALID=The file part number is invalid FIRSTNAME_INVALID=The first name is invalid INPUT_METHOD_INVALID=The invoked method does not exist anymore or has never existed -INPUT_REQUEST_TOO_LONG=The input request was too long. This may be a bug in the library as it can occur when serializing more bytes than it should (likeappending the vector constructor code at the end of a message) +INPUT_REQUEST_TOO_LONG=The input request was too long. This may be a bug in the library as it can occur when serializing more bytes than it should (like appending the vector constructor code at the end of a message) LASTNAME_INVALID=The last name is invalid LIMIT_INVALID=An invalid limit was provided. See https://core.telegram.org/api/files#downloading-files LOCATION_INVALID=The location given for a file was invalid. See https://core.telegram.org/api/files#downloading-files