import: contenido inicial de la skill whatsapp
This commit is contained in:
10
.env.example
Normal file
10
.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
# whatsapp-bridge skill — config local (NO versionar)
|
||||
#
|
||||
# Copiar a `.env` en este mismo directorio y completar.
|
||||
|
||||
# Token bearer que usas para autenticarte con la API.
|
||||
# Obtener de /srv/NucleOS/whatsapp-bridge/.env en nucleo001 (var API_TOKEN).
|
||||
WA_BRIDGE_TOKEN=
|
||||
|
||||
# (opcional) Override de la URL del bridge — útil si expones local en otro host.
|
||||
# WA_BRIDGE_URL=https://whatsapp.nucleoriofrio.com
|
||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Local config con secrets — NUNCA committear
|
||||
.env
|
||||
|
||||
# Cache local
|
||||
.cache/
|
||||
|
||||
# Temporales
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
49
README.md
49
README.md
@@ -1,3 +1,50 @@
|
||||
# skill-whatsapp
|
||||
|
||||
Skill de Claude Code para consultar y enviar mensajes de WhatsApp del beneficio Rio Frio (numero +504 9742-9865) via el servicio whatsapp-bridge desplegado en nucleo001.
|
||||
Skill de [Claude Code](https://claude.com/claude-code) para consultar y enviar
|
||||
mensajes de WhatsApp del beneficio Rio Frio (número **+504 9742-9865**, nombre
|
||||
"Nucleo🌐🧠🤖") vía el servicio `whatsapp-bridge` desplegado en nucleo001
|
||||
(`https://whatsapp.nucleoriofrio.com`).
|
||||
|
||||
> Mirror público del directorio local `~/.claude/skills/whatsapp/` en la PC
|
||||
> del usuario humano. Existe para que el usuario pueda **auditar** lo que el
|
||||
> bot lee y envía. Clonar y correr en otra máquina **no funciona
|
||||
> out-of-the-box** — requiere bearer token del bridge en `.env` (no incluido)
|
||||
> y acceso al endpoint público.
|
||||
|
||||
## Doc principal
|
||||
|
||||
Ver **[SKILL.md](SKILL.md)** — flujos típicos, endpoints disponibles
|
||||
(read + write), reglas de comportamiento.
|
||||
|
||||
Cheat sheet en **[endpoints.md](endpoints.md)** y mapping de JIDs conocidos
|
||||
en **[contacts.md](contacts.md)**.
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
.
|
||||
├── SKILL.md ← docs canónicas
|
||||
├── endpoints.md ← cheat sheet API del bridge
|
||||
├── contacts.md ← mapping JID → contacto/grupo conocido
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
├── README.md ← este archivo
|
||||
└── scripts/
|
||||
└── query.sh ← helper REST autenticado (Bearer token)
|
||||
```
|
||||
|
||||
## Lo que NO está en este repo
|
||||
|
||||
- `.env` (token del bridge)
|
||||
|
||||
## Backend
|
||||
|
||||
El servicio `whatsapp-bridge` (Baileys) corre como stack en nucleo001 — código
|
||||
fuente y deploy: [NucleOS/whatsapp-bridge](https://gitea.nucleoriofrio.com/NucleOS/whatsapp-bridge)
|
||||
(repo del backend, no de esta skill).
|
||||
|
||||
## Dependencias
|
||||
|
||||
- `curl`
|
||||
- `python` (parsear JSON)
|
||||
- Token válido en `.env` (`WA_BRIDGE_TOKEN`)
|
||||
|
||||
237
SKILL.md
Normal file
237
SKILL.md
Normal file
@@ -0,0 +1,237 @@
|
||||
---
|
||||
name: whatsapp
|
||||
description: Consulta y envía mensajes de WhatsApp del beneficio Rio Frio a través del servicio whatsapp-bridge desplegado en nucleo001 (https://whatsapp.nucleoriofrio.com). Úsala cuando el usuario pida buscar, resumir, listar, extraer o enviar información a chats o grupos de WhatsApp de ese número. Cubre casos como "qué se habló en el grupo X", "alguien mencionó retenciones esta semana", "cuál fue el último precio discutido", "mandale un mensaje a Y".
|
||||
allowed-tools: Bash, Read, Grep
|
||||
---
|
||||
|
||||
# WhatsApp Bridge — skill de consulta on-demand
|
||||
|
||||
## Qué es este servicio
|
||||
|
||||
`whatsapp-bridge` es un servicio de Nucleo que mantiene una sesión de WhatsApp
|
||||
vía Baileys contra el número **+504 9742-9865** del beneficio Rio Frio. Guarda
|
||||
todos los mensajes de los chats (individuales y grupos) a los que ese número
|
||||
pertenece en Postgres, con retención infinita.
|
||||
|
||||
La API expone consultas read + write. **El endpoint `/send` está habilitado**.
|
||||
|
||||
- **Base URL**: `https://whatsapp.nucleoriofrio.com`
|
||||
- **Auth**: `Authorization: Bearer $WA_BRIDGE_TOKEN`
|
||||
- **El token vive en**: `~/.claude/skills/whatsapp/.env` (local, no versionado)
|
||||
|
||||
**Nota sobre el endpoint MCP**: `https://whatsapp.nucleoriofrio.com/mcp` existe
|
||||
para que Claude.ai web/móvil consuma el mismo servicio como Custom Integration.
|
||||
**No lo uses tú desde Claude Code** — tu path principal es la API REST vía
|
||||
`scripts/query.sh`. El MCP es solo para los otros clientes de Claude.
|
||||
|
||||
## Cómo invocarla correctamente
|
||||
|
||||
Antes de cualquier query:
|
||||
1. Carga el token: `source ~/.claude/skills/whatsapp/.env` (o lee `$WA_BRIDGE_TOKEN` del entorno).
|
||||
2. Si no hay token, avisa al usuario: pedir que cree `~/.claude/skills/whatsapp/.env` con `WA_BRIDGE_TOKEN=<valor>`.
|
||||
3. Verifica estado: `GET /api/auth/status`. Si `status !== "open"`, **no hagas queries** — avísale al usuario que el bridge está caído o necesita re-pairing.
|
||||
|
||||
El helper `scripts/query.sh` ya maneja el bearer:
|
||||
```bash
|
||||
~/.claude/skills/whatsapp/scripts/query.sh /api/auth/status
|
||||
```
|
||||
|
||||
## Endpoints disponibles
|
||||
|
||||
Todos bajo `https://whatsapp.nucleoriofrio.com` con `Authorization: Bearer $TOKEN`.
|
||||
|
||||
### Diagnóstico
|
||||
|
||||
- `GET /healthz` — liveness, sin auth. Úsalo para verificar que el servicio responde.
|
||||
- `GET /api/auth/status` → `{ status, hasSession, user, pairingCode, lastError }`.
|
||||
Valores de `status`: `idle | connecting | open | closed | needs-pair`.
|
||||
|
||||
### Listar chats
|
||||
|
||||
- `GET /api/chats?type=<group|individual|all>&q=<texto>&limit=100&offset=0`
|
||||
- `type`: filtrar por tipo. Default `all`.
|
||||
- `q`: búsqueda parcial por nombre del chat o JID.
|
||||
- Responde: `{ count, chats: [{ jid, isGroup, name, lastMessageAt, participants }] }`.
|
||||
- **Patrón típico**: cuando el usuario menciona un grupo por nombre (ej "el grupo de productores"), primero `GET /api/chats?type=group&q=productores` para resolver el JID, después consulta mensajes con ese JID.
|
||||
|
||||
### Un grupo específico
|
||||
|
||||
- `GET /api/groups/:jid` — metadata (live + stored). El `:jid` debe ser URL-encoded (termina en `@g.us`).
|
||||
- `GET /api/groups/:jid/messages` — mensajes del grupo. Query params:
|
||||
- `since=2026-04-14T00:00:00Z` (ISO timestamp)
|
||||
- `until=2026-04-21T23:59:59Z`
|
||||
- `q=<texto>` — búsqueda parcial ILIKE en el contenido
|
||||
- `sender=<jid>` — filtrar por quién escribió (participant)
|
||||
- `order=asc|desc` (default `desc`)
|
||||
- `limit=100` (máx 1000), `offset=0`
|
||||
- Responde: `{ jid, count, filters, messages: [{ msgId, fromMe, senderJid, pushName, messageType, content, contextInfo, timestamp }] }`.
|
||||
|
||||
### Chats individuales
|
||||
|
||||
- `GET /api/messages/individual/:jid?since=&until=&q=&order=&limit=&offset=`
|
||||
- `:jid` termina en `@s.whatsapp.net`.
|
||||
- Responde mensajes de ese chat.
|
||||
|
||||
### Búsqueda cross-chat
|
||||
|
||||
- `GET /api/messages/search?q=<texto>&since=&until=&chatType=<group|individual|all>&limit=100`
|
||||
- Busca en TODO el historial. Úsalo cuando el usuario pide algo como "alguien mencionó retenciones" o "busca el precio por quintal de la semana pasada".
|
||||
- Responde: `{ query, count, matches: [{ chatJid, msgId, senderJid, pushName, isGroup, content, timestamp }] }`.
|
||||
- Para pasar de matches a contexto, obtén el `chatJid` de un match y pide `GET /api/groups/:jid/messages` con `since` unos minutos antes.
|
||||
|
||||
### Endpoints de escritura
|
||||
|
||||
- `POST /api/messages/send` — texto.
|
||||
Body: `{ jid, text, quotedChatJid?, quotedMsgId?, mentions? }`.
|
||||
`quotedMsgId` opcional para **responder** a un mensaje existente (pass quotedChatJid si es de otro chat).
|
||||
`mentions` array de JIDs para @-menciones.
|
||||
|
||||
- `POST /api/messages/forward` — reenviar un mensaje del historial a otro chat.
|
||||
Body: `{ sourceChatJid, sourceMsgId, destJid }`.
|
||||
El mensaje debe existir en nuestro storage (lo vio el socket en vivo).
|
||||
|
||||
- `POST /api/messages/react` — reaccionar con emoji.
|
||||
Body: `{ chatJid, msgId, emoji }`. Emoji vacío `""` = quitar reacción previa.
|
||||
|
||||
- `POST /api/messages/send-media` — multimedia (image/video/audio/document).
|
||||
Body: `{ jid, url, type, caption?, fileName?, mimetype?, ptt?, quotedChatJid?, quotedMsgId? }`.
|
||||
La URL debe ser HTTP(S) público — IPs privadas bloqueadas por SSRF guard. `type` es obligatorio.
|
||||
|
||||
- `POST /api/messages/send-location` — ubicación geográfica.
|
||||
Body: `{ jid, latitude, longitude, name?, address?, quotedChatJid?, quotedMsgId? }`.
|
||||
|
||||
- `POST /api/messages/send-poll` — encuesta.
|
||||
Body: `{ jid, name, values[2..12], selectableCount?, quotedChatJid?, quotedMsgId? }`.
|
||||
`selectableCount=1` (default) = single-choice; `=0` = multi-choice; `=N` = hasta N opciones.
|
||||
|
||||
- `POST /api/messages/send-contact` — tarjeta(s) de contacto vCard. Hasta 20.
|
||||
Body: `{ jid, contacts: [{ displayName, firstName?, lastName?, phone?, phoneDisplay?, email?, organization?, title?, vcard? }], quotedChatJid?, quotedMsgId? }`.
|
||||
Si `phone` está en E.164 sin `+`, WhatsApp lo detecta como contacto vinculable (tap → chatear).
|
||||
|
||||
**Eventos**: NO soportado todavía. Baileys 6.17 no tiene API estable para crear event messages. Si el user lo pide, avisale del estado actual.
|
||||
|
||||
### Descarga de archivos (image/video/audio/document/sticker)
|
||||
|
||||
Los mensajes en respuestas de `/groups/:jid/messages`, `/individual/:jid`, `/search` incluyen un campo `media` cuando contienen archivo:
|
||||
|
||||
```json
|
||||
"media": {
|
||||
"type": "image",
|
||||
"mimetype": "image/jpeg",
|
||||
"size": 123456,
|
||||
"caption": "...",
|
||||
"downloadUrl": "/api/messages/media/<encoded-chatJid>/<msgId>"
|
||||
}
|
||||
```
|
||||
|
||||
Para obtener el archivo **desencriptado**:
|
||||
|
||||
```bash
|
||||
~/.claude/skills/whatsapp/scripts/query.sh \
|
||||
"/api/messages/media/<encoded-chatJid>/<msgId>" \
|
||||
-o /tmp/wa-image.jpg
|
||||
```
|
||||
|
||||
Eso guarda el binario. `query.sh` pasa el bearer automáticamente; la ruta que copies del campo `downloadUrl` ya funciona.
|
||||
|
||||
Para solo metadata sin descargar: `GET /api/messages/media-info/:chatJid/:msgId`.
|
||||
|
||||
Todos retornan `{ ok: true, msgId: "..." }` al éxito, 4xx/5xx con `{ error: "..." }` si falla.
|
||||
Si retornan 403 significa que `ENABLE_SEND=false` — hay que cambiarlo en `stack.yml` y redeployar.
|
||||
|
||||
### Pairing (setup / recovery)
|
||||
|
||||
- `POST /api/auth/pair` → devuelve `{ code }` (válido ~60s).
|
||||
- El usuario toma su teléfono con el número, abre WhatsApp > Ajustes > Dispositivos vinculados > Vincular con número, y teclea el código.
|
||||
- `POST /api/auth/logout` con header `X-Confirm-Wipe: yes` → borra la sesión. **DESTRUCTIVO**: después hay que re-pairear. Nunca llamar esto salvo que el usuario lo pida explícitamente.
|
||||
|
||||
## Reglas de comportamiento
|
||||
|
||||
### Antes de ejecutar queries
|
||||
- Si el status no es `open`, avísale al usuario y no consumas la API en vano.
|
||||
- Si el usuario menciona un grupo por nombre, resuelve el JID primero con `/api/chats?type=group&q=…` y pídele confirmar "¿te refieres a este grupo: <subject>?".
|
||||
|
||||
### Cuándo usar qué endpoint
|
||||
- **Resumir un grupo en una ventana de tiempo** → `/api/groups/:jid/messages?since=&until=` con `order=asc` para leer en orden cronológico.
|
||||
- **Buscar menciones** (palabra clave) → `/api/messages/search?q=` primero; si hay resultados, fetch contexto con `/api/groups/:jid/messages?since=&until=` alrededor del match.
|
||||
- **Ver actividad reciente** → `/api/chats?type=group&limit=20` ordena por `lastMessageAt` desc.
|
||||
- **Perfilar un contacto** (cuánto escribió, en qué chats) → `/api/messages/search?q=&sender=…` (no implementado aún — si es necesario, usa el endpoint de grupo con `sender=` repetidamente).
|
||||
|
||||
### Cómo mostrar resultados al usuario
|
||||
- Los JIDs (`549…@g.us`) son ruido visual. Muestra `pushName` o `name` del chat, no el JID, salvo que el usuario lo pida explícitamente.
|
||||
- Agrupa por chat cuando haya resultados de múltiples chats.
|
||||
- Si la lista es larga (>30 mensajes), resume en lugar de volcar todo.
|
||||
- Incluye el `timestamp` en formato humano (ej `2026-04-21 15:30`) y `pushName` del autor.
|
||||
- Mensajes con `fromMe: true` son del número del bridge (no los confundas con respuestas de otros).
|
||||
|
||||
### Errores típicos y cómo reaccionar
|
||||
- **401 unauthorized** → el token está mal. Revisa `WA_BRIDGE_TOKEN` en `~/.claude/skills/whatsapp/.env`.
|
||||
- **503 socket not ready** → el bridge está caído o desconectado. `GET /api/auth/status` para diagnosticar. Si `status=needs-pair`, el usuario tiene que re-pairear.
|
||||
- **404 en un JID** → el chat no existe en nuestro storage. El número puede no estar en ese grupo, o el JID está mal escrito.
|
||||
|
||||
## Ejemplos de sesiones
|
||||
|
||||
### Caso: "Resume lo que se habló hoy en el grupo de operaciones"
|
||||
```bash
|
||||
# 1. Resolver el JID
|
||||
~/.claude/skills/whatsapp/scripts/query.sh '/api/chats?type=group&q=operaciones'
|
||||
# → { chats: [{ jid: "123@g.us", name: "Operaciones Rio Frio", ... }] }
|
||||
|
||||
# 2. Pedir mensajes de hoy
|
||||
TODAY=$(date -u +%Y-%m-%dT00:00:00Z)
|
||||
~/.claude/skills/whatsapp/scripts/query.sh "/api/groups/123%40g.us/messages?since=$TODAY&order=asc&limit=500"
|
||||
# → resumir manualmente en la respuesta al usuario
|
||||
```
|
||||
|
||||
### Caso: "¿Alguien mencionó el precio por quintal esta semana?"
|
||||
```bash
|
||||
WEEK_AGO=$(date -u -d '7 days ago' +%Y-%m-%dT00:00:00Z)
|
||||
~/.claude/skills/whatsapp/scripts/query.sh "/api/messages/search?q=quintal&since=$WEEK_AGO&chatType=group"
|
||||
# → presentar los matches agrupados por chat con timestamp y autor
|
||||
```
|
||||
|
||||
### Caso: "Envía un mensaje al grupo X diciendo Y"
|
||||
POST:
|
||||
```bash
|
||||
~/.claude/skills/whatsapp/scripts/query.sh -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"jid":"123@g.us","text":"..."}' /api/messages/send
|
||||
```
|
||||
Si responde 403, avisale al user que `ENABLE_SEND=false` en `stack.yml`.
|
||||
|
||||
### Caso: "Responde a ese mensaje con X"
|
||||
Pasá `quotedMsgId` (y opcional `quotedChatJid` si es de otro chat):
|
||||
```bash
|
||||
~/.claude/skills/whatsapp/scripts/query.sh -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"jid":"123@g.us","text":"claro, ya vamos","quotedMsgId":"ABCD..."}' \
|
||||
/api/messages/send
|
||||
```
|
||||
|
||||
### Caso: "Reenvía ese mensaje al grupo de productores"
|
||||
1. Obtené el JID del grupo destino (vía `/api/chats`).
|
||||
2. POST a `/api/messages/forward` con `{ sourceChatJid, sourceMsgId, destJid }`.
|
||||
|
||||
### Caso: "Reacciona con 👍 a ese mensaje"
|
||||
```bash
|
||||
~/.claude/skills/whatsapp/scripts/query.sh -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"chatJid":"123@g.us","msgId":"ABCD","emoji":"👍"}' \
|
||||
/api/messages/react
|
||||
```
|
||||
|
||||
### Caso: "Mandá esa imagen con el caption X"
|
||||
El usuario te pasa una URL pública (imgur, Gitea attachment, etc.):
|
||||
```bash
|
||||
~/.claude/skills/whatsapp/scripts/query.sh -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"jid":"123@g.us","url":"https://i.imgur.com/X.jpg","type":"image","caption":"..."}' \
|
||||
/api/messages/send-media
|
||||
```
|
||||
Si no tiene URL pública y el archivo está local, primero hay que subirlo a algún lado accesible. No tenemos (aún) endpoint de upload directo multipart.
|
||||
|
||||
## Archivos de referencia en esta skill
|
||||
- `endpoints.md` — tabla de referencia rápida de todos los endpoints y sus parámetros.
|
||||
- `contacts.md` — **LEELO** siempre que vayas a mostrar o enviar a un JID; contiene el mapping de identificadores a personas conocidas.
|
||||
- `scripts/query.sh` — helper de curl autenticado.
|
||||
- `.env` (no versionado) — contiene `WA_BRIDGE_TOKEN=<valor>`.
|
||||
27
contacts.md
Normal file
27
contacts.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Contactos conocidos — mapping JID → persona/grupo
|
||||
|
||||
Consultá este archivo **siempre** que una respuesta del bridge incluya un JID o
|
||||
LID, para mostrarle al usuario un nombre humano en lugar del identificador crudo.
|
||||
Actualizá la tabla cuando se vinculen grupos o aparezcan contactos recurrentes.
|
||||
|
||||
## Individuales
|
||||
|
||||
| JID | Nombre en WA | Es | Notas |
|
||||
|---|---|---|---|
|
||||
| `260300838305975@lid` | 📀🧮📡🌐 | **El usuario** (jodarioel87, owner de NucleOS) | Al enviarle mensajes, van a su WhatsApp personal. Default destinatario cuando el user dice "mándame X" sin especificar. |
|
||||
| `50497429865@s.whatsapp.net` | — | Número propio del bridge (self-chat) | Al vincular un dispositivo, WhatsApp crea este entry. No es un chat funcional. Ignorar en listados. |
|
||||
|
||||
## Grupos
|
||||
|
||||
(vacío — el número recién vinculado, aún no participa en grupos)
|
||||
|
||||
Cuando el bridge detecte grupos nuevos (via `groups.upsert` event), van a aparecer
|
||||
en `GET /api/chats?type=group`. Agregalos acá con una nota del propósito.
|
||||
|
||||
## Mi propia identidad (el bridge)
|
||||
|
||||
- JID: `50497429865:3@s.whatsapp.net`
|
||||
- LID: `265300851306681:3@lid`
|
||||
- Nombre: **Nucleo🌐🧠🤖**
|
||||
|
||||
Cuando veas `fromMe: true` en un mensaje, es el bridge (= yo, actuando bajo órdenes del usuario).
|
||||
304
endpoints.md
Normal file
304
endpoints.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# whatsapp-bridge — referencia rápida de endpoints
|
||||
|
||||
Base URL: `https://whatsapp.nucleoriofrio.com`
|
||||
Auth: `Authorization: Bearer $WA_BRIDGE_TOKEN` (excepto `/healthz`)
|
||||
|
||||
## Diagnóstico
|
||||
|
||||
| Método | Ruta | Respuesta |
|
||||
|---|---|---|
|
||||
| GET | `/healthz` | `{ ok: true, ts }` |
|
||||
| GET | `/api/auth/status` | `{ status, hasSession, user, pairingCode, lastError }` |
|
||||
|
||||
`status` ∈ `idle | connecting | open | closed | needs-pair`.
|
||||
|
||||
## Auth / pairing
|
||||
|
||||
| Método | Ruta | Body / Headers |
|
||||
|---|---|---|
|
||||
| POST | `/api/auth/pair` | — → `{ code, hint }` |
|
||||
| POST | `/api/auth/logout` | Header `X-Confirm-Wipe: yes` (destructivo) |
|
||||
|
||||
## Chats
|
||||
|
||||
```
|
||||
GET /api/chats
|
||||
?type=group|individual|all (default all)
|
||||
&q=<texto> (busca en name + jid, ILIKE)
|
||||
&limit=<1..500> (default 100)
|
||||
&offset=<int> (default 0)
|
||||
```
|
||||
|
||||
Respuesta:
|
||||
```json
|
||||
{
|
||||
"count": N,
|
||||
"chats": [
|
||||
{ "jid": "...@g.us", "isGroup": true, "name": "...", "lastMessageAt": "ISO", "participants": 42 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Grupo específico
|
||||
|
||||
```
|
||||
GET /api/groups/:jid (URL-encode el jid, termina en @g.us)
|
||||
```
|
||||
Respuesta: `{ jid, stored: {…}, live: GroupMetadata | null }`.
|
||||
|
||||
```
|
||||
GET /api/groups/:jid/messages
|
||||
?since=<ISO> (timestamp >=)
|
||||
&until=<ISO> (timestamp <=)
|
||||
&q=<texto> (ILIKE en content)
|
||||
&sender=<jid> (filtra por participant)
|
||||
&order=asc|desc (default desc)
|
||||
&limit=<1..1000> (default 100)
|
||||
&offset=<int>
|
||||
```
|
||||
|
||||
Respuesta:
|
||||
```json
|
||||
{
|
||||
"jid": "...@g.us",
|
||||
"count": N,
|
||||
"filters": { ... },
|
||||
"messages": [
|
||||
{
|
||||
"msgId": "...",
|
||||
"fromMe": false,
|
||||
"senderJid": "...@s.whatsapp.net",
|
||||
"pushName": "Don Fulano",
|
||||
"messageType": "extendedTextMessage",
|
||||
"content": "texto extraído",
|
||||
"contextInfo": { "quotedMsgId": "...", "mentionedJid": ["..."] } | null,
|
||||
"timestamp": "ISO",
|
||||
"media": null | {
|
||||
"type": "image"|"video"|"audio"|"document"|"sticker",
|
||||
"mimetype": "image/jpeg",
|
||||
"size": 12345,
|
||||
"fileName": "... (solo document)",
|
||||
"caption": "... (image/video/document)",
|
||||
"durationSec": 30 (video/audio),
|
||||
"ptt": true (solo audio = voice note),
|
||||
"width": 800 (image/video/sticker),
|
||||
"height": 600,
|
||||
"pages": 5 (solo document),
|
||||
"isAnimated": true (solo sticker),
|
||||
"downloadUrl": "/api/messages/media/<enc-chatJid>/<msgId>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
El campo `media` aparece en cada mensaje que contiene archivo; `downloadUrl` es la ruta relativa al endpoint que descarga + desencripta.
|
||||
|
||||
## Mensajes individuales
|
||||
|
||||
```
|
||||
GET /api/messages/individual/:jid (termina en @s.whatsapp.net)
|
||||
?since=&until=&q=&order=&limit=&offset=
|
||||
```
|
||||
|
||||
## Búsqueda cross-chat
|
||||
|
||||
```
|
||||
GET /api/messages/search
|
||||
?q=<texto> (requerido, min 1 char, ILIKE)
|
||||
&since=<ISO>
|
||||
&until=<ISO>
|
||||
&chatType=group|individual|all (default all)
|
||||
&limit=<1..500> (default 100)
|
||||
```
|
||||
|
||||
Respuesta:
|
||||
```json
|
||||
{
|
||||
"query": "quintal",
|
||||
"count": N,
|
||||
"matches": [
|
||||
{ "chatJid": "...", "msgId": "...", "senderJid": "...", "pushName": "...", "isGroup": true, "content": "...", "timestamp": "ISO" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Envío de texto (gated por ENABLE_SEND)
|
||||
|
||||
```
|
||||
POST /api/messages/send
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"jid": "...@g.us" | "...@s.whatsapp.net" | "...@lid",
|
||||
"text": "...",
|
||||
"quotedChatJid": "...", // opcional; default = mismo jid destino
|
||||
"quotedMsgId": "...", // opcional; responder a mensaje previo
|
||||
"mentions": ["jid1", ...] // opcional; notifica a esos JIDs
|
||||
}
|
||||
```
|
||||
|
||||
Respuesta: `{ ok: true, msgId: "..." }` o 403 si ENABLE_SEND=false.
|
||||
|
||||
## Reenvío (forward)
|
||||
|
||||
```
|
||||
POST /api/messages/forward
|
||||
|
||||
{
|
||||
"sourceChatJid": "...", // chat donde está el mensaje original
|
||||
"sourceMsgId": "...", // id del mensaje
|
||||
"destJid": "..." // destino
|
||||
}
|
||||
```
|
||||
|
||||
Requiere que el mensaje esté en la DB del bridge (escuchado mientras el socket estaba vivo).
|
||||
|
||||
## Reacción (emoji)
|
||||
|
||||
```
|
||||
POST /api/messages/react
|
||||
|
||||
{
|
||||
"chatJid": "...",
|
||||
"msgId": "...",
|
||||
"emoji": "👍" // vacío "" = quitar reacción
|
||||
}
|
||||
```
|
||||
|
||||
## Envío de multimedia
|
||||
|
||||
```
|
||||
POST /api/messages/send-media
|
||||
|
||||
{
|
||||
"jid": "...",
|
||||
"url": "https://...", // URL pública del archivo
|
||||
"type": "image" | "video" | "audio" | "document",
|
||||
"caption": "...", // opcional (solo image/video/document)
|
||||
"fileName": "...", // opcional (solo document)
|
||||
"mimetype": "...", // opcional (override del content-type)
|
||||
"ptt": false, // solo audio: true = voice note
|
||||
"quotedChatJid": "...", // opcional
|
||||
"quotedMsgId": "..." // opcional
|
||||
}
|
||||
```
|
||||
|
||||
Límites y guards:
|
||||
- **URL debe ser HTTP/HTTPS pública** — localhost, 127.0.0.1, 10.x, 192.168.x, 172.16-31.x son bloqueadas (SSRF protection).
|
||||
- **Tamaño máximo ~100 MB** (aunque WhatsApp limita más bajo según tipo).
|
||||
- Timeout del fetch: 60s.
|
||||
|
||||
## Envío de ubicación
|
||||
|
||||
```
|
||||
POST /api/messages/send-location
|
||||
|
||||
{
|
||||
"jid": "...",
|
||||
"latitude": 14.123,
|
||||
"longitude": -88.456,
|
||||
"name": "...", // opcional (ej: "Beneficio Rio Frio")
|
||||
"address": "...", // opcional (ej: "Santa Bárbara, Honduras")
|
||||
"quotedChatJid": "...",
|
||||
"quotedMsgId": "..."
|
||||
}
|
||||
```
|
||||
|
||||
## Envío de encuesta (poll)
|
||||
|
||||
```
|
||||
POST /api/messages/send-poll
|
||||
|
||||
{
|
||||
"jid": "...",
|
||||
"name": "Pregunta",
|
||||
"values": ["Opción A", "Opción B", "Opción C"], // 2-12 opciones
|
||||
"selectableCount": 1, // 1=single (default), 0=multi-choice, N=hasta N
|
||||
"quotedChatJid": "...",
|
||||
"quotedMsgId": "..."
|
||||
}
|
||||
```
|
||||
|
||||
## Envío de contacto(s) (vCard)
|
||||
|
||||
```
|
||||
POST /api/messages/send-contact
|
||||
|
||||
{
|
||||
"jid": "...",
|
||||
"contacts": [
|
||||
{
|
||||
"displayName": "Don Fulano",
|
||||
"firstName": "Fulano", // opcional
|
||||
"lastName": "Pérez", // opcional
|
||||
"phone": "50499999999", // opcional, E.164 sin + → vinculable a WA
|
||||
"phoneDisplay": "+504 9999-9999", // opcional, default = "+" + phone
|
||||
"email": "...", // opcional
|
||||
"organization": "...", // opcional
|
||||
"title": "...", // opcional
|
||||
"vcard": "BEGIN:VCARD..." // opcional, override total
|
||||
}
|
||||
],
|
||||
"quotedChatJid": "...",
|
||||
"quotedMsgId": "..."
|
||||
}
|
||||
```
|
||||
|
||||
Hasta 20 contactos por mensaje. Si `phone` está en formato E.164 (sin `+`), WhatsApp lo detecta y permite iniciar chat con un tap.
|
||||
|
||||
## Descarga de media (desencriptado server-side)
|
||||
|
||||
Los mensajes con media incluyen `media.downloadUrl` en las respuestas. Ese path
|
||||
apunta a un endpoint que, al llamarlo con el bearer, descarga el archivo del
|
||||
CDN de WhatsApp, lo desencripta con la `mediaKey` del mensaje, y devuelve el
|
||||
binario listo para usar.
|
||||
|
||||
```
|
||||
GET /api/messages/media/:chatJid/:msgId[?disposition=inline|attachment]
|
||||
```
|
||||
|
||||
Headers de respuesta:
|
||||
- `Content-Type`: MIME correcto (image/jpeg, video/mp4, etc.)
|
||||
- `Content-Length`: tamaño en bytes
|
||||
- `Content-Disposition`: `inline` (default) o `attachment`
|
||||
- `X-Wa-Media-Type`: `image|video|audio|document|sticker`
|
||||
|
||||
Solo metadata (sin bajar el binario):
|
||||
|
||||
```
|
||||
GET /api/messages/media-info/:chatJid/:msgId
|
||||
→ { chatJid, msgId, media: { type, mimetype, size, fileName, caption, ..., downloadUrl } }
|
||||
```
|
||||
|
||||
Errores:
|
||||
- 404 si el mensaje no existe o no tiene media
|
||||
- 503 si el socket de WhatsApp está caído
|
||||
- 500 si el CDN de WA ya no tiene el archivo (muy viejo → no se puede recuperar)
|
||||
|
||||
## Eventos (no soportado todavía)
|
||||
|
||||
Baileys 6.17 no expone una API estable para crear event messages (los puede recibir/parsear pero no enviar via `sendMessage`). Sería necesario construir `proto.Message.IEventMessage` manualmente y usar `relayMessage` low-level — todavía no implementado en el bridge. Si lo necesitás urgente, abrir issue.
|
||||
|
||||
Errores típicos: 400 con `fetch failed: ...` si la URL no responde bien o es privada.
|
||||
|
||||
## Formatos de JID
|
||||
|
||||
| JID | Tipo | Ejemplo |
|
||||
|---|---|---|
|
||||
| `<phone>@s.whatsapp.net` | chat individual | `50499999999@s.whatsapp.net` |
|
||||
| `<phone>-<timestamp>@g.us` | grupo | `50499999999-1614182736@g.us` |
|
||||
| `status@broadcast` | estados | (ignorado por el bridge) |
|
||||
|
||||
URL-encoding en path params: `@` → `%40`, `.` se deja tal cual.
|
||||
|
||||
## Códigos de error
|
||||
|
||||
| Status | Significado |
|
||||
|---|---|
|
||||
| 400 | parámetros inválidos (ver body con `.error`) |
|
||||
| 401 | bearer ausente o inválido |
|
||||
| 403 | endpoint gated (ej send cuando ENABLE_SEND=false) |
|
||||
| 404 | chat/grupo no existe en storage |
|
||||
| 500 | error interno (revisar logs del contenedor) |
|
||||
| 503 | socket no listo (status != open) |
|
||||
60
scripts/query.sh
Normal file
60
scripts/query.sh
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# Helper para llamar al whatsapp-bridge API con el bearer ya aplicado.
|
||||
#
|
||||
# Uso:
|
||||
# query.sh <path> # GET
|
||||
# query.sh -X POST -d '<json>' <path> # POST con body
|
||||
# query.sh -H 'X-Confirm-Wipe: yes' -X POST /api/auth/logout
|
||||
#
|
||||
# Requiere:
|
||||
# ~/.claude/skills/whatsapp/.env con WA_BRIDGE_TOKEN=<valor>
|
||||
# (opcional) WA_BRIDGE_URL (default https://whatsapp.nucleoriofrio.com)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ENV_FILE="${HOME}/.claude/skills/whatsapp/.env"
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "ERROR: $ENV_FILE missing. Create it with WA_BRIDGE_TOKEN=<token>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
set -a; . "$ENV_FILE"; set +a
|
||||
|
||||
if [ -z "${WA_BRIDGE_TOKEN:-}" ]; then
|
||||
echo "ERROR: WA_BRIDGE_TOKEN not set in $ENV_FILE" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
BASE="${WA_BRIDGE_URL:-https://whatsapp.nucleoriofrio.com}"
|
||||
|
||||
# El último argumento es el path; todo lo anterior son flags de curl pasados through.
|
||||
args=()
|
||||
path=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--) shift; break ;;
|
||||
-*) args+=("$1"); [ $# -ge 2 ] && args+=("$2") && shift; shift ;;
|
||||
*) path="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
# Si quedó algún argumento posicional al final (tras --)
|
||||
[ $# -gt 0 ] && path="${path:-$1}"
|
||||
|
||||
if [ -z "$path" ]; then
|
||||
echo "Usage: query.sh [curl flags] <path>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Asegura que path empiece con /
|
||||
case "$path" in
|
||||
/*) ;;
|
||||
http*) echo "ERROR: pass only the path, not a full URL" >&2; exit 1 ;;
|
||||
*) path="/$path" ;;
|
||||
esac
|
||||
|
||||
exec curl -fsS \
|
||||
-H "Authorization: Bearer ${WA_BRIDGE_TOKEN}" \
|
||||
-H "Accept: application/json" \
|
||||
"${args[@]}" \
|
||||
"${BASE}${path}"
|
||||
Reference in New Issue
Block a user