summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2024-04-02 11:42:24 +0200
committerLibravatar GitHub <noreply@github.com>2024-04-02 10:42:24 +0100
commitf05874be3095d3fb3cefd1a92b3c35fe3ae3bf28 (patch)
tree52f1616259b51d0a8a94a786278b9c0aa5ab2298 /internal
parent[feature] Add `enable` command to mirror existing `disable` command; update d... (diff)
downloadgotosocial-f05874be3095d3fb3cefd1a92b3c35fe3ae3bf28.tar.xz
[feature] Option to hide followers/following (#2788)
Diffstat (limited to 'internal')
-rw-r--r--internal/ap/activitystreams_test.go60
-rw-r--r--internal/ap/collections.go7
-rw-r--r--internal/api/client/accounts/accountupdate.go16
-rw-r--r--internal/api/client/accounts/followers.go2
-rw-r--r--internal/api/client/accounts/following.go2
-rw-r--r--internal/api/client/admin/reportsget_test.go4
-rw-r--r--internal/api/model/account.go10
-rw-r--r--internal/processing/account/relationships.go32
-rw-r--r--internal/processing/account/update.go4
-rw-r--r--internal/processing/fedi/collections.go31
-rw-r--r--internal/processing/fedi/status.go1
-rw-r--r--internal/typeutils/internaltofrontend.go67
-rw-r--r--internal/typeutils/internaltofrontend_test.go5
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"
}