[feature] Federate pinned posts (aka featuredCollection) in and out (#1560)

* start fiddling

* the ol' fiddle + update

* start working on fetching statuses

* poopy doopy doo where r u uwu

* further adventures in featuring statuses

* finishing up

* fmt

* simply status unpin loop

* move empty featured check back to caller function

* remove unnecessary log.WithContext calls

* remove unnecessary IsIRI() checks

* add explanatory comment about status URIs

* change log level to error

* better test names
This commit is contained in:
tobi 2023-03-01 18:52:44 +01:00 committed by GitHub
commit 24cec4e7aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 783 additions and 278 deletions

View file

@ -181,9 +181,14 @@ func (c *converter) ASRepresentationToAccount(ctx context.Context, accountable a
acct.FollowersURI = accountable.GetActivityStreamsFollowers().GetIRI().String()
}
// FeaturedURI
if accountable.GetTootFeatured() != nil && accountable.GetTootFeatured().GetIRI() != nil {
acct.FeaturedCollectionURI = accountable.GetTootFeatured().GetIRI().String()
// FeaturedURI aka pinned collection:
// Only trust featured URI if it has at least two domains,
// from the right, in common with the domain of the account
if featured := accountable.GetTootFeatured(); featured != nil && featured.IsIRI() {
if featuredURI := featured.GetIRI(); // nocollapse
featuredURI != nil && dns.CompareDomainName(acct.Domain, featuredURI.Host) >= 2 {
acct.FeaturedCollectionURI = featuredURI.String()
}
}
// TODO: FeaturedTagsURI

View file

@ -178,6 +178,9 @@ type TypeConverter interface {
//
// Appropriate 'next' and 'prev' fields will be created based on the highest and lowest IDs present in the statuses slice.
StatusesToASOutboxPage(ctx context.Context, outboxID string, maxID string, minID string, statuses []*gtsmodel.Status) (vocab.ActivityStreamsOrderedCollectionPage, error)
// StatusesToASFeaturedCollection converts a slice of statuses into an ordered collection
// of URIs, suitable for serializing and serving via the activitypub API.
StatusesToASFeaturedCollection(ctx context.Context, featuredCollectionID string, statuses []*gtsmodel.Status) (vocab.ActivityStreamsOrderedCollection, error)
// ReportToASFlag converts a gts model report into an activitystreams FLAG, suitable for federation.
ReportToASFlag(ctx context.Context, r *gtsmodel.Report) (vocab.ActivityStreamsFlag, error)

View file

@ -1296,6 +1296,34 @@ func (c *converter) OutboxToASCollection(ctx context.Context, outboxID string) (
return collection, nil
}
func (c *converter) StatusesToASFeaturedCollection(ctx context.Context, featuredCollectionID string, statuses []*gtsmodel.Status) (vocab.ActivityStreamsOrderedCollection, error) {
collection := streams.NewActivityStreamsOrderedCollection()
collectionIDProp := streams.NewJSONLDIdProperty()
featuredCollectionIDURI, err := url.Parse(featuredCollectionID)
if err != nil {
return nil, fmt.Errorf("error parsing url %s", featuredCollectionID)
}
collectionIDProp.SetIRI(featuredCollectionIDURI)
collection.SetJSONLDId(collectionIDProp)
itemsProp := streams.NewActivityStreamsOrderedItemsProperty()
for _, s := range statuses {
uri, err := url.Parse(s.URI)
if err != nil {
return nil, fmt.Errorf("error parsing url %s", s.URI)
}
itemsProp.AppendIRI(uri)
}
collection.SetActivityStreamsOrderedItems(itemsProp)
totalItemsProp := streams.NewActivityStreamsTotalItemsProperty()
totalItemsProp.Set(len(statuses))
collection.SetActivityStreamsTotalItems(totalItemsProp)
return collection, nil
}
func (c *converter) ReportToASFlag(ctx context.Context, r *gtsmodel.Report) (vocab.ActivityStreamsFlag, error) {
flag := streams.NewActivityStreamsFlag()

View file

@ -21,11 +21,13 @@ package typeutils_test
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@ -544,6 +546,96 @@ func (suite *InternalToASTestSuite) TestReportToAS() {
}`, string(bytes))
}
func (suite *InternalToASTestSuite) TestPinnedStatusesToASSomeItems() {
ctx := context.Background()
testAccount := suite.testAccounts["admin_account"]
statuses, err := suite.db.GetAccountPinnedStatuses(ctx, testAccount.ID)
if err != nil {
suite.FailNow(err.Error())
}
collection, err := suite.typeconverter.StatusesToASFeaturedCollection(ctx, testAccount.FeaturedCollectionURI, statuses)
if err != nil {
suite.FailNow(err.Error())
}
ser, err := streams.Serialize(collection)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
suite.NoError(err)
suite.Equal(`{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://localhost:8080/users/admin/collections/featured",
"orderedItems": [
"http://localhost:8080/users/admin/statuses/01F8MHAAY43M6RJ473VQFCVH37",
"http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R"
],
"totalItems": 2,
"type": "OrderedCollection"
}`, string(bytes))
}
func (suite *InternalToASTestSuite) TestPinnedStatusesToASNoItems() {
ctx := context.Background()
testAccount := suite.testAccounts["local_account_1"]
statuses, err := suite.db.GetAccountPinnedStatuses(ctx, testAccount.ID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
suite.FailNow(err.Error())
}
collection, err := suite.typeconverter.StatusesToASFeaturedCollection(ctx, testAccount.FeaturedCollectionURI, statuses)
if err != nil {
suite.FailNow(err.Error())
}
ser, err := streams.Serialize(collection)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
suite.NoError(err)
suite.Equal(`{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://localhost:8080/users/the_mighty_zork/collections/featured",
"orderedItems": [],
"totalItems": 0,
"type": "OrderedCollection"
}`, string(bytes))
}
func (suite *InternalToASTestSuite) TestPinnedStatusesToASOneItem() {
ctx := context.Background()
testAccount := suite.testAccounts["local_account_2"]
statuses, err := suite.db.GetAccountPinnedStatuses(ctx, testAccount.ID)
if err != nil {
suite.FailNow(err.Error())
}
collection, err := suite.typeconverter.StatusesToASFeaturedCollection(ctx, testAccount.FeaturedCollectionURI, statuses)
if err != nil {
suite.FailNow(err.Error())
}
ser, err := streams.Serialize(collection)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
suite.NoError(err)
suite.Equal(`{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://localhost:8080/users/1happyturtle/collections/featured",
"orderedItems": "http://localhost:8080/users/1happyturtle/statuses/01G20ZM733MGN8J344T4ZDDFY1",
"totalItems": 1,
"type": "OrderedCollection"
}`, string(bytes))
}
func TestInternalToASTestSuite(t *testing.T) {
suite.Run(t, new(InternalToASTestSuite))
}