Gestire le conversazioni con i clienti su Facebook, Instagram, Twitter, Bluesky, Reddit e Telegram richiede un continuo cambio di contesto. Una casella di posta unificata per i social media risolve questo problema aggregando tutti i messaggi in un'unica interfaccia, permettendo al tuo team di rispondere più velocemente senza saltare tra le piattaforme.
Questa guida illustra come costruire una casella di posta multi-piattaforma da zero. Imparerai come progettare il modello dati, normalizzare i messaggi da diverse API, gestire i webhook in tempo reale e implementare la paginazione basata su cursori che funziona su tutte le piattaforme. Alla fine, avrai un'architettura pronta per la produzione per aggregare i messaggi social su larga scala.
La Sfida della Messaggistica Multi-Piattaforma
Ogni piattaforma social ha la propria API di messaggistica con flussi di autenticazione unici, strutture dati, limiti di frequenza e formati webhook. Ecco cosa devi affrontare:
Facebook e Instagram utilizzano la Messenger Platform API con ID utente con scope a livello di pagina. I messaggi arrivano tramite webhook, e hai bisogno di token di accesso separati per ogni account connesso.
Twitter/X fornisce la Direct Messages API, ma l'accesso richiede livelli API elevati. Il polling dei messaggi è il metodo principale poiché il supporto webhook è limitato.
Bluesky utilizza l'AT Protocol con un'architettura decentralizzata. I messaggi diretti funzionano diversamente rispetto alle piattaforme tradizionali, richiedendo query basate su lexicon.
Reddit offre messaggistica privata attraverso la loro API, ma i limiti di frequenza sono aggressivi (60 richieste al minuto per i client OAuth).
Telegram fornisce la Bot API con long polling o webhook. Ogni bot ottiene il proprio token, e i messaggi includono metadati ricchi sui tipi di chat.
Le sfide principali quando si costruisce un'API per casella di posta social media includono:
- Complessità dell'autenticazione: Ogni piattaforma richiede diversi flussi OAuth, strategie di refresh dei token e scope di permessi
- Normalizzazione dei dati: Un "messaggio" significa qualcosa di diverso su ogni piattaforma
- Consegna in tempo reale: I formati webhook variano enormemente, e alcune piattaforme non supportano affatto i webhook
- Gestione dei limiti di frequenza: Raggiungere i limiti su una piattaforma non dovrebbe bloccare l'intera casella di posta
- Inconsistenza della paginazione: Alcune usano cursori, altre offset, e la paginazione basata su timestamp si comporta diversamente ovunque
Panoramica dell'Architettura per una Casella di Posta Unificata per Social Media
Una casella di posta multi-piattaforma ben progettata separa le responsabilità in livelli distinti:

// Architecture layers for a unified inbox system
interface InboxArchitecture {
// Layer 1: Platform Adapters
adapters: {
facebook: FacebookAdapter;
instagram: InstagramAdapter;
twitter: TwitterAdapter;
bluesky: BlueskyAdapter;
reddit: RedditAdapter;
telegram: TelegramAdapter;
};
// Layer 2: Normalization Engine
normalizer: {
transformMessage: (platformMessage: unknown, platform: string) => NormalizedMessage;
transformConversation: (platformConvo: unknown, platform: string) => NormalizedConversation;
};
// Layer 3: Aggregation Service
aggregator: {
fetchAllConversations: (accounts: SocialAccount[]) => Promise<AggregatedResult<Conversation>>;
fetchAllMessages: (conversationIds: string[]) => Promise<AggregatedResult<Message>>;
};
// Layer 4: Storage Layer
storage: {
conversations: ConversationRepository;
messages: MessageRepository;
accounts: AccountRepository;
};
// Layer 5: API Layer
api: {
getConversations: (filters: ConversationFilters) => Promise<PaginatedResponse<Conversation>>;
getMessages: (conversationId: string, cursor?: string) => Promise<PaginatedResponse<Message>>;
sendMessage: (conversationId: string, content: MessageContent) => Promise<Message>;
};
}
Il pattern adapter isola la logica specifica della piattaforma. Quando Instagram cambia la loro API, aggiorni un solo adapter senza toccare il resto del tuo codice.
Piattaforme Supportate e le Loro API
Non tutte le piattaforme supportano tutte le funzionalità della casella di posta. Ecco un approccio guidato dalla configurazione per gestire le capacità delle piattaforme:
// Platform support configuration for inbox features
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];
// Check if a platform supports a specific feature
export function isPlatformSupported(platform: string, feature: InboxFeature): boolean {
return (INBOX_PLATFORMS[feature] as readonly string[]).includes(platform);
}
// Validate and return helpful error messages
export function validatePlatformSupport(
platform: string,
feature: InboxFeature
): { valid: true } | { valid: false; error: string; supportedPlatforms: readonly string[] } {
if (!isPlatformSupported(platform, feature)) {
const featureLabel = feature === 'messages' ? 'direct messages' : feature;
return {
valid: false,
error: `Platform '${platform}' does not support ${featureLabel}`,
supportedPlatforms: INBOX_PLATFORMS[feature],
};
}
return { valid: true };
}
Nota: TikTok e Pinterest sono notevolmente assenti dalle liste di messaggi e commenti. Le loro API non forniscono accesso in lettura ai messaggi o commenti degli utenti, rendendole inadatte per l'aggregazione della casella di posta.
Ecco un confronto delle capacità API tra le piattaforme:
| Piattaforma | Messaggi | Commenti | Recensioni | Webhook | Limiti di Frequenza |
|---|---|---|---|---|---|
| ✅ | ✅ | ✅ | ✅ | 200/ora/utente | |
| ✅ | ✅ | ❌ | ✅ | 200/ora/utente | |
| ✅ | ✅ | ❌ | Limitato | 15/15min (DM) | |
| Bluesky | ✅ | ✅ | ❌ | ❌ | 3000/5min |
| ✅ | ✅ | ❌ | ❌ | 60/min | |
| Telegram | ✅ | ❌ | ❌ | ✅ | 30/sec |
| ❌ | ✅ (Org) | ❌ | ✅ | 100/giorno | |
| YouTube | ❌ | ✅ | ❌ | ❌ | 10000/giorno |
Progettazione del Modello Dati per Conversazioni e Messaggi
Il tuo modello dati deve gestire l'unione di tutte le funzionalità delle piattaforme mantenendo relazioni pulite. Ecco uno schema MongoDB che funziona bene:
// Conversation schema - represents a thread with a 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,
},
// Platform's native conversation identifier
platformConversationId: {
type: String,
required: true,
index: true,
},
status: {
type: String,
enum: ["active", "archived"],
default: "active",
index: true,
},
// Cached participant info for fast display
participantName: String,
participantUsername: String,
participantPicture: String,
participantId: String,
// Preview data for inbox list view
lastMessage: String,
lastMessageAt: {
type: Date,
index: true,
},
// Platform-specific extras
metadata: {
type: Map,
of: mongoose.Schema.Types.Mixed,
},
},
{ timestamps: true }
);
// Prevent duplicate conversations per account
conversationSchema.index(
{ accountId: 1, platformConversationId: 1 },
{ unique: true }
);
// Optimize inbox list queries
conversationSchema.index(
{ accountId: 1, status: 1, lastMessageAt: -1 }
);
Lo schema dei messaggi cattura l'intera gamma di tipi di contenuto:
// Message schema - individual messages within 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,
}],
// Instagram story interactions
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,
},
// Keep raw data for debugging
rawPayload: mongoose.Schema.Types.Mixed,
},
{ timestamps: true }
);
// Prevent duplicate messages
messageSchema.index(
{ accountId: 1, platformMessageId: 1 },
{ unique: true }
);
// Chronological message fetching
messageSchema.index(
{ conversationId: 1, platformTimestamp: -1 }
);
// Unread count queries
messageSchema.index(
{ conversationId: 1, direction: 1, isRead: 1 }
);
Il campo direction è critico. Distingue tra i messaggi inviati dal tuo team (outgoing) e i messaggi dai clienti (incoming). Questo alimenta funzionalità come il conteggio dei non letti e le analitiche sui tempi di risposta.
Strategia di Aggregazione e Normalizzazione
Il livello di aggregazione recupera i dati da più account in parallelo gestendo con grazia i fallimenti. Ecco l'implementazione principale:
// Aggregation utilities for multi-account data fetching
export interface AggregationError {
accountId: string;
accountUsername?: string;
platform: string;
error: string;
code?: string;
retryAfter?: number;
}
export interface AggregatedResult<T> {
items: T[];
errors: AggregationError[];
}
export async function aggregateFromAccounts<T>(
accounts: SocialAccount[],
fetcher: (account: SocialAccount) => Promise<T[]>,
options?: { timeout?: number }
): Promise<AggregatedResult<T>> {
const timeout = options?.timeout || 10000;
const results: T[] = [];
const errors: AggregationError[] = [];
const fetchPromises = accounts.map(async (account) => {
try {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Request timeout')), 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 || 'Unknown error',
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 };
}
Questo pattern assicura che un limite di frequenza su Twitter non impedisca di mostrare i messaggi di Facebook. L'array errors permette al tuo frontend di visualizzare risultati parziali con avvisi appropriati.
Per normalizzare i dati specifici della piattaforma nel tuo formato unificato:
// Message normalization from platform-specific formats
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 ? 'o