Todo sobre FSM en palabras simples / Sudo Null IT News

Amigos, saludos! Hoy tocaremos un tema importante y no tan complicado: la implementación de máquinas de estados finitos (FSM) en telegram bots en Aiogram 3.x.

Para una mejor comprensión, te recomiendo que leas mis publicaciones anteriores sobre el tema Aiogram 3.x:

  • Bots de Telegram en Aiogram 3.x: primeros pasos

  • Telegram Bots en Aiogram 3.x: teclado de texto y menú de comandos

  • Bots de Telegram en Aiogram 3.x: botones en línea y fecha de devolución de llamada

  • Telegram Bots en Aiogram 3.x: La magia de los filtros

  • Trabajar con mensajes de texto

  • trabajando con los medios

  • Levantando un servidor Redis: Guía completa (Usaremos Redis como almacenamiento para FSM)

También le recomiendo que implemente un servidor Redis y lo utilice como almacenamiento de datos para FSM. Pero, si no quiere molestarse con Redis, le mostraré cómo usar su análogo (MemoryStorage) y le diré por qué es mejor no hacerlo.

¿Qué haremos hoy?

Hoy analizaremos FSM usando un ejemplo específico: configuraremos un perfil de usuario, capturaremos su inicio de sesión, nombre, edad, foto e información sobre él mismo, y luego mostraremos estos datos.

Después de estudiar el material, dominará por completo la habilidad de interactuar con FSM en Aiogram 3.x, y todo lo que queda es aprender a escribir estos datos en una base de datos PostgreSQL (lo haremos en el próximo artículo).

Instalación de los módulos necesarios

Primero, instalemos todos los módulos necesarios:

pip install aiogram python-decouple redis
  • redis – para interactuar con la base de datos de Redis.

  • python-decouple — para trabajar con archivos .env.

  • aiogram — una biblioteca para crear bots.

Acceso a Redis

  • Anfitrión

  • Puerto

  • Número de base de datos (predeterminado de 0 a 15)

  • (Opcional) Nombre de usuario y contraseña, si se especifica

Un ejemplo de una línea de conexión a Redis sin nombre de usuario ni contraseña:

redis://HOST:PORT/NUM_DB

Ejemplo de línea de conexión a Redis con login y contraseña:

redis://LOGIN:PASSWORD@HOST:PORT/NUM_DB

Configurar create_bot.py

En archivo create_bot.py creemos un objeto storage para almacenar datos FSM. Importe variables desde el archivo .env:

from decouple import config
redis_url = config('REDIS_URL')

Configurar el almacenamiento

Importando el módulo RedisStorage:

from aiogram.fsm.storage.redis import RedisStorage

Esto importa la clase. RedisStorage de la biblioteca de aiogramas, que se utiliza para almacenar datos de máquinas de estados finitos (FSM) en Redis.

Crear un objeto RedisStorage:

storage = RedisStorage.from_url(config('REDIS_URL'))

Aquí creamos un objeto. RedisStorageusando la URL de conexión de Redis, que tomamos de la variable de entorno REDIS_URLcargado usando config de la biblioteca python-decouple.

Inicialización Dispatcher con RedisStorage:

dp = Dispatcher(storage=storage)

Creamos un objeto Dispatcherentregándole nuestro storage para almacenar el estado de la máquina de estado en Redis. Esto permite que el bot guarde y restaure los estados de los usuarios utilizando Redis como almacenamiento.

Si no desea utilizar Redis como almacenamiento, puede utilizar MemoryStorage:

from aiogram.fsm.storage.memory import MemoryStorage


dp = Dispatcher(storage=MemoryStorage())

MemoryStorage utiliza RAM, lo que conduce a la pérdida total de datos en caso de cualquier falla en el servidor o bot.

Redis también usa RAM para almacenar datos, pero a diferencia de MemoryStorage, admite la escritura periódica de datos en el disco y puede operar en un entorno de clúster, lo que garantiza la escalabilidad y confiabilidad del sistema.

Por lo tanto, el uso de RedisStorage en FSM para los bots de Telegram proporciona un alto rendimiento y confiabilidad, lo que lo convierte en la opción preferida para cualquier bot de Telegram, pero con fines de capacitación, puede usar MemoryStorage.

Ahora daré 2 ejemplos que demostrarán claramente las diferencias.

  1. Ejecutamos un guión de encuesta (5 preguntas en total)

  2. Después de la tercera pregunta, reinicia el bot.

Si se usa MemoryStorage , entonces se perderán todos los datos y el script deberá comenzar de nuevo. Usando Redis – el guión de cada usuario continuará desde donde lo dejó.

¿Qué es el MEV?

FSM, o Finite State Machine, es una forma sencilla de gestionar interacciones complejas en su bot de Telegram. Ayuda al robot a “recordar” en qué paso del proceso se encuentra el usuario y qué hacer a continuación.

Por ejemplo, creemos un cuestionario que guiará al usuario a través de los siguientes pasos:

  1. Primero pregunta el género.

  2. Luego envejece.

  3. Luego el nombre y apellido.

  4. Lo siguiente es iniciar sesión.

  5. Te pedirá que envíes una foto.

  6. Finalmente, te pedirá que agregues una descripción sobre ti.

FSM ayuda al bot a rastrear en cuál de estos pasos se encuentra el usuario y qué preguntar a continuación. Si el usuario envió un nombre, el bot lo “recuerda” y sabe que el siguiente paso es solicitar un inicio de sesión.

Configurando el primer script FSM

Importe los módulos necesarios:

from aiogram.dispatcher import FSMContext
from aiogram.dispatcher.filters.state import State, StatesGroup

Definamos los estados:

class Form(StatesGroup): 
    name = State()
    age = State()

Para cada estado, escribiremos controladores separados que responderán a la entrada de texto (nombre y edad):

import asyncio
from create_bot import bot
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message
from aiogram.utils.chat_action import ChatActionSender
import re


def extract_number(text):
    match = re.search(r'\b(\d+)\b', text)
    if match:
        return int(match.group(1))
    else:
        return None

      
class Form(StatesGroup):
    name = State()
    age = State()

    
questionnaire_router = Router()


@questionnaire_router.message(Command('start_questionnaire'))
async def start_questionnaire_process(message: Message, state: FSMContext):
    async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
        await asyncio.sleep(2)
        await message.answer('Привет. Напиши как тебя зовут: ')
    await state.set_state(Form.name)

    
@questionnaire_router.message(F.text, Form.name)
async def capture_name(message: Message, state: FSMContext):
    await state.update_data(name=message.text)
    async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
        await asyncio.sleep(2)
        await message.answer('Супер! А теперь напиши сколько тебе полных лет: ')
    await state.set_state(Form.age)

    
@questionnaire_router.message(F.text, Form.age)
async def capture_age(message: Message, state: FSMContext):
    check_age = extract_number(message.text)

    if not check_age or not (1 <= check_age <= 100):
        await message.reply('Пожалуйста, введите корректный возраст (число от 1 до 100).')
        return
    await state.update_data(age=check_age)

    data = await state.get_data()
    msg_text = (f'Вас зовут <b>{data.get("name")}</b> и вам <b>{data.get("age")}</b> лет. '
                f'Спасибо за то что ответили на мои вопросы.')
    await message.answer(msg_text)
    await state.clear()

importamos FSMContext de aiogram.dispatchery State y StatesGroup de aiogram.dispatcher.filters.state para trabajar con máquina de estados finitos (FSM) en nuestro bot. He aquí una explicación sencilla:

  1. FSMContexto:

    • Este es un objeto especial que nos ayuda a gestionar los estados de los usuarios. Almacena datos sobre el estado actual del usuario y permite cambiarlos moviendo al usuario entre diferentes estados.

    • Ejemplo de uso: uso FSMContext podemos guardar el nombre de usuario y luego pasar al siguiente paso donde preguntamos la edad.

  2. Estado y grupo de estados:

    • State representa un estado específico en el que puede encontrarse el usuario.

    • StatesGroup le permite combinar varios estados en un grupo lógico.

    • Ejemplo de uso: Creamos una clase. Formque se hereda de StatesGroupy dentro de él definimos los estados name y age. Esto nos ayuda a estructurar y gestionar la secuencia de pasos del cuestionario.

Ya estás familiarizado con otras importaciones si leíste mis artículos anteriores sobre el tema aiograma

Función extract_number extrae un número del texto. Útil en caso de que el usuario escriba “tengo 20 años” en lugar de “20”. Esta función obtendrá 20 e inmediatamente transformará esta entrada en int.

Ahora puedes ver que tenemos un nuevo argumento en la función: state. Le permite administrar los estados del usuario, mover al usuario a través de estados y más.

Para la anotación de tipo usamos FSMContextque fue importado anteriormente.

También creé un nuevo enrutador para el cuestionario. Nos será útil más adelante cuando hagamos el cuestionario de “combate”.

Por belleza, agregué una simulación de mecanografía. Y aquí lo más importante es un diseño de este tipo: await state.set_state(Form.name).

Esta entrada indica que el usuario, al llegar a este punto de la función, estará en el estado Form.namelo que significa que su envío de datos (en nuestro caso, introducir un nombre) ya estará en este estado.

Vayamos más allá:

@questionnaire_router.message(F.text, Form.name)
async def start_questionnaire_process(message: Message, state: FSMContext):
    await state.update_data(name=message.text)
    async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
        await asyncio.sleep(2)
        await message.answer('Супер! А теперь напиши сколько тебе полных лет:')
    await state.set_state(Form.age)

Aquí hemos aplicado un nuevo método, es decir, almacenar datos del usuario en una variable. name Con ayuda state.update_data. Después, como ya sabes, simplemente transferimos al usuario a un nuevo estado. Form.age.

@questionnaire_router.message(F.text, Form.age)
async def start_questionnaire_process(message: Message, state: FSMContext):
    check_age = extract_number(message.text)

    if not check_age or not (1 <= int(message.text) <= 100):
        await message.reply("Пожалуйста, введите корректный возраст (число от 1 до 100).")
        await state.set_state(Form.age)
    else:
        await state.update_data(age=check_age)

        data = await state.get_data()
        msg_text = (f'Вас зовут <b>{data.get("name")}</b> и вам <b>{data.get("age")}</b> лет. '
                    f'Спасибо за то, что ответили на мои вопросы.')
        await message.answer(msg_text)
        await state.clear()

Aquí, primero verificamos si el último mensaje del usuario tiene una edad y si está en el rango de 1 a 100. Si no es así, enviamos al usuario al estado de entrada de edad.

En el caso de que solo necesites dejar al usuario en el mismo estado cuando ocurre un error, puedes usar:

if not check_age or not (1 <= int(message.text) <= 100):
    await message.reply("Пожалуйста, введите корректный возраст (число от 1 до 100).")
    return

Al hacer esto, te indicaremos que debes quedarte donde estás, pero en tales casos suelo indicar explícitamente a qué estado debes ir (cuando, por ejemplo, después de seis meses regresas a tu proyecto, esto resulta muy útil). ).

Si la edad se ingresó correctamente, la escribimos en el almacenamiento (esto tampoco se pudo hacer, ya que aún finalizamos el script en este controlador, pero creo que lo explícito es mejor que lo implícito).

Para obtener datos del almacenamiento, utilizamos la entrada:

data = await state.get_data()

En este caso data es un diccionario Python normal, con el que puedes hacer todo lo que se puede hacer con los diccionarios. Por ejemplo, obtenga valores por clave.

Preste especial atención a:

await state.clear()

Esto es imprescindible cuando completas un escenario. Aquí todo es sencillo. Si no completa el script, el bot no se dará cuenta de que se ha recibido la edad y la esperará indefinidamente.

Ahora veamos el caso más común entre los principiantes.

El usuario completa el formulario y luego cambia de opinión. Clics a través del menú de comandos. /start, pero no le pasa nada. El caso es que el guión que lanzamos no acabó.

Las cosas pasan. En este caso, en lugar del nombre, el bot escribe el nombre “/inicio” y luego envía una nueva pregunta “Ingrese edad”. El usuario vuelve a pulsar /starty todo esto continúa hasta que el usuario elimina el bot y cree que fue creado por personas incompetentes.

Para evitar este problema, debe incorporar en su arquitectura la capacidad de salir del escenario de la encuesta. Personalmente, siempre estuve en el equipo. /start y en otros equipos (en sus manejadores) reinician el escenario. Para hacer esto necesitas lo siguiente:

async def cmd_start(message: Message, state: FSMContext):
    await state.clear()

En este caso, cerrará automáticamente el script de la encuesta y el usuario, al hacer clic en Inicio, simplemente recibirá un restablecimiento de datos.

