439 lines
15 KiB
Python
439 lines
15 KiB
Python
# 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'))) |