Integrating My Blog with Mastodon


Decentralization is cool again! Twitter is burning and people are fleeing to greener pastures. There are a bunch of alternatives, but the most promising one is Mastodon Mastodon - Decentralized social media Learn more about Mastodon, the radically different, free and open-source decentralized social media platform. joinmastodon.org/ — not because of any particular feature, but because it’s built on an open standard called ActivityPub ActivityPub Rocks! activitypub.rocks/ .

ActivityPub is a protocol: a bunch of conventions that apps can use to talk to each other. That means that if you build an app that understands ActivityPub, any other app that also understands it can communicate with it. Technically, this is called federation, not decentralization, and the ecosystem of apps that work together this way is lovingly referred to as the Fediverse.

I decided to dip my toes in and see how ActivityPub works. What I built is pretty simple: a tiny server that can interact with the rest of the Fediverse, plus a GitHub Actions workflow that posts whenever I publish a new blog post.

The server I made is called ActivityPub Starter Kit GitHub - jakelazaroff/activitypub-starter-kit: A tiny, single user, Mastodon-compatible ActivityPub server. A tiny, single user, Mastodon-compatible ActivityPub server. - GitHub - jakelazaroff/activitypub-starter-kit: A tiny, single user, Mastodon-compatible ActivityPub server. github.com/jakelazaroff/activitypub-starter-kit . It’s built with NodeJS and stores its data in SQLite. To make hacking on it easier, I also made a simpler version on Glitch Glitch Combining automated deployment, instant hosting & collaborative editing, Glitch gets you straight to coding so you can build full-stack web apps, fast glitch.com/edit/#!/activitypub-starter-kit . As soon as you remix it, you get an account that you can follow from any Fediverse app — no extra setup required!

In the rest of this blog post, I’ll show how easy it is to set up an integration between your blog and the Fediverse. We’ll assume that we’re using the Glitch app with an account called jake.

So: ActivityPub Starter Kit. It’s a very basic ActivityPub server with only a few endpoints. There are two that matter to us:1

My blog is built on a static site generator that outputs an RSS feed. So there are four basic steps here:

  1. Get the latest post from the RSS feed.
  2. Get the latest post from the ActivityPub Outbox.
  3. Compare the URLs.
  4. If they don’t match, create a new ActivityPub post and notify followers.

There are only two libraries we need for this: node-fetch node-fetch A light-weight module that brings Fetch API to node.js. Latest version: 3.3.1, last published: 4 months ago. Start using node-fetch in your project by running `npm i node-fetch`. There are 31880 other projects in the npm registry using node-fetch. www.npmjs.com/package/node-fetch (to make HTTP requests more easily) and fast-xml-parser fast-xml-parser Validate XML, Parse XML, Build XML without C/C++ based libraries. Latest version: 4.2.5, last published: 20 days ago. Start using fast-xml-parser in your project by running `npm i fast-xml-parser`. There are 2013 other projects in the npm registry using fast-xml-parser. www.npmjs.com/package/fast-xml-parser (to parse the RSS feed into JSON).

import { XMLParser } from "fast-xml-parser";
import fetch from "node-fetch";

There are a few variables we’ll need that we may as well define now:

const RSS_URL = "https://jakelazaroff.com/rss.xml";
const ACTIVITYPUB_HOST = "https://shine-thunder-forgery.glitch.me";
const OUTBOX_URL = ACTIVITYPUB_HOST + "/jake/outbox";
const CREATE_URL = ACTIVITYPUB_HOST + "/admin/create";
const ADMIN_USERNAME = "adminuser";
const ADMIN_PASSWORD = "secretpassword!";
  • RSS_URL is the URL of the RSS feed with the blog posts.
  • ACTIVITYPUB_HOST is the URL of the ActivityPub server (just the protocol and hostname, so we can build our other URLs).
  • OUTBOX_URL is the URL of the Outbox.
  • CREATE_URL is the URL of the endpoint we’ll use to actually create the post.
  • ADMIN_USERNAME and ADMIN_PASSWORD correspond to environment variables we can set in the server to prevent randos from creating posts.

Okay, onto the main event. First, we’ll fetch the RSS feed and get the latest blog post:

console.log(`Fetching RSS feed from ${RSS_URL}`);
const rss = await fetch(RSS_URL);
const feed = new XMLParser().parse(await rss.text());
const latest = feed.rss.channel.item[0];

Next, we’ll fetch the ActivityPub feed and get the latest post there:

console.log(`Fetching ActivityPub feed from ${OUTBOX_URL}`);
const outbox = await fetch(OUTBOX_URL);
const posts = await outbox.json();
const post = posts.orderedItems[0];

Now that we have the latest post from each, we can check to see if their URLs match:

if (latest.link === post.object?.url) console.log("No new post in feed.");

If they don’t match, we’ll make a new post in the ActivityPub feed:

else {
  console.log("Posting to ActivityPub feed…");

Before we can send the request, we need to calculate the Basic authentication header. That’s just the string Basic username:password, encoded in base 64:

const authorization = "Basic " + Buffer.from(ADMIN_USERNAME + ":" + ADMIN_PASSWORD).toString("base64");

We also need to create the request body. ActivityPub supports a bunch of different types of “Objects” Activity Vocabulary www.w3.org/TR/activitystreams-vocabulary/#activity-types , but since this is a blog, the posts will all be of type Article Activity Vocabulary www.w3.org/TR/activitystreams-vocabulary/#dfn-article . We’ll supply title, summary, content, url and published with data from the RSS feed:

const date = new Date(latest.pubDate);
const body = JSON.stringify({
  object: {
    type: "Article",
    url: latest.link,
    title: latest.title,
    summary: latest.description,
    content: latest["content:encoded"],
    published: date.toISOString()
  }
});

You’ll also notice that we’re nesting everything inside an object property. That’s because each item in the Outbox is actually a Create Activity Activity Vocabulary www.w3.org/TR/activitystreams-vocabulary/#dfn-create that contains an Object.3 That Activity has its own set of properties, and we can override them by including them in the request body.

For example, if we wanted to set the ActivityPub post’s date to be the same as the blog post, we could add published to the top level alongside object, like so:

const date = new Date(latest.pubDate);
const body = JSON.stringify({
  published: date.toISOString(),
  object: {
    type: "Article",
    url: latest.link,
    title: latest.title,
    summary: latest.description,
    content: latest["content:encoded"],
    published: date.toISOString()
  }
});

After we have the body, all that’s left is to send off the request:

  const res = await fetch(CREATE_URL, {
    method: "POST",
    body,
    headers: { authorization, "content-type": "application/json" },
  });

  if (!res.ok) throw new Error(`Got ${res.status} ${res.statusText} when creating post.`);
}

And that’s it! People can now follow your blog in the Fediverse.

Here’s the whole script Publish the latest post in an RSS feed to an ActivityPub Starter Kit feed Publish the latest post in an RSS feed to an ActivityPub Starter Kit feed - rss2activitypub.js gist.github.com/jakelazaroff/5256e91395188da72cecbf086723ee6f . It’s short — barely 40 lines. I included my full GitHub Actions deploy workflow in there, in case the context helps.

And of course, this is just one example of what to build. You could have your ActivityPub server follow people and turn their posts into a reading list or a blogroll blogroll - IndieWeb indieweb.org/blogroll . You could make a bot that looks up information, or uses ChatGPT to write poems for people. Check out ActivityPub Starter Kit GitHub - jakelazaroff/activitypub-starter-kit: A tiny, single user, Mastodon-compatible ActivityPub server. A tiny, single user, Mastodon-compatible ActivityPub server. - GitHub - jakelazaroff/activitypub-starter-kit: A tiny, single user, Mastodon-compatible ActivityPub server. github.com/jakelazaroff/activitypub-starter-kit and make something cool or quirky or fun!

Footnotes

  1. Technically, there’s also a third: /jake/inbox, our account’s Inbox ActivityPub The ActivityPub protocol is a decentralized social networking protocol based upon the [ActivityStreams] 2.0 data format. 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 content. www.w3.org/TR/activitypub/#inbox . We don’t ever make requests to our own Inbox, though — other servers will do that when they want to notify us of something. When we make a new post, ActivityPub Starter Kit will go through each follower and make a request to their Inbox.

  2. /admin/create is not, strictly speaking, an ActivityPub endpoint, but it does follow the client-to-server interaction guidance on Object creation without the Create Activity ActivityPub The ActivityPub protocol is a decentralized social networking protocol based upon the [ActivityStreams] 2.0 data format. 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 content. www.w3.org/TR/activitypub/#object-without-create .

  3. In ActivityPub parlance, an Actor (the account) performs an Activity (some sort of action) on an Object (a thing). Darius Kazemi has a good blog post breaking this down a bit more reading-activitypub tinysubversions.com/notes/reading-activitypub/ .