Post APruVvfR3kqzUIy90S by blobplague@blob.cat
 (DIR) More posts by blobplague@blob.cat
 (DIR) Post #APqAmzx3atXaeGS7oe by s3lph@s3lph.me
       2022-11-18T00:00:00Z
       
       1 likes, 1 repeats
       
       There already are multiple blogging solutions which are part of theFediverse.  Among them are dedicated Fediverse blogs suchas Plume or WriteFreely, but there also areplugins which retrofit existing CMS, e.g. for Wordpress orDrupal.ActivityPub, the network protocol behind the Fediverse can onlybe fully implemented by means of an active server component: Amongother things, incoming messages delivered to inboxes have to beprocessed.  Sometimes they need to be forwarded, and outgoing messagesneed to be signed.              Figure 1:    The basic mechanisms behind ActivityPub: Users publish resources in their outbox, and retrieve received resources from their inbox.  Nevertheless I wanted to figure out, which parts of the ActivityPubprotocol can be implemented in a purely static website, and how wellother servers in the Fediverse interact with it.  My goal was to attachthis blog to the Fediverse.  The blog is generated using the staticsite generator software Pelican.Metadata EndpointsOne prerequisite for implementing ActivityPub are multiple staticmetadata endpoints, with which a server signals whether itsupports ActivityPub, and under which HTTP endpoints the variousActivityPub resources are reachable:/.well-known/nodeinfo simply links to the "real" nodeinfo endpoint:{  "links": [    {      "href": "https://s3lph.me/activitypub/nodeinfo",      "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0"    }  ]}Behind the linked path (which can be chosen freely ), the globalmetadata of this ActivityPub instance is published:{  "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"  }}This JSON document describes the server, however we still need todiscover the individual users of this instance.  This is where thewebfinger endpoint comes in.  Usually it is found at/.well-known/webfinger, but some pieces of software insist on firstresolving this path using the /.well-known/host-meta endpoint:<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>Now we already stumbled over the fist obstacle preventing a purelystatic implementation: The name of the user to be resolved is passedas an URL parameter, which needs to be handled by a HTTP server.There are two possible workarounds:If there only is a single user on the server, the parameter can be  ignored.  Instead, always the same static response is returned, no  matter the URL parameter.If there are multiple users, we can generate a static webfinger  endpoint per user.  This requires the webserver to be configured to  redirect the regular webfinger endpoint to these user specific  endpoints.For example, using the Apache webserver, this can be achieved like this:RewriteEngine onRewriteRule ^/.well-known/webfinger?resource=acct:([^@]+)@s3lph.me$ /.well-known/_webfinger/$1 [L]The webfinger endpoint links to the actual user resources:{  "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 this case, the author @s3lph@s3lph.me is resolved to two aliasURLs: The author's feed in the blog, as well as the ActivityPubPerson resource.ActivityPub: Persons and ArticlesNow that we can resolve usernames such as @s3lph@s3lph.me toActivityPub RULS such as https://s3lph.me/activitypub/users/s3lph,we can take a closer look at the Person resource behind this URL:{  "@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"  },}This resource is a bit bigger than before, so let's look at it pieceby piece.  At first the schema of this JSON document is described:{  "@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"    }  ],Next the basic information about the person are described.  These areused by ActivityPub-Clients to display the user's profile page:  "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": {},Some additional metadata can be provided:  "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"  ],"PropertyValue" attachments are rendered as tables on the user'sprofile page by most ActivityPub clients.  tags list attributes suchas hashtags and mentions of other users within the profile'sdescription, so that they can be easily indexed by other servers inthe Fediverse.movedTo and alsoKnownAs are used to indicate that the account hasmoved, and the new account should be followed instead.With all of this, we get a user profile that can be viewed onFediverse client applications, such as the Mastodon app "Tusky":              Figure 2:    The profile of @s3lph@s3lph.me, as shown in the Mastodon app Tusky  Finally, the URLs to linked ActivityPub resources have to be provided:  "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",I've already mentioned the relevance of the inbox and outboxbefore. Since this implementation does not process inboxes, there onlyis an empty colllection (list of ActivityPub resources) behind theinbox URL.  The same goes for the "following" and "followers"collections:{  "@context": [    "https://www.w3.org/ns/activitystreams"  ],  "type": "OrderedCollection",  "id": "https://s3lph.me/activitypub/collections/inbox/s3lph",  "totalItems": 0,  "orderedItems": []}The outbox is a collection as well, containing the articles publishedby this user.  One of these articles (actually this one) looks likethis:{  "@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"    }  ]}The beginning of an article is more or less equal to that of a person,so I won't be repeating it here.Next the relationships to other resources are described, e.g. whowrote the article, and to whom it is addressed.  The special URLhttps://www.w3.org/ns/activitystreams#Public describes that thearticle is public, and should e.g. be listed in global timelines:  "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"  ],Title and content of the article can be multilingual.  However, thecontentMap und nameMap attributes are only supported by a fewActivityPub servers or clients.  Most others simply always show theuntranslated default content:  "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,Finally, same as with persons, additional data like tags and mentionsof other users are listed in a machine readable form:  "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"    }  ]Sow now we have modelled authors and their articles as ActivityPubresources.  This is also everything which can be reasonablyimplemented as a purely static site.The code for generating these ActivityPub resources is available as aPelican plugin.  However, before you go ahead andinstall this on your own Pelican blog, I'd advise you to finishreading this article.Compatibility to Fediverse ServicesFor testing my implementation, I set up three different Fediverseinstances to interact with my blog:Mastodon,Pleroma and Misskey.All three services are able to retrieve and show the profile@s3lph@s3lph.me, and the number of blog articles is shown.  However,the articles themselves are not shown.  As it turns out, articles fromother instances are usually not retrieved automatically.The articles would only be shown if they were POSTed from their origininstance to the target instance.  Alternatively, all three testinstances were able to show the articles by pasting the article's URLinto the search bar.  Afterwards these articles were also shown in thetimeline of their author.Unfortunately, this is almost everything that can be achieved with apurely static implementation.  Especially the following importantfunctions are not available:Following: If Alice wants to follow the Fediverse account of Bob,  she has to send a follow request to Bob.  Bob (or his instance)  has to confirm this request before it becomes effective.Replies: If Alice wants to reply to a message of Bob, her  response is only visible on Alice's instance at first.  The response  will also be posted to the inbox of Bob, and it would be in his  instance's responsibility to forward the reply to all other involved  instances.  This crucial step is not possible with a static  ActivityPub endpoint.Deletion: If an article ha been cached by another instance, it  is usually kept in the cache until it's explicitly deleted by its  origin instance.  For this, a cryptographically signed deletion  request would have to be submitted to the instance's inbox.What does work are likes and boosts, tough they do not increment thecounters under an article.Additionally, it turns out that e.g. Mastodon only shows a linkpreview to the article, since the Article object type is not fullysupported; the Note type is used for short messages.  However, bothPleroma and Misskey show the full article natively.ConclusionThe question "How much ActivityPub can a Static Site Generatorimplement?" can thus be best answered as "quite a lot, but not enoughto be relevant in practice."  Even for this blog, the purely staticActivityPub implementation will most likely not have any relevance,however the articles will presumably keep being published on theFediverse in this limited form.PS: If you're reading this article on the Fediverse and want to writea reply, please (additionally) address it to @s3lph@chaos.social,otherwise I won't see it.
       
 (DIR) Post #APruT6GDDZhBTOp1tY by blobplague@blob.cat
       2022-11-22T17:17:43.323084Z
       
       0 likes, 0 repeats
       
       @s3lph @s3lph Nice article, though webfinger part might not be completely necessary. It does help with reachability since that's what fedi software hits when searching for user tag, but as long as there are user and object endpoints, posts should be able to get fetched either way. Tale a look at @tedu@honk.tedunangst.com's AP server, honk. There's nothing in .well-known, no nodeinfo, no webfinger. Yet it can interact with other servers just fine.
       
 (DIR) Post #APruVvfR3kqzUIy90S by blobplague@blob.cat
       2022-11-22T17:18:04.466012Z
       
       0 likes, 0 repeats
       
       @s3lph @s3lph Failed to tag Ted. @tedu
       
 (DIR) Post #APs6IbbDYl8SvvPRvU by s3lph@chaos.social
       2022-11-22T19:20:44Z
       
       1 likes, 0 repeats
       
       @blobplague Except that there is a webfinger endpoint. You can check for yourself: https://honk.tedunangst.com/.well-known/webfinger?resource=acct:tedu@honk.tedunangst.comAnd while it's probably not exactly required, it greatly increases usability because you can find users via their handle, instead of having to provide the entire URL to their ActivityPub Person resource.