Просто добавь команд: как реализовать диалоговый Телеграм бот на основе шаблона

от автора

В предыдущей статье я рассказала, как быстро создать инфраструктуру для диалогового бота на основе Yandex Serverless Functions и базы данных YDB. В качестве примера использовался примитивный бот, реализованный в моём репозитории ydb_serverless_telegram_bot.

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

Все статьи цикла

  1. Создаём основу для диалогового Телеграм бота в облаке

  2. — вы здесь —

  3. Стейты, БД и логи — разбираем шаблон диалогового Телеграм бота

Подготовка

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

  • Скачать код из репозитория ydb_serverless_telegram_bot – будем дополнять именно его, вооружитесь вашим любимым редактором кода.

  • Освоить заливку кода в функцию (см. раздел Запускаем бота в предыдущей статье).

Также полезно прочесть раздел Поговорите с ботом! предыдущей статьи, чтобы вспомнить, как в консоли Яндекс.Облака:

  • Проверять содержимое таблиц базы данных YDB.

  • Смотреть логи serverless функции для дебага

Что бот умеет сейчас?

Если вы ещё не успели сделать свою версию бота по инструкции из предыдущей статьи, то с моей реализацией бота можно познакомиться по ссылке — YDB serverless example.

Бот поддерживает 4 основные команды:

  • /start — показать приветственное сообщение

  • /register — “зарегистрировать” пользователя, т.е. спросить пошагово его имя, фамилию, возраст и сохранить в базу

  • /show_data — показать данные, которые сохранены в базе про текущего пользователя

  • /delete_account — спрашивает подтверждение, что пользователь действительно хочет удалить аккаунт, и, если пользователь подтверждает, удаляет все данные про него из базы данных

Дополнительно во время регистрации доступна команда /cancel, прерывающая процесс.

Текущая схема работы бота:

Что добавим?

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

Дополнительные требования:

  • Пользователь, который ещё не зарегистрировался, не может менять данные.

  • Выбор поля для изменения должен быть реализован кнопками, при этом неизвестное название поля, введённое вручную, приниматься не дожно.

  • Во время всего процесса должна быть доступна команда /cancel, завершающая процесс без изменений.

  • Значение возраста, как и в сценарии регистрации, должно проверяться: если введено не число, пользователь получает замечание и предложение попровать ещё раз до тех пор, пока не будет введено число или команда /cancel.

Вот верхнеуровневая схема сценария, которая поможет нам написать код:

Реализация нового сценария

Скачайте код из репозитория ydb_serverless_telegram_bot, откройте его в вашем любимом редакторе кода.

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

? Облачком с троеточием будут обозначаться куски кода, которые нужно скопировать в ваш редактор. Чтобы не путать объяснения и финальную версию.

❗️Восклицательным знаком помечены дополнительные замечания.

❓Вопросительным — те места, которые сейчас могут быть не до конца понятны, но будут разобраны в следующей статье.

Готовим каркас для сценария

Логика бота задаётся функциями-обработчиками сообщений (как команд, так и текстов в свободной форме) и фильтрами, которые определяют, в какой ситуации какой обработчик применяется к очередному сообщению пользователя. Разберём обработчики и фильтры отдельно, а затем соберём вместе в каркас логики бота.

Обработчики

Прежде всего продумаем, какие обработчики нам понадобятся:

  • Обработчик команды /change_data, который проверяет регистрацию пользователя и предлагает выбор полей для изменения.

  • Обработчик команды /cancel на протяжении всего процесса.

  • Обработчик выбранного поля, который проверяет, что поле валидно, и предлагает ввести значение для этого поля.

  • Обработчик нового значения, который проверяет корректность и записывает его в базу.

Обработчики определяются в файле bot/handlers.py.

? Добавим плейсхолдеры для обработчиков в конец файла bot/handlers.py:

