diff --git a/pub/actor.go b/pub/actor.go index 05d3a24..8fb1995 100644 --- a/pub/actor.go +++ b/pub/actor.go @@ -1,6 +1,7 @@ package pub import ( + "github.com/go-fed/activity/streams/vocab" "context" "net/http" "net/url" @@ -96,13 +97,18 @@ type Actor interface { // Activity to a federating peer. type FederatingActor interface { Actor - // Deliver sends a federated message. + // Send a federated activity. // - // The provided url must be the outbox of the sender for identity - // purposes only. It is up to the caller to ensure that the provided - // activity has been added to the outbox. + // The provided url must be the outbox of the sender. All processing of + // the activity occurs similarly to the C2S flow: + // - If t is not an Activity, it is wrapped in a Create activity. + // - A new ID is generated for the activity + // - The activity is added to the specified outbox + // - The activity is prepared and delivered to recipients // // Note that this function will only behave as expected if the - // implementation has been constructed to support federation. - Deliver(c context.Context, outbox *url.URL, activity Activity) error + // implementation has been constructed to support federation. This + // method will guaranteed work for non-custom Actors. For custom actors, + // care should be used to not call this method if only C2S is supported. + Send(c context.Context, outbox *url.URL, t vocab.Type) (Activity, error) } diff --git a/pub/base_actor.go b/pub/base_actor.go index 08be50c..a25456f 100644 --- a/pub/base_actor.go +++ b/pub/base_actor.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "github.com/go-fed/activity/streams" + "github.com/go-fed/activity/streams/vocab" "io/ioutil" "net/http" "net/url" @@ -329,50 +330,18 @@ func (b *baseActor) PostOutbox(c context.Context, w http.ResponseWriter, r *http w.WriteHeader(http.StatusBadRequest) return true, nil } - // If the value is not an Activity or type extending from Activity, then - // we need to wrap it in a Create Activity. - if !streams.IsOrExtendsActivityStreamsActivity(asValue) { - asValue, err = b.delegate.WrapInCreate(c, asValue, r.URL) - if err != nil { - return true, err - } - } - // At this point, this should be a safe conversion. If this error is - // triggered, then there is either a bug in the delegation of - // WrapInCreate, behavior is not lining up in the generated ExtendedBy - // code, or something else is incorrect with the type system. - activity, ok := asValue.(Activity) - if !ok { - return true, fmt.Errorf("activity streams value is not an Activity: %T", asValue) - } - // Delegate generating new IDs for the activity and all new objects. - if err = b.delegate.AddNewIds(c, activity); err != nil { - return true, err - } - // Post the activity to the actor's outbox and trigger side effects for - // that particular Activity type. - deliverable, err := b.delegate.PostOutbox(c, activity, r.URL, m) - if err != nil { - // Special case: We know it is a bad request if the object or - // target properties needed to be populated, but weren't. - // - // Send the rejection to the peer. - if err == ErrObjectRequired || err == ErrTargetRequired { - w.WriteHeader(http.StatusBadRequest) - return true, nil - } - return true, err - } - // Request has been processed and all side effects internal to this - // application server have finished. Begin side effects affecting other - // servers and/or the client who sent this request. + // The HTTP request steps are complete, complete the rest of the outbox + // and delivery process. + activity, err := b.deliver(c, r.URL, asValue, m) + // Special case: We know it is a bad request if the object or + // target properties needed to be populated, but weren't. // - // If we are federating and the type is a deliverable one, then deliver - // the activity to federating peers. - if b.enableFederatedProtocol && deliverable { - if err := b.delegate.Deliver(c, r.URL, activity); err != nil { - return true, err - } + // Send the rejection to the client. + if err == ErrObjectRequired || err == ErrTargetRequired { + w.WriteHeader(http.StatusBadRequest) + return true, nil + } else if err != nil { + return true, err } // Respond to the request with the new Activity's IRI location. w.Header().Set(locationHeader, activity.GetActivityStreamsId().Get().String()) @@ -423,7 +392,67 @@ func (b *baseActor) GetOutbox(c context.Context, w http.ResponseWriter, r *http. return true, nil } -// Deliver delegates directly to the delegate actor. -func (b *baseActorFederating) Deliver(c context.Context, outbox *url.URL, activity Activity) error { - return b.delegate.Deliver(c, outbox, activity) +// deliver delegates all outbox handling steps and optionally will federate the +// activity if the federated protocol is enabled. +// +// This function is not exported so an Actor that only supports C2S cannot be +// type casted to a FederatingActor. It doesn't exactly fit the Send method +// signature anyways. +// +// Note: 'm' is nilable. +func (b *baseActor) deliver(c context.Context, outbox *url.URL, asValue vocab.Type, m map[string]interface{}) (activity Activity, err error) { + // If the value is not an Activity or type extending from Activity, then + // we need to wrap it in a Create Activity. + if !streams.IsOrExtendsActivityStreamsActivity(asValue) { + asValue, err = b.delegate.WrapInCreate(c, asValue, outbox) + if err != nil { + return + } + } + // At this point, this should be a safe conversion. If this error is + // triggered, then there is either a bug in the delegation of + // WrapInCreate, behavior is not lining up in the generated ExtendedBy + // code, or something else is incorrect with the type system. + var ok bool + activity, ok = asValue.(Activity) + if !ok { + err = fmt.Errorf("activity streams value is not an Activity: %T", asValue) + return + } + // Delegate generating new IDs for the activity and all new objects. + if err = b.delegate.AddNewIds(c, activity); err != nil { + return + } + // Post the activity to the actor's outbox and trigger side effects for + // that particular Activity type. + // + // Since 'm' is nil-able and side effects may need access to literal nil + // values, such as for Update activities, ensure 'm' is non-nil. + if m == nil { + m, err = asValue.Serialize() + if err != nil { + return + } + } + deliverable, err := b.delegate.PostOutbox(c, activity, outbox, m) + if err != nil { + return + } + // Request has been processed and all side effects internal to this + // application server have finished. Begin side effects affecting other + // servers and/or the client who sent this request. + // + // If we are federating and the type is a deliverable one, then deliver + // the activity to federating peers. + if b.enableFederatedProtocol && deliverable { + if err = b.delegate.Deliver(c, outbox, activity); err != nil { + return + } + } + return +} + +// Send is programmatically accessible if the federated protocol is enabled. +func (b *baseActorFederating) Send(c context.Context, outbox *url.URL, t vocab.Type) (Activity, error) { + return b.deliver(c, outbox, t, nil) }