También recomiendo agregar la posibilidad de salir haciendo clic en un botón del teclado. Podría ser un botón de texto con la etiqueta “Cancelar” o un botón en línea con call_data = cancely luego solo un controlador que cerrará (borrará) el almacenamiento, sacando así al usuario del escenario.

Presta especial atención a esto, ya que existen muchos problemas asociados con “no escapar” del escenario.

Esto es lo que pasó.

Esto es lo que pasó.

Espero que ya hayas comprendido los principios generales de interacción con FSM, lo que significa que podemos comenzar a completar el cuestionario de “combate”. El bot seguirá este escenario:

  1. Pregunta primero el género (teclado de texto con opciones de género)

  2. Luego envejece (quita el teclado)

  3. Luego nombre y apellido

  4. Siguiente inicio de sesión (teclado en línea con la posibilidad de seleccionar su inicio de sesión desde su perfil de Telegram si tiene uno)

  5. Le pedirá que envíe una foto (el punto aquí es capturar la foto file_id)

  6. Finalmente, te pedirá que agregues una descripción sobre ti.

Ahora simplemente reescribiremos nuestro código agregando nuevos estados y, como resultado, generaremos los datos ingresados ​​por el usuario con la pregunta “¿Está todo correcto?” y las opciones “Todo está correcto” y “Rellenar primero” (esta opción reseteará el cuestionario y lo iniciará desde el momento en que se ingresa el nombre).

Primero registremos los teclados:

Teclado de texto de selección de género:

def gender_kb():
    kb_list = (
        (KeyboardButton(text="👨‍🦱Мужчина")), (KeyboardButton(text="👩‍🦱Женщина"))
    )
    keyboard = ReplyKeyboardMarkup(keyboard=kb_list,
                                   resize_keyboard=True,
                                   one_time_keyboard=True,
                                   input_field_placeholder="Выбери пол:")
    return keyboard

Aquí utilicé el teclado de texto solo como ejemplo. Normalmente trato de usar teclados en línea.

Teclado en línea para verificar la finalización de datos:

def check_data():
    kb_list = (
        (InlineKeyboardButton(text="✅Все верно", callback_data="correct")),
        (InlineKeyboardButton(text="❌Заполнить сначала", callback_data="incorrect"))
    )
    keyboard = InlineKeyboardMarkup(inline_keyboard=kb_list)
    return keyboard

Un teclado en línea que te permitirá utilizar el login especificado por el usuario en el TG al hacer clic en:

def get_login_tg():
    kb_list = (
        (InlineKeyboardButton(text="Использовать мой логин с ТГ", callback_data="in_login"))
    )
    keyboard = InlineKeyboardMarkup(inline_keyboard=kb_list)
    return keyboard

Aquí también agregaremos un check para que si no hay inicio de sesión en Telegram, se deba especificar.

Aquí está el código completo del cuestionario (mira el código y luego te daré una explicación):

import asyncio
from create_bot import bot
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import Message, ReplyKeyboardRemove, CallbackQuery
from aiogram.utils.chat_action import ChatActionSender
from keyboards.all_kb import gender_kb, get_login_tg, check_data
from utils.utils import extract_number


class Form(StatesGroup):
    gender = State()
    age = State()
    full_name = State()
    user_login = State()
    photo = State()
    about = State()
    check_state = State()


questionnaire_router = Router()


@questionnaire_router.message(Command('start_questionnaire'))
async def start_questionnaire_process(message: Message, state: FSMContext):
    await state.clear()
    async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
        await asyncio.sleep(2)
        await message.answer('Привет. Для начала выбери свой пол: ', reply_markup=gender_kb())
    await state.set_state(Form.gender)


@questionnaire_router.message((F.text.lower().contains('мужчина')) | (F.text.lower().contains('женщина')), Form.gender)
async def start_questionnaire_process(message: Message, state: FSMContext):
    await state.update_data(gender=message.text, user_id=message.from_user.id)
    async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
        await asyncio.sleep(2)
        await message.answer('Супер! А теперь напиши сколько тебе полных лет: ', reply_markup=ReplyKeyboardRemove())
    await state.set_state(Form.age)


@questionnaire_router.message(F.text, Form.gender)
async def start_questionnaire_process(message: Message, state: FSMContext):
    await state.update_data(name=message.text)
    async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
        await asyncio.sleep(2)
        await message.answer('Пожалуйста, выбери вариант из тех что в клавиатуре: ', reply_markup=gender_kb())
    await state.set_state(Form.gender)


@questionnaire_router.message(F.text, Form.age)
async def start_questionnaire_process(message: Message, state: FSMContext):
    check_age = extract_number(message.text)

    if not check_age or not (1 <= int(message.text) <= 100):
        await message.reply("Пожалуйста, введите корректный возраст (число от 1 до 100).")
        return

    await state.update_data(age=check_age)
    await message.answer('Теперь укажите свое полное имя:')
    await state.set_state(Form.full_name)


@questionnaire_router.message(F.text, Form.full_name)
async def start_questionnaire_process(message: Message, state: FSMContext):
    await state.update_data(full_name=message.text)
    text="Теперь укажите ваш логин, который будет использоваться в боте"

    if message.from_user.username:
        text += ' или нажмите на кнопку ниже и в этом случае вашим логином будет логин из вашего телеграмм: '
        await message.answer(text, reply_markup=get_login_tg())
    else:
        text += ' : '
        await message.answer(text)

    await state.set_state(Form.user_login)

# вариант когда мы берем логин из профиля телеграмм
@questionnaire_router.callback_query(F.data, Form.user_login)
async def start_questionnaire_process(call: CallbackQuery, state: FSMContext):
    await call.answer('Беру логин с телеграмм профиля')
    await call.message.edit_reply_markup(reply_markup=None)
    await state.update_data(user_login=call.from_user.username)
    await call.message.answer('А теперь отправьте фото, которое будет использоваться в вашем профиле: ')
    await state.set_state(Form.photo)


# вариант когда мы берем логин из введенного пользователем
@questionnaire_router.message(F.text, Form.user_login)
async def start_questionnaire_process(message: Message, state: FSMContext):
    await state.update_data(user_login=message.from_user.username)
    await message.answer('А теперь отправьте фото, которое будет использоваться в вашем профиле: ')
    await state.set_state(Form.photo)


@questionnaire_router.message(F.photo, Form.photo)
async def start_questionnaire_process(message: Message, state: FSMContext):
    photo_id = message.photo(-1).file_id
    await state.update_data(photo=photo_id)
    await message.answer('А теперь расскажите пару слов о себе: ')
    await state.set_state(Form.about)


@questionnaire_router.message(F.document.mime_type.startswith('image/'), Form.photo)
async def start_questionnaire_process(message: Message, state: FSMContext):
    photo_id = message.document.file_id
    await state.update_data(photo=photo_id)
    await message.answer('А теперь расскажите пару слов о себе: ')
    await state.set_state(Form.about)


@questionnaire_router.message(F.document, Form.photo)
async def start_questionnaire_process(message: Message, state: FSMContext):
    await message.answer('Пожалуйста, отправьте фото!')
    await state.set_state(Form.photo)


@questionnaire_router.message(F.text, Form.about)
async def start_questionnaire_process(message: Message, state: FSMContext):
    await state.update_data(about=message.text)

    data = await state.get_data()

    caption = f'Пожалуйста, проверьте все ли верно: \n\n' \
              f'<b>Полное имя</b>: {data.get("full_name")}\n' \
              f'<b>Пол</b>: {data.get("gender")}\n' \
              f'<b>Возраст</b>: {data.get("age")} лет\n' \
              f'<b>Логин в боте</b>: {data.get("user_login")}\n' \
              f'<b>О себе</b>: {data.get("about")}'

    await message.answer_photo(photo=data.get('photo'), caption=caption, reply_markup=check_data())
    await state.set_state(Form.check_state)

# сохраняем данные
@questionnaire_router.callback_query(F.data == 'correct', Form.check_state)
async def start_questionnaire_process(call: CallbackQuery, state: FSMContext):
    await call.answer('Данные сохранены')
    await call.message.edit_reply_markup(reply_markup=None)
    await call.message.answer('Благодарю за регистрацию. Ваши данные успешно сохранены!')
    await state.clear()


# запускаем анкету сначала
@questionnaire_router.callback_query(F.data == 'incorrect', Form.check_state)
async def start_questionnaire_process(call: CallbackQuery, state: FSMContext):
    await call.answer('Запускаем сценарий с начала')
    await call.message.edit_reply_markup(reply_markup=None)
    await call.message.answer('Привет. Для начала выбери свой пол: ', reply_markup=gender_kb())
    await state.set_state(Form.gender)

Tenga en cuenta los cambios en la clase Formulario, se han agregado nuevos estados:

class Form(StatesGroup):
    gender = State()
    age = State()
    full_name = State()
    user_login = State()
    photo = State()
    about = State()
    check_state = State()

Ahora veamos los controladores:

@questionnaire_router.message((F.text.lower().contains('мужчина')) | (F.text.lower().contains('женщина')), Form.gender)
async def start_questionnaire_process(message: Message, state: FSMContext):
    await state.update_data(gender=message.text)
    async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
        await asyncio.sleep(2)
        await message.answer('Супер! А теперь напиши сколько тебе полных лет: ', reply_markup=ReplyKeyboardRemove())
    await state.set_state(Form.age)


@questionnaire_router.message(F.text, Form.gender)
async def start_questionnaire_process(message: Message, state: FSMContext):
    await state.update_data(name=message.text)
    async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
        await asyncio.sleep(2)
        await message.answer('Пожалуйста, выбери вариант из тех что в клавиатуре: ', reply_markup=gender_kb())
    await state.set_state(Form.gender)

    

Aquí especificamos Form.gender en dos controladores, pero uno tiene filtros que le permiten ir más lejos, mientras que el otro no, y el bot nuevamente le pide que seleccione un género (discutimos en detalle en el tema de filtros mágicos por qué esto sucede).

Tenga en cuenta que también agregué el telegram_id del usuario al almacenamiento bajo la clave user_id. Necesitaremos esta información para registrar al usuario en la base de datos.

El procesamiento de la edad se analizó anteriormente; no hay nada interesante en el procesamiento del nombre. Veamos el formato para capturar el inicio de sesión de un usuario.

text="Теперь укажите ваш логин, который будет использоваться в боте"

if message.from_user.username:
    text += ' или нажмите на кнопку ниже и в этом случае вашим логином будет логин из вашего телеграмм: '
    await message.answer(text, reply_markup=get_login_tg())
else:
    text += ‘ : ‘
    await message.answer(text)

Aquí verifiqué si el usuario tiene un inicio de sesión en su perfil de Telegram. Si tiene un inicio de sesión, el bot le permite usarlo haciendo clic en un botón. De lo contrario, no hay opción para seleccionar un inicio de sesión desde su perfil.

# вариант когда мы берем логин из профиля телеграмм
@questionnaire_router.callback_query(F.data, Form.user_login)
async def start_questionnaire_process(call: CallbackQuery, state: FSMContext):
    await call.answer('Беру логин с телеграмм профиля')
    await call.message.edit_reply_markup(reply_markup=None)
    await state.update_data(user_login=call.from_user.username)
    await call.message.answer('А теперь отправьте фото, которое будет использоваться в вашем профиле: ')
    await state.set_state(Form.photo)


# вариант когда мы берем логин из введенного пользователем
@questionnaire_router.message(F.text, Form.user_login)
async def start_questionnaire_process(message: Message, state: FSMContext):
    await state.update_data(user_login=message.from_user.username)
    await message.answer('А теперь отправьте фото, которое будет использоваться в вашем профиле: ')
    await state.set_state(Form.photo)

Tenga en cuenta que aquí en callback_query utilicé:

await call.message.edit_reply_markup(reply_markup=None)

Gracias a esta entrada, eliminé el teclado en línea después de hacer clic en él.

Observe también cómo utilicé filtros. Las situaciones de trabajo con callback_query y con un mensaje simple se han procesado por separado.

Hay un momento en la foto. Probablemente sepas que las fotos se pueden enviar con compresión (en cuyo caso el robot verá el objeto de la foto) o sin compresión (en cuyo caso la foto se enviará como documento).

En mi código tomé en cuenta todas las opciones, y esto es lo que pasó:

@questionnaire_router.message(F.photo, Form.photo)
async def start_questionnaire_process(message: Message, state: FSMContext):
    photo_id = message.photo(-1).file_id
    await state.update_data(photo=photo_id)
    await message.answer('А теперь расскажите пару слов о себе: ')
    await state.set_state(Form.about)


