diff --git a/photo_bot/handlers.py b/photo_bot/handlers.py index d792d2f..f8f8a3d 100644 --- a/photo_bot/handlers.py +++ b/photo_bot/handlers.py @@ -2,8 +2,6 @@ import os import asyncio import logging -import httpx -import smtplib import time from datetime import datetime from email.mime.text import MIMEText @@ -15,18 +13,20 @@ from trueconf.types import FSInputFile import config.config as config from utils import states from utils.menu import MENU_TEXT -from photo_bot.photo_processor import prepare_ad_photo +from photo_bot.photo_processor import prepare_ad_photo, get_crop_coordinates from utils.texts import ( EMOJI_DIGITS, PHOTO_MAIN_MENU_TEXT, system_error_text, photo_error_handling, - PHOTO_UPLOADING_TEXT, - photo_success_sent, + PHOTO_PROCESSING_FACE_TEXT, + PHOTO_UPLOADING_API_TEXT, + photo_success_published, + photo_success_pending, + photo_error_rejected, + photo_error_already_pending, photo_cancel_text, - PHOTO_PROCESSING_TEXT, photo_preview_ready, - photo_error_processing, ) # 🔌 Импортируем централизованную функцию сбора статистики из main @@ -36,6 +36,7 @@ logger = logging.getLogger(__name__) router = Router() # Словарь для временного хранения путей к фото пользователей +# Формат: {user_id: {'path': '/tmp/orig_user.jpg', 'coords': (x1,y1,x2,y2)}} pending_photos = {} # ========================================================= @@ -136,53 +137,76 @@ async def photo_module_handler(msg: Message): log_menu_stats(user_id, "Photo Bot", "Выход в главное меню") states.clear_state(user_id) if user_id in pending_photos: - if os.path.exists(pending_photos[user_id]): - os.remove(pending_photos[user_id]) + photo_data = pending_photos[user_id] + if os.path.exists(photo_data['path']): + os.remove(photo_data['path']) + if 'preview_path' in photo_data and os.path.exists(photo_data['preview_path']): + os.remove(photo_data['preview_path']) del pending_photos[user_id] await msg.answer(MENU_TEXT, parse_mode="html") return - # --- ПОДТВЕРЖДЕНИЕ ОТПРАВКИ (Кнопка 1 или 2) --- + # --- ПОДТВЕРЖДЕНИЕ ОТПРАВКИ (Кнопка 1) --- if not is_attachment and user_id in pending_photos: if cmd == "1": - photo_path = pending_photos[user_id] - final_filename = f"{user_login}.bmp" - - await msg.answer(PHOTO_UPLOADING_TEXT, parse_mode="html") + photo_data = pending_photos[user_id] + original_path = photo_data['path'] + + await msg.answer(PHOTO_UPLOADING_API_TEXT, parse_mode="html") try: - async with httpx.AsyncClient(timeout=10.0) as client: - with open(photo_path, "rb") as f: - files = {'file': (final_filename, f, 'image/bmp')} - response = await client.post(config.TRASH_API_URL, files=files) - - if response.status_code == 200: - # ФИКСАЦИЯ СТАТИСТИКИ: Успешная выгрузка фото на утверждение - log_menu_stats(user_id, "Photo Bot", "Отправка фото на утверждение") - await msg.answer(photo_success_sent(), parse_mode="html") - - states.clear_state(user_id) - if os.path.exists(photo_path): - os.remove(photo_path) - del pending_photos[user_id] - - await asyncio.sleep(0.5) - await msg.answer(MENU_TEXT, parse_mode="html") + # Отправляем фото через API + result = await asyncio.to_thread( + prepare_ad_photo, original_path, user_login + ) + + if result["success"]: + verdict = result.get("verdict") + if verdict == "published": + log_menu_stats(user_id, "Photo Bot", "Фото опубликовано") + await msg.answer(photo_success_published(), parse_mode="html") + elif verdict == "pending_moderation": + log_menu_stats(user_id, "Photo Bot", "Фото на модерации") + await msg.answer(photo_success_pending(), parse_mode="html") + else: + msg_text = result.get("message", "Фото обработано") + await msg.answer(f"✅ {msg_text}", parse_mode="html") else: - error_msg = f"API вернул статус-код {response.status_code}" - await handle_photo_error(msg, user_id, "Отправка фото по API", error_msg) + error = result.get("error", "Неизвестная ошибка") + if "уже есть фото" in error: + await msg.answer(photo_error_already_pending(), parse_mode="html") + elif "отклонено" in error.lower() or "не прошел" in error.lower(): + await msg.answer(photo_error_rejected(error), parse_mode="html") + else: + await msg.answer(f"❌ Ошибка: {error}", parse_mode="html") except Exception as e: - await handle_photo_error(msg, user_id, "Подключение к API загрузки", str(e)) + await handle_photo_error(msg, user_id, "Отправка фото через API", str(e)) + + # Убираем за собой + if os.path.exists(original_path): + os.remove(original_path) + if 'preview_path' in photo_data and os.path.exists(photo_data['preview_path']): + os.remove(photo_data['preview_path']) + states.clear_state(user_id) + if user_id in pending_photos: + del pending_photos[user_id] + + await asyncio.sleep(0.5) + await msg.answer(MENU_TEXT, parse_mode="html") return # --- ОТМЕНА ФОТО (Кнопка 2) --- elif cmd == "2": log_menu_stats(user_id, "Photo Bot", "Отмена загруженного фото") - photo_path = pending_photos[user_id] - if os.path.exists(photo_path): - os.remove(photo_path) + photo_data = pending_photos[user_id] + original_path = photo_data['path'] + if os.path.exists(original_path): + os.remove(original_path) + if 'preview_path' in photo_data and os.path.exists(photo_data['preview_path']): + os.remove(photo_data['preview_path']) del pending_photos[user_id] + states.clear_state(user_id) await msg.answer(photo_cancel_text(), parse_mode="html") return @@ -194,43 +218,77 @@ async def photo_module_handler(msg: Message): # --- ПРИЕМ НОВОГО ФОТО --- if is_attachment: file_id = getattr(msg.content, "file_id", None) - ready_path = f"/tmp/{user_login}.bmp" raw_path = None - await msg.answer(PHOTO_PROCESSING_TEXT, parse_mode="html") + await msg.answer(PHOTO_PROCESSING_FACE_TEXT, parse_mode="html") try: downloaded_file = await states.tc_bot.download_file_by_id(file_id) raw_path = downloaded_file - result = await asyncio.to_thread(prepare_ad_photo, raw_path, ready_path, 96) - - if result["success"]: - # ФИКСАЦИЯ СТАТИСТИКИ: Факт успешной загрузки и обработки нового фото - log_menu_stats(user_id, "Photo Bot", "Загрузка и конвертация нового фото") - pending_photos[user_id] = ready_path + + # Читаем байты + with open(raw_path, 'rb') as f: + image_bytes = f.read() + + # Ищем лицо и получаем координаты + coords = await asyncio.to_thread(get_crop_coordinates, image_bytes) + + if not coords: + await msg.answer( + "❌ Лицо не найдено. Пожалуйста, сделайте фото анфас при хорошем освещении.\n\n" + f"{EMOJI_DIGITS['0']} — В главное меню", + parse_mode="html" + ) + if os.path.exists(raw_path): + os.remove(raw_path) + return + + # Генерируем превью + try: + from PIL import Image + import io + img = Image.open(io.BytesIO(image_bytes)) + if img.mode != 'RGB': + img = img.convert('RGB') + + x1, y1, x2, y2 = coords + cropped = img.crop((x1, y1, x2, y2)) + + # Ресайз до квадрата + side = min(cropped.size[0], cropped.size[1]) + left = (cropped.size[0] - side) // 2 + top = (cropped.size[1] - side) // 2 + cropped = cropped.crop((left, top, left + side, top + side)) + + preview = cropped.resize((400, 400), Image.Resampling.LANCZOS) + preview_path = f"/tmp/prev_{user_login}.jpg" - try: - Image.open(ready_path).save(preview_path, format="JPEG") - p_main = FSInputFile(preview_path, filename="preview.jpg") - p_thumb = FSInputFile(preview_path, filename="preview_thumb.jpg") - await msg.answer_photo(p_main, p_thumb) - except Exception as lib_err: - await asyncio.to_thread(log_and_notify_photo_error, user_id, "Генерация превью", str(lib_err)) - finally: - if os.path.exists(preview_path): - os.remove(preview_path) - - await asyncio.sleep(0.5) - await msg.answer( - photo_preview_ready(), - parse_mode="html" - ) - else: - error_text = result.get('error', 'Не удалось обработать фото.') - await msg.answer( - photo_error_processing(error_text), - parse_mode="html" - ) + preview.save(preview_path, format="JPEG", quality=85) + + preview_doc = FSInputFile(preview_path, filename="preview.jpg") + await msg.answer_photo(preview_doc, preview_doc) + except Exception as lib_err: + await asyncio.to_thread(log_and_notify_photo_error, user_id, "Генерация превью", str(lib_err)) + raise Exception("Ошибка генерации превью") + + # Сохраняем оригинал и координаты + original_path = f"/tmp/orig_{user_login}.jpg" + with open(original_path, 'wb') as f: + f.write(image_bytes) + + pending_photos[user_id] = { + 'path': original_path, + 'coords': coords, + 'preview_path': preview_path + } + + log_menu_stats(user_id, "Photo Bot", "Загрузка и обработка нового фото") + + await asyncio.sleep(0.5) + await msg.answer( + photo_preview_ready(), + parse_mode="html" + ) except Exception as e: await handle_photo_error(msg, user_id, "Скачивание и обработка файла", str(e)) finally: