diff options
Diffstat (limited to 'internal/ap')
| -rw-r--r-- | internal/ap/activitystreams.go | 203 | ||||
| -rw-r--r-- | internal/ap/activitystreams_test.go | 221 | 
2 files changed, 424 insertions, 0 deletions
| diff --git a/internal/ap/activitystreams.go b/internal/ap/activitystreams.go index e8a362800..34a3013a4 100644 --- a/internal/ap/activitystreams.go +++ b/internal/ap/activitystreams.go @@ -17,6 +17,15 @@  package ap +import ( +	"net/url" +	"strconv" + +	"github.com/superseriousbusiness/activity/streams" +	"github.com/superseriousbusiness/activity/streams/vocab" +	"github.com/superseriousbusiness/gotosocial/internal/paging" +) +  // https://www.w3.org/TR/activitystreams-vocabulary  const (  	ActivityAccept          = "Accept"          // ActivityStreamsAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accept @@ -77,3 +86,197 @@ const (  	// and https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tag  	TagHashtag = "Hashtag"  ) + +type CollectionParams struct { +	// Containing collection +	// ID (i.e. NOT the page). +	ID *url.URL + +	// Total no. items. +	Total int +} + +type CollectionPageParams struct { +	// containing collection. +	CollectionParams + +	// Paging details. +	Current *paging.Page +	Next    *paging.Page +	Prev    *paging.Page +	Query   url.Values + +	// Item appender for each item at index. +	Append func(int, ItemsPropertyBuilder) +	Count  int +} + +// CollectionPage is a simplified interface type +// that can be fulfilled by either of (where required): +// vocab.ActivityStreamsCollection +// vocab.ActivityStreamsOrderedCollection +type CollectionBuilder interface { +	SetJSONLDId(vocab.JSONLDIdProperty) +	SetActivityStreamsFirst(vocab.ActivityStreamsFirstProperty) +	SetActivityStreamsTotalItems(i vocab.ActivityStreamsTotalItemsProperty) +} + +// CollectionPageBuilder is a simplified interface type +// that can be fulfilled by either of (where required): +// vocab.ActivityStreamsCollectionPage +// vocab.ActivityStreamsOrderedCollectionPage +type CollectionPageBuilder interface { +	SetJSONLDId(vocab.JSONLDIdProperty) +	SetActivityStreamsPartOf(vocab.ActivityStreamsPartOfProperty) +	SetActivityStreamsNext(vocab.ActivityStreamsNextProperty) +	SetActivityStreamsPrev(vocab.ActivityStreamsPrevProperty) +	SetActivityStreamsTotalItems(i vocab.ActivityStreamsTotalItemsProperty) +} + +// ItemsPropertyBuilder is a simplified interface type +// that can be fulfilled by either of (where required): +// vocab.ActivityStreamsItemsProperty +// vocab.ActivityStreamsOrderedItemsProperty +type ItemsPropertyBuilder interface { +	AppendIRI(*url.URL) + +	// NOTE: add more of the items-property-like interface +	// functions here as you require them for building pages. +} + +// NewASCollection builds and returns a new ActivityStreams Collection from given parameters. +func NewASCollection(params CollectionParams) vocab.ActivityStreamsCollection { +	collection := streams.NewActivityStreamsCollection() +	buildCollection(collection, params, 40) +	return collection +} + +// NewASCollectionPage builds and returns a new ActivityStreams CollectionPage from given parameters (including item property appending function). +func NewASCollectionPage(params CollectionPageParams) vocab.ActivityStreamsCollectionPage { +	collectionPage := streams.NewActivityStreamsCollectionPage() +	itemsProp := streams.NewActivityStreamsItemsProperty() +	buildCollectionPage(collectionPage, itemsProp, collectionPage.SetActivityStreamsItems, params) +	return collectionPage +} + +// NewASOrderedCollection builds and returns a new ActivityStreams OrderedCollection from given parameters. +func NewASOrderedCollection(params CollectionParams) vocab.ActivityStreamsOrderedCollection { +	collection := streams.NewActivityStreamsOrderedCollection() +	buildCollection(collection, params, 40) +	return collection +} + +// NewASOrderedCollectionPage builds and returns a new ActivityStreams OrderedCollectionPage from given parameters (including item property appending function). +func NewASOrderedCollectionPage(params CollectionPageParams) vocab.ActivityStreamsOrderedCollectionPage { +	collectionPage := streams.NewActivityStreamsOrderedCollectionPage() +	itemsProp := streams.NewActivityStreamsOrderedItemsProperty() +	buildCollectionPage(collectionPage, itemsProp, collectionPage.SetActivityStreamsOrderedItems, params) +	return collectionPage +} + +func buildCollection[C CollectionBuilder](collection C, params CollectionParams, pageLimit int) { +	// Add the collection ID property. +	idProp := streams.NewJSONLDIdProperty() +	idProp.SetIRI(params.ID) +	collection.SetJSONLDId(idProp) + +	// Add the collection totalItems count property. +	totalItems := streams.NewActivityStreamsTotalItemsProperty() +	totalItems.Set(params.Total) +	collection.SetActivityStreamsTotalItems(totalItems) + +	// Clone the collection ID page +	// to add first page query data. +	firstIRI := new(url.URL) +	*firstIRI = *params.ID + +	// Note that simply adding a limit signals to our +	// endpoint to use paging (which will start at beginning). +	limit := "limit=" + strconv.Itoa(pageLimit) +	firstIRI.RawQuery = appendQuery(firstIRI.RawQuery, limit) + +	// Add the collection first IRI property. +	first := streams.NewActivityStreamsFirstProperty() +	first.SetIRI(firstIRI) +	collection.SetActivityStreamsFirst(first) +} + +func buildCollectionPage[C CollectionPageBuilder, I ItemsPropertyBuilder](collectionPage C, itemsProp I, setItems func(I), params CollectionPageParams) { +	// Add the partOf property for its containing collection ID. +	partOfProp := streams.NewActivityStreamsPartOfProperty() +	partOfProp.SetIRI(params.ID) +	collectionPage.SetActivityStreamsPartOf(partOfProp) + +	// Build the current page link IRI. +	currentIRI := params.Current.ToLinkURL( +		params.ID.Scheme, +		params.ID.Host, +		params.ID.Path, +		params.Query, +	) + +	// Add the collection ID property for +	// the *current* collection page params. +	idProp := streams.NewJSONLDIdProperty() +	idProp.SetIRI(currentIRI) +	collectionPage.SetJSONLDId(idProp) + +	// Build the next page link IRI. +	nextIRI := params.Next.ToLinkURL( +		params.ID.Scheme, +		params.ID.Host, +		params.ID.Path, +		params.Query, +	) + +	if nextIRI != nil { +		// Add the collection next property for the next page. +		nextProp := streams.NewActivityStreamsNextProperty() +		nextProp.SetIRI(nextIRI) +		collectionPage.SetActivityStreamsNext(nextProp) +	} + +	// Build the prev page link IRI. +	prevIRI := params.Prev.ToLinkURL( +		params.ID.Scheme, +		params.ID.Host, +		params.ID.Path, +		params.Query, +	) + +	if prevIRI != nil { +		// Add the collection prev property for the prev page. +		prevProp := streams.NewActivityStreamsPrevProperty() +		prevProp.SetIRI(prevIRI) +		collectionPage.SetActivityStreamsPrev(prevProp) +	} + +	// Add the collection totalItems count property. +	totalItems := streams.NewActivityStreamsTotalItemsProperty() +	totalItems.Set(params.Total) +	collectionPage.SetActivityStreamsTotalItems(totalItems) + +	if params.Append == nil { +		// nil check outside the for loop. +		panic("nil params.Append function") +	} + +	// Append each of the items to the provided +	// pre-allocated items property builder type. +	for i := 0; i < params.Count; i++ { +		params.Append(i, itemsProp) +	} + +	// Set the collection +	// page items property. +	setItems(itemsProp) +} + +// appendQuery appends part to an existing raw +// query with ampersand, else just returning part. +func appendQuery(raw, part string) string { +	if raw != "" { +		return raw + "&" + part +	} +	return part +} diff --git a/internal/ap/activitystreams_test.go b/internal/ap/activitystreams_test.go new file mode 100644 index 000000000..ee03f9b0f --- /dev/null +++ b/internal/ap/activitystreams_test.go @@ -0,0 +1,221 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program.  If not, see <http://www.gnu.org/licenses/>. +package ap_test + +import ( +	"encoding/json" +	"net/url" +	"testing" + +	"github.com/stretchr/testify/assert" +	"github.com/superseriousbusiness/activity/streams/vocab" +	"github.com/superseriousbusiness/gotosocial/internal/ap" +	"github.com/superseriousbusiness/gotosocial/internal/paging" +) + +func TestASCollection(t *testing.T) { +	const ( +		proto = "https" +		host  = "zorg.flabormagorg.xyz" +		path  = "/users/itsa_me_mario" + +		idURI = proto + "://" + host + path +		total = 10 +	) + +	// Create JSON string of expected output. +	expect := toJSON(map[string]any{ +		"@context":   "https://www.w3.org/ns/activitystreams", +		"type":       "Collection", +		"id":         idURI, +		"first":      idURI + "?limit=40", +		"totalItems": total, +	}) + +	// Create new collection using builder function. +	c := ap.NewASCollection(ap.CollectionParams{ +		ID:    parseURI(idURI), +		Total: total, +	}) + +	// Serialize collection. +	s := toJSON(c) + +	// Ensure outputs are equal. +	assert.Equal(t, s, expect) +} + +func TestASCollectionPage(t *testing.T) { +	const ( +		proto = "https" +		host  = "zorg.flabormagorg.xyz" +		path  = "/users/itsa_me_mario" + +		idURI = proto + "://" + host + path +		total = 10 + +		minID = "minimum" +		maxID = "maximum" +		limit = 40 +		count = 2 +	) + +	// Create the current page. +	currPg := &paging.Page{ +		Limit: 40, +	} + +	// Create JSON string of expected output. +	expect := toJSON(map[string]any{ +		"@context":   "https://www.w3.org/ns/activitystreams", +		"type":       "CollectionPage", +		"id":         currPg.ToLink(proto, host, path, nil), +		"partOf":     idURI, +		"next":       currPg.Next(minID, maxID).ToLink(proto, host, path, nil), +		"prev":       currPg.Prev(minID, maxID).ToLink(proto, host, path, nil), +		"items":      []interface{}{}, +		"totalItems": total, +	}) + +	// Create new collection page using builder function. +	p := ap.NewASCollectionPage(ap.CollectionPageParams{ +		CollectionParams: ap.CollectionParams{ +			ID:    parseURI(idURI), +			Total: total, +		}, + +		Current: currPg, +		Next:    currPg.Next(minID, maxID), +		Prev:    currPg.Prev(minID, maxID), + +		Append: func(i int, ipb ap.ItemsPropertyBuilder) {}, +		Count:  count, +	}) + +	// Serialize page. +	s := toJSON(p) + +	// Ensure outputs are equal. +	assert.Equal(t, s, expect) +} + +func TestASOrderedCollection(t *testing.T) { +	const ( +		idURI = "https://zorg.flabormagorg.xyz/users/itsa_me_mario" +		total = 10 +	) + +	// Create JSON string of expected output. +	expect := toJSON(map[string]any{ +		"@context":   "https://www.w3.org/ns/activitystreams", +		"type":       "OrderedCollection", +		"id":         idURI, +		"first":      idURI + "?limit=40", +		"totalItems": total, +	}) + +	// Create new collection using builder function. +	c := ap.NewASOrderedCollection(ap.CollectionParams{ +		ID:    parseURI(idURI), +		Total: total, +	}) + +	// Serialize collection. +	s := toJSON(c) + +	// Ensure outputs are equal. +	assert.Equal(t, s, expect) +} + +func TestASOrderedCollectionPage(t *testing.T) { +	const ( +		proto = "https" +		host  = "zorg.flabormagorg.xyz" +		path  = "/users/itsa_me_mario" + +		idURI = proto + "://" + host + path +		total = 10 + +		minID = "minimum" +		maxID = "maximum" +		limit = 40 +		count = 2 +	) + +	// Create the current page. +	currPg := &paging.Page{ +		Limit: 40, +	} + +	// Create JSON string of expected output. +	expect := toJSON(map[string]any{ +		"@context":     "https://www.w3.org/ns/activitystreams", +		"type":         "OrderedCollectionPage", +		"id":           currPg.ToLink(proto, host, path, nil), +		"partOf":       idURI, +		"next":         currPg.Next(minID, maxID).ToLink(proto, host, path, nil), +		"prev":         currPg.Prev(minID, maxID).ToLink(proto, host, path, nil), +		"orderedItems": []interface{}{}, +		"totalItems":   total, +	}) + +	// Create new collection page using builder function. +	p := ap.NewASOrderedCollectionPage(ap.CollectionPageParams{ +		CollectionParams: ap.CollectionParams{ +			ID:    parseURI(idURI), +			Total: total, +		}, + +		Current: currPg, +		Next:    currPg.Next(minID, maxID), +		Prev:    currPg.Prev(minID, maxID), + +		Append: func(i int, ipb ap.ItemsPropertyBuilder) {}, +		Count:  count, +	}) + +	// Serialize page. +	s := toJSON(p) + +	// Ensure outputs are equal. +	assert.Equal(t, s, expect) +} + +func parseURI(s string) *url.URL { +	u, err := url.Parse(s) +	if err != nil { +		panic(err) +	} +	return u +} + +// toJSON will return indented JSON serialized form of 'a'. +func toJSON(a any) string { +	v, ok := a.(vocab.Type) +	if ok { +		m, err := ap.Serialize(v) +		if err != nil { +			panic(err) +		} +		a = m +	} +	b, err := json.MarshalIndent(a, "", "  ") +	if err != nil { +		panic(err) +	} +	return string(b) +} | 
