Outline support for inbox and outbox handlers and routing.
このコミットが含まれているのは:
コミット
3662ac5bf9
317
pub/fed.go
317
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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++ {
|
||||
|
|
読み込み中…
新しいイシューから参照