From 0cd2bd2960e29a3e8c39de543f7e9a8d635e2221 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Thu, 30 Sep 2021 12:27:42 +0200 Subject: [PATCH] allow dereferencing of groups (#256) --- internal/federation/dereferencing/account.go | 24 +- .../federation/dereferencing/account_test.go | 57 +++++ .../dereferencing/dereferencer_test.go | 29 ++- testrig/testmodels.go | 214 ++++++++++++++++++ 4 files changed, 310 insertions(+), 14 deletions(-) create mode 100644 internal/federation/dereferencing/account_test.go diff --git a/internal/federation/dereferencing/account.go b/internal/federation/dereferencing/account.go index b16b53fee..35ea2507d 100644 --- a/internal/federation/dereferencing/account.go +++ b/internal/federation/dereferencing/account.go @@ -165,18 +165,30 @@ func (d *deref) dereferenceAccountable(ctx context.Context, username string, rem } switch t.GetTypeName() { - case ap.ActorPerson: - p, ok := t.(vocab.ActivityStreamsPerson) - if !ok { - return nil, errors.New("DereferenceAccountable: error resolving type as activitystreams person") - } - return p, nil case ap.ActorApplication: p, ok := t.(vocab.ActivityStreamsApplication) if !ok { return nil, errors.New("DereferenceAccountable: error resolving type as activitystreams application") } return p, nil + case ap.ActorGroup: + p, ok := t.(vocab.ActivityStreamsGroup) + if !ok { + return nil, errors.New("DereferenceAccountable: error resolving type as activitystreams group") + } + return p, nil + case ap.ActorOrganization: + p, ok := t.(vocab.ActivityStreamsOrganization) + if !ok { + return nil, errors.New("DereferenceAccountable: error resolving type as activitystreams organization") + } + return p, nil + case ap.ActorPerson: + p, ok := t.(vocab.ActivityStreamsPerson) + if !ok { + return nil, errors.New("DereferenceAccountable: error resolving type as activitystreams person") + } + return p, nil case ap.ActorService: p, ok := t.(vocab.ActivityStreamsService) if !ok { diff --git a/internal/federation/dereferencing/account_test.go b/internal/federation/dereferencing/account_test.go new file mode 100644 index 000000000..04b455631 --- /dev/null +++ b/internal/federation/dereferencing/account_test.go @@ -0,0 +1,57 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 dereferencing_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/ap" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type AccountTestSuite struct { + DereferencerStandardTestSuite +} + +func (suite *AccountTestSuite) TestDereferenceGroup() { + fetchingAccount := suite.testAccounts["local_account_1"] + + groupURL := testrig.URLMustParse("https://unknown-instance.com/groups/some_group") + group, new, err := suite.dereferencer.GetRemoteAccount(context.Background(), fetchingAccount.Username, groupURL, false) + suite.NoError(err) + suite.NotNil(group) + suite.NotNil(group) + suite.True(new) + + // group values should be set + suite.Equal("https://unknown-instance.com/groups/some_group", group.URI) + suite.Equal("https://unknown-instance.com/@some_group", group.URL) + + // group should be in the database + dbGroup, err := suite.db.GetAccountByURI(context.Background(), group.URI) + suite.NoError(err) + suite.Equal(group.ID, dbGroup.ID) + suite.Equal(ap.ActorGroup, dbGroup.ActorType) +} + +func TestAccountTestSuite(t *testing.T) { + suite.Run(t, new(AccountTestSuite)) +} diff --git a/internal/federation/dereferencing/dereferencer_test.go b/internal/federation/dereferencing/dereferencer_test.go index 41909ec4d..0eeabc64a 100644 --- a/internal/federation/dereferencing/dereferencer_test.go +++ b/internal/federation/dereferencing/dereferencer_test.go @@ -45,7 +45,8 @@ type DereferencerStandardTestSuite struct { storage *kv.KVStore testRemoteStatuses map[string]vocab.ActivityStreamsNote - testRemoteAccounts map[string]vocab.ActivityStreamsPerson + testRemotePeople map[string]vocab.ActivityStreamsPerson + testRemoteGroups map[string]vocab.ActivityStreamsGroup testRemoteAttachments map[string]testrig.RemoteAttachmentFile testAccounts map[string]*gtsmodel.Account @@ -55,7 +56,8 @@ type DereferencerStandardTestSuite struct { func (suite *DereferencerStandardTestSuite) SetupSuite() { suite.testAccounts = testrig.NewTestAccounts() suite.testRemoteStatuses = testrig.NewTestFediStatuses() - suite.testRemoteAccounts = testrig.NewTestFediPeople() + suite.testRemotePeople = testrig.NewTestFediPeople() + suite.testRemoteGroups = testrig.NewTestFediGroups() suite.testRemoteAttachments = testrig.NewTestFediAttachments("../../../testrig/media") } @@ -89,8 +91,7 @@ func (suite *DereferencerStandardTestSuite) mockTransportController() transport. responseType := "" responseLength := 0 - note, ok := suite.testRemoteStatuses[req.URL.String()] - if ok { + if note, ok := suite.testRemoteStatuses[req.URL.String()]; ok { // the request is for a note that we have stored noteI, err := streams.Serialize(note) if err != nil { @@ -104,8 +105,7 @@ func (suite *DereferencerStandardTestSuite) mockTransportController() transport. responseType = "application/activity+json" } - person, ok := suite.testRemoteAccounts[req.URL.String()] - if ok { + if person, ok := suite.testRemotePeople[req.URL.String()]; ok { // the request is for a person that we have stored personI, err := streams.Serialize(person) if err != nil { @@ -119,8 +119,21 @@ func (suite *DereferencerStandardTestSuite) mockTransportController() transport. responseType = "application/activity+json" } - attachment, ok := suite.testRemoteAttachments[req.URL.String()] - if ok { + if group, ok := suite.testRemoteGroups[req.URL.String()]; ok { + // the request is for a person that we have stored + groupI, err := streams.Serialize(group) + if err != nil { + panic(err) + } + groupJson, err := json.Marshal(groupI) + if err != nil { + panic(err) + } + responseBytes = groupJson + responseType = "application/activity+json" + } + + if attachment, ok := suite.testRemoteAttachments[req.URL.String()]; ok { responseBytes = attachment.Data responseType = attachment.ContentType } diff --git a/testrig/testmodels.go b/testrig/testmodels.go index fd2eca6d5..23762707c 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -1327,6 +1327,37 @@ func NewTestFediPeople() map[string]vocab.ActivityStreamsPerson { } } +func NewTestFediGroups() map[string]vocab.ActivityStreamsGroup { + newGroup1Priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + newGroup1Pub := &newGroup1Priv.PublicKey + + return map[string]vocab.ActivityStreamsGroup{ + "https://unknown-instance.com/groups/some_group": newGroup( + URLMustParse("https://unknown-instance.com/groups/some_group"), + URLMustParse("https://unknown-instance.com/groups/some_group/following"), + URLMustParse("https://unknown-instance.com/groups/some_group/followers"), + URLMustParse("https://unknown-instance.com/groups/some_group/inbox"), + URLMustParse("https://unknown-instance.com/groups/some_group/outbox"), + URLMustParse("https://unknown-instance.com/groups/some_group/collections/featured"), + "some_group", + "This is a group about... something?", + "", + URLMustParse("https://unknown-instance.com/@some_group"), + true, + URLMustParse("https://unknown-instance.com/groups/some_group#main-key"), + newGroup1Pub, + nil, + "image/jpeg", + nil, + "image/png", + false, + ), + } +} + // RemoteAttachmentFile mimics a remote (federated) attachment type RemoteAttachmentFile struct { Data []byte @@ -1688,6 +1719,189 @@ func newPerson( return person } +func newGroup( + profileIDURI *url.URL, + followingURI *url.URL, + followersURI *url.URL, + inboxURI *url.URL, + outboxURI *url.URL, + featuredURI *url.URL, + username string, + displayName string, + note string, + profileURL *url.URL, + discoverable bool, + publicKeyURI *url.URL, + pkey *rsa.PublicKey, + avatarURL *url.URL, + avatarContentType string, + headerURL *url.URL, + headerContentType string, + manuallyApprovesFollowers bool) vocab.ActivityStreamsGroup { + group := streams.NewActivityStreamsGroup() + + // id should be the activitypub URI of this group + // something like https://example.org/users/example_group + idProp := streams.NewJSONLDIdProperty() + idProp.SetIRI(profileIDURI) + group.SetJSONLDId(idProp) + + // following + // The URI for retrieving a list of accounts this group is following + followingProp := streams.NewActivityStreamsFollowingProperty() + followingProp.SetIRI(followingURI) + group.SetActivityStreamsFollowing(followingProp) + + // followers + // The URI for retrieving a list of this user's followers + followersProp := streams.NewActivityStreamsFollowersProperty() + followersProp.SetIRI(followersURI) + group.SetActivityStreamsFollowers(followersProp) + + // inbox + // the activitypub inbox of this user for accepting messages + inboxProp := streams.NewActivityStreamsInboxProperty() + inboxProp.SetIRI(inboxURI) + group.SetActivityStreamsInbox(inboxProp) + + // outbox + // the activitypub outbox of this user for serving messages + outboxProp := streams.NewActivityStreamsOutboxProperty() + outboxProp.SetIRI(outboxURI) + group.SetActivityStreamsOutbox(outboxProp) + + // featured posts + // Pinned posts. + featuredProp := streams.NewTootFeaturedProperty() + featuredProp.SetIRI(featuredURI) + group.SetTootFeatured(featuredProp) + + // featuredTags + // NOT IMPLEMENTED + + // preferredUsername + // Used for Webfinger lookup. Must be unique on the domain, and must correspond to a Webfinger acct: URI. + preferredUsernameProp := streams.NewActivityStreamsPreferredUsernameProperty() + preferredUsernameProp.SetXMLSchemaString(username) + group.SetActivityStreamsPreferredUsername(preferredUsernameProp) + + // name + // Used as profile display name. + nameProp := streams.NewActivityStreamsNameProperty() + if displayName != "" { + nameProp.AppendXMLSchemaString(displayName) + } else { + nameProp.AppendXMLSchemaString(username) + } + group.SetActivityStreamsName(nameProp) + + // summary + // Used as profile bio. + if note != "" { + summaryProp := streams.NewActivityStreamsSummaryProperty() + summaryProp.AppendXMLSchemaString(note) + group.SetActivityStreamsSummary(summaryProp) + } + + // url + // Used as profile link. + urlProp := streams.NewActivityStreamsUrlProperty() + urlProp.AppendIRI(profileURL) + group.SetActivityStreamsUrl(urlProp) + + // manuallyApprovesFollowers + manuallyApprovesFollowersProp := streams.NewActivityStreamsManuallyApprovesFollowersProperty() + manuallyApprovesFollowersProp.Set(manuallyApprovesFollowers) + group.SetActivityStreamsManuallyApprovesFollowers(manuallyApprovesFollowersProp) + + // discoverable + // Will be shown in the profile directory. + discoverableProp := streams.NewTootDiscoverableProperty() + discoverableProp.Set(discoverable) + group.SetTootDiscoverable(discoverableProp) + + // devices + // NOT IMPLEMENTED, probably won't implement + + // alsoKnownAs + // Required for Move activity. + // TODO: NOT IMPLEMENTED **YET** -- this needs to be added as an activitypub extension to https://github.com/go-fed/activity, see https://github.com/go-fed/activity/tree/master/astool + + // publicKey + // Required for signatures. + publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty() + + // create the public key + publicKey := streams.NewW3IDSecurityV1PublicKey() + + // set ID for the public key + publicKeyIDProp := streams.NewJSONLDIdProperty() + publicKeyIDProp.SetIRI(publicKeyURI) + publicKey.SetJSONLDId(publicKeyIDProp) + + // set owner for the public key + publicKeyOwnerProp := streams.NewW3IDSecurityV1OwnerProperty() + publicKeyOwnerProp.SetIRI(profileIDURI) + publicKey.SetW3IDSecurityV1Owner(publicKeyOwnerProp) + + // set the pem key itself + encodedPublicKey, err := x509.MarshalPKIXPublicKey(pkey) + if err != nil { + panic(err) + } + publicKeyBytes := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: encodedPublicKey, + }) + publicKeyPEMProp := streams.NewW3IDSecurityV1PublicKeyPemProperty() + publicKeyPEMProp.Set(string(publicKeyBytes)) + publicKey.SetW3IDSecurityV1PublicKeyPem(publicKeyPEMProp) + + // append the public key to the public key property + publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKey) + + // set the public key property on the Person + group.SetW3IDSecurityV1PublicKey(publicKeyProp) + + // tag + // TODO: Any tags used in the summary of this profile + + // attachment + // Used for profile fields. + // TODO: The PropertyValue type has to be added: https://schema.org/PropertyValue + + // endpoints + // NOT IMPLEMENTED -- this is for shared inbox which we don't use + + // icon + // Used as profile avatar. + iconProperty := streams.NewActivityStreamsIconProperty() + iconImage := streams.NewActivityStreamsImage() + mediaType := streams.NewActivityStreamsMediaTypeProperty() + mediaType.Set(avatarContentType) + iconImage.SetActivityStreamsMediaType(mediaType) + avatarURLProperty := streams.NewActivityStreamsUrlProperty() + avatarURLProperty.AppendIRI(avatarURL) + iconImage.SetActivityStreamsUrl(avatarURLProperty) + iconProperty.AppendActivityStreamsImage(iconImage) + group.SetActivityStreamsIcon(iconProperty) + + // image + // Used as profile header. + headerProperty := streams.NewActivityStreamsImageProperty() + headerImage := streams.NewActivityStreamsImage() + headerMediaType := streams.NewActivityStreamsMediaTypeProperty() + mediaType.Set(headerContentType) + headerImage.SetActivityStreamsMediaType(headerMediaType) + headerURLProperty := streams.NewActivityStreamsUrlProperty() + headerURLProperty.AppendIRI(headerURL) + headerImage.SetActivityStreamsUrl(headerURLProperty) + headerProperty.AppendActivityStreamsImage(headerImage) + group.SetActivityStreamsImage(headerProperty) + + return group +} + func newMention(uri *url.URL, namestring string) vocab.ActivityStreamsMention { mention := streams.NewActivityStreamsMention()