@logged_execution def handle_change_data(message, bot, pool):     pass  @logged_execution def handle_cancel_change_data(message, bot, pool):     pass  @logged_execution def handle_choose_field_to_change(message, bot, pool):     pass  @logged_execution def handle_save_changed_data(message, bot, pool):     pass

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

❓ В сниппете кода выше декоратор logged_executionпомогает логировать выполнение функций. О нём подробнее в следующий раз, сейчас примем его как необходимость.

Фильтры

Метод register_message_handler библиотеки TeleBot предусматривает дефолтный набор фильтров — по команде, по типу контента в сообщении (текст, фото, видео, …), по регулярному выражению, произвольной функции от сообщения и т. д. В библиотеке также реализованы специальные фильтры. В данном боте-шаблоне удобной основой являются фильтры по стейтам (состояниям) пользователя, которые позволяют легко и компактно обрабатывать контекст сообщения.

❗️ В библиотеке доступны и другие специальные фильтры, которые мы не будем использовать в этой статье, например, текст сообщения - число или автор сообщения - админ, больше примеров можно найти в репозитории библиотеки TeleBot.

Подробно про то, как реализовано хранилище стейтов и зачем оно нужно, расскажу в следующий раз, а сейчас примем, что нам необходимы два вида фильтров для реализации сценария, и научимся их объявлять в коде:

  • Фильтр по команде

  • Фильтр по стейту (состоянию) пользователя

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

В новом сценарии состояния пользователя меняются так:

  • В начале сценария состояние пустое, ожидаем команду /change_data.

  • После начала сценария бот ждёт от пользователя, какое поле необходимо поменять, поэтому пользователь находится в статусе “выбирает поле”.

  • После выбора поля бот ожидает новое значение для поля, чтобы записать его в базу, поэтому пользователь находится в состоянии “придумывает новое значение”.

  • В конце сценария или при отмене процесса состояние пользователя снова обнуляется.

Таким образом, для целей фильтрации необходимо завести 2 новых уникальных состояния. Состояния объявляются в файле bot/states.py и для удобства объединяются по сценарию.

? Заведём новый класс для сценария изменения данных и состояния в нём, для этого в конец файла bot/states.pyдобавим:

class ChangeDataState(StatesGroup):     select_field = State()     write_new_value = State()

Теперь можно присваивать пользователю статусы ChangeDataState.select_field и ChangeDataState.write_new_value и задавать их в качестве условия обработчикам.

Собираем обработчики и фильтры вместе

Верхнеуровневая логика бота собрана в файле bot/structure.py. Для удобства поддержки каждый сценарий задаётся функцией, возвращающей список правил-хэндлеров. Как говорилось выше, правило определяется функцией-обработчиком сообщения и фильтрами.

Например, следующее правило означает, что по команде /register будет вызвана функция handle_register из файла bot/handlers.py.

Handler(callback=handlers.handle_register, commands=["register"])

А такое правило говорит, что функция handle_finish_delete_account будет вызвана, если любое текстовое сообщение придёт от пользователя в состоянии DeleteAccountState.are_you_sure.

Handler(     callback=handlers.handle_finish_delete_account,     state=bot_states.DeleteAccountState.are_you_sure, )

Также можно комбинировать команды и состояния, а ещё перечислять в качестве фильтра список из нескольких состояний и / или команд, например, как в следующем правиле. Оно значит, что только команда /cancel от пользователя в одном из перечисленных в списке состояний вызовет обработчик handle_cancel_registration.

Handler(     callback=handlers.handle_cancel_registration,     commands=["cancel"],     state=[         bot_states.RegisterState.first_name,         bot_states.RegisterState.last_name,         bot_states.RegisterState.age,     ], )

❗️ Последовательность правил в списке важна! Из всех подошедших правил выполнится первое, поэтому надо внимательно выбирать порядок правил. Если ни одно правило не подошло, сообщение будет проигнорировано.

Итак, мы готовы описать новый сценарий.

? Для этого в файле bot/structure.py, например, после функции get_delete_account_handlers добавим новую функцию:

def get_change_data_handlers():     return [         Handler(callback=handlers.handle_change_data, commands=["change_data"]),         Handler(             callback=handlers.handle_cancel_change_data,             commands=["cancel"],             state=[                 bot_states.ChangeDataState.select_field,                 bot_states.ChangeDataState.write_new_value,             ],         ),         Handler(             callback=handlers.handle_choose_field_to_change,             state=bot_states.ChangeDataState.select_field,         ),         Handler(             callback=handlers.handle_save_changed_data,             state=bot_states.ChangeDataState.write_new_value,         ),     ]

❗️ Здесь Handler — это простой класс для упрощения синтаксиса регистрации правил. Он принимает такие же аргументы, как и более традиционный метод register_message_handler библиотеки TeleBot.

❗️ Обратите внимание на последовательность правил — правило для команды /cancel должно быть зарегистрировано раньше, чем правило для обработки исправляемого поля или обработки нового значения поля, иначе сообщение /cancel будет восприниматься как текст для этих двух правил.

Далее необходимо добавить новый сценарий в бота

? Для этого в функции create_bot в файле bot/structure.py нужно пополнить общий список правил правилами из нового сценария:

handlers = [] handlers.extend(get_start_handlers()) handlers.extend(get_registration_handlers()) handlers.extend(get_show_data_handlers()) handlers.extend(get_delete_account_handlers())  # new scenario handlers.extend(get_change_data_handlers())

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

Готовим тексты, которые увидит пользователь

Все тексты, которые в том или ином сценарии видит пользователь, собраны в файле user_interaction/texts.py.

? Чтобы не усложнять туториал, добавим сразу все тексты для нового сценария в конец файла user_interaction/texts.py:

FIELD_LIST = ["first_name", "last_name", "age"] UNKNOWN_FIELD = "Unknown field, choose a field from the list below:" SELECT_FIELD = "Choose a field to change:" WRITE_NEW_VALUE = "Write new value for the field {}" CANCEL_CHANGE = "Cancelled! Your data is not changed." CHANGE_DATA_DONE = "Done! Your data is updated."

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

Создаём интерфейс обращения в базу данных

Работа с базой данных YDB реализована в директории database. SQL запросы перечислены в файле database/queries.py, а интерфейс для выполнения запросов из Python кода — в файле database/model.py.

Запросы пишутся на особенном диалекте SQL — YQL (документация). Потренироваться писать запросы можно, нажав кнопку Новый SQL-запрос во вкладке Навигация базы данных YDB, созданной для бота по инструкции из предыдущей статьи.

В нашем случае понадобится добавить запрос, обновляющий данные для user_id в таблице user_personal_info, которая была создана запросом:

CREATE TABLE `user_personal_info` (   `user_id` Uint64,   `last_name` Utf8,   `first_name` Utf8,   `age` Uint64,   PRIMARY KEY (`user_id`) );

? Такой запрос нужно добавить в конец файла database/queries.py:

update_user_info = f"""     DECLARE $user_id AS Uint64;     DECLARE $first_name AS Utf8;     DECLARE $last_name AS Utf8;     DECLARE $age AS Uint64;          REPLACE INTO `{USERS_INFO_TABLE_PATH}`     SELECT         $user_id AS user_id,         $first_name AS first_name,         $last_name AS last_name,         $age AS age,     FROM `{USERS_INFO_TABLE_PATH}`     WHERE user_id == $user_id; """

Запрос ищет в таблице user_personal_info строку для переданного user_id и меняет значения в строке на переданные в переменные first_name, last_name и age.

Пояснение

Если бы мы писали запрос в интерфейсе Яндекс.Облака, нажав во вкладке Навигация базы данных кнопку Новый SQL-запрос, то он бы выглядел так:

$user_id = CAST(123 AS Uint64); $first_name = CAST("Michael" AS Utf8); $last_name = CAST("Scott" AS Utf8); $age = CAST(42 AS Uint64);  REPLACE INTO `user_personal_info` SELECT     $user_id AS user_id,     $first_name AS first_name,     $last_name AS last_name,     $age AS age, FROM `user_personal_info` WHERE user_id == $user_id;

В Python-коде нам придётся подставлять для каждого вызова свои значения параметров, поэтому объявляем их с помощью DECLARE.

❓ Почему запрос устроен именно так — рассмотрим подробно в следующий раз. Сейчас будем рассматривать шапку запроса как объявление параметров, значения которых будут подставляться из Python кода.

Теперь нужно создать Python функцию, которая будет выполнять запрос.

? Для этого в файле database/model.py добавляем:

def update_user_data(pool, user_id, first_name, last_name, age):     execute_update_query(         pool,         queries.update_user_info,         user_id=user_id,         first_name=first_name,         last_name=last_name,         age=age,     )

❗️ Для выполнения запросов созданы две функции-помощника — execute_update_query и execute_select_query. Первая подразумевает изменение содержимого базы (добавление, удаление, обновление) и не возвращает никаких данных, а вторая, наоборот, возвращает результат SELECT. В данном случае мы только обновляем базу, поэтому используем execute_update_query. Пример использования execute_select_query можно найти в репозитории.

❗️ Задача обновления данных пользователя может быть достигнута гораздо проще — заменой операции INSERT INTO на UPSERT INTO (документация) в уже существующем запросе add_user_info. Но здесь мне хотелось продемонстрировать полноценное заведение новых запросов в коде.

Заполняем код обработчиков

Вернёмся в файл bot/handlers.py, где мы оставили заготовку для обработчиков, и завершим их реализацию.

Каждый обработчик принимает на вход:

  • message: telebot.types.Message — обрабатываемое сообщение, объект класса Message.

  • bot: telebot.TeleBot — бот, объект класса TeleBot.

  • pool: ydb.SessionPoolпул для соединения в базой данных YDB.

Здесь снова нужно вспомнить про состояния пользователей, которые мы создали в файле bot/states.py, потому что нам предстоит назначать подходящие состояния пользователям по мере продвижения по сценарию или очищать их состояния после завершения сценария.

Для назначения и удаления состояния воспользуемся методами объекта bot класса TeleBot:

bot.set_state(user_id: int, state: StatesGroup, chat_id: int) -> None bot.delete_state(user_id: int, chat_id: int) -> None

Также нам потребуется передавать дополнительную информацию о пользователе между обработчиками. Запись и чтение дополнительной информации происходит с помощью контекст-менеджера:

with bot.retrieve_data(user_id, chat_id) as data:    data["field"] = "some value" # set any json-serializable value    some_variable = data["field"] # read value of "field" into variable some_variable

❗️ Переназначение состояния с помощью метода bot.set_state не изменяет дополнительную информацию.

❗️ Удаление состояния методом bot.delete_stateудаляет также дополнительную информацию.

❗️ Дополнительная информация оборачивается в json перед записью в хранилище состояний, поэтому дополнительная информация должна быть json-сериализуемой.

Напоминание — обновление кода в функции

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

О том, как заливать код в функцию можно прочитать в предыдущей статье в разделе Запускаем бота. Для обновления кода повторяйте Шаг 1 и Шаг 2 подраздела В ручном режиме или выполняйте файл create_function_version.sh из подраздела С помощью командной строки (Linux, MacOS), если вы выбрали обновление через командную строку.

Приступим к реализации обработчиков.

Обработчик команды /change_data

Правило, вызывающее этот обработчик, выглядит так:

Handler(callback=handlers.handle_change_data, commands=["change_data"]),

Когда пользователь присылает команду /change_data, первым делом нужно проверить, что данные про пользователя уже есть в базе, то есть пользователь зарегистрирован. Если пользователь ещё не зарегистрирован — отправим ему сообщение об этом и прекратим сценарий.

Данные в базе хранятся по ключу user_id. Информация об отправителе сообщения, в том числе идентификаторы пользователя и чата, содержится в аргументе message:

message.from_user.id

