mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 02:42:25 -05:00 
			
		
		
		
	allow dereferencing of groups (#256)
This commit is contained in:
		
					parent
					
						
							
								231075f28d
							
						
					
				
			
			
				commit
				
					
						0cd2bd2960
					
				
			
		
					 4 changed files with 310 additions and 14 deletions
				
			
		|  | @ -165,18 +165,30 @@ func (d *deref) dereferenceAccountable(ctx context.Context, username string, rem | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	switch t.GetTypeName() { | 	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: | 	case ap.ActorApplication: | ||||||
| 		p, ok := t.(vocab.ActivityStreamsApplication) | 		p, ok := t.(vocab.ActivityStreamsApplication) | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			return nil, errors.New("DereferenceAccountable: error resolving type as activitystreams application") | 			return nil, errors.New("DereferenceAccountable: error resolving type as activitystreams application") | ||||||
| 		} | 		} | ||||||
| 		return p, nil | 		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: | 	case ap.ActorService: | ||||||
| 		p, ok := t.(vocab.ActivityStreamsService) | 		p, ok := t.(vocab.ActivityStreamsService) | ||||||
| 		if !ok { | 		if !ok { | ||||||
|  |  | ||||||
							
								
								
									
										57
									
								
								internal/federation/dereferencing/account_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								internal/federation/dereferencing/account_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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)) | ||||||
|  | } | ||||||
|  | @ -45,7 +45,8 @@ type DereferencerStandardTestSuite struct { | ||||||
| 	storage *kv.KVStore | 	storage *kv.KVStore | ||||||
| 
 | 
 | ||||||
| 	testRemoteStatuses    map[string]vocab.ActivityStreamsNote | 	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 | 	testRemoteAttachments map[string]testrig.RemoteAttachmentFile | ||||||
| 	testAccounts          map[string]*gtsmodel.Account | 	testAccounts          map[string]*gtsmodel.Account | ||||||
| 
 | 
 | ||||||
|  | @ -55,7 +56,8 @@ type DereferencerStandardTestSuite struct { | ||||||
| func (suite *DereferencerStandardTestSuite) SetupSuite() { | func (suite *DereferencerStandardTestSuite) SetupSuite() { | ||||||
| 	suite.testAccounts = testrig.NewTestAccounts() | 	suite.testAccounts = testrig.NewTestAccounts() | ||||||
| 	suite.testRemoteStatuses = testrig.NewTestFediStatuses() | 	suite.testRemoteStatuses = testrig.NewTestFediStatuses() | ||||||
| 	suite.testRemoteAccounts = testrig.NewTestFediPeople() | 	suite.testRemotePeople = testrig.NewTestFediPeople() | ||||||
|  | 	suite.testRemoteGroups = testrig.NewTestFediGroups() | ||||||
| 	suite.testRemoteAttachments = testrig.NewTestFediAttachments("../../../testrig/media") | 	suite.testRemoteAttachments = testrig.NewTestFediAttachments("../../../testrig/media") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -89,8 +91,7 @@ func (suite *DereferencerStandardTestSuite) mockTransportController() transport. | ||||||
| 		responseType := "" | 		responseType := "" | ||||||
| 		responseLength := 0 | 		responseLength := 0 | ||||||
| 
 | 
 | ||||||
| 		note, ok := suite.testRemoteStatuses[req.URL.String()] | 		if note, ok := suite.testRemoteStatuses[req.URL.String()]; ok { | ||||||
| 		if ok { |  | ||||||
| 			// the request is for a note that we have stored | 			// the request is for a note that we have stored | ||||||
| 			noteI, err := streams.Serialize(note) | 			noteI, err := streams.Serialize(note) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
|  | @ -104,8 +105,7 @@ func (suite *DereferencerStandardTestSuite) mockTransportController() transport. | ||||||
| 			responseType = "application/activity+json" | 			responseType = "application/activity+json" | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		person, ok := suite.testRemoteAccounts[req.URL.String()] | 		if person, ok := suite.testRemotePeople[req.URL.String()]; ok { | ||||||
| 		if ok { |  | ||||||
| 			// the request is for a person that we have stored | 			// the request is for a person that we have stored | ||||||
| 			personI, err := streams.Serialize(person) | 			personI, err := streams.Serialize(person) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
|  | @ -119,8 +119,21 @@ func (suite *DereferencerStandardTestSuite) mockTransportController() transport. | ||||||
| 			responseType = "application/activity+json" | 			responseType = "application/activity+json" | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		attachment, ok := suite.testRemoteAttachments[req.URL.String()] | 		if group, ok := suite.testRemoteGroups[req.URL.String()]; ok { | ||||||
| 		if 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 | 			responseBytes = attachment.Data | ||||||
| 			responseType = attachment.ContentType | 			responseType = attachment.ContentType | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -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 | // RemoteAttachmentFile mimics a remote (federated) attachment | ||||||
| type RemoteAttachmentFile struct { | type RemoteAttachmentFile struct { | ||||||
| 	Data        []byte | 	Data        []byte | ||||||
|  | @ -1688,6 +1719,189 @@ func newPerson( | ||||||
| 	return person | 	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 { | func newMention(uri *url.URL, namestring string) vocab.ActivityStreamsMention { | ||||||
| 	mention := streams.NewActivityStreamsMention() | 	mention := streams.NewActivityStreamsMention() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue