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 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"
photo_data = pending_photos[user_id]
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:
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)
# Отправляем фото через API
result = await asyncio.to_thread(
prepare_ad_photo, original_path, user_login
)
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")
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)
preview.save(preview_path, format="JPEG", quality=85)
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_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: