Zum Inhalt springen

Bluesky-Anbindung (technische Umsetzung)

Stand: 2026-11-02. Was gilt, regelt die Verordnung über die Interoperabilität (VO). Dieses Dokument beschreibt, wie sie umgesetzt ist.

Alle drei Wege sind im Code, in test ausgeschaltet (kein Relay, APPVIEW_CROSSPOST_ENABLED=false, BSKY_POLL_ENABLED=false). In prod nach FEDERATION.md. Föderation der Teilnahme (fremde PDS, weitere Clients): ROADMAP.md.

atvoto-Records (ch.atvoto.*) sind für Bluesky unsichtbar: bsky.app zeigt nur app.bsky.*. Die Anbindung läuft deshalb über Spiegelposts.

Crosspost (Writer) Poller (Indexer)
Argument ─────────────────────► app.bsky.feed.post ─────────────────► app_comments
(kanonische Kopie, im Community-Konto Antworten auf (origin = 'extern')
Community-Konto) @dossier-<id>.… Bluesky
▲
│ getFeedSkeleton
Feed-Generator (AppView)
  • Wer: Writer (services/writer/src/atproto/crosspost.py), Hintergrund-Schleife.
  • Was: jedes Argument ohne bsky_post_uri als app.bsky.feed.post unter dem Community-Konto des Dossiers ([PRO] Titel + Text, max. 300 Zeichen; bei laufendem Peer Review mit [Preliminary]). URI und CID landen in app_arguments.bsky_post_*.
  • Nicht: Dossiers selbst (CMS-Inhalt), Kommentare, Bewertungen.
  • Steuerung: APPVIEW_CROSSPOST_ENABLED (Standard false), APPVIEW_CROSSPOST_POLL_INTERVAL_SECONDS (Standard 30).
  • Wer: Indexer (services/indexer/src/bsky_poller.js).
  • Was: für jedes gespiegelte Argument app.bsky.feed.getPostThread über die öffentliche Bluesky-API (Tiefe 10); Antworten werden als Kommentare mit origin = 'extern' in app_comments übernommen, Antwortzähler in app_arguments.bsky_reply_count. Ältere Argumente (> 48 h) seltener.
  • Steuerung: BSKY_POLL_ENABLED (Standard false), BSKY_POLL_INTERVAL_MS (Standard 10 min).
  • Grenzen: keine Zitat-Posts, Tiefe max. 10, Verzögerung bis zum Poll-Intervall.
  • Wer: AppView (services/appview/src/routes/atproto/feed.py), app.bsky.feed.describeFeedGenerator und getFeedSkeleton.
  • Was: die gespiegelten Posts (Argumente und externe Kommentare mit bsky_post_uri), neueste zuerst. Bluesky hydriert die URIs selbst.
  • Identität: did:web der AppView (APPVIEW_FEED_GENERATOR_DID), Dienst #bsky_fg im DID-Dokument.
  • Noch zu tun für prod: den Feed als Record app.bsky.feed.generator in einem Konto veröffentlichen (FEED_ACCOUNT_DID). Der rkey ist atvoto (FEED_URI); nach dem Veröffentlichen nicht mehr ändern.

Weitere app.bsky.*-Aufrufe (Timeline, Profile, Threads) reicht die AppView an Bluesky weiter.

  • Konten von atvoto können sich bei Bluesky anmelden, wenn sie ein App-Passwort haben. smartinfo stellt keines für die Person aus (nur client:<id> für sich selbst).
  • Bluesky-Konten können bei smartinfo nicht mitreden (VO Konten und Teilnahme § 1). Die Möglichkeit für Clients: ROADMAP Stufe 1.

Stand 2026-11-01. Technischer Hintergrund zu den Diskussionspunkten der VO. Code unverändert.

Variante In bsky.app sichtbar Eignung
Spiegelpost mit Link-Card (app.bsky.embed.external) ja, Post mit Vorschaukarte Argumente
Custom Record einbetten (app.bsky.embed.record auf ch.atvoto.*) nein, Bluesky rendert fremde Typen nicht –
standard.site (site.standard.document) nicht als Post; nur Reader-Apps (Leaflet u.a.) bzw. Link-Card beim Teilen der URL Langtexte, eher Dossiers (CMS)
Post im Repo des Autors statt der Community ja, im Profil der Person Kommentare, nur mit Opt-in

Vorschlag für den Argument-Spiegel:

  • Text nur [PRO] Titel, dazu embed.external auf die Argument-Seite des Clients (Titel, Anriss, Bild). Diskussion zu VO § 3; braucht eine Basis-URL als Client-Setting und OG-Tags (og:title, og:description, og:image) im Client.
  • Rückverweis als Zusatzfeld im Post, z.B. "ch.atvoto.source": {uri, cid}. Lexicon-Objekte sind offen; Bluesky ignoriert das Feld, Poller und Dritte können damit zuordnen.

Kommentare nicht unter dem Community-Konto spiegeln (spricht im Namen der Person, Autorschaft geht verloren). Höchstens als Opt-in im Repo der Person, als app.bsky.feed.post mit reply: {root, parent} auf den Argument-Spiegel. Ob das erlaubt ist, regelt der Client (bei smartinfo heikel: politische Meinung öffentlich im eigenen Profil).

Client Community-PDS Bluesky-Netz
────── ───────────── ────────────
1. Argument ──Writer──► ch.atvoto.dossier.argument
app.bsky.feed.post (Spiegel) ────► Feed, Link-Card
2. Kommentar ──► User-Repo: ch.atvoto.dossier.comment
(opt-in) Spiegel als Reply ───────────────► Antwort im Thread
3. Bluesky-User antwortet:
app.bsky.feed.post im EIGENEN PDS
reply.root = Argument-Spiegel
reply.parent = Spiegel / andere Antwort
│
Poller / Jetstream◄┘
▼
AppView: app_comments (origin='extern')
Community-PDS: Optionen unten

Die Antwort ist bereits ein ATProto-Record im PDS der antwortenden Person. Niemand sonst kann ihn dort schreiben oder ändern. Offen ist nur, ob das Community-PDS zusätzlich etwas schreibt:

  1. Nur AppView (Ist-Zustand). Antworten nur in app_comments. ➕ einfach, Hoheit bleibt bei der Person. ➖ Dritt-AppViews sehen die Auswahl nicht; Zugehörigkeit nur in der DB; Poller träge und erkennt Löschungen schlecht.
  2. Kopie ins Community-PDS (Muster wie Argumente: ch.atvoto.dossier.comment mit source: extern, originUri, originCid). ➕ Community-Repo vollständig. ➖ veröffentlicht Inhalte von Nicht-Mitgliedern ohne Zustimmung (VO Konten und Teilnahme § 1, Datenschutz); Löschungen müssen nachgezogen werden; Kopie wirkt wie von der Community geprüft, lief aber nie durch participation_check. Eher vermeiden.
  3. Nur Verweis ins Community-PDS, z.B. ch.atvoto.dossier.externalReply {argument, subject: {uri, cid}, status}, ohne Text. ➕ Dritt-AppViews sehen dieselbe kuratierte Auswahl; Moderation nachvollziehbar (status: hidden); Löschung an der Quelle wirkt überall. ➖ zusätzliches Lexicon, ein Write pro Antwort.

Unabhängig davon der Eingang:

  • Poller (heute): keine Infrastruktur, aber Verzögerung, Tiefe 10, Löschungen nur über Abgleich.
  • Jetstream/Firehose: app.bsky.feed.post filtern, deren reply.root eine Spiegel-URI ist. Echtzeit, Löschungen als delete-Event.

Threading: Werden Kommentare gespiegelt, antworten Leute auch auf diese Spiegel (reply.parent = Kommentar-Spiegel). Siehe T4.

VO § 5 Abs. 4 hält Option 1 fest; Option 3 bleibt Diskussionspunkt, Option 2 ist zu vermeiden. Eingang: Poller (T2).

Ziel: In einem Bluesky-Feed die Diskussion im Client verfolgen, dort antworten und bewerten; die Antworten erscheinen wieder im Client. Die Optionen aus 5.2 betreffen nur, was zusätzlich im Community-PDS steht. Der Rückweg in den Client läuft in allen drei über die AppView.

Schritt mit Option 1 Wie
Argumente im Bluesky-Feed ✅ Crosspost + Feed-Generator
In Bluesky antworten ✅ normale Antwort auf den Spiegel, im PDS der Person
Antworten erscheinen im Client ✅ Poller → app_comments (origin='extern'); Anzeige ist Client-Regel
Likes aus Bluesky teilweise externe Kommentare: app_comments.bsky_like_count vorhanden; Argumente: Spalte fehlt (T3)

Lücken, unabhängig von der Option:

  1. Client-Kommentare fehlen in Bluesky. Gespiegelt wird nur das Argument; wer in Bluesky mitliest, sieht Argument und Bluesky-Antworten, die Diskussion im Client nur hinter der Link-Card. Für einen gemeinsamen Thread müssten Kommentare als Replies gespiegelt werden: unter dem Community-Konto (einfach, spricht aber im Namen der Person; nur wenn die Client-Regeln das offenlegen) oder als Opt-in im Repo der Person (siehe 5.1).
  2. Bewerten heisst in Bluesky nur Like. Kein PRO/CONTRA, keine Skala, kein ch.atvoto.content.rating, keine Teilnahmeregeln. Im Client getrennt anzeigen („♥ 12 auf Bluesky“), nicht mit eigenen Bewertungen vermischen.
  3. Relay nötig. Spiegelposts erscheinen in Bluesky nur, wenn der Bluesky-Relay das Community-PDS crawlt. In test gibt es keinen Relay; für prod Voraussetzung (FEDERATION.md, T1).

Option 3 schliesst keine der Lücken aus 5.3. Sie ändert nur, wo die Auswahl externer Antworten festgehalten ist: in der DB der AppView oder signiert im Community-Repo.

Besser:

  • Dritt-AppViews und -Clients sehen dieselbe Auswahl ohne Zugriff auf die DB.
  • Die AppView kann die Zuordnung aus dem Community-Repo neu aufbauen statt aus Poller-Stand oder Backup.
  • Moderation ist nachvollziehbar: „ausgeblendet“ ist ein signierter Record, keine stille DB-Änderung.

Schlechter:

  • Personenbezug bleibt. Auch ohne Text steht öffentlich „DID X hat auf Argument Y geantwortet“; bei politischen Themen lässt das Rückschlüsse zu. Löscht die Person ihren Post, muss der Verweis mitgelöscht werden.
  • Ausblenden wird öffentlich. status: hidden zeigt jedem, wessen Antwort ausgeblendet wurde: transparent, aber Angriffsfläche für Zensurvorwürfe und blossstellend für die Person.
  • Jemand muss annehmen. Bei automatischer Annahme spiegelt der Record nur die DB (wenig Mehrwert); bei echter Auswahl braucht es einen Prozess.
  • Aufwand: zusätzliches Lexicon, ein Write pro Antwort, wachsende Community-Repos, Konsistenz zwischen DB und Records.

Fazit: Option 3 erst, wenn es tatsächlich Dritt-AppViews mit Bedarf an derselben kuratierten Sicht gibt oder Moderation bewusst öffentlich prüfbar sein soll. Für Client ↔ Bluesky allein bringt sie Nutzern nichts Sichtbares.

Stand 2026-11-02.

VO Inhalt Stand im Code
§ 3 Abs. 1–2, 4 Argument-Spiegel unter Community-Konto, PRO/CONTRA, vorläufig; Feed umgesetzt (§§ 1, 3 oben)
§ 3 Abs. 3 Spiegel beim Löschen/Zurückziehen entfernen fehlt (T5)
§ 4 Kommentare nicht spiegeln Ist-Zustand
§ 5 Abs. 1–2 externe Antworten eingeklappt anzeigen, nicht als Kommentare zählen, Hinweis «extern, nicht bei smartinfo registriert» Poller schreibt origin='extern'; Anzeige im Client offen
§ 5 Abs. 3 Löschung an der Quelle übernehmen fehlt, Poller erkennt Löschungen nicht (T6)
§ 5 Abs. 4 nichts ins Community-Konto Ist-Zustand (Option 1)
§ 6 Bluesky-Likes getrennt anzeigen, nicht in like_count teilweise (T3)
§ 7 Ausblenden in der Ansicht MODERATION.md
§ 8 Relay T1
  • T1 Relay ist Bedingung (VO § 8). Ohne Relay, der das Community-PDS crawlt, ist nichts davon in Bluesky sichtbar. Vor dem Einschalten von APPVIEW_CROSSPOST_ENABLED in prod muss das PDS am Relay hängen (FEDERATION.md).
  • T2 Eingang bleibt der Poller. Jetstream/Firehose ist Option für später (Echtzeit, Löschungen als Event), heute nicht nötig.
  • T3 Neue Spalte app_arguments.bsky_like_count (VO § 6). Der Poller holt likeCount der Argument-Spiegel bereits über getPosts, verwirft ihn aber. Externe Kommentare haben bsky_like_count schon, aktualisiert aber nur, wenn der Thread neu geladen wird (bei geänderter Antwortzahl); auch die Like-Zahlen der Kommentare über getPosts auffrischen.
  • T4 Gespiegelte Kommentare (nur falls VO § 4 geändert wird): kein neues Feld, app_comments.bsky_post_uri/bsky_post_cid gibt es schon. Der Crosspost setzt sie bei internen Kommentaren; der Poller bildet reply.parent über bsky_post_uri auf den nativen Kommentar ab statt auf die Bluesky-URI.
  • T5 Spiegel löschen (VO § 3 Abs. 3). deleteArgument im Indexer gibt bsky_post_uri zurück, aber niemand löscht den Post im Community-Konto. Der Writer muss ihn bei Löschung und Rückzug entfernen.
  • T6 Löschungen an der Quelle (VO § 5 Abs. 3). Der Poller sieht nur vorhandene Antworten. Fehlende Antworten bei einem vollständigen Thread-Abruf als gelöscht markieren (notFoundPost oder nicht mehr im Thread). Mit Jetstream kämen Löschungen als Event.

Die normativen Fragen stehen als Diskussionspunkte in der VO (§§ 3, 4, 5, 7). Technisch offen:

  1. Teilnahmeregel für externe Antworten (VO § 5). Antworten aus Bluesky kommen über den Poller, nicht über den Firehose, und laufen deshalb nicht durch participation_check. Umsetzung z.B. als Aktion comment.extern oder members: public (ROADMAP „Später“).
  2. Form des Argument-Spiegels (VO § 3): embed.external braucht eine Basis-URL als Client-Setting und OG-Tags im Client; Rückverweisfeld ch.atvoto.source im Post (5.1).
  3. Einklappen nicht prüfbar (VO § 5 Abs. 1–2, Stand 2026-11-06): Ohne externe Antworten gibt es dafür auf dem Testcluster nichts zu sehen, und auch keine Änderung für Nutzende.

APPVIEW_CROSSPOST_ENABLED=false Es wird nicht nach Bluesky gepostet (crosspost.py:113).