Skip to content

feat: WhatsApp Gateway — sending campaigns via OpenWA #2828

@baderdean

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
    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