Blog

Créer une boîte de réception unifiée pour les réseaux sociaux avec des API

Découvrez comment créer une boîte de réception unifiée pour les réseaux sociaux qui regroupe les messages de Facebook, Instagram, Twitter et plus dans une seule

Par

+8

Publiez partout. Une API.

Try Free

Gérer les conversations avec les clients sur Facebook, Instagram, Twitter, Bluesky, Reddit et Telegram nécessite de passer constamment d'un contexte à un autre. Une boîte de réception unifiée pour les réseaux sociaux résout ce problème en regroupant tous les messages dans une seule interface, permettant à votre équipe de répondre plus rapidement sans avoir à naviguer entre les plateformes.

Ce guide vous accompagne dans la création d'une boîte de réception multi-plateformes à partir de zéro. Vous apprendrez à concevoir le modèle de données, à normaliser les messages provenant de différentes API, à gérer les webhooks en temps réel et à mettre en œuvre une pagination basée sur des curseurs qui fonctionne sur toutes les plateformes. À la fin, vous disposerez d'une architecture prête pour la production pour agréger des messages sociaux à grande échelle.

Le défi de la messagerie multi-plateforme

Chaque plateforme sociale dispose de sa propre API de messagerie avec des flux d'authentification uniques, des structures de données, des limites de taux et des formats de webhook spécifiques. Voici ce à quoi vous devez faire face :

Facebook et Instagram utilisez l'API de la plateforme Messenger avec des identifiants d'utilisateur spécifiques à la page. Les messages arrivent via des webhooks, et vous avez besoin de jetons d'accès de page distincts pour chaque compte connecté.

Twitter/X fournit l'API des Messages Directs, mais l'accès nécessite des niveaux d'API supérieurs. Le sondage des messages est la méthode principale, car le support des webhooks est limité.

Bluesky utilise le protocole AT avec une architecture décentralisée. Les messages directs fonctionnent différemment des plateformes traditionnelles, nécessitant des requêtes basées sur un lexique.

Reddit propose la messagerie privée via leur API, mais les limites de taux sont strictes (60 requêtes par minute pour les clients OAuth).

Telegram fournit à l'API Bot un accès par polling long ou par webhooks. Chaque bot dispose de son propre token, et les messages incluent des métadonnées riches sur les types de chat.

Les principaux défis lors de la création d'une API de boîte de réception pour les réseaux sociaux incluent :

  1. Complexité de l'authentificationChaque plateforme nécessite des flux OAuth différents, des stratégies de rafraîchissement de jetons variées et des portées de permissions spécifiques.
  2. Normalisation des donnéesUn « message » a une signification différente sur chaque plateforme.
  3. Livraison en temps réelLes formats de webhook varient énormément, et certaines plateformes ne prennent même pas en charge les webhooks.
  4. Gestion des limites de tauxAtteindre des limites sur une plateforme ne devrait pas faire tomber tout votre fil d'actualités.
  5. Incohérence de paginationCertains utilisent des curseurs, d'autres des décalages, et la pagination basée sur les horodatages fonctionne différemment selon les plateformes.

Vue d'ensemble de l'architecture pour une boîte de réception unifiée des réseaux sociaux

Une boîte de réception bien conçue pour plusieurs plateformes sépare les préoccupations en couches distinctes :

Unified Inbox Architecture

// Couches d'architecture pour un système de boîte de réception unifiée

interface InboxArchitecture {
  // Couche 1 : Adaptateurs de plateforme
  adapters: {
    facebook: FacebookAdapter;
    instagram: InstagramAdapter;
    twitter: TwitterAdapter;
    bluesky: BlueskyAdapter;
    reddit: RedditAdapter;
    telegram: TelegramAdapter;
  };
  
  // Couche 2 : Moteur de normalisation
  normalizer: {
    transformMessage: (platformMessage: unknown, platform: string) => NormalizedMessage;
    transformConversation: (platformConvo: unknown, platform: string) => NormalizedConversation;
  };
  
  // Couche 3 : Service d'agrégation
  aggregator: {
    fetchAllConversations: (accounts: SocialAccount[]) => Promise>;
    fetchAllMessages: (conversationIds: string[]) => Promise>;
  };
  
  // Couche 4 : Couche de stockage
  storage: {
    conversations: ConversationRepository;
    messages: MessageRepository;
    accounts: AccountRepository;
  };
  
  // Couche 5 : Couche API
  api: {
    getConversations: (filters: ConversationFilters) => Promise>;
    getMessages: (conversationId: string, cursor?: string) => Promise>;
    sendMessage: (conversationId: string, content: MessageContent) => Promise;
  };
}

Le modèle d'adaptateur isole la logique spécifique à chaque plateforme. Lorsque Instagram modifie son API, vous mettez à jour un adaptateur sans toucher au reste de votre code.

Plateformes prises en charge et leurs API

Toutes les plateformes ne prennent pas en charge toutes les fonctionnalités de la boîte de réception. Voici une approche basée sur la configuration pour gérer les capacités des plateformes :

```typescript
// Configuration de support de plateforme pour les fonctionnalités de la boîte de réception

export type InboxFeature = 'messages' | 'comments' | 'reviews';

export const INBOX_PLATFORMS = {
  messages: ['facebook', 'instagram', 'twitter', 'bluesky', 'reddit', 'telegram'] as const,
  comments: ['facebook', 'instagram', 'twitter', 'bluesky', 'threads', 'youtube', 'linkedin', 'reddit'] as const,
  reviews: ['facebook', 'googlebusiness'] as const,
} as const;

export type MessagesPlatform = (typeof INBOX_PLATFORMS.messages)[number];
export type CommentsPlatform = (typeof INBOX_PLATFORMS.comments)[number];
export type ReviewsPlatform = (typeof INBOX_PLATFORMS.reviews)[number];

// Vérifie si une plateforme prend en charge une fonctionnalité spécifique
export function isPlatformSupported(platform: string, feature: InboxFeature): boolean {
  return (INBOX_PLATFORMS[feature] as readonly string[]).includes(platform);
}

// Valide et retourne des messages d'erreur utiles
export function validatePlatformSupport(
  platform: string,
  feature: InboxFeature
): { valid: true } | { valid: false; error: string; supportedPlatforms: readonly string[] } {
  if (!isPlatformSupported(platform, feature)) {
    const featureLabel = feature === 'messages' ? 'messages directs' : feature;
    return {
      valid: false,
      error: `La plateforme '${platform}' ne prend pas en charge ${featureLabel}`,
      supportedPlatforms: INBOX_PLATFORMS[feature],
    };
  }
  return { valid: true };
}
```

Remarque : TikTok et Pinterest sont notablement absents des listes de messages et de commentaires. Leurs API ne permettent pas d'accéder en lecture aux messages ou commentaires des utilisateurs, ce qui les rend inadaptés à l'agrégation de la messagerie.

Voici une comparaison des capacités des API entre les différentes plateformes :

PlatformMessagesCommentsReviewsWebhooksLimites de taux
Facebook200 €/heure/utilisateur
Instagram200 €/heure/utilisateur
TwitterLimited15/15min (DMs)
Bluesky3000/5min
Reddit60/min
Telegram30/s
LinkedIn✅ (Organisations)100/jour
YouTube10000/jour

Conception du modèle de données pour les conversations et les messages

Votre modèle de données doit gérer l'union de toutes les fonctionnalités des plateformes tout en maintenant des relations claires. Voici un schéma MongoDB qui fonctionne bien :

```javascript
// Schéma de conversation - représente un fil de discussion avec un participant

import mongoose from "mongoose";

const conversationSchema = new mongoose.Schema(
  {
    userId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "User",
      required: true,
      index: true,
    },
    accountId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "SocialAccount",
      required: true,
      index: true,
    },
    profileId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "Profile",
      required: false,
    },
    platform: {
      type: String,
      enum: ["instagram", "facebook", "telegram", "twitter", "bluesky", "reddit"],
      required: true,
      index: true,
    },
    // Identifiant de conversation natif de la plateforme
    platformConversationId: {
      type: String,
      required: true,
      index: true,
    },
    status: {
      type: String,
      enum: ["active", "archived"],
      default: "active",
      index: true,
    },
    // Informations sur le participant mises en cache pour un affichage rapide
    participantName: String,
    participantUsername: String,
    participantPicture: String,
    participantId: String,
    // Données d'aperçu pour la vue de la liste de la boîte de réception
    lastMessage: String,
    lastMessageAt: {
      type: Date,
      index: true,
    },
    // Extras spécifiques à la plateforme
    metadata: {
      type: Map,
      of: mongoose.Schema.Types.Mixed,
    },
  },
  { timestamps: true }
);

// Empêcher les conversations en double par compte
conversationSchema.index(
  { accountId: 1, platformConversationId: 1 }, 
  { unique: true }
);

// Optimiser les requêtes de la liste de la boîte de réception
conversationSchema.index(
  { accountId: 1, status: 1, lastMessageAt: -1 }
);
```

