diff --git a/internal/federation/dereferencing/account_test.go b/internal/federation/dereferencing/account_test.go index cb6f9588c..75c02af75 100644 --- a/internal/federation/dereferencing/account_test.go +++ b/internal/federation/dereferencing/account_test.go @@ -51,6 +51,26 @@ func (suite *AccountTestSuite) TestDereferenceGroup() { suite.Equal(ap.ActorGroup, dbGroup.ActorType) } +func (suite *AccountTestSuite) TestDereferenceService() { + fetchingAccount := suite.testAccounts["local_account_1"] + + serviceURL := testrig.URLMustParse("https://owncast.example.org/federation/user/rgh") + service, err := suite.dereferencer.GetRemoteAccount(context.Background(), fetchingAccount.Username, serviceURL, false, false) + suite.NoError(err) + suite.NotNil(service) + suite.NotNil(service) + + // service values should be set + suite.Equal("https://owncast.example.org/federation/user/rgh", service.URI) + suite.Equal("https://owncast.example.org/federation/user/rgh", service.URL) + + // service should be in the database + dbService, err := suite.db.GetAccountByURI(context.Background(), service.URI) + suite.NoError(err) + suite.Equal(service.ID, dbService.ID) + suite.Equal(ap.ActorService, dbService.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 339490e5d..96ec7869f 100644 --- a/internal/federation/dereferencing/dereferencer_test.go +++ b/internal/federation/dereferencing/dereferencer_test.go @@ -46,6 +46,7 @@ type DereferencerStandardTestSuite struct { testRemoteStatuses map[string]vocab.ActivityStreamsNote testRemotePeople map[string]vocab.ActivityStreamsPerson testRemoteGroups map[string]vocab.ActivityStreamsGroup + testRemoteServices map[string]vocab.ActivityStreamsService testRemoteAttachments map[string]testrig.RemoteAttachmentFile testAccounts map[string]*gtsmodel.Account @@ -60,6 +61,7 @@ func (suite *DereferencerStandardTestSuite) SetupTest() { suite.testRemoteStatuses = testrig.NewTestFediStatuses() suite.testRemotePeople = testrig.NewTestFediPeople() suite.testRemoteGroups = testrig.NewTestFediGroups() + suite.testRemoteServices = testrig.NewTestFediServices() suite.testRemoteAttachments = testrig.NewTestFediAttachments("../../../testrig/media") suite.db = testrig.NewTestDB() @@ -126,6 +128,19 @@ func (suite *DereferencerStandardTestSuite) mockTransportController() transport. responseType = "application/activity+json" } + if service, ok := suite.testRemoteServices[req.URL.String()]; ok { + serviceI, err := streams.Serialize(service) + if err != nil { + panic(err) + } + serviceJson, err := json.Marshal(serviceI) + if err != nil { + panic(err) + } + responseBytes = serviceJson + responseType = "application/activity+json" + } + if attachment, ok := suite.testRemoteAttachments[req.URL.String()]; ok { responseBytes = attachment.Data responseType = attachment.ContentType diff --git a/internal/typeutils/astointernal_test.go b/internal/typeutils/astointernal_test.go index d06178fb1..25fe0884a 100644 --- a/internal/typeutils/astointernal_test.go +++ b/internal/typeutils/astointernal_test.go @@ -157,6 +157,49 @@ func (suite *ASToInternalTestSuite) TestParseReplyWithMention() { suite.Equal(gtsmodel.VisibilityUnlocked, status.Visibility) } +func (suite *ASToInternalTestSuite) TestParseOwncastService() { + m := make(map[string]interface{}) + err := json.Unmarshal([]byte(owncastService), &m) + suite.NoError(err) + + t, err := streams.ToType(context.Background(), m) + suite.NoError(err) + + rep, ok := t.(ap.Accountable) + suite.True(ok) + + acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, false) + suite.NoError(err) + + suite.Equal("rgh", acct.Username) + suite.Equal("owncast.example.org", acct.Domain) + suite.Equal("https://owncast.example.org/logo/external", acct.AvatarRemoteURL) + suite.Equal("https://owncast.example.org/logo/external", acct.HeaderRemoteURL) + suite.Equal("Rob's Owncast Server", acct.DisplayName) + suite.Equal("linux audio stuff ", acct.Note) + suite.True(acct.Bot) + suite.False(acct.Locked) + suite.True(acct.Discoverable) + suite.Equal("https://owncast.example.org/federation/user/rgh", acct.URI) + suite.Equal("https://owncast.example.org/federation/user/rgh", acct.URL) + suite.Equal("https://owncast.example.org/federation/user/rgh/inbox", acct.InboxURI) + suite.Equal("https://owncast.example.org/federation/user/rgh/outbox", acct.OutboxURI) + suite.Equal("https://owncast.example.org/federation/user/rgh/followers", acct.FollowersURI) + suite.Equal("Service", acct.ActorType) + suite.Equal("https://owncast.example.org/federation/user/rgh#main-key", acct.PublicKeyURI) + + acct.ID = "01G42D57DTCJQE8XT9KD4K88RK" + + apiAcct, err := suite.typeconverter.AccountToAPIAccountPublic(context.Background(), acct) + suite.NoError(err) + suite.NotNil(apiAcct) + + b, err := json.Marshal(apiAcct) + suite.NoError(err) + + fmt.Printf("\n\n\n%s\n\n\n", string(b)) +} + func TestASToInternalTestSuite(t *testing.T) { suite.Run(t, new(ASToInternalTestSuite)) } diff --git a/internal/typeutils/converter_test.go b/internal/typeutils/converter_test.go index 4d782c822..0983669ac 100644 --- a/internal/typeutils/converter_test.go +++ b/internal/typeutils/converter_test.go @@ -414,6 +414,57 @@ const ( } } ` + owncastService = ` + { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "http://joinmastodon.org/ns", + "https://w3id.org/security/v1" + ], + "attachment": { + "name": "Stream", + "type": "PropertyValue", + "value": "https://owncast.example.org" + }, + "discoverable": true, + "followers": "https://owncast.example.org/federation/user/rgh/followers", + "icon": { + "type": "Image", + "url": "https://owncast.example.org/logo/external" + }, + "id": "https://owncast.example.org/federation/user/rgh", + "image": { + "type": "Image", + "url": "https://owncast.example.org/logo/external" + }, + "inbox": "https://owncast.example.org/federation/user/rgh/inbox", + "manuallyApprovesFollowers": false, + "name": "Rob's Owncast Server", + "outbox": "https://owncast.example.org/federation/user/rgh/outbox", + "preferredUsername": "rgh", + "publicKey": { + "id": "https://owncast.example.org/federation/user/rgh#main-key", + "owner": "https://owncast.example.org/federation/user/rgh", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAurN+lUNwcGV2poLNtaoT\naRtJzN6s4SDcBmIFk82lxhdMKC6/Nssm+hvDuxWGqL0+dHwSvrG11rA6irGuSzRk\niHjYyVwYe/p1CxqJxzUfZVJAWdsCFWy+HtDrTWs5sggj1MiL59uGxvkCep+OYBuG\nBI8CvSOMLrDp8soCg3EY+zSgpXtGMuRaaUukavsfuglApShB61ny7W8LG252iKC5\nmyO8L7l8TNa5BrIi/pRHLzvv9aWiCa8VKtvmqj+mClEhpkRhImSk5GPJXgouTTgl\ntT28NYYciSf9YYgZ0SNWHdLUCdkMF592j4+BbkPvdgzc70G4yyu2GcWnTzBuvF5X\nYwIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "published": "2022-05-22T18:44:57Z", + "summary": "linux audio stuff ", + "tag": [ + { + "href": "https://directory.owncast.online/tags/owncast", + "name": "#owncast", + "type": "Hashtag" + }, + { + "href": "https://directory.owncast.online/tags/streaming", + "name": "#streaming", + "type": "Hashtag" + } + ], + "type": "Service", + "url": "https://owncast.example.org/federation/user/rgh" + } +` ) type TypeUtilsTestSuite struct { diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 98dcdfefb..ee601e053 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -1821,6 +1821,37 @@ func NewTestFediGroups() map[string]vocab.ActivityStreamsGroup { } } +func NewTestFediServices() map[string]vocab.ActivityStreamsService { + newService1Priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + newService1Pub := &newService1Priv.PublicKey + + return map[string]vocab.ActivityStreamsService{ + "https://owncast.example.org/federation/user/rgh": newAPService( + URLMustParse("https://owncast.example.org/federation/user/rgh"), + nil, + URLMustParse("https://owncast.example.org/federation/user/rgh/followers"), + URLMustParse("https://owncast.example.org/federation/user/rgh/inbox"), + URLMustParse("https://owncast.example.org/federation/user/rgh/outbox"), + nil, + "rgh", + "linux audio stuff ", + "", + URLMustParse("https://owncast.example.org/federation/user/rgh"), + true, + URLMustParse("https://owncast.example.org/federation/user/rgh#main-key"), + newService1Pub, + nil, + "image/jpeg", + nil, + "image/png", + false, + ), + } +} + // RemoteAttachmentFile mimics a remote (federated) attachment type RemoteAttachmentFile struct { Data []byte @@ -2459,6 +2490,190 @@ func newAPGroup( return group } +func newAPService( + 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.ActivityStreamsService { + service := streams.NewActivityStreamsService() + + // id should be the activitypub URI of this group + // something like https://example.org/users/example_group + idProp := streams.NewJSONLDIdProperty() + idProp.SetIRI(profileIDURI) + service.SetJSONLDId(idProp) + + // following + // The URI for retrieving a list of accounts this group is following + followingProp := streams.NewActivityStreamsFollowingProperty() + followingProp.SetIRI(followingURI) + service.SetActivityStreamsFollowing(followingProp) + + // followers + // The URI for retrieving a list of this user's followers + followersProp := streams.NewActivityStreamsFollowersProperty() + followersProp.SetIRI(followersURI) + service.SetActivityStreamsFollowers(followersProp) + + // inbox + // the activitypub inbox of this user for accepting messages + inboxProp := streams.NewActivityStreamsInboxProperty() + inboxProp.SetIRI(inboxURI) + service.SetActivityStreamsInbox(inboxProp) + + // outbox + // the activitypub outbox of this user for serving messages + outboxProp := streams.NewActivityStreamsOutboxProperty() + outboxProp.SetIRI(outboxURI) + service.SetActivityStreamsOutbox(outboxProp) + + // featured posts + // Pinned posts. + featuredProp := streams.NewTootFeaturedProperty() + featuredProp.SetIRI(featuredURI) + service.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) + service.SetActivityStreamsPreferredUsername(preferredUsernameProp) + + // name + // Used as profile display name. + nameProp := streams.NewActivityStreamsNameProperty() + if displayName != "" { + nameProp.AppendXMLSchemaString(displayName) + } else { + nameProp.AppendXMLSchemaString(username) + } + service.SetActivityStreamsName(nameProp) + + // summary + // Used as profile bio. + if note != "" { + summaryProp := streams.NewActivityStreamsSummaryProperty() + summaryProp.AppendXMLSchemaString(note) + service.SetActivityStreamsSummary(summaryProp) + } + + // url + // Used as profile link. + urlProp := streams.NewActivityStreamsUrlProperty() + urlProp.AppendIRI(profileURL) + service.SetActivityStreamsUrl(urlProp) + + // manuallyApprovesFollowers + manuallyApprovesFollowersProp := streams.NewActivityStreamsManuallyApprovesFollowersProperty() + manuallyApprovesFollowersProp.Set(manuallyApprovesFollowers) + service.SetActivityStreamsManuallyApprovesFollowers(manuallyApprovesFollowersProp) + + // discoverable + // Will be shown in the profile directory. + discoverableProp := streams.NewTootDiscoverableProperty() + discoverableProp.Set(discoverable) + service.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 + service.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) + service.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) + service.SetActivityStreamsImage(headerProperty) + + return service +} + func newAPMention(uri *url.URL, namestring string) vocab.ActivityStreamsMention { mention := streams.NewActivityStreamsMention()