diff --git a/pub/federating_wrapped_callbacks.go b/pub/federating_wrapped_callbacks.go index 25da2c3..0f3a39a 100644 --- a/pub/federating_wrapped_callbacks.go +++ b/pub/federating_wrapped_callbacks.go @@ -125,10 +125,8 @@ type FederatingWrappedCallbacks struct { // db is the Database the FederatingWrappedCallbacks should use. db Database // inboxIRI is the inboxIRI that is handling this callback. - // TODO: Populate inboxIRI *url.URL // newTransport creates a new Transport. - // TODO: Populate newTransport func(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t Transport, err error) } @@ -820,7 +818,7 @@ func (w FederatingWrappedCallbacks) announce(c context.Context, a vocab.Activity } // Get 'shares' value, defaulting to a collection. sharesT := shares.GetType() - if sharesT== nil { + if sharesT == nil { col := streams.NewActivityStreamsCollection() sharesT = col shares.SetActivityStreamsCollection(col) diff --git a/pub/property_interfaces.go b/pub/property_interfaces.go index 9435943..265d310 100644 --- a/pub/property_interfaces.go +++ b/pub/property_interfaces.go @@ -2,6 +2,7 @@ package pub import ( "github.com/go-fed/activity/streams/vocab" + "net/url" ) // inReplyToer is an ActivityStreams type with a 'inReplyTo' property @@ -49,26 +50,31 @@ type publisheder interface { // toer is an ActivityStreams type with a 'to' property type toer interface { GetActivityStreamsTo() vocab.ActivityStreamsToProperty + SetActivityStreamsTo(i vocab.ActivityStreamsToProperty) } // btoer is an ActivityStreams type with a 'bto' property type btoer interface { GetActivityStreamsBto() vocab.ActivityStreamsBtoProperty + SetActivityStreamsBto(i vocab.ActivityStreamsBtoProperty) } // ccer is an ActivityStreams type with a 'cc' property type ccer interface { GetActivityStreamsCc() vocab.ActivityStreamsCcProperty + SetActivityStreamsCc(i vocab.ActivityStreamsCcProperty) } // bccer is an ActivityStreams type with a 'bcc' property type bccer interface { GetActivityStreamsBcc() vocab.ActivityStreamsBccProperty + SetActivityStreamsBcc(i vocab.ActivityStreamsBccProperty) } // audiencer is an ActivityStreams type with a 'audience' property type audiencer interface { GetActivityStreamsAudience() vocab.ActivityStreamsAudienceProperty + SetActivityStreamsAudience(i vocab.ActivityStreamsAudienceProperty) } // inboxer is an ActivityStreams type with a 'inbox' property @@ -79,6 +85,7 @@ type inboxer interface { // attributedToer is an ActivityStreams type with a 'attributedTo' property type attributedToer interface { GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty + SetActivityStreamsAttributedTo(i vocab.ActivityStreamsAttributedToProperty) } // likeser is an ActivityStreams type with a 'likes' property @@ -98,3 +105,8 @@ type actorer interface { GetActivityStreamsActor() vocab.ActivityStreamsActorProperty SetActivityStreamsActor(i vocab.ActivityStreamsActorProperty) } + +// appendIRIer is an ActivityStreams type that can Append IRIs. +type appendIRIer interface { + AppendIRI(v *url.URL) +} diff --git a/pub/side_effect_actor.go b/pub/side_effect_actor.go index 22e0f44..f85ce2a 100644 --- a/pub/side_effect_actor.go +++ b/pub/side_effect_actor.go @@ -99,8 +99,8 @@ func (a *sideEffectActor) PostInbox(c context.Context, inboxIRI *url.URL, activi wrapped, other := a.s2s.Callbacks(c) // Populate side channels. wrapped.db = a.db - wrapped.inboxIRI= inboxIRI - wrapped.newTransport= a.s2s.NewTransport + wrapped.inboxIRI = inboxIRI + wrapped.newTransport = a.s2s.NewTransport if err = wrapped.disjoint(other); err != nil { return err } diff --git a/pub/social_wrapped_callbacks.go b/pub/social_wrapped_callbacks.go index 2189177..a436caa 100644 --- a/pub/social_wrapped_callbacks.go +++ b/pub/social_wrapped_callbacks.go @@ -2,7 +2,10 @@ package pub import ( "context" + "fmt" + "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" + "net/url" ) // SocialWrappedCallbacks lists the callback functions that already have some @@ -15,8 +18,7 @@ type SocialWrappedCallbacks struct { // // The wrapping callback for the Social Protocol copies the actor(s) to // the 'attributedTo' property, copying recipients between the Create - // activity and all objects, save the entry in the database, and adds it - // to the outbox. + // activity and all objects. It then saves the entry in the database. Create func(context.Context, vocab.ActivityStreamsCreate) error // Update handles additional side effects for the Update ActivityStreams // type. @@ -58,9 +60,16 @@ type SocialWrappedCallbacks struct { // // TODO: Describe Block func(context.Context, vocab.ActivityStreamsBlock) error + + // Sidechannel data -- this is set at request handling time. These must + // be set before the callbacks are used. + // db is the Database the SocialWrappedCallbacks should use. It must be // set before calling the callbacks. db Database + // deliverable is a sidechannel out, indicating if the handled activity + // should be delivered to a peer. + deliverable bool } // disjoint ensures that the functions given do not share a type signature with @@ -104,8 +113,6 @@ func (w SocialWrappedCallbacks) callbacks() []interface{} { w.update, w.deleteFn, w.follow, - w.accept, - w.reject, w.add, w.remove, w.like, @@ -116,120 +123,85 @@ func (w SocialWrappedCallbacks) callbacks() []interface{} { // create implements the social Create activity side effects. func (w SocialWrappedCallbacks) create(c context.Context, a vocab.ActivityStreamsCreate) error { - *deliverable = true - if s.LenObject() == 0 { - return errObjectRequired + w.deliverable = true + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired } - c = s.Raw() - // When a Create activity is posted, the actor of the activity - // SHOULD be copied onto the object's attributedTo field. - // Presumably only if it doesn't already exist, to prevent - // duplicate deliveries. - createActorIds := make(map[string]interface{}) - for i := 0; i < c.ActorLen(); i++ { - if c.IsActorObject(i) { - obj := c.GetActorObject(i) - id := obj.GetId() - createActorIds[id.String()] = obj - } else if c.IsActorLink(i) { - l := c.GetActorLink(i) - href := l.GetHref() - createActorIds[href.String()] = l - } else if c.IsActorIRI(i) { - iri := c.GetActorIRI(i) - createActorIds[iri.String()] = iri + // Obtain all actor IRIs. + actors := a.GetActivityStreamsActor() + createActorIds := make(map[string]*url.URL, actors.Len()) + for iter := actors.Begin(); iter != actors.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err } + createActorIds[id.String()] = id } - var obj []vocab.ObjectType - for i := 0; i < c.ObjectLen(); i++ { - if !c.IsObject(i) { - return fmt.Errorf("unsupported: Create Activity with 'object' that is only an IRI") - } - obj = append(obj, c.GetObject(i)) - } - objectAttributedToIds := make([]map[string]interface{}, len(obj)) + // Obtain each object's 'attributedTo' IRIs. + objectAttributedToIds := make([]map[string]*url.URL, op.Len()) for i := range objectAttributedToIds { - objectAttributedToIds[i] = make(map[string]interface{}) + objectAttributedToIds[i] = make(map[string]*url.URL) } - for k, o := range obj { - for i := 0; i < o.AttributedToLen(); i++ { - if o.IsAttributedToObject(i) { - at := o.GetAttributedToObject(i) - id := o.GetId() - objectAttributedToIds[k][id.String()] = at - } else if o.IsAttributedToLink(i) { - at := o.GetAttributedToLink(i) - href := at.GetHref() - objectAttributedToIds[k][href.String()] = at - } else if o.IsAttributedToIRI(i) { - iri := o.GetAttributedToIRI(i) - objectAttributedToIds[k][iri.String()] = iri + for i := 0; i < op.Len(); i++ { + t := op.At(i).GetType() + attrToer, ok := t.(attributedToer) + if !ok { + continue + } + attr := attrToer.GetActivityStreamsAttributedTo() + if attr == nil { + attr = streams.NewActivityStreamsAttributedToProperty() + attrToer.SetActivityStreamsAttributedTo(attr) + } + for iter := attr.Begin(); iter != attr.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err } + objectAttributedToIds[i][id.String()] = id } } + // Put all missing actor IRIs onto all object attributedTo properties. for k, v := range createActorIds { for i, attributedToMap := range objectAttributedToIds { if _, ok := attributedToMap[k]; !ok { - var iri *url.URL - if vObj, ok := v.(vocab.ObjectType); ok { - if !vObj.HasId() { - return fmt.Errorf("create actor object missing id") - } - iri = vObj.GetId() - } else if vLink, ok := v.(vocab.LinkType); ok { - if !vLink.HasHref() { - return fmt.Errorf("create actor link missing href") - } - iri = vLink.GetHref() - } else if vIRI, ok := v.(*url.URL); ok { - iri = vIRI + t := op.At(i).GetType() + attrToer, ok := t.(attributedToer) + if !ok { + continue } - obj[i].AppendAttributedToIRI(iri) + attr := attrToer.GetActivityStreamsAttributedTo() + attr.AppendIRI(v) } } } + // Put all missing object attributedTo IRIs onto the actor property. for _, attributedToMap := range objectAttributedToIds { for k, v := range attributedToMap { if _, ok := createActorIds[k]; !ok { - var iri *url.URL - if vObj, ok := v.(vocab.ObjectType); ok { - if !vObj.HasId() { - return fmt.Errorf("attributedTo object missing id") - } - iri = vObj.GetId() - } else if vLink, ok := v.(vocab.LinkType); ok { - if !vLink.HasHref() { - return fmt.Errorf("attributedTo link missing href") - } - iri = vLink.GetHref() - } else if vIRI, ok := v.(*url.URL); ok { - iri = vIRI - } - c.AppendActorIRI(iri) + actors.AppendIRI(v) } } } - // As such, a server SHOULD copy any recipients of the Create activity to its - // object upon initial distribution, and likewise with copying recipients from - // the object to the wrapping Create activity. - if err := f.sameRecipients(c); err != nil { + // Copy over the 'to', 'bto', 'cc', 'bcc', and 'audience' recipients + // between the activity and all child objects and vice versa. + if err := normalizeRecipients(a); err != nil { return err } - // Create requires the client application to persist the 'object' that - // was created. - for _, o := range obj { - if err := f.App.Set(ctx, o); err != nil { + // Persist all objects we've created, which will include sensitive + // recipients such as 'bcc' and 'bto'. + for i := 0; i < op.Len(); i++ { + obj := op.At(i).GetType() + // TODO: Lock + if err := w.db.Create(c, obj); err != nil { return err } } - // Persist the above changes in the outbox - var err error - *toAddToOutbox = make(map[string]interface{}) - *toAddToOutbox, err = c.Serialize() - if err != nil { - return err + if w.Create != nil { + return w.Create(c, a) } - return f.ClientCallbacker.Create(ctx, s) + return nil } // update implements the social Update activity side effects. diff --git a/pub/util.go b/pub/util.go index 1d8116b..8938386 100644 --- a/pub/util.go +++ b/pub/util.go @@ -539,3 +539,264 @@ func mustHaveActivityActorsMatchObjectActors(a vocab.ActivityType) error { } return nil } + +// normalizeRecipients ensures the activity and object have the same 'to', +// 'bto', 'cc', 'bcc', and 'audience' properties. Copy the Activity's recipients +// to objects, and the objects to the activity, but does NOT copy objects' +// recipients to each other. +func normalizeRecipients(a vocab.ActivityStreamsCreate) error { + // Phase 0: Acquire all recipients on the activity. + // + // Obtain the actorTo map + actorToMap := make(map[string]interface{}) + actorTo := a.GetActivityStreamsTo() + if actorTo == nil { + actorTo = streams.NewActivityStreamsToProperty() + a.SetActivityStreamsTo(actorTo) + } + for iter := actorTo.Begin(); iter != actorTo.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + actorToMap[id.String()] = id + } + // Obtain the actorBto map + actorBtoMap := make(map[string]interface{}) + actorBto := a.GetActivityStreamsBto() + if actorBto == nil { + actorBto = streams.NewActivityStreamsBtoProperty() + a.SetActivityStreamsBto(actorBto) + } + for iter := actorBto.Begin(); iter != actorBto.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + actorBtoMap[id.String()] = id + } + // Obtain the actorCc map + actorCcMap := make(map[string]interface{}) + actorCc := a.GetActivityStreamsCc() + if actorCc == nil { + actorCc = streams.NewActivityStreamsCcProperty() + a.SetActivityStreamsCc(actorCc) + } + for iter := actorCc.Begin(); iter != actorCc.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + actorCcMap[id.String()] = id + } + // Obtain the actorBcc map + actorBccMap := make(map[string]interface{}) + actorBcc := a.GetActivityStreamsBcc() + if actorBcc == nil { + actorBcc = streams.NewActivityStreamsBccProperty() + a.SetActivityStreamsBcc(actorBcc) + } + for iter := actorBcc.Begin(); iter != actorBcc.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + actorBccMap[id.String()] = id + } + // Obtain the actorAudience map + actorAudienceMap := make(map[string]interface{}) + actorAudience := a.GetActivityStreamsAudience() + if actorAudience == nil { + actorAudience = streams.NewActivityStreamsAudienceProperty() + a.SetActivityStreamsAudience(actorAudience) + } + for iter := actorAudience.Begin(); iter != actorAudience.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + actorAudienceMap[id.String()] = id + } + // Obtain the objects maps for each recipient type. + o := a.GetActivityStreamsObject() + objsTo := make([]map[string]interface{}, o.Len()) + objsBto := make([]map[string]interface{}, o.Len()) + objsCco := make([]map[string]interface{}, o.Len()) + objsBcc := make([]map[string]interface{}, o.Len()) + objsAudience := make([]map[string]interface{}, o.Len()) + for i := 0; i < o.Len(); i++ { + // Phase 1: Acquire all existing recipients on the object. + // + // Object to + objsTo[i] = make(map[string]interface{}) + var oTo vocab.ActivityStreamsToProperty + if tr, ok := o.At(i).(toer); !ok { + return fmt.Errorf("the Create object at %d has no 'to' property", i) + } else { + oTo = tr.GetActivityStreamsTo() + if oTo == nil { + oTo = streams.NewActivityStreamsToProperty() + tr.SetActivityStreamsTo(oTo) + } + } + for iter := oTo.Begin(); iter != oTo.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + objsTo[i][id.String()] = id + } + // Object bto + objsBto[i] = make(map[string]interface{}) + var oBto vocab.ActivityStreamsBtoProperty + if tr, ok := o.At(i).(btoer); !ok { + return fmt.Errorf("the Create object at %d has no 'bto' property", i) + } else { + oBto = tr.GetActivityStreamsBto() + if oBto == nil { + oBto = streams.NewActivityStreamsBtoProperty() + tr.SetActivityStreamsBto(oBto) + } + } + for iter := oBto.Begin(); iter != oBto.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + objsBto[i][id.String()] = id + } + // Object cc + objsCc[i] = make(map[string]interface{}) + var oCc vocab.ActivityStreamsCcProperty + if tr, ok := o.At(i).(ccer); !ok { + return fmt.Errorf("the Create object at %d has no 'cc' property", i) + } else { + oCc = tr.GetActivityStreamsCc() + if oCc == nil { + oCc = streams.NewActivityStreamsCcProperty() + tr.SetActivityStreamsCc(oCc) + } + } + for iter := oCc.Begin(); iter != oCc.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + objsCc[i][id.String()] = id + } + // Object bcc + objsBcc[i] = make(map[string]interface{}) + var oBcc vocab.ActivityStreamsBccProperty + if tr, ok := o.At(i).(bccer); !ok { + return fmt.Errorf("the Create object at %d has no 'bcc' property", i) + } else { + oBcc = tr.GetActivityStreamsBcc() + if oBcc == nil { + oBcc = streams.NewActivityStreamsBccProperty() + tr.SetActivityStreamsBcc(oBcc) + } + } + for iter := oBcc.Begin(); iter != oBcc.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + objsBcc[i][id.String()] = id + } + // Object audience + objsAudience[i] = make(map[string]interface{}) + var oAudience vocab.ActivityStreamsAudienceProperty + if tr, ok := o.At(i).(audiencer); !ok { + return fmt.Errorf("the Create object at %d has no 'audience' property", i) + } else { + oAudience = tr.GetActivityStreamsAudience() + if oAudience == nil { + oAudience = streams.NewActivityStreamsAudienceProperty() + tr.SetActivityStreamsAudience(oAudience) + } + } + for iter := oAudience.Begin(); iter != oAudience.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + objsAudience[i][id.String()] = id + } + // Phase 2: Apply missing recipients to the object from the + // activity. + // + // Activity to -> Object to + for k, v := range actorToMap { + if _, ok := objsTo[i][k]; !ok { + oTo.AppendIRI(v) + } + } + // Activity bto -> Object bto + for k, v := range actorBtoMap { + if _, ok := objsBto[i][k]; !ok { + oBto.AppendIRI(v) + } + } + // Activity cc -> Object cc + for k, v := range actorCcMap { + if _, ok := objsCc[i][k]; !ok { + oCc.AppendIRI(v) + } + } + // Activity bcc -> Object bcc + for k, v := range actorBccMap { + if _, ok := objsBcc[i][k]; !ok { + oBcc.AppendIRI(v) + } + } + // Activity audience -> Object audience + for k, v := range actorAudienceMap { + if _, ok := objsAudience[i][k]; !ok { + oAudience.AppendIRI(v) + } + } + } + // Phase 3: Apply missing recipients to the activity from the objects. + // + // Object to -> Activity to + for i := 0; i < len(objsTo); i++ { + for k, v := range objsTo[i] { + if _, ok := actorToMap[k]; !ok { + actorTo.AppendIRI(v) + } + } + } + // Object bto -> Activity bto + for i := 0; i < len(objsBto); i++ { + for k, v := range objsBto[i] { + if _, ok := actorBtoMap[k]; !ok { + actorBto.AppendIRI(v) + } + } + } + // Object cc -> Activity cc + for i := 0; i < len(objsCc); i++ { + for k, v := range objsCc[i] { + if _, ok := actorCcMap[k]; !ok { + actorCc.AppendIRI(v) + } + } + } + // Object bcc -> Activity bcc + for i := 0; i < len(objsBcc); i++ { + for k, v := range objsBcc[i] { + if _, ok := actorBccMap[k]; !ok { + actorBcc.AppendIRI(v) + } + } + } + // Object audience -> Activity audience + for i := 0; i < len(objsAudience); i++ { + for k, v := range objsAudience[i] { + if _, ok := actorAudienceMap[k]; !ok { + actorAudience.AppendIRI(v) + } + } + } + return nil +}