import json from datetime import datetime, timedelta from typing import Any, Dict, List, Optional from sqlalchemy import delete as sql_delete from sqlmodel import Session, select from models.platform import BotActivityEvent from schemas.platform import PlatformActivityItem from services.platform_settings_service import get_activity_event_retention_days ACTIVITY_EVENT_PRUNE_INTERVAL = timedelta(minutes=10) OPERATIONAL_ACTIVITY_EVENT_TYPES = { "bot_created", "bot_started", "bot_stopped", "bot_warning", "bot_enabled", "bot_disabled", "bot_deactivated", "command_submitted", "command_failed", "history_cleared", } _last_activity_event_prune_at: Optional[datetime] = None def _utcnow() -> datetime: return datetime.utcnow() def prune_expired_activity_events(session: Session, force: bool = False) -> int: global _last_activity_event_prune_at now = _utcnow() if not force and _last_activity_event_prune_at and now - _last_activity_event_prune_at < ACTIVITY_EVENT_PRUNE_INTERVAL: return 0 retention_days = get_activity_event_retention_days(session) cutoff = now - timedelta(days=retention_days) result = session.exec(sql_delete(BotActivityEvent).where(BotActivityEvent.created_at < cutoff)) _last_activity_event_prune_at = now return int(getattr(result, "rowcount", 0) or 0) def record_activity_event( session: Session, bot_id: str, event_type: str, request_id: Optional[str] = None, channel: str = "dashboard", detail: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, ) -> None: normalized_event_type = str(event_type or "unknown").strip().lower() or "unknown" if normalized_event_type not in OPERATIONAL_ACTIVITY_EVENT_TYPES: return prune_expired_activity_events(session, force=False) row = BotActivityEvent( bot_id=bot_id, request_id=request_id, event_type=normalized_event_type, channel=str(channel or "dashboard").strip().lower() or "dashboard", detail=(str(detail or "").strip() or None), metadata_json=json.dumps(metadata or {}, ensure_ascii=False) if metadata else None, created_at=_utcnow(), ) session.add(row) def list_activity_events( session: Session, bot_id: Optional[str] = None, limit: int = 100, ) -> List[Dict[str, Any]]: deleted = prune_expired_activity_events(session, force=False) if deleted > 0: session.commit() safe_limit = max(1, min(int(limit), 500)) stmt = select(BotActivityEvent).order_by(BotActivityEvent.created_at.desc(), BotActivityEvent.id.desc()).limit(safe_limit) if bot_id: stmt = stmt.where(BotActivityEvent.bot_id == bot_id) rows = session.exec(stmt).all() items: List[Dict[str, Any]] = [] for row in rows: try: metadata = json.loads(row.metadata_json or "{}") except Exception: metadata = {} items.append( PlatformActivityItem( id=int(row.id or 0), bot_id=row.bot_id, request_id=row.request_id, event_type=row.event_type, channel=row.channel, detail=row.detail, metadata=metadata if isinstance(metadata, dict) else {}, created_at=row.created_at.isoformat() + "Z", ).model_dump() ) return items def get_bot_activity_stats(session: Session) -> List[Dict[str, Any]]: from sqlalchemy import and_, func from models.bot import BotInstance stmt = ( select(BotInstance.id, BotInstance.name, func.count(BotActivityEvent.id).label("count")) .select_from(BotInstance) .join( BotActivityEvent, and_( BotActivityEvent.bot_id == BotInstance.id, BotActivityEvent.request_id.is_not(None), func.length(func.trim(BotActivityEvent.request_id)) > 0, ), isouter=True, ) .where(BotInstance.enabled.is_(True)) .group_by(BotInstance.id, BotInstance.name) .order_by(func.count(BotActivityEvent.id).desc(), BotInstance.name.asc(), BotInstance.id.asc()) ) results = session.exec(stmt).all() return [ {"bot_id": row[0], "name": row[1] or row[0], "count": row[2]} for row in results ]