Grand test fixup (#138)

* start fixing up tests

* fix up tests + automate with drone

* fiddle with linting

* messing about with drone.yml

* some more fiddling

* hmmm

* add cache

* add vendor directory

* verbose

* ci updates

* update some little things

* update sig
This commit is contained in:
Tobi Smethurst 2021-08-12 21:03:24 +02:00 committed by GitHub
commit 98263a7de6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2677 changed files with 1090869 additions and 219 deletions

270
vendor/github.com/go-fed/activity/pub/README.md generated vendored Normal file
View file

@ -0,0 +1,270 @@
# pub
Implements the Social and Federating Protocols in the ActivityPub specification.
## Reference & Tutorial
The [go-fed website](https://go-fed.org/) contains tutorials and reference
materials, in addition to the rest of this README.
## How To Use
```
go get github.com/go-fed/activity
```
The root of all ActivityPub behavior is the `Actor`, which requires you to
implement a few interfaces:
```golang
import (
"github.com/go-fed/activity/pub"
)
type myActivityPubApp struct { /* ... */ }
type myAppsDatabase struct { /* ... */ }
type myAppsClock struct { /* ... */ }
var (
// Your app will implement pub.CommonBehavior, and either
// pub.SocialProtocol, pub.FederatingProtocol, or both.
myApp = &myActivityPubApp{}
myCommonBehavior pub.CommonBehavior = myApp
mySocialProtocol pub.SocialProtocol = myApp
myFederatingProtocol pub.FederatingProtocol = myApp
// Your app's database implementation.
myDatabase pub.Database = &myAppsDatabase{}
// Your app's clock.
myClock pub.Clock = &myAppsClock{}
)
// Only support the C2S Social protocol
actor := pub.NewSocialActor(
myCommonBehavior,
mySocialProtocol,
myDatabase,
myClock)
// OR
//
// Only support S2S Federating protocol
actor = pub.NewFederatingActor(
myCommonBehavior,
myFederatingProtocol,
myDatabase,
myClock)
// OR
//
// Support both C2S Social and S2S Federating protocol.
actor = pub.NewActor(
myCommonBehavior,
mySocialProtocol,
myFederatingProtocol,
myDatabase,
myClock)
```
Next, hook the `Actor` into your web server:
```golang
// The application's actor
var actor pub.Actor
var outboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
c := context.Background()
// Populate c with request-specific information
if handled, err := actor.PostOutbox(c, w, r); err != nil {
// Write to w
return
} else if handled {
return
} else if handled, err = actor.GetOutbox(c, w, r); err != nil {
// Write to w
return
} else if handled {
return
}
// else:
//
// Handle non-ActivityPub request, such as serving a webpage.
}
var inboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
c := context.Background()
// Populate c with request-specific information
if handled, err := actor.PostInbox(c, w, r); err != nil {
// Write to w
return
} else if handled {
return
} else if handled, err = actor.GetInbox(c, w, r); err != nil {
// Write to w
return
} else if handled {
return
}
// else:
//
// Handle non-ActivityPub request, such as serving a webpage.
}
// Add the handlers to a HTTP server
serveMux := http.NewServeMux()
serveMux.HandleFunc("/actor/outbox", outboxHandler)
serveMux.HandleFunc("/actor/inbox", inboxHandler)
var server http.Server
server.Handler = serveMux
```
To serve ActivityStreams data:
```golang
myHander := pub.NewActivityStreamsHandler(myDatabase, myClock)
var activityStreamsHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
c := context.Background()
// Populate c with request-specific information
if handled, err := myHandler(c, w, r); err != nil {
// Write to w
return
} else if handled {
return
}
// else:
//
// Handle non-ActivityPub request, such as serving a webpage.
}
serveMux.HandleFunc("/some/data/like/a/note", activityStreamsHandler)
```
### Dependency Injection
Package `pub` relies on dependency injection to provide out-of-the-box support
for ActivityPub. The interfaces to be satisfied are:
* `CommonBehavior` - Behavior needed regardless of which Protocol is used.
* `SocialProtocol` - Behavior needed for the Social Protocol.
* `FederatingProtocol` - Behavior needed for the Federating Protocol.
* `Database` - The data store abstraction, not tied to the `database/sql`
package.
* `Clock` - The server's internal clock.
* `Transport` - Responsible for the network that serves requests and deliveries
of ActivityStreams data. A `HttpSigTransport` type is provided.
These implementations form the core of an application's behavior without
worrying about the particulars and pitfalls of the ActivityPub protocol.
Implementing these interfaces gives you greater assurance about being
ActivityPub compliant.
### Application Logic
The `SocialProtocol` and `FederatingProtocol` are responsible for returning
callback functions compatible with `streams.TypeResolver`. They also return
`SocialWrappedCallbacks` and `FederatingWrappedCallbacks`, which are nothing
more than a bundle of default behaviors for types like `Create`, `Update`, and
so on.
Applications will want to focus on implementing their specific behaviors in the
callbacks, and have fine-grained control over customization:
```golang
// Implements the FederatingProtocol interface.
//
// This illustration can also be applied for the Social Protocol.
func (m *myAppsFederatingProtocol) Callbacks(c context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}) {
// The context 'c' has request-specific logic and can be used to apply complex
// logic building the right behaviors, if desired.
//
// 'c' will later be passed through to the callbacks created below.
wrapped = pub.FederatingWrappedCallbacks{
Create: func(ctx context.Context, create vocab.ActivityStreamsCreate) error {
// This function is wrapped by default behavior.
//
// More application specific logic can be written here.
//
// 'ctx' will have request-specific information from the HTTP handler. It
// is the same as the 'c' passed to the Callbacks method.
// 'create' has, at this point, already triggered the recommended
// ActivityPub side effect behavior. The application can process it
// further as needed.
return nil
},
}
// The 'other' must contain functions that satisfy the signature pattern
// required by streams.JSONResolver.
//
// If they are not, at runtime errors will be returned to indicate this.
other = []interface{}{
// The FederatingWrappedCallbacks has default behavior for an "Update" type,
// but since we are providing this behavior in "other" and not in the
// FederatingWrappedCallbacks.Update member, we will entirely replace the
// default behavior provided by go-fed. Be careful that this still
// implements ActivityPub properly.
func(ctx context.Context, update vocab.ActivityStreamsUpdate) error {
// This function is NOT wrapped by default behavior.
//
// Application specific logic can be written here.
//
// 'ctx' will have request-specific information from the HTTP handler. It
// is the same as the 'c' passed to the Callbacks method.
// 'update' will NOT trigger the recommended ActivityPub side effect
// behavior. The application should do so in addition to any other custom
// side effects required.
return nil
},
// The "Listen" type has no default suggested behavior in ActivityPub, so
// this just makes this application able to handle "Listen" activities.
func(ctx context.Context, listen vocab.ActivityStreamsListen) error {
// This function is NOT wrapped by default behavior. There's not a
// FederatingWrappedCallbacks.Listen member to wrap.
//
// Application specific logic can be written here.
//
// 'ctx' will have request-specific information from the HTTP handler. It
// is the same as the 'c' passed to the Callbacks method.
// 'listen' can be processed with side effects as the application needs.
return nil
},
}
return
}
```
The `pub` package supports applications that grow into more custom solutions by
overriding the default behaviors as needed.
### ActivityStreams Extensions: Future-Proofing An Application
Package `pub` relies on the `streams.TypeResolver` and `streams.JSONResolver`
code generated types. As new ActivityStreams extensions are developed and their
code is generated, `pub` will automatically pick up support for these
extensions.
The steps to rapidly implement a new extension in a `pub` application are:
1. Generate an OWL definition of the ActivityStreams extension. This definition
could be the same one defining the vocabulary at the `@context` IRI.
2. Run `astool` to autogenerate the golang types in the `streams` package.
3. Implement the application's callbacks in the `FederatingProtocol.Callbacks`
or `SocialProtocol.Callbacks` for the new behaviors needed.
4. Build the application, which builds `pub`, with the newly generated `streams`
code. No code changes in `pub` are required.
Whether an author of an ActivityStreams extension or an application developer,
these quick steps should reduce the barrier to adopion in a statically-typed
environment.
### DelegateActor
For those that need a near-complete custom ActivityPub solution, or want to have
that possibility in the future after adopting go-fed, the `DelegateActor`
interface can be used to obtain an `Actor`:
```golang
// Use custom ActivityPub implementation
actor = pub.NewCustomActor(
myDelegateActor,
isSocialProtocolEnabled,
isFederatedProtocolEnabled,
myAppsClock)
```
It does not guarantee that an implementation adheres to the ActivityPub
specification. It acts as a stepping stone for applications that want to build
up to a fully custom solution and not be locked into the `pub` package
implementation.

49
vendor/github.com/go-fed/activity/pub/activity.go generated vendored Normal file
View file

@ -0,0 +1,49 @@
package pub
import (
"github.com/go-fed/activity/streams/vocab"
)
// Activity represents any ActivityStreams Activity type.
//
// The Activity types provided in the streams package implement this.
type Activity interface {
// Activity is also a vocab.Type
vocab.Type
// GetActivityStreamsActor returns the "actor" property if it exists, and
// nil otherwise.
GetActivityStreamsActor() vocab.ActivityStreamsActorProperty
// GetActivityStreamsAudience returns the "audience" property if it
// exists, and nil otherwise.
GetActivityStreamsAudience() vocab.ActivityStreamsAudienceProperty
// GetActivityStreamsBcc returns the "bcc" property if it exists, and nil
// otherwise.
GetActivityStreamsBcc() vocab.ActivityStreamsBccProperty
// GetActivityStreamsBto returns the "bto" property if it exists, and nil
// otherwise.
GetActivityStreamsBto() vocab.ActivityStreamsBtoProperty
// GetActivityStreamsCc returns the "cc" property if it exists, and nil
// otherwise.
GetActivityStreamsCc() vocab.ActivityStreamsCcProperty
// GetActivityStreamsTo returns the "to" property if it exists, and nil
// otherwise.
GetActivityStreamsTo() vocab.ActivityStreamsToProperty
// GetActivityStreamsAttributedTo returns the "attributedTo" property if
// it exists, and nil otherwise.
GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty
// GetActivityStreamsObject returns the "object" property if it exists,
// and nil otherwise.
GetActivityStreamsObject() vocab.ActivityStreamsObjectProperty
// SetActivityStreamsActor sets the "actor" property.
SetActivityStreamsActor(i vocab.ActivityStreamsActorProperty)
// SetActivityStreamsObject sets the "object" property.
SetActivityStreamsObject(i vocab.ActivityStreamsObjectProperty)
// SetActivityStreamsTo sets the "to" property.
SetActivityStreamsTo(i vocab.ActivityStreamsToProperty)
// SetActivityStreamsBto sets the "bto" property.
SetActivityStreamsBto(i vocab.ActivityStreamsBtoProperty)
// SetActivityStreamsBcc sets the "bcc" property.
SetActivityStreamsBcc(i vocab.ActivityStreamsBccProperty)
// SetActivityStreamsAttributedTo sets the "attributedTo" property.
SetActivityStreamsAttributedTo(i vocab.ActivityStreamsAttributedToProperty)
}

127
vendor/github.com/go-fed/activity/pub/actor.go generated vendored Normal file
View file

@ -0,0 +1,127 @@
package pub
import (
"context"
"github.com/go-fed/activity/streams/vocab"
"net/http"
"net/url"
)
// Actor represents ActivityPub's actor concept. It conceptually has an inbox
// and outbox that receives either a POST or GET request, which triggers side
// effects in the federating application.
//
// An Actor within an application may federate server-to-server (Federation
// Protocol), client-to-server (Social API), or both. The Actor represents the
// server in either use case.
//
// An actor can be created by calling NewSocialActor (only the Social Protocol
// is supported), NewFederatingActor (only the Federating Protocol is
// supported), NewActor (both are supported), or NewCustomActor (neither are).
//
// Not all Actors have the same behaviors depending on the constructor used to
// create them. Refer to the constructor's documentation to determine the exact
// behavior of the Actor on an application.
//
// The behaviors documented here are common to all Actors returned by any
// constructor.
type Actor 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 and may still be handled by the caller in
// another way, such as serving a web page.
//
// 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.
//
// If the Actor was constructed with the Federated Protocol enabled,
// side effects will occur.
//
// If the Federated Protocol is not enabled, writes the
// http.StatusMethodNotAllowed status code in the response. No side
// effects occur.
//
// The request and data of your application will be interpreted as
// having an HTTPS protocol scheme.
PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
// PostInboxScheme is similar to PostInbox, except clients are able to
// specify which protocol scheme to handle the incoming request and the
// data stored within the application (HTTP, HTTPS, etc).
PostInboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (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 and may still be handled by the caller in another way, such
// as serving a web page.
//
// 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.
//
// If the request is an ActivityPub request, the Actor will defer to the
// application to determine the correct authorization of the request and
// the resulting OrderedCollection to respond with. The Actor handles
// serializing this OrderedCollection and responding with the correct
// headers and http.StatusOK.
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 and may still be handled by the caller in another
// way, such as serving a web page.
//
// 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.
//
// If the Actor was constructed with the Social Protocol enabled, side
// effects will occur.
//
// If the Social Protocol is not enabled, writes the
// http.StatusMethodNotAllowed status code in the response. No side
// effects occur.
//
// If the Social and Federated Protocol are both enabled, it will handle
// the side effects of receiving an ActivityStream Activity, and then
// federate the Activity to peers.
//
// The request will be interpreted as having an HTTPS scheme.
PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
// PostOutboxScheme is similar to PostOutbox, except clients are able to
// specify which protocol scheme to handle the incoming request and the
// data stored within the application (HTTP, HTTPS, etc).
PostOutboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (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.
//
// If the request is an ActivityPub request, the Actor will defer to the
// application to determine the correct authorization of the request and
// the resulting OrderedCollection to respond with. The Actor handles
// serializing this OrderedCollection and responding with the correct
// headers and http.StatusOK.
GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
}
// FederatingActor is an Actor that allows programmatically delivering an
// Activity to a federating peer.
type FederatingActor interface {
Actor
// Send a federated activity.
//
// The provided url must be the outbox of the sender. All processing of
// the activity occurs similarly to the C2S flow:
// - If t is not an Activity, it is wrapped in a Create activity.
// - A new ID is generated for the activity.
// - The activity is added to the specified outbox.
// - The activity is prepared and delivered to recipients.
//
// Note that this function will only behave as expected if the
// implementation has been constructed to support federation. This
// method will guaranteed work for non-custom Actors. For custom actors,
// care should be used to not call this method if only C2S is supported.
Send(c context.Context, outbox *url.URL, t vocab.Type) (Activity, error)
}

494
vendor/github.com/go-fed/activity/pub/base_actor.go generated vendored Normal file
View file

@ -0,0 +1,494 @@
package pub
import (
"context"
"encoding/json"
"fmt"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"io/ioutil"
"net/http"
"net/url"
)
// baseActor must satisfy the Actor interface.
var _ Actor = &baseActor{}
// baseActor is an application-independent ActivityPub implementation. It does
// not implement the entire protocol, and relies on a delegate to do so. It
// only implements the part of the protocol that is side-effect-free, allowing
// an existing application to write a DelegateActor that glues their application
// into the ActivityPub world.
//
// It is preferred to use a DelegateActor provided by this library, so that the
// application does not need to worry about the ActivityPub implementation.
type baseActor struct {
// delegate contains application-specific delegation logic.
delegate DelegateActor
// enableSocialProtocol enables or disables the Social API, the client to
// server part of ActivityPub. Useful if permitting remote clients to
// act on behalf of the users of the client application.
enableSocialProtocol bool
// enableFederatedProtocol enables or disables the Federated Protocol, or the
// server to server part of ActivityPub. Useful to permit integrating
// with the rest of the federative web.
enableFederatedProtocol bool
// clock simply tracks the current time.
clock Clock
}
// baseActorFederating must satisfy the FederatingActor interface.
var _ FederatingActor = &baseActorFederating{}
// baseActorFederating is a baseActor that also satisfies the FederatingActor
// interface.
//
// The baseActor is preserved as an Actor which will not successfully cast to a
// FederatingActor.
type baseActorFederating struct {
baseActor
}
// NewSocialActor builds a new Actor concept that handles only the Social
// Protocol part of ActivityPub.
//
// This Actor can be created once in an application and reused to handle
// multiple requests concurrently and for different endpoints.
//
// It leverages as much of go-fed as possible to ensure the implementation is
// compliant with the ActivityPub specification, while providing enough freedom
// to be productive without shooting one's self in the foot.
//
// Do not try to use NewSocialActor and NewFederatingActor together to cover
// both the Social and Federating parts of the protocol. Instead, use NewActor.
func NewSocialActor(c CommonBehavior,
c2s SocialProtocol,
db Database,
clock Clock) Actor {
return &baseActor{
delegate: &sideEffectActor{
common: c,
c2s: c2s,
db: db,
clock: clock,
},
enableSocialProtocol: true,
clock: clock,
}
}
// NewFederatingActor builds a new Actor concept that handles only the Federating
// Protocol part of ActivityPub.
//
// This Actor can be created once in an application and reused to handle
// multiple requests concurrently and for different endpoints.
//
// It leverages as much of go-fed as possible to ensure the implementation is
// compliant with the ActivityPub specification, while providing enough freedom
// to be productive without shooting one's self in the foot.
//
// Do not try to use NewSocialActor and NewFederatingActor together to cover
// both the Social and Federating parts of the protocol. Instead, use NewActor.
func NewFederatingActor(c CommonBehavior,
s2s FederatingProtocol,
db Database,
clock Clock) FederatingActor {
return &baseActorFederating{
baseActor{
delegate: &sideEffectActor{
common: c,
s2s: s2s,
db: db,
clock: clock,
},
enableFederatedProtocol: true,
clock: clock,
},
}
}
// NewActor builds a new Actor concept that handles both the Social and
// Federating Protocol parts of ActivityPub.
//
// This Actor can be created once in an application and reused to handle
// multiple requests concurrently and for different endpoints.
//
// It leverages as much of go-fed as possible to ensure the implementation is
// compliant with the ActivityPub specification, while providing enough freedom
// to be productive without shooting one's self in the foot.
func NewActor(c CommonBehavior,
c2s SocialProtocol,
s2s FederatingProtocol,
db Database,
clock Clock) FederatingActor {
return &baseActorFederating{
baseActor{
delegate: &sideEffectActor{
common: c,
c2s: c2s,
s2s: s2s,
db: db,
clock: clock,
},
enableSocialProtocol: true,
enableFederatedProtocol: true,
clock: clock,
},
}
}
// NewCustomActor allows clients to create a custom ActivityPub implementation
// for the Social Protocol, Federating Protocol, or both.
//
// It still uses the library as a high-level scaffold, which has the benefit of
// allowing applications to grow into a custom ActivityPub solution without
// having to refactor the code that passes HTTP requests into the Actor.
//
// It is possible to create a DelegateActor that is not ActivityPub compliant.
// Use with due care.
func NewCustomActor(delegate DelegateActor,
enableSocialProtocol, enableFederatedProtocol bool,
clock Clock) FederatingActor {
return &baseActorFederating{
baseActor{
delegate: delegate,
enableSocialProtocol: enableSocialProtocol,
enableFederatedProtocol: enableFederatedProtocol,
clock: clock,
},
}
}
// PostInbox implements the generic algorithm for handling a POST request to an
// actor's inbox independent on an application. It relies on a delegate to
// implement application specific functionality.
//
// Only supports serving data with identifiers having the HTTPS scheme.
func (b *baseActor) PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
return b.PostInboxScheme(c, w, r, "https")
}
// PostInbox implements the generic algorithm for handling a POST request to an
// actor's inbox independent on an application. It relies on a delegate to
// implement application specific functionality.
//
// Specifying the "scheme" allows for retrieving ActivityStreams content with
// identifiers such as HTTP, HTTPS, or other protocol schemes.
func (b *baseActor) PostInboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) {
// Do nothing if it is not an ActivityPub POST request.
if !isActivityPubPost(r) {
return false, nil
}
// If the Federated Protocol is not enabled, then this endpoint is not
// enabled.
if !b.enableFederatedProtocol {
w.WriteHeader(http.StatusMethodNotAllowed)
return true, nil
}
// Check the peer request is authentic.
c, authenticated, err := b.delegate.AuthenticatePostInbox(c, w, r)
if err != nil {
return true, err
} else if !authenticated {
return true, nil
}
// Begin processing the request, but have not yet applied
// authorization (ex: blocks). Obtain the activity reject unknown
// activities.
raw, err := ioutil.ReadAll(r.Body)
if err != nil {
return true, err
}
var m map[string]interface{}
if err = json.Unmarshal(raw, &m); err != nil {
return true, err
}
asValue, err := streams.ToType(c, m)
if err != nil && !streams.IsUnmatchedErr(err) {
return true, err
} else if streams.IsUnmatchedErr(err) {
// Respond with bad request -- we do not understand the type.
w.WriteHeader(http.StatusBadRequest)
return true, nil
}
activity, ok := asValue.(Activity)
if !ok {
return true, fmt.Errorf("activity streams value is not an Activity: %T", asValue)
}
if activity.GetJSONLDId() == nil {
w.WriteHeader(http.StatusBadRequest)
return true, nil
}
// Allow server implementations to set context data with a hook.
c, err = b.delegate.PostInboxRequestBodyHook(c, r, activity)
if err != nil {
return true, err
}
// Check authorization of the activity.
authorized, err := b.delegate.AuthorizePostInbox(c, w, activity)
if err != nil {
return true, err
} else if !authorized {
return true, nil
}
// Post the activity to the actor's inbox and trigger side effects for
// that particular Activity type. It is up to the delegate to resolve
// the given map.
inboxId := requestId(r, scheme)
err = b.delegate.PostInbox(c, inboxId, activity)
if err != nil {
// Special case: We know it is a bad request if the object or
// target properties needed to be populated, but weren't.
//
// Send the rejection to the peer.
if err == ErrObjectRequired || err == ErrTargetRequired {
w.WriteHeader(http.StatusBadRequest)
return true, nil
}
return true, err
}
// Our side effects are complete, now delegate determining whether to
// do inbox forwarding, as well as the action to do it.
if err := b.delegate.InboxForwarding(c, inboxId, activity); err != nil {
return true, err
}
// Request has been processed. Begin responding to the request.
//
// Simply respond with an OK status to the peer.
w.WriteHeader(http.StatusOK)
return true, nil
}
// GetInbox implements the generic algorithm for handling a GET request to an
// actor's inbox independent on an application. It relies on a delegate to
// implement application specific functionality.
func (b *baseActor) GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
// Do nothing if it is not an ActivityPub GET request.
if !isActivityPubGet(r) {
return false, nil
}
// Delegate authenticating and authorizing the request.
c, authenticated, err := b.delegate.AuthenticateGetInbox(c, w, r)
if err != nil {
return true, err
} else if !authenticated {
return true, nil
}
// Everything is good to begin processing the request.
oc, err := b.delegate.GetInbox(c, r)
if err != nil {
return true, err
}
// Deduplicate the 'orderedItems' property by ID.
err = dedupeOrderedItems(oc)
if err != nil {
return true, err
}
// Request has been processed. Begin responding to the request.
//
// Serialize the OrderedCollection.
m, err := streams.Serialize(oc)
if err != nil {
return true, err
}
raw, err := json.Marshal(m)
if err != nil {
return true, err
}
// Write the response.
addResponseHeaders(w.Header(), b.clock, raw)
w.WriteHeader(http.StatusOK)
n, err := w.Write(raw)
if err != nil {
return true, err
} else if n != len(raw) {
return true, fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(raw))
}
return true, nil
}
// PostOutbox implements the generic algorithm for handling a POST request to an
// actor's outbox independent on an application. It relies on a delegate to
// implement application specific functionality.
//
// Only supports serving data with identifiers having the HTTPS scheme.
func (b *baseActor) PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
return b.PostOutboxScheme(c, w, r, "https")
}
// PostOutbox implements the generic algorithm for handling a POST request to an
// actor's outbox independent on an application. It relies on a delegate to
// implement application specific functionality.
//
// Specifying the "scheme" allows for retrieving ActivityStreams content with
// identifiers such as HTTP, HTTPS, or other protocol schemes.
func (b *baseActor) PostOutboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) {
// Do nothing if it is not an ActivityPub POST request.
if !isActivityPubPost(r) {
return false, nil
}
// If the Social API is not enabled, then this endpoint is not enabled.
if !b.enableSocialProtocol {
w.WriteHeader(http.StatusMethodNotAllowed)
return true, nil
}
// Delegate authenticating and authorizing the request.
c, authenticated, err := b.delegate.AuthenticatePostOutbox(c, w, r)
if err != nil {
return true, err
} else if !authenticated {
return true, nil
}
// Everything is good to begin processing the request.
raw, err := ioutil.ReadAll(r.Body)
if err != nil {
return true, err
}
var m map[string]interface{}
if err = json.Unmarshal(raw, &m); err != nil {
return true, err
}
// Note that converting to a Type will NOT successfully convert types
// not known to go-fed. This prevents accidentally wrapping an Activity
// type unknown to go-fed in a Create below. Instead,
// streams.ErrUnhandledType will be returned here.
asValue, err := streams.ToType(c, m)
if err != nil && !streams.IsUnmatchedErr(err) {
return true, err
} else if streams.IsUnmatchedErr(err) {
// Respond with bad request -- we do not understand the type.
w.WriteHeader(http.StatusBadRequest)
return true, nil
}
// Allow server implementations to set context data with a hook.
c, err = b.delegate.PostOutboxRequestBodyHook(c, r, asValue)
if err != nil {
return true, err
}
// The HTTP request steps are complete, complete the rest of the outbox
// and delivery process.
outboxId := requestId(r, scheme)
activity, err := b.deliver(c, outboxId, asValue, m)
// Special case: We know it is a bad request if the object or
// target properties needed to be populated, but weren't.
//
// Send the rejection to the client.
if err == ErrObjectRequired || err == ErrTargetRequired {
w.WriteHeader(http.StatusBadRequest)
return true, nil
} else if err != nil {
return true, err
}
// Respond to the request with the new Activity's IRI location.
w.Header().Set(locationHeader, activity.GetJSONLDId().Get().String())
w.WriteHeader(http.StatusCreated)
return true, nil
}
// GetOutbox implements the generic algorithm for handling a Get request to an
// actor's outbox independent on an application. It relies on a delegate to
// implement application specific functionality.
func (b *baseActor) GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
// Do nothing if it is not an ActivityPub GET request.
if !isActivityPubGet(r) {
return false, nil
}
// Delegate authenticating and authorizing the request.
c, authenticated, err := b.delegate.AuthenticateGetOutbox(c, w, r)
if err != nil {
return true, err
} else if !authenticated {
return true, nil
}
// Everything is good to begin processing the request.
oc, err := b.delegate.GetOutbox(c, r)
if err != nil {
return true, err
}
// Request has been processed. Begin responding to the request.
//
// Serialize the OrderedCollection.
m, err := streams.Serialize(oc)
if err != nil {
return true, err
}
raw, err := json.Marshal(m)
if err != nil {
return true, err
}
// Write the response.
addResponseHeaders(w.Header(), b.clock, raw)
w.WriteHeader(http.StatusOK)
n, err := w.Write(raw)
if err != nil {
return true, err
} else if n != len(raw) {
return true, fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(raw))
}
return true, nil
}
// deliver delegates all outbox handling steps and optionally will federate the
// activity if the federated protocol is enabled.
//
// This function is not exported so an Actor that only supports C2S cannot be
// type casted to a FederatingActor. It doesn't exactly fit the Send method
// signature anyways.
//
// Note: 'm' is nilable.
func (b *baseActor) deliver(c context.Context, outbox *url.URL, asValue vocab.Type, m map[string]interface{}) (activity Activity, err error) {
// If the value is not an Activity or type extending from Activity, then
// we need to wrap it in a Create Activity.
if !streams.IsOrExtendsActivityStreamsActivity(asValue) {
asValue, err = b.delegate.WrapInCreate(c, asValue, outbox)
if err != nil {
return
}
}
// At this point, this should be a safe conversion. If this error is
// triggered, then there is either a bug in the delegation of
// WrapInCreate, behavior is not lining up in the generated ExtendedBy
// code, or something else is incorrect with the type system.
var ok bool
activity, ok = asValue.(Activity)
if !ok {
err = fmt.Errorf("activity streams value is not an Activity: %T", asValue)
return
}
// Delegate generating new IDs for the activity and all new objects.
if err = b.delegate.AddNewIDs(c, activity); err != nil {
return
}
// Post the activity to the actor's outbox and trigger side effects for
// that particular Activity type.
//
// Since 'm' is nil-able and side effects may need access to literal nil
// values, such as for Update activities, ensure 'm' is non-nil.
if m == nil {
m, err = asValue.Serialize()
if err != nil {
return
}
}
deliverable, err := b.delegate.PostOutbox(c, activity, outbox, m)
if err != nil {
return
}
// Request has been processed and all side effects internal to this
// application server have finished. Begin side effects affecting other
// servers and/or the client who sent this request.
//
// If we are federating and the type is a deliverable one, then deliver
// the activity to federating peers.
if b.enableFederatedProtocol && deliverable {
if err = b.delegate.Deliver(c, outbox, activity); err != nil {
return
}
}
return
}
// Send is programmatically accessible if the federated protocol is enabled.
func (b *baseActorFederating) Send(c context.Context, outbox *url.URL, t vocab.Type) (Activity, error) {
return b.deliver(c, outbox, t, nil)
}

