diff --git a/internal/ap/activitystreams.go b/internal/ap/activitystreams.go index a185edabe..0d1bc62c9 100644 --- a/internal/ap/activitystreams.go +++ b/internal/ap/activitystreams.go @@ -98,10 +98,17 @@ const ( // that we don't *yet* know what type of Object something is. ObjectUnknown = "Unknown" - // Extensions and unofficial additions. + /* Extensions and unofficial additions */ + + /* GtS stuff */ + ObjectLikeApproval = "LikeApproval" ObjectReplyApproval = "ReplyApproval" ObjectAnnounceApproval = "AnnounceApproval" + + /* Funkwhale stuff */ + + ObjectAlbum = "Album" ) // isActivity returns whether AS type name is of an Activity (NOT IntransitiveActivity). diff --git a/internal/ap/extract.go b/internal/ap/extract.go index 20f8eb98a..d454d69f6 100644 --- a/internal/ap/extract.go +++ b/internal/ap/extract.go @@ -1078,7 +1078,14 @@ func ExtractInteractionPolicy( statusable Statusable, owner *gtsmodel.Account, ) *gtsmodel.InteractionPolicy { - policyProp := statusable.GetGoToSocialInteractionPolicy() + ipa, ok := statusable.(InteractionPolicyAware) + if !ok { + // Not a type with interaction + // policy properties settable. + return nil + } + + policyProp := ipa.GetGoToSocialInteractionPolicy() if policyProp == nil || policyProp.Len() != 1 { return nil } diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go index 0fd982eb8..4c91c57f6 100644 --- a/internal/ap/interfaces.go +++ b/internal/ap/interfaces.go @@ -76,7 +76,8 @@ func IsStatusable(typeName string) bool { ObjectEvent, ObjectPlace, ObjectProfile, - ActivityQuestion: + ActivityQuestion, + ObjectAlbum: return true default: return false @@ -226,11 +227,13 @@ type Statusable interface { WithTo WithCc WithSensitive - WithConversation WithContent WithAttachment WithTag WithReplies +} + +type InteractionPolicyAware interface { WithInteractionPolicy WithApprovedBy } @@ -589,10 +592,6 @@ type WithSensitive interface { SetActivityStreamsSensitive(vocab.ActivityStreamsSensitiveProperty) } -// WithConversation ... -type WithConversation interface { // TODO -} - // WithContent represents an activity with ActivityStreamsContentProperty type WithContent interface { GetActivityStreamsContent() vocab.ActivityStreamsContentProperty diff --git a/internal/ap/resolve_test.go b/internal/ap/resolve_test.go index aaf90ab0a..b70dba77b 100644 --- a/internal/ap/resolve_test.go +++ b/internal/ap/resolve_test.go @@ -80,6 +80,56 @@ func (suite *ResolveTestSuite) TestResolveNonAPJSONAsAccountable() { suite.Nil(accountable) } +func (suite *ResolveTestSuite) TestResolveBandwagonAlbumAsStatusable() { + b := []byte(`{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "discoverable": "toot:discoverable", + "indexable": "toot:indexable", + "toot": "https://joinmastodon.org/ns#" + }, + "https://funkwhale.audio/ns" + ], + "artists": [ + { + "id": "https://bandwagon.fm/@67a0a0808121f77ed3466870", + "name": "Luka Prinčič", + "type": "Artist" + } + ], + "attachment": [ + { + "mediaType": "image/webp", + "name": "image", + "type": "Document", + "url": "https://bandwagon.fm/67a0a219f050061c8b4ce427/attachments/67a0a21bf050061c8b4ce429" + } + ], + "attributedTo": "https://bandwagon.fm/@67a0a0808121f77ed3466870", + "content": "... a transgenre mutation, a fluid entity, jagged pop, electro-funk, techno-cabaret, a schlager, and soft alternative, queer to the core, satire and tragedy, sharp and fun indulgence for the dance of bodies and brains, activism and hedonism, which would all like to steal your attention.\r\n\r\nDRAGX̶FUNK is pronounced /dɹæɡɑːfʌŋk/.\r\n\r\n---\r\n\r\n## Buy digital\r\n💳 [Stripe](https://buy.stripe.com/6oE8x52iG1Kq5pKeV3)\r\n\r\n---\r\n\r\n## Buy dl/merch\r\n🎵 [Bandcamp](https://lukaprincic.bandcamp.com/album/dragx-funk) \r\n\r\n---\r\n\r\n## More:\r\n🌐 [prin.lu](https://prin.lu/music/241205_dragx-funk/) \r\n👉 [kamizdat.si](https://kamizdat.si/releases/dragx-funk-2/)\r\n", + "context": "https://bandwagon.fm/67a0a219f050061c8b4ce427", + "id": "https://bandwagon.fm/67a0a219f050061c8b4ce427", + "library": "https://bandwagon.fm/67a0a219f050061c8b4ce427/pub/children", + "license": "CC-BY-NC-SA", + "name": "DRAGX̶FUNK", + "published": "2025-03-17T11:40:53Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "tracks": "https://bandwagon.fm/67a0a219f050061c8b4ce427/pub/children", + "type": "Album", + "url": "https://bandwagon.fm/67a0a219f050061c8b4ce427" +}`) + + statusable, err := ap.ResolveStatusable( + context.Background(), io.NopCloser(bytes.NewReader(b)), + ) + suite.NoError(err) + suite.NotNil(statusable) +} + func TestResolveTestSuite(t *testing.T) { suite.Run(t, &ResolveTestSuite{}) } diff --git a/internal/ap/serialize.go b/internal/ap/serialize.go index f1dc095f6..65ba69ba1 100644 --- a/internal/ap/serialize.go +++ b/internal/ap/serialize.go @@ -153,7 +153,9 @@ func serializeStatusable(t vocab.Type, includeContext bool) (map[string]interfac NormalizeOutgoingAttachmentProp(statusable, data) NormalizeOutgoingContentProp(statusable, data) - NormalizeOutgoingInteractionPolicyProp(statusable, data) + if ipa, ok := statusable.(InteractionPolicyAware); ok { + NormalizeOutgoingInteractionPolicyProp(ipa, data) + } return data, nil } diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index 16aa430a3..741e1509e 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -434,9 +434,10 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab return nil, gtserror.SetMalformed(err) } - // Status was sent to us or dereffed - // by us so it must be federated. + // Status was sent to us or dereffed by + // us so it must be federated and not local. status.Federated = util.Ptr(true) + status.Local = util.Ptr(false) // Derive interaction policy for this status. status.InteractionPolicy = ap.ExtractInteractionPolicy( @@ -446,9 +447,11 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab // Set approvedByURI if present, // for later dereferencing. - approvedByURI := ap.GetApprovedBy(statusable) - if approvedByURI != nil { - status.ApprovedByURI = approvedByURI.String() + if ipa, ok := statusable.(ap.InteractionPolicyAware); ok { + approvedByURI := ap.GetApprovedBy(ipa) + if approvedByURI != nil { + status.ApprovedByURI = approvedByURI.String() + } } // Assume not pending approval; this may diff --git a/internal/typeutils/astointernal_test.go b/internal/typeutils/astointernal_test.go index 458913f6f..67b7d75af 100644 --- a/internal/typeutils/astointernal_test.go +++ b/internal/typeutils/astointernal_test.go @@ -21,7 +21,6 @@ import ( "bytes" "context" "encoding/json" - "fmt" "io" "testing" @@ -224,8 +223,7 @@ func (suite *ASToInternalTestSuite) TestParseOwncastService() { b, err := json.Marshal(apiAcct) suite.NoError(err) - - fmt.Printf("\n\n\n%s\n\n\n", string(b)) + suite.NotNil(b) } func (suite *ASToInternalTestSuite) TestParseBookwyrmStatus() { @@ -282,6 +280,65 @@ func (suite *ASToInternalTestSuite) TestParseBookwyrmStatus() { suite.Len(status.Attachments, 1) } +func (suite *ASToInternalTestSuite) TestParseBandwagonAlbum() { + authorAccount := suite.testAccounts["remote_account_1"] + + raw := `{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "discoverable": "toot:discoverable", + "indexable": "toot:indexable", + "toot": "https://joinmastodon.org/ns#" + }, + "https://funkwhale.audio/ns" + ], + "artists": [ + { + "id": "https://bandwagon.fm/@67a0a0808121f77ed3466870", + "name": "Luka Prinčič", + "type": "Artist" + } + ], + "attachment": [ + { + "mediaType": "image/webp", + "name": "image", + "type": "Document", + "url": "https://bandwagon.fm/67a0a219f050061c8b4ce427/attachments/67a0a21bf050061c8b4ce429" + } + ], + "attributedTo": "` + authorAccount.URI + `", + "content": "... a transgenre mutation, a fluid entity, jagged pop, electro-funk, techno-cabaret, a schlager, and soft alternative, queer to the core, satire and tragedy, sharp and fun indulgence for the dance of bodies and brains, activism and hedonism, which would all like to steal your attention.\r\n\r\nDRAGX̶FUNK is pronounced /dɹæɡɑːfʌŋk/.\r\n\r\n---\r\n\r\n## Buy digital\r\n💳 [Stripe](https://buy.stripe.com/6oE8x52iG1Kq5pKeV3)\r\n\r\n---\r\n\r\n## Buy dl/merch\r\n🎵 [Bandcamp](https://lukaprincic.bandcamp.com/album/dragx-funk) \r\n\r\n---\r\n\r\n## More:\r\n🌐 [prin.lu](https://prin.lu/music/241205_dragx-funk/) \r\n👉 [kamizdat.si](https://kamizdat.si/releases/dragx-funk-2/)\r\n", + "context": "https://bandwagon.fm/67a0a219f050061c8b4ce427", + "id": "https://bandwagon.fm/67a0a219f050061c8b4ce427", + "library": "https://bandwagon.fm/67a0a219f050061c8b4ce427/pub/children", + "license": "CC-BY-NC-SA", + "name": "DRAGX̶FUNK", + "published": "2025-03-17T11:40:53Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "tracks": "https://bandwagon.fm/67a0a219f050061c8b4ce427/pub/children", + "type": "Album", + "url": "https://bandwagon.fm/67a0a219f050061c8b4ce427" +}` + + t := suite.jsonToType(raw) + asArticle, ok := t.(ap.Statusable) + if !ok { + suite.FailNow("type not coercible") + } + + s, err := suite.typeconverter.ASStatusToStatus(context.Background(), asArticle) + if err != nil { + suite.FailNow(err.Error()) + } + suite.NotNil(s) + suite.NoError(err) +} + func (suite *ASToInternalTestSuite) TestParseFlag1() { reportedAccount := suite.testAccounts["local_account_1"] reportingAccount := suite.testAccounts["remote_account_1"] diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index c5e055638..7d420de2c 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -705,35 +705,38 @@ func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (ap.Stat status.SetActivityStreamsSensitive(sensitiveProp) // interactionPolicy - var p *gtsmodel.InteractionPolicy - if s.InteractionPolicy != nil { - // Use InteractionPolicy - // set on the status. - p = s.InteractionPolicy - } else { - // Fall back to default policy - // for the status's visibility. - p = gtsmodel.DefaultInteractionPolicyFor(s.Visibility) - } - policy, err := c.InteractionPolicyToASInteractionPolicy(ctx, p, s) - if err != nil { - return nil, fmt.Errorf("error creating interactionPolicy: %w", err) - } - - policyProp := streams.NewGoToSocialInteractionPolicyProperty() - policyProp.AppendGoToSocialInteractionPolicy(policy) - status.SetGoToSocialInteractionPolicy(policyProp) - - // Parse + set approvedBy. - if s.ApprovedByURI != "" { - approvedBy, err := url.Parse(s.ApprovedByURI) + if ipa, ok := status.(ap.InteractionPolicyAware); ok { + var p *gtsmodel.InteractionPolicy + if s.InteractionPolicy != nil { + // Use InteractionPolicy + // set on the status. + p = s.InteractionPolicy + } else { + // Fall back to default policy + // for the status's visibility. + p = gtsmodel.DefaultInteractionPolicyFor(s.Visibility) + } + policy, err := c.InteractionPolicyToASInteractionPolicy(ctx, p, s) if err != nil { - return nil, fmt.Errorf("error parsing approvedBy: %w", err) + return nil, fmt.Errorf("error creating interactionPolicy: %w", err) } - approvedByProp := streams.NewGoToSocialApprovedByProperty() - approvedByProp.Set(approvedBy) - status.SetGoToSocialApprovedBy(approvedByProp) + // Set interaction policy. + policyProp := streams.NewGoToSocialInteractionPolicyProperty() + policyProp.AppendGoToSocialInteractionPolicy(policy) + ipa.SetGoToSocialInteractionPolicy(policyProp) + + // Parse + set approvedBy. + if s.ApprovedByURI != "" { + approvedBy, err := url.Parse(s.ApprovedByURI) + if err != nil { + return nil, fmt.Errorf("error parsing approvedBy: %w", err) + } + + approvedByProp := streams.NewGoToSocialApprovedByProperty() + approvedByProp.Set(approvedBy) + ipa.SetGoToSocialApprovedBy(approvedByProp) + } } return status, nil