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.
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
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
which should return a successful response. We can then lookup the misskey account kitty
via
Here webfinger
is a convenience wrapper around lookup_uri_with_webfinger.
The actor profile of kitty can be retrieved by using the
actor via
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
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()
{
"@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
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:
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
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
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
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:
Options:
edit
Allows one to edit the main server file
Usage:
Options:
serve
Serves the app from the bovine tutorial as a server
Usage:
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:
Options: