From 3662ac5bf9942648faa6634a58ea57bb052fe138 Mon Sep 17 00:00:00 2001 From: Cory Slep Date: Wed, 31 Jan 2018 22:43:13 +0100 Subject: [PATCH] Outline support for inbox and outbox handlers and routing. --- pub/fed.go | 317 +++++++++++++++++++++++++++++++++++----------- pub/interfaces.go | 8 ++ pub/internal.go | 86 +++++++++++-- 3 files changed, 325 insertions(+), 86 deletions(-) diff --git a/pub/fed.go b/pub/fed.go index 3629be8..e4948e2 100644 --- a/pub/fed.go +++ b/pub/fed.go @@ -1,8 +1,11 @@ package pub import ( + "encoding/json" + "fmt" "github.com/go-fed/activity/streams" "github.com/go-fed/activity/vocab" + "io/ioutil" "net/http" "net/url" ) @@ -103,72 +106,118 @@ type Endpoint interface { var _ Endpoint = &streams.Object{} -// Storer is a long term storage solution provided by clients so that data can -// be saved and retrieved by the ActivityPub federated server. -type Storer interface { - // TODO +type clienter struct { } -// Server implements receiving the federated portion of the ActivityPub -// specification. -// -// It implements a single 'sharedinbox' to trade off numerous messages over the -// network for increased internal processing. Additionally, it will keep track -// of externally-known 'sharedInbox' in order to send public messages -// efficiently, as permitted by the spec. -// -// Fields that are able to be 'nil' are marked as such; otherwise assume that -// all fields are required. -type Server struct { - // S is used for long-term storage and retrieval of ActivityPub - // messages. - S Storer +func (c *clienter) PostInbox() (http.HandlerFunc, error) { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusMethodNotAllowed) + }, nil } -// OnInbox proides a handler function for when an Actor receives an Activity. -func (s *Server) OnInbox() (http.HandlerFunc, error) { +func (c *clienter) handleCreate() error { + // TODO: Enforce object // TODO: Implement - return nil, nil + return nil } -func (s *Server) OnOutbox() (http.HandlerFunc, error) { +func (c *clienter) handleUpdate() error { + // TODO: Enforce object // TODO: Implement - return nil, nil + return nil } -func (s *Server) OnSharedInbox() (http.HandlerFunc, error) { +func (c *clienter) handleDelete() error { + // TODO: Enforce object // TODO: Implement - return nil, nil + return nil } -func (s *Server) OnFollowers() (http.HandlerFunc, error) { +func (c *clienter) handleFollow() error { + // TODO: Enforce object // TODO: Implement - return nil, nil + return nil } -func (s *Server) OnFollowing() (http.HandlerFunc, error) { +func (c *clienter) handleAccept() error { // TODO: Implement - return nil, nil + return nil } -func (s *Server) OnLiked() (http.HandlerFunc, error) { +func (c *clienter) handleReject() error { // TODO: Implement - return nil, nil + return nil } -func (s *Server) OnLikes() (http.HandlerFunc, error) { +func (c *clienter) handleAdd() error { + // TODO: Enforce object & target // TODO: Implement - return nil, nil + return nil } -func (s *Server) OnShares() (http.HandlerFunc, error) { +func (c *clienter) handleRemove() error { + // TODO: Enforce object & target // TODO: Implement - return nil, nil + return nil } -// Client implements sending the federated portion of the ActivityPub -// specification. -type Client struct { +func (c *clienter) handleLike() error { + // TODO: Enforce object + // TODO: Implement + return nil +} + +func (c *clienter) handleUndo() error { + // TODO: Enforce object + // TODO: Implement + return nil +} + +// FederatorStorer is a long term storage solution provided by clients so that +// data can be saved and retrieved by the ActivityPub federated server. +type FederatorStorer interface { + // GetInbox returns the OrderedCollection inbox of the actor with the + // provided ID. + GetInbox(id string, r *http.Request) (vocab.OrderedCollectionType, error) + // Create requires the client application to persist the 'object' that + // was created. + Create(id string, s *streams.Create) error + // Update should completely replace the 'object' with the same 'id'. + Update(id string, s *streams.Update) error + // Delete SHOULD completely remove the 'object' with its 'id', or have + // the 'object' be replaced by a 'Tombstone' ActivityStream type. + Delete(id string, s *streams.Delete) error + // Follow means the client application SHOULD reply with an 'Accept' or + // 'Reject' ActivityStream with the 'Follow' as the 'object' and deliver + // it to the 'actor' of the 'Follow'. This can be human-triggered or + // automatically triggered. + Follow(id string, s *streams.Follow) error + // Accept can be client application specific. However, if this 'Accept' + // is in response to a 'Follow' then the follower should be added by the + // client application. + Accept(id string, s *streams.Accept) error + // Reject can be client application specific. However, if this 'Reject' + // is in response to a 'Follow' then the client MUST NOT go forward with + // adding the follower. + Reject(id string, s *streams.Reject) error + // Add is client application specific, generally involving adding an + // 'object' to a specific 'target' collection. + Add(id string, s *streams.Add) error + // Remove is client application specific, generally involving removing + // an 'object' from a specific 'target' collection. + Remove(id string, s *streams.Remove) error + // Like triggers adding the like to an object's `like` collection. + Like(id string, s *streams.Like) error + // Undo negates a previous action. The 'actor' on the 'Undo' MUST be the + // same as the 'actor' on the Activity being undone, and the client + // application is responsible for enforcing this. + Undo(id string, s *streams.Undo) error +} + +type federator struct { + // Storer is the permanent storage solution provided by the client + // application. + Storer FederatorStorer // Client is used to federate with other ActivityPub servers. Client *http.Client // Agent is the User-Agent string to use in HTTP headers when @@ -179,62 +228,178 @@ type Client struct { // delivery. It must be at least 1 to be compliant with the ActivityPub // spec. MaxDepth int + // EnableClient 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. + EnableClient bool + // EnableServer 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. + EnableServer bool } -func (c *Client) Create() error { - // TODO: Enforce object - // TODO: Implement - return nil +func (f *federator) PostInbox(id string) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) (bool, error) { + if !isActivityPubPost(r) { + return false, nil + } + if !f.EnableServer { + w.WriteHeader(http.StatusMethodNotAllowed) + return true, nil + } + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return true, err + } + var m map[string]interface{} + if err = json.Unmarshal(b, &m); err != nil { + return true, err + } + if err = f.getPostInboxResolver(id).Deserialize(m); err != nil { + return true, err + } + // TODO: 7.1.2 Inbox forwarding + w.WriteHeader(http.StatusOK) + return true, nil + } } -func (c *Client) Update() error { - // TODO: Enforce object - // TODO: Implement - return nil +func (f *federator) GetInbox(id string) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) (bool, error) { + if !isActivityPubGet(r) { + return false, nil + } + oc, err := f.Storer.GetInbox(id, r) + if err != nil { + return true, err + } + oc, err = f.dedupeOrderedItems(oc) + if err != nil { + return true, err + } + m, err := oc.Serialize() + if err != nil { + return true, err + } + b, err := json.Marshal(m) + if err != nil { + return true, err + } + w.Header().Set(contentTypeHeader, responseContentTypeHeader) + w.WriteHeader(http.StatusOK) + n, err := w.Write(b) + if err != nil { + return true, err + } else if n != len(b) { + return true, fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(b)) + } + return true, nil + } } -func (c *Client) Delete() error { - // TODO: Enforce object - // TODO: Implement - return nil +func (f *federator) PostOutbox(id string) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) (bool, error) { + if !isActivityPubPost(r) { + return false, nil + } + if !f.EnableClient { + w.WriteHeader(http.StatusMethodNotAllowed) + return true, nil + } + // TODO: Implement + if f.EnableServer { + // TODO: Hook in delivery + } + return true, nil + } } -func (c *Client) Follow() error { - // TODO: Enforce object - // TODO: Implement - return nil +func (f *federator) GetOutbox(id string) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) (bool, error) { + if !isActivityPubGet(r) { + return false, nil + } + // TODO: Implement + return true, nil + } } -func (c *Client) Accept() error { - // TODO: Implement - return nil +func (f *federator) getPostInboxResolver(id string) *streams.Resolver { + return &streams.Resolver{ + CreateCallback: f.handleCreate(id), + UpdateCallback: f.handleUpdate(id), + DeleteCallback: f.handleDelete(id), + FollowCallback: f.handleFollow(id), + AcceptCallback: f.handleAccept(id), + RejectCallback: f.handleReject(id), + AddCallback: f.handleAdd(id), + RemoveCallback: f.handleRemove(id), + LikeCallback: f.handleLike(id), + UndoCallback: f.handleUndo(id), + // TODO: Extended activity types, such as Announce, Arrive, etc. + } } -func (c *Client) Reject() error { - // TODO: Implement - return nil +func (f *federator) handleCreate(id string) func(s *streams.Create) error { + return func(s *streams.Create) error { + return f.Storer.Create(id, s) + } } -func (c *Client) Add() error { - // TODO: Enforce object & target - // TODO: Implement - return nil +func (f *federator) handleUpdate(id string) func(s *streams.Update) error { + return func(s *streams.Update) error { + // TODO: The receiving server MUST take care to be sure that the Update + // is authorized to modify its object. + return f.Storer.Update(id, s) + } } -func (c *Client) Remove() error { - // TODO: Enforce object & target - // TODO: Implement - return nil +func (f *federator) handleDelete(id string) func(s *streams.Delete) error { + return func(s *streams.Delete) error { + // TODO: Verify ownership. I think the spec unintentionally suggests to + // just assume it is owned, so we will actually verify. + return f.Storer.Delete(id, s) + } } -func (c *Client) Like() error { - // TODO: Enforce object - // TODO: Implement - return nil +func (f *federator) handleFollow(id string) func(s *streams.Follow) error { + return func(s *streams.Follow) error { + return f.Storer.Follow(id, s) + } } -func (c *Client) Undo() error { - // TODO: Enforce object - // TODO: Implement - return nil +func (f *federator) handleAccept(id string) func(s *streams.Accept) error { + return func(s *streams.Accept) error { + return f.Storer.Accept(id, s) + } +} + +func (f *federator) handleReject(id string) func(s *streams.Reject) error { + return func(s *streams.Reject) error { + return f.Storer.Reject(id, s) + } +} + +func (f *federator) handleAdd(id string) func(s *streams.Add) error { + return func(s *streams.Add) error { + return f.Storer.Add(id, s) + } +} + +func (f *federator) handleRemove(id string) func(s *streams.Remove) error { + return func(s *streams.Remove) error { + return f.Storer.Remove(id, s) + } +} + +func (f *federator) handleLike(id string) func(s *streams.Like) error { + return func(s *streams.Like) error { + return f.Storer.Like(id, s) + } +} + +func (f *federator) handleUndo(id string) func(s *streams.Undo) error { + return func(s *streams.Undo) error { + return f.Storer.Undo(id, s) + } } diff --git a/pub/interfaces.go b/pub/interfaces.go index 97654b0..b642578 100644 --- a/pub/interfaces.go +++ b/pub/interfaces.go @@ -2,9 +2,17 @@ package pub import ( "github.com/go-fed/activity/vocab" + "net/http" "net/url" ) +// HandlerFunc returns true if it was able to handle the request as an +// ActivityPub request. If it handled the request then the error should be +// checked. The response will have already been written to when handled. Client +// applications can freely choose how to handle the request if this function +// does not handle it. +type HandlerFunc func(http.ResponseWriter, *http.Request) (bool, error) + // ActorObject is an object that has "actor" or "attributedTo" properties, // representing the author or originator of the object. type ActorObject interface { diff --git a/pub/internal.go b/pub/internal.go index 014dc06..f792b09 100644 --- a/pub/internal.go +++ b/pub/internal.go @@ -13,13 +13,14 @@ import ( ) const ( - postContentTypeHeader = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - getAcceptHeader = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - contentTypeHeader = "Content-Type" - acceptHeader = "Accept" - publicActivityPub = "https://www.w3.org/ns/activitystreams#Public" - publicJsonLD = "Public" - publicJsonLDAS = "as:Public" + postContentTypeHeader = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + responseContentTypeHeader = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + getAcceptHeader = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + contentTypeHeader = "Content-Type" + acceptHeader = "Accept" + publicActivityPub = "https://www.w3.org/ns/activitystreams#Public" + publicJsonLD = "Public" + publicJsonLDAS = "as:Public" ) var alternatives = []string{"application/activity+json"} @@ -86,7 +87,7 @@ func dereference(c *http.Client, u url.URL, agent string) ([]byte, error) { // prepare takes a DeliverableObject and returns a list of the proper recipient // target URIs. Additionally, the DeliverableObject will have any hidden // hidden recipients ("bto" and "bcc") stripped from it. -func (c *Client) prepare(o DeliverableObject) ([]url.URL, error) { +func (c *federator) prepare(o DeliverableObject) ([]url.URL, error) { // Get inboxes of recipients var r []url.URL r = append(r, getToIRIs(o)...) @@ -94,7 +95,16 @@ func (c *Client) prepare(o DeliverableObject) ([]url.URL, error) { r = append(r, getCcIRIs(o)...) r = append(r, getBccIRIs(o)...) r = append(r, getAudienceIRIs(o)...) - // TODO: Handle public collection + // TODO: Support delivery to shared inbox + // 1. When an object is being delivered to the originating actor's + // followers, a server MAY reduce the number of receiving actors + // delivered to by identifying all followers which share the same + // sharedInbox who would otherwise be individual recipients and instead + // deliver objects to said sharedInbox. + // 2. If an object is addressed to the Public special collection, a + // server MAY deliver that object to all known sharedInbox endpoints on + // the network. + r = filterURLs(r, isPublic) receiverActors, err := c.resolveInboxes(r, 0, c.MaxDepth) if err != nil { return nil, err @@ -115,7 +125,7 @@ func (c *Client) prepare(o DeliverableObject) ([]url.URL, error) { // resolveInboxes takes a list of Actor id URIs and returns them as concrete // instances of ActorObject. It applies recursively when it encounters a target // that is a Collection or OrderedCollection. -func (c *Client) resolveInboxes(r []url.URL, depth int, max int) ([]ActorObject, error) { +func (c *federator) resolveInboxes(r []url.URL, depth int, max int) ([]ActorObject, error) { if depth >= max { return nil, nil } @@ -282,6 +292,62 @@ func dedupeIRIs(recipients, ignored []url.URL) (out []url.URL) { return } +// dedupeOrderedItems will deduplicate the 'orderedItems' within an ordered +// collection type. Deduplication happens by simply examining the 'id'. +func (f *federator) dedupeOrderedItems(oc vocab.OrderedCollectionType) (vocab.OrderedCollectionType, error) { + i := 0 + seen := make(map[string]bool, oc.OrderedItemsLen()) + for i < oc.OrderedItemsLen() { + var id string + var removeFn func(int) + if oc.IsOrderedItemsObject(i) { + removeFn = oc.RemoveOrderedItemsObject + iri := oc.GetOrderedItemsObject(i).GetId() + pIri := &iri + id = pIri.String() + } else if oc.IsOrderedItemsLink(i) { + removeFn = oc.RemoveOrderedItemsLink + iri := oc.GetOrderedItemsLink(i).GetId() + pIri := &iri + id = pIri.String() + } else if oc.IsOrderedItemsIRI(i) { + removeFn = oc.RemoveOrderedItemsIRI + b, err := dereference(f.Client, oc.GetOrderedItemsIRI(i), f.Agent) + var m map[string]interface{} + if err := json.Unmarshal(b, &m); err != nil { + return oc, err + } + var iri url.URL + var hasIri bool + if err = toIdResolver(&hasIri, &iri).Deserialize(m); err != nil { + return oc, err + } + pIri := &iri + id = pIri.String() + } + if seen[id] { + removeFn(i) + } else { + seen[id] = true + i++ + } + } + return oc, nil +} + +// filterURLs removes urls whose strings match the provided filter +func filterURLs(u []url.URL, fn func(s string) bool) []url.URL { + i := 0 + for i < len(u) { + if fn(u[i].String()) { + u = append(u[:i], u[i+1:]...) + } else { + i++ + } + } + return u +} + func getToIRIs(o DeliverableObject) []url.URL { var r []url.URL for i := 0; i < o.ToLen(); i++ {