Documentation Wiki

Common pitfalls when using GoActivityPub

Calling the IsCollection, IsLink, IsObject on objects

In a desire to siplify the API of the Item interface we removed its methods IsCollection, IsLink, and IsObject in favour of using identically named functions in the module.

This has the downside that if the new functions get called on a struct that has an embedded Item instance, the functions will not return the expected value - matching the call to the method that was previously there.

These calls require the calling code explicitly dereferences the Item argument.

Equality between IRIs and Items

Be careful when checking equality between an Item instance wrapping an IRI and one that wraps one of the struct types.

This equality check is not yet commutative. See below for the example.

This effect can be quite insidious and can result in unexpected behaviour when using methods like Contains() or Append() of the ItemCollection and IRIs types, which make use of the equality in their implementations.

A potential solution for this case is to convert the ItemCollection to the corresponding IRIs slice and check if the Item or IRI is contained in that.

	var it = &vocab.Object{ID: "http://example.com"}
	var iri = vocab.IRI("http://example.com")

	// true because it requires an explicit IRI to check equality against 
	fmt.Printf("%s == %s = %t\n", iri, it, iri.Equal(it.GetID()))
	// false
	fmt.Printf("%s == %s = %t\n", it, iri, it.Equals(iri))

	var col1 = vocab.ItemCollection{it}
	var col2 = vocab.ItemCollection{iri}

	// true
	fmt.Printf("%v contains %s = %t\n", col1, it, col1.Contains(it))
	// false
	fmt.Printf("%v contains %s = %t\n", col1, iri, col1.Contains(iri))

	// false
	fmt.Printf("%v contains %s = %t\n", col2, it, col2.Contains(it))
	// true
	fmt.Printf("%v contains %s = %t\n", col2, iri, col2.Contains(iri))

	// true
	fmt.Printf("%v contains %s = %t\n", col1.IRIs(), it, col1.IRIs().Contains(it))
	// true
	fmt.Printf("%v contains %s = %t\n", col1.IRIs(), iri, col1.IRIs().Contains(iri))

	// true
	fmt.Printf("%v contains %s = %t\n", col2.IRIs(), it, col2.IRIs().Contains(it))
	// true
	fmt.Printf("%v contains %s = %t\n", col2.IRIs(), iri, col2.IRIs().Contains(iri))

Using OnItem() when operating on properties that are likely to be an ItemCollection

When we want to operate on an object that we received over the network and we are not aware of the exact shape of its properties, we can use the OnItem helper funciton to apply our logic on it even when it’s an ItemCollection not just a simple Item.


var ob := &pub.Object{
    AttributedTo: vocab.ItemCollection{vocab.IRI("http://example.com/jdoe"), vocab.IRI("http:/example.com/alice")},
}

_ = vocab.OnItem(ob.AttributedTo, func(it vocab.Item) error {
    fmt.Printf("AttributedTo: %s\n", it.GetLink())
    return nil
})

// Output: 
// http://example.com/jdoe
// http://example.com/alice

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.

Automatic collection creation for Actors and Objects

The processing module of GoActivityPub, has a convenience mechanism that automatically creates collections for Objects and Actors, but only if the respective properties are set to a correct IRI.

For example this Actor won’t have an Inbox collection, but will have an Outbox:

{
  "id": "https://fedbox.local/actors/1",
  "type": "Person",
  "url": "https://fedbox.local/actors/1",
  "outbox": "https://fedbox.local/actors/1/outbox",
  "preferredUsername": "no-federation",
  "summary": "This actor will also federate because the inbox collection exists"
}

This version will have both, because both Outbox and Inbox are set:

{
  "id": "https://fedbox.local/actors/2",
  "type": "Person",
  "url": "https://fedbox.local/actors/2",
  "outbox": "https://fedbox.local/actors/2/outbox",
  "inbox": "https://fedbox.local/actors/2/inbox",
  "preferredUsername": "federated",
  "summary": "This actor will also federate because the inbox collection exists"
}

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 the Activity Vocabulary allows non-functional 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 vocab "github.com/go-ap/activitypub"

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

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

This functionality relies on unsafe behaviour when asserting the vocab.ObjectOrLink interface to a pointer to vocab.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 vocab.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 vocab "github.com/go-ap/activitypub"

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

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

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

err := vocab.OnActivity(example, func (act *vocab.Activity) error {
    // should work, but it should be used from an OnInstransitiveActivity call
    // as above
    act.Actor = vocab.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 = vocab.IRI("https://example.com/lorem-ipsum")
    return nil
})

err := vocab.OnQuestion(example, func(q *vocab.Question) error {
    // and this also works, accessing the Question specific properties
    q.AnyOf = vocab.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 vocab "github.com/go-ap/activitypub"

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

_ = vocab.OnObject(it, func(ob *vocab.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 := vocab.ToActor(it)
if err != nil {
    panic(err) // panic: unable to convert *activityvocab.Object to *activityvocab.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