Bluesky DM API: Nachrichtenübermittlung mit dem AT-Protokoll
The Bluesky API bietet eine leistungsstarke Grundlage für den Aufbau sozialer Anwendungen, und die Funktionen für Direktnachrichten eröffnen Entwicklern spannende Möglichkeiten. Im Gegensatz zu traditionellen sozialen Plattformen mit proprietären APIs bietet Bluesky AT-Protokoll Nachrichten Das System basiert auf offenen Standards, die Ihnen die volle Kontrolle darüber geben, wie Sie Chat-Funktionen implementieren.
In diesem Leitfaden erfahren Sie, wie Sie integrieren Bluesky Direktnachrichten in Ihre Anwendungen mit TypeScript. Wir behandeln alles von der Authentifizierung über das Senden von Nachrichten, die Verarbeitung von Rich Text bis hin zur Implementierung von Echtzeit-Updates.
Verstehen der dezentralen Architektur
Bevor Sie mit dem Programmieren beginnen, ist es wichtig zu verstehen, wie sich das Messaging-System von Bluesky von zentralisierten Plattformen unterscheidet. Das AT-Protokoll trennt die Anliegen über mehrere Dienste hinweg:
| Component | Purpose | Endpoint |
|---|---|---|
| PDS (Persönlicher Datenserver) | Speichert Benutzerdaten und verwaltet die Authentifizierung | Benutzerspezifisch (über PLC gelöst) |
| Chat-Service | Verwaltet die Weiterleitung und Speicherung von Direktnachrichten. | api.bsky.chat |
| PLC-Verzeichnis | Löst DIDs in Dienstendpunkte auf | plc.directory |
| AppView | Öffentliche Feed-Aggregation | bsky.social |
The Bluesky DM API leitet Anfragen über deinen Personal Data Server (PDS) weiter, der sie dann an den Chatdienst weiterleitet. Diese Architektur bedeutet, dass du nicht einfach einen einzelnen Endpunkt ansteuern kannst. Stattdessen musst du zuerst den PDS des Nutzers auflösen.
interface BlueskySession {
accessJwt: string;
refreshJwt: string;
did: string;
handle: string;
}
async function resolvePdsBaseUrl(userDid: string): Promise {
try {
const plcResponse = await fetch(
`https://plc.directory/${encodeURIComponent(userDid)}`
);
if (!plcResponse.ok) {
throw new Error(`PLC-Abfrage fehlgeschlagen: ${plcResponse.status}`);
}
const plcDoc = await plcResponse.json();
const services = plcDoc?.service || [];
const pdsService = services.find((service: any) =>
service?.type?.toLowerCase().includes('atprotopersonaldataserver') ||
service?.id?.includes('#atproto_pds')
);
if (pdsService?.serviceEndpoint) {
return pdsService.serviceEndpoint.replace(/\/$/, '');
}
throw new Error('Kein PDS-Endpunkt im DID-Dokument gefunden');
} catch (error) {
console.warn(`PDS für ${userDid} konnte nicht aufgelöst werden:`, error);
return 'https://bsky.social'; // Fallback auf Standard
}
}
Hinweis: Der Schritt zur PDS-Resolution ist entscheidend. Das Überspringen dieses Schrittes führt zu Authentifizierungsfehlern beim Zugriff auf den Chat-Service, insbesondere für Nutzer von selbst gehosteten PDS-Instanzen.
Authentifizierung mit App-Passwörtern
The Bluesky API verwendet App-Passwörter zur Authentifizierung anstelle von OAuth 2.0. Für den Zugriff auf DMs müssen Sie ein App-Passwort mit aktivierten spezifischen Nachrichtenberechtigungen erstellen.
interface AuthConfig {
identifier: string; // Benutzername oder E-Mail
password: string; // App-Passwort (nicht das Hauptpasswort)
}
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(`Authentifizierung fehlgeschlagen: ${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('Aktualisierung der Sitzung fehlgeschlagen. Bitte erneut authentifizieren.');
}
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 Fehler beim Aufrufen der Chat-Endpunkte.
PDS-Auflösungs- und Chatdienst
Der Chatdienst benötigt einen speziellen Proxy-Header, um Anfragen korrekt weiterzuleiten. Hier ist eine vollständige Implementierung des Chat-Anforderungshandlers:
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;
// Benutzer-PDS-Endpunkt auflösen
const pdsBaseUrl = await resolvePdsBaseUrl(userDid);
const url = new URL(`${pdsBaseUrl}/xrpc/${endpoint}`);
// Abfrageparameter für GET-Anfragen hinzufügen
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();
// Token-Ablauf mit automatischer Erneuerung behandeln
if (refreshToken && errorBody.includes('ExpiredToken')) {
const newSession = await refreshSession(refreshToken);
// Mit neuem Token erneut versuchen
return chatRequest({
...options,
accessToken: newSession.accessJwt,
refreshToken: newSession.refreshJwt,
});
}
// Hilfreiche Fehlermeldungen bereitstellen
if
Auflistung von Konversationen mit der Bluesky DM API
Jetzt lassen Sie uns die Kernfunktionalität für das Auflisten von Konversationen implementieren. AT-Protokoll Nachrichten Das System gibt Gespräche mit Teilnehmerinformationen und der letzten Nachricht zurück:
interface Teilnehmer {
did: string;
handle: string;
anzeigeName?: string;
avatar?: string;
}
interface Nachricht {
id: string;
text: string;
gesendetAm: string;
absender: {
did: string;
};
}
interface Unterhaltung {
id: string;
rev: string;
ungelesenAnzahl: number;
stumm: boolean;
teilnehmer: Teilnehmer[];
letzteNachricht: Nachricht | null;
}
interface ListUnterhaltungenOptionen {
limit?: number;
cursor?: string;
leseStatus?: 'ungelesen';
status?: 'anfrage' | 'akzeptiert';
}
async function listUnterhaltungen(
accessToken: string,
userDid: string,
optionen: ListUnterhaltungenOptionen = {}
): Promise<{ unterhaltungen: Unterhaltung[]; cursor?: string }> {
const params: Record = {
limit: optionen.limit || 50,
};
if (optionen.cursor) params.cursor = optionen.cursor;
if (optionen.leseStatus) params.leseStatus = optionen.leseStatus;
if (optionen.status) params.status = optionen.status;
const result = await chatRequest({
accessToken,
method: 'GET',
endpoint: 'chat.bsky.convo.listConvos',
params,
userDid,
});
const unterhaltungen = (result.data.convos || []).map((convo: any) => ({
id: convo.id,
rev: convo.rev,
ungelesenAnzahl: convo.unreadCount || 0,
stumm: convo.muted || false,
teilnehmer: (convo.members || []).map((mitglied: any) => ({
did: mitglied.did,
handle: mitglied.handle,
anzeigeName: mitglied.displayName,
avatar: mitglied.avatar,
})),
letzteNachricht: convo.lastMessage ? {
id: convo.lastMessage.id,
text: convo.lastMessage.text,
gesendetAm: convo.lastMessage.sentAt,
absender: { did: convo.lastMessage.sender?.did },
} : null,
}));
return {
unterhaltungen,
cursor: result.data.cursor,
};
}
// Beispiel für die Nutzung
async function displayPosteingang
Bluesky Direktnachrichten senden
Nachrichten über die Bluesky DM API erfordert die Konversations-ID. Sie können entweder eine bestehende Konversation verwenden oder eine neue mit bestimmten Teilnehmern erstellen:
interface SendMessageResult {
id: string;
rev: string;
text: string;
sentAt: string;
}
async function sendMessage(
accessToken: string,
userDid: string,
conversationId: string,
text: string
): Promise<SendMessageResult> {
const result = await chatRequest<any>({
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<any>({
accessToken,
method: 'GET',
endpoint: 'chat.bsky.convo.getConvoForMembers',
params: { members: memberDids },
userDid,
});
return {
id: result.data.convo.id,
rev: result.data.convo.rev,
};
}
// Vollständiges Beispiel: Sende eine DM an einen Nutzer über seinen Handle
async function sendDirectMessage(
session: BlueskySession,
recipientHandle: string,
messageText: string
): Promise<SendMessageResult> {
// Zuerst den DID des Empfängers anhand seines Handles auflösen
const resolveResponse = await fetch(
`https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(recipientHandle)}`
);
if (!resolveResponse.ok) {
throw new Error(`Handle konnte nicht aufgelöst werden: ${recipientHandle}`);
}
const { did: recipientDid } = await resolveResponse.json();
// Konversation abrufen oder erstellen
const conversation = await getOrCreateConversation(
session.accessJwt,
session.did,
[recipientDid]
);
// Nachricht senden
return sendMessage(
session.accessJwt,
Reicher Text und Facetten
Das AT-Protokoll unterstützt Rich Text über „Facetten“, das sind Anmerkungen, die Links, Erwähnungen und Hashtags zu einfachem Text hinzufügen. Beim Erstellen von Nachrichten müssen Sie Byte-Offsets (nicht Zeichen-Offsets) berechnen, um eine korrekte Darstellung zu gewährleisten:
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[] = [];
// URLs parsen
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;
// Zeichenpositionen in Byte-Positionen umwandeln
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,
}],
});
}
// Erwähnungen parsen (@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, // Wird auf die tatsächliche DID aufgelöst
}],
});
}
// Nach Position sortieren
facets.sort((a, b) => a.index.byteStart - b.index.byteStart);
return facets;
}
// Erwähnungs-Handles in DIDs auflösen
async function resolveMentionDids(
accessToken: string
Hinweis: Die Berechnung des Byte-Offsets ist entscheidend für nicht-ASCII-Zeichen. Ein einzelnes Emoji kann 4 Bytes groß sein, daher führen die Verwendung von Zeichenindizes zu Problemen bei der Darstellung von Rich Text.
Echtzeit-Updates
Für Echtzeit-Nachrichtenaktualisierungen müssen Sie Polling implementieren oder den Event-Stream des AT-Protokolls nutzen. Hier ist eine praktische Implementierung des Pollings:
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, // Standard 5 Sekunden
...options,
};
}
async start(): Promise {
// Initiale Nachrichten abrufen, um eine Basis festzulegen
const { messages } = await this.fetchMessages();
if (messages.length > 0) {
this.lastMessageId = messages[0].id;
}
this.intervalId = setInterval(async () => {
try {
const { messages } = await this.fetchMessages();
// Neue Nachrichten finden
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;
// Chronologische Zustellung
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')
.map((msg: any) => Arbeiten mit AT-URIs
Das AT-Protokoll verwendet URIs im Format at://did/collection/rkey um Datensätze zu identifizieren. Dieses Format zu verstehen, ist entscheidend für die Arbeit mit Bluesky Direktnachrichten:
interface ATUri {
did: string;
collection: string;
rkey: string;
}
function parseATUri(uri: string): ATUri {
// Format: at://did:plc:xxx/app.bsky.feed.post/abc123
const match = uri.match(/^at:\/\/(did:[^/]+)\/([^/]+)\/(.+)$/);
if (!match) {
throw new Error(`Ungültige AT-URI: ${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}`;
}
// Extrahiere rkey für Löschoperationen
function extractRkey(uri: string): string {
const parts = uri.split('/');
return parts[parts.length - 1];
}
Fehlerbehandlung und Ratenlimits
The Bluesky API hat Ratenlimits, die je nach Endpunkt variieren. Implementieren Sie eine angemessene Fehlerbehandlung und exponentielles Backoff:
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();
// Ratenbegrenzung
if (response.status === 429 || lowerBody.includes('rate limit')) {
const resetHeader = response.headers.get('ratelimit-reset');
return new BlueskyApiError(
'Die Ratenbegrenzung wurde überschritten. Bitte warten Sie, bevor Sie es erneut versuchen.',
response.status,
'rate-limit',
resetHeader ? {
limit: parseInt(response.headers.get('ratelimit-limit') || '0'),
remaining: 0,
reset: new Date(parseInt(resetHeader) * 1000),
} : undefined
);
}
// Authentifizierungsfehler
if (
response.status === 401 ||
lowerBody.includes('invalid token') ||
lowerBody.includes('expired')
) {
return new BlueskyApiError(
'Authentifizierung fehlgeschlagen. Bitte verbinden Sie Ihr Konto erneut.',
response.status,
'auth'
);
}
// Validierungsfehler
if (response.status === 400 || lowerBody.includes('invalid')) {
return new BlueskyApiError(
`Validierungsfehler: ${body}`,
response.status,
'validation'
);
}
// Serverfehler
if (response.status >= 500) {
return new BlueskyApiError(
'Der Bluesky-Dienst ist vorübergehend nicht verfügbar.',
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;
| Fehlerart | Statuscode | Wiederholungsstrategie |
|---|---|---|
| Rate-Limitierung | 429 | Warten Sie auf den Reset-Header und versuchen Sie es dann erneut. |
| Abgelaufenes Token | 401 | Aktualisieren Sie das Token und versuchen Sie es erneut. |
| Validation | 400 | Anfrage nicht erneut versuchen, bitte korrigieren. |
| Serverfehler | 5xx | Exponentielles Backoff, maximal 3 Wiederholungen |
Bluesky-Integration mit Late nutzen
Aufbau und Pflege direkter Integrationen mit der Bluesky DM API erfordert die Handhabung von PDS-Resolution, Token-Management, Fehlerbehandlung und das Mitverfolgen von Protokolländerungen. Diese Komplexität vervielfacht sich, wenn Sie mehrere soziale Plattformen unterstützen müssen.
Late bietet eine einheitliche API, die diese Komplexitäten abstrahiert. Anstatt separate Integrationen für Bluesky, Twitter, Instagram und andere Plattformen zu verwalten, können Sie eine einzige, konsistente Schnittstelle nutzen:
```javascript
// Mit der einheitlichen API von Late
import { Late } from '@late/sdk';
const late = new Late({ apiKey: process.env.LATE_API_KEY });
// Eine Nachricht über jede unterstützte Plattform senden
await late.messages.send({
platform: 'bluesky',
accountId: 'dein-verbundenes-konto',
conversationId: 'convo-123',
text: 'Hallo von Late!',
});
// Gespräche mit automatischer Token-Aktualisierung auflisten
const { conversations } = await late.messages.listConversations({
platform: 'bluesky',
accountId: 'dein-verbundenes-konto',
});
```
Late kümmert sich um die Infrastruktur-Herausforderungen für Sie:
- Automatische PDS-AuflösungEs ist nicht notwendig, das PLC-Verzeichnis selbst abzufragen.
- TokenverwaltungAutomatische Aktualisierung und sichere Speicherung
- Umgang mit Rate LimitsIntegrierte Wiederholungslogik mit exponentiellem Backoff
- FehlernormalisierungKonsistente Fehlerformate über alle Plattformen hinweg
- Webhook-UnterstützungEchtzeitbenachrichtigungen für neue Nachrichten
Egal, ob Sie ein Social-Media-Management-Tool, ein Kundenservicetool oder eine Community-Plattform entwickeln, der einheitliche Ansatz von Late ermöglicht es Ihnen, sich auf Ihr Produkt zu konzentrieren, anstatt sich um die API-Wartung zu kümmern.
Schau dir die an Late-Dokumentation um in Minuten, nicht Tagen, mit der Bluesky-Nachrichtenintegration zu beginnen.