Blog

Eine einheitliche Social-Media-Posteingang mit APIs erstellen

Erfahren Sie, wie Sie ein einheitliches Social-Media-Postfach erstellen, das Nachrichten von Facebook, Instagram, Twitter und mehr in einer einzigen API-Oberflä

Von

+8

Überall posten. Eine API.

Try Free

Die Verwaltung von Kundenkonversationen über Facebook, Instagram, Twitter, Bluesky, Reddit und Telegram erfordert ständiges Wechseln zwischen den Kontexten. Ein einheitliches Social-Media-Postfach löst dieses Problem, indem es alle Nachrichten in einer einzigen Benutzeroberfläche zusammenführt. So kann Ihr Team schneller reagieren, ohne zwischen den Plattformen hin und her springen zu müssen.

Dieser Leitfaden zeigt, wie man ein plattformübergreifendes Postfach von Grund auf neu erstellt. Sie lernen, wie Sie das Datenmodell entwerfen, Nachrichten aus verschiedenen APIs normalisieren, Echtzeit-Webhooks verarbeiten und eine plattformübergreifende, cursor-basierte Paginierung implementieren. Am Ende verfügen Sie über eine produktionsbereite Architektur zur Aggregation sozialer Nachrichten in großem Maßstab.

Die Herausforderung der plattformübergreifenden Kommunikation

Jede soziale Plattform verfügt über ihre eigene Messaging-API mit einzigartigen Authentifizierungsabläufen, Datenstrukturen, Ratenlimits und Webhook-Formaten. Das sind die Herausforderungen, mit denen Sie konfrontiert sind:

Facebook und Instagram Verwenden Sie die Messenger Platform API mit seitenbezogenen Benutzer-IDs. Nachrichten werden über Webhooks empfangen, und Sie benötigen separate Seitenzugriffstoken für jedes verbundene Konto.

Twitter/X bietet die Direct Messages API an, jedoch ist der Zugriff auf höhere API-Stufen angewiesen. Die Nachrichtenabfrage ist die primäre Methode, da die Unterstützung für Webhooks eingeschränkt ist.

Bluesky verwendet das AT-Protokoll mit einer dezentralen Architektur. Direktnachrichten funktionieren anders als auf herkömmlichen Plattformen und erfordern lexikonbasierte Abfragen.

Reddit bietet private Nachrichten über ihre API an, jedoch sind die Ratenlimits sehr streng (60 Anfragen pro Minute für OAuth-Clients).

Telegram stellt der Bot-API Long Polling oder Webhooks zur Verfügung. Jeder Bot erhält sein eigenes Token, und Nachrichten enthalten umfangreiche Metadaten über die Chat-Typen.

Die zentralen Herausforderungen beim Aufbau einer API für ein Social-Media-Postfach sind:

  1. Authentifizierungs-KomplexitätJede Plattform erfordert unterschiedliche OAuth-Workflows, Strategien zur Token-Aktualisierung und Berechtigungsbereiche.
  2. DatennormalisierungEine „Nachricht“ hat auf jeder Plattform eine andere Bedeutung.
  3. EchtzeitlieferungWebhook-Formate variieren stark, und einige Plattformen unterstützen Webhooks überhaupt nicht.
  4. Verwaltung von Rate LimitsDie Grenzen einer Plattform sollten nicht deinen gesamten Posteingang zum Stillstand bringen.
  5. Inkonsistenz bei der PaginierungEinige verwenden Cursors, andere setzen auf Offsets, und die zeitstempelbasierte Paginierung verhält sich überall anders.

Architekturübersicht für ein einheitliches Social-Media-Postfach

Ein gut gestalteter Multi-Plattform-Posteingang trennt die Anliegen in verschiedene Ebenen:

Unified Inbox Architecture

// Architekturschichten für ein einheitliches Posteingangssystem

interface InboxArchitecture {
  // Schicht 1: Plattformadapter
  adapters: {
    facebook: FacebookAdapter;
    instagram: InstagramAdapter;
    twitter: TwitterAdapter;
    bluesky: BlueskyAdapter;
    reddit: RedditAdapter;
    telegram: TelegramAdapter;
  };
  
  // Schicht 2: Normalisierungs-Engine
  normalizer: {
    transformMessage: (platformMessage: unknown, platform: string) => NormalizedMessage;
    transformConversation: (platformConvo: unknown, platform: string) => NormalizedConversation;
  };
  
  // Schicht 3: Aggregationsdienst
  aggregator: {
    fetchAllConversations: (accounts: SocialAccount[]) => Promise>;
    fetchAllMessages: (conversationIds: string[]) => Promise>;
  };
  
  // Schicht 4: Speicherschicht
  storage: {
    conversations: ConversationRepository;
    messages: MessageRepository;
    accounts: AccountRepository;
  };
  
  // Schicht 5: API-Schicht
  api: {
    getConversations: (filters: ConversationFilters) => Promise>;
    getMessages: (conversationId: string, cursor?: string) => Promise>;
    sendMessage: (conversationId: string, content: MessageContent) => Promise;
  };
}

Das Adaptermuster isoliert plattformspezifische Logik. Wenn Instagram seine API ändert, aktualisierst du einfach einen Adapter, ohne den Rest deines Codes anzufassen.

Unterstützte Plattformen und deren APIs

Nicht jede Plattform unterstützt jede Funktion des Posteingangs. Hier ist ein konfigurationsbasierter Ansatz zur Verwaltung der Plattformfähigkeiten:

```typescript
// Plattformunterstützungskonfiguration für Postfachfunktionen

export type InboxFeature = 'Nachrichten' | 'Kommentare' | 'Bewertungen';

export const INBOX_PLATFORMS = {
  Nachrichten: ['facebook', 'instagram', 'twitter', 'bluesky', 'reddit', 'telegram'] as const,
  Kommentare: ['facebook', 'instagram', 'twitter', 'bluesky', 'threads', 'youtube', 'linkedin', 'reddit'] as const,
  Bewertungen: ['facebook', 'googlebusiness'] as const,
} as const;

export type MessagesPlatform = (typeof INBOX_PLATFORMS.Nachrichten)[number];
export type CommentsPlatform = (typeof INBOX_PLATFORMS.Kommentare)[number];
export type ReviewsPlatform = (typeof INBOX_PLATFORMS.Bewertungen)[number];

// Überprüfen, ob eine Plattform eine bestimmte Funktion unterstützt
export function isPlatformSupported(platform: string, feature: InboxFeature): boolean {
  return (INBOX_PLATFORMS[feature] as readonly string[]).includes(platform);
}

// Validieren und hilfreiche Fehlermeldungen zurückgeben
export function validatePlatformSupport(
  platform: string,
  feature: InboxFeature
): { valid: true } | { valid: false; error: string; supportedPlatforms: readonly string[] } {
  if (!isPlatformSupported(platform, feature)) {
    const featureLabel = feature === 'Nachrichten' ? 'Direktnachrichten' : feature;
    return {
      valid: false,
      error: `Plattform '${platform}' unterstützt ${featureLabel} nicht`,
      supportedPlatforms: INBOX_PLATFORMS[feature],
    };
  }
  return { valid: true };
}
```

Hinweis: TikTok und Pinterest fehlen auffällig in den Listen der Nachrichten und Kommentare. Ihre APIs bieten keinen Lesezugriff auf Benutzernachrichten oder Kommentare, wodurch sie sich nicht für die Zusammenführung von Postfächern eignen.

Hier ist ein Vergleich der API-Funktionen über verschiedene Plattformen hinweg:

PlatformMessagesCommentsReviewsWebhooksRatenlimits
Facebook200 €/Stunde/Nutzer
Instagram200 €/Stunde/Nutzer
TwitterLimited15/15min (DMs)
Bluesky3000/5Min
Reddit60/min
Telegram30/Sekunde
LinkedIn✅ (Organisationen)100 pro Tag
YouTube10.000 pro Tag

Datenmodellierung für Gespräche und Nachrichten

Ihr Datenmodell muss die Vereinigung aller Plattformfunktionen verwalten und gleichzeitig saubere Beziehungen aufrechterhalten. Hier ist ein MongoDB-Schema, das gut funktioniert:

```javascript
// Gesprächsschema - repräsentiert einen Thread mit einem Teilnehmer

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,
    },
    // Plattform-spezifische Gesprächskennung
    platformConversationId: {
      type: String,
      required: true,
      index: true,
    },
    status: {
      type: String,
      enum: ["active", "archived"],
      default: "active",
      index: true,
    },
    // Zwischengespeicherte Teilnehmerinformationen für schnelle Anzeige
    participantName: String,
    participantUsername: String,
    participantPicture: String,
    participantId: String,
    // Vorschau-Daten für die Inbox-Listenansicht
    lastMessage: String,
    lastMessageAt: {
      type: Date,
      index: true,
    },
    // Plattform-spezifische Extras
    metadata: {
      type: Map,
      of: mongoose.Schema.Types.Mixed,
    },
  },
  { timestamps: true }
);

// Verhindere doppelte Gespräche pro Konto
conversationSchema.index(
  { accountId: 1, platformConversationId: 1 }, 
  { unique: true }
);

// Optimiere Abfragen der Inbox-Liste
conversationSchema.index(
  { accountId: 1, status: 1, lastMessageAt: -1 }
);
```

Das Nachrichten-Schema erfasst die gesamte Bandbreite an Inhaltsarten:

// Nachrichten-Schema - individuelle Nachrichten innerhalb von Konversationen

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,
    }],
    // Interaktionen mit Instagram-Stories
    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,
    },
    // Rohdaten für Debugging speichern
    rawPayload: mongoose.Schema.Types.Mixed,
  },
  { timestamps: true }
);

// Doppelte Nachrichten verhindern
messageSchema.index(
  { accountId: 1, platformMessageId: 1 }, 
  { unique: true }
);

// Chronologisches Abrufen von Nachrichten
messageSchema.index(
  { conversationId: 1, platformTimestamp: -1 }
);

// Abfragen der ungelesenen

The direction Feld ist entscheidend. Es unterscheidet zwischen den Nachrichten, die Ihr Team gesendet hat (outgoing) und Nachrichten von Kunden (incoming). Dies ermöglicht Funktionen wie ungelesene Nachrichten und Analysen der Reaktionszeiten.

Aggregationsstrategie und Normalisierung

Die Aggregationsschicht ruft Daten parallel von mehreren Konten ab und geht dabei elegant mit Fehlern um. Hier ist die zentrale Implementierung:

```typescript
// Aggregationshilfsprogramme für das Abrufen von Daten aus mehreren Konten

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('Zeitüberschreitung der Anfrage')), 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 || 'Unbekannter Fehler',
          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 };
}
```

Dieses Muster stellt sicher, dass eine Rate-Limitierung auf Twitter dich nicht daran hindert, Facebook-Nachrichten anzuzeigen. errors Array ermöglicht es deinem Frontend, teilweise Ergebnisse mit entsprechenden Warnungen anzuzeigen.

Zur Normalisierung plattformspezifischer Daten in Ihr einheitliches Format:

```typescript
// Nachrichten-Normalisierung von plattformspezifischen Formaten

interface NormalizedMessage {
  plattformNachrichtenId: string;
  plattform: string;
  richtung: 'eingehend' | 'ausgehend';
  senderId: string;
  senderName?: string;
  text?: string;
  anhänge: Attachment[];
  plattformZeitstempel: Date;
}

function normalizeInstagramMessage(
  raw: InstagramMessage, 
  accountId: string
): NormalizedMessage {
  const isOutgoing = raw.from.id === accountId;
  
  return {
    plattformNachrichtenId: raw.id,
    plattform: 'instagram',
    richtung: isOutgoing ? 'ausgehend' : 'eingehend',
    senderId: raw.from.id,
    senderName: raw.from.username,
    text: raw.message,
    anhänge: (raw.attachments?.data || []).map(att => ({
      typ: mapInstagramAttachmentType(att.type),
      url: att.url || att.payload?.url,
      payload: att.payload,
    })),
    plattformZeitstempel: new Date(raw.created_time),
  };
}

function normalizeTwitterMessage(
  raw: TwitterDM,
  accountId: string
): NormalizedMessage {
  const isOutgoing = raw.sender_id === accountId;
  
  return {
    plattformNachrichtenId: raw.id,
    plattform: 'twitter',
    richtung: isOutgoing ? 'ausgehend' : 'eingehend',
    senderId: raw.sender_id,
    senderName: raw.sender?.name,
    text: raw.text,
    anhänge: (raw.attachments?.media_keys || []).map(key => ({
      typ: 'bild', // Twitter DM-Anhänge sind typischerweise Bilder
      url: raw.includes?.media?.find(m => m.media_key === key)?.url,
    })),
    plattformZeitstempel: new Date(raw.created_at),
  };
}
```

Echtzeit-Updates mit Webhooks

Webhooks beseitigen die Notwendigkeit ständiger Abfragen. Jede Plattform hat ihr eigenes Webhook-Format, daher benötigen Sie plattformspezifische Handler, die in Ihre einheitliche Verarbeitungs-Pipeline eingespeist werden.

// Webhook-Handler für Instagram-Nachrichten

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');
  
  // Authentizität des Webhooks überprüfen
  if (!verifyInstagramSignature(body, signature)) {
    return NextResponse.json({ error: 'Ungültige Signatur' }, { status: 401 });
  }
  
  const payload = JSON.parse(body);
  
  // Jedes Entry verarbeiten (kann mehrere Updates enthalten)
  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
) {
  // Verbundenes Konto finden
  const account = await SocialAccount.findOne({
    platform: 'instagram',
    platformAccountId: pageId,
    isActive: true,
  });
  
  if (!account) {
    console.warn(`Kein aktives Konto für die Instagram-Seite ${pageId} gefunden`);
    return;
  }
  
  // Konversation finden oder erstellen
  const conversation = await findOrCreateConversation({
    accountId: account._id,
    platform: 'instagram',
    platformConversationId: messaging.sender.id, // Für Instagram ist die Sender-ID die Konversations-ID
    participantId: messaging.sender.id,
  });
  
  // Nachricht normalisieren und speichern
  const normalizedMessage = normalizeInstagramWebhookMessage(messaging, account);
  
  await Message.findOneAndUpdate(
    { 
      accountId: account._id, 
      platformMessageId: normalizedMessage.platformMessageId 
    },
    {

Hinweis: Überprüfen Sie immer die Signaturen von Webhooks, bevor Sie diese verarbeiten. Ohne Überprüfung könnten Angreifer gefälschte Nachrichten in Ihr System einschleusen.

Für Plattformen ohne Webhook-Unterstützung (wie Bluesky und Reddit) implementieren Sie einen Polling-Dienst:

// Abfragedienst für Plattformen ohne 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(`Polling-Fehler für ${accountId}:`, error);
      }
    };
    
    // Erste Abfrage
    await poll();
    
    // Wiederkehrende Abfragen planen
    const interval = setInterval(poll, pollInterval);
    this.intervals.set(accountId, interval);
  }
  
  private getPollInterval(platform: string): number {
    // Rate-Limits mit konservativen Intervallen beachten
    const intervals: Record = {
      bluesky: 30000,  // 30 Sekunden
      reddit: 60000,   // 60 Sekunden (strenge Rate-Limits)
      twitter: 60000,  // 60 Sekunden (eingeschränkter DM-Zugriff)
    };
    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

Cursor-basierte Seitennavigation über Plattformen hinweg

Wenn Sie soziale Nachrichten aus mehreren Konten aggregieren, versagt die herkömmliche Offset-Paginierung. Eine neue Nachricht, die eintrifft, verschiebt alle Offsets. Die cursorbasierte Paginierung löst dieses Problem, indem sie stabile Identifikatoren verwendet.

Die Herausforderung: Jede Plattform verwendet unterschiedliche Cursor-Formate. Ihre einheitliche API benötigt einen Cursor, der genügend Informationen kodiert, um die Paginierung über alle Quellen hinweg fortzusetzen.

```typescript
// Cursor-basierte Paginierung für aggregierte Ergebnisse

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

// Cursor-Format: {timestamp}_{accountId}_{itemId}
// Dies ermöglicht eine stabile Paginierung, selbst wenn Elemente hinzugefügt werden

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;

  // Cursor-Filter anwenden, falls vorhanden
  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);

      // Elemente vor dem Cursor (für absteigende Reihenfolge)
      if (itemTime < cursorTime) return true;
      if (itemTime === cursorTime) {
        if (itemAccountId > cursorAccountId) return true;
        if (itemAccountId === cursorAccountId && itemId > cursorItemId) return true;
      }
      return false;
    });
  }

  // Limit anwenden
  const paginatedItems = filteredItems.slice(0, limit);
  const hasMore = filteredItems.length > limit;

  // Nächsten Cursor aus dem letzten Element generieren
  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}`;

Verwendung in Ihrem API-Endpunkt:

// API-Endpunkt mit Cursor-Pagination

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';
  
  // Benutzerkonten abrufen
  const accounts = await getInboxAccounts(userId, 'messages');
  
  // Konversationen von allen Konten abrufen
  const { items: conversations, errors } = await aggregateFromAccounts(
    accounts,
    async (account) => {
      return Conversation.find({
        accountId: account._id,
        status,
      })
        .sort({ lastMessageAt: -1 })
        .limit(limit * 2) // Zusätzliche abrufen für das Merging
        .lean();
    }
  );
  
  // Alle Konversationen nach der letzten Nachrichtenzeit sortieren
  const sorted = sortItems(
    conversations,
    'lastMessageAt',
    'desc',
    { lastMessageAt: (c) => c.lastMessageAt }
  );
  
  // Cursor-Pagination anwenden
  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),
  });
}

Umgang mit plattformspezifischen Funktionen

Jede Plattform bietet einzigartige Funktionen, die sich nicht einfach in ein einheitliches Modell übertragen lassen. Instagram hat Story-Antworten und Erwähnungen. Twitter bietet zitierte DMs. Telegram verfügt über Antwortketten und Reaktionen.

Der Schlüssel liegt darin, plattformspezifische Daten flexibel zu speichern. metadata Feld, während Sie gängige Funktionen über Ihre standardisierte Schnittstelle präsentieren:

```typescript
// Verarbeitung von Antworten auf Instagram Stories

interface InstagramStoryReply {
  storyId: string;
  storyUrl: string;
  storyMediaType: 'image' | 'video';
  expiresAt: Date; // Stories laufen nach 24 Stunden ab
}

function normalizeInstagramWebhookMessage(
  messaging: InstagramMessagingEvent,
  account: SocialAccount
): Partial {
  const base = {
    platformMessageId: messaging.message.mid,
    platform: 'instagram',
    direction: messaging.sender.id === account.platformAccountId ? 'outgoing' : 'incoming',
    senderId: messaging.sender.id,
    platformTimestamp: new Date(messaging.timestamp),
  };
  
  // Verarbeitung von Antworten auf 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,
    };
  }
  
  // Verarbeitung von Story-Erwähnungen
  if (messaging.message.attachments?.[0]?.type === 'story_mention') {
    return {
      ...base,
      isStoryMention: true,
      attachments: [{
        type: 'share',
        url: messaging.message.attachments[0].payload.url,
        payload: messaging.message.attachments[0].payload,
      }],
    };
  }
  
  // Standardnachricht
  return {
    ...base,
    text: messaging.message.text,
    attachments: normalizeAttachments(messaging.message.attachments),
  };
}
```

Für LinkedIn ist eine besondere Handhabung erforderlich, da die Funktionen des Posteingangs nur mit Unternehmenskonten funktionieren:

// Validierung des LinkedIn-Organisationskontos

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

  // Mongoose Map-Typ behandeln
  if (metadata instanceof Map) {
    return metadata.get('accountType') === 'organization' || 
           metadata.has('selectedOrganization');
  }

  // Einfaches Objekt behandeln
  return metadata.accountType === 'organization' || 
         'selectedOrganization' in metadata;
}

// Konten filtern, um nur die zu unterstützen, die Inbox-Funktionen bieten
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 erfordert den Kontotyp "Organisation"
    if (account.platform === 'linkedin' && !isLinkedInOrgAccount(account.metadata)) {
      return false;
    }

    return true;
  });
}

Duplikate entfernen und Nachrichten-Threading

Beim Aggregieren aus mehreren Quellen können Duplikate auftreten. Eine Nachricht könnte über einen Webhook eintreffen und dann erneut während einer Polling-Synchronisation. Ihr System benötigt eine zuverlässige Duplikaterkennung:

```javascript
// Duplikate entfernen

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

// Verwendung: Nachrichten nach Plattform-ID duplizieren
const eindeutigeNachrichten = duplikateEntfernen(
  alleNachrichten,
  (msg) => `${msg.plattform}_${msg.plattformNachrichtId}`
);
```

