Blog

Construindo uma Caixa de Entrada Unificada para Redes Sociais com APIs

Aprenda a criar uma caixa de entrada unificada para redes sociais que agrega mensagens do Facebook, Instagram, Twitter e mais em uma única interface API.

Por

+8

Poste em tudo. Uma API.

Try Free

Gerenciar conversas com clientes em Facebook, Instagram, Twitter, Bluesky, Reddit e Telegram exige uma constante troca de contexto. Uma caixa de entrada unificada para redes sociais resolve isso ao agregar todas as mensagens em uma única interface, permitindo que sua equipe responda mais rapidamente sem precisar alternar entre plataformas.

Este guia orienta na construção de uma caixa de entrada multi-plataforma do zero. Você aprenderá a projetar o modelo de dados, normalizar mensagens de diferentes APIs, lidar com webhooks em tempo real e implementar paginação baseada em cursor que funcione em várias plataformas. Ao final, você terá uma arquitetura pronta para produção para agregar mensagens sociais em grande escala.

O Desafio da Mensagem em Múltiplas Plataformas

Cada plataforma social possui sua própria API de mensagens, com fluxos de autenticação únicos, estruturas de dados, limites de taxa e formatos de webhook. Aqui está o que você encontrará:

Facebook e Instagram use a API da Plataforma Messenger com IDs de usuários específicos da página. As mensagens chegam através de webhooks, e você precisa de tokens de acesso separados para cada conta conectada.

Twitter/X oferece a API de Mensagens Diretas, mas o acesso requer níveis elevados de API. A consulta de mensagens é o método principal, uma vez que o suporte a webhooks é limitado.

Bluesky usa o Protocolo AT com uma arquitetura descentralizada. As mensagens diretas funcionam de forma diferente das plataformas tradicionais, exigindo consultas baseadas em léxico.

Reddit oferece mensagens privadas através da sua API, mas os limites de taxa são rigorosos (60 solicitações por minuto para clientes OAuth).

Telegram fornece à API do Bot opções de polling longo ou webhooks. Cada bot recebe seu próprio token, e as mensagens incluem metadados ricos sobre os tipos de chat.

Os principais desafios ao construir uma API de caixa de entrada para redes sociais incluem:

  1. Complexidade de autenticaçãoCada plataforma exige diferentes fluxos de OAuth, estratégias de atualização de tokens e escopos de permissão.
  2. Normalização de dadosUma "mensagem" tem um significado diferente em cada plataforma.
  3. Entrega em tempo realOs formatos de webhook variam bastante, e algumas plataformas não oferecem suporte a webhooks.
  4. Gestão de limites de taxaUltrapassar os limites em uma plataforma não deveria comprometer toda a sua caixa de entrada.
  5. Inconsistência na paginaçãoAlguns utilizam cursores, outros usam deslocamentos, e a paginação baseada em timestamps se comporta de maneira diferente em cada lugar.

Visão Geral da Arquitetura para uma Caixa de Entrada Unificada de Mídias Sociais

Uma caixa de entrada bem projetada para múltiplas plataformas separa as preocupações em camadas distintas:

Unified Inbox Architecture

```
// Camadas da arquitetura para um sistema de caixa de entrada unificada

interface InboxArchitecture {
  // Camada 1: Adaptadores de Plataforma
  adapters: {
    facebook: FacebookAdapter;
    instagram: InstagramAdapter;
    twitter: TwitterAdapter;
    bluesky: BlueskyAdapter;
    reddit: RedditAdapter;
    telegram: TelegramAdapter;
  };
  
  // Camada 2: Motor de Normalização
  normalizer: {
    transformMessage: (platformMessage: unknown, platform: string) => NormalizedMessage;
    transformConversation: (platformConvo: unknown, platform: string) => NormalizedConversation;
  };
  
  // Camada 3: Serviço de Agregação
  aggregator: {
    fetchAllConversations: (accounts: SocialAccount[]) => Promise<AggregatedResult<Conversation>>;
    fetchAllMessages: (conversationIds: string[]) => Promise<AggregatedResult<Message>>;
  };
  
  // Camada 4: Camada de Armazenamento
  storage: {
    conversations: ConversationRepository;
    messages: MessageRepository;
    accounts: AccountRepository;
  };
  
  // Camada 5: Camada de API
  api: {
    getConversations: (filters: ConversationFilters) => Promise<PaginatedResponse<Conversation>>;
    getMessages: (conversationId: string, cursor?: string) => Promise<PaginatedResponse<Message>>;
    sendMessage: (conversationId: string, content: MessageContent) => Promise<Message>;
  };
}
```

O padrão de adaptador isola a lógica específica da plataforma. Quando o Instagram altera sua API, você atualiza apenas um adaptador sem precisar mexer no restante do seu código.

Plataformas Suportadas e Suas APIs

Nem todas as plataformas suportam todos os recursos de caixa de entrada. Aqui está uma abordagem orientada por configurações para gerenciar as capacidades das plataformas:

```typescript
// Configuração de suporte da plataforma para recursos de caixa de entrada

export type InboxFeature = 'mensagens' | 'comentários' | 'avaliações';

export const INBOX_PLATFORMS = {
  mensagens: ['facebook', 'instagram', 'twitter', 'bluesky', 'reddit', 'telegram'] as const,
  comentários: ['facebook', 'instagram', 'twitter', 'bluesky', 'threads', 'youtube', 'linkedin', 'reddit'] as const,
  avaliações: ['facebook', 'googlebusiness'] as const,
} as const;

export type MessagesPlatform = (typeof INBOX_PLATFORMS.mensagens)[number];
export type CommentsPlatform = (typeof INBOX_PLATFORMS.comentários)[number];
export type ReviewsPlatform = (typeof INBOX_PLATFORMS.avaliações)[number];

// Verifica se uma plataforma suporta um recurso específico
export function isPlatformSupported(platform: string, feature: InboxFeature): boolean {
  return (INBOX_PLATFORMS[feature] as readonly string[]).includes(platform);
}

// Valida e retorna mensagens de erro úteis
export function validatePlatformSupport(
  platform: string,
  feature: InboxFeature
): { valid: true } | { valid: false; error: string; supportedPlatforms: readonly string[] } {
  if (!isPlatformSupported(platform, feature)) {
    const featureLabel = feature === 'mensagens' ? 'mensagens diretas' : feature;
    return {
      valid: false,
      error: `A plataforma '${platform}' não suporta ${featureLabel}`,
      supportedPlatforms: INBOX_PLATFORMS[feature],
    };
  }
  return { valid: true };
}
```

Nota: O TikTok e o Pinterest estão notavelmente ausentes das listas de mensagens e comentários. As suas APIs não oferecem acesso de leitura às mensagens ou comentários dos usuários, tornando-os inadequados para a agregação de caixas de entrada.

Aqui está uma comparação das capacidades da API entre as plataformas:

PlatformMessagesCommentsReviewsWebhooksLimites de Taxa
Facebook200/hora/usuário
Instagram200/hora/usuário
TwitterLimited15/15min (DMs)
Bluesky3000/5min
Reddit60/minuto
Telegram30/seg
LinkedIn✅ (Organizações)100/dia
YouTube10.000/dia

Design de Modelo de Dados para Conversas e Mensagens

Seu modelo de dados precisa gerenciar a união de todos os recursos das plataformas, mantendo relacionamentos claros. Aqui está um esquema MongoDB que funciona bem:

```javascript
// Esquema de conversa - representa um thread com um participante

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,
    },
    // Identificador nativo da conversa na plataforma
    platformConversationId: {
      type: String,
      required: true,
      index: true,
    },
    status: {
      type: String,
      enum: ["active", "archived"],
      default: "active",
      index: true,
    },
    // Informações do participante em cache para exibição rápida
    participantName: String,
    participantUsername: String,
    participantPicture: String,
    participantId: String,
    // Dados de pré-visualização para a lista de mensagens
    lastMessage: String,
    lastMessageAt: {
      type: Date,
      index: true,
    },
    // Extras específicos da plataforma
    metadata: {
      type: Map,
      of: mongoose.Schema.Types.Mixed,
    },
  },
  { timestamps: true }
);

// Impedir conversas duplicadas por conta
conversationSchema.index(
  { accountId: 1, platformConversationId: 1 }, 
  { unique: true }
);

// Otimizar consultas da lista de mensagens
conversationSchema.index(
  { accountId: 1, status: 1, lastMessageAt: -1 }
);
```

O esquema de mensagem abrange toda a gama de tipos de conteúdo:

// Esquema de mensagem - mensagens individuais dentro de conversas

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,
    }],
    // Interações de histórias do 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,
    },
    // Manter dados brutos para depuração
    rawPayload: mongoose.Schema.Types.Mixed,
  },
  { timestamps: true }
);

// Prevenir mensagens duplicadas
messageSchema.index(
  { accountId: 1, platformMessageId: 1 }, 
  { unique: true }
);

// Busca de mensagens em ordem cronológica
messageSchema.index(
  { conversationId: 1, platformTimestamp: -1 }
);

// Consultas de

The direction o campo é crítico. Ele distingue entre as mensagens enviadas pela sua equipe (outgoing) e mensagens de clientes (incomingIsso possibilita funcionalidades como contagem de mensagens não lidas e análises de tempo de resposta.

Estratégia de Agregação e Normalização

A camada de agregação busca dados de várias contas em paralelo, lidando com falhas de forma eficiente. Aqui está a implementação central:

```typescript
// Utilitários de agregação para busca de dados em múltiplas contas

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('Tempo de solicitação esgotado')), 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 || 'Erro desconhecido',
          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 };
}
```

Este padrão garante que um limite de taxa no Twitter não impeça que você exiba mensagens do Facebook. errors array permite que seu frontend exiba resultados parciais com os avisos apropriados.

Para normalizar dados específicos de plataformas em seu formato unificado:

```typescript
// Normalização de mensagens de formatos específicos da plataforma

interface NormalizedMessage {
  platformMessageId: string;
  platform: string;
  direction: 'incoming' | 'outgoing';
  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 ? 'outgoing' : 'incoming',
    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 ? 'outgoing' : 'incoming',
    senderId: raw.sender_id,
    senderName: raw.sender?.name,
    text: raw.text,
    attachments: (raw.attachments?.media_keys || []).map(key => ({
      type: 'image', // Os anexos de DM do Twitter são tipicamente imagens
      url: raw.includes?.media?.find(m => m.media_key === key)?.url,
    })),
    platformTimestamp: new Date(raw.created_at),
  };
}
```

Atualizações em tempo real com Webhooks

Os webhooks eliminam a necessidade de polling constante. Cada plataforma possui seu próprio formato de webhook, portanto, você precisa de manipuladores específicos para cada plataforma que se integrem ao seu pipeline de processamento unificado.

// Manipulador de webhook para mensagens do 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');
  
  // Verificar a autenticidade do webhook
  if (!verifyInstagramSignature(body, signature)) {
    return NextResponse.json({ error: 'Assinatura inválida' }, { status: 401 });
  }
  
  const payload = JSON.parse(body);
  
  // Processar cada entrada (pode conter várias atualizações)
  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
) {
  // Encontrar a conta conectada
  const account = await SocialAccount.findOne({
    platform: 'instagram',
    platformAccountId: pageId,
    isActive: true,
  });
  
  if (!account) {
    console.warn(`Nenhuma conta ativa encontrada para a página do Instagram ${pageId}`);
    return;
  }
  
  // Encontrar ou criar conversa
  const conversation = await findOrCreateConversation({
    accountId: account._id,
    platform: 'instagram',
    platformConversationId: messaging.sender.id, // Para o Instagram, o ID do remetente é o ID da conversa
    participantId: messaging.sender.id,
  });
  
  // Normalizar e armazenar a mensagem
  const normalizedMessage = normalizeInstagramWebhookMessage(messaging, account);
  
  await Message.findOneAndUpdate(
    { 
      accountId: account._id, 
      platformMessageId: normalizedMessage.platformMessageId

Nota: Verifique sempre as assinaturas dos webhooks antes de processá-los. Sem essa verificação, atacantes podem injetar mensagens falsas em seu sistema.

Para plataformas sem suporte a webhook (como Bluesky e Reddit), implemente um serviço de polling:

// Serviço de polling para plataformas sem webhooks

class MessagePollingService {
  private intervals: Map = 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(`Erro de polling para ${accountId}:`, error);
      }
    };
    
    // Polling inicial
    await poll();
    
    // Agendar polls recorrentes
    const interval = setInterval(poll, pollInterval);
    this.intervals.set(accountId, interval);
  }
  
  private getPollInterval(platform: string): number {
    // Respeitar limites de taxa com intervalos conservadores
    const intervals: Record = {
      bluesky: 30000,  // 30 segundos
      reddit: 60000,   // 60 segundos (limites de taxa rigorosos)
      twitter: 60000,  // 60 segundos (acesso limitado a DMs)
    };
    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

Paginação Baseada em Cursor em Múltiplas Plataformas

Quando você agrega mensagens sociais de várias contas, a paginação tradicional por offset falha. A chegada de uma nova mensagem desloca todos os offsets. A paginação baseada em cursor resolve isso utilizando identificadores estáveis.

O desafio: cada plataforma utiliza formatos de cursor diferentes. A sua API unificada precisa de um cursor que codifique informações suficientes para retomar a paginação em todas as fontes.

```typescript
// Paginação baseada em cursor para resultados agregados

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

// Formato do cursor: {timestamp}_{accountId}_{itemId}
// Isso permite uma paginação estável mesmo quando itens são adicionados

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;

  // Aplica o filtro de cursor se fornecido
  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);

      // Itens antes do cursor (para ordem decrescente)
      if (itemTime < cursorTime) return true;
      if (itemTime === cursorTime) {
        if (itemAccountId > cursorAccountId) return true;
        if (itemAccountId === cursorAccountId && itemId > cursorItemId) return true;
      }
      return false;
    });
  }

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

  // Gera o próximo cursor a partir do último item
  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}_${itemId}`;

Uso no seu endpoint de API:

// Endpoint da API utilizando paginação por cursor

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';
  
  // Obter contas do usuário
  const accounts = await getInboxAccounts(userId, 'messages');
  
  // Buscar conversas de todas as contas
  const { items: conversations, errors } = await aggregateFromAccounts(
    accounts,
    async (account) => {
      return Conversation.find({
        accountId: account._id,
        status,
      })
        .sort({ lastMessageAt: -1 })
        .limit(limit * 2) // Buscar extras para mesclar
        .lean();
    }
  );
  
  // Ordenar todas as conversas pelo horário da última mensagem
  const sorted = sortItems(
    conversations,
    'lastMessageAt',
    'desc',
    { lastMessageAt: (c) => c.lastMessageAt }
  );
  
  // Aplicar paginação por cursor
  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),
  });
}

Gerenciando Funcionalidades Específicas da Plataforma

Cada plataforma possui características únicas que não se encaixam perfeitamente em um modelo unificado. O Instagram tem respostas a stories e menções. O Twitter possui DMs citadas. O Telegram conta com cadeias de respostas e reações.

A chave é armazenar dados específicos da plataforma de forma flexível. metadata campo enquanto expõe recursos comuns através da sua interface normalizada:

```typescript
// Tratamento de respostas a histórias do Instagram

interface InstagramStoryReply {
  storyId: string;
  storyUrl: string;
  storyMediaType: 'imagem' | 'vídeo';
  expiresAt: Date; // As histórias expiram após 24 horas
}

function normalizeInstagramWebhookMessage(
  messaging: InstagramMessagingEvent,
  account: SocialAccount
): Partial {
  const base = {
    platformMessageId: messaging.message.mid,
    platform: 'instagram',
    direction: messaging.sender.id === account.platformAccountId ? 'saída' : 'entrada',
    senderId: messaging.sender.id,
    platformTimestamp: new Date(messaging.timestamp),
  };
  
  // Tratamento de respostas a histórias
  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,
    };
  }
  
  // Tratamento de menções a histórias
  if (messaging.message.attachments?.[0]?.type === 'story_mention') {
    return {
      ...base,
      isStoryMention: true,
      attachments: [{
        type: 'compartilhar',
        url: messaging.message.attachments[0].payload.url,
        payload: messaging.message.attachments[0].payload,
      }],
    };
  }
  
  // Mensagem padrão
  return {
    ...base,
    text: messaging.message.text,
    attachments: normalizeAttachments(messaging.message.attachments),
  };
}
```

Para o LinkedIn, é necessário um tratamento especial, pois os recursos de inbox funcionam apenas com contas de organização:

```javascript
// Validação de conta organizacional do LinkedIn

export function isLinkedInOrgAccount(
  metadata: Map | Record | null | undefined
): boolean {
  if (!metadata) return false;

  // Lidar com o tipo Mongoose Map
  if (metadata instanceof Map) {
    return metadata.get('accountType') === 'organization' || 
           metadata.has('selectedOrganization');
  }

  // Lidar com objeto simples
  return metadata.accountType === 'organization' || 
         'selectedOrganization' in metadata;
}

// Filtrar contas para apenas aquelas que suportam recursos de caixa de entrada
export function filterAccountsForFeature(
  accounts: SocialAccount[],
  feature: InboxFeature
): SocialAccount[] {
  const supportedPlatforms = INBOX_PLATFORMS[feature];

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

    // O LinkedIn requer tipo de conta organizacional
    if (account.platform === 'linkedin' && !isLinkedInOrgAccount(account.metadata)) {
      return false;
    }

    return true;
  });
}
```

Desduplicação e Agrupamento de Mensagens

Ao agregar de várias fontes, podem surgir duplicatas. Uma mensagem pode chegar via webhook e depois novamente durante uma sincronização de polling. Seu sistema precisa de uma deduplicação robusta:

```javascript
// Utilitários de deduplicação

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

// Uso: deduplicar mensagens por ID de plataforma
const uniqueMessages = deduplicateItems(
  allMessages,
  (msg) => `${msg.platform}_${msg.platformMessageId}`
);
```

Para deduplicação a nível de base de dados, utilize operações upsert com índices únicos:

```javascript
// Padrão Upsert para processamento de mensagens de 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,
      // Retornar o documento, seja ele inserido ou atualizado
      rawResult: false,
    }
  );
}
```

O índice único em { accountId: 1, platformMessageId: 1 } garante que o MongoDB rejeite duplicatas verdadeiras no nível do banco de dados, mesmo durante o processamento simultâneo de webhooks.

Otimização de Desempenho e Cache

Uma caixa de entrada multiplataforma faz várias chamadas de API. Sem cache, você atingirá os limites de taxa e criará experiências lentas para os usuários.

// Camada de cache Redis para listas de conversas

import Redis from 'ioredis';

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

interface CacheOptions {
  ttl: number; // segundos
  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(',')}`;
  
  // Tenta o cache primeiro
  const cached = await redis.get(cacheKey);
  if (cached) {
    const data = JSON.parse(cached);
    const age = Date.now() - data.timestamp;
    
    // Retorna os dados do cache imediatamente
    if (age < options.ttl * 1000) {
      return { conversations: data.conversations, fromCache: true };
    }
    
    // Stale-while-revalidate: retorna dados desatualizados mas aciona a atualização em segundo plano
    if (age < (options.staleWhileRevalidate || options.ttl) * 1000) {
      // Atualização em segundo plano
      refreshConversationsCache(userId, accountIds, cacheKey).catch(console.error);
      return { conversations: data.conversations, fromCache: true };
    }
  }
  
  // Cache não encontrado ou expirado: busca dados novos
  const conversations = await fetchConversationsFromDB(userId, accountIds);
  
  // Armazena no 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, // 5 minutos de TTL para atualização em segundo plano
    JSON.stringify({ conversations, timestamp: Date.now() })
  );
}

Estratégias adicionais de otimização:

  1. Processamento de webhooks em loteColoque os webhooks recebidos em fila e processe em lotes para reduzir as idas e vindas ao banco de dados.
  2. Pooling de conexõesReutilize conexões HTTP para APIs da plataforma
  3. Sincronização incremental: Registe a última marcação de sincronização por conta e busque apenas as mensagens mais recentes.
  4. DenormalizationArmazene informações dos participantes diretamente nas conversas para evitar junções.

Construindo a Interface Frontend

Seu frontend precisa lidar com as complexidades de uma caixa de entrada multi-plataforma, enquanto apresenta uma interface limpa. Aqui está um padrão de componente React:

```javascript
// Hook React para caixa de entrada unificada

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

interface UseInboxOptions {
  status?: 'ativo' | 'arquivado';
  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('Falha ao buscar conversas');
      }
      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('Falha ao buscar mensagens');
      }
      return response.json();
    },
    getNextPageParam: (lastPage) => lastPage.pagination.nextCursor,
    initialPageParam: null as string | null,
    enabled: !!conversationId,
  });
}
```

Exiba recursos específicos da plataforma com renderização condicional:

```javascript
// Componente de mensagem com renderização específica para cada plataforma

function MessageBubble({ message }: { message: Message }) {
  const isOutgoing = message.direction === 'outgoing';
  
  return (
    
{/* Indicador de resposta a história */} {message.storyReply && (
Respondeu à sua história {message.storyReply.storyUrl && ( História )}
)} {/* Indicador de menção em história */} {message.isStoryMention && (
Mencionou você em sua história
)} {/* Texto da mensagem */} {message.text && (

{message.text}

)} {/* Anexos */} {message.attachments?.map((attachment, i) => ( ))} {/* Metadados */}
); } ```

Utilizando a API da Caixa de Entrada Unificada do Late

Construir uma caixa de entrada unificada para redes sociais do zero exige meses de desenvolvimento. É necessário manter integrações com várias plataformas, lidar com as mudanças em suas APIs, gerenciar fluxos de OAuth e enfrentar limites de taxa.

Late oferece uma API de caixa de entrada de redes sociais pré-construída que lida com toda essa complexidade. Em vez de integrar com seis APIs de plataformas diferentes, você integra com uma só.

Veja como o Late simplifica a integração da caixa de entrada:

```javascript
// Usando a API de caixa de entrada unificada do Late

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

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

// Buscar conversas de todas as contas conectadas
const { conversations, pagination, meta } = await late.inbox.getConversations({
  status: 'active',
  limit: 20,
});

// Obter mensagens de uma conversa específica
const { messages } = await late.inbox.getMessages(conversationId, {
  limit: 50,
});

// Enviar uma resposta (o Late cuida da formatação específica da plataforma)
await late.inbox.sendMessage(conversationId, {
  text: 'Obrigado por entrar em contato! Como posso ajudar?',
});

// Filtrar por plataforma, se necessário
const instagramOnly = await late.inbox.getConversations({
  platform: 'instagram',
});
```

A API unificada da Late oferece:

  • Fluxo OAuth únicoConecte suas contas uma vez e acesse todos os recursos de caixa de entrada.
  • Dados normalizadosFormato de mensagem consistente em Facebook, Instagram, Twitter, Bluesky, Reddit e Telegram
  • Webhooks em tempo realUm endpoint de webhook para todas as plataformas
  • Paginação integradaPaginação baseada em cursor que funciona em várias contas
  • Tratamento de errosDegradação elegante quando plataformas individuais apresentam problemas
  • Gestão de limites de taxaLógica automática de retrocesso e nova tentativa

A meta de agregação nas respostas do Late informa exatamente quais contas tiveram sucesso e quais enfrentaram problemas:

// A resposta do Late inclui metadados de agregação

interface LateInboxResponse {
  conversas: Conversation[];
  paginação: {
    temMais: boolean;
    próximoCursor: string | null;
  };
  meta: {
    contasConsultadas: number;
    contasComFalha: number;
    contasFalhadas: Array<{
      idConta: string;
      plataforma: string;
      erro: string;
      tentarNovamenteApos?: number;
    }>;
    ultimaAtualização: string;
  };
}

Essa transparência permite que você crie interfaces que informam os usuários quando contas específicas enfrentam problemas temporários, sem comprometer toda a experiência da caixa de entrada.

Confira Documentação do Late para começar a usar a API de caixa de entrada unificada. Você pode ter uma caixa de entrada multi-plataforma funcionando em poucas horas, em vez de meses.

Uma API. 13+ plataformas.

Integre redes sociais em minutos, não semanas.

Criado para desenvolvedores. Adorado por agências. Confiado por 6.325 usuários.