Complete migration guide from other bot libraries

This commit is contained in:
Lonami Exo
2023-10-01 13:37:28 +02:00
parent 18895748c4
commit 4df1f4537b
15 changed files with 521 additions and 49 deletions

View File

@@ -17,13 +17,13 @@ Quoting their main page:
.. epigraph::
The Bot API is an HTTP-based interface created for developers keen on building bots for Telegram.
The :term:`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 <https://core.telegram.org/bots>`_
and `Bot FAQ <https://core.telegram.org/bots/faq>`_.
Bot API is simply an HTTP endpoint offering a custom HTTP API.
:term:`Bot API` is simply an HTTP endpoint offering a custom HTTP API.
Underneath, it uses `tdlib <https://core.telegram.org/tdlib>`_ to talk to Telegram's servers.
You can configure your bot details via `@BotFather <https://t.me/BotFather>`_.
@@ -51,6 +51,19 @@ 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`.
Why is an API ID and hash needed for bots with MTProto?
-------------------------------------------------------
When talking to Telegram's API directly, you need an API ID and hash to sign in to their servers.
API access is forbidden without an API ID, and the sign in can only be done with the API hash.
When using the :term:`Bot API`, that layer talks to the MTProto API underneath.
To do so, it uses its own private API ID and hash.
When you cut on the intermediary, you need to provide your own.
In a similar manner, the authorization key which remembers that you logged-in must be kept locally.
Advantages of MTProto over Bot API
----------------------------------
@@ -109,9 +122,306 @@ and you were making API requests manually, or if you used a wrapper library like
or `pyTelegramBotAPI <https://pytba.readthedocs.io/en/latest/index.html>`.
You will surely be pleased with Telethon!
If you were using an asynchronous library like `aiohttp <https://docs.aiohttp.org/en/stable>`_
or a wrapper like `aiogram <https://docs.aiohttp.org/en/stable>`_, the switch will be even easier.
If you were using an asynchronous library like `aiohttp <https://docs.aiohttp.org/en/stable/>`_
or a wrapper like `aiogram <https://docs.aiogram.dev/en/latest/>`_, the switch will be even easier.
Migrating from TODO
^^^^^^^^^^^^^^^^^^^
Migrating from PTB v13.x
^^^^^^^^^^^^^^^^^^^^^^^^
Using one of the examples from their v13 wiki with the ``.ext`` module:
.. code-block:: python
from telegram import Update
from telegram.ext import Updater, CallbackContext, CommandHandler
updater = Updater(token='TOKEN', use_context=True)
dispatcher = updater.dispatcher
def start(update: Update, context: CallbackContext):
context.bot.send_message(chat_id=update.effective_chat.id, text="I'm a bot, please talk to me!")
start_handler = CommandHandler('start', start)
dispatcher.add_handler(start_handler)
updater.start_polling()
The code creates an ``Updater`` instance.
This will take care of polling updates for the bot associated with the given token.
Then, a ``CommandHandler`` using our ``start`` function is added to the dispatcher.
At the end, we block, telling the updater to do its job.
In Telethon:
.. code-block:: python
import asyncio
from telethon import Client
from telethon.events import NewMessage, filters
updater = Client('bot', api_id, api_hash)
async def start(update: NewMessage):
await update.client.send_message(chat=update.chat.id, text="I'm a bot, please talk to me!")
start_filter = filters.Command('/start')
updater.add_event_handler(start, NewMessage, start_filter)
async def main():
async with updater:
await updater.interactive_login('TOKEN')
await updater.run_until_disconnected()
asyncio.run(main())
Key differences:
* Telethon only has a :class:`~telethon.Client`, not separate ``Bot`` or ``Updater`` classes.
* There is no separate dispatcher. The :class:`~telethon.Client` is capable of dispatching updates.
* Telethon handlers only have one parameter, the event.
* There is no context, but the :attr:`~telethon.events.Event.client` property exists in all events.
* Handler types are :mod:`~telethon.events.filters` and don't have a ``Handler`` suffix.
* Telethon must define the update type (:class:`~telethon.events.NewMessage`) and filter.
* The setup to run the client (and dispatch updates) is a bit more involved with :mod:`asyncio`.
Here's the above code in idiomatic Telethon:
.. code-block:: python
import asyncio
from telethon import Client, events
from telethon.events import filters
client = Client('bot', api_id, api_hash)
@client.on(events.NewMessage, filters.Command('/start'))
async def start(event):
await event.respond("I'm a bot, please talk to me!")
async def main():
async with client:
await client.interactive_login('TOKEN')
await client.run_until_disconnected()
asyncio.run(main())
Events can be added using decorators and methods such as :meth:`types.Message.respond` help reduce the verbosity.
Migrating from PTB v20.x
^^^^^^^^^^^^^^^^^^^^^^^^
Using one of the examples from their v13 wiki with the ``.ext`` module:
.. code-block:: python
from telegram import Update
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await context.bot.send_message(chat_id=update.effective_chat.id, text="I'm a bot, please talk to me!")
if __name__ == '__main__':
application = ApplicationBuilder().token('TOKEN').build()
start_handler = CommandHandler('start', start)
application.add_handler(start_handler)
application.run_polling()
No need to import the :mod:`asyncio` module directly!
Now instead there are builders to help set stuff up.
In Telethon:
.. code-block:: python
import asyncio
from telethon import Client
from telethon.events import NewMessage, filters
async def start(update: NewMessage):
await update.client.send_message(chat=update.chat.id, text="I'm a bot, please talk to me!")
async def main():
application = Client('bot', api_id, api_hash)
start_filter = filters.Command('/start')
application.add_event_handler(start, NewMessage, start_filter)
async with application:
await application.interactive_login('TOKEN')
await application.run_until_disconnected()
asyncio.run(main())
Key differences:
* No builders. Telethon tries to get out of your way on how you structure your code.
* The client must be connected before it can run, hence the ``async with``.
Here's the above code in idiomatic Telethon:
.. code-block:: python
import asyncio
from telethon import Client, events
from telethon.events import filters
@client.on(events.NewMessage, filters.Command('/start'))
async def start(event):
await event.respond("I'm a bot, please talk to me!")
async def main():
async with Client('bot', api_id, api_hash) as client:
await client.interactive_login('TOKEN')
client.add_event_handler(start, NewMessage, filters.Command('/start'))
await client.run_until_disconnected()
asyncio.run(main())
Note how the client can be created and started in the same line.
This makes it easy to have clean disconnections once the script exits.
Migrating from asynchronous TeleBot
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Using one of the examples from their v4 pyTelegramBotAPI documentation:
.. code-block:: python
from telebot.async_telebot import AsyncTeleBot
bot = AsyncTeleBot('TOKEN')
# Handle '/start' and '/help'
@bot.message_handler(commands=['help', 'start'])
async def send_welcome(message):
await bot.reply_to(message, """\
Hi there, I am EchoBot.
I am here to echo your kind words back to you. Just say anything nice and I'll say the exact same thing to you!\
""")
# Handle all other messages with content_type 'text' (content_types defaults to ['text'])
@bot.message_handler(func=lambda message: True)
async def echo_message(message):
await bot.reply_to(message, message.text)
import asyncio
asyncio.run(bot.polling())
This showcases a command handler and a catch-all echo handler, both added with decorators.
In Telethon:
.. code-block:: python
from telethon import Client, events
from telethon.events.filters import Any, Command, TextOnly
bot = Client('bot', api_id, api_hash)
# Handle '/start' and '/help'
@bot.on(events.NewMessage, Any(Command('/help'), Command('/start')))
async def send_welcome(message: NewMessage):
await message.reply("""\
Hi there, I am EchoBot.
I am here to echo your kind words back to you. Just say anything nice and I'll say the exact same thing to you!\
""")
# Handle all other messages with only 'text'
@bot.on(events.NewMessage, TextOnly())
async def echo_message(message: NewMessage):
await message.reply(message.text)
import asyncio
async def main():
async with bot:
await bot.interactive_login('TOKEN')
await bot.run_until_disconnected()
asyncio.run(main())
Key differences:
* The handler type is defined using the event type instead of being a specific method in the client.
* Filters are also separate instances instead of being tied to specific event types.
* The ``reply_to`` helper is in the message, not the client instance.
* Setup is a bit more involved because the connection is not implicit.
For the most part, it's a 1-to-1 translation and the result is idiomatic Telethon.
Migrating from aiogram
``````````````````````
Using one of the examples from their v3 documentation with logging and comments removed:
.. code-block:: python
import asyncio
from aiogram import Bot, Dispatcher, types
from aiogram.enums import ParseMode
from aiogram.filters import CommandStart
from aiogram.types import Message
from aiogram.utils.markdown import hbold
dp = Dispatcher()
@dp.message(CommandStart())
async def command_start_handler(message: Message) -> None:
await message.answer(f"Hello, {hbold(message.from_user.full_name)}!")
@dp.message()
async def echo_handler(message: types.Message) -> None:
try:
await message.send_copy(chat_id=message.chat.id)
except TypeError:
await message.answer("Nice try!")
async def main() -> None:
bot = Bot(TOKEN, parse_mode=ParseMode.HTML)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
We can see a specific handler for the ``/start`` command and a catch-all echo handler:
In Telethon:
.. code-block:: python
import asyncio, html
from telethon import Client, RpcError, types, events
from telethon.events.filters import Command
from telethon.types import Message
client = Client("bot", api_id, api_hash)
@client.on(events.NewMessage, Command("/start"))
async def command_start_handler(message: Message) -> None:
await message.respond(html=f"Hello, <b>{html.escape(message.sender.full_name)}</b>!")
@dp.message()
async def echo_handler(message: types.Message) -> None:
try:
await message.respond(message)
except RpcError:
await message.respond("Nice try!")
async def main() -> None:
async with bot:
await bot.interactive_login(TOKEN)
await bot.run_until_disconnected()
if __name__ == "__main__":
asyncio.run(main())
Key differences:
* There is no separate dispatcher. Handlers are added to the client.
* There is no specific handler for the ``/start`` command.
* The ``answer`` method is for callback queries. Messages have :meth:`~types.Message.respond`.
* Telethon doesn't have functions to format messages. Instead, markdown or HTML are used.
* Telethon cannot have a default parse mode. Instead, it should be specified when responding.
* Telethon doesn't have ``send_copy``. Instead, :meth:`Client.send_message` accepts :class:`~types.Message`.
* If sending a message fails, the error will be :class:`RpcError`, because it comes from Telegram.

View File

@@ -43,10 +43,10 @@ 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` can have either the ``broadcast`` or the ``megagroup`` flag set to :data:`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:`channel` with the ``broadcast`` flag set to :data:`True` is known as a broadcast channel.
A bare :tl:`channel` with the ``megagroup`` flag set to :data:`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.

View File

@@ -44,6 +44,7 @@ Glossary
.. seealso:: The :doc:`../concepts/botapi-vs-mtproto` concept.
Bot API
HTTP Bot API
Telegram's simplified HTTP API to control bot accounts only.

View File

@@ -45,7 +45,7 @@ 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.
If the filter returns :data:`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:
@@ -74,3 +74,39 @@ You can use :func:`isinstance` if your filter can only deal with certain types o
If you need to perform asynchronous operations, you can't use a filter.
Instead, manually check for those conditions inside your handler.
Setting priority on handlers
----------------------------
There is no explicit way to define a different priority for different handlers.
Instead, the library will call all your handlers in the order you added them.
This means that, if you want a "catch-all" handler, it should be registered last.
By default, the library will stop calling the rest of handlers after one is called:
.. code-block:: python
@client.on(events.NewMessage)
async def first(event):
print('This is always called on new messages!')
@client.on(events.NewMessage)
async def second(event):
print('This will never be called, because "first" already ran.')
This is often the desired behaviour if you're using filters.
If you have more complicated filters executed *inside* the handler,
Telethon believes your handler completed and will stop calling the rest.
If that's the case, you can instruct Telethon to check all your handlers:
.. code-block:: python
client = Client(..., check_all_handlers=True)
# ^^^^^^^^^^^^^^^^^^^^^^^
# Now the code above will call both handlers
If you need a more complicated setup, consider sorting all your handlers beforehand.
Then, use :meth:`Client.add_event_handler` on all of them to ensure the correct order.