The Cattle Drive Protocol
- Account AsyncAPI
-
Python client library
Todo
The above API has incorrect payloads, see faststream#2040.
For a given account on cattle_grid
the cattle drive protocol is meant to be used by a single client to manage everything about the account, this includes:
- Creating new Fediverse actors
- Performing actions as these Fediverse actors, e.g. posting, following other actors, or looking up (remote) objects
- Managing the actors, e.g. updating their profile
- Deleting the Actors
The name cattle drive protocol is a work in progress and might still be changed.
Info
In info, I introduce a protocol parameter. As many Fediverse applications are single actor, and thus are not really able to implement creating and deleting actors in a meaningful way, they should set this parameter to CowDrive
. However, I believe these are still valid use cases of this protocol.
Todo
Clarify the relationship to ActivityPub. See Relationship to ActivityPub. One might call the protocol implemented
by cattle_grid as ActivityDrive
. It would correspond to FediWire + CattleDrive. The advantage of ActivityDrive
would be that it has a test suite.
Basics
An account has a name NAME
. Corresponding to each account are two channels, one for receiving messages and one for sending messages. These are called receive.NAME
and send.NAME
in the following diagram and the current implementation.
flowchart LR
CG[cattle_grid] <-->|magic| R
R[RabbitMQ] -->|receive.NAME.#| C[Client]
C -->|send.NAME.#| R
Message Types
- Events: Events in cattle_grid triggering a message on
receive.NAME
. Examples are: cattle_grid receiving a message from the Fediverse for one of the actors associated with the account. Similarly, for one of the actors associated with the account sending a message. - Triggers: Triggering an event from the client through sending a message on
send.NAME
. Examples are: An actor sending a message to its followers. We note that this also triggers a message onreceive.NAME
, but this is an indirect side effect of how processing works. - Requests: Messages on
send.NAME
requesting something for cattle_grid, e.g. create a new actor, list existing actors, or fetching an object. - Responses: Messages on
receive.NAME
containing the result of a request onsend.NAME
. The result and request can be matched via the correlation data.
Used topics
type | Topics | Comments |
---|---|---|
Events | receive.NAME.incoming , receive.NAME.outgoing |
The format can be extended by using transformer extensions |
Triggers | send.NAME.trigger.# |
Supported by default are send_message , update_actor , and delete_actor . Additional methods can be added through extensions |
Requests | send.NAME.request.create_actor , send.NAME.request.info , send.NAME.request.fetch |
Only the response to fetch can be extended, and it through transformers |
Responses | receive.NAME.result.create_actor , receive.NAME.result.info , receive.NAME.result.fetch |
Corresponds to a request. Uses correlation data to be matched. |
Errors | error.NAME |
Something has gone wrong |
Here NAME
is the name of the account. Furthermore, one should ensure that only authorized accounts have access to these topics.
Message flows
Let us revisit what the different channels look like. First for an incoming message from the Fediverse:
flowchart LR
R[RabbitMQ] -->|receive.alice.incoming| C[Client]
This should be most messages. Messages such as Blocks or messages failing validation will not be forwarded. Next, we have the trigger
flowchart LR
C -->|send.alice.trigger.send_message| R
R[RabbitMQ] -->|receive.alice.outgoing| C[Client]
This is due to triggers being the way to send messages. Most triggers will follow the above pattern. The same holds true for requests and responses:
flowchart LR
C -->|send.alice.request.info| R
R[RabbitMQ] -->|receive.alice.response.info| C[Client]
The difference between a request and a trigger is that requests are features of the server. In difference to this triggers create a cascade of effects that can be modified. For cattle_grid, one can create new triggers and add side effects to existing ones using extensions.
Lastly, when something fails one can expect an error message:
flowchart LR
C -->|send.alice.request.info| R
R[RabbitMQ] -->|error.alice| C[Client]
We note that error messages are not guaranteed to be send if something goes wrong for triggers.
Discussion
Scope of the protocol
There are two main issues that stop the Cattle Drive Protocol from being useful for general purpose clients:
- The protocol does not contain any information that would allow to route to a client. This means that all messages are send to each client, and no filtering occurs server side.
- The protocol has no history mechanism. That means it is only useful for clients that are always online.
We note that there is nothing stopping one to use this protocol as an intermediate between cattle_grid
and a server supporting multiple clients.
Similarly, this protocol is good for always online stuff such as building Fediverse bots.
Relationship to ActivityPub
Some people think the Fediverse implements ActivityPub. As what I propose doing requires some mental gymnastics to decide if it implements ActivityPub or not, I will try to explain the relationship between the two.
There are at least two things that are in ActivityPub
- A way to create decentralized servers hosting actors that exchange messages via HTTP post requests and look up information via HTTP get requests.
- A way to format these messages (mostly in the introduction).
I will call the first part FediWire and the second part FediMessage. With this I would say that we implement
flowchart LR
FW[FediWire] -->|Cattle Drive| FM[FediMessage]
This means that Cattle Drive allows one to communicate between something implementing FediWire and something implementing FediMessage.
Info
A formal specification of Cattle Drive would not involve ActivityPub. The protocol would specify the asynchronous API. Implementing Fediverse Applications that do ActivityPub would then be the main example.
FediWire as implemented by cattle_grid
FediWire could be specified as what cattle_grid implements. This means
- Handling the actor profile, its inbox, followers, and following
- Posting messages to the inboxes of other Fediverse actors with correctly resolving the recipients, i.e. resolving the followers collection
- Handling Follow and Block requests
- Handling a subset of Accept, Reject, Delete, Undo, and Update requests
- Sending appropriate Update, Delete, and Accept rejects (in certain cases)
The rest is handled either through extensions or FediMessage. If you are looking for something where the division FediWire vs FediMessage is in particular vague, consider Publish Object through the Simple Storage Extension of cattle_grid. With this the person implementing a client through Cattle Drive, does not need to know about Create activities. Without it, the client needs to know about create activities.
Similarly, the entire question about who assigns what ids to activities and objects is at best vague in the above division and at worst undefined.
MooQTT a concrete application
MooQTT is the codename for connecting to the RabbitMQ broker baking up cattle_grid through the web_mqtt plugin of RabbitMQ.
This means that for MooQTT just two topics are relevant: send.NAME
and receive.NAME
.
Message Formats
In these messages, we assume the actor is http://host.example/actor/123
and the account name is alice
. We use the format
to describe the message being send. We use AMQP notation for the routing keys. To use it with MQTT, you should replace the dot .
with a slash /
.
Events
There are two types of events incoming
and outgoing
.
These events corresponds to messages on ActivityExchange
being created with a routing key incoming.#
or outgoing.#
. These correspond to messages being received from respectively sent to the Fediverse.
These events have the form
{
"event_type": "incoming",
"actor": "http://host.example/actor/123",
"data": {
"raw": {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
...
}, ...
}
}
here data.raw
is the ActivityPub object. The other entries
of data are produced by the transformers specified through the
extension mechanism.
Triggers
Triggers have the following form
{
"actor": "http://host.example/actor/123",
"data": {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Follow",
...
}
}
method
indicates the method to call. These are implemented as routing keys on the ActivityExchange in cattle_grid, and by default
send_message
delivers the object to the Fediverseupdate_actor
delete_actor
should be implemented. Further methods can be implemented through extensions.
data
is the ActivityPub object, e.g. for send_message
the
activity to send.
Requests and Responses
There are three types of requests and responses:
info
returns information about the account and underlying servercreate_actor
creates a new actor associated with the accountfetch
allows one to retrieve an object from the Fediverse as an actor
Correlation of request and response is done via correlation id for AMQP and the correlation data for MQTT. We assume that these values are uuids.
Info
info
and create_actor
are only necessary if the underlying
server allows one to manage multiple actors. If one retrofits
an existing Fediverse server to use this protocol, one will
probably want to only allow one actor.
There will be some way to creating actors is impossible in
info for this case. One should still implement the info request
as it allows one to communicate the protocol version and the
available actor id.
info
Requesting the information is done with an empty message
The response has the form
{
"actors": ["http://host.example/actor/1", "http://host.example/actor/2"],
"baseUrls": ["http://host.example"],
"protocol": {
"name": "CattleDrive",
"version": "1alpha"
},
"backend": {
"name": "cattle_grid",
"version": "0.2.0alpha"
}
}
where more values might be added in the future. The current protocol version is 0.
Todo
Adjust implementation and think of additional information, e.g. the set of available triggers.
create_actor
A request has the following form
{
"baseUrl": "http://host.example",
"profile": {
"name": "A new actor",
"summary": "A new actor just created through this fancy protocol",
},
"automaticallyAcceptFollowers": true
}
where the exact allowed value in profile
may depend on the implications. Similarly, the allowed options for the actor.
The response currently is the actor object
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://host.example/actor/new",
"type": "Person",
"name": "A new actor",
"summary": "A new actor just created through this fancy protocol", ...
}
Todo
Is this really the response one wants?
fetch
This is done via
{
"actor": "http://host.example/actor/123",
"uri": "http://remote.example/object/4"
}
where the result will be transformed to
{
"actor": "http://host.example/actor/123",
"uri": "http://remote.example/object/4",
"data": {
"raw": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://remote.example/object/4", ...
}, ...
}
}
where the same can be said about applying the transformers as for the received events.
Error handling
If an error happens, it should create a message of the form
{
"error": "This preferredUsername is already taken",
"routing_key": "send.alice.request.create_actor"
...
}
where additional fields are allowed.