GoActivityPub library

Common pitfalls when using GoActivityPub

Changing the type of an object

This can be quite a disruptive operation as in GoActivityPub types are related to the shape the object has in memory and on disk. So if changing from an actor type to an object type some properties (like preferredUsername) will be lost. Similarly for activities or for specifically shaped objects (like Tombstone, or Place).

It’ always safe to change from a more restricted object type to a “larger” one (ie, from Note to Profile for example, because the later has the additional describes property).

Changing from an Object type to a Link type is even more fraught and should probably be done with a manual copy as the shapes of the two types are very different.

Using the wrong OnXXX function for the wrong types

To abstract over the fact that ActivityPub allows non-functional[1] properties to be simple objects or IRIs or even arrays composed of these, we created some convenience functions in the activitypub package to allow developers to avoid checking for all these options.

The most common of them is:

import ap "github.com/go-ap/activitypub"

// We load "example" from an external request, and it represents an object
// that implements interface ap.ObjectOrLink.
var example Item = new(ap.Tombstone)

ap.OnObject(example, func(ob *ap.Object) error {
    // do something that requires access to specific properties of an ap.Object
    ob.AttributedTo = ap.IRI("https://example.com/test")
    return nil
})

This functionality relies on unsafe behaviour when asserting the ap.ObjectOrLink interface to a pointer to ap.Object.

For example if the interface holds a pointer to one of the other types that implements it, and which can have a different memory layout than ap.Object, some information can be lost.

Generally this works as most of the package’s types are compatible with the Object one and each function’s documenation should specify where it should be used.

When using the other OnXXX functions the problem is more pervasive, especially where the difference in structure is not immediately apparent. An example, the Question object (which is an intransitive activity) doesn’t conform to the memory model of an Activity, and should not be used with OnActivity, but either with OnInstransitiveActivity or with OnQuestion.

import ap "github.com/go-ap/activitypub"

var example ap.Item = new(ap.Question)

err := ap.OnObject(example, func (ob *ap.Object) error {
    // works
    ob.ID = ap.IRI("https://example.com")
    ob.Type = ap.QuestionType
    return nil
})

err := ap.OnIntransitiveActivity(example, func (act *ap.IntransitiveActivity) error {
    // still works
    act.Actor = ap.IRI("https://example.com/1")
})

err := ap.OnActivity(example, func (act *ap.Activity) error {
    // should work, but it should be used from an OnInstransitiveActivity call
    // as above
    act.Actor = ap.IRI("https://example.com/2")

    // does not work, as the object property does not exist in the
    // Question type, which is an intransitive activity
    act.Object = ap.IRI("https://example.com/lorem-ipsum")
    return nil
})

err := ap.OnQuestion(example, func(q *ap.Question) error {
    // and this also works, accessing the Question specific properties
    q.AnyOf = ap.ItemCollection{}
    q.Closed = true
    return nil
})

Reassigning to the original pointer from inside OnXXX function

One very important caveat is that the pointer to the Item interface should not be reassigned from inside the functions.

If the type encapsulated by the interface is has a more restricted shape than the original type, the extra information contained will be lost in the following example.

import ap "github.com/go-ap/activitypub"

// In this example "it" will hold a pointer to an ap.Actor
var it ap.Item = ...// client.LoadActor("https://example.com/actor/1")

_ = ap.OnObject(it, func(ob *ap.Object) error {
    // reassigning the ob pointer to the it interface is valid syntactically
    // but we're losing the extra properties that Actor has compared to Object.

    it = ob // Wrong: possibility of losing information

    // Instead, it's enough to manipulate the object, and that will preserve
    // the information in the outside scope, so there is no need to reassign.
    ob.Name = "Jean Doe"
    return nil
})

// "it" is now of type pointer to Object and the Actor specific properties
// like "preferredUsername", "endpoints", "streams" or "publicKey" are no
// longer accessible.
//
// Unfortunatelly we don't currently allow passing the resulting pointer to
// ToActor and receive back a pointer to Actor, because in general there is no
// guarantee that the memory is shaped as an Actor struct. In this particular
// case it would be, but not always.

_, err := ap.ToActor(it)
if err != nil {
    panic(err) // panic: unable to convert *activitypub.Object to *activitypub.Actor
}

Here’s a directional table of how types can be converted:

OrderedCollectionPage OrderedCollection Object
CollectionPage Collection Object
Activity IntransitiveActivity Object
Question IntransitiveActivity Object
- Actor Object
- Place Object
- Tombstone Object
- Profile Object
- Relationship Object
- Mention Link

[1] https://www.w3.org/TR/activitystreams-vocabulary/#properties

NaturalLanguageValues are not ordered

The type we’re using for holding content values is a Go map, and as such the order of the elements is not guaranteed.

This makes it that marshaling the type as JSON can lead to different results.

This makes it that tests that have to assert equality on these maps are hard to write. Currently we’re using a very imperfect method of generating all possible JSON values and trying until one succeeds.

Ideas (and patches) welcome.