Blog

Bluesky DM API: Nachrichtenübermittlung mit dem AT-Protokoll

Erfahren Sie, wie Sie Direktnachrichtenfunktionen mit der Bluesky API und dem AT-Protokoll erstellen. Umfassender Leitfaden mit TypeScript-Beispielen für die Ch

Von

+8

Überall posten. Eine API.

Try Free

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:

ComponentPurposeEndpoint
PDS (Persönlicher Datenserver)Speichert Benutzerdaten und verwaltet die AuthentifizierungBenutzerspezifisch (über PLC gelöst)
Chat-ServiceVerwaltet die Weiterleitung und Speicherung von Direktnachrichten.api.bsky.chat
PLC-VerzeichnisLöst DIDs in Dienstendpunkte aufplc.directory
AppViewÖffentliche Feed-Aggregationbsky.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,

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

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;
FehlerartStatuscodeWiederholungsstrategie
Rate-Limitierung429Warten Sie auf den Reset-Header und versuchen Sie es dann erneut.
Abgelaufenes Token401Aktualisieren Sie das Token und versuchen Sie es erneut.
Validation400Anfrage nicht erneut versuchen, bitte korrigieren.
Serverfehler5xxExponentielles 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.

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.