handlers.py: полная переработка под Django API, get_crop_coordinates, превью

This commit is contained in:
2026-08-14 09:57:52 +07:00
parent 5bd26d706e
commit a38fc7d889
+121 -63
View File
@@ -2,8 +2,6 @@
import os import os
import asyncio import asyncio
import logging import logging
import httpx
import smtplib
import time import time
from datetime import datetime from datetime import datetime
from email.mime.text import MIMEText from email.mime.text import MIMEText
@@ -15,18 +13,20 @@ from trueconf.types import FSInputFile
import config.config as config import config.config as config
from utils import states from utils import states
from utils.menu import MENU_TEXT 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 ( from utils.texts import (
EMOJI_DIGITS, EMOJI_DIGITS,
PHOTO_MAIN_MENU_TEXT, PHOTO_MAIN_MENU_TEXT,
system_error_text, system_error_text,
photo_error_handling, photo_error_handling,
PHOTO_UPLOADING_TEXT, PHOTO_PROCESSING_FACE_TEXT,
photo_success_sent, PHOTO_UPLOADING_API_TEXT,
photo_success_published,
photo_success_pending,
photo_error_rejected,
photo_error_already_pending,
photo_cancel_text, photo_cancel_text,
PHOTO_PROCESSING_TEXT,
photo_preview_ready, photo_preview_ready,
photo_error_processing,
) )
# 🔌 Импортируем централизованную функцию сбора статистики из main # 🔌 Импортируем централизованную функцию сбора статистики из main
@@ -36,6 +36,7 @@ logger = logging.getLogger(__name__)
router = Router() router = Router()
# Словарь для временного хранения путей к фото пользователей # Словарь для временного хранения путей к фото пользователей
# Формат: {user_id: {'path': '/tmp/orig_user.jpg', 'coords': (x1,y1,x2,y2)}}
pending_photos = {} pending_photos = {}
# ========================================================= # =========================================================
@@ -136,53 +137,76 @@ async def photo_module_handler(msg: Message):
log_menu_stats(user_id, "Photo Bot", "Выход в главное меню") log_menu_stats(user_id, "Photo Bot", "Выход в главное меню")
states.clear_state(user_id) states.clear_state(user_id)
if user_id in pending_photos: if user_id in pending_photos:
if os.path.exists(pending_photos[user_id]): photo_data = pending_photos[user_id]
os.remove(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] del pending_photos[user_id]
await msg.answer(MENU_TEXT, parse_mode="html") await msg.answer(MENU_TEXT, parse_mode="html")
return return
# --- ПОДТВЕРЖДЕНИЕ ОТПРАВКИ (Кнопка 1 или 2) --- # --- ПОДТВЕРЖДЕНИЕ ОТПРАВКИ (Кнопка 1) ---
if not is_attachment and user_id in pending_photos: if not is_attachment and user_id in pending_photos:
if cmd == "1": if cmd == "1":
photo_path = pending_photos[user_id] photo_data = pending_photos[user_id]
final_filename = f"{user_login}.bmp" original_path = photo_data['path']
await msg.answer(PHOTO_UPLOADING_TEXT, parse_mode="html") await msg.answer(PHOTO_UPLOADING_API_TEXT, parse_mode="html")
try: try:
async with httpx.AsyncClient(timeout=10.0) as client: # Отправляем фото через API
with open(photo_path, "rb") as f: result = await asyncio.to_thread(
files = {'file': (final_filename, f, 'image/bmp')} prepare_ad_photo, original_path, user_login
response = await client.post(config.TRASH_API_URL, files=files) )
if response.status_code == 200: if result["success"]:
# ФИКСАЦИЯ СТАТИСТИКИ: Успешная выгрузка фото на утверждение verdict = result.get("verdict")
log_menu_stats(user_id, "Photo Bot", "Отправка фото на утверждение") if verdict == "published":
await msg.answer(photo_success_sent(), parse_mode="html") log_menu_stats(user_id, "Photo Bot", "Фото опубликовано")
await msg.answer(photo_success_published(), parse_mode="html")
states.clear_state(user_id) elif verdict == "pending_moderation":
if os.path.exists(photo_path): log_menu_stats(user_id, "Photo Bot", "Фото на модерации")
os.remove(photo_path) await msg.answer(photo_success_pending(), parse_mode="html")
del pending_photos[user_id] else:
msg_text = result.get("message", "Фото обработано")
await asyncio.sleep(0.5) await msg.answer(f"{msg_text}", parse_mode="html")
await msg.answer(MENU_TEXT, parse_mode="html")
else: else:
error_msg = f"API вернул статус-код {response.status_code}" error = result.get("error", "Неизвестная ошибка")
await handle_photo_error(msg, user_id, "Отправка фото по API", error_msg) 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: 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 return
# --- ОТМЕНА ФОТО (Кнопка 2) --- # --- ОТМЕНА ФОТО (Кнопка 2) ---
elif cmd == "2": elif cmd == "2":
log_menu_stats(user_id, "Photo Bot", "Отмена загруженного фото") log_menu_stats(user_id, "Photo Bot", "Отмена загруженного фото")
photo_path = pending_photos[user_id] photo_data = pending_photos[user_id]
if os.path.exists(photo_path): original_path = photo_data['path']
os.remove(photo_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] del pending_photos[user_id]
states.clear_state(user_id)
await msg.answer(photo_cancel_text(), parse_mode="html") await msg.answer(photo_cancel_text(), parse_mode="html")
return return
@@ -194,43 +218,77 @@ async def photo_module_handler(msg: Message):
# --- ПРИЕМ НОВОГО ФОТО --- # --- ПРИЕМ НОВОГО ФОТО ---
if is_attachment: if is_attachment:
file_id = getattr(msg.content, "file_id", None) file_id = getattr(msg.content, "file_id", None)
ready_path = f"/tmp/{user_login}.bmp"
raw_path = None raw_path = None
await msg.answer(PHOTO_PROCESSING_TEXT, parse_mode="html") await msg.answer(PHOTO_PROCESSING_FACE_TEXT, parse_mode="html")
try: try:
downloaded_file = await states.tc_bot.download_file_by_id(file_id) downloaded_file = await states.tc_bot.download_file_by_id(file_id)
raw_path = downloaded_file raw_path = downloaded_file
result = await asyncio.to_thread(prepare_ad_photo, raw_path, ready_path, 96)
if result["success"]: # Читаем байты
# ФИКСАЦИЯ СТАТИСТИКИ: Факт успешной загрузки и обработки нового фото with open(raw_path, 'rb') as f:
log_menu_stats(user_id, "Photo Bot", "Загрузка и конвертация нового фото") image_bytes = f.read()
pending_photos[user_id] = ready_path
# Ищем лицо и получаем координаты
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" preview_path = f"/tmp/prev_{user_login}.jpg"
try: preview.save(preview_path, format="JPEG", quality=85)
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) preview_doc = FSInputFile(preview_path, filename="preview.jpg")
await msg.answer( await msg.answer_photo(preview_doc, preview_doc)
photo_preview_ready(), except Exception as lib_err:
parse_mode="html" await asyncio.to_thread(log_and_notify_photo_error, user_id, "Генерация превью", str(lib_err))
) raise Exception("Ошибка генерации превью")
else:
error_text = result.get('error', 'Не удалось обработать фото.') # Сохраняем оригинал и координаты
await msg.answer( original_path = f"/tmp/orig_{user_login}.jpg"
photo_error_processing(error_text), with open(original_path, 'wb') as f:
parse_mode="html" 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: except Exception as e:
await handle_photo_error(msg, user_id, "Скачивание и обработка файла", str(e)) await handle_photo_error(msg, user_id, "Скачивание и обработка файла", str(e))
finally: finally: