""" Support Router — система поддержки пользователей. Пользователи пишут админу о багах, неточностях и предложениях. Админ отвечает. Обе стороны могут переводить сообщения через AI (MiniMax-M3). """ import logging import httpx import re from fastapi import APIRouter, Request, HTTPException from auth import require_auth from database import get_pool from config import TOGETHER_API_KEY, ADMIN_IDS logger = logging.getLogger(__name__) router = APIRouter(prefix="/support", tags=["support"]) TOGETHER_API_URL = "https://api.together.xyz/v1/chat/completions" SUPPORT_MODEL = "MiniMaxAI/MiniMax-M3" async def call_ai_translate(text: str, target_lang: str) -> str: """Переводит текст через AI (MiniMax-M3).""" if not TOGETHER_API_KEY: return "" try: async with httpx.AsyncClient(timeout=90.0) as client: response = await client.post( TOGETHER_API_URL, headers={ "Authorization": f"Bearer {TOGETHER_API_KEY}", "Content-Type": "application/json", }, json={ "model": SUPPORT_MODEL, "messages": [ {"role": "system", "content": f"Ты — переводчик. Переведи текст на язык: {target_lang}. Отвечай только переводом без пояснений. Не используй markdown."}, {"role": "user", "content": text}, ], "max_tokens": 500, "temperature": 0.3, }, ) if response.status_code != 200: logger.error(f"Translate API error: {response.status_code}") return "" data = response.json() translated = data["choices"][0]["message"]["content"].strip() translated = re.sub(r'\s*\([a-z_]+_[a-z_]+\)', '', translated) return translated except Exception as e: logger.error(f"Translate error: {str(e)}") return "" async def get_user_lang(conn, user_id: int) -> str: """Получает язык интерфейса пользователя.""" lang = await conn.fetchval(""" SELECT app_language FROM users WHERE telegram_id = $1 """, user_id) return lang if lang in ["pl", "ru", "uk", "en"] else "pl" @router.post("/send") async def send_support_message(request: Request): """ Пользователь отправляет сообщение в поддержку. Если у пользователя нет открытого тикета — создаётся новый. """ telegram_id, _ = await require_auth(request) body = await request.json() message = body.get("message", "").strip() if not message: raise HTTPException(400, "message is required") if len(message) > 1000: raise HTTPException(400, "Message too long (max 1000 characters)") pool = await get_pool() async with pool.acquire() as conn: # Ищем открытый тикет пользователя ticket = await conn.fetchrow(""" SELECT id FROM support_tickets WHERE user_id = $1 AND status = 'open' ORDER BY id DESC LIMIT 1 """, telegram_id) ticket_id = None if ticket: ticket_id = ticket["id"] # Обновляем updated_at await conn.execute(""" UPDATE support_tickets SET updated_at = NOW() WHERE id = $1 """, ticket_id) else: # Создаём новый тикет new_ticket = await conn.fetchrow(""" INSERT INTO support_tickets (user_id, status) VALUES ($1, 'open') RETURNING id """, telegram_id) ticket_id = new_ticket["id"] # Сохраняем сообщение (юзер написал — админ ещё не прочитал) await conn.execute(""" INSERT INTO support_messages (ticket_id, sender_id, message, is_read) VALUES ($1, $2, $3, FALSE) """, ticket_id, telegram_id, message) return {"status": "ok", "ticket_id": ticket_id} @router.get("/user-tickets") async def get_user_tickets(request: Request): """Пользователь получает список своих тикетов.""" telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: tickets = await conn.fetch(""" SELECT id, status, created_at, updated_at FROM support_tickets WHERE user_id = $1 ORDER BY updated_at DESC """, telegram_id) return {"tickets": [dict(t) for t in tickets]} @router.get("/user-messages/{ticket_id}") async def get_user_messages(request: Request, ticket_id: int): """Пользователь получает сообщения своего тикета и отмечает ответы админа прочитанными.""" telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: # Проверяем, что тикет принадлежит пользователю ticket = await conn.fetchrow(""" SELECT id FROM support_tickets WHERE id = $1 AND user_id = $2 """, ticket_id, telegram_id) if not ticket: raise HTTPException(404, "Ticket not found") # Отмечаем сообщения админа как прочитанные await conn.execute(""" UPDATE support_messages SET is_read = TRUE WHERE ticket_id = $1 AND sender_id != $2 """, ticket_id, telegram_id) messages = await conn.fetch(""" SELECT id, sender_id, message, is_read, created_at FROM support_messages WHERE ticket_id = $1 ORDER BY id ASC """, ticket_id) return {"messages": [dict(m) for m in messages]} @router.get("/user-unread-count") async def get_user_unread_count(request: Request): """Пользователь получает количество непрочитанных ответов админа.""" telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: count = await conn.fetchval(""" SELECT COUNT(*) FROM support_messages m JOIN support_tickets t ON m.ticket_id = t.id WHERE t.user_id = $1 AND m.is_read = FALSE AND m.sender_id != $1 """, telegram_id) return {"unread_count": count or 0} @router.post("/delete-ticket/{ticket_id}") async def delete_ticket(request: Request, ticket_id: int): """ Удаляет тикет. Может удалить: сам пользователь (свой тикет) или админ (любой). """ telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: # Проверяем права if telegram_id in ADMIN_IDS: # Админ может удалить любой result = await conn.execute("DELETE FROM support_tickets WHERE id = $1", ticket_id) else: # Пользователь может удалить только свой result = await conn.execute(""" DELETE FROM support_tickets WHERE id = $1 AND user_id = $2 """, ticket_id, telegram_id) affected = int(result.split()[-1]) if affected == 0: raise HTTPException(404, "Ticket not found or no permission") return {"status": "ok"} # ============ АДМИНСКИЕ ЭНДПОИНТЫ ============ @router.get("/admin-tickets") async def get_admin_tickets(request: Request): """Админ получает список всех тикетов.""" telegram_id, _ = await require_auth(request) if telegram_id not in ADMIN_IDS: raise HTTPException(403, "Access denied") pool = await get_pool() async with pool.acquire() as conn: tickets = await conn.fetch(""" SELECT t.id, t.user_id, t.status, t.created_at, t.updated_at, u.username, u.app_language, (SELECT COUNT(*) FROM support_messages m WHERE m.ticket_id = t.id AND m.is_read = FALSE) as unread_count, (SELECT message FROM support_messages m WHERE m.ticket_id = t.id ORDER BY m.id DESC LIMIT 1) as last_message FROM support_tickets t LEFT JOIN users u ON t.user_id = u.telegram_id ORDER BY t.updated_at DESC """) return {"tickets": [dict(t) for t in tickets]} @router.get("/admin-messages/{ticket_id}") async def get_admin_messages(request: Request, ticket_id: int): """Админ получает сообщения тикета и отмечает их как прочитанные.""" telegram_id, _ = await require_auth(request) if telegram_id not in ADMIN_IDS: raise HTTPException(403, "Access denied") pool = await get_pool() async with pool.acquire() as conn: ticket = await conn.fetchrow(""" SELECT id FROM support_tickets WHERE id = $1 """, ticket_id) if not ticket: raise HTTPException(404, "Ticket not found") # Отмечаем сообщения пользователя как прочитанные await conn.execute(""" UPDATE support_messages SET is_read = TRUE WHERE ticket_id = $1 AND sender_id != $2 """, ticket_id, telegram_id) messages = await conn.fetch(""" SELECT id, sender_id, message, is_read, created_at FROM support_messages WHERE ticket_id = $1 ORDER BY id ASC """, ticket_id) return {"messages": [dict(m) for m in messages]} @router.post("/admin-reply") async def admin_reply(request: Request): """Админ отвечает пользователю.""" telegram_id, _ = await require_auth(request) if telegram_id not in ADMIN_IDS: raise HTTPException(403, "Access denied") body = await request.json() ticket_id = body.get("ticket_id") message = body.get("message", "").strip() if not ticket_id or not message: raise HTTPException(400, "ticket_id and message required") if len(message) > 1000: raise HTTPException(400, "Message too long (max 1000 characters)") pool = await get_pool() async with pool.acquire() as conn: ticket = await conn.fetchrow(""" SELECT id FROM support_tickets WHERE id = $1 """, ticket_id) if not ticket: raise HTTPException(404, "Ticket not found") # 🔧 ИСПРАВЛЕНО: админ написал — юзер ЕЩЁ НЕ ПРОЧИТАЛ, поэтому is_read = FALSE # Раньше было TRUE, из-за чего бейдж непрочитанных у пользователя не появлялся await conn.execute(""" INSERT INTO support_messages (ticket_id, sender_id, message, is_read) VALUES ($1, $2, $3, FALSE) """, ticket_id, telegram_id, message) await conn.execute(""" UPDATE support_tickets SET updated_at = NOW() WHERE id = $1 """, ticket_id) return {"status": "ok"} @router.get("/unread-count") async def get_unread_count(request: Request): """Админ получает количество непрочитанных сообщений.""" telegram_id, _ = await require_auth(request) if telegram_id not in ADMIN_IDS: raise HTTPException(403, "Access denied") pool = await get_pool() async with pool.acquire() as conn: count = await conn.fetchval(""" SELECT COUNT(*) FROM support_messages WHERE is_read = FALSE AND sender_id != $1 """, telegram_id) return {"unread_count": count or 0} @router.post("/translate") async def translate_message(request: Request): """ Перевод сообщения через AI (MiniMax-M3). Доступно и пользователю, и админу. """ telegram_id, _ = await require_auth(request) body = await request.json() text = body.get("text", "").strip() target_lang = body.get("target_lang", "ru") if not text: raise HTTPException(400, "text is required") if len(text) > 1000: raise HTTPException(400, "Text too long (max 1000 characters)") if target_lang not in ["pl", "ru", "uk", "en"]: target_lang = "ru" translated = await call_ai_translate(text, target_lang) if not translated: raise HTTPException(500, "Translation failed") return {"translation": translated}