Бот анонимных вопросов на телеграм, или квиз бот.

Вступление

Привет в этом кейсе я продемонстрирую тебе, как я сделал своего бота опросника.
Приведу исходный код python. Объясню структуру, и предназначение каждого файла и обработчика.Ну и конечно сможешь по взаимодействовать с моим, уже готовым ботом.
    Ты сможешь найти его через:
  • Ссылку https://t.me/TimQuizzerBot
  • Или вбив название бота, TimQuizzer в телеграме
Увы, когда я его делал, я незадумывал его мультиязычным. А когда спохватился, всё переделывать не решился. Да и суть данного бота не в мультиязычности. Но к следующему разу, я точно сделаю мультиязычного бота. Так что всем не русскоговорящим будет легче.

Пример создания телеграм бота-опроса.

Структура бота-опроса

Мой бот имеет линейную/последовательную структуру. С некоторыми 'плавающими' обработчиками. Что я под этим подразумеваю ?
Так как у меня бот-опросник. И он должен задавать вопросы последовательно, один за другим. То и обработчики, которые обрабатывают ответы пользователя, должны вызываться последовательно, друг за другом.
Под 'плавающими' обработчиками, я имел в виду, те функции, которые могут быть вызваны в любой момент общения с ботом.

Команда /start

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

@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))
		

Страна опрашиваемого

Создаёт словарь user_values, и сразу же присваивает ей возраст и id пользователя.
Создаёт кнопки на клавиатуре, с эмоджи стран на них. И само-сабой спрашивает страну опрашиваемого.
твоя страна телеграм кнопка

@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))
		

Пол опрашиваемого

Ловит текст выбранной страны и добавляет в user_values.
Спрашивает пол, и создаёт кнопки на клавиатуре со значками ♂ и ♀
твой пол телеграм кнопка

@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))
		

Работа опрашиваемого

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

@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)
	)
		

Машина опрашиваемого

Сохраняет значение работы пользователя в user_values
Спрашивает, есть ли машина ?
Создаёт кнопки на клавиатуре
Есть ли у тебя машина телеграм кнопка

@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))
		

Конец опроса

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

@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())
		

Главное меню

Данный обработчик можно вызвать через команды */help* или */menu*.
Ничего особенного он не делает. Создаём кнопки в сообщении. Выводим стандартное сообщение.
команда help telegram бота

@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())
		

Настройки

команда setting telegram бот
Данный обработчик разбит на 4 функции. Одна, *setting_update_format_numbers*, реализует непосредственно функционал изменения формата отображаемой информации.

@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())
		
Другая, *setting*, показывает возможные настройки, создаёт соответствующие кнопки.

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())
		
И две другие реализованы через *callback_query* и *message*. И обе они вызывают *setting*. Сделано это было для того, чтобы была возможность работать с этой функцией, и как с командой и как кнопкой.

@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", другой для вызова через кнопки. Обе, они имеют один и тот же функционал *result*.
команда результатов телеграм бот

@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)
		
Сам же обработчик *result*, занимается тем что форматирует result_template.md. Вставляет данные при помощи функций из *placers.py*. А формат меняет при помощи *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)
		

Скачать готового телеграм бота, или скопировать код

Ты можешь скачать бота с моего репозитория. TimQuizzer-bot
Или ты можешь просмотреть исходный код, ниже. Скопировав интересующие тебя части

main.py

[Развернуть] Шеврон

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

[Развернуть] Шеврон

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

[Развернуть] Шеврон

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

[Развернуть] Шеврон

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

[Развернуть] Шеврон

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

[Развернуть] Шеврон

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

*Страна*, divisiontype:


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

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

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

Заключение

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

сердце 5
3 соединённые точки 1