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
whatsapp — MiningType.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.ts — createWhatsAppMining()
- 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
Summary
Intégrer OpenWA comme passerelle WhatsApp dans Leadminer — extraction de contacts depuis les listes contacts WhatsApp et les membres de groupes.
Architecture
mining_sourceLeadminer ↔ 1sessionIdOpenWA — stocké dansprivate.mining_sources.credentialswhatsapp—MiningType.WHATSAPP,source_type = 'whatsapp'Phases
Phase 1 — Schéma SQL (dépend de #2824)
Note : La colonne
phone_primarysurprivate.personssera ajoutée par #2824.Phase 2 — OpenWAClient + SessionManager
backend/src/services/whatsapp-fetching/WhatsAppFetcherClient.ts— HTTP client typé vers OpenWAcreateSession,startSession,getQR,listContacts,listGroups,getGroupInfo,getChatMessagesX-API-Key, retry + rate-limitbackend/src/services/whatsapp-fetching/SessionManager.ts— cycle de vie sessionsbackend/src/services/whatsapp-fetching/normalize.ts— Contact WA → Person normalizer (sans IA)phone_primarycomme identifiant principal (grâce à feat: Support contacts without email (phone-only contacts) #2824)Phase 3 — WhatsAppFetchTask + Pipeline
backend/src/services/tasks-manager-v2/tasks/WhatsAppFetchTask.ts— calqué surGoogleContactsFetchTaskupstreamDone = true, liste contacts + groupes (si toggle actif)Person, pousse versmessagesStreambackend/src/services/tasks-manager-v2/factories.ts—createWhatsAppMining()TaskType.WhatsAppFetch,TaskId.WhatsAppFetch,MiningSource.type: 'whatsapp'Phase 3bis — PersonMatcher cross-canal
backend/src/services/extractors/whatsapp/personMatcher.tsphone_primaryE.164 normalisé (grâce à feat: Support contacts without email (phone-only contacts) #2824)emailapparaît (vCard/business profile)private.persons.phone_primarypour lookupPhase 4 — Routes Backend
/api/whatsapp/sessionswhatsapp_sessions/api/whatsapp/sessions/:id/qr/api/whatsapp/sessions/:id/status/api/whatsapp/mine/:userId/api/whatsapp/sessions/:idPhase 5 — Passive Mining
whatsappdans dispatcherPhase 6 — Frontend
6a — Refacto
SOURCE_PROVIDERSAbstraction pour éviter la duplication :
6b — Bouton + modale WA
GET /api/whatsapp/sessions/:id/qrPhase 7 — Déploiement
docker-compose.yml:Nouvelles variables :
OPENWA_BASE_URL,OPENWA_API_KEY,OPENWA_WEBHOOK_SECRETPR jumelle
leadminer.io: Kubernetes service + PVC + secret + ingress webhookDédup contacts
phone_primarycomme identifiant principal (grâce à feat: Support contacts without email (phone-only contacts) #2824)phone_primarydirectementpersonMatcherPoints de vigilance
whatsapp-web.jsnon-officiel → risque de ban. Documenter + recommander compte secondaire.sessions/d'OpenWA à monter (sinon re-scan QR à chaque restart).Estimation
~15 jours-dev (hors QA et durcissement)
Dépendances
Références