Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение17 мин
Количество просмотров14K

В предыдущей статье я рассказала, как быстро создать инфраструктуру для диалогового бота на основе 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!

Теги:
Хабы:
Всего голосов 6: ↑5 и ↓1+5
Комментарии0

Публикации

Истории

Работа

Python разработчик
119 вакансий
Data Scientist
78 вакансий

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань