mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-02-08 17:16:31 +01:00
[feature] Add instance-stats-randomize
config option (#3718)
* [feature] Add `instance-stats-randomize` config option * don't use cache (overkill)
This commit is contained in:
parent
c47b9bd1d1
commit
a55bd6d2bd
13 changed files with 183 additions and 10 deletions
|
@ -138,4 +138,15 @@ instance-subscriptions-process-from: "23:00"
|
||||||
# Examples: ["24h", "72h", "12h"]
|
# Examples: ["24h", "72h", "12h"]
|
||||||
# Default: "24h" (once per day).
|
# Default: "24h" (once per day).
|
||||||
instance-subscriptions-process-every: "24h"
|
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
|
||||||
```
|
```
|
||||||
|
|
|
@ -425,6 +425,17 @@ instance-subscriptions-process-from: "23:00"
|
||||||
# Default: "24h" (once per day).
|
# Default: "24h" (once per day).
|
||||||
instance-subscriptions-process-every: "24h"
|
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 #####
|
##### ACCOUNTS CONFIG #####
|
||||||
###########################
|
###########################
|
||||||
|
|
|
@ -21,7 +21,9 @@
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
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/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
@ -58,6 +60,12 @@ func (m *Module) InstanceInformationGETHandlerV1(c *gin.Context) {
|
||||||
return
|
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)
|
apiutil.JSON(c, http.StatusOK, instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,5 +101,10 @@ func (m *Module) InstanceInformationGETHandlerV2(c *gin.Context) {
|
||||||
return
|
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)
|
apiutil.JSON(c, http.StatusOK, instance)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,10 @@
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import "mime/multipart"
|
import (
|
||||||
|
"mime/multipart"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// InstanceSettingsUpdateRequest models an instance update request.
|
// InstanceSettingsUpdateRequest models an instance update request.
|
||||||
//
|
//
|
||||||
|
@ -148,3 +151,11 @@ type InstanceConfigurationEmojis struct {
|
||||||
// example: 51200
|
// example: 51200
|
||||||
EmojiSizeLimit int `json:"emoji_size_limit"`
|
EmojiSizeLimit int `json:"emoji_size_limit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swagger:ignore
|
||||||
|
type RandomStats struct {
|
||||||
|
Statuses int64
|
||||||
|
TotalUsers int64
|
||||||
|
MonthlyActiveUsers int64
|
||||||
|
Generated time.Time
|
||||||
|
}
|
||||||
|
|
|
@ -110,6 +110,13 @@ type InstanceV1 struct {
|
||||||
Terms string `json:"terms,omitempty"`
|
Terms string `json:"terms,omitempty"`
|
||||||
// Raw (unparsed) version of terms.
|
// Raw (unparsed) version of terms.
|
||||||
TermsRaw string `json:"terms_text,omitempty"`
|
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.
|
// InstanceV1URLs models instance-relevant URLs for client application consumption.
|
||||||
|
|
|
@ -74,6 +74,13 @@ type InstanceV2 struct {
|
||||||
Terms string `json:"terms,omitempty"`
|
Terms string `json:"terms,omitempty"`
|
||||||
// Raw (unparsed) version of terms.
|
// Raw (unparsed) version of terms.
|
||||||
TermsText string `json:"terms_text,omitempty"`
|
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.
|
// Usage data for this instance.
|
||||||
|
|
|
@ -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)."`
|
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'."`
|
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."`
|
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."`
|
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?"`
|
AccountsReasonRequired bool `name:"accounts-reason-required" usage:"Do new account signups require a reason to be submitted on registration?"`
|
||||||
|
|
|
@ -92,6 +92,7 @@ func (s *ConfigState) AddServerFlags(cmd *cobra.Command) {
|
||||||
cmd.Flags().StringSlice(InstanceLanguagesFlag(), cfg.InstanceLanguages.TagStrs(), fieldtag("InstanceLanguages", "usage"))
|
cmd.Flags().StringSlice(InstanceLanguagesFlag(), cfg.InstanceLanguages.TagStrs(), fieldtag("InstanceLanguages", "usage"))
|
||||||
cmd.Flags().String(InstanceSubscriptionsProcessFromFlag(), cfg.InstanceSubscriptionsProcessFrom, fieldtag("InstanceSubscriptionsProcessFrom", "usage"))
|
cmd.Flags().String(InstanceSubscriptionsProcessFromFlag(), cfg.InstanceSubscriptionsProcessFrom, fieldtag("InstanceSubscriptionsProcessFrom", "usage"))
|
||||||
cmd.Flags().Duration(InstanceSubscriptionsProcessEveryFlag(), cfg.InstanceSubscriptionsProcessEvery, fieldtag("InstanceSubscriptionsProcessEvery", "usage"))
|
cmd.Flags().Duration(InstanceSubscriptionsProcessEveryFlag(), cfg.InstanceSubscriptionsProcessEvery, fieldtag("InstanceSubscriptionsProcessEvery", "usage"))
|
||||||
|
cmd.Flags().Bool(InstanceStatsRandomizeFlag(), cfg.InstanceStatsRandomize, fieldtag("InstanceStatsRandomize", "usage"))
|
||||||
|
|
||||||
// Accounts
|
// Accounts
|
||||||
cmd.Flags().Bool(AccountsRegistrationOpenFlag(), cfg.AccountsRegistrationOpen, fieldtag("AccountsRegistrationOpen", "usage"))
|
cmd.Flags().Bool(AccountsRegistrationOpenFlag(), cfg.AccountsRegistrationOpen, fieldtag("AccountsRegistrationOpen", "usage"))
|
||||||
|
|
|
@ -1057,6 +1057,31 @@ func SetInstanceSubscriptionsProcessEvery(v time.Duration) {
|
||||||
global.SetInstanceSubscriptionsProcessEvery(v)
|
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
|
// GetAccountsRegistrationOpen safely fetches the Configuration value for state's 'AccountsRegistrationOpen' field
|
||||||
func (st *ConfigState) GetAccountsRegistrationOpen() (v bool) {
|
func (st *ConfigState) GetAccountsRegistrationOpen() (v bool) {
|
||||||
st.mutex.RLock()
|
st.mutex.RLock()
|
||||||
|
@ -2699,7 +2724,7 @@ func (st *ConfigState) SetAdvancedRateLimitExceptionsParsed(v []netip.Prefix) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdvancedRateLimitExceptionsParsedFlag returns the flag name for the 'AdvancedRateLimitExceptionsParsed' field
|
// 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
|
// GetAdvancedRateLimitExceptionsParsed safely fetches the value for global configuration 'AdvancedRateLimitExceptionsParsed' field
|
||||||
func GetAdvancedRateLimitExceptionsParsed() []netip.Prefix {
|
func GetAdvancedRateLimitExceptionsParsed() []netip.Prefix {
|
||||||
|
|
|
@ -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.
|
// NodeInfoGet returns a node info struct in response to a node info request.
|
||||||
func (p *Processor) NodeInfoGet(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode) {
|
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 config.GetInstanceStatsRandomize() {
|
||||||
if err != nil {
|
// Use randomized stats.
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
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)
|
userCount, err = p.state.DB.CountInstanceUsers(ctx, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
postCount, err = p.state.DB.CountInstanceStatuses(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &apimodel.Nodeinfo{
|
return &apimodel.Nodeinfo{
|
||||||
|
|
|
@ -18,10 +18,17 @@
|
||||||
package typeutils
|
package typeutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
crand "crypto/rand"
|
||||||
|
"math/big"
|
||||||
|
"math/rand"
|
||||||
"sync"
|
"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/interaction"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,6 +38,7 @@ type Converter struct {
|
||||||
randAvatars sync.Map
|
randAvatars sync.Map
|
||||||
visFilter *visibility.Filter
|
visFilter *visibility.Filter
|
||||||
intFilter *interaction.Filter
|
intFilter *interaction.Filter
|
||||||
|
randStats atomic.Pointer[apimodel.RandomStats]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConverter(state *state.State) *Converter {
|
func NewConverter(state *state.State) *Converter {
|
||||||
|
@ -41,3 +49,53 @@ func NewConverter(state *state.State) *Converter {
|
||||||
intFilter: interaction.NewFilter(state),
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1745,6 +1745,12 @@ func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins
|
||||||
stats["domain_count"] = util.Ptr(domainCount)
|
stats["domain_count"] = util.Ptr(domainCount)
|
||||||
instance.Stats = stats
|
instance.Stats = stats
|
||||||
|
|
||||||
|
if config.GetInstanceStatsRandomize() {
|
||||||
|
// Whack some random stats on the instance
|
||||||
|
// to be injected by API handlers.
|
||||||
|
instance.RandomStats = c.RandomStats()
|
||||||
|
}
|
||||||
|
|
||||||
// thumbnail
|
// thumbnail
|
||||||
iAccount, err := c.state.DB.GetInstanceAccount(ctx, "")
|
iAccount, err := c.state.DB.GetInstanceAccount(ctx, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1821,6 +1827,12 @@ func (c *Converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins
|
||||||
instance.Debug = util.Ptr(true)
|
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
|
||||||
thumbnail := apimodel.InstanceV2Thumbnail{}
|
thumbnail := apimodel.InstanceV2Thumbnail{}
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,7 @@ EXPECT=$(cat << "EOF"
|
||||||
"nl",
|
"nl",
|
||||||
"en-GB"
|
"en-GB"
|
||||||
],
|
],
|
||||||
|
"instance-stats-randomize": true,
|
||||||
"instance-subscriptions-process-every": 86400000000000,
|
"instance-subscriptions-process-every": 86400000000000,
|
||||||
"instance-subscriptions-process-from": "23:00",
|
"instance-subscriptions-process-from": "23:00",
|
||||||
"landing-page-user": "admin",
|
"landing-page-user": "admin",
|
||||||
|
@ -248,6 +249,7 @@ GTS_INSTANCE_FEDERATION_SPAM_FILTER=true \
|
||||||
GTS_INSTANCE_DELIVER_TO_SHARED_INBOXES=false \
|
GTS_INSTANCE_DELIVER_TO_SHARED_INBOXES=false \
|
||||||
GTS_INSTANCE_INJECT_MASTODON_VERSION=true \
|
GTS_INSTANCE_INJECT_MASTODON_VERSION=true \
|
||||||
GTS_INSTANCE_LANGUAGES="nl,en-gb" \
|
GTS_INSTANCE_LANGUAGES="nl,en-gb" \
|
||||||
|
GTS_INSTANCE_STATS_RANDOMIZE=true \
|
||||||
GTS_ACCOUNTS_ALLOW_CUSTOM_CSS=true \
|
GTS_ACCOUNTS_ALLOW_CUSTOM_CSS=true \
|
||||||
GTS_ACCOUNTS_CUSTOM_CSS_LENGTH=5000 \
|
GTS_ACCOUNTS_CUSTOM_CSS_LENGTH=5000 \
|
||||||
GTS_ACCOUNTS_REGISTRATION_OPEN=true \
|
GTS_ACCOUNTS_REGISTRATION_OPEN=true \
|
||||||
|
|
Loading…
Reference in a new issue