diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/ap/activitystreams.go | 234 | ||||
| -rw-r--r-- | internal/ap/collections.go | 359 | ||||
| -rw-r--r-- | internal/ap/interfaces.go | 38 | ||||
| -rw-r--r-- | internal/federation/dereferencing/collectionpage.go | 94 | ||||
| -rw-r--r-- | internal/federation/dereferencing/thread.go | 99 | 
5 files changed, 506 insertions, 318 deletions
diff --git a/internal/ap/activitystreams.go b/internal/ap/activitystreams.go index 34a3013a4..f6c412e51 100644 --- a/internal/ap/activitystreams.go +++ b/internal/ap/activitystreams.go @@ -17,15 +17,6 @@  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 @@ -63,21 +54,22 @@ const (  	ActorPerson       = "Person"       // ActivityStreamsPerson https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person  	ActorService      = "Service"      // ActivityStreamsService https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service -	ObjectArticle           = "Article"           // ActivityStreamsArticle https://www.w3.org/TR/activitystreams-vocabulary/#dfn-article -	ObjectAudio             = "Audio"             // ActivityStreamsAudio https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audio -	ObjectDocument          = "Document"          // ActivityStreamsDocument https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document -	ObjectEvent             = "Event"             // ActivityStreamsEvent https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event -	ObjectImage             = "Image"             // ActivityStreamsImage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image -	ObjectNote              = "Note"              // ActivityStreamsNote https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note -	ObjectPage              = "Page"              // ActivityStreamsPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-page -	ObjectPlace             = "Place"             // ActivityStreamsPlace https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place -	ObjectProfile           = "Profile"           // ActivityStreamsProfile https://www.w3.org/TR/activitystreams-vocabulary/#dfn-profile -	ObjectRelationship      = "Relationship"      // ActivityStreamsRelationship https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship -	ObjectTombstone         = "Tombstone"         // ActivityStreamsTombstone https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone -	ObjectVideo             = "Video"             // ActivityStreamsVideo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video -	ObjectCollection        = "Collection"        // ActivityStreamsCollection https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collection -	ObjectCollectionPage    = "CollectionPage"    // ActivityStreamsCollectionPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage -	ObjectOrderedCollection = "OrderedCollection" // ActivityStreamsOrderedCollection https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection +	ObjectArticle               = "Article"               // ActivityStreamsArticle https://www.w3.org/TR/activitystreams-vocabulary/#dfn-article +	ObjectAudio                 = "Audio"                 // ActivityStreamsAudio https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audio +	ObjectDocument              = "Document"              // ActivityStreamsDocument https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document +	ObjectEvent                 = "Event"                 // ActivityStreamsEvent https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event +	ObjectImage                 = "Image"                 // ActivityStreamsImage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image +	ObjectNote                  = "Note"                  // ActivityStreamsNote https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note +	ObjectPage                  = "Page"                  // ActivityStreamsPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-page +	ObjectPlace                 = "Place"                 // ActivityStreamsPlace https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place +	ObjectProfile               = "Profile"               // ActivityStreamsProfile https://www.w3.org/TR/activitystreams-vocabulary/#dfn-profile +	ObjectRelationship          = "Relationship"          // ActivityStreamsRelationship https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship +	ObjectTombstone             = "Tombstone"             // ActivityStreamsTombstone https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone +	ObjectVideo                 = "Video"                 // ActivityStreamsVideo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video +	ObjectCollection            = "Collection"            // ActivityStreamsCollection https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collection +	ObjectCollectionPage        = "CollectionPage"        // ActivityStreamsCollectionPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage +	ObjectOrderedCollection     = "OrderedCollection"     // ActivityStreamsOrderedCollection https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection +	ObjectOrderedCollectionPage = "OrderedCollectionPage" // ActivityStreamsOrderedCollectionPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollectionPage  	// Hashtag is not in the AS spec per se, but it tends to get used  	// as though 'Hashtag' is a named type under the Tag property. @@ -86,197 +78,3 @@ 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/collections.go b/internal/ap/collections.go new file mode 100644 index 000000000..471dae0a1 --- /dev/null +++ b/internal/ap/collections.go @@ -0,0 +1,359 @@ +// 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 + +import ( +	"fmt" +	"net/url" +	"strconv" + +	"github.com/superseriousbusiness/activity/streams" +	"github.com/superseriousbusiness/activity/streams/vocab" +	"github.com/superseriousbusiness/gotosocial/internal/paging" +) + +// ToCollectionPageIterator attempts to resolve the given vocab type as a CollectionPage +// like object and wrap in a standardised interface in order to iterate its contents. +func ToCollectionPageIterator(t vocab.Type) (CollectionPageIterator, error) { +	switch name := t.GetTypeName(); name { +	case ObjectCollectionPage: +		t := t.(vocab.ActivityStreamsCollectionPage) //nolint:forcetypeassert +		return WrapCollectionPage(t), nil +	case ObjectOrderedCollectionPage: +		t := t.(vocab.ActivityStreamsOrderedCollectionPage) //nolint:forcetypeassert +		return WrapOrderedCollectionPage(t), nil +	default: +		return nil, fmt.Errorf("%T(%s) was not CollectionPage-like", t, name) +	} +} + +// WrapCollectionPage wraps an ActivityStreamsCollectionPage in a standardised collection page interface. +func WrapCollectionPage(page vocab.ActivityStreamsCollectionPage) CollectionPageIterator { +	return ®ularCollectionPageIterator{ActivityStreamsCollectionPage: page} +} + +// WrapOrderedCollectionPage wraps an ActivityStreamsOrderedCollectionPage in a standardised collection page interface. +func WrapOrderedCollectionPage(page vocab.ActivityStreamsOrderedCollectionPage) CollectionPageIterator { +	return &orderedCollectionPageIterator{ActivityStreamsOrderedCollectionPage: page} +} + +// regularCollectionPageIterator implements CollectionPageIterator +// for the vocab.ActivitiyStreamsCollectionPage type. +type regularCollectionPageIterator struct { +	vocab.ActivityStreamsCollectionPage +	items vocab.ActivityStreamsItemsPropertyIterator +	once  bool // only init items once +} + +func (iter *regularCollectionPageIterator) NextPage() WithIRI { +	if iter.ActivityStreamsCollectionPage == nil { +		return nil +	} +	return iter.GetActivityStreamsNext() +} + +func (iter *regularCollectionPageIterator) PrevPage() WithIRI { +	if iter.ActivityStreamsCollectionPage == nil { +		return nil +	} +	return iter.GetActivityStreamsPrev() +} + +func (iter *regularCollectionPageIterator) NextItem() IteratorItemable { +	if !iter.initItems() { +		return nil +	} +	cur := iter.items +	iter.items = iter.items.Next() +	return cur +} + +func (iter *regularCollectionPageIterator) PrevItem() IteratorItemable { +	if !iter.initItems() { +		return nil +	} +	cur := iter.items +	iter.items = iter.items.Prev() +	return cur +} + +func (iter *regularCollectionPageIterator) initItems() bool { +	if iter.once { +		return (iter.items != nil) +	} +	iter.once = true +	if iter.ActivityStreamsCollectionPage == nil { +		return false // no page set +	} +	items := iter.GetActivityStreamsItems() +	if items == nil { +		return false // no items found +	} +	iter.items = items.Begin() +	return (iter.items != nil) +} + +// orderedCollectionPageIterator implements CollectionPageIterator +// for the vocab.ActivitiyStreamsOrderedCollectionPage type. +type orderedCollectionPageIterator struct { +	vocab.ActivityStreamsOrderedCollectionPage +	items vocab.ActivityStreamsOrderedItemsPropertyIterator +	once  bool // only init items once +} + +func (iter *orderedCollectionPageIterator) NextPage() WithIRI { +	if iter.ActivityStreamsOrderedCollectionPage == nil { +		return nil +	} +	return iter.GetActivityStreamsNext() +} + +func (iter *orderedCollectionPageIterator) PrevPage() WithIRI { +	if iter.ActivityStreamsOrderedCollectionPage == nil { +		return nil +	} +	return iter.GetActivityStreamsPrev() +} + +func (iter *orderedCollectionPageIterator) NextItem() IteratorItemable { +	if !iter.initItems() { +		return nil +	} +	cur := iter.items +	iter.items = iter.items.Next() +	return cur +} + +func (iter *orderedCollectionPageIterator) PrevItem() IteratorItemable { +	if !iter.initItems() { +		return nil +	} +	cur := iter.items +	iter.items = iter.items.Prev() +	return cur +} + +func (iter *orderedCollectionPageIterator) initItems() bool { +	if iter.once { +		return (iter.items != nil) +	} +	iter.once = true +	if iter.ActivityStreamsOrderedCollectionPage == nil { +		return false // no page set +	} +	items := iter.GetActivityStreamsOrderedItems() +	if items == nil { +		return false // no items found +	} +	iter.items = items.Begin() +	return (iter.items != nil) +} + +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/interfaces.go b/internal/ap/interfaces.go index 611f4bde0..5372eb01e 100644 --- a/internal/ap/interfaces.go +++ b/internal/ap/interfaces.go @@ -17,7 +17,11 @@  package ap -import "github.com/superseriousbusiness/activity/streams/vocab" +import ( +	"net/url" + +	"github.com/superseriousbusiness/activity/streams/vocab" +)  // Accountable represents the minimum activitypub interface for representing an 'account'.  // This interface is fulfilled by: Person, Application, Organization, Service, and Group @@ -153,14 +157,23 @@ type ReplyToable interface {  	WithInReplyTo  } -// CollectionPageable represents the minimum interface for an activitystreams 'CollectionPage' object. -type CollectionPageable interface { +// CollectionPageIterator represents the minimum interface for interacting with a wrapped +// CollectionPage or OrderedCollectionPage in order to access both next / prev pages and items. +type CollectionPageIterator interface {  	WithJSONLDId  	WithTypeName -	WithNext -	WithPartOf -	WithItems +	NextPage() WithIRI +	PrevPage() WithIRI + +	NextItem() IteratorItemable +	PrevItem() IteratorItemable +} + +// IteratorItemable represents the minimum interface for an item in an iterator. +type IteratorItemable interface { +	WithIRI +	WithType  }  // Flaggable represents the minimum interface for an activitystreams 'Flag' activity. @@ -173,11 +186,22 @@ type Flaggable interface {  	WithObject  } -// WithJSONLDId represents an activity with JSONLDIdProperty +// WithJSONLDId represents an activity with JSONLDIdProperty.  type WithJSONLDId interface {  	GetJSONLDId() vocab.JSONLDIdProperty  } +// WithIRI represents an object (possibly) representable as an IRI. +type WithIRI interface { +	GetIRI() *url.URL +	IsIRI() bool +} + +// WithType ... +type WithType interface { +	GetType() vocab.Type +} +  // WithTypeName represents an activity with a type name  type WithTypeName interface {  	GetTypeName() string diff --git a/internal/federation/dereferencing/collectionpage.go b/internal/federation/dereferencing/collectionpage.go index d76c4b2ab..dc4ac7b4b 100644 --- a/internal/federation/dereferencing/collectionpage.go +++ b/internal/federation/dereferencing/collectionpage.go @@ -20,49 +20,115 @@ package dereferencing  import (  	"context"  	"encoding/json" -	"errors" -	"fmt"  	"net/url"  	"github.com/superseriousbusiness/activity/streams"  	"github.com/superseriousbusiness/activity/streams/vocab"  	"github.com/superseriousbusiness/gotosocial/internal/ap" +	"github.com/superseriousbusiness/gotosocial/internal/gtserror" +	"github.com/superseriousbusiness/gotosocial/internal/log"  )  // dereferenceCollectionPage returns the activitystreams CollectionPage at the specified IRI, or an error if something goes wrong. -func (d *deref) dereferenceCollectionPage(ctx context.Context, username string, pageIRI *url.URL) (ap.CollectionPageable, error) { +func (d *deref) dereferenceCollectionPage(ctx context.Context, username string, pageIRI *url.URL) (ap.CollectionPageIterator, error) {  	if blocked, err := d.state.DB.IsDomainBlocked(ctx, pageIRI.Host); blocked || err != nil { -		return nil, fmt.Errorf("DereferenceCollectionPage: domain %s is blocked", pageIRI.Host) +		return nil, gtserror.Newf("domain %s is blocked", pageIRI.Host)  	}  	transport, err := d.transportController.NewTransportForUsername(ctx, username)  	if err != nil { -		return nil, fmt.Errorf("DereferenceCollectionPage: error creating transport: %s", err) +		return nil, gtserror.Newf("error creating transport: %w", err)  	}  	b, err := transport.Dereference(ctx, pageIRI)  	if err != nil { -		return nil, fmt.Errorf("DereferenceCollectionPage: error deferencing %s: %s", pageIRI.String(), err) +		return nil, gtserror.Newf("error deferencing %s: %w", pageIRI.String(), err)  	}  	m := make(map[string]interface{})  	if err := json.Unmarshal(b, &m); err != nil { -		return nil, fmt.Errorf("DereferenceCollectionPage: error unmarshalling bytes into json: %s", err) +		return nil, gtserror.Newf("error unmarshalling bytes into json: %w", err)  	}  	t, err := streams.ToType(ctx, m)  	if err != nil { -		return nil, fmt.Errorf("DereferenceCollectionPage: error resolving json into ap vocab type: %s", err) +		return nil, gtserror.Newf("error resolving json into ap vocab type: %w", err)  	} -	if t.GetTypeName() != ap.ObjectCollectionPage { -		return nil, fmt.Errorf("DereferenceCollectionPage: type name %s not supported", t.GetTypeName()) +	page, err := ap.ToCollectionPageIterator(t) +	if err != nil { +		return nil, gtserror.Newf("error resolving vocab type as page: %w", err) +	} + +	return page, nil +} + +// getAttachedStatusCollection is a small utility function to fetch the first page of an +// attached activity streams collection from a provided statusable object, along with a URI. +func getAttachedStatusCollectionPage(status ap.Statusable) (ap.CollectionPageIterator, string) { //nolint:gocritic +	// Look for an attached status replies (as collection) +	replies := status.GetActivityStreamsReplies() +	if replies == nil { +		return nil, "" +	} + +	// Look for an attached collection page, wrap and return. +	if page := getRepliesCollectionPage(replies); page != nil { +		return ap.WrapCollectionPage(page), getIDString(page) +	} + +	// Look for an attached ordered collection page, wrap and return. +	if page := getRepliesOrderedCollectionPage(replies); page != nil { +		return ap.WrapOrderedCollectionPage(page), getIDString(page)  	} -	p, ok := t.(vocab.ActivityStreamsCollectionPage) -	if !ok { -		return nil, errors.New("DereferenceCollectionPage: error resolving type as activitystreams collection page") +	log.Warnf(nil, "replies without collection page: %s", getIDString(status)) +	return nil, "" +} + +func getRepliesCollectionPage(replies vocab.ActivityStreamsRepliesProperty) vocab.ActivityStreamsCollectionPage { +	// Get the status replies collection +	collection := replies.GetActivityStreamsCollection() +	if collection == nil { +		return nil  	} -	return p, nil +	// Get the "first" property of the replies collection +	first := collection.GetActivityStreamsFirst() +	if first == nil { +		return nil +	} + +	// Return the first activity stream collection page +	return first.GetActivityStreamsCollectionPage() +} + +func getRepliesOrderedCollectionPage(replies vocab.ActivityStreamsRepliesProperty) vocab.ActivityStreamsOrderedCollectionPage { +	// Get the status replies collection +	collection := replies.GetActivityStreamsOrderedCollection() +	if collection == nil { +		return nil +	} + +	// Get the "first" property of the replies collection +	first := collection.GetActivityStreamsFirst() +	if first == nil { +		return nil +	} + +	// Return the first activity stream collection page +	return first.GetActivityStreamsOrderedCollectionPage() +} + +// getIDString is shorthand to fetch an ID URI string from AP type with attached JSONLDId. +func getIDString(a ap.WithJSONLDId) string { +	id := a.GetJSONLDId() +	if id == nil { +		return "" +	} +	uri := id.Get() +	if uri == nil { +		return "" +	} +	return uri.String()  } diff --git a/internal/federation/dereferencing/thread.go b/internal/federation/dereferencing/thread.go index d35627ff1..6d8c913c7 100644 --- a/internal/federation/dereferencing/thread.go +++ b/internal/federation/dereferencing/thread.go @@ -25,7 +25,6 @@ import (  	"codeberg.org/gruf/go-kv"  	"github.com/superseriousbusiness/activity/pub" -	"github.com/superseriousbusiness/activity/streams/vocab"  	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/db" @@ -247,15 +246,12 @@ func (d *deref) DereferenceStatusDescendants(ctx context.Context, username strin  		// page is the current activity streams  		// collection page we are on (as we often  		// push a frame to stack mid-paging). -		page ap.CollectionPageable +		page ap.CollectionPageIterator  		// pageURI is the URI string of  		// the frame's collection page  		// (is useful for logging).  		pageURI string - -		// items is the entity iterator for frame's page. -		items vocab.ActivityStreamsItemsPropertyIterator  	}  	var ( @@ -270,7 +266,7 @@ func (d *deref) DereferenceStatusDescendants(ctx context.Context, username strin  		stack = []*frame{  			func() *frame {  				// Start input frame is built from the first input. -				page, pageURI := getAttachedStatusCollection(parent) +				page, pageURI := getAttachedStatusCollectionPage(parent)  				if page == nil {  					return nil  				} @@ -305,34 +301,18 @@ stackLoop:  	pageLoop:  		for { -			if current.items == nil { -				// Get the items associated with this page -				items := current.page.GetActivityStreamsItems() -				if items == nil { -					continue stackLoop -				} - -				// Start off the item iterator -				current.items = items.Begin() -			} -  			l.Tracef("following collection page: %s", current.pageURI)  		itemLoop:  			for { -				// Check for remaining iter -				if current.items == nil { +				// Get next item from page iter. +				next := current.page.NextItem() +				if next == nil {  					break itemLoop  				} -				// Get current item iterator -				itemIter := current.items - -				// Set the next available iterator -				current.items = itemIter.Next() -  				// Check for available IRI on item -				itemIRI, _ := pub.ToId(itemIter) +				itemIRI, _ := pub.ToId(next)  				if itemIRI == nil {  					continue itemLoop  				} @@ -364,8 +344,8 @@ stackLoop:  					continue itemLoop  				} -				// Extract any attached collection + URI from status. -				page, pageURI := getAttachedStatusCollection(statusable) +				// Extract any attached collection + ID URI from status. +				page, pageURI := getAttachedStatusCollectionPage(statusable)  				if page == nil {  					continue itemLoop  				} @@ -380,80 +360,41 @@ stackLoop:  				continue stackLoop  			} -			// Get the current page's "next" property. -			pageNext := current.page.GetActivityStreamsNext() -			if pageNext == nil || !pageNext.IsIRI() { +			// Get the next page from iterator. +			next := current.page.NextPage() +			if next == nil || !next.IsIRI() {  				continue stackLoop  			} -			// Get the IRI of the "next" property. -			pageNextURI := pageNext.GetIRI() -			pageNextURIStr := pageNextURI.String() +			// Get the next page IRI. +			nextURI := next.GetIRI() +			nextURIStr := nextURI.String()  			// Check whether this page has already been deref'd. -			if _, ok := derefdPages[pageNextURIStr]; ok { -				l.Warnf("self referencing collection page(s): %s", pageNextURIStr) +			if _, ok := derefdPages[nextURIStr]; ok { +				l.Warnf("self referencing collection page(s): %s", nextURIStr)  				continue stackLoop  			}  			// Mark this collection page as deref'd. -			derefdPages[pageNextURIStr] = struct{}{} +			derefdPages[nextURIStr] = struct{}{}  			// Dereference this next collection page by its IRI.  			collectionPage, err := d.dereferenceCollectionPage(ctx,  				username, -				pageNextURI, +				nextURI,  			)  			if err != nil { -				l.Errorf("error dereferencing collection page %q: %s", pageNextURIStr, err) +				l.Errorf("error dereferencing collection page %q: %s", nextURIStr, err)  				continue stackLoop  			}  			// Set the next collection page.  			current.page = collectionPage -			current.pageURI = pageNextURIStr +			current.pageURI = nextURIStr  			continue pageLoop  		}  	}  	return gtserror.Newf("reached %d descendant iterations for %q", maxIter, statusIRIStr)  } - -// getAttachedStatusCollection is a small utility function to fetch the first page -// of an attached activity streams collection from a provided statusable object . -func getAttachedStatusCollection(status ap.Statusable) (page ap.CollectionPageable, uri string) { //nolint:gocritic -	// Look for an attached status replies (as collection) -	replies := status.GetActivityStreamsReplies() -	if replies == nil { -		return nil, "" -	} - -	// Get the status replies collection -	collection := replies.GetActivityStreamsCollection() -	if collection == nil { -		return nil, "" -	} - -	// Get the "first" property of the replies collection -	first := collection.GetActivityStreamsFirst() -	if first == nil { -		return nil, "" -	} - -	// Return the first activity stream collection page -	page = first.GetActivityStreamsCollectionPage() -	if page == nil { -		return nil, "" -	} - -	if pageID := page.GetJSONLDId(); pageID != nil { -		// By default use collection JSONLD ID -		return page, pageID.Get().String() -	} else if statusID := status.GetJSONLDId(); statusID != nil { -		// Else, if possible use status JSONLD ID -		return page, statusID.Get().String() -	} else { -		// MUST have some kind of ID -		return nil, "" -	} -}  | 
