commit 0cd2bd2960e29a3e8c39de543f7e9a8d635e2221
parent 231075f28df55bfb622f54859f858528445cae0c
Author: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Thu, 30 Sep 2021 12:27:42 +0200
allow dereferencing of groups (#256)
Diffstat:
4 files changed, 310 insertions(+), 14 deletions(-)
diff --git 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
@@ -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 <http://www.gnu.org/licenses/>.
+*/
+
+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
@@ -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
@@ -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()