Skip to content

feat: WhatsApp integration via OpenWA gateway #2826

@baderdean

Description

@baderdean

Summary

Intégrer OpenWA comme passerelle WhatsApp dans Leadminer — extraction de contacts depuis les listes contacts WhatsApp et les membres de groupes.

⚠️ Dépendance bloquante : #2824 (Support contacts without email / phone-only contacts) doit être résolu avant de commencer cette feature.

Architecture

  • OpenWA = service externe déployé à côté du stack (Docker), multi-sessions (1 session = 1 compte WhatsApp Leadminer)
  • 1 mining_source Leadminer ↔ 1 sessionId OpenWA — stocké dans private.mining_sources.credentials
  • Nouveau type whatsappMiningType.WHATSAPP, source_type = 'whatsapp'
  • Pull uniquement (comme l'email) — cron pg_cron existant réutilisé, pas de webhook push en v1
  • UX identique à email — l'utilisateur scanne le QR, puis clique "Extraire les contacts"
  • Même message de consentement que l'email (les CC non-contacts sont aussi scannés en email)
  • Toggle "Extraire les contacts des groupes" activé par défaut

Phases

Phase 1 — Schéma SQL (dépend de #2824)

-- Table sessions WhatsApp
CREATE TABLE private.whatsapp_sessions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  mining_source_email TEXT NOT NULL,
  openwa_session_id TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'DRAFT'
    CHECK (status IN ('DRAFT','SCAN_QR','CONNECTING','CONNECTED','DISCONNECTED','ERROR','ARCHIVED')),
  phone_number TEXT,
  last_qr_at TIMESTAMPTZ,
  connected_at TIMESTAMPTZ,
  daily_send_count INT DEFAULT 0,
  last_send_reset_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(user_id, openwa_session_id)
);

-- Nouveau task type
ALTER TYPE private.task_type_enum ADD VALUE 'whatsapp-fetch';

Note : La colonne phone_primary sur private.persons sera ajoutée par #2824.

Phase 2 — OpenWAClient + SessionManager

  • backend/src/services/whatsapp-fetching/WhatsAppFetcherClient.ts — HTTP client typé vers OpenWA
    • createSession, startSession, getQR, listContacts, listGroups, getGroupInfo, getChatMessages
    • Intercepteur X-API-Key, retry + rate-limit
  • backend/src/services/whatsapp-fetching/SessionManager.ts — cycle de vie sessions
  • backend/src/services/whatsapp-fetching/normalize.ts — Contact WA → Person normalizer (sans IA)

Phase 3 — WhatsAppFetchTask + Pipeline

  • backend/src/services/tasks-manager-v2/tasks/WhatsAppFetchTask.ts — calqué sur GoogleContactsFetchTask
    • upstreamDone = true, liste contacts + groupes (si toggle actif)
    • Normalise en Person, pousse vers messagesStream
  • backend/src/services/tasks-manager-v2/factories.tscreateWhatsAppMining()
  • Nouveaux types : TaskType.WhatsAppFetch, TaskId.WhatsAppFetch, MiningSource.type: 'whatsapp'

Phase 3bis — PersonMatcher cross-canal

Phase 4 — Routes Backend

Méthode Route Description
POST /api/whatsapp/sessions Créer session OpenWA, persister dans whatsapp_sessions
GET /api/whatsapp/sessions/:id/qr SSE/polling QR code image base64
GET /api/whatsapp/sessions/:id/status Statut session
POST /api/whatsapp/mine/:userId Déclencher mining WhatsApp
DELETE /api/whatsapp/sessions/:id Logout + cleanup

Phase 5 — Passive Mining

  • PR séparée : fix bug latent dispatcher passive-mining (google/azure/imap)
  • Puis ajouter branche whatsapp dans dispatcher
  • Toggle "Extraire les contacts des groupes" (activé par défaut)

Phase 6 — Frontend

6a — Refacto SOURCE_PROVIDERS

Abstraction pour éviter la duplication :

const SOURCE_PROVIDERS = [
  { type: 'google',   icon: 'pi pi-google',    addFn: addGoogleSource,   reconnectFn: ... },
  { type: 'azure',    icon: 'pi pi-microsoft', addFn: addAzureSource,    reconnectFn: ... },
  { type: 'imap',     icon: 'pi pi-inbox',     addFn: openImapDialog,    reconnectFn: ... },
  { type: 'whatsapp', icon: 'pi pi-whatsapp',  addFn: openWhatsAppQR,    reconnectFn: ... },
];

6b — Bouton + modale WA

  • Bouton WhatsApp dans dialog "Ajouter une source"
  • QR Modal avec polling GET /api/whatsapp/sessions/:id/qr
  • Bouton "Extraire les contacts" post-connexion
  • Icône WhatsApp dans liste sources
  • i18n popin passive mining

Phase 7 — Déploiement

docker-compose.yml :

openwa:
  image: ghcr.io/rmyndharis/openwa:latest
  environment:
    API_KEY: ${OPENWA_API_KEY}
    DATABASE_URL: postgresql://openwa:${OPENWA_DB_PASS}@postgres:5432/openwa
    WEBHOOK_BASE_URL: http://backend:8081/api/whatsapp/webhook
  volumes:
    - openwa-sessions:/data
  depends_on:
    - postgres

Nouvelles variables : OPENWA_BASE_URL, OPENWA_API_KEY, OPENWA_WEBHOOK_SECRET

PR jumelle leadminer.io : Kubernetes service + PVC + secret + ingress webhook

Dédup contacts

Points de vigilance

  • CGU WhatsApp : whatsapp-web.js non-officiel → risque de ban. Documenter + recommander compte secondaire.
  • RGPD : numéros = PII. Consentement explicite + purge possible.
  • Persistance : volume sessions/ d'OpenWA à monter (sinon re-scan QR à chaque restart).
  • Rate limits : WhatsApp bannit les bots → pagination lente + jitter.

Estimation

~15 jours-dev (hors QA et durcissement)

# Phase Estim.
1 Migration SQL 2 j
2 OpenWAClient + SessionManager 1.5 j
3 Fetcher + extracteurs 2 j
3bis PersonMatcher cross-canal 0.5 j
4 Factory pipeline 1.5 j
5 Routes WA 1.5 j
6a Fix dispatcher passive-mining (PR séparée) 0.5 j
6b Branche whatsapp + toggle groupes 0.5 j
7a Refacto SOURCE_PROVIDERS 0.5 j
7b Bouton + modale WA + i18n 1.5 j
8 Docker compose dev 0.5 j
9 PR jumelle leadminer.io 1.5 j
10 Tests E2E + runbook 1 j

Dépendances

Références

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions