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 @@ 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 @@ // 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 . + +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 @@ 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) } - p, ok := t.(vocab.ActivityStreamsCollectionPage) - if !ok { - return nil, errors.New("DereferenceCollectionPage: error resolving type as activitystreams collection page") - } - - return p, nil + 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) + } + + 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 + } + + // 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 @@ "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 @@ type frame struct { // 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 @@ type frame struct { 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 @@ func() *frame { 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 @@ func() *frame { 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 @@ func() *frame { 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, "" - } -}