Easy to use parser of the Activity Streams 2.0 specification in Go, especially suitable for projects implementing ActivityPub.
package main
import (
"encoding/json"
"fmt"
"github.com/MatejLach/activitystreams"
)
func main() {
// Decode a Note object
noteJSON := `{
"type": "Note",
"content": "Hello, world!",
"published": "2023-01-01T00:00:00Z"
}`
var note activitystreams.Note
err := json.Unmarshal([]byte(noteJSON), ¬e)
if err != nil {
panic(err)
}
fmt.Printf("Note content: %s\n", note.Content)
// Encode it back to JSON
encodedNote, err := json.Marshal(¬e)
if err != nil {
panic(err)
}
fmt.Println(string(encodedNote))
}Object- The base ActivityStreams object typeLink- The base Link typeActor- Actor types (Person, Organization, etc.)Activity- Activity types (Create, Like, Follow, etc.)IntransitiveActivity- Intransitive activities (Arrive, Travel, etc.)
Collection- Standard collectionCollectionPage- Paginated collectionOrderedCollection- Ordered collectionOrderedCollectionPage- Paginated ordered collection
ObjectOrLink- Slice that can contain any ActivityStreams object or linkObjectOrLinkOrString- Can hold objects/links or string URLsIcons- Icon representation supporting both URL and Object formsLocation- Geographic location data
// Decode any ActivityStreams object using the generic decoder
func decodeObject(jsonData []byte) (activitystreams.ObjectLinker, error) {
var obj activitystreams.ObjectOrLink
err := json.Unmarshal(jsonData, &obj)
if err != nil {
return nil, err
}
if len(obj) > 0 {
return obj[0], nil
}
return nil, errors.New("empty object")
}
// Decode specific types
func decodeNote(jsonData []byte) (*activitystreams.Note, error) {
var note activitystreams.Note
err := json.Unmarshal(jsonData, ¬e)
return ¬e, err
}// Encode any ActivityStreams object
func encodeObject(obj activitystreams.ObjectLinker) ([]byte, error) {
return json.Marshal(obj)
}
// Using the generic encoder
func encodeGeneric[T activitystreams.ActivityStreamer](toEncode T) ([]byte, error) {
return activitystreams.EncodeJSON(toEncode)
}Slices can contain any mix of Object or Link (sub)types:
// ObjectOrLink handles heterogeneous arrays
var items activitystreams.ObjectOrLink
items = append(items, activitystreams.Note{Content: "Hello"})
items = append(items, activitystreams.Person{Name: "Alice"})
jsonBytes, err := json.Marshal(items)
// Results in JSON array with mixed types// Create and populate a heterogeneous collection
var items activitystreams.ObjectOrLink
// Add different types of objects
note := activitystreams.Note{Content: "Hello"}
person := activitystreams.Person{Name: "Alice"}
items = append(items, note)
items = append(items, person)
// Encode the collection
jsonBytes, err := json.Marshal(items)// Create a Collection
collection := activitystreams.Collection{
TotalItems: 100,
Items: &activitystreams.ObjectOrLinkOrString{
Target: activitystreams.ObjectOrLink{
activitystreams.Note{Content: "First note"},
activitystreams.Note{Content: "Second note"},
},
},
}// Decode any valid ActivityStreams object
note, err := activitystreams.DecodeJSON[activitystreams.Note](strings.NewReader(noteJSON))
if err != nil {
// handle error
}
// Works with any AS type
person, err := activitystreams.DecodeJSON[activitystreams.Person](strings.NewReader(personJSON))
activity, err := activitystreams.DecodeJSON[activitystreams.Create](strings.NewReader(activityJSON))// Generic encoder for any valid ActivityStreams object
note := activitystreams.Note{Content: "Hello"}
bytes, err := activitystreams.EncodeJSON(note)
if err != nil {
// handle error
}person := activitystreams.Person{
ID: "https://example.com/users/alice",
Type: "Person",
Name: "Alice Smith",
PreferredUsername: "alice",
Summary: "A person on the web",
Inbox: &activitystreams.StringWithOrderedCollection{
URL: "https://example.com/users/alice/inbox",
},
Outbox: &activitystreams.StringWithOrderedCollection{
URL: "https://example.com/users/alice/outbox",
},
}// Create a followers collection
followers := activitystreams.Collection{
TotalItems: 42,
First: &activitystreams.StringWithCollectionPage{
CollectionPage: activitystreams.CollectionPage{
// pagination details
},
URL: "https://example.com/users/alice/followers?page=1",
},
}createActivity := activitystreams.Create{
Type: "Create",
Actor: &activitystreams.ObjectOrLinkOrString{
Target: activitystreams.ObjectOrLink{
activitystreams.Person{
ID: "https://example.com/users/alice",
Name: "Alice Smith",
},
},
},
ActivityObject: &activitystreams.ObjectOrLinkOrString{
Target: activitystreams.ObjectOrLink{
activitystreams.Note{
Content: "Hello, world!",
Published: &time.Now(),
},
},
},
}func processObject(obj activitystreams.ObjectLinker) {
reflectType, asType := activitystreams.ConcreteType(obj)
fmt.Printf("Reflection type: %s, AS type: %s\n", reflectType, asType)
switch asType {
case "Note":
if note, ok := obj.(activitystreams.Note); ok {
// Handle Note
}
case "Person":
if person, ok := obj.(activitystreams.Person); ok {
// Handle Person
}
}
}// Safe type assertions
func asNote(obj activitystreams.ObjectLinker) (*activitystreams.Note, bool) {
if n, ok := obj.(activitystreams.Note); ok {
return &n, true
}
return nil, false
}
func asPerson(obj activitystreams.ObjectLinker) (*activitystreams.Person, bool) {
if p, ok := obj.(activitystreams.Person); ok {
return &p, true
}
return nil, false
}func decodePayload(jsonData []byte) (activitystreams.JsonPayload, error) {
reader := bytes.NewReader(jsonData)
payloadType, err := activitystreams.DecodePayloadObjectType(reader)
if err != nil {
return payloadType, err
}
switch payloadType.Type {
case "Note":
// Handle Note
case "Person":
// Handle Person
default:
// Handle generic object or unknown type
}
return payloadType, nil
}ObjectArticle,Document,Audio,Video,ImageEvent,Note,PageCollection,CollectionPageOrderedCollection,OrderedCollectionPageLocation,Profile,TombstoneRelationship,Question
LinkMention,Hashtag,PropertyValue
Application,Group,Organization,Person,Service
Accept,Add,Announce,Arrive,Block,Create,DeleteDislike,Follow,Flag,Ignore,Invite,Join,LeaveLike,Listen,Move,Offer,Read,Reject,RemoveTentativeAccept,TentativeReject,Travel,Undo,Update,View
Contributions are welcome. If you've found a bug, or have a feature request, don't hesitate to open an issue.