Für die Datenbankebene-Deduplizierung verwenden Sie Upsert-Operationen mit eindeutigen Indizes:

// Upsert-Muster für die Verarbeitung von Webhook-Nachrichten

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,
      // Dokument zurückgeben, unabhängig davon, ob es eingefügt oder aktualisiert wurde
      rawResult: false,
    }
  );
}

Der einzigartige Index auf { accountId: 1, platformMessageId: 1 } stellt sicher, dass MongoDB echte Duplikate auf Datenbankebene ablehnt, selbst bei gleichzeitiger Verarbeitung von Webhooks.

Leistungsoptimierung und Caching

Ein plattformübergreifender Posteingang tätigt viele API-Anfragen. Ohne Caching erreichen Sie die Ratenlimits und sorgen für langsame Benutzererlebnisse.

// Redis-Caching-Schicht für Konversationslisten

import Redis from 'ioredis';

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

interface CacheOptions {
  ttl: number; // Sekunden
  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(',')}`;
  
  // Zuerst den Cache versuchen
  const cached = await redis.get(cacheKey);
  if (cached) {
    const data = JSON.parse(cached);
    const age = Date.now() - data.timestamp;
    
    // Sofort die zwischengespeicherten Daten zurückgeben
    if (age < options.ttl * 1000) {
      return { conversations: data.conversations, fromCache: true };
    }
    
    // Stale-while-revalidate: veraltete Daten zurückgeben, aber Hintergrundaktualisierung auslösen
    if (age < (options.staleWhileRevalidate || options.ttl) * 1000) {
      // Hintergrundaktualisierung auslösen
      refreshConversationsCache(userId, accountIds, cacheKey).catch(console.error);
      return { conversations: data.conversations, fromCache: true };
    }
  }
  
  // Cache-Fehler oder abgelaufen: frische Daten abrufen
  const conversations = await fetchConversationsFromDB(userId, accountIds);
  
  // Im Cache speichern
  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 Minuten TTL für Hintergrundaktualisierung
    JSON.stringify({ conversations, timestamp: Date.now() })
  );
}

Zusätzliche Optimierungsstrategien:

  1. Batch-Webhooks-VerarbeitungWebhooks in Warteschlange stellen und in Chargen verarbeiten, um Datenbankzugriffe zu minimieren.
  2. VerbindungspoolingWiederverwendung von HTTP-Verbindungen zu Plattform-APIs
  3. Inkrementelle SynchronisierungVerfolgen Sie den letzten Synchronisierungszeitstempel pro Konto und laden Sie nur neuere Nachrichten herunter.
  4. DenormalizationSpeichern Sie Teilnehmerinformationen direkt in den Gesprächen, um Verknüpfungen zu vermeiden.

Frontend-Oberfläche erstellen

Ihr Frontend muss die Komplexität eines plattformübergreifenden Posteingangs bewältigen und gleichzeitig eine übersichtliche Benutzeroberfläche präsentieren. Hier ist ein React-Komponentenmuster:

```javascript
// React Hook für ein einheitliches Postfach

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

interface UseInboxOptions {
  status?: 'aktiv' | 'archiviert';
  plattform?: string;
  kontoId?: string;
}

export function useInbox(options: UseInboxOptions = {}) {
  return useInfiniteQuery({
    queryKey: ['postfach', 'gespräche', options],
    queryFn: async ({ pageParam }) => {
      const params = new URLSearchParams();
      if (pageParam) params.set('cursor', pageParam);
      if (options.status) params.set('status', options.status);
      if (options.plattform) params.set('plattform', options.plattform);
      if (options.kontoId) params.set('kontoId', options.kontoId);
      
      const response = await fetch(`/api/postfach/gespräche?${params}`);
      if (!response.ok) {
        throw new Error('Fehler beim Abrufen der Gespräche');
      }
      return response.json();
    },
    getNextPageParam: (lastPage) => lastPage.pagination.nextCursor,
    initialPageParam: null as string | null,
  });
}

export function useConversationMessages(conversationId: string) {
  return useInfiniteQuery({
    queryKey: ['postfach', 'nachrichten', conversationId],
    queryFn: async ({ pageParam }) => {
      const params = new URLSearchParams();
      if (pageParam) params.set('cursor', pageParam);
      
      const response = await fetch(
        `/api/postfach/gespräche/${conversationId}/nachrichten?${params}`
      );
      if (!response.ok) {
        throw new Error('Fehler beim Abrufen der Nachrichten');
      }
      return response.json();
    },
    getNextPageParam: (lastPage) => lastPage.pagination.nextCursor,
    initialPageParam: null as string | null,
    enabled: !!conversationId,
  });
}
```

Plattform-spezifische Funktionen mit bedingter Anzeige darstellen:

// Nachrichtenkomponente mit plattformspezifischer Darstellung

function MessageBubble({ message }: { message: Message }) {
  const isOutgoing = message.direction === 'outgoing';
  
  return (
    
{/* Indikator für Story-Antwort */} {message.storyReply && (
Auf deine Story geantwortet {message.storyReply.storyUrl && ( Story )}
)} {/* Indikator für Story-Erwähnung */} {message.isStoryMention && (
Hat dich in seiner Story erwähnt
)} {/* Nachrichtentext */} {message.text && (

{message.text}

)} {/* Anhänge */} {message.attachments?.map((attachment, i) => ( ))} {/* Metadaten */}
); }

Verwendung von Lates Unified Inbox API

Der Aufbau eines einheitlichen Social-Media-Postfachs von Grund auf erfordert monatelange Entwicklungszeit. Sie müssen die Integrationen mit mehreren Plattformen aufrechterhalten, deren API-Änderungen verwalten, OAuth-Workflows steuern und sich mit Rate-Limits auseinandersetzen.

Late bietet eine vorgefertigte API für soziale Medien, die all diese Komplexität übernimmt. Anstatt sich mit sechs verschiedenen Plattform-APIs zu integrieren, integrierst du dich mit nur einer.

So funktioniert die Integration des Posteingangs mit Late ganz einfach:

```javascript
// Verwendung der einheitlichen Inbox-API von Late

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

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

// Gespräche von allen verbundenen Konten abrufen
const { conversations, pagination, meta } = await late.inbox.getConversations({
  status: 'active',
  limit: 20,
});

// Nachrichten für ein bestimmtes Gespräch abrufen
const { messages } = await late.inbox.getMessages(conversationId, {
  limit: 50,
});

// Eine Antwort senden (Late kümmert sich um plattformspezifische Formatierungen)
await late.inbox.sendMessage(conversationId, {
  text: 'Danke für Ihre Nachricht! Wie kann ich Ihnen helfen?',
});

// Nach Plattform filtern, falls erforderlich
const instagramOnly = await late.inbox.getConversations({
  platform: 'instagram',
});
```

Die einheitliche API von Late bietet:

  • Einzelner OAuth-FlowKonten einmal verbinden, alle Posteingangsfunktionen nutzen
  • Normalisierte DatenKonsistentes Nachrichtenformat über Facebook, Instagram, Twitter, Bluesky, Reddit und Telegram hinweg
  • Echtzeit-WebhooksEin Webhook-Endpunkt für alle Plattformen
  • Integrierte PaginierungCursor-basierte Pagination, die über Konten hinweg funktioniert
  • FehlerbehandlungElegante Fehlertoleranz bei Problemen einzelner Plattformen
  • Verwaltung von Rate LimitsAutomatische Rückoff- und Wiederholungslogik

Die Aggregationsmetadaten in den Antworten von Late zeigen dir genau, welche Konten erfolgreich waren und welche Probleme hatten:

// Lates Antwort enthält Aggregationsmetadaten

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

Diese Transparenz ermöglicht es Ihnen, Benutzeroberflächen zu erstellen, die die Nutzer informieren, wenn bestimmte Konten vorübergehende Probleme haben, ohne das gesamte Posteingangserlebnis zu beeinträchtigen.

Schau dir das an Die Dokumentation von Late Um mit der einheitlichen Inbox-API zu beginnen. Sie können innerhalb von Stunden anstatt Monaten eine funktionierende Multi-Plattform-Inbox integrieren.

Eine API. 13+ Plattformen.

Social Media Features in Minuten integrieren, nicht Wochen.

Für Entwickler gemacht. Von Agenturen geschätzt. Von 6.325 Nutzern vertraut.