11
vendor/github.com/go-fed/activity/pub/clock.go generated vendored Normal file
View file

@ -0,0 +1,11 @@
package pub
import (
"time"
)
// Clock determines the time.
type Clock interface {
// Now returns the current time.
Now() time.Time
}

View file

@ -0,0 +1,89 @@
package pub
import (
"context"
"github.com/go-fed/activity/streams/vocab"
"net/http"
"net/url"
)
// Common contains functions required for both the Social API and Federating
// Protocol.
//
// It is passed to the library as a dependency injection from the client
// application.
type CommonBehavior interface {
// AuthenticateGetInbox delegates the authentication of a GET to an
// inbox.
//
// Always called, regardless whether the Federated Protocol or Social
// API is enabled.
//
// If an error is returned, it is passed back to the caller of
// GetInbox. In this case, the implementation must not write a
// response to the ResponseWriter as is expected that the client will
// do so when handling the error. The 'authenticated' is ignored.
//
// If no error is returned, but authentication or authorization fails,
// then authenticated must be false and error nil. It is expected that
// the implementation handles writing to the ResponseWriter in this
// case.
//
// Finally, if the authentication and authorization succeeds, then
// authenticated must be true and error nil. The request will continue
// to be processed.
AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
// AuthenticateGetOutbox delegates the authentication of a GET to an
// outbox.
//
// Always called, regardless whether the Federated Protocol or Social
// API is enabled.
//
// If an error is returned, it is passed back to the caller of
// GetOutbox. In this case, the implementation must not write a
// response to the ResponseWriter as is expected that the client will
// do so when handling the error. The 'authenticated' is ignored.
//
// If no error is returned, but authentication or authorization fails,
// then authenticated must be false and error nil. It is expected that
// the implementation handles writing to the ResponseWriter in this
// case.
//
// Finally, if the authentication and authorization succeeds, then
// authenticated must be true and error nil. The request will continue
// to be processed.
AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
// GetOutbox returns the OrderedCollection inbox of the actor for this
// context. It is up to the implementation to provide the correct
// collection for the kind of authorization given in the request.
//
// AuthenticateGetOutbox will be called prior to this.
//
// Always called, regardless whether the Federated Protocol or Social
// API is enabled.
GetOutbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error)
// NewTransport returns a new Transport on behalf of a specific actor.
//
// The actorBoxIRI will be either the inbox or outbox of an actor who is
// attempting to do the dereferencing or delivery. Any authentication
// scheme applied on the request must be based on this actor. The
// request must contain some sort of credential of the user, such as a
// HTTP Signature.
//
// The gofedAgent passed in should be used by the Transport
// implementation in the User-Agent, as well as the application-specific
// user agent string. The gofedAgent will indicate this library's use as
// well as the library's version number.
//
// Any server-wide rate-limiting that needs to occur should happen in a
// Transport implementation. This factory function allows this to be
// created, so peer servers are not DOS'd.
//
// Any retry logic should also be handled by the Transport
// implementation.
//
// Note that the library will not maintain a long-lived pointer to the
// returned Transport so that any private credentials are able to be
// garbage collected.
NewTransport(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t Transport, err error)
}

139
vendor/github.com/go-fed/activity/pub/database.go generated vendored Normal file
View file

@ -0,0 +1,139 @@
package pub
import (
"context"
"github.com/go-fed/activity/streams/vocab"
"net/url"
)
type Database interface {
// Lock takes a lock for the object at the specified id. If an error
// is returned, the lock must not have been taken.
//
// The lock must be able to succeed for an id that does not exist in
// the database. This means acquiring the lock does not guarantee the
// entry exists in the database.
//
// Locks are encouraged to be lightweight and in the Go layer, as some
// processes require tight loops acquiring and releasing locks.
//
// Used to ensure race conditions in multiple requests do not occur.
Lock(c context.Context, id *url.URL) error
// Unlock makes the lock for the object at the specified id available.
// If an error is returned, the lock must have still been freed.
//
// Used to ensure race conditions in multiple requests do not occur.
Unlock(c context.Context, id *url.URL) error
// InboxContains returns true if the OrderedCollection at 'inbox'
// contains the specified 'id'.
//
// The library makes this call only after acquiring a lock first.
InboxContains(c context.Context, inbox, id *url.URL) (contains bool, err error)
// GetInbox returns the first ordered collection page of the outbox at
// the specified IRI, for prepending new items.
//
// The library makes this call only after acquiring a lock first.
GetInbox(c context.Context, inboxIRI *url.URL) (inbox vocab.ActivityStreamsOrderedCollectionPage, err error)
// SetInbox saves the inbox value given from GetInbox, with new items
// prepended. Note that the new items must not be added as independent
// database entries. Separate calls to Create will do that.
//
// The library makes this call only after acquiring a lock first.
SetInbox(c context.Context, inbox vocab.ActivityStreamsOrderedCollectionPage) error
// Owns returns true if the database has an entry for the IRI and it
// exists in the database.
//
// The library makes this call only after acquiring a lock first.
Owns(c context.Context, id *url.URL) (owns bool, err error)
// ActorForOutbox fetches the actor's IRI for the given outbox IRI.
//
// The library makes this call only after acquiring a lock first.
ActorForOutbox(c context.Context, outboxIRI *url.URL) (actorIRI *url.URL, err error)
// ActorForInbox fetches the actor's IRI for the given outbox IRI.
//
// The library makes this call only after acquiring a lock first.
ActorForInbox(c context.Context, inboxIRI *url.URL) (actorIRI *url.URL, err error)
// OutboxForInbox fetches the corresponding actor's outbox IRI for the
// actor's inbox IRI.
//
// The library makes this call only after acquiring a lock first.
OutboxForInbox(c context.Context, inboxIRI *url.URL) (outboxIRI *url.URL, err error)
// Exists returns true if the database has an entry for the specified
// id. It may not be owned by this application instance.
//
// The library makes this call only after acquiring a lock first.
Exists(c context.Context, id *url.URL) (exists bool, err error)
// Get returns the database entry for the specified id.
//
// The library makes this call only after acquiring a lock first.
Get(c context.Context, id *url.URL) (value vocab.Type, err error)
// Create adds a new entry to the database which must be able to be
// keyed by its id.
//
// Note that Activity values received from federated peers may also be
// created in the database this way if the Federating Protocol is
// enabled. The client may freely decide to store only the id instead of
// the entire value.
//
// The library makes this call only after acquiring a lock first.
//
// Under certain conditions and network activities, Create may be called
// multiple times for the same ActivityStreams object.
Create(c context.Context, asType vocab.Type) error
// Update sets an existing entry to the database based on the value's
// id.
//
// Note that Activity values received from federated peers may also be
// updated in the database this way if the Federating Protocol is
// enabled. The client may freely decide to store only the id instead of
// the entire value.
//
// The library makes this call only after acquiring a lock first.
Update(c context.Context, asType vocab.Type) error
// Delete removes the entry with the given id.
//
// Delete is only called for federated objects. Deletes from the Social
// Protocol instead call Update to create a Tombstone.
//
// The library makes this call only after acquiring a lock first.
Delete(c context.Context, id *url.URL) error
// GetOutbox returns the first ordered collection page of the outbox
// at the specified IRI, for prepending new items.
//
// The library makes this call only after acquiring a lock first.
GetOutbox(c context.Context, outboxIRI *url.URL) (outbox vocab.ActivityStreamsOrderedCollectionPage, err error)
// SetOutbox saves the outbox value given from GetOutbox, with new items
// prepended. Note that the new items must not be added as independent
// database entries. Separate calls to Create will do that.
//
// The library makes this call only after acquiring a lock first.
SetOutbox(c context.Context, outbox vocab.ActivityStreamsOrderedCollectionPage) error
// NewID creates a new IRI id for the provided activity or object. The
// implementation does not need to set the 'id' property and simply
// needs to determine the value.
//
// The go-fed library will handle setting the 'id' property on the
// activity or object provided with the value returned.
NewID(c context.Context, t vocab.Type) (id *url.URL, err error)
// Followers obtains the Followers Collection for an actor with the
// given id.
//
// If modified, the library will then call Update.
//
// The library makes this call only after acquiring a lock first.
Followers(c context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error)
// Following obtains the Following Collection for an actor with the
// given id.
//
// If modified, the library will then call Update.
//
// The library makes this call only after acquiring a lock first.
Following(c context.Context, actorIRI *url.URL) (following vocab.ActivityStreamsCollection, err error)
// Liked obtains the Liked Collection for an actor with the
// given id.
//
// If modified, the library will then call Update.
//
// The library makes this call only after acquiring a lock first.
Liked(c context.Context, actorIRI *url.URL) (liked vocab.ActivityStreamsCollection, err error)
}

248
vendor/github.com/go-fed/activity/pub/delegate_actor.go generated vendored Normal file
View file

@ -0,0 +1,248 @@
package pub
import (
"context"
"github.com/go-fed/activity/streams/vocab"
"net/http"
"net/url"
)
// DelegateActor contains the detailed interface an application must satisfy in
// order to implement the ActivityPub specification.
//
// Note that an implementation of this interface is implicitly provided in the
// calls to NewActor, NewSocialActor, and NewFederatingActor.
//
// Implementing the DelegateActor requires familiarity with the ActivityPub
// specification because it does not a strong enough abstraction for the client
// application to ignore the ActivityPub spec. It is very possible to implement
// this interface and build a foot-gun that trashes the fediverse without being
// ActivityPub compliant. Please use with due consideration.
//
// Alternatively, build an application that uses the parts of the pub library
// that do not require implementing a DelegateActor so that the ActivityPub
// implementation is completely provided out of the box.
type DelegateActor interface {
// Hook callback after parsing the request body for a federated request
// to the Actor's inbox.
//
// Can be used to set contextual information based on the Activity
// received.
//
// Only called if the Federated Protocol is enabled.
//
// Warning: Neither authentication nor authorization has taken place at
// this time. Doing anything beyond setting contextual information is
// strongly discouraged.
//
// If an error is returned, it is passed back to the caller of
// PostInbox. In this case, the DelegateActor implementation must not
// write a response to the ResponseWriter as is expected that the caller
// to PostInbox will do so when handling the error.
PostInboxRequestBodyHook(c context.Context, r *http.Request, activity Activity) (context.Context, error)
// Hook callback after parsing the request body for a client request
// to the Actor's outbox.
//
// Can be used to set contextual information based on the
// ActivityStreams object received.
//
// Only called if the Social API is enabled.
//
// Warning: Neither authentication nor authorization has taken place at
// this time. Doing anything beyond setting contextual information is
// strongly discouraged.
//
// If an error is returned, it is passed back to the caller of
// PostOutbox. In this case, the DelegateActor implementation must not
// write a response to the ResponseWriter as is expected that the caller
// to PostOutbox will do so when handling the error.
PostOutboxRequestBodyHook(c context.Context, r *http.Request, data vocab.Type) (context.Context, error)
// AuthenticatePostInbox delegates the authentication of a POST to an
// inbox.
//
// Only called if the Federated Protocol is enabled.
//
// If an error is returned, it is passed back to the caller of
// PostInbox. In this case, the implementation must not write a
// response to the ResponseWriter as is expected that the client will
// do so when handling the error. The 'authenticated' is ignored.
//
// If no error is returned, but authentication or authorization fails,
// then authenticated must be false and error nil. It is expected that
// the implementation handles writing to the ResponseWriter in this
// case.
//
// Finally, if the authentication and authorization succeeds, then
// authenticated must be true and error nil. The request will continue
// to be processed.
AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
// AuthenticateGetInbox delegates the authentication of a GET to an
// inbox.
//
// Always called, regardless whether the Federated Protocol or Social
// API is enabled.
//
// If an error is returned, it is passed back to the caller of
// GetInbox. In this case, the implementation must not write a
// response to the ResponseWriter as is expected that the client will
// do so when handling the error. The 'authenticated' is ignored.
//
// If no error is returned, but authentication or authorization fails,
// then authenticated must be false and error nil. It is expected that
// the implementation handles writing to the ResponseWriter in this
// case.
//
// Finally, if the authentication and authorization succeeds, then
// authenticated must be true and error nil. The request will continue
// to be processed.
AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
// AuthorizePostInbox delegates the authorization of an activity that
// has been sent by POST to an inbox.
//
// Only called if the Federated Protocol is enabled.
//
// If an error is returned, it is passed back to the caller of
// PostInbox. In this case, the implementation must not write a
// response to the ResponseWriter as is expected that the client will
// do so when handling the error. The 'authorized' is ignored.
//
// If no error is returned, but authorization fails, then authorized
// must be false and error nil. It is expected that the implementation
// handles writing to the ResponseWriter in this case.
//
// Finally, if the authentication and authorization succeeds, then
// authorized must be true and error nil. The request will continue
// to be processed.
AuthorizePostInbox(c context.Context, w http.ResponseWriter, activity Activity) (authorized bool, err error)
// PostInbox delegates the side effects of adding to the inbox and
// determining if it is a request that should be blocked.
//
// Only called if the Federated Protocol is enabled.
//
// As a side effect, PostInbox sets the federated data in the inbox, but
// not on its own in the database, as InboxForwarding (which is called
// later) must decide whether it has seen this activity before in order
// to determine whether to do the forwarding algorithm.
//
// If the error is ErrObjectRequired or ErrTargetRequired, then a Bad
// Request status is sent in the response.
PostInbox(c context.Context, inboxIRI *url.URL, activity Activity) error
// InboxForwarding delegates inbox forwarding logic when a POST request
// is received in the Actor's inbox.
//
// Only called if the Federated Protocol is enabled.
//
// The delegate is responsible for determining whether to do the inbox
// forwarding, as well as actually conducting it if it determines it
// needs to.
//
// As a side effect, InboxForwarding must set the federated data in the
// database, independently of the inbox, however it sees fit in order to
// determine whether it has seen the activity before.
//
// The provided url is the inbox of the recipient of the Activity. The
// Activity is examined for the information about who to inbox forward
// to.
//
// If an error is returned, it is returned to the caller of PostInbox.
InboxForwarding(c context.Context, inboxIRI *url.URL, activity Activity) error
// PostOutbox delegates the logic for side effects and adding to the
// outbox.
//
// Always called, regardless whether the Federated Protocol or Social
// API is enabled. In the case of the Social API being enabled, side
// effects of the Activity must occur.
//
// The delegate is responsible for adding the activity to the database's
// general storage for independent retrieval, and not just within the
// actor's outbox.
//
// If the error is ErrObjectRequired or ErrTargetRequired, then a Bad
// Request status is sent in the response.
//
// Note that 'rawJSON' is an unfortunate consequence where an 'Update'
// Activity is the only one that explicitly cares about 'null' values in
// JSON. Since go-fed does not differentiate between 'null' values and
// values that are simply not present, the 'rawJSON' map is ONLY needed
// for this narrow and specific use case.
PostOutbox(c context.Context, a Activity, outboxIRI *url.URL, rawJSON map[string]interface{}) (deliverable bool, e error)
// AddNewIDs sets new URL ids on the activity. It also does so for all
// 'object' properties if the Activity is a Create type.
//
// Only called if the Social API is enabled.
//
// If an error is returned, it is returned to the caller of PostOutbox.
AddNewIDs(c context.Context, a Activity) error
// Deliver sends a federated message. Called only if federation is
// enabled.
//
// Called if the Federated Protocol is enabled.
//
// The provided url is the outbox of the sender. The Activity contains
// the information about the intended recipients.
//
// If an error is returned, it is returned to the caller of PostOutbox.
Deliver(c context.Context, outbox *url.URL, activity Activity) error
// AuthenticatePostOutbox delegates the authentication and authorization
// of a POST to an outbox.
//
// Only called if the Social API is enabled.
//
// If an error is returned, it is passed back to the caller of
// PostOutbox. In this case, the implementation must not write a
// response to the ResponseWriter as is expected that the client will
// do so when handling the error. The 'authenticated' is ignored.
//
// If no error is returned, but authentication or authorization fails,
// then authenticated must be false and error nil. It is expected that
// the implementation handles writing to the ResponseWriter in this
// case.
//
// Finally, if the authentication and authorization succeeds, then
// authenticated must be true and error nil. The request will continue
// to be processed.
AuthenticatePostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
// AuthenticateGetOutbox delegates the authentication of a GET to an
// outbox.
//
// Always called, regardless whether the Federated Protocol or Social
// API is enabled.
//
// If an error is returned, it is passed back to the caller of
// GetOutbox. In this case, the implementation must not write a
// response to the ResponseWriter as is expected that the client will
// do so when handling the error. The 'authenticated' is ignored.
//
// If no error is returned, but authentication or authorization fails,
// then authenticated must be false and error nil. It is expected that
// the implementation handles writing to the ResponseWriter in this
// case.
//
// Finally, if the authentication and authorization succeeds, then
// authenticated must be true and error nil. The request will continue
// to be processed.
AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
// WrapInCreate wraps the provided object in a Create ActivityStreams
// activity. The provided URL is the actor's outbox endpoint.
//
// Only called if the Social API is enabled.
WrapInCreate(c context.Context, value vocab.Type, outboxIRI *url.URL) (vocab.ActivityStreamsCreate, error)
// GetOutbox returns the OrderedCollection inbox of the actor for this
// context. It is up to the implementation to provide the correct
// collection for the kind of authorization given in the request.
//
// AuthenticateGetOutbox will be called prior to this.
//
// Always called, regardless whether the Federated Protocol or Social
// API is enabled.
GetOutbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error)
// GetInbox returns the OrderedCollection inbox of the actor for this
// context. It is up to the implementation to provide the correct
// collection for the kind of authorization given in the request.
//
// AuthenticateGetInbox will be called prior to this.
//
// Always called, regardless whether the Federated Protocol or Social
// API is enabled.
GetInbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error)
}

9
vendor/github.com/go-fed/activity/pub/doc.go generated vendored Normal file
View file

@ -0,0 +1,9 @@
// Package pub implements the ActivityPub protocol.
//
// Note that every time the ActivityStreams types are changed (added, removed)
// due to code generation, the internal function toASType needs to be modified
// to know about these types.
//
// Note that every version change should also include a change in the version.go
// file.
package pub

View file

@ -0,0 +1,124 @@
package pub
import (
"context"
"github.com/go-fed/activity/streams/vocab"
"net/http"
"net/url"
)
// FederatingProtocol contains behaviors an application needs to satisfy for the
// full ActivityPub S2S implementation to be supported by this library.
//
// It is only required if the client application wants to support the server-to-
// server, or federating, protocol.
//
// It is passed to the library as a dependency injection from the client
// application.
type FederatingProtocol interface {
// Hook callback after parsing the request body for a federated request
// to the Actor's inbox.
//
// Can be used to set contextual information based on the Activity
// received.
//
// Only called if the Federated Protocol is enabled.
//
// Warning: Neither authentication nor authorization has taken place at
// this time. Doing anything beyond setting contextual information is
// strongly discouraged.
//
// If an error is returned, it is passed back to the caller of
// PostInbox. In this case, the DelegateActor implementation must not
// write a response to the ResponseWriter as is expected that the caller
// to PostInbox will do so when handling the error.
PostInboxRequestBodyHook(c context.Context, r *http.Request, activity Activity) (context.Context, error)
// AuthenticatePostInbox delegates the authentication of a POST to an
// inbox.
//
// If an error is returned, it is passed back to the caller of
// PostInbox. In this case, the implementation must not write a
// response to the ResponseWriter as is expected that the client will
// do so when handling the error. The 'authenticated' is ignored.
//
// If no error is returned, but authentication or authorization fails,
// then authenticated must be false and error nil. It is expected that
// the implementation handles writing to the ResponseWriter in this
// case.
//
// Finally, if the authentication and authorization succeeds, then
// authenticated must be true and error nil. The request will continue
// to be processed.
AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
// Blocked should determine whether to permit a set of actors given by
// their ids are able to interact with this particular end user due to
// being blocked or other application-specific logic.
//
// If an error is returned, it is passed back to the caller of
// PostInbox.
//
// If no error is returned, but authentication or authorization fails,
// then blocked must be true and error nil. An http.StatusForbidden
// will be written in the wresponse.
//
// Finally, if the authentication and authorization succeeds, then
// blocked must be false and error nil. The request will continue
// to be processed.
Blocked(c context.Context, actorIRIs []*url.URL) (blocked bool, err error)
// FederatingCallbacks returns the application logic that handles
// ActivityStreams received from federating peers.
//
// Note that certain types of callbacks will be 'wrapped' with default
// behaviors supported natively by the library. Other callbacks
// compatible with streams.TypeResolver can be specified by 'other'.
//
// For example, setting the 'Create' field in the
// FederatingWrappedCallbacks lets an application dependency inject
// additional behaviors they want to take place, including the default
// behavior supplied by this library. This is guaranteed to be compliant
// with the ActivityPub Social protocol.
//
// To override the default behavior, instead supply the function in
// 'other', which does not guarantee the application will be compliant
// with the ActivityPub Social Protocol.
//
// Applications are not expected to handle every single ActivityStreams
// type and extension. The unhandled ones are passed to DefaultCallback.
FederatingCallbacks(c context.Context) (wrapped FederatingWrappedCallbacks, other []interface{}, err error)
// DefaultCallback is called for types that go-fed can deserialize but
// are not handled by the application's callbacks returned in the
// Callbacks method.
//
// Applications are not expected to handle every single ActivityStreams
// type and extension, so the unhandled ones are passed to
// DefaultCallback.
DefaultCallback(c context.Context, activity Activity) error
// MaxInboxForwardingRecursionDepth determines how deep to search within
// an activity to determine if inbox forwarding needs to occur.
//
// Zero or negative numbers indicate infinite recursion.
MaxInboxForwardingRecursionDepth(c context.Context) int
// MaxDeliveryRecursionDepth determines how deep to search within
// collections owned by peers when they are targeted to receive a
// delivery.
//
// Zero or negative numbers indicate infinite recursion.
MaxDeliveryRecursionDepth(c context.Context) int
// FilterForwarding allows the implementation to apply business logic
// such as blocks, spam filtering, and so on to a list of potential
// Collections and OrderedCollections of recipients when inbox
// forwarding has been triggered.
//
// The activity is provided as a reference for more intelligent
// logic to be used, but the implementation must not modify it.
FilterForwarding(c context.Context, potentialRecipients []*url.URL, a Activity) (filteredRecipients []*url.URL, err error)
// GetInbox returns the OrderedCollection inbox of the actor for this
// context. It is up to the implementation to provide the correct
// collection for the kind of authorization given in the request.
//
// AuthenticateGetInbox will be called prior to this.
//
// Always called, regardless whether the Federated Protocol or Social
// API is enabled.
GetInbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error)
}

View file

@ -0,0 +1,907 @@
package pub
import (
"context"
"encoding/json"
"fmt"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"net/url"
)
// OnFollowBehavior enumerates the different default actions that the go-fed
// library can provide when receiving a Follow Activity from a peer.
type OnFollowBehavior int
const (
// OnFollowDoNothing does not take any action when a Follow Activity
// is received.
OnFollowDoNothing OnFollowBehavior = iota
// OnFollowAutomaticallyAccept triggers the side effect of sending an
// Accept of this Follow request in response.
OnFollowAutomaticallyAccept
// OnFollowAutomaticallyAccept triggers the side effect of sending a
// Reject of this Follow request in response.
OnFollowAutomaticallyReject
)
// FederatingWrappedCallbacks lists the callback functions that already have
// some side effect behavior provided by the pub library.
//
// These functions are wrapped for the Federating Protocol.
type FederatingWrappedCallbacks struct {
// Create handles additional side effects for the Create ActivityStreams
// type, specific to the application using go-fed.
//
// The wrapping callback for the Federating Protocol ensures the
// 'object' property is created in the database.
//
// Create calls Create for each object in the federated Activity.
Create func(context.Context, vocab.ActivityStreamsCreate) error
// Update handles additional side effects for the Update ActivityStreams
// type, specific to the application using go-fed.
//
// The wrapping callback for the Federating Protocol ensures the
// 'object' property is updated in the database.
//
// Update calls Update on the federated entry from the database, with a
// new value.
Update func(context.Context, vocab.ActivityStreamsUpdate) error
// Delete handles additional side effects for the Delete ActivityStreams
// type, specific to the application using go-fed.
//
// Delete removes the federated entry from the database.
Delete func(context.Context, vocab.ActivityStreamsDelete) error
// Follow handles additional side effects for the Follow ActivityStreams
// type, specific to the application using go-fed.
//
// The wrapping function can have one of several default behaviors,
// depending on the value of the OnFollow setting.
Follow func(context.Context, vocab.ActivityStreamsFollow) error
// OnFollow determines what action to take for this particular callback
// if a Follow Activity is handled.
OnFollow OnFollowBehavior
// Accept handles additional side effects for the Accept ActivityStreams
// type, specific to the application using go-fed.
//
// The wrapping function determines if this 'Accept' is in response to a
// 'Follow'. If so, then the 'actor' is added to the original 'actor's
// 'following' collection.
//
// Otherwise, no side effects are done by go-fed.
Accept func(context.Context, vocab.ActivityStreamsAccept) error
// Reject handles additional side effects for the Reject ActivityStreams
// type, specific to the application using go-fed.
//
// The wrapping function has no default side effects. 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.
Reject func(context.Context, vocab.ActivityStreamsReject) error
// Add handles additional side effects for the Add ActivityStreams
// type, specific to the application using go-fed.
//
// The wrapping function will add the 'object' IRIs to a specific
// 'target' collection if the 'target' collection(s) live on this
// server.
Add func(context.Context, vocab.ActivityStreamsAdd) error
// Remove handles additional side effects for the Remove ActivityStreams
// type, specific to the application using go-fed.
//
// The wrapping function will remove all 'object' IRIs from a specific
// 'target' collection if the 'target' collection(s) live on this
// server.
Remove func(context.Context, vocab.ActivityStreamsRemove) error
// Like handles additional side effects for the Like ActivityStreams
// type, specific to the application using go-fed.
//
// The wrapping function will add the activity to the "likes" collection
// on all 'object' targets owned by this server.
Like func(context.Context, vocab.ActivityStreamsLike) error
// Announce handles additional side effects for the Announce
// ActivityStreams type, specific to the application using go-fed.
//
// The wrapping function will add the activity to the "shares"
// collection on all 'object' targets owned by this server.
Announce func(context.Context, vocab.ActivityStreamsAnnounce) error
// Undo handles additional side effects for the Undo ActivityStreams
// type, specific to the application using go-fed.
//
// The wrapping function ensures the 'actor' on the 'Undo'
// is be the same as the 'actor' on all Activities being undone.
// It enforces that the actors on the Undo must correspond to all of the
// 'object' actors in some manner.
//
// It is expected that the application will implement the proper
// reversal of activities that are being undone.
Undo func(context.Context, vocab.ActivityStreamsUndo) error
// Block handles additional side effects for the Block ActivityStreams
// type, specific to the application using go-fed.
//
// The wrapping function provides no default side effects. It simply
// calls the wrapped function. However, note that Blocks should not be
// received from a federated peer, as delivering Blocks explicitly
// deviates from the original ActivityPub specification.
Block func(context.Context, vocab.ActivityStreamsBlock) error
// Sidechannel data -- this is set at request handling time. These must
// be set before the callbacks are used.
// db is the Database the FederatingWrappedCallbacks should use.
db Database
// inboxIRI is the inboxIRI that is handling this callback.
inboxIRI *url.URL
// addNewIds creates new 'id' entries on an activity and its objects if
// it is a Create activity.
addNewIds func(c context.Context, activity Activity) error
// deliver delivers an outgoing message.
deliver func(c context.Context, outboxIRI *url.URL, activity Activity) error
// newTransport creates a new Transport.
newTransport func(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t Transport, err error)
}
// callbacks returns the WrappedCallbacks members into a single interface slice
// for use in streams.Resolver callbacks.
//
// If the given functions have a type that collides with the default behavior,
// then disable our default behavior
func (w FederatingWrappedCallbacks) callbacks(fns []interface{}) []interface{} {
enableCreate := true
enableUpdate := true
enableDelete := true
enableFollow := true
enableAccept := true
enableReject := true
enableAdd := true
enableRemove := true
enableLike := true
enableAnnounce := true
enableUndo := true
enableBlock := true
for _, fn := range fns {
switch fn.(type) {
default:
continue
case func(context.Context, vocab.ActivityStreamsCreate) error:
enableCreate = false
case func(context.Context, vocab.ActivityStreamsUpdate) error:
enableUpdate = false
case func(context.Context, vocab.ActivityStreamsDelete) error:
enableDelete = false
case func(context.Context, vocab.ActivityStreamsFollow) error:
enableFollow = false
case func(context.Context, vocab.ActivityStreamsAccept) error:
enableAccept = false
case func(context.Context, vocab.ActivityStreamsReject) error:
enableReject = false
case func(context.Context, vocab.ActivityStreamsAdd) error:
enableAdd = false
case func(context.Context, vocab.ActivityStreamsRemove) error:
enableRemove = false
case func(context.Context, vocab.ActivityStreamsLike) error:
enableLike = false
case func(context.Context, vocab.ActivityStreamsAnnounce) error:
enableAnnounce = false
case func(context.Context, vocab.ActivityStreamsUndo) error:
enableUndo = false
case func(context.Context, vocab.ActivityStreamsBlock) error:
enableBlock = false
}
}
if enableCreate {
fns = append(fns, w.create)
}
if enableUpdate {
fns = append(fns, w.update)
}
if enableDelete {
fns = append(fns, w.deleteFn)
}
if enableFollow {
fns = append(fns, w.follow)
}
if enableAccept {
fns = append(fns, w.accept)
}
if enableReject {
fns = append(fns, w.reject)
}
if enableAdd {
fns = append(fns, w.add)
}
if enableRemove {
fns = append(fns, w.remove)
}
if enableLike {
fns = append(fns, w.like)
}
if enableAnnounce {
fns = append(fns, w.announce)
}
if enableUndo {
fns = append(fns, w.undo)
}
if enableBlock {
fns = append(fns, w.block)
}
return fns
}
// create implements the federating Create activity side effects.
func (w FederatingWrappedCallbacks) create(c context.Context, a vocab.ActivityStreamsCreate) error {
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
// Create anonymous loop function to be able to properly scope the defer
// for the database lock at each iteration.
loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error {
t := iter.GetType()
if t == nil && iter.IsIRI() {
// Attempt to dereference the IRI instead
tport, err := w.newTransport(c, w.inboxIRI, goFedUserAgent())
if err != nil {
return err
}
b, err := tport.Dereference(c, iter.GetIRI())
if err != nil {
return err
}
var m map[string]interface{}
if err = json.Unmarshal(b, &m); err != nil {
return err
}
t, err = streams.ToType(c, m)
if err != nil {
return err
}
} else if t == nil {
return fmt.Errorf("cannot handle federated create: object is neither a value nor IRI")
}
id, err := GetId(t)
if err != nil {
return err
}
err = w.db.Lock(c, id)
if err != nil {
return err
}
defer w.db.Unlock(c, id)
if err := w.db.Create(c, t); err != nil {
return err
}
return nil
}
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
if err := loopFn(iter); err != nil {
return err
}
}
if w.Create != nil {
return w.Create(c, a)
}
return nil
}
// update implements the federating Update activity side effects.
func (w FederatingWrappedCallbacks) update(c context.Context, a vocab.ActivityStreamsUpdate) error {
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
if err := mustHaveActivityOriginMatchObjects(a); err != nil {
return err
}
// Create anonymous loop function to be able to properly scope the defer
// for the database lock at each iteration.
loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error {
t := iter.GetType()
if t == nil {
return fmt.Errorf("update requires an object to be wholly provided")
}
id, err := GetId(t)
if err != nil {
return err
}
err = w.db.Lock(c, id)
if err != nil {
return err
}
defer w.db.Unlock(c, id)
if err := w.db.Update(c, t); err != nil {
return err
}
return nil
}
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
if err := loopFn(iter); err != nil {
return err
}
}
if w.Update != nil {
return w.Update(c, a)
}
return nil
}
// deleteFn implements the federating Delete activity side effects.
func (w FederatingWrappedCallbacks) deleteFn(c context.Context, a vocab.ActivityStreamsDelete) error {
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
if err := mustHaveActivityOriginMatchObjects(a); err != nil {
return err
}
// Create anonymous loop function to be able to properly scope the defer
// for the database lock at each iteration.
loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error {
id, err := ToId(iter)
if err != nil {
return err
}
err = w.db.Lock(c, id)
if err != nil {
return err
}
defer w.db.Unlock(c, id)
if err := w.db.Delete(c, id); err != nil {
return err
}
return nil
}
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
if err := loopFn(iter); err != nil {
return err
}
}
if w.Delete != nil {
return w.Delete(c, a)
}
return nil
}
// follow implements the federating Follow activity side effects.
func (w FederatingWrappedCallbacks) follow(c context.Context, a vocab.ActivityStreamsFollow) error {
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
// Check that we own at least one of the 'object' properties, and ensure
// it is to the actor that owns this inbox.
//
// If not then don't send a response. It was federated to us as an FYI,
// by mistake, or some other reason.
if err := w.db.Lock(c, w.inboxIRI); err != nil {
return err
}
// WARNING: Unlock not deferred.
actorIRI, err := w.db.ActorForInbox(c, w.inboxIRI)
if err != nil {
w.db.Unlock(c, w.inboxIRI)
return err
}
w.db.Unlock(c, w.inboxIRI)
// Unlock must be called by now and every branch above.
isMe := false
if w.OnFollow != OnFollowDoNothing {
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
if id.String() == actorIRI.String() {
isMe = true
break
}
}
}
if isMe {
// Prepare the response.
var response Activity
if w.OnFollow == OnFollowAutomaticallyAccept {
response = streams.NewActivityStreamsAccept()
} else if w.OnFollow == OnFollowAutomaticallyReject {
response = streams.NewActivityStreamsReject()
} else {
return fmt.Errorf("unknown OnFollowBehavior: %d", w.OnFollow)
}
// Set us as the 'actor'.
me := streams.NewActivityStreamsActorProperty()
response.SetActivityStreamsActor(me)
me.AppendIRI(actorIRI)
// Set the Follow as the 'object' property.
op := streams.NewActivityStreamsObjectProperty()
response.SetActivityStreamsObject(op)
op.AppendActivityStreamsFollow(a)
// Add all actors on the original Follow to the 'to' property.
recipients := make([]*url.URL, 0)
to := streams.NewActivityStreamsToProperty()
response.SetActivityStreamsTo(to)
followActors := a.GetActivityStreamsActor()
for iter := followActors.Begin(); iter != followActors.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
to.AppendIRI(id)
recipients = append(recipients, id)
}
if w.OnFollow == OnFollowAutomaticallyAccept {
// If automatically accepting, then also update our
// followers collection with the new actors.
//
// If automatically rejecting, do not update the
// followers collection.
if err := w.db.Lock(c, actorIRI); err != nil {
return err
}
// WARNING: Unlock not deferred.
followers, err := w.db.Followers(c, actorIRI)
if err != nil {
w.db.Unlock(c, actorIRI)
return err
}
items := followers.GetActivityStreamsItems()
if items == nil {
items = streams.NewActivityStreamsItemsProperty()
followers.SetActivityStreamsItems(items)
}
for _, elem := range recipients {
items.PrependIRI(elem)
}
if err = w.db.Update(c, followers); err != nil {
w.db.Unlock(c, actorIRI)
return err
}
w.db.Unlock(c, actorIRI)
// Unlock must be called by now and every branch above.
}
// Lock without defer!
w.db.Lock(c, w.inboxIRI)
outboxIRI, err := w.db.OutboxForInbox(c, w.inboxIRI)
if err != nil {
w.db.Unlock(c, w.inboxIRI)
return err
}
w.db.Unlock(c, w.inboxIRI)
// Everything must be unlocked by now.
if err := w.addNewIds(c, response); err != nil {
return err
} else if err := w.deliver(c, outboxIRI, response); err != nil {
return err
}
}
if w.Follow != nil {
return w.Follow(c, a)
}
return nil
}
// accept implements the federating Accept activity side effects.
func (w FederatingWrappedCallbacks) accept(c context.Context, a vocab.ActivityStreamsAccept) error {
op := a.GetActivityStreamsObject()
if op != nil && op.Len() > 0 {
// Get this actor's id.
if err := w.db.Lock(c, w.inboxIRI); err != nil {
return err
}
// WARNING: Unlock not deferred.
actorIRI, err := w.db.ActorForInbox(c, w.inboxIRI)
if err != nil {
w.db.Unlock(c, w.inboxIRI)
return err
}
w.db.Unlock(c, w.inboxIRI)
// Unlock must be called by now and every branch above.
//
// Determine if we are in a follow on the 'object' property.
//
// TODO: Handle Accept multiple Follow.
var maybeMyFollowIRI *url.URL
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
t := iter.GetType()
if t == nil && iter.IsIRI() {
// Attempt to dereference the IRI instead
tport, err := w.newTransport(c, w.inboxIRI, goFedUserAgent())
if err != nil {
return err
}
b, err := tport.Dereference(c, iter.GetIRI())
if err != nil {
return err
}
var m map[string]interface{}
if err = json.Unmarshal(b, &m); err != nil {
return err
}
t, err = streams.ToType(c, m)
if err != nil {
return err
}
} else if t == nil {
return fmt.Errorf("cannot handle federated create: object is neither a value nor IRI")
}
// Ensure it is a Follow.
if !streams.IsOrExtendsActivityStreamsFollow(t) {
continue
}
follow, ok := t.(Activity)
if !ok {
return fmt.Errorf("a Follow in an Accept does not satisfy the Activity interface")
}
followId, err := GetId(follow)
if err != nil {
return err
}
// Ensure that we are one of the actors on the Follow.
actors := follow.GetActivityStreamsActor()
for iter := actors.Begin(); iter != actors.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
if id.String() == actorIRI.String() {
maybeMyFollowIRI = followId
break
}
}
// Continue breaking if we found ourselves
if maybeMyFollowIRI != nil {
break
}
}
// If we received an Accept whose 'object' is a Follow with an
// Accept that we sent, add to the following collection.
if maybeMyFollowIRI != nil {
// Verify our Follow request exists and the peer didn't
// fabricate it.
activityActors := a.GetActivityStreamsActor()
if activityActors == nil || activityActors.Len() == 0 {
return fmt.Errorf("an Accept with a Follow has no actors")
}
// This may be a duplicate check if we dereferenced the
// Follow above. TODO: Separate this logic to avoid
// redundancy.
//
// Use an anonymous function to properly scope the
// database lock, immediately call it.
err = func() error {
if err := w.db.Lock(c, maybeMyFollowIRI); err != nil {
return err
}
defer w.db.Unlock(c, maybeMyFollowIRI)
t, err := w.db.Get(c, maybeMyFollowIRI)
if err != nil {
return err
}
if !streams.IsOrExtendsActivityStreamsFollow(t) {
return fmt.Errorf("peer gave an Accept wrapping a Follow but provided a non-Follow id")
}
follow, ok := t.(Activity)
if !ok {
return fmt.Errorf("a Follow in an Accept does not satisfy the Activity interface")
}
// Ensure that we are one of the actors on the Follow.
ok = false
actors := follow.GetActivityStreamsActor()
for iter := actors.Begin(); iter != actors.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
if id.String() == actorIRI.String() {
ok = true
break
}
}
if !ok {
return fmt.Errorf("peer gave an Accept wrapping a Follow but we are not the actor on that Follow")
}
// Build map of original Accept actors
acceptActors := make(map[string]bool)
for iter := activityActors.Begin(); iter != activityActors.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
acceptActors[id.String()] = false
}
// Verify all actor(s) were on the original Follow.
followObj := follow.GetActivityStreamsObject()
for iter := followObj.Begin(); iter != followObj.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
if _, ok := acceptActors[id.String()]; ok {
acceptActors[id.String()] = true
}
}
for _, found := range acceptActors {
if !found {
return fmt.Errorf("peer gave an Accept wrapping a Follow but was not an object in the original Follow")
}
}
return nil
}()
if err != nil {
return err
}
// Add the peer to our following collection.
if err := w.db.Lock(c, actorIRI); err != nil {
return err
}
// WARNING: Unlock not deferred.
following, err := w.db.Following(c, actorIRI)
if err != nil {
w.db.Unlock(c, actorIRI)
return err
}
items := following.GetActivityStreamsItems()
if items == nil {
items = streams.NewActivityStreamsItemsProperty()
following.SetActivityStreamsItems(items)
}
for iter := activityActors.Begin(); iter != activityActors.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
w.db.Unlock(c, actorIRI)
return err
}
items.PrependIRI(id)
}
if err = w.db.Update(c, following); err != nil {
w.db.Unlock(c, actorIRI)
return err
}
w.db.Unlock(c, actorIRI)
// Unlock must be called by now and every branch above.
}
}
if w.Accept != nil {
return w.Accept(c, a)
}
return nil
}
// reject implements the federating Reject activity side effects.
func (w FederatingWrappedCallbacks) reject(c context.Context, a vocab.ActivityStreamsReject) error {
if w.Reject != nil {
return w.Reject(c, a)
}
return nil
}
// add implements the federating Add activity side effects.
func (w FederatingWrappedCallbacks) add(c context.Context, a vocab.ActivityStreamsAdd) error {
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
target := a.GetActivityStreamsTarget()
if target == nil || target.Len() == 0 {
return ErrTargetRequired
}
if err := add(c, op, target, w.db); err != nil {
return err
}
if w.Add != nil {
return w.Add(c, a)
}
return nil
}
// remove implements the federating Remove activity side effects.
func (w FederatingWrappedCallbacks) remove(c context.Context, a vocab.ActivityStreamsRemove) error {
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
target := a.GetActivityStreamsTarget()
if target == nil || target.Len() == 0 {
return ErrTargetRequired
}
if err := remove(c, op, target, w.db); err != nil {
return err
}
if w.Remove != nil {
return w.Remove(c, a)
}
return nil
}
// like implements the federating Like activity side effects.
func (w FederatingWrappedCallbacks) like(c context.Context, a vocab.ActivityStreamsLike) error {
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
id, err := GetId(a)
if err != nil {
return err
}
// Create anonymous loop function to be able to properly scope the defer
// for the database lock at each iteration.
loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error {
objId, err := ToId(iter)
if err != nil {
return err
}
if err := w.db.Lock(c, objId); err != nil {
return err
}
defer w.db.Unlock(c, objId)
if owns, err := w.db.Owns(c, objId); err != nil {
return err
} else if !owns {
return nil
}
t, err := w.db.Get(c, objId)
if err != nil {
return err
}
l, ok := t.(likeser)
if !ok {
return fmt.Errorf("cannot add Like to likes collection for type %T", t)
}
// Get 'likes' property on the object, creating default if
// necessary.
likes := l.GetActivityStreamsLikes()
if likes == nil {
likes = streams.NewActivityStreamsLikesProperty()
l.SetActivityStreamsLikes(likes)
}
// Get 'likes' value, defaulting to a collection.
likesT := likes.GetType()
if likesT == nil {
col := streams.NewActivityStreamsCollection()
likesT = col
likes.SetActivityStreamsCollection(col)
}
// Prepend the activity's 'id' on the 'likes' Collection or
// OrderedCollection.
if col, ok := likesT.(itemser); ok {
items := col.GetActivityStreamsItems()
if items == nil {
items = streams.NewActivityStreamsItemsProperty()
col.SetActivityStreamsItems(items)
}
items.PrependIRI(id)
} else if oCol, ok := likesT.(orderedItemser); ok {
oItems := oCol.GetActivityStreamsOrderedItems()
if oItems == nil {
oItems = streams.NewActivityStreamsOrderedItemsProperty()
oCol.SetActivityStreamsOrderedItems(oItems)
}
oItems.PrependIRI(id)
} else {
return fmt.Errorf("likes type is neither a Collection nor an OrderedCollection: %T", likesT)
}
err = w.db.Update(c, t)
if err != nil {
return err
}
return nil
}
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
if err := loopFn(iter); err != nil {
return err
}
}
if w.Like != nil {
return w.Like(c, a)
}
return nil
}
// announce implements the federating Announce activity side effects.
func (w FederatingWrappedCallbacks) announce(c context.Context, a vocab.ActivityStreamsAnnounce) error {
id, err := GetId(a)
if err != nil {
return err
}
op := a.GetActivityStreamsObject()
// Create anonymous loop function to be able to properly scope the defer
// for the database lock at each iteration.
loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error {
objId, err := ToId(iter)
if err != nil {
return err
}
if err := w.db.Lock(c, objId); err != nil {
return err
}
defer w.db.Unlock(c, objId)
if owns, err := w.db.Owns(c, objId); err != nil {
return err
} else if !owns {
return nil
}
t, err := w.db.Get(c, objId)
if err != nil {
return err
}
s, ok := t.(shareser)
if !ok {
return fmt.Errorf("cannot add Announce to Shares collection for type %T", t)
}
// Get 'shares' property on the object, creating default if
// necessary.
shares := s.GetActivityStreamsShares()
if shares == nil {
shares = streams.NewActivityStreamsSharesProperty()
s.SetActivityStreamsShares(shares)
}
// Get 'shares' value, defaulting to a collection.
sharesT := shares.GetType()
if sharesT == nil {
col := streams.NewActivityStreamsCollection()
sharesT = col
shares.SetActivityStreamsCollection(col)
}
// Prepend the activity's 'id' on the 'shares' Collection or
// OrderedCollection.
if col, ok := sharesT.(itemser); ok {
items := col.GetActivityStreamsItems()
if items == nil {
items = streams.NewActivityStreamsItemsProperty()
col.SetActivityStreamsItems(items)
}
items.PrependIRI(id)
} else if oCol, ok := sharesT.(orderedItemser); ok {
oItems := oCol.GetActivityStreamsOrderedItems()
if oItems == nil {
oItems = streams.NewActivityStreamsOrderedItemsProperty()
oCol.SetActivityStreamsOrderedItems(oItems)
}
oItems.PrependIRI(id)
} else {
return fmt.Errorf("shares type is neither a Collection nor an OrderedCollection: %T", sharesT)
}
err = w.db.Update(c, t)
if err != nil {
return err
}
return nil
}
if op != nil {
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
if err := loopFn(iter); err != nil {
return err
}
}
}
if w.Announce != nil {
return w.Announce(c, a)
}
return nil
}
// undo implements the federating Undo activity side effects.
func (w FederatingWrappedCallbacks) undo(c context.Context, a vocab.ActivityStreamsUndo) error {
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
actors := a.GetActivityStreamsActor()
if err := mustHaveActivityActorsMatchObjectActors(c, actors, op, w.newTransport, w.inboxIRI); err != nil {
return err
}
if w.Undo != nil {
return w.Undo(c, a)
}
return nil
}
// block implements the federating Block activity side effects.
func (w FederatingWrappedCallbacks) block(c context.Context, a vocab.ActivityStreamsBlock) error {
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
if w.Block != nil {
return w.Block(c, a)
}
return nil
}

113
vendor/github.com/go-fed/activity/pub/handlers.go generated vendored Normal file
View file

@ -0,0 +1,113 @@
package pub
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/go-fed/activity/streams"
)
var ErrNotFound = errors.New("go-fed/activity: ActivityStreams data not found")
// HandlerFunc determines whether an incoming HTTP request is an ActivityStreams
// GET request, and if so attempts to serve ActivityStreams data.
//
// If an error is returned, then the calling function is responsible for writing
// to the ResponseWriter as part of error handling.
//
// If 'isASRequest' is false and there is no error, then the calling function
// may continue processing the request, and the HandlerFunc will not have
// written anything to the ResponseWriter. For example, a webpage may be served
// instead.
//
// If 'isASRequest' is true and there is no error, then the HandlerFunc
// successfully served the request and wrote to the ResponseWriter.
//
// Callers are responsible for authorized access to this resource.
type HandlerFunc func(c context.Context, w http.ResponseWriter, r *http.Request) (isASRequest bool, err error)
// NewActivityStreamsHandler creates a HandlerFunc to serve ActivityStreams
// requests which are coming from other clients or servers that wish to obtain
// an ActivityStreams representation of data.
//
// Strips retrieved ActivityStreams values of sensitive fields ('bto' and 'bcc')
// before responding with them. Sets the appropriate HTTP status code for
// Tombstone Activities as well.
//
// Defaults to supporting content to be retrieved by HTTPS only.
func NewActivityStreamsHandler(db Database, clock Clock) HandlerFunc {
return NewActivityStreamsHandlerScheme(db, clock, "https")
}
// NewActivityStreamsHandlerScheme creates a HandlerFunc to serve
// ActivityStreams requests which are coming from other clients or servers that
// wish to obtain an ActivityStreams representation of data provided by the
// specified protocol scheme.
//
// Strips retrieved ActivityStreams values of sensitive fields ('bto' and 'bcc')
// before responding with them. Sets the appropriate HTTP status code for
// Tombstone Activities as well.
//
// Specifying the "scheme" allows for retrieving ActivityStreams content with
// identifiers such as HTTP, HTTPS, or other protocol schemes.
//
// Returns ErrNotFound when the database does not retrieve any data and no
// errors occurred during retrieval.
func NewActivityStreamsHandlerScheme(db Database, clock Clock, scheme string) HandlerFunc {
return func(c context.Context, w http.ResponseWriter, r *http.Request) (isASRequest bool, err error) {
// Do nothing if it is not an ActivityPub GET request
if !isActivityPubGet(r) {
return
}
isASRequest = true
id := requestId(r, scheme)
// Lock and obtain a copy of the requested ActivityStreams value
err = db.Lock(c, id)
if err != nil {
return
}
// WARNING: Unlock not deferred
t, err := db.Get(c, id)
if err != nil {
db.Unlock(c, id)
return
}
db.Unlock(c, id)
// Unlock must have been called by this point and in every
// branch above
if t == nil {
err = ErrNotFound
return
}
// Remove sensitive fields.
clearSensitiveFields(t)
// Serialize the fetched value.
m, err := streams.Serialize(t)
if err != nil {
return
}
raw, err := json.Marshal(m)
if err != nil {
return
}
// Construct the response.
addResponseHeaders(w.Header(), clock, raw)
// Write the response.
if streams.IsOrExtendsActivityStreamsTombstone(t) {
w.WriteHeader(http.StatusGone)
} else {
w.WriteHeader(http.StatusOK)
}
n, err := w.Write(raw)
if err != nil {
return
} else if n != len(raw) {
err = fmt.Errorf("only wrote %d of %d bytes", n, len(raw))
return
}
return
}
}

View file

@ -0,0 +1,117 @@
package pub
import (
"github.com/go-fed/activity/streams/vocab"
"net/url"
)
// inReplyToer is an ActivityStreams type with an 'inReplyTo' property
type inReplyToer interface {
GetActivityStreamsInReplyTo() vocab.ActivityStreamsInReplyToProperty
}
// objecter is an ActivityStreams type with an 'object' property
type objecter interface {
GetActivityStreamsObject() vocab.ActivityStreamsObjectProperty
}
// targeter is an ActivityStreams type with a 'target' property
type targeter interface {
GetActivityStreamsTarget() vocab.ActivityStreamsTargetProperty
}
// tagger is an ActivityStreams type with a 'tag' property
type tagger interface {
GetActivityStreamsTag() vocab.ActivityStreamsTagProperty
}
// hrefer is an ActivityStreams type with a 'href' property
type hrefer interface {
GetActivityStreamsHref() vocab.ActivityStreamsHrefProperty
}
// itemser is an ActivityStreams type with an 'items' property
type itemser interface {
GetActivityStreamsItems() vocab.ActivityStreamsItemsProperty
SetActivityStreamsItems(vocab.ActivityStreamsItemsProperty)
}
// orderedItemser is an ActivityStreams type with an 'orderedItems' property
type orderedItemser interface {
GetActivityStreamsOrderedItems() vocab.ActivityStreamsOrderedItemsProperty
SetActivityStreamsOrderedItems(vocab.ActivityStreamsOrderedItemsProperty)
}
// publisheder is an ActivityStreams type with a 'published' property
type publisheder interface {
GetActivityStreamsPublished() vocab.ActivityStreamsPublishedProperty
}
// updateder is an ActivityStreams type with an 'updateder' property
type updateder interface {
GetActivityStreamsUpdated() vocab.ActivityStreamsUpdatedProperty
}
// toer is an ActivityStreams type with a 'to' property
type toer interface {
GetActivityStreamsTo() vocab.ActivityStreamsToProperty
SetActivityStreamsTo(i vocab.ActivityStreamsToProperty)
}
// btoer is an ActivityStreams type with a 'bto' property
type btoer interface {
GetActivityStreamsBto() vocab.ActivityStreamsBtoProperty
SetActivityStreamsBto(i vocab.ActivityStreamsBtoProperty)
}
// ccer is an ActivityStreams type with a 'cc' property
type ccer interface {
GetActivityStreamsCc() vocab.ActivityStreamsCcProperty
SetActivityStreamsCc(i vocab.ActivityStreamsCcProperty)
}
// bccer is an ActivityStreams type with a 'bcc' property
type bccer interface {
GetActivityStreamsBcc() vocab.ActivityStreamsBccProperty
SetActivityStreamsBcc(i vocab.ActivityStreamsBccProperty)
}
// audiencer is an ActivityStreams type with an 'audience' property
type audiencer interface {
GetActivityStreamsAudience() vocab.ActivityStreamsAudienceProperty
SetActivityStreamsAudience(i vocab.ActivityStreamsAudienceProperty)
}
// inboxer is an ActivityStreams type with an 'inbox' property
type inboxer interface {
GetActivityStreamsInbox() vocab.ActivityStreamsInboxProperty
}
// attributedToer is an ActivityStreams type with an 'attributedTo' property
type attributedToer interface {
GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty
SetActivityStreamsAttributedTo(i vocab.ActivityStreamsAttributedToProperty)
}
// likeser is an ActivityStreams type with a 'likes' property
type likeser interface {
GetActivityStreamsLikes() vocab.ActivityStreamsLikesProperty
SetActivityStreamsLikes(i vocab.ActivityStreamsLikesProperty)
}
// shareser is an ActivityStreams type with a 'shares' property
type shareser interface {
GetActivityStreamsShares() vocab.ActivityStreamsSharesProperty
SetActivityStreamsShares(i vocab.ActivityStreamsSharesProperty)
}
// actorer is an ActivityStreams type with an 'actor' property
type actorer interface {
GetActivityStreamsActor() vocab.ActivityStreamsActorProperty
SetActivityStreamsActor(i vocab.ActivityStreamsActorProperty)
}
// appendIRIer is an ActivityStreams type that can Append IRIs.
type appendIRIer interface {
AppendIRI(v *url.URL)
}

View file

@ -0,0 +1,810 @@
package pub
import (
"context"
"encoding/json"
"fmt"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"net/http"
"net/url"
)
// sideEffectActor must satisfy the DelegateActor interface.
var _ DelegateActor = &sideEffectActor{}
// sideEffectActor is a DelegateActor that handles the ActivityPub
// implementation side effects, but requires a more opinionated application to
// be written.
//
// Note that when using the sideEffectActor with an application that good-faith
// implements its required interfaces, the ActivityPub specification is
// guaranteed to be correctly followed.
type sideEffectActor struct {
common CommonBehavior
s2s FederatingProtocol
c2s SocialProtocol
db Database
clock Clock
}
// PostInboxRequestBodyHook defers to the delegate.
func (a *sideEffectActor) PostInboxRequestBodyHook(c context.Context, r *http.Request, activity Activity) (context.Context, error) {
return a.s2s.PostInboxRequestBodyHook(c, r, activity)
}
// PostOutboxRequestBodyHook defers to the delegate.
func (a *sideEffectActor) PostOutboxRequestBodyHook(c context.Context, r *http.Request, data vocab.Type) (context.Context, error) {
return a.c2s.PostOutboxRequestBodyHook(c, r, data)
}
// AuthenticatePostInbox defers to the delegate to authenticate the request.
func (a *sideEffectActor) AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) {
return a.s2s.AuthenticatePostInbox(c, w, r)
}
// AuthenticateGetInbox defers to the delegate to authenticate the request.
func (a *sideEffectActor) AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) {
return a.common.AuthenticateGetInbox(c, w, r)
}
// AuthenticatePostOutbox defers to the delegate to authenticate the request.
func (a *sideEffectActor) AuthenticatePostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) {
return a.c2s.AuthenticatePostOutbox(c, w, r)
}
// AuthenticateGetOutbox defers to the delegate to authenticate the request.
func (a *sideEffectActor) AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) {
return a.common.AuthenticateGetOutbox(c, w, r)
}
// GetOutbox delegates to the SocialProtocol.
func (a *sideEffectActor) GetOutbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
return a.common.GetOutbox(c, r)
}
// GetInbox delegates to the FederatingProtocol.
func (a *sideEffectActor) GetInbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
return a.s2s.GetInbox(c, r)
}
// AuthorizePostInbox defers to the federating protocol whether the peer request
// is authorized based on the actors' ids.
func (a *sideEffectActor) AuthorizePostInbox(c context.Context, w http.ResponseWriter, activity Activity) (authorized bool, err error) {
authorized = false
actor := activity.GetActivityStreamsActor()
if actor == nil {
err = fmt.Errorf("no actors in post to inbox")
return
}
var iris []*url.URL
for i := 0; i < actor.Len(); i++ {
iter := actor.At(i)
if iter.IsIRI() {
iris = append(iris, iter.GetIRI())
} else if t := iter.GetType(); t != nil {
iris = append(iris, activity.GetJSONLDId().Get())
} else {
err = fmt.Errorf("actor at index %d is missing an id", i)
return
}
}
// Determine if the actor(s) sending this request are blocked.
var blocked bool
if blocked, err = a.s2s.Blocked(c, iris); err != nil {
return
} else if blocked {
w.WriteHeader(http.StatusForbidden)
return
}
authorized = true
return
}
// PostInbox handles the side effects of determining whether to block the peer's
// request, adding the activity to the actor's inbox, and triggering side
// effects based on the activity's type.
func (a *sideEffectActor) PostInbox(c context.Context, inboxIRI *url.URL, activity Activity) error {
isNew, err := a.addToInboxIfNew(c, inboxIRI, activity)
if err != nil {
return err
}
if isNew {
wrapped, other, err := a.s2s.FederatingCallbacks(c)
if err != nil {
return err
}
// Populate side channels.
wrapped.db = a.db
wrapped.inboxIRI = inboxIRI
wrapped.newTransport = a.common.NewTransport
wrapped.deliver = a.Deliver
wrapped.addNewIds = a.AddNewIDs
res, err := streams.NewTypeResolver(wrapped.callbacks(other)...)
if err != nil {
return err
}
if err = res.Resolve(c, activity); err != nil && !streams.IsUnmatchedErr(err) {
return err
} else if streams.IsUnmatchedErr(err) {
err = a.s2s.DefaultCallback(c, activity)
if err != nil {
return err
}
}
}
return nil
}
// InboxForwarding implements the 3-part inbox forwarding algorithm specified in
// the ActivityPub specification. Does not modify the Activity, but may send
// outbound requests as a side effect.
//
// InboxForwarding sets the federated data in the database.
func (a *sideEffectActor) InboxForwarding(c context.Context, inboxIRI *url.URL, activity Activity) error {
// 1. Must be first time we have seen this Activity.
//
// Obtain the id of the activity
id := activity.GetJSONLDId()
// Acquire a lock for the id. To be held for the rest of execution.
err := a.db.Lock(c, id.Get())
if err != nil {
return err
}
// WARNING: Unlock is not deferred
//
// If the database already contains the activity, exit early.
exists, err := a.db.Exists(c, id.Get())
if err != nil {
a.db.Unlock(c, id.Get())
return err
} else if exists {
a.db.Unlock(c, id.Get())
return nil
}
// Attempt to create the activity entry.
err = a.db.Create(c, activity)
if err != nil {
a.db.Unlock(c, id.Get())
return err
}
a.db.Unlock(c, id.Get())
// Unlock by this point and in every branch above.
//
// 2. The values of 'to', 'cc', or 'audience' are Collections owned by
// this server.
var r []*url.URL
to := activity.GetActivityStreamsTo()
if to != nil {
for iter := to.Begin(); iter != to.End(); iter = iter.Next() {
val, err := ToId(iter)
if err != nil {
return err
}
r = append(r, val)
}
}
cc := activity.GetActivityStreamsCc()
if cc != nil {
for iter := cc.Begin(); iter != cc.End(); iter = iter.Next() {
val, err := ToId(iter)
if err != nil {
return err
}
r = append(r, val)
}
}
audience := activity.GetActivityStreamsAudience()
if audience != nil {
for iter := audience.Begin(); iter != audience.End(); iter = iter.Next() {
val, err := ToId(iter)
if err != nil {
return err
}
r = append(r, val)
}
}
// Find all IRIs owned by this server. We need to find all of them so
// that forwarding can properly occur.
var myIRIs []*url.URL
for _, iri := range r {
if err != nil {
return err
}
err = a.db.Lock(c, iri)
if err != nil {
return err
}
// WARNING: Unlock is not deferred
if owns, err := a.db.Owns(c, iri); err != nil {
a.db.Unlock(c, iri)
return err
} else if !owns {
a.db.Unlock(c, iri)
continue
}
a.db.Unlock(c, iri)
// Unlock by this point and in every branch above.
myIRIs = append(myIRIs, iri)
}
// Finally, load our IRIs to determine if they are a Collection or
// OrderedCollection.
//
// Load the unfiltered IRIs.
var colIRIs []*url.URL
col := make(map[string]itemser)
oCol := make(map[string]orderedItemser)
for _, iri := range myIRIs {
err = a.db.Lock(c, iri)
if err != nil {
return err
}
// WARNING: Not Unlocked
t, err := a.db.Get(c, iri)
if err != nil {
return err
}
if streams.IsOrExtendsActivityStreamsOrderedCollection(t) {
if im, ok := t.(orderedItemser); ok {
oCol[iri.String()] = im
colIRIs = append(colIRIs, iri)
defer a.db.Unlock(c, iri)
} else {
a.db.Unlock(c, iri)
}
} else if streams.IsOrExtendsActivityStreamsCollection(t) {
if im, ok := t.(itemser); ok {
col[iri.String()] = im
colIRIs = append(colIRIs, iri)
defer a.db.Unlock(c, iri)
} else {
a.db.Unlock(c, iri)
}
} else {
a.db.Unlock(c, iri)
}
}
// If we own none of the Collection IRIs in 'to', 'cc', or 'audience'
// then no need to do inbox forwarding. We have nothing to forward to.
if len(colIRIs) == 0 {
return nil
}
// 3. The values of 'inReplyTo', 'object', 'target', or 'tag' are owned
// by this server. This is only a boolean trigger: As soon as we get
// a hit that we own something, then we should do inbox forwarding.
maxDepth := a.s2s.MaxInboxForwardingRecursionDepth(c)
ownsValue, err := a.hasInboxForwardingValues(c, inboxIRI, activity, maxDepth, 0)
if err != nil {
return err
}
// If we don't own any of the 'inReplyTo', 'object', 'target', or 'tag'
// values, then no need to do inbox forwarding.
if !ownsValue {
return nil
}
// Do the inbox forwarding since the above conditions hold true. Support
// the behavior of letting the application filter out the resulting
// collections to be targeted.
toSend, err := a.s2s.FilterForwarding(c, colIRIs, activity)
if err != nil {
return err
}
recipients := make([]*url.URL, 0, len(toSend))
for _, iri := range toSend {
if c, ok := col[iri.String()]; ok {
if it := c.GetActivityStreamsItems(); it != nil {
for iter := it.Begin(); iter != it.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
recipients = append(recipients, id)
}
}
} else if oc, ok := oCol[iri.String()]; ok {
if oit := oc.GetActivityStreamsOrderedItems(); oit != nil {
for iter := oit.Begin(); iter != oit.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
recipients = append(recipients, id)
}
}
}
}
return a.deliverToRecipients(c, inboxIRI, activity, recipients)
}
// PostOutbox handles the side effects of adding the activity to the actor's
// outbox, and triggering side effects based on the activity's type.
//
// This implementation assumes all types are meant to be delivered except for
// the ActivityStreams Block type.
func (a *sideEffectActor) PostOutbox(c context.Context, activity Activity, outboxIRI *url.URL, rawJSON map[string]interface{}) (deliverable bool, err error) {
// TODO: Determine this if c2s is nil
deliverable = true
if a.c2s != nil {
var wrapped SocialWrappedCallbacks
var other []interface{}
wrapped, other, err = a.c2s.SocialCallbacks(c)
if err != nil {
return
}
// Populate side channels.
wrapped.db = a.db
wrapped.outboxIRI = outboxIRI
wrapped.rawActivity = rawJSON
wrapped.clock = a.clock
wrapped.newTransport = a.common.NewTransport
undeliverable := false
wrapped.undeliverable = &undeliverable
var res *streams.TypeResolver
res, err = streams.NewTypeResolver(wrapped.callbacks(other)...)
if err != nil {
return
}
if err = res.Resolve(c, activity); err != nil && !streams.IsUnmatchedErr(err) {
return
} else if streams.IsUnmatchedErr(err) {
deliverable = true
err = a.c2s.DefaultCallback(c, activity)
if err != nil {
return
}
} else {
deliverable = !undeliverable
}
}
err = a.addToOutbox(c, outboxIRI, activity)
return
}
// AddNewIDs creates new 'id' entries on an activity and its objects if it is a
// Create activity.
func (a *sideEffectActor) AddNewIDs(c context.Context, activity Activity) error {
id, err := a.db.NewID(c, activity)
if err != nil {
return err
}
activityId := streams.NewJSONLDIdProperty()
activityId.Set(id)
activity.SetJSONLDId(activityId)
if streams.IsOrExtendsActivityStreamsCreate(activity) {
o, ok := activity.(objecter)
if !ok {
return fmt.Errorf("cannot add new id for Create: %T has no object property", activity)
}
if oProp := o.GetActivityStreamsObject(); oProp != nil {
for iter := oProp.Begin(); iter != oProp.End(); iter = iter.Next() {
t := iter.GetType()
if t == nil {
return fmt.Errorf("cannot add new id for object in Create: object is not embedded as a value literal")
}
id, err = a.db.NewID(c, t)
if err != nil {
return err
}
objId := streams.NewJSONLDIdProperty()
objId.Set(id)
t.SetJSONLDId(objId)
}
}
}
return nil
}
// deliver will complete the peer-to-peer sending of a federated message to
// another server.
//
// Must be called if at least the federated protocol is supported.
func (a *sideEffectActor) Deliver(c context.Context, outboxIRI *url.URL, activity Activity) error {
recipients, err := a.prepare(c, outboxIRI, activity)
if err != nil {
return err
}
return a.deliverToRecipients(c, outboxIRI, activity, recipients)
}
// WrapInCreate wraps an object with a Create activity.
func (a *sideEffectActor) WrapInCreate(c context.Context, obj vocab.Type, outboxIRI *url.URL) (create vocab.ActivityStreamsCreate, err error) {
err = a.db.Lock(c, outboxIRI)
if err != nil {
return
}
// WARNING: No deferring the Unlock
actorIRI, err := a.db.ActorForOutbox(c, outboxIRI)
if err != nil {
a.db.Unlock(c, outboxIRI)
return
}
a.db.Unlock(c, outboxIRI)
// Unlock the lock at this point and every branch above
return wrapInCreate(c, obj, actorIRI)
}
// deliverToRecipients will take a prepared Activity and send it to specific
// recipients on behalf of an actor.
func (a *sideEffectActor) deliverToRecipients(c context.Context, boxIRI *url.URL, activity Activity, recipients []*url.URL) error {
m, err := streams.Serialize(activity)
if err != nil {
return err
}
b, err := json.Marshal(m)
if err != nil {
return err
}
tp, err := a.common.NewTransport(c, boxIRI, goFedUserAgent())
if err != nil {
return err
}
return tp.BatchDeliver(c, b, recipients)
}
// addToOutbox adds the activity to the outbox and creates the activity in the
// internal database as its own entry.
func (a *sideEffectActor) addToOutbox(c context.Context, outboxIRI *url.URL, activity Activity) error {
// Set the activity in the database first.
id := activity.GetJSONLDId()
err := a.db.Lock(c, id.Get())
if err != nil {
return err
}
// WARNING: Unlock not deferred
err = a.db.Create(c, activity)
if err != nil {
a.db.Unlock(c, id.Get())
return err
}
a.db.Unlock(c, id.Get())
// WARNING: Unlock(c, id) should be called by this point and in every
// return before here.
//
// Acquire a lock to read the outbox. Defer release.
err = a.db.Lock(c, outboxIRI)
if err != nil {
return err
}
defer a.db.Unlock(c, outboxIRI)
outbox, err := a.db.GetOutbox(c, outboxIRI)
if err != nil {
return err
}
// Prepend the activity to the list of 'orderedItems'.
oi := outbox.GetActivityStreamsOrderedItems()
if oi == nil {
oi = streams.NewActivityStreamsOrderedItemsProperty()
}
oi.PrependIRI(id.Get())
outbox.SetActivityStreamsOrderedItems(oi)
// Save in the database.
err = a.db.SetOutbox(c, outbox)
return err
}
// addToInboxIfNew will add the activity to the inbox at the specified IRI if
// the activity's ID has not yet been added to the inbox.
//
// It does not add the activity to this database's know federated data.
//
// Returns true when the activity is novel.
func (a *sideEffectActor) addToInboxIfNew(c context.Context, inboxIRI *url.URL, activity Activity) (isNew bool, err error) {
// Acquire a lock to read the inbox. Defer release.
err = a.db.Lock(c, inboxIRI)
if err != nil {
return
}
defer a.db.Unlock(c, inboxIRI)
// Obtain the id of the activity
id := activity.GetJSONLDId()
// If the inbox already contains the URL, early exit.
contains, err := a.db.InboxContains(c, inboxIRI, id.Get())
if err != nil {
return
} else if contains {
return
}
// It is a new id, acquire the inbox.
isNew = true
inbox, err := a.db.GetInbox(c, inboxIRI)
if err != nil {
return
}
// Prepend the activity to the list of 'orderedItems'.
oi := inbox.GetActivityStreamsOrderedItems()
if oi == nil {
oi = streams.NewActivityStreamsOrderedItemsProperty()
}
oi.PrependIRI(id.Get())
inbox.SetActivityStreamsOrderedItems(oi)
// Save in the database.
err = a.db.SetInbox(c, inbox)
return
}
// Given an ActivityStreams value, recursively examines ownership of the id or
// href and the ones on properties applicable to inbox forwarding.
//
// Recursion may be limited by providing a 'maxDepth' greater than zero. A
// value of zero or a negative number will result in infinite recursion.
func (a *sideEffectActor) hasInboxForwardingValues(c context.Context, inboxIRI *url.URL, val vocab.Type, maxDepth, currDepth int) (bool, error) {
// Stop recurring if we are exceeding the maximum depth and the maximum
// is a positive number.
if maxDepth > 0 && currDepth >= maxDepth {
return false, nil
}
// Determine if we own the 'id' of any values on the properties we care
// about.
types, iris := getInboxForwardingValues(val)
// For IRIs, simply check if we own them.
for _, iri := range iris {
err := a.db.Lock(c, iri)
if err != nil {
return false, err
}
// WARNING: Unlock is not deferred
if owns, err := a.db.Owns(c, iri); err != nil {
a.db.Unlock(c, iri)
return false, err
} else if owns {
a.db.Unlock(c, iri)
return true, nil
}
a.db.Unlock(c, iri)
// Unlock by this point and in every branch above
}
// For embedded literals, check the id.
for _, val := range types {
id, err := GetId(val)
if err != nil {
return false, err
}
err = a.db.Lock(c, id)
if err != nil {
return false, err
}
// WARNING: Unlock is not deferred
if owns, err := a.db.Owns(c, id); err != nil {
a.db.Unlock(c, id)
return false, err
} else if owns {
a.db.Unlock(c, id)
return true, nil
}
a.db.Unlock(c, id)
// Unlock by this point and in every branch above
}
// Recur Preparation: Try fetching the IRIs so we can recur into them.
for _, iri := range iris {
// Dereferencing the IRI.
tport, err := a.common.NewTransport(c, inboxIRI, goFedUserAgent())
if err != nil {
return false, err
}
b, err := tport.Dereference(c, iri)
if err != nil {
// Do not fail the entire process if the data is
// missing.
continue
}
var m map[string]interface{}
if err = json.Unmarshal(b, &m); err != nil {
return false, err
}
t, err := streams.ToType(c, m)
if err != nil {
// Do not fail the entire process if we cannot handle
// the type.
continue
}
types = append(types, t)
}
// Recur.
for _, nextVal := range types {
if has, err := a.hasInboxForwardingValues(c, inboxIRI, nextVal, maxDepth, currDepth+1); err != nil {
return false, err
} else if has {
return true, nil
}
}
return false, nil
}
// 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.
//
// Only call if both the social and federated protocol are supported.
func (a *sideEffectActor) prepare(c context.Context, outboxIRI *url.URL, activity Activity) (r []*url.URL, err error) {
// Get inboxes of recipients
if to := activity.GetActivityStreamsTo(); to != nil {
for iter := to.Begin(); iter != to.End(); iter = iter.Next() {
var val *url.URL
val, err = ToId(iter)
if err != nil {
return
}
r = append(r, val)
}
}
if bto := activity.GetActivityStreamsBto(); bto != nil {
for iter := bto.Begin(); iter != bto.End(); iter = iter.Next() {
var val *url.URL
val, err = ToId(iter)
if err != nil {
return
}
r = append(r, val)
}
}
if cc := activity.GetActivityStreamsCc(); cc != nil {
for iter := cc.Begin(); iter != cc.End(); iter = iter.Next() {
var val *url.URL
val, err = ToId(iter)
if err != nil {
return
}
r = append(r, val)
}
}
if bcc := activity.GetActivityStreamsBcc(); bcc != nil {
for iter := bcc.Begin(); iter != bcc.End(); iter = iter.Next() {
var val *url.URL
val, err = ToId(iter)
if err != nil {
return
}
r = append(r, val)
}
}
if audience := activity.GetActivityStreamsAudience(); audience != nil {
for iter := audience.Begin(); iter != audience.End(); iter = iter.Next() {
var val *url.URL
val, err = ToId(iter)
if err != nil {
return
}
r = append(r, val)
}
}
// 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)
t, err := a.common.NewTransport(c, outboxIRI, goFedUserAgent())
if err != nil {
return nil, err
}
receiverActors, err := a.resolveInboxes(c, t, r, 0, a.s2s.MaxDeliveryRecursionDepth(c))
if err != nil {
return nil, err
}
targets, err := getInboxes(receiverActors)
if err != nil {
return nil, err
}
// Get inboxes of sender.
err = a.db.Lock(c, outboxIRI)
if err != nil {
return
}
// WARNING: No deferring the Unlock
actorIRI, err := a.db.ActorForOutbox(c, outboxIRI)
if err != nil {
a.db.Unlock(c, outboxIRI)
return
}
a.db.Unlock(c, outboxIRI)
// Get the inbox on the sender.
err = a.db.Lock(c, actorIRI)
if err != nil {
return nil, err
}
// BEGIN LOCK
thisActor, err := a.db.Get(c, actorIRI)
a.db.Unlock(c, actorIRI)
// END LOCK -- Still need to handle err
if err != nil {
return nil, err
}
// Post-processing
var ignore *url.URL
ignore, err = getInbox(thisActor)
if err != nil {
return nil, err
}
r = dedupeIRIs(targets, []*url.URL{ignore})
stripHiddenRecipients(activity)
return r, nil
}
// resolveInboxes takes a list of Actor id URIs and returns them as concrete
// instances of actorObject. It attempts to apply recursively when it encounters
// a target that is a Collection or OrderedCollection.
//
// If maxDepth is zero or negative, then recursion is infinitely applied.
//
// If a recipient is a Collection or OrderedCollection, then the server MUST
// dereference the collection, WITH the user's credentials.
//
// Note that this also applies to CollectionPage and OrderedCollectionPage.
func (a *sideEffectActor) resolveInboxes(c context.Context, t Transport, r []*url.URL, depth, maxDepth int) (actors []vocab.Type, err error) {
if maxDepth > 0 && depth >= maxDepth {
return
}
for _, u := range r {
var act vocab.Type
var more []*url.URL
// TODO: Determine if more logic is needed here for inaccessible
// collections owned by peer servers.
act, more, err = a.dereferenceForResolvingInboxes(c, t, u)
if err != nil {
// Missing recipient -- skip.
continue
}
var recurActors []vocab.Type
recurActors, err = a.resolveInboxes(c, t, more, depth+1, maxDepth)
if err != nil {
return
}
if act != nil {
actors = append(actors, act)
}
actors = append(actors, recurActors...)
}
return
}
// dereferenceForResolvingInboxes dereferences an IRI solely for finding an
// actor's inbox IRI to deliver to.
//
// The returned actor could be nil, if it wasn't an actor (ex: a Collection or
// OrderedCollection).
func (a *sideEffectActor) dereferenceForResolvingInboxes(c context.Context, t Transport, actorIRI *url.URL) (actor vocab.Type, moreActorIRIs []*url.URL, err error) {
var resp []byte
resp, err = t.Dereference(c, actorIRI)
if err != nil {
return
}
var m map[string]interface{}
if err = json.Unmarshal(resp, &m); err != nil {
return
}
actor, err = streams.ToType(c, m)
if err != nil {
return
}
// Attempt to see if the 'actor' is really some sort of type that has
// an 'items' or 'orderedItems' property.
if v, ok := actor.(itemser); ok {
if i := v.GetActivityStreamsItems(); i != nil {
for iter := i.Begin(); iter != i.End(); iter = iter.Next() {
var id *url.URL
id, err = ToId(iter)
if err != nil {
return
}
moreActorIRIs = append(moreActorIRIs, id)
}
}
actor = nil
} else if v, ok := actor.(orderedItemser); ok {
if i := v.GetActivityStreamsOrderedItems(); i != nil {
for iter := i.Begin(); iter != i.End(); iter = iter.Next() {
var id *url.URL
id, err = ToId(iter)
if err != nil {
return
}
moreActorIRIs = append(moreActorIRIs, id)
}
}
actor = nil
}
return
}

View file

@ -0,0 +1,82 @@
package pub
import (
"context"
"github.com/go-fed/activity/streams/vocab"
"net/http"
)
// SocialProtocol contains behaviors an application needs to satisfy for the
// full ActivityPub C2S implementation to be supported by this library.
//
// It is only required if the client application wants to support the client-to-
// server, or social, protocol.
//
// It is passed to the library as a dependency injection from the client
// application.
type SocialProtocol interface {
// Hook callback after parsing the request body for a client request
// to the Actor's outbox.
//
// Can be used to set contextual information based on the
// ActivityStreams object received.
//
// Only called if the Social API is enabled.
//
// Warning: Neither authentication nor authorization has taken place at
// this time. Doing anything beyond setting contextual information is
// strongly discouraged.
//
// If an error is returned, it is passed back to the caller of
// PostOutbox. In this case, the DelegateActor implementation must not
// write a response to the ResponseWriter as is expected that the caller
// to PostOutbox will do so when handling the error.
PostOutboxRequestBodyHook(c context.Context, r *http.Request, data vocab.Type) (context.Context, error)
// AuthenticatePostOutbox delegates the authentication of a POST to an
// outbox.
//
// Only called if the Social API is enabled.
//
// If an error is returned, it is passed back to the caller of
// PostOutbox. In this case, the implementation must not write a
// response to the ResponseWriter as is expected that the client will
// do so when handling the error. The 'authenticated' is ignored.
//
// If no error is returned, but authentication or authorization fails,
// then authenticated must be false and error nil. It is expected that
// the implementation handles writing to the ResponseWriter in this
// case.
//
// Finally, if the authentication and authorization succeeds, then
// authenticated must be true and error nil. The request will continue
// to be processed.
AuthenticatePostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
// SocialCallbacks returns the application logic that handles
// ActivityStreams received from C2S clients.
//
// Note that certain types of callbacks will be 'wrapped' with default
// behaviors supported natively by the library. Other callbacks
// compatible with streams.TypeResolver can be specified by 'other'.
//
// For example, setting the 'Create' field in the SocialWrappedCallbacks
// lets an application dependency inject additional behaviors they want
// to take place, including the default behavior supplied by this
// library. This is guaranteed to be compliant with the ActivityPub
// Social protocol.
//
// To override the default behavior, instead supply the function in
// 'other', which does not guarantee the application will be compliant
// with the ActivityPub Social Protocol.
//
// Applications are not expected to handle every single ActivityStreams
// type and extension. The unhandled ones are passed to DefaultCallback.
SocialCallbacks(c context.Context) (wrapped SocialWrappedCallbacks, other []interface{}, err error)
// DefaultCallback is called for types that go-fed can deserialize but
// are not handled by the application's callbacks returned in the
// Callbacks method.
//
// Applications are not expected to handle every single ActivityStreams
// type and extension, so the unhandled ones are passed to
// DefaultCallback.
DefaultCallback(c context.Context, activity Activity) error
}

View file

@ -0,0 +1,531 @@
package pub
import (
"context"
"fmt"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"net/url"
)
// SocialWrappedCallbacks lists the callback functions that already have some
// side effect behavior provided by the pub library.
//
// These functions are wrapped for the Social Protocol.
type SocialWrappedCallbacks struct {
// Create handles additional side effects for the Create ActivityStreams
// type.
//
// The wrapping callback copies the actor(s) to the 'attributedTo'
// property and copies recipients between the Create activity and all
// objects. It then saves the entry in the database.
Create func(context.Context, vocab.ActivityStreamsCreate) error
// Update handles additional side effects for the Update ActivityStreams
// type.
//
// The wrapping callback applies new top-level values on an object to
// the stored objects. Any top-level null literals will be deleted on
// the stored objects as well.
Update func(context.Context, vocab.ActivityStreamsUpdate) error
// Delete handles additional side effects for the Delete ActivityStreams
// type.
//
// The wrapping callback replaces the object(s) with tombstones in the
// database.
Delete func(context.Context, vocab.ActivityStreamsDelete) error
// Follow handles additional side effects for the Follow ActivityStreams
// type.
//
// The wrapping callback only ensures the 'Follow' has at least one
// 'object' entry, but otherwise has no default side effect.
Follow func(context.Context, vocab.ActivityStreamsFollow) error
// Add handles additional side effects for the Add ActivityStreams
// type.
//
//
// The wrapping function will add the 'object' IRIs to a specific
// 'target' collection if the 'target' collection(s) live on this
// server.
Add func(context.Context, vocab.ActivityStreamsAdd) error
// Remove handles additional side effects for the Remove ActivityStreams
// type.
//
// The wrapping function will remove all 'object' IRIs from a specific
// 'target' collection if the 'target' collection(s) live on this
// server.
Remove func(context.Context, vocab.ActivityStreamsRemove) error
// Like handles additional side effects for the Like ActivityStreams
// type.
//
// The wrapping function will add the objects on the activity to the
// "liked" collection of this actor.
Like func(context.Context, vocab.ActivityStreamsLike) error
// Undo handles additional side effects for the Undo ActivityStreams
// type.
//
//
// The wrapping function ensures the 'actor' on the 'Undo'
// is be the same as the 'actor' on all Activities being undone.
// It enforces that the actors on the Undo must correspond to all of the
// 'object' actors in some manner.
//
// It is expected that the application will implement the proper
// reversal of activities that are being undone.
Undo func(context.Context, vocab.ActivityStreamsUndo) error
// Block handles additional side effects for the Block ActivityStreams
// type.
//
// The wrapping callback only ensures the 'Block' has at least one
// 'object' entry, but otherwise has no default side effect. It is up
// to the wrapped application function to properly enforce the new
// blocking behavior.
//
// Note that go-fed does not federate 'Block' activities received in the
// Social Protocol.
Block func(context.Context, vocab.ActivityStreamsBlock) error
// Sidechannel data -- this is set at request handling time. These must
// be set before the callbacks are used.
// db is the Database the SocialWrappedCallbacks should use. It must be
// set before calling the callbacks.
db Database
// outboxIRI is the outboxIRI that is handling this callback.
outboxIRI *url.URL
// rawActivity is the JSON map literal received when deserializing the
// request body.
rawActivity map[string]interface{}
// clock is the server's clock.
clock Clock
// newTransport creates a new Transport.
newTransport func(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t Transport, err error)
// undeliverable is a sidechannel out, indicating if the handled activity
// should not be delivered to a peer.
//
// Its provided default value will always be used when a custom function
// is called.
undeliverable *bool
}
// callbacks returns the WrappedCallbacks members into a single interface slice
// for use in streams.Resolver callbacks.
//
// If the given functions have a type that collides with the default behavior,
// then disable our default behavior
func (w SocialWrappedCallbacks) callbacks(fns []interface{}) []interface{} {
enableCreate := true
enableUpdate := true
enableDelete := true
enableFollow := true
enableAdd := true
enableRemove := true
enableLike := true
enableUndo := true
enableBlock := true
for _, fn := range fns {
switch fn.(type) {
default:
continue
case func(context.Context, vocab.ActivityStreamsCreate) error:
enableCreate = false
case func(context.Context, vocab.ActivityStreamsUpdate) error:
enableUpdate = false
case func(context.Context, vocab.ActivityStreamsDelete) error:
enableDelete = false
case func(context.Context, vocab.ActivityStreamsFollow) error:
enableFollow = false
case func(context.Context, vocab.ActivityStreamsAdd) error:
enableAdd = false
case func(context.Context, vocab.ActivityStreamsRemove) error:
enableRemove = false
case func(context.Context, vocab.ActivityStreamsLike) error:
enableLike = false
case func(context.Context, vocab.ActivityStreamsUndo) error:
enableUndo = false
case func(context.Context, vocab.ActivityStreamsBlock) error:
enableBlock = false
}
}
if enableCreate {
fns = append(fns, w.create)
}
if enableUpdate {
fns = append(fns, w.update)
}
if enableDelete {
fns = append(fns, w.deleteFn)
}
if enableFollow {
fns = append(fns, w.follow)
}
if enableAdd {
fns = append(fns, w.add)
}
if enableRemove {
fns = append(fns, w.remove)
}
if enableLike {
fns = append(fns, w.like)
}
if enableUndo {
fns = append(fns, w.undo)
}
if enableBlock {
fns = append(fns, w.block)
}
return fns
}
// create implements the social Create activity side effects.
func (w SocialWrappedCallbacks) create(c context.Context, a vocab.ActivityStreamsCreate) error {
*w.undeliverable = false
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
// Obtain all actor IRIs.
actors := a.GetActivityStreamsActor()
createActorIds := make(map[string]*url.URL)
if actors != nil {
createActorIds = make(map[string]*url.URL, actors.Len())
for iter := actors.Begin(); iter != actors.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
createActorIds[id.String()] = id
}
}
// Obtain each object's 'attributedTo' IRIs.
objectAttributedToIds := make([]map[string]*url.URL, op.Len())
for i := range objectAttributedToIds {
objectAttributedToIds[i] = make(map[string]*url.URL)
}
for i := 0; i < op.Len(); i++ {
t := op.At(i).GetType()
attrToer, ok := t.(attributedToer)
if !ok {
continue
}
attr := attrToer.GetActivityStreamsAttributedTo()
if attr == nil {
attr = streams.NewActivityStreamsAttributedToProperty()
attrToer.SetActivityStreamsAttributedTo(attr)
}
for iter := attr.Begin(); iter != attr.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
objectAttributedToIds[i][id.String()] = id
}
}
// Put all missing actor IRIs onto all object attributedTo properties.
for k, v := range createActorIds {
for i, attributedToMap := range objectAttributedToIds {
if _, ok := attributedToMap[k]; !ok {
t := op.At(i).GetType()
attrToer, ok := t.(attributedToer)
if !ok {
continue
}
attr := attrToer.GetActivityStreamsAttributedTo()
attr.AppendIRI(v)
}
}
}
// Put all missing object attributedTo IRIs onto the actor property
// if there is one.
if actors != nil {
for _, attributedToMap := range objectAttributedToIds {
for k, v := range attributedToMap {
if _, ok := createActorIds[k]; !ok {
actors.AppendIRI(v)
}
}
}
}
// Copy over the 'to', 'bto', 'cc', 'bcc', and 'audience' recipients
// between the activity and all child objects and vice versa.
if err := normalizeRecipients(a); err != nil {
return err
}
// Create anonymous loop function to be able to properly scope the defer
// for the database lock at each iteration.
loopFn := func(i int) error {
obj := op.At(i).GetType()
id, err := GetId(obj)
if err != nil {
return err
}
err = w.db.Lock(c, id)
if err != nil {
return err
}
defer w.db.Unlock(c, id)
if err := w.db.Create(c, obj); err != nil {
return err
}
return nil
}
// Persist all objects we've created, which will include sensitive
// recipients such as 'bcc' and 'bto'.
for i := 0; i < op.Len(); i++ {
if err := loopFn(i); err != nil {
return err
}
}
if w.Create != nil {
return w.Create(c, a)
}
return nil
}
// update implements the social Update activity side effects.
func (w SocialWrappedCallbacks) update(c context.Context, a vocab.ActivityStreamsUpdate) error {
*w.undeliverable = false
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
// Obtain all object ids, which should be owned by this server.
objIds := make([]*url.URL, 0, op.Len())
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
objIds = append(objIds, id)
}
// Create anonymous loop function to be able to properly scope the defer
// for the database lock at each iteration.
loopFn := func(idx int, loopId *url.URL) error {
err := w.db.Lock(c, loopId)
if err != nil {
return err
}
defer w.db.Unlock(c, loopId)
t, err := w.db.Get(c, loopId)
if err != nil {
return err
}
m, err := t.Serialize()
if err != nil {
return err
}
// Copy over new top-level values.
objType := op.At(idx).GetType()
if objType == nil {
return fmt.Errorf("object at index %d is not a literal type value", idx)
}
newM, err := objType.Serialize()
if err != nil {
return err
}
for k, v := range newM {
m[k] = v
}
// Delete top-level values where the raw Activity had nils.
for k, v := range w.rawActivity {
if _, ok := m[k]; v == nil && ok {
delete(m, k)
}
}
newT, err := streams.ToType(c, m)
if err != nil {
return err
}
if err = w.db.Update(c, newT); err != nil {
return err
}
return nil
}
for i, id := range objIds {
if err := loopFn(i, id); err != nil {
return err
}
}
if w.Update != nil {
return w.Update(c, a)
}
return nil
}
// deleteFn implements the social Delete activity side effects.
func (w SocialWrappedCallbacks) deleteFn(c context.Context, a vocab.ActivityStreamsDelete) error {
*w.undeliverable = false
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
// Obtain all object ids, which should be owned by this server.
objIds := make([]*url.URL, 0, op.Len())
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
objIds = append(objIds, id)
}
// Create anonymous loop function to be able to properly scope the defer
// for the database lock at each iteration.
loopFn := func(idx int, loopId *url.URL) error {
err := w.db.Lock(c, loopId)
if err != nil {
return err
}
defer w.db.Unlock(c, loopId)
t, err := w.db.Get(c, loopId)
if err != nil {
return err
}
tomb := toTombstone(t, loopId, w.clock.Now())
if err := w.db.Update(c, tomb); err != nil {
return err
}
return nil
}
for i, id := range objIds {
if err := loopFn(i, id); err != nil {
return err
}
}
if w.Delete != nil {
return w.Delete(c, a)
}
return nil
}
// follow implements the social Follow activity side effects.
func (w SocialWrappedCallbacks) follow(c context.Context, a vocab.ActivityStreamsFollow) error {
*w.undeliverable = false
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
if w.Follow != nil {
return w.Follow(c, a)
}
return nil
}
// add implements the social Add activity side effects.
func (w SocialWrappedCallbacks) add(c context.Context, a vocab.ActivityStreamsAdd) error {
*w.undeliverable = false
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
target := a.GetActivityStreamsTarget()
if target == nil || target.Len() == 0 {
return ErrTargetRequired
}
if err := add(c, op, target, w.db); err != nil {
return err
}
if w.Add != nil {
return w.Add(c, a)
}
return nil
}
// remove implements the social Remove activity side effects.
func (w SocialWrappedCallbacks) remove(c context.Context, a vocab.ActivityStreamsRemove) error {
*w.undeliverable = false
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
target := a.GetActivityStreamsTarget()
if target == nil || target.Len() == 0 {
return ErrTargetRequired
}
if err := remove(c, op, target, w.db); err != nil {
return err
}
if w.Remove != nil {
return w.Remove(c, a)
}
return nil
}
// like implements the social Like activity side effects.
func (w SocialWrappedCallbacks) like(c context.Context, a vocab.ActivityStreamsLike) error {
*w.undeliverable = false
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
// Get this actor's IRI.
if err := w.db.Lock(c, w.outboxIRI); err != nil {
return err
}
// WARNING: Unlock not deferred.
actorIRI, err := w.db.ActorForOutbox(c, w.outboxIRI)
if err != nil {
w.db.Unlock(c, w.outboxIRI)
return err
}
w.db.Unlock(c, w.outboxIRI)
// Unlock must be called by now and every branch above.
//
// Now obtain this actor's 'liked' collection.
if err := w.db.Lock(c, actorIRI); err != nil {
return err
}
defer w.db.Unlock(c, actorIRI)
liked, err := w.db.Liked(c, actorIRI)
if err != nil {
return err
}
likedItems := liked.GetActivityStreamsItems()
if likedItems == nil {
likedItems = streams.NewActivityStreamsItemsProperty()
liked.SetActivityStreamsItems(likedItems)
}
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
objId, err := ToId(iter)
if err != nil {
return err
}
likedItems.PrependIRI(objId)
}
err = w.db.Update(c, liked)
if err != nil {
return err
}
if w.Like != nil {
return w.Like(c, a)
}
return nil
}
// undo implements the social Undo activity side effects.
func (w SocialWrappedCallbacks) undo(c context.Context, a vocab.ActivityStreamsUndo) error {
*w.undeliverable = false
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
actors := a.GetActivityStreamsActor()
if err := mustHaveActivityActorsMatchObjectActors(c, actors, op, w.newTransport, w.outboxIRI); err != nil {
return err
}
if w.Undo != nil {
return w.Undo(c, a)
}
return nil
}
// block implements the social Block activity side effects.
func (w SocialWrappedCallbacks) block(c context.Context, a vocab.ActivityStreamsBlock) error {
*w.undeliverable = true
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return ErrObjectRequired
}
if w.Block != nil {
return w.Block(c, a)
}
return nil
}

207
vendor/github.com/go-fed/activity/pub/transport.go generated vendored Normal file
View file

@ -0,0 +1,207 @@
package pub
import (
"bytes"
"context"
"crypto"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"sync"
"github.com/go-fed/httpsig"
)
const (
// acceptHeaderValue is the Accept header value indicating that the
// response should contain an ActivityStreams object.
acceptHeaderValue = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
)
// isSuccess returns true if the HTTP status code is either OK, Created, or
// Accepted.
func isSuccess(code int) bool {
return code == http.StatusOK ||
code == http.StatusCreated ||
code == http.StatusAccepted
}
// Transport makes ActivityStreams calls to other servers in order to send or
// receive ActivityStreams data.
//
// It is responsible for setting the appropriate request headers, signing the
// requests if needed, and facilitating the traffic between this server and
// another.
//
// The transport is exclusively used to issue requests on behalf of an actor,
// and is never sending requests on behalf of the server in general.
//
// It may be reused multiple times, but never concurrently.
type Transport interface {
// Dereference fetches the ActivityStreams object located at this IRI
// with a GET request.
Dereference(c context.Context, iri *url.URL) ([]byte, error)
// Deliver sends an ActivityStreams object.
Deliver(c context.Context, b []byte, to *url.URL) error
// BatchDeliver sends an ActivityStreams object to multiple recipients.
BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error
}
// Transport must be implemented by HttpSigTransport.
var _ Transport = &HttpSigTransport{}
// HttpSigTransport makes a dereference call using HTTP signatures to
// authenticate the request on behalf of a particular actor.
//
// No rate limiting is applied.
//
// Only one request is tried per call.
type HttpSigTransport struct {
client HttpClient
appAgent string
gofedAgent string
clock Clock
getSigner httpsig.Signer
getSignerMu *sync.Mutex
postSigner httpsig.Signer
postSignerMu *sync.Mutex
pubKeyId string
privKey crypto.PrivateKey
}
// NewHttpSigTransport returns a new Transport.
//
// It sends requests specifically on behalf of a specific actor on this server.
// The actor's credentials are used to add an HTTP Signature to requests, which
// requires an actor's private key, a unique identifier for their public key,
// and an HTTP Signature signing algorithm.
//
// The client lets users issue requests through any HTTP client, including the
// standard library's HTTP client.
//
// The appAgent uniquely identifies the calling application's requests, so peers
// may aid debugging the requests incoming from this server. Note that the
// agent string will also include one for go-fed, so at minimum peer servers can
// reach out to the go-fed library to aid in notifying implementors of malformed
// or unsupported requests.
func NewHttpSigTransport(
client HttpClient,
appAgent string,
clock Clock,
getSigner, postSigner httpsig.Signer,
pubKeyId string,
privKey crypto.PrivateKey) *HttpSigTransport {
return &HttpSigTransport{
client: client,
appAgent: appAgent,
gofedAgent: goFedUserAgent(),
clock: clock,
getSigner: getSigner,
getSignerMu: &sync.Mutex{},
postSigner: postSigner,
postSignerMu: &sync.Mutex{},
pubKeyId: pubKeyId,
privKey: privKey,
}
}
// Dereference sends a GET request signed with an HTTP Signature to obtain an
// ActivityStreams value.
func (h HttpSigTransport) Dereference(c context.Context, iri *url.URL) ([]byte, error) {
req, err := http.NewRequest("GET", iri.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(c)
req.Header.Add(acceptHeader, acceptHeaderValue)
req.Header.Add("Accept-Charset", "utf-8")
req.Header.Add("Date", h.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
req.Header.Add("User-Agent", fmt.Sprintf("%s %s", h.appAgent, h.gofedAgent))
req.Header.Set("Host", iri.Host)
h.getSignerMu.Lock()
err = h.getSigner.SignRequest(h.privKey, h.pubKeyId, req, nil)
h.getSignerMu.Unlock()
if err != nil {
return nil, err
}
resp, err := h.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status)
}
return ioutil.ReadAll(resp.Body)
}
// Deliver sends a POST request with an HTTP Signature.
func (h HttpSigTransport) Deliver(c context.Context, b []byte, to *url.URL) error {
req, err := http.NewRequest("POST", to.String(), bytes.NewReader(b))
if err != nil {
return err
}
req = req.WithContext(c)
req.Header.Add(contentTypeHeader, contentTypeHeaderValue)
req.Header.Add("Accept-Charset", "utf-8")
req.Header.Add("Date", h.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
req.Header.Add("User-Agent", fmt.Sprintf("%s %s", h.appAgent, h.gofedAgent))
req.Header.Set("Host", to.Host)
h.postSignerMu.Lock()
err = h.postSigner.SignRequest(h.privKey, h.pubKeyId, req, b)
h.postSignerMu.Unlock()
if err != nil {
return err
}
resp, err := h.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if !isSuccess(resp.StatusCode) {
return fmt.Errorf("POST request to %s failed (%d): %s", to.String(), resp.StatusCode, resp.Status)
}
return nil
}
// BatchDeliver sends concurrent POST requests. Returns an error if any of the
// requests had an error.
func (h HttpSigTransport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error {
var wg sync.WaitGroup
errCh := make(chan error, len(recipients))
for _, recipient := range recipients {
wg.Add(1)
go func(r *url.URL) {
defer wg.Done()
if err := h.Deliver(c, b, r); err != nil {
errCh <- err
}
}(recipient)
}
wg.Wait()
errs := make([]string, 0, len(recipients))
outer:
for {
select {
case e := <-errCh:
errs = append(errs, e.Error())
default:
break outer
}
}
if len(errs) > 0 {
return fmt.Errorf("batch deliver had at least one failure: %s", strings.Join(errs, "; "))
}
return nil
}
// HttpClient sends http requests, and is an abstraction only needed by the
// HttpSigTransport. The standard library's Client satisfies this interface.
type HttpClient interface {
Do(req *http.Request) (*http.Response, error)
}
// HttpClient must be implemented by http.Client.
var _ HttpClient = &http.Client{}

995
vendor/github.com/go-fed/activity/pub/util.go generated vendored Normal file
View file

@ -0,0 +1,995 @@
package pub
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"net/http"
"net/url"
"strings"
"time"
)
var (
// ErrObjectRequired indicates the activity needs its object property
// set. Can be returned by DelegateActor's PostInbox or PostOutbox so a
// Bad Request response is set.
ErrObjectRequired = errors.New("object property required on the provided activity")
// ErrTargetRequired indicates the activity needs its target property
// set. Can be returned by DelegateActor's PostInbox or PostOutbox so a
// Bad Request response is set.
ErrTargetRequired = errors.New("target property required on the provided activity")
)
// activityStreamsMediaTypes contains all of the accepted ActivityStreams media
// types. Generated at init time.
var activityStreamsMediaTypes []string
func init() {
activityStreamsMediaTypes = []string{
"application/activity+json",
}
jsonLdType := "application/ld+json"
for _, semi := range []string{";", " ;", " ; ", "; "} {
for _, profile := range []string{
"profile=https://www.w3.org/ns/activitystreams",
"profile=\"https://www.w3.org/ns/activitystreams\"",
} {
activityStreamsMediaTypes = append(
activityStreamsMediaTypes,
fmt.Sprintf("%s%s%s", jsonLdType, semi, profile))
}
}
}
// headerIsActivityPubMediaType returns true if the header string contains one
// of the accepted ActivityStreams media types.
//
// Note we don't try to build a comprehensive parser and instead accept a
// tolerable amount of whitespace since the HTTP specification is ambiguous
// about the format and significance of whitespace.
func headerIsActivityPubMediaType(header string) bool {
for _, mediaType := range activityStreamsMediaTypes {
if strings.Contains(header, mediaType) {
return true
}
}
return false
}
const (
// The Content-Type header.
contentTypeHeader = "Content-Type"
// The Accept header.
acceptHeader = "Accept"
)
// isActivityPubPost returns true if the request is a POST request that has the
// ActivityStreams content type header
func isActivityPubPost(r *http.Request) bool {
return r.Method == "POST" && headerIsActivityPubMediaType(r.Header.Get(contentTypeHeader))
}
// isActivityPubGet returns true if the request is a GET request that has the
// ActivityStreams content type header
func isActivityPubGet(r *http.Request) bool {
return r.Method == "GET" && headerIsActivityPubMediaType(r.Header.Get(acceptHeader))
}
// dedupeOrderedItems deduplicates the 'orderedItems' within an ordered
// collection type. Deduplication happens by the 'id' property.
func dedupeOrderedItems(oc orderedItemser) error {
oi := oc.GetActivityStreamsOrderedItems()
if oi == nil {
return nil
}
seen := make(map[string]bool, oi.Len())
for i := 0; i < oi.Len(); {
var id *url.URL
iter := oi.At(i)
asType := iter.GetType()
if asType != nil {
var err error
id, err = GetId(asType)
if err != nil {
return err
}
} else if iter.IsIRI() {
id = iter.GetIRI()
} else {
return fmt.Errorf("element %d in OrderedCollection does not have an ID nor is an IRI", i)
}
if seen[id.String()] {
oi.Remove(i)
} else {
seen[id.String()] = true
i++
}
}
return nil
}
const (
// The Location header
locationHeader = "Location"
// Contains the ActivityStreams Content-Type value.
contentTypeHeaderValue = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
// The Date header.
dateHeader = "Date"
// The Digest header.
digestHeader = "Digest"
// The delimiter used in the Digest header.
digestDelimiter = "="
// SHA-256 string for the Digest header.
sha256Digest = "SHA-256"
)
// addResponseHeaders sets headers needed in the HTTP response, such but not
// limited to the Content-Type, Date, and Digest headers.
func addResponseHeaders(h http.Header, c Clock, responseContent []byte) {
h.Set(contentTypeHeader, contentTypeHeaderValue)
// RFC 7231 §7.1.1.2
h.Set(dateHeader, c.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
// RFC 3230 and RFC 5843
var b bytes.Buffer
b.WriteString(sha256Digest)
b.WriteString(digestDelimiter)
hashed := sha256.Sum256(responseContent)
b.WriteString(base64.StdEncoding.EncodeToString(hashed[:]))
h.Set(digestHeader, b.String())
}
// IdProperty is a property that can readily have its id obtained
type IdProperty interface {
// GetIRI returns the IRI of this property. When IsIRI returns false,
// GetIRI will return an arbitrary value.
GetIRI() *url.URL
// GetType returns the value in this property as a Type. Returns nil if
// the value is not an ActivityStreams type, such as an IRI or another
// value.
GetType() vocab.Type
// IsIRI returns true if this property is an IRI.
IsIRI() bool
}
// ToId returns an IdProperty's id.
func ToId(i IdProperty) (*url.URL, error) {
if i.GetType() != nil {
return GetId(i.GetType())
} else if i.IsIRI() {
return i.GetIRI(), nil
}
return nil, fmt.Errorf("cannot determine id of activitystreams property")
}
// GetId will attempt to find the 'id' property or, if it happens to be a
// Link or derived from Link type, the 'href' property instead.
//
// Returns an error if the id is not set and either the 'href' property is not
// valid on this type, or it is also not set.
func GetId(t vocab.Type) (*url.URL, error) {
if id := t.GetJSONLDId(); id != nil {
return id.Get(), nil
} else if h, ok := t.(hrefer); ok {
if href := h.GetActivityStreamsHref(); href != nil {
return href.Get(), nil
}
}
return nil, fmt.Errorf("cannot determine id of activitystreams value")
}
// getInboxForwardingValues obtains the 'inReplyTo', 'object', 'target', and
// 'tag' values on an ActivityStreams value.
func getInboxForwardingValues(o vocab.Type) (t []vocab.Type, iri []*url.URL) {
// 'inReplyTo'
if i, ok := o.(inReplyToer); ok {
if irt := i.GetActivityStreamsInReplyTo(); irt != nil {
for iter := irt.Begin(); iter != irt.End(); iter = iter.Next() {
if tv := iter.GetType(); tv != nil {
t = append(t, tv)
} else {
iri = append(iri, iter.GetIRI())
}
}
}
}
// 'tag'
if i, ok := o.(tagger); ok {
if tag := i.GetActivityStreamsTag(); tag != nil {
for iter := tag.Begin(); iter != tag.End(); iter = iter.Next() {
if tv := iter.GetType(); tv != nil {
t = append(t, tv)
} else {
iri = append(iri, iter.GetIRI())
}
}
}
}
// 'object'
if i, ok := o.(objecter); ok {
if obj := i.GetActivityStreamsObject(); obj != nil {
for iter := obj.Begin(); iter != obj.End(); iter = iter.Next() {
if tv := iter.GetType(); tv != nil {
t = append(t, tv)
} else {
iri = append(iri, iter.GetIRI())
}
}
}
}
// 'target'
if i, ok := o.(targeter); ok {
if tar := i.GetActivityStreamsTarget(); tar != nil {
for iter := tar.Begin(); iter != tar.End(); iter = iter.Next() {
if tv := iter.GetType(); tv != nil {
t = append(t, tv)
} else {
iri = append(iri, iter.GetIRI())
}
}
}
}
return
}
// wrapInCreate will automatically wrap the provided object in a Create
// activity. This will copy over the 'to', 'bto', 'cc', 'bcc', and 'audience'
// properties. It will also copy over the published time if present.
func wrapInCreate(ctx context.Context, o vocab.Type, actor *url.URL) (c vocab.ActivityStreamsCreate, err error) {
c = streams.NewActivityStreamsCreate()
// Object property
oProp := streams.NewActivityStreamsObjectProperty()
oProp.AppendType(o)
c.SetActivityStreamsObject(oProp)
// Actor Property
actorProp := streams.NewActivityStreamsActorProperty()
actorProp.AppendIRI(actor)
c.SetActivityStreamsActor(actorProp)
// Published Property
if v, ok := o.(publisheder); ok {
c.SetActivityStreamsPublished(v.GetActivityStreamsPublished())
}
// Copying over properties.
if v, ok := o.(toer); ok {
if to := v.GetActivityStreamsTo(); to != nil {
activityTo := streams.NewActivityStreamsToProperty()
for iter := to.Begin(); iter != to.End(); iter = iter.Next() {
var id *url.URL
id, err = ToId(iter)
if err != nil {
return
}
activityTo.AppendIRI(id)
}
c.SetActivityStreamsTo(activityTo)
}
}
if v, ok := o.(btoer); ok {
if bto := v.GetActivityStreamsBto(); bto != nil {
activityBto := streams.NewActivityStreamsBtoProperty()
for iter := bto.Begin(); iter != bto.End(); iter = iter.Next() {
var id *url.URL
id, err = ToId(iter)
if err != nil {
return
}
activityBto.AppendIRI(id)
}
c.SetActivityStreamsBto(activityBto)
}
}
if v, ok := o.(ccer); ok {
if cc := v.GetActivityStreamsCc(); cc != nil {
activityCc := streams.NewActivityStreamsCcProperty()
for iter := cc.Begin(); iter != cc.End(); iter = iter.Next() {
var id *url.URL
id, err = ToId(iter)
if err != nil {
return
}
activityCc.AppendIRI(id)
}
c.SetActivityStreamsCc(activityCc)
}
}
if v, ok := o.(bccer); ok {
if bcc := v.GetActivityStreamsBcc(); bcc != nil {
activityBcc := streams.NewActivityStreamsBccProperty()
for iter := bcc.Begin(); iter != bcc.End(); iter = iter.Next() {
var id *url.URL
id, err = ToId(iter)
if err != nil {
return
}
activityBcc.AppendIRI(id)
}
c.SetActivityStreamsBcc(activityBcc)
}
}
if v, ok := o.(audiencer); ok {
if aud := v.GetActivityStreamsAudience(); aud != nil {
activityAudience := streams.NewActivityStreamsAudienceProperty()
for iter := aud.Begin(); iter != aud.End(); iter = iter.Next() {
var id *url.URL
id, err = ToId(iter)
if err != nil {
return
}
activityAudience.AppendIRI(id)
}
c.SetActivityStreamsAudience(activityAudience)
}
}
return
}
// 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
}
const (
// PublicActivityPubIRI is the IRI that indicates an Activity is meant
// to be visible for general public consumption.
PublicActivityPubIRI = "https://www.w3.org/ns/activitystreams#Public"
publicJsonLD = "Public"
publicJsonLDAS = "as:Public"
)
// IsPublic determines if an IRI string is the Public collection as defined in
// the spec, including JSON-LD compliant collections.
func IsPublic(s string) bool {
return s == PublicActivityPubIRI || s == publicJsonLD || s == publicJsonLDAS
}
// getInboxes extracts the 'inbox' IRIs from actor types.
func getInboxes(t []vocab.Type) (u []*url.URL, err error) {
for _, elem := range t {
var iri *url.URL
iri, err = getInbox(elem)
if err != nil {
return
}
u = append(u, iri)
}
return
}
// getInbox extracts the 'inbox' IRI from an actor type.
func getInbox(t vocab.Type) (u *url.URL, err error) {
ib, ok := t.(inboxer)
if !ok {
err = fmt.Errorf("actor type %T has no inbox", t)
return
}
inbox := ib.GetActivityStreamsInbox()
return ToId(inbox)
}
// dedupeIRIs will deduplicate final inbox IRIs. The ignore list is applied to
// the final list.
func dedupeIRIs(recipients, ignored []*url.URL) (out []*url.URL) {
ignoredMap := make(map[string]bool, len(ignored))
for _, elem := range ignored {
ignoredMap[elem.String()] = true
}
outMap := make(map[string]bool, len(recipients))
for _, k := range recipients {
kStr := k.String()
if !ignoredMap[kStr] && !outMap[kStr] {
out = append(out, k)
outMap[kStr] = true
}
}
return
}
// stripHiddenRecipients removes "bto" and "bcc" from the activity.
//
// Note that this requirement of the specification is under "Section 6: Client
// to Server Interactions", the Social API, and not the Federative API.
func stripHiddenRecipients(activity Activity) {
activity.SetActivityStreamsBto(nil)
activity.SetActivityStreamsBcc(nil)
op := activity.GetActivityStreamsObject()
if op != nil {
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
if v, ok := iter.GetType().(btoer); ok {
v.SetActivityStreamsBto(nil)
}
if v, ok := iter.GetType().(bccer); ok {
v.SetActivityStreamsBcc(nil)
}
}
}
}
// mustHaveActivityOriginMatchObjects ensures that the Host in the activity id
// IRI matches all of the Hosts in the object id IRIs.
func mustHaveActivityOriginMatchObjects(a Activity) error {
originIRI, err := GetId(a)
if err != nil {
return err
}
originHost := originIRI.Host
op := a.GetActivityStreamsObject()
if op == nil || op.Len() == 0 {
return nil
}
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
iri, err := ToId(iter)
if err != nil {
return err
}
if originHost != iri.Host {
return fmt.Errorf("object %q: not in activity origin", iri)
}
}
return nil
}
// normalizeRecipients ensures the activity and object have the same 'to',
// 'bto', 'cc', 'bcc', and 'audience' properties. Copy the Activity's recipients
// to objects, and the objects to the activity, but does NOT copy objects'
// recipients to each other.
func normalizeRecipients(a vocab.ActivityStreamsCreate) error {
// Phase 0: Acquire all recipients on the activity.
//
// Obtain the actorTo map
actorToMap := make(map[string]*url.URL)
actorTo := a.GetActivityStreamsTo()
if actorTo == nil {
actorTo = streams.NewActivityStreamsToProperty()
a.SetActivityStreamsTo(actorTo)
}
for iter := actorTo.Begin(); iter != actorTo.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
actorToMap[id.String()] = id
}
// Obtain the actorBto map
actorBtoMap := make(map[string]*url.URL)
actorBto := a.GetActivityStreamsBto()
if actorBto == nil {
actorBto = streams.NewActivityStreamsBtoProperty()
a.SetActivityStreamsBto(actorBto)
}
for iter := actorBto.Begin(); iter != actorBto.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
actorBtoMap[id.String()] = id
}
// Obtain the actorCc map
actorCcMap := make(map[string]*url.URL)
actorCc := a.GetActivityStreamsCc()
if actorCc == nil {
actorCc = streams.NewActivityStreamsCcProperty()
a.SetActivityStreamsCc(actorCc)
}
for iter := actorCc.Begin(); iter != actorCc.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
actorCcMap[id.String()] = id
}
// Obtain the actorBcc map
actorBccMap := make(map[string]*url.URL)
actorBcc := a.GetActivityStreamsBcc()
if actorBcc == nil {
actorBcc = streams.NewActivityStreamsBccProperty()
a.SetActivityStreamsBcc(actorBcc)
}
for iter := actorBcc.Begin(); iter != actorBcc.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
actorBccMap[id.String()] = id
}
// Obtain the actorAudience map
actorAudienceMap := make(map[string]*url.URL)
actorAudience := a.GetActivityStreamsAudience()
if actorAudience == nil {
actorAudience = streams.NewActivityStreamsAudienceProperty()
a.SetActivityStreamsAudience(actorAudience)
}
for iter := actorAudience.Begin(); iter != actorAudience.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
actorAudienceMap[id.String()] = id
}
// Obtain the objects maps for each recipient type.
o := a.GetActivityStreamsObject()
objsTo := make([]map[string]*url.URL, o.Len())
objsBto := make([]map[string]*url.URL, o.Len())
objsCc := make([]map[string]*url.URL, o.Len())
objsBcc := make([]map[string]*url.URL, o.Len())
objsAudience := make([]map[string]*url.URL, o.Len())
for i := 0; i < o.Len(); i++ {
iter := o.At(i)
// Phase 1: Acquire all existing recipients on the object.
//
// Object to
objsTo[i] = make(map[string]*url.URL)
var oTo vocab.ActivityStreamsToProperty
if tr, ok := iter.GetType().(toer); !ok {
return fmt.Errorf("the Create object at %d has no 'to' property", i)
} else {
oTo = tr.GetActivityStreamsTo()
if oTo == nil {
oTo = streams.NewActivityStreamsToProperty()
tr.SetActivityStreamsTo(oTo)
}
}
for iter := oTo.Begin(); iter != oTo.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
objsTo[i][id.String()] = id
}
// Object bto
objsBto[i] = make(map[string]*url.URL)
var oBto vocab.ActivityStreamsBtoProperty
if tr, ok := iter.GetType().(btoer); !ok {
return fmt.Errorf("the Create object at %d has no 'bto' property", i)
} else {
oBto = tr.GetActivityStreamsBto()
if oBto == nil {
oBto = streams.NewActivityStreamsBtoProperty()
tr.SetActivityStreamsBto(oBto)
}
}
for iter := oBto.Begin(); iter != oBto.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
objsBto[i][id.String()] = id
}
// Object cc
objsCc[i] = make(map[string]*url.URL)
var oCc vocab.ActivityStreamsCcProperty
if tr, ok := iter.GetType().(ccer); !ok {
return fmt.Errorf("the Create object at %d has no 'cc' property", i)
} else {
oCc = tr.GetActivityStreamsCc()
if oCc == nil {
oCc = streams.NewActivityStreamsCcProperty()
tr.SetActivityStreamsCc(oCc)
}
}
for iter := oCc.Begin(); iter != oCc.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
objsCc[i][id.String()] = id
}
// Object bcc
objsBcc[i] = make(map[string]*url.URL)
var oBcc vocab.ActivityStreamsBccProperty
if tr, ok := iter.GetType().(bccer); !ok {
return fmt.Errorf("the Create object at %d has no 'bcc' property", i)
} else {
oBcc = tr.GetActivityStreamsBcc()
if oBcc == nil {
oBcc = streams.NewActivityStreamsBccProperty()
tr.SetActivityStreamsBcc(oBcc)
}
}
for iter := oBcc.Begin(); iter != oBcc.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
objsBcc[i][id.String()] = id
}
// Object audience
objsAudience[i] = make(map[string]*url.URL)
var oAudience vocab.ActivityStreamsAudienceProperty
if tr, ok := iter.GetType().(audiencer); !ok {
return fmt.Errorf("the Create object at %d has no 'audience' property", i)
} else {
oAudience = tr.GetActivityStreamsAudience()
if oAudience == nil {
oAudience = streams.NewActivityStreamsAudienceProperty()
tr.SetActivityStreamsAudience(oAudience)
}
}
for iter := oAudience.Begin(); iter != oAudience.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
objsAudience[i][id.String()] = id
}
// Phase 2: Apply missing recipients to the object from the
// activity.
//
// Activity to -> Object to
for k, v := range actorToMap {
if _, ok := objsTo[i][k]; !ok {
oTo.AppendIRI(v)
}
}
// Activity bto -> Object bto
for k, v := range actorBtoMap {
if _, ok := objsBto[i][k]; !ok {
oBto.AppendIRI(v)
}
}
// Activity cc -> Object cc
for k, v := range actorCcMap {
if _, ok := objsCc[i][k]; !ok {
oCc.AppendIRI(v)
}
}
// Activity bcc -> Object bcc
for k, v := range actorBccMap {
if _, ok := objsBcc[i][k]; !ok {
oBcc.AppendIRI(v)
}
}
// Activity audience -> Object audience
for k, v := range actorAudienceMap {
if _, ok := objsAudience[i][k]; !ok {
oAudience.AppendIRI(v)
}
}
}
// Phase 3: Apply missing recipients to the activity from the objects.
//
// Object to -> Activity to
for i := 0; i < len(objsTo); i++ {
for k, v := range objsTo[i] {
if _, ok := actorToMap[k]; !ok {
actorTo.AppendIRI(v)
}
}
}
// Object bto -> Activity bto
for i := 0; i < len(objsBto); i++ {
for k, v := range objsBto[i] {
if _, ok := actorBtoMap[k]; !ok {
actorBto.AppendIRI(v)
}
}
}
// Object cc -> Activity cc
for i := 0; i < len(objsCc); i++ {
for k, v := range objsCc[i] {
if _, ok := actorCcMap[k]; !ok {
actorCc.AppendIRI(v)
}
}
}
// Object bcc -> Activity bcc
for i := 0; i < len(objsBcc); i++ {
for k, v := range objsBcc[i] {
if _, ok := actorBccMap[k]; !ok {
actorBcc.AppendIRI(v)
}
}
}
// Object audience -> Activity audience
for i := 0; i < len(objsAudience); i++ {
for k, v := range objsAudience[i] {
if _, ok := actorAudienceMap[k]; !ok {
actorAudience.AppendIRI(v)
}
}
}
return nil
}
// toTombstone creates a Tombstone object for the given ActivityStreams value.
func toTombstone(obj vocab.Type, id *url.URL, now time.Time) vocab.ActivityStreamsTombstone {
tomb := streams.NewActivityStreamsTombstone()
// id property
idProp := streams.NewJSONLDIdProperty()
idProp.Set(id)
tomb.SetJSONLDId(idProp)
// formerType property
former := streams.NewActivityStreamsFormerTypeProperty()
tomb.SetActivityStreamsFormerType(former)
// Populate Former Type
former.AppendXMLSchemaString(obj.GetTypeName())
// Copy over the published property if it existed
if pubber, ok := obj.(publisheder); ok {
if pub := pubber.GetActivityStreamsPublished(); pub != nil {
tomb.SetActivityStreamsPublished(pub)
}
}
// Copy over the updated property if it existed
if upder, ok := obj.(updateder); ok {
if upd := upder.GetActivityStreamsUpdated(); upd != nil {
tomb.SetActivityStreamsUpdated(upd)
}
}
// Set deleted time to now.
deleted := streams.NewActivityStreamsDeletedProperty()
deleted.Set(now)
tomb.SetActivityStreamsDeleted(deleted)
return tomb
}
// mustHaveActivityActorsMatchObjectActors ensures that the actors on types in
// the 'object' property are all listed in the 'actor' property.
func mustHaveActivityActorsMatchObjectActors(c context.Context,
actors vocab.ActivityStreamsActorProperty,
op vocab.ActivityStreamsObjectProperty,
newTransport func(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t Transport, err error),
boxIRI *url.URL) error {
activityActorMap := make(map[string]bool, actors.Len())
for iter := actors.Begin(); iter != actors.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
activityActorMap[id.String()] = true
}
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
iri, err := ToId(iter)
if err != nil {
return err
}
// Attempt to dereference the IRI, regardless whether it is a
// type or IRI
tport, err := newTransport(c, boxIRI, goFedUserAgent())
if err != nil {
return err
}
b, err := tport.Dereference(c, iri)
if err != nil {
return err
}
var m map[string]interface{}
if err = json.Unmarshal(b, &m); err != nil {
return err
}
t, err := streams.ToType(c, m)
if err != nil {
return err
}
ac, ok := t.(actorer)
if !ok {
return fmt.Errorf("cannot verify actors: object value has no 'actor' property")
}
objActors := ac.GetActivityStreamsActor()
for iter := objActors.Begin(); iter != objActors.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
if !activityActorMap[id.String()] {
return fmt.Errorf("activity does not have all actors from its object's actors")
}
}
}
return nil
}
// add implements the logic of adding object ids to a target Collection or
// OrderedCollection. This logic is shared by both the C2S and S2S protocols.
func add(c context.Context,
op vocab.ActivityStreamsObjectProperty,
target vocab.ActivityStreamsTargetProperty,
db Database) error {
opIds := make([]*url.URL, 0, op.Len())
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
opIds = append(opIds, id)
}
targetIds := make([]*url.URL, 0, op.Len())
for iter := target.Begin(); iter != target.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
targetIds = append(targetIds, id)
}
// Create anonymous loop function to be able to properly scope the defer
// for the database lock at each iteration.
loopFn := func(t *url.URL) error {
if err := db.Lock(c, t); err != nil {
return err
}
defer db.Unlock(c, t)
if owns, err := db.Owns(c, t); err != nil {
return err
} else if !owns {
return nil
}
tp, err := db.Get(c, t)
if err != nil {
return err
}
if streams.IsOrExtendsActivityStreamsOrderedCollection(tp) {
oi, ok := tp.(orderedItemser)
if !ok {
return fmt.Errorf("type extending from OrderedCollection cannot convert to orderedItemser interface")
}
oiProp := oi.GetActivityStreamsOrderedItems()
if oiProp == nil {
oiProp = streams.NewActivityStreamsOrderedItemsProperty()
oi.SetActivityStreamsOrderedItems(oiProp)
}
for _, objId := range opIds {
oiProp.AppendIRI(objId)
}
} else if streams.IsOrExtendsActivityStreamsCollection(tp) {
i, ok := tp.(itemser)
if !ok {
return fmt.Errorf("type extending from Collection cannot convert to itemser interface")
}
iProp := i.GetActivityStreamsItems()
if iProp == nil {
iProp = streams.NewActivityStreamsItemsProperty()
i.SetActivityStreamsItems(iProp)
}
for _, objId := range opIds {
iProp.AppendIRI(objId)
}
} else {
return fmt.Errorf("target in Add is neither a Collection nor an OrderedCollection")
}
err = db.Update(c, tp)
if err != nil {
return err
}
return nil
}
for _, t := range targetIds {
if err := loopFn(t); err != nil {
return err
}
}
return nil
}
// remove implements the logic of removing object ids to a target Collection or
// OrderedCollection. This logic is shared by both the C2S and S2S protocols.
func remove(c context.Context,
op vocab.ActivityStreamsObjectProperty,
target vocab.ActivityStreamsTargetProperty,
db Database) error {
opIds := make(map[string]bool, op.Len())
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
opIds[id.String()] = true
}
targetIds := make([]*url.URL, 0, op.Len())
for iter := target.Begin(); iter != target.End(); iter = iter.Next() {
id, err := ToId(iter)
if err != nil {
return err
}
targetIds = append(targetIds, id)
}
// Create anonymous loop function to be able to properly scope the defer
// for the database lock at each iteration.
loopFn := func(t *url.URL) error {
if err := db.Lock(c, t); err != nil {
return err
}
defer db.Unlock(c, t)
if owns, err := db.Owns(c, t); err != nil {
return err
} else if !owns {
return nil
}
tp, err := db.Get(c, t)
if err != nil {
return err
}
if streams.IsOrExtendsActivityStreamsOrderedCollection(tp) {
oi, ok := tp.(orderedItemser)
if !ok {
return fmt.Errorf("type extending from OrderedCollection cannot convert to orderedItemser interface")
}
oiProp := oi.GetActivityStreamsOrderedItems()
if oiProp != nil {
for i := 0; i < oiProp.Len(); /*Conditional*/ {
id, err := ToId(oiProp.At(i))
if err != nil {
return err
}
if opIds[id.String()] {
oiProp.Remove(i)
} else {
i++
}
}
}
} else if streams.IsOrExtendsActivityStreamsCollection(tp) {
i, ok := tp.(itemser)
if !ok {
return fmt.Errorf("type extending from Collection cannot convert to itemser interface")
}
iProp := i.GetActivityStreamsItems()
if iProp != nil {
for i := 0; i < iProp.Len(); /*Conditional*/ {
id, err := ToId(iProp.At(i))
if err != nil {
return err
}
if opIds[id.String()] {
iProp.Remove(i)
} else {
i++
}
}
}
} else {
return fmt.Errorf("target in Remove is neither a Collection nor an OrderedCollection")
}
err = db.Update(c, tp)
if err != nil {
return err
}
return nil
}
for _, t := range targetIds {
if err := loopFn(t); err != nil {
return err
}
}
return nil
}
// clearSensitiveFields removes the 'bto' and 'bcc' entries on the given value
// and recursively on every 'object' property value.
func clearSensitiveFields(obj vocab.Type) {
if t, ok := obj.(btoer); ok {
t.SetActivityStreamsBto(nil)
}
if t, ok := obj.(bccer); ok {
t.SetActivityStreamsBcc(nil)
}
if t, ok := obj.(objecter); ok {
op := t.GetActivityStreamsObject()
if op != nil {
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
clearSensitiveFields(iter.GetType())
}
}
}
}
// requestId forms an ActivityPub id based on the HTTP request. Always assumes
// that the id is HTTPS.
func requestId(r *http.Request, scheme string) *url.URL {
id := r.URL
id.Host = r.Host
id.Scheme = scheme
return id
}

15
vendor/github.com/go-fed/activity/pub/version.go generated vendored Normal file
View file

@ -0,0 +1,15 @@
package pub
import (
"fmt"
)
const (
// Version string, used in the User-Agent
version = "v1.0.0"
)
// goFedUserAgent returns the user agent string for the go-fed library.
func goFedUserAgent() string {
return fmt.Sprintf("(go-fed/activity %s)", version)
}