Skip to content

feat: WhatsApp Gateway — sending campaigns via OpenWA #2828

Description

@baderdean

Summary

Ajouter un WhatsApp Gateway au système Unified Sender de Leadminer, permettant d'envoyer des campagnes WhatsApp via OpenWA. Le dialogue de création de campagne sera similaire à SMS, avec une différence majeure : pas de calcul de segments SMS.

Dépendance : #2826 (WhatsApp extraction via OpenWA) — partage l'infrastructure OpenWA (sessions, docker-compose, edge function whatsapp-fleet).

Architecture

┌──────────────────────────────────────────────────────────────────┐
│                        Leadminer                                  │
│                                                                   │
│  ┌────────────────────┐    ┌────────────────────────────────┐    │
│  │  Frontend           │    │  Supabase Edge Functions       │    │
│  │                     │    │                                │    │
│  │  WhatsAppCampaign   │───▶│  whatsapp-campaigns            │    │
│  │  ComposerDialog     │    │  (création + preview)          │    │
│  │                     │    │                                │    │
│  │  WhatsAppSession    │───▶│  whatsapp-fleet                │    │
│  │  QR Dialog          │    │  (gateways + sessions OpenWA)  │    │
│  │                     │    │                                │    │
│  │  SmsFleetManagement │    │  whatsapp-campaigns-process    │    │
│  │  (extended)         │    │  (envoi batch + fleet mgmt)    │    │
│  └────────────────────┘    └──────────────┬─────────────────┘    │
│                                           │                       │
│                     Table: sms_fleet_gateways (provider='openwa') │
│                     Tables: whatsapp_campaigns / _recipients     │
└───────────────────────────────────────────┼───────────────────────┘
                                            │ HTTP
                                    ┌───────┴───────┐
                                    │    OpenWA      │
                                    │  (Docker)      │
                                    │  Sessions:     │
                                    │  - wa_123      │
                                    │  - wa_456      │
                                    └───────────────┘

Décisions clés

Décision Choix
Channel Nouveau 'whatsapp' (séparé de 'sms')
Gateway management Réutiliser SmsFleetManagement (étendre provider list)
Provider interface Implémenter SmsProvider pour OpenWA
Session QR Interface dans Leadminer (wizard + polling)
Fleet mode v1 Multi-gateway (distribution des destinataires)
OpenWA dev Ajouter au docker-compose

Phase 1 — Base de données

Migration : tables WhatsApp campaigns

-- Status enums
CREATE TYPE private.whatsapp_campaign_status AS ENUM (
  'queued', 'processing', 'completed', 'failed', 'cancelled'
);
CREATE TYPE private.whatsapp_recipient_status AS ENUM (
  'pending', 'sent', 'delivered', 'read', 'failed', 'skipped'
);

-- WhatsApp campaigns
CREATE TABLE private.whatsapp_campaigns (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  sender_name TEXT NOT NULL,
  provider TEXT NOT NULL DEFAULT 'fleet',
  message_template TEXT NOT NULL,
  footer_text_template TEXT,
  use_short_links BOOLEAN DEFAULT false,
  recipient_count INTEGER DEFAULT 0,
  sent_count INTEGER DEFAULT 0,
  delivered_count INTEGER DEFAULT 0,
  read_count INTEGER DEFAULT 0,
  failed_count INTEGER DEFAULT 0,
  click_count INTEGER DEFAULT 0,
  unsubscribe_count INTEGER DEFAULT 0,
  status private.whatsapp_campaign_status DEFAULT 'queued',
  fleet_mode_enabled BOOLEAN DEFAULT false,
  selected_gateway_ids UUID[] DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  started_at TIMESTAMPTZ,
  completed_at TIMESTAMPTZ
);

-- WhatsApp recipients
CREATE TABLE private.whatsapp_campaign_recipients (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  campaign_id UUID NOT NULL REFERENCES private.whatsapp_campaigns(id) ON DELETE CASCADE,
  phone TEXT NOT NULL,
  message TEXT NOT NULL,
  send_status private.whatsapp_recipient_status DEFAULT 'pending',
  provider_message_id TEXT,
  provider_error TEXT,
  attempt_count INTEGER DEFAULT 0,
  sent_at TIMESTAMPTZ,
  delivered_at TIMESTAMPTZ,
  read_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Recipient ↔ Gateway assignments (fleet mode)
CREATE TABLE private.whatsapp_campaign_recipient_gateways (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  campaign_id UUID NOT NULL REFERENCES private.whatsapp_campaigns(id) ON DELETE CASCADE,
  recipient_id UUID NOT NULL REFERENCES private.whatsapp_campaign_recipients(id) ON DELETE CASCADE,
  gateway_id UUID REFERENCES private.sms_fleet_gateways(id) ON DELETE SET NULL,
  gateway_name TEXT,
  gateway_provider TEXT,
  assigned_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(campaign_id, recipient_id)
);

-- Indexes + RLS (même pattern que sms_campaigns)

Migration : étendre sms_fleet_gateways provider

Pas de changement de schéma — le provider est en text, pas un enum. La valeur 'openwa' est acceptée directement.

Migration : étendre get_unified_campaigns_overview()

Ajouter le UNION ALL pour whatsapp_campaigns avec les colonnes delivered_count et read_count.

Phase 2 — Edge Functions Backend

2.1 whatsapp-fleet — Gestion gateways + sessions OpenWA

Fichier : supabase/functions/whatsapp-fleet/index.ts

Calqué sur sms-fleet/index.ts avec extensions :

  • CRUD gateways : GET/POST/PUT/DELETE /gateways (provider 'openwa')
  • Sessions OpenWA (proxy vers OpenWA API) :
    • POST /sessions/create — Crée session OpenWA, retourne QR code
    • GET /sessions/:id/qr — Récupère QR code (pour polling frontend)
    • GET /sessions/:id/status — Statut session (connecting/connected/disconnected)
    • DELETE /sessions/:id — Supprime session OpenWA

2.2 whatsapp-campaigns — Gestion campagnes

Fichier : supabase/functions/whatsapp-campaigns/index.ts

Calqué sur sms-campaigns/index.ts avec :

  • Endpoints : POST /campaigns/create, GET /campaigns/list, POST /campaigns/preview
  • Pas de calcul de segments SMS (différence clé)
  • Template avec variables {{name}}, {{email}}, etc.
  • Footer avec unsubscribe
  • Short links optionnel
  • Sélection de gateways fleet (multi-select)

2.3 Provider OpenWA

Fichier : supabase/functions/whatsapp-campaigns/providers/openwa-provider.ts

class OpenWaProvider implements SmsProvider {
  name = 'openwa';
  
  constructor(private config: { baseUrl: string; sessionId: string }) {}

  async send(params: SendSmsParams): Promise<SendSmsResult> {
    // POST /api/send-message/{sessionId}
    // Body: { chatId: params.to, text: params.body }
    const res = await fetch(
      `${this.config.baseUrl}/api/send-message/${this.config.sessionId}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ chatId: params.to, text: params.body }),
      }
    );
    if (!res.ok) return { success: false, error: await res.text() };
    const data = await res.json();
    return { success: true, messageId: data.messageId };
  }
}

Factory :

export function createWhatsAppProvider(
  type: 'openwa',
  options: { openwa: { baseUrl: string; sessionId: string } }
): SmsProvider

2.4 whatsapp-campaigns-process — Processor d'envoi

Fichier : supabase/functions/whatsapp-campaigns-process/index.ts

Calqué sur sms-campaigns-process/index.ts avec :

  • Tables whatsapp_campaigns / whatsapp_campaign_recipients
  • Fleet mode : distribution des destinataires entre gateways OpenWA
  • Création providers via config gateway (baseUrl + sessionId)
  • Gestion erreurs :
    • 404 = numéro non-WhatsApp → failed permanent
    • 429 = rate limit → retry avec backoff
    • Session expirée → marquer gateway failed
  • Statuts enrichis : sentdeliveredread (polling ou webhook v2)
  • beforeunload handler pour sauvegarder la progression partielle

Phase 3 — Frontend

3.1 Types

// types/campaign.ts
type CampaignChannel = 'email' | 'sms' | 'whatsapp';

// types/sms-fleet.ts  
type SmsGatewayProvider = 'smsgate' | 'simple-sms-gateway' | 'twilio' | 'openwa';

3.2 Nouveau composant WhatsAppCampaignComposerDialog.vue

Inspiré de SmsCampaignComposerDialog.vue :

  • Sélecteur de gateway via FleetGatewaySelector (filtré sur provider openwa)
  • Template message avec variables (textarea)
  • Pas de compteur/calcul de segments SMS
  • Limite destinataires (InputNumber)
  • Footer template avec unsubscribe
  • Short links toggle
  • Preview modal

3.3 Nouveau composant WhatsAppSessionDialog.vue

Wizard de création de session OpenWA :

  1. Saisir le nom de la session
  2. POST /whatsapp-fleet/sessions/create
  3. Affichage du QR code (image base64)
  4. Polling du statut jusqu'à connected
  5. Création automatique du gateway dans sms_fleet_gateways
  6. Redirection vers configuration gateway

3.4 Extension de SmsFleetManagement.vue

  • Ajouter 'openwa' dans la liste des providers
  • Nouveau ProviderForm pour OpenWA :
    • OpenWA Base URL (input)
    • Session ID (input, ou bouton "Connecter avec QR" → lance WhatsAppSessionDialog)
    • Read-only : phone number associé
  • FleetGatewaySelector fonctionne déjà avec n'importe quel provider

3.5 Modification de pages/campaigns.vue

  • Ajouter 'whatsapp' au type SenderFilter
  • Tab "WhatsApp" dans SenderFilterTabs
  • Affichage campagnes WhatsApp : message preview, stats sent/delivered/read/failed, badge "OpenWA"

3.6 Store sms-fleet.ts

  • Gère aussi les gateways openwa (même CRUD)
  • Pas de changement d'interface nécessaire

Phase 4 — Infrastructure

Docker Compose (dev)

services:
  openwa:
    image: openwa/openwa:latest
    container_name: leadminer-openwa
    ports:
      - "3001:3000"
    volumes:
      - openwa_sessions:/app/sessions
    environment:
      - OPENWA_PORT=3000
      - OPENWA_SESSION_NAME=leadminer

volumes:
  openwa_sessions:

Variables d'environnement

  • OPENWA_BASE_URL — URL du service OpenWA
  • OPENWA_API_KEY — Clé API OpenWA (optionnel selon config)

Métriques WhatsApp vs SMS

Métrique SMS WhatsApp
sent
delivered
read ✅ (read receipts)
failed
click ✅ (même système)
unsubscribe
segment calculation

Points de vigilance

  • QR Code session : Timeout de 60s pour le scan. Polling frontend → backend → OpenWA.
  • Session expirée : WhatsApp Web sessions expire. Détecter + proposer reconnexion.
  • Rate limits : ~1 msg/sec par session. Throttle dans le processor.
  • Format numéro : E.164 obligatoire pour WhatsApp.
  • CGU : whatsapp-web.js = non-officiel. Documenter le risque de ban.
  • Pas de SMS segments : 1 message WhatsApp = 1 message. Pas de calcul.

Répartition des fichiers

supabase/functions/
├── whatsapp-fleet/
│   └── index.ts              # CRUD gateways + sessions OpenWA
├── whatsapp-campaigns/
│   ├── index.ts              # Création campagnes WhatsApp
│   ├── providers/
│   │   ├── types.ts          # Reuse SmsProvider interface
│   │   ├── mod.ts            # createWhatsAppProvider()
│   │   └── openwa-provider.ts
│   └── utils/
│       ├── phone.ts          # Validation E.164
│       └── short-link.ts     # Share with sms-campaigns
└── whatsapp-campaigns-process/
    └── index.ts              # Envoi batch + fleet

supabase/migrations/
└── YYYYMMDDHHMMSS_add_whatsapp_campaigns.sql

frontend/src/
├── components/
│   ├── whatsapp-fleet/
│   │   └── WhatsAppSessionDialog.vue
│   └── campaigns/
│       └── WhatsAppCampaignComposerDialog.vue
├── pages/
│   └── campaigns.vue         # (modifié: + filtre WhatsApp)
├── types/
│   ├── campaign.ts           # (modifié: + whatsapp channel)
│   └── sms-fleet.ts          # (modifié: + openwa provider)
└── stores/
    └── sms-fleet.ts          # (modifié: gère aussi openwa)

Estimation

# Phase Estim.
1 Migration SQL (tables + overview) 1 j
2a whatsapp-fleet edge function (CRUD + sessions) 1.5 j
2b whatsapp-campaigns edge function 1.5 j
2c OpenWA provider + factory 0.5 j
2d whatsapp-campaigns-process (batch + fleet) 2 j
3a WhatsAppCampaignComposerDialog 1.5 j
3b WhatsAppSessionDialog (QR wizard) 1.5 j
3c Extension SmsFleetManagement + ProviderForm 1 j
3d Modification campaigns.vue + SenderFilterTabs 0.5 j
4 Docker compose dev 0.5 j
5 Tests + QA 1.5 j
Total ~13 j-dev

Dépendances

Références

  • OpenWA GitHub
  • sms-campaigns/index.ts — Pattern à suivre
  • sms-campaigns-process/index.ts — Pattern fleet mode
  • sms-fleet/index.ts — Pattern CRUD gateways
  • SmsCampaignComposerDialog.vue — Pattern composer dialog

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions