Простой Telegram-бот на Python за 30 минут

В данной статье мы напишем telegram bot на python, который сможет отвечать на наши сообщения, взаимодействовать с нами, предлагая варианты ответов в виде кнопок и обрабатывать выбранный нами результат, выполняя команды на сервере. Взаимодействовать с Telegram Bot Api мы будем с помощью библиотеки pyTelegramBotAPI (telebot) написанной на Python.

Создание бота

Для регистрации нового бота необходимо обратиться к боту BotFather. Для этого в строке поиска наберите BotFather и в показанных результатах найдите интересующего нас бота:

<amp></amp>

Обратите внимание на его имя, изображение и знак в виде галочки, говорящий о том, что это действительно отец всех ботов.

Выберите его и в диалоговом окне напишите команду /start и бот в ответном сообщение пришлет список всех доступных команд:

<amp></amp>

Нас интересует создание нового бота, поэтому выбираем команду /newbot. Команду можно как напечатать самостоятельно, так и выбрать мышью в сообщении и она автоматически отправится:

<amp></amp>

Первым шагом нам предлагают дать имя новому боту, оно может быть произвольным. Мы назовем его PocketAdmin:

<amp></amp>

Теперь требуется указать идентификатор бота (username), он должен заканчиваться на _bot и быть уникальным в системе. Мы укажем PocketAdminTech_bot:

<amp></amp>

На этом создание бота завершено. В последнем сообщении нам пришла ссылка на нашего нового ботаt.me/PocketAdminTech_bot и токен (закрашен), необходимый для взаимодействия с API.

Обязательно сохраните токен и храните его в тайне!

Установка Python и библиотеки pyTelegramBotAPI

Скачать Python можно с официального сайта (как установить пакет на Centos 8 можно ознакомиться в данной заметке) и мы не будем заострять внимание на данном вопросе.

Чтобы установить пакет pyTelegramBotAPI воспользуемся pip:

pip install pytelegrambotapi

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

Пишем Telegram Bot на Python

Так как наш бот создается в ознакомительных целях и не будет содержать много кода, то писать я его буду сразу на сервере с установленной Centos 8 используя обычный редактор nano. Создадим файл bot.py, открыв его nano:

nano bot.py

Для начала импортируем библиотеку pyTelegramBotAPI:

import telebot

Затем зададим переменную token равную нашему токену, который мы получили от BotFather для взаимодействия с Telegram Bot Api:

token = 'ваш token api'

Объявим бота:

bot = telebot.TeleBot(token)

Далее задается декоратор. Пока наш бот будет обрабатывать только команду start:

@bot.message_handler(commands=['start'])

и в ответ писать нам “Привет!”:

def start_message(message):     bot.send_message(message.chat.id, 'Привет!') 

Чтобы бот постоянно ожидал запрос от пользователя в конце пропишем:

bot.polling()

В итоге мы получим код:

import telebot  token = 'ваш token api'  bot = telebot.TeleBot(token)  @bot.message_handler(commands=['start']) def start_message(message):     bot.send_message(message.chat.id, 'Привет!')  bot.polling()

Запустим его:

python bot.py

Затем откроем нашего бота (можно найти по имени) и напишем ему команду /start:

<amp></amp>

Поздравлю с первыми словами нашего бота PocketAdmin!

Использование прокси в telebot

При запуске скрипта может появиться ошибка следующего вида:

requests.exceptions.ConnectionError: ('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer'))

Чтобы исправить её, можно попробовать подключиться через прокси:

from telebot import apihelper apihelper.proxy = {     'https':'socks5://login:password@ip:port'}

где login:password@ip:port – соответствующие данные для подключения к прокси.

Если при использовании прокси возникают ошибки, подобные: Not supported proxy scheme socks5 или Missing dependencies for SOCKS support, то необходимо установить модули:

pip install requests[socks] PySocks

Ответы бота на сообщения пользователя

Аналогично хэндлерам для команд, в telegram bot api есть возможность обрабатывать сообщения от пользователя. Для этого используется тип text. Например, мы можем запрограммировать бота отвечать на определенные фразы или слова пользователя:

@bot.message_handler(content_types=['text']) def send_text(message):     if message.text.lower() == 'привет':         bot.send_message(message.chat.id, 'Ещё раз привет!')     elif message.text.lower() == 'пока':         bot.send_message(message.chat.id, 'Пока!')

Думаю тут все понятно. На слово “Привет” бот будет отвечать “Ещё раз привет!”, а на “Пока” – “Пока!”. Весь код нашего telegram bot на python теперь будет выглядеть следующим образом:

import telebot  token = 'ваш token api'  bot = telebot.TeleBot(token)  @bot.message_handler(commands=['start']) def start_message(message):     bot.send_message(message.chat.id, 'Привет!')  @bot.message_handler(content_types=['text']) def send_text(message):     if message.text.lower() == 'привет':         bot.send_message(message.chat.id, 'Ещё раз привет!')     elif message.text.lower() == 'пока':         bot.send_message(message.chat.id, 'Пока!')  bot.polling()

Перезапустим скрипт и пообщаемся с ботом:

<amp></amp>

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

Клавиатура в Telegram Bot на Python

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

Добавим в обработчик команды /start клавиатуру с кнопками “Привет “и “Пока”:

@bot.message_handler(commands=['start']) def start_message(message):     keyboard = telebot.types.ReplyKeyboardMarkup(True)     keyboard.row('Привет', 'Пока')     bot.send_message(message.chat.id, 'Привет!', reply_markup=keyboard)

И запустим измененный скрипт. Как только мы отправим боту команду /start у нас внизу появится наша клавиатура:

<amp></amp>

Теперь для отправки сообщений достаточно лишь нажать на соответствующую кнопку. Это очень удобно в мобильной версии телеграма.

InLine клавиатура

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

Давайте добавим простой вопрос от бота на команду /test:

@bot.message_handler(commands=['test']) def start_message(message):     markup = telebot.types.InlineKeyboardMarkup()     markup.add(telebot.types.InlineKeyboardButton(text='Три', callback_data=3))     markup.add(telebot.types.InlineKeyboardButton(text='Четыре', callback_data=4))     markup.add(telebot.types.InlineKeyboardButton(text='Пять', callback_data=5))     bot.send_message(message.chat.id, text="Какая средняя оценка была у Вас в школе?", reply_markup=markup) 

Переменная markup объявляет новую переменную с inline keyboard, а markup.add – создает отдельную кнопку. Основные параметры при создании кнопки – text и callback_data: первый отвечает за текст на кнопке, второй – данные, которые будут переданы боту при выборе пользователем определенного варианта ответа.

Запустим скрипт и напишем /test:

<amp></amp>

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

@bot.callback_query_handler(func=lambda call: True) def query_handler(call):      bot.answer_callback_query(callback_query_id=call.id, text='Спасибо за честный ответ!')     answer = ''     if call.data == '3':         answer = 'Вы троечник!'     elif call.data == '4':         answer = 'Вы хорошист!'     elif call.data == '5':         answer = 'Вы отличник!'      bot.send_message(call.message.chat.id, answer) 

bot.answer_callback_quer – это всплывающее окно, которое будет показано пользователю после нажатия кнопки. А в call.data будет передано значение, которое мы указывали при создании клавиатуры в параметре callback_data. Ответим боту, выбрав один из ответов:

<amp></amp>

Отлично, все работает. Но будет лучше, если после ответа, клавиатура будет исчезать из чата. Это можно сделать добавив в конец функции query_handler следующую строку:

bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id)

Это функция редактирования клавиатуры, вызванная без указания объекта клавиатуры. Теперь после ответа пользователя клавиатура будет убрана ботом:

<amp></amp>

Конечный листинг телеграм бот на питоне

Мы рассмотрели лишь малую часть возможностей telegram bot api, однако, это очень полезные инструменты по работе с ним. В конце приведем полный листинг получившегося у нас telegram bot на python:

import telebot from telebot import apihelper  apihelper.proxy = {     'https':'socks5://login:password@ip:port'}  token = 'ваш token api'  bot = telebot.TeleBot(token)  @bot.message_handler(commands=['start']) def start_message(message):     keyboard = telebot.types.ReplyKeyboardMarkup(True)     keyboard.row('Привет', 'Пока')     bot.send_message(message.chat.id, 'Привет!', reply_markup=keyboard)  @bot.message_handler(commands=['test']) def start_message(message):     markup = telebot.types.InlineKeyboardMarkup()     markup.add(telebot.types.InlineKeyboardButton(text='Три', callback_data=3))     markup.add(telebot.types.InlineKeyboardButton(text='Четыре', callback_data=4))     markup.add(telebot.types.InlineKeyboardButton(text='Пять', callback_data=5))     bot.send_message(message.chat.id, text="Какая средняя оценка была у Вас в школе?", reply_markup=markup)  @bot.message_handler(content_types=['text']) def send_text(message):     if message.text.lower() == 'привет':         bot.send_message(message.chat.id, 'Ещё раз привет!')     elif message.text.lower() == 'пока':         bot.send_message(message.chat.id, 'Пока!')  @bot.callback_query_handler(func=lambda call: True) def query_handler(call):      bot.answer_callback_query(callback_query_id=call.id, text='Спасибо за честный ответ!')     answer = ''     if call.data == '3':         answer = 'Вы троечник!'     elif call.data == '4':         answer = 'Вы хорошист!'     elif call.data == '5':         answer = 'Вы отличник!'      bot.send_message(call.message.chat.id, answer)     bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id)  bot.polling()

На Хабре, да и не только, про ботов рассказано уже так много, что даже слишком. Но заинтересовавшись пару недель назад данной темой, найти нормальный материал у меня так и не вышло: все статьи были либо для совсем чайников и ограничивались отправкой сообщения в ответ на сообщение пользователя, либо были неактуальны. Это и подтолкнуло меня на написание статьи, которая бы объяснила такому же новичку, как я, как написать и запустить более-менее осмысленного бота (с возможностью расширения функциональности).

Часть 1: Регистрация бота

Самая простая и описанная часть. Очень коротко: нужно найти бота @BotFather, написать ему /start, или /newbot, заполнить поля, которые он спросит (название бота и его короткое имя), и получить сообщение с токеном бота и ссылкой на документацию. Токен нужно сохранить, желательно надёжно, так как это единственный ключ для авторизации бота и взаимодействия с ним.

Часть 2: Подготовка к написанию кода

Как уже было сказано в заголовке, писать бота мы будем на Python’е. В данной статье будет описана работа с библиотекой PyTelegramBotAPI (Telebot). Если у вас не установлен Python, то сперва нужно сделать это: в терминале Linux нужно ввести

sudo apt-get install python python-pip

Если же вы пользуетесь Windows, то нужно скачать Python с официального сайта . После, в терминале Linux, или командной строке Windows вводим

pip install pytelegrambotapi

Теперь все готово для написания кода.

Часть 3: Получаем сообщения и говорим «Привет»

Небольшое отступление. Телеграмм умеет сообщать боту о действиях пользователя двумя способами: через ответ на запрос сервера (Long Poll), и через Webhook, когда сервер Телеграмма сам присылает сообщение о том, что кто-то написал боту. Второй способ явно выглядит лучше, но требует выделенного IP-адреса, и установленного SSL на сервере. В этой статье я хочу рассказать о написании бота, а не настройке сервера, поэтому пользоваться мы будем Long Poll’ом. Открывайте ваш любимый текстовый редактор, и давайте писать код бота! Первое, что нужно сделать это импортировать нашу библиотеку и подключить токен бота:

import telebot; bot = telebot.TeleBot('%ваш токен%');

Теперь объявим метод для получения текстовых сообщений:

@bot.message_handler(content_types=['text']) def get_text_messages(message):

В этом участке кода мы объявили слушателя для текстовых сообщений и метод их обработки. Поле content_types может принимать разные значения, и не только одно, например

@bot.message_handler(content_types=['text', 'document', 'audio'])

Будет реагировать на текстовые сообщения, документы и аудио. Более подробно можно почитать в официальной документации Теперь добавим в наш метод немного функционала: если пользователь напишет нам «Привет», то скажем ему «Привет, чем я могу помочь?», а если нам напишут команду «/help», то скажем пользователю написать «Привет»:

if message.text == "Привет":     bot.send_message(message.from_user.id, "Привет, чем я могу тебе помочь?") elif message.text == "/help":     bot.send_message(message.from_user.id, "Напиши привет") else:     bot.send_message(message.from_user.id, "Я тебя не понимаю. Напиши /help.")

Данный участок кода не требует комментариев, как мне кажется. Теперь нужно добавить в наш код только одну строчку (вне всех методов).

bot.polling(none_stop=True, interval=0)

Теперь наш бот будет постоянно спрашивать у сервера Телеграмма «Мне кто-нибудь написал?», и если мы напишем нашему боту, то Телеграмм передаст ему наше сообщение. Сохраняем весь файл, и пишем в консоли

python bot.py

Где bot.py – имя нашего файла. Теперь можно написать боту и посмотреть на результат:hoooc8j1mwnptnjfde8ll7nqzea.jpeg

Часть 4: Кнопки и ветки сообщений

Отправлять сообщения это несомненно весело, но ещё веселее вести с пользователем диалог: задавать ему вопросы и получать на них ответы. Допустим, теперь наш бот будет спрашивать у пользователя по очереди его имя, фамилию и возраст. Для этого мы будем использовать метод register_next_step_handler бота:

name = ''; surname = ''; age = 0; @bot.message_handler(content_types=['text']) def start(message):     if message.text == '/reg':         bot.send_message(message.from_user.id, "Как тебя зовут?");         bot.register_next_step_handler(message, get_name); #следующий шаг – функция get_name     else:         bot.send_message(message.from_user.id, 'Напиши /reg');  def get_name(message): #получаем фамилию     global name;     name = message.text;     bot.send_message(message.from_user.id, 'Какая у тебя фамилия?');     bot.register_next_step_handler(message, get_surnme);  def get_surname(message):     global surname;     surname = message.text;     bot.send_message('Сколько тебе лет?');     bot.register_next_step_handler(message, get_age);  def get_age(message):     global age;     while age == 0: #проверяем что возраст изменился         try:              age = int(message.text) #проверяем, что возраст введен корректно         except Exception:              bot.send_message(message.from_user.id, 'Цифрами, пожалуйста');       bot.send_message(message.from_user.id, 'Тебе '+str(age)+' лет, тебя зовут '+name+' '+surname+'?') 

И так, данные пользователя мы записали. В этом примере показан очень упрощённый пример, по хорошему, хранить промежуточные данные и состояния пользователя нужно в БД, но мы сегодня работаем с ботом, а не с базами данных. Последний штрих – запросим у пользователей подтверждение того, что все введено верно, да не просто так, а с кнопками! Для этого немного отредактируем код метода get_age

def get_age(message):     global age;     while age == 0: #проверяем что возраст изменился         try:              age = int(message.text) #проверяем, что возраст введен корректно         except Exception:              bot.send_message(message.from_user.id, 'Цифрами, пожалуйста');       keyboard = types.InlineKeyboardMarkup(); #наша клавиатура       key_yes = types.InlineKeyboardButton(text='Да', callback_data='yes'); #кнопка «Да»       keyboard.add(key_yes); #добавляем кнопку в клавиатуру       key_no= types.InlineKeyboardButton(text='Нет', callback_data='no');       keyboard.add(key_no);       question = 'Тебе '+str(age)+' лет, тебя зовут '+name+' '+surname+'?';       bot.send_message(message.from_user.id, text=question, reply_markup=keyboard) 

И теперь наш бот отправляет клавиатуру, но если на нее нажать, то ничего не произойдёт. Потому что мы не написали метод-обработчик. Давайте напишем:

@bot.callback_query_handler(func=lambda call: True) def callback_worker(call):     if call.data == "yes": #call.data это callback_data, которую мы указали при объявлении кнопки         .... #код сохранения данных, или их обработки         bot.send_message(call.message.chat.id, 'Запомню : )');     elif call.data == "no":          ... #переспрашиваем 

Остаётся только дописать в начало файла одну строку:

from telebot import types

Вот и всё, сохраняем и запускаем нашего бота:ltgb1tt3oizmmk92_kr7r2ztkom.jpeg

В этой главе мы познакомимся с такой замечательной фичей 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() 

На этом глава про кнопки окончена, но про некоторые других их виды мы поговорим в следующих главах.

Введение

В начале апреля 2016 года вышло первое по-настоящему крупное обновление API для ботов. Изменений довольно много, поэтому материал я разобью на несколько частей. Сегодня поговорим об inline-кнопках и редактировании сообщений, а затем обсудим новые инлайн-режимы вместе со специальными кнопками для отправки геолокации и номера телефона.

Новые возможности

Начнём с двух важных изменений:

  1. Каждая кнопка, будь то обычная или инлайн, это теперь самостоятельный объект KeyboardButton или InlineKeyboardButton, не забудьте обновить своих ботов!
  2. В Inline-режиме все текстовые поля теперь представлены отдельными объектами InputMessageContent, которые, в свою очередь могут быть аж 4-х типов (подробности тут).

URL-кнопки

Итак, инлайн-кнопки. Что это такое? Это специальные объекты, которые “цепляются” к конкретным сообщениям и распространяют своё действие, в общем случае, только на них. Делятся такие кнопки на три типа: URL-кнопки, Callback-кнопки и Switch-кнопки. Самыми простыми являются кнопки-ссылки (URL). Как видно из названия, их цель — просто перекидывать пользователей по определенным веб-адресам. Давайте сразу напишем обработчик, который будет на любое сообщение отвечать каким-либо текстом и предложением перейти, например, на Яндекс.

@bot.message_handler(content_types=["text"]) def default_test(message):     keyboard = types.InlineKeyboardMarkup()     url_button = types.InlineKeyboardButton(text="Перейти на Яндекс", url="https://ya.ru")     keyboard.add(url_button)     bot.send_message(message.chat.id, "Привет! Нажми на кнопку и перейди в поисковик.", reply_markup=keyboard) 

Инлайн-клавиатура представляет собой объект InlineKeyboardMarkup, а каждая инлайн-кнопка – это объект InlineKeyboardButton. Чтобы получилась URL-кнопка, нужно указать значения параметров text (текст на кнопке) и url (валидный веб-адрес). В результате бот пришлет нам такое сообщение (см. рис.). В целях обеспечения безопасности, перед переходом по URL-кнопкам появляется всплывающее окно, в котором видна ссылка целиком.

l8_1.png

URL-кнопка

Callback-кнопки и редактирование сообщений

Прежде, чем мы перейдем к другим кнопкам, давайте познакомимся с функциями редактирования сообщений, коих тоже три: editMessageText (редактирование текста), editMessageCaption (редактирование подписи к медиа) и editMessageReplyMarkup (редактирование инлайн-клавиатуры). В рамках этого урока рассмотрим только первую функцию, остальные работают аналогично и предлагаются для самостоятельного изучения.Чтобы отредактировать сообщение, нам надо знать, про какое именно идёт речь. В случае, если оно было отправлено самим ботом, идентификаторами служит связка chat_id + message_id. Но если сообщение было отправлено в инлайн-режиме, то ориентироваться надо по параметру inline_message_id.

И вот теперь вернемся к нашим баранам кнопкам. На очереди – Callback. Это, на мой взгляд, самая крутая фича нового обновления. Колбэк-кнопки позволяют выполнять произвольные действия по их нажатию. Всё зависит от того, какие параметры каждая кнопка в себе несёт. Соответственно, все нажатия будут приводить к отправке боту объекта CallbackQuery, содержащему поле data, в котором написана некоторая строка, заложенная в кнопку, а также либо объект Message, если сообщение отправлено ботом в обычном режиме, либо поле inline_message_id, если сообщение отправлено в инлайн-режиме.

Приведу пример, после которого все вопросы должны отпасть: пусть, например, если сообщение отправлено ботом в обычном режиме, то нажатие на кнопку заменит текст сообщения на “Пыщь”, если в инлайн – то “Бдыщь”. При этом в обоих случаях значение callback_data будет равно test. Что для этого нужно сделать: во-первых, написать простейший хэндлер для всех входящих сообщений, во-вторых, написать простейший хэндлер для инлайн-сообщений, в-третьих, написать простейший хэндлер для колбэка, который определит, из какого режима пришло сообщение.

# Обычный режим @bot.message_handler(content_types=["text"]) def any_msg(message):     keyboard = types.InlineKeyboardMarkup()     callback_button = types.InlineKeyboardButton(text="Нажми меня", callback_data="test")     keyboard.add(callback_button)     bot.send_message(message.chat.id, "Я – сообщение из обычного режима", reply_markup=keyboard)   # Инлайн-режим с непустым запросом @bot.inline_handler(lambda query: len(query.query) > 0) def query_text(query):     kb = types.InlineKeyboardMarkup()     # Добавляем колбэк-кнопку с содержимым "test"     kb.add(types.InlineKeyboardButton(text="Нажми меня", callback_data="test"))     results = []     single_msg = types.InlineQueryResultArticle(         id="1", title="Press me",         input_message_content=types.InputTextMessageContent(message_text="Я – сообщение из инлайн-режима"),         reply_markup=kb     )     results.append(single_msg)     bot.answer_inline_query(query.id, results)   # В большинстве случаев целесообразно разбить этот хэндлер на несколько маленьких @bot.callback_query_handler(func=lambda call: True) def callback_inline(call):     # Если сообщение из чата с ботом     if call.message:         if call.data == "test":             bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="Пыщь")     # Если сообщение из инлайн-режима     elif call.inline_message_id:         if call.data == "test":             bot.edit_message_text(inline_message_id=call.inline_message_id, text="Бдыщь")  if __name__ == '__main__':     bot.infinity_polling() 

Запускаем бота, отправляем инлайн-сообщение, которое, в свою очередь, вызовет обычное:

l8_2.png

Нажмем на обе кнопки, результат правильный:

l8_3.png

После проверки

Таким образом, callback-кнопки – это очень мощный инструмент для взаимодействия пользователей с ботом, а редактирование сообщений дополнительно помогает в этом. Более того, нажатие на колбэк-кнопку может дополнительно тригернуть либо уведомление в верхней части экрана, либо всплывающее окно. Покажу первый вариант. Пускай помимо изменения сообщения на “Пыщь”, аналогичное слово показывается уведомлением. Для этого перепишем первое if-условие в хендлере колбэков:

if call.message:     if call.data == "test":         bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="Пыщь")         bot.answer_callback_query(callback_query_id=call.id, show_alert=False, text="Пыщь!") 

Результат – на скриншоте. Попробуйте, кстати, изменить аргумент show_alert на True и посмотрите, что получится.

l8_4.png

Всплывающее уведомление

Switch-кнопки

Наконец, остался последний тип кнопок — Switch (переключатель). Они нужны, чаще всего, для обучения пользователей работе с ботом в инлайн-режиме. Чтобы активировать сделать кнопку такого типа, нужно указать аргумент switch_inline_query либо пустой, либо с каким-либо текстом. В последнем случае этот текст будет сразу подставлен в поле ввода, например, для показа демонстрации инлайна. Как вообще работает такая кнопка? При нажатии на неё Telegram предложит выбрать чат, после чего подставит в поле ввода ник вашего бота и (если есть), текст, указанный вами в аргументе switch_inline_query. Давайте попробуем так сделать. Добавим кнопку, которая будет перенаправлять пользователя в какой-либо чат и предлагать в инлайн-режиме запрос “Telegram”. Код всего хендлера выглядит вот так:

@bot.message_handler(content_types=["text"]) def any_msg(message):     keyboard = types.InlineKeyboardMarkup()     switch_button = types.InlineKeyboardButton(text="Нажми меня", switch_inline_query="Telegram")     keyboard.add(switch_button)     bot.send_message(message.chat.id, "Я – сообщение из обычного режима", reply_markup=keyboard) 

Теперь, если мы нажмем на кнопку и выберем чат, вот что получится:

l8_5.png

Результат

Итак, в этом уроке мы познакомились с новыми кнопками в Telegram Bot API, научились переписывать историю редактировать сообщения и отправлять небольшие уведомления по нажатию. В следующий раз продолжим изучать новые возможности для ботов. А исходники к этому уроку можно найти в этом репозитории.

← Урок №7Урок №9 →

Используемые источники:

  • https://pocketadmin.tech/ru/telegram-bot-на-python/
  • https://habr.com/ru/post/442800/
  • https://mastergroosha.github.io/telegram-tutorial-2/buttons/
  • https://mastergroosha.github.io/telegram-tutorial/docs/lesson_08/

Оцените статью
Рейтинг автора
5
Материал подготовил
Илья Коршунов
Наш эксперт
Написано статей
134
Добавить комментарий