/activitypunk

live in fediverse without a server

Primary LanguagePython

ActivityPunk

A command-line tool for participating in the fediverse without a server.

Install with pip

pip install git+https://github.com/jakekara/activitypunk.git

Webfinger and actor lookup

Use the following command to do a webfinger lookup:

apunk webfinger leo@twit.social

The output looks like this:

{
  "subject": "acct:leo@twit.social",
  "aliases": ["https://twit.social/@leo", "https://twit.social/users/leo"],
  "links": [
    {
      "rel": "http://webfinger.net/rel/profile-page",
      "type": "text/html",
      "href": "https://twit.social/@leo"
    },
    {
      "rel": "self",
      "type": "application/activity+json",
      "href": "https://twit.social/users/leo"
    },
    {
      "rel": "http://ostatus.org/schema/1.0/subscribe",
      "template": "https://twit.social/authorize_interaction?uri={uri}"
    }
  ]
}

Get a user's actor data

Use the following command to get a user's actor object.

The output will look like:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
    {
      "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
      "toot": "http://joinmastodon.org/ns#",
      "featured": {
        "@id": "toot:featured",
        "@type": "@id"
      },
      "featuredTags": {
        "@id": "toot:featuredTags",
        "@type": "@id"
      },
      "alsoKnownAs": {
        "@id": "as:alsoKnownAs",
        "@type": "@id"
      },
      "movedTo": {
        "@id": "as:movedTo",
        "@type": "@id"
      },
      "schema": "http://schema.org#",
      "PropertyValue": "schema:PropertyValue",
      "value": "schema:value",
      "discoverable": "toot:discoverable",
      "Device": "toot:Device",
      "Ed25519Signature": "toot:Ed25519Signature",
      "Ed25519Key": "toot:Ed25519Key",
      "Curve25519Key": "toot:Curve25519Key",
      "EncryptedMessage": "toot:EncryptedMessage",
      "publicKeyBase64": "toot:publicKeyBase64",
      "deviceId": "toot:deviceId",
      "claim": {
        "@type": "@id",
        "@id": "toot:claim"
      },
      "fingerprintKey": {
        "@type": "@id",
        "@id": "toot:fingerprintKey"
      },
      "identityKey": {
        "@type": "@id",
        "@id": "toot:identityKey"
      },
      "devices": {
        "@type": "@id",
        "@id": "toot:devices"
      },
      "messageFranking": "toot:messageFranking",
      "messageType": "toot:messageType",
      "cipherText": "toot:cipherText",
      "suspended": "toot:suspended",
      "Emoji": "toot:Emoji",
      "focalPoint": {
        "@container": "@list",
        "@id": "toot:focalPoint"
      }
    }
  ],
  "id": "https://twit.social/users/leo",
  "type": "Person",
  "following": "https://twit.social/users/leo/following",
  "followers": "https://twit.social/users/leo/followers",
  "inbox": "https://twit.social/users/leo/inbox",
  "outbox": "https://twit.social/users/leo/outbox",
  "featured": "https://twit.social/users/leo/collections/featured",
  "featuredTags": "https://twit.social/users/leo/collections/tags",
  "preferredUsername": "leo",
  "name": "Chief TWiT :twit:",
  "summary": "<p>Leo Laporte, podcaster, broadcaster, tech pundit. Founder of the TWiT Podcast Network <a href=\"https://twit.tv\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><span class=\"invisible\">https://</span><span class=\"\">twit.tv</span><span class=\"invisible\"></span></a>. The Tech Guy on the Premiere Radio Networks nationwide. <a href=\"https://techguylabs.com\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><span class=\"invisible\">https://</span><span class=\"\">techguylabs.com</span><span class=\"invisible\"></span></a>. Blog and contact info at <a href=\"https://leo.fm/about\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><span class=\"invisible\">https://</span><span class=\"\">leo.fm/about</span><span class=\"invisible\"></span></a></p>",
  "url": "https://twit.social/@leo",
  "manuallyApprovesFollowers": false,
  "discoverable": true,
  "published": "2019-12-31T00:00:00Z",
  "devices": "https://twit.social/users/leo/collections/devices",
  "alsoKnownAs": ["https://mastodon.social/users/leolaporte"],
  "publicKey": {
    "id": "https://twit.social/users/leo#main-key",
    "owner": "https://twit.social/users/leo",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqewyymBKsYBblF14/cgm\n4ImlXAqBAN/W65oNMZJZ0Y83SSHkdDasav0402ISY4e12sWQfSJYdFfg/AqEq9Ok\n09zDtmlMxhB6evcFLwUIlQm4Nxx/iKowiPIzj5N0E7JO1JhIvrNNBufqFfp1DHgO\nQralXQpcfClqymG1ljntjLMmjlopJ8FzulCo4LCHijrJzPfswrtvL5KcWR5xEouu\nTeknhcd63PiTQpSJRi3sGF1LZcxZ4GHaOOtjCMqs7YwqWXytM0W7p8EoLybN4q8K\np1iLLaYLM1OojPp9APw+anIqrz1x7Fj5ejApIFgygGSunWcO1YYZR86wtIh4Jfs8\nBwIDAQAB\n-----END PUBLIC KEY-----\n"
  }
}

Sending a DM

This is where it gets interesting. While the webfinger and actor subcommands do read-only stuff, this allows you to DM anyone on the fediverse. (Well, actually I've only tested it out with Mastodon.)

Before you can do these steps, you need to follow this tutorial on the Mastodon blog. Don't worry that the parts about sending messages won't actually work because they're out-dated. All you need to do is make sure you set up your webfinger and actor files hosted on a static website and you have the public.pem and private.pem keyfiles from that tutorial. By that point you should be able to look yourself up by doing a user search from any mastodon instance -- pretty cool!

Ok, back in business.

Create a config file at ~/.activitypunk/config that defines some stuff about your identity. For example, mine looks like this:

[default]
user_at_host = jake@jakekara.com
actor = https://jakekara@jakekara.com/activitypub/actors.jakekara.json
private_key_file = ~/.activitypunk/private.pem
public_key_file = ~/.activitypunk/public.pem
s3_bucket = activitypub
data_dir = ~/.activitypunk/data

Now we should be ready to send a DM. Let's assume I want to send one to myself on twit.social:

apunk dm --to jakek@twit.social "Hello there fellow hacktivitypunk!"

If that works, you'll see nothing interesting. Probably just the following output:

b''

which means pretty much nothing. But you can now go over to your accountin your favorite Mastodon client, if you sent a message to yourself, and see your message!

Setting up an inbox

So far we've established our inbox and our ability to send messages. But we need a way to receive them.

We're going to build an ActivityPub inbox in AWS lambda that listens for HTTP requests and writes them to an s3 bucket called activitypub in a folder called events.

If you prefer something other than AWS Lambda and S3, you can still use this appraoch as a guide and reimplement in your stack, like Flask or whatever.

Go set up an s3 bucket called activitypub and lambda that writes to. You can grab code for the lambda with this gist I wrote. I'll assume you can set up the necessary permissions to write to the lambda. Now add an API gateway trigger that passes requests to that lambda. The API gateway will have a public facing URL. Remember that actor file you created when doing the Mastodon blog post tutorial? Make sure the inbox property in the root of your actor file points to this URL.

I'll wait.

So you might have noticed these lines in the config file:

s3_bucket = activitypub
data_dir = ~/.activitypunk/data

These correspond to an s3_bucket you created, and a local folder where you want to download local copies of the events.

Now that you have your inbox set up and listening, respond to that DM you sent to yourself in the last section from your favorite Mastodon client.

Cool.

Now run:

apunk inbox --sync

There should be at least one file created in ~/.activitypunk/data/s3/events, and if you open it up, it will be your message. It will be the entire HTTP request. The ActivityPub part is in the body property.

Pretty cool, right?

Use cron to autosync

Oh before you go, here's a way to run that sync every hour:

# Run at the 18th minute every hour
18 * * * * apunk inbox --sync >> ~/.activitypunk/synclog.txt