import logging from datetime import datetime from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder from tortoise.exceptions import DoesNotExist from app import config from app.models import Review, Settings from app.services import GPTClient, OzonClient, WildberriesClient, YandexMarketClient logger = logging.getLogger(__name__) router = Router() def _settings_keyboard(settings: Settings) -> types.InlineKeyboardMarkup: """Клавиатура настроек: автоответ с текстом, автоответ без текста, анализ, модель.""" return types.InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text=f"Авто-ответ (с текстом): {'✅' if settings.auto_response_enabled else '❌'}", callback_data="toggle_auto_response", ) ], [ InlineKeyboardButton( text=f"Авто-ответ (без текста): {'✅' if settings.auto_response_empty_enabled else '❌'}", callback_data="toggle_auto_response_empty", ) ], [ InlineKeyboardButton( text=f"Анализ отзывов: {'✅' if settings.analysis_enabled else '❌'}", callback_data="toggle_analyze", ) ], [ InlineKeyboardButton( text=f"GPT модель: {settings.gpt_model}", callback_data="change_model", ) ], ] ) class CustomReplyStates(StatesGroup): waiting_for_text = State() @router.callback_query(F.data.startswith("auto:")) async def callback_auto(call: CallbackQuery) -> None: action = call.data.split(":", 1)[1] settings = await Settings.get_from_context( message_thread_id=call.message.message_thread_id ) if action == "enable": settings.auto_response_enabled = True await settings.save() await call.answer("Автоответы включены.") elif action == "disable": settings.auto_response_enabled = False await settings.save() await call.answer("Автоответы выключены.") status_text = "Включены" if settings.auto_response_enabled else "Выключены" await call.message.edit_text( f"Статус автоответов: {status_text}", parse_mode="HTML", reply_markup=call.message.reply_markup, ) @router.callback_query(F.data.startswith("model:")) async def callback_model(call: CallbackQuery) -> None: model = call.data.split(":", 1)[1] settings = await Settings.get_from_context( message_thread_id=call.message.message_thread_id ) settings.gpt_model = model await settings.save() await call.answer(f"Модель изменена на {model}") await call.message.edit_text( f"Модель изменена на {model}", parse_mode="HTML" ) @router.callback_query(F.data.startswith("ozon_delete:")) async def callback_ozon_delete(call: CallbackQuery) -> None: comment_id = call.data.split(":", 1)[1] ozon_client = OzonClient() result = await ozon_client.delete_comment(comment_id) if "error" not in result: try: review = await Review.filter(response_id=comment_id).first() if review: review.response_text = None review.response_id = None review.responded_at = None await review.save() except Exception as e: logger.error(f"Error updating review after comment deletion: {e}") await call.answer("Комментарий успешно удалён на Ozon.") await call.message.reply("🗑 Ответ успешно удален с Ozon") else: await call.answer("Не удалось удалить комментарий.") await call.message.reply( f"❌ Ошибка удаления комментария: {result.get('error')}" ) @router.callback_query(F.data.startswith("wb_delete:")) async def callback_wb_delete(call: CallbackQuery) -> None: reply_id = call.data.split(":", 1)[1] wb_client = WildberriesClient() result = await wb_client.delete_comment(reply_id) if result.get("success"): try: review = await Review.filter(response_id=reply_id).first() if review: review.response_text = None review.response_id = None review.responded_at = None await review.save() except Exception as e: logger.error(f"Error updating review after comment deletion: {e}") await call.answer("Комментарий успешно удалён на Wildberries.") await call.message.reply("🗑 Ответ успешно удален с Wildberries") else: await call.answer("Не удалось удалить комментарий.") await call.message.reply( f"❌ Ошибка удаления комментария: {result.get('error')}" ) @router.callback_query(F.data.startswith("ym_delete:")) async def callback_ym_delete(call: CallbackQuery) -> None: comment_id = call.data.split(":", 1)[1] ym_client = YandexMarketClient() result = await ym_client.delete_comment(comment_id) if "error" not in result: try: review = await Review.filter(response_id=comment_id, platform="ym").first() if review: review.response_text = None review.response_id = None review.responded_at = None await review.save() except Exception as e: logger.error(f"Error updating review after comment deletion: {e}") await call.answer("Комментарий успешно удалён на Яндекс Маркете.") await call.message.reply("🗑 Ответ успешно удален с Яндекс Маркета") else: await call.answer("Не удалось удалить комментарий.") await call.message.reply( f"❌ Ошибка удаления комментария: {result.get('error')}" ) @router.callback_query(F.data.startswith("generate_answer:")) async def callback_generate_answer(call: CallbackQuery) -> None: review_id = call.data.split(":", 1)[1] try: review = await Review.get(id=review_id) settings = await Settings.get_from_context( message_thread_id=call.message.message_thread_id ) gpt_client = GPTClient() answer = await gpt_client.generate_answer(settings, review) markup = InlineKeyboardBuilder() markup.row( InlineKeyboardButton( text="Редактировать", callback_data=f"custom_reply:{review_id}", ), InlineKeyboardButton( text="Отправить", callback_data=f"send_answer:{review_id}", ), ) await call.message.reply( f"#review{review_id}\n\n
{answer}", parse_mode="HTML", reply_markup=markup.as_markup(), ) # Store the generated answer temporarily review.response_text = answer await review.save() await call.answer("Ответ сгенерирован") except DoesNotExist: await call.answer("Отзыв не найден.") except Exception as e: logger.error(f"Error generating answer: {e}") await call.answer("Произошла ошибка при генерации ответа.") @router.callback_query(F.data.startswith("quick_reply:")) async def callback_quick_reply(call: CallbackQuery) -> None: """Подставить шаблонный ответ для пустого отзыва и показать кнопку «Отправить ответ».""" parts = call.data.split(":", 2) if len(parts) != 3: await call.answer("Неверные данные.") return _, review_id, template = parts try: review = await Review.get(id=review_id) settings = await Settings.get_from_context(review=review) text = ( (settings.template_empty_high_rating or config.TEMPLATE_EMPTY_HIGH_RATING) if template == "high" else (settings.template_empty_low_rating or config.TEMPLATE_EMPTY_LOW_RATING) ) review.response_text = text await review.save() markup = InlineKeyboardBuilder() markup.row( InlineKeyboardButton( text="Отправить ответ", callback_data=f"send_answer:{review_id}", ), ) await call.message.reply( f"#review{review_id}\n\nБудет отправлено:\n
{text}\n\nНажмите «Отправить ответ».", parse_mode="HTML", reply_markup=markup.as_markup(), ) await call.answer("Текст подставлен. Нажмите «Отправить ответ».") except DoesNotExist: await call.answer("Отзыв не найден.") except Exception as e: logger.error(f"Error in quick_reply: {e}") await call.answer("Ошибка при подстановке ответа.") @router.callback_query(F.data.startswith("custom_reply:")) async def callback_custom_reply(call: CallbackQuery, state: FSMContext) -> None: """Initiate custom reply for a review.""" review_id = call.data.split(":", 1)[1] await state.update_data(review_id=review_id) await state.set_state(CustomReplyStates.waiting_for_text) await call.message.reply( f"✏️ Введите ваш ответ на отзыв #review{review_id}:", parse_mode="HTML" ) await call.answer("Введите ваш ответ") @router.message(CustomReplyStates.waiting_for_text) async def process_custom_reply(message: Message, state: FSMContext) -> None: custom_text = message.text state_data = await state.get_data() review_id = state_data.get("review_id") try: review = await Review.get(id=review_id) review.response_text = custom_text await review.save() await state.clear() markup = InlineKeyboardBuilder() markup.row( InlineKeyboardButton( text="Редактировать", callback_data=f"custom_reply:{review_id}", ), InlineKeyboardButton( text="Отправить", callback_data=f"send_answer:{review_id}", ), ) await message.answer( f"#review{review_id}\n\n
{custom_text}\n\nВыберите действие:", parse_mode="HTML", reply_markup=markup.as_markup(), reply_to_message_id=review.message_id, ) except DoesNotExist: await message.reply("Отзыв не найден.") await state.clear() except Exception as e: logger.error(f"Error processing custom reply: {e}") await message.reply("Произошла ошибка при обработке ответа.") await state.clear() @router.callback_query(F.data.startswith("send_answer:")) async def callback_send_answer(call: CallbackQuery) -> None: review_id = call.data.split(":")[1] try: review = await Review.get(id=review_id) if not review.response_text: await call.answer("Сначала выберите или введите ответ.", show_alert=True) return if review.responded_at: await call.message.edit_text("Ответ на отзыв уже отправлен.") await call.answer("Ответ уже отправлен.", show_alert=True) return if review.platform == "ozon": ozon_client = OzonClient() result = await ozon_client.post_comment( review.external_id, review.response_text ) if "comment_id" in result: review.response_id = result["comment_id"] await review.save() markup = InlineKeyboardBuilder() markup.add( InlineKeyboardButton( text="Удалить", callback_data=f"ozon_delete:{result['comment_id']}", ) ) await call.message.answer( f"✅ Ответ на #review{review_id} успешно отправлен на Ozon", reply_to_message_id=review.message_id, reply_markup=markup.as_markup(), ) else: await call.message.reply( f"❌ Ошибка отправки ответа на Ozon: {result.get('error')}" ) elif review.platform == "wb": wb_client = WildberriesClient() result = await wb_client.post_comment( review.external_id, review.response_text ) if result.get("success"): await call.message.answer( f"✅ Ответ на #review{review_id} успешно отправлен на Wildberries!", reply_to_message_id=review.message_id, ) else: await call.message.reply( f"❌ Ошибка отправки ответа на Wildberries: {result.get('error')}" ) elif review.platform == "ym": ym_client = YandexMarketClient() result = await ym_client.post_comment( review.external_id, review.response_text ) if "comment_id" in result: review.response_id = result["comment_id"] await review.save() markup = InlineKeyboardBuilder() markup.add( InlineKeyboardButton( text="Удалить", callback_data=f"ym_delete:{result['comment_id']}", ) ) await call.message.answer( f"✅ Ответ на #review{review_id} успешно отправлен на Яндекс Маркет", reply_to_message_id=review.message_id, reply_markup=markup.as_markup(), ) else: await call.message.reply( f"❌ Ошибка отправки ответа на Яндекс Маркет: {result.get('error')}" ) await call.answer("Ответ отправлен") review.responded_at = datetime.now() await review.save() except DoesNotExist: await call.answer("Отзыв не найден.") except Exception as e: logger.error(f"Error sending answer: {e}") await call.answer("Произошла ошибка при отправке ответа.") @router.callback_query(F.data == "toggle_auto_response") async def toggle_auto_response(callback: types.CallbackQuery) -> None: settings = await Settings.get_from_context( message_thread_id=callback.message.message_thread_id ) settings.auto_response_enabled = not settings.auto_response_enabled await settings.save() status_text = "Включен" if settings.auto_response_enabled else "Выключен" await callback.answer(f"Авто-ответ (с текстом): {status_text}") await callback.message.edit_reply_markup(reply_markup=_settings_keyboard(settings)) @router.callback_query(F.data == "toggle_auto_response_empty") async def toggle_auto_response_empty(callback: types.CallbackQuery) -> None: settings = await Settings.get_from_context( message_thread_id=callback.message.message_thread_id ) settings.auto_response_empty_enabled = not settings.auto_response_empty_enabled await settings.save() status_text = "Включен" if settings.auto_response_empty_enabled else "Выключен" await callback.answer(f"Авто-ответ (без текста): {status_text}") await callback.message.edit_reply_markup(reply_markup=_settings_keyboard(settings)) @router.callback_query(F.data == "change_model") async def change_model(callback: types.CallbackQuery) -> None: settings = await Settings.get_from_context( message_thread_id=callback.message.message_thread_id ) if settings.gpt_model == "gpt-4o": settings.gpt_model = "gpt-4o-mini" else: settings.gpt_model = "gpt-4o" await settings.save() await callback.answer(f"Модель изменена на {settings.gpt_model}") await callback.message.edit_reply_markup(reply_markup=_settings_keyboard(settings)) @router.callback_query(F.data == "toggle_analyze") async def toggle_analyze(callback: types.CallbackQuery) -> None: settings = await Settings.get_from_context( message_thread_id=callback.message.message_thread_id ) settings.analysis_enabled = not settings.analysis_enabled await settings.save() status_text = "Включен" if settings.analysis_enabled else "Выключен" await callback.answer(f"Анализ отзывов: {status_text}") await callback.message.edit_reply_markup(reply_markup=_settings_keyboard(settings))