mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-12-09 20:58:06 -06:00
[feature] Option to hide followers/following (#2788)
This commit is contained in:
parent
29972e2c93
commit
f05874be30
19 changed files with 322 additions and 83 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue