573 lines
28 KiB
Python
573 lines
28 KiB
Python
# /opt/trueconf_bot/photo_bot/handlers.py
|
||
import os
|
||
import asyncio
|
||
import logging
|
||
import smtplib
|
||
import time
|
||
from datetime import datetime
|
||
from email.mime.text import MIMEText
|
||
from email.mime.multipart import MIMEMultipart
|
||
from PIL import Image
|
||
from trueconf import Router, Message
|
||
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, get_crop_coordinates, check_photo_api
|
||
from utils.texts import (
|
||
UNKNOWN_MAIN_CMD_TEXT,
|
||
EMOJI_DIGITS,
|
||
PHOTO_MAIN_MENU_TEXT,
|
||
photo_error_handling,
|
||
PHOTO_PROCESSING_FACE_TEXT,
|
||
PHOTO_AI_PROCESSING_TEXT,
|
||
photo_success_published,
|
||
photo_success_pending,
|
||
photo_error_rejected,
|
||
photo_error_already_pending,
|
||
photo_cancel_text,
|
||
photo_preview_ready,
|
||
photo_welcome_with_status,
|
||
photo_test_welcome,
|
||
PHOTO_TEST_WELCOME_TEXT,
|
||
PHOTO_TEST_PREVIEW_TEXT,
|
||
PHOTO_TEST_AI_CHECKING_TEXT,
|
||
photo_test_result,
|
||
photo_test_ai_error,
|
||
)
|
||
|
||
# 🔌 Импортируем централизованную функцию сбора статистики из main
|
||
from utils.stats_logger import log_menu_stats
|
||
|
||
logger = logging.getLogger(__name__)
|
||
router = Router()
|
||
|
||
# Словарь для временного хранения путей к фото пользователей
|
||
# Формат: {user_id: {'path': '/tmp/orig_user.jpg', 'coords': (x1,y1,x2,y2)}}
|
||
pending_photos = {}
|
||
|
||
# Пользователи, находящиеся в под-режиме «тест проверка фото»
|
||
test_submode_users = set()
|
||
|
||
# Пользователи, у которых фото на модерации (вход заблокирован,
|
||
# модуль остаётся открыт: в тест-режиме можно отправить фото на тест)
|
||
moderation_pending_users = set()
|
||
|
||
def in_test_submode(user_id: str) -> bool:
|
||
"""Проверяет, находится ли пользователь в под-режиме тест-проверки фото"""
|
||
if user_id not in test_submode_users:
|
||
return False
|
||
# Если сессия истекла (состояние сброшено) — под-режим тоже недействителен
|
||
return states.get_state(user_id) == "PHOTO_MODE"
|
||
|
||
# =========================================================
|
||
# СИСТЕМА ОБРАБОТКИ ОШИБОК И ЛОГИРОВАНИЯ
|
||
# =========================================================
|
||
EMAIL_COOLDOWN_SEC = 10.0
|
||
last_email_time = 0.0
|
||
|
||
def log_and_notify_photo_error(user_id: str, action: str, error_details: str):
|
||
"""Записывает системную ошибку в лог-файл и отправляет email-алерт администраторам"""
|
||
global last_email_time
|
||
|
||
log_dir = os.path.dirname(os.path.abspath(__file__))
|
||
os.makedirs(log_dir, exist_ok=True)
|
||
log_file = os.path.join(log_dir, "photo_errors.log")
|
||
|
||
current_time = time.time()
|
||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
log_entry = f"[{timestamp}] User: {user_id} | Action: {action} | Error: {error_details}\n"
|
||
|
||
with open(log_file, "a", encoding="utf-8") as f:
|
||
f.write(log_entry)
|
||
|
||
if current_time - last_email_time < EMAIL_COOLDOWN_SEC:
|
||
with open(log_file, "a", encoding="utf-8") as f:
|
||
f.write(f"[{timestamp}] ALERT: Email suppressed due to rate limiting.\n")
|
||
return
|
||
|
||
last_email_time = current_time
|
||
|
||
try:
|
||
msg = MIMEMultipart()
|
||
msg['From'] = str(getattr(config, 'DEFAULT_EMAIL_FROM', 'bot@noreply.com'))
|
||
|
||
to_emails = getattr(config, 'ALERTS_SUPPORT_EMAILS', 'admin@example.com')
|
||
msg['To'] = ", ".join(to_emails) if isinstance(to_emails, list) else str(to_emails)
|
||
|
||
msg['Subject'] = f"TrueConf Bot Error: Фото Профиль - {action}"
|
||
|
||
body = (
|
||
f"Обнаружена системная ошибка в модуле загрузки фото профиля.\n\n"
|
||
f"Время: {timestamp}\n"
|
||
f"Пользователь (ID): {user_id}\n"
|
||
f"Действие: {action}\n\n"
|
||
f"Детали ошибки:\n{error_details}\n\n"
|
||
f"(Уведомления об ошибках ограничены: не чаще 1 раза в {EMAIL_COOLDOWN_SEC} сек.)"
|
||
)
|
||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||
|
||
smtp_server = getattr(config, 'SMTP_SERVER', 'localhost')
|
||
smtp_port = int(getattr(config, 'SMTP_PORT', 25))
|
||
|
||
with smtplib.SMTP(smtp_server, smtp_port) as server:
|
||
smtp_user = getattr(config, 'SMTP_USER', None)
|
||
smtp_pass = getattr(config, 'SMTP_PASSWORD', None)
|
||
if smtp_user and smtp_pass:
|
||
server.login(smtp_user, smtp_pass)
|
||
server.send_message(msg)
|
||
except Exception as e:
|
||
with open(log_file, "a", encoding="utf-8") as f:
|
||
f.write(f"[{timestamp}] СБОЙ ОТПРАВКИ EMAIL: {e}\n")
|
||
|
||
|
||
async def handle_photo_error(msg: Message, user_id: str, action: str, error_details: str):
|
||
"""Вызывает системное логирование и выдает заглушку пользователю"""
|
||
logger.error(f"🔥 Ошибка фото-бота для {user_id}, действие: {action}: {error_details}")
|
||
await asyncio.to_thread(log_and_notify_photo_error, user_id, action, error_details)
|
||
|
||
await msg.answer(photo_error_handling(), parse_mode="html")
|
||
|
||
|
||
async def check_moderation_status(user_id: str) -> dict:
|
||
"""Проверяет текущий статус модерации пользователя через API"""
|
||
from photo_bot.photo_processor import API_URL, API_KEY
|
||
import requests
|
||
|
||
# Извлекаем логин из email (user_id@tcs.sibcem.ru -> user_id)
|
||
login = user_id.split('@')[0]
|
||
|
||
status_url = API_URL.replace("/upload/", "/status/")
|
||
try:
|
||
response = requests.get(
|
||
status_url,
|
||
params={"user_id": login},
|
||
headers={"X-API-Key": API_KEY},
|
||
timeout=15,
|
||
verify=False
|
||
)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
except Exception as e:
|
||
logger.error(f"Ошибка проверки статуса модерации: {e}")
|
||
return {"status": "error", "reason": str(e)}
|
||
|
||
|
||
# =========================================================
|
||
# ВХОД В РАЗДЕЛ ФОТО: проверка статуса модерации + показ меню
|
||
# =========================================================
|
||
async def send_dynamic_photo_main_menu(msg: Message, user_id: str):
|
||
"""Вход в раздел фото (паттерн ЛК): модуль сам запрашивает данные и показывает их.
|
||
|
||
- pending — сообщение «фото на модерации»; модуль остаётся открыт
|
||
(цифры → ошибка неверного выбора, 0 — выход). Тест-режим:
|
||
фото можно отправить на проверку в ИИ;
|
||
- rejected — ОДНО сообщение: меню + причина отказа (кнопки на месте);
|
||
- иначе/ошибка API — штатное меню входа.
|
||
Статус проверяется и показывается всегда, включая тестовый режим.
|
||
"""
|
||
# Новый вход в модуль (кнопка 4 из главного меню) = свежий сеанс.
|
||
# Тайм-аут сессии чистит только user_states, а module-level pending_photos
|
||
# и test_submode_users остаются: висящий тест-кадр из прошлой сессии иначе
|
||
# улетит на проверку сразу после нажатия «1». Сбрасываем здесь.
|
||
if user_id in pending_photos:
|
||
_pd = pending_photos[user_id]
|
||
for _k in ('path', 'preview_path'):
|
||
_p = _pd.get(_k)
|
||
if _p and os.path.exists(_p):
|
||
try:
|
||
os.remove(_p)
|
||
except OSError:
|
||
pass
|
||
del pending_photos[user_id]
|
||
test_submode_users.discard(user_id)
|
||
|
||
# Статус модерации запрашиваем всегда (и в тестовом режиме тоже)
|
||
status = await check_moderation_status(user_id)
|
||
if status.get("status") == "ok" and status.get("moderation_status") == "pending":
|
||
# Фото на модерации — штатная загрузка заблокирована,
|
||
# но модуль остаётся открыт (PHOTO_MODE), чтобы цифры не ушли в главное меню
|
||
log_menu_stats(user_id, "Photo Bot", "Вход — фото на модерации")
|
||
states.set_state(user_id, "PHOTO_MODE")
|
||
moderation_pending_users.add(user_id)
|
||
# Единый вход: «фото на модерации» (+ кнопка 1 — тест, если тест-режим включён).
|
||
# Описание тест-режима показываем ТОЛЬКО после нажатия кнопки 1
|
||
await msg.answer(
|
||
photo_welcome_with_status(status, show_test=config.PHOTO_TEST_MODE),
|
||
parse_mode="html"
|
||
)
|
||
return
|
||
if status.get("status") == "ok" and status.get("moderation_status") == "rejected":
|
||
log_menu_stats(user_id, "Photo Bot", "Вход — фото отклонено, показана причина")
|
||
elif status.get("status") != "ok":
|
||
logger.warning(
|
||
f"Не удалось проверить статус модерации для {user_id}: {status.get('reason')}"
|
||
)
|
||
status = None
|
||
|
||
states.set_state(user_id, "PHOTO_MODE")
|
||
log_menu_stats(user_id, "Photo Bot (TEST)" if config.PHOTO_TEST_MODE else "Photo Bot", "Вход")
|
||
# Одно универсальное сообщение: меню (+ тест-кнопка) + (если нужно) статус модерации
|
||
entry_text = photo_welcome_with_status(status, show_test=config.PHOTO_TEST_MODE)
|
||
await msg.answer(entry_text, parse_mode="html")
|
||
|
||
|
||
# =========================================================
|
||
# ОСНОВНОЙ ОБРАБОТЧИК
|
||
# =========================================================
|
||
@router.message()
|
||
async def photo_module_handler(msg: Message):
|
||
user_id = msg.from_user.id
|
||
|
||
if states.get_state(user_id) != "PHOTO_MODE":
|
||
return
|
||
|
||
# Помечаем сообщение как обработанное
|
||
msg.handled = True
|
||
|
||
user_login = user_id.split('@')[0] if '@' in user_id else user_id
|
||
msg_text = getattr(msg.content, "text", "").strip() if hasattr(msg.content, "text") else ""
|
||
is_attachment = hasattr(msg.type, "name") and msg.type.name == "ATTACHMENT"
|
||
|
||
cmd = msg_text.lower()
|
||
|
||
# 🔄 НОРМАЛИЗАЦИЯ КНОПОК ВК-ЭМОДЗИ
|
||
for raw_num, emoji_num in EMOJI_DIGITS.items():
|
||
if cmd == emoji_num:
|
||
cmd = raw_num
|
||
break
|
||
|
||
# --- ТЕСТ-ПОД-РЕЖИМ: 9 = назад в меню фото (не выход из модуля) ---
|
||
if in_test_submode(user_id) and cmd == "9" and not is_attachment:
|
||
test_submode_users.discard(user_id)
|
||
log_menu_stats(user_id, "Photo Bot (TEST)", "Назад из тест-под-режима")
|
||
# Висящее тест-фото снимаем (тест-режим: оно никуда не публиковалось)
|
||
if user_id in pending_photos:
|
||
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]
|
||
status = await check_moderation_status(user_id)
|
||
await msg.answer(
|
||
photo_welcome_with_status(status, show_test=config.PHOTO_TEST_MODE),
|
||
parse_mode="html"
|
||
)
|
||
return
|
||
|
||
# --- ВЫХОД (Кнопки 0 и 9 возвращают в общее меню с очисткой кэша) ---
|
||
if cmd in ["0", "9", "/start", "меню"]:
|
||
log_menu_stats(user_id, "Photo Bot", "Выход в главное меню")
|
||
states.clear_state(user_id)
|
||
test_submode_users.discard(user_id)
|
||
moderation_pending_users.discard(user_id)
|
||
if user_id in pending_photos:
|
||
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
|
||
|
||
# --- СОСТОЯНИЕ «ФОТО НА МОДЕРАЦИИ»: активного фото нет ---
|
||
if user_id in moderation_pending_users and user_id not in pending_photos:
|
||
if not is_attachment:
|
||
status = await check_moderation_status(user_id)
|
||
still_pending = (
|
||
status.get("status") == "ok"
|
||
and status.get("moderation_status") == "pending"
|
||
)
|
||
if still_pending:
|
||
if config.PHOTO_TEST_MODE and cmd == "1":
|
||
# Кнопка 1 — вход в тест-под-режим (можно отправить фото в ИИ)
|
||
test_submode_users.add(user_id)
|
||
log_menu_stats(user_id, "Photo Bot (TEST)", "Тест-под-режим (фото на модерации)")
|
||
await msg.answer(PHOTO_TEST_WELCOME_TEXT, parse_mode="html")
|
||
else:
|
||
# Неверная команда → глобальное сообщение
|
||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||
return
|
||
# Статус изменился (одобрено/отклонено) — разблокируем
|
||
was_in_test = in_test_submode(user_id)
|
||
moderation_pending_users.discard(user_id)
|
||
if config.PHOTO_TEST_MODE and was_in_test:
|
||
await msg.answer(PHOTO_TEST_WELCOME_TEXT, parse_mode="html")
|
||
else:
|
||
await msg.answer(
|
||
photo_welcome_with_status(status), parse_mode="html"
|
||
)
|
||
return
|
||
# is_attachment — пропускаем: фото обрабатывается штатно ниже
|
||
# (тест-под-режим: превью + отправка в ИИ)
|
||
|
||
# --- ПРОВЕРКА СТАТУСА: если нет фото в pending_photos, проверяем статус модерации ---
|
||
# (в тестовом режиме статусы тоже показываем — поведение как в обычном режиме)
|
||
if user_id not in pending_photos and not is_attachment:
|
||
status = await check_moderation_status(user_id)
|
||
if status.get("status") == "ok" and status.get("moderation_status") == "pending":
|
||
await msg.answer(
|
||
photo_welcome_with_status(status, show_test=config.PHOTO_TEST_MODE),
|
||
parse_mode="html"
|
||
)
|
||
return
|
||
elif status.get("status") != "ok":
|
||
# Ошибка проверки — не блокируем, просто логируем
|
||
logger.warning(f"Не удалось проверить статус модерации для {user_id}: {status.get('reason')}")
|
||
|
||
# --- ВХОД В ТЕСТ-ПОД-РЕЖИМ: кнопка 1 в меню фото-режима (тест включен) ---
|
||
if (not is_attachment and user_id not in pending_photos
|
||
and not in_test_submode(user_id)
|
||
and config.PHOTO_TEST_MODE and cmd == "1"):
|
||
test_submode_users.add(user_id)
|
||
log_menu_stats(user_id, "Photo Bot (TEST)", "Вход в тест-под-режим")
|
||
await msg.answer(PHOTO_TEST_WELCOME_TEXT, parse_mode="html")
|
||
return
|
||
|
||
# --- ТЕСТ-ПОД-РЕЖИМ: кнопка 1 отправляет фото НАПРЯМУЮ в ИИ, фото никуда не публикуется ---
|
||
if not is_attachment and user_id in pending_photos and in_test_submode(user_id):
|
||
if cmd == "1":
|
||
photo_data = pending_photos[user_id]
|
||
original_path = photo_data['path']
|
||
|
||
await msg.answer(PHOTO_TEST_AI_CHECKING_TEXT, parse_mode="html")
|
||
|
||
try:
|
||
with open(original_path, 'rb') as f:
|
||
raw_photo = f.read()
|
||
# Dry-run: полный пайплайн проверки на САЙТЕ (CV + гримасы + ИИ),
|
||
# фото в БД/AD/Orion не попадает
|
||
check_res = await asyncio.to_thread(check_photo_api, raw_photo)
|
||
except Exception as e:
|
||
logger.error(f"🔥 [TEST MODE] Ошибка проверки фото: {e}")
|
||
check_res = {"status": "error", "reason": f"Ошибка бота: {e}"}
|
||
|
||
if check_res.get("status") != "ok":
|
||
# Фото остаётся в pending — можно повторить проверку (кнопка 1)
|
||
log_menu_stats(user_id, "Photo Bot (TEST)", f"Ошибка проверки: {check_res.get('reason')}")
|
||
await msg.answer(
|
||
photo_test_ai_error(check_res.get('reason', 'Неизвестная ошибка')),
|
||
parse_mode="html"
|
||
)
|
||
return
|
||
|
||
verdict = check_res.get('verdict', '—')
|
||
log_menu_stats(user_id, "Photo Bot (TEST)", f"Вердикт проверки: {verdict}")
|
||
|
||
# Фото остаётся в pending — можно повторить проверку другим фото (кнопка 2)
|
||
await msg.answer(
|
||
photo_test_result(
|
||
verdict,
|
||
check_res.get('ai_reason') or '',
|
||
check_res.get('cv_approved'),
|
||
check_res.get('cv_reason') or '',
|
||
check_res.get('forced_moderation'),
|
||
check_res.get('timings'),
|
||
check_res.get('ai_metrics'),
|
||
),
|
||
parse_mode="html"
|
||
)
|
||
return
|
||
|
||
elif cmd == "2":
|
||
log_menu_stats(user_id, "Photo Bot (TEST)", "Загрузка другого фото")
|
||
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]
|
||
await msg.answer(PHOTO_TEST_WELCOME_TEXT, parse_mode="html")
|
||
return
|
||
|
||
# --- Неверная команда при висящем превью → глобальное сообщение ---
|
||
else:
|
||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||
return
|
||
|
||
# --- ПОДТВЕРЖДЕНИЕ ОТПРАВКИ (Кнопка 1) ---
|
||
if not is_attachment and user_id in pending_photos:
|
||
if cmd == "1":
|
||
photo_data = pending_photos[user_id]
|
||
original_path = photo_data['path']
|
||
|
||
await msg.answer(PHOTO_AI_PROCESSING_TEXT, parse_mode="html")
|
||
|
||
try:
|
||
# Отправляем фото через 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 = result.get("error", "Неизвестная ошибка")
|
||
# Если фото уже на модерации — показываем конкретное сообщение
|
||
if "уже есть фото на модерации" in error_msg:
|
||
log_menu_stats(user_id, "Photo Bot", "Попытка повторной загрузки на модерации")
|
||
await msg.answer(photo_error_already_pending(), parse_mode="html")
|
||
else:
|
||
await msg.answer(photo_error_handling(), parse_mode="html")
|
||
except Exception as 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]
|
||
|
||
return
|
||
|
||
# --- ОТМЕНА ФОТО (Кнопка 2) ---
|
||
elif cmd == "2":
|
||
log_menu_stats(user_id, "Photo Bot", "Отмена загруженного фото")
|
||
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
|
||
|
||
# --- ИСПРАВЛЕНО: Защита от мусорного ввода при висящем превью ---
|
||
else:
|
||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||
return
|
||
|
||
# --- НЕВЕРНАЯ КОМАНДА (висящего фото нет) → глобальное сообщение ---
|
||
if not is_attachment:
|
||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||
return
|
||
|
||
# --- ФОТО НА МОДЕРАЦИИ: в обычном режиме новое фото не принимается ---
|
||
if is_attachment and not in_test_submode(user_id) and user_id in moderation_pending_users:
|
||
status = await check_moderation_status(user_id)
|
||
if status.get("status") == "ok" and status.get("moderation_status") == "pending":
|
||
log_menu_stats(user_id, "Photo Bot", "Отказ в приёме фото — есть фото на модерации")
|
||
await msg.answer(
|
||
photo_welcome_with_status(status, show_test=config.PHOTO_TEST_MODE),
|
||
parse_mode="html"
|
||
)
|
||
return
|
||
# Статус изменился (одобрено/отклонено) — снимаем блок, фото принимается штатно
|
||
moderation_pending_users.discard(user_id)
|
||
|
||
# --- ПРИЕМ НОВОГО ФОТО ---
|
||
if is_attachment:
|
||
file_id = getattr(msg.content, "file_id", None)
|
||
raw_bytes = None
|
||
|
||
await msg.answer(PHOTO_PROCESSING_FACE_TEXT, parse_mode="html")
|
||
|
||
try:
|
||
downloaded_file = await states.tc_bot.download_file_by_id(file_id)
|
||
logger.info(f"Скачан файл для {user_login}: type={type(downloaded_file)}, len={len(downloaded_file) if isinstance(downloaded_file, (bytes, str)) else 'N/A'}")
|
||
|
||
# Скачиваем в байты напрямую
|
||
if isinstance(downloaded_file, bytes):
|
||
raw_bytes = downloaded_file
|
||
elif isinstance(downloaded_file, str) and os.path.exists(downloaded_file):
|
||
with open(downloaded_file, 'rb') as f:
|
||
raw_bytes = f.read()
|
||
else:
|
||
# Пробуем как путь
|
||
raw_path = downloaded_file if isinstance(downloaded_file, str) else None
|
||
if raw_path and os.path.exists(raw_path):
|
||
with open(raw_path, 'rb') as f:
|
||
raw_bytes = f.read()
|
||
else:
|
||
raise Exception(f"Не удалось получить байты файла: {type(downloaded_file)}")
|
||
|
||
logger.info(f"Прочитано {len(raw_bytes)} байт для {user_login}")
|
||
|
||
# Ищем лицо и получаем координаты
|
||
coords = await asyncio.to_thread(get_crop_coordinates, raw_bytes)
|
||
logger.info(f"get_crop_coordinates вернул: {coords} для {user_login}")
|
||
|
||
if not coords:
|
||
await msg.answer(
|
||
"❌ Лицо не найдено. Пожалуйста, сделайте фото анфас при хорошем освещении.\n\n"
|
||
f"{EMOJI_DIGITS['0']} — В главное меню",
|
||
parse_mode="html"
|
||
)
|
||
return
|
||
|
||
# Генерируем превью
|
||
try:
|
||
from PIL import Image
|
||
import io
|
||
img = Image.open(io.BytesIO(raw_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.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(raw_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)
|
||
if in_test_submode(user_id):
|
||
await msg.answer(
|
||
PHOTO_TEST_PREVIEW_TEXT,
|
||
parse_mode="html"
|
||
)
|
||
else:
|
||
await msg.answer(
|
||
photo_preview_ready(),
|
||
parse_mode="html"
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"🔥 Полная ошибка: {e}", exc_info=True)
|
||
await handle_photo_error(msg, user_id, "Скачивание и обработка файла", str(e))
|
||
return
|