Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Add instance-stats-randomize config option #3718

Merged
merged 2 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/configuration/instance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
11 changes: 11 additions & 0 deletions example/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 #####
###########################
Expand Down
13 changes: 13 additions & 0 deletions internal/api/client/instance/instanceget.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
13 changes: 12 additions & 1 deletion internal/api/model/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@

package model

import "mime/multipart"
import (
"mime/multipart"
"time"
)

// InstanceSettingsUpdateRequest models an instance update request.
//
Expand Down Expand Up @@ -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
}
7 changes: 7 additions & 0 deletions internal/api/model/instancev1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions internal/api/model/instancev2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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?"`
Expand Down
1 change: 1 addition & 0 deletions internal/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
27 changes: 26 additions & 1 deletion internal/config/helpers.gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
30 changes: 22 additions & 8 deletions internal/processing/fedi/wellknown.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
58 changes: 58 additions & 0 deletions internal/typeutils/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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 {
Expand All @@ -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,
}
}
12 changes: 12 additions & 0 deletions internal/typeutils/internaltofrontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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{}

Expand Down
2 changes: 2 additions & 0 deletions test/envparsing.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 \
Expand Down