Получить сохранённые регистрационных данные пользователя можно с помощью функции get_user_info из database/model.py

А отправить сообщение — методом bot.send_message. Наш бот иногда использует специальную клавиатуру ReplyKeyboardMarkup для упрощения выбора ответа из списка, поэтому в случаях, когда клавиатура не требуется, будем заявлять об этом явно с помощью значения аргумента reply_markup=keyboards.EMPTY.

Таким образом, проверка на наличие сохранённых данных выглядит так:

current_data = db_model.get_user_info(pool, message.from_user.id)  if not current_data:     bot.send_message(         message.chat.id, texts.NOT_REGISTERED, reply_markup=keyboards.EMPTY     )     return

Иначе, если пользователь уже зарегистрирован, предлагаем ему выбрать одно из полей из списка texts.FIELD_LIST для изменения и выставляем ему статус ChangeDataState.select_field — чтобы следующее сообщение пользователя имело контекст.

Для выбора пользователем одного поля из списка нужно создать ReplyKeyboardMarkup — клавиатуру с опциями для следующего ответа пользователя. Для создания можно воспользоваться конструктором get_reply_keyboard(options, additional=None, **kwargs) из файла bot/keyboards.py. В options передаётся список строк с основным выбором, а в additional удобно передать технические команды, которые будут отображаться в нижнем ряду клавиатуры, в нашем случае — /cancel.

bot.set_state(     message.from_user.id, states.ChangeDataState.select_field, message.chat.id ) bot.send_message(     message.chat.id,     texts.SELECT_FIELD,     reply_markup=keyboards.get_reply_keyboard(texts.FIELD_LIST, ["/cancel"]), )

? Собираем обработчик целиком и заполняем плейсхолдер в bot/handlers.py:

@logged_execution def handle_change_data(message, bot, pool):     current_data = db_model.get_user_info(pool, message.from_user.id)      if not current_data:         bot.send_message(             message.chat.id, texts.NOT_REGISTERED, reply_markup=keyboards.EMPTY         )         return      bot.set_state(         message.from_user.id, states.ChangeDataState.select_field, message.chat.id     )     bot.send_message(         message.chat.id,         texts.SELECT_FIELD,         reply_markup=keyboards.get_reply_keyboard(texts.FIELD_LIST, ["/cancel"]),     )

Если сейчас залить код в функцию и написать боту команду /change_data, то в зависимости от наличия регистрации получим две разные реакции.

Скриншот — что увидит незарегистрированный пользователь

Скриншот — что увидит зарегистрированный пользователь

В базе данных в таблице states для зарегистрированного пользователя меняется хранимое состояние.

Скриншот — состояние пользователя после начала сценария
Состояние поменялось на ChangeDataState:select_filed

Состояние поменялось на ChangeDataState:select_filed
Скриншот — изначальные регистрационные данные в базе

That’s what she said!

Обработчик команды /cancel

В bot/structure.py мы добавили правило для обработки команды /cancel в любом состоянии нового сценария.

Handler(     callback=handlers.handle_cancel_change_data,     commands=["cancel"],     state=[         bot_states.ChangeDataState.select_field,         bot_states.ChangeDataState.write_new_value,     ], ),

В случае отмены нужно сбросить состояние пользователя и отправить ему сообщение-подтверждение о том, что процесс отменён. Это просто — все эти действия мы уже умеем делать!

? Заполняем плейсхолдер обработчика кодом в bot/handlers.py:

@logged_execution def handle_cancel_change_data(message, bot, pool):     bot.delete_state(message.from_user.id, message.chat.id)     bot.send_message(         message.chat.id,         texts.CANCEL_CHANGE,         reply_markup=keyboards.EMPTY,     )

Залейте обновлённый код в функцию и проверьте, что получилось.

Скриншот — /cancel во время выбора поля для изменения

Состояние пользователя в таблице states после выполнения команды /cancelснова должно стать пустым.

Обработчик выбора поля для изменения

Согласно правилу

