API DM di Bluesky: Messaggistica con il Protocollo AT
The Bluesky API fornisce una base potente per la creazione di applicazioni sociali, e le sue capacità di messaggistica diretta aprono a possibilità entusiasmanti per gli sviluppatori. A differenza delle piattaforme social tradizionali con API proprietarie, Bluesky's Messaggistica AT Protocol Il sistema è basato su standard aperti, offrendoti il pieno controllo su come implementare le funzionalità di chat.
In questa guida, imparerai come integrare Messaggi diretti di Bluesky nelle tue applicazioni utilizzando TypeScript. Tratteremo tutto, dall'autenticazione all'invio di messaggi, dalla gestione del testo formattato all'implementazione di aggiornamenti in tempo reale.
Comprendere l'Architettura Decentralizzata
Prima di scrivere qualsiasi codice, è fondamentale comprendere come il sistema di messaggistica di Bluesky si differenzi da quello delle piattaforme centralizzate. Il Protocollo AT separa le funzionalità tra diversi servizi:
| Component | Purpose | Endpoint |
|---|---|---|
| Server di Dati Personali (PDS) | Memorizza i dati degli utenti e gestisce l'autenticazione. | Specifico per l'utente (risolto tramite PLC) |
| Servizio di chat | Gestisce il routing e l'archiviazione dei messaggi diretti. | api.bsky.chat |
| Directory PLC | Risolvi i DID in endpoint di servizio | plc.directory |
| AppView | Aggregazione dei feed pubblici | bsky.social |
The API DM di Bluesky instrada le richieste attraverso il tuo Server Dati Personale (PDS), che poi le inoltra al servizio di chat. Questa architettura significa che non puoi semplicemente colpire un singolo endpoint. Invece, devi prima risolvere il PDS dell'utente.
interfaccia BlueskySession {
accessJwt: string;
refreshJwt: string;
did: string;
handle: string;
}
async function risolviUrlBasePds(userDid: string): Promise<string> {
try {
const plcResponse = await fetch(
`https://plc.directory/${encodeURIComponent(userDid)}`
);
if (!plcResponse.ok) {
throw new Error(`Errore nella ricerca PLC: ${plcResponse.status}`);
}
const plcDoc = await plcResponse.json();
const servizi = plcDoc?.service || [];
const servizioPds = servizi.find((servizio: any) =>
servizio?.type?.toLowerCase().includes('atprotopersonaldataserver') ||
servizio?.id?.includes('#atproto_pds')
);
if (servizioPds?.serviceEndpoint) {
return servizioPds.serviceEndpoint.replace(/\/$/, '');
}
throw new Error('Nessun endpoint PDS trovato nel documento DID');
} catch (error) {
console.warn(`Impossibile risolvere PDS per ${userDid}:`, error);
return 'https://bsky.social'; // Ripristina il valore predefinito
}
}
Nota: Il passaggio di risoluzione PDS è fondamentale. Saltarlo porterà a errori di autenticazione quando si accede al servizio di chat, specialmente per gli utenti su istanze PDS auto-ospitate.
Autenticazione con le Password per le App
The Bluesky API utilizza le Password App per l'autenticazione invece di OAuth 2.0. Per accedere ai DM, devi creare una Password App con i permessi di messaggistica espliciti abilitati.
interface AuthConfig {
identifier: string; // Nome utente o email
password: string; // Password dell'app (non la password principale)
}
async function createSession(config: AuthConfig): Promise<BlueskySession> {
const response = await fetch(
'https://bsky.social/xrpc/com.atproto.server.createSession',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
identifier: config.identifier,
password: config.password,
}),
}
);
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Autenticazione fallita: ${response.status} ${errorBody}`);
}
const session = await response.json();
return {
accessJwt: session.accessJwt,
refreshJwt: session.refreshJwt,
did: session.did,
handle: session.handle,
};
}
async function refreshSession(refreshJwt: string): Promise<BlueskySession> {
const response = await fetch(
'https://bsky.social/xrpc/com.atproto.server.refreshSession',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${refreshJwt}`,
},
}
);
if (!response.ok) {
throw new Error('Rinnovo della sessione fallito. Si prega di ri-autenticarsi.');
}
return response.json();
}
When creating your App Password in Bluesky settings, make sure to check "Allow access to your direct messages." Without this permission, you'll receive XRPCNotSupported errori durante la chiamata agli endpoint chat.
Servizio di Risoluzione PDS e Chat
Il servizio di chat richiede un'intestazione proxy speciale per instradare correttamente le richieste. Ecco un'implementazione completa del gestore delle richieste di chat:
const CHAT_SERVICE_DID = 'did:web:api.bsky.chat#bsky_chat';
interface ChatRequestOptions {
accessToken: string;
method: 'GET' | 'POST';
endpoint: string;
params?: Record;
body?: any;
userDid: string;
refreshToken?: string;
}
interface ChatResponse {
data: T;
newTokens?: {
accessToken: string;
refreshToken: string;
expiresIn: number;
};
}
async function chatRequest(
options: ChatRequestOptions
): Promise> {
const { accessToken, method, endpoint, params, body, userDid, refreshToken } = options;
// Risolvi l'endpoint PDS dell'utente
const pdsBaseUrl = await resolvePdsBaseUrl(userDid);
const url = new URL(`${pdsBaseUrl}/xrpc/${endpoint}`);
// Aggiungi parametri di query per le richieste GET
if (method === 'GET' && params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
if (Array.isArray(value)) {
value.forEach(v => url.searchParams.append(key, String(v)));
} else {
url.searchParams.append(key, String(value));
}
}
});
}
const headers: Record = {
'Authorization': `Bearer ${accessToken}`,
'atproto-proxy': CHAT_SERVICE_DID,
};
if (method === 'POST' && body) {
headers['Content-Type'] = 'application/json';
}
const response = await fetch(url.toString(), {
method,
headers,
body: method === 'POST' && body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
const errorBody = await response.text();
// Gestisci l'espirazione del token con auto-refresh
if (refreshToken && errorBody.includes('ExpiredToken')) {
const newSession = await refreshSession(refreshToken);
// Ritenta con il nuovo token
return chatRequest({
...options,
accessToken: newSession.accessJwt,
refreshToken: newSession.refreshJwt,
});
}
// Fornisci messaggi
Elenco delle conversazioni con l'API DM di Bluesky
Ora implementiamo la funzionalità principale per elencare le conversazioni. Il Messaggistica AT Protocol il sistema restituisce le conversazioni con le informazioni sui partecipanti e l'ultimo messaggio:
interface Partecipante {
did: string;
handle: string;
nomeVisibile?: string;
avatar?: string;
}
interface Messaggio {
id: string;
testo: string;
inviatoIl: string;
mittente: {
did: string;
};
}
interface Conversazione {
id: string;
rev: string;
conteggioNonLetti: number;
silenziato: boolean;
partecipanti: Partecipante[];
ultimoMessaggio: Messaggio | null;
}
interface OpzioniListaConversazioni {
limite?: number;
cursore?: string;
statoLettura?: 'non letto';
stato?: 'richiesta' | 'accettato';
}
async function listaConversazioni(
tokenAccesso: string,
userDid: string,
opzioni: OpzioniListaConversazioni = {}
): Promise<{ conversazioni: Conversazione[]; cursore?: string }> {
const parametri: Record = {
limite: opzioni.limite || 50,
};
if (opzioni.cursore) parametri.cursore = opzioni.cursore;
if (opzioni.statoLettura) parametri.statoLettura = opzioni.statoLettura;
if (opzioni.stato) parametri.stato = opzioni.stato;
const risultato = await chatRequest({
tokenAccesso,
metodo: 'GET',
endpoint: 'chat.bsky.convo.listConvos',
parametri,
userDid,
});
const conversazioni = (risultato.data.convos || []).map((convo: any) => ({
id: convo.id,
rev: convo.rev,
conteggioNonLetti: convo.conteggioNonLetti || 0,
silenziato: convo.silenziato || false,
partecipanti: (convo.members || []).map((membro: any) => ({
did: membro.did,
handle: membro.handle,
nomeVisibile: membro.nomeVisibile,
avatar: membro.avatar,
})),
ultimoMessaggio: convo.ultimoMessaggio ? {
id: convo.ultimoMessaggio.id,
testo: convo.ultimoMessaggio.testo,
inviatoIl: convo.ultimoMessaggio.inviatoIl,
Inviare Messaggi Diretti su Bluesky
Inviare messaggi tramite il API DM di Bluesky richiede l'ID della conversazione. Puoi utilizzare una conversazione esistente o crearne una con partecipanti specifici:
interface SendMessageResult {
id: string;
rev: string;
text: string;
sentAt: string;
}
async function sendMessage(
accessToken: string,
userDid: string,
conversationId: string,
text: string
): Promise {
const result = await chatRequest({
accessToken,
method: 'POST',
endpoint: 'chat.bsky.convo.sendMessage',
body: {
convoId: conversationId,
message: { text },
},
userDid,
});
return {
id: result.data.id,
rev: result.data.rev,
text: result.data.text,
sentAt: result.data.sentAt,
};
}
async function getOrCreateConversation(
accessToken: string,
userDid: string,
memberDids: string[]
): Promise<{ id: string; rev: string }> {
const result = await chatRequest({
accessToken,
method: 'GET',
endpoint: 'chat.bsky.convo.getConvoForMembers',
params: { members: memberDids },
userDid,
});
return {
id: result.data.convo.id,
rev: result.data.convo.rev,
};
}
// Esempio completo: Invia un DM a un utente tramite il suo handle
async function sendDirectMessage(
session: BlueskySession,
recipientHandle: string,
messageText: string
): Promise {
// Prima, risolvi il DID del destinatario dal suo handle
const resolveResponse = await fetch(
`https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(recipientHandle)}`
);
if (!resolveResponse.ok) {
throw new Error(`Impossibile risolvere l'handle: ${recipientHandle}`);
}
const { did: recipientDid } = await resolveResponse.json();
// Ottieni o crea la conversazione
const conversation = await getOrCreateConversation(
session.accessJwt,
session.did,
[recipientDid]
);
// Invia il messaggio
return sendMessage(
session.accessJwt,
session.did,
conversation.id,
messageText
);
}
Testo Ricco e Facce
Il protocollo AT supporta il testo formattato tramite "facets", che sono annotazioni che aggiungono link, menzioni e hashtag al testo semplice. Quando crei messaggi, è necessario calcolare gli offset in byte (non gli offset in caratteri) per una corretta visualizzazione:
interface Facet {
index: {
byteStart: number;
byteEnd: number;
};
features: Array<{
$type: string;
uri?: string;
did?: string;
tag?: string;
}>;
}
function parseRichTextFacets(text: string): Facet[] {
const facets: Facet[] = [];
// Analizza gli URL
const urlRegex = /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi;
let urlMatch;
while ((urlMatch = urlRegex.exec(text)) !== null) {
const url = urlMatch[0];
const start = urlMatch.index;
const end = start + url.length;
// Converti le posizioni dei caratteri in posizioni dei byte
const byteStart = Buffer.byteLength(text.substring(0, start), 'utf8');
const byteEnd = Buffer.byteLength(text.substring(0, end), 'utf8');
facets.push({
index: { byteStart, byteEnd },
features: [{
$type: 'app.bsky.richtext.facet#link',
uri: url,
}],
});
}
// Analizza le menzioni (@handle.domain)
const mentionRegex = /@([a-zA-Z0-9._-]+(?:\.[a-zA-Z0-9._-]+)*)/gi;
let mentionMatch;
while ((mentionMatch = mentionRegex.exec(text)) !== null) {
const mention = mentionMatch[0];
const handle = mentionMatch[1];
const start = mentionMatch.index;
const end = start + mention.length;
const byteStart = Buffer.byteLength(text.substring(0, start), 'utf8');
const byteEnd = Buffer.byteLength(text.substring(0, end), 'utf8');
facets.push({
index: { byteStart, byteEnd },
features: [{
$type: 'app.bsky.richtext.facet#mention',
did: handle, // Sarà risolto nel DID effettivo
}],
});
}
// Ordina per posizione
facets.sort((a, b) => a.index.byteStart - b.index.byteStart);
return facets;
}
// Risolvi i handle delle menzioni in DIDs
async function resolveMentionDids(
Nota: Il calcolo dell'offset dei byte è fondamentale per i caratteri non ASCII. Un singolo emoji può occupare 4 byte, quindi utilizzare gli indici dei caratteri comprometterà il rendering del testo ricco.
Aggiornamenti in tempo reale
Per aggiornamenti in tempo reale dei messaggi, dovrai implementare il polling o utilizzare il flusso di eventi del Protocollo AT. Ecco un'implementazione pratica del polling:
interface MessagePollerOptions {
accessToken: string;
userDid: string;
conversationId: string;
onMessage: (message: Message) => void;
onError: (error: Error) => void;
pollInterval?: number;
}
class MessagePoller {
private intervalId: NodeJS.Timeout | null = null;
private lastMessageId: string | null = null;
private options: MessagePollerOptions;
constructor(options: MessagePollerOptions) {
this.options = {
pollInterval: 5000, // 5 secondi di default
...options,
};
}
async start(): Promise {
// Ottieni i messaggi iniziali per impostare la base
const { messages } = await this.fetchMessages();
if (messages.length > 0) {
this.lastMessageId = messages[0].id;
}
this.intervalId = setInterval(async () => {
try {
const { messages } = await this.fetchMessages();
// Trova nuovi messaggi
const newMessages: Message[] = [];
for (const msg of messages) {
if (msg.id === this.lastMessageId) break;
newMessages.push(msg);
}
if (newMessages.length > 0) {
this.lastMessageId = newMessages[0].id;
// Consegna in ordine cronologico
newMessages.reverse().forEach(msg => {
this.options.onMessage(msg);
});
}
} catch (error) {
this.options.onError(error as Error);
}
}, this.options.pollInterval);
}
stop(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
private async fetchMessages(): Promise<{ messages: Message[] }> {
const result = await chatRequest({
accessToken: this.options.accessToken,
method: 'GET',
endpoint: 'chat.bsky.convo.getMessages',
params: {
convoId: this.options.conversationId,
limit: 20,
},
userDid: this.options.userDid,
});
return {
messages: (result.data.messages || [])
.filter((msg: any) => msg.$type !== 'chat.bsky.convo.defs#deletedMessageView')
.
Lavorare con gli URI AT
Il protocollo AT utilizza URI nel formato at://did/collection/rkey per identificare i record. Comprendere questo formato è fondamentale per lavorare con Messaggi diretti di Bluesky:
interface ATUri {
did: string;
collection: string;
rkey: string;
}
function parseATUri(uri: string): ATUri {
// Formato: at://did:plc:xxx/app.bsky.feed.post/abc123
const match = uri.match(/^at:\/\/(did:[^/]+)\/([^/]+)\/(.+)$/);
if (!match) {
throw new Error(`URI AT non valido: ${uri}`);
}
return {
did: match[1],
collection: match[2],
rkey: match[3],
};
}
function buildATUri(did: string, collection: string, rkey: string): string {
return `at://${did}/${collection}/${rkey}`;
}
// Estrai rkey per operazioni di cancellazione
function extractRkey(uri: string): string {
const parts = uri.split('/');
return parts[parts.length - 1];
}
Gestione degli Errori e Limiti di Frequenza
The Bluesky API ha limiti di frequenza che variano a seconda dell'endpoint. Implementa una corretta gestione degli errori e un backoff esponenziale:
```typescript
interface RateLimitInfo {
limit: number;
remaining: number;
reset: Date;
}
class BlueskyApiError extends Error {
constructor(
message: string,
public statusCode: number,
public errorType: 'rate-limit' | 'auth' | 'validation' | 'server' | 'unknown',
public rateLimitInfo?: RateLimitInfo
) {
super(message);
this.name = 'BlueskyApiError';
}
}
function parseApiError(response: Response, body: string): BlueskyApiError {
const lowerBody = body.toLowerCase();
// Limitazioni di velocità
if (response.status === 429 || lowerBody.includes('rate limit')) {
const resetHeader = response.headers.get('ratelimit-reset');
return new BlueskyApiError(
'Limite di velocità superato. Attendere prima di riprovare.',
response.status,
'rate-limit',
resetHeader ? {
limit: parseInt(response.headers.get('ratelimit-limit') || '0'),
remaining: 0,
reset: new Date(parseInt(resetHeader) * 1000),
} : undefined
);
}
// Errori di autenticazione
if (
response.status === 401 ||
lowerBody.includes('invalid token') ||
lowerBody.includes('expired')
) {
return new BlueskyApiError(
'Autenticazione fallita. Riconnetti il tuo account.',
response.status,
'auth'
);
}
// Errori di validazione
if (response.status === 400 || lowerBody.includes('invalid')) {
return new BlueskyApiError(
`Errore di validazione: ${body}`,
response.status,
'validation'
);
}
// Errori del server
if (response.status >= 500) {
return new BlueskyApiError(
'Servizio Bluesky temporaneamente non disponibile.',
response.status,
'server'
);
}
return new BlueskyApiError(body, response.status, 'unknown');
}
async function withRetry(
operation: () => Promise,
maxRetries: number = 3
): Promise {
let lastError: Error | null = null;
for (let attempt = 0; attempt
| Tipo di errore | Codice di Stato | Strategia di Ripetizione |
|---|---|---|
| Limite di Richiesta | 429 | Attendi il reset dell'intestazione, poi riprova. |
| Token scaduto | 401 | Aggiorna il token, poi riprova. |
| Validation | 400 | Non riprovare, correggi la richiesta. |
| Errore del server | 5xx | Ritardo esponenziale, massimo 3 tentativi |
Utilizzare Late per l'integrazione con Bluesky
Costruire e mantenere integrazioni dirette con il API DM di Bluesky richiede la gestione della risoluzione PDS, della gestione dei token, della gestione degli errori e dell'adeguamento ai cambiamenti del protocollo. Questa complessità si moltiplica quando è necessario supportare più piattaforme social.
Late fornisce un'API unificata che semplifica queste complessità. Invece di gestire integrazioni separate per Bluesky, Twitter, Instagram e altre piattaforme, puoi utilizzare un'unica interfaccia coerente:
```javascript
// Con l'API unificata di Late
import { Late } from '@late/sdk';
const late = new Late({ apiKey: process.env.LATE_API_KEY });
// Invia un messaggio su qualsiasi piattaforma supportata
await late.messages.send({
platform: 'bluesky',
accountId: 'il-tuo-account-collegato',
conversationId: 'convo-123',
text: 'Ciao da Late!',
});
// Elenca le conversazioni con aggiornamento automatico del token
const { conversations } = await late.messages.listConversations({
platform: 'bluesky',
accountId: 'il-tuo-account-collegato',
});
```
Late si occupa delle sfide infrastrutturali per te:
- Risoluzione PDS AutomaticaNon è necessario interrogare tu stesso il directory PLC.
- Gestione dei TokenAggiornamento automatico e archiviazione sicura
- Gestione dei Limiti di RichiestaLogica di riprova integrata con backoff esponenziale
- Normalizzazione degli ErroriFormati di errore coerenti su tutte le piattaforme
- Supporto WebhookNotifiche in tempo reale per nuovi messaggi
Che tu stia sviluppando uno strumento di gestione dei social media, un sistema di supporto clienti o una piattaforma comunitaria, l'approccio unificato di Late ti consente di concentrarti sul tuo prodotto invece di occuparti della manutenzione dell'API.
Scopri il Documentazione di Late per iniziare con l'integrazione della messaggistica di Bluesky in pochi minuti, non giorni.