First commit

This commit is contained in:
unknown
2026-06-09 21:18:13 -03:00
commit 5bff6b938b
66 changed files with 10922 additions and 0 deletions
View File
+220
View File
@@ -0,0 +1,220 @@
"""
alerts.py
Contiene endpoint para administrar el CRUD y la lógica de las alertas.
"""
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy.orm import Session
from database import get_db
import models
import schemas
from typing import List, Optional
from datetime import datetime
from auth import get_current_user
from audit import log_action
router = APIRouter()
@router.post("/alerts/", response_model=schemas.AlertResponse, tags=['Alerts'])
def create_alert(
alert: schemas.AlertCreate,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_message = db.query(models.Message).filter(
models.Message.id_mess_g == alert.message_id,
models.Message.group_id == alert.group_id
).first()
if not db_message:
raise HTTPException(status_code=404, detail="Message not found")
db_rule = db.query(models.Rule).filter(models.Rule.id == alert.rule_id).first()
if not db_rule:
raise HTTPException(status_code=404, detail="Rule not found")
db_alert = models.Alert(
message_id=alert.message_id,
group_id=alert.group_id,
rule_id=alert.rule_id,
status=alert.status,
notes=alert.notes,
created_at=datetime.utcnow()
)
db.add(db_alert)
db.flush()
log_action(
db=db, entity_type='alert',
entity_id=db_alert.id,
action='create', user_id=current_user,
after=db_alert, ip_address=request.client.host
)
db.commit()
db.refresh(db_alert)
return db_alert
@router.get("/alerts/", response_model=List[schemas.AlertResponse], tags=['Alerts'])
def read_alerts(
skip: int = 0,
limit: int = 100,
status: Optional[str] = None,
severity: Optional[str] = None,
date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
query = db.query(models.Alert)
if status:
query = query.filter(models.Alert.status == status)
if severity:
query = query.filter(models.Alert.rule.has(models.Rule.severity == severity))
if date_from:
query = query.filter(models.Alert.created_at >= date_from)
if date_to:
query = query.filter(models.Alert.created_at <= date_to)
return query.offset(skip).limit(limit).all()
@router.get("/alerts/{alert_id}", response_model=schemas.AlertResponse, tags=['Alerts'])
def read_alert(
alert_id: int,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_alert = db.query(models.Alert).filter(models.Alert.id == alert_id).first()
if not db_alert:
raise HTTPException(status_code=404, detail="Alert not found")
return db_alert
@router.put("/alerts/{alert_id}", response_model=schemas.AlertResponse, tags=['Alerts'])
def update_alert(
alert_id: int,
alert: schemas.AlertCreate,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_alert = db.query(models.Alert).filter(models.Alert.id == alert_id).first()
if not db_alert:
raise HTTPException(status_code=404, detail="Alert not found")
before_snapshot = {
'id': db_alert.id,
'message_id': db_alert.message_id,
'group_id': db_alert.group_id,
'rule_id': db_alert.rule_id,
'status': db_alert.status,
'notes': db_alert.notes,
}
for field, value in alert.model_dump().items():
setattr(db_alert, field, value)
log_action(
db=db, entity_type='alert', entity_id=alert_id,
action='update', user_id=current_user,
before=before_snapshot, after=db_alert,
ip_address=request.client.host
)
db.commit()
db.refresh(db_alert)
return db_alert
@router.delete("/alerts/{alert_id}", tags=['Alerts'])
def delete_alert(
alert_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_alert = db.query(models.Alert).filter(models.Alert.id == alert_id).first()
if not db_alert:
raise HTTPException(status_code=404, detail="Alert not found")
log_action(
db=db, entity_type='alert', entity_id=alert_id,
action='delete', user_id=current_user,
before=db_alert, ip_address=request.client.host
)
db.delete(db_alert)
db.commit()
return {"message": "Alert deleted successfully"}
@router.post("/alerts/{alert_id}/resolve", tags=['Alerts'])
def resolve_alert(
alert_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_alert = db.query(models.Alert).filter(models.Alert.id == alert_id).first()
if not db_alert:
raise HTTPException(status_code=404, detail="Alert not found")
log_action(
db=db, entity_type='alert', entity_id=alert_id,
action='status_change', user_id=current_user,
before={'status': db_alert.status},
after={'status': 'close'},
ip_address=request.client.host
)
db_alert.status = "close"
db.commit()
db.refresh(db_alert)
return db_alert
@router.post("/alerts/{alert_id}/reopen", tags=['Alerts'])
def open_alert(
alert_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_alert = db.query(models.Alert).filter(models.Alert.id == alert_id).first()
if not db_alert:
raise HTTPException(status_code=404, detail="Alert not found")
log_action(
db=db, entity_type='alert', entity_id=alert_id,
action='status_change', user_id=current_user,
before={'status': db_alert.status},
after={'status': 'open'},
ip_address=request.client.host
)
db_alert.status = "open"
db.commit()
db.refresh(db_alert)
return db_alert
@router.post("/alerts/{alert_id}/in-progress", tags=['Alerts'])
def set_alert_in_progress(
alert_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_alert = db.query(models.Alert).filter(models.Alert.id == alert_id).first()
if not db_alert:
raise HTTPException(status_code=404, detail="Alert not found")
# Solo cambiar si está abierta, no sobreescribir estados más avanzados
if db_alert.status != "open":
return db_alert
log_action(
db=db, entity_type='alert', entity_id=alert_id,
action='status_change', user_id=current_user,
before={'status': db_alert.status},
after={'status': 'in_progress'},
ip_address=request.client.host
)
db_alert.status = "in_progress"
db.commit()
db.refresh(db_alert)
return db_alert
+157
View File
@@ -0,0 +1,157 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from database import get_db
import models
import schemas
from typing import List
from datetime import datetime
from auth import get_current_user
from fastapi.responses import StreamingResponse
from io import BytesIO
from integrations.chats import TelegramChatSingleton
router = APIRouter()
# ATTACHMENT CRUD
@router.post("/attachments/", response_model=schemas.AttachmentResponse, tags=['Attachments'])
def create_attachment(attachment: schemas.AttachmentCreate, db: Session = Depends(get_db)):
# Verificar que el mensaje existe (FK compuesta)
db_message = db.query(models.Message).filter(
models.Message.id_mess_g == attachment.message_id,
models.Message.group_id == attachment.group_id
).first()
if not db_message:
raise HTTPException(status_code=404, detail="Message not found")
db_attachment = models.Attachment(**attachment.model_dump())
db.add(db_attachment)
db.commit()
db.refresh(db_attachment)
return db_attachment
@router.get("/attachments/", response_model=List[schemas.AttachmentResponse], tags=['Attachments'])
def read_attachments(skip: int = 0, limit: int = 100, db: Session = Depends(get_db), current_user: str = Depends(get_current_user)):
attachments = db.query(models.Attachment).offset(skip).limit(limit).all()
return attachments
@router.get("/attachments/{attachment_id}", response_model=schemas.AttachmentResponse, tags=['Attachments'])
def read_attachment(attachment_id: int, db: Session = Depends(get_db), current_user: str = Depends(get_current_user)):
db_attachment = db.query(models.Attachment).filter(models.Attachment.id == attachment_id).first()
if not db_attachment:
raise HTTPException(status_code=404, detail="Attachment not found")
return db_attachment
@router.put("/attachments/{attachment_id}", response_model=schemas.AttachmentResponse, tags=['Attachments'])
def update_attachment(attachment_id: int, attachment: schemas.AttachmentCreate, db: Session = Depends(get_db), current_user: str = Depends(get_current_user)):
db_attachment = db.query(models.Attachment).filter(models.Attachment.id == attachment_id).first()
if not db_attachment:
raise HTTPException(status_code=404, detail="Attachment not found")
for field, value in attachment.model_dump().items():
setattr(db_attachment, field, value)
db.commit()
db.refresh(db_attachment)
return db_attachment
@router.delete("/attachments/{attachment_id}", tags=['Attachments'])
def delete_attachment(attachment_id: int, db: Session = Depends(get_db), current_user: str = Depends(get_current_user)):
db_attachment = db.query(models.Attachment).filter(models.Attachment.id == attachment_id).first()
if not db_attachment:
raise HTTPException(status_code=404, detail="Attachment not found")
db.delete(db_attachment)
db.commit()
return {"message": "Attachment deleted successfully"}
def _download_sync(group_id: int, message_id: int) -> bytes:
"""
Descarga el media de un mensaje de Telegram de forma síncrona.
Pensado para correrse dentro de asyncio.to_thread().
Usa el TelegramChatSingleton existente para no abrir una segunda sesión.
"""
scraper = TelegramChatSingleton.get_scraper()
client = scraper.client # TelegramClient ya conectado y autorizado
# get_messages es una corrutina de Telethon — la corremos en el event loop
# del cliente, que es síncrono en el contexto del scraper
message = client.loop.run_until_complete(
client.get_messages(group_id, ids=message_id)
)
if message is None:
raise ValueError(f"Mensaje {message_id} no encontrado en grupo {group_id}")
if not message.media:
raise ValueError(f"El mensaje {message_id} no tiene media adjunta")
buffer = BytesIO()
client.loop.run_until_complete(
client.download_media(message, file=buffer)
)
buffer.seek(0)
return buffer.read()
@router.get("/attachments/{attachment_id}/download", tags=["Attachments"])
def download_attachment(
attachment_id: int,
db: Session = Depends(get_db),
current_user: str = Depends(get_current_user),
):
db_attachment = db.query(models.Attachment).filter(
models.Attachment.id == attachment_id
).first()
if not db_attachment:
raise HTTPException(status_code=404, detail="Attachment not found")
print(f"[DL] attachment encontrado: id={db_attachment.id}, type={db_attachment.type}, "
f"group_id={db_attachment.group_id}, message_id={db_attachment.message_id}")
try:
scraper = TelegramChatSingleton.get_tmp_scraper()
print(f"[DL] scraper obtenido: {scraper}")
except Exception as e:
msg = f"No se pudo obtener el scraper: {type(e).__name__}: {e}"
print(f"[DL] ERROR — {msg}")
raise HTTPException(status_code=503, detail=msg)
try:
buffer = scraper.download_attachment_to_buffer(
db_attachment.group_id,
db_attachment.message_id
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
msg = f"{type(e).__name__}: {e}"
print(f"[DL] ERROR en download_attachment_to_buffer — {msg}")
raise HTTPException(status_code=503, detail=msg)
content_type_map = {
"photo": ("image/jpeg", "jpg"),
"video": ("video/mp4", "mp4"),
"audio": ("audio/mpeg", "mp3"),
"voice": ("audio/ogg", "ogg"),
"sticker": ("image/webp", "webp"),
"document": ("application/octet-stream", "bin"),
"gif": ("image/gif", "gif"),
"zip": ("application/zip", ".zip"),
"pdf": ("application/pdf", ".pdf"),
"msword": ("application/msword", ".doc"),
"vnd.openxmlformats-officedocument.wordprocessingml.document": ("application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx"),
"vnd.ms-excel": ("application/vnd.ms-excel", ".xls"),
"vnd.openxmlformats-officedocument.spreadsheetml.sheet": ("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx"),
"plain": ("text/plain", ".txt"),
"csv": ("text/csv", ".csv"),
}
adj_type = db_attachment.type or "document"
content_type, ext = content_type_map.get(adj_type, ("application/octet-stream", "bin"))
filename = f"attachment_{attachment_id}.{ext}"
return StreamingResponse(
buffer,
media_type=content_type,
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
+64
View File
@@ -0,0 +1,64 @@
"""
audit.py
Contiene la lógica de los endpoints utilizados para crear y consultar datos de auditoria.
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import desc
from database import get_db
from typing import List, Optional
from datetime import datetime
import models
import schemas
from auth import get_current_user
router = APIRouter()
@router.get("/audit/", response_model=List[schemas.AuditLogResponse], tags=["Audit"])
def get_audit_logs(
entity_type: Optional[str] = None,
entity_id: Optional[str] = None,
action: Optional[str] = None,
user_id: Optional[int] = None,
date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None,
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: str = Depends(get_current_user),
):
"""
Consulta el log de auditoría con filtros opcionales.
Resultados ordenados del más reciente al más antiguo.
"""
query = db.query(models.AuditLog).order_by(desc(models.AuditLog.timestamp))
if entity_type:
query = query.filter(models.AuditLog.entity_type == entity_type)
if entity_id:
query = query.filter(models.AuditLog.entity_id == entity_id)
if action:
query = query.filter(models.AuditLog.action == action)
if user_id:
query = query.filter(models.AuditLog.user_id == user_id)
if date_from:
query = query.filter(models.AuditLog.timestamp >= date_from)
if date_to:
query = query.filter(models.AuditLog.timestamp <= date_to)
return query.offset(skip).limit(limit).all()
@router.get("/audit/{log_id}", response_model=schemas.AuditLogResponse, tags=["Audit"])
def get_audit_log(
log_id: int,
db: Session = Depends(get_db),
current_user: str = Depends(get_current_user),
):
log = db.query(models.AuditLog).filter(models.AuditLog.id == log_id).first()
if not log:
raise HTTPException(status_code=404, detail="Audit log not found")
return log
+166
View File
@@ -0,0 +1,166 @@
"""
groups.py
Contiene los endpoints de CRUD de Grupos.
"""
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy.orm import Session
from database import get_db
import models
import schemas
from typing import List
from auth import get_current_user
from audit import log_action
router = APIRouter()
@router.post("/groups/", response_model=schemas.GroupResponse, tags=['Groups'])
def create_group(
group: schemas.GroupCreate,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
existing = db.query(models.Group).filter(
models.Group.id_telegram == group.id_telegram
).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Group with Telegram ID {group.id_telegram} already exists"
)
db_group = models.Group(**group.model_dump())
db.add(db_group)
db.flush()
log_action(
db=db, entity_type='group', entity_id=db_group.id_telegram,
action='create', user_id=current_user,
after=db_group, ip_address=request.client.host
)
db.commit()
db.refresh(db_group)
return db_group
@router.get("/groups/", response_model=List[schemas.GroupResponse], tags=['Groups'])
def read_groups(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
return db.query(models.Group).filter(
models.Group.type != 'P'
).offset(skip).limit(limit).all()
@router.get("/groups/{group_id}", response_model=schemas.GroupResponse, tags=['Groups'])
def read_group(
group_id: int,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_group = db.query(models.Group).filter(
models.Group.id_telegram == group_id
).first()
if not db_group:
raise HTTPException(status_code=404, detail="Group not found")
return db_group
@router.get("/groups/telegram/{id_telegram}", response_model=schemas.GroupResponse, tags=['Groups'])
def read_group_telegram(id_telegram: int, db: Session = Depends(get_db)):
db_group = db.query(models.Group).filter(
models.Group.id_telegram == id_telegram
).first()
if not db_group:
raise HTTPException(status_code=404, detail="Group not found")
return db_group
@router.patch("/groups/{group_id}/update-position", response_model=schemas.GroupResponse, tags=['Groups'])
def update_group_message_position(
group_id: int,
update_data: schemas.GroupUpdatePosition,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_group = db.query(models.Group).filter(
models.Group.id_telegram == group_id
).first()
if not db_group:
raise HTTPException(status_code=404, detail=f"Group with ID {group_id} not found")
before_pos = db_group.message_position
db_group.message_position = update_data.message_position
log_action(
db=db, entity_type='group', entity_id=group_id,
action='update', user_id=current_user,
before={'message_position': before_pos},
after={'message_position': update_data.message_position},
ip_address=request.client.host
)
db.commit()
db.refresh(db_group)
return db_group
@router.patch("/groups/{group_id}", response_model=schemas.GroupResponse, tags=['Groups'])
def update_group(
group_id: int,
update_data: schemas.GroupUpdate,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_group = db.query(models.Group).filter(
models.Group.id_telegram == group_id
).first()
if not db_group:
raise HTTPException(status_code=404, detail=f"Group with ID {group_id} not found")
before_snapshot = {
'name': db_group.name,
'description': db_group.description,
'type': db_group.type,
}
db_group.name = update_data.name
db_group.description = update_data.description
db_group.type = update_data.type
log_action(
db=db, entity_type='group', entity_id=group_id,
action='update', user_id=current_user,
before=before_snapshot, after=update_data.model_dump(),
ip_address=request.client.host
)
db.commit()
db.refresh(db_group)
return db_group
@router.delete("/groups/{group_id}", tags=['Groups'])
def delete_group(
group_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_group = db.query(models.Group).filter(
models.Group.id_telegram == group_id
).first()
if not db_group:
raise HTTPException(status_code=404, detail="Group not found")
log_action(
db=db, entity_type='group', entity_id=group_id,
action='delete', user_id=current_user,
before=db_group, ip_address=request.client.host
)
db.delete(db_group)
db.commit()
return {"message": "Group deleted successfully"}
+264
View File
@@ -0,0 +1,264 @@
"""
Mannage.py
Contiene los endpoints utilziados para adminitración o manejo de grupos o sesiones de telegram.
"""
from time import time
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from integrations.chats import TelegramChatSingleton
from dotenv import load_dotenv
import os
from typing import Dict, Optional
import requests
import asyncio
from integrations.api_implementations import post_api_sync, create_telegram_group
from telethon import TelegramClient
from telethon.sessions import StringSession
from telethon.errors import SessionPasswordNeededError
import httpx
import uuid
from auth import get_current_user
#Almacenes temporales
login_flows = {} # flow_id -> {"client": TelegramClient, "phone": str}
user_sessions = {} # token -> session_string
class VerifyCodeRequest(BaseModel):
flow_id: str
code: str
password: Optional[str] = None
class VerifyCodeResponse(BaseModel):
token: str
message: str
router = APIRouter()
load_dotenv()
API_BASE_URL = os.getenv('API_URL')
HEADERS = {"Content-Type": "application/json"}
TIMEOUT = 5.0
cache_status = {"connected": None, "last_check": 0}
TTL = 10 # segundos
router = APIRouter()
load_dotenv()
@router.post("/manage/", tags=['Manage'])
def add_pending_chat(channel: int, current_user: str = Depends(get_current_user)):
"""
Agrega un canal/grupo viendo si existe en telegram anteriormente.
Primero valida que el ID sea accesible en Telegram,
luego lo guarda con los datos reales (nombre, tipo).
"""
# 1. Verificar que el cliente de Telegram está disponible
try:
scraper = TelegramChatSingleton.get_scraper()
except Exception as e:
raise HTTPException(
status_code=503,
detail=f"El cliente de Telegram no está disponible: {str(e)}"
)
# 2. Validar el canal contra Telegram
try:
validation = scraper.validate_chat(channel)
except RuntimeError as e:
raise HTTPException(status_code=503, detail=str(e))
except Exception as e:
raise HTTPException(
status_code=503,
detail=f"Error al validar en Telegram: {str(e)}"
)
if not validation["valid"]:
raise HTTPException(
status_code=400,
detail=f"Canal no válido o inaccesible: {validation.get('error', 'desconocido')}"
)
info = validation["info"]
# 3. Guardar con datos reales de Telegram — sin pasar por estado pendiente
from integrations.api_implementations import refresh_telegram_group
tipo_interno = info["type"]
nombre = info["username"] if info["username"] != str(info["id_telegram"]) else info["title"] or str(info["id_telegram"])
# Intentar crear primero (caso nuevo)
new_group = {
"id_telegram": info["id_telegram"],
"name": nombre,
"description": info["description"] or info["title"] or "",
"type": tipo_interno,
"message_position": 0,
}
created_group = create_telegram_group(new_group)
if created_group:
return {
"status": "success",
"group": created_group,
"message": "Grupo validado y agregado correctamente",
"details": {
"title": info["title"],
"type": info["type"],
"id_telegram": info["id_telegram"],
}
}
# Si ya existía, actualizar sus datos con los de Telegram
update_data = {
"name": nombre,
"description": info["description"] or info["title"] or "",
"type": tipo_interno,
}
updated_group = refresh_telegram_group(info["id_telegram"], update_data)
if updated_group:
return {
"status": "updated",
"group": updated_group,
"message": "El grupo ya existía y fue actualizado con los datos de Telegram",
"details": {
"title": info["title"],
"type": info["type"],
"id_telegram": info["id_telegram"],
}
}
return {
"status": "exists",
"message": f"El grupo con ID {info['id_telegram']} ya existe en el sistema"
}
@router.get("/manage/connection-status", tags=['Manage'])
async def telegram_status():
ahora = time()
if ahora - cache_status["last_check"] < TTL and cache_status["connected"] is not None:
return {"connected": cache_status["connected"], "cached": True}
# Realizar la verificación real
try:
response = requests.get("https://api.telegram.org", timeout=2)
cache_status["connected"] = response.status_code == 200 or 302
except:
cache_status["connected"] = False
cache_status["last_check"] = ahora
return {"connected": cache_status["connected"], "cached": False}
@router.get("/manage/init-session", tags=['Manage'])
async def init_session(current_user: str = Depends(get_current_user)):
flow_id = str(uuid.uuid4())
session_dir = os.path.join(os.getcwd(), "telegram_sessions")
os.makedirs(session_dir, exist_ok=True)
# --- Limpiar sesiones anteriores de forma segura ---
active_session_files = {
f"{data['client'].session.filename}"
for data in login_flows.values()
if "client" in data
}
for filename in os.listdir(session_dir):
if not filename.endswith(".session"):
continue
filepath = os.path.join(session_dir, filename)
# Verificar que es un archivo real y no está en uso
if not os.path.isfile(filepath):
continue
if filepath in active_session_files:
print(f"[INFO] Sesión en uso, se omite: {filename}")
continue
try:
os.remove(filepath)
print(f"[INFO] Sesión eliminada: {filename}")
except Exception as e:
print(f"[WARN] No se pudo eliminar {filename}: {e}")
# Usar el flow_id como nombre de sesión para evitar conflictos entre flujos
session_file = os.path.join(session_dir, f"session_{flow_id}")
client = TelegramClient(
session_file,
int(os.getenv('TELEGRAM_API_ID')), # debe ser int
os.getenv('TELEGRAM_API_HASH')
)
# ✅ await directamente, nunca asyncio.run() dentro de async def
await client.connect()
if not await client.is_user_authorized():
try:
await client.send_code_request(phone=os.getenv('TELEGRAM_TELEPHONE'))
except Exception as e:
await client.disconnect()
raise HTTPException(status_code=400, detail=f"Error enviando código: {str(e)}")
login_flows[flow_id] = {
"client": client,
"phone": os.getenv('TELEGRAM_TELEPHONE')
}
return {
"flow_id": flow_id,
"message": "Código enviado. Usa /verify-code para completar la autenticación."
}
@router.post("/manage/verify-code", tags=['Manage'])
async def verify_code(request: VerifyCodeRequest, current_user: str = Depends(get_current_user)):
flow = login_flows.get(request.flow_id)
if not flow:
raise HTTPException(status_code=404, detail="Flow no encontrado o expirado")
client: TelegramClient = flow["client"]
phone = flow["phone"]
# Reconectar si el cliente se desconectó entre requests
if not client.is_connected():
await client.connect()
try:
await client.sign_in(phone=phone, code=request.code)
except SessionPasswordNeededError:
if not request.password:
raise HTTPException(
status_code=428,
detail="Se requiere contraseña de dos factores"
)
try:
await client.sign_in(password=request.password)
except Exception as e:
await client.disconnect()
del login_flows[request.flow_id]
raise HTTPException(status_code=400, detail=f"Error en contraseña 2FA: {str(e)}")
except Exception as e:
await client.disconnect()
del login_flows[request.flow_id]
raise HTTPException(status_code=400, detail=f"Error verificando código: {str(e)}")
# Éxito: guardar sesión y limpiar
session_string = client.session.save()
token = str(uuid.uuid4())
user_sessions[token] = session_string
await asyncio.sleep(1)
await client.disconnect()
del login_flows[request.flow_id]
# Limpiar la instancia singleton para que tome la nueva sesión
TelegramChatSingleton.cleanup()
#Inicializa scraper.
TelegramChatSingleton.get_scraper()
return {"token": token, "message": "Autenticación exitosa"}
+191
View File
@@ -0,0 +1,191 @@
"""
Messages.py
Contiene los endpoints de CRUD de mensajes.
"""
import re
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy.orm import Session
from database import get_db
import models
import schemas
from typing import List, Optional
from datetime import datetime
from auth import get_current_user
from audit import log_action
router = APIRouter()
@router.post("/messages/", response_model=schemas.MessageResponse, tags=['Messages'])
def create_message(
message: schemas.MessageCreate,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
existing = db.query(models.Message).filter(
models.Message.id_mess_g == message.id_mess_g,
models.Message.group_id == message.group_id
).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Message in Group ID {message.group_id} with Message ID {message.id_mess_g} already exists"
)
db_message = models.Message(**message.model_dump())
db.add(db_message)
db.flush()
log_action(
db=db, entity_type='message',
entity_id=f"{db_message.id_mess_g}_{db_message.group_id}",
action='create', user_id=current_user,
after={'id_mess_g': db_message.id_mess_g, 'group_id': db_message.group_id,
'sender_id': db_message.sender_id, 'date': str(db_message.date)},
ip_address=request.client.host
)
rules = db.query(models.Rule).filter(models.Rule.is_active == True).all()
alerts = []
for rule in rules:
try:
if re.search(rule.regex, db_message.content, re.IGNORECASE):
new_alert = models.Alert(
message_id=db_message.id_mess_g,
group_id=db_message.group_id,
rule_id=rule.id,
status="open",
notes=f"Detected pattern: {rule.regex}",
created_at=datetime.utcnow()
)
db.add(new_alert)
alerts.append(new_alert)
except re.error:
print(f"[WARN] Regex inválida en regla {rule.id}: {rule.regex}")
continue
db.commit()
db.refresh(db_message)
return db_message
@router.get("/messages/", response_model=List[schemas.MessageResponse], tags=['Messages'])
def read_messages(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
return db.query(models.Message).offset(skip).limit(limit).all()
@router.get("/messages/search/", response_model=List[schemas.MessageResponse], tags=['Messages'])
def search_messages(
q: str = "",
group_id: int = None,
date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None,
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
query = db.query(models.Message)
if q:
query = query.filter(models.Message.content.ilike(f"%{q}%"))
if group_id:
query = query.filter(models.Message.group_id == group_id)
if date_from:
query = query.filter(models.Message.date >= date_from)
if date_to:
query = query.filter(models.Message.date <= date_to)
return query.offset(skip).limit(limit).all()
@router.get("/groups/{group_id}/messages/", response_model=List[schemas.MessageResponse], tags=['Messages'])
def read_messages_by_group(
group_id: int,
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_group = db.query(models.Group).filter(
models.Group.id_telegram == group_id
).first()
if not db_group:
raise HTTPException(status_code=404, detail="Group not found")
return db.query(models.Message).filter(
models.Message.group_id == group_id
).offset(skip).limit(limit).all()
@router.get("/senders/{sender_id}/messages/", response_model=List[schemas.MessageResponse], tags=['Messages'])
def read_messages_by_sender(
sender_id: int,
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_sender = db.query(models.Sender).filter(
models.Sender.id_telegram == sender_id
).first()
if not db_sender:
raise HTTPException(status_code=404, detail="Sender not found")
return db.query(models.Message).filter(
models.Message.sender_id == sender_id
).offset(skip).limit(limit).all()
@router.get("/groups/{group_id}/messages/{message_id}", response_model=schemas.MessageResponse, tags=['Messages'])
def read_message(
group_id: int,
message_id: int,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_message = db.query(models.Message).filter(
models.Message.id_mess_g == message_id,
models.Message.group_id == group_id
).first()
if not db_message:
raise HTTPException(
status_code=404,
detail=f"Message with ID {message_id} in group {group_id} not found"
)
return db_message
@router.delete("/groups/{group_id}/messages/{message_id}", tags=['Messages'])
def delete_message(
group_id: int,
message_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_message = db.query(models.Message).filter(
models.Message.id_mess_g == message_id,
models.Message.group_id == group_id
).first()
if not db_message:
raise HTTPException(
status_code=404,
detail=f"Message with ID {message_id} in group {group_id} not found"
)
log_action(
db=db, entity_type='message',
entity_id=f"{message_id}_{group_id}",
action='delete', user_id=current_user,
before={'id_mess_g': message_id, 'group_id': group_id},
ip_address=request.client.host
)
db.delete(db_message)
db.commit()
return {"message": "Message deleted successfully"}
+90
View File
@@ -0,0 +1,90 @@
"""
notes.py
Contiene los endpoints de CRUD de notas.
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from database import get_db
import models
import schemas
from typing import List
from datetime import datetime
router = APIRouter()
@router.post("/notes/", response_model=schemas.NoteResponse, tags=['Notes'])
def create_note(note: schemas.NoteCreate, db: Session = Depends(get_db)):
# verificar que la alerta existe
db_alert = db.query(models.Alert).filter(models.Alert.id == note.alert_id).first()
if not db_alert:
raise HTTPException(status_code=404, detail="Alert not found")
db_note = models.Note(
alert_id=note.alert_id,
user_id=note.user_id,
content=note.content,
creation_date=datetime.utcnow()
)
db.add(db_note)
db.commit()
db.refresh(db_note)
return db_note
@router.get("/notes/", response_model=List[schemas.NoteResponse], tags=['Notes'])
def read_notes(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
notes = db.query(models.Note).offset(skip).limit(limit).all()
return notes
@router.get("/notes/{note_id}", response_model=schemas.NoteResponse, tags=['Notes'])
def read_note(note_id: int, db: Session = Depends(get_db)):
db_note = db.query(models.Note).filter(models.Note.id == note_id).first()
if not db_note:
raise HTTPException(status_code=404, detail="Note not found")
return db_note
@router.put("/notes/{note_id}", response_model=schemas.NoteResponse, tags=['Notes'])
def update_note(note_id: int, note: schemas.NoteCreate, db: Session = Depends(get_db)):
db_note = db.query(models.Note).filter(models.Note.id == note_id).first()
if not db_note:
raise HTTPException(status_code=404, detail="Note not found")
# validar cambios de alerta
if note.alert_id != db_note.alert_id:
db_alert = db.query(models.Alert).filter(models.Alert.id == note.alert_id).first()
if not db_alert:
raise HTTPException(status_code=404, detail="New alert not found")
# validar cambio de usuario
if note.user_id != db_note.user_id:
existing = db.query(models.Note).filter(models.Note.user_id == note.user_id).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Note for user {note.user_id} already exists"
)
for field, value in note.model_dump().items():
setattr(db_note, field, value)
db.commit()
db.refresh(db_note)
return db_note
@router.delete("/notes/{note_id}", tags=['Notes'])
def delete_note(note_id: int, db: Session = Depends(get_db)):
db_note = db.query(models.Note).filter(models.Note.id == note_id).first()
if not db_note:
raise HTTPException(status_code=404, detail="Note not found")
db.delete(db_note)
db.commit()
return {"message": "Note deleted successfully"}
# Special endpoints
@router.get("/alerts/{alert_id}/notes", response_model=List[schemas.NoteResponse], tags=['Notes'])
def get_alert_notes(alert_id: int, db: Session = Depends(get_db)):
db_alert = db.query(models.Alert).filter(models.Alert.id == alert_id).first()
if not db_alert:
raise HTTPException(status_code=404, detail="Alert not found")
return db.query(models.Note).filter(models.Note.alert_id == alert_id).all()
@router.get("/users/{user_id}/notes", response_model=List[schemas.NoteResponse], tags=['Notes'])
def get_user_notes(user_id: int, db: Session = Depends(get_db)):
notes = db.query(models.Note).filter(models.Note.user_id == user_id).all()
return notes
+162
View File
@@ -0,0 +1,162 @@
"""
Rules.py
Contiene los endpoints de CRUD de reglas.
"""
import re
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy.orm import Session
from database import get_db
import models
import schemas
from typing import List
from datetime import datetime
from auth import get_current_user
from audit import log_action
router = APIRouter()
@router.post("/rules/", response_model=schemas.RuleResponse, tags=['Rules'])
def create_rule(
rule: schemas.RuleCreate,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
# Validar que la regex compile antes de guardar
try:
re.compile(rule.regex)
except re.error as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Expresión regular inválida: {e}"
)
db_rule = models.Rule(**rule.model_dump(exclude={"apply_to_history"}))
db.add(db_rule)
db.flush() # para tener db_rule.id antes del commit
# Auditoría
log_action(
db=db, entity_type='rule', entity_id=db_rule.id,
action='create', user_id=current_user,
after=db_rule, ip_address=request.client.host
)
if rule.apply_to_history:
# Traer mensajes en batches para no cargar todo en memoria
batch_size = 500
offset = 0
while True:
messages = db.query(models.Message).offset(offset).limit(batch_size).all()
if not messages:
break
for message in messages:
try:
if re.search(rule.regex, message.content, re.IGNORECASE):
new_alert = models.Alert(
message_id=message.id_mess_g,
group_id=message.group_id,
rule_id=db_rule.id,
status="open",
notes=f"Detected pattern: {rule.regex}",
created_at=datetime.utcnow()
)
db.add(new_alert)
except re.error:
break
offset += batch_size
db.commit()
db.refresh(db_rule)
return db_rule
@router.get("/rules/", response_model=List[schemas.RuleResponse], tags=['Rules'])
def read_rules(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
return db.query(models.Rule).offset(skip).limit(limit).all()
@router.get("/rules/search/", response_model=List[schemas.RuleResponse], tags=['Rules'])
def search_rules(
q: str,
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
return db.query(models.Rule).filter(
models.Rule.description.ilike(f"%{q}%")
).offset(skip).limit(limit).all()
@router.get("/rules/{rule_id}", response_model=schemas.RuleResponse, tags=['Rules'])
def read_rule(
rule_id: int,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_rule = db.query(models.Rule).filter(models.Rule.id == rule_id).first()
if not db_rule:
raise HTTPException(status_code=404, detail="Rule not found")
return db_rule
@router.put("/rules/{rule_id}", response_model=schemas.RuleResponse, tags=['Rules'])
def update_rule(
rule_id: int,
rule: schemas.RuleCreate,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_rule = db.query(models.Rule).filter(models.Rule.id == rule_id).first()
if not db_rule:
raise HTTPException(status_code=404, detail="Rule not found")
# Capturar estado anterior antes de modificar
before_snapshot = {
'id': db_rule.id,
'description': db_rule.description,
'regex': db_rule.regex,
'severity': db_rule.severity,
'is_active': db_rule.is_active,
}
for field, value in rule.model_dump().items():
setattr(db_rule, field, value)
log_action(
db=db, entity_type='rule', entity_id=rule_id,
action='update', user_id=current_user,
before=before_snapshot, after=db_rule,
ip_address=request.client.host
)
db.commit()
db.refresh(db_rule)
return db_rule
@router.delete("/rules/{rule_id}", tags=['Rules'])
def delete_rule(
rule_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_rule = db.query(models.Rule).filter(models.Rule.id == rule_id).first()
if not db_rule:
raise HTTPException(status_code=404, detail="Rule not found")
log_action(
db=db, entity_type='rule', entity_id=rule_id,
action='delete', user_id=current_user,
before=db_rule, ip_address=request.client.host
)
db.delete(db_rule)
db.commit()
return {"message": "Rule deleted successfully"}
+127
View File
@@ -0,0 +1,127 @@
"""
Senders.py
Contiene los endpoints de CRUD de remitentes.
"""
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy.orm import Session
from database import get_db
import models
import schemas
from typing import List
from auth import get_current_user
from audit import log_action
router = APIRouter()
@router.post("/senders/", response_model=schemas.SenderResponse, tags=['Senders'])
def create_sender(
sender: schemas.SenderCreate,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
existing = db.query(models.Sender).filter(
models.Sender.id_telegram == sender.id_telegram
).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Sender with Telegram ID {sender.id_telegram} already exists"
)
db_sender = models.Sender(**sender.model_dump())
db.add(db_sender)
db.flush()
log_action(
db=db, entity_type='sender', entity_id=db_sender.id_telegram,
action='create', user_id=current_user,
after=db_sender, ip_address=request.client.host
)
db.commit()
db.refresh(db_sender)
return db_sender
@router.get("/senders/", response_model=List[schemas.SenderResponse], tags=['Senders'])
def read_senders(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
return db.query(models.Sender).offset(skip).limit(limit).all()
@router.get("/senders/{sender_id}", response_model=schemas.SenderResponse, tags=['Senders'])
def read_sender(
sender_id: int,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_sender = db.query(models.Sender).filter(
models.Sender.id_telegram == sender_id
).first()
if not db_sender:
raise HTTPException(status_code=404, detail="Sender not found")
return db_sender
@router.put("/senders/{sender_id}", response_model=schemas.SenderResponse, tags=['Senders'])
def update_sender(
sender_id: int,
sender: schemas.SenderCreate,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_sender = db.query(models.Sender).filter(
models.Sender.id_telegram == sender_id
).first()
if not db_sender:
raise HTTPException(status_code=404, detail="Sender not found")
before_snapshot = {
'id_telegram': db_sender.id_telegram,
'type': db_sender.type,
'username': db_sender.username,
'first_name': db_sender.first_name,
'last_name': db_sender.last_name,
'phone': db_sender.phone,
}
for field, value in sender.model_dump().items():
setattr(db_sender, field, value)
log_action(
db=db, entity_type='sender', entity_id=sender_id,
action='update', user_id=current_user,
before=before_snapshot, after=db_sender,
ip_address=request.client.host
)
db.commit()
db.refresh(db_sender)
return db_sender
@router.delete("/senders/{sender_id}", tags=['Senders'])
def delete_sender(
sender_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: int = Depends(get_current_user)
):
db_sender = db.query(models.Sender).filter(
models.Sender.id_telegram == sender_id
).first()
if not db_sender:
raise HTTPException(status_code=404, detail="Sender not found")
log_action(
db=db, entity_type='sender', entity_id=sender_id,
action='delete', user_id=current_user,
before=db_sender, ip_address=request.client.host
)
db.delete(db_sender)
db.commit()
return {"message": "Sender deleted successfully"}
+320
View File
@@ -0,0 +1,320 @@
"""
Stats.py
Contiene la lógica de los endpoints utilizados para crear y consultar datos estadísticos.
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func, cast, Integer, extract
from database import get_db
from typing import Optional
from datetime import datetime, timedelta
import models
from auth import get_current_user
router = APIRouter()
@router.get("/stats/", tags=["Stats"])
def get_stats(
date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None,
db: Session = Depends(get_db),
current_user: str = Depends(get_current_user),
):
"""
Devuelve todas las estadísticas agregadas en una sola llamada.
Si no se pasan fechas, usa los últimos 30 días.
"""
if date_to is None:
date_to = datetime.utcnow()
if date_from is None:
date_from = date_to - timedelta(days=30)
# ------------------------------------------------------------------
# 1. Resumen ejecutivo
# ------------------------------------------------------------------
total_alertas = db.query(models.Alert).filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
).count()
alertas_abiertas = db.query(models.Alert).filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to,
models.Alert.status == "open"
).count()
alertas_en_progreso = db.query(models.Alert).filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to,
models.Alert.status == "in_progress"
).count()
alertas_cerradas = total_alertas - alertas_abiertas - alertas_en_progreso
total_mensajes = db.query(models.Message).filter(
models.Message.date >= date_from,
models.Message.date <= date_to
).count()
grupos_activos = db.query(models.Group).filter(
models.Group.type != "P"
).count()
reglas_activas = db.query(models.Rule).filter(
models.Rule.is_active == True
).count()
# ------------------------------------------------------------------
# 2. Alertas por día (para gráfico de línea)
# ------------------------------------------------------------------
alertas_por_dia_raw = (
db.query(
func.date(models.Alert.created_at).label("dia"),
models.Alert.status,
func.count(models.Alert.id).label("total")
)
.filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
)
.group_by(
func.date(models.Alert.created_at),
models.Alert.status
)
.order_by(func.date(models.Alert.created_at))
.all()
)
alertas_por_dia = {}
for row in alertas_por_dia_raw:
dia = str(row.dia)
if dia not in alertas_por_dia:
alertas_por_dia[dia] = {"abiertas": 0, "cerradas": 0}
if row.status == "open":
alertas_por_dia[dia]["abiertas"] = row.total
else:
alertas_por_dia[dia]["cerradas"] = row.total
# ------------------------------------------------------------------
# 3. Distribución por severidad
# ------------------------------------------------------------------
sev_raw = (
db.query(
models.Rule.severity,
func.count(models.Alert.id).label("total")
)
.join(models.Alert, models.Alert.rule_id == models.Rule.id)
.filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
)
.group_by(models.Rule.severity)
.all()
)
distribucion_severidad = {row.severity: row.total for row in sev_raw}
# ------------------------------------------------------------------
# 4. Alertas por grupo
# ------------------------------------------------------------------
alertas_por_grupo_raw = (
db.query(
models.Alert.group_id,
func.count(models.Alert.id).label("total")
)
.filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
)
.group_by(models.Alert.group_id)
.order_by(func.count(models.Alert.id).desc())
.limit(10)
.all()
)
# Resolver nombres de grupos
group_ids = [r.group_id for r in alertas_por_grupo_raw]
grupos_map = {
g.id_telegram: g.name
for g in db.query(models.Group).filter(
models.Group.id_telegram.in_(group_ids)
).all()
}
alertas_por_grupo = [
{
"grupo": grupos_map.get(r.group_id, str(r.group_id)),
"total": r.total
}
for r in alertas_por_grupo_raw
]
# ------------------------------------------------------------------
# 5. Reglas más disparadas (top 10)
# ------------------------------------------------------------------
reglas_top_raw = (
db.query(
models.Rule.id,
models.Rule.description,
models.Rule.severity,
func.count(models.Alert.id).label("total")
)
.join(models.Alert, models.Alert.rule_id == models.Rule.id)
.filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
)
.group_by(models.Rule.id, models.Rule.description, models.Rule.severity)
.order_by(func.count(models.Alert.id).desc())
.limit(10)
.all()
)
reglas_top = [
{
"id": row.id,
"description": row.description,
"severity": row.severity,
"total": row.total
}
for row in reglas_top_raw
]
# ------------------------------------------------------------------
# 6. Volumen de mensajes por día
# ------------------------------------------------------------------
mensajes_por_dia_raw = (
db.query(
func.date(models.Message.date).label("dia"),
func.count(models.Message.id_mess_g).label("total")
)
.filter(
models.Message.date >= date_from,
models.Message.date <= date_to
)
.group_by(func.date(models.Message.date))
.order_by(func.date(models.Message.date))
.all()
)
mensajes_por_dia = {str(row.dia): row.total for row in mensajes_por_dia_raw}
# ------------------------------------------------------------------
# 7. Heatmap: mensajes por hora × día de semana
# ------------------------------------------------------------------
# MariaDB usa DAYOFWEEK() (1=Dom..7=Sab) y HOUR() en lugar de extract("dow"/"hour")
from sqlalchemy import text
heatmap_raw = (
db.query(
func.dayofweek(models.Message.date).label("dia_semana"),
func.hour(models.Message.date).label("hora"),
func.count(models.Message.id_mess_g).label("total")
)
.filter(
models.Message.date >= date_from,
models.Message.date <= date_to
)
.group_by(
func.dayofweek(models.Message.date),
func.hour(models.Message.date)
)
.all()
)
# MariaDB DAYOFWEEK: 1=Dom, 2=Lun, ..., 7=Sáb
DIAS = {1: "Dom", 2: "Lun", 3: "Mar", 4: "Mié", 5: "Jue", 6: "Vie", 7: "Sáb"}
heatmap = [
{
"dia": DIAS.get(int(row.dia_semana), str(row.dia_semana)),
"hora": int(row.hora),
"total": row.total
}
for row in heatmap_raw
]
# ------------------------------------------------------------------
# 8. Top senders por actividad
# ------------------------------------------------------------------
top_senders_raw = (
db.query(
models.Message.sender_id,
func.count(models.Message.id_mess_g).label("mensajes")
)
.filter(
models.Message.date >= date_from,
models.Message.date <= date_to
)
.group_by(models.Message.sender_id)
.order_by(func.count(models.Message.id_mess_g).desc())
.limit(10)
.all()
)
sender_ids = [r.sender_id for r in top_senders_raw]
senders_map = {
s.id_telegram: s
for s in db.query(models.Sender).filter(
models.Sender.id_telegram.in_(sender_ids)
).all()
}
# Contar alertas por sender
alertas_sender_raw = (
db.query(
models.Message.sender_id,
func.count(models.Alert.id).label("alertas")
)
.join(models.Alert, (
models.Alert.message_id == models.Message.id_mess_g) & (
models.Alert.group_id == models.Message.group_id)
)
.filter(
models.Message.date >= date_from,
models.Message.date <= date_to,
models.Message.sender_id.in_(sender_ids)
)
.group_by(models.Message.sender_id)
.all()
)
alertas_por_sender = {r.sender_id: r.alertas for r in alertas_sender_raw}
top_senders = []
for row in top_senders_raw:
s = senders_map.get(row.sender_id)
nombre = ""
if s:
nombre = f"{s.first_name or ''} {s.last_name or ''}".strip() or s.username or str(row.sender_id)
else:
nombre = str(row.sender_id)
top_senders.append({
"id_telegram": row.sender_id,
"nombre": nombre,
"username": s.username if s else None,
"mensajes": row.mensajes,
"alertas": alertas_por_sender.get(row.sender_id, 0),
})
# ------------------------------------------------------------------
# Respuesta final
# ------------------------------------------------------------------
return {
"periodo": {
"desde": date_from.isoformat(),
"hasta": date_to.isoformat(),
},
"resumen": {
"total_alertas": total_alertas,
"alertas_abiertas": alertas_abiertas,
"alertas_cerradas": alertas_cerradas,
"total_mensajes": total_mensajes,
"grupos_activos": grupos_activos,
"reglas_activas": reglas_activas,
},
"alertas_por_dia": alertas_por_dia,
"distribucion_severidad": distribucion_severidad,
"alertas_por_grupo": alertas_por_grupo,
"reglas_top": reglas_top,
"mensajes_por_dia": mensajes_por_dia,
"heatmap": heatmap,
"top_senders": top_senders,
}