"""
Утилиты для работы с FSM состояниями и обновлениями
"""
from typing import Optional, Any, Set, Union
from contextlib import suppress
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State
from aiogram.types import CallbackQuery, Message, ReplyKeyboardRemove
from aiogram.exceptions import TelegramBadRequest
from middleware.loggers import logger
__all__ = (
'clear_state',
'answer_callback',
'safe_answer_callback',
'safe_delete_message',
'safe_edit_message',
'clear_state_keep_data',
'get_state_data',
'set_state_data',
'update_state_data',
'is_state_active',
'inline_clear',
'status_clear',
'delete_messages',
'set_state_with_data',
'get_or_create_data',
'increment_state_value',
'append_to_state_list',
'remove_from_state_list',
'toggle_state_flag',
'debug_state'
)
# ================= РАБОТА С FSM СОСТОЯНИЯМИ =================
async def clear_state(
state: FSMContext,
log: bool = True
) -> None:
"""
Очищает FSM состояние.
Args:
state: Контекст FSM
log: Логировать очистку
Example:
>> await clear_state(state)
"""
current_state = await state.get_state()
if log and current_state:
logger.debug(
f"Очистка FSM состояния: {current_state}",
log_type='FSM'
)
await state.clear()
async def clear_state_keep_data(
state: FSMContext,
keep_keys: Optional[Set[str]] = None
) -> None:
"""
Очищает FSM состояние, но сохраняет определенные данные.
Args:
state: Контекст FSM
keep_keys: Множество ключей для сохранения
Example:
>> # Очищаем состояние, но сохраняем user_id и language
>> await clear_state_keep_data(state, keep_keys={'user_id', 'language'})
"""
if keep_keys:
# Получаем текущие данные
current_data = await state.get_data()
# Сохраняем только нужные ключи
saved_data = {
key: value for key, value in current_data.items()
if key in keep_keys
}
# Очищаем состояние
await state.clear()
# Восстанавливаем сохраненные данные
if saved_data:
await state.update_data(**saved_data)
logger.debug(
f"FSM очищен, сохранены ключи: {', '.join(keep_keys)}",
log_type='FSM'
)
else:
await state.clear()
async def get_state_data(
state: FSMContext,
key: Optional[str] = None,
default: Any = None
) -> Any:
"""
Получает данные из FSM состояния.
Args:
state: Контекст FSM
key: Ключ для получения (если None, возвращает все данные)
default: Значение по умолчанию
Returns:
Any: Данные из состояния
Example:
>> # Получить все данные
>> data = await get_state_data(state)
>> # Получить конкретный ключ
>> user_id = await get_state_data(state, 'user_id')
>> # С значением по умолчанию
>> lang = await get_state_data(state, 'language', default='ru')
"""
data = await state.get_data()
if key is None:
return data
return data.get(key, default)
async def set_state_data(
state: FSMContext,
key: str,
value: Any
) -> None:
"""
Устанавливает данные в FSM состояние.
Args:
state: Контекст FSM
key: Ключ
value: Значение
Example:
>> await set_state_data(state, 'user_id', 123456789)
"""
await state.update_data(**{key: value})
async def update_state_data(
state: FSMContext,
**kwargs
) -> None:
"""
Обновляет несколько полей в FSM состоянии.
Args:
state: Контекст FSM
**kwargs: Пары ключ-значение для обновления
Example:
>> await update_state_data(
... state,
... user_id=123456789,
... language='ru',
... step=1
... )
"""
await state.update_data(**kwargs)
async def is_state_active(state: FSMContext) -> bool:
"""
Проверяет, активно ли какое-либо состояние.
Args:
state: Контекст FSM
Returns:
bool: True если есть активное состояние
Example:
>> if await is_state_active(state):
... await message.answer("У вас есть незавершенное действие")
"""
current_state = await state.get_state()
return current_state is not None
# ================= РАБОТА С CALLBACK QUERIES =================
async def answer_callback(
callback: CallbackQuery,
text: Optional[str] = None,
show_alert: bool = False,
cache_time: int = 0
) -> bool:
"""
Отвечает на callback query.
Args:
callback: Callback query
text: Текст уведомления
show_alert: Показать как alert
cache_time: Время кэширования
Returns:
bool: True если успешно
Example:
>> await answer_callback(callback, "✅ Готово!")
>> await answer_callback(callback, "⚠️ Ошибка", show_alert=True)
"""
try:
await callback.answer(text=text, show_alert=show_alert, cache_time=cache_time)
return True
except TelegramBadRequest as e:
logger.warning(
f"Не удалось ответить на callback: {e}",
log_type='CALLBACK'
)
return False
async def safe_answer_callback(
callback: CallbackQuery,
text: Optional[str] = None,
show_alert: bool = False
) -> None:
"""
Безопасно отвечает на callback query (подавляет ошибки).
Args:
callback: Callback query
text: Текст уведомления
show_alert: Показать как alert
Example:
>> await safe_answer_callback(callback, "✅ Готово!")
"""
with suppress(TelegramBadRequest):
await callback.answer(text=text, show_alert=show_alert)
# ================= РАБОТА С СООБЩЕНИЯМИ =================
async def safe_delete_message(
message: Message,
log: bool = False
) -> bool:
"""
Безопасно удаляет сообщение.
Args:
message: Сообщение для удаления
log: Логировать попытку удаления
Returns:
bool: True если успешно удалено
Example:
>> await safe_delete_message(message)
"""
try:
await message.delete()
if log:
logger.debug(
f"Сообщение удалено: {message.message_id}",
log_type='MESSAGE'
)
return True
except TelegramBadRequest as e:
if log:
logger.warning(
f"Не удалось удалить сообщение: {e}",
log_type='MESSAGE'
)
return False
async def safe_edit_message(
message: Message,
text: str,
**kwargs
) -> bool:
"""
Безопасно редактирует сообщение.
Args:
message: Сообщение для редактирования
text: Новый текст
**kwargs: Дополнительные параметры (reply_markup, parse_mode, и т.д.)
Returns:
bool: True если успешно отредактировано
Example:
>> await safe_edit_message(
... message,
... "Новый текст",
... parse_mode="HTML"
... )
"""
try:
await message.edit_text(text, **kwargs)
return True
except TelegramBadRequest as e:
logger.warning(
f"Не удалось отредактировать сообщение: {e}",
log_type='MESSAGE'
)
return False
async def delete_messages(
chat_id: int,
message_ids: list[int],
bot
) -> int:
"""
Удаляет несколько сообщений.
Args:
chat_id: ID чата
message_ids: Список ID сообщений
bot: Экземпляр бота
Returns:
int: Количество успешно удаленных сообщений
Example:
>> deleted = await delete_messages(
... chat_id=message.chat.id,
... message_ids=[123, 124, 125],
... bot=bot
... )
>> print(f"Удалено {deleted} сообщений")
"""
deleted_count = 0
for message_id in message_ids:
try:
await bot.delete_message(chat_id=chat_id, message_id=message_id)
deleted_count += 1
except TelegramBadRequest:
pass
return deleted_count
# ================= КОМБИНИРОВАННЫЕ ФУНКЦИИ =================
async def inline_clear(update: Union[Message, CallbackQuery]) -> None:
"""
Очищает все инлайн взаимодействия (отвечает на callback).
Args:
update: Объект обновления (Message или CallbackQuery)
Example:
>> await inline_clear(callback)
"""
if isinstance(update, CallbackQuery):
await safe_answer_callback(update)
async def status_clear(
update: Union[Message, CallbackQuery],
state: FSMContext,
keep_data: Optional[Set[str]] = None,
remove_keyboard: bool = False
) -> None:
"""
Полная очистка: состояние FSM + ответ на callback + удаление клавиатуры.
Args:
update: Объект обновления
state: Контекст FSM
keep_data: Данные для сохранения
remove_keyboard: Удалить клавиатуру (только для Message)
Example:
>> # Полная очистка
>> await status_clear(message, state)
>> # С сохранением данных
>> await status_clear(
... callback,
... state,
... keep_data={'user_id', 'language'}
... )
>> # С удалением клавиатуры
>> await status_clear(message, state, remove_keyboard=True)
"""
# Очищаем состояние
if keep_data:
await clear_state_keep_data(state, keep_keys=keep_data)
else:
await clear_state(state, log=True)
# Отвечаем на callback
await inline_clear(update)
# Удаляем клавиатуру если нужно
if remove_keyboard and isinstance(update, Message):
with suppress(TelegramBadRequest):
await update.answer(
"Отменено",
reply_markup=ReplyKeyboardRemove()
)
# ================= УТИЛИТЫ ДЛЯ РАБОТЫ С СОСТОЯНИЯМИ =================
async def set_state_with_data(
state: FSMContext,
new_state: State,
**data
) -> None:
"""
Устанавливает новое состояние и данные одновременно.
Args:
state: Контекст FSM
new_state: Новое состояние
**data: Данные для сохранения
Example:
>> await set_state_with_data(
... state,
... FormStates.waiting_name,
... user_id=123456789,
... step=1
... )
"""
await state.set_state(new_state)
if data:
await state.update_data(**data)
logger.debug(
f"Установлено состояние: {new_state.state}",
log_type='FSM'
)
async def get_or_create_data(
state: FSMContext,
key: str,
factory: Any
) -> Any:
"""
Получает данные из состояния или создает их если их нет.
Args:
state: Контекст FSM
key: Ключ данных
factory: Значение по умолчанию или функция для создания
Returns:
Any: Данные из состояния или созданные
Example:
>> # С простым значением
>> items = await get_or_create_data(state, 'items', [])
>> # С функцией
>> data = await get_or_create_data(state, 'data', lambda: {'count': 0})
"""
data = await state.get_data()
if key not in data:
# Создаем значение
if callable(factory):
value = factory()
else:
value = factory
await state.update_data(**{key: value})
return value
return data[key]
async def increment_state_value(
state: FSMContext,
key: str,
amount: int = 1
) -> int:
"""
Инкрементирует числовое значение в состоянии.
Args:
state: Контекст FSM
key: Ключ значения
amount: Величина инкремента
Returns:
int: Новое значение
Example:
>> # Увеличиваем счетчик
>> new_count = await increment_state_value(state, 'attempts')
>> if new_count >= 3:
... await message.answer("Слишком много попыток!")
"""
data = await state.get_data()
current = data.get(key, 0)
new_value = current + amount
await state.update_data(**{key: new_value})
return new_value
async def append_to_state_list(
state: FSMContext,
key: str,
value: Any
) -> list:
"""
Добавляет значение в список в состоянии.
Args:
state: Контекст FSM
key: Ключ списка
value: Значение для добавления
Returns:
list: Обновленный список
Example:
>> # Добавляем товар в корзину
>> cart = await append_to_state_list(state, 'cart', product_id)
>> await message.answer(f"В корзине {len(cart)} товаров")
"""
data = await state.get_data()
current_list = data.get(key, [])
if not isinstance(current_list, list):
current_list = []
current_list.append(value)
await state.update_data(**{key: current_list})
return current_list
async def remove_from_state_list(
state: FSMContext,
key: str,
value: Any
) -> list:
"""
Удаляет значение из списка в состоянии.
Args:
state: Контекст FSM
key: Ключ списка
value: Значение для удаления
Returns:
list: Обновленный список
Example:
>> # Удаляем товар из корзины
>> cart = await remove_from_state_list(state, 'cart', product_id)
"""
data = await state.get_data()
current_list = data.get(key, [])
if isinstance(current_list, list) and value in current_list:
current_list.remove(value)
await state.update_data(**{key: current_list})
return current_list
async def toggle_state_flag(
state: FSMContext,
key: str
) -> bool:
"""
Переключает boolean флаг в состоянии.
Args:
state: Контекст FSM
key: Ключ флага
Returns:
bool: Новое значение флага
Example:
>> # Переключаем режим
>> is_active = await toggle_state_flag(state, 'notifications')
>> await message.answer(
... f"Уведомления: {'включены' if is_active else 'выключены'}"
... )
"""
data = await state.get_data()
current = data.get(key, False)
new_value = not current
await state.update_data(**{key: new_value})
return new_value
# ================= ОТЛАДКА =================
async def debug_state(state: FSMContext) -> str:
"""
Возвращает отладочную информацию о состоянии.
Args:
state: Контекст FSM
Returns:
str: Форматированная информация о состоянии
Example:
>> debug_info = await debug_state(state)
>> print(debug_info)
"""
current_state = await state.get_state()
data = await state.get_data()
lines = [
"🔍 Debug FSM:\n",
f"📊 Состояние: {current_state or 'None'}\n",
f"📦 Данных: {len(data)}\n"
]
if data:
lines.append("\nДанные:")
for key, value in data.items():
value_str = str(value)
if len(value_str) > 50:
value_str = value_str[:50] + "..."
lines.append(f"• {key}: {value_str}")
return "\n".join(lines)