Handler(     callback=handlers.handle_choose_field_to_change,     state=bot_states.ChangeDataState.select_field, ),

обработчик вызывается, когда пользователь присылает любое текстовое сообщение, находясь в состоянии ChangeDataState.select_field.

Несмотря на то, что на предыдущем шаге мы с помощью ReplyKeyboardMarkup ограничили выбор ответа, всё равно необходимо проверить корректность содержания сообщения, так как клавиатура не запрещает ввод сообщения в свободной форме. Поэтому первым делом убеждаемся, что текст сообщения — одно из доступных полей, а если нет — отправляем пользователю замечание и не меняем его состояние.

Текст сообщения можно получить так — message.text.

if message.text not in texts.FIELD_LIST:     bot.send_message(         message.chat.id,         texts.UNKNOWN_FIELD,         reply_markup=keyboards.get_reply_keyboard(texts.FIELD_LIST, ["/cancel"]),     )     return

Теперь, когда мы знаем, что в message.text содержится допустимое значение, можем сохранить его в дополнительную информацию, поменять статус пользователя на ChangeDataState.write_new_value и послать ему сообщение с предложением ввести новое значение для выбранного поля. Кнопка /cancel должна быть по-прежнему доступна.

Меняем статус и сохраняем выбранное поле:

bot.set_state(     message.from_user.id, states.ChangeDataState.write_new_value, message.chat.id ) with bot.retrieve_data(message.from_user.id, message.chat.id) as data:     data["field"] = message.text

Отправляем сообщение, подставляя в текст название выбранного поля, и оставляем в клавиатуре одну кнопку для отмены процесса:

bot.send_message(     message.chat.id,     texts.WRITE_NEW_VALUE.format(message.text),     reply_markup=keyboards.get_reply_keyboard(["/cancel"]), )

? Собираем весь код целиком и заполняем плейсхолдер в bot/handlers.py:

@logged_execution def handle_choose_field_to_change(message, bot, pool):     if message.text not in texts.FIELD_LIST:         bot.send_message(             message.chat.id,             texts.UNKNOWN_FIELD,             reply_markup=keyboards.get_reply_keyboard(texts.FIELD_LIST, ["/cancel"]),         )         return      bot.set_state(         message.from_user.id, states.ChangeDataState.write_new_value, message.chat.id     )     with bot.retrieve_data(message.from_user.id, message.chat.id) as data:         data["field"] = message.text      bot.send_message(         message.chat.id,         texts.WRITE_NEW_VALUE.format(message.text),         reply_markup=keyboards.get_reply_keyboard(["/cancel"]),     )

Залейте новый код в функцию и проверьте, что получилось! Обратите внимание, что команда /cancel работает и на этапе после выбора поля без каких-либо дополнительных действий.

Скриншот — выбор поля last_name

Скриншот — отмена после выбора поля last_name

В таблице состояний после выбора поля произошли изменения: обновилось название состояния и добавилась дополнительная информация — название изменяемого поля.

Скриншот — состояние пользователя после выбора поля

Обработчик нового значения поля

Правило вызова обработчика — любое текстовое сообщение от пользователя в состоянии ChangeDataState.write_new_value:

Handler(     callback=handlers.handle_save_changed_data,     state=bot_states.ChangeDataState.write_new_value, ),

В сообщении должно содержаться новое значение для поля, но обработчик handle_save_changed_data уже не знает, какое именно поле нужно обновить, поэтому сначала получим название поля из сохранённой дополнительной информации:

with bot.retrieve_data(message.from_user.id, message.chat.id) as data:     field = data["field"]

Если изменяемое поле — имя или фамилия, то никаких ограничений на новое значение не накладывается. Однако в случае возраста необходимо проверить, что пользователь прислал число. Если это не так, то нужно сообщить об этом пользователю и оставить его в прежнем состоянии. message.text — это всегда строка, поэтому воспользуемся методом isdigit() и приведём значение к числу, если меняется возраст.

new_value = message.text  if field == "age" and not new_value.isdigit():     bot.send_message(         message.chat.id,         texts.AGE_IS_NOT_NUMBER,         reply_markup=keyboards.get_reply_keyboard(["/cancel"]),     )     return elif field == "age":     new_value = int(new_value)

На этом этапе переменная new_value содержит новое значение в том формате, в котором мы готовы записать его в базу: имя и фамилия такие, как ввёл пользователь, а возраст — число типа int. Осталось записать изменения в базу, сообщить пользователю об успехе и очистить его состояние.

Записываем изменения в базу с помощью заранее подготовленной функции update_user_data:

bot.delete_state(message.from_user.id, message.chat.id) current_data = db_model.get_user_info(pool, message.from_user.id) current_data[field] = new_value db_model.update_user_data(pool, **current_data)

Отправляем сообщение о завершении процесса с пустой клавиатурой:

bot.send_message(     message.chat.id,     texts.CHANGE_DATA_DONE,     reply_markup=keyboards.EMPTY, )

? Собираем код обработчика целиком в bot/handlers.py:

@logged_execution def handle_save_changed_data(message, bot, pool):     with bot.retrieve_data(message.from_user.id, message.chat.id) as data:         field = data["field"]      new_value = message.text      if field == "age" and not new_value.isdigit():         bot.send_message(             message.chat.id,             texts.AGE_IS_NOT_NUMBER,             reply_markup=keyboards.get_reply_keyboard(["/cancel"]),         )         return     elif field == "age":         new_value = int(new_value)      bot.delete_state(message.from_user.id, message.chat.id)     current_data = db_model.get_user_info(pool, message.from_user.id)     current_data[field] = new_value     db_model.update_user_data(pool, **current_data)      bot.send_message(         message.chat.id,         texts.CHANGE_DATA_DONE,         reply_markup=keyboards.EMPTY,     )

Зальём окончательную версию кода в функцию и протестируем сценарий.

Скриншот — успешное изменение фамилии

Скриншот — 2 попытки поменять возраст

Состояние пользователя по окончанию процесса должно оказаться пустым, а данные в базе — обновиться. Также обновлённые данные должны показываться после команды /show_data.

Скриншот — обновлённые данные в базе

I say, I say, I say, I’ll sit on you!

Успех! Подводим итоги.

В этой статье мы научились создавать обработчики сообщений, регистрировать их и добавлять сценарии в бота. Для этого создавали новые наборы состояний пользователей. По пути добавляли тексты для пользователя и обогатили интерфейс общения с базой данных новым запросом.

Я уверена, что вы справились с инструкцией, но если есть необходимость подсмотреть в полный код с реализаций — загляните в ветку репозитория. Мой бот  YDB serverless example также поддерживает команду /change_data, хотя она и отсутствует в меню – это тайная функциональность, о которой знают только избранные!

Полученных умений достаточно, чтобы реализовать диалогового бота, который будет делать то, что вам захочется. Создавайте новые таблицы в базе данных, изучайте документацию TeleBot, добавляйте функциональность и не забывайте смотреть во вкладку Логи функции для поиска ошибок.

Скорее всего у вас уже есть море своих идей, но, если хотите ещё потренироваться на шаблоне, – реализуйте игрушечную аутентификацию: добавьте к процессу регистрации задание кодового слова, чтобы показывать данные /show_data и менять данные /change_data только после его проверки.

Удачи и хороших вам ботов!

Что дальше?

Несмотря на то, что в этой статье мы добавили в бота целый сценарий, я всё ещё не вдавалась в подробности реализации шаблона, которые позволили нам это сделать. В следующей статье бота менять уже не будем, а поговорим о том, как устроен шаблон: о деталях пользовательских стейтов, логировании и корректном взаимодействии с базой данных. После этого хочу рассказать об end-to-end тестировании бота. Kiitos lukemisesta ja nähdään!


ссылка на оригинал статьи https://habr.com/ru/articles/753596/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *