summaryrefslogtreecommitdiff
path: root/vendor/github.com/superseriousbusiness/activity/pub/base_actor.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/superseriousbusiness/activity/pub/base_actor.go')
-rw-r--r--vendor/github.com/superseriousbusiness/activity/pub/base_actor.go495
1 files changed, 495 insertions, 0 deletions
diff --git a/vendor/github.com/superseriousbusiness/activity/pub/base_actor.go b/vendor/github.com/superseriousbusiness/activity/pub/base_actor.go
new file mode 100644
index 000000000..2692a0b0c
--- /dev/null
+++ b/vendor/github.com/superseriousbusiness/activity/pub/base_actor.go
@@ -0,0 +1,495 @@
+package pub
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+
+ "github.com/superseriousbusiness/activity/streams"
+ "github.com/superseriousbusiness/activity/streams/vocab"
+)
+
+// baseActor must satisfy the Actor interface.
+var _ Actor = &baseActor{}
+
+// baseActor is an application-independent ActivityPub implementation. It does
+// not implement the entire protocol, and relies on a delegate to do so. It
+// only implements the part of the protocol that is side-effect-free, allowing
+// an existing application to write a DelegateActor that glues their application
+// into the ActivityPub world.
+//
+// It is preferred to use a DelegateActor provided by this library, so that the
+// application does not need to worry about the ActivityPub implementation.
+type baseActor struct {
+ // delegate contains application-specific delegation logic.
+ delegate DelegateActor
+ // enableSocialProtocol enables or disables the Social API, the client to
+ // server part of ActivityPub. Useful if permitting remote clients to
+ // act on behalf of the users of the client application.
+ enableSocialProtocol bool
+ // enableFederatedProtocol enables or disables the Federated Protocol, or the
+ // server to server part of ActivityPub. Useful to permit integrating
+ // with the rest of the federative web.
+ enableFederatedProtocol bool
+ // clock simply tracks the current time.
+ clock Clock
+}
+
+// baseActorFederating must satisfy the FederatingActor interface.
+var _ FederatingActor = &baseActorFederating{}
+
+// baseActorFederating is a baseActor that also satisfies the FederatingActor
+// interface.
+//
+// The baseActor is preserved as an Actor which will not successfully cast to a
+// FederatingActor.
+type baseActorFederating struct {
+ baseActor
+}
+
+// NewSocialActor builds a new Actor concept that handles only the Social
+// Protocol part of ActivityPub.
+//
+// This Actor can be created once in an application and reused to handle
+// multiple requests concurrently and for different endpoints.
+//
+// It leverages as much of go-fed as possible to ensure the implementation is
+// compliant with the ActivityPub specification, while providing enough freedom
+// to be productive without shooting one's self in the foot.
+//
+// Do not try to use NewSocialActor and NewFederatingActor together to cover
+// both the Social and Federating parts of the protocol. Instead, use NewActor.
+func NewSocialActor(c CommonBehavior,
+ c2s SocialProtocol,
+ db Database,
+ clock Clock) Actor {
+ return &baseActor{
+ delegate: &sideEffectActor{
+ common: c,
+ c2s: c2s,
+ db: db,
+ clock: clock,
+ },
+ enableSocialProtocol: true,
+ clock: clock,
+ }
+}
+
+// NewFederatingActor builds a new Actor concept that handles only the Federating
+// Protocol part of ActivityPub.
+//
+// This Actor can be created once in an application and reused to handle
+// multiple requests concurrently and for different endpoints.
+//
+// It leverages as much of go-fed as possible to ensure the implementation is
+// compliant with the ActivityPub specification, while providing enough freedom
+// to be productive without shooting one's self in the foot.
+//
+// Do not try to use NewSocialActor and NewFederatingActor together to cover
+// both the Social and Federating parts of the protocol. Instead, use NewActor.
+func NewFederatingActor(c CommonBehavior,
+ s2s FederatingProtocol,
+ db Database,
+ clock Clock) FederatingActor {
+ return &baseActorFederating{
+ baseActor{
+ delegate: &sideEffectActor{
+ common: c,
+ s2s: s2s,
+ db: db,
+ clock: clock,
+ },
+ enableFederatedProtocol: true,
+ clock: clock,
+ },
+ }
+}
+
+// NewActor builds a new Actor concept that handles both the Social and
+// Federating Protocol parts of ActivityPub.
+//
+// This Actor can be created once in an application and reused to handle
+// multiple requests concurrently and for different endpoints.
+//
+// It leverages as much of go-fed as possible to ensure the implementation is
+// compliant with the ActivityPub specification, while providing enough freedom
+// to be productive without shooting one's self in the foot.
+func NewActor(c CommonBehavior,
+ c2s SocialProtocol,
+ s2s FederatingProtocol,
+ db Database,
+ clock Clock) FederatingActor {
+ return &baseActorFederating{
+ baseActor{
+ delegate: &sideEffectActor{
+ common: c,
+ c2s: c2s,
+ s2s: s2s,
+ db: db,
+ clock: clock,
+ },
+ enableSocialProtocol: true,
+ enableFederatedProtocol: true,
+ clock: clock,
+ },
+ }
+}
+
+// NewCustomActor allows clients to create a custom ActivityPub implementation
+// for the Social Protocol, Federating Protocol, or both.
+//
+// It still uses the library as a high-level scaffold, which has the benefit of
+// allowing applications to grow into a custom ActivityPub solution without
+// having to refactor the code that passes HTTP requests into the Actor.
+//
+// It is possible to create a DelegateActor that is not ActivityPub compliant.
+// Use with due care.
+func NewCustomActor(delegate DelegateActor,
+ enableSocialProtocol, enableFederatedProtocol bool,
+ clock Clock) FederatingActor {
+ return &baseActorFederating{
+ baseActor{
+ delegate: delegate,
+ enableSocialProtocol: enableSocialProtocol,
+ enableFederatedProtocol: enableFederatedProtocol,
+ clock: clock,
+ },
+ }
+}
+
+// PostInbox implements the generic algorithm for handling a POST request to an
+// actor's inbox independent on an application. It relies on a delegate to
+// implement application specific functionality.
+//
+// Only supports serving data with identifiers having the HTTPS scheme.
+func (b *baseActor) PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
+ return b.PostInboxScheme(c, w, r, "https")
+}
+
+// PostInbox implements the generic algorithm for handling a POST request to an
+// actor's inbox independent on an application. It relies on a delegate to
+// implement application specific functionality.
+//
+// Specifying the "scheme" allows for retrieving ActivityStreams content with
+// identifiers such as HTTP, HTTPS, or other protocol schemes.
+func (b *baseActor) PostInboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) {
+ // Do nothing if it is not an ActivityPub POST request.
+ if !isActivityPubPost(r) {
+ return false, nil
+ }
+ // If the Federated Protocol is not enabled, then this endpoint is not
+ // enabled.
+ if !b.enableFederatedProtocol {
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return true, nil
+ }
+ // Check the peer request is authentic.
+ c, authenticated, err := b.delegate.AuthenticatePostInbox(c, w, r)
+ if err != nil {
+ return true, err
+ } else if !authenticated {
+ return true, nil
+ }
+ // Begin processing the request, but have not yet applied
+ // authorization (ex: blocks). Obtain the activity reject unknown
+ // activities.
+ raw, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ return true, err
+ }
+ var m map[string]interface{}
+ if err = json.Unmarshal(raw, &m); err != nil {
+ return true, err
+ }
+ asValue, err := streams.ToType(c, m)
+ if err != nil && !streams.IsUnmatchedErr(err) {
+ return true, err
+ } else if streams.IsUnmatchedErr(err) {
+ // Respond with bad request -- we do not understand the type.
+ w.WriteHeader(http.StatusBadRequest)
+ return true, nil
+ }
+ activity, ok := asValue.(Activity)
+ if !ok {
+ return true, fmt.Errorf("activity streams value is not an Activity: %T", asValue)
+ }
+ if activity.GetJSONLDId() == nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return true, nil
+ }
+ // Allow server implementations to set context data with a hook.
+ c, err = b.delegate.PostInboxRequestBodyHook(c, r, activity)
+ if err != nil {
+ return true, err
+ }
+ // Check authorization of the activity.
+ authorized, err := b.delegate.AuthorizePostInbox(c, w, activity)
+ if err != nil {
+ return true, err
+ } else if !authorized {
+ return true, nil
+ }
+ // Post the activity to the actor's inbox and trigger side effects for
+ // that particular Activity type. It is up to the delegate to resolve
+ // the given map.
+ inboxId := requestId(r, scheme)
+ err = b.delegate.PostInbox(c, inboxId, activity)
+ 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
+ }
+ // Our side effects are complete, now delegate determining whether to
+ // do inbox forwarding, as well as the action to do it.
+ if err := b.delegate.InboxForwarding(c, inboxId, activity); err != nil {
+ return true, err
+ }
+ // Request has been processed. Begin responding to the request.
+ //
+ // Simply respond with an OK status to the peer.
+ w.WriteHeader(http.StatusOK)
+ return true, nil
+}
+
+// GetInbox implements the generic algorithm for handling a GET request to an
+// actor's inbox independent on an application. It relies on a delegate to
+// implement application specific functionality.
+func (b *baseActor) GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
+ // Do nothing if it is not an ActivityPub GET request.
+ if !isActivityPubGet(r) {
+ return false, nil
+ }
+ // Delegate authenticating and authorizing the request.
+ c, authenticated, err := b.delegate.AuthenticateGetInbox(c, w, r)
+ if err != nil {
+ return true, err
+ } else if !authenticated {
+ return true, nil
+ }
+ // Everything is good to begin processing the request.
+ oc, err := b.delegate.GetInbox(c, r)
+ if err != nil {
+ return true, err
+ }
+ // Deduplicate the 'orderedItems' property by ID.
+ err = dedupeOrderedItems(oc)
+ if err != nil {
+ return true, err
+ }
+ // Request has been processed. Begin responding to the request.
+ //
+ // Serialize the OrderedCollection.
+ m, err := streams.Serialize(oc)
+ if err != nil {
+ return true, err
+ }
+ raw, err := json.Marshal(m)
+ if err != nil {
+ return true, err
+ }
+ // Write the response.
+ addResponseHeaders(w.Header(), b.clock, raw)
+ w.WriteHeader(http.StatusOK)
+ n, err := w.Write(raw)
+ if err != nil {
+ return true, err
+ } else if n != len(raw) {
+ return true, fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(raw))
+ }
+ return true, nil
+}
+
+// PostOutbox implements the generic algorithm for handling a POST request to an
+// actor's outbox independent on an application. It relies on a delegate to
+// implement application specific functionality.
+//
+// Only supports serving data with identifiers having the HTTPS scheme.
+func (b *baseActor) PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
+ return b.PostOutboxScheme(c, w, r, "https")
+}
+
+// PostOutbox implements the generic algorithm for handling a POST request to an
+// actor's outbox independent on an application. It relies on a delegate to
+// implement application specific functionality.
+//
+// Specifying the "scheme" allows for retrieving ActivityStreams content with
+// identifiers such as HTTP, HTTPS, or other protocol schemes.
+func (b *baseActor) PostOutboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) {
+ // Do nothing if it is not an ActivityPub POST request.
+ if !isActivityPubPost(r) {
+ return false, nil
+ }
+ // If the Social API is not enabled, then this endpoint is not enabled.
+ if !b.enableSocialProtocol {
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return true, nil
+ }
+ // Delegate authenticating and authorizing the request.
+ c, authenticated, err := b.delegate.AuthenticatePostOutbox(c, w, r)
+ if err != nil {
+ return true, err
+ } else if !authenticated {
+ return true, nil
+ }
+ // Everything is good to begin processing the request.
+ raw, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ return true, err
+ }
+ var m map[string]interface{}
+ if err = json.Unmarshal(raw, &m); err != nil {
+ return true, err
+ }
+ // Note that converting to a Type will NOT successfully convert types
+ // not known to go-fed. This prevents accidentally wrapping an Activity
+ // type unknown to go-fed in a Create below. Instead,
+ // streams.ErrUnhandledType will be returned here.
+ asValue, err := streams.ToType(c, m)
+ if err != nil && !streams.IsUnmatchedErr(err) {
+ return true, err
+ } else if streams.IsUnmatchedErr(err) {
+ // Respond with bad request -- we do not understand the type.
+ w.WriteHeader(http.StatusBadRequest)
+ return true, nil
+ }
+ // Allow server implementations to set context data with a hook.
+ c, err = b.delegate.PostOutboxRequestBodyHook(c, r, asValue)
+ if err != nil {
+ return true, err
+ }
+ // The HTTP request steps are complete, complete the rest of the outbox
+ // and delivery process.
+ outboxId := requestId(r, scheme)
+ activity, err := b.deliver(c, outboxId, asValue, m)
+ // 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 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.GetJSONLDId().Get().String())
+ w.WriteHeader(http.StatusCreated)
+ return true, nil
+}
+
+// GetOutbox implements the generic algorithm for handling a Get request to an
+// actor's outbox independent on an application. It relies on a delegate to
+// implement application specific functionality.
+func (b *baseActor) GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
+ // Do nothing if it is not an ActivityPub GET request.
+ if !isActivityPubGet(r) {
+ return false, nil
+ }
+ // Delegate authenticating and authorizing the request.
+ c, authenticated, err := b.delegate.AuthenticateGetOutbox(c, w, r)
+ if err != nil {
+ return true, err
+ } else if !authenticated {
+ return true, nil
+ }
+ // Everything is good to begin processing the request.
+ oc, err := b.delegate.GetOutbox(c, r)
+ if err != nil {
+ return true, err
+ }
+ // Request has been processed. Begin responding to the request.
+ //
+ // Serialize the OrderedCollection.
+ m, err := streams.Serialize(oc)
+ if err != nil {
+ return true, err
+ }
+ raw, err := json.Marshal(m)
+ if err != nil {
+ return true, err
+ }
+ // Write the response.
+ addResponseHeaders(w.Header(), b.clock, raw)
+ w.WriteHeader(http.StatusOK)
+ n, err := w.Write(raw)
+ if err != nil {
+ return true, err
+ } else if n != len(raw) {
+ return true, fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(raw))
+ }
+ return true, nil
+}
+
+// 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)
+}