First commit
This commit is contained in:
@@ -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
|
||||
@@ -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}"}
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user