activity/pub/fed.go

598 行
18 KiB
Go

package pub
import (
"encoding/json"
"fmt"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/vocab"
"io/ioutil"
"net/http"
"net/url"
)
// Object is the interface for ActivityPub compliant ActivityStream types.
//
// ActivityPub obects must have 'id' and 'type' properties to be spec compliant.
// This library enforces the "MUST" requirement and purposefully does NOT
// support transient objects.
//
// Furthermore, the spec extends the Object ActivityStream type to also contain
// the 'source' property, whose value appears to be an Object with only
// 'content' and 'mediaType' properties being semantically useful.
//
// TODO: Possibly delete this interface
type Object interface {
GetId() (streams.Resolution, url.URL)
SetId(url.URL)
HasId() streams.Presence
LenType() int
GetType(int) (streams.Resolution, string)
AddType(interface{})
RemoveType(int)
ResolveSource(*streams.Resolver) (streams.Resolution, error)
HasSource() streams.Presence
SetSource(vocab.ObjectType)
Serialize() (m map[string]interface{}, err error)
}
var _ Object = &streams.Object{}
// Actor is the interface for ActivityPub compliant ActivityStream types.
//
// ActivityPub actors must have a valid unique IRI as the 'id' property in
// addition to the 'type' property. Furthermore, actors must have the 'inbox'
// and 'outbox' properties to be considered actors. There are more suggested
// properties by the spec, which we include here.
//
// TODO: Possibly delete this interface
type Actor interface {
GetId() (streams.Resolution, url.URL)
SetId(url.URL)
LenType() int
GetType(int) (streams.Resolution, string)
AddType(interface{})
RemoveType(int)
GetInbox() (streams.Resolution, url.URL)
HasInbox() streams.Presence
SetInbox(url.URL)
GetOutbox() (streams.Resolution, url.URL)
HasOutbox() streams.Presence
SetOutbox(url.URL)
GetFollowing() (streams.Resolution, url.URL)
HasFollowing() streams.Presence
SetFollowing(url.URL)
GetFollowers() (streams.Resolution, url.URL)
HasFollowers() streams.Presence
SetFollowers(url.URL)
GetLiked() (streams.Resolution, url.URL)
HasLiked() streams.Presence
SetLiked(url.URL)
LenStreams() int
GetStreams(int) (streams.Resolution, url.URL)
AddStreams(vocab.CollectionType)
RemoveStreams(int)
GetPreferredUsername() (streams.Resolution, string)
HasPreferredUsername() streams.Presence
SetPreferredUsername(string)
PreferredUsernameLanguages() []string
GetPreferredUsernameForLanguage(string) string
SetPreferredUsernameForLanguage(string, string)
ResolveEndpoints(streams.Resolver) (streams.Resolution, error)
HasEndpoints() streams.Presence
SetEndpoints(vocab.ObjectType)
Serialize() (m map[string]interface{}, err error)
}
var _ Object = &streams.Object{}
// Endpoint is a logical grouping of specific properties within the ActivityPub
// specification.
//
// TODO: Possibly delete this interface
type Endpoint interface {
GetProxyUrl() (streams.Resolution, url.URL)
HasProxyUrl() streams.Presence
SetProxyUrl(url.URL)
GetOauthAuthorizationEndpoint() (streams.Resolution, url.URL)
HasOauthAuthorizationEndpoint() streams.Presence
SetOauthAuthorizationEndpoint(url.URL)
GetOauthTokenEndpoint() (streams.Resolution, url.URL)
HasOauthTokenEndpoint() streams.Presence
SetOauthTokenEndpoint(url.URL)
GetProvideClientKey() (streams.Resolution, url.URL)
HasProvideClientKey() streams.Presence
SetProvideClientKey(url.URL)
GetSignClientKey() (streams.Resolution, url.URL)
HasSignClientKey() streams.Presence
SetSignClientKey(url.URL)
GetSharedInbox() (streams.Resolution, url.URL)
HasSharedInbox() streams.Presence
SetSharedInbox(url.URL)
}
var _ Endpoint = &streams.Object{}
// Typer is an object that has a type.
type Typer interface {
TypeLen() (l int)
GetType(index int) (v interface{})
}
type typeIder interface {
Typer
SetId(v url.URL)
Serialize() (m map[string]interface{}, e error)
}
// Application is provided by users of this library in order to
type Application interface {
// GetInbox returns the OrderedCollection inbox of the actor with the
// provided ID. It is up to the implementation to provide the correct
// collection for the kind of authorization given in the request.
GetInbox(id string, r *http.Request) (vocab.OrderedCollectionType, error)
// GetOutbox returns the OrderedCollection inbox of the actor with the
// provided ID. It is up to the implementation to provide the correct
// collection for the kind of authorization given in the request.
GetOutbox(id string, r *http.Request) (vocab.OrderedCollectionType, error)
// PostOutboxAuthorized determines whether the request is able to post
// an Activity to the outbox for the specified id.
PostOutboxAuthorized(id string, r *http.Request) (bool, error)
// NewId takes in a client id token and returns an ActivityStreams IRI
// id for a new Activity posted to the outbox. The object is provided
// as a Typer so clients can use it to decide how to generate the IRI.
NewId(id string, t Typer) url.URL
// AddToOutboxResolver returns the client's Resolver which must store
// the provided object in the outbox of the user represented by the
// client id token.
AddToOutboxResolver(id string) (*streams.Resolver, error)
// ActorIRI returns the actor's IRI associated with the given client ID
// token.
ActorIRI(id string) (url.URL, error)
}
// Receiver is provided by users of this library and designed to handle
// receiving federated messages through the Federated Protocol.
type Receiver interface {
// Create requires the client application to persist the 'object' that
// was created.
Create(id string, s *streams.Create) error
// Update should completely replace the 'object' with the same 'id'.
Update(id string, s *streams.Update) error
// Delete SHOULD completely remove the 'object' with its 'id', or have
// the 'object' be replaced by a 'Tombstone' ActivityStream type.
Delete(id string, s *streams.Delete) error
// Follow means the client application SHOULD reply with an 'Accept' or
// 'Reject' ActivityStream with the 'Follow' as the 'object' and deliver
// it to the 'actor' of the 'Follow'. This can be human-triggered or
// automatically triggered.
Follow(id string, s *streams.Follow) error
// Accept can be client application specific. However, if this 'Accept'
// is in response to a 'Follow' then the follower should be added by the
// client application.
Accept(id string, s *streams.Accept) error
// Reject can be client application specific. However, if this 'Reject'
// is in response to a 'Follow' then the client MUST NOT go forward with
// adding the follower.
Reject(id string, s *streams.Reject) error
// Add is client application specific, generally involving adding an
// 'object' to a specific 'target' collection.
Add(id string, s *streams.Add) error
// Remove is client application specific, generally involving removing
// an 'object' from a specific 'target' collection.
Remove(id string, s *streams.Remove) error
// Like triggers adding the like to an object's `like` collection.
Like(id string, s *streams.Like) error
// Undo negates a previous action. The 'actor' on the 'Undo' MUST be the
// same as the 'actor' on the Activity being undone, and the client
// application is responsible for enforcing this.
Undo(id string, s *streams.Undo) error
}
type federator struct {
// App is the client application that is ActivityPub aware.
//
// It is always required.
App Application
// Receiver provides callbacks when handling incoming messages received
// via the Federated Protocol.
//
// It is only required if EnableServer is true.
Receiver Receiver
// Client is used to federate with other ActivityPub servers.
//
// It is only required if EnableServer is true.
Client *http.Client
// Agent is the User-Agent string to use in HTTP headers when
// federating with another server. It will automatically be appended
// with '(go-fed ActivityPub)'.
//
// It is only required if EnableServer is true.
Agent string
// MaxDeliveryDepth is how deep collections of recipients will be
// expanded for delivery. It must be at least 1 to be compliant with the
// ActivityPub spec.
//
// It is only required if EnableServer is true.
MaxDeliveryDepth int
// EnableClient enables or disables the Social API, the client to
// server part of ActivityPub. Useful if permitting remote clients to
// act on behalf of the users of the client application.
EnableClient bool
// EnableServer enables or disables the Federated Protocol, or the
// server to server part of ActivityPub. Useful to permit integrating
// with the rest of the federative web.
EnableServer bool
// pool properly handles rate-limiting and retrying deliveries to other
// federated servers.
//
// It is only required if EnableServer is true.
pool *delivererPool
}
func (f *federator) Stop() {
if f.pool != nil {
f.pool.Stop()
}
}
func (f *federator) Errors() <-chan error {
if f.pool != nil {
return f.pool.Errors()
}
return nil
}
func (f *federator) PostInbox(id string) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) (bool, error) {
if !isActivityPubPost(r) {
return false, nil
}
if !f.EnableServer {
w.WriteHeader(http.StatusMethodNotAllowed)
return true, nil
}
b, err := ioutil.ReadAll(r.Body)
if err != nil {
return true, err
}
var m map[string]interface{}
if err = json.Unmarshal(b, &m); err != nil {
return true, err
}
if err = f.getPostInboxResolver(id).Deserialize(m); err != nil {
return true, err
}
// TODO: 7.1.2 Inbox forwarding
w.WriteHeader(http.StatusOK)
return true, nil
}
}
func (f *federator) GetInbox(id string) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) (bool, error) {
if !isActivityPubGet(r) {
return false, nil
}
oc, err := f.App.GetInbox(id, r)
if err != nil {
return true, err
}
oc, err = f.dedupeOrderedItems(oc)
if err != nil {
return true, err
}
m, err := oc.Serialize()
if err != nil {
return true, err
}
b, err := json.Marshal(m)
if err != nil {
return true, err
}
w.Header().Set(contentTypeHeader, responseContentTypeHeader)
w.WriteHeader(http.StatusOK)
n, err := w.Write(b)
if err != nil {
return true, err
} else if n != len(b) {
return true, fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(b))
}
return true, nil
}
}
func (f *federator) PostOutbox(id string) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) (bool, error) {
if !isActivityPubPost(r) {
return false, nil
}
if !f.EnableClient {
w.WriteHeader(http.StatusMethodNotAllowed)
return true, nil
}
ok, err := f.App.PostOutboxAuthorized(id, r)
if err != nil {
return true, err
}
if !ok {
w.WriteHeader(http.StatusForbidden)
return true, nil
}
b, err := ioutil.ReadAll(r.Body)
if err != nil {
return true, err
}
var m map[string]interface{}
if err = json.Unmarshal(b, &m); err != nil {
return true, err
}
typer, err := toTypeIder(m)
if err != nil {
return true, err
}
newId := f.App.NewId(id, typer)
if !isActivityType(typer) {
actorIri, err := f.App.ActorIRI(id)
if err != nil {
return true, err
}
obj, ok := typer.(vocab.ObjectType)
if !ok {
return true, fmt.Errorf("wrap in create: cannot convert to vocab.ObjectType: %T", typer)
}
typer = f.wrapInCreate(obj, actorIri)
}
typer.SetId(newId)
if m, err = typer.Serialize(); err != nil {
return true, err
}
outboxAdder, err := f.App.AddToOutboxResolver(id)
if err != nil {
return true, err
}
if err = outboxAdder.Deserialize(m); err != nil {
return true, err
}
if err = f.getPostOutboxResolver(id).Deserialize(m); err != nil {
return true, err
}
if f.EnableServer {
obj, err := toAnyActivity(m)
if err != nil {
return true, err
}
recipients, err := f.prepare(obj)
if err != nil {
return true, err
}
m, err := obj.Serialize()
if err != nil {
return true, err
}
m["@context"] = "https://www.w3.org/ns/activitystreams"
b, err := json.Marshal(m)
if err != nil {
return true, err
}
for _, to := range recipients {
f.pool.Do(b, to, f.postToOutbox)
}
}
w.Header().Set("Location", newId.String())
w.WriteHeader(http.StatusCreated)
return true, nil
}
}
func (f *federator) GetOutbox(id string) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) (bool, error) {
if !isActivityPubGet(r) {
return false, nil
}
oc, err := f.App.GetOutbox(id, r)
if err != nil {
return true, err
}
m, err := oc.Serialize()
if err != nil {
return true, err
}
b, err := json.Marshal(m)
if err != nil {
return true, err
}
w.Header().Set(contentTypeHeader, responseContentTypeHeader)
w.WriteHeader(http.StatusOK)
n, err := w.Write(b)
if err != nil {
return true, err
} else if n != len(b) {
return true, fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(b))
}
return true, nil
}
}
func (f *federator) getPostOutboxResolver(id string) *streams.Resolver {
return &streams.Resolver{
CreateCallback: f.handleClientCreate(id),
UpdateCallback: f.handleClientUpdate(id),
DeleteCallback: f.handleClientDelete(id),
FollowCallback: f.handleClientFollow(id),
AcceptCallback: f.handleClientAccept(id),
RejectCallback: f.handleClientReject(id),
AddCallback: f.handleClientAdd(id),
RemoveCallback: f.handleClientRemove(id),
LikeCallback: f.handleClientLike(id),
UndoCallback: f.handleClientUndo(id),
BlockCallback: f.handleClientBlock(id),
// TODO: Extended activity types, such as Announce, Arrive, etc.
}
}
func (f *federator) handleClientCreate(id string) func(s *streams.Create) error {
return func(s *streams.Create) error {
// TODO: Enforce object
// TODO: Implement
return nil
}
}
func (f *federator) handleClientUpdate(id string) func(s *streams.Update) error {
return func(s *streams.Update) error {
// TODO: Enforce object
// TODO: Implement
return nil
}
}
func (f *federator) handleClientDelete(id string) func(s *streams.Delete) error {
return func(s *streams.Delete) error {
// TODO: Enforce object
// TODO: Implement
return nil
}
}
func (f *federator) handleClientFollow(id string) func(s *streams.Follow) error {
return func(s *streams.Follow) error {
// TODO: Enforce object
// TODO: Implement
return nil
}
}
func (f *federator) handleClientAccept(id string) func(s *streams.Accept) error {
return func(s *streams.Accept) error {
// TODO: Implement
return nil
}
}
func (f *federator) handleClientReject(id string) func(s *streams.Reject) error {
return func(s *streams.Reject) error {
// TODO: Implement
return nil
}
}
func (f *federator) handleClientAdd(id string) func(s *streams.Add) error {
return func(s *streams.Add) error {
// TODO: Enforce object & target
// TODO: Implement
return nil
}
}
func (f *federator) handleClientRemove(id string) func(s *streams.Remove) error {
return func(s *streams.Remove) error {
// TODO: Enforce object & target
// TODO: Implement
return nil
}
}
func (f *federator) handleClientLike(id string) func(s *streams.Like) error {
return func(s *streams.Like) error {
// TODO: Enforce object
// TODO: Implement
return nil
}
}
func (f *federator) handleClientUndo(id string) func(s *streams.Undo) error {
return func(s *streams.Undo) error {
// TODO: Enforce object
// TODO: Implement
return nil
}
}
func (f *federator) handleClientBlock(id string) func(s *streams.Block) error {
return func(s *streams.Block) error {
// TODO: Enforce object
// TODO: Implement
return nil
}
}
func (f *federator) getPostInboxResolver(id string) *streams.Resolver {
return &streams.Resolver{
CreateCallback: f.handleCreate(id),
UpdateCallback: f.handleUpdate(id),
DeleteCallback: f.handleDelete(id),
FollowCallback: f.handleFollow(id),
AcceptCallback: f.handleAccept(id),
RejectCallback: f.handleReject(id),
AddCallback: f.handleAdd(id),
RemoveCallback: f.handleRemove(id),
LikeCallback: f.handleLike(id),
UndoCallback: f.handleUndo(id),
// TODO: Extended activity types, such as Announce, Arrive, etc.
}
}
func (f *federator) handleCreate(id string) func(s *streams.Create) error {
return func(s *streams.Create) error {
return f.Receiver.Create(id, s)
}
}
func (f *federator) handleUpdate(id string) func(s *streams.Update) error {
return func(s *streams.Update) error {
// TODO: The receiving server MUST take care to be sure that the Update
// is authorized to modify its object.
return f.Receiver.Update(id, s)
}
}
func (f *federator) handleDelete(id string) func(s *streams.Delete) error {
return func(s *streams.Delete) error {
// TODO: Verify ownership. I think the spec unintentionally suggests to
// just assume it is owned, so we will actually verify.
return f.Receiver.Delete(id, s)
}
}
func (f *federator) handleFollow(id string) func(s *streams.Follow) error {
return func(s *streams.Follow) error {
return f.Receiver.Follow(id, s)
}
}
func (f *federator) handleAccept(id string) func(s *streams.Accept) error {
return func(s *streams.Accept) error {
return f.Receiver.Accept(id, s)
}
}
func (f *federator) handleReject(id string) func(s *streams.Reject) error {
return func(s *streams.Reject) error {
return f.Receiver.Reject(id, s)
}
}
func (f *federator) handleAdd(id string) func(s *streams.Add) error {
return func(s *streams.Add) error {
return f.Receiver.Add(id, s)
}
}
func (f *federator) handleRemove(id string) func(s *streams.Remove) error {
return func(s *streams.Remove) error {
return f.Receiver.Remove(id, s)
}
}
func (f *federator) handleLike(id string) func(s *streams.Like) error {
return func(s *streams.Like) error {
return f.Receiver.Like(id, s)
}
}
func (f *federator) handleUndo(id string) func(s *streams.Undo) error {
return func(s *streams.Undo) error {
return f.Receiver.Undo(id, s)
}
}