Skip to content

Roboherd

Roboherd is a tool to build automated Fediverse actors. It does so by connecting to a server through the Cattle Drive protocol protocol. Once you have configured the connection, all other tasks can be done using python code and a toml configuration file.

Tour of the functionality

We will now tour how to write bots with Roboherd. We start with a basic bot that does nothing except have a profile. Then we will start by adding features.

First, you will need to install roboherd, see Installation. For our purposes, you can start by running the docker container via

docker run --rm -ti -v .:/app helgekr/roboherd

Then you can run roboherd with local files by running

python -mroboherd run

Basic bot aka how to configure the profile

The configuration file defines the actor by a name and the used python file, e.g.

roboherd.toml
base_url = "https://dev.bovine.social"

[cow.devnull]
bot = "roboherd.examples.dev_null:bot"

Where the module is defined as

roboherd/examples/dev_null.py
from roboherd import RoboCow

from .meta import meta_information

bot = RoboCow.create(
    name="/dev/null",
    description="""I don't do anything.""",
    handle="devnull",
    meta_information=meta_information,
)

One should note that the above definition describes the properties of the actor. In mastodon this looks like

Mastodon Screenshot

Screen shot of @devnull@dev.bovine.social on mastodon.social

Actor Profile as json
{
    "attachment": [
        {
        "type": "PropertyValue",
        "name": "Author",
        "value": "acct:helge@mymath.rocks"
        },
        {
        "type": "PropertyValue",
        "name": "Source",
        "value": "https://codeberg.org/bovine/roboherd"
        }
    ],
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v1",
        {
        "PropertyValue": {
            "@id": "https://schema.org/PropertyValue",
            "@context": {
            "value": "https://schema.org/value",
            "name": "https://schema.org/name"
            }
        }
        }
    ],
    "publicKey": {
        "id": "https://dev.bovine.social/actor/5Dhr1Bk_E_WuNik_ii0BBQ#legacy-key-1",
        "owner": "https://dev.bovine.social/actor/5Dhr1Bk_E_WuNik_ii0BBQ",
        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvUWtZLOftKTRpNJ+t6Bq\nma6hyRKwcbl6LVuZl+dJ+lAfKpEPRx/2uo/1LkA4mUVEVewyhWMQ8SyIoRayuDqP\njDUaai4mohSg+ReNvmWXD4mgGB7XPc00A4Yk2R/u2hkmu8hWLU7260BTQwmpjsyK\nDG7QWvZYbob1Hyr1QabOpn4tUS3pOl6KqGw8ijNUtAYw22kZl/aZ0v/mk7+V2xhT\nsZbRAC/7o39sFvgGdRHsDwXbJX63aU+qpBc7UX42fdOH9pfmRl5Ps7xCfB1iSlQP\nR4BTGbJB9DOLYlvb2iTqh5cn37VYBxNG3haAv34Q3fBPONOe77CeV1abKTr8/jCG\nsQIDAQAB\n-----END PUBLIC KEY-----\n"
    },
    "id": "https://dev.bovine.social/actor/5Dhr1Bk_E_WuNik_ii0BBQ",
    "type": "Service",
    "inbox": "https://dev.bovine.social/inbox/nAy02r0LlQJ0WO5ry2-LFA",
    "outbox": "https://dev.bovine.social/outbox/7JZSBh-1ZXgTXzkSDjGr-A",
    "followers": "https://dev.bovine.social/followers/90-KDZpuvSif5KAL_TzoYQ",
    "following": "https://dev.bovine.social/following/jrp61fFlLDA5HcYDN98JRA",
    "preferredUsername": "devnull",
    "name": "/dev/null",
    "summary": "I don't do anything.",
    "identifiers": [
        "acct:devnull@dev.bovine.social",
        "https://dev.bovine.social/actor/5Dhr1Bk_E_WuNik_ii0BBQ"
    ],
    "endpoints": {
        "sharedInbox": "https://dev.bovine.social/shared_inbox"
    }
}

Setting me as the author and the source code is contained in meta_information.

roboherd/examples/meta.py
from roboherd.cow.types import MetaInformation

meta_information = MetaInformation(
    author="acct:helge@mymath.rocks", source="https://codeberg.org/bovine/roboherd"
)

By setting to icon property of roboherd.cow.types.Information, one can change the profile picture.

Config overrides

By running

roboherd.toml
base_url = "https://dev.bovine.social"

[cow.devnull]
bot = "roboherd.examples.dev_null:bot"
handle = "nothingness"

one would instead create a bot with the handle acct:nothingness@dev.bovine.social. The full list of options can be found in roboherd.herd.manager.config.ConfigOverrides.

Posting on startup

The simplest way to post is to use

@bot.startup
async def startup(poster: MarkdownPoster):
    await poster("__Booo!__ 🐦")

here MarkdownPoster is an annotation. Annotations are injected through FastDepends. The markdown poster abstracts away the following steps

  • Convert markdown to HTML
  • Create a note based with the HTML as content (done below with the object factory)
  • Send the note to the server (done below with publish object)

Manually posting

By adding

@bot.startup
async def startup(publish_object: PublishObject, object_factory: ObjectFactory):
    note = object_factory.note(content="Booo! 🐦").as_public().build()
    await publish_object(note)

one can post on startup. Here PublishObject and ObjectFactory are annotations.

PublishObject depends on the simple storage extension of cattle_grid, whereas ObjectFactory injects an ObjectFactory provided by the bovine library.

Full source
roboherd/examples/scarecrow.py
from roboherd import RoboCow, MarkdownPoster

# from roboherd import PublishObject, ObjectFactory

from .meta import meta_information

bot = RoboCow.create(
    name="The scare crow 👩‍🌾",
    description="""On startup I scare crows""",
    handle="scarecrow",
    meta_information=meta_information,
)


# @bot.startup
# async def startup(publish_object: PublishObject, object_factory: ObjectFactory):
#     note = object_factory.note(content="Booo! 🐦").as_public().build()  # type: ignore
#     await publish_object(note)


@bot.startup
async def startup(poster: MarkdownPoster):
    await poster("__Booo!__ 🐦")

Hint

Similarly to PublishObject, one can use PublishActivity to post activities.

Replying to messages

By adding a block such as

@bot.incoming_create
async def create(
    raw: RawData, publish_object: PublishObject, object_factory: ObjectFactory
):
    note = (
        object_factory.reply(
            raw.get("object"),
            content=reply_content(raw),
        )
        .as_public()
        .build()
    )

    await publish_object(note)

one can reply to messages. Here RawData is an annotation that will ensure that the received activity is injected through FastDepends. The same is true for PublishObject and ObjectFactory. The last one depends on bovine.

@bot.incoming_create indicates that the coroutine create should be called when a Create activity is received. See roboherd.cow.RoboCow.incoming_create.

Full source
roboherd/examples/json_echo.py
import json

from roboherd import RoboCow, RawData, PublishObject, ObjectFactory

from .meta import meta_information

bot = RoboCow.create(
    handle="jsonecho",
    name="JSON Echo {}",
    description="""<p>I'm a silly bot that replies to
you with the JSON as received through a HTTP
post request by 
<a href="https://codeberg.org/helge/cattle_grid">cattle_grid</a>.</p>

    """,
    meta_information=meta_information,
)


def reply_content(raw: dict) -> str:
    """Formats and escapes the JSON data:

    ```pycon
    >>> reply_content({"html": "<b>bold</b>"})
    '<pre><code>{\\n  "html": "&lt;b&gt;bold&lt;/b&gt;"\\n}</code></re>'

    ```
    """
    json_formatted = (
        json.dumps(raw, indent=2)
        .replace("&", "&amp;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
    )
    return f"<pre><code>{json_formatted}</code></re>"


@bot.incoming_create
async def create(
    raw: RawData, publish_object: PublishObject, object_factory: ObjectFactory
):
    note = (
        object_factory.reply(  # type: ignore
            raw.get("object"),
            content=reply_content(raw),
        )
        .as_public()
        .build()
    )

    await publish_object(note)

Scheduling a message

By including

@bot.cron("42 * * * *")
async def crow(publisher: PublishObject, object_factory: ObjectFactory):
    await publisher(
        object_factory.note(content="cock-a-doodle-doo").as_public().build()
    )

one can schedule a message to be published at the 42nd minute of every hour. Here "42 * * * *" is a cron expression.

Full source
roboherd/examples/rooster.py
from roboherd import RoboCow, PublishObject, ObjectFactory

from .meta import meta_information

bot = RoboCow.create(
    handle="rooster",
    name="The crowing rooster 🐓",
    meta_information=meta_information,
)


@bot.cron("42 * * * *")
async def crow(publisher: PublishObject, object_factory: ObjectFactory):
    await publisher(
        object_factory.note(content="cock-a-doodle-doo").as_public().build()  # type: ignore
    )

Tip

Alternatively one can write bots that post on startup and schedule them using cron.

Installation

Using the docker container

By running

docker run --rm -ti -v .:/app helgekr/roboherd

you can run a docker container mounting the current directory. If the current directory contains a roboherd.toml file and the appropriate bots, one can run these bots by running

python -mroboherd run

inside the container. Alternatively, this can be done directly by running

docker run --rm -v .:/app helgekr/roboherd python -mroboherd run

Installing roboherd as a python package

To install roboherd with bovine support run

pip install roboherd[bovine]

You are then able to run the roboherd command

python -mroboherd run

The roboherd command

You can also run roboherd by running

roboherd run

The difference to python -mroboherd run is that with the former, the current directory is not added to the python path. It will still be used to find the roboherd.toml configuration file.

Configuration

roboherd has three global configuration variables, e.g.

roboherd.toml
connection_string = "wss://NAME:PASSWORD@host.example/ws"
base_url = "https://host.example"
echo = false

connection_string points to the server providing a Cattle Drive endpoint. This connection string currently contains the authentication data.

base_url specifies the domain being used to create the bots. With the above configuration file, it would lead to bots with Fediverse handle acct:BOT-NAME@host.example. This might be relaxed in the future with roboherd#12.

Setting echo to true will cause roboherd to print all messages performed through Cattle Drive to the console.

Overriding the configuration through environment variables

Roboherd uses environment variables starting with ROBOHERD_ as overrides for the configuration. This is done through DynaConf. So for example

ROBOHERD_CONNECTION_STRING=wss://NAME:PASSWORD@other.example/ws\
    roboherd run

will connect to other.example instead of host.example.

Using dev.bovine.social

In order to encourage experimentation, I host an instance at dev.bovine.social, where you can quickly sign up and deploy some bots.

Getting start is simple, run

docker run --rm -v .:/app helgekr/roboherd\
    roboherd register --name NAME --fediverse acct:your@handle.example

you will then be prompted for a password. Afterwards a roboherd.toml file with the appropriate configuration will be created. Now, you can write a bot, add it to roboherd.toml and run it via

docker run --rm -v .:/app helgekr/roboherd roboherd run

Please be respectful to the rest of the Fediverse when using this service.

console.bovine.social

After signing up, you can open the cattle_grid console and use it to explore your bots. First one has to sign in at the top right corner. Afterwards, one should be able to see the incoming and outgoing messages by selecting the appropriate points in the menu on the left.

Warning

This still seems somewhat buggy, see cattle_grid#83

The console also allows you to lookup Fediverse objects as seen through cattle_grid and perform actions manually.

Developing roboherd

Roboherd uses astral-uv and hatch for development. After cloning the repository, run

uv sync --all-extras

to install dependencies using uv. Then one can run tests via

uv run pytest
uv run ruff check .
uv run ruff format .

Releasing

Releases, packages, documentation, and docker containers are build automatically using the CI. For this increase the version, have a milestone matching the new version, and appropriate entries in CHANGES.md. Then once a pull request with this is merged, the rest happens automatically.

Acknowledgements

Logo via Lorc.