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
Then you can run roboherd with local files by running
Basic bot aka how to configure the profile
The configuration file defines the actor by a name and the used python file, e.g.
base_url = "https://dev.bovine.social"
[cow.devnull]
bot = "roboherd.examples.dev_null:bot"
Where the module is defined as
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
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.
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
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
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
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
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": "<b>bold</b>"\\n}</code></re>'
```
"""
json_formatted = (
json.dumps(raw, indent=2)
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
)
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
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
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
inside the container. Alternatively, this can be done directly by running
Installing roboherd as a python package
To install roboherd with bovine support run
You are then able to run the roboherd command
The roboherd command
You can also run roboherd by running
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.
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
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
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
to install dependencies using uv. Then one can run tests via
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.