Folded back handlers typer functionality into the activitypub package

This commit is contained in:
Marius Orcsik 2022-05-30 14:02:51 +02:00
commit 051d30fa3f
No known key found for this signature in database
GPG key ID: DBF5E47F5DBC4D21
9 changed files with 606 additions and 180 deletions

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017 Marius Orcsik
Copyright (c) 2017 Golang ActitvityPub
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -143,18 +143,12 @@ type Collection struct {
type (
// FollowersCollection is a collection of followers
FollowersCollection = Followers
// Followers is a Collection type
Followers = Collection
FollowersCollection = Collection
// FollowingCollection is a list of everybody that the actor has followed, added as a side effect.
// The following collection MUST be either an OrderedCollection or a Collection and MAY
// be filtered on privileges of an authenticated user or as appropriate when no authentication is given.
FollowingCollection = Following
// Following is a type alias for a simple Collection
Following = Collection
FollowingCollection = Collection
)
// CollectionNew initializes a new Collection
@ -347,20 +341,6 @@ func ToCollection(it Item) (*Collection, error) {
return nil, ErrorInvalidType[Collection](it)
}
// FollowingNew initializes a new Following
func FollowingNew() *Following {
id := ID("following")
i := Following{ID: id, Type: CollectionType}
i.Name = NaturalLanguageValuesNew()
i.Content = NaturalLanguageValuesNew()
i.Summary = NaturalLanguageValuesNew()
i.TotalItems = 0
return &i
}
// ItemsMatch
func (c Collection) ItemsMatch(col ...Item) bool {
for _, it := range col {

View file

@ -27,7 +27,7 @@ func TestCollection_Append(t *testing.T) {
c.Append(val)
if c.Count() != 1 {
t.Errorf("Inbox collection of %q should have one element", c.GetID())
t.Errorf("Inbox collectionPath of %q should have one element", c.GetID())
}
if !reflect.DeepEqual(c.Items[0], val) {
t.Errorf("First item in Inbox is does not match %q", val.ID)

2
go.mod
View file

@ -4,6 +4,8 @@ go 1.18
require (
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20200411073322-f0bcc40f0bf2
github.com/go-ap/errors v0.0.0-20220529131844-4c7dbeabb369
github.com/go-ap/jsonld v0.0.0-20200327122108-fafac2de2660
github.com/go-ap/storage v0.0.0-20220529132413-43d0dcf851c6
github.com/valyala/fastjson v1.6.3
)

View file

@ -40,6 +40,27 @@ type WithOrderedCollectionPageFn func(*OrderedCollectionPage) error
// WithItemCollectionFn represents a function type that can be used as a parameter for OnItemCollection helper function
type WithItemCollectionFn func(*ItemCollection) error
/*
func To[T Objects](it Item) (*T, error) {
ob, ok := it.(T)
if !ok {
return nil, errors.New("Invalid cast to %T for object %T", T, it)
}
return &ob, nil
}
func On[T Objects](it Item, fn func(ob *T)) error {
if IsNil(it) {
return nil
}
ob, err := To[T](it)
if err != nil {
return err
}
return fn(ob)
}
*/
// OnLink calls function fn on it Item if it can be asserted to type *Link
//
// This function should be safe to use for all types with a structure compatible

View file

@ -126,45 +126,30 @@ type (
// In general, the owner of an inbox is likely to be able to access all of their inbox contents.
// Depending on access control, some other content may be public, whereas other content may
// require authentication for non-owner users, if they can access the inbox at all.
InboxStream = Inbox
// Inbox is a type alias for an Ordered Collection
Inbox = OrderedCollection
InboxStream = OrderedCollection
// LikedCollection is a list of every object from all of the actor's Like activities,
// added as a side effect. The liked collection MUST be either an OrderedCollection or
// a Collection and MAY be filtered on privileges of an authenticated user or as
// appropriate when no authentication is given.
LikedCollection = Liked
// Liked is a type alias for an Ordered Collection
Liked = OrderedCollection
LikedCollection = OrderedCollection
// LikesCollection is a list of all Like activities with this object as the object property,
// added as a side effect. The likes collection MUST be either an OrderedCollection or a Collection
// and MAY be filtered on privileges of an authenticated user or as appropriate when
// no authentication is given.
LikesCollection = Likes
// Likes is a type alias for an Ordered Collection
Likes = OrderedCollection
LikesCollection = OrderedCollection
// OutboxStream contains activities the user has published,
// subject to the ability of the requestor to retrieve the activity (that is,
// the contents of the outbox are filtered by the permissions of the person reading it).
OutboxStream = Outbox
// Outbox is a type alias for an Ordered Collection
Outbox = OrderedCollection
OutboxStream = OrderedCollection
// SharesCollection is a list of all Announce activities with this object as the object property,
// added as a side effect. The shares collection MUST be either an OrderedCollection or a Collection
// and MAY be filtered on privileges of an authenticated user or as appropriate when no authentication
// is given.
SharesCollection = Shares
// Shares is a type alias for an Ordered Collection
Shares = OrderedCollection
SharesCollection = OrderedCollection
)
// GetType returns the OrderedCollection's type
@ -382,71 +367,6 @@ func copyOrderedCollectionToPage(c *OrderedCollection, p *OrderedCollectionPage)
return nil
}
// InboxNew initializes a new Inbox
func InboxNew() *OrderedCollection {
id := ID("inbox")
i := OrderedCollection{ID: id, Type: CollectionType}
i.Name = NaturalLanguageValuesNew()
i.Content = NaturalLanguageValuesNew()
i.TotalItems = 0
return &i
}
// LikedCollection initializes a new outbox
func LikedNew() *OrderedCollection {
id := ID("liked")
l := OrderedCollection{ID: id, Type: CollectionType}
l.Name = NaturalLanguageValuesNew()
l.Content = NaturalLanguageValuesNew()
l.TotalItems = 0
return &l
}
// LikesCollection initializes a new outbox
func LikesNew() *Likes {
id := ID("likes")
l := Likes{ID: id, Type: CollectionType}
l.Name = NaturalLanguageValuesNew()
l.Content = NaturalLanguageValuesNew()
l.TotalItems = 0
return &l
}
// OutboxNew initializes a new outbox
func OutboxNew() *Outbox {
id := ID("outbox")
i := Outbox{ID: id, Type: OrderedCollectionType}
i.Name = NaturalLanguageValuesNew()
i.Content = NaturalLanguageValuesNew()
i.TotalItems = 0
i.OrderedItems = make(ItemCollection, 0)
return &i
}
// SharesNew initializes a new Shares
func SharesNew() *Shares {
id := ID("Shares")
i := Shares{ID: id, Type: CollectionType}
i.Name = NaturalLanguageValuesNew()
i.Content = NaturalLanguageValuesNew()
i.TotalItems = 0
return &i
}
// ItemsMatch
func (o OrderedCollection) ItemsMatch(col ...Item) bool {
for _, it := range col {

View file

@ -269,77 +269,6 @@ func TestOrderedCollection_Contains(t *testing.T) {
t.Skipf("TODO")
}
func TestInboxNew(t *testing.T) {
i := InboxNew()
id := ID("inbox")
if i.ID != id {
t.Errorf("%T should be initialized with %q as %T", i, id, id)
}
if len(i.Name) != 0 {
t.Errorf("%T should be initialized with 0 length Name", i)
}
if len(i.Content) != 0 {
t.Errorf("%T should be initialized with 0 length Content", i)
}
if len(i.Summary) != 0 {
t.Errorf("%T should be initialized with 0 length Summary", i)
}
if i.TotalItems != 0 {
t.Errorf("%T should be initialized with 0 TotalItems", i)
}
}
func TestLikedNew(t *testing.T) {
l := LikedNew()
id := ID("liked")
if l.ID != id {
t.Errorf("%T should be initialized with %q as %T", l, id, id)
}
if len(l.Name) != 0 {
t.Errorf("%T should be initialized with 0 length Name", l)
}
if len(l.Content) != 0 {
t.Errorf("%T should be initialized with 0 length Content", l)
}
if len(l.Summary) != 0 {
t.Errorf("%T should be initialized with 0 length Summary", l)
}
if l.TotalItems != 0 {
t.Errorf("%T should be initialized with 0 TotalItems", l)
}
}
func TestLikesNew(t *testing.T) {
t.Skipf("TODO")
}
func TestOutboxNew(t *testing.T) {
o := OutboxNew()
id := ID("outbox")
if o.ID != id {
t.Errorf("%T should be initialized with %q as %T", o, id, id)
}
if len(o.Name) != 0 {
t.Errorf("%T should be initialized with 0 length Name", o)
}
if len(o.Content) != 0 {
t.Errorf("%T should be initialized with 0 length Content", o)
}
if len(o.Summary) != 0 {
t.Errorf("%T should be initialized with 0 length Summary", o)
}
if o.TotalItems != 0 {
t.Errorf("%T should be initialized with 0 TotalItems", o)
}
}
func TestSharesNew(t *testing.T) {
t.Skipf("TODO")
}
func TestOrderedCollection_MarshalJSON(t *testing.T) {
t.Skipf("TODO")
}

292
typer.go Normal file
View file

@ -0,0 +1,292 @@
package activitypub
import (
"fmt"
"net/http"
"path"
"strings"
"github.com/go-ap/errors"
)
// collectionPath
type collectionPath string
// CollectionPaths
type CollectionPaths []collectionPath
const (
Unknown = collectionPath("")
Outbox = collectionPath("outbox")
Inbox = collectionPath("inbox")
Shares = collectionPath("shares")
Replies = collectionPath("replies") // activitystreams
Following = collectionPath("following")
Followers = collectionPath("followers")
Liked = collectionPath("liked")
Likes = collectionPath("likes")
)
func CollectionPath(s string) collectionPath {
return collectionPath(s)
}
// Typer is the static package variable that determines a collectionPath type for a particular request
// It can be overloaded from outside packages.
// @TODO(marius): This should be moved as a property on an instantiable package object, instead of keeping it here
var Typer CollectionTyper = pathTyper{}
// CollectionTyper allows external packages to tell us which collectionPath the current HTTP request addresses
type CollectionTyper interface {
Type(r *http.Request) collectionPath
}
type pathTyper struct{}
func (d pathTyper) Type(r *http.Request) collectionPath {
if r.URL == nil || len(r.URL.Path) == 0 {
return Unknown
}
col := Unknown
pathElements := strings.Split(r.URL.Path[1:], "/") // Skip first /
for i := len(pathElements) - 1; i >= 0; i-- {
col = collectionPath(pathElements[i])
if typ := getValidActivityCollection(col); typ != Unknown {
return typ
}
if typ := getValidObjectCollection(col); typ != Unknown {
return typ
}
}
return col
}
var (
validActivityCollection = CollectionPaths{
Outbox,
Inbox,
Likes,
Shares,
Replies, // activitystreams
}
OfObject = CollectionPaths{
Likes,
Shares,
Replies,
}
OfActor = CollectionPaths{
Outbox,
Inbox,
Liked,
Following,
Followers,
}
ActivityPubCollections = CollectionPaths{
Outbox,
Inbox,
Liked,
Following,
Followers,
Likes,
Shares,
Replies,
}
)
func (t CollectionPaths) Contains(typ collectionPath) bool {
for _, tt := range t {
if strings.ToLower(string(typ)) == string(tt) {
return true
}
}
return false
}
// Split splits the IRI in an actor IRI and its collectionPath
// if the collectionPath is found in the elements in the t CollectionPaths slice
func (t CollectionPaths) Split(i IRI) (IRI, collectionPath) {
maybeActor, maybeCol := path.Split(i.String())
tt := collectionPath(maybeCol)
if !t.Contains(tt) {
tt = ""
maybeActor = i.String()
}
iri := IRI(strings.TrimRight(maybeActor, "/"))
return iri, tt
}
// IRIf formats an IRI from an existing IRI and the collectionPath type
func IRIf(i IRI, t collectionPath) IRI {
onePastLast := len(i)
if onePastLast > 1 && i[onePastLast-1] == '/' {
i = i[:onePastLast-1]
}
return IRI(fmt.Sprintf("%s/%s", i, t))
}
// IRI gives us the IRI of the t collectionPath type corresponding to the i Item,
// or generates a new one if not found.
func (t collectionPath) IRI(i Item) IRI {
if IsNil(i) {
return IRIf("", t)
}
if IsObject(i) {
if it := t.Of(i); !IsNil(it) {
return it.GetLink()
}
}
return IRIf(i.GetLink(), t)
}
// Of gives us the property of the i Item that corresponds to the t collectionPath type.
func (t collectionPath) Of(i Item) Item {
if IsNil(i) || !i.IsObject() {
return nil
}
var it Item
if OfActor.Contains(t) && ActorTypes.Contains(i.GetType()) {
OnActor(i, func(a *Actor) error {
switch t {
case Inbox:
it = a.Inbox
case Outbox:
it = a.Outbox
case Liked:
it = a.Liked
case Following:
it = a.Following
case Followers:
it = a.Followers
}
return nil
})
}
OnObject(i, func(o *Object) error {
switch t {
case Likes:
it = o.Likes
case Shares:
it = o.Shares
case Replies:
it = o.Replies
}
return nil
})
return it
}
// OfActor returns the base IRI of received i, if i represents an IRI matching collectionPath type t
func (t collectionPath) OfActor(i IRI) (IRI, error) {
maybeActor, maybeCol := path.Split(i.String())
if strings.ToLower(maybeCol) == strings.ToLower(string(t)) {
maybeActor = strings.TrimRight(maybeActor, "/")
return IRI(maybeActor), nil
}
return EmptyIRI, errors.Newf("IRI does not represent a valid %s collectionPath", t)
}
// Split returns the base IRI of received i, if i represents an IRI matching collectionPath type t
func Split(i IRI) (IRI, collectionPath) {
return ActivityPubCollections.Split(i)
}
func getValidActivityCollection(t collectionPath) collectionPath {
if validActivityCollection.Contains(t) {
return t
}
return Unknown
}
// ValidActivityCollection shows if the current ActivityPub end-point type is a valid one for handling Activities
func ValidActivityCollection(typ collectionPath) bool {
return getValidActivityCollection(typ) != Unknown
}
var validObjectCollection = []collectionPath{
Following,
Followers,
Liked,
}
func getValidObjectCollection(typ collectionPath) collectionPath {
for _, t := range validObjectCollection {
if strings.ToLower(string(typ)) == string(t) {
return t
}
}
return Unknown
}
// ValidActivityCollection shows if the current ActivityPub end-point type is a valid one for handling Objects
func ValidObjectCollection(typ collectionPath) bool {
return getValidObjectCollection(typ) != Unknown
}
func getValidCollection(typ collectionPath) collectionPath {
if typ := getValidActivityCollection(typ); typ != Unknown {
return typ
}
if typ := getValidObjectCollection(typ); typ != Unknown {
return typ
}
return Unknown
}
func ValidCollection(typ collectionPath) bool {
return getValidCollection(typ) != Unknown
}
func ValidCollectionIRI(i IRI) bool {
_, t := Split(i)
return getValidCollection(t) != Unknown
}
// AddTo adds collectionPath type IRI on the corresponding property of the i Item
func (t collectionPath) AddTo(i Item) (IRI, bool) {
if IsNil(i) || !i.IsObject() {
return NilIRI, false
}
status := false
var iri IRI
if OfActor.Contains(t) {
OnActor(i, func(a *Actor) error {
if status = t == Inbox && IsNil(a.Inbox); status {
a.Inbox = IRIf(a.GetLink(), t)
iri = a.Inbox.GetLink()
} else if status = t == Outbox && IsNil(a.Outbox); status {
a.Outbox = IRIf(a.GetLink(), t)
iri = a.Outbox.GetLink()
} else if status = t == Liked && IsNil(a.Liked); status {
a.Liked = IRIf(a.GetLink(), t)
iri = a.Liked.GetLink()
} else if status = t == Following && IsNil(a.Following); status {
a.Following = IRIf(a.GetLink(), t)
iri = a.Following.GetLink()
} else if status = t == Followers && IsNil(a.Followers); status {
a.Followers = IRIf(a.GetLink(), t)
iri = a.Followers.GetLink()
}
return nil
})
} else if OfObject.Contains(t) {
OnObject(i, func(o *Object) error {
if status = t == Likes && IsNil(o.Likes); status {
o.Likes = IRIf(o.GetLink(), t)
iri = o.Likes.GetLink()
} else if status = t == Shares && IsNil(o.Shares); status {
o.Shares = IRIf(o.GetLink(), t)
iri = o.Shares.GetLink()
} else if status = t == Replies && IsNil(o.Replies); status {
o.Replies = IRIf(o.GetLink(), t)
iri = o.Replies.GetLink()
}
return nil
})
} else {
iri = IRIf(i.GetLink(), t)
}
return iri, status
}

282
typer_test.go Normal file
View file

@ -0,0 +1,282 @@
package activitypub
import (
"testing"
)
func TestPathTyper_Type(t *testing.T) {
t.Skipf("TODO")
}
func TestValidActivityCollection(t *testing.T) {
t.Skipf("TODO")
}
func TestValidCollection(t *testing.T) {
t.Skipf("TODO")
}
func TestValidObjectCollection(t *testing.T) {
t.Skipf("TODO")
}
func TestValidCollectionIRI(t *testing.T) {
t.Skipf("TODO")
}
func TestSplit(t *testing.T) {
t.Skipf("TODO")
}
func TestCollectionTypes_Of(t *testing.T) {
type args struct {
o Item
t collectionPath
}
tests := []struct {
name string
args args
want Item
}{
{
name: "nil from nil object",
args: args{
o: nil,
t: "likes",
},
want: nil,
},
{
name: "nil from invalid collectionPath type",
args: args{
o: Object{
Likes: IRI("test"),
},
t: "like",
},
want: nil,
},
{
name: "nil from nil collectionPath type",
args: args{
o: Object{
Likes: nil,
},
t: "likes",
},
want: nil,
},
{
name: "get likes iri",
args: args{
o: Object{
Likes: IRI("test"),
},
t: "likes",
},
want: IRI("test"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if ob := test.args.t.Of(test.args.o); ob != test.want {
t.Errorf("Object received %#v is different, expected #%v", ob, test.want)
}
})
}
}
func TestCollectionType_IRI(t *testing.T) {
type args struct {
o Item
t collectionPath
}
tests := []struct {
name string
args args
want IRI
}{
{
name: "just path from nil object",
args: args{
o: nil,
t: "likes",
},
want: IRI("/likes"),
},
{
name: "emptyIRI from invalid collectionPath type",
args: args{
o: Object{
Likes: IRI("test"),
},
t: "like",
},
want: "/like",
},
{
name: "just path from object without ID",
args: args{
o: Object{},
t: "likes",
},
want: IRI("/likes"),
},
{
name: "likes iri on object",
args: args{
o: Object{
ID: "http://example.com",
Likes: IRI("test"),
},
t: "likes",
},
want: IRI("test"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if ob := test.args.t.IRI(test.args.o); ob != test.want {
t.Errorf("IRI received %q is different, expected %q", ob, test.want)
}
})
}
}
func TestCollectionType_OfActor(t *testing.T) {
t.Skipf("TODO")
}
func TestCollectionTypes_Contains(t *testing.T) {
t.Skipf("TODO")
}
func TestIRIf(t *testing.T) {
type args struct {
i IRI
t collectionPath
}
tests := []struct {
name string
args args
want IRI
}{
{
name: "empty iri",
args: args{
i: "",
t: "inbox",
},
want: "/inbox",
},
{
name: "plain concat",
args: args{
i: "https://example.com",
t: "inbox",
},
want: "https://example.com/inbox",
},
{
name: "strip root from iri",
args: args{
i: "https://example.com/",
t: "inbox",
},
want: "https://example.com/inbox",
},
{
name: "invalid iri",
args: args{
i: "example.com",
t: "test",
},
want: "example.com/test",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IRIf(tt.args.i, tt.args.t); got != tt.want {
t.Errorf("IRIf() = %v, want %v", got, tt.want)
}
})
}
}
func TestCollectionType_AddTo(t *testing.T) {
type args struct {
i Item
}
var i Item
var o *Object
tests := []struct {
name string
t collectionPath
args args
want IRI
want1 bool
}{
{
name: "simple",
t: "test",
args: args{
i: &Object{ID: "http://example.com/addTo"},
},
want: "http://example.com/addTo/test",
want1: false, // this seems to always be false
},
{
name: "on-nil-item",
t: "test",
args: args{
i: i,
},
want: NilIRI,
want1: false,
},
{
name: "on-nil",
t: "test",
args: args{
i: nil,
},
want: NilIRI,
want1: false,
},
{
name: "on-nil-object",
t: "test",
args: args{
i: o,
},
want: NilIRI,
want1: false,
},
{
name: "on-nil-item",
t: "test",
args: args{
i: i,
},
want: NilIRI,
want1: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1 := tt.t.AddTo(tt.args.i)
if got != tt.want {
t.Errorf("AddTo() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("AddTo() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
func TestCollectionTypes_Split(t *testing.T) {
t.Skipf("TODO")
}