Привет! Продолжаем разбор темы разработки Telegram ботов с помощью Aiogram 3. В прошлой статье мы рассмотрели:

  • Магические фильтры (кратко)

  • Фильтры Command и CommandStart

  • Роутеры и диспетчер

  • Создание токена бота через BotFather

  • Выполнили первый запуск бота

  • Работали в рамках структуры, разработанной мной

Если вы новичок, предлагаю следовать моей структуре бота, но дальше – на ваше усмотрение.

О чём сегодня пойдёт речь:

  • Текстовые клавиатуры (markup)

  • ReplyKeyboardBuilder (генератор текстовых клавиатур)

  • Командное меню

  • Магические фильтры (в контексте клавиатур)

  • Бонус: Command object

Давайте обо всём по порядку.

Текстовая клавиатура

Текстовая клавиатура отображается под полем набора сообщения. Основная её особенность в том, что она не содержит никакой информации, кроме текста на кнопках (исключение – специальные кнопки, такие как геолокация о которых мы поговорим далее).

Другими словами, текст на кнопке отправляется боту, который реагирует на это сообщение, например, через F.text ==.

Начнем с кода. Реализуем его в файле all_kb.py, который находится в пакете keyboards (см. предыдущую статью для деталей).

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

Импорты в all_kb.py:

from aiogram.types import KeyboardButton, ReplyKeyboardMarkup
from create_bot import admins

Создание клавиатуры "Главное меню":

def main_kb(user_telegram_id: int):
    kb_list = [
        [KeyboardButton(text="? О нас"), KeyboardButton(text="? Профиль")],
        [KeyboardButton(text="? Заполнить анкету"), KeyboardButton(text="? Каталог")]
    ]
    if user_telegram_id in admins:
        kb_list.append([KeyboardButton(text="⚙️ Админ панель")])
    keyboard = ReplyKeyboardMarkup(keyboard=kb_list, resize_keyboard=True, one_time_keyboard=True)
    return keyboard

Описание функции main_kb

Функция main_kb принимает один аргумент user_telegram_id типа int, который представляет собой ID пользователя в Telegram.

Создание списка кнопок:

kb_list = [
    [KeyboardButton(text="? О нас"), KeyboardButton(text="? Профиль")],
    [KeyboardButton(text="? Заполнить анкету"), KeyboardButton(text="? Каталог")]
]
  • kb_list: список списков с объектами KeyboardButton.

  • Первая строка кнопок: "? О нас" и "? Профиль".

  • Вторая строка кнопок: "? Заполнить анкету" и "? Каталог".

Добавление кнопки для админов:

if user_telegram_id in admins:
    kb_list.append([KeyboardButton(text="⚙️ Админ панель")])

Если user_telegram_id присутствует в списке admins, добавляется строка с кнопкой "⚙️ Админ панель".

Создание и возврат объекта клавиатуры:

keyboard = ReplyKeyboardMarkup(keyboard=kb_list, resize_keyboard=True, one_time_keyboard=True)
return keyboard

Функция возвращает созданную клавиатуру, которую мы затем привязываем к сообщению.

Привязка клавиатуры к сообщению (handlers/start.py):

from aiogram import Router, F
from aiogram.filters import CommandStart
from aiogram.types import Message
from keyboards.all_kb import main_kb

start_router = Router()

@start_router.message(CommandStart())
async def cmd_start(message: Message):
    await message.answer('Запуск сообщения по команде /start используя фильтр CommandStart()',
                         reply_markup=main_kb(message.from_user.id))

Для нашего фильтра мы вытянули телеграм айди пользователя из объекта message (message.from_user.id).

Импортируем клавиатуру из пакета keyboards и при помощи reply_markup привязываем её к сообщению. Давайте посмотрим что у нас получилось:

Большие кнопки
Большие кнопки

Так как у нас кнопок было достаточно много — они у нас получились более-менее обычного размера, но, если бы это была всего 1 кнопка, то тут бы вышло такое:

На любителя. Давайте исправим.
На любителя. Давайте исправим.

Улучшение клавиатуры

def main_kb(user_telegram_id: int):
    kb_list = [
        [KeyboardButton(text="? О нас"), KeyboardButton(text="? Профиль")],
        [KeyboardButton(text="? Заполнить анкету"), KeyboardButton(text="? Каталог")]
    ]
    if user_telegram_id in admins:
        kb_list.append([KeyboardButton(text="⚙️ Админ панель")])
    keyboard = ReplyKeyboardMarkup(
        keyboard=kb_list,
        resize_keyboard=True,
        one_time_keyboard=True,
        input_field_placeholder="Воспользуйтесь меню:"
    )
    return keyboard
  • resize_keyboard=True: клавиатура будет автоматически изменять размер.

  • one_time_keyboard=True: клавиатура скрывается после одного использования.

  • input_field_placeholder: заменяет стандартную подпись «Написать сообщение...» на пользовательскую.

Смотрим что получилось:

Красиво, правда?
Красиво, правда?

Особые текстовые кнопки

Теперь создадим клавиатуру с "особыми кнопками". На примере будут кнопки:

  • Поделиться контактами

  • Поделиться локацией

  • Создать викторину/опрос

Создание специальной клавиатуры:

def create_spec_kb():
    kb_list = [
        [KeyboardButton(text="Отправить гео", request_location=True)],
        [KeyboardButton(text="Поделиться номером", request_contact=True)],
        [KeyboardButton(text="Отправить викторину/опрос", request_poll=KeyboardButtonPollType())]
    ]
    keyboard = ReplyKeyboardMarkup(keyboard=kb_list,
                                   resize_keyboard=True,
                                   one_time_keyboard=True,
                                   input_field_placeholder="Воспользуйтесь специальной клавиатурой:")
    return keyboard
  • request_location=True: позволяет пользователю отправить геолокацию.

  • request_contact=True: позволяет отправить контактный номер.

  • request_poll=KeyboardButtonPollType(): позволяет создать викторину или опрос. Может принимать один из параметров type = «quiz» (викторина) или «regular» (опрос). В нашем случае и то и то будет обработано.

Привязка специальной клавиатуры под обработчик /start_2:

@start_router.message(Command('start_2'))
async def cmd_start(message: Message):
    await message.answer('Запуск сообщения по команде /start_2 используя фильтр Command()',
                         reply_markup=create_spec_kb())

Смотрим что получилось:

Визуально ничем не отличается от обычной текстовой клавиатуры.
Визуально ничем не отличается от обычной текстовой клавиатуры.

Геолокацию можно отправить только со смартфона. При вызове этой опции через пк - получим такое сообщение:

Со смартфона данные передаются корректно, и теперь остается только обработать гео-данные. Как это сделать мы подробно обговорим в теме про FSM.

При клике на "Поделиться номером" (текст может быть любой). Пользователь увидит всплывающее окно:

Так выглядит всплывающее окно на этом этапе.
Так выглядит всплывающее окно на этом этапе.

После клика на "Поделиться" произойдет отправка номера телефона, который привязан к профилю телеграмм. Далее останется захватить ответ. Как это сделать мы тоже подробно обговорим в теме про FSM.

Так выглядит универсально окно. Давайте создадим опрос.
Так выглядит универсально окно. Давайте создадим опрос.
Пример
Пример

Штуку с викториной и опросником удобно использовать в группах и телеграмм каналах, которые будет администрировать ваш бот.

Использование ReplyKeyboardBuilder

Теперь давайте воспользуемся нововведением в aiogram 3, а именно строителем текстовых клавиатур. Для начала сделаем импорт:

from aiogram.utils.keyboard import ReplyKeyboardBuilder

Давайте сгенирируем некую шкалу голосования в котором результаты у нас записаны в виде баллов от 1 до 10. Пример кода:

def create_rat():
    builder = ReplyKeyboardBuilder()
    for item in [str(i) for i in range(1, 11)]:
        builder.button(text=item)
    builder.button(text='Назад')
    builder.adjust(4, 4, 2, 1)
    return builder.as_markup(resize_keyboard=True)

Описание функции create_rat

def create_rat():
    builder = ReplyKeyboardBuilder()

Создаем объект ReplyKeyboardBuilder, который будет использоваться для построения клавиатуры.

Добавление кнопок с оценками

for item in [str(i) for i in range(1, 11)]:
        builder.button(text=item)

Создаем список строк от '1' до '10' с помощью генератора списка.

Для каждого элемента в этом списке добавляем кнопку с текстом, равным этому элементу.

Добавление кнопки "Назад"

builder.button(text='Назад')

Настройка расположения кнопок

builder.adjust(4, 4, 2, 1)

Устанавливаем расположение кнопок на клавиатуре.

adjust(4, 4, 2, 1) указывает, что кнопки должны быть размещены в строках по 4, 4, 2 и 1 кнопке соответственно.

  • Первая строка будет содержать 4 кнопки.

  • Вторая строка будет содержать 4 кнопки.

  • Третья строка будет содержать 2 кнопки.

  • Четвертая строка будет содержать 1 кнопку ("Назад").

Возврат разметки клавиатуры

return builder.as_markup(resize_keyboard=True)

Преобразуем построенную клавиатуру в объект ReplyKeyboardMarkup с помощью метода as_markup.

Указываем параметр resize_keyboard=True, чтобы клавиатура автоматически изменяла размер в зависимости от количества и размера кнопок.

