import os import asyncio import logging import sys import io # Добавляем путь к корню проекта sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from trueconf import Bot, Dispatcher, Router, Message from trueconf.types import FSInputFile # Наши обработчики from photo_processor import prepare_ad_photo, get_crop_coordinates from utils.texts import ( PHOTO_MAIN_MENU_TEXT, PHOTO_UNKNOWN_CMD_TEXT, PHOTO_PROCESSING_FACE_TEXT, PHOTO_UPLOADING_API_TEXT, photo_preview_ready, photo_success_published, photo_success_pending, photo_error_rejected, photo_error_already_pending, photo_cancel_text, photo_error_handling, EMOJI_DIGITS, ) # Настройки from config.config import * # --- НАСТРОЙКА ЛОГИРОВАНИЯ --- logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.FileHandler("photo_bot_log.txt", encoding="utf-8"), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) router = Router() tc_bot = None # 🧠 ПАМЯТЬ БОТА: Храним пути к фото, ожидающим подтверждения # Пример: {'ds.krivochenko': {'path': '/tmp/orig_abc.jpg', 'coords': (100, 50, 300, 400)}} pending_photos = {} def _generate_preview(image_bytes: bytes, coords: tuple, size: int = 400) -> bytes | None: """Генерирует превью фото с кропом по координатам.""" try: from PIL import Image 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((size, size), Image.Resampling.LANCZOS) buf = io.BytesIO() preview.save(buf, format="JPEG", quality=85) buf.seek(0) return buf.getvalue() except Exception as e: logger.error(f"Ошибка генерации превью: {e}") return None @router.message() async def handle_message(msg: Message): global tc_bot # Определяем логин пользователя user_login = msg.from_user.id.split('@')[0] if '@' in msg.from_user.id else msg.from_user.id # Проверяем, вложение ли это is_attachment = hasattr(msg.type, "name") and msg.type.name == "ATTACHMENT" # ===================================================================== # СЦЕНАРИЙ 1: Бот ждет ответа, и пользователь написал текст (Да/Нет) # ===================================================================== if not is_attachment and user_login in pending_photos and msg.text: answer = msg.text.strip().lower() # Если согласен if answer in ["да", "yes", "+", "загрузить", "давай", "конечно"]: photo_data = pending_photos[user_login] original_path = photo_data['path'] coords = photo_data['coords'] await msg.answer(PHOTO_UPLOADING_API_TEXT) try: # Отправляем фото через API result = await asyncio.to_thread( prepare_ad_photo, original_path, user_login ) if result["success"]: verdict = result.get("verdict") if verdict == "published": await msg.answer(photo_success_published()) elif verdict == "pending_moderation": await msg.answer(photo_success_pending()) else: # Успех, но вердикт другой — показываем сообщение msg_text = result.get("message", "Фото обработано") await msg.answer(f"✅ {msg_text}") else: error = result.get("error", "Неизвестная ошибка") if "уже есть фото" in error: await msg.answer(photo_error_already_pending()) elif "отклонено" in error.lower() or "не прошел" in error.lower(): await msg.answer(photo_error_rejected(error)) else: await msg.answer(f"❌ Ошибка: {error}") except Exception as e: logger.error(f"Ошибка отправки фото: {e}") await msg.answer(photo_error_handling()) # Убираем за собой if os.path.exists(original_path): os.remove(original_path) del pending_photos[user_login] return # Если отказался elif answer in ["нет", "no", "-", "отмена"]: photo_data = pending_photos[user_login] original_path = photo_data['path'] await msg.answer(photo_cancel_text()) if os.path.exists(original_path): os.remove(original_path) del pending_photos[user_login] return # Если написал что-то непонятное else: await msg.answer( f"❓ Пожалуйста, нажмите:\n" f"1️⃣ — Подтвердить\n" f"2️⃣ — Вырезать заново\n\n" f"Или просто отправьте новое фото." ) return # ===================================================================== # СЦЕНАРИЙ 2: Пользователь прислал новую фотографию # ===================================================================== if is_attachment: file_id = getattr(msg.content, "file_id", None) if not file_id: await msg.answer("❌ Не удалось получить файл. Попробуйте еще раз.") return await msg.answer(PHOTO_PROCESSING_FACE_TEXT) try: downloaded_file = await tc_bot.download_file_by_id(file_id) raw_path = downloaded_file # Читаем байты 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']} — В главное меню" ) if os.path.exists(raw_path): os.remove(raw_path) return # Генерируем превью preview_bytes = _generate_preview(image_bytes, coords, size=400) if preview_bytes: # Сохраняем оригинал и координаты preview_path = f"/tmp/preview_{user_login}.jpg" with open(preview_path, 'wb') as f: f.write(preview_bytes) # Сохраняем оригинал original_path = f"/tmp/orig_{user_login}.jpg" with open(original_path, 'wb') as f: f.write(image_bytes) # Отправляем превью preview_doc = FSInputFile(preview_path, filename=f"preview_{user_login}.jpg") await msg.answer_photo( preview_doc, preview_doc, caption=photo_preview_ready() ) # Сохраняем состояние pending_photos[user_login] = { 'path': original_path, 'coords': coords, 'preview_path': preview_path } logger.info(f"Фото подготовлено для {user_login}, координаты: {coords}") else: await msg.answer("❌ Ошибка обработки фото. Попробуйте снова.") except Exception as e: logger.error(f"Ошибка при работе с фото для {user_login}: {e}") await msg.answer(photo_error_handling()) finally: # Удаляем оригинал TrueConf, но не превью (оно в pending_photos) if 'raw_path' in locals() and os.path.exists(raw_path): try: os.remove(raw_path) except: pass return # ===================================================================== # СЦЕНАРИЙ 3: Дефолтное приветствие (если просто написали текст) # ===================================================================== await msg.answer(PHOTO_MAIN_MENU_TEXT) async def main(): global tc_bot logger.info(f"Запуск фото-бота на сервере {TC_SERVER}...") dp = Dispatcher() dp.include_router(router) tc_bot = Bot.from_credentials( server=TC_SERVER, username=TC_LOGIN, password=TC_PASSWORD, dispatcher=dp, verify_ssl=False ) await tc_bot.run() if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: logger.info("Бот остановлен пользователем.")