Основы программирования Telegram-бота (aiogram)

0. Подготовка к работе

Для работы с Telegram-ботами используется библиотека aiogram. Установка выполняется с помощью менеджера пакетов:

pip install aiogram

Для разработки рекомендуется использовать виртуальное окружение Python, как было показано в предыдущих уроках. Это позволяет изолировать зависимости проекта и избежать конфликтов между библиотеками.

Перед началом работы необходимо создать Telegram-бота и получить его токен. Создание бота и выдача токена выполняются через официальный бот BotFather(@BotFather) в Telegram.

1. Общая идея работы Telegram-бота

Telegram-бот — это программа, которая:

Бот не «работает сам по себе», он реагирует на события. Основным событием является сообщение пользователя.

2. Основные объекты программы

Минимальная программа Telegram-бота на aiogram строится вокруг трёх элементов:

2.1 Объект Bot

Объект Bot отвечает за связь с серверами Telegram.

bot = Bot(token=TOKEN)

Через него отправляются сообщения, изображения и другие данные.
В учебных примерах он обычно используется не напрямую, а через методы объекта Message.

2.2 Объект Dispatcher

Dispatcher — это центральный объект программы.

dp = Dispatcher()

Его задача:

Dispatcher содержит список handlers, зарегистрированных в программе.

3. Handler (обработчик сообщений)

3.1 Что такое handler

Handler — это асинхронная функция, которая вызывается, когда приходит подходящее сообщение.

@dp.message()
async def handler(message: Message):
    await message.answer("Привет!")

Здесь:

3.2 Как выбирается handler

Когда пользователь отправляет сообщение:

Один handler обрабатывает одно сообщение.

4. Объект Message

4.1 Что такое Message

Message — это объект, который представляет одно сообщение пользователя.

Через него можно:

4.2 Основные поля Message (для работы с текстом)

На начальном этапе используются следующие поля:

message.text        # текст сообщения
message.chat.id     # идентификатор чата
message.from_user   # информация о пользователе
message.message_id  # идентификатор сообщения

Наиболее часто используется message.text.

4.3 Основные методы Message

Методы Message упрощают отправку ответов.

await message.answer("Текст ответа")

Основные методы:

await message.answer(text)   # отправить сообщение в чат
await message.reply(text)    # ответить на конкретное сообщение
await message.delete()       # удалить сообщение

Метод answer() автоматически отправляет сообщение в тот же чат, откуда пришёл запрос.

5. Работа с текстом в handler

Пример простейшей обработки текста:

@dp.message()
async def echo(message: Message):
    await message.answer(message.text)

Здесь:

6. Минимальная структура программы

В упрощённом виде программа Telegram-бота состоит из:

1. Инициализация Bot
2. Инициализация Dispatcher
3. Регистрация handlers
4. Запуск обработки сообщений

Пример:

bot = Bot(token=TOKEN)
dp = Dispatcher()

@dp.message()
async def handler(message: Message):
    await message.answer("Привет")

dp.start_polling(bot)

7. Пример программы

import asyncio
import logging
import argparse

from aiogram import Bot, Dispatcher, F
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.filters import CommandStart, Command
from aiogram.types import Message


def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--token",
        required=True,
        help="Telegram bot token from BotFather"
    )
    return parser.parse_args()


async def main(token: str) -> None:
    bot = Bot(
        token=token,
        default=DefaultBotProperties(parse_mode=ParseMode.HTML),
    )

    dp = Dispatcher()

    @dp.message(CommandStart())
    async def cmd_start(message: Message) -> None:
        await message.answer(
            "Привет! Я dummy-бот на aiogram.\n"
            "Напиши любой текст и я его повторю."
        )

    @dp.message(Command("help"))
    async def cmd_help(message: Message) -> None:
        await message.answer("Просто напиши любой текст и я повторю его")

    @dp.message(F.text)
    async def echo(message: Message) -> None:
        await message.answer(message.text)

    await bot.delete_webhook(drop_pending_updates=True)
    await dp.start_polling(bot)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)

    args = parse_args()
    asyncio.run(main(args.token))

FSM (Finite State Machine) в Telegram-ботах

1. Зачем нужна FSM

FSM (конечный автомат состояний) используется, когда бот должен вести пошаговый диалог с пользователем.

FSM позволяет боту:

Без FSM каждое сообщение обрабатывается одинаково, независимо от контекста.

2. Основные элементы FSM в aiogram

Работа с FSM строится вокруг четырёх основных элементов:

3. StatesGroup и State

3.1 Что такое State

State — это логическая метка, обозначающая этап диалога.

Примеры:

3.2 Описание набора состояний

Состояния объединяются в класс-наследник StatesGroup.

from aiogram.fsm.state import StatesGroup, State

class Form(StatesGroup):
    waiting_name = State()
    waiting_age = State()

Каждый атрибут класса — отдельное состояние.

4. Хранилище состояния (Storage)

FSM должна где-то хранить:

Для учебных целей используется хранилище в памяти:

from aiogram.fsm.storage.memory import MemoryStorage

dp = Dispatcher(storage=MemoryStorage())

Хранилище автоматически связывает состояние с пользователем и чатом.

5. FSMContext

5.1 Что такое FSMContext

FSMContext — объект, через который handler:

Он передаётся в handler автоматически.

5.2 Установка состояния

await state.set_state(Form.waiting_name)

После этого все следующие сообщения пользователя будут обрабатываться handlers, привязанными к этому состоянию.

5.3 Очистка состояния

await state.clear()

Используется, когда диалог завершён или отменён.

6. Обработчики с состояниями

6.1 Handler, работающий только в определённом состоянии

@dp.message(Form.waiting_name)
async def process_name(message: Message, state: FSMContext):
    ...

Такой handler будет вызван только если пользователь находится в указанном состоянии.

6.2 Последовательность handlers

@dp.message(CommandStart())
async def start(message: Message, state: FSMContext):
    await state.set_state(Form.waiting_name)

@dp.message(Form.waiting_name)
async def name_step(message: Message, state: FSMContext):
    await state.set_state(Form.waiting_age)

@dp.message(Form.waiting_age)
async def age_step(message: Message, state: FSMContext):
    await state.clear()

Каждый handler:

7. Хранение данных в состоянии

7.1 Сохранение данных

await state.update_data(name=message.text)

Можно сохранять несколько значений:

await state.update_data(name="Иван", age=18)

7.2 Получение данных

data = await state.get_data()
name = data["name"]

Данные представляют собой обычный словарь.

8. Что можно писать в handlers с FSM

В handlers с FSM обычно выполняются следующие действия:

Пример проверки без смены состояния:

if not message.text.isalpha():
    await message.answer("Введите корректное значение")
    return

Состояние остаётся прежним, пока данные не будут корректными.

9. Поведение после завершения FSM

После очистки состояния:

await state.clear()

пользователь:

Это позволяет разделять:

10. Пример программы с FSM

import asyncio
import logging
import argparse
import re

from aiogram import Bot, Dispatcher, F
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.filters import CommandStart
from aiogram.types import Message

from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage


NAME_RE = re.compile(r"^[A-Za-zА-Яа-яЁё]+$")


def is_valid_name(value: str) -> bool:
    value = value.strip()
    return bool(value) and bool(NAME_RE.fullmatch(value))


class Profile(StatesGroup):
    waiting_first_name = State()
    waiting_last_name = State()
    ready = State()


def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument("--token", required=True, help="Telegram bot token")
    return parser.parse_args()


async def main(token: str) -> None:
    bot = Bot(
        token=token,
        default=DefaultBotProperties(parse_mode=ParseMode.HTML),
    )

    dp = Dispatcher(storage=MemoryStorage())

    @dp.message(CommandStart())
    async def cmd_start(message: Message, state: FSMContext) -> None:
        await state.clear()
        await message.answer("Привет! Я учебный бот")
        await message.answer("Как твое имя?")
        await state.set_state(Profile.waiting_first_name)

    @dp.message(Profile.waiting_first_name, F.text)
    async def first_name_step(message: Message, state: FSMContext) -> None:
        text = (message.text or "").strip()

        if not is_valid_name(text):
            await message.answer(
                "Хмммм либо у тебя дурацкое имя, либо ты что-то перепутал, попробуй еще раз"
            )
            return

        await state.update_data(first_name=text)
        await message.answer("Отлично! А теперь фамилия?")
        await state.set_state(Profile.waiting_last_name)

    @dp.message(Profile.waiting_last_name, F.text)
    async def last_name_step(message: Message, state: FSMContext) -> None:
        text = (message.text or "").strip()

        if not is_valid_name(text):
            await message.answer(
                "Ух, если у тебя будет свадьба, советую взять фамилию супруга, а то это какой-то кошмар. Попробуй еще раз"
            )
            return

        await state.update_data(last_name=text)
        data = await state.get_data()

        first_name = data["first_name"]
        last_name = data["last_name"]

        await message.answer(f"Рад познакомиться, {first_name} {last_name}!")
        await state.set_state(Profile.ready)

    @dp.message(Profile.ready)
    async def after_ready(message: Message, state: FSMContext) -> None:
        data = await state.get_data()
        first_name = data.get("first_name", "друг")
        last_name = data.get("last_name", "")

        full_name = (first_name + " " + last_name).strip()
        await message.answer(f"Извини, {full_name}, но я пока не умею общаться, в твоих руках это исправить!")

    @dp.message(Profile.waiting_first_name)
    @dp.message(Profile.waiting_last_name)
    async def non_text_during_form(message: Message) -> None:
        await message.answer("Общайся текстом, а то что ты как неандерталец")

    await bot.delete_webhook(drop_pending_updates=True)
    await dp.start_polling(bot)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    args = parse_args()
    asyncio.run(main(args.token))

Бонус: работа с изображениями в Telegram-боте

1. Получение изображений от пользователя

Когда пользователь отправляет изображение, объект Message содержит поле:

Для работы используется последний элемент списка:

photo = message.photo[-1]

Объект PhotoSize содержит:

При получении изображения поле message.text не используется.

2. Обработка сообщений с изображениями

Для обработки сообщений с изображениями используется фильтр F.photo:

@dp.message(F.photo)
async def on_photo(message: Message):
    photo = message.photo[-1]
    file_id = photo.file_id
    await message.answer("Изображение получено")

Handler вызывается только для сообщений, содержащих изображение.

3. Отправка изображений пользователю

3.1 Отправка изображения по file_id

Если имеется file_id, изображение отправляется следующим образом:

await message.answer_photo(photo=file_id, caption="Изображение")

Telegram использует файл, сохранённый на своих серверах.

3.2 Отправка изображения с диска

Для отправки изображения, хранящегося на диске, используется объект FSInputFile:

from aiogram.types import FSInputFile

photo = FSInputFile("images/image.jpg")
await message.answer_photo(photo=photo, caption="Изображение с диска")

Путь к файлу указывается относительно рабочей директории программы или как абсолютный путь.

4. Сохранение полученного изображения на диск

Для сохранения изображения выполняются следующие шаги:

Пример:

import os

@dp.message(F.photo)
async def save_photo(message: Message, bot: Bot):
    photo = message.photo[-1]
    file_id = photo.file_id

    file = await bot.get_file(file_id)

    os.makedirs("downloads", exist_ok=True)
    filename = f"downloads/{file_id}.jpg"

    await bot.download_file(file.file_path, filename)
    await message.answer("Изображение сохранено")

Изображение сохраняется в указанную директорию.

5. Различие между изображением и файлом

Изображение может быть отправлено двумя способами:

При отправке как файл используется объект Document, содержащий:

Работа с файлами выполняется аналогично работе с изображениями.

6. Основные элементы для работы с изображениями

Для работы с изображениями используются следующие элементы: