handlers.py: полная переработка под Django API, get_crop_coordinates, превью
This commit is contained in:
+125
-67
@@ -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:
|
||||||
# ФИКСАЦИЯ СТАТИСТИКИ: Факт успешной загрузки и обработки нового фото
|
image_bytes = f.read()
|
||||||
log_menu_stats(user_id, "Photo Bot", "Загрузка и конвертация нового фото")
|
|
||||||
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")
|
preview_doc = FSInputFile(preview_path, filename="preview.jpg")
|
||||||
p_thumb = FSInputFile(preview_path, filename="preview_thumb.jpg")
|
await msg.answer_photo(preview_doc, preview_doc)
|
||||||
await msg.answer_photo(p_main, p_thumb)
|
except Exception as lib_err:
|
||||||
except Exception as lib_err:
|
await asyncio.to_thread(log_and_notify_photo_error, user_id, "Генерация превью", str(lib_err))
|
||||||
await asyncio.to_thread(log_and_notify_photo_error, user_id, "Генерация превью", str(lib_err))
|
raise Exception("Ошибка генерации превью")
|
||||||
finally:
|
|
||||||
if os.path.exists(preview_path):
|
# Сохраняем оригинал и координаты
|
||||||
os.remove(preview_path)
|
original_path = f"/tmp/orig_{user_login}.jpg"
|
||||||
|
with open(original_path, 'wb') as f:
|
||||||
await asyncio.sleep(0.5)
|
f.write(image_bytes)
|
||||||
await msg.answer(
|
|
||||||
photo_preview_ready(),
|
pending_photos[user_id] = {
|
||||||
parse_mode="html"
|
'path': original_path,
|
||||||
)
|
'coords': coords,
|
||||||
else:
|
'preview_path': preview_path
|
||||||
error_text = result.get('error', 'Не удалось обработать фото.')
|
}
|
||||||
await msg.answer(
|
|
||||||
photo_error_processing(error_text),
|
log_menu_stats(user_id, "Photo Bot", "Загрузка и обработка нового фото")
|
||||||
parse_mode="html"
|
|
||||||
)
|
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:
|
||||||
|
|||||||
Reference in New Issue
Block a user