mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-04 00:29:49 +00:00
[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>
This commit is contained in:
parent
29d481d769
commit
bfc8c31e5f
5 changed files with 238 additions and 6 deletions
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue