41
METRICS_API_EXAMPLES.md
Normal file
41
METRICS_API_EXAMPLES.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Metrics API Examples
|
||||||
|
|
||||||
|
## Registrar métrica manualmente
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8004/metrics/record" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"event_type": "user_created",
|
||||||
|
"entity_id": "123",
|
||||||
|
"entity_type": "user",
|
||||||
|
"user_id": 456,
|
||||||
|
"metadata": {"email": "user@example.com"}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Obtener reporte de últimos 7 días
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8004/metrics/report?days=7"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Obtener estadísticas de un día específico
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8004/metrics/daily-stats?date=2026-05-04T00:00:00"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Obtener resumen de eventos en rango
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8004/metrics/summary?start_date=2026-04-27T00:00:00&end_date=2026-05-04T23:59:59"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health check
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8004/metrics/health"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Eventos automáticos (RabbitMQ)
|
||||||
|
El consumer de métricas escucha automáticamente eventos de:
|
||||||
|
- Users Queue: `user_created`, `user_updated`, `user_deleted`
|
||||||
|
- Reports Queue: `report_created`, `report_resolved`
|
||||||
|
- Notifications Queue: `notification_sent`
|
||||||
|
- Moderations Queue: `moderation_completed`
|
||||||
58
METRICS_INTEGRATION_SUMMARY.md
Normal file
58
METRICS_INTEGRATION_SUMMARY.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# API de Métricas y Analítica - Resumen de Implementación
|
||||||
|
|
||||||
|
## ✅ Integración Completada
|
||||||
|
|
||||||
|
### Estructura Creada
|
||||||
|
|
||||||
|
**Dominio:**
|
||||||
|
- `src/domain/metrics.py` - Modelos: `Metric`, `DailyStats`, `AnalyticsReport`, `EventType`
|
||||||
|
|
||||||
|
**Aplicación:**
|
||||||
|
- `src/application/ports/metrics_repository.py` - Interfaz del repositorio
|
||||||
|
- `src/application/services/metrics_services.py` - Lógica de negocio
|
||||||
|
|
||||||
|
**Infraestructura:**
|
||||||
|
- `src/infrastructure/adapters/persistence/metrics_repository_postgres.py` - Implementación PostgreSQL
|
||||||
|
- `src/infrastructure/api/metrics/` - API REST FastAPI
|
||||||
|
- `app.py` - Factory de la aplicación
|
||||||
|
- `router.py` - Endpoints
|
||||||
|
- `schemas.py` - Modelos Pydantic
|
||||||
|
|
||||||
|
**Consumidor:**
|
||||||
|
- `src/consumers/metrics_consumer.py` - Listener RabbitMQ para todos los eventos
|
||||||
|
|
||||||
|
### Base de Datos
|
||||||
|
|
||||||
|
**PostgreSQL añadido al docker-compose:**
|
||||||
|
- Host: localhost:5432
|
||||||
|
- DB: voxpopuli_metrics
|
||||||
|
- Usuario: voxpopuli / voxpopuli_pass
|
||||||
|
- Tabla `metrics` creada automáticamente
|
||||||
|
|
||||||
|
### Integración sin Cambios Significativos
|
||||||
|
|
||||||
|
✅ **main.py** - Añadida API y consumer de métricas sin modificar APIs existentes
|
||||||
|
✅ **docker-compose.yaml** - PostgreSQL añadido sin cambiar servicios existentes
|
||||||
|
✅ **requirements.txt** - Añadido psycopg2-binary
|
||||||
|
✅ RabbitMQ NO añadido al docker-compose (usa instancias previas)
|
||||||
|
|
||||||
|
### Endpoints API (Puerto 8004)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /metrics/record - Registrar métrica
|
||||||
|
GET /metrics/report - Reporte de últimos N días
|
||||||
|
GET /metrics/daily-stats - Estadísticas diarias
|
||||||
|
GET /metrics/summary - Resumen de eventos por rango
|
||||||
|
GET /metrics/health - Health check
|
||||||
|
GET /docs - Swagger UI
|
||||||
|
```
|
||||||
|
|
||||||
|
### Eventos Automáticos Capturados
|
||||||
|
|
||||||
|
El consumer escucha automáticamente:
|
||||||
|
- `users_queue` → user_created, user_updated, user_deleted
|
||||||
|
- `reports_queue` → report_created, report_resolved
|
||||||
|
- `notifications_queue` → notification_sent
|
||||||
|
- `moderations_queue` → moderation_completed
|
||||||
|
|
||||||
|
Cada evento genera una métrica en PostgreSQL con timestamp, usuario_id, metadata y tipo de evento.
|
||||||
60
METRICS_SETUP.md
Normal file
60
METRICS_SETUP.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Metrics API Setup
|
||||||
|
|
||||||
|
## Configuración Base de Datos
|
||||||
|
|
||||||
|
PostgreSQL se ejecuta automáticamente en el contenedor Docker.
|
||||||
|
|
||||||
|
**Conexión:**
|
||||||
|
- Host: localhost
|
||||||
|
- Puerto: 5432
|
||||||
|
- Base de datos: voxpopuli_metrics
|
||||||
|
- Usuario: voxpopuli
|
||||||
|
- Contraseña: voxpopuli_pass
|
||||||
|
|
||||||
|
**Tabla creada automáticamente:**
|
||||||
|
```
|
||||||
|
metrics:
|
||||||
|
- id (PK)
|
||||||
|
- event_type (STRING, INDEXED)
|
||||||
|
- entity_id (STRING)
|
||||||
|
- entity_type (STRING)
|
||||||
|
- timestamp (DATETIME, INDEXED)
|
||||||
|
- metadata (JSON)
|
||||||
|
- user_id (INT, NULLABLE)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Arquitectura
|
||||||
|
|
||||||
|
```
|
||||||
|
API Metrics (Puerto 8004)
|
||||||
|
├── FastAPI Router
|
||||||
|
│ ├── POST /metrics/record - Registrar métrica
|
||||||
|
│ ├── GET /metrics/report - Reporte analítica
|
||||||
|
│ ├── GET /metrics/daily-stats - Estadísticas diarias
|
||||||
|
│ ├── GET /metrics/summary - Resumen eventos
|
||||||
|
│ └── GET /metrics/health - Health check
|
||||||
|
│
|
||||||
|
├── Metrics Service (Lógica)
|
||||||
|
│ └── Metrics Repository (Postgres)
|
||||||
|
│
|
||||||
|
└── Metrics Consumer (RabbitMQ)
|
||||||
|
├── Escucha: users_queue
|
||||||
|
├── Escucha: reports_queue
|
||||||
|
├── Escucha: notifications_queue
|
||||||
|
└── Escucha: moderations_queue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inicio
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Levantar DB (docker)
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Instalar dependencias
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Ejecutar todas las APIs
|
||||||
|
python -m src.main
|
||||||
|
```
|
||||||
|
|
||||||
|
La API de métricas se ejecutará automáticamente en puerto 8004.
|
||||||
310
MODERATION_API.md
Normal file
310
MODERATION_API.md
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
# API de Moderación - VoxPopuli
|
||||||
|
|
||||||
|
## Descripción General
|
||||||
|
|
||||||
|
La API de Moderación es un microservicio complementario integrado al proyecto VoxPopuli que proporciona funcionalidades de moderación de contenido y gestión de usuarios. Utiliza arquitectura hexagonal y se comunica mediante **RabbitMQ** para mantener bajo acoplamiento con el resto del sistema.
|
||||||
|
|
||||||
|
## Características Principales
|
||||||
|
|
||||||
|
### 1. **Eliminación de Reportes**
|
||||||
|
- Permite a moderadores eliminar reportes del sistema
|
||||||
|
- Evento: `moderation.delete_report`
|
||||||
|
- Endpoint: `POST /moderation/reports/delete`
|
||||||
|
|
||||||
|
### 2. **Cierre de Cuentas**
|
||||||
|
- Cierre permanente o temporal de cuentas de usuario
|
||||||
|
- Evento: `moderation.close_account`
|
||||||
|
- Endpoint: `POST /moderation/accounts/close`
|
||||||
|
|
||||||
|
### 3. **Baneo de Usuarios**
|
||||||
|
- Ban permanente o temporal (en días)
|
||||||
|
- Evento: `moderation.ban_user`
|
||||||
|
- Endpoint: `POST /moderation/users/ban`
|
||||||
|
|
||||||
|
### 4. **Advertencias**
|
||||||
|
- Registra advertencias contra usuarios
|
||||||
|
- Evento: `moderation.warn_user`
|
||||||
|
- Endpoint: `POST /moderation/users/warn`
|
||||||
|
|
||||||
|
### 5. **Revisión de Contenido**
|
||||||
|
- Aprueba, rechaza o solicita más información sobre reportes
|
||||||
|
- Evento: `moderation.review_content`
|
||||||
|
- Endpoint: `POST /moderation/content/review`
|
||||||
|
|
||||||
|
## Arquitectura
|
||||||
|
|
||||||
|
### Componentes
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ API REST de Moderación (FastAPI) │
|
||||||
|
│ Puerto 8003 - Endpoints de moderación │
|
||||||
|
└──────────────┬──────────────────────────────────────────┘
|
||||||
|
│ Envía eventos
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ RabbitMQ Queue │
|
||||||
|
│ moderation_queue │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│ Consume
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Moderation Consumer │
|
||||||
|
│ (Thread - background) │
|
||||||
|
└──────────┬──────────────┘
|
||||||
|
│ Ejecuta acciones
|
||||||
|
▼
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ MongoDB │
|
||||||
|
│ - moderation_actions │
|
||||||
|
│ - moderation_logs │
|
||||||
|
└──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Capas de la Arquitectura
|
||||||
|
|
||||||
|
```
|
||||||
|
domain/
|
||||||
|
└── moderations.py # Entidades de negocio: ModerationAction, ModerationLog
|
||||||
|
|
||||||
|
application/
|
||||||
|
├── ports/
|
||||||
|
│ └── moderation_repository.py # Interfaz de persistencia
|
||||||
|
└── services/
|
||||||
|
└── moderation_services.py # Use cases: DeleteReport, CloseAccount, etc.
|
||||||
|
|
||||||
|
infrastructure/
|
||||||
|
├── adapters/
|
||||||
|
│ ├── moderation_repository_mongo.py # Implementación MongoDB
|
||||||
|
│ └── rabbitmq/
|
||||||
|
│ └── messages.py # Esquemas de eventos RabbitMQ
|
||||||
|
├── api/
|
||||||
|
│ └── moderations/
|
||||||
|
│ ├── app.py # Aplicación FastAPI
|
||||||
|
│ ├── router.py # Rutas principales
|
||||||
|
│ ├── moderations.py # Endpoints
|
||||||
|
│ ├── schemas.py # Validación Pydantic
|
||||||
|
│ └── root.py # Rutas raíz
|
||||||
|
└── consumers/
|
||||||
|
└── moderation_consumer.py # Consumidor de eventos
|
||||||
|
|
||||||
|
main.py # Punto de entrada (integración)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integración con RabbitMQ
|
||||||
|
|
||||||
|
### Flujo de Eventos
|
||||||
|
|
||||||
|
1. **Cliente envía solicitud HTTP** a API de Moderación
|
||||||
|
2. **Servicio valida** la solicitud
|
||||||
|
3. **Servicio envía evento** a cola `moderation_queue` en RabbitMQ
|
||||||
|
4. **Consumer escucha** la cola en background
|
||||||
|
5. **Consumer procesa** el evento y ejecuta acciones
|
||||||
|
6. **Sistema persiste** en MongoDB
|
||||||
|
|
||||||
|
### Implementación de ModerationConsumer
|
||||||
|
|
||||||
|
El `ModerationConsumer` está integrado en `main.py` ejecutándose como un thread daemon:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# En main.py
|
||||||
|
moderations_consumer_thread = threading.Thread(
|
||||||
|
target=run_moderations_consumer,
|
||||||
|
daemon=True,
|
||||||
|
name="Moderations-Consumer"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mensajes de Evento
|
||||||
|
|
||||||
|
Todos los eventos se envían a través de `RabbitMQSender`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action_id": "uuid-aqui",
|
||||||
|
"event_type": "moderation.delete_report",
|
||||||
|
"moderator_id": 123,
|
||||||
|
"report_id": "report-uuid",
|
||||||
|
"reason": "Contenido violento",
|
||||||
|
"description": "Publicación violenta contra usuarios",
|
||||||
|
"timestamp": "2026-05-04T10:30:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints API
|
||||||
|
|
||||||
|
### 1. Eliminación de Reporte
|
||||||
|
```http
|
||||||
|
POST /moderation/reports/delete HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Reporte marcado para eliminación",
|
||||||
|
"action_id": "action-uuid"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Cierre de Cuenta
|
||||||
|
```http
|
||||||
|
POST /moderation/accounts/close HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Baneo de Usuario
|
||||||
|
```http
|
||||||
|
POST /moderation/users/ban HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Advertencia
|
||||||
|
```http
|
||||||
|
POST /moderation/users/warn HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Revisión de Contenido
|
||||||
|
```http
|
||||||
|
POST /moderation/content/review HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ejecución
|
||||||
|
|
||||||
|
### Iniciar todo el sistema
|
||||||
|
```bash
|
||||||
|
python src/main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Esto iniciará:
|
||||||
|
- ✓ API de Usuarios (puerto 8000)
|
||||||
|
- ✓ API de Reportes (puerto 8001)
|
||||||
|
- ✓ API de Notificaciones (puerto 8002)
|
||||||
|
- ✓ **API de Moderación (puerto 8003)**
|
||||||
|
- Consumer de Usuarios
|
||||||
|
- Consumer de Reportes
|
||||||
|
- Consumer de Notificaciones
|
||||||
|
- **Consumer de Moderación**
|
||||||
|
|
||||||
|
### Acceder a documentación
|
||||||
|
- Moderación: http://localhost:8003/docs
|
||||||
|
|
||||||
|
## Persistencia
|
||||||
|
|
||||||
|
La API de Moderación almacena datos en **MongoDB** con dos colecciones:
|
||||||
|
|
||||||
|
### `moderation_actions`
|
||||||
|
Almacena todas las acciones de moderación:
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"action_id": "uuid",
|
||||||
|
"moderator_id": 123,
|
||||||
|
"action_type": "delete_report",
|
||||||
|
"target_type": "report",
|
||||||
|
"target_id": "report-uuid",
|
||||||
|
"reason": "Contenido inapropiado",
|
||||||
|
"status": "pending",
|
||||||
|
"fecha_creacion": "2026-05-04T10:30:00",
|
||||||
|
"fecha_ejecucion": null,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `moderation_logs`
|
||||||
|
Registro auditable de todas las operaciones:
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"log_id": "uuid",
|
||||||
|
"action_id": "action-uuid",
|
||||||
|
"moderator_id": 123,
|
||||||
|
"resultado": "success",
|
||||||
|
"fecha_log": "2026-05-04T10:30:00",
|
||||||
|
"ip_moderator": "192.168.1.100"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integración Sin Cambios Significativos
|
||||||
|
|
||||||
|
Esta API fue diseñada para integrarse sin modificar el código existente:
|
||||||
|
|
||||||
|
✓ **No modifica** `docker-compose.yaml` (usa instancias RabbitMQ existentes)
|
||||||
|
✓ **No modifica** APIs existentes (Usuarios, Reportes, Notificaciones)
|
||||||
|
✓ **No modifica** consumidores existentes
|
||||||
|
✓ **Comunicación desacoplada** via RabbitMQ
|
||||||
|
✓ **Base de datos independiente** (MongoDB)
|
||||||
|
✓ **Puerto separado** (8003)
|
||||||
|
|
||||||
|
## Extensibilidad
|
||||||
|
|
||||||
|
Para agregar nuevas acciones de moderación:
|
||||||
|
|
||||||
|
1. **Crear Use Case** en `application/services/moderation_services.py`
|
||||||
|
2. **Crear evento** en `infrastructure/adapters/rabbitmq/messages.py`
|
||||||
|
3. **Agregar endpoint** en `infrastructure/api/moderations/moderations.py`
|
||||||
|
4. **Manejar evento** en `consumers/moderation_consumer.py`
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- FastAPI
|
||||||
|
- Pydantic
|
||||||
|
- pika (RabbitMQ client)
|
||||||
|
- pymongo (MongoDB driver)
|
||||||
|
- MongoDB instancia
|
||||||
|
- RabbitMQ instancia
|
||||||
|
|
||||||
|
## Seguridad
|
||||||
|
|
||||||
|
### Recomendaciones Implementadas
|
||||||
|
- ✓ Validación de entrada con Pydantic
|
||||||
|
- ✓ Logs auditables de todas las acciones
|
||||||
|
- ✓ Soporte para IP del moderador
|
||||||
|
- ✓ Eventos inmutables en RabbitMQ
|
||||||
|
- ✓ Estados de acción para auditoría
|
||||||
|
|
||||||
|
### Recomendaciones Futuras
|
||||||
|
- Implementar autenticación JWT para moderadores
|
||||||
|
- Rate limiting por moderador
|
||||||
|
- Notificaciones a usuarios afectados
|
||||||
|
- Dashboard de auditoría
|
||||||
|
## Extensibilidad
|
||||||
|
|
||||||
|
Para agregar nuevas acciones de moderación:
|
||||||
|
|
||||||
|
1. **Crear Use Case** en `application/services/moderation_services.py`
|
||||||
|
2. **Crear evento** en `infrastructure/adapters/rabbitmq/messages.py`
|
||||||
|
3. **Agregar endpoint** en `infrastructure/api/moderations/moderations.py`
|
||||||
|
4. **Manejar evento** en `consumers/moderation_consumer.py`
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- FastAPI
|
||||||
|
- Pydantic
|
||||||
|
- pika (RabbitMQ client)
|
||||||
|
- pymongo (MongoDB driver)
|
||||||
|
- MongoDB instancia
|
||||||
|
- RabbitMQ instancia
|
||||||
|
|
||||||
|
## Seguridad
|
||||||
|
|
||||||
|
### Recomendaciones Implementadas
|
||||||
|
- ✓ Validación de entrada con Pydantic
|
||||||
|
- ✓ Logs auditables de todas las acciones
|
||||||
|
- ✓ Soporte para IP del moderador
|
||||||
|
- ✓ Eventos inmutables en RabbitMQ
|
||||||
|
- ✓ Estados de acción para auditoría
|
||||||
|
|
||||||
|
### Recomendaciones Futuras
|
||||||
|
- Implementar autenticación JWT para moderadores
|
||||||
|
- Rate limiting por moderador
|
||||||
|
- Notificaciones a usuarios afectados
|
||||||
|
- Dashboard de auditoría
|
||||||
264
MODERATION_API_EXAMPLES.json
Normal file
264
MODERATION_API_EXAMPLES.json
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
{
|
||||||
|
"moderation_examples": {
|
||||||
|
"delete_report": {
|
||||||
|
"description": "Eliminar un reporte inapropiado",
|
||||||
|
"endpoint": "POST /moderation/reports/delete",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"request_body": {
|
||||||
|
"moderator_id": 1,
|
||||||
|
"report_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"reason": "Contenido violento",
|
||||||
|
"description": "La imagen contiene violencia explícita"
|
||||||
|
},
|
||||||
|
"response_success": {
|
||||||
|
"status": 200,
|
||||||
|
"body": {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Reporte marcado para eliminación",
|
||||||
|
"action_id": "action-uuid-123"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response_error": {
|
||||||
|
"status": 400,
|
||||||
|
"body": {
|
||||||
|
"detail": "Razón debe tener al menos 5 caracteres"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"close_account": {
|
||||||
|
"description": "Cerrar una cuenta de usuario por violación de términos",
|
||||||
|
"endpoint": "POST /moderation/accounts/close",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"request_body": {
|
||||||
|
"moderator_id": 1,
|
||||||
|
"user_id": 42,
|
||||||
|
"reason": "Violación grave de términos de servicio",
|
||||||
|
"description": "Comportamiento acosador y spam sistemático",
|
||||||
|
"is_permanent": true
|
||||||
|
},
|
||||||
|
"response_success": {
|
||||||
|
"status": 200,
|
||||||
|
"body": {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Cuenta marcada para cierre",
|
||||||
|
"action_id": "action-uuid-456"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"ban_user": {
|
||||||
|
"description": "Banear un usuario temporalmente",
|
||||||
|
"endpoint": "POST /moderation/users/ban",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"request_body_temporal": {
|
||||||
|
"moderator_id": 1,
|
||||||
|
"user_id": 42,
|
||||||
|
"reason": "Lenguaje inapropiado repetido",
|
||||||
|
"duration_days": 7,
|
||||||
|
"description": "Ban de 7 días por contenido ofensivo"
|
||||||
|
},
|
||||||
|
"request_body_permanent": {
|
||||||
|
"moderator_id": 1,
|
||||||
|
"user_id": 99,
|
||||||
|
"reason": "Servidumbre reiterada de términos",
|
||||||
|
"duration_days": null,
|
||||||
|
"description": "Ban permanente"
|
||||||
|
},
|
||||||
|
"response_success": {
|
||||||
|
"status": 200,
|
||||||
|
"body": {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Usuario marcado para ban",
|
||||||
|
"action_id": "action-uuid-789"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"warn_user": {
|
||||||
|
"description": "Emitir una advertencia a un usuario",
|
||||||
|
"endpoint": "POST /moderation/users/warn",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"request_body": {
|
||||||
|
"moderator_id": 1,
|
||||||
|
"user_id": 42,
|
||||||
|
"reason": "Primera violación menor de comunidad",
|
||||||
|
"description": "Publicación con spam de enlaces"
|
||||||
|
},
|
||||||
|
"response_success": {
|
||||||
|
"status": 200,
|
||||||
|
"body": {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Advertencia registrada",
|
||||||
|
"action_id": "action-uuid-012"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"review_content": {
|
||||||
|
"description": "Revisar y tomar decisión sobre un reporte",
|
||||||
|
"endpoint": "POST /moderation/content/review",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"request_body_approve": {
|
||||||
|
"moderator_id": 1,
|
||||||
|
"report_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"action": "approve",
|
||||||
|
"reason": "Contenido reportado es legítimamente violatorio",
|
||||||
|
"notes": "Proceder inmediatamente con eliminación"
|
||||||
|
},
|
||||||
|
"request_body_reject": {
|
||||||
|
"moderator_id": 1,
|
||||||
|
"report_id": "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
"action": "reject",
|
||||||
|
"reason": "Contenido está dentro de políticas",
|
||||||
|
"notes": "El usuario tiene derecho a expresar esta opinión"
|
||||||
|
},
|
||||||
|
"request_body_needs_info": {
|
||||||
|
"moderator_id": 1,
|
||||||
|
"report_id": "550e8400-e29b-41d4-a716-446655440002",
|
||||||
|
"action": "needs_more_info",
|
||||||
|
"reason": "Necesaria información contextual adicional",
|
||||||
|
"notes": "Contactar con el reportante para detalles"
|
||||||
|
},
|
||||||
|
"response_success": {
|
||||||
|
"status": 200,
|
||||||
|
"body": {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Revisión registrada: approve",
|
||||||
|
"action_id": "action-uuid-345"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"health_check": {
|
||||||
|
"description": "Verificar estado de la API",
|
||||||
|
"endpoint": "GET /health",
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"body": {
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "Moderation API"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"root_info": {
|
||||||
|
"description": "Obtener información sobre la API",
|
||||||
|
"endpoint": "GET /",
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"body": {
|
||||||
|
"status": "ok",
|
||||||
|
"service": "VoxPopuli Moderation API",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "API para gestión de acciones de moderación",
|
||||||
|
"endpoints": {
|
||||||
|
"delete_report": "POST /moderation/reports/delete",
|
||||||
|
"close_account": "POST /moderation/accounts/close",
|
||||||
|
"ban_user": "POST /moderation/users/ban",
|
||||||
|
"warn_user": "POST /moderation/users/warn",
|
||||||
|
"review_content": "POST /moderation/content/review"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"curl_examples": {
|
||||||
|
"delete_report": "curl -X POST http://localhost:8003/moderation/reports/delete -H 'Content-Type: application/json' -d '{\"moderator_id\": 1, \"report_id\": \"550e8400-e29b-41d4-a716-446655440000\", \"reason\": \"Contenido violento\", \"description\": \"Imagen con violencia explícita\"}'",
|
||||||
|
|
||||||
|
"ban_user": "curl -X POST http://localhost:8003/moderation/users/ban -H 'Content-Type: application/json' -d '{\"moderator_id\": 1, \"user_id\": 42, \"reason\": \"Lenguaje inapropiado\", \"duration_days\": 7}'",
|
||||||
|
|
||||||
|
"warn_user": "curl -X POST http://localhost:8003/moderation/users/warn -H 'Content-Type: application/json' -d '{\"moderator_id\": 1, \"user_id\": 42, \"reason\": \"Primera advertencia\", \"description\": \"Spam de enlaces\"}'",
|
||||||
|
|
||||||
|
"close_account": "curl -X POST http://localhost:8003/moderation/accounts/close -H 'Content-Type: application/json' -d '{\"moderator_id\": 1, \"user_id\": 42, \"reason\": \"Violación de términos\", \"is_permanent\": true}'",
|
||||||
|
|
||||||
|
"review_approve": "curl -X POST http://localhost:8003/moderation/content/review -H 'Content-Type: application/json' -d '{\"moderator_id\": 1, \"report_id\": \"550e8400-e29b-41d4-a716-446655440000\", \"action\": \"approve\", \"reason\": \"Contenido legítimamente violatorio\"}'"
|
||||||
|
},
|
||||||
|
|
||||||
|
"rabbitmq_events": {
|
||||||
|
"moderation_delete_report": {
|
||||||
|
"queue": "moderation_queue",
|
||||||
|
"event_type": "moderation.delete_report",
|
||||||
|
"payload": {
|
||||||
|
"action_id": "uuid",
|
||||||
|
"event_type": "moderation.delete_report",
|
||||||
|
"moderator_id": 1,
|
||||||
|
"report_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"reason": "Contenido violento",
|
||||||
|
"description": "Imagen con violencia explícita",
|
||||||
|
"timestamp": "2026-05-04T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"moderation_close_account": {
|
||||||
|
"queue": "moderation_queue",
|
||||||
|
"event_type": "moderation.close_account",
|
||||||
|
"payload": {
|
||||||
|
"action_id": "uuid",
|
||||||
|
"event_type": "moderation.close_account",
|
||||||
|
"moderator_id": 1,
|
||||||
|
"user_id": 42,
|
||||||
|
"reason": "Violación de términos",
|
||||||
|
"description": "Comportamiento acosador",
|
||||||
|
"is_permanent": true,
|
||||||
|
"timestamp": "2026-05-04T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"moderation_ban_user": {
|
||||||
|
"queue": "moderation_queue",
|
||||||
|
"event_type": "moderation.ban_user",
|
||||||
|
"payload": {
|
||||||
|
"action_id": "uuid",
|
||||||
|
"event_type": "moderation.ban_user",
|
||||||
|
"moderator_id": 1,
|
||||||
|
"user_id": 42,
|
||||||
|
"reason": "Lenguaje inapropiado",
|
||||||
|
"duration_days": 7,
|
||||||
|
"is_permanent": false,
|
||||||
|
"description": "Ban temporal por contenido ofensivo",
|
||||||
|
"timestamp": "2026-05-04T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"moderation_warn_user": {
|
||||||
|
"queue": "moderation_queue",
|
||||||
|
"event_type": "moderation.warn_user",
|
||||||
|
"payload": {
|
||||||
|
"action_id": "uuid",
|
||||||
|
"event_type": "moderation.warn_user",
|
||||||
|
"moderator_id": 1,
|
||||||
|
"user_id": 42,
|
||||||
|
"reason": "Primera advertencia",
|
||||||
|
"description": "Spam de enlaces",
|
||||||
|
"timestamp": "2026-05-04T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"moderation_review_content": {
|
||||||
|
"queue": "moderation_queue",
|
||||||
|
"event_type": "moderation.review_content",
|
||||||
|
"payload": {
|
||||||
|
"action_id": "uuid",
|
||||||
|
"event_type": "moderation.review_content",
|
||||||
|
"moderator_id": 1,
|
||||||
|
"report_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"review_action": "approve",
|
||||||
|
"reason": "Contenido legítimamente violatorio",
|
||||||
|
"notes": "Proceder inmediatamente",
|
||||||
|
"timestamp": "2026-05-04T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
250
MODERATION_SETUP.md
Normal file
250
MODERATION_SETUP.md
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
# Guía de Instalación - API de Moderación
|
||||||
|
|
||||||
|
## Verificar Requisitos Previos
|
||||||
|
|
||||||
|
La API de Moderación requiere que ya tengas instalados y funcionando:
|
||||||
|
|
||||||
|
### 1. RabbitMQ
|
||||||
|
Debe estar ejecutándose con una instancia ya configurada:
|
||||||
|
```bash
|
||||||
|
# Verificar conexión
|
||||||
|
telnet localhost 5672
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. MongoDB
|
||||||
|
Debe estar ejecutándose y accesible:
|
||||||
|
```bash
|
||||||
|
# Verificar conexión
|
||||||
|
mongosh --eval "db.version()"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Python 3.8+
|
||||||
|
```bash
|
||||||
|
python --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Dependencias del Proyecto
|
||||||
|
Las dependencias ya están en `requirements.txt`:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Instalación de la API de Moderación
|
||||||
|
|
||||||
|
### Paso 1: Actualizar dependencias (si es necesario)
|
||||||
|
|
||||||
|
La API utiliza las siguientes librerías (ya incluidas en requirements.txt):
|
||||||
|
- `fastapi` - Framework web
|
||||||
|
- `uvicorn` - Servidor ASGI
|
||||||
|
- `pydantic` - Validación de datos
|
||||||
|
- `pika` - Cliente de RabbitMQ
|
||||||
|
- `pymongo` - Driver de MongoDB
|
||||||
|
|
||||||
|
Si necesitas instalarlas manualmente:
|
||||||
|
```bash
|
||||||
|
pip install fastapi uvicorn pydantic pika pymongo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paso 2: Verificar estructura de directorios
|
||||||
|
|
||||||
|
Asegúrate que existan los siguientes directorios:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── domain/
|
||||||
|
│ └── moderations.py ✓ Creado
|
||||||
|
├── application/
|
||||||
|
│ ├── ports/
|
||||||
|
│ │ └── moderation_repository.py ✓ Creado
|
||||||
|
│ └── services/
|
||||||
|
│ └── moderation_services.py ✓ Creado
|
||||||
|
├── consumers/
|
||||||
|
│ └── moderation_consumer.py ✓ Creado
|
||||||
|
└── infrastructure/
|
||||||
|
├── adapters/
|
||||||
|
│ └── moderation_repository_mongo.py ✓ Creado
|
||||||
|
└── api/
|
||||||
|
└── moderations/ ✓ Creado
|
||||||
|
├── __init__.py
|
||||||
|
├── app.py
|
||||||
|
├── router.py
|
||||||
|
├── moderations.py
|
||||||
|
├── schemas.py
|
||||||
|
└── root.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paso 3: Actualizar archivo principal
|
||||||
|
|
||||||
|
El archivo `src/main.py` ya fue actualizado para incluir:
|
||||||
|
- ✓ Importación de la API de Moderación
|
||||||
|
- ✓ Función `run_moderations_api()`
|
||||||
|
- ✓ Función `run_moderations_consumer()`
|
||||||
|
- ✓ Threads para ejecutar ambos componentes
|
||||||
|
|
||||||
|
### Paso 4: Configurar MongoDB (si es necesario)
|
||||||
|
|
||||||
|
Si MongoDB no está configurado, crea las colecciones:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// En mongosh o cliente MongoDB
|
||||||
|
use voxpopuli
|
||||||
|
|
||||||
|
// Las colecciones se crearán automáticamente al insertar datos
|
||||||
|
// Pero puedes pre-crearlas si lo prefieres:
|
||||||
|
db.createCollection("moderation_actions")
|
||||||
|
db.createCollection("moderation_logs")
|
||||||
|
|
||||||
|
// Crear índices para mejor rendimiento
|
||||||
|
db.moderation_actions.createIndex({ "action_id": 1 })
|
||||||
|
db.moderation_actions.createIndex({ "moderator_id": 1 })
|
||||||
|
db.moderation_actions.createIndex({ "target_id": 1 })
|
||||||
|
db.moderation_logs.createIndex({ "action_id": 1 })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paso 5: Configurar RabbitMQ (si es necesario)
|
||||||
|
|
||||||
|
La cola se crea automáticamente, pero si quieres pre-crearla:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Acceder a RabbitMQ Management Console
|
||||||
|
# http://localhost:15672 (usuario: guest, password: guest)
|
||||||
|
|
||||||
|
# O via CLI:
|
||||||
|
rabbitmqctl declare_queue moderation_queue -d true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ejecutar la API
|
||||||
|
|
||||||
|
### Opción 1: Ejecutar todo (Recomendado)
|
||||||
|
```bash
|
||||||
|
python src/main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Esto iniciará automáticamente:
|
||||||
|
- API de Usuarios (8000)
|
||||||
|
- API de Reportes (8001)
|
||||||
|
- API de Notificaciones (8002)
|
||||||
|
- **API de Moderación (8003)** ← NUEVA
|
||||||
|
- Todos los consumidores (incluyendo Moderación)
|
||||||
|
|
||||||
|
### Opción 2: Ejecutar solo la API de Moderación
|
||||||
|
```bash
|
||||||
|
cd src
|
||||||
|
python -c "from infrastructure.api.moderations.app import create_app; import uvicorn; app = create_app(); uvicorn.run(app, host='0.0.0.0', port=8003)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verificar que funciona
|
||||||
|
|
||||||
|
### 1. Health Check
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8003/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Respuesta esperada:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "Moderation API"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Información de la API
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8003/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Documentación Interactiva
|
||||||
|
Abre en tu navegador:
|
||||||
|
- http://localhost:8003/docs (Swagger UI)
|
||||||
|
- http://localhost:8003/redoc (ReDoc)
|
||||||
|
|
||||||
|
### 4. Probar un endpoint
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8003/moderation/reports/delete \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"moderator_id": 1,
|
||||||
|
"report_id": "test-report-123",
|
||||||
|
"reason": "Contenido inapropiado",
|
||||||
|
"description": "Test description"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Solucionar Problemas
|
||||||
|
|
||||||
|
### Error: "Connection refused" a RabbitMQ
|
||||||
|
```
|
||||||
|
Error: [Errno 111] Connection refused
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solución:**
|
||||||
|
- Verifica que RabbitMQ esté ejecutándose
|
||||||
|
- Verifica el host y puerto en `core/config.py`
|
||||||
|
|
||||||
|
### Error: "ConnectionFailure" a MongoDB
|
||||||
|
```
|
||||||
|
Error: ServerSelectionTimeoutError
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solución:**
|
||||||
|
- Verifica que MongoDB esté ejecutándose
|
||||||
|
- Verifica la configuración de conexión en `infrastructure/adapters/persistence/mongodb.py`
|
||||||
|
|
||||||
|
### Error: "Port 8003 already in use"
|
||||||
|
```
|
||||||
|
Error: [Errno 48] Address already in use
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solución:**
|
||||||
|
Cambia el puerto en `main.py`:
|
||||||
|
```python
|
||||||
|
def run_moderations_api():
|
||||||
|
uvicorn.run(
|
||||||
|
app_moderations,
|
||||||
|
host=ConfSettings.host,
|
||||||
|
port=8004, # Cambiar puerto aquí
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Los eventos no se procesan
|
||||||
|
**Causas posibles:**
|
||||||
|
1. Consumer no está ejecutándose
|
||||||
|
- Verifica que `run_moderations_consumer()` esté ejecutándose
|
||||||
|
- Revisa los logs del consumer
|
||||||
|
|
||||||
|
2. Cola no existe
|
||||||
|
- Se crea automáticamente al enviar el primer evento
|
||||||
|
- Verifica en RabbitMQ Management Console
|
||||||
|
|
||||||
|
3. MongoDB no está guardando datos
|
||||||
|
- Verifica que `moderation_actions` colección exista
|
||||||
|
- Revisa permisos de MongoDB
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
Los logs se guardarán en:
|
||||||
|
```
|
||||||
|
src/logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
Para ver logs en tiempo real:
|
||||||
|
```bash
|
||||||
|
tail -f src/logs/moderation_consumer.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Próximos Pasos
|
||||||
|
|
||||||
|
1. **Integrar autenticación JWT** para moderadores
|
||||||
|
2. **Crear dashboard** para visualizar acciones
|
||||||
|
3. **Agregar notificaciones** a usuarios afectados
|
||||||
|
4. **Implementar auditoría** más detallada
|
||||||
|
5. **Agregar rate limiting** por moderador
|
||||||
|
|
||||||
|
## Contacto y Soporte
|
||||||
|
|
||||||
|
Para problemas con la instalación:
|
||||||
|
1. Revisa logs en `src/logs/`
|
||||||
|
2. Verifica configuración en `src/core/config.py`
|
||||||
|
3. Consulta MODERATION_API.md para arquitectura
|
||||||
|
4. Revisa MODERATION_API_EXAMPLES.json para ejemplos
|
||||||
@@ -49,7 +49,25 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: voxpopuli_postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: voxpopuli
|
||||||
|
POSTGRES_PASSWORD: voxpopuli_pass
|
||||||
|
POSTGRES_DB: voxpopuli_metrics
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U voxpopuli -d voxpopuli_metrics"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql_data:
|
mysql_data:
|
||||||
mongo_data:
|
mongo_data:
|
||||||
mongo_data_notifications:
|
mongo_data_notifications:
|
||||||
|
postgres_data:
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ Pillow
|
|||||||
PyJWT
|
PyJWT
|
||||||
passlib[argon2]
|
passlib[argon2]
|
||||||
python-multipart
|
python-multipart
|
||||||
|
psycopg2-binary
|
||||||
|
|||||||
33
src/application/ports/metrics_repository.py
Normal file
33
src/application/ports/metrics_repository.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Dict
|
||||||
|
from domain.metrics import Metric, DailyStats, AnalyticsReport
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsRepository(ABC):
|
||||||
|
"""Puerto para persistencia de métricas"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def save_metric(self, metric: Metric) -> Metric:
|
||||||
|
"""Guarda una métrica"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_metrics_by_date_range(self, start_date: datetime, end_date: datetime) -> List[Metric]:
|
||||||
|
"""Obtiene métricas en un rango de fechas"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_daily_stats(self, date: datetime) -> List[DailyStats]:
|
||||||
|
"""Obtiene estadísticas diarias"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_event_count_by_type(self, start_date: datetime, end_date: datetime) -> Dict[str, int]:
|
||||||
|
"""Obtiene conteo de eventos por tipo"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def generate_report(self, start_date: datetime, end_date: datetime) -> AnalyticsReport:
|
||||||
|
"""Genera reporte de analítica"""
|
||||||
|
pass
|
||||||
43
src/application/ports/moderation_repository.py
Normal file
43
src/application/ports/moderation_repository.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""Puerto de abstracción para repositorio de Moderaciones"""
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from domain.moderations import ModerationAction, ModerationLog
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class ModerationRepository(ABC):
|
||||||
|
"""Interfaz (puerto) para operaciones de persistencia de moderaciones"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def save_moderation_action(self, action: ModerationAction) -> bool:
|
||||||
|
"""Guarda una acción de moderación"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_moderation_action(self, action_id: str) -> Optional[ModerationAction]:
|
||||||
|
"""Obtiene una acción de moderación por ID"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_actions_by_moderator(self, moderator_id: int) -> List[ModerationAction]:
|
||||||
|
"""Obtiene todas las acciones de un moderador"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_actions_by_target(self, target_id: str, target_type: str) -> List[ModerationAction]:
|
||||||
|
"""Obtiene todas las acciones sobre un target específico"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update_action_status(self, action_id: str, status: str) -> bool:
|
||||||
|
"""Actualiza el estado de una acción"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def save_moderation_log(self, log: ModerationLog) -> bool:
|
||||||
|
"""Guarda un log de moderación"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_moderation_logs(self, action_id: str) -> List[ModerationLog]:
|
||||||
|
"""Obtiene todos los logs de una acción"""
|
||||||
|
pass
|
||||||
39
src/application/services/metrics_services.py
Normal file
39
src/application/services/metrics_services.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List, Dict
|
||||||
|
from domain.metrics import Metric, DailyStats, AnalyticsReport, EventType
|
||||||
|
from application.ports.metrics_repository import MetricsRepository
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsService:
|
||||||
|
"""Servicio de lógica de negocio para métricas"""
|
||||||
|
|
||||||
|
def __init__(self, repository: MetricsRepository):
|
||||||
|
self.repository = repository
|
||||||
|
|
||||||
|
def record_event(self, event_type: str, entity_id: str, entity_type: str,
|
||||||
|
user_id: int = None, metadata: dict = None) -> Metric:
|
||||||
|
"""Registra un evento de métrica"""
|
||||||
|
metric = Metric(
|
||||||
|
metric_id=None,
|
||||||
|
event_type=event_type,
|
||||||
|
entity_id=entity_id,
|
||||||
|
entity_type=entity_type,
|
||||||
|
timestamp=datetime.now(),
|
||||||
|
user_id=user_id,
|
||||||
|
metadata=metadata or {}
|
||||||
|
)
|
||||||
|
return self.repository.save_metric(metric)
|
||||||
|
|
||||||
|
def get_metrics_report(self, days_back: int = 7) -> AnalyticsReport:
|
||||||
|
"""Obtiene reporte de últimos N días"""
|
||||||
|
end_date = datetime.now()
|
||||||
|
start_date = end_date - timedelta(days=days_back)
|
||||||
|
return self.repository.generate_report(start_date, end_date)
|
||||||
|
|
||||||
|
def get_daily_statistics(self, date: datetime) -> List[DailyStats]:
|
||||||
|
"""Obtiene estadísticas de un día específico"""
|
||||||
|
return self.repository.get_daily_stats(date)
|
||||||
|
|
||||||
|
def get_event_summary(self, start_date: datetime, end_date: datetime) -> Dict[str, int]:
|
||||||
|
"""Obtiene resumen de eventos en rango"""
|
||||||
|
return self.repository.get_event_count_by_type(start_date, end_date)
|
||||||
347
src/application/services/moderation_services.py
Normal file
347
src/application/services/moderation_services.py
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
"""Servicios de aplicación para Moderación"""
|
||||||
|
from domain.moderations import ModerationAction, ModerationLog
|
||||||
|
from application.ports.moderation_repository import ModerationRepository
|
||||||
|
from infrastructure.adapters.rabbitmq.sender import send_to_queue
|
||||||
|
from infrastructure.adapters.rabbitmq.messages import ModerationMessage, ModerationEventType
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
import uuid
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteReportUseCase:
|
||||||
|
"""Use case para eliminar un reporte como moderador"""
|
||||||
|
def __init__(self, repo: ModerationRepository):
|
||||||
|
if not isinstance(repo, ModerationRepository):
|
||||||
|
raise TypeError("repo must implement ModerationRepository")
|
||||||
|
self.repo = repo
|
||||||
|
|
||||||
|
def execute(self, moderator_id: int, report_id: str, reason: str,
|
||||||
|
description: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Envía un evento a RabbitMQ para eliminar un reporte
|
||||||
|
|
||||||
|
Args:
|
||||||
|
moderator_id: ID del moderador
|
||||||
|
report_id: ID del reporte a eliminar
|
||||||
|
reason: Razón de la eliminación
|
||||||
|
description: Descripción adicional
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary con status y detalles
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
action_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Crear acción de moderación
|
||||||
|
action = ModerationAction(
|
||||||
|
action_id=action_id,
|
||||||
|
moderator_id=moderator_id,
|
||||||
|
action_type="delete_report",
|
||||||
|
target_type="report",
|
||||||
|
target_id=report_id,
|
||||||
|
reason=reason,
|
||||||
|
description=description,
|
||||||
|
status="pending",
|
||||||
|
fecha_creacion=datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Guardar acción
|
||||||
|
self.repo.save_moderation_action(action)
|
||||||
|
|
||||||
|
# Enviar evento a RabbitMQ
|
||||||
|
message = {
|
||||||
|
"action_id": action_id,
|
||||||
|
"event_type": "moderation.delete_report",
|
||||||
|
"moderator_id": moderator_id,
|
||||||
|
"report_id": report_id,
|
||||||
|
"reason": reason,
|
||||||
|
"description": description or "",
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
send_to_queue('moderation_queue', message)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Reporte marcado para eliminación",
|
||||||
|
"action_id": action_id
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting report: {e}")
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Error al eliminar reporte: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CloseAccountUseCase:
|
||||||
|
"""Use case para cerrar una cuenta de usuario"""
|
||||||
|
def __init__(self, repo: ModerationRepository):
|
||||||
|
if not isinstance(repo, ModerationRepository):
|
||||||
|
raise TypeError("repo must implement ModerationRepository")
|
||||||
|
self.repo = repo
|
||||||
|
|
||||||
|
def execute(self, moderator_id: int, user_id: int, reason: str,
|
||||||
|
description: Optional[str] = None, is_permanent: bool = True) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Envía un evento para cerrar una cuenta de usuario
|
||||||
|
|
||||||
|
Args:
|
||||||
|
moderator_id: ID del moderador
|
||||||
|
user_id: ID del usuario cuya cuenta cerrar
|
||||||
|
reason: Razón del cierre
|
||||||
|
description: Descripción adicional
|
||||||
|
is_permanent: Si el cierre es permanente o temporal
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary con status y detalles
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
action_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
action = ModerationAction(
|
||||||
|
action_id=action_id,
|
||||||
|
moderator_id=moderator_id,
|
||||||
|
action_type="close_account",
|
||||||
|
target_type="user",
|
||||||
|
target_id=str(user_id),
|
||||||
|
reason=reason,
|
||||||
|
description=description,
|
||||||
|
is_permanent=is_permanent,
|
||||||
|
status="pending",
|
||||||
|
fecha_creacion=datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.repo.save_moderation_action(action)
|
||||||
|
|
||||||
|
message = {
|
||||||
|
"action_id": action_id,
|
||||||
|
"event_type": "moderation.close_account",
|
||||||
|
"moderator_id": moderator_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"reason": reason,
|
||||||
|
"description": description or "",
|
||||||
|
"is_permanent": is_permanent,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
send_to_queue('moderation_queue', message)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Cuenta marcada para cierre",
|
||||||
|
"action_id": action_id
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error closing account: {e}")
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Error al cerrar cuenta: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BanUserUseCase:
|
||||||
|
"""Use case para banear un usuario"""
|
||||||
|
def __init__(self, repo: ModerationRepository):
|
||||||
|
if not isinstance(repo, ModerationRepository):
|
||||||
|
raise TypeError("repo must implement ModerationRepository")
|
||||||
|
self.repo = repo
|
||||||
|
|
||||||
|
def execute(self, moderator_id: int, user_id: int, reason: str,
|
||||||
|
duration_days: Optional[int] = None, description: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Envía un evento para banear un usuario
|
||||||
|
|
||||||
|
Args:
|
||||||
|
moderator_id: ID del moderador
|
||||||
|
user_id: ID del usuario a banear
|
||||||
|
reason: Razón del ban
|
||||||
|
duration_days: Duración en días (None para ban permanente)
|
||||||
|
description: Descripción adicional
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary con status y detalles
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
action_id = str(uuid.uuid4())
|
||||||
|
is_permanent = duration_days is None
|
||||||
|
|
||||||
|
action = ModerationAction(
|
||||||
|
action_id=action_id,
|
||||||
|
moderator_id=moderator_id,
|
||||||
|
action_type="ban_user",
|
||||||
|
target_type="user",
|
||||||
|
target_id=str(user_id),
|
||||||
|
reason=reason,
|
||||||
|
description=description,
|
||||||
|
duration_days=duration_days,
|
||||||
|
is_permanent=is_permanent,
|
||||||
|
status="pending",
|
||||||
|
fecha_creacion=datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.repo.save_moderation_action(action)
|
||||||
|
|
||||||
|
message = {
|
||||||
|
"action_id": action_id,
|
||||||
|
"event_type": "moderation.ban_user",
|
||||||
|
"moderator_id": moderator_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"reason": reason,
|
||||||
|
"duration_days": duration_days,
|
||||||
|
"is_permanent": is_permanent,
|
||||||
|
"description": description or "",
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
send_to_queue('moderation_queue', message)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Usuario marcado para ban",
|
||||||
|
"action_id": action_id
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error banning user: {e}")
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Error al banear usuario: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WarnUserUseCase:
|
||||||
|
"""Use case para advertir a un usuario"""
|
||||||
|
def __init__(self, repo: ModerationRepository):
|
||||||
|
if not isinstance(repo, ModerationRepository):
|
||||||
|
raise TypeError("repo must implement ModerationRepository")
|
||||||
|
self.repo = repo
|
||||||
|
|
||||||
|
def execute(self, moderator_id: int, user_id: int, reason: str,
|
||||||
|
description: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Envía un evento para advertir a un usuario
|
||||||
|
|
||||||
|
Args:
|
||||||
|
moderator_id: ID del moderador
|
||||||
|
user_id: ID del usuario a advertir
|
||||||
|
reason: Razón de la advertencia
|
||||||
|
description: Descripción adicional
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary con status y detalles
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
action_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
action = ModerationAction(
|
||||||
|
action_id=action_id,
|
||||||
|
moderator_id=moderator_id,
|
||||||
|
action_type="warn_user",
|
||||||
|
target_type="user",
|
||||||
|
target_id=str(user_id),
|
||||||
|
reason=reason,
|
||||||
|
description=description,
|
||||||
|
status="pending",
|
||||||
|
fecha_creacion=datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.repo.save_moderation_action(action)
|
||||||
|
|
||||||
|
message = {
|
||||||
|
"action_id": action_id,
|
||||||
|
"event_type": "moderation.warn_user",
|
||||||
|
"moderator_id": moderator_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"reason": reason,
|
||||||
|
"description": description or "",
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
send_to_queue('moderation_queue', message)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Advertencia registrada",
|
||||||
|
"action_id": action_id
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error warning user: {e}")
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Error al advertir usuario: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewContentUseCase:
|
||||||
|
"""Use case para revisar contenido reportado"""
|
||||||
|
def __init__(self, repo: ModerationRepository):
|
||||||
|
if not isinstance(repo, ModerationRepository):
|
||||||
|
raise TypeError("repo must implement ModerationRepository")
|
||||||
|
self.repo = repo
|
||||||
|
|
||||||
|
def execute(self, moderator_id: int, report_id: str, action: str,
|
||||||
|
reason: Optional[str] = None, notes: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Envía un evento para revisar y actuar sobre contenido reportado
|
||||||
|
|
||||||
|
Args:
|
||||||
|
moderator_id: ID del moderador
|
||||||
|
report_id: ID del reporte
|
||||||
|
action: Acción a tomar (approve, reject, needs_more_info)
|
||||||
|
reason: Razón de la decisión
|
||||||
|
notes: Notas adicionales
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary con status y detalles
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if action not in ["approve", "reject", "needs_more_info"]:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "Acción inválida"
|
||||||
|
}
|
||||||
|
|
||||||
|
action_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
moderation_action = ModerationAction(
|
||||||
|
action_id=action_id,
|
||||||
|
moderator_id=moderator_id,
|
||||||
|
action_type="review_content",
|
||||||
|
target_type="report",
|
||||||
|
target_id=report_id,
|
||||||
|
reason=reason or action,
|
||||||
|
description=notes,
|
||||||
|
status="approved",
|
||||||
|
fecha_creacion=datetime.now(),
|
||||||
|
fecha_ejecucion=datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.repo.save_moderation_action(moderation_action)
|
||||||
|
|
||||||
|
message = {
|
||||||
|
"action_id": action_id,
|
||||||
|
"event_type": "moderation.review_content",
|
||||||
|
"moderator_id": moderator_id,
|
||||||
|
"report_id": report_id,
|
||||||
|
"review_action": action,
|
||||||
|
"reason": reason or "",
|
||||||
|
"notes": notes or "",
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
send_to_queue('moderation_queue', message)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Revisión registrada: {action}",
|
||||||
|
"action_id": action_id
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reviewing content: {e}")
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Error al revisar contenido: {str(e)}"
|
||||||
|
}
|
||||||
139
src/consumers/metrics_consumer.py
Normal file
139
src/consumers/metrics_consumer.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
"""Metrics RabbitMQ Consumer - Processes system events and saves metrics"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Add src to path to import modules
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
from infrastructure.adapters.rabbitmq.consumer import RabbitMQConsumer
|
||||||
|
from infrastructure.adapters.rabbitmq.messages import (
|
||||||
|
UserMessage, ReportMessage, NotificationMessage, ModerationMessage
|
||||||
|
)
|
||||||
|
from application.services.metrics_services import MetricsService
|
||||||
|
from infrastructure.adapters.persistence.metrics_repository_postgres import MetricsRepositoryPostgres
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler('logs/metrics_consumer.log'),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsConsumer:
|
||||||
|
"""Consumer for all system events from RabbitMQ"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.metrics_service = MetricsService(MetricsRepositoryPostgres())
|
||||||
|
|
||||||
|
# Create separate consumers for each event type
|
||||||
|
self.user_consumer = RabbitMQConsumer(queue_name='users_queue')
|
||||||
|
self.report_consumer = RabbitMQConsumer(queue_name='reports_queue')
|
||||||
|
self.notification_consumer = RabbitMQConsumer(queue_name='notifications_queue')
|
||||||
|
self.moderation_consumer = RabbitMQConsumer(queue_name='moderations_queue')
|
||||||
|
|
||||||
|
# Set callbacks
|
||||||
|
self.user_consumer.set_callback(self.process_user_event)
|
||||||
|
self.report_consumer.set_callback(self.process_report_event)
|
||||||
|
self.notification_consumer.set_callback(self.process_notification_event)
|
||||||
|
self.moderation_consumer.set_callback(self.process_moderation_event)
|
||||||
|
|
||||||
|
def process_user_event(self, message_dict: dict):
|
||||||
|
"""Procesa eventos de usuario"""
|
||||||
|
try:
|
||||||
|
message = UserMessage.from_dict(message_dict)
|
||||||
|
self.metrics_service.record_event(
|
||||||
|
event_type=f"user_{message.event_type.value}",
|
||||||
|
entity_id=str(message.user_id),
|
||||||
|
entity_type="user",
|
||||||
|
user_id=message.user_id,
|
||||||
|
metadata={"email": message.email}
|
||||||
|
)
|
||||||
|
logger.info(f"Métrica registrada: user_{message.event_type.value}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error procesando evento de usuario: {e}")
|
||||||
|
|
||||||
|
def process_report_event(self, message_dict: dict):
|
||||||
|
"""Procesa eventos de reportes"""
|
||||||
|
try:
|
||||||
|
message = ReportMessage.from_dict(message_dict)
|
||||||
|
self.metrics_service.record_event(
|
||||||
|
event_type=f"report_{message.event_type.value}",
|
||||||
|
entity_id=str(message.report_id),
|
||||||
|
entity_type="report",
|
||||||
|
user_id=message.user_id,
|
||||||
|
metadata={"status": message.status}
|
||||||
|
)
|
||||||
|
logger.info(f"Métrica registrada: report_{message.event_type.value}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error procesando evento de reporte: {e}")
|
||||||
|
|
||||||
|
def process_notification_event(self, message_dict: dict):
|
||||||
|
"""Procesa eventos de notificaciones"""
|
||||||
|
try:
|
||||||
|
message = NotificationMessage.from_dict(message_dict)
|
||||||
|
self.metrics_service.record_event(
|
||||||
|
event_type=f"notification_{message.event_type.value}",
|
||||||
|
entity_id=str(message.notification_id),
|
||||||
|
entity_type="notification",
|
||||||
|
user_id=message.user_id,
|
||||||
|
metadata={"type": message.notification_type}
|
||||||
|
)
|
||||||
|
logger.info(f"Métrica registrada: notification_{message.event_type.value}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error procesando evento de notificación: {e}")
|
||||||
|
|
||||||
|
def process_moderation_event(self, message_dict: dict):
|
||||||
|
"""Procesa eventos de moderación"""
|
||||||
|
try:
|
||||||
|
message = ModerationMessage.from_dict(message_dict)
|
||||||
|
self.metrics_service.record_event(
|
||||||
|
event_type=f"moderation_{message.event_type.value}",
|
||||||
|
entity_id=str(message.moderation_id),
|
||||||
|
entity_type="moderation",
|
||||||
|
user_id=message.moderator_id,
|
||||||
|
metadata={"action": message.action}
|
||||||
|
)
|
||||||
|
logger.info(f"Métrica registrada: moderation_{message.event_type.value}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error procesando evento de moderación: {e}")
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Inicia todos los consumers"""
|
||||||
|
logger.info("Iniciando Metrics Consumer...")
|
||||||
|
try:
|
||||||
|
# Start consumers in separate threads
|
||||||
|
import threading
|
||||||
|
threads = [
|
||||||
|
threading.Thread(target=self.user_consumer.start, daemon=True),
|
||||||
|
threading.Thread(target=self.report_consumer.start, daemon=True),
|
||||||
|
threading.Thread(target=self.notification_consumer.start, daemon=True),
|
||||||
|
threading.Thread(target=self.moderation_consumer.start, daemon=True),
|
||||||
|
]
|
||||||
|
for t in threads:
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
# Keep main thread alive
|
||||||
|
for t in threads:
|
||||||
|
t.join()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Metrics Consumer detenido")
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Detiene los consumers"""
|
||||||
|
self.user_consumer.stop()
|
||||||
|
self.report_consumer.stop()
|
||||||
|
self.notification_consumer.stop()
|
||||||
|
self.moderation_consumer.stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
consumer = MetricsConsumer()
|
||||||
|
consumer.start()
|
||||||
174
src/consumers/moderation_consumer.py
Normal file
174
src/consumers/moderation_consumer.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
"""Consumidor de eventos de Moderación desde RabbitMQ"""
|
||||||
|
import pika
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from threading import Thread
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ModerationConsumer:
|
||||||
|
"""Consumidor de eventos de moderación desde RabbitMQ"""
|
||||||
|
|
||||||
|
def __init__(self, host: str = 'localhost', port: int = 5672):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.queue_name = 'moderation_queue'
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Inicia el consumidor en un thread separado"""
|
||||||
|
thread = Thread(target=self._consume, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
logger.info("Moderation Consumer started")
|
||||||
|
|
||||||
|
def _consume(self):
|
||||||
|
"""Consume mensajes de RabbitMQ"""
|
||||||
|
try:
|
||||||
|
connection = pika.BlockingConnection(
|
||||||
|
pika.ConnectionParameters(host=self.host, port=self.port)
|
||||||
|
)
|
||||||
|
channel = connection.channel()
|
||||||
|
|
||||||
|
# Declarar cola
|
||||||
|
channel.queue_declare(queue=self.queue_name, durable=True)
|
||||||
|
|
||||||
|
# Configurar QoS
|
||||||
|
channel.basic_qos(prefetch_count=1)
|
||||||
|
|
||||||
|
# Configurar callback
|
||||||
|
channel.basic_consume(
|
||||||
|
queue=self.queue_name,
|
||||||
|
on_message_callback=self._process_message
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Moderation Consumer listening on {self.queue_name}")
|
||||||
|
self.running = True
|
||||||
|
channel.start_consuming()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in moderation consumer: {e}")
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
def _process_message(self, ch, method, properties, body):
|
||||||
|
"""Procesa un mensaje de moderación"""
|
||||||
|
try:
|
||||||
|
message = json.loads(body)
|
||||||
|
logger.info(f"Processing moderation event: {message.get('event_type')}")
|
||||||
|
|
||||||
|
event_type = message.get('event_type')
|
||||||
|
|
||||||
|
if event_type == 'moderation.delete_report':
|
||||||
|
self._handle_delete_report(message)
|
||||||
|
elif event_type == 'moderation.close_account':
|
||||||
|
self._handle_close_account(message)
|
||||||
|
elif event_type == 'moderation.ban_user':
|
||||||
|
self._handle_ban_user(message)
|
||||||
|
elif event_type == 'moderation.warn_user':
|
||||||
|
self._handle_warn_user(message)
|
||||||
|
elif event_type == 'moderation.review_content':
|
||||||
|
self._handle_review_content(message)
|
||||||
|
else:
|
||||||
|
logger.warning(f"Unknown moderation event type: {event_type}")
|
||||||
|
|
||||||
|
# Confirmar mensaje
|
||||||
|
ch.basic_ack(delivery_tag=method.delivery_tag)
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.error("Error decoding JSON message")
|
||||||
|
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing moderation message: {e}")
|
||||||
|
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
|
||||||
|
|
||||||
|
def _handle_delete_report(self, message: Dict[str, Any]):
|
||||||
|
"""Maneja evento de eliminación de reporte"""
|
||||||
|
try:
|
||||||
|
action_id = message.get('action_id')
|
||||||
|
report_id = message.get('report_id')
|
||||||
|
reason = message.get('reason')
|
||||||
|
|
||||||
|
logger.info(f"Deleting report {report_id} (Action: {action_id})")
|
||||||
|
|
||||||
|
# Aquí implementarías la lógica para eliminar el reporte en la BD
|
||||||
|
# Por ahora solo registramos el evento
|
||||||
|
logger.info(f"Report {report_id} marked as deleted by moderator {message.get('moderator_id')}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error handling delete report: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _handle_close_account(self, message: Dict[str, Any]):
|
||||||
|
"""Maneja evento de cierre de cuenta"""
|
||||||
|
try:
|
||||||
|
action_id = message.get('action_id')
|
||||||
|
user_id = message.get('user_id')
|
||||||
|
is_permanent = message.get('is_permanent', True)
|
||||||
|
|
||||||
|
logger.info(f"Closing account for user {user_id} (Action: {action_id}, Permanent: {is_permanent})")
|
||||||
|
|
||||||
|
# Aquí implementarías la lógica para cerrar la cuenta en la BD
|
||||||
|
logger.info(f"Account for user {user_id} marked as closed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error handling close account: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _handle_ban_user(self, message: Dict[str, Any]):
|
||||||
|
"""Maneja evento de ban de usuario"""
|
||||||
|
try:
|
||||||
|
action_id = message.get('action_id')
|
||||||
|
user_id = message.get('user_id')
|
||||||
|
duration_days = message.get('duration_days')
|
||||||
|
is_permanent = message.get('is_permanent', False)
|
||||||
|
reason = message.get('reason')
|
||||||
|
|
||||||
|
logger.info(f"Banning user {user_id} (Action: {action_id}, Duration: {duration_days} days, Permanent: {is_permanent})")
|
||||||
|
|
||||||
|
# Aquí implementarías la lógica para banear al usuario
|
||||||
|
if is_permanent:
|
||||||
|
logger.info(f"User {user_id} permanently banned")
|
||||||
|
else:
|
||||||
|
logger.info(f"User {user_id} banned for {duration_days} days")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error handling ban user: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _handle_warn_user(self, message: Dict[str, Any]):
|
||||||
|
"""Maneja evento de advertencia a usuario"""
|
||||||
|
try:
|
||||||
|
action_id = message.get('action_id')
|
||||||
|
user_id = message.get('user_id')
|
||||||
|
reason = message.get('reason')
|
||||||
|
|
||||||
|
logger.info(f"Warning user {user_id} (Action: {action_id}, Reason: {reason})")
|
||||||
|
|
||||||
|
# Aquí implementarías la lógica para registrar la advertencia
|
||||||
|
logger.info(f"Warning recorded for user {user_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error handling warn user: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _handle_review_content(self, message: Dict[str, Any]):
|
||||||
|
"""Maneja evento de revisión de contenido"""
|
||||||
|
try:
|
||||||
|
action_id = message.get('action_id')
|
||||||
|
report_id = message.get('report_id')
|
||||||
|
review_action = message.get('review_action') # approve, reject, needs_more_info
|
||||||
|
|
||||||
|
logger.info(f"Reviewing report {report_id} (Action: {action_id}, Decision: {review_action})")
|
||||||
|
|
||||||
|
# Aquí implementarías la lógica para procesar la revisión
|
||||||
|
logger.info(f"Report {report_id} review: {review_action}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error handling review content: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# Crear instancia global
|
||||||
|
moderation_consumer = ModerationConsumer()
|
||||||
46
src/domain/metrics.py
Normal file
46
src/domain/metrics.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class EventType(str, Enum):
|
||||||
|
"""Tipos de eventos que se rastrean"""
|
||||||
|
USER_CREATED = "user_created"
|
||||||
|
USER_UPDATED = "user_updated"
|
||||||
|
USER_DELETED = "user_deleted"
|
||||||
|
REPORT_CREATED = "report_created"
|
||||||
|
REPORT_RESOLVED = "report_resolved"
|
||||||
|
NOTIFICATION_SENT = "notification_sent"
|
||||||
|
MODERATION_COMPLETED = "moderation_completed"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Metric:
|
||||||
|
"""Modelo de dominio para Métrica"""
|
||||||
|
metric_id: int
|
||||||
|
event_type: str
|
||||||
|
entity_id: str
|
||||||
|
entity_type: str
|
||||||
|
timestamp: datetime
|
||||||
|
metadata: dict = field(default_factory=dict)
|
||||||
|
user_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DailyStats:
|
||||||
|
"""Estadísticas diarias"""
|
||||||
|
date: datetime
|
||||||
|
event_type: str
|
||||||
|
count: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AnalyticsReport:
|
||||||
|
"""Reporte de analítica"""
|
||||||
|
report_id: int
|
||||||
|
start_date: datetime
|
||||||
|
end_date: datetime
|
||||||
|
total_events: int
|
||||||
|
events_by_type: dict
|
||||||
|
creation_date: datetime = field(default_factory=datetime.now)
|
||||||
35
src/domain/moderations.py
Normal file
35
src/domain/moderations.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Literal
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ModerationAction:
|
||||||
|
"""Modelo de dominio para Acciones de Moderación"""
|
||||||
|
action_id: str
|
||||||
|
moderator_id: int
|
||||||
|
action_type: Literal["delete_report", "close_account", "ban_user", "warn_user", "review_content"]
|
||||||
|
target_type: Literal["report", "user"]
|
||||||
|
target_id: str # ID del reporte o usuario afectado
|
||||||
|
reason: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
duration_days: Optional[int] = None # Para bans temporales
|
||||||
|
is_permanent: bool = False
|
||||||
|
status: Literal["pending", "approved", "rejected", "executed"] = "pending"
|
||||||
|
fecha_creacion: Optional[datetime] = None
|
||||||
|
fecha_ejecucion: Optional[datetime] = None
|
||||||
|
observaciones: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ModerationLog:
|
||||||
|
"""Log de todas las acciones de moderación realizadas"""
|
||||||
|
log_id: str
|
||||||
|
action_id: str
|
||||||
|
moderator_id: int
|
||||||
|
action_type: str
|
||||||
|
target_type: str
|
||||||
|
target_id: str
|
||||||
|
resultado: Literal["success", "failed", "pending"]
|
||||||
|
mensaje: Optional[str] = None
|
||||||
|
fecha_log: Optional[datetime] = None
|
||||||
|
ip_moderator: Optional[str] = None
|
||||||
176
src/infrastructure/adapters/moderation_repository_mongo.py
Normal file
176
src/infrastructure/adapters/moderation_repository_mongo.py
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
"""Adaptador de repositorio de Moderación con MongoDB"""
|
||||||
|
from application.ports.moderation_repository import ModerationRepository
|
||||||
|
from domain.moderations import ModerationAction, ModerationLog
|
||||||
|
from typing import List, Optional
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ModerationRepositoryMongo(ModerationRepository):
|
||||||
|
"""Implementación de ModerationRepository usando MongoDB"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.db = None
|
||||||
|
self.moderation_actions_collection = None
|
||||||
|
self.moderation_logs_collection = None
|
||||||
|
self._initialize_db()
|
||||||
|
|
||||||
|
def _initialize_db(self):
|
||||||
|
"""Inicializa conexión a MongoDB"""
|
||||||
|
try:
|
||||||
|
from infrastructure.adapters.persistence.mongodb import mongo_db
|
||||||
|
self.db = mongo_db
|
||||||
|
|
||||||
|
# Obtener o crear colecciones
|
||||||
|
self.moderation_actions_collection = self.db['moderation_actions']
|
||||||
|
self.moderation_logs_collection = self.db['moderation_logs']
|
||||||
|
|
||||||
|
# Crear índices
|
||||||
|
self.moderation_actions_collection.create_index('action_id')
|
||||||
|
self.moderation_actions_collection.create_index('moderator_id')
|
||||||
|
self.moderation_actions_collection.create_index('target_id')
|
||||||
|
self.moderation_logs_collection.create_index('action_id')
|
||||||
|
|
||||||
|
logger.info("Moderation MongoDB adapter initialized")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error initializing MongoDB for moderations: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def save_moderation_action(self, action: ModerationAction) -> bool:
|
||||||
|
"""Guarda una acción de moderación en MongoDB"""
|
||||||
|
try:
|
||||||
|
action_dict = {
|
||||||
|
'action_id': action.action_id,
|
||||||
|
'moderator_id': action.moderator_id,
|
||||||
|
'action_type': action.action_type,
|
||||||
|
'target_type': action.target_type,
|
||||||
|
'target_id': action.target_id,
|
||||||
|
'reason': action.reason,
|
||||||
|
'description': action.description,
|
||||||
|
'duration_days': action.duration_days,
|
||||||
|
'is_permanent': action.is_permanent,
|
||||||
|
'status': action.status,
|
||||||
|
'fecha_creacion': action.fecha_creacion or datetime.now(),
|
||||||
|
'fecha_ejecucion': action.fecha_ejecucion,
|
||||||
|
'observaciones': action.observaciones
|
||||||
|
}
|
||||||
|
|
||||||
|
self.moderation_actions_collection.insert_one(action_dict)
|
||||||
|
logger.info(f"Moderation action saved: {action.action_id}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error saving moderation action: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_moderation_action(self, action_id: str) -> Optional[ModerationAction]:
|
||||||
|
"""Obtiene una acción de moderación por ID"""
|
||||||
|
try:
|
||||||
|
doc = self.moderation_actions_collection.find_one({'action_id': action_id})
|
||||||
|
if doc:
|
||||||
|
return self._doc_to_moderation_action(doc)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting moderation action: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_actions_by_moderator(self, moderator_id: int) -> List[ModerationAction]:
|
||||||
|
"""Obtiene todas las acciones de un moderador"""
|
||||||
|
try:
|
||||||
|
docs = self.moderation_actions_collection.find({'moderator_id': moderator_id})
|
||||||
|
return [self._doc_to_moderation_action(doc) for doc in docs]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting actions by moderator: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_actions_by_target(self, target_id: str, target_type: str) -> List[ModerationAction]:
|
||||||
|
"""Obtiene todas las acciones sobre un target específico"""
|
||||||
|
try:
|
||||||
|
docs = self.moderation_actions_collection.find({
|
||||||
|
'target_id': target_id,
|
||||||
|
'target_type': target_type
|
||||||
|
})
|
||||||
|
return [self._doc_to_moderation_action(doc) for doc in docs]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting actions by target: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def update_action_status(self, action_id: str, status: str) -> bool:
|
||||||
|
"""Actualiza el estado de una acción"""
|
||||||
|
try:
|
||||||
|
result = self.moderation_actions_collection.update_one(
|
||||||
|
{'action_id': action_id},
|
||||||
|
{'$set': {'status': status}}
|
||||||
|
)
|
||||||
|
return result.modified_count > 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating action status: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def save_moderation_log(self, log: ModerationLog) -> bool:
|
||||||
|
"""Guarda un log de moderación"""
|
||||||
|
try:
|
||||||
|
log_dict = {
|
||||||
|
'log_id': log.log_id,
|
||||||
|
'action_id': log.action_id,
|
||||||
|
'moderator_id': log.moderator_id,
|
||||||
|
'action_type': log.action_type,
|
||||||
|
'target_type': log.target_type,
|
||||||
|
'target_id': log.target_id,
|
||||||
|
'resultado': log.resultado,
|
||||||
|
'mensaje': log.mensaje,
|
||||||
|
'fecha_log': log.fecha_log or datetime.now(),
|
||||||
|
'ip_moderator': log.ip_moderator
|
||||||
|
}
|
||||||
|
|
||||||
|
self.moderation_logs_collection.insert_one(log_dict)
|
||||||
|
logger.info(f"Moderation log saved: {log.log_id}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error saving moderation log: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_moderation_logs(self, action_id: str) -> List[ModerationLog]:
|
||||||
|
"""Obtiene todos los logs de una acción"""
|
||||||
|
try:
|
||||||
|
docs = self.moderation_logs_collection.find({'action_id': action_id})
|
||||||
|
return [self._doc_to_moderation_log(doc) for doc in docs]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting moderation logs: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _doc_to_moderation_action(doc: dict) -> ModerationAction:
|
||||||
|
"""Convierte un documento de MongoDB a ModerationAction"""
|
||||||
|
return ModerationAction(
|
||||||
|
action_id=doc.get('action_id'),
|
||||||
|
moderator_id=doc.get('moderator_id'),
|
||||||
|
action_type=doc.get('action_type'),
|
||||||
|
target_type=doc.get('target_type'),
|
||||||
|
target_id=doc.get('target_id'),
|
||||||
|
reason=doc.get('reason'),
|
||||||
|
description=doc.get('description'),
|
||||||
|
duration_days=doc.get('duration_days'),
|
||||||
|
is_permanent=doc.get('is_permanent', False),
|
||||||
|
status=doc.get('status', 'pending'),
|
||||||
|
fecha_creacion=doc.get('fecha_creacion'),
|
||||||
|
fecha_ejecucion=doc.get('fecha_ejecucion'),
|
||||||
|
observaciones=doc.get('observaciones')
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _doc_to_moderation_log(doc: dict) -> ModerationLog:
|
||||||
|
"""Convierte un documento de MongoDB a ModerationLog"""
|
||||||
|
return ModerationLog(
|
||||||
|
log_id=doc.get('log_id'),
|
||||||
|
action_id=doc.get('action_id'),
|
||||||
|
moderator_id=doc.get('moderator_id'),
|
||||||
|
action_type=doc.get('action_type'),
|
||||||
|
target_type=doc.get('target_type'),
|
||||||
|
target_id=doc.get('target_id'),
|
||||||
|
resultado=doc.get('resultado'),
|
||||||
|
mensaje=doc.get('mensaje'),
|
||||||
|
fecha_log=doc.get('fecha_log'),
|
||||||
|
ip_moderator=doc.get('ip_moderator')
|
||||||
|
)
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
from sqlalchemy import Column, Integer, String, DateTime, JSON, create_engine
|
||||||
|
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
from domain.metrics import Metric, DailyStats, AnalyticsReport, EventType
|
||||||
|
from application.ports.metrics_repository import MetricsRepository
|
||||||
|
from core.config import ConfSettings
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class MetricModel(Base):
|
||||||
|
"""Modelo SQLAlchemy para métricas en Postgres"""
|
||||||
|
__tablename__ = "metrics"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
event_type = Column(String, nullable=False, index=True)
|
||||||
|
entity_id = Column(String, nullable=False)
|
||||||
|
entity_type = Column(String, nullable=False)
|
||||||
|
timestamp = Column(DateTime, nullable=False, default=datetime.now, index=True)
|
||||||
|
metadata = Column(JSON, default={})
|
||||||
|
user_id = Column(Integer, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsRepositoryPostgres(MetricsRepository):
|
||||||
|
"""Implementación de repositorio de métricas con PostgreSQL"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
db_url = f"postgresql://voxpopuli:voxpopuli_pass@localhost:5432/voxpopuli_metrics"
|
||||||
|
self.engine = create_engine(db_url, echo=False)
|
||||||
|
Base.metadata.create_all(self.engine)
|
||||||
|
self.SessionLocal = sessionmaker(bind=self.engine)
|
||||||
|
|
||||||
|
def save_metric(self, metric: Metric) -> Metric:
|
||||||
|
"""Guarda una métrica en la base de datos"""
|
||||||
|
session = self.SessionLocal()
|
||||||
|
try:
|
||||||
|
db_metric = MetricModel(
|
||||||
|
event_type=metric.event_type,
|
||||||
|
entity_id=metric.entity_id,
|
||||||
|
entity_type=metric.entity_type,
|
||||||
|
timestamp=metric.timestamp,
|
||||||
|
metadata=metric.metadata,
|
||||||
|
user_id=metric.user_id
|
||||||
|
)
|
||||||
|
session.add(db_metric)
|
||||||
|
session.commit()
|
||||||
|
metric.metric_id = db_metric.id
|
||||||
|
return metric
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def get_metrics_by_date_range(self, start_date: datetime, end_date: datetime) -> List[Metric]:
|
||||||
|
"""Obtiene métricas en un rango de fechas"""
|
||||||
|
session = self.SessionLocal()
|
||||||
|
try:
|
||||||
|
db_metrics = session.query(MetricModel).filter(
|
||||||
|
MetricModel.timestamp >= start_date,
|
||||||
|
MetricModel.timestamp <= end_date
|
||||||
|
).all()
|
||||||
|
|
||||||
|
return [
|
||||||
|
Metric(
|
||||||
|
metric_id=m.id,
|
||||||
|
event_type=m.event_type,
|
||||||
|
entity_id=m.entity_id,
|
||||||
|
entity_type=m.entity_type,
|
||||||
|
timestamp=m.timestamp,
|
||||||
|
metadata=m.metadata or {},
|
||||||
|
user_id=m.user_id
|
||||||
|
)
|
||||||
|
for m in db_metrics
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def get_daily_stats(self, date: datetime) -> List[DailyStats]:
|
||||||
|
"""Obtiene estadísticas diarias"""
|
||||||
|
session = self.SessionLocal()
|
||||||
|
try:
|
||||||
|
start = datetime(date.year, date.month, date.day)
|
||||||
|
end = datetime(date.year, date.month, date.day, 23, 59, 59)
|
||||||
|
|
||||||
|
results = session.query(
|
||||||
|
MetricModel.event_type,
|
||||||
|
func.count(MetricModel.id).label('count')
|
||||||
|
).filter(
|
||||||
|
MetricModel.timestamp >= start,
|
||||||
|
MetricModel.timestamp <= end
|
||||||
|
).group_by(MetricModel.event_type).all()
|
||||||
|
|
||||||
|
return [
|
||||||
|
DailyStats(date=date, event_type=r[0], count=r[1])
|
||||||
|
for r in results
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def get_event_count_by_type(self, start_date: datetime, end_date: datetime) -> Dict[str, int]:
|
||||||
|
"""Obtiene conteo de eventos por tipo"""
|
||||||
|
session = self.SessionLocal()
|
||||||
|
try:
|
||||||
|
results = session.query(
|
||||||
|
MetricModel.event_type,
|
||||||
|
func.count(MetricModel.id).label('count')
|
||||||
|
).filter(
|
||||||
|
MetricModel.timestamp >= start_date,
|
||||||
|
MetricModel.timestamp <= end_date
|
||||||
|
).group_by(MetricModel.event_type).all()
|
||||||
|
|
||||||
|
return {r[0]: r[1] for r in results}
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def generate_report(self, start_date: datetime, end_date: datetime) -> AnalyticsReport:
|
||||||
|
"""Genera reporte de analítica"""
|
||||||
|
session = self.SessionLocal()
|
||||||
|
try:
|
||||||
|
total_events = session.query(func.count(MetricModel.id)).filter(
|
||||||
|
MetricModel.timestamp >= start_date,
|
||||||
|
MetricModel.timestamp <= end_date
|
||||||
|
).scalar() or 0
|
||||||
|
|
||||||
|
events_by_type = self.get_event_count_by_type(start_date, end_date)
|
||||||
|
|
||||||
|
return AnalyticsReport(
|
||||||
|
report_id=None,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
total_events=total_events,
|
||||||
|
events_by_type=events_by_type
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
@@ -27,6 +27,15 @@ class NotificationEventType(str, Enum):
|
|||||||
REPORT_STATUS_CHANGE = "notification.report_status_change"
|
REPORT_STATUS_CHANGE = "notification.report_status_change"
|
||||||
|
|
||||||
|
|
||||||
|
class ModerationEventType(str, Enum):
|
||||||
|
"""Types of moderation events"""
|
||||||
|
DELETE_REPORT = "moderation.delete_report"
|
||||||
|
CLOSE_ACCOUNT = "moderation.close_account"
|
||||||
|
BAN_USER = "moderation.ban_user"
|
||||||
|
WARN_USER = "moderation.warn_user"
|
||||||
|
REVIEW_CONTENT = "moderation.review_content"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class UserMessage:
|
class UserMessage:
|
||||||
"""Message for user events"""
|
"""Message for user events"""
|
||||||
@@ -122,3 +131,36 @@ class NotificationMessage:
|
|||||||
"""Create from dictionary"""
|
"""Create from dictionary"""
|
||||||
data['event_type'] = NotificationEventType(data['event_type'])
|
data['event_type'] = NotificationEventType(data['event_type'])
|
||||||
return NotificationMessage(**data)
|
return NotificationMessage(**data)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ModerationMessage:
|
||||||
|
"""Message for moderation events"""
|
||||||
|
event_type: ModerationEventType
|
||||||
|
action_id: Optional[str] = None
|
||||||
|
moderator_id: Optional[int] = None
|
||||||
|
report_id: Optional[str] = None
|
||||||
|
user_id: Optional[int] = None
|
||||||
|
reason: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
duration_days: Optional[int] = None # Para bans temporales
|
||||||
|
is_permanent: Optional[bool] = None
|
||||||
|
review_action: Optional[str] = None # approve, reject, needs_more_info
|
||||||
|
notes: Optional[str] = None
|
||||||
|
fecha_creacion: Optional[str] = None # ISO format datetime string
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""Convert to dictionary"""
|
||||||
|
data = asdict(self)
|
||||||
|
data['event_type'] = self.event_type.value
|
||||||
|
return data
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
"""Convert to JSON string"""
|
||||||
|
return json.dumps(self.to_dict())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(data: dict) -> 'ModerationMessage':
|
||||||
|
"""Create from dictionary"""
|
||||||
|
data['event_type'] = ModerationEventType(data['event_type'])
|
||||||
|
return ModerationMessage(**data)
|
||||||
|
|||||||
1
src/infrastructure/api/metrics/__init__.py
Normal file
1
src/infrastructure/api/metrics/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Metrics API
|
||||||
13
src/infrastructure/api/metrics/app.py
Normal file
13
src/infrastructure/api/metrics/app.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from infrastructure.api.metrics.router import router
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> FastAPI:
|
||||||
|
"""Factory para crear la aplicación de Métricas"""
|
||||||
|
app = FastAPI(
|
||||||
|
title="Métricas Microservice",
|
||||||
|
version="1.0.0",
|
||||||
|
description="Microservicio de métricas y analítica"
|
||||||
|
)
|
||||||
|
app.include_router(router)
|
||||||
|
return app
|
||||||
80
src/infrastructure/api/metrics/router.py
Normal file
80
src/infrastructure/api/metrics/router.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from fastapi import APIRouter, Query
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from infrastructure.api.metrics.schemas import (
|
||||||
|
MetricRequest, MetricResponse, AnalyticsReportResponse,
|
||||||
|
EventSummaryResponse, DailyStatsResponse
|
||||||
|
)
|
||||||
|
from application.services.metrics_services import MetricsService
|
||||||
|
from infrastructure.adapters.persistence.metrics_repository_postgres import MetricsRepositoryPostgres
|
||||||
|
from domain.metrics import Metric
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/metrics", tags=["metrics"])
|
||||||
|
metrics_service = MetricsService(MetricsRepositoryPostgres())
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/record", response_model=MetricResponse)
|
||||||
|
async def record_metric(metric: MetricRequest):
|
||||||
|
"""Registra un nuevo evento de métrica"""
|
||||||
|
saved_metric = metrics_service.record_event(
|
||||||
|
event_type=metric.event_type,
|
||||||
|
entity_id=metric.entity_id,
|
||||||
|
entity_type=metric.entity_type,
|
||||||
|
user_id=metric.user_id,
|
||||||
|
metadata=metric.metadata
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"metric_id": saved_metric.metric_id,
|
||||||
|
"event_type": saved_metric.event_type,
|
||||||
|
"entity_id": saved_metric.entity_id,
|
||||||
|
"entity_type": saved_metric.entity_type,
|
||||||
|
"timestamp": saved_metric.timestamp,
|
||||||
|
"user_id": saved_metric.user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/report", response_model=AnalyticsReportResponse)
|
||||||
|
async def get_analytics_report(days: int = Query(7, ge=1, le=365)):
|
||||||
|
"""Obtiene reporte de analítica de los últimos N días"""
|
||||||
|
report = metrics_service.get_metrics_report(days_back=days)
|
||||||
|
return {
|
||||||
|
"start_date": report.start_date,
|
||||||
|
"end_date": report.end_date,
|
||||||
|
"total_events": report.total_events,
|
||||||
|
"events_by_type": report.events_by_type
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/daily-stats", response_model=list[DailyStatsResponse])
|
||||||
|
async def get_daily_stats(date: datetime = Query(default_factory=datetime.now)):
|
||||||
|
"""Obtiene estadísticas de un día específico"""
|
||||||
|
stats = metrics_service.get_daily_statistics(date)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"date": s.date,
|
||||||
|
"event_type": s.event_type,
|
||||||
|
"count": s.count
|
||||||
|
}
|
||||||
|
for s in stats
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/summary", response_model=EventSummaryResponse)
|
||||||
|
async def get_event_summary(
|
||||||
|
start_date: datetime = Query(default_factory=lambda: datetime.now() - timedelta(days=7)),
|
||||||
|
end_date: datetime = Query(default_factory=datetime.now)
|
||||||
|
):
|
||||||
|
"""Obtiene resumen de eventos en un rango de fechas"""
|
||||||
|
summary = metrics_service.get_event_summary(start_date, end_date)
|
||||||
|
return {
|
||||||
|
"summary": summary,
|
||||||
|
"date_range": {
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": end_date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Health check endpoint"""
|
||||||
|
return {"status": "healthy"}
|
||||||
43
src/infrastructure/api/metrics/schemas.py
Normal file
43
src/infrastructure/api/metrics/schemas.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class MetricRequest(BaseModel):
|
||||||
|
"""Esquema para crear una métrica"""
|
||||||
|
event_type: str
|
||||||
|
entity_id: str
|
||||||
|
entity_type: str
|
||||||
|
user_id: Optional[int] = None
|
||||||
|
metadata: Optional[Dict] = None
|
||||||
|
|
||||||
|
|
||||||
|
class MetricResponse(BaseModel):
|
||||||
|
"""Esquema de respuesta para métrica"""
|
||||||
|
metric_id: int
|
||||||
|
event_type: str
|
||||||
|
entity_id: str
|
||||||
|
entity_type: str
|
||||||
|
timestamp: datetime
|
||||||
|
user_id: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
|
class DailyStatsResponse(BaseModel):
|
||||||
|
"""Esquema de estadísticas diarias"""
|
||||||
|
date: datetime
|
||||||
|
event_type: str
|
||||||
|
count: int
|
||||||
|
|
||||||
|
|
||||||
|
class AnalyticsReportResponse(BaseModel):
|
||||||
|
"""Esquema de reporte de analítica"""
|
||||||
|
start_date: datetime
|
||||||
|
end_date: datetime
|
||||||
|
total_events: int
|
||||||
|
events_by_type: Dict[str, int]
|
||||||
|
|
||||||
|
|
||||||
|
class EventSummaryResponse(BaseModel):
|
||||||
|
"""Esquema de resumen de eventos"""
|
||||||
|
summary: Dict[str, int]
|
||||||
|
date_range: Dict[str, datetime]
|
||||||
0
src/infrastructure/api/moderations/__init__.py
Normal file
0
src/infrastructure/api/moderations/__init__.py
Normal file
46
src/infrastructure/api/moderations/app.py
Normal file
46
src/infrastructure/api/moderations/app.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"""Aplicación FastAPI para API de Moderación"""
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from infrastructure.api.moderations.router import router
|
||||||
|
from core.config import ConfSettings
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configurar logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=ConfSettings.log_level.upper(),
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> FastAPI:
|
||||||
|
"""
|
||||||
|
Factory function para crear la aplicación FastAPI de Moderación
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
FastAPI application instance
|
||||||
|
"""
|
||||||
|
app = FastAPI(
|
||||||
|
title="VoxPopuli Moderation API",
|
||||||
|
description="API para gestión de acciones de moderación en VoxPopuli",
|
||||||
|
version="1.0.0",
|
||||||
|
docs_url="/docs",
|
||||||
|
redoc_url="/redoc",
|
||||||
|
openapi_url="/openapi.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Agregar CORS middleware
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=ConfSettings.cors_origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Incluir rutas
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
|
logger.info("Moderation API initialized")
|
||||||
|
|
||||||
|
return app
|
||||||
182
src/infrastructure/api/moderations/moderations.py
Normal file
182
src/infrastructure/api/moderations/moderations.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
"""Endpoints de moderación para gestión de reportes, cuentas y usuarios"""
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from infrastructure.api.moderations.schemas import (
|
||||||
|
DeleteReportRequest,
|
||||||
|
CloseAccountRequest,
|
||||||
|
BanUserRequest,
|
||||||
|
WarnUserRequest,
|
||||||
|
ReviewContentRequest,
|
||||||
|
ModerationActionResponse
|
||||||
|
)
|
||||||
|
from application.services.moderation_services import (
|
||||||
|
DeleteReportUseCase,
|
||||||
|
CloseAccountUseCase,
|
||||||
|
BanUserUseCase,
|
||||||
|
WarnUserUseCase,
|
||||||
|
ReviewContentUseCase
|
||||||
|
)
|
||||||
|
from infrastructure.adapters.persistence.mongodb import mongo_db
|
||||||
|
from infrastructure.adapters.persistence.db import get_db
|
||||||
|
from infrastructure.adapters.moderation_repository_mongo import ModerationRepositoryMongo
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
# Instanciar repositorio de moderación
|
||||||
|
moderation_repo = ModerationRepositoryMongo()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reports/delete", response_model=ModerationActionResponse)
|
||||||
|
async def delete_report(request: DeleteReportRequest):
|
||||||
|
"""
|
||||||
|
Eliminar un reporte como moderador
|
||||||
|
|
||||||
|
- **moderator_id**: ID del moderador que ejecuta la acción
|
||||||
|
- **report_id**: ID del reporte a eliminar
|
||||||
|
- **reason**: Razón de la eliminación (mínimo 5 caracteres)
|
||||||
|
- **description**: Descripción adicional (opcional)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
use_case = DeleteReportUseCase(moderation_repo)
|
||||||
|
result = use_case.execute(
|
||||||
|
moderator_id=request.moderator_id,
|
||||||
|
report_id=request.report_id,
|
||||||
|
reason=request.reason,
|
||||||
|
description=request.description
|
||||||
|
)
|
||||||
|
|
||||||
|
if result["status"] == "error":
|
||||||
|
raise HTTPException(status_code=400, detail=result["message"])
|
||||||
|
|
||||||
|
return ModerationActionResponse(**result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in delete_report: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Error interno del servidor")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/accounts/close", response_model=ModerationActionResponse)
|
||||||
|
async def close_account(request: CloseAccountRequest):
|
||||||
|
"""
|
||||||
|
Cerrar una cuenta de usuario
|
||||||
|
|
||||||
|
- **moderator_id**: ID del moderador
|
||||||
|
- **user_id**: ID del usuario cuya cuenta cerrar
|
||||||
|
- **reason**: Razón del cierre (mínimo 5 caracteres)
|
||||||
|
- **description**: Descripción adicional (opcional)
|
||||||
|
- **is_permanent**: Si el cierre es permanente (por defecto True)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
use_case = CloseAccountUseCase(moderation_repo)
|
||||||
|
result = use_case.execute(
|
||||||
|
moderator_id=request.moderator_id,
|
||||||
|
user_id=request.user_id,
|
||||||
|
reason=request.reason,
|
||||||
|
description=request.description,
|
||||||
|
is_permanent=request.is_permanent
|
||||||
|
)
|
||||||
|
|
||||||
|
if result["status"] == "error":
|
||||||
|
raise HTTPException(status_code=400, detail=result["message"])
|
||||||
|
|
||||||
|
return ModerationActionResponse(**result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in close_account: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Error interno del servidor")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/users/ban", response_model=ModerationActionResponse)
|
||||||
|
async def ban_user(request: BanUserRequest):
|
||||||
|
"""
|
||||||
|
Banear a un usuario
|
||||||
|
|
||||||
|
- **moderator_id**: ID del moderador
|
||||||
|
- **user_id**: ID del usuario a banear
|
||||||
|
- **reason**: Razón del ban (mínimo 5 caracteres)
|
||||||
|
- **duration_days**: Duración del ban en días (None para permanente)
|
||||||
|
- **description**: Descripción adicional (opcional)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
use_case = BanUserUseCase(moderation_repo)
|
||||||
|
result = use_case.execute(
|
||||||
|
moderator_id=request.moderator_id,
|
||||||
|
user_id=request.user_id,
|
||||||
|
reason=request.reason,
|
||||||
|
duration_days=request.duration_days,
|
||||||
|
description=request.description
|
||||||
|
)
|
||||||
|
|
||||||
|
if result["status"] == "error":
|
||||||
|
raise HTTPException(status_code=400, detail=result["message"])
|
||||||
|
|
||||||
|
return ModerationActionResponse(**result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in ban_user: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Error interno del servidor")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/users/warn", response_model=ModerationActionResponse)
|
||||||
|
async def warn_user(request: WarnUserRequest):
|
||||||
|
"""
|
||||||
|
Advertir a un usuario
|
||||||
|
|
||||||
|
- **moderator_id**: ID del moderador
|
||||||
|
- **user_id**: ID del usuario a advertir
|
||||||
|
- **reason**: Razón de la advertencia (mínimo 5 caracteres)
|
||||||
|
- **description**: Descripción adicional (opcional)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
use_case = WarnUserUseCase(moderation_repo)
|
||||||
|
result = use_case.execute(
|
||||||
|
moderator_id=request.moderator_id,
|
||||||
|
user_id=request.user_id,
|
||||||
|
reason=request.reason,
|
||||||
|
description=request.description
|
||||||
|
)
|
||||||
|
|
||||||
|
if result["status"] == "error":
|
||||||
|
raise HTTPException(status_code=400, detail=result["message"])
|
||||||
|
|
||||||
|
return ModerationActionResponse(**result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in warn_user: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Error interno del servidor")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/content/review", response_model=ModerationActionResponse)
|
||||||
|
async def review_content(request: ReviewContentRequest):
|
||||||
|
"""
|
||||||
|
Revisar y actuar sobre contenido reportado
|
||||||
|
|
||||||
|
- **moderator_id**: ID del moderador
|
||||||
|
- **report_id**: ID del reporte a revisar
|
||||||
|
- **action**: Acción a tomar (approve, reject, needs_more_info)
|
||||||
|
- **reason**: Razón de la decisión (opcional)
|
||||||
|
- **notes**: Notas adicionales (opcional)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
use_case = ReviewContentUseCase(moderation_repo)
|
||||||
|
result = use_case.execute(
|
||||||
|
moderator_id=request.moderator_id,
|
||||||
|
report_id=request.report_id,
|
||||||
|
action=request.action,
|
||||||
|
reason=request.reason,
|
||||||
|
notes=request.notes
|
||||||
|
)
|
||||||
|
|
||||||
|
if result["status"] == "error":
|
||||||
|
raise HTTPException(status_code=400, detail=result["message"])
|
||||||
|
|
||||||
|
return ModerationActionResponse(**result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in review_content: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Error interno del servidor")
|
||||||
32
src/infrastructure/api/moderations/root.py
Normal file
32
src/infrastructure/api/moderations/root.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""Rutas raíz para API de Moderación"""
|
||||||
|
from fastapi import APIRouter, Response
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
async def root():
|
||||||
|
"""Root endpoint - información de la API"""
|
||||||
|
return JSONResponse({
|
||||||
|
"status": "ok",
|
||||||
|
"service": "VoxPopuli Moderation API",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "API para gestión de acciones de moderación",
|
||||||
|
"endpoints": {
|
||||||
|
"delete_report": "POST /moderation/reports/delete",
|
||||||
|
"close_account": "POST /moderation/accounts/close",
|
||||||
|
"ban_user": "POST /moderation/users/ban",
|
||||||
|
"warn_user": "POST /moderation/users/warn",
|
||||||
|
"review_content": "POST /moderation/content/review"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Health check endpoint"""
|
||||||
|
return JSONResponse({
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "Moderation API"
|
||||||
|
})
|
||||||
18
src/infrastructure/api/moderations/router.py
Normal file
18
src/infrastructure/api/moderations/router.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""Router principal para API de Moderación"""
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from infrastructure.api.moderations.moderations import router as moderations_router
|
||||||
|
from infrastructure.api.moderations.root import router as root_router
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
router.include_router(
|
||||||
|
moderations_router,
|
||||||
|
prefix="/moderation",
|
||||||
|
tags=["moderation"]
|
||||||
|
)
|
||||||
|
|
||||||
|
router.include_router(
|
||||||
|
root_router,
|
||||||
|
prefix='',
|
||||||
|
tags=["root"]
|
||||||
|
)
|
||||||
60
src/infrastructure/api/moderations/schemas.py
Normal file
60
src/infrastructure/api/moderations/schemas.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""Pydantic schemas para validación de datos en API de Moderación"""
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, Literal
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteReportRequest(BaseModel):
|
||||||
|
"""Esquema para solicitud de eliminación de reporte"""
|
||||||
|
moderator_id: int = Field(..., description="ID del moderador")
|
||||||
|
report_id: str = Field(..., description="ID del reporte a eliminar")
|
||||||
|
reason: str = Field(..., min_length=5, description="Razón de la eliminación")
|
||||||
|
description: Optional[str] = Field(None, description="Descripción adicional")
|
||||||
|
|
||||||
|
|
||||||
|
class CloseAccountRequest(BaseModel):
|
||||||
|
"""Esquema para solicitud de cierre de cuenta"""
|
||||||
|
moderator_id: int = Field(..., description="ID del moderador")
|
||||||
|
user_id: int = Field(..., description="ID del usuario")
|
||||||
|
reason: str = Field(..., min_length=5, description="Razón del cierre")
|
||||||
|
description: Optional[str] = Field(None, description="Descripción adicional")
|
||||||
|
is_permanent: bool = Field(True, description="Si el cierre es permanente")
|
||||||
|
|
||||||
|
|
||||||
|
class BanUserRequest(BaseModel):
|
||||||
|
"""Esquema para solicitud de ban de usuario"""
|
||||||
|
moderator_id: int = Field(..., description="ID del moderador")
|
||||||
|
user_id: int = Field(..., description="ID del usuario a banear")
|
||||||
|
reason: str = Field(..., min_length=5, description="Razón del ban")
|
||||||
|
duration_days: Optional[int] = Field(None, description="Duración en días (None para permanente)")
|
||||||
|
description: Optional[str] = Field(None, description="Descripción adicional")
|
||||||
|
|
||||||
|
|
||||||
|
class WarnUserRequest(BaseModel):
|
||||||
|
"""Esquema para solicitud de advertencia"""
|
||||||
|
moderator_id: int = Field(..., description="ID del moderador")
|
||||||
|
user_id: int = Field(..., description="ID del usuario a advertir")
|
||||||
|
reason: str = Field(..., min_length=5, description="Razón de la advertencia")
|
||||||
|
description: Optional[str] = Field(None, description="Descripción adicional")
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewContentRequest(BaseModel):
|
||||||
|
"""Esquema para solicitud de revisión de contenido"""
|
||||||
|
moderator_id: int = Field(..., description="ID del moderador")
|
||||||
|
report_id: str = Field(..., description="ID del reporte a revisar")
|
||||||
|
action: Literal["approve", "reject", "needs_more_info"] = Field(..., description="Acción de revisión")
|
||||||
|
reason: Optional[str] = Field(None, description="Razón de la decisión")
|
||||||
|
notes: Optional[str] = Field(None, description="Notas adicionales")
|
||||||
|
|
||||||
|
|
||||||
|
class ModerationActionResponse(BaseModel):
|
||||||
|
"""Esquema de respuesta para acciones de moderación"""
|
||||||
|
status: str = Field(..., description="Estado de la acción (success/error)")
|
||||||
|
message: str = Field(..., description="Mensaje descriptivo")
|
||||||
|
action_id: Optional[str] = Field(None, description="ID de la acción creada")
|
||||||
|
|
||||||
|
|
||||||
|
class ActionStatusRequest(BaseModel):
|
||||||
|
"""Esquema para actualizar estado de acción"""
|
||||||
|
status: Literal["pending", "approved", "rejected", "executed"] = Field(..., description="Nuevo estado")
|
||||||
|
notes: Optional[str] = Field(None, description="Notas sobre el cambio de estado")
|
||||||
51
src/main.py
51
src/main.py
@@ -1,13 +1,17 @@
|
|||||||
"""
|
"""
|
||||||
Punto de entrada principal para VoxPopuli Microservices
|
Punto de entrada principal para VoxPopuli Microservices
|
||||||
Ejecuta dos APIs en paralelo: Usuarios (puerto 8000) y Reportes (puerto 8001), Notificaciones (puerto 8002)
|
Ejecuta cinco APIs en paralelo: Usuarios (8000), Reportes (8001), Notificaciones (8002), Moderación (8003), Métricas (8004)
|
||||||
"""
|
"""
|
||||||
from infrastructure.api.users.app import create_app as create_users_app
|
from infrastructure.api.users.app import create_app as create_users_app
|
||||||
from infrastructure.api.reports.app import create_app as create_reports_app
|
from infrastructure.api.reports.app import create_app as create_reports_app
|
||||||
from infrastructure.api.notifications.app import create_app as create_notifications_app
|
from infrastructure.api.notifications.app import create_app as create_notifications_app
|
||||||
|
from infrastructure.api.moderations.app import create_app as create_moderations_app
|
||||||
|
from infrastructure.api.metrics.app import create_app as create_metrics_app
|
||||||
from consumers.report_consumer import ReportConsumer
|
from consumers.report_consumer import ReportConsumer
|
||||||
from consumers.user_consumer import UserConsumer
|
from consumers.user_consumer import UserConsumer
|
||||||
from consumers.notification_consumer import NotificationConsumer
|
from consumers.notification_consumer import NotificationConsumer
|
||||||
|
from consumers.moderation_consumer import ModerationConsumer
|
||||||
|
from consumers.metrics_consumer import MetricsConsumer
|
||||||
from core.config import ConfSettings
|
from core.config import ConfSettings
|
||||||
import threading
|
import threading
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@@ -45,6 +49,28 @@ def run_notifications_api():
|
|||||||
log_level=ConfSettings.log_level,
|
log_level=ConfSettings.log_level,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def run_moderations_api():
|
||||||
|
"""Ejecuta la API de Moderación en puerto 8003"""
|
||||||
|
app_moderations = create_moderations_app()
|
||||||
|
uvicorn.run(
|
||||||
|
app_moderations,
|
||||||
|
host=ConfSettings.host,
|
||||||
|
port=8003,
|
||||||
|
reload=False,
|
||||||
|
log_level=ConfSettings.log_level,
|
||||||
|
)
|
||||||
|
|
||||||
|
def run_metrics_api():
|
||||||
|
"""Ejecuta la API de Métricas en puerto 8004"""
|
||||||
|
app_metrics = create_metrics_app()
|
||||||
|
uvicorn.run(
|
||||||
|
app_metrics,
|
||||||
|
host=ConfSettings.host,
|
||||||
|
port=8004,
|
||||||
|
reload=False,
|
||||||
|
log_level=ConfSettings.log_level,
|
||||||
|
)
|
||||||
|
|
||||||
def run_user_consumer():
|
def run_user_consumer():
|
||||||
consumer = UserConsumer()
|
consumer = UserConsumer()
|
||||||
consumer.start()
|
consumer.start()
|
||||||
@@ -57,6 +83,14 @@ def run_notifications_consumer():
|
|||||||
consumer = NotificationConsumer()
|
consumer = NotificationConsumer()
|
||||||
consumer.start()
|
consumer.start()
|
||||||
|
|
||||||
|
def run_moderations_consumer():
|
||||||
|
consumer = ModerationConsumer()
|
||||||
|
consumer.start()
|
||||||
|
|
||||||
|
def run_metrics_consumer():
|
||||||
|
consumer = MetricsConsumer()
|
||||||
|
consumer.start()
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
"""Inicia todas las APIs en threads separados"""
|
"""Inicia todas las APIs en threads separados"""
|
||||||
@@ -67,31 +101,44 @@ def run():
|
|||||||
users_thread = threading.Thread(target=run_users_api, daemon=True, name="Users-API")
|
users_thread = threading.Thread(target=run_users_api, daemon=True, name="Users-API")
|
||||||
reports_thread = threading.Thread(target=run_reports_api, daemon=True, name="Reports-API")
|
reports_thread = threading.Thread(target=run_reports_api, daemon=True, name="Reports-API")
|
||||||
notifications_thread = threading.Thread(target=run_notifications_api, daemon=True, name="Notifications-API")
|
notifications_thread = threading.Thread(target=run_notifications_api, daemon=True, name="Notifications-API")
|
||||||
|
moderations_thread = threading.Thread(target=run_moderations_api, daemon=True, name="Moderations-API")
|
||||||
|
metrics_thread = threading.Thread(target=run_metrics_api, daemon=True, name="Metrics-API")
|
||||||
user_consumer_thread = threading.Thread(target=run_user_consumer, daemon=True, name="Users-Consumer")
|
user_consumer_thread = threading.Thread(target=run_user_consumer, daemon=True, name="Users-Consumer")
|
||||||
report_consumer_thread = threading.Thread(target=run_reports_consumer, daemon=True, name="Reports-Consumer")
|
report_consumer_thread = threading.Thread(target=run_reports_consumer, daemon=True, name="Reports-Consumer")
|
||||||
notifications_consumer_thread = threading.Thread(target=run_notifications_consumer, daemon=True, name="Notifications-Consumer")
|
notifications_consumer_thread = threading.Thread(target=run_notifications_consumer, daemon=True, name="Notifications-Consumer")
|
||||||
|
moderations_consumer_thread = threading.Thread(target=run_moderations_consumer, daemon=True, name="Moderations-Consumer")
|
||||||
|
metrics_consumer_thread = threading.Thread(target=run_metrics_consumer, daemon=True, name="Metrics-Consumer")
|
||||||
|
|
||||||
|
|
||||||
users_thread.start()
|
users_thread.start()
|
||||||
reports_thread.start()
|
reports_thread.start()
|
||||||
notifications_thread.start()
|
notifications_thread.start()
|
||||||
|
moderations_thread.start()
|
||||||
|
metrics_thread.start()
|
||||||
user_consumer_thread.start()
|
user_consumer_thread.start()
|
||||||
report_consumer_thread.start()
|
report_consumer_thread.start()
|
||||||
notifications_consumer_thread.start()
|
notifications_consumer_thread.start()
|
||||||
|
moderations_consumer_thread.start()
|
||||||
|
metrics_consumer_thread.start()
|
||||||
|
|
||||||
print("\n✓ API de Usuarios ejecutándose en http://0.0.0.0:8000")
|
print("\n✓ API de Usuarios ejecutándose en http://0.0.0.0:8000")
|
||||||
print("✓ API de Reportes ejecutándose en http://0.0.0.0:8001")
|
print("✓ API de Reportes ejecutándose en http://0.0.0.0:8001")
|
||||||
print("✓ API de Notificaciones ejecutándose en http://0.0.0.0:8002")
|
print("✓ API de Notificaciones ejecutándose en http://0.0.0.0:8002")
|
||||||
|
print("✓ API de Moderación ejecutándose en http://0.0.0.0:8003")
|
||||||
|
print("✓ API de Métricas ejecutándose en http://0.0.0.0:8004")
|
||||||
print("\nDocumentación disponible en:")
|
print("\nDocumentación disponible en:")
|
||||||
print(" - Usuarios: http://localhost:8000/docs")
|
print(" - Usuarios: http://localhost:8000/docs")
|
||||||
print(" - Reportes: http://localhost:8001/docs")
|
print(" - Reportes: http://localhost:8001/docs")
|
||||||
print(" - Notificaciones: http://localhost:8002/docs")
|
print(" - Notificaciones: http://localhost:8002/docs")
|
||||||
|
print(" - Moderación: http://localhost:8003/docs")
|
||||||
|
print(" - Métricas: http://localhost:8004/docs")
|
||||||
print("\n" + "=" * 60 + "\n")
|
print("\n" + "=" * 60 + "\n")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
users_thread.join()
|
users_thread.join()
|
||||||
reports_thread.join()
|
reports_thread.join()
|
||||||
notifications_thread.join()
|
notifications_thread.join()
|
||||||
|
moderations_thread.join()
|
||||||
|
metrics_thread.join()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n\nRecibiendo señal de salida...")
|
print("\n\nRecibiendo señal de salida...")
|
||||||
print("Cerrando APIs...")
|
print("Cerrando APIs...")
|
||||||
|
|||||||
Reference in New Issue
Block a user