Модуль 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):
- Словарь бот-мейкера: 11 главных слов — курс «Создание чат-ботов»
