The Fediverse
- collective term for servers that are used for web publishing (i.e. social networking, microblogging, macroblogging, or websites) and file hosting that allow their users to communicate by supporting one or more open standards.
- On different servers (instances), users can create so called identities (these can communicate over the boundaries of the instances because the software running on the servers support one or more communication protocols which follow the open standard.
It's any piece of software that implements the ActivityPub protocol.
- The beauty here is that the barrier to entry is near zero, you can communicate with every other instance on the network fairly and openly. You can choose which servers to interact with and which to block.
- A decentralized network deliberately relinquishes control of the platform owner, by essentially not having one.
- That means the network is not subject to my whims; it can adapt to situations faster than I can, and it can serve use cases I couldn’t have predicted.
- decentralization is about fixing power asymmetry. A centralized social media platform has a hierarchical structure where rules and their enforcement, as well as the development and direction of the platform, are decided by the CEO, with the users having close to no ways to disagree. You can’t walk away when the platform holds all your friends, contacts and audience
Software that makes up the Fediverse
Micro blogging software built around 500 character text posts.
ActivityPub is the latest recommendation to come out of the World Wide Web Consortium's (W3C) Social Web Networking Group. It's based on lessons learned from the older standard OStatus. Seems to be the standard with the most adoption in the past year or two.
Based on pump.io's implementation of the W3C's implementation of Activity Streams. Which is a recommendation they put out in 2017 which outlines data and object types that is used in ActivityPub.
Nextcloud (https://nextcloud.com/ )
Nextcloud is file sharing software with plugins that allow sharing calendars, bookmarks, notes etc Many open source projects are implementing ActivityPub where possible
ActivityPub provides two layers:
- A server to server federation protocol (so decentralized websites can share information)
- A client to server protocol (so users, including real-world users, bots, and other automated processes, can communicate with ActivityPub using their accounts on servers, from a phone or desktop or web application or whatever)
It provides a client to server API for creating, updating and deleting content, as well as a federated server to server API for delivering notifications and subscribing to content.
An ActivityPub Actor
In ActivityPub, a user is represented by "actors" via the user's accounts on servers. User's accounts on different servers correspond to different actors. Every Actor has:
- An inbox (how they get messages from the world)
- An outbox (how they send messages to others)
These are endpoints, or really, just URLs which are listed in the ActivityPub actor's ActivityStreams description. (More on ActivityStreams later).
An ActivityPub Actor
{"@context": "https://www.w3.org/ns/activitystreams",
"type": "Person",
"id": "https://social.example/alyssa/",
"name": "Alyssa P. Hacker",
"preferredUsername": "alyssa",
"summary": "Lisp enthusiast hailing from MIT",
"inbox": "https://social.example/alyssa/inbox/",
"outbox": "https://social.example/alyssa/outbox/",
"followers": "https://social.example/alyssa/followers/",
"following": "https://social.example/alyssa/following/",
"liked": "https://social.example/alyssa/liked/"}
- You can POST to someone's inbox to send them a message (server-to-server / federation only... this is federation!)< br/>
- You can GET from your inbox to read your latest messages (client-to-server; this is like reading your social network stream)
- You can POST to your outbox to send messages to the world (client-to-server)
- You can GET from someone's outbox to see what messages they've posted (or at least the ones you're authorized to see). (client-to-server and/or server-to-server)
Of course, if that last one (GET'ing from someone's outbox) was the only way to see what people have sent, this wouldn't be a very efficient federation protocol! Indeed, federation happens usually by servers posting messages sent by actors to actors on other servers' inboxes.
Post a message
{"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"to": ["https://chatty.example/ben/"],
"attributedTo": "https://social.example/alyssa/",
"content": "Say, did you finish reading that book I lent you?"}
Since this is a non-activity object, the server recognizes that this is an object being newly created, and does the courtesy of wrapping it in a Create activity. (Activities sent around in ActivityPub generally follow the pattern of some activity by some actor being taken on some object. In this case the activity is a Create of a Note object, posted by a Person).
Wrap in a create object
{"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "https://social.example/alyssa/posts/a29a6843-9feb-4c74-a7f7-081b9c9201d3",
"to": ["https://chatty.example/ben/"],
"actor": "https://social.example/alyssa/",
"object": {"type": "Note",
"id": "https://social.example/alyssa/posts/49e2d03d-b53a-4c4c-a95c-94a6abf45a19",
"attributedTo": "https://social.example/alyssa/",
"to": ["https://chatty.example/ben/"],
"content": "Say, did you finish reading that book I lent you?"}}
Alyssa's server looks up Ben's ActivityStreams actor object, find his inbox endpoint, and POSTs her object to his inbox
Technically these are two seperate steps. One is client to server. The other is server to server (federation).
Alyssa's phone then polls her inbox via GET and she sees:
Get a response
{"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "https://chatty.example/ben/p/51086",
"to": ["https://social.example/alyssa/"],
"actor": "https://chatty.example/ben/",
"object": {"type": "Note",
"id": "https://chatty.example/ben/p/51085",
"attributedTo": "https://chatty.example/ben/",
"to": ["https://social.example/alyssa/"],
"inReplyTo": "https://social.example/alyssa/posts/49e2d03d-b53a-4c4c-a95c-94a6abf45a19",
"content": "Argh, yeah, sorry, I'll get it back to you tomorrow.
I was reviewing the section on register machines,
since it's been a while since I wrote one.
"}}
Alyssa's phone polls her inbox via GET and she sees the following.
Cool. Then she hits 'like'
Alyssa hits 'like'
{"@context": "https://www.w3.org/ns/activitystreams",
"type": "Like",
"id": "https://social.example/alyssa/posts/5312e10e-5110-42e5-a09b-934882b3ecec",
"to": ["https://chatty.example/ben/"],
"actor": "https://social.example/alyssa/",
"object": "https://chatty.example/ben/p/51086"}
Since this is an activity, the server knows it doesn't need to wrap it in a Create object. She decides to write a new public message to her followers.
Then she decides to write a new public post to her followers.
New public post
{"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "https://social.example/alyssa/posts/9282e9cc-14d0-42b3-a758-d6aeca6c876b",
"to": ["https://social.example/alyssa/followers/",
"https://www.w3.org/ns/activitystreams#Public"],
"actor": "https://social.example/alyssa/",
"object": {"type": "Note",
"id": "https://social.example/alyssa/posts/d18c55d4-8a63-4181-9745-4e6cf7938fa1",
"attributedTo": "https://social.example/alyssa/",
"to": ["https://social.example/alyssa/followers/",
"https://www.w3.org/ns/activitystreams#Public"],
"content": "Lending books to friends is nice. Getting them back is even nicer! :)"}}
We've explored some default vocabulary of ActivityPub, the 'like' and 'create' types. There are many more like 'follow', 'ignore', 'read', 'delete'. These activity types can be extended as can the create types (i.e. audio, video, image, note etc)
Implementing a basic ActivityPub server
Goal: Send a Mastodon user a message from our own, non-Mastodon server
Create a verifiable actor
Format the message with ActivityPub
Cryptographically sign the message then POST
Generate RSA keypair
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
Create an actor
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"id": "https://my-example.com/actor",
"type": "Person",
"preferredUsername": "alice",
"inbox": "https://my-example.com/inbox",
"publicKey": {
"id": "https://my-example.com/actor#main-key",
"owner": "https://my-example.com/actor",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----"
}
}
The id must be the URL of the document (it’s a self-reference), and all URLs should be using HTTPS. You need to include an inbox even if you don’t plan on receiving messages in response, because for legacy purposes Mastodon doesn’t acknowledge inbox-less actors as compatible.
The most complicated part of this document is the publicKey as it involves cryptography. The id will in this case refer to the actor itself, with a fragment (the part after #) to identify it–this is because we are not going to host the key in a separate document (although we could). The owner must be the actor’s id. Now to the hard part: You’ll need to generate an RSA keypair.
Discoverable via Webfinger
endpoint: /.well-known/webfinger
queries: /.well-known/webfinger?resource=acct:bob@my-example.com
{
"subject": "acct:alice@my-example.com",
"links": [
{
"rel": "self",
"type": "application/activity+json",
"href": "https://my-example.com/actor"
}
]
}
The subject property here consists of the username (same as preferredUsername earlier) and the domain you’re hosting on. This is how your actor will be stored on other Mastodon servers and how people will be able to mention it in toots. Only one link is required in the Webfinger response, and it’s the link to the actor document.
After this is uploaded to your webhost and available under your domain with a valid SSL certificate, you could already look up your actor from another Mastodon by entering alice@my-example.com into the search bar. Although it’ll look quite barren.
Construct our message
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://my-example.com/create-hello-world",
"type": "Create",
"actor": "https://my-example.com/actor",
"object": {
"id": "https://my-example.com/hello-world",
"type": "Note",
"published": "2018-06-23T17:17:11Z",
"attributedTo": "https://my-example.com/actor",
"inReplyTo": "https://mastodon.social/@Gargron/100254678717223630",
"content": "Hello world
",
"to": "https://www.w3.org/ns/activitystreams#Public"
}
}
ActivityPub messages practically consist of two parts, the message itself (the object) and a wrapper that communicates what’s happening with the message (the activity). In our case, it’s going to be a Create activity. Let’s say “Hello world” in response to my toot about writing this blog post:
With the inReplyTo property we’re chaining our message to a parent. The content property may contain HTML, although of course it will be sanitized by the receiving servers according to their needs — different implementations may find use for a different set of markup. Mastodon will only keep p, br, a and span tags. With the to property we are defining who should be able to view our message, in this case it’s a special value to mean “everyone”.
For our purposes, we don’t actually need to host this document publicly, although ideally both the activity and the object would be separately available under their respective id. Let’s just save it under create-hello-world.json because we’ll need it later.
Create HTTP signature and POST
require 'http'
require 'openssl'
document = File.read('create-hello-world.json')
date = Time.now.utc.httpdate
keypair = OpenSSL::PKey::RSA.new(File.read('private.pem'))
signed_string = "(request-target): post /inbox\nhost: mastodon.social\ndate: #{date}"
signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest::SHA256.new, signed_string))
header = 'keyId="https://my-example.com/actor",headers="(request-target) host date",signature="' + signature + '"'
HTTP.headers({ 'Host': 'mastodon.social', 'Date': date, 'Signature': header })
.post('https://mastodon.social/inbox', body: document)
Mastodon-cat
$input = file_get_contents("php://stdin");
require 'test_credentials.php';
$name = 'Adelaide'; $instance = 'glasgow.social';
$oAuth = new Colorfield\Mastodon\MastodonOAuth($name, $instance);
$oAuth->config->setClientId($client_id);
$oAuth->config->setClientSecret($client_secret);
$oAuth->config->setBearer($bearer);
$mastodonAPI = new Colorfield\Mastodon\MastodonAPI($oAuth->config);
$params['status'] = $input;
$params['visibility'] = "unlisted";
$post = $mastodonAPI->post('/statuses', $params);
Mastodon/Reddit PHP Bot
/* SNIP (Get Reddit RSS feed and save as $summary) */
$params['status'] = $summary;
$params['visibility'] = "public";
require 'bot_credentials.php';
$name = 'RedditBot'; $instance = 'glasgow.social';
$oAuth = new Colorfield\Mastodon\MastodonOAuth($name, $instance);
$oAuth->config->setClientId($client_id);
$oAuth->config->setClientSecret($client_secret);
$oAuth->config->setBearer($bearer);
$mastodonAPI = new Colorfield\Mastodon\MastodonAPI($oAuth->config);
$post = $mastodonAPI->post('/statuses', $params);