Привяжем клавиатуру к команде /start_3

@start_router.message(F.text == '/start_3')
async def cmd_start(message: Message):
    await message.answer('Запуск сообщения по команде /start_3 используя магический фильтр F.text!',
                         reply_markup=create_rat())

Смотрим что получилось:

Как вам?
Как вам?

Видим что все получилось как задумывали.

Мне кажется, что я раскрыл максимум из того что могло иметь отношение к текстовым кнопкам. Теперь поговорим про командное меню.

Работа с командным меню

Так выглядит командное меню в BotFather
Так выглядит командное меню в BotFather

Похожее меню мы можем создать двумя способами:

  1. Через BotFather

  2. Напрямую через код

Оба способа мы рассмотрим далее.

Настройка командного меню через BotFather

Вводим /mybots (или выбираем через командное меню)
Вводим /mybots (или выбираем через командное меню)
Выбираем нашего бота
Выбираем нашего бота
Выбираем Edit bot
Выбираем Edit bot
Выбираем Edit command
Выбираем Edit command
Отправляем боту сообщение такого вида как на скрине
Отправляем боту сообщение такого вида как на скрине
start - Главная страница
start_3 - Вызов спец. клавиатуры

После этих действий в боте появится кнопка «Меню» с этими командами. Как вы понимаете, для того чтоб все работало необходимо в боте привязать к каждой команде нужное действие.

Настройка командного меню через код:

from aiogram.types import BotCommand, BotCommandScopeDefault

async def set_commands():
    commands = [BotCommand(command='start', description='Старт'),
                BotCommand(command='start_2', description='Старт 2'),
                BotCommand(command='start_3', description='Старт 3')]
    await bot.set_my_commands(commands, BotCommandScopeDefault())

Давайте разберем код функции set_commands построчно, чтобы понять, что он делает.

Импортируемые модули и объекты

from aiogram.types import BotCommand, BotCommandScopeDefault
  • BotCommand: объект, используемый для создания команд бота. Каждая команда имеет два атрибута: command (имя команды) и description (описание команды).

  • BotCommandScopeDefault: объект, определяющий область действия команд. В данном случае используется область по умолчанию, что означает, что команды будут действовать для всех пользователей.

Описание функции set_commands

async def set_commands():

Объявляем асинхронную функцию set_commands, которая будет использоваться для установки команд бота. Асинхронная функция используется, потому что взаимодействие с API Telegram требует выполнения асинхронных запросов.

Создание списка команд

commands = [
        BotCommand(command='start', description='Старт'),
        BotCommand(command='start_2', description='Старт 2'),
        BotCommand(command='start_3', description='Старт 3')
    ]

Создаем список commands, содержащий три команды:

  • BotCommand(command='start', description='Старт'): команда /start с описанием "Старт".

  • BotCommand(command='start_2', description='Старт 2'): команда /start_2 с описанием "Старт 2".

  • BotCommand(command='start_3', description='Старт 3'): команда /start_3 с описанием "Старт 3".

Установка команд бота

await bot.set_my_commands(commands, BotCommandScopeDefault())

Вызываем метод set_my_commands объекта bot для установки команд бота.

Передаем в метод два аргумента:

  • commands: список команд, который мы создали ранее.

  • BotCommandScopeDefault(): область действия команд по умолчанию, которая устанавливает команды для всех пользователей.

и вызовем функцию в конце главной функции (то есть наш бот сначала будет запускаться, а после отправлять командное меню):

async def main():
    # scheduler.add_job(send_time_msg, 'interval', seconds=10)
    # scheduler.start()
    dp.include_router(start_router)
    await bot.delete_webhook(drop_pending_updates=True)
    await dp.start_polling(bot)
    await set_commands()

Проверяем:

Все сработало
Все сработало

Использование Command Object для обработки аргументов команды

Вы, возможно, знали (если нет, то сейчас узнаете), что ссылки вида https://t.me/your_bot?start=12345 имеют особое значение для Telegram ботов. Бот может захватывать и обрабатывать информацию, следующую за start=. Это особенно полезно для реализации реферальных программ или отслеживания источника, откуда пришел пользователь.

Рассмотрим пример использования этой возможности на практическом примере. Писал бота клиники, который периодически рекламируется в различных Telegram-каналах и группах. Вместо простой ссылки на бота, такой как https://t.me/your_bot, менеджеры используют ссылки с метками, например, https://t.me/your_bot?start=habr, где habr - это метка источника.

Когда бот обнаруживает нового пользователя, он проверяет, была ли передана специальная информация в стартовой ссылке (метка). Если метка присутствует, бот фиксирует приход пользователя с конкретного источника. Эта информация затем отправляется на админ-панель для анализа маркетологами.

