1275 行
38 KiB
Go
1275 行
38 KiB
Go
package pub
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/go-fed/activity/streams"
|
|
"github.com/go-fed/activity/vocab"
|
|
"github.com/go-fed/httpsig"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
)
|
|
|
|
var (
|
|
// errObjectRequired means the activity needs its object property set.
|
|
errObjectRequired = errors.New("object property required")
|
|
// errTargetRequired means the activity needs its target property set.
|
|
errTargetRequired = errors.New("target property required")
|
|
)
|
|
|
|
// TODO: Helper for sending arbitrary ActivityPub objects.
|
|
|
|
// Pubber provides methods for interacting with ActivityPub clients and
|
|
// ActivityPub federating servers.
|
|
type Pubber interface {
|
|
// PostInbox returns true if the request was handled as an ActivityPub
|
|
// POST to an actor's inbox. If false, the request was not an
|
|
// ActivityPub request.
|
|
//
|
|
// If the error is nil, then the ResponseWriter's headers and response
|
|
// has already been written. If a non-nil error is returned, then no
|
|
// response has been written.
|
|
PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
|
|
// GetInbox returns true if the request was handled as an ActivityPub
|
|
// GET to an actor's inbox. If false, the request was not an ActivityPub
|
|
// request.
|
|
//
|
|
// If the error is nil, then the ResponseWriter's headers and response
|
|
// has already been written. If a non-nil error is returned, then no
|
|
// response has been written.
|
|
GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
|
|
// PostOutbox returns true if the request was handled as an ActivityPub
|
|
// POST to an actor's outbox. If false, the request was not an
|
|
// ActivityPub request.
|
|
//
|
|
// If the error is nil, then the ResponseWriter's headers and response
|
|
// has already been written. If a non-nil error is returned, then no
|
|
// response has been written.
|
|
PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
|
|
// GetOutbox returns true if the request was handled as an ActivityPub
|
|
// GET to an actor's outbox. If false, the request was not an
|
|
// ActivityPub request.
|
|
//
|
|
// If the error is nil, then the ResponseWriter's headers and response
|
|
// has already been written. If a non-nil error is returned, then no
|
|
// response has been written.
|
|
GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
|
|
}
|
|
|
|
// NewSocialPubber provides a Pubber that implements only the Social API in
|
|
// ActivityPub.
|
|
func NewSocialPubber(clock Clock, app SocialApplication, cb Callbacker) Pubber {
|
|
return &federator{
|
|
Clock: clock,
|
|
App: app,
|
|
SocialAPI: app,
|
|
ClientCallbacker: cb,
|
|
EnableClient: true,
|
|
}
|
|
}
|
|
|
|
// NewFederatingPubber provides a Pubber that implements only the Federating API
|
|
// in ActivityPub.
|
|
func NewFederatingPubber(clock Clock, app FederateApplication, cb Callbacker, d Deliverer, client HttpClient, userAgent string, maxDeliveryDepth, maxForwardingDepth int) Pubber {
|
|
return &federator{
|
|
Clock: clock,
|
|
App: app,
|
|
FederateAPI: app,
|
|
ServerCallbacker: cb,
|
|
Client: client,
|
|
Agent: userAgent,
|
|
MaxDeliveryDepth: maxDeliveryDepth,
|
|
MaxInboxForwardingDepth: maxForwardingDepth,
|
|
EnableServer: true,
|
|
deliverer: d,
|
|
}
|
|
}
|
|
|
|
// NewPubber provides a Pubber that implements both the Social API and the
|
|
// Federating API in ActivityPub.
|
|
func NewPubber(clock Clock, app SocialFederateApplication, client, server Callbacker, d Deliverer, httpClient HttpClient, userAgent string, maxDeliveryDepth, maxForwardingDepth int) Pubber {
|
|
return &federator{
|
|
Clock: clock,
|
|
App: app,
|
|
SocialAPI: app,
|
|
FederateAPI: app,
|
|
Client: httpClient,
|
|
Agent: userAgent,
|
|
MaxDeliveryDepth: maxDeliveryDepth,
|
|
MaxInboxForwardingDepth: maxForwardingDepth,
|
|
ServerCallbacker: server,
|
|
ClientCallbacker: client,
|
|
EnableClient: true,
|
|
EnableServer: true,
|
|
deliverer: d,
|
|
}
|
|
}
|
|
|
|
type federator struct {
|
|
// 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
|
|
// Clock determines the time of this federator.
|
|
Clock Clock
|
|
// App is the client application that is ActivityPub aware.
|
|
//
|
|
// It is always required.
|
|
App Application
|
|
// FederateAPI provides utility when handling incoming messages received
|
|
// via the Federated Protocol, or server-to-server communications.
|
|
//
|
|
// It is only required if EnableServer is true.
|
|
FederateAPI FederateAPI
|
|
// SocialAPI provides utility when handling incoming messages
|
|
// received via the Social API, or client-to-server communications.
|
|
//
|
|
// It is only required if EnableClient is true.
|
|
SocialAPI SocialAPI
|
|
// ClientCallbacker for handing Social API messages.
|
|
ClientCallbacker Callbacker
|
|
// ServerCallbacker for handling Federated API messages.
|
|
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
|
|
// MaxInboxForwardingDepth is how deep the values are examined for
|
|
// determining ownership of whether to forward an Activity to
|
|
// collections or followers. Once this maximum is exceeded, the ghost
|
|
// replies issue may become a problem, but users may not mind.
|
|
//
|
|
// It is only required if EnableServer is true.
|
|
MaxInboxForwardingDepth int
|
|
// deliverer handles deliveries to other federated servers.
|
|
//
|
|
// It is only required if EnableServer is true.
|
|
deliverer Deliverer
|
|
}
|
|
|
|
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.FederateAPI.Unblocked(c, iris); err != nil {
|
|
return true, err
|
|
}
|
|
if err := f.addToInboxIfNew(c, r, m, func() error {
|
|
if err = f.getPostInboxResolver(c, r.URL).Deserialize(m); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
if err == errObjectRequired || err == errTargetRequired {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return true, nil
|
|
} else {
|
|
return true, err
|
|
}
|
|
}
|
|
if err := f.inboxForwarding(c, m); err != nil {
|
|
return true, err
|
|
}
|
|
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, Read)
|
|
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
|
|
}
|
|
addJSONLDContext(m)
|
|
b, err := json.Marshal(m)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
addResponseHeaders(w.Header(), f.Clock, b)
|
|
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
|
|
}
|
|
// By default, enforce HTTP Signatures.
|
|
authenticated := false
|
|
authorized := true
|
|
if verifier := f.SocialAPI.GetSocialAPIVerifier(c); verifier != nil {
|
|
var err error
|
|
// Use custom Social API method to authenticate and authorize.
|
|
authenticated, authorized, err = verifier.VerifyForOutbox(r, r.URL)
|
|
if err != nil {
|
|
return true, err
|
|
} else if authenticated && !authorized {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return true, nil
|
|
} else if !authenticated && !authorized {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return true, nil
|
|
}
|
|
}
|
|
if !authenticated && authorized {
|
|
// Use HTTP Signatures to authenticate and authorize.
|
|
v, err := httpsig.NewVerifier(r)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return true, nil
|
|
}
|
|
pk, algo, err := f.SocialAPI.GetPublicKeyForOutbox(c, v.KeyId(), r.URL)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
err = v.Verify(pk, algo)
|
|
if err != nil {
|
|
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 !vocab.IsActivityType(typer) {
|
|
actorIri, err := f.SocialAPI.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, err = f.wrapInCreate(obj, actorIri)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
}
|
|
activity, ok := typer.(vocab.ActivityType)
|
|
if !ok {
|
|
return true, fmt.Errorf("assigning new ids: cannot convert to vocab.ActivityType: %T", typer)
|
|
}
|
|
f.addNewIds(c, activity)
|
|
if m, err = activity.Serialize(); err != nil {
|
|
return true, err
|
|
}
|
|
deliverable := false
|
|
if err = f.getPostOutboxResolver(c, m, &deliverable, &m, r.URL).Deserialize(m); err != nil {
|
|
if err == errObjectRequired || err == errTargetRequired {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return true, nil
|
|
}
|
|
return true, err
|
|
}
|
|
if err = f.addToOutbox(c, r, 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, r.URL); err != nil {
|
|
return true, err
|
|
}
|
|
}
|
|
w.Header().Set("Location", activity.GetId().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, Read)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
m, err := oc.Serialize()
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
addJSONLDContext(m)
|
|
b, err := json.Marshal(m)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
addResponseHeaders(w.Header(), f.Clock, b)
|
|
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) getPostOutboxResolver(c context.Context, rawJson map[string]interface{}, deliverable *bool, toAddToOutbox *map[string]interface{}, outboxURL *url.URL) *streams.Resolver {
|
|
return &streams.Resolver{
|
|
CreateCallback: f.handleClientCreate(c, deliverable, toAddToOutbox),
|
|
UpdateCallback: f.handleClientUpdate(c, rawJson, 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, outboxURL),
|
|
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, toAddToOutbox *map[string]interface{}) 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) {
|
|
return fmt.Errorf("unsupported: Create Activity with 'object' that is only an IRI")
|
|
}
|
|
obj = append(obj, c.GetObject(i))
|
|
}
|
|
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 {
|
|
var iri *url.URL
|
|
if vObj, ok := v.(vocab.ObjectType); ok {
|
|
if !vObj.HasId() {
|
|
return fmt.Errorf("create actor object missing id")
|
|
}
|
|
iri = vObj.GetId()
|
|
} else if vLink, ok := v.(vocab.LinkType); ok {
|
|
if !vLink.HasHref() {
|
|
return fmt.Errorf("create actor link missing href")
|
|
}
|
|
iri = vLink.GetHref()
|
|
} else if vIRI, ok := v.(*url.URL); ok {
|
|
iri = vIRI
|
|
}
|
|
obj[i].AppendAttributedToIRI(iri)
|
|
}
|
|
}
|
|
}
|
|
for _, attributedToMap := range objectAttributedToIds {
|
|
for k, v := range attributedToMap {
|
|
if _, ok := createActorIds[k]; !ok {
|
|
var iri *url.URL
|
|
if vObj, ok := v.(vocab.ObjectType); ok {
|
|
if !vObj.HasId() {
|
|
return fmt.Errorf("attributedTo object missing id")
|
|
}
|
|
iri = vObj.GetId()
|
|
} else if vLink, ok := v.(vocab.LinkType); ok {
|
|
if !vLink.HasHref() {
|
|
return fmt.Errorf("attributedTo link missing href")
|
|
}
|
|
iri = vLink.GetHref()
|
|
} else if vIRI, ok := v.(*url.URL); ok {
|
|
iri = vIRI
|
|
}
|
|
c.AppendActorIRI(iri)
|
|
}
|
|
}
|
|
}
|
|
// 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.
|
|
if err := f.sameRecipients(c); err != nil {
|
|
return err
|
|
}
|
|
// 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
|
|
}
|
|
}
|
|
// Persist the above changes in the outbox
|
|
var err error
|
|
*toAddToOutbox = make(map[string]interface{})
|
|
*toAddToOutbox, err = c.Serialize()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return f.ClientCallbacker.Create(ctx, s)
|
|
}
|
|
}
|
|
|
|
func (f *federator) handleClientUpdate(c context.Context, rawJson map[string]interface{}, deliverable *bool) func(s *streams.Update) error {
|
|
return func(s *streams.Update) error {
|
|
*deliverable = true
|
|
if s.LenObject() == 0 {
|
|
return errObjectRequired
|
|
}
|
|
// 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, ReadWrite)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m, err := pObj.Serialize()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !s.Raw().IsObject(idx) {
|
|
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
|
|
}
|
|
if rawUpdatedObject := getRawObject(rawJson, id.String()); rawUpdatedObject != nil {
|
|
recursivelyApplyDeletes(m, rawUpdatedObject)
|
|
}
|
|
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, ReadWrite)
|
|
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
|
|
}
|
|
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, outboxURL *url.URL) func(s *streams.Add) error {
|
|
return func(s *streams.Add) error {
|
|
*deliverable = true
|
|
if s.LenObject() == 0 {
|
|
return errObjectRequired
|
|
} else if s.LenTarget() == 0 {
|
|
return errTargetRequired
|
|
}
|
|
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.App.Owns(c, id) {
|
|
continue
|
|
}
|
|
target, err := f.App.Get(c, id, ReadWrite)
|
|
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++ {
|
|
var obj vocab.ObjectType
|
|
var objId *url.URL
|
|
if raw.IsObjectIRI(i) {
|
|
objId = raw.GetObjectIRI(i)
|
|
if f.App.Owns(c, objId) {
|
|
pObj, err := f.App.Get(c, objId, Read)
|
|
var ok bool
|
|
if obj, ok = pObj.(vocab.ObjectType); !ok {
|
|
return fmt.Errorf("add object must be an activitypub object: %v", raw)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
obj, err = f.dereferenceAsUser(outboxURL, objId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else if raw.IsObject(i) {
|
|
obj = raw.GetObject(i)
|
|
if !obj.HasId() {
|
|
return fmt.Errorf("add object missing iri")
|
|
}
|
|
objId = obj.GetId()
|
|
} else {
|
|
return fmt.Errorf("add object must be of object or iri type: %v", raw)
|
|
}
|
|
for _, target := range targets {
|
|
if !f.App.CanAdd(c, obj, target) {
|
|
continue
|
|
}
|
|
if ct, ok := target.(vocab.CollectionType); ok {
|
|
ct.AppendItemsIRI(objId)
|
|
} else if oct, ok := target.(vocab.OrderedCollectionType); ok {
|
|
oct.AppendOrderedItemsIRI(objId)
|
|
}
|
|
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.LenTarget() == 0 {
|
|
return errTargetRequired
|
|
}
|
|
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.App.Owns(c, id) {
|
|
continue
|
|
}
|
|
target, err := f.App.Get(c, id, ReadWrite)
|
|
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.App.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
|
|
}
|
|
getter := func(actor vocab.ObjectType, lc *vocab.CollectionType, loc *vocab.OrderedCollectionType) (bool, error) {
|
|
if actor.IsLikedAnyURI() {
|
|
pObj, err := f.App.Get(ctx, actor.GetLikedAnyURI(), ReadWrite)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
ok := false
|
|
if *lc, ok = pObj.(vocab.CollectionType); !ok {
|
|
if *loc, ok = pObj.(vocab.OrderedCollectionType); !ok {
|
|
return true, fmt.Errorf("actors liked collection not CollectionType nor OrderedCollectionType")
|
|
}
|
|
}
|
|
return true, nil
|
|
} else if actor.IsLikedCollection() {
|
|
*lc = actor.GetLikedCollection()
|
|
return false, nil
|
|
} else if actor.IsLikedOrderedCollection() {
|
|
*loc = actor.GetLikedOrderedCollection()
|
|
return false, nil
|
|
}
|
|
*loc = &vocab.OrderedCollection{}
|
|
actor.SetLikedOrderedCollection(*loc)
|
|
return false, nil
|
|
}
|
|
if err := f.addAllObjectsToActorCollection(ctx, getter, s.Raw(), true); 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
|
|
}
|
|
raw := s.Raw()
|
|
if err := f.ensureActivityActorsMatchObjectActors(raw); err != nil {
|
|
return err
|
|
}
|
|
// 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, inboxURL *url.URL) *streams.Resolver {
|
|
return &streams.Resolver{
|
|
CreateCallback: f.handleCreate(c),
|
|
UpdateCallback: f.handleUpdate(c),
|
|
DeleteCallback: f.handleDelete(c),
|
|
FollowCallback: f.handleFollow(c, inboxURL),
|
|
AcceptCallback: f.handleAccept(c),
|
|
RejectCallback: f.handleReject(c),
|
|
AddCallback: f.handleAdd(c),
|
|
RemoveCallback: f.handleRemove(c),
|
|
LikeCallback: f.handleLike(c),
|
|
UndoCallback: f.handleUndo(c),
|
|
BlockCallback: f.handleBlock(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.
|
|
if s.LenObject() == 0 {
|
|
return errObjectRequired
|
|
}
|
|
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 {
|
|
if s.LenObject() == 0 {
|
|
return errObjectRequired
|
|
}
|
|
raw := s.Raw()
|
|
if err := f.ensureActivityOriginMatchesObjects(raw); err != nil {
|
|
return err
|
|
}
|
|
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 {
|
|
if s.LenObject() == 0 {
|
|
return errObjectRequired
|
|
}
|
|
raw := s.Raw()
|
|
if err := f.ensureActivityOriginMatchesObjects(raw); err != nil {
|
|
return err
|
|
}
|
|
ids, err := getObjectIds(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, ReadWrite)
|
|
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, inboxURL *url.URL) func(s *streams.Follow) error {
|
|
return func(s *streams.Follow) error {
|
|
// Permit either human-triggered or automatically triggering
|
|
// 'Accept'/'Reject'.
|
|
if s.LenObject() == 0 {
|
|
return errObjectRequired
|
|
}
|
|
todo := f.FederateAPI.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.AppendObject(raw)
|
|
for i := 0; i < raw.ActorLen(); i++ {
|
|
var to *url.URL
|
|
if raw.IsActorObject(i) {
|
|
obj := raw.GetActorObject(i)
|
|
if !obj.HasId() {
|
|
return fmt.Errorf("to object missing id")
|
|
}
|
|
to = obj.GetId()
|
|
} else if raw.IsActorLink(i) {
|
|
href := raw.GetActorLink(i)
|
|
if !href.HasHref() {
|
|
return fmt.Errorf("to link missing href")
|
|
}
|
|
to = href.GetHref()
|
|
} else if raw.IsActorIRI(i) {
|
|
to = raw.GetActorIRI(i)
|
|
}
|
|
activity.AppendToIRI(to)
|
|
}
|
|
ownsAny := false
|
|
if todo == AutomaticAccept {
|
|
getter := func(object vocab.ObjectType, lc *vocab.CollectionType, loc *vocab.OrderedCollectionType) (bool, error) {
|
|
if object.IsFollowersAnyURI() {
|
|
pObj, err := f.App.Get(c, object.GetFollowersAnyURI(), ReadWrite)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
ok := false
|
|
if *lc, ok = pObj.(vocab.CollectionType); !ok {
|
|
if *loc, ok = pObj.(vocab.OrderedCollectionType); !ok {
|
|
return true, fmt.Errorf("object followers collection not CollectionType nor OrderedCollectionType")
|
|
}
|
|
}
|
|
return true, nil
|
|
} else if object.IsFollowersCollection() {
|
|
*lc = object.GetFollowersCollection()
|
|
return false, nil
|
|
} else if object.IsFollowersOrderedCollection() {
|
|
*loc = object.GetFollowersOrderedCollection()
|
|
return false, nil
|
|
}
|
|
*loc = &vocab.OrderedCollection{}
|
|
object.SetFollowersOrderedCollection(*loc)
|
|
return false, nil
|
|
}
|
|
var err error
|
|
if ownsAny, err = f.addAllActorsToObjectCollection(c, getter, raw, true); err != nil {
|
|
return err
|
|
}
|
|
} else if todo == AutomaticReject {
|
|
var err error
|
|
ownsAny, err = f.ownsAnyObjects(c, raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if ownsAny {
|
|
if err := f.deliver(activity, inboxURL); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|
|
getter := func(actor vocab.ObjectType, lc *vocab.CollectionType, loc *vocab.OrderedCollectionType) (bool, error) {
|
|
if actor.IsFollowingAnyURI() {
|
|
pObj, err := f.App.Get(c, actor.GetFollowingAnyURI(), ReadWrite)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
ok := false
|
|
if *lc, ok = pObj.(vocab.CollectionType); !ok {
|
|
if *loc, ok = pObj.(vocab.OrderedCollectionType); !ok {
|
|
return true, fmt.Errorf("actors following collection not CollectionType nor OrderedCollectionType")
|
|
}
|
|
}
|
|
return true, nil
|
|
} else if actor.IsFollowingCollection() {
|
|
*lc = actor.GetFollowingCollection()
|
|
return false, nil
|
|
} else if actor.IsFollowingOrderedCollection() {
|
|
*loc = actor.GetFollowingOrderedCollection()
|
|
return false, nil
|
|
}
|
|
*loc = &vocab.OrderedCollection{}
|
|
actor.SetFollowingOrderedCollection(*loc)
|
|
return false, nil
|
|
}
|
|
if err := f.addAllObjectsToActorCollection(c, getter, follow, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
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.LenTarget() == 0 {
|
|
return errTargetRequired
|
|
}
|
|
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.App.Owns(c, id) {
|
|
continue
|
|
}
|
|
target, err := f.App.Get(c, id, ReadWrite)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ct, okCollection := target.(vocab.CollectionType)
|
|
oct, okOrdered := target.(vocab.OrderedCollectionType)
|
|
if okCollection {
|
|
targets = append(targets, ct)
|
|
} else if okOrdered {
|
|
targets = append(targets, oct)
|
|
}
|
|
// else ignore non-collection, let Application handle.
|
|
}
|
|
for i := 0; i < raw.ObjectLen(); i++ {
|
|
var obj vocab.ObjectType
|
|
var objId *url.URL
|
|
if raw.IsObject(i) {
|
|
obj = raw.GetObject(i)
|
|
if !obj.HasId() {
|
|
return fmt.Errorf("add object missing id")
|
|
}
|
|
objId = obj.GetId()
|
|
} else if raw.IsObjectIRI(i) {
|
|
// TODO: Fetch IRI
|
|
return fmt.Errorf("add object must not be IRI")
|
|
}
|
|
for _, target := range targets {
|
|
if !f.App.CanAdd(c, obj, target) {
|
|
continue
|
|
}
|
|
if ct, ok := target.(vocab.CollectionType); ok {
|
|
ct.AppendItemsIRI(objId)
|
|
} else if oct, ok := target.(vocab.OrderedCollectionType); ok {
|
|
oct.AppendOrderedItemsIRI(objId)
|
|
}
|
|
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.LenTarget() == 0 {
|
|
return errTargetRequired
|
|
}
|
|
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.App.Owns(c, id) {
|
|
continue
|
|
}
|
|
target, err := f.App.Get(c, id, ReadWrite)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ct, okCollection := target.(vocab.CollectionType)
|
|
oct, okOrdered := target.(vocab.OrderedCollectionType)
|
|
if okCollection {
|
|
targets = append(targets, ct)
|
|
} else if okOrdered {
|
|
targets = append(targets, oct)
|
|
}
|
|
// else ignore non-collection, let Application handle.
|
|
}
|
|
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.App.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 s.LenObject() == 0 {
|
|
return errObjectRequired
|
|
}
|
|
getter := func(object vocab.ObjectType, lc *vocab.CollectionType, loc *vocab.OrderedCollectionType) (bool, error) {
|
|
if object.IsLikesAnyURI() {
|
|
pObj, err := f.App.Get(c, object.GetLikesAnyURI(), ReadWrite)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
ok := false
|
|
if *lc, ok = pObj.(vocab.CollectionType); !ok {
|
|
if *loc, ok = pObj.(vocab.OrderedCollectionType); !ok {
|
|
return true, fmt.Errorf("object likes collection not CollectionType nor OrderedCollectionType")
|
|
}
|
|
}
|
|
return true, nil
|
|
} else if object.IsLikesCollection() {
|
|
*lc = object.GetLikesCollection()
|
|
return false, nil
|
|
} else if object.IsLikesOrderedCollection() {
|
|
*loc = object.GetLikesOrderedCollection()
|
|
return false, nil
|
|
}
|
|
*loc = &vocab.OrderedCollection{}
|
|
object.SetLikesOrderedCollection(*loc)
|
|
return false, nil
|
|
}
|
|
if _, err := f.addActivityToObjectCollection(c, getter, s.Raw(), true); 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.
|
|
// Here we enforce that the actors on the Undo must correspond
|
|
// to all objects' original actors in some manner.
|
|
if s.LenObject() == 0 {
|
|
return errObjectRequired
|
|
}
|
|
raw := s.Raw()
|
|
if err := f.ensureActivityActorsMatchObjectActors(raw); err != nil {
|
|
return err
|
|
}
|
|
return f.ServerCallbacker.Undo(c, s)
|
|
}
|
|
}
|
|
|
|
func (f *federator) handleBlock(c context.Context) func(s *streams.Block) error {
|
|
// Servers SHOULD NOT deliver Block Activities to their object. So in
|
|
// this case we will explicitly ignore it, but validate it as if we
|
|
// were to accept it.
|
|
return func(s *streams.Block) error {
|
|
if s.LenObject() == 0 {
|
|
return errObjectRequired
|
|
}
|
|
return nil
|
|
}
|
|
}
|