Message handler and text tricks

Greetings! Thank you for your subscriptions, likes and other positive responses to my activities. Let's continue.

We have already discussed:

This means that all that remains from the database is to deal with messages and work with media. After this, you can move on to more complex and serious topics, such as: payments in the bot, middleware, fsm states, admin panels, etc. But that’s all later, and today we will look at the following topics:

  • Message objects

  • Possibilities for sending text messages (reply, forward, answer)

  • Possibility of working with text messages (copying, deleting, replacing text and keyboards)

  • Formatting text messages (HTML)

  • Text Message Tricks (I'm sure you didn't know a lot of what I'm going to show you today)

Despite its apparent simplicity, the topic is quite important and serious. Even if you already have experience with aiogram 2 or aiogram 3, I strongly recommend that you read this text.

I arrived at much of what will be stated below through a bunch of mistakes and bumps.

Types of messages (content)

Telegram bots provide the following types of messages:

  • Stickers (gifs)

  • Video messages

  • Voice messages

  • Photo message

  • Messages with documents

  • Messages with media groups (the most unpleasant thing for a developer, if you have already worked with aiogram in the media group format, you know what I mean).

Each of these message types is processed using a Message handler:

  • When working through decorators, it is written as @dp.message or @router.message (previously the entry was @dp.message_handler).

  • When registering, it is written as dp.message.register.

Today we will consider only text messages, and next time we will close the topic with media content. Much of what we will discuss today will have the same logic when working with media (with a few exceptions).

Since we are working with text messages, our content type will be TEXT. If you have already read my previous articles on the topic aiogram 3, then you know that in order to catch such messages, we need to use the F.text magic filter (analogous to content_type=ContentType.TEXT).

Once our magic filter has computed a text message, it has quite a lot of options it can do with the message object (a lot more than it seems).

Here is a complete list of possibilities for working with messages in Telegram bots on aiogram 3.x:

  • Get data from a message (user data, message ID, chat ID, where it was sent, etc.)

  • Reply to a message using the bot object and using the message object itself (we’ll look at it in detail later)

  • Copy or forward message

  • Delete message

  • Simulate a bot typing a text message

  • Format a message or get message formatting from the user

  • A message can be pinned (the principle is the same as pinning in a personal chat)

  • Change/remove keyboard from message

And please note that we've been talking about text messages all this time. Now let's get to practice.

What data from the message is most often used in practice?

Using my practice as an example, I will say that I most often work with the following data from the message object (in the context of text messages):

  • Message.message_id (message id)

  • Message.date (date and time the message was sent – useful for logging)

  • Message.text (message text)

  • Message.html_text (we take the text with htm tags)

  • Message.from_user (here you can take data such as: username, first_name, last_name, full_name, is_premium, etc.)

  • Message.chat (id, type, title, username/channel)

Let's practice to make it more clear what's what.

Let's attach the topic with the values ​​from the message object. Let's imagine that we have the task of writing a handler that will respond to a text message containing the word “hunter”. If the bot sees that someone has written such a message, it will perform 2 actions:

  1. Reply to a message with some text (quote, regular reply, reply via message forwarding)

  2. The bot will generate a dictionary with the following data:

    • Telegram user ID

    • Full name

    • Login

    • ID messages

    • Time to send the message

Next, we’ll simply output this dictionary to the console.

I understand that the example may be a bit stupid, but when you look at this code, you will immediately understand what I was telling you about here.

@start_router.message(F.text.lower().contains('охотник'))
async def cmd_start(message: Message, state: FSMContext):
    # отправка обычного сообщения
    await message.answer('Я думаю, что ты тут про радугу рассказываешь')

    # то же действие, но через объект бота
    await bot.send_message(chat_id=message.from_user.id, text="Для меня это слишком просто")

    # ответ через цитату
    msg = await message.reply('Ну вот что за глупости!?')

    # ответ через цитату, через объект bot
    await bot.send_message(chat_id=message.from_user.id, text="Хотя, это забавно...",
                           reply_to_message_id=msg.message_id)

    await bot.forward_message(chat_id=message.from_user.id, from_chat_id=message.from_user.id, message_id=msg.message_id)

    data_task = {'user_id': message.from_user.id, 'full_name': message.from_user.full_name,
                 'username': message.from_user.username, 'message_id': message.message_id, 'date': get_msc_date(message.date)}
    print(data_task)

Let's look at the print result:

{'user_id': 0000000, 'full_name': 'Alexey Yakovenko', 'username': 'yakvenalexx', 'message_id': 337, 'date': datetime.datetime(2024, 6, 13, 19, 53, 1, tzinfo=TzInfo(UTC))}

All data has been received. Please note that the date the message was sent is specified in datetime format in the UTC (Coordinated Universal Time) time zone.

If you need to convert to Moscow time, you can use this approach:

import pytz

def get_msc_date(utc_time):
    # Задаем московский часовой пояс
    moscow_tz = pytz.timezone('Europe/Moscow')
    # Переводим время в московский часовой пояс
    moscow_time = utc_time.astimezone(moscow_tz)
    return moscow_time

'date': get_msc_date(message.date)

In this case, this value will be transmitted in this format:

datetime.datetime(2024, 6, 13, 22, 57, 10, tzinfo=<DstTzInfo 'Europe/Moscow' MSK+3:00:00 STD>)

Let's look at the code

In this code, we used several methods, but each of them performed one task: sending messages.

As you saw, some of the methods used the bot object as a basis, and the other used message.

# отправка обычного сообщения
await message.answer('Я думаю, что ты тут про радугу рассказываешь')
    
# ответ через цитату
msg = await message.reply('Ну вот что за глупости!?')

These two methods are quite convenient and concise. Their main feature is that they do not require mandatory indication of the ID to whom the message needs to be sent and to what message – this is all already hidden in the answer and reply method itself.

I advise you to use these methods whenever possible.

Methods like bot.send_message, bot.send_message with the reply_to_message_id flag, and bot.forward_message deserve more attention, as does this entry:

msg = await message.reply('Ну вот что за глупости!?')

Let's go in order.

When sending a message using a bot, a mandatory parameter will always be an indication of who needs to send the message. In the context of my example:

await message.answer('Я думаю, что ты тут про радугу рассказываешь')

And

await bot.send_message(chat_id=message.from_user.id, text="Для меня это слишком просто")

We performed similar actions with the same result, and therefore there was no point in putting this into a separate object, but there are situations when sending via message.answer is impossible.

Let's consider this situation using the example of sending messages to telegram bots from the admin panel.

  1. The administrator writes a message

  2. Selects send to everyone

  3. The bot takes message IDs from the database

  4. Using bot.send_message it will send a message.

Convenient, right?

Cases with method bot.forward_message And bot.send_messagewhen we explicitly indicate which message needs to be answered, it’s a little more complicated. The main difficulty here appears in the mandatory indication message_id (ID of the message to be replied/forwarded).

But there is one very big plus. When you know the message ID, you have almost unlimited possibilities for working with it (when the bot is the owner of this message – otherwise you need to resort to tricks, which I will discuss later). For example, you can constantly re-record the same message, simulating typing and creating a continuous animation (I'm sure you've seen this before).

I think it will be easier to explain this with a specific example. I'll show you how to make a bot simulate typing.

We import:

from aiogram.utils.chat_action import ChatActionSender

The design for simulating typing will look like this:

async with ChatActionSender(bot=bot, chat_id=message.from_user.id, action="typing"):
    await asyncio.sleep(2)

The pause is needed to ensure that the text message is not sent instantly. There is one interesting feature here. The design that I described above is only responsible for simulating typing. That is, nothing prevents you from alternating imitation with asynchronous pauses (await asyncio.sleep), creating a full-fledged imitation of communication with a real person (you know, when they are typing at you, get distracted, and then continue typing again).

Now I'll show you how you can change the text of your message over time. It is important to understand here that the bot can only change those messages that it itself sent. But here there is one interesting trick that I will share with you.

Let's create a handler that will respond to the command /test_edit_msg.

@start_router.message(Command('test_edit_msg'))
async def cmd_start(message: Message, state: FSMContext):
    # Бот делает отправку сообщения с сохранением объекта msg
    msg = await message.answer('Отправляю сообщение')

    # Достаем ID сообщения
    msg_id = msg.message_id

    # Имитируем набор текста 2 секунды и отправляеВ коде оставлены комментарии. Единственное, на что нужно обратить внимание, — строка:

м какое-то сообщение
    async with ChatActionSender(bot=bot, chat_id=message.from_user.id, action="typing"):
        await asyncio.sleep(2)
        await message.answer('Новое сообщение')

    # Делаем паузу ещё на 2 секунды
    await asyncio.sleep(2)

    # Изменяем текст сообщения, ID которого мы сохранили
    await bot.edit_message_text(text="<b>Отправляю сообщение!!!</b>", chat_id=message.from_user.id, message_id=msg_id)

Comments are left in the code. The only thing you need to pay attention to is the line:

await bot.edit_message_text(text="<b>Отправляю сообщение!!!</b>", chat_id=message.from_user.id, message_id=msg_id)

Firstly, we used a new method – edit_message_text. It accepts the new text, the chat in which the message needs to be changed, and, most importantly, the message ID. After changing the text of a message, its ID does not change. This means that as long as the message exists, it can be replaced as many times as desired. However, be careful with this method: if the user deletes the message, the bot will simply crash when trying to edit it.

Without error handling we catch:

aiogram.exceptions.TelegramBadRequest: Telegram server says - Bad Request: message to edit not found

So be careful!

We will get a similar error if we try to change someone else's message. Let's say we change the message with the command /test_edit_msg:

@start_router.message(Command('test_edit_msg'))
async def cmd_start(message: Message, state: FSMContext):
    # Бот пытается изменить сообщение, которое не отправлял
    await bot.edit_message_text(text="<b>Отправляю сообщение!!!</b>", chat_id=message.from_user.id, message_id=message.message_id)

We get:

aiogram.exceptions.TelegramBadRequest: Telegram server says - Bad Request: message can't be edited

Here the bot will say that the message cannot be changed, and this is true, because the bot is not the author of this message. Now look at the trick you can do here:

  • The bot will assign this message to itself (copy it).

  • Will delete a message from the user.

  • It will overwrite an already copied message and send it.

Let's look:

@start_router.message(Command('test_edit_msg'))
async def cmd_start(message: Message, state: FSMContext):
    new_msg = await bot.copy_message(
        chat_id=message.from_user.id,
        from_chat_id=message.from_user.id, 
        message_id=message.message_id
    )
    await message.delete()
    await bot.edit_message_text(
        text="<b>Отправляю сообщение!!!</b>",
        chat_id=message.from_user.id,
        message_id=new_msg.message_id
    )

Please note that we have used new methods here – new_msg = await bot.copy_message And await message.delete(). The method await message.delete() there is an analogue from the method bot. It is written like this:

await bot.delete_message(chat_id=message.from_user.id, message_id=message.message_id)

As you can imagine, it performs the same action, but with a more cumbersome recording. Sometimes it is very useful.

A little about keyboards

In previous articles, I told you everything about text and inline keyboards, and you already know that messages, in particular text messages, can be accompanied by one or another type of keyboard.

Now I will show you how to remove the keyboard and how to replace the keyboard in the message.

First, let's import a method for removing keyboards (it will work on both text and inline keyboards):

from aiogram.types import ReplyKeyboardRemove

This method should be used when the scenario requires the user to move from one state to another. For example, a text keyboard with gender selection “Male” and “Female”. He makes a choice, and after that a new question awaits him, for example, “Indicate the year of birth.”

If we don’t remove the keyboard (we don’t replace it with another one as part of the script), the keyboard will continue to hang. Beginners have this problem when the user completed the script, and at one of the stages there was a choice of a city. After this, the keyboard with the choice of city follows him.

Don't let this happen. Everything is simple here: instead of specifying the keyboard in the method reply_markup pass it on ReplyKeyboardRemove(). We will definitely look at this in the FSM topic. Now I would like to talk about something else.

Sometimes the keyboard needs to be replaced as part of one specific message, such as over time. You can change the keyboard in a message (delete) in several ways.

Method 1:

await bot.edit_message_text(chat_id=message.chat.id, message_id=msg.message_id, text="Пока!", reply_markup=inline_kb())

Here we again use the familiar one edit_message_text. We simply rewrite the entire message and bind the keyboard directly to it. Please note: this method expects an inline keyboard. By passing a text keyboard to this method, you will get:

input_value=ReplyKeyboardMarkup(keybo...ню:', selective=None), input_type=ReplyKeyboardMarkup] For further information visit https://errors.pydantic.dev/2.7/v/model_type

But replacing inline keyboards within one message_id possible ad infinitum.

Method 2:

await bot.edit_message_reply_markup(chat_id=message.chat.id, message_id=msg.message_id, reply_markup=inline_kb())

Here we replaced only the keyboard, leaving the text unchanged.

How can we add a text keyboard to a message?

Unfortunately, there is no direct replacement method, but nothing prevents us from using the crutch of copying a message, right?

@start_router.message(Command('test_edit_msg'))
async def cmd_start(message: Message, state: FSMContext):
    msg = await message.answer('Привет!')
    await asyncio.sleep(2)
    old_text = msg.text
    await msg.delete()
    await message.answer(old_text, reply_markup=main_kb(message.from_user.id))

This is our simple design. In order not to be so sophisticated, I always try to give preference to inline keyboards and I advise you to do so. Well, learn to weave text buttons into scripts so that they appear and disappear in a logical way (overwritten by other text keyboards or deleted using the ReplyKeyboardRemove()).

Text formatting

To be honest, I never use Markdown formatting when working with aiogram 3 and now I’ll explain why.

Firstly, as for me, the syntax there is inconvenient. Secondly, at the start of aiogram 3 there were a lot of bugs around Markdown formatting, and those who transferred their projects to three suffered greatly because they chose Markdown.

Next, I will talk about formatting using HTML as an example, but you can use the type that is more pleasant to you.

In order not to transmit to each of your messages parse_mode="HTML" When initiating a bot, I advise you to use the following construction:

from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode

bot = Bot(token=config('TOKEN'), default=DefaultBotProperties(parse_mode=ParseMode.HTML))

In this case, the design DefaultBotProperties(parse_mode=ParseMode.HTML) will set default HTML formatting to each message.

Let's look at the main HTML tags that can be used in formatting:

<b>Жирный</b>
<i>Курсив</i>
<u>Подчеркнутый</u>
<s>Зачеркнутый</s>
<tg-spoiler>Спойлер (скрытый текст)</tg-spoiler>
<a href="http://www.example.com/">Ссылка в тексте</a>
<code>Код с копированием текста при клике</code>
<pre>Спойлер с копированием текста</pre>
This is how formatted text looks in the bot.

This is how formatted text looks in the bot.

Use them carefully, as there is one very unpleasant thing. Some users decide to use < And > in your logins or in messages sent to the bot.

If HTML text formatting mode is enabled in the code, the bot catches an error:

aiogram.exceptions.TelegramBadRequest: Telegram server says - Bad Request: can't parse entities: Unsupported start tag "<>"

We use html.escape to solve this problem. Let's import:

from html import escape

And then we simply run those places where an incorrect tag may occur through the method escape.

Well, the last thing I promised you is capturing formatted text. Now I’ll give an example so that you immediately understand what I mean (real practice).

In the admin panel I added the ability to create posts with formatted text (with HTML tags). The client formatted the text before sending (simply using standard Telegram methods: highlighted with the mouse, selected “Bold”, and so on). After these steps, the formatted text was sent to the bot, which captured the HTML using message.html_text, and then made an entry into the database with tags. Next, when displaying the post, the bot simply took the text with HTML tags and sent the already formatted text. This is a kind of pocket HTML editor for Telegram.

Here's a simple example:

@start_router.message(Command('test_edit_msg'))
async def cmd_start(message: Message, state: FSMContext):
    msg = await message.answer('<b>ПРИВЕТ!</b>')
    print(msg.html_text)

The bot displayed in the console:

<b>ПРИВЕТ!</b>

Although we did not save a variable with an HTML tag anywhere.

Conclusion

Today we have made the maximum analysis of the topic of working with text messages through aiogram 3. Now you can do the breeze.

A lot of time, effort and energy are spent on writing such articles. Therefore, if you want similar material to be published more often, do not forget to give feedback through subscriptions, likes and comments. The situation is this: according to some publications, my bookmarks exceed 200, and my karma is at the level of a foul.

Thank you for your attention. See you later.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *