Введение
В мире разработки чат-ботов на платформе Telegram создание интерактивных опросников может быть задачей нетривиальной. В этом посте я поделюсь системой, которую разработал на основе библиотеки aiogram 2.x
. Она позволяет легко создавать и обрабатывать опросники с текстовыми ответами и вариантами выбора, а также управлять состояниями бота. В статье мы разберем ключевые аспекты реализации, включая обработку состояний, сохранение ответов и управление сообщениями.
Основная идея
Система основана на использовании словаря, где каждый ключ представляет собой шаг опросника. В зависимости от типа вопроса (варианты ответов или текстовое поле) обработчик будет корректно реагировать на взаимодействие с пользователем. Для управления состояниями используется aiogram.dispatcher.FSMContext
.
Сама идея родилась когда мне прилетела задача перелопатить опросники в легаси-боте, в которых каждый шаг был определен явно, что привело бы к долгим часам сверки состояний и замен step_13 на step_12.
Структура опросника
Опросник представлен в виде словаря, где ключами являются номера вопросов, а значениями — словари, описывающие текст вопроса, варианты ответов и тип вопроса. Пример структуры:
test_survey_payload = {
1: {
'text': 'Вопрос 1 с вариантами',
'options': custom_kb({
'вариант 1': 'test_survey 1:1',
'вариант 2': 'test_survey 1:2',
'вариант 3': 'test_survey 1:3',
}),
'type': 'vars',
},
2: {
'text': 'Вопрос 2 с текстом',
'options': custom_kb({
'назад': 'test_survey back'
}),
'type': 'text',
},
# ...
}
Здесь custom_kb
— функция для создания кастомной клавиатуры (inline keyboard), а ключи type
определяют, как бот должен обрабатывать ответы.
def custom_kb(button_data: dict) -> InlineKeyboardMarkup:
"""
Creates custom keyboard from dict {'button_text': 'button_callback'}
"""
returned_keyboard = InlineKeyboardMarkup(row_width=1)
for key, value in button_data.items():
button = InlineKeyboardButton(text=key, callback_data=value)
returned_keyboard.add(button)
return returned_keyboard
Определение состояний
Для управления состояниями мы используем StatesGroup
из aiogram. Это позволяет определить шаги опросника, которые бот будет проходить последовательно:
from aiogram.dispatcher.filters.state import StatesGroup, State
class TestSurvey(StatesGroup):
step_1 = State()
step_2 = State()
step_3 = State()
step_4 = State()
step_5 = State()
finish = State()
Обработчик опросника
Основной обработчик отвечает за взаимодействие с пользователем в процессе прохождения опросника. Он принимает и обрабатывает как текстовые сообщения, так и нажатия на кнопки:
@dp.message_handler(state=TestSurvey)
@dp.callback_query_handler(lambda c: c.data.startswith('test_survey'), state=TestSurvey)
async def test_survey(event: types.CallbackQuery | types.Message, state: FSMContext):
current_state = await state.get_state()
step = get_step(current_state, test_survey_payload)
if not is_answer_correct_type(event, step, test_survey_payload):
return
await delete_prew_messages(state, event)
if isinstance(event, types.CallbackQuery) and event.data == 'test_survey back':
await TestSurvey.previous()
current_state = await state.get_state()
step = get_step(current_state, test_survey_payload)
await process_new_question(step - 1, state, test_survey_payload, event)
return
if await process_save_answer(state, current_state, step, test_survey_payload, event) == 'finish':
return
await process_new_question(step, state, test_survey_payload, event)
await TestSurvey.next()
Основные функции
process_save_answer
Эта функция сохраняет ответы пользователя и завершает опросник, если достигнут финальный шаг.
async def process_save_answer(state: FSMContext, current_state: str, step: int, test_survey_payload: dict,
event: types.CallbackQuery | types.Message):
answer = event.data if isinstance(event, types.CallbackQuery) else event.text
if current_state.split(':')[-1] != 'finish':
step -= 1
async with state.proxy() as data:
question = test_survey_payload.get(step)
data[step] = {
'question': question.get('text') if question else 'Начало опроса',
'answer': answer
}
if current_state.split(':')[-1] == 'finish':
del data['message_to_delete_id']
await state.finish()
return 'finish', dict(data)
return 'next', ''
process_new_question
Отправляет новый вопрос пользователю, обновляя прогресс и сохраняя ID сообщения для последующего удаления (для прогресс-бара я использую эмодзи):
async def process_new_question(step: int, state: FSMContext, test_survey_payload: dict,
event: types.CallbackQuery | types.Message = None, show_progres: bool = True):
payload = test_survey_payload.get(step)
message = event.message if isinstance(event, types.CallbackQuery) else event
if show_progres:
total_steps = len(test_survey_payload)
current_step = f'{"⬜" * step}{"⬛" * (total_steps - step)}'
answer_text = f'{current_step}\n\n{payload.get("text")}'
else:
answer_text = payload.get('text')
if payload.get('options'):
answer = await message.answer(answer_text, reply_markup=payload.get('options'))
else:
answer = await message.answer(answer_text)
async with state.proxy() as data:
data['message_to_delete_id'] = answer.message_id
Проверка корректности ответа
Для каждого шага опросника проверяется, является ли ответ корректным в зависимости от типа вопроса:
def is_answer_correct_type(event: types.CallbackQuery | types.Message, step: int, test_survey_payload: dict) -> bool:
question = test_survey_payload.get(step - 1)
if question:
question_type = question.get('type')
if question_type == 'vars' and isinstance(event, types.Message):
return False
return True
Удаление предыдущих сообщений
Удаляет предыдущие сообщения опросника, чтобы интерфейс оставался чистым:
async def delete_prew_messages(state: FSMContext, event: types.CallbackQuery | types.Message):
message = event.message if isinstance(event, types.CallbackQuery) else event
await message.delete()
async with state.proxy() as data:
if data.get('message_to_delete_id'):
try:
await bot.delete_message(chat_id=message.chat.id, message_id=data['message_to_delete_id'])
except (MessageCantBeDeleted, MessageToDeleteNotFound):
pass
Пример использования
Теперь рассмотрим, как можно использовать созданную систему. Предположим, что мы хотим создать опросник для сбора отзывов о продукте. У нас есть несколько вопросов, на которые пользователи могут ответить:
feedback_survey_payload = {
1: {
'text': 'Оцените качество нашего продукта:',
'options': custom_kb({
'Отлично': 'feedback_survey 1:1',
'Хорошо': 'feedback_survey 1:2',
'Удовлетворительно': 'feedback_survey 1:3',
'Плохо': 'feedback_survey 1:4',
}),
'type': 'vars',
},
2: {
'text': 'Что вам понравилось в нашем продукте?',
'options': custom_kb({
'назад': 'feedback_survey back'
}),
'type': 'text',
},
3: {
'text': 'Как мы можем улучшить наш продукт?',
'options': custom_kb({
'назад': 'feedback_survey back'
}),
'type': 'text',
},
4: {
'text': 'Спасибо за ваши ответы!',
'type': 'text',
}
}
Заключение
В данной статье мы рассмотрели, как можно легко и гибко реализовать систему опросников на основе библиотеки aiogram 2.x
. Эта система может быть адаптирована под различные нужды, будь то сбор отзывов, проведение опросов или даже обучение через бот. Надеюсь, что этот подход поможет вам в разработке собственных проектов на Python!
Примечание: В статье я использовал библиотеку aiogram
, поскольку она является широко используемой и хорошо документированной для создания ботов на Python. Версия 2.x выбрана по принуждению, так как приходится работать с легаси.
d2d8
Забавно, что chatgpt и amazon q генерят код для aiogram версии 2, а вот с тройкой не дружат совсем.
WhLeeto Автор
Одинаково дружат и с 2.х и с 3.x