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 |