@questionnaire_router.message(F.document.mime_type.startswith(‘image/’), Form.photo)
async def start_questionnaire_process(message: Message, state: FSMContext):
    photo_id = message.document.file_id
    await state.update_data(photo=photo_id)
    await message.answer('А теперь расскажите пару слов о себе: ')
    await state.set_state(Form.about)


@questionnaire_router.message(F.document, Form.photo)
async def start_questionnaire_process(message: Message, state: FSMContext):
    await message.answer(‘Пожалуйста, отправьте фото!’)
    await state.set_state(Form.photo)

Cuando el tipo de contenido es foto, creo que todo queda claro sin mucha explicación, pero en la versión con foto sin compresión utilicé un filtro mágico especial:

F.document.mime_type.startswith(‘image/’)

Comprueba si el tipo MIME del documento es una imagen. Si comienza con 'imagen/' muestra que es una imagen, lo cual está bien para nosotros.

En caso de que solo se haya enviado un documento (por ejemplo, pdf), escribí este controlador:

@questionnaire_router.message(F.document, Form.photo)
async def start_questionnaire_process(message: Message, state: FSMContext):
    await message.answer(‘Пожалуйста, отправьте фото!’)
    await state.set_state(Form.photo)

Su propósito es hacer que el usuario vuelva a enviar una foto si envió algo incorrecto.

A continuación, guardamos una descripción sobre nosotros mismos y recuperamos los datos del usuario del almacenamiento. Como queremos enviar un mensaje en formato de encuesta, responderemos al usuario enviándole un mensaje con foto.

@questionnaire_router.message(F.text, Form.about)
async def start_questionnaire_process(message: Message, state: FSMContext):
    await state.update_data(about=message.text)

    data = await state.get_data()

    caption = f'Пожалуйста, проверьте все ли верно: \n\n' \
              f'<b>Полное имя</b>: {data.get("full_name")}\n' \
              f'<b>Пол</b>: {data.get("gender")}\n' \
              f'<b>Возраст</b>: {data.get("age")} лет\n' \
              f'<b>Логин в боте</b>: {data.get("user_login")}\n' \
              f'<b>О себе</b>: {data.get("about")}'

    await message.answer_photo(photo=data.get('photo'), caption=caption, reply_markup=check_data())
    await state.set_state(Form.check_state)

A continuación, dependiendo de la opción de verificación, guardaremos los datos del usuario en la base de datos (próximo artículo) o ejecutaremos el script desde el principio.

# сохраняем данные
@questionnaire_router.callback_query(F.data == 'correct', Form.check_state)
async def start_questionnaire_process(call: CallbackQuery, state: FSMContext):
    await call.answer('Данные сохранены')
    await call.message.edit_reply_markup(reply_markup=None)
    await call.message.answer('Благодарю за регистрацию. Ваши данные успешно сохранены!')
    await state.clear()


# запускаем анкету сначала
@questionnaire_router.callback_query(F.data == 'incorrect', Form.check_state)
async def start_questionnaire_process(call: CallbackQuery, state: FSMContext):
    await call.answer('Запускаем сценарий с начала')
    await call.message.edit_reply_markup(reply_markup=None)
    await call.message.answer('Привет. Для начала выбери свой пол: ', reply_markup=gender_kb())
    await state.set_state(Form.gender)

Así es como se ve el cuestionario en acción en las capturas de pantalla:

Conclusión

Hoy hemos cubierto un tema fundamental que le abrirá el camino para crear bots con escenarios FSM increíblemente complejos y emocionantes. Comprender esto es clave para crear bots verdaderamente inteligentes e interactivos.

Entiendo que aún puede haber dudas, porque este tema requiere tiempo y práctica para dominarlo por completo. Si tiene alguna pregunta, no dude en hacerla en los comentarios. Lo admito, en algún momento tampoco me resultó fácil comprender todos los matices de FSM, pero el resultado vale la pena.

Si te gustó este artículo y quieres más contenido como este, como por ejemplo cómo integrar un bot con una base de datos PostgreSQL, deja un comentario, dale me gusta y suscríbete. Este es solo el comienzo de nuestro viaje al mundo de los bots avanzados de Telegram.

¡Hasta luego!

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *