Quizbot, or questionnaire bot for Telegram. Using python, aiogram.

Introduction

Hello, in this case I will show you how, I made my quiz bot.
I will provide the source code of the python and bash scripts. I will explain the structure and purpose of each file and the handler. And of course you can interact with my ready-made bot.
Alas, when I made it, I did not intend it to be multilingual. And when I realized it, I didn’t dare to redo everything. Yes and The essence of this bot is not multilingual. But next time, I will definitely make a multilingual bot. So it will be easier for all non-Russian speakers.

An example of creating a Telegram survey bot.

Quiz bot structure

My bot has a linear/sequential structure. With some 'floating' handlers. What do I mean by this?
Since I have a survey bot. And he must ask questions sequentially, one by one. Then the handlers that process user responses must be called sequentially, one after another.
By 'floating' handlers, I meant those functions that can be called at any time during communication with the bot.
    There are 5 of them.
  • /start ➩ The bot starts. Welcome message.
  • /help ➩ Shows available commands
  • /menu ➩ Shows available commands
  • /settings ➩ Configure result output
  • /result ➩ Displays the result of people's surveys
    Let's look at the structure of the survey bot project.
  • txt file.env ➩ File containing the bot token. When downloading from the repository, it will not be there. You need to do it yourself. Take a look at README.md file
  • js filedata.json ➩ Stores data about all users. Database. Yes, without MySQL and other SQL for now. Created automatically.
  • markdown fileresult_template.md ➩ Template that is used to display information.
  • txt filerequirements.txt ➩ Necessary packages for working in a virtual environment
  • python filemain.py ➩ Entry point. Contains all request handlers
  • python fileconfig.py ➩ Configuration file. Contains global variables.
  • python fileformaters.py ➩ Functions, the essence of which is to be able to change the output of the results in the message.
  • python fileplacers.py ➩ Functions that interact with the template. Insert data into it.
  • python fileutils.py ➩ Various utilities.

/start command

Welcomes new arrivals. Creates buttons in a message that invites you to take a survey.
start command for telegram bot

@bot_dispatcher.message(CommandStart())
async def command_start_handler(message: Message) -> None:
	builder = InlineKeyboardBuilder()
	for menu_item in menu:
	builder.button(text=menu_item, callback_data="menu_"+menu_item)
	builder.adjust(2)
	await message.answer(f"Привет, *{message.from_user.first_name}*\!\nЯ бот опросник и я могу _провести опрос_ и _показать общие результаты_ опросов других\. \nТакже ты можешь _настроить тип и формат_ выводимой информации\.", reply_markup=builder.as_markup(),  parse_mode=ParseMode.MARKDOWN_V2)
		

Age of respondent

Asks for age and creates keyboard buttons.
your age telegram bot button

@bot_dispatcher.callback_query(F.data == "menu_"+menu[0])
async def your_age_handler(callback: types.CallbackQuery) -> None:
	builder = ReplyKeyboardBuilder()
	for age in ages:
	builder.add(types.KeyboardButton(text=age))
	builder.adjust(1)
	await callback.message.answer("Твой возраст? ", reply_markup=builder.as_markup(one_time_keyboard=True))
		

Country of interviewee

Creates a dictionary user_values, and immediately assigns it the age and user id.
Creates buttons on the keyboard with country emojis on them. And of course he asks the country of the person being interviewed.
your country telegram button

@bot_dispatcher.message(F.text.startswith('от') | F.text.contains('до'))
async def your_country_handler(message: Message) -> None:
	user_values = {
		"user_id": message.from_user.id,
		"age": message.text,
		"country": None,
		"sex": None,
		"work": None,
		"car": None,
		"is_complete": False,
	}
	with open(f"user_{message.from_user.id}.json", "w", encoding="utf-8") as file:
	json.dump(user_values, file)

	builder = ReplyKeyboardBuilder()
	for contry in countries:
	builder.add(types.KeyboardButton(text=f"|{contry['emoji']} {contry['name']}|"))
	builder.adjust(5)

	await message.answer("Твоя страна? ", reply_markup=builder.as_markup(one_time_keyboard=True))
		

Gender of respondent

Catches the text of the selected country and adds it to user_values.
Asks for gender, and creates buttons on the keyboard with stubs ♂ and ♀
your sex telegram button

@bot_dispatcher.message(F.text.startswith('|') | F.text.endswith('|'))
async def your_sex_handler(message: Message) -> None:
	start_pos = message.text.find(' ') + 1
	await update_user(message.from_user.id, 'country', message.text[start_pos:])

	builder = ReplyKeyboardBuilder()
	builder.add(types.KeyboardButton(text="♂ Мужчина"))
	builder.add(types.KeyboardButton(text="♀ Женщина"))
	await message.answer("Твой пол? ", reply_markup=builder.as_markup(one_time_keyboard=True))
		

Interviewee's work

Processes text from the previous handler, your_sex_handler, adding gender to the user.
Asks about work, creates buttons in the message.
your labor telegram button

@bot_dispatcher.message(F.text.startswith('♂') | F.text.startswith('♀'))
async def your_work_handler(message: Message) -> None:
	start_pos = message.text.find(' ') + 1
	await update_user(message.from_user.id, 'sex', message.text[start_pos:])

	builder = InlineKeyboardBuilder()
	for work in works:
	builder.button(text=work, callback_data="work_" + work)
	builder.adjust(3)

	await message.answer(
		"Где работаешь, то есть, какая сфера ?",
		reply_markup=builder.as_markup(one_time_keyboard=True)
	)
		

Interviewee's car

Stores the user's job value in user_values
He asks if there is a car?
Creates buttons on the keyboard
Do you have a car telegram button

@bot_dispatcher.callback_query(F.data.startswith("work_"))
async def your_car_handler(callback: types.CallbackQuery) -> None:
	start_pos = callback.data.find('_') + 1
	await update_user(callback.from_user.id, 'work', callback.data[start_pos:])

	builder = ReplyKeyboardBuilder()
	for car in cars:
	builder.add(types.KeyboardButton(text=car))
	await callback.message.answer("Есть ли у тебя машина ?", reply_markup=builder.as_markup(one_time_keyboard=True))
		

End of survey

Adds information about the machine to the user.
After which, it saves the user to the data.json database. Unless of course the user polled completely, rather than accidentally calling a handler in the middle.
It might be worth adding a check for duplicate users. But I didn't do that because I wanted to give users the opportunity to participate several times.
Displays a standard message and a button in the message with the ability to show the result.
end of quiz telegram bot

@bot_dispatcher.message(F.text.contains("машин"))
async def end_quiz_handler(message: Message) -> None:
    await update_user(message.from_user.id, 'car', message.text)

    if await is_user_completed(message.from_user.id):
        with open(f"user_{message.from_user.id}.json", "r",  encoding="utf-8") as file:
            user_values = json.load(file)
        if not os.path.exists(FILE):
            with open(FILE, "a", encoding="utf-8") as file:
                file.write("[]")
        with open(FILE, "r", encoding="utf-8") as file:
            users = json.load(file)
            users.append(user_values)
        with open(FILE, "w", encoding="utf-8") as file:
            json.dump(users, file)

    os.remove(f"user_{message.from_user.id}.json")

    builder = InlineKeyboardBuilder()
    builder.button(text="Результат", callback_data="menu_" + menu[1])
    await message.answer("Опрос закончен.\nСпасибо за участие.")
    await message.answer("Теперь можно посмотреть на результат.", reply_markup=builder.as_markup())
		

Main Menu

This handler can be called via the commands */help* or */menu*.
He doesn't do anything special. Create buttons in the message. We display a standard message.
help command of telegram bot

@bot_dispatcher.message(Command('help', 'menu'))
async def menu_handler(message: Message) -> None:
    builder = InlineKeyboardBuilder()
    for menu_item in menu:
        builder.button(text=menu_item, callback_data="menu_"+menu_item)
    builder.adjust(2)
    await message.answer(
        """
        Комманды для ручного ввода:
        \t*/help* или */menu* ➩ Это меню\.
        \t*/start* ➩ Общее меню опроса\.
        \t*/result* ➩ Результаты опроса участников\.
        \t*/settings* ➩ Настройки опросника\.
        """,
        parse_mode=ParseMode.MARKDOWN_V2)
    await message.answer("Или как InlineButtons", reply_markup=builder.as_markup())
		

Settings

setting command by telegram bot
This handler is divided into 4 functions. One, *setting_update_format_numbers*, directly implements the functionality of changing the format of the displayed information.

@bot_dispatcher.callback_query(F.data.contains("format_setting_"))
async def setting_update_format_numbers(callback: types.CallbackQuery):
    builder = InlineKeyboardBuilder()
    for set_menu in format_settings_menu:
        if set_menu in callback.data:
            builder.button(text="+ " + set_menu, callback_data="format_setting_"+set_menu)
            global division_type
            division_type = set_menu
        else:
            builder.button(text=set_menu, callback_data="format_setting_"+set_menu)
    builder.adjust(2)
    await callback.bot.edit_message_reply_markup(chat_id=callback.message.chat.id, message_id=callback.message.message_id, reply_markup=builder.as_markup())
		
The other one, *setting*, shows possible settings, creates the corresponding buttons.

async def setting(message: Message) -> None:
    builder = InlineKeyboardBuilder()
    for set_menu in format_settings_menu:
        builder.button(text=set_menu, callback_data="format_setting_"+set_menu)
    builder.adjust(2)
    await message.answer("Настройки формата", reply_markup=builder.as_markup())
		
And the other two are implemented via *callback_query* and *message*. And they both cause *setting*. This was done in order to be able to work with this function, both with a command and with a button.

@bot_dispatcher.message(Command("settings"))
async def setting_command_handler(message: Message) -> None:
    await setting(message)
		

@bot_dispatcher.callback_query(F.data == "menu_"+menu[2])
async def setting_callback_handler(callback: types.CallbackQuery):
    await setting(callback.message)
		

Result

There are two handlers. One for processing the "/result" command, the other for calling through buttons. Both have the same functionality *result*.
result command telegram bot

@bot_dispatcher.message(Command("result"))
async def result_command_handler(message: Message) -> None:
    await result(message)
		

@bot_dispatcher.callback_query(F.data == "menu_"+menu[1])
async def result_callback_handler(callback: types.CallbackQuery) -> None:
    await result(callback.message)
		
The *result* handler itself is responsible for formatting result_template.md. Inserts data using functions from *placers.py*. And the format is changed using *formaters.py*

async def result(message: Message) -> None:
    if not os.path.exists(FILE):
        builder = InlineKeyboardBuilder()
        builder.button(text="Начать", callback_data="menu_" + menu[0])
        await message.answer("Извини, база данных пуста. Пройди опрос первым !", reply_markup=builder.as_markup())
        return

    with open(FILE, "r", encoding="utf-8") as file:
        users = json.load(file)
    with open("result_template.md", "r", encoding="utf-8") as file:
        template = file.read()
    # Define result output
    result = template.replace("divisiontype", division_type)
    # Result output in absolute numbers
    if division_type == format_settings_menu[0]:
        result = place_age(result, users, in_absolute)
        result = place_country(result, users, in_absolute)
        result = place_sex(result, users, in_absolute)
        result = place_work(result, users, in_absolute)
        result = place_car(result, users, in_absolute)
    else:
        result = place_age(result, users, in_percent)
        result = place_country(result, users, in_percent)
        result = place_sex(result, users, in_percent)
        result = place_work(result, users, in_percent)
        result = place_car(result, users, in_percent)

    await message.answer(result, parse_mode=ParseMode.MARKDOWN_V2)
		

Download a ready-made telegram bot, or copy the code

You can download the bot from my repository. TimQuizzer-bot
Or you can view the source code here and copy the parts you are interested in

main.py

[Expand] Chevron

import asyncio
import logging
import sys
import json
import os

from aiogram import F
from aiogram import Bot, Dispatcher, types
from aiogram.enums import ParseMode
from aiogram.filters import CommandStart, Command
from aiogram.types import Message
from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder

from config import TOKEN, FILE, menu, division_type, format_settings_menu, cars, ages, sexes, works, countries
from utils import update_user, is_user_completed
from formaters import in_absolute, in_percent
from placers import place_age, place_car, place_sex, place_work, place_country

bot_dispatcher = Dispatcher()


@bot_dispatcher.message(Command('help', 'menu'))
async def menu_handler(message: Message) -> None:
    builder = InlineKeyboardBuilder()
    for menu_item in menu:
        builder.button(text=menu_item, callback_data="menu_"+menu_item)
    builder.adjust(2)
    await message.answer(
        """
        Комманды для ручного ввода:
        \t*/help* или */menu* ➩ Это меню\.
        \t*/start* ➩ Общее меню опроса\.
        \t*/result* ➩ Результаты опроса участников\.
        \t*/settings* ➩ Настройки опросника\.
        """,
        parse_mode=ParseMode.MARKDOWN_V2)
    await message.answer("Или как InlineButtons", reply_markup=builder.as_markup())


@bot_dispatcher.message(CommandStart())
async def command_start_handler(message: Message) -> None:
    builder = InlineKeyboardBuilder()
    for menu_item in menu:
        builder.button(text=menu_item, callback_data="menu_"+menu_item)
    builder.adjust(2)
    await message.answer(f"Привет, *{message.from_user.first_name}*\!\nЯ бот опросник и я могу _провести опрос_ и _показать общие результаты_ опросов других\. \nТакже ты можешь _настроить тип и формат_ выводимой информации\.", reply_markup=builder.as_markup(),  parse_mode=ParseMode.MARKDOWN_V2)


@bot_dispatcher.callback_query(F.data == "menu_"+menu[0])
async def your_age_handler(callback: types.CallbackQuery) -> None:
    builder = ReplyKeyboardBuilder()
    for age in ages:
        builder.add(types.KeyboardButton(text=age))
    builder.adjust(1)
    await callback.message.answer("Твой возраст? ", reply_markup=builder.as_markup(one_time_keyboard=True))


@bot_dispatcher.message(F.text.startswith('от') | F.text.contains('до'))
async def your_country_handler(message: Message) -> None:
    user_values = {
            "user_id": message.from_user.id,
            "age": message.text,
            "country": None,
            "sex": None,
            "work": None,
            "car": None,
            "is_complete": False,
    }
    with open(f"user_{message.from_user.id}.json", "w", encoding="utf-8") as file:
        json.dump(user_values, file)

    builder = ReplyKeyboardBuilder()
    for contry in countries:
        builder.add(types.KeyboardButton(text=f"|{contry['emoji']} {contry['name']}|"))
    builder.adjust(5)

    await message.answer("Твоя страна? ", reply_markup=builder.as_markup(one_time_keyboard=True))


@bot_dispatcher.message(F.text.startswith('|') | F.text.endswith('|'))
async def your_sex_handler(message: Message) -> None:
    start_pos = message.text.find(' ') + 1
    await update_user(message.from_user.id, 'country', message.text[start_pos:])

    builder = ReplyKeyboardBuilder()
    builder.add(types.KeyboardButton(text="♂ Мужчина"))
    builder.add(types.KeyboardButton(text="♀ Женщина"))
    await message.answer("Твой пол? ", reply_markup=builder.as_markup(one_time_keyboard=True))