То же самое можно сделать для реферальной программы. Например, реферальная ссылка может содержать Telegram ID пользователя. Бот проверяет, является ли пользователь новым, и если это так, то пользователю, который поделился своей реферальной ссылкой, начисляются бонусы.

Для начала нам нужно выполнить новый импорт (CommandObject):

from aiogram.filters import CommandStart, Command, CommandObject

Изменение функции cmd_start:

@start_router.message(CommandStart())
async def cmd_start(message: Message, command: CommandObject):
    command_args: str = command.args
    if command_args:
        await message.answer(
            f'Запуск сообщения по команде /start используя фильтр CommandStart() с меткой <b>{command_args}</b>',
            reply_markup=main_kb(message.from_user.id))
    else:
        await message.answer(
            f'Запуск сообщения по команде /start используя фильтр CommandStart() без метки',
            reply_markup=main_kb(message.from_user.id))

Обратите внимание, что в тройке больше не работает: message.get_args()!

Тут нас может заинтересовать только извлечение аргументов из команды:

command_args: str = cmd.args

Далее уже работаем с переменной command_args, как с обычным значением. Если метки не будет, то command_args будет равно None (этот случай я так же обработал в своем коде).

Тестируем:

Тут я сделал клик по ссылке и после нажал на "Запустить". Видим что мы захватили метку.
Тут я сделал клик по ссылке и после нажал на "Запустить". Видим что мы захватили метку.

Тот же результат мы получим если передадим свою метку в конструкцию такого вида:

/start метка

Сработало!
Сработало!

Мы видим, что бот успешно обработал метку. Ничто не мешает добавить такой же обработчик к любой другой команде. Это будет работать аналогично команде “/start”, за исключением того, что другие команды нельзя использовать в стартовой ссылке.

Заключение

Сегодня мы подробно разобрали темы командного меню и текстовых кнопок. Если у вас возникли вопросы по этой теме, пишите их в комментариях — я всегда стараюсь ответить каждому.

Если эта статья была для вас полезной, оставьте комментарий, подпишитесь и поставьте лайк. На создание таких туториалов уходит много времени и сил, и ваша поддержка очень важна для меня. Обратная связь от аудитории вдохновляет продолжать делиться знаниями и опытом.

Спасибо за внимание, и до новых встреч!

Комментарии (8)


  1. Shopech
    11.06.2024 07:12
    +1

    Жду искренне в 3 части разбор мидлварей aiogram и их настройки


    1. yakvenalex Автор
      11.06.2024 07:12
      +1

      К сожалению или к счатью, но в третьей части у меня разбор CallBack (если время будет сегодня напишу и завтра утром выложу). А после фильтры, так как это более "попсовая" штука в разработке ботов, но до всего дойдем) Скажу честно, примерно в половине своих проектов я не использую мидлвари, но, конечно, в будущих статьях и это обсудим)


  1. it_police
    11.06.2024 07:12
    +1

    Спасибо за статью. Подписался.


  1. yailya
    11.06.2024 07:12

    вопрос не ради разжигания холивара на национальной почве, а скорее из соображений безопасности. Aiogram создан украинцем и им же модерируется. документация доступна только на двух языках - украинском и английском, при том, что, очевидно, у библиотеки есть огромное количество русскоязычных пользователей.

    нет ли тут потенциальной угрозы отключения всего и вся или неочевидного поведения для русскоязычных пользователей?


    1. markoni
      11.06.2024 07:12

      Никто не мешает клонировать-проверить, а также пользоваться англ. документацией. Тем более, что автор оперативно и адекватно отвечает на вопросы.


      1. yailya
        11.06.2024 07:12

        да. я понимаю, что всегда можно смотреть все обновления библиотеки. но ведь никто так не делает. и 90% пользователей не будут, потому что это требует очень глубокого погружения и опыта исследований кода.

        про документацию никаких вопросов. мне и англ хватит.


    1. yakvenalex Автор
      11.06.2024 07:12

      Думаю, что в современном мире заблокировать доступ к чему-либо по территориальному признаку невозможно. Яркий тому пример ВК в Украине, Instagram, FB, Openai, RuTracker в РФ и так далее. Кроме установки библиотеки и копирования (клонирования) - всегда можно воспользоваться ВПН (писал недавно как свой ВПН за 5 минут развернуть на ВПС). Так что причин для беспокойств тут не вижу)


      1. yailya
        11.06.2024 07:12

        пример: вы развернули бота на сервере в РФ. библиотека по ip узнает, где произошло развёртывание и дальше делает что-то (скачивает доп скрипты какие-то и тд)

        да, можно установить VPN. но тогда на этом серваке будут недоступны какие-то российские сервисы...