mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-02-23 06:50:43 +03:00
Complete migration guide from other bot libraries
This commit is contained in:
parent
18895748c4
commit
4df1f4537b
|
@ -3,7 +3,7 @@ Telethon
|
|||
|
||||
.. epigraph::
|
||||
|
||||
⭐️ Thanks **everyone** who has starred the project, it means a lot!
|
||||
⭐️ Thanks **everyone** who has starred the project, it means a lot!
|
||||
|
||||
|logo| **Telethon** is an asyncio_ **Python 3**
|
||||
MTProto_ library to interact with Telegram_'s API
|
||||
|
@ -31,7 +31,7 @@ Installing
|
|||
|
||||
.. code-block:: sh
|
||||
|
||||
pip install telethon
|
||||
pip install telethon
|
||||
|
||||
|
||||
Creating a client
|
||||
|
@ -47,7 +47,7 @@ Creating a client
|
|||
api_hash = '0123456789abcdef0123456789abcdef'
|
||||
|
||||
async with TelegramClient('session_name', api_id, api_hash) as client:
|
||||
...
|
||||
await client.interactive_login()
|
||||
|
||||
|
||||
Doing stuff
|
||||
|
@ -58,7 +58,7 @@ Doing stuff
|
|||
print(await client.get_me())
|
||||
|
||||
await client.send_message('username', 'Hello! Talking to you from Telethon')
|
||||
await client.send_message('username', photo='/home/myself/Pictures/holidays.jpg')
|
||||
await client.send_photo('username', '/home/myself/Pictures/holidays.jpg')
|
||||
|
||||
async for message in client.get_messages('username', 1):
|
||||
path = await message.download_media()
|
||||
|
|
|
@ -97,6 +97,15 @@ 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.
|
||||
|
||||
To summarize:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import Client
|
||||
client = Client('name', 12345, '0123456789abcdef0123456789abcdef')
|
||||
await client.connect()
|
||||
await client.interactive_login()
|
||||
|
||||
|
||||
Manual login
|
||||
------------
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -306,6 +306,20 @@ This means getting those identifiers is up to you, and you can handle it in a wa
|
|||
Behaviour changes in events
|
||||
---------------------------
|
||||
|
||||
Events produced by the client itself will now also be received as updates.
|
||||
This means, for example, that your :class:`events.NewMessage` handlers will run when you use :meth:`Client.send_message`.
|
||||
This is needed to properly handle updates.
|
||||
|
||||
In v1, there was a backwards-compatibility hack that flagged results from the client as their "own".
|
||||
But in some rare cases, it was possible to still receive messages sent by the client itself in v1.
|
||||
The hack has been removed so now the library will consistently deliver all updates.
|
||||
|
||||
``events.StopPropagation`` no longer exists.
|
||||
In v1, all handlers were always called.
|
||||
Now handlers are called in order until the filter for one returns :data:`True`.
|
||||
The default behaviour is that handlers after that one are not called.
|
||||
This behaviour can be changed with the ``check_all_handlers`` flag in :class:`Client` constructor.
|
||||
|
||||
:class:`events.CallbackQuery` no longer also handles "inline bot callback queries".
|
||||
This was a hacky workaround.
|
||||
|
||||
|
|
|
@ -125,14 +125,20 @@ async def sign_in(
|
|||
return await complete_login(self, result)
|
||||
|
||||
|
||||
async def interactive_login(self: Client) -> User:
|
||||
async def interactive_login(
|
||||
self: Client,
|
||||
phone_or_token: Optional[str] = None,
|
||||
*,
|
||||
password: Optional[str] = None,
|
||||
) -> User:
|
||||
if me := await self.get_me():
|
||||
return me
|
||||
|
||||
phone_or_token = ""
|
||||
while not re.match(r"\+?[\s()]*\d", phone_or_token):
|
||||
print("Please enter your phone (+1 23...) or bot token (12:abcd...)")
|
||||
phone_or_token = input(": ").strip()
|
||||
if not phone_or_token:
|
||||
phone_or_token = ""
|
||||
while not re.match(r"\+?[\s()]*\d", phone_or_token):
|
||||
print("Please enter your phone (+1 23...) or bot token (12:abcd...)")
|
||||
phone_or_token = input(": ").strip()
|
||||
|
||||
# Bot flow
|
||||
if re.match(r"\d+:", phone_or_token):
|
||||
|
@ -160,16 +166,21 @@ async def interactive_login(self: Client) -> User:
|
|||
break
|
||||
|
||||
if isinstance(user_or_token, PasswordToken):
|
||||
while True:
|
||||
print("Please enter your password (prompt is hidden; type and press enter)")
|
||||
password = getpass.getpass(": ")
|
||||
try:
|
||||
user = await self.check_password(user_or_token, password)
|
||||
except RpcError as e:
|
||||
if e.name.startswith("PASSWORD"):
|
||||
print("Invalid password:", e)
|
||||
else:
|
||||
raise
|
||||
if password:
|
||||
user = await self.check_password(user_or_token, password)
|
||||
else:
|
||||
while True:
|
||||
print(
|
||||
"Please enter your password (prompt is hidden; type and press enter)"
|
||||
)
|
||||
password = getpass.getpass(": ")
|
||||
try:
|
||||
user = await self.check_password(user_or_token, password)
|
||||
except RpcError as e:
|
||||
if e.name.startswith("PASSWORD"):
|
||||
print("Invalid password:", e)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
user = user_or_token
|
||||
|
||||
|
|
|
@ -168,6 +168,14 @@ class Client:
|
|||
|
||||
:param update_queue_limit:
|
||||
Maximum amount of updates to keep in memory before dropping them.
|
||||
|
||||
:param check_all_handlers:
|
||||
Whether to always check all event handlers or stop early.
|
||||
|
||||
The library will call event handlers in the order they were added.
|
||||
By default, the library stops checking handlers as soon as a filter returns :data:`True`.
|
||||
|
||||
By setting ``check_all_handlers=True``, the library will keep calling handlers after the first match.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
@ -175,6 +183,7 @@ class Client:
|
|||
session: Optional[Union[str, Path, Storage]],
|
||||
api_id: int,
|
||||
api_hash: Optional[str] = None,
|
||||
check_all_handlers: bool = False,
|
||||
) -> None:
|
||||
self._sender: Optional[Sender] = None
|
||||
self._sender_lock = asyncio.Lock()
|
||||
|
@ -190,6 +199,7 @@ class Client:
|
|||
api_id=api_id,
|
||||
api_hash=api_hash or "",
|
||||
)
|
||||
|
||||
self._message_box = MessageBox()
|
||||
self._chat_hashes = ChatHashCache(None)
|
||||
self._last_update_limit_warn: Optional[float] = None
|
||||
|
@ -197,10 +207,10 @@ class Client:
|
|||
Tuple[abcs.Update, Dict[int, Chat]]
|
||||
] = asyncio.Queue(maxsize=self._config.update_queue_limit or 0)
|
||||
self._dispatcher: Optional[asyncio.Task[None]] = None
|
||||
self._downloader_map = object()
|
||||
self._handlers: Dict[
|
||||
Type[Event], List[Tuple[Callable[[Any], Awaitable[Any]], Optional[Filter]]]
|
||||
] = {}
|
||||
self._shortcircuit_handlers = not check_all_handlers
|
||||
|
||||
if self_user := self._config.session.user:
|
||||
self._dc_id = self_user.dc
|
||||
|
@ -795,11 +805,19 @@ class Client:
|
|||
"""
|
||||
return await inline_query(self, bot, query, chat=chat)
|
||||
|
||||
async def interactive_login(self) -> User:
|
||||
async def interactive_login(
|
||||
self, phone_or_token: Optional[str] = None, *, password: Optional[str] = None
|
||||
) -> User:
|
||||
"""
|
||||
Begin an interactive login if needed.
|
||||
If the account was already logged-in, this method simply returns :term:`yourself`.
|
||||
|
||||
:param phone_or_token:
|
||||
Bypass the phone number or bot token prompt, and use this value instead.
|
||||
|
||||
:param password:
|
||||
Bypass the 2FA password prompt, and use this value instead.
|
||||
|
||||
:return: The user corresponding to :term:`yourself`.
|
||||
|
||||
.. rubric:: Example
|
||||
|
@ -809,11 +827,14 @@ class Client:
|
|||
me = await client.interactive_login()
|
||||
print('Logged in as:', me.full_name)
|
||||
|
||||
# or, to make sure you're logged-in as a bot
|
||||
await client.interactive_login('1234:ab56cd78ef90)
|
||||
|
||||
.. seealso::
|
||||
|
||||
In-depth explanation for :doc:`/basic/signing-in`.
|
||||
"""
|
||||
return await interactive_login(self)
|
||||
return await interactive_login(self, phone_or_token, password=password)
|
||||
|
||||
async def is_authorized(self) -> bool:
|
||||
"""
|
||||
|
@ -1210,7 +1231,7 @@ class Client:
|
|||
async def send_message(
|
||||
self,
|
||||
chat: ChatLike,
|
||||
text: Optional[str] = None,
|
||||
text: Optional[Union[str, Message]] = None,
|
||||
*,
|
||||
markdown: Optional[str] = None,
|
||||
html: Optional[str] = None,
|
||||
|
@ -1225,6 +1246,8 @@ class Client:
|
|||
:param text:
|
||||
Message text, with no formatting.
|
||||
|
||||
When given a :class:`Message` instance, a copy of the message will be sent.
|
||||
|
||||
:param text_markdown:
|
||||
Message text, parsed as CommonMark.
|
||||
|
||||
|
|
|
@ -124,16 +124,21 @@ def extend_update_queue(
|
|||
|
||||
async def dispatcher(client: Client) -> None:
|
||||
while client.connected:
|
||||
update, chat_map = await client._updates.get()
|
||||
for event_cls, handlers in client._handlers.items():
|
||||
if event := event_cls._try_from_update(client, update, chat_map):
|
||||
for handler, filter in handlers:
|
||||
if not filter or filter(event):
|
||||
try:
|
||||
await handler(event)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception:
|
||||
# TODO proper logger
|
||||
name = getattr(handler, "__name__", repr(handler))
|
||||
logging.exception("Unhandled exception on %s", name)
|
||||
try:
|
||||
await dispatch_next(client)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception:
|
||||
# TODO proper logger
|
||||
logging.exception("Unhandled exception in event handler")
|
||||
|
||||
|
||||
async def dispatch_next(client: Client) -> None:
|
||||
update, chat_map = await client._updates.get()
|
||||
for event_cls, handlers in client._handlers.items():
|
||||
if event := event_cls._try_from_update(client, update, chat_map):
|
||||
for handler, filter in handlers:
|
||||
if not filter or filter(event):
|
||||
await handler(event)
|
||||
if client._shortcircuit_handlers:
|
||||
return
|
||||
|
|
|
@ -15,6 +15,13 @@ class Event(metaclass=NoPublicConstructor):
|
|||
The base type of all events.
|
||||
"""
|
||||
|
||||
@property
|
||||
def client(self) -> Client:
|
||||
"""
|
||||
The :class:`~telethon.Client` that received this update.
|
||||
"""
|
||||
return self._client
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def _try_from_update(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from .combinators import All, Any, Not
|
||||
from .common import Chats, Filter, Senders
|
||||
from .messages import Command, Forward, Incoming, Outgoing, Reply, Text
|
||||
from .messages import Command, Forward, Incoming, Media, Outgoing, Reply, Text, TextOnly
|
||||
|
||||
__all__ = [
|
||||
"All",
|
||||
|
@ -12,7 +12,9 @@ __all__ = [
|
|||
"Command",
|
||||
"Forward",
|
||||
"Incoming",
|
||||
"Media",
|
||||
"Outgoing",
|
||||
"Reply",
|
||||
"Text",
|
||||
"TextOnly",
|
||||
]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from typing import TYPE_CHECKING, Literal, Optional, Union
|
||||
|
||||
from ..event import Event
|
||||
|
||||
|
@ -18,6 +18,9 @@ class Text:
|
|||
|
||||
The match, if any, is discarded. If you need to access captured groups,
|
||||
you need to manually perform the check inside the handler instead.
|
||||
|
||||
Note that the caption text in messages with media is also searched.
|
||||
If you want to filter based on media, use :class:`TextOnly` or :class:`Media`.
|
||||
"""
|
||||
|
||||
__slots__ = ("_pattern",)
|
||||
|
@ -131,3 +134,50 @@ class Reply:
|
|||
|
||||
def __call__(self, event: Event) -> bool:
|
||||
return getattr(event, "reply", None) is not None
|
||||
|
||||
|
||||
class TextOnly:
|
||||
"""
|
||||
Filter by messages with some text and no media.
|
||||
|
||||
Note that link previews are only considered media if they have a photo or document.
|
||||
"""
|
||||
|
||||
|
||||
MediaTypes = Union[Literal["photo"], Literal["audio"], Literal["video"]]
|
||||
|
||||
|
||||
class Media:
|
||||
"""
|
||||
Filter by the media type in the message.
|
||||
|
||||
By default, this filter will pass if the message has any media.
|
||||
|
||||
Note that link previews are only considered media if they have a photo or document.
|
||||
|
||||
When you specify one or more media types, *only* those types will be considered.
|
||||
|
||||
You can use literal strings or the constants defined by the filter.
|
||||
"""
|
||||
|
||||
PHOTO = "photo"
|
||||
AUDIO = "audio"
|
||||
VIDEO = "video"
|
||||
|
||||
__slots__ = "_types"
|
||||
|
||||
def __init__(self, types: Optional[MediaTypes] = None) -> None:
|
||||
self._types = types
|
||||
|
||||
@property
|
||||
def types(self) -> MediaTypes:
|
||||
"""
|
||||
The media types being checked.
|
||||
"""
|
||||
return self._types
|
||||
|
||||
def __call__(self, event: Event) -> bool:
|
||||
if self._types is None:
|
||||
return getattr(event, "file", None) is not None
|
||||
else:
|
||||
return any(getattr(event, ty, None) is not None for ty in self._types)
|
||||
|
|
|
@ -21,7 +21,7 @@ def del_surrogate(text: str) -> str:
|
|||
|
||||
def within_surrogate(text: str, index: int, *, length: Optional[int] = None) -> bool:
|
||||
"""
|
||||
`True` if ``index`` is within a surrogate (before and after it, not at!).
|
||||
:data:`True` if ``index`` is within a surrogate (before and after it, not at!).
|
||||
"""
|
||||
if length is None:
|
||||
length = len(text)
|
||||
|
|
|
@ -15,11 +15,13 @@ from .._impl.client.events.filters import (
|
|||
Filter,
|
||||
Forward,
|
||||
Incoming,
|
||||
Media,
|
||||
Not,
|
||||
Outgoing,
|
||||
Reply,
|
||||
Senders,
|
||||
Text,
|
||||
TextOnly,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
@ -30,9 +32,11 @@ __all__ = [
|
|||
"Filter",
|
||||
"Forward",
|
||||
"Incoming",
|
||||
"Media",
|
||||
"Not",
|
||||
"Outgoing",
|
||||
"Reply",
|
||||
"Senders",
|
||||
"Text",
|
||||
"TextOnly",
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue
Block a user