summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2025-01-31 19:27:18 +0100
committerLibravatar GitHub <noreply@github.com>2025-01-31 19:27:18 +0100
commita55bd6d2bd7b11aed653f4614836caed4103bec3 (patch)
tree40ddcac7b495307d3c08c482455f88e3598952b6
parent[chore] Add "object" uri to outgoing Accept + Reject messages (#3717) (diff)
downloadgotosocial-a55bd6d2bd7b11aed653f4614836caed4103bec3.tar.xz
[feature] Add `instance-stats-randomize` config option (#3718)
* [feature] Add `instance-stats-randomize` config option * don't use cache (overkill)
-rw-r--r--docs/configuration/instance.md11
-rw-r--r--example/config.yaml11
-rw-r--r--internal/api/client/instance/instanceget.go13
-rw-r--r--internal/api/model/instance.go13
-rw-r--r--internal/api/model/instancev1.go7
-rw-r--r--internal/api/model/instancev2.go7
-rw-r--r--internal/config/config.go1
-rw-r--r--internal/config/flags.go1
-rw-r--r--internal/config/helpers.gen.go27
-rw-r--r--internal/processing/fedi/wellknown.go30
-rw-r--r--internal/typeutils/converter.go58
-rw-r--r--internal/typeutils/internaltofrontend.go12
-rwxr-xr-xtest/envparsing.sh2
13 files changed, 183 insertions, 10 deletions
diff --git a/docs/configuration/instance.md b/docs/configuration/instance.md
index cc793b7fe..fdaf324cf 100644
--- a/docs/configuration/instance.md
+++ b/docs/configuration/instance.md
@@ -138,4 +138,15 @@ instance-subscriptions-process-from: "23:00"
# Examples: ["24h", "72h", "12h"]
# Default: "24h" (once per day).
instance-subscriptions-process-every: "24h"
+
+# Bool. Set this to true to randomize stats served at
+# the /api/v1|v2/instance and /nodeinfo/2.0 endpoints.
+#
+# This can be useful when you don't want bots to obtain
+# reliable information about the amount of users and
+# statuses on your instance.
+#
+# Options: [true, false]
+# Default: false
+instance-stats-randomize: false
```
diff --git a/example/config.yaml b/example/config.yaml
index 164eea7b2..10d7799c6 100644
--- a/example/config.yaml
+++ b/example/config.yaml
@@ -425,6 +425,17 @@ instance-subscriptions-process-from: "23:00"
# Default: "24h" (once per day).
instance-subscriptions-process-every: "24h"
+# Bool. Set this to true to randomize stats served at
+# the /api/v1|v2/instance and /nodeinfo/2.0 endpoints.
+#
+# This can be useful when you don't want bots to obtain
+# reliable information about the amount of users and
+# statuses on your instance.
+#
+# Options: [true, false]
+# Default: false
+instance-stats-randomize: false
+
###########################
##### ACCOUNTS CONFIG #####
###########################
diff --git a/internal/api/client/instance/instanceget.go b/internal/api/client/instance/instanceget.go
index 6690e7e98..d7a688b43 100644
--- a/internal/api/client/instance/instanceget.go
+++ b/internal/api/client/instance/instanceget.go
@@ -21,7 +21,9 @@ import (
"net/http"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/gin-gonic/gin"
)
@@ -58,6 +60,12 @@ func (m *Module) InstanceInformationGETHandlerV1(c *gin.Context) {
return
}
+ if config.GetInstanceStatsRandomize() {
+ // Replace actual stats with cached randomized ones.
+ instance.Stats["user_count"] = util.Ptr(int(instance.RandomStats.TotalUsers))
+ instance.Stats["status_count"] = util.Ptr(int(instance.RandomStats.Statuses))
+ }
+
apiutil.JSON(c, http.StatusOK, instance)
}
@@ -93,5 +101,10 @@ func (m *Module) InstanceInformationGETHandlerV2(c *gin.Context) {
return
}
+ if config.GetInstanceStatsRandomize() {
+ // Replace actual stats with cached randomized ones.
+ instance.Usage.Users.ActiveMonth = int(instance.RandomStats.MonthlyActiveUsers)
+ }
+
apiutil.JSON(c, http.StatusOK, instance)
}
diff --git a/internal/api/model/instance.go b/internal/api/model/instance.go
index d59424fa5..aaa01d837 100644
--- a/internal/api/model/instance.go
+++ b/internal/api/model/instance.go
@@ -17,7 +17,10 @@
package model
-import "mime/multipart"
+import (
+ "mime/multipart"
+ "time"
+)
// InstanceSettingsUpdateRequest models an instance update request.
//
@@ -148,3 +151,11 @@ type InstanceConfigurationEmojis struct {
// example: 51200
EmojiSizeLimit int `json:"emoji_size_limit"`
}
+
+// swagger:ignore
+type RandomStats struct {
+ Statuses int64
+ TotalUsers int64
+ MonthlyActiveUsers int64
+ Generated time.Time
+}
diff --git a/internal/api/model/instancev1.go b/internal/api/model/instancev1.go
index 6dedd04cc..57e32c80a 100644
--- a/internal/api/model/instancev1.go
+++ b/internal/api/model/instancev1.go
@@ -110,6 +110,13 @@ type InstanceV1 struct {
Terms string `json:"terms,omitempty"`
// Raw (unparsed) version of terms.
TermsRaw string `json:"terms_text,omitempty"`
+
+ // Random stats generated for the instance.
+ // Only used if `instance-stats-randomize` is true.
+ // Not serialized to the frontend.
+ //
+ // swagger:ignore
+ RandomStats `json:"-"`
}
// InstanceV1URLs models instance-relevant URLs for client application consumption.
diff --git a/internal/api/model/instancev2.go b/internal/api/model/instancev2.go
index b3d11dee2..96399ea06 100644
--- a/internal/api/model/instancev2.go
+++ b/internal/api/model/instancev2.go
@@ -74,6 +74,13 @@ type InstanceV2 struct {
Terms string `json:"terms,omitempty"`
// Raw (unparsed) version of terms.
TermsText string `json:"terms_text,omitempty"`
+
+ // Random stats generated for the instance.
+ // Only used if `instance-stats-randomize` is true.
+ // Not serialized to the frontend.
+ //
+ // swagger:ignore
+ RandomStats `json:"-"`
}
// Usage data for this instance.
diff --git a/internal/config/config.go b/internal/config/config.go
index 33b4553a8..807d686d5 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -90,6 +90,7 @@ type Configuration struct {
InstanceLanguages language.Languages `name:"instance-languages" usage:"BCP47 language tags for the instance. Used to indicate the preferred languages of instance residents (in order from most-preferred to least-preferred)."`
InstanceSubscriptionsProcessFrom string `name:"instance-subscriptions-process-from" usage:"Time of day from which to start running instance subscriptions processing jobs. Should be in the format 'hh:mm:ss', eg., '15:04:05'."`
InstanceSubscriptionsProcessEvery time.Duration `name:"instance-subscriptions-process-every" usage:"Period to elapse between instance subscriptions processing jobs, starting from instance-subscriptions-process-from."`
+ InstanceStatsRandomize bool `name:"instance-stats-randomize" usage:"Set to true to randomize the stats served at api/v1/instance and api/v2/instance endpoints. Home page stats remain unchanged."`
AccountsRegistrationOpen bool `name:"accounts-registration-open" usage:"Allow anyone to submit an account signup request. If false, server will be invite-only."`
AccountsReasonRequired bool `name:"accounts-reason-required" usage:"Do new account signups require a reason to be submitted on registration?"`
diff --git a/internal/config/flags.go b/internal/config/flags.go
index 6f0957c36..b0b530d0b 100644
--- a/internal/config/flags.go
+++ b/internal/config/flags.go
@@ -92,6 +92,7 @@ func (s *ConfigState) AddServerFlags(cmd *cobra.Command) {
cmd.Flags().StringSlice(InstanceLanguagesFlag(), cfg.InstanceLanguages.TagStrs(), fieldtag("InstanceLanguages", "usage"))
cmd.Flags().String(InstanceSubscriptionsProcessFromFlag(), cfg.InstanceSubscriptionsProcessFrom, fieldtag("InstanceSubscriptionsProcessFrom", "usage"))
cmd.Flags().Duration(InstanceSubscriptionsProcessEveryFlag(), cfg.InstanceSubscriptionsProcessEvery, fieldtag("InstanceSubscriptionsProcessEvery", "usage"))
+ cmd.Flags().Bool(InstanceStatsRandomizeFlag(), cfg.InstanceStatsRandomize, fieldtag("InstanceStatsRandomize", "usage"))
// Accounts
cmd.Flags().Bool(AccountsRegistrationOpenFlag(), cfg.AccountsRegistrationOpen, fieldtag("AccountsRegistrationOpen", "usage"))
diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go
index 0f8ec02ce..469c46a7a 100644
--- a/internal/config/helpers.gen.go
+++ b/internal/config/helpers.gen.go
@@ -1057,6 +1057,31 @@ func SetInstanceSubscriptionsProcessEvery(v time.Duration) {
global.SetInstanceSubscriptionsProcessEvery(v)
}
+// GetInstanceStatsRandomize safely fetches the Configuration value for state's 'InstanceStatsRandomize' field
+func (st *ConfigState) GetInstanceStatsRandomize() (v bool) {
+ st.mutex.RLock()
+ v = st.config.InstanceStatsRandomize
+ st.mutex.RUnlock()
+ return
+}
+
+// SetInstanceStatsRandomize safely sets the Configuration value for state's 'InstanceStatsRandomize' field
+func (st *ConfigState) SetInstanceStatsRandomize(v bool) {
+ st.mutex.Lock()
+ defer st.mutex.Unlock()
+ st.config.InstanceStatsRandomize = v
+ st.reloadToViper()
+}
+
+// InstanceStatsRandomizeFlag returns the flag name for the 'InstanceStatsRandomize' field
+func InstanceStatsRandomizeFlag() string { return "instance-stats-randomize" }
+
+// GetInstanceStatsRandomize safely fetches the value for global configuration 'InstanceStatsRandomize' field
+func GetInstanceStatsRandomize() bool { return global.GetInstanceStatsRandomize() }
+
+// SetInstanceStatsRandomize safely sets the value for global configuration 'InstanceStatsRandomize' field
+func SetInstanceStatsRandomize(v bool) { global.SetInstanceStatsRandomize(v) }
+
// GetAccountsRegistrationOpen safely fetches the Configuration value for state's 'AccountsRegistrationOpen' field
func (st *ConfigState) GetAccountsRegistrationOpen() (v bool) {
st.mutex.RLock()
@@ -2699,7 +2724,7 @@ func (st *ConfigState) SetAdvancedRateLimitExceptionsParsed(v []netip.Prefix) {
}
// AdvancedRateLimitExceptionsParsedFlag returns the flag name for the 'AdvancedRateLimitExceptionsParsed' field
-func AdvancedRateLimitExceptionsParsedFlag() string { return "" }
+func AdvancedRateLimitExceptionsParsedFlag() string { return "advanced-rate-limit-exceptions-parsed" }
// GetAdvancedRateLimitExceptionsParsed safely fetches the value for global configuration 'AdvancedRateLimitExceptionsParsed' field
func GetAdvancedRateLimitExceptionsParsed() []netip.Prefix {
diff --git a/internal/processing/fedi/wellknown.go b/internal/processing/fedi/wellknown.go
index 4784b4bf7..ac92370c8 100644
--- a/internal/processing/fedi/wellknown.go
+++ b/internal/processing/fedi/wellknown.go
@@ -65,16 +65,30 @@ func (p *Processor) NodeInfoRelGet(ctx context.Context) (*apimodel.WellKnownResp
// NodeInfoGet returns a node info struct in response to a node info request.
func (p *Processor) NodeInfoGet(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode) {
- host := config.GetHost()
+ var (
+ userCount int
+ postCount int
+ err error
+ )
- userCount, err := p.state.DB.CountInstanceUsers(ctx, host)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
+ if config.GetInstanceStatsRandomize() {
+ // Use randomized stats.
+ stats := p.converter.RandomStats()
+ userCount = int(stats.TotalUsers)
+ postCount = int(stats.Statuses)
+ } else {
+ // Count actual stats.
+ host := config.GetHost()
- postCount, err := p.state.DB.CountInstanceStatuses(ctx, host)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
+ userCount, err = p.state.DB.CountInstanceUsers(ctx, host)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ postCount, err = p.state.DB.CountInstanceStatuses(ctx, host)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
}
return &apimodel.Nodeinfo{
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
index 311839dc0..4fbe1dfd3 100644
--- a/internal/typeutils/converter.go
+++ b/internal/typeutils/converter.go
@@ -18,10 +18,17 @@
package typeutils
import (
+ crand "crypto/rand"
+ "math/big"
+ "math/rand"
"sync"
+ "sync/atomic"
+ "time"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/state"
)
@@ -31,6 +38,7 @@ type Converter struct {
randAvatars sync.Map
visFilter *visibility.Filter
intFilter *interaction.Filter
+ randStats atomic.Pointer[apimodel.RandomStats]
}
func NewConverter(state *state.State) *Converter {
@@ -41,3 +49,53 @@ func NewConverter(state *state.State) *Converter {
intFilter: interaction.NewFilter(state),
}
}
+
+// RandomStats returns or generates
+// and returns random instance stats.
+func (c *Converter) RandomStats() apimodel.RandomStats {
+ now := time.Now()
+ stats := c.randStats.Load()
+ if stats != nil && time.Since(stats.Generated) < time.Hour {
+ // Random stats are still
+ // fresh (less than 1hr old),
+ // so return them as-is.
+ return *stats
+ }
+
+ // Generate new random stats.
+ newStats := genRandStats()
+ newStats.Generated = now
+ c.randStats.Store(&newStats)
+ return newStats
+}
+
+func genRandStats() apimodel.RandomStats {
+ const (
+ statusesMax = 10000000
+ usersMax = 1000000
+ )
+
+ statusesB, err := crand.Int(crand.Reader, big.NewInt(statusesMax))
+ if err != nil {
+ // Only errs if something is buggered with the OS.
+ log.Panicf(nil, "error randomly generating statuses count: %v", err)
+ }
+
+ totalUsersB, err := crand.Int(crand.Reader, big.NewInt(usersMax))
+ if err != nil {
+ // Only errs if something is buggered with the OS.
+ log.Panicf(nil, "error randomly generating users count: %v", err)
+ }
+
+ // Monthly users should only ever
+ // be <= 100% of total users.
+ totalUsers := totalUsersB.Int64()
+ activeRatio := rand.Float64() //nolint
+ mau := int64(float64(totalUsers) * activeRatio)
+
+ return apimodel.RandomStats{
+ Statuses: statusesB.Int64(),
+ TotalUsers: totalUsers,
+ MonthlyActiveUsers: mau,
+ }
+}
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 71ff71f8b..487e8434e 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -1745,6 +1745,12 @@ func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins
stats["domain_count"] = util.Ptr(domainCount)
instance.Stats = stats
+ if config.GetInstanceStatsRandomize() {
+ // Whack some random stats on the instance
+ // to be injected by API handlers.
+ instance.RandomStats = c.RandomStats()
+ }
+
// thumbnail
iAccount, err := c.state.DB.GetInstanceAccount(ctx, "")
if err != nil {
@@ -1821,6 +1827,12 @@ func (c *Converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins
instance.Debug = util.Ptr(true)
}
+ if config.GetInstanceStatsRandomize() {
+ // Whack some random stats on the instance
+ // to be injected by API handlers.
+ instance.RandomStats = c.RandomStats()
+ }
+
// thumbnail
thumbnail := apimodel.InstanceV2Thumbnail{}
diff --git a/test/envparsing.sh b/test/envparsing.sh
index f9f3f25bc..565ecb1af 100755
--- a/test/envparsing.sh
+++ b/test/envparsing.sh
@@ -118,6 +118,7 @@ EXPECT=$(cat << "EOF"
"nl",
"en-GB"
],
+ "instance-stats-randomize": true,
"instance-subscriptions-process-every": 86400000000000,
"instance-subscriptions-process-from": "23:00",
"landing-page-user": "admin",
@@ -248,6 +249,7 @@ GTS_INSTANCE_FEDERATION_SPAM_FILTER=true \
GTS_INSTANCE_DELIVER_TO_SHARED_INBOXES=false \
GTS_INSTANCE_INJECT_MASTODON_VERSION=true \
GTS_INSTANCE_LANGUAGES="nl,en-gb" \
+GTS_INSTANCE_STATS_RANDOMIZE=true \
GTS_ACCOUNTS_ALLOW_CUSTOM_CSS=true \
GTS_ACCOUNTS_CUSTOM_CSS_LENGTH=5000 \
GTS_ACCOUNTS_REGISTRATION_OPEN=true \