mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-27 19:01:01 +00: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
|
@ -225,7 +225,9 @@ definitions:
|
||||||
type: array
|
type: array
|
||||||
x-go-name: Emojis
|
x-go-name: Emojis
|
||||||
enable_rss:
|
enable_rss:
|
||||||
description: Account has enabled RSS feed.
|
description: |-
|
||||||
|
Account has enabled RSS feed.
|
||||||
|
Key/value omitted if false.
|
||||||
type: boolean
|
type: boolean
|
||||||
x-go-name: EnableRSS
|
x-go-name: EnableRSS
|
||||||
fields:
|
fields:
|
||||||
|
@ -256,6 +258,12 @@ definitions:
|
||||||
example: https://example.org/media/some_user/header/static/header.png
|
example: https://example.org/media/some_user/header/static/header.png
|
||||||
type: string
|
type: string
|
||||||
x-go-name: HeaderStatic
|
x-go-name: HeaderStatic
|
||||||
|
hide_collections:
|
||||||
|
description: |-
|
||||||
|
Account has opted to hide their followers/following collections.
|
||||||
|
Key/value omitted if false.
|
||||||
|
type: boolean
|
||||||
|
x-go-name: HideCollections
|
||||||
id:
|
id:
|
||||||
description: The account id.
|
description: The account id.
|
||||||
example: 01FBVD42CQ3ZEEVMW180SBX03B
|
example: 01FBVD42CQ3ZEEVMW180SBX03B
|
||||||
|
@ -2898,6 +2906,8 @@ paths:
|
||||||
```
|
```
|
||||||
<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"
|
<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.
|
||||||
operationId: accountFollowers
|
operationId: accountFollowers
|
||||||
parameters:
|
parameters:
|
||||||
- description: Account ID.
|
- description: Account ID.
|
||||||
|
@ -2962,6 +2972,8 @@ paths:
|
||||||
```
|
```
|
||||||
<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"
|
<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.
|
||||||
operationId: accountFollowing
|
operationId: accountFollowing
|
||||||
parameters:
|
parameters:
|
||||||
- description: Account ID.
|
- description: Account ID.
|
||||||
|
@ -3552,6 +3564,10 @@ paths:
|
||||||
in: formData
|
in: formData
|
||||||
name: source[status_content_type]
|
name: source[status_content_type]
|
||||||
type: string
|
type: string
|
||||||
|
- 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.
|
||||||
|
in: formData
|
||||||
|
name: theme
|
||||||
|
type: string
|
||||||
- description: Custom CSS to use when rendering this account's profile or statuses. String must be no more than 5,000 characters (~5kb).
|
- description: Custom CSS to use when rendering this account's profile or statuses. String must be no more than 5,000 characters (~5kb).
|
||||||
in: formData
|
in: formData
|
||||||
name: custom_css
|
name: custom_css
|
||||||
|
@ -3560,6 +3576,10 @@ paths:
|
||||||
in: formData
|
in: formData
|
||||||
name: enable_rss
|
name: enable_rss
|
||||||
type: boolean
|
type: boolean
|
||||||
|
- description: Hide the account's following/followers collections.
|
||||||
|
in: formData
|
||||||
|
name: hide_collections
|
||||||
|
type: boolean
|
||||||
- description: Name of 1st profile field to be added to this account's profile. (The index may be any string; add more indexes to send more fields.)
|
- description: Name of 1st profile field to be added to this account's profile. (The index may be any string; add more indexes to send more fields.)
|
||||||
in: formData
|
in: formData
|
||||||
name: fields_attributes[0][name]
|
name: fields_attributes[0][name]
|
||||||
|
|
|
@ -106,7 +106,9 @@ This ensures that remote servers cannot flood a GoToSocial instance with spuriou
|
||||||
|
|
||||||
For more details on request throttling and rate limiting behavior, please see the [throttling](../api/throttling.md) and [rate limiting](../api/ratelimiting.md) documents.
|
For more details on request throttling and rate limiting behavior, please see the [throttling](../api/throttling.md) and [rate limiting](../api/ratelimiting.md) documents.
|
||||||
|
|
||||||
## Inbox
|
## Actors and Actor Properties
|
||||||
|
|
||||||
|
### Inbox
|
||||||
|
|
||||||
GoToSocial implements Inboxes for Actors following the ActivityPub specification [here](https://www.w3.org/TR/activitypub/#inbox).
|
GoToSocial implements Inboxes for Actors following the ActivityPub specification [here](https://www.w3.org/TR/activitypub/#inbox).
|
||||||
|
|
||||||
|
@ -132,7 +134,7 @@ Invalidly-formed Inbox POST requests will receive a [400 - Bad Request](https://
|
||||||
|
|
||||||
Even if GoToSocial returns a `202` status code, it may not continue processing the Activity delivered, depending on the originator(s), target(s) and type of the Activity. ActivityPub is an extensive protocol, and GoToSocial does not cover every combination of Activity and Object.
|
Even if GoToSocial returns a `202` status code, it may not continue processing the Activity delivered, depending on the originator(s), target(s) and type of the Activity. ActivityPub is an extensive protocol, and GoToSocial does not cover every combination of Activity and Object.
|
||||||
|
|
||||||
## Outbox
|
### Outbox
|
||||||
|
|
||||||
GoToSocial implements Outboxes for Actors (ie., instance accounts) following the ActivityPub specification [here](https://www.w3.org/TR/activitypub/#outbox).
|
GoToSocial implements Outboxes for Actors (ie., instance accounts) following the ActivityPub specification [here](https://www.w3.org/TR/activitypub/#outbox).
|
||||||
|
|
||||||
|
@ -180,6 +182,58 @@ The `orderedItems` array will contain up to 30 entries. To get more entries beyo
|
||||||
|
|
||||||
Note that in the returned `orderedItems`, all activity types will be `Create`. On each activity, the `object` field will be the AP URI of an original public status created by the Actor who owns the Outbox (ie., a `Note` with `https://www.w3.org/ns/activitystreams#Public` in the `to` field, which is not a reply to another status). Callers can use the returned AP URIs to dereference the content of the notes.
|
Note that in the returned `orderedItems`, all activity types will be `Create`. On each activity, the `object` field will be the AP URI of an original public status created by the Actor who owns the Outbox (ie., a `Note` with `https://www.w3.org/ns/activitystreams#Public` in the `to` field, which is not a reply to another status). Callers can use the returned AP URIs to dereference the content of the notes.
|
||||||
|
|
||||||
|
### Followers / Following Collections
|
||||||
|
|
||||||
|
GoToSocial implements followers and following collections as `OrderedCollection`s. A properly-signed `GET` request to an Actor's Following collection, for example, will return something like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"first": "https://example.org/users/someone/following?limit=40",
|
||||||
|
"id": "https://example.org/users/someone/following",
|
||||||
|
"totalItems": 397,
|
||||||
|
"type": "OrderedCollection"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From there, you can use the `first` page to start getting items. For example, a `GET` request to `https://example.org/users/someone/following?limit=40` will produce something like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id": "https://example.org/users/someone/following?limit=40",
|
||||||
|
"next": "https://example.org/users/someone/following?limit=40&max_id=01V1AY4ZJT4JK1NT271SH2WMGH",
|
||||||
|
"orderedItems": [
|
||||||
|
"https://example.org/users/someone_else",
|
||||||
|
"https://somewhere.else.example.org/users/another_account",
|
||||||
|
[... 38 more entries here ...]
|
||||||
|
],
|
||||||
|
"partOf": "https://example.org/users/someone/following",
|
||||||
|
"prev": "https://example.org/users/someone/following?limit=40&since_id=021HKBY346X7BPFYANPPJN493P",
|
||||||
|
"totalItems": 397,
|
||||||
|
"type": "OrderedCollectionPage"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then use the `next` and `prev` endpoints to page down and up through the OrderedCollection.
|
||||||
|
|
||||||
|
!!! Info "Hidden Followers / Following Collections"
|
||||||
|
|
||||||
|
GoToSocial allows users to hide their followers/following collections if they wish.
|
||||||
|
|
||||||
|
If a user has chosen to hide their collections, then only a stub collection with `totalItems` will be returned, and you will not be able to page through the Actor's followers/following collections.
|
||||||
|
|
||||||
|
A `GET` to the following collection of an Actor with hidden collections will look like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id": "https://example.org/users/someone/following",
|
||||||
|
"type": "OrderedCollection",
|
||||||
|
"totalItems": 397
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Conversation Threads
|
## Conversation Threads
|
||||||
|
|
||||||
Due to the nature of decentralization and federation, it is practically impossible for any one server on the fediverse to be aware of every post in a given conversation thread.
|
Due to the nature of decentralization and federation, it is practically impossible for any one server on the fediverse to be aware of every post in a given conversation thread.
|
||||||
|
|
|
@ -88,15 +88,6 @@ This option is often referred to on the fediverse as "locking" your account.
|
||||||
|
|
||||||
After ticking or unticking the checkbox, be sure to click on the `Save profile info` button at the bottom to save your new settings.
|
After ticking or unticking the checkbox, be sure to click on the `Save profile info` button at the bottom to save your new settings.
|
||||||
|
|
||||||
#### Enable RSS Feed of Public Posts
|
|
||||||
|
|
||||||
RSS feeds for users are disabled by default, but can be opted into with this checkbox. For more information see [RSS](./rss.md).
|
|
||||||
|
|
||||||
This feed only includes posts set as 'Public' (see [Privacy Settings](./posts.md#privacy-settings)).
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
Exposing your RSS feed allows *anyone* to subscribe to updates on your Public posts anonymously, bypassing follows and follow requests.
|
|
||||||
|
|
||||||
#### Mark Account as Discoverable by Search Engines and Directories
|
#### Mark Account as Discoverable by Search Engines and Directories
|
||||||
|
|
||||||
This setting updates the 'discoverable' flag on your account.
|
This setting updates the 'discoverable' flag on your account.
|
||||||
|
@ -114,6 +105,21 @@ Turning on the discoverable flag may take a week or more to propagate; your acco
|
||||||
!!! info
|
!!! info
|
||||||
The discoverable setting is about **discoverability of your account**, not searchability of your posts. It has nothing to do with indexing of your posts for search by Mastodon instances, or other federated instances that use full text search!
|
The discoverable setting is about **discoverability of your account**, not searchability of your posts. It has nothing to do with indexing of your posts for search by Mastodon instances, or other federated instances that use full text search!
|
||||||
|
|
||||||
|
#### Enable RSS Feed of Public Posts
|
||||||
|
|
||||||
|
RSS feeds for users are disabled by default, but can be opted into with this checkbox. For more information see [RSS](./rss.md).
|
||||||
|
|
||||||
|
This feed only includes posts set as 'Public' (see [Privacy Settings](./posts.md#privacy-settings)).
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
Exposing your RSS feed allows *anyone* to subscribe to updates on your Public posts anonymously, bypassing follows and follow requests.
|
||||||
|
|
||||||
|
#### Hide Who You Follow / Are Followed By
|
||||||
|
|
||||||
|
By default, GoToSocial shows your following/followers counts on your public web profile, and allows others to see who you follow and are followed by. This can be useful for account discovery purposes. However, for privacy + safety reasons you may wish to hide these counts, and to hide your following/followers lists from other accounts. You can do this by checking this box.
|
||||||
|
|
||||||
|
With the box checked, your following/followers counts will be hidden from your public web profile, and others will not be able to page through your following/followers lists.
|
||||||
|
|
||||||
### Advanced
|
### Advanced
|
||||||
|
|
||||||
#### Custom CSS
|
#### Custom CSS
|
||||||
|
|
|
@ -49,6 +49,7 @@ func TestASCollection(t *testing.T) {
|
||||||
// Create new collection using builder function.
|
// Create new collection using builder function.
|
||||||
c := ap.NewASCollection(ap.CollectionParams{
|
c := ap.NewASCollection(ap.CollectionParams{
|
||||||
ID: parseURI(idURI),
|
ID: parseURI(idURI),
|
||||||
|
First: new(paging.Page),
|
||||||
Query: url.Values{"limit": []string{"40"}},
|
Query: url.Values{"limit": []string{"40"}},
|
||||||
Total: total,
|
Total: total,
|
||||||
})
|
})
|
||||||
|
@ -60,6 +61,37 @@ func TestASCollection(t *testing.T) {
|
||||||
assert.Equal(t, expect, s)
|
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) {
|
func TestASCollectionPage(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
proto = "https"
|
proto = "https"
|
||||||
|
@ -132,6 +164,7 @@ func TestASOrderedCollection(t *testing.T) {
|
||||||
// Create new collection using builder function.
|
// Create new collection using builder function.
|
||||||
c := ap.NewASOrderedCollection(ap.CollectionParams{
|
c := ap.NewASOrderedCollection(ap.CollectionParams{
|
||||||
ID: parseURI(idURI),
|
ID: parseURI(idURI),
|
||||||
|
First: new(paging.Page),
|
||||||
Query: url.Values{"limit": []string{"40"}},
|
Query: url.Values{"limit": []string{"40"}},
|
||||||
Total: total,
|
Total: total,
|
||||||
})
|
})
|
||||||
|
@ -143,6 +176,33 @@ func TestASOrderedCollection(t *testing.T) {
|
||||||
assert.Equal(t, expect, s)
|
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) {
|
func TestASOrderedCollectionPage(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
proto = "https"
|
proto = "https"
|
||||||
|
|
|
@ -281,7 +281,7 @@ type CollectionParams struct {
|
||||||
ID *url.URL
|
ID *url.URL
|
||||||
|
|
||||||
// First page details.
|
// First page details.
|
||||||
First paging.Page
|
First *paging.Page
|
||||||
Query url.Values
|
Query url.Values
|
||||||
|
|
||||||
// Total no. items.
|
// Total no. items.
|
||||||
|
@ -377,6 +377,11 @@ func buildCollection[C CollectionBuilder](collection C, params CollectionParams)
|
||||||
totalItems.Set(params.Total)
|
totalItems.Set(params.Total)
|
||||||
collection.SetActivityStreamsTotalItems(totalItems)
|
collection.SetActivityStreamsTotalItems(totalItems)
|
||||||
|
|
||||||
|
// No First page means we're done.
|
||||||
|
if params.First == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Append paging query params
|
// Append paging query params
|
||||||
// to those already in ID prop.
|
// to those already in ID prop.
|
||||||
pageQueryParams := appendQuery(
|
pageQueryParams := appendQuery(
|
||||||
|
|
|
@ -108,6 +108,14 @@ import (
|
||||||
// description: Default content type to use for authored statuses (text/plain or text/markdown).
|
// description: Default content type to use for authored statuses (text/plain or text/markdown).
|
||||||
// type: string
|
// 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
|
// name: custom_css
|
||||||
// in: formData
|
// in: formData
|
||||||
// description: >-
|
// description: >-
|
||||||
|
@ -120,6 +128,11 @@ import (
|
||||||
// description: Enable RSS feed for this account's Public posts at `/[username]/feed.rss`
|
// description: Enable RSS feed for this account's Public posts at `/[username]/feed.rss`
|
||||||
// type: boolean
|
// type: boolean
|
||||||
// -
|
// -
|
||||||
|
// name: hide_collections
|
||||||
|
// in: formData
|
||||||
|
// description: Hide the account's following/followers collections.
|
||||||
|
// type: boolean
|
||||||
|
// -
|
||||||
// name: fields_attributes[0][name]
|
// name: fields_attributes[0][name]
|
||||||
// in: formData
|
// in: formData
|
||||||
// description: Name of 1st profile field to be added to this account's profile.
|
// 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.FieldsAttributes == nil &&
|
||||||
form.Theme == nil &&
|
form.Theme == nil &&
|
||||||
form.CustomCSS == nil &&
|
form.CustomCSS == nil &&
|
||||||
form.EnableRSS == nil) {
|
form.EnableRSS == nil &&
|
||||||
|
form.HideCollections == nil) {
|
||||||
return nil, errors.New("empty form submitted")
|
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"
|
// <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:
|
// tags:
|
||||||
// - accounts
|
// - 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"
|
// <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:
|
// tags:
|
||||||
// - accounts
|
// - accounts
|
||||||
|
|
|
@ -236,6 +236,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
||||||
"verified_at": null
|
"verified_at": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"hide_collections": true,
|
||||||
"role": {
|
"role": {
|
||||||
"name": "user"
|
"name": "user"
|
||||||
}
|
}
|
||||||
|
@ -397,6 +398,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
||||||
"verified_at": null
|
"verified_at": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"hide_collections": true,
|
||||||
"role": {
|
"role": {
|
||||||
"name": "user"
|
"name": "user"
|
||||||
}
|
}
|
||||||
|
@ -618,6 +620,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
|
||||||
"verified_at": null
|
"verified_at": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"hide_collections": true,
|
||||||
"role": {
|
"role": {
|
||||||
"name": "user"
|
"name": "user"
|
||||||
}
|
}
|
||||||
|
@ -839,6 +842,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
|
||||||
"verified_at": null
|
"verified_at": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"hide_collections": true,
|
||||||
"role": {
|
"role": {
|
||||||
"name": "user"
|
"name": "user"
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,12 +94,16 @@ type Account struct {
|
||||||
// CustomCSS to include when rendering this account's profile or statuses.
|
// CustomCSS to include when rendering this account's profile or statuses.
|
||||||
CustomCSS string `json:"custom_css,omitempty"`
|
CustomCSS string `json:"custom_css,omitempty"`
|
||||||
// Account has enabled RSS feed.
|
// Account has enabled RSS feed.
|
||||||
|
// Key/value omitted if false.
|
||||||
EnableRSS bool `json:"enable_rss,omitempty"`
|
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.
|
// Role of the account on this instance.
|
||||||
// Omitted for remote accounts.
|
// Key/value omitted for remote accounts.
|
||||||
Role *AccountRole `json:"role,omitempty"`
|
Role *AccountRole `json:"role,omitempty"`
|
||||||
// If set, indicates that this account is currently inactive, and has migrated to the given account.
|
// 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"`
|
Moved *Account `json:"moved,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,6 +176,8 @@ type UpdateCredentialsRequest struct {
|
||||||
CustomCSS *string `form:"custom_css" json:"custom_css"`
|
CustomCSS *string `form:"custom_css" json:"custom_css"`
|
||||||
// Enable RSS feed of public toots for this account at /@[username]/feed.rss
|
// Enable RSS feed of public toots for this account at /@[username]/feed.rss
|
||||||
EnableRSS *bool `form:"enable_rss" json:"enable_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.
|
// UpdateSource is to be used specifically in an UpdateCredentialsRequest.
|
||||||
|
|
|
@ -31,11 +31,25 @@ import (
|
||||||
// FollowersGet fetches a list of the target account's followers.
|
// 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) {
|
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.
|
// 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 {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
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)
|
follows, err := p.state.DB.GetAccountFollowers(ctx, targetAccountID, page)
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
err = gtserror.Newf("db error getting followers: %w", err)
|
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.
|
// 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) {
|
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.
|
// 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 {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
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.
|
// Fetch known accounts that follow given target account ID.
|
||||||
follows, err := p.state.DB.GetAccountFollows(ctx, targetAccountID, page)
|
follows, err := p.state.DB.GetAccountFollows(ctx, targetAccountID, page)
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
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
|
account.Settings.EnableRSS = form.EnableRSS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if form.HideCollections != nil {
|
||||||
|
account.Settings.HideCollections = form.HideCollections
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.state.DB.UpdateAccount(ctx, account); err != nil {
|
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))
|
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.ID = collectionID
|
||||||
params.Total = total
|
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
|
// i.e. paging disabled, return collection
|
||||||
// that links to first page (i.e. path below).
|
// that links to first page (i.e. path below).
|
||||||
|
params.First = new(paging.Page)
|
||||||
params.Query = make(url.Values, 1)
|
params.Query = make(url.Values, 1)
|
||||||
params.Query.Set("limit", "40") // enables paging
|
params.Query.Set("limit", "40") // enables paging
|
||||||
obj = ap.NewASOrderedCollection(params)
|
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.
|
// Get the request page of full follower objects with attached accounts.
|
||||||
followers, err := p.state.DB.GetAccountFollowers(ctx, receiver.ID, page)
|
followers, err := p.state.DB.GetAccountFollowers(ctx, receiver.ID, page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -239,15 +249,24 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page
|
||||||
params.ID = collectionID
|
params.ID = collectionID
|
||||||
params.Total = total
|
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
|
// i.e. paging disabled, return collection
|
||||||
// that links to first page (i.e. path below).
|
// that links to first page (i.e. path below).
|
||||||
|
params.First = new(paging.Page)
|
||||||
params.Query = make(url.Values, 1)
|
params.Query = make(url.Values, 1)
|
||||||
params.Query.Set("limit", "40") // enables paging
|
params.Query.Set("limit", "40") // enables paging
|
||||||
obj = ap.NewASOrderedCollection(params)
|
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.
|
// Get the request page of full follower objects with attached accounts.
|
||||||
follows, err := p.state.DB.GetAccountFollows(ctx, receiver.ID, page)
|
follows, err := p.state.DB.GetAccountFollows(ctx, receiver.ID, page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -156,6 +156,7 @@ func (p *Processor) StatusRepliesGet(
|
||||||
if page == nil {
|
if page == nil {
|
||||||
// i.e. paging disabled, return collection
|
// i.e. paging disabled, return collection
|
||||||
// that links to first page (i.e. path below).
|
// that links to first page (i.e. path below).
|
||||||
|
params.First = new(paging.Page)
|
||||||
params.Query = make(url.Values, 1)
|
params.Query = make(url.Values, 1)
|
||||||
params.Query.Set("limit", "20") // enables paging
|
params.Query.Set("limit", "20") // enables paging
|
||||||
obj = ap.NewASOrderedCollection(params)
|
obj = ap.NewASOrderedCollection(params)
|
||||||
|
|
|
@ -170,7 +170,7 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
||||||
// Bits that vary between remote + local accounts:
|
// Bits that vary between remote + local accounts:
|
||||||
// - Account (acct) string.
|
// - Account (acct) string.
|
||||||
// - Role.
|
// - Role.
|
||||||
// - Settings things (enableRSS, theme, customCSS).
|
// - Settings things (enableRSS, theme, customCSS, hideCollections).
|
||||||
|
|
||||||
var (
|
var (
|
||||||
acct string
|
acct string
|
||||||
|
@ -178,6 +178,7 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
||||||
enableRSS bool
|
enableRSS bool
|
||||||
theme string
|
theme string
|
||||||
customCSS string
|
customCSS string
|
||||||
|
hideCollections bool
|
||||||
)
|
)
|
||||||
|
|
||||||
if a.IsRemote() {
|
if a.IsRemote() {
|
||||||
|
@ -211,6 +212,7 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
||||||
enableRSS = *a.Settings.EnableRSS
|
enableRSS = *a.Settings.EnableRSS
|
||||||
theme = a.Settings.Theme
|
theme = a.Settings.Theme
|
||||||
customCSS = a.Settings.CustomCSS
|
customCSS = a.Settings.CustomCSS
|
||||||
|
hideCollections = *a.Settings.HideCollections
|
||||||
}
|
}
|
||||||
|
|
||||||
acct = a.Username // omit domain
|
acct = a.Username // omit domain
|
||||||
|
@ -277,6 +279,7 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
||||||
Theme: theme,
|
Theme: theme,
|
||||||
CustomCSS: customCSS,
|
CustomCSS: customCSS,
|
||||||
EnableRSS: enableRSS,
|
EnableRSS: enableRSS,
|
||||||
|
HideCollections: hideCollections,
|
||||||
Role: role,
|
Role: role,
|
||||||
Moved: moved,
|
Moved: moved,
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,6 +161,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved()
|
||||||
"verified_at": null
|
"verified_at": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"hide_collections": true,
|
||||||
"role": {
|
"role": {
|
||||||
"name": "user"
|
"name": "user"
|
||||||
}
|
}
|
||||||
|
@ -1313,6 +1314,7 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend2() {
|
||||||
"verified_at": null
|
"verified_at": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"hide_collections": true,
|
||||||
"role": {
|
"role": {
|
||||||
"name": "user"
|
"name": "user"
|
||||||
}
|
}
|
||||||
|
@ -1428,6 +1430,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
|
||||||
"verified_at": null
|
"verified_at": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"hide_collections": true,
|
||||||
"role": {
|
"role": {
|
||||||
"name": "user"
|
"name": "user"
|
||||||
}
|
}
|
||||||
|
@ -1599,6 +1602,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
|
||||||
"verified_at": null
|
"verified_at": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"hide_collections": true,
|
||||||
"role": {
|
"role": {
|
||||||
"name": "user"
|
"name": "user"
|
||||||
}
|
}
|
||||||
|
@ -1864,6 +1868,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
|
||||||
"emojis": [],
|
"emojis": [],
|
||||||
"fields": [],
|
"fields": [],
|
||||||
"suspended": true,
|
"suspended": true,
|
||||||
|
"hide_collections": true,
|
||||||
"role": {
|
"role": {
|
||||||
"name": "user"
|
"name": "user"
|
||||||
}
|
}
|
||||||
|
|
|
@ -702,7 +702,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
||||||
Sensitive: util.Ptr(true),
|
Sensitive: util.Ptr(true),
|
||||||
Language: "fr",
|
Language: "fr",
|
||||||
EnableRSS: util.Ptr(false),
|
EnableRSS: util.Ptr(false),
|
||||||
HideCollections: util.Ptr(false),
|
HideCollections: util.Ptr(true),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,7 @@ function UserProfileForm({ data: profile }) {
|
||||||
- file avatar
|
- file avatar
|
||||||
- file header
|
- file header
|
||||||
- bool enable_rss
|
- bool enable_rss
|
||||||
|
- bool hide_collections
|
||||||
- string custom_css (if enabled)
|
- string custom_css (if enabled)
|
||||||
- string theme
|
- string theme
|
||||||
*/
|
*/
|
||||||
|
@ -98,6 +99,7 @@ function UserProfileForm({ data: profile }) {
|
||||||
locked: useBoolInput("locked", { source: profile }),
|
locked: useBoolInput("locked", { source: profile }),
|
||||||
discoverable: useBoolInput("discoverable", { source: profile}),
|
discoverable: useBoolInput("discoverable", { source: profile}),
|
||||||
enableRSS: useBoolInput("enable_rss", { source: profile }),
|
enableRSS: useBoolInput("enable_rss", { source: profile }),
|
||||||
|
hideCollections: useBoolInput("hide_collections", { source: profile }),
|
||||||
fields: useFieldArrayInput("fields_attributes", {
|
fields: useFieldArrayInput("fields_attributes", {
|
||||||
defaultValue: profile?.source?.fields,
|
defaultValue: profile?.source?.fields,
|
||||||
length: instanceConfig.maxPinnedFields
|
length: instanceConfig.maxPinnedFields
|
||||||
|
@ -208,6 +210,10 @@ function UserProfileForm({ data: profile }) {
|
||||||
field={form.enableRSS}
|
field={form.enableRSS}
|
||||||
label="Enable RSS feed of Public posts"
|
label="Enable RSS feed of Public posts"
|
||||||
/>
|
/>
|
||||||
|
<Checkbox
|
||||||
|
field={form.hideCollections}
|
||||||
|
label="Hide who you follow / are followed by"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="form-section-docs">
|
<div className="form-section-docs">
|
||||||
<h3>Advanced</h3>
|
<h3>Advanced</h3>
|
||||||
|
|
|
@ -98,9 +98,9 @@
|
||||||
<dt>Posts</dt>
|
<dt>Posts</dt>
|
||||||
<dd>{{- .account.StatusesCount -}}</dd>
|
<dd>{{- .account.StatusesCount -}}</dd>
|
||||||
<dt>Followed by</dt>
|
<dt>Followed by</dt>
|
||||||
<dd>{{- .account.FollowersCount -}}</dd>
|
<dd>{{- if .account.HideCollections -}}<i>hidden</i>{{- else -}}{{- .account.FollowersCount -}}{{- end -}}</dd>
|
||||||
<dt>Following</dt>
|
<dt>Following</dt>
|
||||||
<dd>{{- .account.FollowingCount -}}</dd>
|
<dd>{{- if .account.HideCollections -}}<i>hidden</i>{{- else -}}{{- .account.FollowingCount -}}{{- end -}}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
<div class="statuses-wrapper" role="region" aria-label="Posts by {{ .account.Username -}}">
|
<div class="statuses-wrapper" role="region" aria-label="Posts by {{ .account.Username -}}">
|
||||||
|
|
Loading…
Reference in a new issue