Files
drivers_bot/app/services/admin_notifications.py
2026-05-18 18:17:53 +09:00

167 lines
5.7 KiB
Python

import logging
from datetime import UTC, datetime
from html import escape
import httpx
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.models.car import AdminNotification
logger = logging.getLogger(__name__)
ADMIN_EVENT_FLAGS = {
"user_registered": "admin_notify_new_users",
"vehicle_created": "admin_notify_new_users",
"first_record_created": "admin_notify_new_users",
"sto_application_created": "admin_notify_sto_applications",
"sto_application_updated": "admin_notify_sto_applications",
"sto_approved": "admin_notify_sto_applications",
"sto_suspended": "admin_notify_sto_applications",
"security_event": "admin_notify_security_events",
"rate_limit_exceeded": "admin_notify_security_events",
"upload_blocked": "admin_notify_security_events",
"system_error": "admin_notify_system_errors",
"ocr_failed": "admin_notify_system_errors",
}
def admin_event_enabled(event_type: str) -> bool:
flag = ADMIN_EVENT_FLAGS.get(event_type)
return bool(getattr(settings, flag, True)) if flag else True
def admin_recipients() -> list[str]:
recipients: list[str] = []
if settings.admin_notification_chat_id:
recipients.append(settings.admin_notification_chat_id)
recipients.extend(str(item) for item in settings.admin_telegram_id_list)
return list(dict.fromkeys(recipients))
def admin_notification_url(entity_type: str | None = None, entity_id: str | int | None = None) -> str:
base = settings.effective_webapp_url
if entity_type == "service_center" and entity_id:
return f"{base}/admin.html?section=sto-applications&entity_id={entity_id}"
if entity_type == "user" and entity_id:
return f"{base}/admin.html?section=users&entity_id={entity_id}"
return f"{base}/admin.html"
async def create_admin_notification(
session: AsyncSession,
*,
event_type: str,
title: str,
body: str | None = None,
entity_type: str | None = None,
entity_id: int | str | None = None,
severity: str = "info",
idempotency_key: str | None = None,
metadata: dict | None = None,
send_telegram: bool = True,
) -> AdminNotification:
key = idempotency_key or f"{event_type}:{entity_type or 'system'}:{entity_id or title}"
existing = (
await session.execute(select(AdminNotification).where(AdminNotification.idempotency_key == key))
).scalar_one_or_none()
if existing:
return existing
notification = AdminNotification(
event_type=event_type,
title=title,
body=body,
entity_type=entity_type,
entity_id=str(entity_id) if entity_id is not None else None,
severity=severity,
idempotency_key=key,
metadata_json=metadata,
telegram_status="pending" if send_telegram else "skipped",
)
session.add(notification)
await session.flush()
if send_telegram and admin_event_enabled(event_type):
await send_admin_telegram_notification(notification)
elif send_telegram:
notification.telegram_status = "skipped"
return notification
async def send_admin_telegram_notification(notification: AdminNotification) -> None:
recipients = admin_recipients()
if not recipients or not settings.bot_token:
notification.telegram_status = "skipped"
return
link = admin_notification_url(notification.entity_type, notification.entity_id)
text = "\n".join(
item
for item in [
f"<b>{escape(notification.title)}</b>",
escape(notification.body or ""),
f"Событие: <code>{escape(notification.event_type)}</code>",
f"Открыть: {escape(link)}",
]
if item
)
errors: list[str] = []
async with httpx.AsyncClient(timeout=8) as client:
for chat_id in recipients:
try:
response = await client.post(
f"https://api.telegram.org/bot{settings.bot_token}/sendMessage",
json={
"chat_id": chat_id,
"text": text,
"parse_mode": "HTML",
"disable_web_page_preview": True,
},
)
response.raise_for_status()
except Exception as error: # noqa: BLE001 - notification delivery is best-effort
logger.warning("Admin Telegram notification failed: %s", error)
errors.append(str(error))
if errors:
notification.telegram_status = "failed"
notification.telegram_error = "; ".join(errors)[:2000]
else:
notification.telegram_status = "sent"
async def retry_admin_telegram_notifications(session: AsyncSession, *, limit: int = 50) -> int:
result = await session.execute(
select(AdminNotification)
.where(AdminNotification.telegram_status.in_(["pending", "failed"]))
.order_by(AdminNotification.created_at.asc())
.limit(limit)
)
delivered = 0
for notification in result.scalars():
await send_admin_telegram_notification(notification)
if notification.telegram_status == "sent":
delivered += 1
await session.commit()
return delivered
async def mark_admin_notification_read(
session: AsyncSession, notification: AdminNotification
) -> AdminNotification:
notification.status = "read"
notification.read_at = datetime.now(UTC)
await session.flush()
return notification
async def dismiss_admin_notification(
session: AsyncSession, notification: AdminNotification
) -> AdminNotification:
notification.status = "dismissed"
notification.dismissed_at = datetime.now(UTC)
await session.flush()
return notification