Почему бот на Aiogram 3 отвечает только на последнее из всех отправленных фото, когда используется FSMContext/State?

Как изменить код так, чтобы бот отправлял сообщение с функцией <b>hadnle_text</b> только один раз, даже если пользователь загрузит несколько фотографий, и при этом сохранял возможность выбирать один из двух вариантов ответа?
  • 26 сентября 2024 г. 18:53
Ответы на вопрос 2
Проблема, с которой вы сталкиваетесь, связана с тем, что бот не может корректно обрабатывать несколько сообщений в одном состоянии FSM (Finite State Machine). Когда пользователь отправляет несколько фото, обработчик срабатывает для последнего фото, и предыдущие фото игнорируются.

Если вы хотите, чтобы функция `handle_text` срабатывала только один раз при загрузке фотографии, и при этом оставалась возможность выбора между двумя вариантами ответа, вам необходимо использовать подход с временным состоянием или флагом, который будет следить за тем, было ли уже обработано фото.

Вот пример того, как можно изменить вашу логику:

```python
from aiogram import Bot, Dispatcher, types
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher import FSMContext
from aiogram.dispatcher import State, StatesGroup
from aiogram.utils import executor

API_TOKEN = 'YOUR_API_TOKEN'

# Инициализация бота и диспетчера
bot = Bot(token=API_TOKEN)
storage = MemoryStorage()
dp = Dispatcher(bot, storage=storage)

# Определяем состояния
class Form(StatesGroup):
    waiting_for_photo = State()
    waiting_for_option = State()

@dp.message_handler(commands='start')
async def cmd_start(message: types.Message):
    await message.answer("Пожалуйста, загрузите фото.")
    await Form.waiting_for_photo.set()

@dp.message_handler(content_types=['photo'], state=Form.waiting_for_photo)
async def handle_photo(message: types.Message, state: FSMContext):
    # Сохранение информации о фото
    photo = message.photo[-1]

    # Здесь можно добавить дополнительно логику для обработки фото
    await state.update_data(photo_id=photo.file_id)

    # Отправляем сообщение с вариантами после первого фото
    await message.answer("Выберите один из вариантов: \n1. Вариант 1 \n2. Вариант 2")
    
    # Переходим к следующему состоянию
    await Form.waiting_for_option.set()

@dp.message_handler(state=Form.waiting_for_option)
async def handle_option(message: types.Message, state: FSMContext):
    user_data = await state.get_data()
    
    if message.text == '1':
        await message.answer("Вы выбрали первый вариант.")
    elif message.text == '2':
        await message.answer("Вы выбрали второй вариант.")
    else:
        await message.answer("Пожалуйста, выберите 1 или 2.")

    # Завершение состояния
    await state.finish()

@dp.message_handler(state='*', content_types=types.ContentTypes.ANY)
async def catch_all(message: types.Message):
    await message.answer("Я не понимаю это сообщение.")

if __name__ == '__main__':
    executor.start_polling(dp, skip_updates=True)
```

### Описание кода:
1. **Состояния FSM**: Мы определяем два состояния: `waiting_for_photo` и `waiting_for_option`.
2. **Обработка фотографий**: В `handle_photo` мы обрабатываем только первое фото и устанавливаем состояние ожидания ответа от пользователя.
3. **Управление состояниями**: После загрузки фотографии мы переходим к следующему состоянию и ждем выбора пользователя.
4. **Завершение FSM**: После обработки выбора мы завершаем сессию с помощью `await state.finish()`, чтобы бот готов был к новым командам.

Этот подход позволит вашему боту обрабатывать только одно отправленное фото и затем перейти к выбору варианта без повторной активации состояния для фото.
Следует почитать на счет того как работает медиагруппа в ТГ, 5 фото в одном сообщении = 5 апдейтов. 

Я бы ушел от FSM в сторону Callback Data Factory

# ...
from aiogram.filters.callback_data import CallbackData


class AnswerCallback(CallbackData, prefix='answer'):
    message_id: int
    answer_type: str


@router.message(F.photo)
async def photo_handle(message: Message):
    await hadnle_text(message)

async def hadnle_text(message: Message):
    builder = InlineKeyboardBuilder()
    builder.add(InlineKeyboardButton(
        text="Answer 1",
        callback_data=AnswerCallback(message_id=message.message_id, answer_type='Answer 1').pack()),
    InlineKeyboardButton(
        text="Answer 2",
        callback_data=AnswerCallback(message_id=message.message_id, answer_type='Answer 2').pack()),
    )
    # Или так
    # builder.button(text="Answer 1", callback_data=AnswerCallback(message_id=message.message_id, answer_type='answer 1'))
    # builder.button(text="Answer 2", callback_data=AnswerCallback(message_id=message.message_id, answer_type='answer 2'))
    await message.answer(
        f"<b>Выберите кнопку</b>",
        reply_markup=builder.as_markup()
    )

@router.callback_query(AnswerCallback.filter())
async def text_state_callback(callback: CallbackQuery, callback_data: AnswerCallback):
    await bot.send_message(
        chat_id=callback.message.chat.id,
        text=callback_data.answer_type,
        reply_to_message_id=callback_data.message_id,
    )
Похожие вопросы