diff options
author | 2024-04-02 11:42:24 +0200 | |
---|---|---|
committer | 2024-04-02 10:42:24 +0100 | |
commit | f05874be3095d3fb3cefd1a92b3c35fe3ae3bf28 (patch) | |
tree | 52f1616259b51d0a8a94a786278b9c0aa5ab2298 /internal | |
parent | [feature] Add `enable` command to mirror existing `disable` command; update d... (diff) | |
download | gotosocial-f05874be3095d3fb3cefd1a92b3c35fe3ae3bf28.tar.xz |
[feature] Option to hide followers/following (#2788)
Diffstat (limited to 'internal')
-rw-r--r-- | internal/ap/activitystreams_test.go | 60 | ||||
-rw-r--r-- | internal/ap/collections.go | 7 | ||||
-rw-r--r-- | internal/api/client/accounts/accountupdate.go | 16 | ||||
-rw-r--r-- | internal/api/client/accounts/followers.go | 2 | ||||
-rw-r--r-- | internal/api/client/accounts/following.go | 2 | ||||
-rw-r--r-- | internal/api/client/admin/reportsget_test.go | 4 | ||||
-rw-r--r-- | internal/api/model/account.go | 10 | ||||
-rw-r--r-- | internal/processing/account/relationships.go | 32 | ||||
-rw-r--r-- | internal/processing/account/update.go | 4 | ||||
-rw-r--r-- | internal/processing/fedi/collections.go | 31 | ||||
-rw-r--r-- | internal/processing/fedi/status.go | 1 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend.go | 67 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend_test.go | 5 |
13 files changed, 197 insertions, 44 deletions
diff --git a/internal/ap/activitystreams_test.go b/internal/ap/activitystreams_test.go index d769fa42f..edade9718 100644 --- a/internal/ap/activitystreams_test.go +++ b/internal/ap/activitystreams_test.go @@ -49,6 +49,7 @@ func TestASCollection(t *testing.T) { // Create new collection using builder function. c := ap.NewASCollection(ap.CollectionParams{ ID: parseURI(idURI), + First: new(paging.Page), Query: url.Values{"limit": []string{"40"}}, Total: total, }) @@ -60,6 +61,37 @@ func TestASCollection(t *testing.T) { assert.Equal(t, expect, s) } +func TestASCollectionTotalOnly(t *testing.T) { + const ( + proto = "https" + host = "zorg.flabormagorg.xyz" + path = "/users/itsa_me_mario" + + idURI = proto + "://" + host + path + total = 10 + ) + + // Create JSON string of expected output. + expect := toJSON(map[string]any{ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Collection", + "id": idURI, + "totalItems": total, + }) + + // Create new collection using builder function. + c := ap.NewASCollection(ap.CollectionParams{ + ID: parseURI(idURI), + Total: total, + }) + + // Serialize collection. + s := toJSON(c) + + // Ensure outputs are equal. + assert.Equal(t, expect, s) +} + func TestASCollectionPage(t *testing.T) { const ( proto = "https" @@ -132,6 +164,7 @@ func TestASOrderedCollection(t *testing.T) { // Create new collection using builder function. c := ap.NewASOrderedCollection(ap.CollectionParams{ ID: parseURI(idURI), + First: new(paging.Page), Query: url.Values{"limit": []string{"40"}}, Total: total, }) @@ -143,6 +176,33 @@ func TestASOrderedCollection(t *testing.T) { assert.Equal(t, expect, s) } +func TestASOrderedCollectionTotalOnly(t *testing.T) { + const ( + idURI = "https://zorg.flabormagorg.xyz/users/itsa_me_mario" + total = 10 + ) + + // Create JSON string of expected output. + expect := toJSON(map[string]any{ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "OrderedCollection", + "id": idURI, + "totalItems": total, + }) + + // Create new collection using builder function. + c := ap.NewASOrderedCollection(ap.CollectionParams{ + ID: parseURI(idURI), + Total: total, + }) + + // Serialize collection. + s := toJSON(c) + + // Ensure outputs are equal. + assert.Equal(t, expect, s) +} + func TestASOrderedCollectionPage(t *testing.T) { const ( proto = "https" diff --git a/internal/ap/collections.go b/internal/ap/collections.go index da789e179..c55bbe70b 100644 --- a/internal/ap/collections.go +++ b/internal/ap/collections.go @@ -281,7 +281,7 @@ type CollectionParams struct { ID *url.URL // First page details. - First paging.Page + First *paging.Page Query url.Values // Total no. items. @@ -377,6 +377,11 @@ func buildCollection[C CollectionBuilder](collection C, params CollectionParams) totalItems.Set(params.Total) collection.SetActivityStreamsTotalItems(totalItems) + // No First page means we're done. + if params.First == nil { + return + } + // Append paging query params // to those already in ID prop. pageQueryParams := appendQuery( diff --git a/internal/api/client/accounts/accountupdate.go b/internal/api/client/accounts/accountupdate.go index 905d11479..cd8ee35f4 100644 --- a/internal/api/client/accounts/accountupdate.go +++ b/internal/api/client/accounts/accountupdate.go @@ -108,6 +108,14 @@ import ( // description: Default content type to use for authored statuses (text/plain or text/markdown). // type: string // - +// name: theme +// in: formData +// description: >- +// FileName of the theme to use when rendering this account's profile or statuses. +// The theme must exist on this server, as indicated by /api/v1/accounts/themes. +// Empty string unsets theme and returns to the default GoToSocial theme. +// type: string +// - // name: custom_css // in: formData // description: >- @@ -120,6 +128,11 @@ import ( // description: Enable RSS feed for this account's Public posts at `/[username]/feed.rss` // type: boolean // - +// name: hide_collections +// in: formData +// description: Hide the account's following/followers collections. +// type: boolean +// - // name: fields_attributes[0][name] // in: formData // description: Name of 1st profile field to be added to this account's profile. @@ -311,7 +324,8 @@ func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest, form.FieldsAttributes == nil && form.Theme == nil && form.CustomCSS == nil && - form.EnableRSS == nil) { + form.EnableRSS == nil && + form.HideCollections == nil) { return nil, errors.New("empty form submitted") } diff --git a/internal/api/client/accounts/followers.go b/internal/api/client/accounts/followers.go index d54fd6084..332788c3a 100644 --- a/internal/api/client/accounts/followers.go +++ b/internal/api/client/accounts/followers.go @@ -39,6 +39,8 @@ import ( // <https://example.org/api/v1/accounts/0657WMDEC3KQDTD6NZ4XJZBK4M/followers?limit=80&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/accounts/0657WMDEC3KQDTD6NZ4XJZBK4M/followers?limit=80&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev" // ```` // +// If account `hide_collections` is true, and requesting account != target account, no results will be returned. +// // --- // tags: // - accounts diff --git a/internal/api/client/accounts/following.go b/internal/api/client/accounts/following.go index 1503eddbf..bdd9ff3de 100644 --- a/internal/api/client/accounts/following.go +++ b/internal/api/client/accounts/following.go @@ -39,6 +39,8 @@ import ( // <https://example.org/api/v1/accounts/0657WMDEC3KQDTD6NZ4XJZBK4M/following?limit=80&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/accounts/0657WMDEC3KQDTD6NZ4XJZBK4M/following?limit=80&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev" // ```` // +// If account `hide_collections` is true, and requesting account != target account, no results will be returned. +// // --- // tags: // - accounts diff --git a/internal/api/client/admin/reportsget_test.go b/internal/api/client/admin/reportsget_test.go index 18f10e489..f2b6ff62a 100644 --- a/internal/api/client/admin/reportsget_test.go +++ b/internal/api/client/admin/reportsget_test.go @@ -236,6 +236,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() { "verified_at": null } ], + "hide_collections": true, "role": { "name": "user" } @@ -397,6 +398,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() { "verified_at": null } ], + "hide_collections": true, "role": { "name": "user" } @@ -618,6 +620,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() { "verified_at": null } ], + "hide_collections": true, "role": { "name": "user" } @@ -839,6 +842,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() { "verified_at": null } ], + "hide_collections": true, "role": { "name": "user" } diff --git a/internal/api/model/account.go b/internal/api/model/account.go index af2a394af..de850637e 100644 --- a/internal/api/model/account.go +++ b/internal/api/model/account.go @@ -94,12 +94,16 @@ type Account struct { // CustomCSS to include when rendering this account's profile or statuses. CustomCSS string `json:"custom_css,omitempty"` // Account has enabled RSS feed. + // Key/value omitted if false. EnableRSS bool `json:"enable_rss,omitempty"` + // Account has opted to hide their followers/following collections. + // Key/value omitted if false. + HideCollections bool `json:"hide_collections,omitempty"` // Role of the account on this instance. - // Omitted for remote accounts. + // Key/value omitted for remote accounts. Role *AccountRole `json:"role,omitempty"` // If set, indicates that this account is currently inactive, and has migrated to the given account. - // Omitted for accounts that haven't moved, and for suspended accounts. + // Key/value omitted for accounts that haven't moved, and for suspended accounts. Moved *Account `json:"moved,omitempty"` } @@ -172,6 +176,8 @@ type UpdateCredentialsRequest struct { CustomCSS *string `form:"custom_css" json:"custom_css"` // Enable RSS feed of public toots for this account at /@[username]/feed.rss EnableRSS *bool `form:"enable_rss" json:"enable_rss"` + // Hide this account's following/followers collections. + HideCollections *bool `form:"hide_collections" json:"hide_collections"` } // UpdateSource is to be used specifically in an UpdateCredentialsRequest. diff --git a/internal/processing/account/relationships.go b/internal/processing/account/relationships.go index b9e9086c9..53d2ee3c7 100644 --- a/internal/processing/account/relationships.go +++ b/internal/processing/account/relationships.go @@ -31,11 +31,25 @@ import ( // FollowersGet fetches a list of the target account's followers. func (p *Processor) FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, page *paging.Page) (*apimodel.PageableResponse, gtserror.WithCode) { // Fetch target account to check it exists, and visibility of requester->target. - _, errWithCode := p.c.GetVisibleTargetAccount(ctx, requestingAccount, targetAccountID) + targetAccount, errWithCode := p.c.GetVisibleTargetAccount(ctx, requestingAccount, targetAccountID) if errWithCode != nil { return nil, errWithCode } + if targetAccount.IsInstance() { + // Instance accounts can't follow/be followed. + return paging.EmptyResponse(), nil + } + + // If account isn't requesting its own followers list, + // but instead the list for a local account that has + // hide_followers set, just return an empty array. + if targetAccountID != requestingAccount.ID && + targetAccount.IsLocal() && + *targetAccount.Settings.HideCollections { + return paging.EmptyResponse(), nil + } + follows, err := p.state.DB.GetAccountFollowers(ctx, targetAccountID, page) if err != nil && !errors.Is(err, db.ErrNoEntries) { err = gtserror.Newf("db error getting followers: %w", err) @@ -76,11 +90,25 @@ func (p *Processor) FollowersGet(ctx context.Context, requestingAccount *gtsmode // FollowingGet fetches a list of the accounts that target account is following. func (p *Processor) FollowingGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, page *paging.Page) (*apimodel.PageableResponse, gtserror.WithCode) { // Fetch target account to check it exists, and visibility of requester->target. - _, errWithCode := p.c.GetVisibleTargetAccount(ctx, requestingAccount, targetAccountID) + targetAccount, errWithCode := p.c.GetVisibleTargetAccount(ctx, requestingAccount, targetAccountID) if errWithCode != nil { return nil, errWithCode } + if targetAccount.IsInstance() { + // Instance accounts can't follow/be followed. + return paging.EmptyResponse(), nil + } + + // If account isn't requesting its own following list, + // but instead the list for a local account that has + // hide_followers set, just return an empty array. + if targetAccountID != requestingAccount.ID && + targetAccount.IsLocal() && + *targetAccount.Settings.HideCollections { + return paging.EmptyResponse(), nil + } + // Fetch known accounts that follow given target account ID. follows, err := p.state.DB.GetAccountFollows(ctx, targetAccountID, page) if err != nil && !errors.Is(err, db.ErrNoEntries) { diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go index 076b6d7f4..670620e19 100644 --- a/internal/processing/account/update.go +++ b/internal/processing/account/update.go @@ -284,6 +284,10 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form account.Settings.EnableRSS = form.EnableRSS } + if form.HideCollections != nil { + account.Settings.HideCollections = form.HideCollections + } + if err := p.state.DB.UpdateAccount(ctx, account); err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account %s: %s", account.ID, err)) } diff --git a/internal/processing/fedi/collections.go b/internal/processing/fedi/collections.go index 282180862..0eacf45da 100644 --- a/internal/processing/fedi/collections.go +++ b/internal/processing/fedi/collections.go @@ -140,15 +140,25 @@ func (p *Processor) FollowersGet(ctx context.Context, requestedUser string, page params.ID = collectionID params.Total = total - if page == nil { + switch { + + case receiver.IsInstance() || + *receiver.Settings.HideCollections: + // Instance account (can't follow/be followed), + // or an account that hides followers/following. + // Respect this by just returning totalItems. + obj = ap.NewASOrderedCollection(params) + + case page == nil: // i.e. paging disabled, return collection // that links to first page (i.e. path below). + params.First = new(paging.Page) params.Query = make(url.Values, 1) params.Query.Set("limit", "40") // enables paging obj = ap.NewASOrderedCollection(params) - } else { - // i.e. paging enabled + default: + // i.e. paging enabled // Get the request page of full follower objects with attached accounts. followers, err := p.state.DB.GetAccountFollowers(ctx, receiver.ID, page) if err != nil { @@ -239,15 +249,24 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page params.ID = collectionID params.Total = total - if page == nil { + switch { + case receiver.IsInstance() || + *receiver.Settings.HideCollections: + // Instance account (can't follow/be followed), + // or an account that hides followers/following. + // Respect this by just returning totalItems. + obj = ap.NewASOrderedCollection(params) + + case page == nil: // i.e. paging disabled, return collection // that links to first page (i.e. path below). + params.First = new(paging.Page) params.Query = make(url.Values, 1) params.Query.Set("limit", "40") // enables paging obj = ap.NewASOrderedCollection(params) - } else { - // i.e. paging enabled + default: + // i.e. paging enabled // Get the request page of full follower objects with attached accounts. follows, err := p.state.DB.GetAccountFollows(ctx, receiver.ID, page) if err != nil { diff --git a/internal/processing/fedi/status.go b/internal/processing/fedi/status.go index 2849d08a4..29c6fe069 100644 --- a/internal/processing/fedi/status.go +++ b/internal/processing/fedi/status.go @@ -156,6 +156,7 @@ func (p *Processor) StatusRepliesGet( if page == nil { // i.e. paging disabled, return collection // that links to first page (i.e. path below). + params.First = new(paging.Page) params.Query = make(url.Values, 1) params.Query.Set("limit", "20") // enables paging obj = ap.NewASOrderedCollection(params) diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index bf44c7254..daa3568a7 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -170,14 +170,15 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A // Bits that vary between remote + local accounts: // - Account (acct) string. // - Role. - // - Settings things (enableRSS, theme, customCSS). + // - Settings things (enableRSS, theme, customCSS, hideCollections). var ( - acct string - role *apimodel.AccountRole - enableRSS bool - theme string - customCSS string + acct string + role *apimodel.AccountRole + enableRSS bool + theme string + customCSS string + hideCollections bool ) if a.IsRemote() { @@ -211,6 +212,7 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A enableRSS = *a.Settings.EnableRSS theme = a.Settings.Theme customCSS = a.Settings.CustomCSS + hideCollections = *a.Settings.HideCollections } acct = a.Username // omit domain @@ -253,32 +255,33 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A // can be populated directly below. accountFrontend := &apimodel.Account{ - ID: a.ID, - Username: a.Username, - Acct: acct, - DisplayName: a.DisplayName, - Locked: locked, - Discoverable: discoverable, - Bot: bot, - CreatedAt: util.FormatISO8601(a.CreatedAt), - Note: a.Note, - URL: a.URL, - Avatar: aviURL, - AvatarStatic: aviURLStatic, - Header: headerURL, - HeaderStatic: headerURLStatic, - FollowersCount: followersCount, - FollowingCount: followingCount, - StatusesCount: statusesCount, - LastStatusAt: lastStatusAt, - Emojis: apiEmojis, - Fields: fields, - Suspended: !a.SuspendedAt.IsZero(), - Theme: theme, - CustomCSS: customCSS, - EnableRSS: enableRSS, - Role: role, - Moved: moved, + ID: a.ID, + Username: a.Username, + Acct: acct, + DisplayName: a.DisplayName, + Locked: locked, + Discoverable: discoverable, + Bot: bot, + CreatedAt: util.FormatISO8601(a.CreatedAt), + Note: a.Note, + URL: a.URL, + Avatar: aviURL, + AvatarStatic: aviURLStatic, + Header: headerURL, + HeaderStatic: headerURLStatic, + FollowersCount: followersCount, + FollowingCount: followingCount, + StatusesCount: statusesCount, + LastStatusAt: lastStatusAt, + Emojis: apiEmojis, + Fields: fields, + Suspended: !a.SuspendedAt.IsZero(), + Theme: theme, + CustomCSS: customCSS, + EnableRSS: enableRSS, + HideCollections: hideCollections, + Role: role, + Moved: moved, } // Bodge default avatar + header in, diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 519888e21..329d66425 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -161,6 +161,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved() "verified_at": null } ], + "hide_collections": true, "role": { "name": "user" } @@ -1313,6 +1314,7 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend2() { "verified_at": null } ], + "hide_collections": true, "role": { "name": "user" } @@ -1428,6 +1430,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() { "verified_at": null } ], + "hide_collections": true, "role": { "name": "user" } @@ -1599,6 +1602,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() { "verified_at": null } ], + "hide_collections": true, "role": { "name": "user" } @@ -1864,6 +1868,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca "emojis": [], "fields": [], "suspended": true, + "hide_collections": true, "role": { "name": "user" } |