diff --git a/bot/handlers/commands/settings/set_avatar.py b/bot/handlers/commands/settings/set_avatar.py new file mode 100644 index 0000000..16f7ae7 --- /dev/null +++ b/bot/handlers/commands/settings/set_avatar.py @@ -0,0 +1,223 @@ +""" +Модуль смены аватарки бота. +Совместим с aiogram 3.22.0 и Bot API 9.4+ + +Использует: +- SetMyProfilePhoto +- InputProfilePhotoStatic +""" + +from __future__ import annotations + +import os +from typing import Union + +from aiogram import Router, Bot, F +from aiogram.filters import Command +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import StatesGroup, State +from aiogram.types import ( + Message, + CallbackQuery, + FSInputFile, + InputProfilePhotoStatic, +) +from aiogram.methods.set_my_profile_photo import SetMyProfilePhoto +from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter +from aiogram.utils.i18n import gettext as _ + +from bot.filters import IsSuperAdmin +from bot.templates import msg +from bot.utils import format_retry_time, status_clear +from bot.core.bots import BotInfo +from bot.handlers.commands.settings.settings_cmd import settings_keyboard +from configs import COMMANDS +from middleware.loggers import logger + +__all__ = ("router",) + +CMD: str = "set_avatar".lower() +router: Router = Router(name=f"{CMD}_router") + + +# ================= FSM ================= + +class SetBotAvatarForm(StatesGroup): + """ + FSM состояния для смены аватарки. + """ + waiting_for_photo: State = State() + + +# ================= CORE ================= + +async def handle_set_avatar( + update: Union[Message, CallbackQuery], + state: FSMContext, + bot: Bot +) -> None: + """ + Устанавливает новую аватарку бота. + + Args: + update: Message или CallbackQuery + state: FSM контекст + bot: Экземпляр бота + """ + + message: Message = update.message if isinstance(update, CallbackQuery) else update + + if not message.photo: + return + + largest_photo = message.photo[-1] + + # Получаем файл от Telegram + tg_file = await bot.get_file(largest_photo.file_id) + + temp_path: str = f"/tmp/{largest_photo.file_unique_id}.jpg" + + await bot.download_file(tg_file.file_path, destination=temp_path) + + try: + method = SetMyProfilePhoto( + photo=InputProfilePhotoStatic( + photo=FSInputFile(temp_path) + ) + ) + + result: bool = await bot(method) + + if result: + logger.info("Аватарка бота успешно обновлена", log_type="BOT_SETUP") + + await state.clear() + + await msg( + update=update, + text=_("✅ Аватарка бота успешно обновлена."), + markup=settings_keyboard(), + state=state + ) + + except TelegramRetryAfter as e: + retry_text: str = format_retry_time(e.retry_after) + + logger.warning( + f"Rate limit при смене аватарки. Повтор через {retry_text}", + log_type="BOT_SETUP" + ) + + await msg( + update=update, + text=_( + "⚠️ Слишком частая смена аватарки.\n" + "Попробуйте снова через: {retry}" + ).format(retry=retry_text), + markup=settings_keyboard(), + state=state + ) + + except TelegramAPIError as e: + logger.error( + f"Ошибка Telegram API при смене аватарки: {e}", + log_type="BOT_SETUP" + ) + + await msg( + update=update, + text=_( + "❌ Ошибка Telegram API:\n" + "
{error}"
+ ).format(error=str(e)),
+ markup=settings_keyboard(),
+ state=state
+ )
+
+ except Exception as e:
+ logger.error(
+ f"Непредвиденная ошибка при смене аватарки: {e}",
+ log_type="BOT_SETUP"
+ )
+
+ await msg(
+ update=update,
+ text=_(
+ "❌ Непредвиденная ошибка:\n"
+ "{error}"
+ ).format(error=str(e)),
+ markup=settings_keyboard(),
+ state=state
+ )
+
+ finally:
+ if os.path.exists(temp_path):
+ os.remove(temp_path)
+
+
+# ================= COMMAND =================
+
+@router.callback_query(F.data.lower() == CMD, IsSuperAdmin())
+@router.message(
+ Command(*COMMANDS.get(CMD, [CMD]), prefix=BotInfo.prefix, ignore_case=True),
+ IsSuperAdmin()
+)
+async def set_avatar_cmd(
+ message: Message | CallbackQuery,
+ state: FSMContext,
+ bot: Bot
+) -> None:
+ """
+ Команда /set_avatar
+
+ Поддерживает:
+ - Фото вместе с командой
+ - Ответ на фото
+ - FSM режим
+ """
+
+ await status_clear(update=message, state=state)
+
+ msg_obj: Message = message.message if isinstance(message, CallbackQuery) else message
+
+ if msg_obj.photo:
+ await handle_set_avatar(message, state, bot)
+ return
+
+ if msg_obj.reply_to_message and msg_obj.reply_to_message.photo:
+ await handle_set_avatar(msg_obj.reply_to_message, state, bot)
+ return
+
+ await msg(
+ update=message,
+ text=_(
+ "🖼 Смена аватарки бота\n\n"
+ "Отправьте фотографию."
+ ),
+ markup=settings_keyboard(),
+ state=state
+ )
+
+ await state.set_state(SetBotAvatarForm.waiting_for_photo)
+
+
+# ================= FSM =================
+
+@router.message(SetBotAvatarForm.waiting_for_photo, IsSuperAdmin(), F.photo)
+async def process_avatar_photo(
+ message: Message,
+ state: FSMContext,
+ bot: Bot
+) -> None:
+ """
+ Обработка фото через FSM.
+ """
+ await handle_set_avatar(message, state, bot)
+
+
+@router.message(SetBotAvatarForm.waiting_for_photo, IsSuperAdmin())
+async def invalid_input(message: Message) -> None:
+ """
+ Обработка некорректного ввода.
+ """
+ await message.answer(_("❌ Пожалуйста, отправьте фотографию."))