PyTelegramBotAPI на примере проекта сбора обратной связи #3. Клавиатура

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

Всем вновь привет! Сегодня мы продолжим цикл статей, посвящённых разработке телеграм бота на PyTelegramBotAPI.

В этом уроке я буду чередовать слова: пользователь и клиент. В контексте нашей задачи эти слова означают одно и тоже — пользователя нашего бота.

<<< Предыдущий урок

В этом уроке мы разберём клавиатуру, как одну из важнейших частей бота.

Но перед этим, вспомним что мы сделали ранее.

В первом уроке мы создали токен бота и написали простую функцию приветствия пользователя. Во втором уроке мы написали словарь для хранения пользовательских данных (имя, фамилия) и написали функции для запроса этих данных.

Знания освежили, теперь займёмся кодом. Прежде чем разбирать клавиатуру, немного доработает наш код. А именно: создадим новую функцию, которая будет запрашивать у пользователя имя. Конечно, вы можете сказать что такая функция уже есть это — welcome. Да, согласен, она запрашивает имя, но всё же эта функция запуска нашего бота, функция приветствия. Поэтому логику запроса данных лучше вынести.

@bot.message_handler(func=lambda message: message.text == 'Написать в поддержку')
def write_to_support(message):
    chat_id = message.chat.id
    bot.send_message(chat_id, 'Введите своё имя')
    users[chat_id] = {}
    bot.register_next_step_handler(message, save_username)

Эта функция и будет запрашивать у пользователя имя. Но, вас могла напрячь первая строка кода — строка с лямдой (прочтите про неё, лямбда нам ещё не раз пригодится). На самом деле ничего страшного тут нет. Так как фраза «Написать в поддержку«не является командой (команды со слеша — /), то и использовать аргумент commands мы не можем. Тут то нам на помощь и приходит func, этот аргумент позволяет определить функцию, которая будет вызываться для проверки. Она (функция) должна `сообщить` библиотеке PyTelegramBotAPI, вызывать эту функцию на текущее сообщение или нет. Наша функция (лямбда) будет возвращать True, только если текст сообщения будет «Написать в поддержку», на другой текст эта функция реагировать не будет. Надеюсь это понятно, если нет — прошу в комментарии, стараюсь отвечать всем.

Теперь поменяем наш welcome. Так как я бы хотел сделать welcome просто функцией приветствия, то уберём весь код отвечающий за запрос имени.

Код обновлённого welcome будет выглядеть так:

@bot.message_handler(commands=['start'])
def welcome(message):
    chat_id = message.chat.id
    bot.send_message(chat_id,
                     'Добро пожаловать в бота сбора обратной связи')

Подготовительная часть завершена, теперь разберёмся с клавиатурой. В начале совсем немного теории из разряда: А зачем это нужно?

Если спросить любого человека, что ему проще — написать команду, ввести текст или кликнуть на одну кнопку, то большинство людей выберет последнее. И действительно, клик это самая быстрая и простая операция, а мы бы, конечно, хотели чтобы пользователи нашего бота были всем довольны. Чтобы бот не вызывал у них раздражение из-за излишней сложности и замудрённости.

Для этого и придумали клавиатура, выглядит она так:

Первый тип клавиатуру

Первый тип клавиатуру

Или так:

Второй тип клавиатуру

Второй тип клавиатуру

Изображения взяты с интернета.

Как вы можете заметить — это две абсолютно разные клавиатуры. Первая находится под строкой ввода сообщения (Reply), а вторая в самом сообщении (Inline).

Напишем клавиатуру Reply (та, которая находится под строкой ввода сообщения. Скриншот 1).

@bot.message_handler(commands=['start'])
def welcome(message):
    chat_id = message.chat.id
    keyboard = telebot.types.ReplyKeyboardMarkup(resize_keyboard=True)
    button_support = telebot.types.KeyboardButton(text="Написать в поддержку")
    keyboard.add(button_support)
    bot.send_message(chat_id,
                     'Добро пожаловать в бота сбора обратной связи',
                     reply_markup=keyboard)

Теперь разберём код. На 4 строке я создал объект keyboard, это объект класса ReplyKeyboardMarkup, который и является той самой Reply клавиатурой, в скобках указано resize_keyboard=True, это сделано для нормальной работы кнопок, в некоторых случаях кнопки могут сильно растягиваться, расширяться и будут выглядеть крайне не презентабельно, resize_keyboard решает эту проблему.

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

В конце кода я добавляю кнопку на клавиатуру (делать это обязательно, или кнопка не отобразиться) и отправляю её в сообщении, указав параметр reply_markup.

Вот так выглядит и работает клавиатуры:

Reply клавиатура с одной кнопкой

Reply клавиатура с одной кнопкой

Поиграемся с кнопками, создадим, например, четыре штуки и будем их по разному размещать.

Кнопки в одной строке:

Кнопки в одном keyboard.add()

Кнопки в одном keyboard.add ()

Код:

@bot.message_handler(commands=['start'])
def welcome(message):
    chat_id = message.chat.id
    keyboard = telebot.types.ReplyKeyboardMarkup(resize_keyboard=True)
    button_support = telebot.types.KeyboardButton(text="Написать в поддержку")
    button1 = telebot.types.KeyboardButton(text="Кнопка 1")
    button2 = telebot.types.KeyboardButton(text="Кнопка 2")
    button3 = telebot.types.KeyboardButton(text="Кнопка 3")
    keyboard.add(button_support, button1, button2, button3)
    bot.send_message(chat_id,
                     'Добро пожаловать в бота сбора обратной связи',
                     reply_markup=keyboard)

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

Теперь по две кнопки:

Кнопки в двух keyboard.add()

Кнопки в двух keyboard.add ()

Код:

    keyboard = telebot.types.ReplyKeyboardMarkup(resize_keyboard=True)
    button_support = telebot.types.KeyboardButton(text="Написать в поддержку")
    button1 = telebot.types.KeyboardButton(text="Кнопка 1")
    button2 = telebot.types.KeyboardButton(text="Кнопка 2")
    button3 = telebot.types.KeyboardButton(text="Кнопка 3")
    keyboard.add(button_support, button1)
    keyboard.add(button2, button3)

Приведена только середина функции, так как начало и конец не изменились.

Как можно заметить, чтобы располагать кнопки на разных строках, достаточно их добавить в разные keyboard.add (), думаю это вы поняли.

Кстати, чтобы убрать Reply клавиатуру, надо использовать класс ReplyKeyboardRemove. Пример:

@bot.message_handler(commands=['remove_keyboard'])
def remove_keyboard(message):
    chat_id = message.chat.id
    keyboard = telebot.types.ReplyKeyboardRemove()
    bot.send_message(chat_id,
                     'Удаляю клавиатуру',
                     reply_markup=keyboard)

Отлично, это была Reply клавиатура, а теперь поговорим про Inline. Напомню, что при этом типе клавиатуры кнопки будут добавлены не под строчку написания сообщения, а под само сообщение.

Для использования клавиатуры Inline, надо будет использовать класс клавиатуры — InlineKeyboardMarkup, а для кнопок — InlineKeyboardButton. Также в кнопках надо прописать параметр callback_data, зачем он нужен я вам расскажу чуть позже.

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

    keyboard = telebot.types.InlineKeyboardMarkup()
    button_save = telebot.types.InlineKeyboardButton(text="Сохранить",
                                                     callback_data='save_data')
    button_change = telebot.types.InlineKeyboardButton(text="Изменить",
                                                     callback_data='change_data')
    keyboard.add(button_save, button_change)

Запустим, проверим, и правда — кнопки есть. Но есть один минус. Они не работают. Дело в том, что Inline кнопки работают не так, как Reply. При нажатии никакого сообщения не отправляется, а сервер телеграма отправляет запрос к нашему боту, что была нажата кнопка. Появляется вопрос:, а как наш бот узнает какая кнопка нажата? Для этого мы и прописывали callback_data, при нажатии бот получит текст, который был указан в callback_data и по этому тексту мы и сможем понять, какая кнопка была нажата.

Добавим обработчик для двух наших кнопок (кнопки сохранения и изменения данных. Под данными я подразумевая имя и фамилию).

@bot.callback_query_handler(func=lambda call: call.data == 'save_data')
def save_btn(call):
    message = call.message
    chat_id = message.chat.id
    bot.send_message(chat_id, f'Данные сохранены')


@bot.callback_query_handler(func=lambda call: call.data == 'change_data')
def save_btn(call):
    message = call.message
    chat_id = message.chat.id
    bot.send_message(chat_id, f'Изменение данных.')
    write_to_support(message)

Тут схожий принцип, что был и ранее. Мы используем лямбду для фильтрации. У нас две функции, реагирующие на разные кнопки, поэтому в первом декораторе мы ожидаем save_data, а во второй change_data.

Теперь разберёмся что же такое call. Как можно заметить, в функциях используется не message, а call. call — это объект, расширяющий стандартный message. Из полезного — в call есть call.data, это название кнопки на которую мы нажали (callback_data). Мы также можем получить и привычный нам message, для этого напишем:

message = call.message

Запустим и протестируем код:

e75bb6f43ff535dc7932ee79f348da23.png

Как можно заметить, всё прекрасно работает.

Получившийся код:

import telebot
from config import TOKEN

bot = telebot.TeleBot(TOKEN)

users = {}


@bot.message_handler(commands=['start'])
def welcome(message):
    chat_id = message.chat.id
    keyboard = telebot.types.ReplyKeyboardMarkup()
    button_save = telebot.types.InlineKeyboardButton(
        text="Написать в поддержку")
    keyboard.add(button_save)
    bot.send_message(chat_id,
                     'Добро пожаловать в бота сбора обратной связи',
                     reply_markup=keyboard)


@bot.message_handler(
    func=lambda message: message.text == 'Написать в поддержку')
def write_to_support(message):
    chat_id = message.chat.id
    bot.send_message(chat_id, 'Введите своё имя')
    users[chat_id] = {}
    bot.register_next_step_handler(message, save_username)


def save_username(message):
    chat_id = message.chat.id
    name = message.text
    users[chat_id]['name'] = name
    bot.send_message(chat_id, f'Отлично, {name}. Теперь укажи свою фамилию')
    bot.register_next_step_handler(message, save_surname)


def save_surname(message):
    chat_id = message.chat.id
    surname = message.text
    users[chat_id]['surname'] = surname
    keyboard = telebot.types.InlineKeyboardMarkup()
    button_save = telebot.types.InlineKeyboardButton(text="Сохранить",
                                                     callback_data='save_data')
    button_change = telebot.types.InlineKeyboardButton(text="Изменить",
                                                       callback_data='change_data')
    keyboard.add(button_save, button_change)

    bot.send_message(chat_id, f'Сохранить данные?', reply_markup=keyboard)


@bot.message_handler(commands=['who_i'])
def who_i(message):
    chat_id = message.chat.id
    name = users[chat_id]['name']
    surname = users[chat_id]['surname']
    bot.send_message(chat_id, f'Вы: {name} {surname}')


@bot.callback_query_handler(func=lambda call: call.data == 'save_data')
def save_btn(call):
    message = call.message
    chat_id = message.chat.id
    bot.send_message(chat_id, f'Данные сохранены')


@bot.callback_query_handler(func=lambda call: call.data == 'change_data')
def save_btn(call):
    message = call.message
    chat_id = message.chat.id
    bot.send_message(chat_id, f'Изменение данных.')
    write_to_support(message)


if __name__ == '__main__':
    print('Бот запущен!')
    bot.infinity_polling()

Отлично, всё работает!

Подведём итого. Сегодня мы изучили:

  1. Изучили фильтрацию запросов через лямбду

  2. Написали Reply клавиатуру

  3. Написали Inline клавиатуру

    Отличный результат, практически все основные возможности изучены, осталось совсем чуть-чуть.

<<< Предыдущий урок

Ну вот, третья часть закончена! Можно считать, что все основные возможности клавиатуры изучены. Конечно, это не весь возможный функционал, но основная его часть.

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

Код с этого урока на GitHub: https://github.com/Ryize/CollectClientsFeedbackBot

© Habrahabr.ru