"""Тикеты поддержки: тексты, лимиты, уведомления.""" from __future__ import annotations import html import logging from datetime import datetime from typing import Any from aiogram import Bot from config import Settings logger = logging.getLogger(__name__) SUBJECT_MAX = 100 BODY_MAX = 2000 MAX_OPEN_PER_USER = 3 STATUS_LABELS: dict[str, str] = { "open": "🟢 Открыт", "answered": "💬 Есть ответ", "closed": "🔒 Закрыт", } STATUS_LABELS_SHORT: dict[str, str] = { "open": "Открыт", "answered": "Ответ", "closed": "Закрыт", } def status_label(status: str, *, short: bool = False) -> str: key = (status or "").strip().lower() table = STATUS_LABELS_SHORT if short else STATUS_LABELS return table.get(key, status or "—") def clamp_subject(text: str) -> str: return (text or "").strip()[:SUBJECT_MAX] def clamp_body(text: str) -> str: return (text or "").strip()[:BODY_MAX] def _dt(value: Any) -> str: if value is None: return "" if isinstance(value, datetime): return value.isoformat() return str(value) def ticket_to_dict(row: dict[str, Any], *, include_user: bool = False) -> dict[str, Any]: out: dict[str, Any] = { "id": int(row["id"]), "telegram_id": int(row["telegram_id"]), "subject": str(row.get("subject") or ""), "status": str(row.get("status") or "open"), "status_label": status_label(str(row.get("status") or "open"), short=True), "created_at": _dt(row.get("created_at")), "updated_at": _dt(row.get("updated_at")), "message_count": int(row.get("message_count") or 0), } if include_user: name = row.get("full_name") or row.get("login") username = row.get("username") if name: display = str(name) elif username: display = f"@{username}" else: display = f"ID {out['telegram_id']}" out["user"] = { "telegram_id": out["telegram_id"], "display_name": display, "username": username, "login": row.get("login"), } return out def message_to_dict(row: dict[str, Any]) -> dict[str, Any]: return { "id": int(row["id"]), "ticket_id": int(row["ticket_id"]), "author_role": str(row.get("author_role") or ""), "author_id": int(row["author_id"]) if row.get("author_id") is not None else None, "body": str(row.get("body") or ""), "created_at": _dt(row.get("created_at")), } def format_ticket_card(ticket: dict[str, Any], messages: list[dict[str, Any]]) -> str: tid = int(ticket["id"]) subject = html.escape(str(ticket.get("subject") or "")) status = status_label(str(ticket.get("status") or "open")) lines = [ f"🎫 Тикет #{tid}", f"Тема: {subject}", f"Статус: {status}", "", ] for msg in messages[-12:]: role = str(msg.get("author_role") or "") if role == "user": who = "Вы" elif role == "ai": who = "ИИ-поддержка" else: who = "Поддержка" body = html.escape(str(msg.get("body") or "")) lines.append(f"{who}:\n{body}") lines.append("") return "\n".join(lines).strip() def format_admin_ticket_card(ticket: dict[str, Any], messages: list[dict[str, Any]]) -> str: tid = int(ticket["id"]) subject = html.escape(str(ticket.get("subject") or "")) status = status_label(str(ticket.get("status") or "open")) uid = int(ticket["telegram_id"]) uname = ticket.get("username") fname = ticket.get("full_name") or ticket.get("login") or "" user_line = f"ID {uid}" if uname: user_line += f" · @{html.escape(str(uname))}" if fname: user_line += f" · {html.escape(str(fname))}" lines = [ f"🎫 Тикет #{tid}", f"Тема: {subject}", f"Статус: {status}", f"Пользователь: {user_line}", "", ] for msg in messages[-15:]: role = str(msg.get("author_role") or "") if role == "user": who = "Пользователь" elif role == "ai": who = "ИИ" else: who = "Админ" body = html.escape(str(msg.get("body") or "")) lines.append(f"{who}:\n{body}") lines.append("") return "\n".join(lines).strip() async def notify_admins_new_ticket( bot: Bot, settings: Settings, *, ticket_id: int, telegram_id: int, subject: str, body: str, ai_handled: bool = False, ) -> None: prefix = "🤖 Новый тикет (ИИ ответил)" if ai_handled else "🆕 Новый тикет" text = ( f"{prefix} #{ticket_id}\n" f"От: {telegram_id}\n" f"Тема: {html.escape(subject)}\n\n" f"{html.escape(body[:500])}" ) from keyboards import admin_ticket_notify_inline kb = admin_ticket_notify_inline(ticket_id) for admin_id in settings.admin_ids: try: await bot.send_message(admin_id, text, reply_markup=kb, parse_mode="HTML") except Exception as exc: # noqa: BLE001 logger.warning("notify admin %s ticket: %s", admin_id, exc) async def notify_admins_ticket_reply( bot: Bot, settings: Settings, *, ticket_id: int, telegram_id: int, body: str, ) -> None: text = ( f"💬 Ответ в тикете #{ticket_id}\n" f"От: {telegram_id}\n\n" f"{html.escape(body[:500])}" ) from keyboards import admin_ticket_notify_inline kb = admin_ticket_notify_inline(ticket_id) for admin_id in settings.admin_ids: try: await bot.send_message(admin_id, text, reply_markup=kb, parse_mode="HTML") except Exception as exc: # noqa: BLE001 logger.warning("notify admin %s reply: %s", admin_id, exc) async def notify_user_ticket_reply( bot: Bot, *, telegram_id: int, ticket_id: int, subject: str, body: str, is_ai: bool = False, ) -> None: from keyboards import ticket_notify_inline title = "🤖 ИИ-поддержка" if is_ai else "💬 Ответ поддержки" text = ( f"{title} · тикет #{ticket_id}\n" f"Тема: {html.escape(subject)}\n\n" f"{html.escape(body[:800])}" ) try: await bot.send_message( telegram_id, text, reply_markup=ticket_notify_inline(ticket_id), parse_mode="HTML", ) except Exception as exc: # noqa: BLE001 logger.warning("notify user %s ticket reply: %s", telegram_id, exc)