Nya House

Nya | @Nya@nya.house

@blobcat ablobcatbongo another blobcat

@icedquinn blobcatgooglycry i think njal.la is way more unstable lately... will change dns next time if its online

i don't like it when blob.cat is unstable... i might change dns to my home next time its up

dafuck nja.la

@puniko blobcatpat das soll dein kopp aber nich

@icedquinn i didnt blobcatgoogly2

> jain wakes up
> blob dies again

blobcataco cat rolled over on the network cable lmao

blobcatoh it looks like njal.la has been having some problems lately... blob.cat is down blobcatsad

@stephanmaus @Jain i know, thx blobcat3c

How much ActivityPub can a Static Site Generator implement?

Es gibt bereits verschiedene Blog-Lösungen, die Teil des Fediverse sind: Darunter sind sowohl dedizierte Fediverse-Blogs wie Plume und WriteFreely, aber es gibt auch Plugins, die bestehende CMS nachrĂŒsten, z.B. fĂŒr Wordpress oder fĂŒr Drupal.

ActivityPub, das Netzwerk-Protokoll hinter dem Fediverse, lĂ€sst sich nur mit einer aktiven Serverkomponente vollstĂ€ndig umsetzen: Unter anderem muss auf eingehende Nachrichten in den Inboxen reagiert werden, teilweise mĂŒssen diese auch an andere Server weitergeleitet werden, und ausgehende Nachrichten mĂŒssen zeitnah signiert werden.

Die grundsÀtzlichen Mechanismen hinter ActivityPub: User veröffentlichen Ressourcen in ihrer eigenen Outbox, und rufen empfangene Ressourcen aus ihrer Inbox ab. Abbildung 1: Die grundsÀtzlichen Mechanismen hinter ActivityPub: User veröffentlichen Ressourcen in ihrer eigenen Outbox, und rufen empfangene Ressourcen aus ihrer Inbox ab.

Trotzdem wollte ich herausfinden, welche Teile des ActivityPub-Protokolls mit einer rein statischen Website implementiert werden können, und wie gut andere Server im Fediverse damit umgehen können. Mein Ziel war, meinen Blog hier ans Fediverse zu hÀngen. Der Blog wird mit der Static Site Generator-Software Pelican erzeugt.

Metadaten-Endpunkte

Eine Grundvoraussetzung, um ActivityPub zu implementieren, sind diverse statische Metadaten-Endpunkte, mit denen der Server signalisiert, dass er ActivityPub unterstĂŒtzt, und unter welchen HTTP-Endpunkten die verschiedenen ActivityPub-Ressourcen zu finden sind:

/.well-known/nodeinfo verlinkt einfach nur auf den "echten" nodeinfo-Endpunkt:

{
  "links": [
    {
      "href": "https://s3lph.me/activitypub/nodeinfo",
      "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0"
    }
  ]
}

Unter dem verlinkten Pfad (der frei gewÀhlt werden kann), werden die globalen Metadaten der ActivityPub-Instanz publiziert:

{
  "version": "2.0",
  "software": {
    "name": "pelican-activitypub",
    "version": "0.1"
  },
  "protocols": [
    "activitypub"
  ],
  "services": {
    "inbound": [],
    "outbound": [
      "atom1.0",
      "rss2.0"
    ]
  },
  "openRegistrations": false,
  "usage": {
    "users": {
      "total": 1
    },
    "localPosts": 27
  },
  "metadata": {
    "nodeName": "s3lph made"
  }
}

Mit diesem JSON-Dokument wird der Server an sich beschrieben, nun mĂŒssen aber noch die einzelnen User der Instanz gefunden werden. Hierzu dient der webfinger-Endpunkt. Dieser ist zwar ĂŒblicherweise unter /.well-known/webfinger zu finden, aber einzelne Softwarelösungen bestehen trotzdem darauf, den Pfad erst unter einem anderen Endpunkt nachzuschlagen, und zwar unter /.well-known/host-meta:

<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
  <Link rel="lrdd" template="https://s3lph.me/.well-known/webfinger?resource={uri}" type="application/xrd+xml" />
</XRD>

Hier sehen wir schon das erste Hindernis, das eine rein statische Implementation verhindert: Der Name des aufzulösenden Users wird als URL-Parameter ĂŒbergeben, der durch einen HTTP-Server behandelt werden muss.

Hierzu gibt es zwei mögliche Lösungen:

  • Wenn es auf dem Server nur einen einzelnen User gibt, kann der Parameter eigentlich ignoriert werden. Stattdessen wird einfach immer die gleiche, statische Antwort zurĂŒckgegeben.
  • Wenn es mehrere User gibt, kann fĂŒr jeden User ein statischer webfinger-Endpunkt generiert werden. Dies setzt aber voraus, dass der Webserver konfiguriert wird, den eigentlichen webfinger-Endpunkt entsprechend umgeleitet werd.

Dies lÀsst sich z.B. im Apache-Webserver so umsetzen:

RewriteEngine on
RewriteRule ^/.well-known/webfinger?resource=acct:([^@]+)@s3lph.me$ /.well-known/_webfinger/$1 [L]

Der webfinger-Endpunkt verlinkt nun weiter auf die tatsÀchlichen User-Ressourcen:

{
  "subject": "acct:s3lph@s3lph.me",
  "aliases": [
    "https://s3lph.me/author/s3lph.html",
    "https://s3lph.me/activitypub/users/s3lph"
  ],
  "links": [
    {
      "rel": "http://webfinger.net/rel/profile-page",
      "type": "text/html",
      "href": "https://s3lph.me/author/s3lph.html"
    },
    {
      "rel": "self",
      "type": "application/activity+json",
      "href": "https://s3lph.me/activitypub/users/s3lph"
    }
  ]
}

In diesem Fall wird der Autor, in diesem Fall @s3lph@s3lph.me auf zwei Alias-URLs aufgelöst: Der Autoren-Feed im Blog, sowie die Person ActivityPub-Ressource.

ActivityPub: Personen und Artikel

Jetzt, wo wir einen Username wie @s3lph@s3lph.me zu einer ActivityPub-URL wie https://s3lph.me/activitypub/users/s3lph auflösen können, können wir die Personen-Ressource unter dieser URL genauer anschauen:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "schema": "http://schema.org#",
      "toot": "http://joinmastodon.org/ns#",
      "PropertyValue": "schema:PropertyValue",
      "value": "schema:value",
      "alsoKnownAs": {
        "@id": "as:alsoKnownAs",
        "@type": "@id"
      },
      "movedTo": {
        "@id": "as:movedTo",
        "@type": "@id"
      },
      "discoverable": "toot:discoverable"
    }
  ],
  "type": "Person",
  "id": "https://s3lph.me/activitypub/users/s3lph",
  "preferredUsername": "s3lph",
  "url": "https://s3lph.me/author/s3lph.html",
  "name": "s3lph made",
  "summary": "This is an EXPERIMENTAL implementation for a read-only ActivityPub feed of my blog.",
  "icon": {
    "type": "Image",
    "mediaType": "image/png",
    "url": "https://s3lph.me/favicon.ico"
  },
  "image": {},
  "tag": [],
  "attachment": [
    {
      "type": "PropertyValue",
      "name": "Web",
      "value": "<a href=\"https://s3lph.me\">s3lph.me</a>"
    },
    {
      "type": "PropertyValue",
      "name": "Mastodon",
      "value": "<a rel=\"me\" href=\"https://chaos.social/@s3lph\">@s3lph@chaos.social</a>"
    },
    {
      "type": "PropertyValue",
      "name": "Matrix",
      "value": "<a rel=\"me\" href=\"https://mto.kabelsalat.ch/#/@s3lph:kabelsalat.ch\">@s3lph:kabelsalat.ch</a>"
    }
  ],
  "movedTo": "https://chaos.social/users/s3lph",
  "alsoKnownAs": [
    "https://chaos.social/users/s3lph"
  ],
  "inbox": "https://s3lph.me/activitypub/collections/inbox/s3lph",
  "outbox": "https://s3lph.me/activitypub/collections/outbox/s3lph",
  "following": "https://s3lph.me/activitypub/collections/following/s3lph",
  "followers": "https://s3lph.me/activitypub/collections/followers/s3lph",
  "discoverable": true,
  "manuallyApprovesFollowers": true,
  "published": "2020-02-05T01:36:00+01:00",
  "updated": "2022-11-12T14:05:23Z",
  "endpoints": {
    "sharedInbox": "https://s3lph.me/activitypub/collections/inbox/s3lph"
  },
}

Diese Ressource ist schon ein gutes StĂŒck grösser, daher schauen wir das am besten StĂŒck fĂŒr StĂŒck an. Zuerst wird das Schema des JSON-Dokuments beschrieben:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "schema": "http://schema.org#",
      "toot": "http://joinmastodon.org/ns#",
      "PropertyValue": "schema:PropertyValue",
      "value": "schema:value",
      "alsoKnownAs": {
        "@id": "as:alsoKnownAs",
        "@type": "@id"
      },
      "movedTo": {
        "@id": "as:movedTo",
        "@type": "@id"
      },
      "discoverable": "toot:discoverable"
    }
  ],

Als nĂ€chstes werden die grundlegenden Informationen ĂŒber die Person beschrieben. Diese werden von ActivityPub-Clients benutzt, um die User-Seite darzustellen:

  "type": "Person",
  "id": "https://s3lph.me/activitypub/users/s3lph",
  "preferredUsername": "s3lph",
  "url": "https://s3lph.me/author/s3lph.html",
  "name": "s3lph made",
  "summary": "This is an EXPERIMENTAL implementation for a read-only ActivityPub feed of my blog...",
  "icon": {
    "type": "Image",
    "mediaType": "image/png",
    "url": "https://s3lph.me/favicon.ico"
  },
  "image": {},

ZusÀtzlich können noch einige weitere Metadaten angegeben werden:

  "tag": [],
  "attachment": [
    {
      "type": "PropertyValue",
      "name": "Web",
      "value": "<a href=\"https://s3lph.me\">s3lph.me</a>"
    },
    {
      "type": "PropertyValue",
      "name": "Mastodon",
      "value": "<a rel=\"me\" href=\"https://chaos.social/@s3lph\">@s3lph@chaos.social</a>"
    },
    {
      "type": "PropertyValue",
      "name": "Matrix",
      "value": "<a rel=\"me\" href=\"https://mto.kabelsalat.ch/#/@s3lph:kabelsalat.ch\">@s3lph:kabelsalat.ch</a>"
    }
  ],
  "movedTo": "https://chaos.social/users/s3lph",
  "alsoKnownAs": [
    "https://chaos.social/users/s3lph"
  ],

Attachments vom Typ "PropertyValue" werden z.B. von den meisten ActivityPub-Clients als Tabelle im Profil des Users dargestellt. Unter tags werden z.B. Hashtags oder ErwÀhnungen von anderen Usern im Profil des Users aufgelistet, damit diese von anderen Servern im Fediverse indexiert werden.

Mit movedTo und alsoKnownAs wird angegeben, dass der Account umgezogen ist, und andere User dem neuen Profil folgen sollen.

Damit kommt ein Profil zustande, das mit Fediverse-Clients aufgerufen werden kann. So zum Beispiel in der Mastodon-App Tusky:

Das Profil von @s3lph@s3lph.me, dargestellt in der Mastodon-App Tusky. Abbildung 2: Das Profil von @s3lph@s3lph.me, dargestellt in der Mastodon-App Tusky.

Schlussendlich mĂŒssen noch die URLs zu den verknĂŒpften ActivityPub-Ressourcen angegeben werden:

  "inbox": "https://s3lph.me/activitypub/collections/inbox/s3lph",
  "outbox": "https://s3lph.me/activitypub/collections/outbox/s3lph",
  "following": "https://s3lph.me/activitypub/collections/following/s3lph",
  "followers": "https://s3lph.me/activitypub/collections/followers/s3lph",

Die Bedeutung von Inbox und Outbox wurden vorhin schon kurz erwĂ€hnt. Da in dieser Implementation keine Inboxen verarbeitet werden, steckt hinter der Inbox einfach nur eine leere "Collection" (Liste aus ActivityPub-Ressourcen). Das gleiche gilt fĂŒr die "following"- und "followers"-Collections:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams"
  ],
  "type": "OrderedCollection",
  "id": "https://s3lph.me/activitypub/collections/inbox/s3lph",
  "totalItems": 0,
  "orderedItems": []
}

