summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar nicole mikołajczyk <git@mkljczk.pl>2025-06-19 15:10:41 +0200
committerLibravatar tobi <kipvandenbos@noreply.codeberg.org>2025-06-19 15:10:41 +0200
commitbfc8c31e5f80bd7e405ad407d58597a92a1e85fd (patch)
tree53645d0ddfdd32469f36eaa649b31be14d8018c3
parent[bugfix] delete interaction requests when deleting account (#4278) (diff)
downloadgotosocial-bfc8c31e5f80bd7e405ad407d58597a92a1e85fd.tar.xz
[feature] Support incoming avatar/header descriptions (#4275)
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl> # Description Follow-up to #4270 Closes https://codeberg.org/superseriousbusiness/gotosocial/issues/3450 ## Checklist - [x] I/we have read the [GoToSocial contribution guidelines](https://codeberg.org/superseriousbusiness/gotosocial/src/branch/main/CONTRIBUTING.md). - [ ] I/we have discussed the proposed changes already, either in an issue on the repository, or in the Matrix chat. - [x] I/we have not leveraged AI to create the proposed changes. - [x] I/we have performed a self-review of added code. - [x] I/we have written code that is legible and maintainable by others. - [x] I/we have commented the added code, particularly in hard-to-understand areas. - [ ] I/we have made any necessary changes to documentation. - [x] I/we have added tests that cover new code. - [x] I/we have run tests and they pass locally with the changes. - [x] I/we have run `go fmt ./...` and `golangci-lint run`. Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4275 Co-authored-by: nicole mikołajczyk <git@mkljczk.pl> Co-committed-by: nicole mikołajczyk <git@mkljczk.pl>
-rw-r--r--internal/ap/extract.go96
-rw-r--r--internal/federation/dereferencing/account.go44
-rw-r--r--internal/federation/dereferencing/account_test.go44
-rw-r--r--testrig/testmodels.go49
-rw-r--r--testrig/transportcontroller.go11
5 files changed, 238 insertions, 6 deletions
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",