Le schéma de message capture l'ensemble des types de contenu :

// Schéma de message - messages individuels au sein des conversations

const messageSchema = new mongoose.Schema(
  {
    userId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "User",
      required: true,
      index: true,
    },
    accountId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "SocialAccount",
      required: true,
      index: true,
    },
    conversationId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "Conversation",
      required: true,
      index: true,
    },
    platform: {
      type: String,
      enum: ["instagram", "facebook", "telegram", "twitter", "bluesky", "reddit"],
      required: true,
      index: true,
    },
    platformMessageId: {
      type: String,
      required: true,
      index: true,
    },
    direction: {
      type: String,
      enum: ["incoming", "outgoing"],
      required: true,
      index: true,
    },
    senderId: {
      type: String,
      required: true,
    },
    senderName: String,
    senderPicture: String,
    text: String,
    attachments: [{
      type: {
        type: String,
        enum: ["image", "video", "audio", "file", "sticker", "share"],
      },
      url: String,
      payload: mongoose.Schema.Types.Mixed,
    }],
    // Interactions avec les stories Instagram
    storyReply: {
      storyId: String,
      storyUrl: String,
    },
    isStoryMention: {
      type: Boolean,
      default: false,
    },
    platformTimestamp: {
      type: Date,
      required: true,
      index: true,
    },
    isRead: {
      type: Boolean,
      default: false,
    },
    autoResponseSent: {
      type: Boolean,
      default: false,
    },
    // Conserver les données brutes pour le débogage
    rawPayload: mongoose.Schema.Types.Mixed,
  },
  { timestamps: true }
);

// Prévenir les messages en double
messageSchema.index(
  { accountId: 1, platformMessageId: 1 }, 
  { unique: true }
);

// Récupération chronologique des messages
messageSchema.index(
  { conversationId: 1, platformTimestamp: -1

The direction le champ est essentiel. Il fait la distinction entre les messages envoyés par votre équipe (outgoing) et messages des clients (incomingCela alimente des fonctionnalités telles que le comptage des messages non lus et l'analyse des temps de réponse.

Stratégie d'agrégation et normalisation

La couche d'agrégation récupère des données de plusieurs comptes en parallèle tout en gérant les échecs de manière fluide. Voici l'implémentation principale :

```typescript
// Utilitaires d'agrégation pour la récupération de données multi-comptes

export interface AggregationError {
  accountId: string;
  accountUsername?: string;
  platform: string;
  error: string;
  code?: string;
  retryAfter?: number;
}

export interface AggregatedResult {
  items: T[];
  errors: AggregationError[];
}

export async function aggregateFromAccounts(
  accounts: SocialAccount[],
  fetcher: (account: SocialAccount) => Promise,
  options?: { timeout?: number }
): Promise> {
  const timeout = options?.timeout || 10000;
  const results: T[] = [];
  const errors: AggregationError[] = [];

  const fetchPromises = accounts.map(async (account) => {
    try {
      const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('Délai d\'attente de la requête dépassé')), timeout);
      });

      const items = await Promise.race([fetcher(account), timeoutPromise]);
      return { account, items, error: null };
    } catch (error: unknown) {
      const err = error as Error & { code?: string; retryAfter?: number };
      return {
        account,
        items: [] as T[],
        error: {
          accountId: account._id?.toString() || account.id,
          accountUsername: account.username,
          platform: account.platform,
          error: err.message || 'Erreur inconnue',
          code: err.code,
          retryAfter: err.retryAfter,
        },
      };
    }
  });

  const settledResults = await Promise.all(fetchPromises);

  for (const result of settledResults) {
    if (result.error) {
      errors.push(result.error);
    } else {
      results.push(...result.items);
    }
  }

  return { items: results, errors };
}
```

Ce modèle garantit qu'une limite de taux sur Twitter ne vous empêche pas d'afficher des messages sur Facebook. errors array permet à votre interface de présenter des résultats partiels avec des avertissements appropriés.

Pour normaliser les données spécifiques à chaque plateforme dans votre format unifié :

```javascript
// Normalisation des messages à partir de formats spécifiques aux plateformes

interface NormalizedMessage {
  platformMessageId: string;
  platform: string;
  direction: 'entrant' | 'sortant';
  senderId: string;
  senderName?: string;
  text?: string;
  attachments: Attachment[];
  platformTimestamp: Date;
}

function normalizeInstagramMessage(
  raw: InstagramMessage, 
  accountId: string
): NormalizedMessage {
  const isOutgoing = raw.from.id === accountId;
  
  return {
    platformMessageId: raw.id,
    platform: 'instagram',
    direction: isOutgoing ? 'sortant' : 'entrant',
    senderId: raw.from.id,
    senderName: raw.from.username,
    text: raw.message,
    attachments: (raw.attachments?.data || []).map(att => ({
      type: mapInstagramAttachmentType(att.type),
      url: att.url || att.payload?.url,
      payload: att.payload,
    })),
    platformTimestamp: new Date(raw.created_time),
  };
}

function normalizeTwitterMessage(
  raw: TwitterDM,
  accountId: string
): NormalizedMessage {
  const isOutgoing = raw.sender_id === accountId;
  
  return {
    platformMessageId: raw.id,
    platform: 'twitter',
    direction: isOutgoing ? 'sortant' : 'entrant',
    senderId: raw.sender_id,
    senderName: raw.sender?.name,
    text: raw.text,
    attachments: (raw.attachments?.media_keys || []).map(key => ({
      type: 'image', // Les pièces jointes des DM Twitter sont généralement des images
      url: raw.includes?.media?.find(m => m.media_key === key)?.url,
    })),
    platformTimestamp: new Date(raw.created_at),
  };
}
```

Mises à jour en temps réel avec les Webhooks

Les webhooks éliminent le besoin de polling constant. Chaque plateforme a son propre format de webhook, vous devez donc disposer de gestionnaires spécifiques à chaque plateforme qui s'intègrent dans votre pipeline de traitement unifié.

// Gestionnaire de webhook pour les messages Instagram

import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get('x-hub-signature-256');
  
  // Vérifier l'authenticité du webhook
  if (!verifyInstagramSignature(body, signature)) {
    return NextResponse.json({ error: 'Signature invalide' }, { status: 401 });
  }
  
  const payload = JSON.parse(body);
  
  // Traiter chaque entrée (peut contenir plusieurs mises à jour)
  for (const entry of payload.entry) {
    for (const messaging of entry.messaging || []) {
      await processInstagramMessage(entry.id, messaging);
    }
  }
  
  return NextResponse.json({ success: true });
}

function verifyInstagramSignature(body: string, signature: string | null): boolean {
  if (!signature || !process.env.INSTAGRAM_APP_SECRET) {
    return false;
  }
  
  const expectedSignature = 'sha256=' + crypto
    .createHmac('sha256', process.env.INSTAGRAM_APP_SECRET)
    .update(body)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

async function processInstagramMessage(
  pageId: string, 
  messaging: InstagramMessagingEvent
) {
  // Trouver le compte connecté
  const account = await SocialAccount.findOne({
    platform: 'instagram',
    platformAccountId: pageId,
    isActive: true,
  });
  
  if (!account) {
    console.warn(`Aucun compte actif trouvé pour la page Instagram ${pageId}`);
    return;
  }
  
  // Trouver ou créer une conversation
  const conversation = await findOrCreateConversation({
    accountId: account._id,
    platform: 'instagram',
    platformConversationId: messaging.sender.id, // Pour Instagram, l'ID de l'expéditeur est l'ID de la conversation
    participantId: messaging.sender.id,
  });
  
  // Normaliser et stocker le message
  const normalizedMessage = normalizeInstagramWebhookMessage(messaging, account);
  
  await Message.findOneAndUpdate(
    { 
      accountId: account._id, 
      platformMessageId: normalizedMessage.platform

Remarque : Vérifiez toujours les signatures des webhooks avant de les traiter. Sans vérification, des attaquants pourraient injecter de faux messages dans votre système.

Pour les plateformes sans support de webhook (comme Bluesky et Reddit), mettez en place un service de polling :

// Service de sondage pour les plateformes sans webhooks

class MessagePollingService {
  private intervals: Map<string, NodeJS.Timeout> = new Map();
  
  async startPolling(accountId: string, platform: string) {
    const pollInterval = this.getPollInterval(platform);
    
    const poll = async () => {
      try {
        const account = await SocialAccount.findById(accountId);
        if (!account || !account.isActive) {
          this.stopPolling(accountId);
          return;
        }
        
        const adapter = this.getAdapter(platform);
        const messages = await adapter.fetchNewMessages(account);
        
        for (const message of messages) {
          await this.processMessage(account, message);
        }
      } catch (error) {
        console.error(`Erreur de sondage pour ${accountId}:`, error);
      }
    };
    
    // Sondage initial
    await poll();
    
    // Planifier des sondages récurrents
    const interval = setInterval(poll, pollInterval);
    this.intervals.set(accountId, interval);
  }
  
  private getPollInterval(platform: string): number {
    // Respecter les limites de fréquence avec des intervalles prudents
    const intervals: Record<string, number> = {
      bluesky: 30000,  // 30 secondes
      reddit: 60000,   // 60 secondes (limites strictes)
      twitter: 60000,  // 60 secondes (accès limité aux DM)
    };
    return intervals[platform] || 30000;
  }
  
  stopPolling(accountId: string) {
    const interval = this.intervals.get(accountId);
    if (interval) {
      clearInterval(interval);
      this.intervals.delete(accountId);
    }
  }
}

Build faster with Late

One API call to post everywhere. No OAuth headaches. No platform-specific code.

Free tier • No credit card • 99.97% uptime

Pagination par curseur sur plusieurs plateformes

Lorsque vous regroupez des messages sociaux provenant de plusieurs comptes, la pagination traditionnelle par décalage devient inefficace. L'arrivée d'un nouveau message déplace tous les décalages. La pagination basée sur les curseurs résout ce problème en utilisant des identifiants stables.

Le défi : chaque plateforme utilise des formats de curseur différents. Votre API unifiée doit disposer d'un curseur qui encode suffisamment d'informations pour reprendre la pagination sur toutes les sources.

```typescript
// Pagination par curseur pour les résultats agrégés

export interface PaginationInfo {
  hasMore: boolean;
  nextCursor: string | null;
  totalCount?: number;
}

// Format du curseur : {timestamp}_{accountId}_{itemId}
// Cela permet une pagination stable même lorsque des éléments sont ajoutés

export function paginateWithCursor<T extends { id?: string; _id?: unknown }>(
  items: T[],
  cursor: string | null,
  limit: number,
  getTimestamp: (item: T) => string,
  getAccountId: (item: T) => string
): { items: T[]; pagination: PaginationInfo } {
  let filteredItems = items;

  // Appliquer le filtre de curseur si fourni
  if (cursor) {
    const [cursorTimestamp, cursorAccountId, cursorItemId] = cursor.split('_');
    const cursorTime = new Date(cursorTimestamp).getTime();

    filteredItems = items.filter((item) => {
      const itemTime = new Date(getTimestamp(item)).getTime();
      const itemAccountId = getAccountId(item);
      const itemId = item.id || String(item._id);

      // Éléments avant le curseur (pour un ordre décroissant)
      if (itemTime < cursorTime) return true;
      if (itemTime === cursorTime) {
        if (itemAccountId > cursorAccountId) return true;
        if (itemAccountId === cursorAccountId && itemId > cursorItemId) return true;
      }
      return false;
    });
  }

  // Appliquer la limite
  const paginatedItems = filteredItems.slice(0, limit);
  const hasMore = filteredItems.length > limit;

  // Générer le prochain curseur à partir du dernier élément
  let nextCursor: string | null = null;
  if (hasMore && paginatedItems.length > 0) {
    const lastItem = paginatedItems[paginatedItems.length - 1];
    const timestamp = getTimestamp(lastItem);
    const accountId = getAccountId(lastItem);
    const itemId = lastItem.id || String(lastItem._id);
    nextCursor = `${timestamp}_${accountId}_${

Utilisation dans votre point de terminaison API :

// Point de terminaison API utilisant la pagination par curseur

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const cursor = searchParams.get('cursor');
  const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 100);
  const status = searchParams.get('status') || 'active';
  
  // Récupérer les comptes de l'utilisateur
  const accounts = await getInboxAccounts(userId, 'messages');
  
  // Récupérer les conversations de tous les comptes
  const { items: conversations, errors } = await aggregateFromAccounts(
    accounts,
    async (account) => {
      return Conversation.find({
        accountId: account._id,
        status,
      })
        .sort({ lastMessageAt: -1 })
        .limit(limit * 2) // Récupérer des éléments supplémentaires pour la fusion
        .lean();
    }
  );
  
  // Trier toutes les conversations par date du dernier message
  const sorted = sortItems(
    conversations,
    'lastMessageAt',
    'desc',
    { lastMessageAt: (c) => c.lastMessageAt }
  );
  
  // Appliquer la pagination par curseur
  const { items: paginated, pagination } = paginateWithCursor(
    sorted,
    cursor,
    limit,
    (c) => c.lastMessageAt?.toISOString() || '',
    (c) => c.accountId.toString()
  );
  
  return NextResponse.json({
    conversations: paginated,
    pagination,
    meta: createAggregationMeta(accounts.length, errors),
  });
}

Gestion des fonctionnalités spécifiques à chaque plateforme

Chaque plateforme possède des fonctionnalités uniques qui ne s'intègrent pas facilement dans un modèle unifié. Instagram propose des réponses aux stories et des mentions. Twitter a des DM cités. Telegram offre des chaînes de réponses et des réactions.

La clé est de stocker les données spécifiques à chaque plateforme de manière flexible. metadata champ tout en exposant les fonctionnalités communes via votre interface normalisée :

// Gestion des réponses aux stories Instagram

interface InstagramStoryReply {
  storyId: string;
  storyUrl: string;
  storyMediaType: 'image' | 'vidéo';
  expiresAt: Date; // Les stories expirent après 24 heures
}

function normalizeInstagramWebhookMessage(
  messaging: InstagramMessagingEvent,
  account: SocialAccount
): Partial {
  const base = {
    platformMessageId: messaging.message.mid,
    platform: 'instagram',
    direction: messaging.sender.id === account.platformAccountId ? 'sortant' : 'entrant',
    senderId: messaging.sender.id,
    platformTimestamp: new Date(messaging.timestamp),
  };
  
  // Gestion des réponses aux stories
  if (messaging.message.reply_to?.story) {
    return {
      ...base,
      text: messaging.message.text,
      storyReply: {
        storyId: messaging.message.reply_to.story.id,
        storyUrl: messaging.message.reply_to.story.url,
      },
      isStoryMention: false,
    };
  }
  
  // Gestion des mentions de stories
  if (messaging.message.attachments?.[0]?.type === 'story_mention') {
    return {
      ...base,
      isStoryMention: true,
      attachments: [{
        type: 'partage',
        url: messaging.message.attachments[0].payload.url,
        payload: messaging.message.attachments[0].payload,
      }],
    };
  }
  
  // Message standard
  return {
    ...base,
    text: messaging.message.text,
    attachments: normalizeAttachments(messaging.message.attachments),
  };
}

Pour LinkedIn, un traitement spécial est nécessaire car les fonctionnalités de messagerie ne fonctionnent qu'avec des comptes d'organisation :

// Validation du compte d'organisation LinkedIn

export function isLinkedInOrgAccount(
  metadata: Map<string, unknown> | Record<string, unknown> | null | undefined
): boolean {
  if (!metadata) return false;

  // Gestion du type Map de Mongoose
  if (metadata instanceof Map) {
    return metadata.get('accountType') === 'organization' || 
           metadata.has('selectedOrganization');
  }

  // Gestion de l'objet simple
  return metadata.accountType === 'organization' || 
         'selectedOrganization' in metadata;
}

// Filtrer les comptes pour ne garder que ceux prenant en charge les fonctionnalités de boîte de réception
export function filterAccountsForFeature(
  accounts: SocialAccount[],
  feature: InboxFeature
): SocialAccount[] {
  const supportedPlatforms = INBOX_PLATFORMS[feature];

  return accounts.filter((account) => {
    if (!supportedPlatforms.includes(account.platform)) {
      return false;
    }

    // LinkedIn nécessite un type de compte d'organisation
    if (account.platform === 'linkedin' && !isLinkedInOrgAccount(account.metadata)) {
      return false;
    }

    return true;
  });
}

Dé-duplication et Fil de messages

Lors de l'agrégation de plusieurs sources, des doublons peuvent apparaître. Un message peut arriver via un webhook puis à nouveau lors d'une synchronisation par polling. Votre système doit disposer d'une déduplication robuste :

// Outils de dé-duplication

export function deduplicateItems<T>(
  items: T[],
  keyFn: (item: T) => string
): T[] {
  const seen = new Set<string>();
  return items.filter((item) => {
    const key = keyFn(item);
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

// Utilisation : dé-duplication des messages par ID de plateforme
const uniqueMessages = deduplicateItems(
  allMessages,
  (msg) => `${msg.platform}_${msg.platformMessageId}`
);

Pour la déduplication au niveau de la base de données, utilisez des opérations d'upsert avec des index uniques :

```javascript
// Modèle d'upsert pour le traitement des messages webhook

async function upsertMessage(
  accountId: string,
  normalizedMessage: NormalizedMessage
): Promise<Message> {
  return Message.findOneAndUpdate(
    { 
      accountId, 
      platformMessageId: normalizedMessage.platformMessageId 
    },
    {
      $set: normalizedMessage,
      $setOnInsert: {
        createdAt: new Date(),
      },
    },
    { 
      upsert: true, 
      new: true,
      // Retourner le document qu'il ait été inséré ou mis à jour
      rawResult: false,
    }
  );
}
```

L'index unique sur { accountId: 1, platformMessageId: 1 } garantit que MongoDB rejette les doublons véritables au niveau de la base de données, même lors du traitement simultané des webhooks.

Optimisation des performances et mise en cache

Une boîte de réception multi-plateforme effectue de nombreux appels API. Sans mise en cache, vous atteindrez les limites de taux et créerez des expériences utilisateur lentes.

// Couche de cache Redis pour les listes de conversations

import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

interface CacheOptions {
  ttl: number; // secondes
  staleWhileRevalidate?: number;
}

async function getCachedConversations(
  userId: string,
  accountIds: string[],
  options: CacheOptions = { ttl: 60, staleWhileRevalidate: 300 }
): Promise<{ conversations: Conversation[]; fromCache: boolean }> {
  const cacheKey = `inbox:${userId}:conversations:${accountIds.sort().join(',')}`;
  
  // Essayer le cache en premier
  const cached = await redis.get(cacheKey);
  if (cached) {
    const data = JSON.parse(cached);
    const age = Date.now() - data.timestamp;
    
    // Retourner les données mises en cache immédiatement
    if (age < options.ttl * 1000) {
      return { conversations: data.conversations, fromCache: true };
    }
    
    // Stale-while-revalidate : retourner des données périmées mais déclencher un rafraîchissement en arrière-plan
    if (age < (options.staleWhileRevalidate || options.ttl) * 1000) {
      // Lancer un rafraîchissement en arrière-plan
      refreshConversationsCache(userId, accountIds, cacheKey).catch(console.error);
      return { conversations: data.conversations, fromCache: true };
    }
  }
  
  // Cache manquant ou expiré : récupérer des données fraîches
  const conversations = await fetchConversationsFromDB(userId, accountIds);
  
  // Stocker dans le cache
  await redis.setex(
    cacheKey,
    options.staleWhileRevalidate || options.ttl,
    JSON.stringify({ conversations, timestamp: Date.now() })
  );
  
  return { conversations, fromCache: false };
}

async function refreshConversationsCache(
  userId: string,
  accountIds: string[],
  cacheKey: string
) {
  const conversations = await fetchConversationsFromDB(userId, accountIds);
  await redis.setex(
    cacheKey,
    300, // TTL de 5 minutes pour le rafraîchissement en arrière-plan
    JSON.stringify({ conversations, timestamp: Date.now() })
  );
}

Stratégies d'optimisation supplémentaires :

  1. Traitement par lots des webhooksMettez en file d'attente les webhooks entrants et traitez-les par lots pour réduire les allers-retours vers la base de données.
  2. Pool de connexionsRéutilisez les connexions HTTP vers les API des plateformes
  3. Synchronisation incrémentale: Suivez l'horodatage de la dernière synchronisation par compte et ne récupérez que les messages plus récents.
  4. DenormalizationStockez les informations des participants directement dans les conversations pour éviter les regroupements.

Création de l'interface frontend

Votre interface frontend doit gérer les complexités d'une boîte de réception multi-plateformes tout en offrant une interface épurée. Voici un modèle de composant React :

```javascript
// Hook React pour une boîte de réception unifiée

import { useQuery, useInfiniteQuery } from '@tanstack/react-query';

interface UseInboxOptions {
  status?: 'actif' | 'archivé';
  platform?: string;
  accountId?: string;
}

export function useInbox(options: UseInboxOptions = {}) {
  return useInfiniteQuery({
    queryKey: ['inbox', 'conversations', options],
    queryFn: async ({ pageParam }) => {
      const params = new URLSearchParams();
      if (pageParam) params.set('cursor', pageParam);
      if (options.status) params.set('status', options.status);
      if (options.platform) params.set('platform', options.platform);
      if (options.accountId) params.set('accountId', options.accountId);
      
      const response = await fetch(`/api/inbox/conversations?${params}`);
      if (!response.ok) {
        throw new Error('Échec de la récupération des conversations');
      }
      return response.json();
    },
    getNextPageParam: (lastPage) => lastPage.pagination.nextCursor,
    initialPageParam: null as string | null,
  });
}

export function useConversationMessages(conversationId: string) {
  return useInfiniteQuery({
    queryKey: ['inbox', 'messages', conversationId],
    queryFn: async ({ pageParam }) => {
      const params = new URLSearchParams();
      if (pageParam) params.set('cursor', pageParam);
      
      const response = await fetch(
        `/api/inbox/conversations/${conversationId}/messages?${params}`
      );
      if (!response.ok) {
        throw new Error('Échec de la récupération des messages');
      }
      return response.json();
    },
    getNextPageParam: (lastPage) => lastPage.pagination.nextCursor,
    initialPageParam: null as string | null,
    enabled: !!conversationId,
  });
}
```

Affichez des fonctionnalités spécifiques à chaque plateforme avec un rendu conditionnel :

```javascript
// Composant Message avec rendu spécifique à la plateforme

function MessageBubble({ message }: { message: Message }) {
  const isOutgoing = message.direction === 'outgoing';
  
  return (
    
{/* Indicateur de réponse à une story */} {message.storyReply && (
Répondu à votre story {message.storyReply.storyUrl && ( Story )}
)} {/* Indicateur de mention dans une story */} {message.isStoryMention && (
Vous a mentionné dans sa story
)} {/* Texte du message */} {message.text && (

{message.text}

)} {/* Pièces jointes */} {message.attachments?.map((attachment, i) => ( ))} {/* Métadonnées */}
); } ```

Utiliser l'API Unified Inbox de Late

Créer une boîte de réception unifiée pour les réseaux sociaux de zéro nécessite des mois de développement. Il faut maintenir les intégrations avec plusieurs plateformes, gérer leurs modifications d'API, administrer les flux OAuth et faire face aux limites de taux.

Late propose une API de boîte de réception pour les réseaux sociaux préconçue qui gère toute cette complexité. Au lieu de vous intégrer à six API de plateformes différentes, vous vous intégrez à une seule.

Voici comment Late simplifie l'intégration de la boîte de réception :

// Utilisation de l'API de boîte de réception unifiée de Late

import { LateClient } from '@late/sdk';

const late = new LateClient({
  apiKey: process.env.LATE_API_KEY,
});

// Récupérer les conversations de tous les comptes connectés
const { conversations, pagination, meta } = await late.inbox.getConversations({
  status: 'active',
  limit: 20,
});

// Obtenir les messages d'une conversation spécifique
const { messages } = await late.inbox.getMessages(conversationId, {
  limit: 50,
});

// Envoyer une réponse (Late gère le formatage spécifique à chaque plateforme)
await late.inbox.sendMessage(conversationId, {
  text: 'Merci de nous avoir contactés ! Comment puis-je vous aider ?',
});

// Filtrer par plateforme si nécessaire
const instagramOnly = await late.inbox.getConversations({
  platform: 'instagram',
});

L'API unifiée de Late propose :

  • Flux OAuth uniqueConnectez vos comptes une fois, accédez à toutes les fonctionnalités de la boîte de réception.
  • Données normaliséesFormat de message cohérent sur Facebook, Instagram, Twitter, Bluesky, Reddit et Telegram
  • Webhooks en temps réelUn point de terminaison webhook pour toutes les plateformes
  • Pagination intégréePagination basée sur le curseur qui fonctionne sur plusieurs comptes
  • Gestion des erreursDégradation gracieuse en cas de problèmes sur des plateformes individuelles
  • Gestion des limites de tauxLogique de retour automatique et de nouvelle tentative

La méta d'agrégation dans les réponses de Late vous indique exactement quels comptes ont réussi et lesquels ont rencontré des problèmes :

// La réponse de Late inclut des métadonnées d'agrégation

interface LateInboxResponse {
  conversations: Conversation[];
  pagination: {
    hasMore: boolean;
    nextCursor: string | null;
  };
  meta: {
    accountsQueried: number;
    accountsFailed: number;
    failedAccounts: Array<{
      accountId: string;
      platform: string;
      error: string;
      retryAfter?: number;
    }>;
    lastUpdated: string;
  };
}

Cette transparence vous permet de créer des interfaces utilisateur qui informent les utilisateurs lorsque des comptes spécifiques rencontrent des problèmes temporaires, sans compromettre l'expérience globale de la boîte de réception.

Découvrez La documentation de Late pour commencer avec l'API de boîte de réception unifiée. Vous pouvez avoir une boîte de réception multi-plateforme opérationnelle en quelques heures au lieu de plusieurs mois.

Une API. 13+ plateformes.

Intégrez les réseaux sociaux en minutes, pas en semaines.

Conçu pour les développeurs. Apprécié par les agences. Fiable pour 6 325 utilisateurs.