Объекты и сущности telegram. Модуль aiogram.types

Модуль aiogram.types содержит классы для всех объектов Telegram Bot API: Message (сообщение), User (пользователь), Chat (чат), CallbackQuery (нажатие кнопки), клавиатуры, фото, документы и ещё около двухсот типов. В aiogram 3 каждый такой класс — модель Pydantic: aiogram получает от Telegram JSON и превращает его в объект с проверенными полями, к которым обращаются через точку, например message.from_user.first_name. Ниже главные типы, их поля и рабочие боты на каждый.

Примеры проверены на aiogram 3.x (версия 3.31.0) и Python 3.14.

Как устроены типы в aiogram 3

Telegram присылает боту данные в формате JSON (текст из пар «ключ: значение»). Работать с сырым JSON неудобно: легко ошибиться в имени ключа, а редактор не подскажет поля. Поэтому aiogram раскладывает его по классам.

В третьей версии эти классы построены на Pydantic (библиотека, которая проверяет данные по описанию полей). Из этого следует несколько практических вещей:

  • у каждого поля есть тип, и редактор кода подсказывает, что лежит в message.chat или user.username;
  • необязательное поле, которого нет в ответе Telegram, равно None. Поэтому user.last_name и user.username перед использованием проверяют;
  • объект можно превратить обратно в словарь: user.model_dump(). Пригодится, чтобы записать данные в лог или базу;
  • полученные объекты нельзя менять: user.first_name = "Мария" вызовет ошибку. Нужна копия с другим значением — user.model_copy(update={...}).

Классы не пишут руками. Разработчики aiogram генерируют их из официального описания Bot API, поэтому новые поля Telegram появляются в библиотеке вскоре после выхода новой версии API. Если в документации Telegram есть поле has_spoiler, в aiogram оно называется так же.

Этот пример работает без токена, его можно запустить прямо сейчас:

from aiogram.types import User

user = User(id=42, is_bot=False, first_name="Анна", username="anna_dev")
print(user.full_name)
print(user.model_dump(exclude_none=True))

try:
    user.first_name = "Мария"
except Exception as e:
    print("Изменить нельзя:", type(e).__name__)

renamed = user.model_copy(update={"first_name": "Мария"})
print(renamed.first_name)

Вывод: Анна, затем словарь {'id': 42, 'is_bot': False, 'first_name': 'Анна', 'username': 'anna_dev'}, затем Изменить нельзя: ValidationError и Мария.

Главные типы aiogram.types

  • Update — одно обновление от Telegram. Внутри заполнено ровно одно поле: message, edited_message, callback_query, inline_query или другое. Диспетчер смотрит, какое именно, и передаёт его нужному обработчику. Поэтому в обработчик приходит сразу Message или CallbackQuery, а не Update.
  • Message — сообщение: текст, отправитель, чат, дата, вложения.
  • User — пользователь или бот: id, имя, ник.
  • Chat — чат: личный диалог, группа, супергруппа или канал.
  • CallbackQuery — нажатие встроенной кнопки. Содержит data (строку, привязанную к кнопке), from_user и message (сообщение, под которым была кнопка).
  • InlineQuery — запрос в inline-режиме, когда пользователь набирает @имя_бота текст в любом чате.
  • InlineKeyboardButton и InlineKeyboardMarkup — кнопка и клавиатура под сообщением.
  • KeyboardButton и ReplyKeyboardMarkup — кнопка и клавиатура вместо обычной клавиатуры телефона.
  • PhotoSize — один из размеров присланной фотографии.
  • Audio, Voice, Video, Document, Sticker — вложения: длительность, размеры, имя файла, вес.
  • Location и Venue — точка на карте и место с названием и адресом.
  • FSInputFile — файл с диска, который бот отправляет. В отличие от остальных, этот тип не приходит от Telegram, его создаёт сам бот.

Полный список — в разделе Types документации aiogram 3.

Класс User

Объект User описывает пользователя Telegram или бота. В обработчике сообщения его берут из message.from_user, в обработчике кнопки — из callback.from_user.

Поля:

  • id — постоянный номер пользователя. Имя и ник человек может сменить, id останется. По нему хранят пользователя в базе.
  • is_bot — True, если это бот.
  • first_name — имя, есть всегда.
  • last_name — фамилия, может быть None.
  • username — ник без «@», может быть None.
  • language_code — язык интерфейса Telegram у пользователя, например ru.

Есть и готовые свойства: full_name склеивает имя и фамилию, mention_html() делает упоминание-ссылку на пользователя.

Бот, который приветствует пользователя

import asyncio
import os

from aiogram import Bot, Dispatcher, Router
from aiogram.filters import CommandStart
from aiogram.types import Message

router = Router()


@router.message(CommandStart())
async def greet(message: Message):
    user = message.from_user
    name = user.first_name
    if user.last_name:
        name += f" {user.last_name}"
    text = f"Привет, {name}!"
    if user.username:
        text += f"\nВаш ник: @{user.username}"
    if user.language_code:
        text += f"\nЯзык Telegram: {user.language_code}"
    await message.answer(text)


async def main():
    bot = Bot(token=os.environ["BOT_TOKEN"])
    dp = Dispatcher()
    dp.include_router(router)
    await dp.start_polling(bot)


if __name__ == "__main__":
    asyncio.run(main())

На /start бот здоровается по имени и, если есть, называет ник и язык. Фамилию и ник проверяем на None: у многих их нет. Токен (ключ бота от BotFather) берётся из переменной окружения BOT_TOKEN: в Windows set BOT_TOKEN=ваш_токен, в Linux и macOS export BOT_TOKEN=ваш_токен. Писать токен прямо в код не стоит: его увидит любой, кто получит файл.

Класс Message

Чаще всего в боте нужны поля message_id (номер сообщения в чате), from_user (отправитель), chat (где написано), date, text (у фото и документов вместо него caption), reply_to_message (на что ответили) и вложения: photo, document, voice. Методы answer(), reply(), edit_text() и delete() отправляют, редактируют и удаляют сообщения. Всё это с примерами разобрано в отдельном уроке по сообщениям.

Класс Chat

Объект Chat описывает чат. В обработчике его берут из message.chat.

Поля:

  • id — номер чата. У личного диалога совпадает с id пользователя, у групп и каналов отрицательный.
  • type — тип: private, group, supergroup или channel.
  • title — название группы или канала.
  • username — ник публичной группы или канала без «@».
  • first_name, last_name — имя собеседника в личном диалоге.

Описание чата (description) и ссылка-приглашение (invite_link) в сообщениях не приходят. Их получают отдельным запросом await bot.get_chat(chat_id): он возвращает расширенный объект ChatFullInfo.

Методы. У объекта Chat есть короткие методы, номер чата в них передавать не нужно, он уже известен:

  • await chat.ban(user_id) — заблокировать участника;
  • await chat.unban(user_id) — снять блокировку;
  • await chat.leave() — бот выходит из чата;
  • await chat.get_member(user_id) — данные участника и его статус;
  • await chat.get_administrators() — список администраторов;
  • await chat.get_member_count() — число участников;
  • await chat.set_title("Новое название") — переименовать группу;
  • await chat.promote(user_id, can_delete_messages=True) — дать участнику права администратора.

Это обёртки над методами бота: chat.ban(user_id) делает то же, что bot.ban_chat_member(chat.id, user_id). Для блокировки, переименования и выдачи прав бот сам должен быть администратором с нужными правами.

Бот, который выдаёт информацию о чате по команде /info

import asyncio
import os

from aiogram import Bot, Dispatcher, Router
from aiogram.filters import Command
from aiogram.types import Message

router = Router()


@router.message(Command("info"))
async def chat_info(message: Message, bot: Bot):
    chat = message.chat
    lines = [f"ID чата: {chat.id}", f"Тип: {chat.type}"]
    if chat.title:
        lines.append(f"Название: {chat.title}")
    if chat.username:
        lines.append(f"Ник: @{chat.username}")

    full = await bot.get_chat(chat.id)
    if full.description:
        lines.append(f"Описание: {full.description}")

    if chat.type != "private":
        count = await chat.get_member_count()
        admins = await chat.get_administrators()
        names = ", ".join(a.user.full_name for a in admins)
        lines.append(f"Участников: {count}")
        lines.append(f"Администраторы: {names}")

    await message.answer("\n".join(lines))


async def main():
    bot = Bot(token=os.environ["BOT_TOKEN"])
    dp = Dispatcher()
    dp.include_router(router)
    await dp.start_polling(bot)


if __name__ == "__main__":
    asyncio.run(main())

В личном диалоге бот покажет id и тип чата. В группе добавит название, описание, число участников и имена администраторов. Объект bot aiogram передаёт в обработчик сам, достаточно указать параметр bot: Bot.

Встроенные кнопки

В Telegram два вида кнопок. Встроенные (inline) висят под конкретным сообщением. Нажатие на такую кнопку не отправляет текст в чат, а присылает боту событие CallbackQuery. Текстовые кнопки появляются вместо клавиатуры телефона, и нажатие отправляет в чат текст с кнопки, как будто пользователь набрал его сам.

Класс InlineKeyboardButton

Одна встроенная кнопка. Поля:

  • text — надпись на кнопке;
  • callback_data — строка до 64 байт, которую Telegram вернёт боту при нажатии;
  • url — ссылка: кнопка откроет сайт, а бот о нажатии не узнает.

Класс InlineKeyboardMarkup и InlineKeyboardBuilder

InlineKeyboardMarkup — сама клавиатура, список рядов кнопок. Методов add(), insert() и row() из aiogram 2 у неё больше нет. Собирать клавиатуру удобнее через InlineKeyboardBuilder из aiogram.utils.keyboard:

  • builder.button(text=..., callback_data=...) — добавить кнопку;
  • builder.add(InlineKeyboardButton(...)) — добавить готовый объект кнопки;
  • builder.row(...) — начать новый ряд с этих кнопок;
  • builder.adjust(2, 1) — разложить кнопки по рядам: две, потом одна;
  • builder.as_markup() — получить клавиатуру, которую передают в reply_markup.

Как к кнопке привязать действие

Бот по команде /inline присылает сообщение с тремя кнопками: две отвечают боту, третья открывает сайт.

import asyncio
import os

from aiogram import Bot, Dispatcher, F, Router
from aiogram.filters import Command, CommandStart
from aiogram.types import (
    CallbackQuery,
    InlineKeyboardButton,
    KeyboardButton,
    Message,
    ReplyKeyboardRemove,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder

router = Router()


@router.message(Command("inline"))
async def send_inline(message: Message):
    builder = InlineKeyboardBuilder()
    builder.add(InlineKeyboardButton(text="Нажми меня", callback_data="pressed"))
    builder.button(text="Информация", callback_data="info_command")
    builder.button(text="Сайт", url="https://victor-komlev.ru/")
    builder.adjust(2, 1)
    await message.answer("Встроенная клавиатура:", reply_markup=builder.as_markup())


@router.callback_query(F.data == "pressed")
async def button_pressed(callback: CallbackQuery):
    await callback.answer("Кнопка нажата")
    await callback.message.answer("Вы нажали на кнопку")


@router.callback_query(F.data == "info_command")
async def info_callback(callback: CallbackQuery):
    await callback.answer()
    await callback.message.answer("Это бот-пример для статьи про aiogram.types")


@router.message(CommandStart())
async def send_reply_keyboard(message: Message):
    builder = ReplyKeyboardBuilder()
    builder.add(KeyboardButton(text="Привет!"))
    builder.button(text="Помощь")
    builder.button(text="Убрать клавиатуру")
    builder.adjust(2, 1)
    await message.answer(
        "Текстовая клавиатура внизу экрана",
        reply_markup=builder.as_markup(resize_keyboard=True),
    )


@router.message(F.text == "Привет!")
async def say_hello(message: Message):
    await message.answer(f"И вам привет, {message.from_user.first_name}!")


@router.message(F.text == "Убрать клавиатуру")
async def remove_keyboard(message: Message):
    await message.answer("Убрал", reply_markup=ReplyKeyboardRemove())


async def main():
    bot = Bot(token=os.environ["BOT_TOKEN"])
    dp = Dispatcher()
    dp.include_router(router)
    await dp.start_polling(bot)


if __name__ == "__main__":
    asyncio.run(main())

Нажатие кнопки ловит обработчик @router.callback_query(...) с фильтром по callback_data: F.data == "pressed". Внутри обработчика объект CallbackQuery: в callback.data строка кнопки, в callback.from_user тот, кто нажал, в callback.message сообщение с клавиатурой. Вызов callback.answer() обязателен, иначе на кнопке будут крутиться часики. Если передать в него текст, он всплывёт коротким уведомлением.

Разные значения callback_data удобнее, чем разбор текста кнопки: надпись можно поменять или перевести, а код продолжит работать.

Текстовые кнопки

KeyboardButton — кнопка текстовой клавиатуры, главное поле у неё одно: text. Клавиатура целиком — ReplyKeyboardMarkup, а собирают её через ReplyKeyboardBuilder так же, как встроенную. Пример выше на /start показывает клавиатуру из трёх кнопок.

Нажатие текстовой кнопки — это обычное сообщение с текстом кнопки. Поэтому его ловят не callback_query, а обработчиком сообщений с фильтром по тексту: @router.message(F.text == "Привет!"). В старой версии статьи здесь стоял callback_query_handler, это была ошибка: текстовая кнопка не присылает CallbackQuery.

Параметр resize_keyboard=True делает кнопки компактными, без него они растянутся на полэкрана. Убрать клавиатуру можно, отправив сообщение с reply_markup=ReplyKeyboardRemove(). Много кнопок на такой клавиатуре путают пользователя и закрывают переписку, обычно хватает двух-четырёх.

Как выровнять кнопки на клавиатуре

Ряды задаёт adjust(). Числа — сколько кнопок в каждом ряду по порядку:

from aiogram.utils.keyboard import ReplyKeyboardBuilder

builder = ReplyKeyboardBuilder()
for i in range(1, 6):
    builder.button(text=f"Кнопка {i}")
builder.adjust(3, 2)  # первый ряд — 3 кнопки, второй — 2
keyboard = builder.as_markup(resize_keyboard=True)

adjust(2) разложит все кнопки по две в ряд. Если чисел меньше, чем рядов, последнее число повторяется. С InlineKeyboardBuilder всё работает так же.

Работа с медиа от пользователя

Бот получает от пользователя фото, видео, голосовое, документ, стикер или точку на карте и сообщает, что пришло. Тип вложения отбирает фильтр: F.photo, F.video и так далее. В aiogram 2 для этого писали content_types=types.ContentTypes.PHOTO.

import asyncio
import os

from aiogram import Bot, Dispatcher, F, Router
from aiogram.types import Message

router = Router()


@router.message(F.photo)
async def handle_photo(message: Message):
    biggest = message.photo[-1]
    await message.reply(
        f"Фото пришло в {len(message.photo)} размерах, самый большой {biggest.width}x{biggest.height}"
    )


@router.message(F.video)
async def handle_video(message: Message):
    video = message.video
    await message.reply(
        f"Видео: {video.duration} с, {video.width}x{video.height} пикселей"
    )


@router.message(F.voice)
async def handle_voice(message: Message):
    await message.reply(f"Голосовое длительностью {message.voice.duration} с")


@router.message(F.document)
async def handle_document(message: Message):
    doc = message.document
    await message.reply(f"Документ {doc.file_name}, {doc.file_size} байт")


@router.message(F.sticker)
async def handle_sticker(message: Message):
    await message.reply(f"Стикер {message.sticker.emoji}")


@router.message(F.location)
async def handle_location(message: Message):
    loc = message.location
    await message.reply(f"Координаты: {loc.latitude}, {loc.longitude}")


async def main():
    bot = Bot(token=os.environ["BOT_TOKEN"])
    dp = Dispatcher()
    dp.include_router(router)
    await dp.start_polling(bot)


if __name__ == "__main__":
    asyncio.run(main())

Одна фотография приходит списком PhotoSize: Telegram сам делает несколько копий разного размера, от миниатюры до оригинала. Последний элемент списка самый большой. В старом примере бот отвечал на каждый размер отдельным сообщением, здесь он отвечает один раз.

У каждого вложения есть file_id. По нему бот может переслать файл, не скачивая: await message.answer_photo(message.photo[-1].file_id). Скачать файл на диск можно так: await bot.download(message.document, destination="file.pdf"). Обратная задача, отправить свой файл с диска, решается через FSInputFile: await message.answer_photo(FSInputFile("photo.jpg")).

Создание опроса с помощью бота

Бот помогает пользователю собрать опрос по шагам: сначала вопрос, потом варианты ответа, и по команде /send отправляет настоящий опрос Telegram (тип Poll) с подсчётом голосов.

import asyncio
import os

from aiogram import Bot, Dispatcher, F, Router
from aiogram.filters import Command, CommandStart
from aiogram.types import Message

router = Router()

polls: dict[int, dict] = {}  # черновики опросов: id пользователя -> данные


@router.message(CommandStart())
async def start_command(message: Message):
    await message.answer("Привет! Чтобы создать опрос, отправьте /question")


@router.message(Command("question"))
async def create_poll(message: Message):
    polls[message.from_user.id] = {"question": "", "options": [], "step": "question"}
    await message.answer("Введите вопрос для опроса:")


@router.message(Command("send"))
async def send_poll(message: Message):
    poll = polls.get(message.from_user.id)
    if poll is None:
        await message.answer("Сначала создайте опрос: /question")
        return
    if not poll["question"] or len(poll["options"]) < 2:
        await message.answer("Нужен вопрос и хотя бы два варианта ответа")
        return
    await message.answer_poll(
        question=poll["question"],
        options=poll["options"],
        is_anonymous=False,
    )
    del polls[message.from_user.id]


@router.message(F.text, F.from_user.id.in_(polls))
async def fill_poll(message: Message):
    poll = polls[message.from_user.id]
    if poll["step"] == "question":
        poll["question"] = message.text
        poll["step"] = "options"
        await message.answer("Введите первый вариант ответа:")
    else:
        poll["options"].append(message.text)
        await message.answer("Вариант добавлен. Введите ещё один или отправьте /send")


async def main():
    bot = Bot(token=os.environ["BOT_TOKEN"])
    dp = Dispatcher()
    dp.include_router(router)
    await dp.start_polling(bot)


if __name__ == "__main__":
    asyncio.run(main())

Черновики хранятся в словаре polls: ключ — id пользователя, значение — вопрос, варианты и текущий шаг. Поэтому у каждого пользователя один черновик за раз. Фильтр F.from_user.id.in_(polls) пропускает в fill_poll только тех, кто начал опрос. Команды стоят выше этого обработчика, поэтому /send не попадёт в варианты ответа. Telegram принимает не больше двенадцати вариантов, а меньше двух бот сам не пропустит: выбирать из одного варианта бессмысленно.

Словарь живёт в памяти и очищается при перезапуске бота. Для пошаговых диалогов в aiogram 3 есть машина состояний (FSM, finite state machine — бот помнит, на каком шаге пользователь) с хранилищем в памяти или в Redis. Этот пример намеренно обходится без неё, чтобы показать логику в чистом виде.

Уроки по теме

Спокойный вход с мини-проектами — уроки курса (в нём более простая библиотека telebot):

Понравилась статья? Поделиться с друзьями:
Школа Виктора Комлева
Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.