package activitypub import ( "encoding" "encoding/json" "fmt" "net/url" "reflect" "strings" "time" "github.com/valyala/fastjson" ) var ( apUnmarshalerType = reflect.TypeOf(new(Item)).Elem() unmarshalerType = reflect.TypeOf(new(json.Unmarshaler)).Elem() textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem() ) // ItemTyperFunc will return an instance of a struct that implements activitystreams.Item // The default for this package is GetItemByType but can be overwritten var ItemTyperFunc TyperFn = GetItemByType // JSONItemUnmarshal can be set externally to populate a custom object based on its type var JSONItemUnmarshal JSONUnmarshalerFn = nil // NotEmptyChecker checks if an object is empty var NotEmptyChecker NotEmptyCheckerFn = NotEmpty // TyperFn is the type of the function which returns an Item struct instance // for a specific ActivityVocabularyType type TyperFn func(ActivityVocabularyType) (Item, error) // JSONUnmarshalerFn is the type of the function that will load the data from a fastjson.Value into an Item // that the current package doesn't know about. type JSONUnmarshalerFn func(ActivityVocabularyType, *fastjson.Value, Item) error // NotEmptyCheckerFn is the type of a function that checks if an object is empty type NotEmptyCheckerFn func(Item) bool func JSONGetID(val *fastjson.Value) ID { i := val.Get("id").GetStringBytes() return ID(i) } func JSONGetType(val *fastjson.Value) ActivityVocabularyType { t := val.Get("type").GetStringBytes() return ActivityVocabularyType(t) } func JSONGetMimeType(val *fastjson.Value, prop string) MimeType { if !val.Exists(prop) { return "" } t := val.GetStringBytes(prop) return MimeType(t) } func JSONGetInt(val *fastjson.Value, prop string) int64 { if !val.Exists(prop) { return 0 } i := val.Get(prop).GetInt64() return i } func JSONGetFloat(val *fastjson.Value, prop string) float64 { if !val.Exists(prop) { return 0.0 } f := val.Get(prop).GetFloat64() return f } func JSONGetString(val *fastjson.Value, prop string) string { if !val.Exists(prop) { return "" } s := val.Get(prop).GetStringBytes() return string(s) } func JSONGetBytes(val *fastjson.Value, prop string) []byte { if !val.Exists(prop) { return nil } s := val.Get(prop).GetStringBytes() return s } func JSONGetBoolean(val *fastjson.Value, prop string) bool { if !val.Exists(prop) { return false } t, _ := val.Get(prop).Bool() return t } func JSONGetNaturalLanguageField(val *fastjson.Value, prop string) NaturalLanguageValues { n := NaturalLanguageValues{} if val == nil { return n } v := val.Get(prop) if v == nil { return nil } switch v.Type() { case fastjson.TypeObject: ob, _ := v.Object() ob.Visit(func(key []byte, v *fastjson.Value) { l := LangRefValue{} l.Ref = LangRef(key) if err := l.Value.UnmarshalJSON(v.GetStringBytes()); err == nil { if l.Ref != NilLangRef || len(l.Value) > 0 { n = append(n, l) } } }) case fastjson.TypeString: l := LangRefValue{} if err := l.UnmarshalJSON(v.GetStringBytes()); err == nil { n = append(n, l) } } return n } func JSONGetTime(val *fastjson.Value, prop string) time.Time { t := time.Time{} if val == nil { return t } if str := val.Get(prop).GetStringBytes(); len(str) > 0 { t.UnmarshalText(str) return t.UTC() } return t } func JSONGetDuration(val *fastjson.Value, prop string) time.Duration { if str := val.Get(prop).GetStringBytes(); len(str) > 0 { // TODO(marius): this needs to be replaced to be compatible with xsd:duration d, _ := time.ParseDuration(string(str)) return d } return 0 } func JSONGetPublicKey(val *fastjson.Value, prop string) PublicKey { key := PublicKey{} if val == nil { return key } val = val.Get(prop) if val == nil { return key } JSONLoadPublicKey(val, &key) return key } func JSONItemsFn(val *fastjson.Value) (Item, error) { if val.Type() == fastjson.TypeArray { it := val.GetArray() if len(it) == 1 { return JSONLoadItem(it[0]) } items := make(ItemCollection, 0) for _, v := range it { if it, _ := JSONLoadItem(v); it != nil { items.Append(it) } } return items, nil } return JSONLoadItem(val) } func JSONLoadItem(val *fastjson.Value) (Item, error) { typ := JSONGetType(val) if typ == "" && val.Type() == fastjson.TypeString { // try to see if it's an IRI if i, ok := asIRI(val); ok { return i, nil } } i, err := ItemTyperFunc(typ) if err != nil || IsNil(i) { return nil, nil } var empty = func(i Item) bool { return !NotEmptyChecker(i) } switch typ { case "": // NOTE(marius): this handles Tags which usually don't have types fallthrough case ObjectType, ArticleType, AudioType, DocumentType, EventType, ImageType, NoteType, PageType, VideoType: err = OnObject(i, func(ob *Object) error { return JSONLoadObject(val, ob) }) case LinkType, MentionType: err = OnLink(i, func(l *Link) error { return JSONLoadLink(val, l) }) case ActivityType, AcceptType, AddType, AnnounceType, BlockType, CreateType, DeleteType, DislikeType, FlagType, FollowType, IgnoreType, InviteType, JoinType, LeaveType, LikeType, ListenType, MoveType, OfferType, RejectType, ReadType, RemoveType, TentativeRejectType, TentativeAcceptType, UndoType, UpdateType, ViewType: err = OnActivity(i, func(act *Activity) error { return JSONLoadActivity(val, act) }) case IntransitiveActivityType, ArriveType, TravelType: err = OnIntransitiveActivity(i, func(act *IntransitiveActivity) error { return JSONLoadIntransitiveActivity(val, act) }) case ActorType, ApplicationType, GroupType, OrganizationType, PersonType, ServiceType: err = OnActor(i, func(a *Actor) error { return JSONLoadActor(val, a) }) case CollectionType: err = OnCollection(i, func(c *Collection) error { return JSONLoadCollection(val, c) }) case OrderedCollectionType: err = OnOrderedCollection(i, func(c *OrderedCollection) error { return JSONLoadOrderedCollection(val, c) }) case CollectionPageType: err = OnCollectionPage(i, func(p *CollectionPage) error { return JSONLoadCollectionPage(val, p) }) case OrderedCollectionPageType: err = OnOrderedCollectionPage(i, func(p *OrderedCollectionPage) error { return JSONLoadOrderedCollectionPage(val, p) }) case PlaceType: err = OnPlace(i, func(p *Place) error { return JSONLoadPlace(val, p) }) case ProfileType: err = OnProfile(i, func(p *Profile) error { return JSONLoadProfile(val, p) }) case RelationshipType: err = OnRelationship(i, func(r *Relationship) error { return JSONLoadRelationship(val, r) }) case TombstoneType: err = OnTombstone(i, func(t *Tombstone) error { return JSONLoadTombstone(val, t) }) case QuestionType: err = OnQuestion(i, func(q *Question) error { return JSONLoadQuestion(val, q) }) default: if JSONItemUnmarshal == nil { return nil, fmt.Errorf("unable to unmarshal custom type %s, you need to set a correct function for JSONItemUnmarshal", typ) } err = JSONItemUnmarshal(typ, val, i) } if err != nil { return nil, err } if empty(i) { return nil, nil } return i, nil } func JSONUnmarshalToItem(val *fastjson.Value) Item { var ( i Item err error ) switch val.Type() { case fastjson.TypeArray: i, err = JSONItemsFn(val) case fastjson.TypeObject: i, err = JSONLoadItem(val) case fastjson.TypeString: if iri, ok := asIRI(val); ok { // try to see if it's an IRI i = iri } } if err != nil { return nil } return i } func asIRI(val *fastjson.Value) (IRI, bool) { if val == nil { return NilIRI, true } s := strings.Trim(val.String(), `"`) u, err := url.ParseRequestURI(s) if err == nil && len(u.Scheme) > 0 && len(u.Host) > 0 { // try to see if it's an IRI return IRI(s), true } return EmptyIRI, false } func JSONGetItem(val *fastjson.Value, prop string) Item { if val == nil { return nil } if val = val.Get(prop); val == nil { return nil } switch val.Type() { case fastjson.TypeString: if i, ok := asIRI(val); ok { // try to see if it's an IRI return i } case fastjson.TypeArray: it, _ := JSONItemsFn(val) return it case fastjson.TypeObject: it, _ := JSONLoadItem(val) return it case fastjson.TypeNumber: fallthrough case fastjson.TypeNull: fallthrough default: return nil } return nil } func JSONGetURIItem(val *fastjson.Value, prop string) Item { if val == nil { return nil } if val = val.Get(prop); val == nil { return nil } switch val.Type() { case fastjson.TypeObject: if it, _ := JSONLoadItem(val); it != nil { return it } case fastjson.TypeArray: if it, _ := JSONItemsFn(val); it != nil { return it } case fastjson.TypeString: return IRI(val.GetStringBytes()) } return nil } func JSONGetItems(val *fastjson.Value, prop string) ItemCollection { if val == nil { return nil } if val = val.Get(prop); val == nil { return nil } it := make(ItemCollection, 0) switch val.Type() { case fastjson.TypeArray: for _, v := range val.GetArray() { if i, _ := JSONLoadItem(v); i != nil { it.Append(i) } } case fastjson.TypeObject: if i := JSONGetItem(val, prop); i != nil { it.Append(i) } case fastjson.TypeString: if iri := val.GetStringBytes(); len(iri) > 0 { it.Append(IRI(iri)) } } if len(it) == 0 { return nil } return it } func JSONGetLangRefField(val *fastjson.Value, prop string) LangRef { s := val.Get(prop).GetStringBytes() return LangRef(s) } func JSONGetIRI(val *fastjson.Value, prop string) IRI { s := val.Get(prop).GetStringBytes() return IRI(s) } // UnmarshalJSON tries to detect the type of the object in the json data and then outputs a matching // ActivityStreams object, if possible func UnmarshalJSON(data []byte) (Item, error) { p := fastjson.Parser{} val, err := p.ParseBytes(data) if err != nil { return nil, err } return JSONUnmarshalToItem(val), nil } func GetItemByType(typ ActivityVocabularyType) (Item, error) { switch typ { case ObjectType, ArticleType, AudioType, DocumentType, EventType, ImageType, NoteType, PageType, VideoType: return ObjectNew(typ), nil case LinkType, MentionType: return &Link{Type: typ}, nil case ActivityType, AcceptType, AddType, AnnounceType, BlockType, CreateType, DeleteType, DislikeType, FlagType, FollowType, IgnoreType, InviteType, JoinType, LeaveType, LikeType, ListenType, MoveType, OfferType, RejectType, ReadType, RemoveType, TentativeRejectType, TentativeAcceptType, UndoType, UpdateType, ViewType: return &Activity{Type: typ}, nil case IntransitiveActivityType, ArriveType, TravelType: return &IntransitiveActivity{Type: typ}, nil case ActorType, ApplicationType, GroupType, OrganizationType, PersonType, ServiceType: return &Actor{Type: typ}, nil case CollectionType: return &Collection{Type: typ}, nil case OrderedCollectionType: return &OrderedCollection{Type: typ}, nil case CollectionPageType: return &CollectionPage{Type: typ}, nil case OrderedCollectionPageType: return &OrderedCollectionPage{Type: typ}, nil case PlaceType: return &Place{Type: typ}, nil case ProfileType: return &Profile{Type: typ}, nil case RelationshipType: return &Relationship{Type: typ}, nil case TombstoneType: return &Tombstone{Type: typ}, nil case QuestionType: return &Question{Type: typ}, nil case "": // when no type is available use a plain Object return &Object{}, nil } return nil, fmt.Errorf("empty ActivityStreams type") } func JSONGetActorEndpoints(val *fastjson.Value, prop string) *Endpoints { str := val.Get(prop).GetStringBytes() var e *Endpoints if len(str) > 0 { e = &Endpoints{} e.UnmarshalJSON(str) } return e } func JSONLoadObject(val *fastjson.Value, o *Object) error { o.ID = JSONGetID(val) o.Type = JSONGetType(val) o.Name = JSONGetNaturalLanguageField(val, "name") o.Content = JSONGetNaturalLanguageField(val, "content") o.Summary = JSONGetNaturalLanguageField(val, "summary") o.Context = JSONGetItem(val, "context") o.URL = JSONGetURIItem(val, "url") o.MediaType = JSONGetMimeType(val, "mediaType") o.Generator = JSONGetItem(val, "generator") o.AttributedTo = JSONGetItem(val, "attributedTo") o.Attachment = JSONGetItem(val, "attachment") o.Location = JSONGetItem(val, "location") o.Published = JSONGetTime(val, "published") o.StartTime = JSONGetTime(val, "startTime") o.EndTime = JSONGetTime(val, "endTime") o.Duration = JSONGetDuration(val, "duration") o.Icon = JSONGetItem(val, "icon") o.Preview = JSONGetItem(val, "preview") o.Image = JSONGetItem(val, "image") o.Updated = JSONGetTime(val, "updated") o.InReplyTo = JSONGetItem(val, "inReplyTo") o.To = JSONGetItems(val, "to") o.Audience = JSONGetItems(val, "audience") o.Bto = JSONGetItems(val, "bto") o.CC = JSONGetItems(val, "cc") o.BCC = JSONGetItems(val, "bcc") o.Replies = JSONGetItem(val, "replies") o.Tag = JSONGetItems(val, "tag") o.Likes = JSONGetItem(val, "likes") o.Shares = JSONGetItem(val, "shares") o.Source = GetAPSource(val) return nil } func JSONLoadIntransitiveActivity(val *fastjson.Value, i *IntransitiveActivity) error { OnObject(i, func(o *Object) error { return JSONLoadObject(val, o) }) i.Actor = JSONGetItem(val, "actor") i.Target = JSONGetItem(val, "target") i.Result = JSONGetItem(val, "result") i.Origin = JSONGetItem(val, "origin") i.Instrument = JSONGetItem(val, "instrument") return nil } func JSONLoadActivity(val *fastjson.Value, a *Activity) error { OnIntransitiveActivity(a, func(i *IntransitiveActivity) error { return JSONLoadIntransitiveActivity(val, i) }) a.Object = JSONGetItem(val, "object") return nil } func JSONLoadQuestion(val *fastjson.Value, q *Question) error { OnIntransitiveActivity(q, func(i *IntransitiveActivity) error { return JSONLoadIntransitiveActivity(val, i) }) q.OneOf = JSONGetItem(val, "oneOf") q.AnyOf = JSONGetItem(val, "anyOf") q.Closed = JSONGetBoolean(val, "closed") return nil } func JSONLoadActor(val *fastjson.Value, a *Actor) error { OnObject(a, func(o *Object) error { return JSONLoadObject(val, o) }) a.PreferredUsername = JSONGetNaturalLanguageField(val, "preferredUsername") a.Followers = JSONGetItem(val, "followers") a.Following = JSONGetItem(val, "following") a.Inbox = JSONGetItem(val, "inbox") a.Outbox = JSONGetItem(val, "outbox") a.Liked = JSONGetItem(val, "liked") a.Endpoints = JSONGetActorEndpoints(val, "endpoints") a.Streams = JSONGetItems(val, "streams") a.PublicKey = JSONGetPublicKey(val, "publicKey") return nil } func JSONLoadCollection(val *fastjson.Value, c *Collection) error { OnObject(c, func(o *Object) error { return JSONLoadObject(val, o) }) c.Current = JSONGetItem(val, "current") c.First = JSONGetItem(val, "first") c.Last = JSONGetItem(val, "last") c.TotalItems = uint(JSONGetInt(val, "totalItems")) c.Items = JSONGetItems(val, "items") return nil } func JSONLoadCollectionPage(val *fastjson.Value, c *CollectionPage) error { OnCollection(c, func(c *Collection) error { return JSONLoadCollection(val, c) }) c.Next = JSONGetItem(val, "next") c.Prev = JSONGetItem(val, "prev") c.PartOf = JSONGetItem(val, "partOf") return nil } func JSONLoadOrderedCollection(val *fastjson.Value, c *OrderedCollection) error { OnObject(c, func(o *Object) error { return JSONLoadObject(val, o) }) c.Current = JSONGetItem(val, "current") c.First = JSONGetItem(val, "first") c.Last = JSONGetItem(val, "last") c.TotalItems = uint(JSONGetInt(val, "totalItems")) c.OrderedItems = JSONGetItems(val, "orderedItems") return nil } func JSONLoadOrderedCollectionPage(val *fastjson.Value, c *OrderedCollectionPage) error { OnOrderedCollection(c, func(c *OrderedCollection) error { return JSONLoadOrderedCollection(val, c) }) c.Next = JSONGetItem(val, "next") c.Prev = JSONGetItem(val, "prev") c.PartOf = JSONGetItem(val, "partOf") c.StartIndex = uint(JSONGetInt(val, "startIndex")) return nil } func JSONLoadPlace(val *fastjson.Value, p *Place) error { OnObject(p, func(o *Object) error { return JSONLoadObject(val, o) }) p.Accuracy = JSONGetFloat(val, "accuracy") p.Altitude = JSONGetFloat(val, "altitude") p.Latitude = JSONGetFloat(val, "latitude") p.Longitude = JSONGetFloat(val, "longitude") p.Radius = JSONGetInt(val, "radius") p.Units = JSONGetString(val, "units") return nil } func JSONLoadProfile(val *fastjson.Value, p *Profile) error { OnObject(p, func(o *Object) error { return JSONLoadObject(val, o) }) p.Describes = JSONGetItem(val, "describes") return nil } func JSONLoadRelationship(val *fastjson.Value, r *Relationship) error { OnObject(r, func(o *Object) error { return JSONLoadObject(val, o) }) r.Subject = JSONGetItem(val, "subject") r.Object = JSONGetItem(val, "object") r.Relationship = JSONGetItem(val, "relationship") return nil } func JSONLoadTombstone(val *fastjson.Value, t *Tombstone) error { OnObject(t, func(o *Object) error { return JSONLoadObject(val, o) }) t.FormerType = ActivityVocabularyType(JSONGetString(val, "formerType")) t.Deleted = JSONGetTime(val, "deleted") return nil } func JSONLoadLink(val *fastjson.Value, l *Link) error { l.ID = JSONGetID(val) l.Type = JSONGetType(val) l.MediaType = JSONGetMimeType(val, "mediaType") l.Preview = JSONGetItem(val, "preview") h := JSONGetInt(val, "height") if h != 0 { l.Height = uint(h) } w := JSONGetInt(val, "width") if w != 0 { l.Width = uint(w) } l.Name = JSONGetNaturalLanguageField(val, "name") hrefLang := JSONGetLangRefField(val, "hrefLang") if len(hrefLang) > 0 { l.HrefLang = hrefLang } href := JSONGetURIItem(val, "href") if href != nil { ll := href.GetLink() if len(ll) > 0 { l.Href = ll } } rel := JSONGetURIItem(val, "rel") if rel != nil { rr := rel.GetLink() if len(rr) > 0 { l.Rel = rr } } return nil } func JSONLoadPublicKey(val *fastjson.Value, p *PublicKey) error { if id := val.GetStringBytes("id"); len(id) > 0 { p.ID = ID(id) } if o := val.GetStringBytes("owner"); len(o) > 0 { p.Owner = IRI(o) } if pub := val.GetStringBytes("publicKeyPem"); len(pub) > 0 { p.PublicKeyPem = string(pub) } return nil }