Skip to content

Using bovine with the fediverse-pasture

The fediverse-pasture is a way to use docker compose to run Fediverse applications in a local environment. The applications run on a common docker network fediverse-pasture. This means that from within this network, one can access mastodon via http://mastodon, and due to port forwarding one can access mastodon at http://localhost:2970/. For this tutorial, we assume that you have one of the provided applications set up and can use its user interface.

This tutorial does not require to install bovine or even use python, instead we will be using docker compose. For this create the following docker compose file.

docker-compose.yaml
services:
  bovine:
    image: helgekr/bovine-3.12:edge
    command: python -mbovine.testing serve --port 80 --reload
  repl:
    image: helgekr/bovine-3.12:edge
    command: python -mbovine.testing shell
    depends_on: [bovine]
networks:
  default:
    name: fediverse-pasture
    external: true

Being in the same network as the containers from the fediverse-pasture is important so that they can talk with each other.

Messaging with the repl

To start the repl (and the server) run

docker compose run repl

This will take you into a ptpython repl with some globals set to allow to communicate with your server — in this example misskey.

To check connectivity, one can run with the aiohttp.ClientSession

>>> await session.get("http://misskey")

which should return a successful response. We can then lookup the misskey account kitty via

>>> kitty = webfinger("acct:kitty@misskey")
>>> kitty

Here webfinger is a convenience wrapper around lookup_uri_with_webfinger. The actor profile of kitty can be retrieved by using the actor via

>>> await actor.get(kitty)
The response
    {
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v1",
        {
            "Key": "sec:Key",
            "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
            "sensitive": "as:sensitive",
            "Hashtag": "as:Hashtag",
            "quoteUrl": "as:quoteUrl",
            "toot": "http://joinmastodon.org/ns#",
            "Emoji": "toot:Emoji",
            "featured": "toot:featured",
            "discoverable": "toot:discoverable",
            "schema": "http://schema.org#",
            "PropertyValue": "schema:PropertyValue",
            "value": "schema:value",
            "misskey": "https://misskey-hub.net/ns#",
            "_misskey_content": "misskey:_misskey_content",
            "_misskey_quote": "misskey:_misskey_quote",
            "_misskey_reaction": "misskey:_misskey_reaction",
            "_misskey_votes": "misskey:_misskey_votes",
            "_misskey_summary": "misskey:_misskey_summary",
            "isCat": "misskey:isCat",
            "vcard": "http://www.w3.org/2006/vcard/ns#",
        },
    ],
    "type": "Person",
    "id": "http://misskey/users/9zhzah70ie0k0001",
    "inbox": "http://misskey/users/9zhzah70ie0k0001/inbox",
    "outbox": "http://misskey/users/9zhzah70ie0k0001/outbox",
    "followers": "http://misskey/users/9zhzah70ie0k0001/followers",
    "following": "http://misskey/users/9zhzah70ie0k0001/following",
    "featured": "http://misskey/users/9zhzah70ie0k0001/collections/featured",
    "sharedInbox": "http://misskey/inbox",
    "endpoints": {"sharedInbox": "http://misskey/inbox"},
    "url": "http://misskey/@kitty",
    "preferredUsername": "kitty",
    "name": None,
    "summary": None,
    "_misskey_summary": None,
    "icon": None,
    "image": None,
    "tag": [],
    "manuallyApprovesFollowers": False,
    "discoverable": True,
    "publicKey": {
        "id": "http://misskey/users/9zhzah70ie0k0001#main-key",
        "type": "Key",
        "owner": "http://misskey/users/9zhzah70ie0k0001",
        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn6b16A7qdnOozeL/OrC3\nKgYwVZaw56CCBEae2T0F0eZrNIHQcL2UGdMtHHRlGYhSAc7ji1xelF7qKNZKlanP\n0y3cEGMmYE2P3MMRlVc4jQkYYjmRDUokxhNwnX72HSt/UlZaKDfg/x6rzWfJek2g\nicyUTXzh42TpN3wsJA4MCVzSpA5SuIyDYHG56ceMl+K7MR/xhFaq0lecKjN6w6mU\nP+8T08LMSlqwi+/XV4rAD9o9DEvuoxGTfGbLDPEdM28DXTiIURrvoHa7lXXtNOdn\nRQaQwZ71GpB9werR2WhJK9xKciUsv9vnYpymXDOWy826TG5QNibST6unHLus5ito\nQQIDAQAB\n-----END PUBLIC KEY-----\n",
    },
    "isCat": False,
}

By updating the profile and refetching, one can see the changes to the ActivityPub object. We will also store the inbox for further use

>>> inbox = (await actor.get(kitty))["inbox"]

Using the object_factory and the activity_factory, we can now build a note addressed to kitty

>>> note = object_factory.note(content="mooo", to={kitty}).build()
>>> activity = activity_factory.create(note).build()
content of the variable activity
{
    "@context": "https://www.w3.org/ns/activitystreams",
    "type": "Create",
    "actor": "http://bovine/milkdrinker",
    "to": ["http://misskey/users/9zhzah70ie0k0001"],
    "id": "http://bovine/akikcrvf",
    "published": "2024-10-18T07:42:15Z",
    "object": {
        "@context": "https://www.w3.org/ns/activitystreams",
        "type": "Note",
        "attributedTo": "http://bovine/milkdrinker",
        "to": ["http://misskey/users/9zhzah70ie0k0001"],
        "id": "http://bovine/UetSfIWf",
        "published": "2024-10-18T07:42:13Z",
        "content": "mooo",
    },
}

We are now ready to post this to the inbox via

>>> await actor.post(inbox, activity)

This should then trigger a sound from the Misskey user interface and you will find a direct message in it.

Receiving messages

Kitty now replies to our message with “meow”. We will not be able to see this from the repl. This is due to the message being received by the server bovine. In order to view it, one has to open another terminal and run:

docker compose logs bovine  -f
Output
bovine-bovine-1  | [2024-10-18 07:28:58 +0000] [1] [INFO] Running on http://0.0.0.0:80 (CTRL + C to quit)
bovine-bovine-1  | INFO:hypercorn.error:Running on http://0.0.0.0:80 (CTRL + C to quit)
bovine-bovine-1  |  * Serving Quart app 'bovine.testing.server'
bovine-bovine-1  |  * Debug mode: False
bovine-bovine-1  |  * Please use an ASGI server (e.g. Hypercorn) directly in production
bovine-bovine-1  |  * Running on http://0.0.0.0:80 (CTRL + C to quit)
bovine-bovine-1  | [2024-10-18 07:44:34 +0000] [1] [INFO] 172.18.0.7:49512 GET /milkdrinker 1.1 200 832 812
bovine-bovine-1  | Received in inbox from http://misskey/users/9zhzah70ie0k0001
bovine-bovine-1  | {
bovine-bovine-1  |   "@context": [
bovine-bovine-1  |     "https://www.w3.org/ns/activitystreams",
bovine-bovine-1  |     "https://w3id.org/security/v1",
bovine-bovine-1  |     {
bovine-bovine-1  |       "Key": "sec:Key",
bovine-bovine-1  |       "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
bovine-bovine-1  |       "sensitive": "as:sensitive",
bovine-bovine-1  |       "Hashtag": "as:Hashtag",
bovine-bovine-1  |       "quoteUrl": "as:quoteUrl",
bovine-bovine-1  |       "toot": "http://joinmastodon.org/ns#",
bovine-bovine-1  |       "Emoji": "toot:Emoji",
bovine-bovine-1  |       "featured": "toot:featured",
bovine-bovine-1  |       "discoverable": "toot:discoverable",
bovine-bovine-1  |       "schema": "http://schema.org#",
bovine-bovine-1  |       "PropertyValue": "schema:PropertyValue",
bovine-bovine-1  |       "value": "schema:value",
bovine-bovine-1  |       "misskey": "https://misskey-hub.net/ns#",
bovine-bovine-1  |       "_misskey_content": "misskey:_misskey_content",
bovine-bovine-1  |       "_misskey_quote": "misskey:_misskey_quote",
bovine-bovine-1  |       "_misskey_reaction": "misskey:_misskey_reaction",
bovine-bovine-1  |       "_misskey_votes": "misskey:_misskey_votes",
bovine-bovine-1  |       "_misskey_summary": "misskey:_misskey_summary",
bovine-bovine-1  |       "isCat": "misskey:isCat",
bovine-bovine-1  |       "vcard": "http://www.w3.org/2006/vcard/ns#"
bovine-bovine-1  |     }
bovine-bovine-1  |   ],
bovine-bovine-1  |   "id": "http://misskey/notes/9zhzqfvlie0k0007/activity",
bovine-bovine-1  |   "actor": "http://misskey/users/9zhzah70ie0k0001",
bovine-bovine-1  |   "type": "Create",
bovine-bovine-1  |   "published": "2024-10-18T07:46:43.809Z",
bovine-bovine-1  |   "object": {
bovine-bovine-1  |     "id": "http://misskey/notes/9zhzqfvlie0k0007",
bovine-bovine-1  |     "type": "Note",
bovine-bovine-1  |     "attributedTo": "http://misskey/users/9zhzah70ie0k0001",
bovine-bovine-1  |     "content": "<p><a href=\"http://bovine/milkdrinker\" class=\"u-url mention\">@milkdrinker@bovine</a> meow</p>",
bovine-bovine-1  |     "published": "2024-10-18T07:46:43.809Z",
bovine-bovine-1  |     "to": [
bovine-bovine-1  |       "http://bovine/milkdrinker"
bovine-bovine-1  |     ],
bovine-bovine-1  |     "cc": [],
bovine-bovine-1  |     "inReplyTo": "http://bovine/UetSfIWf",
bovine-bovine-1  |     "attachment": [],
bovine-bovine-1  |     "sensitive": false,
bovine-bovine-1  |     "tag": [
bovine-bovine-1  |       {
bovine-bovine-1  |         "type": "Mention",
bovine-bovine-1  |         "href": "http://bovine/milkdrinker",
bovine-bovine-1  |         "name": "@milkdrinker@bovine"
bovine-bovine-1  |       }
bovine-bovine-1  |     ]
bovine-bovine-1  |   },
bovine-bovine-1  |   "to": [
bovine-bovine-1  |     "http://bovine/milkdrinker"
bovine-bovine-1  |   ],
bovine-bovine-1  |   "cc": []
bovine-bovine-1  | }
bovine-bovine-1  |
bovine-bovine-1  |
bovine-bovine-1  | [2024-10-18 07:46:43 +0000] [1] [INFO] 172.18.0.7:51036 POST /inbox 1.1 202 7 21042

One sees that the message was received.

Editing the server

By running

docker compose exec bovine python -mbovine.testing edit

one can open the server python file with vim. It is similar to what is describe in the server tutorial.

Creating a note

In this section, we will serve a new note in server. For this we will need to edit the function create_app that creates the Quart app

def create_app():
    app = Quart(__name__)

    ...

    return app

To add a new endpoint serving a note, one can use the following code

    @app.get("/note")
    async def note():
        from bovine.activitystreams import factories_for_actor_object
        _, object_factory = factories_for_actor_object(actor_object)

        note = object_factory.note(id="http://bovine/note",
            content="mooooo").as_public().build()

        return note

where we use object_factory in order to build the note. We can verify the note exists via the shell

>>> await actor.get("http://bovine/note")
{
    "@context": "https://www.w3.org/ns/activitystreams",
    "attributedTo": "http://bovine/milkdrinker",
    "content": "mooooo",
    "published": "2024-10-19T11:09:10Z",
    "to": ["https://www.w3.org/ns/activitystreams#Public"],
    "type": "Note",
}

You can also get it with mitra. If you try to get it with mastodon, you will not be able to find it. The reason for this is that the response will have the content-type application/json. However, most Fediverse applications require the content-type to be either application/activity+json or application/ld+json; profile="https://www.w3.org/ns/activitystreams". So if we update the last line of our function with

        return note, 200, {"content-type": "application/activity+json"}

everything should work.

Updating the actor

By editing actor_object as follows, we add a description to the actor. See the reference for Actor.

actor_object = Actor(
    id=actor_id,
    preferred_username=handle_name,
    name="The Milk Drinker",
    summary="Cows are the best"
    inbox=f"http://{hostname}/inbox",
    outbox=actor_id,
    public_key=public_key,
    public_key_name="main-key",
).build()

When we now reopen the actor in one of the Fediverse applications, we should see the new profile.

Warning

Unfortunately, Fediverse applications often do not offer the ability to refetch the actor object, so you might need to spin up a new one to check (this is easy with the fediverse pasture).

As an exercise, one could now for example implement the /.well-known/host-meta endpoint as many applications seem to like to query it. One could then investigate which applications allow one to use the url specified there instead of the standard webfinger path (if they query it).

The Command line interface

This is the documentation of the python -mbovine.testing command that underlies this tutorial.

python -m bovine.testing

Command line to tool to manage a testing environment

Usage:

python -m bovine.testing [OPTIONS] COMMAND [ARGS]...

Options:

  --debug  Sets the log level to debug.
  --help   Show this message and exit.
edit

Allows one to edit the main server file

Usage:

python -m bovine.testing edit [OPTIONS]

Options:

  --nano  Use nano editor
  --help  Show this message and exit.
serve

Serves the app from the bovine tutorial as a server

Usage:

python -m bovine.testing serve [OPTIONS]

Options:

  --port INTEGER      Port the application runs on
  --reload            Enable auto reloading
  --save_config TEXT  Filename to save configuration to
  --help              Show this message and exit.
shell

Opens a REPL to perform actions using the BovineActor from the server tutorial

Usage:

python -m bovine.testing shell [OPTIONS]

Options:

  --load_config TEXT  Filename to load configuration from
  --help              Show this message and exit.