diff --git a/DEVELOPING.md b/DEVELOPING.md index 5e7d7799..43778762 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -8,13 +8,15 @@ python tools/codegen.py Formatting, type-checking and testing: ```sh -pip install isort black mypy pytest pytest-asyncio +pip install -e client/[dev] python tools/check.py ``` -Documentation: +Documentation (requires [sphinx](https://www.sphinx-doc.org) and [graphviz](https://www.graphviz.org)'s `dot`): ```sh -pip install sphinx_rtd_theme +pip install -e client/[doc] python tools/docgen.py ``` + +Note that multiple optional dependency sets can be specified by separating them with a comma (`[dev,doc]`). diff --git a/client/doc/basic/installation.rst b/client/doc/basic/installation.rst new file mode 100644 index 00000000..0abde3ac --- /dev/null +++ b/client/doc/basic/installation.rst @@ -0,0 +1,59 @@ +Installation +============ + +Telethon is a Python 3 library, which means you need to download and install Python to use it. +Installing Python, using virtual environments, and the basics of the language, are outside of the scope of this guide. + +You can find the official resources to `download Python `_, +learn about the `Python Setup and Usage `_ for different platforms, +or follow the `The Python Tutorial `_ to learn the basics. +These are not necessarily the best resources to learn, but they are official. +Be sure to search online if you prefer learning in video form or otherwise. + +You can confirm that you have Python installed with: + +.. code-block:: shell + + python --version + +Which should print something similar to ``Python 3.11.5`` (or newer). + + +Installing the latest stable version +------------------------------------ + +Once you have a working Python 3 installation, you can install or upgrade the ``telethon`` package with ``pip``: + +.. code-block:: shell + + python -m pip install --upgrade telethon + +Be sure to use lock-files if your project depends on a specific, older version of the library! + + +Installing development versions +------------------------------- + +If you want the *latest* unreleased changes, you can run the following command instead: + +.. code-block:: shell + + python -m pip install --upgrade https://github.com/LonamiWebs/Telethon/archive/v2.zip + +.. note:: + + The development version may have bugs and is not recommended for production use. + However, when you are `reporting a library bug `, + you must reproduce the issue in this version before reporting the problem. + + +Verifying the installation +-------------------------- + +To verify that the library is installed correctly, run the following command: + +.. code-block:: shell + + python -c "import telethon; print(telethon.__version__)" + +The version number of the library should show in the output. diff --git a/client/doc/basic/next-steps.rst b/client/doc/basic/next-steps.rst new file mode 100644 index 00000000..4c1e7abf --- /dev/null +++ b/client/doc/basic/next-steps.rst @@ -0,0 +1,32 @@ +Next steps +========== + +.. currentmodule:: telethon + +By now, you should have successfully gone through both the :doc:`installation` and :doc:`signing-in` processes. + +With a :class:`Client` instance connected and authorized, you can send any request to Telegram. +Some requests are bot-specific, and some are user-specific, but most can be used by any account. +You will need to have the correct permissions and pass valid parameters, but after that, your imagination is the limit. + +Telethon features extensive documentation for every public item offered by the library. +All methods within the :class:`Client` also contain one or more examples on how to use them. + +Whatever you build, remember to comply with both `Telegram's Terms of Service `_ +and `Telegram's API ToS `_. +There are `several requests that applications must make `_: + +.. epigraph:: + + […] when logging in as an existing user, apps are supposed to call :tl:`help.getTermsOfServiceUpdate` + to check for any updates to the Terms of Service; + this call should be repeated after ``expires`` seconds have elapsed. + If an update to the Terms Of Service is available, clients are supposed to show a consent popup; + if accepted, clients should call :tl:`help.acceptTermsOfService`, + providing the ``termsOfService id`` JSON object; + in case of denial, clients are to delete the account using :tl:`account.deleteAccount`, + providing Decline ToS update as deletion reason. + +The library will not make these calls for you, as it cannot know how users interact with the application being developed. +If you use an official client alongside the application you are developing, +it should be safe to rely on that client making the requests instead. diff --git a/client/doc/basic/signing-in.rst b/client/doc/basic/signing-in.rst new file mode 100644 index 00000000..9adb91e4 --- /dev/null +++ b/client/doc/basic/signing-in.rst @@ -0,0 +1,211 @@ +Signing in +========== + +.. currentmodule:: telethon + +Most of Telegram's API methods are gated behind an account login. +But before you can interact with the API at all, you will need to obtain an API ID and hash pair for your application. + + +Registering your Telegram application +------------------------------------- + +Before working with Telegram's API, you (as the application developer) need to get an API ID and hash: + +1. `Login to your Telegram account `_ with the phone number of the developer account to use. + +2. Click under *API Development tools*. + +3. A *Create new application* window will appear. Fill in your application details. + There is no need to enter any *URL*, and only the first two fields (*App title* and *Short name*) can currently be changed later. + +4. Click on *Create application* at the end. + Remember that your **API hash is secret** and Telegram won't let you revoke it. + Don't post it anywhere! + +This API ID and hash can now be used to develop an application using Telegram's API. +Telethon consumes this API ID and hash in order to make the requests to Telegram. + +It is important to note that this API ID and hash is attached to a developer account, +and can be used to develop applications or otherwise using libraries such as Telethon. + +The *users* of the application you develop do *not* need to provide their own API ID and hash. +The API ID and hash values are meant to be hardcoded in the application. +Any user is then able to login with just their phone number or bot token, even if they have not registered an application themselves. + +.. important:: + + The API ID and hash are meant to be *secret*, but Python is often distributed in source-code form. + These two things conflict with eachother! + You can opt to obfuscate the values somehow, or perhaps distribute an executable binary file instead. + Depending on what you are developing, it might be reasonable to expect users to provide their own API ID and hash instead. + + Official applications *also* must embed the API ID and hash, but these are often distributed as binary files. + Whatever you do, **do not use other people's API ID and hash!** + Telegram may detect this as suspicious and ban the accounts. + +If you receive an error, Telegram is likely blocking the registration of a new applications. +The best you can do is wait and try again later. +If the issue persists, you may try contacting them, using a proxy or using a VPN. +Be aware that some phone numbers are not eligible to register applications with. + + +Interactive login +----------------- + +The library offers a method for "quick and dirty" scripts known as :meth:`~Client.interactive_login`. +This method will first check whether the account was previously logged-in, and if not, ask for a phone number to be input. + +You can write the code in a file (such as ``hello.py``) and then run it, or use the built-in ``asyncio``-enabled REPL. +For this tutorial, we'll be using the ``asyncio`` REPL: + +.. code-block:: shell + + python -m asyncio + +.. important:: + + If you opt to write your code in a file, do **not** call your script ``telethon.py``! + Python will try to import from there and it will fail with an error such as "ImportError: cannot import name ...". + +The first thing we need to do is import the :class:`Client` class and create an instance of it: + +.. code-block:: python + + from telethon import Client + + client = Client('name', 12345, '0123456789abcdef0123456789abcdef') + +The second and third parameters must be the API ID and hash, respectively. +We have a client instance now, but we can't send requests to Telegram until we connect! +So the next step is to :meth:`~Client.connect`: + +.. code-block:: python + + await client.connect() + +If all went well, you will have connected to one of Telegram's servers. +If you run into issues, you might need to try a different hosting provider or use some sort of proxy. + +Once you're connected, we can begin the :meth:`~Client.interactive_login`: + +.. code-block:: python + + await client.interactive_login() + +Do as the prompts say on the terminal, and you will have successfully logged-in! + +Once you're done, make sure to :meth:`~Client.disconnect` for a graceful shutdown. + + +Manual login +------------ + +We've talked about the second and third parameters of the :class:`Client` constructor, but not the first: + +.. code-block:: python + + client = Client('name', 12345, '0123456789abcdef0123456789abcdef') + +The first parameter is the "session". +When using a string or a :class:`~pathlib.Path`, the library will create a SQLite database in that path. +The session path can contain directory separators and live anywhere in the file system. +Telethon will automatically append the ``.session`` extension if you don't provide any. + +Briefly, the session contains some of the information needed to connect to Telegram. +This includes the datacenter belonging to the account logged-in, and the authorization key used for encryption, among other things. + +.. important:: + + **Do not leak the session file!** + Anyone with that file can login to the account stored in it. + If you believe someone else has obtained this file, immediately revoke all active sessions from an official client. + +Let's take a look at what :meth:`~Client.interactive_login` does under the hood. + +1. First, it's using an equivalent of :meth:`~Client.is_authorized` to check whether the session was logged-in previously. +2. Then, it will either :meth:`~Client.bot_sign_in` with a bot token or :meth:`~Client.request_login_code` with a phone number. + + * If it logged-in as a bot account, a :class:`~types.User` is returned and we're done. + * Otherwise, a login code was sent. Go to step 3. + +3. Attempt to complete the user sign-in with :meth:`~Client.sign_in`, by entering the login code. + + * If a :class:`~types.User` is returned, we're done. + * Otherwise, a 2FA password is required. Go to step 4. + +4. Use :meth:`Client.check_password` to check that the password is correct. + + * If the password is correct, :class:`~types.User` is returned and we're done. + +Put into code, a user can thus login as follows: + +.. code-block:: python + + from telethon import Client + from telethon.types import User + + # SESSION, API_ID, API_HASH should be previously defined in your code + async with Client(SESSION, API_ID, API_HASH) as client: + if not await client.is_authorized(): + phone = input('phone: ') + login_token = await client.request_login_code(phone_or_token) + + code = input('code: ') + user_or_token = await client.sign_in(login_token, code) + + if isinstance(user_or_token, User): + return user_or_token + + # user_or_token is PasswordToken + password_token = user_or_token + + import getpass + password = getpass.getpass("password: ") + user = await client.check_password(password_token, password) + return user + +A bot account does not need to request login code and cannot have passwords, so the login flow is much simpler: + +.. code-block:: python + + from telethon import Client + + # SESSION, API_ID, API_HASH should be previously defined in your code + async with Client(SESSION, API_ID, API_HASH) as client: + bot_token = input('token: ') + bot_user = await client.bot_sign_in(bot_token) + return bot_user + +To get a bot account, you need to talk with `@BotFather `_. + +You may have noticed the ``async with`` keywords. +The :class:`Client` can be used in a context-manager. +This will automatically call :meth:`Client.connect` and :meth:`Client.disconnect` for you. + +A good way to structure your code is as follows: + +.. code-block:: python + + import asyncio + from telethon import Client + + SESSION = ... + API_ID = ... + API_HASH = ... + + async def main(): + async with Client(SESSION, API_ID, API_HASH) as client: + ... # use client to your heart's content + + if __name__ == '__main__': + asyncio.run(main()) + +This way, both the :mod:`asyncio` event loop and the :class:`Client` will exit cleanly. +Otherwise, you might run into errors such as tasks being destroyed while pending. + +.. note:: + + Once a :class:`Client` instance has been connected, you cannot change the :mod:`asyncio` event loop. + Methods like :func:`asyncio.run` setup and tear-down a new event loop every time. + If the loop changes, the client is likely to be "stuck" because its loop cannot advance. diff --git a/client/doc/concepts/botapi-vs-mtproto.rst b/client/doc/concepts/botapi-vs-mtproto.rst new file mode 100644 index 00000000..da8b12dd --- /dev/null +++ b/client/doc/concepts/botapi-vs-mtproto.rst @@ -0,0 +1,117 @@ +HTTP Bot API vs MTProto +======================= + +.. currentmodule:: telethon + +Telethon is more than capable to develop bots for Telegram. +If you haven't decided which wrapper library for bots to use yet, +using Telethon from the beginning may save you some headaches later. + + +What is Bot API? +---------------- + +`Telegram's HTTP Bot API `_, +from now on referred to as simply "Bot API", is Telegram's official way for developers to control their own Telegram bots. +Quoting their main page: + +.. epigraph:: + + The Bot API is an HTTP-based interface created for developers keen on building bots for Telegram. + + To learn how to create and set up a bot, please consult our + `Introduction to Bots `_ + and `Bot FAQ `_. + +Bot API is simply an HTTP endpoint offering a custom HTTP API. +Underneath, it uses `tdlib `_ to talk to Telegram's servers. + +You can configure your bot details via `@BotFather `_. +This includes name, commands, and auto-completion. + + +What is MTProto? +---------------- + +`MTProto `_ stands for "Mobile Transport Protocol". +It is the language that the Telegram servers "speak". +You can think of it as an alternative to HTTP. + +Telegram offers multiple APIs. +All user accounts must use the API offered via MTProto. +We will call this API the "MTProto API". +This is the canonical Telegram API. + +The MTProto API is different from Bot API, but bot accounts can use either in the same way. +In fact, the Bot API is implemented to use the MTProto API to map the requests and responses. + +Telethon implements the MTProto and offers classes and methods that can be called to send requests. +In Telethon, all the methods and types generated from Telegram's API definitions are also known as :term:`Raw API`. +This name was chosen because it gives you "raw" access to the MTProto API. +Telethon's :class:`Client` and other custom types are implemented using the :term:`Raw API`. + + +Advantages of MTProto over Bot API +---------------------------------- + +MTProto clients (like Telethon) connect directly to Telegram's servers via TCP or UDP. +There is no HTTP connection, no "polling", and no "web hooks". +We can compare the two visually: + +.. graphviz:: + :caption: Communication between a Client and the Bot API + + digraph botapi { + rankdir=LR; + "Client" -> "HTTP API"; + "HTTP API" -> "MTProto API"; + "MTProto API" -> "Telegram Servers"; + + "Telegram Servers" -> "MTProto API" [label="IPC"]; + "MTProto API" -> "HTTP API" [label="MTProto"]; + "HTTP API" -> "Client" [label="JSON"]; + } + +.. graphviz:: + :caption: Communication between a Client and the MTProto API + + digraph botapi { + rankdir=LR; + "Client" -> "MTProto API"; + "MTProto API" -> "Telegram Servers"; + + "Telegram Servers" -> "MTProto API" [label="IPC"]; + "MTProto API" -> "Client" [label="MTProto"]; + } + +When interacting with the MTProto API directly, we can cut down one intermediary (the HTTP API). +This is less theoretical overhead and latency. +It also means that, even if the Bot API endpoint is down, talking to the MTProto API could still work. + +The methods offered by the Bot API map to some of the methods in the MTProto API, but not all. +The Bot API is its own abstraction, and chooses to expose less details. +By talking to the MTProto API directly, you unlock the `full potential `_. + +The serialization format used by MTProto is more compact than JSON and can still be compressed. + +Another benefit of avoiding the Bot API is the ease to switch to user accounts instead of bots. +The MTProto API is the same for users and bots, so by using Telethon, you don't need to learn to use a second library. + + +Migrating from Bot API to Telethon +---------------------------------- + +If the above points convinced you to switch to Telethon, the following short guides should help you make the switch! + +It doesn't matter if you wrote your bot with `requests `_ +and you were making API requests manually, or if you used a wrapper library like +`python-telegram-bot `_ +or `pyTelegramBotAPI `. +You will surely be pleased with Telethon! + +If you were using an asynchronous library like `aiohttp `_ +or a wrapper like `aiogram `_, the switch will be even easier. + + +Migrating from TODO +^^^^^^^^^^^^^^^^^^^ diff --git a/client/doc/concepts/chats.rst b/client/doc/concepts/chats.rst new file mode 100644 index 00000000..cabd1204 --- /dev/null +++ b/client/doc/concepts/chats.rst @@ -0,0 +1,108 @@ +Chats +===== + +.. currentmodule:: telethon + +The term :term:`chat` is extremely overloaded, so it's no surprise many are confused by what it means. +This section should hopefully clear that up. + + +Telethon Chat +------------- + +The word :term:`chat` in Telethon is used to refer a place where messages are sent to. +Therefore, a Telethon :term:`chat` can be another user, a bot, a group, or a broadcast channel. +All of those are places where messages can be sent. + +Of course, chats do more things than contain messages. +They often have a name, username, photo, description, and other information. + +When a :term:`chat` appears in a parameter or as a property, +it means that it will be either a :class:`~types.User`, :class:`~types.Group` or :class:`~types.Channel`. + +When a parameter must be "chat-like", it means Telethon will accept anything that can be "converted" to a :term:`chat`. +The following types are chat-like: + +* The ``'me'`` literal string. This represents the account that is logged in ("yourself"). +* An ``'@username'``. The at-sign ``@`` is optional. Note that links are not supported. +* An ``'+1 23'`` phone number string. It must be an ``str`` and start with the plus-sign ``+`` character. +* An ``123`` integer identifier. It must be an ``int`` and cannot be negative. +* An existing :class:`~types.User`, :class:`~types.Group` or :class:`~types.Channel`. +* A :class:`~types.PackedChat`. + +Previous versions of Telethon referred to this term as "entity" or "entities" instead. + + +Telegram Chat +------------- + +The Telegram API is very confusing when it comes to the word "chat". +You only need to know about this if you plan to use the :term:`Raw API`. + +In the schema definitions, there are two boxed types, :tl:`User` and :tl:`Chat`. +A boxed :tl:`User` can only be the bare :tl:`user`, but the boxed :tl:`Chat` can be either a bare :tl:`chat` or a bare :tl:`channel`. + +A bare :tl:`chat` always refers to small groups. +A bare :tl:`channel` can have either the ``broadcast`` or the ``megagroup`` flag set to ``True``. + +A bare :tl:`channel` with the ``broadcast`` flag set to ``True`` is known as a broadcast channel. +A bare :tl:`channel` with the ``megagroup`` flag set to ``True`` is known as a supergroup. + +A bare :tl:`chat` with has less features than a bare :tl:`channel` ``megagroup``. +Official clients are very good at hiding this difference. +They will implicitly convert bare :tl:`chat` to bare :tl:`channel` ``megagroup`` when doing certain operations. +Doing things like setting a username is actually a two-step process (migration followed by updating the username). +Official clients transparently merge the history of migrated :tl:`channel` with their old :tl:`chat`. + +In Telethon: + +* A :class:`~types.User` always corresponds to :tl:`user`. +* A :class:`~types.Group` represents either a :tl:`chat` or a :tl:`channel` ``megagroup``. +* A :class:`~types.Channel` represents a :tl:`channel` ``broadcast``. + +Telethon classes aim to map to similar concepts in official applications. + + +Bot API chat +------------ + +The Bot API follows a certain convention when it comes to identifiers: + +* User IDs are positive. +* Chat IDs are negative. +* Channel IDs are prefixed with ``-100``. + +Telethon encourages the use of :class:`~types.PackedChat` instead of naked identifiers. +As a reminder, negative identifiers are not supported in Telethon's chat-like parameters. + + +Encountering chats +------------------ + +The way you encounter chats in Telethon is no different from official clients. +If you: + +* …have joined a group or channel, or have sent private messages to some user, you can :meth:`~Client.get_dialogs`. +* …know the user is in your contact list, you can :meth:`~Client.get_contacts`. +* …know the user has a common chat with you, you can :meth:`~Client.get_participants` of the chat in common. +* …know the username of the user, group, or channel, you can :meth:`~Client.resolve_username`. +* …are a bot responding to users, you will be able to access the :attr:`types.Message.sender`. + +Chats access hash +----------------- + +Users, supergroups and channels all need an :term:`access hash`. + +In Telethon, the :class:`~types.PackedChat` is the recommended way to deal with the identifier-hash pairs. +This compact type can be used anywhere a chat is expected. +It's designed to be easy to store and cache in any way your application chooses. + +Bot accounts can get away with an invalid :term:`access hash` for certain operations under certain conditions. +The same is true for user accounts, although to a lesser extent. + +When using just the identifier to refer to a chat, Telethon will attempt to retrieve its hash from its in-memory cache. +If this fails, an invalid hash will be used. This may or may not make the API call succeed. +For this reason, it is recommended that you always use :class:`~types.PackedChat` instead. + +Remember that an :term:`access hash` is account-bound. +You cannot obtain an :term:`access hash` in Account-A and use it in Account-B. diff --git a/client/doc/concepts/errors.rst b/client/doc/concepts/errors.rst new file mode 100644 index 00000000..10a2487a --- /dev/null +++ b/client/doc/concepts/errors.rst @@ -0,0 +1,40 @@ +RPC Errors +========== + +.. currentmodule:: telethon + +:term:`RPC` stands for Remote Procedure Call. +By extension, RPC Errors occur when a RPC fails to execute in the server. +In Telethon, a :term:`RPC error` corresponds to the :class:`RpcError` class. + +Telethon will only ever raise :class:`RpcError` when the result to a :term:`RPC` is an error. +If the error is raised, you know it comes from Telegram. +Consequently, when using :term:`Raw API`, if a :class:`RpcError` occurs, it is never a bug in the library. + +:term:`RPC error` consist of an integer :attr:`~RpcError.code` and a string :attr:`~RpcError.name`. +The :attr:`RpcError.code` is roughly the same as `HTTP status codes `_. +The :attr:`RpcError.name` is often a string in ``SCREAMING_CASE`` and refers to what went wrong. + +Certain error names also contain an integer value. +This value is removed from the :attr:`~RpcError.name` and put into :attr:`RpcError.value`. +If Telegram responds with ``FLOOD_WAIT_60``, the name would be ``'FLOOD_WAIT'`` and the value ``60``. + +A very common error is ``FLOOD_WAIT``. +It occurs when you have attempted to use a request too many times during a certain window of time: + +.. code-block:: python + + import asyncio + from telethon import RpcError + + try: + await client.send_message('me', 'Spam') + except RpcError as e: + # If we get a flood error, sleep. Else, propagate the error. + if e.name == 'FLOOD_WAIT': + await asyncio.sleep(e.value) + else: + raise + +Note that the library can automatically handle and retry on ``FLOOD_WAIT`` for you. +Refer to the ``flood_sleep_threshold`` of the :class:`Client` to learn how. diff --git a/client/doc/concepts/full-api.rst b/client/doc/concepts/full-api.rst new file mode 100644 index 00000000..0bfd75c8 --- /dev/null +++ b/client/doc/concepts/full-api.rst @@ -0,0 +1,66 @@ +The Full API +============ + +.. currentmodule:: telethon + +The API surface offered by Telethon is not exhaustive. +Telegram is constantly adding new features, and both implementing and documenting custom methods would an exhausting, never-ending job. + +Telethon concedes to this fact and implements only commonly-used features to keep a lean API. +Access to the entirity of Telegram's API via Telethon's :term:`Raw API` is a necessary evil. + +The ``telethon._tl`` module has a leading underscore to signal that it is private. +It is not covered by the semver guarantees of the library, but you may need to use it regardless. +If the :class:`Client` doesn't offer a method for what you need, using the :term:`Raw API` is inevitable. + + +Invoking Raw API methods +------------------------ + +The :term:`Raw API` can be *invoked* in a very similar way to other client methods: + +.. code-block:: python + + from telethon import _tl as tl + + was_reset = await client(tl.functions.account.reset_wall_papers()) + +Inside ``telethon._tl.functions`` you will find a function for every single :term:`RPC` supported by Telegram. +The parameters are keyword-only and do not have defaults. +Whatever arguments you pass is exactly what Telegram will receive. +Whatever is returned is exactly what Telegram responded with. + +All functions inside ``telethon._tl.functions`` will return the serialized request. +When calling a :class:`Client` instance with this request as input, it will be sent to Telegram and wait for a response. + +Multiple requests may be in-flight at the same time, specially when using :mod:`asyncio`. +Telethon will attempt to combine these into a single "container" when possible as an optimization. + + +Exploring the Raw API +--------------------- + +Everything under ``telethon._tl.types`` implements :func:`repr`. +This means you can print any response and get the Python representation of that object. + +All types are proper classes with attributes. +You do not need to use a regular expression on the string representation to access the field you want. + +Most :term:`RPC` return an abstract class from ``telethon._tl.abcs``. +To check for a concrete type, you can use :func:`isinstance`: + +.. code-block:: python + + invite = await client(tl.functions.messages.check_chat_invite(hash='aBcDeF')) + if isinstance(invite, tl.types.ChatInviteAlready): + print(invite.chat) + +The ``telethon._tl`` module is not documented here because it would result in tens of megabytes. +Instead, there are multiple alternatives: + +* Use Telethon's separate site to search in the `Telethon Raw API `_. + This is the recommended way. It also features auto-generated examples. +* Use Python's built-in :func:`help` and :func:`dir` to help you navigate the module. +* Use an editor with autocompletion support. +* Choose the right layer from `Telegram's official API Layers `_. + Note that the `TL Schema `_ might not be up-to-date. diff --git a/client/doc/concepts/glossary.rst b/client/doc/concepts/glossary.rst new file mode 100644 index 00000000..11ca8d74 --- /dev/null +++ b/client/doc/concepts/glossary.rst @@ -0,0 +1,42 @@ +Glossary +======== + +.. currentmodule:: telethon + +.. glossary:: + :sorted: + + chat + A :class:`~types.User`, :class:`~types.Group` or :class:`~types.Channel`. + + .. seealso:: The :doc:`../concepts/chats` concept. + + Raw API + Functions and types under ``telethon._tl`` that enable access to all of Telegram's API. + + .. seealso:: The :doc:`../concepts/full-api` concept. + + access hash + Account-bound integer tied to a specific resource. + Users, channels, photos and documents are all resources with an access hash. + The access hash doesn't change, but every account will see a different value for the same resource. + + RPC + Remote Procedure Call. + Invoked when calling a :class:`Client` with a function from ``telethon._tl.functions``. + + RPC Error + Error type returned by Telegram. + :class:`RpcError` contains an integer code similar to HTTP status codes and a name. + + .. seealso:: The :doc:`../concepts/errors` concept. + + session + Data used to securely connect to Telegram and other state related to the logged-in account. + + .. seealso:: The :doc:`../concepts/sessions` concept. + + MTProto + Mobile Transport Protocol used to interact with Telegram's API. + + .. seealso:: The :doc:`../concepts/botapi-vs-mtproto` concept. diff --git a/client/doc/concepts/sessions.rst b/client/doc/concepts/sessions.rst new file mode 100644 index 00000000..20af0bf3 --- /dev/null +++ b/client/doc/concepts/sessions.rst @@ -0,0 +1,55 @@ +Sessions +======== + +.. currentmodule:: telethon + +In Telethon, the word :term:`session` is used to refer to the set of data needed to connect to Telegram. +This includes the server address of your home datacenter, as well as the authorization key bound to an account. +When you first connect to Telegram, an authorization key is generated to encrypt all communication. +After login, Telegram remembers this authorization key as logged-in, so you don't need to login again. + +.. important:: + + **Do not leak the session file!** + Anyone with that file can login to the account stored in it. + If you believe someone else has obtained this file, immediately revoke all active sessions from an official client. + +Some auxiliary information such as the user ID of the logged-in user is also kept. + +The update state, which can change every time an update is received from Telegram, is also stored in the session. +Telethon needs this information to catch up on all missed updates while your code was not running. +This is why it's important to call :meth:`Client.disconnect`. +Doing so flushes all the update state to the session and saves it. + + +Session files +------------- + +Telethon defaults to using SQLite to store the session state. +The session state is written to ``.session`` files, so make sure your VCS ignores them! +To make sure the ``.session`` file is saved, you should call :meth:`Client.disconnect` before exiting the program. + +The first parameter in the :class:`Client` constructor is the session to use. +You can use a `str`, a :class:`pathlib.Path` or a :class:`session.Storage`. +The string or path are relative to the Current Working Directory. +You can use absolute paths or relative paths to folders elsewhere. +The ``.session`` extension is automatically added if the path has no extension. + + +Session storages +---------------- + +The :class:`session.Storage` abstract base class defines the required methods to create custom storages. +Telethon comes with two built-in storages: + +* :class:`~session.SqliteSession`. This is used by default when a string or path is used. +* :class:`~session.MemorySession`. This is used by default when the path is ``None``. + You can also use it directly when you have a :class:`~session.Session` instance. + It's useful when you don't have file-system access. + +If you would like to store the session state in a different way, you can subclass :class:`session.Storage`. + +Some Python installations do not have the ``sqlite3`` module. +In this case, attempting to use the default :class:`~session.SqliteSession` will fail. +If this happens, you can try reinstalling Python. +If you still don't have the ``sqlite3`` module, you should use a different storage. diff --git a/client/doc/concepts/updates.rst b/client/doc/concepts/updates.rst new file mode 100644 index 00000000..9fe6af01 --- /dev/null +++ b/client/doc/concepts/updates.rst @@ -0,0 +1,75 @@ +Updates +======= + +.. currentmodule:: telethon + +Updates are an important topic in a messaging platform like Telegram. +After all, you want to be notified as soon as certain events happen, such as new message arrives. + +Telethon abstracts away Telegram updates with :mod:`~telethon.events`. + +.. important:: + + It is strongly advised to configure logging when working with events: + + .. code-block:: python + + import logging + logging.basicConfig( + format='[%(levelname) 5s/%(asctime)s] %(name)s: %(message)s', + level=logging.WARNING + ) + + With the above, you will see all warnings and errors and when they happened. + + +Filtering events +---------------- + +There is no way to tell Telegram to only send certain updates. +Telethon must be received and process all updates to ensure correct ordering. + +Filters are not magic. +They work all the same as ``if`` conditions inside your event handlers. +However, they offer a more convenient and consistent way to check for certain conditions. + +All built-in filters can be found in :mod:`telethon.events.filters`. + +When registering an event handler, you can optionally define the filter to use. +You can retrieve a handler's filter with :meth:`~Client.get_handler_filter`. +You can set (and overwrite) a handler's filter with :meth:`~Client.set_handler_filter`. + +Filters are meant to be fast and never raise exceptions. +For this reason, filters cannot be asynchronous. +This reduces the chance a filter will do slow IO and potentially fail. + +A filter is simply a callable function that takes an event as input and returns a boolean. +If the filter returns ``True``, the handler will be called. +Using this knowledge, you can create custom filters too. +If you need state, you can use a class with a ``__call__`` method defined: + +.. code-block:: python + + def only_odd_messages(event): + "A filter that only handles messages when their ID is divisible by 2" + return event.id % 2 == 0 + + client.add_event_handler(handler, events.NewMessage, only_odd_messages) + + # ... + + class OnlyDivisibleMessages: + "A filter that only handles messages when their ID is divisible by some amount" + def __init__(self, divisible_by): + self.divisible_by = divisible_by + + def __call__(self, event): + return event.id % self.divisible_by == 0 + + client.add_event_handler(handler, events.NewMessage, OnlyDivisibleMessages(7)) + +Custom filters should accept any :class:`~events.Event`. +You can use :func:`isinstance` if your filter can only deal with certain types of events. + +If you need to perform asynchronous operations, you can't use a filter. +Instead, manually check for those conditions inside your handler. diff --git a/client/doc/conf.py b/client/doc/conf.py index b577c7df..c4864dce 100644 --- a/client/doc/conf.py +++ b/client/doc/conf.py @@ -3,6 +3,11 @@ # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +import os +import sys + +sys.path.insert(0, os.path.abspath(os.curdir)) # for custom extensions + # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information @@ -14,11 +19,21 @@ release = "2.0.0a0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -# extensions = [] - -templates_path = ["_templates"] -# exclude_patterns = [] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.graphviz", + "roles.tl", +] +intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} +tl_ref_url = "https://tl.telethon.dev" +autodoc_default_options = { + "members": True, + "undoc-members": True, +} +modindex_common_prefix = ["telethon."] +graphviz_output_format = "svg" # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output diff --git a/client/doc/developing/changelog.rst b/client/doc/developing/changelog.rst new file mode 100644 index 00000000..52869da3 --- /dev/null +++ b/client/doc/developing/changelog.rst @@ -0,0 +1,8 @@ +Changelog (Version History) +=========================== + + +v2 alpha +-------- + +WIP! diff --git a/client/doc/developing/coding-style.rst b/client/doc/developing/coding-style.rst new file mode 100644 index 00000000..fd9fce04 --- /dev/null +++ b/client/doc/developing/coding-style.rst @@ -0,0 +1,17 @@ +Coding style +============ + +Knowledge of Python is a obviously a must to develop a Python library. +A good online resource is `Dive Into Python 3 `_. + +Telethon uses multiple tools to automatically format the code and check for linting rules. +This means you can simply ignore formatting and let the tools handle it for you. +You can find these tools under the ``tools/`` folder. + +The documentation is written with mostly a newline after every period. +This is not a hard rule. +Lines can be cut earlier if they become too long to be comfortable. + +Commit messages should be short and descriptive. +They should start with an action in the present ("Fix" and not "Fixed"). +This saves a few characters and represents what the commit will "do" after applied. diff --git a/client/doc/developing/migration-guide.rst b/client/doc/developing/migration-guide.rst new file mode 100644 index 00000000..6a1ec3b8 --- /dev/null +++ b/client/doc/developing/migration-guide.rst @@ -0,0 +1,4 @@ +Migrating from v1 to v2 +======================= + +WIP! diff --git a/client/doc/developing/philosophy.rst b/client/doc/developing/philosophy.rst new file mode 100644 index 00000000..435d6150 --- /dev/null +++ b/client/doc/developing/philosophy.rst @@ -0,0 +1,10 @@ +Philosophy +========== + +* Dependencies should only be added when absolutely necessary. +* Dependencies written in anything other than Python cannot be mandatory. +* The library must work correctly with no system dependencies other than Python 3. +* Strict type-checking is required to pass everywhere in the library to make upgrades easier. +* The code structure must make use of hard and clear boundaries to keep the different parts decoupled. +* The API should cover only the most commonly used features to avoid bloat and reduce maintenance costs. +* Documentation must be a pleasure to use and contain plenty of code examples. diff --git a/client/doc/developing/project-structure.rst b/client/doc/developing/project-structure.rst new file mode 100644 index 00000000..978524ec --- /dev/null +++ b/client/doc/developing/project-structure.rst @@ -0,0 +1,95 @@ +Project Structure +================= + +.. currentmodule:: telethon + +The repository contains several folders, each with their own "package". + + +benches/ +-------- + +This folder contains different benchmarks. +Pretty straightforward. + + +stubs/ +------ + +If a dependency doesn't support typing, files here must work around that. + + +tools/ +------ + +Various utility scripts. +Each script should have a "comment" at the top explaining what they are for. + +See ``DEVELOPING.md`` in the repository root to learn how to use some of the tools. + + +generator/ +---------- + +A package that should not be published and is only used when developing the library. +The implementation is private and exists under the ``src/*/_impl/`` folder. +Only select parts are exported under public modules. +Tests live under ``tests/``. + +The implementation consists of a parser and a code generator. + +The parser is able to read parse ``.tl`` files (Type-Language definition files). +It doesn't do anything with the files other than to represent the content as Python objects. + +The code generator uses the parsed definitions to generate Python code. +Most of the code to serialize and deserialize objects lives under ``serde/``. + +An in-memory "filesystem" structure is kept before writing all files to disk. +This makes it possible to execute most of the process in a sans-io manner. +Once the code generation finishes, all files are written to disk at once. + +See ``DEVELOPING.md`` in the repository root to learn how to generate code. + + +client/ +------- + +The Telethon client library and documentation lives here. +This is the package that gets published. +The implementation is private and exists under the ``src/*/_impl/`` folder. +Only select parts are exported under public modules. +Tests live under ``tests/``. + +The client implementation consists of several subpackages. + +The ``tl`` package sits at the bottom. +It is where the generated code is placed. +It also contains some of the definitions needed for the generated code to work. +Even though all the :term:`RPC` live here, this package can't do anything by itself. + +The ``crypto`` package implements all the encryption and decryption rules used by Telegram. +Details concerning the :term:`MTProto` are mostly avoided, so the package can be generally useful. + +The ``mtproto`` package implements the logic required to talk to Telegram. +It is implemented in a sans-io manner. +This package is responsible for generating an authorization key and serializing packets. +It also contains some optimizations which are not strictly necessary when implementing the library. + +The ``mtsender`` package simply adds IO to ``mtproto``. +It is responsible for driving the network, enqueuing requests, and waiting for results. + +The ``session`` crate implements what's needed to manage the :term:`session` state. +The logic to handle and correctly order updates also lives here, in a sans-io manner. + +The ``client`` ties everything together. +This is what defines the Pythonic API to interact with Telegram. +Custom object and event types also live here. + +Even though only common methods are implemented, the code is still huge. +For this reason, the :class:`Client` implementation is separated from the class definition. +The class definition only contains documentation and calls functions defined in other files. +A tool under ``tools/`` exists to make it easy to keep these two in sync. + +If you plan to port the library to a different language, good luck! +You will need a code generator, the ``crypto``, ``mtproto`` and ``mtsender`` packages to have an initial working version. +The tests are your friend, write them too! diff --git a/client/doc/index.rst b/client/doc/index.rst index b8b239cd..7ffdbe55 100644 --- a/client/doc/index.rst +++ b/client/doc/index.rst @@ -1,15 +1,126 @@ -Telethon's documentation -======================== +.. |svglogo| image:: ../../logo.svg + :width: 24pt + :height: 24pt + +.. only:: html + + .. highlights:: |svglogo| **Welcome to Telethon's documentation!** + +.. only:: not html + + .. highlights:: **Welcome to Telethon's documentation!** + +.. code-block:: python + + import asyncio + from telethon import Client, events + from telethon.events import filters + + async def main(): + async with Client('name', api_id, api_hash) as client: + me = await client.interactive_login() + await client.send_message(me, f'Hello, {me.full_name}!') + + @client.on(events.NewMessage, filters.Text(r'(?i)hello')) + async def handler(event): + await event.reply('Hey!') + + await client.run_until_disconnected() + + asyncio.run(main()) + +* Are you new here? Jump straight into :doc:`basic/installation`! +* Looking for the Client API reference? See :doc:`modules/client`. +* Did you upgrade the library? Please read :doc:`developing/changelog`. +* Coming from Bot API or want to create new bots? See :doc:`concepts/botapi-vs-mtproto`. +* Used Telethon before v2.0? See :doc:`developing/migration-guide`. +* Want to hack away with the raw API? Search in `Telethon Raw API `_. + + +Preface +======= + +.. rubric:: What is this? + +Telegram is a popular messaging application. +This library is meant to make it easy for you to write Python programs that can interact with Telegram. +Think of it as a wrapper that has already done the hard work for you, so you can focus on developing an application. + + +.. rubric:: How should I use the documentation? + +This documentation is divided in multiple sections. The first few sections are a guide, while others contain the API reference or a glossary of terms. +The documentation assumes some familiarity with Python. + +If you are getting started with the library, you should follow the documentation in order by pressing the "Next" button at the bottom-right of every page. + +You can also use the menu on the left to quickly skip over sections if you're looking for something in particular or want to continue where you left off. + + +First steps +=========== + +In this section you will learn how to install the library and login to your Telegram account. + +:doc:`‣ Start reading Installation ` .. toctree:: - :maxdepth: 2 - :caption: Contents: + :hidden: + :caption: First steps + + basic/installation + basic/signing-in + basic/next-steps +API reference +============= -Indices and tables -================== +This section contains all the functions and types offered by the library. -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +:doc:`‣ Start reading Client API ` + +.. toctree:: + :hidden: + :caption: API reference + + modules/client + modules/events + modules/types + modules/sessions + +Concepts +======== + +A more in-depth explanation of some of the concepts and words used in Telethon. + +:doc:`‣ Start reading Chat concept ` + +.. toctree:: + :hidden: + :caption: Concepts + + concepts/chats + concepts/updates + concepts/sessions + concepts/errors + concepts/botapi-vs-mtproto + concepts/full-api + concepts/glossary + +Developing +========== + +Tips and tricks to develop both with the library and for the library. + +:doc:`‣ Start reading Changelog ` + +.. toctree:: + :hidden: + :caption: Developing + + developing/changelog + developing/migration-guide + developing/philosophy.rst + developing/coding-style.rst + developing/project-structure.rst diff --git a/client/doc/modules/client.rst b/client/doc/modules/client.rst new file mode 100644 index 00000000..ac028519 --- /dev/null +++ b/client/doc/modules/client.rst @@ -0,0 +1,4 @@ +Client +====== + +.. autoclass:: telethon.Client diff --git a/client/doc/modules/events.rst b/client/doc/modules/events.rst new file mode 100644 index 00000000..aee1d44f --- /dev/null +++ b/client/doc/modules/events.rst @@ -0,0 +1,12 @@ +Events and filters +================== + +Events +------ + +.. automodule:: telethon.events + +Filters +------- + +.. automodule:: telethon.events.filters diff --git a/client/doc/modules/sessions.rst b/client/doc/modules/sessions.rst new file mode 100644 index 00000000..6426e19a --- /dev/null +++ b/client/doc/modules/sessions.rst @@ -0,0 +1,4 @@ +Session storages +================ + +.. automodule:: telethon.session diff --git a/client/doc/modules/types.rst b/client/doc/modules/types.rst new file mode 100644 index 00000000..56d1227e --- /dev/null +++ b/client/doc/modules/types.rst @@ -0,0 +1,6 @@ +Types +===== + +.. automodule:: telethon.types + +.. autoclass:: telethon.RpcError diff --git a/client/doc/roles/tl.py b/client/doc/roles/tl.py new file mode 100644 index 00000000..683441bd --- /dev/null +++ b/client/doc/roles/tl.py @@ -0,0 +1,34 @@ +from docutils import nodes, utils +from docutils.parsers.rst.roles import set_classes + + +def make_link_node(rawtext, app, name, options): + try: + base = app.config.tl_ref_url + if not base: + raise AttributeError + except AttributeError as e: + raise ValueError("tl_ref_url config value is not set") from e + + if base[-1] != "/": + base += "/" + + set_classes(options) + node = nodes.reference( + rawtext, utils.unescape(name), refuri="{}?q={}".format(base, name), **options + ) + return node + + +def tl_role(name, rawtext, text, lineno, inliner, options=None, content=None): + if options is None: + options = {} + + app = inliner.document.settings.env.app + node = make_link_node(rawtext, app, text, options) + return [node], [] + + +def setup(app): + app.add_role("tl", tl_role) + app.add_config_value("tl_ref_url", None, "env") diff --git a/client/pyproject.toml b/client/pyproject.toml index 4e26967a..a418124d 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -28,9 +28,16 @@ dynamic = ["version"] [project.optional-dependencies] cryptg = ["cryptg~=0.4"] dev = [ + "isort~=5.12", + "black~=23.3.0", + "mypy~=1.3", "pytest~=7.3", "pytest-asyncio~=0.21", ] +doc = [ + "sphinx_rtd_theme~=1.2", + "types-docutils~=0.20", +] [project.urls] "Homepage" = "https://telethon.dev/" @@ -43,4 +50,4 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [tool.setuptools.dynamic] -version = {attr = "telethon.__version__"} +version = {attr = "telethon.version.__version__"}