mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-12-04 21:28:08 -06:00
[feature] Set/show instance language(s); show post language on frontend (#2362)
* update go text, include text/display * [feature] Set instance langs, show post lang on frontend * go fmt * WebGet * set language for whole article, don't use FA icon * mention instance languages + other optional config vars * little tweak * put languages in config properly * warn log language parse * change some naming around * tidy up validate a bit * lint * rename LanguageTmpl in template
This commit is contained in:
parent
4ee436e98a
commit
fc02d3c6f7
73 changed files with 55005 additions and 141 deletions
|
|
@ -23,6 +23,7 @@ import (
|
|||
|
||||
"codeberg.org/gruf/go-bytesize"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/language"
|
||||
)
|
||||
|
||||
// cfgtype is the reflected type information of Configuration{}.
|
||||
|
|
@ -76,13 +77,14 @@ type Configuration struct {
|
|||
WebTemplateBaseDir string `name:"web-template-base-dir" usage:"Basedir for html templating files for rendering pages and composing emails."`
|
||||
WebAssetBaseDir string `name:"web-asset-base-dir" usage:"Directory to serve static assets from, accessible at example.org/assets/"`
|
||||
|
||||
InstanceFederationMode string `name:"instance-federation-mode" usage:"Set instance federation mode."`
|
||||
InstanceExposePeers bool `name:"instance-expose-peers" usage:"Allow unauthenticated users to query /api/v1/instance/peers?filter=open"`
|
||||
InstanceExposeSuspended bool `name:"instance-expose-suspended" usage:"Expose suspended instances via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=suspended"`
|
||||
InstanceExposeSuspendedWeb bool `name:"instance-expose-suspended-web" usage:"Expose list of suspended instances as webpage on /about/suspended"`
|
||||
InstanceExposePublicTimeline bool `name:"instance-expose-public-timeline" usage:"Allow unauthenticated users to query /api/v1/timelines/public"`
|
||||
InstanceDeliverToSharedInboxes bool `name:"instance-deliver-to-shared-inboxes" usage:"Deliver federated messages to shared inboxes, if they're available."`
|
||||
InstanceInjectMastodonVersion bool `name:"instance-inject-mastodon-version" usage:"This injects a Mastodon compatible version in /api/v1/instance to help Mastodon clients that use that version for feature detection"`
|
||||
InstanceFederationMode string `name:"instance-federation-mode" usage:"Set instance federation mode."`
|
||||
InstanceExposePeers bool `name:"instance-expose-peers" usage:"Allow unauthenticated users to query /api/v1/instance/peers?filter=open"`
|
||||
InstanceExposeSuspended bool `name:"instance-expose-suspended" usage:"Expose suspended instances via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=suspended"`
|
||||
InstanceExposeSuspendedWeb bool `name:"instance-expose-suspended-web" usage:"Expose list of suspended instances as webpage on /about/suspended"`
|
||||
InstanceExposePublicTimeline bool `name:"instance-expose-public-timeline" usage:"Allow unauthenticated users to query /api/v1/timelines/public"`
|
||||
InstanceDeliverToSharedInboxes bool `name:"instance-deliver-to-shared-inboxes" usage:"Deliver federated messages to shared inboxes, if they're available."`
|
||||
InstanceInjectMastodonVersion bool `name:"instance-inject-mastodon-version" usage:"This injects a Mastodon compatible version in /api/v1/instance to help Mastodon clients that use that version for feature detection"`
|
||||
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)."`
|
||||
|
||||
AccountsRegistrationOpen bool `name:"accounts-registration-open" usage:"Allow anyone to submit an account signup request. If false, server will be invite-only."`
|
||||
AccountsApprovalRequired bool `name:"accounts-approval-required" usage:"Do account signups require approval by an admin or moderator before user can log in? If false, new registrations will be automatically approved."`
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import (
|
|||
|
||||
"codeberg.org/gruf/go-bytesize"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/language"
|
||||
)
|
||||
|
||||
// Defaults contains a populated Configuration with reasonable defaults. Note that
|
||||
|
|
@ -62,6 +63,7 @@ var Defaults = Configuration{
|
|||
InstanceExposeSuspended: false,
|
||||
InstanceExposeSuspendedWeb: false,
|
||||
InstanceDeliverToSharedInboxes: true,
|
||||
InstanceLanguages: make(language.Languages, 0),
|
||||
|
||||
AccountsRegistrationOpen: true,
|
||||
AccountsApprovalRequired: true,
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ func (s *ConfigState) AddServerFlags(cmd *cobra.Command) {
|
|||
cmd.Flags().Bool(InstanceExposeSuspendedFlag(), cfg.InstanceExposeSuspended, fieldtag("InstanceExposeSuspended", "usage"))
|
||||
cmd.Flags().Bool(InstanceExposeSuspendedWebFlag(), cfg.InstanceExposeSuspendedWeb, fieldtag("InstanceExposeSuspendedWeb", "usage"))
|
||||
cmd.Flags().Bool(InstanceDeliverToSharedInboxesFlag(), cfg.InstanceDeliverToSharedInboxes, fieldtag("InstanceDeliverToSharedInboxes", "usage"))
|
||||
// cmd.Flags().StringSlice(InstanceLanguagesFlag(), cfg.InstanceLanguages.TagStrs(), fieldtag("InstanceLanguages", "usage"))
|
||||
|
||||
// Accounts
|
||||
cmd.Flags().Bool(AccountsRegistrationOpenFlag(), cfg.AccountsRegistrationOpen, fieldtag("AccountsRegistrationOpen", "usage"))
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ func main() {
|
|||
fmt.Fprint(output, "import (\n")
|
||||
fmt.Fprint(output, "\t\"time\"\n\n")
|
||||
fmt.Fprint(output, "\t\"codeberg.org/gruf/go-bytesize\"\n")
|
||||
fmt.Fprint(output, "\t\"github.com/superseriousbusiness/gotosocial/internal/langs\"\n")
|
||||
fmt.Fprint(output, ")\n\n")
|
||||
generateFields(output, nil, reflect.TypeOf(config.Configuration{}))
|
||||
_ = output.Close()
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import (
|
|||
"time"
|
||||
|
||||
"codeberg.org/gruf/go-bytesize"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/language"
|
||||
)
|
||||
|
||||
// GetLogLevel safely fetches the Configuration value for state's 'LogLevel' field
|
||||
|
|
@ -924,6 +925,31 @@ func GetInstanceInjectMastodonVersion() bool { return global.GetInstanceInjectMa
|
|||
// SetInstanceInjectMastodonVersion safely sets the value for global configuration 'InstanceInjectMastodonVersion' field
|
||||
func SetInstanceInjectMastodonVersion(v bool) { global.SetInstanceInjectMastodonVersion(v) }
|
||||
|
||||
// GetInstanceLanguages safely fetches the Configuration value for state's 'InstanceLanguages' field
|
||||
func (st *ConfigState) GetInstanceLanguages() (v language.Languages) {
|
||||
st.mutex.RLock()
|
||||
v = st.config.InstanceLanguages
|
||||
st.mutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// SetInstanceLanguages safely sets the Configuration value for state's 'InstanceLanguages' field
|
||||
func (st *ConfigState) SetInstanceLanguages(v language.Languages) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
st.config.InstanceLanguages = v
|
||||
st.reloadToViper()
|
||||
}
|
||||
|
||||
// InstanceLanguagesFlag returns the flag name for the 'InstanceLanguages' field
|
||||
func InstanceLanguagesFlag() string { return "instance-languages" }
|
||||
|
||||
// GetInstanceLanguages safely fetches the value for global configuration 'InstanceLanguages' field
|
||||
func GetInstanceLanguages() language.Languages { return global.GetInstanceLanguages() }
|
||||
|
||||
// SetInstanceLanguages safely sets the value for global configuration 'InstanceLanguages' field
|
||||
func SetInstanceLanguages(v language.Languages) { global.SetInstanceLanguages(v) }
|
||||
|
||||
// GetAccountsRegistrationOpen safely fetches the Configuration value for state's 'AccountsRegistrationOpen' field
|
||||
func (st *ConfigState) GetAccountsRegistrationOpen() (v bool) {
|
||||
st.mutex.RLock()
|
||||
|
|
|
|||
|
|
@ -18,85 +18,131 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/language"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
)
|
||||
|
||||
// Validate validates global config settings which don't have defaults, to make sure they are set sensibly.
|
||||
// Validate validates global config settings.
|
||||
func Validate() error {
|
||||
errs := []error{}
|
||||
// Gather all validation errors in
|
||||
// easily readable format for admins.
|
||||
var (
|
||||
errs gtserror.MultiError
|
||||
errf = func(format string, a ...any) {
|
||||
errs = append(errs, fmt.Errorf(format, a...))
|
||||
}
|
||||
)
|
||||
|
||||
// host
|
||||
// `host`
|
||||
host := GetHost()
|
||||
if host == "" {
|
||||
errs = append(errs, fmt.Errorf("%s must be set", HostFlag()))
|
||||
errf("%s must be set", HostFlag())
|
||||
}
|
||||
|
||||
// accountDomain; only check if host was set, otherwise there's no point
|
||||
// If `account-domain` and `host`
|
||||
// are set, `host` must be a valid
|
||||
// subdomain of `account-domain`.
|
||||
if host != "" {
|
||||
switch ad := GetAccountDomain(); ad {
|
||||
case "":
|
||||
ad := GetAccountDomain()
|
||||
if ad == "" {
|
||||
// `account-domain` not set, fall
|
||||
// back by setting it to `host`.
|
||||
SetAccountDomain(GetHost())
|
||||
default:
|
||||
if !dns.IsSubDomain(ad, host) {
|
||||
errs = append(errs, fmt.Errorf("%s was %s and %s was %s, but %s is not a valid subdomain of %s", HostFlag(), host, AccountDomainFlag(), ad, host, ad))
|
||||
}
|
||||
} else if !dns.IsSubDomain(ad, host) {
|
||||
errf(
|
||||
"%s %s is not a valid subdomain of %s %s",
|
||||
AccountDomainFlag(), ad, HostFlag(), host,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// protocol
|
||||
// Ensure `protocol` sensibly set.
|
||||
switch proto := GetProtocol(); proto {
|
||||
case "https":
|
||||
// no problem
|
||||
break
|
||||
// No problem.
|
||||
|
||||
case "http":
|
||||
log.Warnf(nil, "%s was set to 'http'; this should *only* be used for debugging and tests!", ProtocolFlag())
|
||||
log.Warnf(
|
||||
nil,
|
||||
"%s was set to 'http'; this should *only* be used for debugging and tests!",
|
||||
ProtocolFlag(),
|
||||
)
|
||||
|
||||
case "":
|
||||
errs = append(errs, fmt.Errorf("%s must be set", ProtocolFlag()))
|
||||
errf("%s must be set", ProtocolFlag())
|
||||
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf("%s must be set to either http or https, provided value was %s", ProtocolFlag(), proto))
|
||||
errf(
|
||||
"%s must be set to either http or https, provided value was %s",
|
||||
ProtocolFlag(), proto,
|
||||
)
|
||||
}
|
||||
|
||||
// federation mode
|
||||
switch federationMode := GetInstanceFederationMode(); federationMode {
|
||||
// `federation-mode` should be
|
||||
// "blocklist" or "allowlist".
|
||||
switch fediMode := GetInstanceFederationMode(); fediMode {
|
||||
case InstanceFederationModeBlocklist, InstanceFederationModeAllowlist:
|
||||
// no problem
|
||||
break
|
||||
// No problem.
|
||||
|
||||
case "":
|
||||
errs = append(errs, fmt.Errorf("%s must be set", InstanceFederationModeFlag()))
|
||||
errf("%s must be set", InstanceFederationModeFlag())
|
||||
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf("%s must be set to either blocklist or allowlist, provided value was %s", InstanceFederationModeFlag(), federationMode))
|
||||
errf(
|
||||
"%s must be set to either blocklist or allowlist, provided value was %s",
|
||||
InstanceFederationModeFlag(), fediMode,
|
||||
)
|
||||
}
|
||||
|
||||
// Parse `instance-languages`, and
|
||||
// set enriched version into config.
|
||||
parsedLangs, err := language.InitLangs(GetInstanceLanguages().TagStrs())
|
||||
if err != nil {
|
||||
errf(
|
||||
"%s could not be parsed as an array of valid BCP47 language tags: %v",
|
||||
InstanceLanguagesFlag(), err,
|
||||
)
|
||||
} else {
|
||||
// Parsed successfully, put enriched
|
||||
// versions in config immediately.
|
||||
SetInstanceLanguages(parsedLangs)
|
||||
}
|
||||
|
||||
// `web-assets-base-dir`.
|
||||
webAssetsBaseDir := GetWebAssetBaseDir()
|
||||
if webAssetsBaseDir == "" {
|
||||
errs = append(errs, fmt.Errorf("%s must be set", WebAssetBaseDirFlag()))
|
||||
errf("%s must be set", WebAssetBaseDirFlag())
|
||||
}
|
||||
|
||||
tlsChain := GetTLSCertificateChain()
|
||||
tlsKey := GetTLSCertificateKey()
|
||||
tlsChainFlag := TLSCertificateChainFlag()
|
||||
tlsKeyFlag := TLSCertificateKeyFlag()
|
||||
// Custom / LE TLS settings.
|
||||
//
|
||||
// Only one of custom certs or LE can be set,
|
||||
// and if using custom certs then all relevant
|
||||
// values must be provided.
|
||||
var (
|
||||
tlsChain = GetTLSCertificateChain()
|
||||
tlsKey = GetTLSCertificateKey()
|
||||
tlsChainFlag = TLSCertificateChainFlag()
|
||||
tlsKeyFlag = TLSCertificateKeyFlag()
|
||||
)
|
||||
|
||||
if GetLetsEncryptEnabled() && (tlsChain != "" || tlsKey != "") {
|
||||
errs = append(errs, fmt.Errorf("%s cannot be enabled when %s and/or %s are also set", LetsEncryptEnabledFlag(), tlsChainFlag, tlsKeyFlag))
|
||||
errf(
|
||||
"%s cannot be true when %s and/or %s are also set",
|
||||
LetsEncryptEnabledFlag(), tlsChainFlag, tlsKeyFlag,
|
||||
)
|
||||
}
|
||||
|
||||
if (tlsChain != "" && tlsKey == "") || (tlsChain == "" && tlsKey != "") {
|
||||
errs = append(errs, fmt.Errorf("%s and %s need to both be set or unset", tlsChainFlag, tlsKeyFlag))
|
||||
errf(
|
||||
"%s and %s need to both be set or unset",
|
||||
tlsChainFlag, tlsKeyFlag,
|
||||
)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
errStrings := []string{}
|
||||
for _, err := range errs {
|
||||
errStrings = append(errStrings, err.Error())
|
||||
}
|
||||
return errors.New(strings.Join(errStrings, "; "))
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ func (suite *ConfigValidateTestSuite) TestValidateAccountDomainNotSubdomain1() {
|
|||
config.SetAccountDomain("example.com")
|
||||
|
||||
err := config.Validate()
|
||||
suite.EqualError(err, "host was gts.example.org and account-domain was example.com, but gts.example.org is not a valid subdomain of example.com")
|
||||
suite.EqualError(err, "account-domain example.com is not a valid subdomain of host gts.example.org")
|
||||
}
|
||||
|
||||
func (suite *ConfigValidateTestSuite) TestValidateAccountDomainNotSubdomain2() {
|
||||
|
|
@ -90,7 +90,7 @@ func (suite *ConfigValidateTestSuite) TestValidateAccountDomainNotSubdomain2() {
|
|||
config.SetAccountDomain("gts.example.org")
|
||||
|
||||
err := config.Validate()
|
||||
suite.EqualError(err, "host was example.org and account-domain was gts.example.org, but example.org is not a valid subdomain of gts.example.org")
|
||||
suite.EqualError(err, "account-domain gts.example.org is not a valid subdomain of host example.org")
|
||||
}
|
||||
|
||||
func (suite *ConfigValidateTestSuite) TestValidateConfigNoProtocol() {
|
||||
|
|
@ -118,7 +118,7 @@ func (suite *ConfigValidateTestSuite) TestValidateConfigNoProtocolOrHost() {
|
|||
config.SetProtocol("")
|
||||
|
||||
err := config.Validate()
|
||||
suite.EqualError(err, "host must be set; protocol must be set")
|
||||
suite.EqualError(err, "host must be set\nprotocol must be set")
|
||||
}
|
||||
|
||||
func (suite *ConfigValidateTestSuite) TestValidateConfigBadProtocol() {
|
||||
|
|
@ -137,7 +137,7 @@ func (suite *ConfigValidateTestSuite) TestValidateConfigBadProtocolNoHost() {
|
|||
config.SetProtocol("foo")
|
||||
|
||||
err := config.Validate()
|
||||
suite.EqualError(err, "host must be set; protocol must be set to either http or https, provided value was foo")
|
||||
suite.EqualError(err, "host must be set\nprotocol must be set to either http or https, provided value was foo")
|
||||
}
|
||||
|
||||
func TestConfigValidateTestSuite(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue