Skip to main content

Core functionality of bovine needed to build client to server applications

Project description

Bovine

This package contains two essential parts of bovine. First it defines BovineActor, which contains all the necessities to write ActivityPub Clients. Furthermore, this package contains the cryptographic routines to verify HTTP signatures.

Furthermore, the folder examples contains a few examples on how BovineActor can be used. The cryptographic routines are used in bovine_fedi to verify signatures.

Documentation is available at ReadTheDocs

Example: Make a post aka Faking at being a Server

While ActivityPub specifies Server to Server and Client to Server, they really are just two sides of the same coin. In this example, we will work through how to use BovineActor to post a message.

Without having an ActivityPub Server supporting Client to Server, this will require a bit of setup. This setup will build a stub server that just allows other ActivityPub servers to associate us with a domain.

The stub server is given by the following snippet. One should note that it just answers with predefined json from a config file, that hasn't been generated yet. One could easily replace it with serving static files. See also the Mastodon Blog for a similar implementation.

import tomli
import json
from quart import Quart

app = Quart(__name__)

with open("server.toml", "rb") as fp:
    config = tomli.load(fp)

@app.get("/.well-known/webfinger")
async def webfinger():
    return json.loads(config["webfinger"])


@app.get("/actor")
async def actor():
    return json.loads(config["actor"])

if __name__ == "__main__":
    app.run()

The following script generates the config files. You will have to adapt the hostname variable and be able to serve the entire thing through https.

import bovine
import tomli_w
import json

hostname = "bovine-demo.mymath.rocks"

public_key, private_key = bovine.utils.crypto.generate_public_private_key()
actor_url = f"https://{hostname}/actor"
actor = (
    bovine.activitystreams.build_actor("actor")
    .with_account_url(actor_url)
    .with_public_key(public_key)
)

webfinger = {
    "subject": f"acct:actor@{hostname}",
    "links": [
        {
            "href": actor_url,
            "rel": "self",
            "type": "application/activity+json",
        }
    ],
}

server_config = {"actor": json.dumps(actor.build()), "webfinger": json.dumps(webfinger)}

actor_config = {
    "account_url": actor_url,
    "public_key_url": f"{actor_url}#main-key",
    "private_key": private_key,
}

with open("server.toml", "wb") as fp:
    tomli_w.dump(server_config, fp)

with open("bovine.toml", "wb") as fp:
    tomli_w.dump(actor_config, fp)

You can now access the urls, which are in my case https://bovine-demo.mymath.rocks/actor and https://bovine-demo.mymath.rocks/.well-known/webfinger?resource=acct:actor@bovine-demo.mymath.rocks. Using this, we can now lookup the fediverse handle actor@bovine-demo.mymath.rocks on most FediVerse applications.

You can now send a post via the following code snippet:

import asyncio

from uuid import uuid4
from bovine import BovineActor

target_account = "https://mas.to/users/themilkman"


async def run():
    async with BovineActor.from_file("bovine.toml") as actor:
        activity_factory, object_factory = actor.factories
        note = (
            object_factory.note("Hello")
            .add_to(target_account)
            .with_mention(target_account)
            .build()
        )
        note["id"] = actor.actor_id + "/" + str(uuid4())
        create = activity_factory.create(note).build()
        create["id"] = actor.actor_id + "/" + str(uuid4())

        remote_actor = await actor.get(target_account)
        target_inbox = remote_actor["inbox"]
        await actor.post(target_inbox, create)


asyncio.run(run())

A few comments are in order:

  • The id needs to be set on the Note and Create in order to be compatible with Mastodon. When using proper Client To Server as below, it is superfluous
  • The form of adding the target_account to both to and mention causes it to be a direct message.

Using BovineClient

One can import it via from bovine import BovineClient. Then one can either use it via:

async with BovineClient(config) as actor:
    ...
# or
actor = BovineClient(config)
await actor.init()

Here the config object can be present in two variants. First it can contain the keys host and private_key, where host is the domain the ActivityPub Actor is on and private_key is a mutlicodec encoded Ed25519 key, whose corresponding did-key has been added to the Actor. In this case Moo-Auth-1 will be used. The second variant is to use HTTP Signatures, where the keys account_url, public_key_url, and private_key need to be present. Alternatively, to passing a config object, one can use BovineActor.from_file(path_to_toml_file).

Making a post

BovineActor contains two factories to create ActivityStreams Objects and ActivityStreams Activities. One can obtain them by running

activity_factory, object_factory = actor.factories

The simplest usage example is a create wrapping a note, that looks like:

activity_factory, object_factory = actor.factories
note = object_factory.note("Hello").as_public().build()
create = activity_factory.create(note).build()

The result should be the something equivalent to the json

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Create",
  "actor": "https://domain/actor",
  "object": {
    "attributedTo": "https://domain/actor",
    "type": "Note",
    "content": "Hello",
    "published": "2023-03-25T08:12:32Z",
    "to": "as:Public",
    "cc": "https://domain/followers_collection"
  },
  "published": "2023-03-25T08:12:32Z",
  "to": "as:Public",
  "cc": "https://domain/followers_collection"
}

The details depend on the used actor and will likely contain superfluous elements until the creation process is improved. We can now send this activity to our outbox using

await actor.send_to_outbox(create)

Note: This is different from what we did in the first example, where we used await actor.post(inbox, create). The difference is that in the first example, we faked being a server, now we are actually using Client To Server.

The inbox and outbox

By running

inbox = await actor.inbox()
outbox = await actor.outbox()

one can obtain CollectionHelper objects. These are meant to make it easier to interact with collection objects. In the simplest use case, one can use

await inbox.next_item()

to get the items from the inbox one after the other. It is also possible to print a summary of all elements that have been fetched from the inbox using await inbox.summary(). Finally, it is possible to iterate over the inbox via

async for item in inbox.iterate(max_number=3):
    do_something(item)

Proxying elements

We have already seen the difference between using post directly to an inbox and posting to the actor's outbox using send_to_outbox. A similar pattern applies to fetching objects. Both of these commands often have a similar result

await actor.get(object_id)
await actor.proxy_element(object_id)

However, they do different things:

  • The first actor.get sends a webrequest to the server object_id is on and retrieves it
  • The second actor.proxy_element sends a request to the actor's server for the object. This request is then either answered from the server's object store or by the server fetching the object. The cache behavior is up to the server. Depending of the evolution of proxyUrl of an Actor, more options might be added here.

As most servers don't support Moo-Auth-1, using proxy_element is the only way to obtain foreign objects, when using it.

Event Source

The event source is demonstrated in examples/sse.py. First, the event source will be specified in a FEP to come. It provides a way to receive updates from the server, whenever a new element is added to the inbox or outbox. The basic usage is

event_source = await actor.event_source()
async for event in event_source:
    if event and event.data:
        data = json.loads(event.data)
        do_something(data)

If you plan on writing long running applications, the event source does not automatically reconnect, so you will need to implement this. mechanical_bull uses the event source in this way.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bovine-0.0.11.tar.gz (59.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bovine-0.0.11-py3-none-any.whl (37.2 kB view details)

Uploaded Python 3

File details

Details for the file bovine-0.0.11.tar.gz.

File metadata

  • Download URL: bovine-0.0.11.tar.gz
  • Upload date:
  • Size: 59.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.4.1 CPython/3.10.10 Linux/5.10.0-21-amd64

File hashes

Hashes for bovine-0.0.11.tar.gz
Algorithm Hash digest
SHA256 32f79907cfc3f30a473316ba699afdbcee866f7ace7b2a8553e070ca19f75e21
MD5 8ed003e8ffc1710fbff25b0e2ea0dce2
BLAKE2b-256 51a5a3de7c2fca6e960e47b680632b882ff509e2eda09138be1b0a77fb271d6f

See more details on using hashes here.

File details

Details for the file bovine-0.0.11-py3-none-any.whl.

File metadata

  • Download URL: bovine-0.0.11-py3-none-any.whl
  • Upload date:
  • Size: 37.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.4.1 CPython/3.10.10 Linux/5.10.0-21-amd64

File hashes

Hashes for bovine-0.0.11-py3-none-any.whl
Algorithm Hash digest
SHA256 41d1f22a55629d6318174c86638c472e9771e9f93826f98f5a1335bfbf622bbc
MD5 ffb8056e39c17aad2436c072ee17f624
BLAKE2b-256 4dbada20ac31636885ed46a8ac83f8751b9768fa734f27b339909925988bd0b6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page