From 495de58925c17db66b328445938b68b3b9c4070a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 16 Sep 2023 19:13:00 +0200 Subject: [PATCH] Expand docs on migration guide --- client/doc/concepts/glossary.rst | 17 + client/doc/concepts/updates.rst | 1 + client/doc/developing/migration-guide.rst | 290 +++++++++++++++++- client/doc/developing/project-structure.rst | 2 +- .../telethon/_impl/client/client/client.py | 6 +- .../src/telethon/_impl/client/client/files.py | 4 +- .../telethon/_impl/client/client/messages.py | 2 +- .../src/telethon/_impl/client/types/file.py | 4 + .../telethon/_impl/client/types/message.py | 4 +- 9 files changed, 321 insertions(+), 9 deletions(-) diff --git a/client/doc/concepts/glossary.rst b/client/doc/concepts/glossary.rst index 5ed51f2c..0d254768 100644 --- a/client/doc/concepts/glossary.rst +++ b/client/doc/concepts/glossary.rst @@ -43,3 +43,20 @@ Glossary Mobile Transport Protocol used to interact with Telegram's API. .. seealso:: The :doc:`../concepts/botapi-vs-mtproto` concept. + + login + Used to refer to the login process as a whole, as opposed to the action to :term:`sign in`. + The "login code" or "login token" get their name because they belong to the login process. + + sign in + Used to refer to the action to sign into either a user or bot account, as opposed to the :term:`login` process. + Likewise, "sign out" is used to signify that the authorization should stop being valid. + + layer + When Telegram releases new features, it does so by releasing a new "layer". + The different layers let Telegram know what a client is capable of and how it should respond to requests. + + TL + Type Language + File format used by Telegram to define all the types and requests available in a :term:`layer`. + Telegram's site has an `Overview of the TL language `_. diff --git a/client/doc/concepts/updates.rst b/client/doc/concepts/updates.rst index 9fe6af01..4bd34701 100644 --- a/client/doc/concepts/updates.rst +++ b/client/doc/concepts/updates.rst @@ -27,6 +27,7 @@ Filtering events ---------------- There is no way to tell Telegram to only send certain updates. +Telegram sends all updates to connected active clients as they occur. Telethon must be received and process all updates to ensure correct ordering. Filters are not magic. diff --git a/client/doc/developing/migration-guide.rst b/client/doc/developing/migration-guide.rst index 6a1ec3b8..775c0cb7 100644 --- a/client/doc/developing/migration-guide.rst +++ b/client/doc/developing/migration-guide.rst @@ -1,4 +1,292 @@ Migrating from v1 to v2 ======================= -WIP! +.. currentmodule:: telethon + +v2 is a complete reboot of Telethon v1. +Because a lot of the library has suffered radical changes, there are no plans to provide "bridge" methods emulating the old interface. +Doing so would take a lot of extra time and energy, and it's honestly not fun. + +What this means is that your v1 code very likely won't run in v2. +Sorry. +I hope you can use this opportunity to shake up your dusty code into a cleaner design, too. + +The common theme in v2 could be described as "no bullshit". + +v1 had grown a lot of features. +A lot of them did a lot of things, at all once, in slightly different ways. +Semver allows additions, so v2 will start out smaller and grow in a controlled manner. + +Custom types were a frankestein monster, combining both raw and manually-defined properties in hacky ways. +Type hinting was an unmaintained disaster. +Features such as file IDs, proxies and a lot of utilities were pretty much abandoned. + +The several attempts at making v2 a reality over the years starting from the top did not work out. +A bottom-up approach was needed. +So a full rewrite was warranted. + +TLSharp was Telethon's seed. +Telethon v0 was needed to learn Python at all. +Telethon v1 was necessary to learn what was a good design, and what wasn't. +This inspired `grammers `_, a Rust re-implementation with a thought-out design. +Telethon v2 completes the loop by porting grammers back to Python, now built with years of experience in the Telegram protocol. + +It turns out static type checking is a very good idea for long-running projects. +So I strongly encourage you to use `mypy `_ when developing code with Telethon v2. +I can guarantee you will into far less problems. + +Without further ado, let's take a look at the biggest changes. +This list may not be exhaustive, but it should give you an idea on what to expect. + + +Complete project restructure +---------------------------- + +The public modules under the ``telethon`` now make actual sense. + +* The root ``telethon`` package contains the basics like the :class:`Client` and :class:`RpcError`. +* :mod:`telethon.types` contains all the types, for your tpye-hinting needs. +* :mod:`telethon.events` contains all the events. +* :mod:`telethon.events.filters` contains all the event filters. +* :mod:`telethon.session` contains the session storages, should you choose to build a custom one. +* :data:`telethon.errors` is no longer a module. + It's actually a factory object returning new error types on demand. + This means you don't need to wait for new library versions to be released to catch them. + +This was also a good opportunity to remove a lot of modules that were not supposed to public in their entirety: +``.crypto``, ``.extensions``, ``.network``, ``.custom``, ``.functions``, ``.helpers``, ``.hints``, ``.password``, ``.requestiter``, ``.sync``, ``.types``, ``.utils``. + + +Raw API is now private +---------------------- + +v2 aims to comply with `Semantic Versioning `. +This is impossible because Telegram is a live service that can change things any time. +But we can get pretty close. + +In v1, minor version changes bumped Telegram's :term:`layer`. +This technically violated semver, because they were part of a public module. + +To allow for new layers to be added without the need for major releases, ``telethon._tl`` is instead private. +Here's the recommended way to import and use it now: + +.. code-block:: python + + from telethon import _tl as tl + + was_reset = await client(tl.functions.account.reset_wall_papers()) + + if isinstance(chat, tl.abcs.User): + if isinstance(chat, tl.types.UserEmpty): + return + # chat is tl.types.User + +There are three modules (four, if you count ``core``, which you probably should not use). +Each of them can have an additional namespace (as seen above with ``account.``). + +* ``tl.functions`` contains every :term:`TL` definition treated as a function. + The naming convention now follows Python's, and are ``snake_case``. + They're no longer a class with attributes. + They serialize the request immediately. +* ``tl.abcs`` contains every abstract class, the "boxed" types from Telegram. + You can use these for your type-hinting needs. +* ``tl.types`` contains concrete instances, the "bare" types Telegram actually returns. + You'll probably use these with :func:`isinstance` a lot. + All types use :term:`__slots__` to save space. + This means you can't add extra fields to these at runtime unless you subclass. + + +Unified client iter and get methods +----------------------------------- + +The client no longer has ``client.iter_...`` methods. + +Instead, the return a type that supports both :keyword:`await` and :keyword:`async for`: + +.. code-block:: python + + messages = await client.get_messages(chat, 100) + # or + async for message in client.get_messages(chat, 100): + ... + +.. note:: + + :meth:`Client.get_messages` no longer has funny rules for the ``limit`` either. + If you ``await`` it without limit, it will probably take a long time to complete. + This is in contrast to v1, where ``get`` defaulted to 1 message and ``iter`` to no limit. + + +Removed client methods and properties +------------------------------------- + +.. rubric:: No ``client.parse_mode`` property. + +Instead, you need to specify how the message text should be interpreted every time. +In :meth:`~Client.send_message`, use ``text=``, ``markdown=`` or ``html=``. +In :meth:`~Client.send_file` and friends, use one of the ``caption`` parameters. + +.. rubric:: No ``client.loop`` property. + +Instead, you can use :func:`asyncio.get_running_loop`. + +.. rubric:: No ``client.conversation()`` method. + +Instead, you will need to `design your own FSM `_. +The simplest approach could be using a global ``states`` dictionary storing the next function to call: + +.. code-block:: python + + from functools import partial + + states = {} + + @client.on(events.NewMessage) + async def conversation_entry_point(event): + if fn := state.get(event.sender.id): + await fn(event) + else: + await event.respond('Hi! What is your name?') + state[event.sender.id] = handle_name + + async def handle_name(event): + await event.respond('What is your age?') + states[event.sender.id] = partial(handle_age, name=event.text) + + async def handle_age(event, name): + age = event.text + await event.respond(f'Hi {name}, I am {age} too!') + del states[event.sender.id] + + + +No message.raw_text or message.message +-------------------------------------- + +Messages no longer have ``.raw_text`` or ``.message`` properties. + +Instead, you can access the :attr:`types.Message.text`, +:attr:`~types.Message.text_markdown` or :attr:`~types.Message.text_html`. +These names aim to be consistent with ``caption_markdown`` and ``caption_html``. + +In v1, messages coming from a client used that client's parse mode as some sort of "global state". +Based on the client's parse mode, v1 ``message.text`` property would return different things. +But not *all* messages did this! +Those coming from the raw API had no client, so ``text`` couldn't know how to format the message. + +Overall, the old design made the parse mode be pretty hidden. +This was not very intuitive and also made it very awkward to combine multiple parse modes. + + +Event and filters are now separate +---------------------------------- + +Event types are no longer callable and do not have filters inside them. +There is no longer nested ``class Event`` inside them either. + +Instead, the event type itself is what the handler will actually be called with. +Because filters are separate, there is no longer a need for v1 ``@events.register`` either. + +Filters are now normal functions that work with any event. +Of course, this doesn't mean all filters make sense for all events. +But you can use them in an unified manner. + +Filters no longer support asynchronous operations, which removes a footgun. +This was most commonly experienced when using usernames as the ``chats`` filter in v1, and getting flood errors you couldn't handle. +In v2, you must pass a list of identifiers. +This means getting those identifiers is up to you, and you can handle it in a way that is appropriated for your application. + +.. seealso:: + + In-depth explanation for :doc:`/concepts/updates`. + + +Streamlined chat, input_chat and chat_id +---------------------------------------- + +The same goes for ``sender``, ``input_sender`` and ``sender_id``. +And also for ``get_chat``, ``get_input_chat``, ``get_sender`` and ``get_input_sender``. +Yeah, it was bad. + +Instead, events with chat information now *always* have a ``.chat``, with *at least* the ``.id``. +The same is true for the ``.sender``, as long as the event has one with at least the user identifier. + +This doesn't mean the ``.chat`` or ``.sender`` will have all the information. +Telegram may still choose to send their ``min`` version with only basic details. +But it means you don't have to remember 5 different ways of using chats. + +To replace the concept of "input chats", v2 introduces :class:`types.PackedChat`. +A "packed chat" is a chat with *just* enough information that you can use it without relying on Telethon's cache. +This is the most efficient way to call methods like :meth:`Client.send_message` too. + +The concept of "marked IDs" also no longer exists. +This means v2 no longer supports the ``-`` or ``-100`` prefixes on identifiers. +:tl:`Peer`-wrapping is gone, too. +Instead, you're strongly encouraged to use :class:`types.PackedChat` instances. + +The concepts of of "entity" or "peer" are unified to simply :term:`chat`. +Overall, dealing with users, groups and channels should feel a lot more natural. + +.. seealso:: + + In-depth explanation for :doc:`/concepts/chats`. + + +Session cache no longer exists +------------------------------ + +At least, not the way it did before. + +The v1 cache that allowed you to use just chat identifiers to call methods is no longer saved to disk. + +Sessions now only contain crucial information to have a working client. +This includes the server address, authorization key, update state, and some very basic details. + +To work around this, you can use :class:`types.PackedChat`, which is designed to be easy to store. +This means your application can choose the best way to deal with them rather than being forced into Telethon's session. + +.. seealso:: + + In-depth explanation for :doc:`/concepts/sessions`. + + +StringSession no longer exists +------------------------------ + +If you need to serialize the session data to a string, you can use something like `jsonpickle `_. +Or even the built-in :mod:`pickle` followed by :mod:`base64` or just :meth:`bytes.hex`. +But be aware that these approaches probably will not be compatible with additions to the :class:`~session.Session`. + + +TelegramClient renamed to Client +-------------------------------- + +You can rename it with :keyword:`as` during import if you want to use the old name. + +Python allows using namespaces via packages and modules. +Therefore, the full name :class:`telethon.Client` already indicates it's from ``telethon``, so the old name was redundant. + + +Changes to start and client context-manager +------------------------------------------- + +You can no longer ``start()`` the client. + +Instead, you will need to first :meth:`~Client.connect` and then start the :meth:`~Client.interactive_login`. + +In v1, the when using the client as a context-manager, ``start()`` was called. +Since that method no longer exists, it now instead only :meth:`~Client.connect` and :meth:`~Client.disconnect`. + +This means you won't get annoying prompts in your terminal if the session was not authorized. +It also means you can now use the context manager even with custom login flows. + +The old ``sign_in()`` method also sent the code, which was rather confusing. +Instead, you must now :meth:`~Client.request_login_code` as a separate operation. + +The old ``log_out`` was also renamed to :meth:`~Client.sign_out` for consistency with :meth:`~Client.sign_in`. + + +No telethon.sync hack +--------------------- + +You can no longer ``import telethon.sync`` to have most calls wrapped in :meth:`asyncio.loop.run_until_complete` for you. diff --git a/client/doc/developing/project-structure.rst b/client/doc/developing/project-structure.rst index 978524ec..f7ed064b 100644 --- a/client/doc/developing/project-structure.rst +++ b/client/doc/developing/project-structure.rst @@ -38,7 +38,7 @@ 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). +The parser is able to read parse ``.tl`` files (:term:`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. diff --git a/client/src/telethon/_impl/client/client/client.py b/client/src/telethon/_impl/client/client/client.py index dbd0f0b4..3db7ebb0 100644 --- a/client/src/telethon/_impl/client/client/client.py +++ b/client/src/telethon/_impl/client/client/client.py @@ -384,6 +384,8 @@ class Client: :param file: The output file path or :term:`file-like object`. + Note that the extension is not automatically added to the path. + You can get the file extension with :attr:`telethon.types.File.ext`. .. rubric:: Example @@ -1020,8 +1022,8 @@ class Client: async def send_message( self, chat: ChatLike, - *, text: Optional[str] = None, + *, markdown: Optional[str] = None, html: Optional[str] = None, link_preview: Optional[bool] = None, @@ -1046,7 +1048,7 @@ class Client: return await send_message( self, chat, - text=text, + text, markdown=markdown, html=html, link_preview=link_preview, diff --git a/client/src/telethon/_impl/client/client/files.py b/client/src/telethon/_impl/client/client/files.py index 7478e9ca..cd68245f 100644 --- a/client/src/telethon/_impl/client/client/files.py +++ b/client/src/telethon/_impl/client/client/files.py @@ -273,7 +273,7 @@ async def upload( async def iter_download(self: Client) -> None: - pass + raise NotImplementedError # result = self( # functions.upload.get_file( # precise=False, @@ -291,4 +291,4 @@ async def iter_download(self: Client) -> None: async def download(self: Client, media: MediaLike, file: OutFileLike) -> None: - pass + raise NotImplementedError diff --git a/client/src/telethon/_impl/client/client/messages.py b/client/src/telethon/_impl/client/client/messages.py index b0b82d4c..2ee7d71f 100644 --- a/client/src/telethon/_impl/client/client/messages.py +++ b/client/src/telethon/_impl/client/client/messages.py @@ -38,8 +38,8 @@ def parse_message( async def send_message( self: Client, chat: ChatLike, - *, text: Optional[str] = None, + *, markdown: Optional[str] = None, html: Optional[str] = None, link_preview: Optional[bool] = None, diff --git a/client/src/telethon/_impl/client/types/file.py b/client/src/telethon/_impl/client/types/file.py index e2f6173b..f28670d9 100644 --- a/client/src/telethon/_impl/client/types/file.py +++ b/client/src/telethon/_impl/client/types/file.py @@ -295,5 +295,9 @@ class File(metaclass=NoPublicConstructor): raw=None, ) + @property + def ext(self): + raise NotImplementedError + async def _read(self, n: int) -> bytes: raise NotImplementedError diff --git a/client/src/telethon/_impl/client/types/message.py b/client/src/telethon/_impl/client/types/message.py index ed334c51..b2ca1cc5 100644 --- a/client/src/telethon/_impl/client/types/message.py +++ b/client/src/telethon/_impl/client/types/message.py @@ -33,11 +33,11 @@ class Message(metaclass=NoPublicConstructor): return getattr(self._raw, "message", None) @property - def html_text(self) -> Optional[str]: + def text_html(self) -> Optional[str]: raise NotImplementedError @property - def markdown_text(self) -> Optional[str]: + def text_markdown(self) -> Optional[str]: raise NotImplementedError @property