Содержание
- Создаём клавиатуру
- Распределяем кнопки по рядам
- Управляем содержимым callback
- Объединение нескольких клавиатур в одну
- Итог
- Обычные кнопки¶
- Инлайн-кнопки¶
- Какие задачи стоят перед ботом?
- Создание простого Telegram-бота с кнопками
- Создаем бота с кнопкой для Telegram под слив на беттинг
- Готовый Telegram-бот с меню
- Подводя итоги
Python *Я пиарюсь
Боты – одна из особенностей Telegram, сделавших мессенджер столь популярным. А его встроенные клавиатуры дают разработчикам большую свободу взаимодействия с пользователями.
Keyboa помогает создавать встроенные клавиатуры любой сложности для ботов, разработанных на базе pyTelegramBotAPI.
В этой статье рассмотрим базовые возможности модуля – создание клавиатур из разных наборов данных, автоматическое и ручное распределение кнопок по рядам, объединение нескольких клавиатур в одну. Научимся создавать сложные, динамические callback, сохраняя в них информацию о выборе пользователя.
Статья рассчитана на тех, кто знает основы Telegram Bot API и хотя бы немного знаком с фреймворком pyTelegramBotAPI.
Модуль требует > Python 3.5 и ставится через pip:
pip install keyboa
В официальной документации Telegram объект inline_keyboard
определен как массив, состоящий из массивов кнопок (Array of Array of InlineKeyboardButton). То есть основной массив (список) – это клавиатура в целом, а его элементы (вложенные списки) – это ряды кнопок. Не переживайте, если определение кажется сложным – позже мы разберём его на простом примере.
Предположим, что сам бот у нас уже настроен:
import os from telebot import TeleBot from keyboa import keyboa_maker token = os.environ["TELEGRAM_BOT_TOKEN"] uid = os.environ["TELEGRAM_USER_ID"] bot = TeleBot(token=token)
Создаём клавиатуру
Предположим, нам нужно отправить пользователю на выбор список фруктов.
fruits = [ "banana", "coconut", "orange", "peach", "apricot", "apple", "pineapple", "avocado", "melon" ]
Создадим самую простую клавиатуру, в которой каждый элемент встанет на отдельный ряд:
kb_fruits = keyboa_maker(items=fruits, copy_text_to_callback=True)
Всё, что потребовалось – передать список в items
. Оставить пустой сallback_data
(ответную часть кнопки) мы не можем, ведь иначе не узнаем, что нажал пользователь. Поэтому добавляем текст каждой кнопки в её callback_data
, устанавливая параметр copy_text_to_callback
.
Отправим сообщение пользователю:
bot.send_message( chat_id=uid, reply_markup=kb_fruits, text="Please select one of the fruit:")
Распределяем кнопки по рядам
Разместить несколько кнопок в ряд можно любым из трёх способов.
Указать количество кнопок в ряду
Определите параметр items_in_row
, чтобы задать количество кнопок в ряду. Их число должно быть от одного до восьми. Больше кнопок нам не позволит добавить сам Telegram.
Создадим клавиатуру и отправим обновлённое сообщение:
kb_fruits = keyboa_maker(items=fruits, copy_text_to_callback=True, items_in_row=3) bot.send_message( chat_id=uid, reply_markup=kb_fruits, text="Please select one of the fruit:")
Использовать автоподбор
Если клавиатура создается динамически, мы не всегда знаем, сколько в ней окажется кнопок. Чтобы автоматически равномерно распределить их по рядам, установите параметр auto_alignment
. Если выставить True
, Keyboa попробует подобрать делитель в диапазоне от трёх до пяти, а можно задать свой диапазон в виде списка чисел от одного до восьми, например, [2, 3, 7]
.
Параметр reverse_alignment_range
определяет, будет ли Keyboa искать делитель с начала или с конца диапазона, указанного в auto_alignment
.
Распределить кнопки вручную
С помощью Keyboa можно легко вручную задать количество кнопок в каждом ряду клавиатуры с помощью вложенных списков. Для примера объединим в ряды часть наших фруктов. Принцип остался прежним: каждый элемент основного списка – отдельный ряд клавиатуры, только теперь состоящий из нескольких элементов – кнопок.
fruitscomplex = [ "banana", ["coconut", "orange"], ["peach", "apricot", "apple"], "pineapple", ["avocado", "melon"], ] kb_fruits_complex = keyboa_maker(items=fruits_complex, copy_text_to_callback=True) bot.send_message( chat_id=uid, reply_markup=kb_fruits_complex, text="Please select one of the fruit:")
Таким способом удобно создавать меню или любые другие клавиатуры со статичной, но сложной структурой.
Управляем содержимым callback
Мы упоминали, что callback_data – это строка, которую Telegram отправляет вам после нажатия кнопки пользователем. По её содержимому вы можете понять, какая именно кнопка была нажата.
Порой необходимо записать в callback_data дополнительные данные, например, идентификатор или другие свойства элемента. Чтобы задать кнопкам уникальные callback_data с требуемой информацией воспользуемся списком словарей или списком кортежей.
fruits_with_ids = [ {"banana": "101"}, {"coconut": "102"}, {"orange": "103"}, {"peach": "104"}, {"apricot": "105"}, {"apple": "106"}, {"pineapple": "107"}, {"avocado": "108"}, {"melon": "109"}, ] # or [ # ("banana", "101"), ("coconut", "102"), ("orange", "103"), # ("peach", "104"), ("apricot", "105"), ("apple", "106"), # ("pineapple", "107"), ("avocado", "108"), ("melon", "109"), ]
В этом случае ключ каждого словаря станет текстом кнопки, а его значение пойдет в callback_data. Для кортежей это будут нулевой и первый элементы соответственно. И поскольку мы явно указываем, что добавить в callback_data, параметр copy_text_to_callback
больше не нужен.
kb_fruits = keyboa_maker(items=fruits_with_ids, items_in_row=3) bot.send_message( chat_id=uid, reply_markup=kb_fruits, text="Please select one of the fruit:")
Теперь, после нажатия пользователем одной из кнопок, мы получим в ответ не название фрукта, а его id номер, записанный в callback_data.
{"text": "banana", "callback_data": "101"}, ...
Этого достаточно, если мы используем числа только для обозначения фруктовых id. А если нужно принимать разные числовые параметры, например вес или цену? Как определить, к чему относится полученное число?
С Keyboa мы можем явно указать это при создании клавиатуры. Параметр front_marker
добавляет одинаковый текст в начало callback_data каждой кнопки, а back_marker
, соответственно, в конец. Рассмотрим на примере:
kb_fruits = keyboa_maker(items=fruits_with_ids, front_marker="&fruit_id=")
Теперь callback_data кнопок будет состоять из текста формата front_marker + value, например: '&fruit_id=101'
, '&fruit_id=102'
и т. д. Такой подход позволит нам легко расшифровать строку и понять, какие получены данные и с какими значениями.
Представим, что на следующем шаге пользователю предлагают выбрать желаемый вес выбранного фрукта:
# коллбек, полученный после нажатия на кнопку, например "&fruit_id=102" selected_fruit_id = call.data available_weight = [1, 2, 5, 10, 100, ] kb_available_weight = keyboa_maker( items=available_weight, copy_text_to_callback=True, front_marker="&weight=", back_marker=selected_fruit_id)
Теперь callback_data каждой кнопки будет выглядеть так:
'&weight=1&fruit_id=102'
, '&weight=2&fruit_id=102'
, '&weight=5&fruit_id=102'
…
Здесь мы используем front_marker
и back_marker
, чтобы обернуть передаваемое значение, указав тип текущего значения и добавив в callback_data уже полученную информацию. Имейте в виду, что у Telegram есть ограничение на длину callback_data в 64 байта, поэтому, если передаваемых параметров будет много – придётся сокращать названия или использовать аббревиатуры.
Объединение нескольких клавиатур в одну
Иногда в одну клавиатуру могут входить статические и динамические элементы, например, навигационное меню и меняющийся набор элементов. Тогда возникает потребность собрать клавиатуру из нескольких частей. Специально для этого у нас есть функция keyboa_combiner()
.
У нее есть только один параметр – keyboards
, представляющий собой набор уже созданных клавиатур. Вот, как это работает:
from keyboa import keyboa_maker, keyboa_combiner controls = [["⏹️", "⏪️", "⏏️", "⏩️", "▶️"], ] tracks = list(range(1, 13)) keyboard_controls = keyboa_maker(items=controls, copy_text_to_callback=True) keyboard_tracks = keyboa_maker(items=tracks, items_in_row=4, copy_text_to_callback=True) keyboard = keyboa_combiner(keyboards=(keyboard_tracks, keyboard_controls)) bot.send_message( chat_id=uid, reply_markup=keyboard, text="Please select the track number:")
Можно объединить сразу много клавиатур – главное, чтобы общее количество кнопок было не больше ста. Это ограничение Telegram.
Итог
В этой статье мы кратко рассмотрели создание клавиатур для ботов в Telegram с помощью Keyboa. Больше информации и описание остальных параметров можно найти на странице проекта в Github.
Буду рад вашим отзывам, идеям и конструктивной критике по развитию проекта.
Сегодняшняя статья будет посвящена тому, каким образом можно создать для бота пользовательскую клавиатуру, что это даёт и как со всем этим работать. Все приводимые в статье примеры кода будут на php.
Какие вообще бывают пользовательские клавиатуры
Пользовательские клавиатуры в Telegram бывают двух видов: Inline и Reply.
Inline (встроенные)-клавиатуры просто добавляют кнопки внизу сообщения, вот так:
Эти клавиатуры привязаны к конкретному сообщению (как бы встроены в чат) и не подменяют собой традиционную клавиатуру, которая всё так же всплывает при нажатии на поле ввода текста. Главная особенность inline-клавиатур заключается в том, что нажатие на их кнопки не вызывает отправку в чат каких-либо сообщений, вместо этого генерируется некоторое «закулисное» событие: обратный вызов (callback), открытие URL или другого чата во встроенном режиме.
Интересной фишкой подобных клавиатур является возможность правки «на лету» сообщений, к которым они привязаны.
Reply-клавиатуры (клавиатуры для ответа) напротив, не привязаны к конкретному сообщению и подменяют (или дополняют) в чате с ботом традиционную клавиатуру. Эти клавиатуры вызывают отправку сообщений в чат и выглядят вот так:
Как сгенерировать и удалить клавиатуру
Для того, чтобы сгенерировать какую-либо пользовательскую клавиатуру — нужно при обращении к одному из предназначенных для отправки данных API-методов (например, при к SendMessage) задать параметр reply_markup, описывающий объект типа InlineKeyboardMarkup — для создания inline-клавиатуры, или объект типа ReplyKeyboardMarkup — для создания reply-клавиатуры. Подробнее о том, к каким методам можно прицепить reply_markup, а к каким — нет, можно прочитать в документации.
Описывающие клавиатуры объекты представляют собой многомерные массивы, содержащие наборы записей о каждой из кнопок (подпись, данные колбэка, открываемый при нажатии url и так далее).
Таким образом, для генерирования клавиатуры нам нужно сформировать в коде соответствующего типа объект, описывающий создаваемую клавиатуру (назовём этот объект, например, $keyboard), закодировать этот объект в виде json-строки и добавить эту json-строку в качестве параметра reply_markup при отправке запроса (GET или POST) telegram-боту.
Пример создания inline-клавиатуры (php)
// создаём $keyboard - объект, описывающий клавиатуру $keyboard = [ "inline_keyboard" => [ /* первый ряд кнопок - массив из наборов {подпись кнопки, даные для колбэка} */ [ /* первые две кнопки вызывают колбэк, а третья - открытие url-а */ [ "text" => "button 1", "callback_data" => "data 1" ], [ "text" => "button 2", "callback_data" => "data 2" ], [ "text" => "button 3", "url" => $any_url ] ] /* здесь мог бы быть второй массив для второго ряда кнопок и так далее */ ] ]; ... ... // далее нужно просто добавить этот параметр в запрос (GET или POST) в качестве параметра reply_markup // например, при использовании в GET-запросе: $keyboard_json = json_encode($keyboard); // перекодируем в json // формируем url для get-запроса и добавляем к этому url-у параметр reply_markup с описанием нашей клавиатуры $url=$telegram_api_url.$bot_token.'/sendMessage?chat_id='.$chat_id.'&text='.urlencode($text_message).'&parse_mode=HTML'.'&reply_markup='.$keyboard_json;
// осталось только выполнить GET-запрос, например с помощью file_get_contents (ну или curl :)) file_get_contents($url); // отправляем get-запрос на сформированный url
[свернуть] Пример создания reply-клавиатуры (php)
// создаём $keyboard - объект, описывающий клавиатуру $keyboard = [ "keyboard" => [ /* первый ряд кнопок - массив из наборов {подпись кнопки} */ [ /* перва кнопка отправляет команду /any_command в виде текстового сообщения, вторая - данные геолокации */ [ "text" => "/any_command" ], [ "text" => "my location", "request_location" => true ] ] /* здесь мог бы быть второй массив для второго ряда кнопок и так далее */ ] ]; ... ... // далее нужно просто добавить этот параметр в запрос (GET или POST) в качестве параметра reply_markup // например, при использовании в GET-запросе: $keyboard_json = json_encode($keyboard); // перекодируем в json // формируем url для get-запроса и добавляем к этому url-у параметр reply_markup с описанием нашей клавиатуры $url=$telegram_api_url.$bot_token.'/sendMessage?chat_id='.$chat_id.'&text='.urlencode($text_message).'&parse_mode=HTML'.'&reply_markup='.$keyboard_json;
// осталось только выполнить GET-запрос, например с помощью file_get_contents (ну или curl :)) file_get_contents($url); // отправляем get-запрос на сформированный url
[свернуть]
Поскольку inline-клавиатуры привязаны к конкретным сообщениям, то с каждым новым сообщением нужно генерировать новую inline-клавиатуру. С reply-клавиатурами ничего подобного делать не нужно. Будучи однажды сгенерированной, такая клавиатура будет заменять дефолтную до тех пор, пока её не удалят или не заменят другой reply-клавиатурой.
Для удаления reply-клавиатуры нужно в параметре reply_markup послать объект, типа ReplyKeyboardRemove.
Пример удаления reply-клавиатуры (php)
// создаём $remove_keyboard - объект, описывающий удаление reply-клавиатуры $remove_keyboard = [ "remove_keyboard" => true ]; ... ... // далее нужно просто добавить этот объект в запрос (GET или POST) в качестве параметра reply_markup // например, при использовании в GET-запросе: $remove_keyboard_json = json_encode($remove_keyboard); // перекодируем в json // формируем url для get-запроса и добавляем к этому url-у параметр reply_markup с описывающим удаление клавиатуры объектом $url=$telegram_api_url.$bot_token.'/sendMessage?chat_id='.$chat_id.'&text='.urlencode($text_message).'&parse_mode=HTML'.'&reply_markup='.$remove_keyboard_json;
// осталось только выполнить GET-запрос, например с помощью file_get_contents (ну или curl :)) file_get_contents($url); // отправляем get-запрос на сформированный url
[свернуть]
Правка сообщений «на лету»
Одна из прикольных фишек inline-клавиатур — возможность «на лету» править сообщения, к которым эти клавиатуры привязаны. Как я уже писал выше, нажатие на кнопки inline-клавиатур не вызывает отправку сообщений в чат, но то же время, получив callback мы всегда можем понять, к какому сообщению была привязана нажатая кнопка. Остаётся только исправить это сообщение при помощи одного из методов Updating messages.
Как получить callback? Для этого просто нужно запросить, получить и обработать очередной update, если бот работает через longpolling, или выполнить только два последних пункта, если бот работает через webhook (в этом случае, как вы помните, ничего запрашивать не нужно, так как телега сама знает по какому адресу отправлять апдейты). Данные колбэка будут содержаться в поле «callback_query» полученного апдейта.
Пример апдейта, полученного после нажатия на одну из кнопок inline-клавиатуры
"{"ok":true,"result": [ { "update_id":xxxxxxxxxx, "callback_query": { "id":"xxxxxxxxxxx", "from": { "id":xxxxxxxxx, "first_name":"xxxxxxx", "last_name":"xxxxxxx", "username":"xxxxxxxxx", "language_code":"ru" }, "message": { "message_id":xxxx, "from": { "id":xxxxxxxxx, "first_name":"xxxxxxxx", "username":"xxxxxxx" }, "chat": { "id":xxxxxxxx, "first_name":"xxxxxxx", "last_name":"xxxxxxxx", "username":"xxxxxxx", "type":"private" }, "date":1499854111, "text":"test" }, "chat_instance":"xxxxxxxxxxxx", "data":"1" } } ] }"
[свернуть] Пример редактирования сообщения налету
// если мы определили, что нам прилетел именно колбэк (по наличию поля "callback_query"), то: // выделяем из этого колбэка необходимые для редактирования сообщения данные $chat_id = $input_array['callback_query']['message']['chat']['id']; // идентификатор чата $message_id = $input_array['callback_query']['message']['message_id']; //идентификатор сообщения $data = $input_array['callback_query']['data']; // данные, которые были отправлены при нажатии на кнопку ... ... // далее нужно просто выполнить запрос (GET или POST), обратившись, например, к методу editMessageText $url=$telegram_api_url.$bot_token.'/editMessageText?chat_id='.$chat_id.'&message_id='.$message_id.'&text='.urlencode($edited_message).'&reply_markup='.$keyboard_json; // $edited_message - отредактированный текст сообщения // $keyboard_json - клавиатура, которая будет содержаться в отредактированном сообщении file_get_contents($url); // отправляем get-запрос на сформированный url
[свернуть]
Вот и всё. Как видите, всё достаточно просто и понятно. В telegram вообще всегда всё просто, понятно и подробно описано. Вообще не понимаю, зачем нужны всякие библиотеки и фреймворки. Все они выглядят гораздо страшнее и монструознее, чем исходные описания из API. Это при том, что все эти фреймворки добавляют коду веса, ошибок и нестабильности. Уф, надо вступить в какое-нибудь движение за чистоту кода. Ну ладно, это я уже отвлёкся. Пока!
В этой главе мы познакомимся с такой замечательной фичей Telegram-ботов, как кнопки. Прежде всего, чтобы избежать путаницы, определимся с названиями. То, что цепляется к низу экрана вашего устройства, будем называть обычными кнопками, а то, что цепляется непосредственно к сообщениям, назовём инлайн-кнопками. Ещё раз картинкой:
Обычные кнопки¶
Кнопки как шаблоны¶
Этот вид кнопок появился вместе с Bot API в далёком 2015 году и представляет собой не что иное, как шаблоны сообщений (за исключением нескольких особых случаев, но о них позже). Принцип простой: что написано на кнопке, то и будет отправлено в текущий чат. Соответственно, чтобы обработать нажатие такой кнопки, бот должен распознавать входящие текстовые сообщения.
Напишем хэндлер, который будет при нажатии на команду /start
отправлять сообщение с двумя кнопками:
# from aiogram import types @dp.message_handler(commands="start") async def cmd_start(message: types.Message): keyboard = types.ReplyKeyboardMarkup() button_1 = types.KeyboardButton(text="С пюрешкой") keyboard.add(button_1) button_2 = "Без пюрешки" keyboard.add(button_2) await message.answer("Как подавать котлеты?", reply_markup=keyboard)
Обратите внимание, что т.к. обычные кнопки суть шаблоны сообщений, то их можно создавать не только как объекты KeyboardButton
, но и как обычные строки. Что ж, запустим бота и обалдеем от громадных кнопок:
Как-то некрасиво. Во-первых, хочется сделать кнопки поменьше, а во-вторых, расположить их горизонтально. Почему вообще они такие большие? Дело в том, что по умолчанию «кнопочная» клавиатура должна занимать на смартфонах столько же места, сколько и обычная буквенная. Для уменьшения кнопок к объекту клавиатуры надо указать дополнительный параметр resize_keyboard=True
. Но как заменить вертикальные кнопки на горизонтальные? С точки зрения Bot API, клавиатура — это массив массивов кнопок, а если говорить проще, массив строк. Метод add()
при каждом вызове создаёт новую строку (ряд) и принимает произвольное число аргументов по количеству желаемых кнопок в строке. Перепишем наш код, чтобы было красиво:
@dp.message_handler(commands="start") async def cmd_start(message: types.Message): keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True) buttons = ["С пюрешкой", "Без пюрешки"] keyboard.add(*buttons) await message.answer("Как подавать котлеты?", reply_markup=keyboard)
Обратите внимание на конструкцию *buttons
. Здесь вам не C++ и звёздочка используется для распаковки списка. Подробнее об операторах *
и **
можно прочитать здесь.
Смотрим — действительно красиво:
Осталось научить бота реагировать на нажатие таких кнопок. Как уже было сказано выше, необходимо делать проверку на полное совпадение текста. Сделаем это двумя способами: через специальный фильтр Text
и обычной лямбдой:
# from aiogram.dispatcher.filters import Text @dp.message_handler(Text(equals="С пюрешкой")) async def with_puree(message: types.Message): await message.reply("Отличный выбор!") @dp.message_handler(lambda message: message.text == "Без пюрешки") async def without_puree(message: types.Message): await message.reply("Так невкусно!")
Чтобы удалить кнопки, необходимо отправить новое сообщение со специальной «удаляющей» клавиатурой типа ReplyKeyboardRemove
. Например: await message.reply("Отличный выбор!", reply_markup=types.ReplyKeyboardRemove())
У объекта обычной клавиатуры есть ещё две полезных опции: one_time_keyboard
для скрытия кнопок после нажатия и selective
для показа клавиатуры лишь некоторым участникам группы. Их использование остаётся для самостоятельного изучения.
Помимо стандартных опций, описанных выше, aiogram немного расширяет функциональность клавиатур параметром row_width
. При его использовании, фреймворк автоматически разобьёт массив кнопок на строки по N элементов в каждой, где N — значение row_width
, например, row_width=2
. Попробуйте!
Специальные обычные кнопки¶
По состоянию на конец ужасного 2020 года в Telegram существует три специальных вида обычных кнопок, не являющихся шаблонами: для отправки текущей геолокации, для отправки своего номера телефона и ярлык для создания опроса/викторины. Для первых двух типов достаточно установить булевый флаг, а для опросов и викторин нужно передать специальный тип KeyboardButtonPollType
и, по желанию, указать тип создаваемого объекта.
Впрочем, проще один раз увидеть код:
@dp.message_handler(commands="special_buttons") async def cmd_special_buttons(message: types.Message): keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True) keyboard.add(types.KeyboardButton(text="Запросить геолокацию", request_location=True)) keyboard.add(types.KeyboardButton(text="Запросить контакт", request_contact=True)) keyboard.add(types.KeyboardButton(text="Создать викторину", request_poll=types.KeyboardButtonPollType(type=types.PollType.QUIZ))) await message.answer("Выберите действие:", reply_markup=keyboard)
Инлайн-кнопки¶
URL-кнопки и колбэки¶
В отличие от обычных кнопок, инлайновые цепляются не к низу экрана, а к сообщению, с которым были отправлены. В этой главе мы рассмотрим два типа таких кнопок: URL и Callback. Ещё один — Switch — будет рассмотрен в главе про инлайн-режим.
Login- и Pay-кнопки в книге рассматриваться не будут вообще. Если у кого-то есть желание помочь хотя бы с рабочим кодом для авторизации или оплаты, пожалуйста, создайте Pull Request на GitHub. Спасибо!
Самые простые инлайн-кнопки относятся к типу URL, т.е. «ссылка». Поддерживаются только протоколы HTTP(S) и tg://
@dp.message_handler(commands="inline_url") async def cmd_inline_url(message: types.Message): buttons = [ types.InlineKeyboardButton(text="GitHub", url="https://github.com"), types.InlineKeyboardButton(text="Оф. канал Telegram", url="tg://resolve?domain=telegram") ] keyboard = types.InlineKeyboardMarkup(row_width=1) keyboard.add(*buttons) await message.answer("Кнопки-ссылки", reply_markup=keyboard)
А если хотите обе кнопки в ряд, то уберите row_width=1
(тогда будет использоваться значение по умолчанию 3).
С URL-кнопками больше обсуждать, по сути, нечего, поэтому перейдём к гвоздю сегодняшней программы — Callback-кнопкам. Это очень мощная штука, которую вы можете встретить практически везде. Кнопки-реакции у постов (лайки), меню у @BotFather и т.д. Суть в чём: у колбэк-кнопок есть специальное значение (data), по которому ваше приложение опознаёт, что нажато и что надо сделать. И выбор правильного data очень важен! Стоит также отметить, что, в отличие от обычных кнопок, нажатие на колбэк-кнопку позволяет сделать практически что угодно, от заказа пиццы до перезагрузки сервера.
Напишем хэндлер, который по команде /random
будет отправлять сообщение с колбэк-кнопкой:
@dp.message_handler(commands="random") async def cmd_random(message: types.Message): keyboard = types.InlineKeyboardMarkup() keyboard.add(types.InlineKeyboardButton(text="Нажми меня", callback_data="random_value")) await message.answer("Нажмите на кнопку, чтобы бот отправил число от 1 до 10", reply_markup=keyboard)
Но как же обработать нажатие? Если раньше мы использовали message_handler
для обработки входящих сообщений, то теперь будем использовать callback_query_handler
для обработки колбэков. Ориентироваться будем на «значение» кнопки, т.е. на её data:
@dp.callback_query_handler(text="random_value") async def send_random_value(call: types.CallbackQuery): await call.message.answer(str(randint(1, 10)))
Важно
Несмотря на то, что параметр кнопки callback_data
, а значение data
лежит в одноимённом поле data
объекта CallbackQuery, собственный фильтр aiogram называется text
.
Ой, а что это за часики? Оказывается, сервер Telegram ждёт от нас подтверждения о доставке колбэка, иначе в течение 30 секунд будет показывать специальную иконку. Чтобы скрыть часики, нужно вызвать метод answer()
у колбэка (или использовать метод API answer_callback_query()
). В общем случае, в метод answer()
можно ничего не передавать, но можно вызвать специальное окошко (всплывающее сверху или поверх экрана):
@dp.callback_query_handler(text="random_value") async def send_random_value(call: types.CallbackQuery): await call.message.answer(str(randint(1, 10))) await call.answer(text="Спасибо, что воспользовались ботом!", show_alert=True) # или просто await call.answer()
Обратите внимание
В функции send_random_value
мы вызывали метод answer()
не у message
, а у call.message
. Это связано с тем, что колбэк-хэндлеры работают не с сообщениями (тип Message), а с колбэками (тип CallbackQuery), у которого другие поля, и само сообщение — всего лишь его часть. Учтите также, что message
— это сообщение, к которому была прицеплена кнопка (т.е. отправитель такого сообщения — сам бот). Если хотите узнать, кто нажал на кнопку, смотрите поле from
(в вашем коде это будет call.from_user
, т.к. слово from
зарезервировано в Python)
Когда вызывать answer()
?
В общем случае, главное — просто не забыть сообщить Telegram о получении колбэк-запроса, но я рекомендую ставить вызов answer()
в самом конце, и вот почему: если вдруг в процессе обработки колбэка случится какая-то ошибка и бот нарвётся на необработанное исключение, пользователь увидит неубирающиеся полминуты часики и поймёт, что что-то не так. В противном случае, часики исчезнут, а пользователь останется в неведении, выполнился его запрос успешно или нет.
Перейдём к примеру посложнее. Пусть пользователю предлагается сообщение с числом 0, а внизу три кнопки: +1, -1 и Подтвердить. Первыми двумя он может редактировать число, а последняя удаляет всю клавиатуру, фиксируя изменения. Хранить значения будем в памяти в словаре (про конечные автоматы поговорим как-нибудь в другой раз).
# Здесь хранятся пользовательские данные. # Т.к. это словарь в памяти, то при перезапуске он очистится user_data = {} def get_keyboard(): # Генерация клавиатуры. buttons = [ types.InlineKeyboardButton(text="-1", callback_data="num_decr"), types.InlineKeyboardButton(text="+1", callback_data="num_incr"), types.InlineKeyboardButton(text="Подтвердить", callback_data="num_finish") ] # Благодаря row_width=2, в первом ряду будет две кнопки, а оставшаяся одна # уйдёт на следующую строку keyboard = types.InlineKeyboardMarkup(row_width=2) keyboard.add(*buttons) return keyboard async def update_num_text(message: types.Message, new_value: int): # Общая функция для обновления текста с отправкой той же клавиатуры await message.edit_text(f"Укажите число: {new_value}", reply_markup=get_keyboard()) @dp.message_handler(commands="numbers") async def cmd_numbers(message: types.Message): user_data[message.from_user.id] = 0 await message.answer("Укажите число: 0", reply_markup=get_keyboard()) @dp.callback_query_handler(Text(startswith="num_")) async def callbacks_num(call: types.CallbackQuery): # Получаем текущее значение для пользователя, либо считаем его равным 0 user_value = user_data.get(call.from_user.id, 0) # Парсим строку и извлекаем действие, например `num_incr` -> `incr` action = call.data.split("_")[1] if action == "incr": user_data[call.from_user.id] = user_value+1 await update_num_text(call.message, user_value+1) elif action == "decr": user_data[call.from_user.id] = user_value-1 await update_num_text(call.message, user_value-1) elif action == "finish": # Если бы мы не меняли сообщение, то можно было бы просто удалить клавиатуру # вызовом await call.message.delete_reply_markup(). # Но т.к. мы редактируем сообщение и не отправляем новую клавиатуру, # то она будет удалена и так. await call.message.edit_text(f"Итого: {user_value}") # Не забываем отчитаться о получении колбэка await call.answer()
И, казалось бы, всё работает:
Но теперь представим, что ушлый пользователь сделал следующее: вызвал команду /numbers
(значение 0), увеличил значение до 1, снова вызвал /numbers
(значение сбросилось до 0) и отредактировал нажал кнопку “+1” на первом сообщении. Что произойдёт? Бот по-честному отправит запрос на редактирование текста со значением 1, но т.к. на том сообщении уже стоит цифра 1, то Bot API вернёт ошибку, что старый и новый тексты совпадают, а бот словит исключение: aiogram.utils.exceptions.MessageNotModified: Message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message
С этой ошибкой вы, скорее всего, будете поначалу часто сталкиваться, пытаясь редактировать сообщения. Но, в действительности, решается проблема очень просто: мы проигнорируем исключение MessageNotModified
. Из первой главы вы уже знаете о такой прекрасной штуке, как errors_handler
, но в этот раз мы поступим чуть иначе и перепишем функцию update_num_text()
следующим образом:
# from aiogram.utils.exceptions import MessageNotModified # from contextlib import suppress async def update_num_text(message: types.Message, new_value: int): with suppress(MessageNotModified): await message.edit_text(f"Укажите число: {new_value}", reply_markup=get_keyboard())
Если теперь вы попробуете повторить пример выше, то указанное исключение в этом блоке кода бот просто-напросто проигнорирует.
Фабрика колбэков¶
В aiogram существует т.н. фабрика колбэков. Вы создаёте объект CallbackData
, указываете ему префикс и произвольное количество доп. аргументов, которые в дальнейшем указываете при создании колбэка для кнопки. Например, рассмотрим следующий объект:
# from aiogram.utils.callback_data import CallbackData cb= CallbackData("post", "id", "action")
Тогда при создании кнопки вам надо указать её параметры так:
button = types.InlineKeyboardButton( text="Лайкнуть", callback_data=cb.new(id=5, action="like") )
В примере выше в кнопку запишется callback_data
, равный post:5:like
, а хэндлер на префикс post
будет выглядеть так:
@dp.callback_query_handler(cb.filter()) async def callbacks(call: types.CallbackQuery, callback_data: dict): post_id = callback_data["id"] action = callback_data["action"]
В предыдущем примере с числами мы грамотно выбрали callback_data
, поэтому смогли легко запихнуть все обработчики в один хэндлер. Но можно логически разнести обработку инкремента и декремента от обработки нажатия на кнопку “Подтвердить”. Для этого в фильтре фабрики можно указать желаемые значения какого-либо параметра. Давайте перепишем наш пример с использоваанием фабрики:
# fabnum - префикс, action - название аргумента, которым будем передавать значение callback_numbers = CallbackData("fabnum", "action") def get_keyboard_fab(): buttons = [ types.InlineKeyboardButton(text="-1", callback_data=callback_numbers.new(action="decr")), types.InlineKeyboardButton(text="+1", callback_data=callback_numbers.new(action="incr")), types.InlineKeyboardButton(text="Подтвердить", callback_data=callback_numbers.new(action="finish")) ] keyboard = types.InlineKeyboardMarkup(row_width=2) keyboard.add(*buttons) return keyboard async def update_num_text_fab(message: types.Message, new_value: int): with suppress(MessageNotModified): await message.edit_text(f"Укажите число: {new_value}", reply_markup=get_keyboard_fab()) @dp.message_handler(commands="numbers_fab") async def cmd_numbers(message: types.Message): user_data[message.from_user.id] = 0 await message.answer("Укажите число: 0", reply_markup=get_keyboard_fab()) @dp.callback_query_handler(callback_numbers.filter(action=["incr", "decr"])) async def callbacks_num_change_fab(call: types.CallbackQuery, callback_data: dict): user_value = user_data.get(call.from_user.id, 0) action = callback_data["action"] if action == "incr": user_data[call.from_user.id] = user_value + 1 await update_num_text_fab(call.message, user_value + 1) elif action == "decr": user_data[call.from_user.id] = user_value - 1 await update_num_text_fab(call.message, user_value - 1) await call.answer() @dp.callback_query_handler(callback_numbers.filter(action=["finish"])) async def callbacks_num_finish_fab(call: types.CallbackQuery): user_value = user_data.get(call.from_user.id, 0) await call.message.edit_text(f"Итого: {user_value}") await call.answer()
На этом глава про кнопки окончена, но про некоторые других их виды мы поговорим в следующих главах.
15 мин
Какие задачи стоят перед ботом?
Сразу заметим, что слив трафика на беттинг — лишь частный пример. Технически с помощью бота можно будет лить трафик на любую вертикаль — лишь бы хватило фантазии, как все это интегрировать в бота. Но именно беттинг-сфера позволяет в полной мере раскрыть возможности бота, поэтому выбор пал на нее. В чем суть:
- Есть сайт букмекерской конторы.
- В нем есть возможность поставить ставку на разные виды спорта.
- В каждом виде спорта есть свои лиги, страны и т. д.
- Всю эту информацию мы соберем и будем выводить в виде каталога.
- Подписчики бота смогут сами выбрать, на какое событие хотят сделать ставку, и уже потом получат ссылку на лендинг.
Скрин интересующей нас навигации сайта БК на примере MostBet
Скрин навигации бота Либо альтернатива:
- У вас есть несколько схем от капперов.
- Вы создаете меню, разбивая схемы по разным категориям.
- Пользователь заходит и видит не очередной пост с одним прогнозом, а полноценный каталог.
- Благодаря богатому выбору он с большим шансом будет вовлечен.
По такому же принципу можно сделать и своего бота — только дать пользователю больше выбора с помощью меню. К слову, эта же тема актуальна и в гембле при сливе через схемы. Впрочем, все это лишь примеры возможного использования бота, и его потенциал ими не ограничивается. По сути, с его помощью можно лить траф не только на беттинг. Но так как специфика беттинга подразумевает решение более сложных задач, чем простое создание меню, то в качестве примера будет использована именно эта вертикаль. Также заметим, что ввиду объемности материала, статья будет состоять из трех частей. В этой части будут реализованы задачи, перечисленные выше. Во второй и третьей части мы будет делать бота более адаптивным и универсальным, чтобы можно было лить на несколько БК сразу.
Создание простого Telegram-бота с кнопками
Первое, что нужно сделать, — настроить наш сервер, создать бота в самом Telegram и получить токен. Для этого: 1. Регистрируемся и авторизируемся на pythonanywhere. 2. Запускаем Bash-консоль, ждем пока сервер будет настроен.
Главное меню бесплатного хостинга pythonanywhere. 3. Инсталлируем pyTelegramBotAPI — модуль, с помощью которого наш бот будет взаимодействовать с API Telegram. Это делается вводом следующей команды: pip3.9 install –user pyTelegramBotAPI
Консоль сервера *3.9 — используемая и сервером pythonanywhere, и Telegram версия Python. Используйте свежую (данная актуальна лишь на момент написания материала) версию, иначе будет ошибка. 4. Запрашиваем у Telegram токен для нашего бота. Для этого просто пишем официальному боту для управления ботами @BotFather и следуем его указаниям. Там ничего сложного, нужно лишь указать название бота в формате ИМЯ_bot. Приставка _bot обязательна. И да, название не должно нарушать правила мессенджера. Когда бот @BotFather выдаст токен — сохраняем его отдельно.
Получение токена через официальный бот Телеги @BotFather 5. Пишем код для логики бота в блокнот или любой другой текстовый редактор: import telebot #подгружаем модуль телебот from telebot import types # для указания типов кнопок bot = telebot.TeleBot(‘ТОКЕН СЮДА’) #задаем токен @bot.message_handler(commands=[‘start’]) #задаем реакцию на команду /start def start(message): markup = types.ReplyKeyboardMarkup(resize_keyboard=True) btn1 = types.KeyboardButton(“👋 Поздороваться”) #создаем кнопку Поздороваться btn2 = types.KeyboardButton(“Меню”) #создаем кнопку Меню markup.add(btn1, btn2) bot.send_message(message.chat.id, text=”Привет, {0.first_name}! Я тестовый бот для читателей trafficcardinal.com”.format(message.from_user), reply_markup=markup) #создаем текст для вывода при активации команды /start @bot.message_handler(content_types=[‘text’]) #задаем реакцию на новые сообщения #(нажатия кнопок — тоже сообщения) def func(message): if(message.text == “👋 Поздороваться”): #задаем реакцию на кнопку Поздороваться bot.send_message(message.chat.id, text=”Привет, {0.first_name}! Я тестовый бот для читателей trafficcardinal.com”.format(message.from_user)) #создаем текст для вывода при активации кнопки Поздороваться elif(message.text == “Меню”): #задаем реакцию на кнопку Меню markup = types.ReplyKeyboardMarkup(resize_keyboard=True) #обновляем тип используемых кнопок btna1 = types.KeyboardButton(“А1”) #создаем кнопку А1 btna2 = types.KeyboardButton(“А2”) #создаем кнопку А2 back = types.KeyboardButton(“Вернуться в главное меню”) #создаем кнопку Вернуться в главное меню markup.add(btna1, btna2, back) #обновляем кнопки у пользователя bot.send_message(message.chat.id, text=”Меню”, reply_markup=markup) #создаем текст для вывода при активации кнопки Меню elif(message.text == “А1”): #задаем реакцию на кнопку А1 bot.send_message(message.chat.id, “А1”) #создаем текст для вывода при активации кнопки А1 elif message.text == “А2”: #задаем реакцию на кнопку А2 bot.send_message(message.chat.id, text=”А2″) #создаем текст для вывода при активации кнопки А2 elif (message.text == “Вернуться в главное меню”): #задаем реакцию на кнопку Вернуться в главное меню markup = types.ReplyKeyboardMarkup(resize_keyboard=True) #обновляем тип используемых кнопок button1 = types.KeyboardButton(“👋 Поздороваться”) #создаем кнопку Поздороваться button2 = types.KeyboardButton(“Меню”) #создаем кнопку Меню markup.add(button1, button2) #обновляем кнопки у пользователя bot.send_message(message.chat.id, text=”Вы вернулись в главное меню”, reply_markup=markup) #создаем текст для вывода при активации кнопки Вернуться в главное меню else: #задаем реакцию на ВСЕ ОСТАЛЬНЫЕ ситуации bot.send_message(message.chat.id, text=”На такую комманду я не запрограммировал..”) #создаем текст реакции для всех остальных ситуаций bot.polling(none_stop=True) #запрещаем скрипту «спать» — нужно чтобы бот был всегда был онлайн Находим фрагмент bot = telebot.TeleBot(‘ТОКЕН СЮДА’) #задаем токен — вставляем токен из ответа @BotFather (см. предыдущий раздел). Сохраняем файл как «bot.py». СКАЧАТЬ ИСХОДНЫЙ КОД 6. Загружаем файл на сервер.
Добавление python-скрипта на сервер 7. Запускаем бота, нажав Run.
Инициация обработки скрипта 8. Проверяем результат. Бот должен создать указанные кнопки и реагировать на их нажатия.
Общий принцип работы бота с кнопками Получилось? Неплохо, поздравляем! Но для полноценного бота под слив трафика двух кнопок маловато, не так ли? Чтобы создать бота под свои нужды, внимательно присмотритесь к коду выше. Символом # отмечены фрагменты кода, которые не будут обрабатываться сервером. В них указаны пояснения ко всему, что происходит в коде. Для расширения функциональности бота нас интересуют следующие фрагменты:
- #задаем реакцию на кнопку;
- #обновляем тип используемых кнопок;
- #создаем кнопку (обычно несколько раз подряд);
- #обновляем кнопки у пользователя;
- #создаем текст для вывода при активации кнопки.
По сути, перед нами упрощенная интерпретация логики работы основной части бота. Чтобы расширить функционал бота и сделать его пригодным для слива на беттинг, достаточно добавить больше кнопок, исходя из ваших нужд. При этом нужно выполнить каждый из указанных шагов выше. Давайте попробуем это сделать и заодно адаптировать бота по беттинг.
Создаем бота с кнопкой для Telegram под слив на беттинг
Итак, допустим, вы ходим сделать так, чтобы пользователь, нажав на меню, получал список из нескольких видов спорта, в каждом из которых будет своя лига, а в каждой лиге — актуальные офферы. Для этого нам нужно немного адаптировать код выше, а именно:
- Добавить больше кнопок в раздел Меню, исходя их видов спорта.
elif(message.text == “Меню”): #ничего не меняли markup = types.ReplyKeyboardMarkup(resize_keyboard=True) #ничего не меняли btna1 = types.KeyboardButton(“Футбол”) #заменили текст А1 на Футбол btna2 = types.KeyboardButton(“Баскетбол”) #заменили текст А2 на баскетбол btna3 = types.KeyboardButton(“ММА”) #создали новую кнопку ММА btna4 = types.KeyboardButton(“Формула-1”) #создали новую кнопку Формула-1 back = types.KeyboardButton(“Вернуться в главное меню”) #ничего не меняли markup.add(btna1, btna2, btna3, bnta4, back) #добавили кнопки btna3 и btna4 bot.send_message(message.chat.id, text=”Меню”, reply_markup=markup) #ничего не меняли
- Добавить реакцию на каждую кнопку раздела с меню, с учетом того, что у каждого элемента меню будет свое подменю.
elif(message.text == “Футбол”): #задаем реакцию на кнопку Футбол (бывшая кнопка А1) markup = types.ReplyKeyboardMarkup(resize_keyboard=True) #обновляем тип используемых кнопок btna1b1 = types.KeyboardButton(“Лига A1B1”) #cоздаем кнопку Лига A1B1 btna1b2 = types.KeyboardButton(“Лига A1B2”) #cоздаем кнопку Лига A1B2 back = types.KeyboardButton(“Вернуться в главное меню”) #cоздаем кнопку Вернуться в главное меню markup.add(btna1b1, btna1b2, back) #обновляем кнопки у пользователя bot.send_message(message.chat.id, text=”Футбол”, reply_markup=markup) #создаем текст для вывода при активации кнопки Футбол По такому же принципу задаем реакцию для нажатия на каждую из кнопок раздела меню — в зависимости от того, сколько нам их нужно.
Создание более сложного меню СКАЧАТЬ ИСХОДНЫЙ КОД
- Создать реакцию на кнопки подменю для каждой из категорий меню. По такому же принципу.
elif(message.text == “Лига A1B1”): #задаем реакцию на кнопку Лига A1B1 markup = types.ReplyKeyboardMarkup(resize_keyboard=True) #обновляем тип используемых кнопок btna1b1c1 = types.KeyboardButton(“Оффер A1B1С1”) #cоздаем кнопку Лига A1B1C1 btna1b2c2 = types.KeyboardButton(“Оффер A1B1С2”) #cоздаем кнопку Лига A1B1C2 back = types.KeyboardButton(“Вернуться в главное меню”) #cоздаем кнопку Вернуться в главное меню markup.add(btna1b1c1, btna1b1c2, back) #обновляем кнопки у пользователя bot.send_message(message.chat.id, text=”Лига A1B1″, reply_markup=markup) #создаем текст для вывода при активации кнопки Лига A1B1 СКАЧАТЬ ИСХОДНЫЙ КОД По такому же принципу задаем реакцию для нажатия на каждую из кнопок разделов подкатегорий меню — в зависимости от того, сколько нам их нужно.
- Создать реакцию на каждый оффер для каждого раздела из подкатегорий меню.
elif(message.text == “Оффер A1B1C1”): #задаем реакцию на кнопку Оффер A1B1C1 markup = types.InlineKeyboardMarkup() #обновляем тип используемых кнопок ОБРАТИТЕ ВНИМАНИЕ — ЗДЕСЬ УЖЕ ДРУГОЙ ТИП btn_my_site= types.InlineKeyboardButton(text=’Наш сайт’, url=’https://trafficcardinal.com’) #cоздаем кнопку со ссылкой markup.add(btn_my_site) #обновляем кнопки у пользователя bot.send_message(message.chat.id, “Нажми на кнопку и перейди на наш сайт.”, reply_markup = markup) #создаем текст для вывода при активации кнопки Оффер A1B1C1
Создание меню с несколькими уровнями вложений и инлайн-кнопкой для перехода по ссылке. Именно здесь можно уже не просто писать текст, а интегрировать полноценные креативы.
- При необходимости сделать большую глубину вложений — просто повторяйте шаги 2-3.
- Не забываем добавлять красоты, например, подойдут смайлики.
- Сохраняем изменения (совет — делайте это в отдельный файл).
- Загружаем новую логику работы бота на сайт.
СКАЧАТЬ ИСХОДНЫЙ КОД
Готовый Telegram-бот с меню
В конце статьи вы сможете скачать готовый бот с меню. К сожалению, код бота получился слишком длинным для того, чтобы размещать его в «теле» статьи. Правильно ли писать настолько длинный код под столь простую задачу с точки зрения алгоритмизации? Нет. И в следующей статье мы будет это исправлять, делая код более оптимальным и универсальным. Но мы с вами не программисты. Нам хватает чем еще заняться при сливе трафика, помимо изучения подобных тем. Поэтому наша задача — научиться создавать ботов с минимальными временными затратами, чтобы можно было «на коленке» собрать своего бота и начать заливать траф. А для этой цели код вполне сгодится.
Демонстрация оформления бота под беттинг Итак, что же может бот:
- Здороваться по имени пользователя при инициации команды /start.
- Перемещаться по навигационному меню на 4 вложения.
- Перемещаться «Вперед» и «Назад» между страницами меню.
- Возвращаться в главное меню с любого «этажа» вложенных подменю.
- Выводить инлайн-кнопку со ссылкой.
Демонстрация оформления бота под беттинг При ручном управлении этого более чем достаточно. Для того, чтобы привести бота в соответствующий вид, пригодный для выполнения задачи по сливу, необходимо лишь:
- Изменить текст в кавычках, описывающий кнопки, на нужный вам. Например, «А1» на «Футбол».
- Вместо ссылки на наш сайт вставить ссылки на продвигаемые вами офферы. Либо же на смартлинк.
- Вместо надписи «Нажми на кнопку и перейди на наш сайт» добавить ваши креативы, исходя из специфики офферов.
*Совет: Обращайте внимание на консоль сервера. Если вы случайно накосячите с кодом — она укажет вам, где именно ошибка. Даже лишний пробел может привести к тому, что бот не будет работать. СКАЧАТЬ ИСХОДНЫЙ КОД
Подводя итоги
Как видите, создать бота с кнопками не так уж и сложно. Пусть даже и приходится долго ковыряться с кодом. Не слишком удобно, не так ли? Мы такого же мнения. Именно поэтому статья и разбита на три части. Во второй части мы научим вас делать бота, код которого не придется ковырять, — достаточно будет лишь создать структуру меню в txt-файле. До скорой встречи, друзья!
ли со статьей или есть что добавить?