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

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

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

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

  2. — вы здесь —

Подготовка

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

  • Скачать код из репозитория 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!

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