activity/pub/fed.go

1088 行
32 KiB
Go

package pub
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/vocab"
"golang.org/x/time/rate"
"io/ioutil"
"net/http"
"net/url"
"time"
)
var (
// ErrObjectRequired means the activity needs its object property set.
ErrObjectRequired = errors.New("object property required")
// ErrTypeRequired means the activity needs its type property set.
ErrTypeRequired = errors.New("type property required")
)
// TODO: Helper http Handler for serving ActivityStream objects
// TODO: Helper http Handler for serving Tombstone objects
// TODO: Helper http Handler for serving deleted objects
// TODO: Helper http Handler for serving actor's likes
// TODO: Helper http Handler for serving actor's followers
// TODO: Helper http Handler for serving actor's following
// TODO: Authorization client-to-server.
// TODO: Authenticate server-to-server deliveries.
// Pubber provides methods for interacting with ActivityPub clients and
// ActivityPub federating servers.
type Pubber interface {
Stop()
Errors() <-chan error
PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
// PostOutbox provides a HTTP handler for ActivityPub requests for the given id
// token. The client ID token is passed forwards to other interfaces for
// application specific behavior. The handler will return true if it handled
// the request as an ActivityPub request. If it returns an error, it is up to
// the client to determine how to respond via HTTP.
//
// Note that the error could be ErrObjectRequired or ErrTypeRequired.
PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
}
// NewPubber provides a Pubber that implements only the Social API in
// ActivityPub.
func NewSocialPubber(clock Clock, app Application, socialApp SocialApp, cb Callbacker) Pubber {
return &federator{
Clock: clock,
App: app,
SocialApp: socialApp,
ClientCallbacker: cb,
EnableClient: true,
}
}
// DeliveryOptions provides options when delivering messages to federated
// servers. All are required unless explicitly stated otherwise.
type DeliveryOptions struct {
// Client to use when sending http requests to federating peers.
Client HttpClient
// Agent string to send in http requests.
Agent string
// Maximum unnesting done when determining who to send messages to. Must
// be at least 1.
MaxDeliveryDepth int
// Initial amount of time to wait before retrying delivery.
InitialRetryTime time.Duration
// The longest amount of time to wait before retrying delivery.
MaximumRetryTime time.Duration
// Rate of backing off retries. Must be at least 1.
BackoffFactor float64
// Maximum number of retries to do when delivering a message. Must be at
// least 1.
MaxRetries int
// Global rate limiter across all deliveries, to prevent spamming
// outbound messages.
RateLimit *rate.Limiter
// Persister allows implementations to save messages that are enqueued
// for delivery between downtimes. It also permits metrics gathering and
// monitoring of outbound messages.
//
// This field is optional.
Persister DeliveryPersister
}
// NewPubber provides a Pubber that implements only the Federating API in
// ActivityPub.
func NewFederatingPubber(clock Clock, app Application, fApp FederateApp, cb Callbacker, d DeliveryOptions) Pubber {
return &federator{
Clock: clock,
App: app,
FederateApp: fApp,
ServerCallbacker: cb,
Client: d.Client,
Agent: d.Agent,
MaxDeliveryDepth: d.MaxDeliveryDepth,
EnableServer: true,
pool: newDelivererPool(d.InitialRetryTime, d.MaximumRetryTime, d.BackoffFactor, d.MaxRetries, d.RateLimit, d.Persister),
}
}
// NewPubber provides a Pubber that implements both the Social API and the
// Federating API in ActivityPub.
func NewPubber(clock Clock, app Application, sApp SocialApp, fApp FederateApp, client, server Callbacker, d DeliveryOptions) Pubber {
return &federator{
Clock: clock,
App: app,
SocialApp: sApp,
FederateApp: fApp,
Client: d.Client,
Agent: d.Agent,
MaxDeliveryDepth: d.MaxDeliveryDepth,
ServerCallbacker: server,
ClientCallbacker: client,
EnableClient: true,
EnableServer: true,
pool: newDelivererPool(d.InitialRetryTime, d.MaximumRetryTime, d.BackoffFactor, d.MaxRetries, d.RateLimit, d.Persister),
}
}
type federator struct {
// Clock determines the time of this federator.
Clock Clock
// App is the client application that is ActivityPub aware.
//
// It is always required.
App Application
// FederateApp provides utility when handling incoming messages received
// via the Federated Protocol, or server-to-server communications.
//
// It is only required if EnableServer is true.
FederateApp FederateApp
// SocialApp provides utility when handling incoming messages
// received via the Social API, or client-to-server communications.
//
// It is only required if EnableClient is true.
SocialApp SocialApp
ClientCallbacker Callbacker
ServerCallbacker Callbacker
// Client is used to federate with other ActivityPub servers.
//
// It is only required if EnableServer is true.
Client HttpClient
// Agent is the User-Agent string to use in HTTP headers when
// federating with another server. It will automatically be appended
// with '(go-fed ActivityPub)'.
//
// It is only required if EnableServer is true.
Agent string
// MaxDeliveryDepth is how deep collections of recipients will be
// expanded for delivery. It must be at least 1 to be compliant with the
// ActivityPub spec.
//
// It is only required if EnableServer is true.
MaxDeliveryDepth 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
// pool properly handles rate-limiting and retrying deliveries to other
// federated servers.
//
// It is only required if EnableServer is true.
pool *delivererPool
}
func (f *federator) Stop() {
if f.pool != nil {
f.pool.Stop()
}
}
func (f *federator) Errors() <-chan error {
if f.pool != nil {
return f.pool.Errors()
}
return nil
}
func (f *federator) PostInbox(c context.Context, 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
}
ao, err := getActorObject(m)
if err != nil {
return true, err
}
var iris []url.URL
for i := 0; i < ao.ActorLen(); i++ {
if ao.IsActorObject(i) {
obj := ao.GetActorObject(i)
if obj.HasId() {
iris = append(iris, obj.GetId())
}
} else if ao.IsActorLink(i) {
l := ao.GetActorLink(i)
if l.HasHref() {
iris = append(iris, l.GetHref())
}
} else if ao.IsActorIRI(i) {
iris = append(iris, ao.GetActorIRI(i))
}
}
if err = f.FederateApp.Unblocked(c, iris); err != nil {
return true, err
}
if err = f.getPostInboxResolver(c).Deserialize(m); err != nil {
return true, err
}
// TODO: 7.1.2 Inbox forwarding
w.WriteHeader(http.StatusOK)
return true, nil
}
func (f *federator) GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
if !isActivityPubGet(r) {
return false, nil
}
oc, err := f.App.GetInbox(c, 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 (f *federator) PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
if !isActivityPubPost(r) {
return false, nil
}
if !f.EnableClient {
w.WriteHeader(http.StatusMethodNotAllowed)
return true, nil
}
ok, err := f.App.PostOutboxAuthorized(c, r)
if err != nil {
return true, err
}
if !ok {
w.WriteHeader(http.StatusForbidden)
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
}
typer, err := toTypeIder(m)
if err != nil {
return true, err
}
if !isActivityType(typer) {
actorIri, err := f.App.ActorIRI(c, r)
if err != nil {
return true, err
}
obj, ok := typer.(vocab.ObjectType)
if !ok {
return true, fmt.Errorf("wrap in create: cannot convert to vocab.ObjectType: %T", typer)
}
typer = f.wrapInCreate(obj, actorIri)
}
newId := f.App.NewId(c, typer)
typer.SetId(newId)
if m, err = typer.Serialize(); err != nil {
return true, err
}
if err := f.addToOutbox(c, r, m); err != nil {
return true, err
}
deliverable := false
if err = f.getPostOutboxResolver(c, &deliverable).Deserialize(m); err != nil {
return true, err
}
if f.EnableServer && deliverable {
obj, err := toAnyActivity(m)
if err != nil {
return true, err
}
if err := f.deliver(obj); err != nil {
return true, err
}
}
w.Header().Set("Location", newId.String())
w.WriteHeader(http.StatusCreated)
return true, nil
}
func (f *federator) GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
if !isActivityPubGet(r) {
return false, nil
}
oc, err := f.App.GetOutbox(c, r)
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 (f *federator) addToOutbox(c context.Context, r *http.Request, m map[string]interface{}) error {
outbox, err := f.App.GetOutbox(c, r)
if err != nil {
return err
}
activity, err := toAnyActivity(m)
if err != nil {
return err
}
outbox.AddOrderedItemsObject(activity)
return f.App.Set(c, outbox)
}
func (f *federator) getPostOutboxResolver(c context.Context, deliverable *bool) *streams.Resolver {
return &streams.Resolver{
CreateCallback: f.handleClientCreate(c, deliverable),
UpdateCallback: f.handleClientUpdate(c, deliverable),
DeleteCallback: f.handleClientDelete(c, deliverable),
FollowCallback: f.handleClientFollow(c, deliverable),
AcceptCallback: f.handleClientAccept(c, deliverable),
RejectCallback: f.handleClientReject(c, deliverable),
AddCallback: f.handleClientAdd(c, deliverable),
RemoveCallback: f.handleClientRemove(c, deliverable),
LikeCallback: f.handleClientLike(c, deliverable),
UndoCallback: f.handleClientUndo(c, deliverable),
BlockCallback: f.handleClientBlock(c, deliverable),
// TODO: Extended activity types, such as Announce, Arrive, etc.
}
}
func (f *federator) handleClientCreate(ctx context.Context, deliverable *bool) func(s *streams.Create) error {
return func(s *streams.Create) error {
*deliverable = true
if s.LenObject() == 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
}
}
var obj []vocab.ObjectType
for i := 0; i < c.ObjectLen(); i++ {
if c.IsObject(i) {
obj = append(obj, c.GetObject(i))
} else if c.IsObjectIRI(i) {
// TODO: Fetch IRIs as well
return fmt.Errorf("unsupported: Create Activity with 'object' that is only an IRI")
}
}
objectAttributedToIds := make([]map[string]interface{}, len(obj))
for i := range objectAttributedToIds {
objectAttributedToIds[i] = make(map[string]interface{})
}
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 k, v := range createActorIds {
for i, attributedToMap := range objectAttributedToIds {
if _, ok := attributedToMap[k]; !ok {
if vObj, ok := v.(vocab.ObjectType); ok {
obj[i].AddAttributedToObject(vObj)
} else if vLink, ok := v.(vocab.LinkType); ok {
obj[i].AddAttributedToLink(vLink)
} else if vIRI, ok := v.(url.URL); ok {
obj[i].AddAttributedToIRI(vIRI)
}
}
}
}
// 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.
// Again, presumably if it does not already exist.
for _, attributedToMap := range objectAttributedToIds {
for k, v := range attributedToMap {
if _, ok := createActorIds[k]; !ok {
if vObj, ok := v.(vocab.ObjectType); ok {
c.AddActorObject(vObj)
} else if vLink, ok := v.(vocab.LinkType); ok {
c.AddActorLink(vLink)
} else if vIRI, ok := v.(url.URL); ok {
c.AddActorIRI(vIRI)
}
}
}
}
// 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 {
return err
}
}
return f.ClientCallbacker.Create(ctx, s)
}
}
func (f *federator) handleClientUpdate(c context.Context, deliverable *bool) func(s *streams.Update) error {
return func(s *streams.Update) error {
*deliverable = true
if s.LenObject() == 0 {
return ErrObjectRequired
}
// TODO: Support redactions via the '"json": null' value.
// Update should partially replace the 'object' with only the
// changed top-level fields.
ids, err := getObjectIds(s.Raw())
if err != nil {
return err
} else if len(ids) == 0 {
return fmt.Errorf("update has no id: %v", s)
}
for idx, id := range ids {
pObj, err := f.App.Get(c, id)
if err != nil {
return err
}
obj, ok := pObj.(vocab.Serializer)
if !ok {
return fmt.Errorf("PubObject is not vocab.Serializer: %T", pObj)
}
m, err := obj.Serialize()
if err != nil {
return err
}
if !s.Raw().IsObject(idx) {
// TODO: Fetch IRIs as well
return fmt.Errorf("update requires object to be wholly provided at index %d", idx)
}
updated, err := s.Raw().GetObject(idx).Serialize()
if err != nil {
return err
}
for k, v := range updated {
m[k] = v
}
p, err := ToPubObject(m)
if err != nil {
return err
}
for _, elem := range p {
if err := f.App.Set(c, elem); err != nil {
return err
}
}
}
return f.ClientCallbacker.Update(c, s)
}
}
func (f *federator) handleClientDelete(c context.Context, deliverable *bool) func(s *streams.Delete) error {
return func(s *streams.Delete) error {
*deliverable = true
if s.LenObject() == 0 {
return ErrObjectRequired
}
ids, err := getObjectIds(s.Raw())
if err != nil {
return err
} else if len(ids) == 0 {
return fmt.Errorf("delete has no id: %v", s)
}
for _, id := range ids {
pObj, err := f.App.Get(c, id)
if err != nil {
return err
}
obj, ok := pObj.(vocab.ObjectType)
if !ok {
return fmt.Errorf("cannot delete non-ObjectType: %T", pObj)
}
tomb := toTombstone(obj, id, f.Clock.Now())
if err := f.App.Set(c, tomb); err != nil {
return err
}
}
return f.ClientCallbacker.Delete(c, s)
}
}
func (f *federator) handleClientFollow(c context.Context, deliverable *bool) func(s *streams.Follow) error {
return func(s *streams.Follow) error {
*deliverable = true
if s.LenObject() == 0 {
return ErrObjectRequired
}
// Nothing extra to do.
return f.ClientCallbacker.Follow(c, s)
}
}
func (f *federator) handleClientAccept(c context.Context, deliverable *bool) func(s *streams.Accept) error {
return func(s *streams.Accept) error {
*deliverable = true
return f.ClientCallbacker.Accept(c, s)
}
}
func (f *federator) handleClientReject(c context.Context, deliverable *bool) func(s *streams.Reject) error {
return func(s *streams.Reject) error {
*deliverable = true
return f.ClientCallbacker.Reject(c, s)
}
}
func (f *federator) handleClientAdd(c context.Context, deliverable *bool) func(s *streams.Add) error {
return func(s *streams.Add) error {
*deliverable = true
if s.LenObject() == 0 {
return ErrObjectRequired
} else if s.LenType() == 0 {
return ErrTypeRequired
}
raw := s.Raw()
ids, err := getTargetIds(raw)
if err != nil {
return err
} else if len(ids) == 0 {
return fmt.Errorf("add target has no ids: %v", s)
}
objIds, err := getObjectIds(s.Raw())
if err != nil {
return err
} else if len(objIds) == 0 {
return fmt.Errorf("add object has no ids: %v", s)
}
var targets []vocab.ObjectType
for _, id := range ids {
if !f.SocialApp.Owns(c, id) {
continue
}
target, err := f.App.Get(c, id)
if err != nil {
return err
}
ct, okCollection := target.(vocab.CollectionType)
oct, okOrdered := target.(vocab.OrderedCollectionType)
if !okCollection && !okOrdered {
return fmt.Errorf("cannot add to type that is not Collection and not OrderedCollection: %v", target)
} else if okCollection {
targets = append(targets, ct)
} else {
targets = append(targets, oct)
}
}
for i := 0; i < raw.ObjectLen(); i++ {
if !raw.IsObject(i) {
// TODO: Fetch IRIs as well
return fmt.Errorf("add object must be object type: %v", raw)
}
obj := raw.GetObject(i)
for _, target := range targets {
if !f.SocialApp.CanAdd(c, obj, target) {
continue
}
if ct, ok := target.(vocab.CollectionType); ok {
ct.AddItemsObject(obj)
} else if oct, ok := target.(vocab.OrderedCollectionType); ok {
oct.AddOrderedItemsObject(obj)
}
if err := f.App.Set(c, target); err != nil {
return err
}
}
}
return f.ClientCallbacker.Add(c, s)
}
}
func (f *federator) handleClientRemove(c context.Context, deliverable *bool) func(s *streams.Remove) error {
return func(s *streams.Remove) error {
*deliverable = true
if s.LenObject() == 0 {
return ErrObjectRequired
} else if s.LenType() == 0 {
return ErrTypeRequired
}
raw := s.Raw()
ids, err := getTargetIds(raw)
if err != nil {
return err
} else if len(ids) == 0 {
return fmt.Errorf("remove target has no ids: %v", s)
}
objIds, err := getObjectIds(s.Raw())
if err != nil {
return err
} else if len(objIds) == 0 {
return fmt.Errorf("remove object has no ids: %v", s)
}
var targets []vocab.ObjectType
for _, id := range ids {
if !f.SocialApp.Owns(c, id) {
continue
}
target, err := f.App.Get(c, id)
if err != nil {
return err
}
ct, okCollection := target.(vocab.CollectionType)
oct, okOrdered := target.(vocab.OrderedCollectionType)
if !okCollection && !okOrdered {
return fmt.Errorf("cannot remove from type that is not Collection and not OrderedCollection: %v", target)
} else if okCollection {
targets = append(targets, ct)
} else {
targets = append(targets, oct)
}
}
for i := 0; i < raw.ObjectLen(); i++ {
if !raw.IsObject(i) {
// TODO: Fetch IRIs as well
return fmt.Errorf("remove object must be object type: %v", raw)
}
obj := raw.GetObject(i)
for _, target := range targets {
if !f.SocialApp.CanRemove(c, obj, target) {
continue
}
if ct, ok := target.(vocab.CollectionType); ok {
removeCollectionItemWithId(ct, obj.GetId())
} else if oct, ok := target.(vocab.OrderedCollectionType); ok {
removeOrderedCollectionItemWithId(oct, obj.GetId())
}
if err := f.App.Set(c, target); err != nil {
return err
}
}
}
return f.ClientCallbacker.Remove(c, s)
}
}
func (f *federator) handleClientLike(ctx context.Context, deliverable *bool) func(s *streams.Like) error {
return func(s *streams.Like) error {
*deliverable = true
if s.LenObject() == 0 {
return ErrObjectRequired
}
if err := f.addToAllActorLikedCollection(ctx, s.Raw()); err != nil {
return err
}
return f.ClientCallbacker.Like(ctx, s)
}
}
func (f *federator) handleClientUndo(c context.Context, deliverable *bool) func(s *streams.Undo) error {
return func(s *streams.Undo) error {
*deliverable = true
if s.LenObject() == 0 {
return ErrObjectRequired
}
// TODO: Determine if we can support common forms of undo natively.
return f.ClientCallbacker.Undo(c, s)
}
}
func (f *federator) handleClientBlock(c context.Context, deliverable *bool) func(s *streams.Block) error {
return func(s *streams.Block) error {
*deliverable = false
if s.LenObject() == 0 {
return ErrObjectRequired
}
return f.ClientCallbacker.Block(c, s)
}
}
func (f *federator) getPostInboxResolver(c context.Context) *streams.Resolver {
return &streams.Resolver{
CreateCallback: f.handleCreate(c),
UpdateCallback: f.handleUpdate(c),
DeleteCallback: f.handleDelete(c),
FollowCallback: f.handleFollow(c),
AcceptCallback: f.handleAccept(c),
RejectCallback: f.handleReject(c),
AddCallback: f.handleAdd(c),
RemoveCallback: f.handleRemove(c),
LikeCallback: f.handleLike(c),
UndoCallback: f.handleUndo(c),
// TODO: Extended activity types, such as Announce, Arrive, etc.
}
}
func (f *federator) handleCreate(c context.Context) func(s *streams.Create) error {
return func(s *streams.Create) error {
// Create requires the client application to persist the 'object' that
// was created.
raw := s.Raw()
for i := 0; i < raw.ObjectLen(); i++ {
if !raw.IsObject(i) {
// TODO: Fetch IRIs as well
return fmt.Errorf("create requires object to be wholly provided at index %d", i)
}
obj := raw.GetObject(i)
if err := f.App.Set(c, obj); err != nil {
return err
}
}
return f.ServerCallbacker.Create(c, s)
}
}
func (f *federator) handleUpdate(c context.Context) 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.
raw := s.Raw()
for i := 0; i < raw.ObjectLen(); i++ {
if !raw.IsObject(i) {
// TODO: Fetch IRIs as well
return fmt.Errorf("update requires object to be wholly provided at index %d", i)
}
obj := raw.GetObject(i)
if err := f.App.Set(c, obj); err != nil {
return err
}
}
return f.ServerCallbacker.Update(c, s)
}
}
func (f *federator) handleDelete(c context.Context) 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.
ids, err := getObjectIds(s.Raw())
if err != nil {
return err
} else if len(ids) == 0 {
return fmt.Errorf("delete has no id: %v", s)
}
for _, id := range ids {
pObj, err := f.App.Get(c, id)
if err != nil {
return err
}
obj, ok := pObj.(vocab.ObjectType)
if !ok {
return fmt.Errorf("cannot delete non-ObjectType: %T", pObj)
}
tomb := toTombstone(obj, id, f.Clock.Now())
if err := f.App.Set(c, tomb); err != nil {
return err
}
}
return f.ServerCallbacker.Delete(c, s)
}
}
func (f *federator) handleFollow(c context.Context) func(s *streams.Follow) error {
return func(s *streams.Follow) error {
// Permit either human-triggered or automatically triggering
// 'Accept'/'Reject'.
todo := f.FederateApp.OnFollow(c, s)
if todo != DoNothing {
var activity vocab.ActivityType
if todo == AutomaticAccept {
activity = &vocab.Accept{}
} else if todo == AutomaticReject {
activity = &vocab.Reject{}
}
raw := s.Raw()
activity.AddObject(raw)
for i := 0; i < raw.ActorLen(); i++ {
if raw.IsActorObject(i) {
activity.AddToObject(raw.GetActorObject(i))
} else if raw.IsActorLink(i) {
activity.AddToLink(raw.GetActorLink(i))
} else if raw.IsActorIRI(i) {
activity.AddToIRI(raw.GetActorIRI(i))
}
}
if err := f.deliver(activity); err != nil {
return err
}
if todo == AutomaticAccept {
// TODO: Add to followers collection.
}
}
return f.ServerCallbacker.Follow(c, s)
}
}
func (f *federator) handleAccept(c context.Context) func(s *streams.Accept) error {
return func(s *streams.Accept) error {
// Accept can be client application specific. However, if this 'Accept'
// is in response to a 'Follow' then the 'actor' should be added to the
// original 'actor's 'following' collection by the client application.
raw := s.Raw()
for i := 0; i < raw.ObjectLen(); i++ {
if raw.IsObject(i) {
obj := raw.GetObject(i)
follow, ok := obj.(vocab.FollowType)
if !ok {
continue
}
for j := 0; j < follow.ActorLen(); j++ {
var iri url.URL
if follow.IsActorObject(j) {
actor := follow.GetActorObject(j)
if !actor.HasId() {
return fmt.Errorf("actor object on follow must have id")
}
iri = actor.GetId()
} else if follow.IsActorLink(j) {
l := follow.GetActorLink(j)
if !l.HasHref() {
return fmt.Errorf("actor link on follow must have href")
}
iri = l.GetHref()
} else if follow.IsActorIRI(j) {
iri = follow.GetActorIRI(j)
}
following, err := f.FederateApp.GetFollowing(c, iri)
if err != nil {
return err
}
// TODO: Deduplication detection.
for k := 0; k < raw.ActorLen(); k++ {
if raw.IsActorObject(k) {
following.AddItemsObject(raw.GetActorObject(k))
} else if raw.IsActorLink(k) {
following.AddItemsLink(raw.GetActorLink(k))
} else if raw.IsActorIRI(k) {
following.AddItemsIRI(raw.GetActorIRI(k))
}
}
f.App.Set(c, following)
}
}
}
return f.ServerCallbacker.Accept(c, s)
}
}
func (f *federator) handleReject(c context.Context) func(s *streams.Reject) error {
return func(s *streams.Reject) 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 'actor' to the original 'actor's 'following' collection
// by the client application.
return f.ServerCallbacker.Reject(c, s)
}
}
func (f *federator) handleAdd(c context.Context) func(s *streams.Add) error {
return func(s *streams.Add) error {
// Add is client application specific, generally involving adding an
// 'object' to a specific 'target' collection.
if s.LenObject() == 0 {
return ErrObjectRequired
} else if s.LenType() == 0 {
return ErrTypeRequired
}
raw := s.Raw()
ids, err := getTargetIds(raw)
if err != nil {
return err
} else if len(ids) == 0 {
return fmt.Errorf("add target has no ids: %v", s)
}
objIds, err := getObjectIds(s.Raw())
if err != nil {
return err
} else if len(objIds) == 0 {
return fmt.Errorf("add object has no ids: %v", s)
}
var targets []vocab.ObjectType
for _, id := range ids {
if !f.FederateApp.Owns(c, id) {
continue
}
target, err := f.App.Get(c, id)
if err != nil {
return err
}
ct, okCollection := target.(vocab.CollectionType)
oct, okOrdered := target.(vocab.OrderedCollectionType)
if !okCollection && !okOrdered {
return fmt.Errorf("cannot add to type that is not Collection and not OrderedCollection: %v", target)
} else if okCollection {
targets = append(targets, ct)
} else {
targets = append(targets, oct)
}
}
for i := 0; i < raw.ObjectLen(); i++ {
if !raw.IsObject(i) {
// TODO: Fetch IRIs as well
return fmt.Errorf("add object must be object type: %v", raw)
}
obj := raw.GetObject(i)
for _, target := range targets {
if !f.FederateApp.CanAdd(c, obj, target) {
continue
}
if ct, ok := target.(vocab.CollectionType); ok {
ct.AddItemsObject(obj)
} else if oct, ok := target.(vocab.OrderedCollectionType); ok {
oct.AddOrderedItemsObject(obj)
}
if err := f.App.Set(c, target); err != nil {
return err
}
}
}
return f.ServerCallbacker.Add(c, s)
}
}
func (f *federator) handleRemove(c context.Context) func(s *streams.Remove) error {
return func(s *streams.Remove) error {
// Remove is client application specific, generally involving removing
// an 'object' from a specific 'target' collection.
if s.LenObject() == 0 {
return ErrObjectRequired
} else if s.LenType() == 0 {
return ErrTypeRequired
}
raw := s.Raw()
ids, err := getTargetIds(raw)
if err != nil {
return err
} else if len(ids) == 0 {
return fmt.Errorf("remove target has no ids: %v", s)
}
objIds, err := getObjectIds(s.Raw())
if err != nil {
return err
} else if len(objIds) == 0 {
return fmt.Errorf("remove object has no ids: %v", s)
}
var targets []vocab.ObjectType
for _, id := range ids {
if !f.FederateApp.Owns(c, id) {
continue
}
target, err := f.App.Get(c, id)
if err != nil {
return err
}
ct, okCollection := target.(vocab.CollectionType)
oct, okOrdered := target.(vocab.OrderedCollectionType)
if !okCollection && !okOrdered {
return fmt.Errorf("cannot remove from type that is not Collection and not OrderedCollection: %v", target)
} else if okCollection {
targets = append(targets, ct)
} else {
targets = append(targets, oct)
}
}
for i := 0; i < raw.ObjectLen(); i++ {
if !raw.IsObject(i) {
// TODO: Fetch IRIs as well
return fmt.Errorf("remove object must be object type: %v", raw)
}
obj := raw.GetObject(i)
for _, target := range targets {
if !f.FederateApp.CanRemove(c, obj, target) {
continue
}
if ct, ok := target.(vocab.CollectionType); ok {
removeCollectionItemWithId(ct, obj.GetId())
} else if oct, ok := target.(vocab.OrderedCollectionType); ok {
removeOrderedCollectionItemWithId(oct, obj.GetId())
}
if err := f.App.Set(c, target); err != nil {
return err
}
}
}
return f.ServerCallbacker.Remove(c, s)
}
}
func (f *federator) handleLike(c context.Context) func(s *streams.Like) error {
return func(s *streams.Like) error {
if err := f.addToAllLikesCollections(c, s.Raw()); err != nil {
return err
}
return f.ServerCallbacker.Like(c, s)
}
}
func (f *federator) handleUndo(c context.Context) func(s *streams.Undo) error {
return func(s *streams.Undo) 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. Note that 'Undo'-ing
// is not a deletion of a previous Activity, but the addition of its
// opposite.
return f.ServerCallbacker.Undo(c, s)
}
}