actualizacion de readme agente Luisa
This commit is contained in:
@@ -1,31 +1,165 @@
|
|||||||
# Reformix Luisa Bot 🤖
|
# Reformix Luisa Bot 🤖
|
||||||
|
|
||||||
Agente de WhatsApp **Luisa** para **Reformix** — cualifica leads de reforma de forma conversacional, recoge 5 datos clave y cierra el flujo según el flag viable/no_viable.
|
Agente de WhatsApp **Luisa** para **Reformix** — cualifica leads de reforma de forma conversacional siguiendo una máquina de estados de 7 pasos, recoge 5 datos clave (espacio, metros, estilo, urgencia, presupuesto) y cierra el flujo según el flag viable/no_viable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
- **NestJS** — framework principal
|
| Capa | Tecnología |
|
||||||
- **Baileys** — conexión con WhatsApp (sin API oficial)
|
|------|-----------|
|
||||||
- **PostgreSQL** — base de datos via TypeORM
|
| **Framework** | NestJS 10 |
|
||||||
- **Claude 4.5** via **OpenRouter** — LLM con soporte de texto, audio e imagen
|
| **WhatsApp** | Baileys 7 (`@whiskeysockets/baileys`) + `baileys-antiban` |
|
||||||
|
| **Base de datos** | PostgreSQL via TypeORM con `synchronize: true` (dev) / migrations (prod) |
|
||||||
|
| **LLM** | Claude 4.5 Sonnet/Haiku + Gemini 2.5 Flash via **OpenRouter** |
|
||||||
|
| **Logging** | Pino |
|
||||||
|
| **QR** | `qrcode-terminal` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Estructura del proyecto
|
## Estructura del proyecto
|
||||||
|
|
||||||
```
|
```
|
||||||
/src
|
/
|
||||||
/whatsapp ← Módulo Baileys: conexión, QR, recepción y envío
|
├── auth_info_baileys/ ← Estado de sesión de WhatsApp (se genera automáticamente)
|
||||||
/leads ← Módulo de leads: CRUD y lógica de estados
|
├── dist/ ← Compilación
|
||||||
/conversacion ← Módulo de historial de mensajes por lead
|
├── node_modules/
|
||||||
/scheduler ← Cron cada 5 min: dispara apertura a leads nuevos
|
├── prompts/ ← Prompts del sistema para Claude
|
||||||
/claude ← Construye el contexto y llama a Claude 4.5
|
│ ├── luisa_core.md ← Identidad, personalidad y máquina de estados
|
||||||
/media ← Procesa audio e imagen antes de pasar a Claude
|
│ ├── luisa_flujo.md ← Flujo de cualificación paso a paso
|
||||||
|
│ └── luisa_casos.md ← Casos edge y ejemplos
|
||||||
/prompts
|
├── src/
|
||||||
luisa_core.md ← Identidad y personalidad de Luisa ← RELLENAR
|
│ ├── main.ts ← Punto de entrada
|
||||||
luisa_flujo.md ← Flujo de cualificación paso a paso ← RELLENAR
|
│ ├── app.module.ts ← Módulo raíz con TypeORM y Schedule
|
||||||
luisa_casos.md ← Casos edge y ejemplos ← RELLENAR
|
│ ├── whatsapp/
|
||||||
|
│ │ ├── whatsapp.module.ts
|
||||||
|
│ │ ├── whatsapp.service.ts ← Conexión Baileys, recepción/envío
|
||||||
|
│ │ └── whatsapp-debounce.service.ts ← Debounce de 3s para coalescer mensajes rápidos
|
||||||
|
│ ├── leads/
|
||||||
|
│ │ ├── leads.module.ts
|
||||||
|
│ │ ├── lead.entity.ts ← Entidad Lead con todos los campos
|
||||||
|
│ │ └── leads.service.ts ← CRUD, máquina de estados, viabilidad
|
||||||
|
│ ├── conversacion/
|
||||||
|
│ │ ├── conversacion.module.ts
|
||||||
|
│ │ ├── conversacion.entity.ts ← Entidad Conversacion (historial)
|
||||||
|
│ │ └── conversacion.service.ts ← Guardado y recuperación de historial
|
||||||
|
│ ├── claude/
|
||||||
|
│ │ ├── claude.module.ts
|
||||||
|
│ │ └── claude.service.ts ← Arquitectura de 4 capas con Claude
|
||||||
|
│ ├── media/
|
||||||
|
│ │ ├── media.module.ts
|
||||||
|
│ │ └── media.service.ts ← Transcripción de audio + análisis de imagen
|
||||||
|
│ └── scheduler/
|
||||||
|
│ ├── scheduler.module.ts
|
||||||
|
│ └── scheduler.service.ts ← Cron cada 5 min: apertura a leads nuevos + limpieza
|
||||||
|
├── .env.example
|
||||||
|
├── nest-cli.json
|
||||||
|
├── package.json
|
||||||
|
├── tsconfig.json
|
||||||
|
└── tsconfig.build.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquitectura de procesamiento (4 capas con Claude)
|
||||||
|
|
||||||
|
Cada mensaje entrante pasa por 4 capas antes de responder:
|
||||||
|
|
||||||
|
```
|
||||||
|
Mensaje entrante (texto / audio / imagen)
|
||||||
|
↓
|
||||||
|
┌───────────────────────────────┐
|
||||||
|
│ PREPROCESAMIENTO │
|
||||||
|
│ • Identificar lead por teléfono│
|
||||||
|
│ • Si audio → transcripción │
|
||||||
|
│ (Gemini 2.5 Flash via │
|
||||||
|
│ OpenRouter) │
|
||||||
|
│ • Si imagen → Vision │
|
||||||
|
│ (Claude Sonnet via │
|
||||||
|
│ OpenRouter) │
|
||||||
|
│ • Si texto → directo │
|
||||||
|
│ • Guardar mensaje usuario en │
|
||||||
|
│ DB │
|
||||||
|
└───────────┬───────────────────┘
|
||||||
|
↓
|
||||||
|
┌───────────────────────────────┐
|
||||||
|
│ CAPA 1: CLASIFICADOR (Haiku) │
|
||||||
|
│ Extrae intención y valor │
|
||||||
|
│ del mensaje: │
|
||||||
|
│ • responde_pregunta: bool │
|
||||||
|
│ • valor_extraido: string|null │
|
||||||
|
│ • es_desvio: bool │
|
||||||
|
│ • intencion: respuesta | │
|
||||||
|
│ desvio | despedida | │
|
||||||
|
│ insulto | pregunta │
|
||||||
|
└───────────┬───────────────────┘
|
||||||
|
↓
|
||||||
|
┌───────────────────────────────┐
|
||||||
|
│ CAPA 2: VALIDADOR (código) │
|
||||||
|
│ Valida contra valores │
|
||||||
|
│ permitidos del estado actual │
|
||||||
|
│ • espacios: cocina, bano, │
|
||||||
|
│ salon, integral, otro │
|
||||||
|
│ • tamaño: menos10, 10a20, │
|
||||||
|
│ 20a40, mas40 │
|
||||||
|
│ • estilo: funcional, cuidado, │
|
||||||
|
│ exclusivo │
|
||||||
|
│ • urgencia: urgente, │
|
||||||
|
│ medio_plazo, frio │
|
||||||
|
│ • presupuesto: evalúa │
|
||||||
|
│ viabilidad (>= 5000€) │
|
||||||
|
└───────────┬───────────────────┘
|
||||||
|
↓
|
||||||
|
┌───────────────────────────────┐
|
||||||
|
│ CAPA 3: GENERADOR (Sonnet) │
|
||||||
|
│ Construye contexto y genera │
|
||||||
|
│ borrador de respuesta │
|
||||||
|
│ • Estado del lead │
|
||||||
|
│ • Datos capturados │
|
||||||
|
│ • Historial completo │
|
||||||
|
│ • Clasificación y validación │
|
||||||
|
│ • Reintentos │
|
||||||
|
└───────────┬───────────────────┘
|
||||||
|
↓
|
||||||
|
┌───────────────────────────────┐
|
||||||
|
│ CAPA 4: REGLAS (Haiku) │
|
||||||
|
│ Corrige el borrador para │
|
||||||
|
│ cumplir identidad de Luisa: │
|
||||||
|
│ • Sin menciones a IA │
|
||||||
|
│ • Máximo 2 líneas │
|
||||||
|
│ • Sin emojis │
|
||||||
|
│ • Tono Madrid/España │
|
||||||
|
└───────────┬───────────────────┘
|
||||||
|
↓
|
||||||
|
Guardar respuesta en DB
|
||||||
|
↓
|
||||||
|
Enviar por Baileys
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variables de entorno
|
||||||
|
|
||||||
|
### `.env.example`
|
||||||
|
|
||||||
|
```env
|
||||||
|
OPENROUTER_API_KEY= # (REQUERIDA) API key de OpenRouter
|
||||||
|
MODEL_GENERADOR=anthropic/claude-sonnet-4-5 # Modelo para generar respuestas (Capa 3)
|
||||||
|
MODEL_CLASIFICADOR=anthropic/claude-haiku-4-5 # Modelo para clasificar mensajes (Capa 1)
|
||||||
|
MODEL_REGLAS=anthropic/claude-haiku-4-5 # Modelo para aplicar reglas (Capa 4)
|
||||||
|
MODEL_TRANSCRIPCION=google/gemini-2.5-flash # Modelo para transcripción de audio
|
||||||
|
MODEL=anthropic/claude-sonnet-4-5 # Fallback general si no se especifica otro
|
||||||
|
DATABASE_URL= # (REQUERIDA) PostgreSQL connection string
|
||||||
|
ALLOWED_NUMBER= # (OPCIONAL) Restringe el bot a un solo número
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notas:**
|
||||||
|
- `ALLOWED_NUMBER` es opcional. Si se define, el bot **solo atiende mensajes de ese número**. Si se deja vacío, acepta mensajes de cualquier número de teléfono.
|
||||||
|
- La transcripción de audio usa **Gemini 2.5 Flash** por defecto porque Claude no soporta `input_audio` en OpenRouter. El resto del pipeline sigue usando Claude.
|
||||||
|
- Si `MODEL` está definido, sirve como fallback para `MODEL_GENERADOR` cuando no se especifica.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Configuración rápida
|
## Configuración rápida
|
||||||
|
|
||||||
### 1. Variables de entorno
|
### 1. Variables de entorno
|
||||||
@@ -34,32 +168,33 @@ Agente de WhatsApp **Luisa** para **Reformix** — cualifica leads de reforma de
|
|||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Edita `.env`:
|
Edita `.env` con tus valores reales:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
OPENROUTER_API_KEY=sk-or-...
|
OPENROUTER_API_KEY=sk-or-v1-tu-api-key-aqui
|
||||||
MODEL=anthropic/claude-sonnet-4-5
|
DATABASE_URL=postgresql://user:password@localhost:5432/reformix
|
||||||
DATABASE_URL=postgresql://user:password@localhost:5432/reformix_luisa
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Base de datos
|
### 2. Base de datos
|
||||||
|
|
||||||
El proyecto usa `synchronize: true` en modo desarrollo, TypeORM creará las tablas automáticamente al arrancar.
|
El proyecto usa `synchronize: true` en modo desarrollo (definido en `src/app.module.ts`). TypeORM creará las tablas automáticamente al arrancar.
|
||||||
|
|
||||||
En producción, desactiva `synchronize` y usa migrations:
|
En producción, cambia `synchronize: false` y usa migrations:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run migration:generate
|
npm run migration:generate -- src/migrations/Init
|
||||||
npm run migration:run
|
npm run migration:run
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Prompts de Luisa
|
### 3. Prompts de Luisa
|
||||||
|
|
||||||
Rellena los 3 archivos en `/prompts` antes de arrancar:
|
Los 3 archivos de prompts ya están creados y contienen la configuración completa:
|
||||||
|
|
||||||
- `luisa_core.md` — identidad, tono, límites
|
- **`prompts/luisa_core.md`** — Identidad de Luisa, personalidad (español de Madrid, cercana, directa), máquina de estados obligatoria con mensajes exactos por estado, extracción de datos con bloque JSON `<DATOS_EXTRAIDOS>`, manejo de casos especiales (desvíos, reintentos, inactividad, multimedia, tono defensivo), y ejemplos few-shot.
|
||||||
- `luisa_flujo.md` — estados, preguntas por estado, condiciones de avance
|
- **`prompts/luisa_flujo.md`** — Secuencia de estados, campos DB con valores permitidos, y mensajes por estado (versión simplificada).
|
||||||
- `luisa_casos.md` — casos edge, fallbacks, ejemplos de conversación
|
- **`prompts/luisa_casos.md`** — Casos edge: desvíos, reintentos (máx 2), inactividad (24h/48h), manejo de audio/imagen/sticker, tono defensivo, usuario que no da presupuesto.
|
||||||
|
|
||||||
|
Puedes modificarlos libremente para ajustar el tono o comportamiento de Luisa.
|
||||||
|
|
||||||
### 4. Arrancar
|
### 4. Arrancar
|
||||||
|
|
||||||
@@ -68,61 +203,139 @@ npm install
|
|||||||
npm run start:dev
|
npm run start:dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Escanea el **QR** que aparece en la terminal con WhatsApp.
|
Aparecerá un **código QR** en la terminal. Escanéalo con WhatsApp → **WhatsApp Web**.
|
||||||
|
|
||||||
Luisa queda conectada y lista.
|
Luisa queda conectada y lista para recibir mensajes.
|
||||||
|
|
||||||
## Flujo de mensajes
|
---
|
||||||
|
|
||||||
```
|
## Máquina de estados del lead
|
||||||
Mensaje entrante (texto / audio / imagen)
|
|
||||||
↓
|
|
||||||
Identificar lead por teléfono (crear si no existe)
|
|
||||||
↓
|
|
||||||
Si audio → Claude 4.5 transcripción
|
|
||||||
Si imagen → Claude 4.5 Vision (prompt según estado)
|
|
||||||
Si texto → directo
|
|
||||||
↓
|
|
||||||
Guardar mensaje usuario en DB
|
|
||||||
↓
|
|
||||||
Construir contexto: estado, datos del lead, historial, prompts MD
|
|
||||||
↓
|
|
||||||
Llamar Claude 4.5 via OpenRouter
|
|
||||||
↓
|
|
||||||
Extraer entidades del turno → actualizar lead en DB
|
|
||||||
↓
|
|
||||||
Evaluar flag viable → cambiar estado si aplica
|
|
||||||
↓
|
|
||||||
Guardar respuesta de Claude en DB
|
|
||||||
↓
|
|
||||||
Enviar respuesta por Baileys
|
|
||||||
```
|
|
||||||
|
|
||||||
## Scheduler (cron cada 5 min)
|
|
||||||
|
|
||||||
- Busca leads con `estado_actual = 'nuevo'`
|
|
||||||
- Marca como `en_proceso` antes de actuar
|
|
||||||
- Genera y envía el mensaje de APERTURA de Luisa
|
|
||||||
- Ignora leads en `completado`, `no_viable`, `perdido`
|
|
||||||
- Marca como `perdido` leads en `en_proceso` sin actividad > 48h
|
|
||||||
|
|
||||||
## Estados del lead
|
|
||||||
|
|
||||||
| Estado | Descripción |
|
| Estado | Descripción |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `nuevo` | Lead creado, aún no contactado |
|
| `nuevo` | Lead creado, aún no contactado |
|
||||||
| `en_proceso` | Luisa le ha enviado el primer mensaje |
|
| `en_proceso` | El scheduler le ha enviado el primer mensaje (transición interna) |
|
||||||
| `recopilando_datos` | Conversación activa |
|
| `apertura` | Luisa se presenta y pregunta disponibilidad |
|
||||||
| `completado` | Todos los datos recogidos, viable=true |
|
| `espacio` | Pregunta: ¿qué espacio quieres reformar? |
|
||||||
| `no_viable` | Lead descartado, viable=false |
|
| `tamano` | Pregunta: ¿rango de metros cuadrados? |
|
||||||
|
| `estilo` | Pregunta: ¿tipo de acabado? |
|
||||||
|
| `urgencia` | Pregunta: ¿cuándo quieres empezar? |
|
||||||
|
| `presupuesto` | Pregunta: ¿presupuesto aproximado? |
|
||||||
|
| `fin_viable` | Transición interna → `completado` si presupuesto >= 5000€ |
|
||||||
|
| `fin_no_viable` | Transición interna → `no_viable` si presupuesto < 5000€ |
|
||||||
|
| `recopilando_datos` | Estado legacy, se normaliza a `apertura` |
|
||||||
|
| `completado` | Todos los datos recogidos y lead viable |
|
||||||
|
| `no_viable` | Lead descartado por presupuesto insuficiente |
|
||||||
| `perdido` | Sin actividad > 48h |
|
| `perdido` | Sin actividad > 48h |
|
||||||
|
|
||||||
|
**Secuencia completa:** `nuevo → apertura → espacio → tamano → estilo → urgencia → presupuesto → fin_viable/fin_no_viable`
|
||||||
|
|
||||||
|
### Datos recolectados por estado
|
||||||
|
|
||||||
|
| Estado | Campo DB | Valores válidos |
|
||||||
|
|--------|----------|-----------------|
|
||||||
|
| `espacio` | `espacio` | `cocina`, `bano`, `salon`, `integral`, `otro` |
|
||||||
|
| `tamano` | `rango_m2` | `menos10`, `10a20`, `20a40`, `mas40` |
|
||||||
|
| `estilo` | `estilo` | `funcional`, `cuidado`, `exclusivo` |
|
||||||
|
| `urgencia` | `urgencia` | `urgente`, `medio_plazo`, `frio` |
|
||||||
|
| `presupuesto` | `presupuesto_declarado` | Cifra o rango en euros |
|
||||||
|
|
||||||
|
### Evaluación de viabilidad
|
||||||
|
|
||||||
|
Un presupuesto es **viable** si su valor numérico es **>= 5000€**. Si no se puede determinar el valor, se asume viable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scheduler (cron cada 5 min)
|
||||||
|
|
||||||
|
El servicio `SchedulerService` se ejecuta cada 5 minutos vía `@nestjs/schedule` y realiza dos tareas:
|
||||||
|
|
||||||
|
1. **Limpiar leads inactivos**: Marca como `perdido` los leads en `en_proceso` sin actualización > 48h.
|
||||||
|
2. **Apertura de leads nuevos**: Busca leads con `estado_actual = 'nuevo'`, los marca como `en_proceso`, llama a Claude para generar el mensaje de apertura, lo guarda en el historial y lo envía por WhatsApp.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debounce de mensajes
|
||||||
|
|
||||||
|
El servicio `WhatsappDebounceService` agrupa mensajes rápidos de un mismo usuario en una ventana de **3 segundos**. Si el usuario envía varios mensajes cortos seguidos, se concatenan en un solo texto antes de procesarlos. Esto evita que Claude reciba múltiples requests simultáneas por el mismo lead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Restricción por número (ALLOWED_NUMBER)
|
||||||
|
|
||||||
|
Si se define `ALLOWED_NUMBER` en `.env`, el bot **ignora cualquier mensaje que no venga de ese número**. Esto es útil para pruebas o para desplegar una instancia dedicada a un solo cliente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Soporte multimedia
|
||||||
|
|
||||||
|
### Audio
|
||||||
|
- Se descarga el buffer del mensaje de audio via Baileys.
|
||||||
|
- Se detecta el formato real por magic bytes (Ogg, MP3, WAV).
|
||||||
|
- Se envía a OpenRouter con `input_audio` usando **Gemini 2.5 Flash** (modelo configurable via `MODEL_TRANSCRIPCION`).
|
||||||
|
- La transcripción conserva coloquialismos y jerga madrileña.
|
||||||
|
- Si falla o el buffer es muy pequeño, se responde con un mensaje de cortesía pidiendo repetición.
|
||||||
|
|
||||||
|
### Imagen
|
||||||
|
- Se descarga el buffer y se envía a OpenRouter con `image_url` usando **Claude Sonnet**.
|
||||||
|
- El prompt de análisis varía según el estado actual del lead:
|
||||||
|
- En `espacio`/`tamano`: infiere tipo de espacio y metros cuadrados.
|
||||||
|
- En `estilo`: infiere el estilo o calidad de acabados.
|
||||||
|
- En otros estados: descripción general.
|
||||||
|
- Si la imagen tiene caption, se combina con la inferencia.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manejo de errores y reconexión
|
||||||
|
|
||||||
|
- Si la conexión de WhatsApp se cierra por cualquier motivo que **no sea logout**, se reintenta automáticamente tras 5 segundos.
|
||||||
|
- Si hay un **logout** (sesión cerrada), se muestra un error pidiendo eliminar `auth_info_baileys` y reiniciar.
|
||||||
|
- Cada mensaje se procesa en un bloque `try/catch`; si falla, se loguea el error y se continúa con el siguiente mensaje.
|
||||||
|
- Si Claude falla repetidamente al clasificar, se usa un fallback conservador (desvío) para no bloquear la conversación.
|
||||||
|
- Si la respuesta generada contiene frases prohibidas ("soy un asistente", "ChatGPT", etc.), se reemplaza automáticamente con un `mensajeFallback` según el estado actual.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scripts disponibles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build # Compilar con NestJS
|
||||||
|
npm run start # Iniciar en producción
|
||||||
|
npm run start:dev # Iniciar en desarrollo con watch
|
||||||
|
npm run start:debug # Iniciar en modo debug
|
||||||
|
npm run lint # Ejecutar ESLint
|
||||||
|
npm run test # Ejecutar tests con Jest
|
||||||
|
npm run migration:generate # Generar migración de TypeORM
|
||||||
|
npm run migration:run # Ejecutar migraciones pendientes
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Qué NO hace este servicio
|
## Qué NO hace este servicio
|
||||||
|
|
||||||
- No genera el presupuesto (lo hace otro worker)
|
- ❌ No genera el presupuesto (lo hace otro worker externo)
|
||||||
- No renderiza el PDF
|
- ❌ No renderiza el PDF
|
||||||
- No envía la URL (la inserta el worker en `url_presupuesto`)
|
- ❌ No envía la URL del presupuesto (la inserta el worker en `url_presupuesto`)
|
||||||
- No tiene panel del reformista
|
- ❌ No tiene panel del reformista
|
||||||
|
- ❌ No maneja conversaciones grupales (@g.us) — se ignoran explícitamente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Desarrollo
|
||||||
|
|
||||||
|
### Requisitos
|
||||||
|
|
||||||
|
- Node.js >= 20
|
||||||
|
- PostgreSQL >= 14
|
||||||
|
- Cuenta en [OpenRouter](https://openrouter.ai) con API key
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test
|
||||||
|
npm run test:watch # Modo watch
|
||||||
|
npm run test:cov # Con cobertura
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user