# src/api.py import requests import streamlit as st from typing import List, Optional, Dict, Any import os class APIClient: """Cliente simple para interactuar con la API""" def __init__(self, user_url: str, feeder_url: str, backup_url: str): self.user_url = user_url self.feeder_url = feeder_url self.backup_url = backup_url def _get_headers(self) -> Optional[Dict[str, str]]: if "token" not in st.session_state: return None return { "Authorization": f"Bearer {st.session_state['token']}", "accept": "application/json", "Content-Type": "application/json" } def _request(self, method: str, url: str, **kwargs) -> Optional[Any]: """Realiza petición HTTP con manejo común de errores y headers. Args: method: 'get', 'post', 'put', 'delete' url: URL completa **kwargs: parámetros (json, params, etc.) para requests """ headers = self._get_headers() if headers is None: return None try: func = getattr(requests, method) response = func(url, headers=headers, timeout=10, **kwargs) if response.status_code in (200, 201, 204): if response.content: return response.json() return {} return None except Exception as e: st.error(f"Error en {method.upper()} {url}: {str(e)}") return None ## Verificaciones ## def has_connection(self): """Verifica la conexión con la API de Telegram (Para funcionamiento del feeder)""" try: response = requests.get(f"{self.feeder_url}/manage/connection-status", timeout=5) return response.status_code == 200 except Exception: return False ## USERS ## def login(self, email: str, password: str) -> Optional[Dict[str, Any]]: """Realiza login en la API""" try: response = requests.post( f"{self.user_url}/api/v1/login", json={"email": email, "password": password}, timeout=10 ) return response.json() if response.status_code == 200 else None except Exception: return None def get_pending_users(self, skip: int = 0, limit: int = 100) -> Optional[List[Dict[str, Any]]]: return self._request( "get", f"{self.user_url}/api/v1/users/pending", params={"skip": skip, "limit": limit} ) def activate_user(self, user_id: int) -> Optional[Dict[str, Any]]: return self._request( "post", f"{self.user_url}/api/v1/users/{user_id}/activate" ) def delete_user(self, user_id: int) -> Optional[Dict[str, Any]]: return self._request( "delete", f"{self.user_url}/api/v1/users/{user_id}" ) def update_user(self, user_id: int, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]: return self._request( "put", f"{self.user_url}/api/v1/users/{user_id}", json=payload ) def get_user(self, user_id: int) -> Optional[Dict[str, Any]]: return self._request( "get", f"{self.user_url}/api/v1/users/{user_id}" ) def search_users(self, q: str = "", skip: int = 0, limit: int = 50) -> Optional[List[Dict[str, Any]]]: return self._request( "get", f"{self.user_url}/api/v1/users", params={"q": q, "skip": skip, "limit": limit} ) def search_messages(self, q: str = "", group_id=None, date_from=None, date_to=None, skip: int = 0, limit: int = 100): if not q.strip() and not group_id and not date_from and not date_to: return self._request("get", f"{self.feeder_url}/messages/", params={"skip": skip, "limit": limit}) params = {"q": q, "skip": skip, "limit": limit} if group_id: params["group_id"] = group_id if date_from: params["date_from"] = date_from if date_to: params["date_to"] = date_to return self._request("get", f"{self.feeder_url}/messages/search/", params=params) def get_user_modifications(self, user_id: Optional[int] = None, skip: int = 0, limit: int = 100) -> Optional[List[Dict[str, Any]]]: return self._request( "get", f"{self.user_url}/api/v1/logs/users", params={"skip": skip, "limit": limit} ) ### FEEDER ### ## Alerts ## def get_alerts(self, skip: int = 0, limit: int = 100, status=None, severity=None, date_from=None, date_to=None): params = {"skip": skip, "limit": limit} if status: params["status"] = status if severity: params["severity"] = severity if date_from: params["date_from"] = date_from if date_to: params["date_to"] = date_to return self._request("get", f"{self.feeder_url}/alerts/", params=params) def get_message(self, group_id: int, message_id: int) -> Optional[Dict[str, Any]]: return self._request( "get", f"{self.feeder_url}/groups/{group_id}/messages/{message_id}" ) def add_note_to_alert(self, alert_id: int, user_id: int, content: str) -> Optional[Dict[str, Any]]: payload: Dict[str, Any] = {"alert_id": alert_id,"user_id": user_id, "content": f"{content}"} return self._request( "post", f"{self.feeder_url}/notes/", json=payload ) def get_notes_for_alert(self, alert_id: int) -> Optional[List[Dict[str, Any]]]: return self._request( "get", f"{self.feeder_url}/alerts/{alert_id}/notes" ) def mark_alert_as_resolved(self, alert_id: int) -> Optional[Dict[str, Any]]: return self._request( "post", f"{self.feeder_url}/alerts/{alert_id}/resolve" ) def mark_alert_as_pending(self, alert_id: int) -> Optional[Dict[str, Any]]: return self._request( "post", f"{self.feeder_url}/alerts/{alert_id}/reopen" ) def set_alert_in_progress(self, alert_id: int) -> Optional[Dict[str, Any]]: return self._request( "post", f"{self.feeder_url}/alerts/{alert_id}/in-progress" ) ## Groups ## def get_groups(self) -> Optional[List[Dict[str, Any]]]: return self._request( "get", f"{self.feeder_url}/groups/" ) def get_group(self, group_id: int) -> Optional[Dict[str, Any]]: return self._request( "get", f"{self.feeder_url}/groups/{group_id}" ) def add_channel(self, channel_id: int) -> Optional[Dict[str, Any]]: return self._request( "post", f"{self.feeder_url}/manage/", params={"channel": channel_id} ) ## Telegram Session ## def telegram_status(self) -> Optional[Dict[str, Any]]: return self._request( "get", f"{self.feeder_url}/telegram-status" ) def init_session_telegram(self) -> Optional[Dict[str, Any]]: return self._request( "get", f"{self.feeder_url}/manage/init-session" ) def verify_code(self, flow_id: str, code: str, password: Optional[str] = None) -> Optional[Dict[str, Any]]: payload: Dict[str, Any] = {"flow_id": flow_id, "code": code} if password: payload["password"] = password return self._request( "post", f"{self.feeder_url}/manage/verify-code", json=payload ) ## Rules ## def get_rules(self, skip: int = 0, limit: int = 100) -> Optional[List[Dict[str, Any]]]: return self._request( "get", f"{self.feeder_url}/rules/", params={"skip": skip, "limit": limit} ) def get_rule(self, rule_id: int) -> Optional[Dict[str, Any]]: return self._request( "get", f"{self.feeder_url}/rules/{rule_id}" ) def search_rules(self, q: str = "", skip: int = 0, limit: int = 100) -> Optional[List[Dict[str, Any]]]: return self._request( "get", f"{self.feeder_url}/rules/search", params={"q": q, "skip": skip, "limit": limit} ) def create_rule(self, rule_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: return self._request( "post", f"{self.feeder_url}/rules/", json=rule_data ) def update_rule(self, rule_id: int, rule_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: return self._request( "put", f"{self.feeder_url}/rules/{rule_id}", json=rule_data ) ##Attachment def get_attachment(self, attachment_id: int) -> Optional[Dict[str, Any]]: """Obtiene los metadatos de un adjunto por su ID.""" return self._request( "get", f"{self.feeder_url}/attachments/{attachment_id}" ) def download_attachment(self, attachment_id: int) -> Optional[bytes]: """ Descarga el contenido binario de un adjunto directamente desde Telegram vía la API. Devuelve los bytes del archivo, o None si falla. """ headers = self._get_headers() if headers is None: return None # El endpoint de descarga no requiere Content-Type JSON download_headers = { "Authorization": headers["Authorization"], "accept": "*/*" } try: response = requests.get( f"{self.feeder_url}/attachments/{attachment_id}/download", headers=download_headers, timeout=(10, 600) # tiempo de espera más alto para archivos grandes ) if response.status_code == 200: return response.content return None except Exception as e: st.error(f"Error al descargar adjunto {attachment_id}: {str(e)}") return None def get_attachments_by_message(self, message_id: int, group_id: int) -> Optional[list]: """ Obtiene los adjuntos asociados a un mensaje específico. Filtra desde el listado general de adjuntos por message_id y group_id. Nota: si en el futuro la API expone un endpoint dedicado, reemplazar esta implementación. """ # Por ahora los adjuntos vienen embebidos en el mensaje via MessageResponse.attachments # Este método es un helper para obtenerlos si solo tenemos los IDs message = self.get_message(group_id, message_id) if message is None: return None return message.get("attachments", []) ## Audit ## def get_audit_logs( self, entity_type: Optional[str] = None, entity_id: Optional[str] = None, action: Optional[str] = None, user_id: Optional[int] = None, date_from=None, date_to=None, skip: int = 0, limit: int = 100, ) -> Optional[List[Dict[str, Any]]]: params = {"skip": skip, "limit": limit} if entity_type: params["entity_type"] = entity_type if entity_id: params["entity_id"] = entity_id if action: params["action"] = action if user_id: params["user_id"] = user_id if date_from: params["date_from"] = date_from if date_to: params["date_to"] = date_to return self._request("get", f"{self.feeder_url}/audit/", params=params) ## Stats ## def get_stats( self, date_from: str = None, date_to: str = None, ) -> Optional[Dict[str, Any]]: """Obtiene todas las estadísticas agregadas del sistema.""" params = {} if date_from: params["date_from"] = date_from if date_to: params["date_to"] = date_to return self._request( "get", f"{self.feeder_url}/stats/", params=params ) ## Senders ## def get_senders(self, skip: int = 0, limit: int = 100) -> Optional[List[Dict[str, Any]]]: """Obtiene la lista de remitentes cargados por el feeder.""" return self._request( "get", f"{self.feeder_url}/senders/", params={"skip": skip, "limit": limit} ) def get_sender(self, sender_id: int) -> Optional[Dict[str, Any]]: """Obtiene un remitente por su ID de Telegram.""" return self._request( "get", f"{self.feeder_url}/senders/{sender_id}" ) def get_messages_by_sender( self, sender_id: int, skip: int = 0, limit: int = 100 ) -> Optional[List[Dict[str, Any]]]: """Obtiene todos los mensajes enviados por un sender específico.""" return self._request( "get", f"{self.feeder_url}/senders/{sender_id}/messages/", params={"skip": skip, "limit": limit} ) ## Backup ## def get_backup_status(self) -> Optional[Dict[str, Any]]: """ Consulta el estado de disponibilidad de cada componente de backup sin generar el archivo. """ if not self.backup_url: return None return self._request("get", f"{self.backup_url}/backup/status") def download_backup( self, include_db: bool = True, include_config: bool = True, include_ssl: bool = True, include_sessions: bool = True, ) -> Optional[bytes]: """ Llama al endpoint /backup del microservicio y devuelve los bytes del ZIP. Usa un timeout largo (10 min) porque mysqldump puede tardar. """ if not self.backup_url: return None headers = self._get_headers() if headers is None: return None dl_headers = { "Authorization": headers["Authorization"], "accept": "application/zip", } params = { "include_db": str(include_db).lower(), "include_config": str(include_config).lower(), "include_ssl": str(include_ssl).lower(), "include_sessions": str(include_sessions).lower(), } try: response = requests.get( f"{self.backup_url}/backup", headers=dl_headers, params=params, timeout=(15, 600), # (connect, read) — dump puede tardar ) if response.status_code == 200: return response.content return None except requests.exceptions.Timeout: return None except Exception: return None # Crear instancia global api_client = APIClient(str(os.getenv('USERS_URL')),str(os.getenv('FEEDER_URL')), str(os.getenv('BACKUP_URL')))