ActivityStreams over XMPP

Mon 17 January 2022

ActivityPub is a federated protocol for social networking that is used by projects such as Pleroma, PeerTube, Mobilizon or Mastadon (and many more).

ActivityPub can be seen as a HTTP based protocol for sending around pieces of information that describe interactions by and between actors. An interaction might be the act of creating a post addressed to some audience, commenting on a post or forwarding a post. These interactions, or activities, are described using the ActivityStreams Vocabulary serialized as JSON-LD.

The openEngiadina project is researching a semantic social network - a system for creating and sharing complex data. As previously mentioned we have shifted from using ActivityPub to XMPP as a protocol for sending around pieces of information. We still use the ActivityStreams vocabulary for describing social interactions.

In this post I'd like to describe how ActivityStreams can be used over XMPP, how we have implemented this in openEngiadina and how a bridge between ActivityPub and XMPP might work.

ActivityStreams as an RDF Vocabulary

The ActivityStreams vocabulary defines types (e.g. Activity, Create or Person), properties (e.g. actor or to), rules how they may be combined as well as their meaning. The vocabulary is defined primarily for usage in JSON documents, or more specifically JSON-LD. The examples in the ActivityStreams specification use JSON and so does ActivityPub.

However, this is not the only way you can use the the ActivityStreams vocabulary. The ActivityStreams vocabulary is also provided as an RDF vocabulary. This allows terms to be used from any RDF data, combined with other existing RDF vocabularies and serialized with formats other than JSON-LD. For example a Create activity might be serialized to RDF/Turtle:

@prefix as: <https://www.w3.org/ns/activitystreams#> .

<urn:uuid:9282e9cc-14d0-42b3-a758-d6aeca6c876b>
    a as:Create ;
    as:object <urn:uuid:d18c55d4-8a63-4181-9745-4e6cf7938fa1> ;
    as:to <https://social.example/alyssa/followers/> .

<urn:uuid:d18c55d4-8a63-4181-9745-4e6cf7938fa1> 
    a as:Note ;
    content: "Turtles all the way down." .

RDF in XML's clothing

Another serialization of RDF is RDF/XML - a serialization of RDF into XML. An ActivityStreams Activity can be serialized to RDF/XML as such:

<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
     xmlns:as="http://www.w3.org/ns/activitystreams#">

  <rdf:Description rdf:about="urn:uuid:9282e9cc-14d0-42b3-a758-d6aeca6c876b">
    <rdf:type rdf:resource="http://www.w3.org/ns/activitystreams#Create"/>
    <as:object rdf:resource="urn:uuid:d18c55d4-8a63-4181-9745-4e6cf7938fa1"/>
    <as:to rdf:resource="https://social.example/alyssa/followers/"/>
  </rdf:Description>

  <rdf:Description rdf:about="urn:uuid:fd18c55d4-8a63-4181-9745-4e6cf7938fa1">
    <rdf:type rdf:resource="http://www.w3.org/ns/activitystreams#Note"/>
    <as:content> XML, oh XML.</as:content>
  </rdf:Description>

</rdf:RDF>

Beautiful, eh?

XML might not be nice and using it as a serialization of RDF is maybe not the best idea. But it works and we can use this to transport ActivityStreams over XMPP.

XEP-0277, Atom and ActivityStreams

As mentioned in a previous post, there is an XMPP extension that allows XMPP to be used for microblogging (XEP-0277: Microblogging over XMPP). The idea is to use the Atom Syndication Format (a XML format) and publish posts on a dedicated node that is attached to a XMPP user (see XEP-0163: Personal Eventing Protocol).

A simple post might look like this:

<entry xmlns='http://www.w3.org/2005/Atom'>
  <title type='text'>hanging out at the Caf&amp;#233; Napolitano</title>
  <id>tag:montague.lit,2008-05-08:posts-1cb57d9c-1c46-11dd-838c-001143d5d5db</id>
  <published>2008-05-08T18:30:02Z</published>
  <updated>2008-05-08T18:30:02Z</updated>
</entry>

Posts of this form are supported by XMPP clients such as Movim and Libervia. When transported over HTTP this format is supported by countless feed aggregators.

We can embed the ActivityStreams activity into an Atom entry by using the RDF/XML serialization:

<entry xmlns="http://www.w3.org/2005/Atom">
  <title type="text">An ActivityStreams Post</title>
  <author>
      <name>Alyssa</name>
      <uri>xmpp:alyssa@social.example</uri>
  </author>
  <id>urn:uuid:9282e9cc-14d0-42b3-a758-d6aeca6c876b</id>
  <updated>2021-01-16T18:28:00Z</updated>
  <content type="application/rdf+xml">
    <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
        xmlns:as="http://www.w3.org/ns/activitystreams#">

    <rdf:Description rdf:about="urn:uuid:9282e9cc-14d0-42b3-a758-d6aeca6c876b">
        <rdf:type rdf:resource="http://www.w3.org/ns/activitystreams#Create"/>
        <as:object rdf:resource="urn:uuid:d18c55d4-8a63-4181-9745-4e6cf7938fa1"/>
        <as:to rdf:resource="https://social.example/alyssa/followers/"/>
    </rdf:Description>

    <rdf:Description rdf:about="urn:uuid:fd18c55d4-8a63-4181-9745-4e6cf7938fa1">
        <rdf:type rdf:resource="http://www.w3.org/ns/activitystreams#Note"/>
        <as:content> XML, oh XML.</as:content>
    </rdf:Description>

    </rdf:RDF>
  </content>
  <link rel="alternate" type=application/json+ld">
      https://social.example/post/0/json-ld
  </link>
</entry>

This is (as far as I understand) a fully compliant Atom entry. Such an embedding would allow us to publish ActivityStreams on XMPP (via XEP-0277). More generally, this can be used to publish ActivityStreams to any Atom feed aggregator (over HTTP).

However, there are some disadvantages:

  • An Atom entry requires certain fields to be present (e.g. author, title). For some of these (e.g. author) it seems very straighforward to extract from the ActivityStreams objects. For others, in particular the title field, it's tricky. For a reliable embedding one would need to find a good heuristic for finding a meaningful title from arbirtary content.
  • In order to understand the content of such an Atom entry, one has to parse and use the RDF/XML content.

Interestingly, the first version of ActivityStreams, that was published in 2011, was defined as an embedding into - you guessed it - Atom (Atom Activity Streams 1.0). Later in 2011 JSON Activity Streams 1.0 was released where ActivityStreams are embedded into JSON. This is then developed into ActivityStreams 2.0, which uses JSON-LD. By using the RDF backdoor of JSON-LD and serializing to RDF/XML we have gone full circle back to Atom in a decade. I wonder what we will be doing in 2032?

A dedicated PEP node

While the embedding of ActivityStreams in Atom is interesting, it does not seem to be very practical. Existing feed readers or XMPP clients that implement XEP-0277 would not be able to parse and understand the RDF/XML content.

For openEngiadina, the ability to use RDF is very important for being able to expressing the multitude of different contents we would like to be able to handle. Supporting RDF is a core tenet and carrying around an RDF/XML parser is a necessary burden. The only benefit of fitting RDF content into Atom posts would have been interoperability with XEP-0277 and feed aggregators. This is not possible, so we might as well not bother with using Atom.

XEP-0163: Personal Eventing Protocol is an XMPP extensions that allows XMPP Publish-subscribe nodes to be attached directly to XMPP users (PEP nodes). Nodes are named and users can create as many as they like. The XMPP extension for microblogging (XEP-0277) uses the name urn:xmpp:microblog:0 for its PEP nodes. Other services use different names (e.g. org.salut-a-toi.event:0 is used for events in Libervia).

In the latest version of GeoPub we have started using the PEP nodes with name net.openengiadina.xmpp.activitystreams for publishing ActivityStreams. ActivityStreams objects are formated as RDF/XML and published as is without any further wrapping.

