diff --git a/internal/ap/extract.go b/internal/ap/extract.go index 8dbb903a3..15d24cd1e 100644 --- a/internal/ap/extract.go +++ b/internal/ap/extract.go @@ -370,6 +370,54 @@ func ExtractIconURI(i WithIcon) (*url.URL, error) { return nil, gtserror.New("could not extract valid image URI from icon") } +// ExtractIconDescription extracts the name property from +// the given WithIcon which links to a supported image file, +// or returns an empty string. +// Input will look something like this: +// +// "icon": { +// "mediaType": "image/jpeg", +// "name": "some description", +// "type": "Image", +// "url": "http://example.org/path/to/some/file.jpeg" +// }, +func ExtractIconDescription(i WithIcon) string { + iconProp := i.GetActivityStreamsIcon() + if iconProp == nil { + return "" + } + + // Icon can potentially contain multiple entries, + // so we iterate through all of them here in order + // to find the first one that meets these criteria: + // + // 1. Is an image. + // 2. Has a URL that we can use to derefereince it. + for iter := iconProp.Begin(); iter != iconProp.End(); iter = iter.Next() { + if !iter.IsActivityStreamsImage() { + continue + } + + image := iter.GetActivityStreamsImage() + if image == nil { + continue + } + + imageURL := GetURL(image) + if len(imageURL) == 0 { + // Nothing here. + continue + } + + imageDescription := ExtractName(image) + + // Got a hit. + return imageDescription + } + + return "" +} + // ExtractImageURI extracts the first URI it can find from // the given WithImage which links to a supported image file. // Input will look something like this: @@ -416,6 +464,54 @@ func ExtractImageURI(i WithImage) (*url.URL, error) { return nil, gtserror.New("could not extract valid image URI from image") } +// ExtractImageDescription extracts the name property from +// the given WithImage which links to a supported image file, +// or returns an empty string. +// Input will look something like this: +// +// "image": { +// "mediaType": "image/jpeg", +// "name": "some description", +// "type": "Image", +// "url": "http://example.org/path/to/some/file.jpeg" +// }, +func ExtractImageDescription(i WithImage) string { + imageProp := i.GetActivityStreamsImage() + if imageProp == nil { + return "" + } + + // Image can potentially contain multiple entries, + // so we iterate through all of them here in order + // to find the first one that meets these criteria: + // + // 1. Is an image. + // 2. Has a URL that we can use to derefereince it. + for iter := imageProp.Begin(); iter != imageProp.End(); iter = iter.Next() { + if !iter.IsActivityStreamsImage() { + continue + } + + image := iter.GetActivityStreamsImage() + if image == nil { + continue + } + + imageURL := GetURL(image) + if len(imageURL) == 0 { + // Nothing here. + continue + } + + imageDescription := ExtractName(image) + + // Got a hit. + return imageDescription + } + + return "" +} + // ExtractSummary extracts the summary/content warning of // the given WithSummary interface. Will return an empty // string if no summary/content warning was present. diff --git a/internal/federation/dereferencing/account.go b/internal/federation/dereferencing/account.go index 6c9afc665..effc22c44 100644 --- a/internal/federation/dereferencing/account.go +++ b/internal/federation/dereferencing/account.go @@ -807,12 +807,12 @@ func (d *Dereferencer) enrichAccount( latestAcc.UpdatedAt = now // Ensure the account's avatar media is populated, passing in existing to check for chages. - if err := d.fetchAccountAvatar(ctx, requestUser, account, latestAcc); err != nil { + if err := d.fetchAccountAvatar(ctx, requestUser, account, latestAcc, apubAcc); err != nil { log.Errorf(ctx, "error fetching remote avatar for account %s: %v", uri, err) } // Ensure the account's avatar media is populated, passing in existing to check for chages. - if err := d.fetchAccountHeader(ctx, requestUser, account, latestAcc); err != nil { + if err := d.fetchAccountHeader(ctx, requestUser, account, latestAcc, apubAcc); err != nil { log.Errorf(ctx, "error fetching remote header for account %s: %v", uri, err) } @@ -854,6 +854,7 @@ func (d *Dereferencer) fetchAccountAvatar( requestUser string, existingAcc *gtsmodel.Account, latestAcc *gtsmodel.Account, + apubAcc ap.Accountable, ) error { if latestAcc.AvatarRemoteURL == "" { // No avatar set on newest model, leave @@ -861,6 +862,8 @@ func (d *Dereferencer) fetchAccountAvatar( return nil } + avatarDescription := ap.ExtractIconDescription(apubAcc) + // Check for an existing stored media attachment // specifically with unchanged remote URL we can use. if existingAcc.AvatarMediaAttachmentID != "" && @@ -883,6 +886,18 @@ func (d *Dereferencer) fetchAccountAvatar( nil, ) + if existing.Description != avatarDescription { + existing.Description = avatarDescription + if err := d.state.DB.UpdateAttachment( + ctx, + existing, + "description", + ); err != nil { + err := gtserror.Newf("db error updating existing avatar description: %w", err) + return gtserror.NewErrorInternalError(err) + } + } + if err != nil { log.Errorf(ctx, "error updating existing attachment: %v", err) @@ -906,8 +921,9 @@ func (d *Dereferencer) fetchAccountAvatar( latestAcc.ID, latestAcc.AvatarRemoteURL, media.AdditionalMediaInfo{ - Avatar: util.Ptr(true), - RemoteURL: &latestAcc.AvatarRemoteURL, + Avatar: util.Ptr(true), + RemoteURL: &latestAcc.AvatarRemoteURL, + Description: &avatarDescription, }, ) if err != nil { @@ -931,6 +947,7 @@ func (d *Dereferencer) fetchAccountHeader( requestUser string, existingAcc *gtsmodel.Account, latestAcc *gtsmodel.Account, + apubAcc ap.Accountable, ) error { if latestAcc.HeaderRemoteURL == "" { // No header set on newest model, leave @@ -938,6 +955,8 @@ func (d *Dereferencer) fetchAccountHeader( return nil } + headerDescription := ap.ExtractImageDescription(apubAcc) + // Check for an existing stored media attachment // specifically with unchanged remote URL we can use. if existingAcc.HeaderMediaAttachmentID != "" && @@ -951,6 +970,18 @@ func (d *Dereferencer) fetchAccountHeader( return gtserror.Newf("error getting attachment %s: %w", existingAcc.HeaderMediaAttachmentID, err) } + if existing.Description != headerDescription { + existing.Description = headerDescription + if err := d.state.DB.UpdateAttachment( + ctx, + existing, + "description", + ); err != nil { + err := gtserror.Newf("db error updating existing header description: %w", err) + return gtserror.NewErrorInternalError(err) + } + } + if existing != nil { // Ensuring existing attachment is up-to-date // and any recaching is performed if required. @@ -983,8 +1014,9 @@ func (d *Dereferencer) fetchAccountHeader( latestAcc.ID, latestAcc.HeaderRemoteURL, media.AdditionalMediaInfo{ - Header: util.Ptr(true), - RemoteURL: &latestAcc.HeaderRemoteURL, + Header: util.Ptr(true), + RemoteURL: &latestAcc.HeaderRemoteURL, + Description: &headerDescription, }, ) if err != nil { diff --git a/internal/federation/dereferencing/account_test.go b/internal/federation/dereferencing/account_test.go index 518f3e89f..0cb23f18d 100644 --- a/internal/federation/dereferencing/account_test.go +++ b/internal/federation/dereferencing/account_test.go @@ -28,6 +28,7 @@ import ( "io" "net/http" "net/url" + "strings" "testing" "time" @@ -473,6 +474,49 @@ func (suite *AccountTestSuite) TestRefreshFederatedRemoteAccountWithKeyChange() suite.True(updatedAcc.PublicKey.Equal(fetchingAcc.PublicKey)) } +func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithAvatarDescription() { + ctx, cncl := context.WithCancel(suite.T().Context()) + defer cncl() + + fetchingAcc := suite.testAccounts["local_account_1"] + remoteURI := "https://shrimpnet.example.org/users/shrimp" + description := "me scrolling fedi on a laptop, there's a monster ultra white and another fedi user on my right." + + // Fetch the remote account to load into the database. + remoteAcc, _, err := suite.dereferencer.GetAccountByURI(ctx, + fetchingAcc.Username, + testrig.URLMustParse(remoteURI), + false, + ) + suite.NoError(err) + suite.NotNil(remoteAcc) + + suite.Equal(remoteAcc.AvatarMediaAttachment.Description, description) + + remotePerson := suite.client.TestRemotePeople[remoteURI] + + description = strings.TrimSuffix(description, ".") + + icon := remotePerson.GetActivityStreamsIcon() + image := icon.Begin().GetActivityStreamsImage() + nameProp := streams.NewActivityStreamsNameProperty() + nameProp.AppendXMLSchemaString(description) + image.SetActivityStreamsName(nameProp) + icon.SetActivityStreamsImage(0, image) + remotePerson.SetActivityStreamsIcon(icon) + + updatedAcc, apAcc, err := suite.dereferencer.RefreshAccount(ctx, + fetchingAcc.Username, + remoteAcc, + remotePerson, + nil, + ) + + suite.NoError(err) + suite.NotNil(apAcc) + suite.Equal(updatedAcc.AvatarMediaAttachment.Description, description) +} + func TestAccountTestSuite(t *testing.T) { suite.Run(t, new(AccountTestSuite)) } diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 86ea32fce..42caf59bd 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -3578,6 +3578,12 @@ func NewTestFediPeople() map[string]vocab.ActivityStreamsPerson { } someUserPub := &someUserPriv.PublicKey + shrimpPriv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + shrimpPub := &shrimpPriv.PublicKey + return map[string]vocab.ActivityStreamsPerson{ "https://unknown-instance.com/users/brand_new_person": newAPPerson( URLMustParse("https://unknown-instance.com/users/brand_new_person"), @@ -3599,7 +3605,9 @@ func NewTestFediPeople() map[string]vocab.ActivityStreamsPerson { nil, "image/jpeg", nil, + nil, "image/png", + nil, false, ), "https://turnip.farm/users/turniplover6969": newAPPerson( @@ -3622,7 +3630,9 @@ func NewTestFediPeople() map[string]vocab.ActivityStreamsPerson { nil, "image/jpeg", nil, + nil, "image/png", + nil, false, ), "http://example.org/users/Some_User": newAPPerson( @@ -3645,7 +3655,34 @@ func NewTestFediPeople() map[string]vocab.ActivityStreamsPerson { nil, "image/jpeg", nil, + nil, "image/png", + nil, + false, + ), + "https://shrimpnet.example.org/users/shrimp": newAPPerson( + URLMustParse("https://shrimpnet.example.org/users/shrimp"), + URLMustParse("https://shrimpnet.example.org/users/shrimp/following"), + URLMustParse("https://shrimpnet.example.org/users/shrimp/followers"), + URLMustParse("https://shrimpnet.example.org/users/shrimp/inbox"), + URLMustParse("https://shrimpnet.example.org/inbox"), + URLMustParse("https://shrimpnet.example.org/users/shrimp/outbox"), + URLMustParse("https://shrimpnet.example.org/users/shrimp/collections/featured"), + nil, + nil, + "shrimp", + "Shrimp", + "", + URLMustParse("https://shrimpnet.example.org/@shrimp"), + true, + URLMustParse("https://shrimpnet.example.org/users/shrimp#main-key"), + shrimpPub, + URLMustParse("https://shrimpnet.example.org/files/public-1c8468b8-eb2d-485f-9967-f4238ded95e7.webp"), + "image/jpeg", + util.Ptr("me scrolling fedi on a laptop, there's a monster ultra white and another fedi user on my right."), + nil, + "image/png", + nil, false, ), } @@ -4398,8 +4435,10 @@ func newAPPerson( pkey *rsa.PublicKey, avatarURL *url.URL, avatarContentType string, + avatarDescription *string, headerURL *url.URL, headerContentType string, + headerDescription *string, manuallyApprovesFollowers bool, ) vocab.ActivityStreamsPerson { person := streams.NewActivityStreamsPerson() @@ -4564,6 +4603,11 @@ func newAPPerson( avatarURLProperty := streams.NewActivityStreamsUrlProperty() avatarURLProperty.AppendIRI(avatarURL) iconImage.SetActivityStreamsUrl(avatarURLProperty) + if avatarDescription != nil { + nameProp := streams.NewActivityStreamsNameProperty() + nameProp.AppendXMLSchemaString(*avatarDescription) + iconImage.SetActivityStreamsName(nameProp) + } iconProperty.AppendActivityStreamsImage(iconImage) person.SetActivityStreamsIcon(iconProperty) @@ -4577,6 +4621,11 @@ func newAPPerson( headerURLProperty := streams.NewActivityStreamsUrlProperty() headerURLProperty.AppendIRI(headerURL) headerImage.SetActivityStreamsUrl(headerURLProperty) + if headerDescription != nil { + nameProp := streams.NewActivityStreamsNameProperty() + nameProp.AppendXMLSchemaString(*headerDescription) + headerImage.SetActivityStreamsName(nameProp) + } headerProperty.AppendActivityStreamsImage(headerImage) person.SetActivityStreamsImage(headerProperty) diff --git a/testrig/transportcontroller.go b/testrig/transportcontroller.go index 641232b73..16bbfe1a6 100644 --- a/testrig/transportcontroller.go +++ b/testrig/transportcontroller.go @@ -566,6 +566,17 @@ func WebfingerResponse(req *http.Request) ( }, }, } + case "https://shrimpnet.example.org/.well-known/webfinger?resource=acct%3Ashrimp%40shrimpnet.example.org": + wfr = &apimodel.WellKnownResponse{ + Subject: "acct:shrimp@shrimpnet.example.org", + Links: []apimodel.Link{ + { + Rel: "self", + Type: applicationActivityJSON, + Href: "https://shrimpnet.example.org/users/shrimp", + }, + }, + } case "https://misconfigured-instance.com/.weird-webfinger-location/webfinger?resource=acct%3Asomeone%40misconfigured-instance.com": wfr = &apimodel.WellKnownResponse{ Subject: "acct:someone@misconfigured-instance.com",