Die Outbox ist ebenfalls eine Collection, die die von diesem User veröffentlichten Artikel enthÀlt. Ein solcher (dieser) Artikel sieht z.B. so aus:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams"
  ],
  "type": "Article",
  "id": "https://s3lph.me/activitypub/posts/activitypub-static-site",
  "published": "2022-11-17T02:00:00+01:00",
  "inReplyTo": null,
  "url": "https://s3lph.me/activitypub-static-site-de.html",
  "attributedTo": "https://s3lph.me/activitypub/users/s3lph",
  "to": [
    "https://www.w3.org/ns/activitystreams#Public"
  ],
  "cc": [
    "https://s3lph.me/activitypub/collections/followers/s3lph",
    "https://chaos.social/users/s3lph"
  ],
  "name": "How much ActivityPub can a Static Site Generator implement?",
  "nameMap": {
    "en": "How much ActivityPub can a Static Site Generator implement?",
    "de": "Wie viel ActivityPub kann ein Static Site Generator?"
  },
  "content": "<p>There already are multiple blogging solutions...",
  "contentMap": {
    "en": "<p>There already are multiple blogging solutions...",
    "de": "<p>Es gibt bereits verschiedene Blog-Lösungen...",
  },
  "summary": null,
  "attachment": [],
  "tag": [
    {
      "type": "Hashtag",
      "name": "#activitypub",
      "href": "https://s3lph.me/activitypub/tags/activitypub"
    },
    {
      "type": "Hashtag",
      "name": "#pelican",
      "href": "https://s3lph.me/activitypub/tags/pelican"
    },
    {
      "type": "Mention",
      "href": "https://chaos.social/users/s3lph",
      "name": "@s3lph@chaos.social"
    }
  ]
}

Der Anfang eines Artikels sieht mehr oder weniger gleich aus wie bei einer Person, daher wiederhole ich das hier nicht nochmal.

Als nĂ€chstes wird die Beziehung zu anderen Ressourcen angegeben, z.B. wer den Artikel verfasst hat, und an wen der Artikel adressiert ist. Die spezielle URL https://www.w3.org/ns/activitystreams#Public beschreibt, dass der Artikel öffentlich ist und z.B. in globalen Timelines aufgefĂŒhrt werden soll:

  "inReplyTo": null,
  "url": "https://s3lph.me/activitypub-static-site-de.html",
  "attributedTo": "https://s3lph.me/activitypub/users/s3lph",
  "to": [
    "https://www.w3.org/ns/activitystreams#Public"
  ],
  "cc": [
    "https://s3lph.me/activitypub/collections/followers/s3lph",
    "https://chaos.social/users/s3lph"
  ],

Titel und Inhalt des Artikels können mehrsprachig angegeben werden. Allerdings werden contentMap und nameMap nur von wenigen ActivityPub-Servern implementiert. Die meisten Server zeigen einfach immer den unĂŒbersetzten Standard-Inhalt an:

  "name": "How much ActivityPub can a Static Site Generator implement?",
  "nameMap": {
    "en": "How much ActivityPub can a Static Site Generator implement?",
    "de": "Wie viel ActivityPub kann ein Static Site Generator?"
  },
  "content": "<p>There already are multiple blogging solutions...",
  "contentMap": {
    "en": "<p>There already are multiple blogging solutions...",
    "de": "<p>Es gibt bereits verschiedene Blog-Lösungen...",
  },
  "summary": null,

Schlussendlich werden - genau wie bei Personen auch - weitere Daten wie Tags oder ErwĂ€hnungen anderer Personen in maschinenlesbar aufgefĂŒhrt:

  "attachment": [],
  "tag": [
    {
      "type": "Hashtag",
      "name": "#activitypub",
      "href": "https://s3lph.me/activitypub/tags/activitypub"
    },
    {
      "type": "Hashtag",
      "name": "#pelican",
      "href": "https://s3lph.me/activitypub/tags/pelican"
    },
    {
      "type": "Mention",
      "href": "https://chaos.social/users/s3lph",
      "name": "@s3lph@chaos.social"
    }
  ]

Damit haben wir Autoren und deren Artikel vollstÀndig als ActivityPub-Ressourcen abgebildet. Damit ist auch alles implementiert, was sich sinnvoll als rein statische Seite implementieren lÀsst.

Der Code, um diese ActivityPub-Ressourcen zu erzeugen, ist als Pelican-Plugin verfĂŒgbar. Bevor irgendjemand das Plugin in das eigene Pelican einbaut, wĂŒrde ich aber dazu raten, diesen Artikel bis zum Ende zu lesen.

KompatibilitÀt mit Fediverse-Diensten

Zum Testen habe drei verschiedenen Fediverse-Testinstanzen aufgesetzt, um mit meinem Blog zu interagieren: Mastodon, Pleroma und Misskey.

Mit allen drei Diensten kann das Profil @s3lph@s3lph.me aufgerufen werden. Auch die Anzahl an Blogartikeln wird korrekt dargestellt, allerdings werden die Artikel selbst nicht angezeigt. Wie sich herausstellt, werden Artikel von anderen Instanzen ĂŒblicherweise nicht automatisch geladen.

Die Artikel wĂŒrden nur dann angezeigt, wenn sie von ihrer Ursprungsinstanz in die Inbox der Zielinstanz gePOSTed werden. Alternativ war es aber bei allen drei Testinstanzen möglich, die URL des Artikels in der Suche einzugeben und so aufzurufen. Danach wird der jeweilige Artikel auch in der Timeline des Autors angezeigt.

Leider ist dies eigentlich auch schon alles, dass mit einer rein statischen Implementation wirklich funktioniert. Insbesondere die folgenden - doch recht zentralen - Funktionen sind nicht verfĂŒgbar:

  • Folgen: Wenn Alice im Fediverse dem Account vom Bob folgen möchte, sendet sie eine Follow Request in die Inbox von Bob. Bob (resp. Bob's Instanz) muss diese Anfrage allerdings erst bestĂ€tigen.
  • Antworten: Wenn Alice auf eine Nachricht von Bob antwortet, ist diese Antwort zunĂ€chst nur auf der Instanz von Alice sichtbar. Die Antwort wird auch in die Inbox von Bob zugestellt, und Bobs Instanz wĂ€re dafĂŒr zustĂ€ndig, die Antwort an alle anderen involvierten Instanzen weiterzuleiten. Dieser Schritt bleibt im statisch generierten Fall aber aus.
  • Löschen: Wenn ein Artikel im Cache einer anderen Instanz ist, wird er dort ĂŒblicherweise behalten, bis er von der Ursprungsinstanz explizit gelöscht wird. Hierzu muss eine signierte Löschanfrage an die Inbox der anderen Instanz gesendet werden.

Was hingegen funktioniert sind Likes und Boosts, allerdings werden diese nicht in den ZĂ€hlern unter den Artikeln reflektiert.

Zudem stellt z.B. Mastodon nur einen Link zum Artikel dar, da die hier verwendeten Article-Objekte nicht vollstĂ€ndig unterstĂŒtzt werden; fĂŒr Kurznachrichten wird der Objecttyp Note verwendet. Sowohl Pleroma als auch Misskey können den Artikel aber vollstĂ€ndig nativ darstelle.

Fazit

Die Frage «Wie viel ActivityPub kann ein Static Site Generator?» lĂ€sst sich zusammenfassend wohl am besten beantworten mit «viel, aber nicht genug, um praktische Relevanz zu haben.» Auch fĂŒr diesen Blog wird eine rein statische ActivityPub-Implementation wahrscheinlich keine wirkliche Relevanz haben, aber die Artikel werden voraussichtlich weiterhin in dieser eingeschrĂ€nkten Form im Fediverse verfĂŒgbar sein.

PS: Falls du diesen Artikel im Fediverse liest, und eine Antwort schreiben willst, adressiere deine Antwort bitte (zusÀtzlich) an @s3lph@chaos.social, sonst bekomme ich davon nichts mit.

mfm

ablobcatattentionreverse blobcatoutage ablobcatattention
one of our hoster has an outage

cw test
cw test

@fluffy blobcateyes i dont care about heaven but i like your fluffy tail ablobcatreach

@tomxcd @tomxcd huh, i didnt expected that

Nyaaa

curry_puddingdoughnut_puddingramen_puddingrolled_sushi_pudding

blobcatgooglytrash

»