immers-space / activitypub-express

Modular ActivityPub implementation as Express JS middleware to easily add decentralization and federation to Node apps
MIT License
297 stars 23 forks source link
activitypub express mongodb nodejs

activitypub-express (apex)

npm npm Build Status Coverage Status Matrix Open Collective

Modular implementation of the ActivityPub decentralized social networking protocol, written for NodeJS as ExpressJS middleware. Includes a interchangeable storage interface with a default MongoDB implementation.

Installation

In order for http request signing to function correctly, a patched version of the http-signature library is required. To ensure that the request library is using the correct version for its sub-dependency, you may need to dedupe after installation.

npm install --save activitypub-express
npm dedupe

Usage

const express = require('express')
const { MongoClient } = require('mongodb')
const ActivitypubExpress = require('activitypub-express')

const port = 8080
const app = express()
const routes = {
  actor: '/u/:actor',
  object: '/o/:id',
  activity: '/s/:id',
  inbox: '/u/:actor/inbox',
  outbox: '/u/:actor/outbox',
  followers: '/u/:actor/followers',
  following: '/u/:actor/following',
  liked: '/u/:actor/liked',
  collections: '/u/:actor/c/:id',
  blocked: '/u/:actor/blocked',
  rejections: '/u/:actor/rejections',
  rejected: '/u/:actor/rejected',
  shares: '/s/:id/shares',
  likes: '/s/:id/likes'
}
const apex = ActivitypubExpress({
  name: 'Apex Example',
  version: '1.0.0',
  domain: 'localhost',
  actorParam: 'actor',
  objectParam: 'id',
  activityParam: 'id',
  routes,
  endpoints: {
    proxyUrl: 'https://localhost/proxy'
  }
})
const client = new MongoClient('mongodb://localhost:27017')

app.use(
  express.json({ type: apex.consts.jsonldTypes }),
  express.urlencoded({ extended: true }),
  apex
)
// define routes using prepacakged middleware collections
app.route(routes.inbox)
  .get(apex.net.inbox.get)
  .post(apex.net.inbox.post)
app.route(routes.outbox)
  .get(apex.net.outbox.get)
  .post(apex.net.outbox.post)
app.get(routes.actor, apex.net.actor.get)
app.get(routes.followers, apex.net.followers.get)
app.get(routes.following, apex.net.following.get)
app.get(routes.liked, apex.net.liked.get)
app.get(routes.object, apex.net.object.get)
app.get(routes.activity, apex.net.activityStream.get)
app.get(routes.shares, apex.net.shares.get)
app.get(routes.likes, apex.net.likes.get)
app.get('/.well-known/webfinger', apex.net.webfinger.get)
app.get('/.well-known/nodeinfo', apex.net.nodeInfoLocation.get)
app.get('/nodeinfo/:version', apex.net.nodeInfo.get)
app.post('/proxy', apex.net.proxy.post)
// custom side-effects for your app
app.on('apex-outbox', msg => {
  if (msg.activity.type === 'Create') {
    console.log(`New ${msg.object.type} from ${msg.actor}`)
  }
})
app.on('apex-inbox', msg => {
  if (msg.activity.type === 'Create') {
    console.log(`New ${msg.object.type} from ${msg.actor} to ${msg.recipient}`)
  }
})

client.connect()
  .then(() => {
    apex.store.db = client.db('DB_NAME')
    return apex.store.setup()
  })
  .then(() => {
    app.listen(port, () => console.log(`apex app listening on port ${port}`))
  })

Next Steps and Examples

Okay so you've got an ActivityPub server, now what?

Server-to-server apps: For an app that people interact with by sending messages from another app (e.g. Mastodon), you'll want to define custom side-effects using app.on('apex-inbox', ({ actor, activity, recipient, object }) => {...}), which is fired for each incoming message.

For an example of a server-to-server app build with activitypub-expresss, check out Guppe Groups - the federated social groups app server built with just 250 lines of code.

Full platform: For a full-fledged app with its own users, the next thing you'll probably want is user authentication and authorization. Apex integrates well with Passport.js, but can work with anything.

For an example of a full social platform built with activitypub-express, check out Immers Space - the federated social platform for virtual worlds.

API

ActivitypubExpress initializer

Configures and returns an express middleware function that must be added to the route before any other apex middleware. It needs to be configured with the routes you will use in order to correctly generate IRIs and actor profiles

const ActivitypubExpress = require('activitypub-express')
const apex = ActivitypubExpress(options)
app.use(apex)
Option Description
Required
name String. Name of your app to list in nodeinfo
version String. Version of your app to list in nodeinfo
baseUrl String. Server URL origin. Also used as URI prefix. Should be the public-facing URL when using a reverse proxy. Overrides domain if set
domain String. Hostname for your app
actorParam String. Express route parameter used for actor name
objectParam String. Express route parameter used for object id
routes Object. The routes your app uses for ActivityPub endpoints (including parameter). Details below
routes.actor Actor profile route & IRI pattern
routes.object Object retrieval route & IRI pattern
routes.activity Activity retrieval route & IRI pattern
routes.inbox Actor inbox route
routes.outbox Actor outbox route
routes.following Actor following collection route
routes.followers Actor followers collection route
routes.liked Actor liked collection route
routes.blocked Actor's blocklist
routes.rejected Activities rejected by actor
routes.rejections Actor's activities that were rejected by recipient
routes.shares Activity shares collection route
routes.likes Activity likes collection route
routes.collections Actors' miscellaneous collections route (must include actorParam and collectionParam)
Optional
activityParam String. Express route parameter used for activity id (defaults to objectParam)
collectionParam String. Express route parameter used for collection id (defaults to objectParam)
pageParam String. Query parameter used for collection page identifier (defaults to page)
itemsPerPage Number. Count of items in each collection page (default 20)
context String, Object, Array. JSON-LD context(s) to use with your app in addition to the base AcivityStreams + Security vocabs
endpoints Object. Optional system-wide api endpoint URLs included in actor objects: proxyUrl, oauthAuthorizationEndpoint, oauthTokenEndpoint, provideClientKey, signClientKey, sharedInbox, uploadMedia
logger Object with info, warn, error methods to replace console
store Replace the default storage model & database backend with your own (see store/interface.js for API)
threadDepth Controls how far up apex will follow links in incoming activities in order to display the conversation thread & check for inbox forwarding needs (default 10)
systemUser Actor object representing system and used for signing GETs (see below)
offlineMode Disable delivery. Useful for running migrations and queueing deliveries to be sent when app is running
requestTimeout Timeout for requests to other servers, ms (default 5000)
openRegistrations Advertise via nodeinfo if an instance allows instant registration (default false)
nodeInfoMetadata Object of additional data to provde in nodeinfo reponses

Blocked, rejections, and rejected: these routes must be defined in order to track these items internally for each actor, but they do not need to be exposed endpoints (and probably should not be public even then)

System User / GET authentication

Some federated apps may require http signature authentication on GET requests. To enable this functionality, set the systemUser property on your apex instance equal to an actor created with createActor (generally of type 'Service') and saved to your object store. Its keys will be used to sign all federated object retrieval requests. This property can be set after initializing your apex instance, as you will need access to the createActor method and a database connection.

const ActivitypubExpress = require('activitypub-express')
const apex = ActivitypubExpress(options)
// ... connect to db
apex.createActor('system-user', 'System user', '', null, 'Service')
  .then(async actor => {
    await apex.store.saveObject(actor)
    apex.systemUser = actor
  })

FAQ

Q: How do I resolve this error seen when receiving/delivering activities or running the federation tests: Uncaught exception: InvalidHeaderError: bad param format

A: Run npm dedupe to ensure that the request library is using the patched version of the http-signature library.

Implementation status

Implementation notes

Federation notes