When publishing some ActivityStreams following XMPP IQ is sent:

<iq xmlns="jabber:client" id="f800e0f4-b178-4bcc-964e-3ffdd5f8d680" 
    type="set" 
    from="user@strawberry.local/WCHe5clN" 
    to="user@strawberry.local">
  <pubsub xmlns="http://jabber.org/protocol/pubsub">
    <publish node="net.openengiadina.xmpp.activitystreams">
      <item id="urn:uuid:82c73927-22bb-4fbd-8816-9eb8b63e7990">
        <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 
                 xmlns:as="http://www.w3.org/ns/activitystreams#" 
                 xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#">
          <rdf:Description rdf:about="urn:uuid:195096cc-fbf1-45b2-8573-696502871116">
            <rdf:type rdf:resource="http://www.w3.org/ns/activitystreams#Note"/>
            <as:content>
              Hello from GeoPub!
            </as:content>
          </rdf:Description>
          <rdf:Description rdf:about="urn:uuid:82c73927-22bb-4fbd-8816-9eb8b63e7990">
            <rdf:type rdf:resource="http://www.w3.org/ns/activitystreams#Create"/>
            <as:actor rdf:resource="xmpp:user@strawberry.local"/>
            <as:object rdf:resource="urn:uuid:195096cc-fbf1-45b2-8573-696502871116"/>
            <as:published rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">
              2022-01-17T10:16:41-00:00
            </as:published>
          </rdf:Description>
        </rdf:RDF>
      </item>
    </publish>

Users who are subscribed to the PEP node will then get the ActivityStreams activity pushed to them in an XMPP message.

What is nice about this PEP node is that it is not bound to microblogging. This node can be used to share short notes, events, music, videos, OpenStreetMap edits, tasks in a ticketing system or any other structured data and also the interconnections between these pieces of data. ActivityStreams is used as a vocabulary to frame these interactions (similar to how Atom is used).

If you would like to try this you can use GeoPub version 0.4.0. A demo installation is available here.

In the future this might be documented as a proper XEP and it we might use the urn:xmpp:activitystreams:0 name for nodes. For now we will experiment with this from openEngiadina and try to gain some experience.

Bridge between ActivityPub and XMPP

By just using a different RDF serialization of ActivityStreams we have a very high level of interoperability. To create a bridge between ActivityPub and XMPP we just convert the content from RDF/XML to JSON-LD. This conversion is lossless, in the sense that the content itself (i.e. in the RDF data model) remains exactly the same. It is just a conversion between two serialization formats.

Granted, this is not an easy conversion. It requires parsing RDF/XML, serializing to JSON-LD (while using the JSON-LD Framing Algorithms to make sure it is in a form expected by ActivityPub) and vice-versa. But it is in principle possible and can be done in more central places, without requiring clients to carry around both serializations. For programming languages such as Python or JavaScript libraries exist for handling both serialization formats.

Another aspect, which I have completely ignored, is addressing. A bridge between ActivityPub and XMPP would also need to make sure content is sent to the right place. The Libervia project is currently working on this and I look forward to interoperability beyond protocols that this might allow.

Next Steps

Now that RDF content can be sent over XMPP we would like to explore ideas on how to make the content discoverable and easier to interact with by quering and infering. Stay tuned!

Fin

My fellow openEngiadina developer rustra is still under arrest in Belarus. Resist political repression and support the victims! Please consider donating to the Anarchist Black Cross Belarus.

Thank you for making it so far and thank you very much for your interest! Thanks also to the kind people at libervia@chat.jabberfr.org for the nice historical trivia on ActivityStreams in Atom and to Jos van der Oever for the interesting discussions.

Please get in touch by mail (pukkamustard [at] posteo [dot] net) or by joining the openEngiadina chat (via IRC or Matrix).

The openEngiadina projects is supported by the NLnet Foundation trough the NGI0 Discovery Fund.

pukkamustard