Envio de alertas Telegram desde el servicio (docs/legacy-telegram.md): por alerta, texto Markdown (content + firelink i18n) + ubicacion, como node-red. - types: canal 'telegram', notif.type 'telegram', campo telegramNotified - telegram/message.ts: buildTelegramMessages (firelink es/en/gl + location) - telegram/client.ts: cliente Bot API (fetch), interpretResponse (429 retry_after, 403 blocked) - telegram/rate.ts: throttle por chat (1 msg/s) + computeWaitMs puro - handlers.processTelegramJob: idempotencia (notification_sends canal telegram), modos dry-run/shadow/canary/full, throttle, 429->retry, bloqueo->sub inactiva - poller/queue/index: canal telegram integrado; worker con rate limiter global BullMQ (25/s por bot); cliente por idioma (es/gl->ES, en->EN) - config: bloque telegram (tokens es/en, server, globalPerSec, perChatMinMs) 85 tests en verde, typecheck y build OK. Sin TELEGRAM_TOKEN_* configurados el worker no se activa (todo inerte). Falta: extraer tokens de bot de flows_cred + rollout con el usuario. Los flujos conversacionales de los bots siguen en node-red.
82 lines
3.9 KiB
Markdown
82 lines
3.9 KiB
Markdown
# tcef-notifications
|
||
|
||
Microservicio de **envío de notificaciones** para *Todos contra el Fuego*
|
||
(alertas tempranas de incendios NASA FIRMS). Sustituye el envío de push que
|
||
hacía la web Meteor con la API legacy de GCM (apagada por Google en jun-2024) y
|
||
saca el envío del observer de Meteor a una cola controlada.
|
||
|
||
Fases **1a** (workers de envío) y **1b** (matching geoespacial) del
|
||
[plan de modernización](../plan-modernizacion/README.md). La 1a consume la
|
||
colección `notifications`; la 1b la **genera** (fuego → subs 2dsphere),
|
||
sustituyendo el matching O(fuegos×subs) de node-red. Telegram (1c) viene después.
|
||
|
||
## Qué hace
|
||
|
||
- **poller**: cada N s busca docs pendientes en `notifications` (con los nombres
|
||
de campo **correctos** — el cron viejo tenía un typo, ver
|
||
[`docs/legacy-behavior.md`](docs/legacy-behavior.md) §2) y encola un job por
|
||
canal.
|
||
- **fcm-worker**: envía push con `firebase-admin` (FCM HTTP v1), preservando el
|
||
payload que espera la app Flutter (`FLUTTER_NOTIFICATION_CLICK`, collapseKey =
|
||
`_id`, data). Purga tokens muertos sin reintentar.
|
||
- **email-worker**: `nodemailer` + las plantillas `new-fire` portadas verbatim.
|
||
- **idempotencia persistente**: colección `notification_sends`, índice único
|
||
`(notificationId, channel)` — sobrevive reinicios y replays de cola.
|
||
|
||
## Salvaguardas anti-spam
|
||
|
||
El riesgo nº1 es spamear a los usuarios. Por eso:
|
||
|
||
- **Modos graduales** `MODE`: `dry-run` → `shadow` → `canary` → `full`. Nunca se
|
||
salta un modo; `full` solo con confirmación explícita del usuario.
|
||
- **Exclusión mutua por canal**: `OWNED_CHANNELS` aquí + `notifDisabledChannels`
|
||
en Meteor. El viejo y el nuevo jamás envían por el mismo canal a la vez.
|
||
- **Kill switch**: `KILL_SWITCH=1` detiene todo envío al instante.
|
||
- **Límites**: máx envíos/usuario/día y circuit breaker por batch (para y
|
||
alerta).
|
||
|
||
Detalle del despliegue y la secuencia de cutover: [`docs/rollout.md`](docs/rollout.md).
|
||
|
||
## Desarrollo
|
||
|
||
```bash
|
||
npm ci
|
||
npm test # 47 tests (vitest + fake-repo en memoria, sin infra externa)
|
||
npm run typecheck
|
||
npm run build # -> dist/ (+ plantillas)
|
||
```
|
||
|
||
> **Nota MongoDB**: producción corre **MongoDB 3.2** (EOL), así que el servicio
|
||
> usa el driver `mongodb` **v3.7** (el moderno no conecta a 3.2) y no hay change
|
||
> streams (fase 1b usará polling). Los tests van contra un fake en memoria
|
||
> porque no hay mongod tan antiguo ejecutable en hosts modernos. Detalle en
|
||
> [`docs/legacy-behavior.md`](docs/legacy-behavior.md) §0.
|
||
|
||
## Ejecución
|
||
|
||
Config por env (ver [`.env.example`](.env.example)) o `config.json` (ver
|
||
`config.example.json`); env gana. Secretos (Mongo, `MAIL_URL`, service account
|
||
de Firebase) desde el repo privado `tcef-private-config` — nunca en este repo.
|
||
|
||
```bash
|
||
MODE=dry-run OWNED_CHANNELS=fcm MONGO_URL=... npm start
|
||
```
|
||
|
||
Despliegue en shiva: `deploy/tcef-notifications.service` (systemd) o
|
||
`deploy/pm2-tcef-notifications.json`. Node 22 standalone (no el Node 8 del
|
||
sistema) + Redis local. En fase 3 pasa a Docker Compose.
|
||
|
||
## Estado
|
||
|
||
- [x] **1a**: workers FCM v1 + email, poller, idempotencia, salvaguardas, tests.
|
||
- [x] **1a**: flag de cutover en Meteor (`notifDisabledChannels`).
|
||
- [x] **1a**: canary FCM verificado (push real entregada a dispositivos propios).
|
||
- [x] **1b**: matcher geoespacial (content i18n, geolib, `sealed` vía Iron —
|
||
compatible con la web —, dedupe 500 m), ingesta por polling, modos
|
||
`shadow`/`full`, comparador de shadow. Tests en verde.
|
||
- [x] **1c**: telegram-worker (texto + ubicación, firelink, rate limiting
|
||
global 25/s + 1/s por chat, 429/bloqueo, idempotencia). Tests en verde.
|
||
**Código migrado; sin tokens configurados → inerte.**
|
||
- [ ] **1a full**: envío masivo FCM (~8176 users) — con el usuario, vigilando.
|
||
- [ ] **1b**: rollout `shadow` ≥3 días → comparar → cutover node-red → `full`.
|
||
- [ ] **1c**: extraer tokens de bot → rollout `shadow`/canary → cutover node-red.
|