@bot_dispatcher.message(F.text.startswith('♂') | F.text.startswith('♀'))
async def your_work_handler(message: Message) -> None:
    start_pos = message.text.find(' ') + 1
    await update_user(message.from_user.id, 'sex', message.text[start_pos:])

    builder = InlineKeyboardBuilder()
    for work in works:
        builder.button(text=work, callback_data="work_" + work)
    builder.adjust(3)

    await message.answer(
            "Где работаешь, то есть, какая сфера ?",
            reply_markup=builder.as_markup(one_time_keyboard=True)
    )


@bot_dispatcher.callback_query(F.data.startswith("work_"))
async def your_car_handler(callback: types.CallbackQuery) -> None:
    start_pos = callback.data.find('_') + 1
    await update_user(callback.from_user.id, 'work', callback.data[start_pos:])

    builder = ReplyKeyboardBuilder()
    for car in cars:
        builder.add(types.KeyboardButton(text=car))
    await callback.message.answer("Есть ли у тебя машина ?", reply_markup=builder.as_markup(one_time_keyboard=True))


@bot_dispatcher.message(F.text.contains("машин"))
async def end_quiz_handler(message: Message) -> None:
    await update_user(message.from_user.id, 'car', message.text)

    if await is_user_completed(message.from_user.id):
        with open(f"user_{message.from_user.id}.json", "r",  encoding="utf-8") as file:
            user_values = json.load(file)
        if not os.path.exists(FILE):
            with open(FILE, "a", encoding="utf-8") as file:
                file.write("[]")
        with open(FILE, "r", encoding="utf-8") as file:
            users = json.load(file)
            users.append(user_values)
        with open(FILE, "w", encoding="utf-8") as file:
            json.dump(users, file)

    os.remove(f"user_{message.from_user.id}.json")

    builder = InlineKeyboardBuilder()
    builder.button(text="Результат", callback_data="menu_" + menu[1])
    await message.answer("Опрос закончен.\nСпасибо за участие.")
    await message.answer("Теперь можно посмотреть на результат.", reply_markup=builder.as_markup())


@bot_dispatcher.callback_query(F.data == "menu_"+menu[2])
async def setting_callback_handler(callback: types.CallbackQuery):
    await setting(callback.message)


@bot_dispatcher.message(Command("settings"))
async def setting_command_handler(message: Message) -> None:
    await setting(message)


@bot_dispatcher.callback_query(F.data.contains("format_setting_"))
async def setting_update_format_numbers(callback: types.CallbackQuery):
    builder = InlineKeyboardBuilder()
    for set_menu in format_settings_menu:
        if set_menu in callback.data:
            builder.button(text="+ " + set_menu, callback_data="format_setting_"+set_menu)
            global division_type
            division_type = set_menu
        else:
            builder.button(text=set_menu, callback_data="format_setting_"+set_menu)
    builder.adjust(2)
    await callback.bot.edit_message_reply_markup(chat_id=callback.message.chat.id, message_id=callback.message.message_id, reply_markup=builder.as_markup())


@bot_dispatcher.message(Command("result"))
async def result_command_handler(message: Message) -> None:
    await result(message)


@bot_dispatcher.callback_query(F.data == "menu_"+menu[1])
async def result_callback_handler(callback: types.CallbackQuery) -> None:
    await result(callback.message)


async def setting(message: Message) -> None:
    builder = InlineKeyboardBuilder()
    for set_menu in format_settings_menu:
        builder.button(text=set_menu, callback_data="format_setting_"+set_menu)
    builder.adjust(2)
    await message.answer("Настройки формата", reply_markup=builder.as_markup())


async def result(message: Message) -> None:
    if not os.path.exists(FILE):
        builder = InlineKeyboardBuilder()
        builder.button(text="Начать", callback_data="menu_" + menu[0])
        await message.answer("Извини, база данных пуста. Пройди опрос первым !", reply_markup=builder.as_markup())
        return

    with open(FILE, "r", encoding="utf-8") as file:
        users = json.load(file)
    with open("result_template.md", "r", encoding="utf-8") as file:
        template = file.read()
    # Define result output
    result = template.replace("divisiontype", division_type)
    # Result output in absolute numbers
    if division_type == format_settings_menu[0]:
        result = place_age(result, users, in_absolute)
        result = place_country(result, users, in_absolute)
        result = place_sex(result, users, in_absolute)
        result = place_work(result, users, in_absolute)
        result = place_car(result, users, in_absolute)
    else:
        result = place_age(result, users, in_percent)
        result = place_country(result, users, in_percent)
        result = place_sex(result, users, in_percent)
        result = place_work(result, users, in_percent)
        result = place_car(result, users, in_percent)

    await message.answer(result, parse_mode=ParseMode.MARKDOWN_V2)


async def main() -> None:
    bot = Bot(TOKEN, parse_mode=ParseMode.HTML)
    await bot_dispatcher.start_polling(bot)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, stream=sys.stdout)
    asyncio.run(main())
		

config.py

[Expand] Chevron

with open(".env", "r") as file:
    buffer = file.read()
    line_pos = buffer.find("BOT_TOKEN")
    TOKEN = buffer[buffer.find("=", line_pos) + 1:buffer.find("\n", line_pos)]
FILE = "data.json"
# Supported countries
countries = [
            {'emoji': '🇺🇸','name': 'США'}, {'emoji': '🇷🇺','name': 'Россия'}, 
            {'emoji': '🇵🇱','name': 'Польша'}, {'emoji': '🇨🇳','name': 'Китай'}, 
            {'emoji': '🇦🇽','name': 'Швеция'}, {'emoji': '🇦🇲','name': 'Армения'}, 
            {'emoji': '🇨🇿','name': 'Чехия'}, {'emoji': '🇩🇰','name': 'Дания'}, 
            {'emoji': '🇯🇴','name': 'Палестина'}, {'emoji': '🇪🇪','name': 'Эстония'}, 
            {'emoji': '🇪🇬','name': 'Египет'}, {'emoji': '🇧🇾','name': 'Беларусь'}, 
            {'emoji': '🇧🇷','name': 'Бразилия'}, {'emoji': '🇨🇦','name': 'Канада'}, 
            {'emoji': '🇫🇮','name': 'Финляндия'}, {'emoji': '🇫🇷','name': 'Франция'}, 
            {'emoji': '🇬🇷','name': 'Греция'}, {'emoji': '🇩🇪','name': 'Германия'}, 
            {'emoji': '🇬🇪','name': 'Грузия'}, {'emoji': '🇧🇬','name': 'Болгария'}, 
            {'emoji': '🇷🇴','name': 'Румыния'}, {'emoji': '🇹🇷','name': 'Турция'}, 
            {'emoji': '🇮🇹','name': 'Италия'}, {'emoji': '🇸🇰','name': 'Словакия'}, 
            {'emoji': '🇸🇦','name': 'Саудовская аравия'}
]
# Supported types of jobs
works = [
        "IT", "Завод", "Финансы, банкинг",
        "Строительство", "Добыча ископаемых",
        "Обслуживание", "Медицина", "Работа с персоналом", "Преподавание"
]
# Age of people
ages = [
    "от 0 до 18",
    "от 18 до 25",
    "от 26 до 35",
    "от 36 до 50",
    "от 50 до смерти",
]
sexes = [
    "Мужчина",
    "Женщина"
]
cars = [
    "Есть машина",
    "Нет машины"
]
menu = [
    "Начать опрос",
    "Показать статистику",
    "Настроить статистику"
]
format_settings_menu = [
    "в абсолютных числах",
    "в процентах",
]
division_type = format_settings_menu[0]
		

formaters.py

[Expand] Chevron

def in_percent(value: int, amount: int, suffix: str = "", prefix: str = "\t") -> str:
    return prefix + str(round(value * 100 / amount)) + "%" + suffix


def in_absolute(value: int, amount: int, suffix: str = "", prefix: str = "\t") -> str:
    return prefix + str(value) + suffix
		

placers.py

[Expand] Chevron

from config import ages, countries, sexes, works, cars


def place_age(template: str, users: list, formater: callable) -> str:
    for age in ages:
        age_num = 0
        for user in users:
            if age in user['age']:
                age_num += 1
        pos_to_insert = template.find("\n", template.find(age))
        template = template[:pos_to_insert] + formater(age_num, len(users)) + template[pos_to_insert:]
    return template


def place_country(template: str, users: list, formater: callable) -> str:
    for country in countries:
        country_num = 0
        for user in users:
            if country["name"] in user['country']:
                country_num += 1
        if country_num > 0:
            pos_to_insert = template.find('\n', template.find("Страна"))
            template = template[:pos_to_insert] + formater(country_num, len(users), prefix="\n\t• " + country["emoji"] + " " + country["name"] + "\t") + template[pos_to_insert:]
    return template


def place_sex(template: str, users: list, formater: callable) -> str:
    for sex in sexes:
        sex_num = 0
        for user in users:
            if sex in user['sex']:
                sex_num += 1
        pos_to_insert = template.find('\n', template.find(sex[:5]))
        template = template[:pos_to_insert] + formater(sex_num, len(users)) + template[pos_to_insert:]
    return template


def place_work(template: str, users: list, formater: callable) -> str:
    for work in works:
        profession_num = 0
        for user in users:
            if work in user['work']:
                profession_num += 1
        pos_to_insert = template.find('\n', template.find(work))
        template = template[: pos_to_insert] + formater(profession_num, len(users)) + template[pos_to_insert:]
    return template


def place_car(template: str, users: list, formater: callable) -> str:
    for car in cars:
        has_car_num = 0
        for user in users:
            if car in user['car']:
                has_car_num += 1
        pos_to_insert = template.find('\n', template.find(car))
        template = template[: pos_to_insert] + formater(has_car_num, len(users)) + template[pos_to_insert:]
    return template
		

utils.py

[Expand] Chevron

import json


async def update_user(user_id: str, key: str, value) -> None:
    with open(f"user_{user_id}.json", "r", encoding="utf-8") as file:
        user_values = json.load(file)
        user_values[key] = value
    with open(f"user_{user_id}.json", "w", encoding="utf-8") as file:
        json.dump(user_values, file)


async def is_user_completed(user_id: str) -> bool:
    with open(f"user_{user_id}.json", "r", encoding="utf-8") as file:
        user_values = json.load(file)
        user_values["is_complete"] = True
        for key in user_values:
            if user_values[key] is None:
                user_values["is_complete"] = False
                return False
    return True
		

result_template.md

[Expand] Chevron

*Возраст*, divisiontype:
    • от 0 до 18:
    • от 18 до 25:
    • от 26 до 35:
    • от 36 до 50:
    • от 50 до смерти:

*Страна*, divisiontype:


*Пол*, divisiontype:
    • \(♂\)Мужчины
    • \(♀\)Женщины

*Профессия*, divisiontype:
    • IT 
    • Завод 
    • Финансы, банкинг 
    • Строительство 
    • Добыча ископаемых
    • Обслуживание 
    • Медицина 
    • Работа с персоналом 
    • Преподавание

*Есть ли машина*, divisiontype:
    • Есть машина
    • Нет машины
		

Conclusion

This is how I wrote my quiz bot. Perhaps it will seem simple to you, and you will be entirely right. But you need to publish articles, right? In any case, I hope this case can help you in writing own survey bot.

heart
6
3 connected dots
1