/* * Amsterdam Web Communities System * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ // Package config contains support for Amsterdam site-wide configuration data. package config import ( _ "embed" "errors" "fmt" "os" "regexp" "strconv" argparse "github.com/alexflint/go-arg" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) // AMSTERDAM_VERSION contains the version number of Amsterdam. const AMSTERDAM_VERSION = "0.0.1" // AMSTERDAM_COPYRIGHT contains the copyright dates for Amsterdam. const AMSTERDAM_COPYRIGHT = "2025-2026" // AmCLI is the command-line interface arguments structure. type AmCLI struct { ConfigFile string `arg:"-C,--config,env:AMSTERDAM_CONFIG" help:"Location of the configuration file."` DebugPanic bool `arg:"--debug-panic" help:"Development Only - disable Echo panic recovery"` BuggyAttachments bool `arg:"--buggy-attachments" help:"Some attachments may be buggy - truncate data if necessary"` } // CommandLine is the command-line arguments passed to Amsterdam. var CommandLine AmCLI // Description (from argparse.Described) returns the description string for the application. func (*AmCLI) Description() string { return "Amsterdam Web Communities System Server" } // Version (from argparse.Versioned) returns the version number string for the application. func (*AmCLI) Version() string { return "Amsterdam " + AMSTERDAM_VERSION } // AmConfig holds the configuration of the application as read from YAML. type AmConfig struct { Site struct { BaseURL string `yaml:"baseURL"` Title string `yaml:"title"` TopRefresh int `yaml:"topRefresh"` LoginCookieName string `yaml:"loginCookieName"` LoginCookieAge int `yaml:"loginCookieAge"` SessionExpire string `yaml:"sessionExpire"` UserAgreement struct { Title string `yaml:"title"` Text string `yaml:"text"` } `yaml:"userAgreement"` ExternalPath string `yaml:"externalPath"` } `yaml:"site"` Database struct { Driver string `yaml:"driver"` Dsn string `yaml:"dsn"` } `yaml:"database"` Defaults struct { Language string `yaml:"language"` TimeZone string `yaml:"timezone"` } `yaml:"defaults"` Email struct { Host string `yaml:"host"` Port int `yaml:"port"` Tls string `yaml:"tls"` AuthType string `yaml:"authType"` User string `yaml:"user"` Password string `yaml:"password"` MailFromAddr string `yaml:"mailFromAddr"` MailFromName string `yaml:"mailFromName"` Signature string `yaml:"signature"` Disclaimer string `yaml:"disclaimer"` } `yaml:"email"` Rendering struct { TemplateDir string `yaml:"templatedir"` CookieKey string `yaml:"cookiekey"` CountryList struct { Prioritize string `yaml:"prioritize"` } `yaml:"countryList"` VeniceCompatibleImageURLs bool `yaml:"veniceCompatibleImageURLs"` } `yaml:"rendering"` Posting struct { ExternalDictionary string `yaml:"externalDictionary"` Uploads struct { MaxSize string `yaml:"maxSize"` NoCompressTypes []string `yaml:"noCompressTypes"` } `yaml:"uploads"` } `yaml:"posting"` Tuning struct { WorkerTasks int `yaml:"workerTasks"` Queues struct { AuditWrites int `yaml:"auditWrites"` ContextRecycle int `yaml:"contextRecycle"` EmailRecycle int `yaml:"emailRecycle"` EmailSend int `yaml:"emailSend"` IPBans int `yaml:"ipBans"` WorkerTasks int `yaml:"workerTasks"` } `yaml:"queues"` Caches struct { Ads int `yaml:"ads"` Communities int `yaml:"communities"` CommunityProps int `yaml:"communityProps"` Conferences int `yaml:"conferences"` ConferenceProps int `yaml:"conferenceProps"` ContactInfo int `yaml:"contactInfo"` Members int `yaml:"members"` Menus int `yaml:"menus"` Services int `yaml:"services"` Users int `yaml:"users"` UserProps int `yaml:"userProps"` } `yaml:"caches"` } `yaml:"tuning"` } type AmConfigComputed struct { UploadMaxSize int32 UploadNoCompress map[string]bool } //go:embed default.yaml var defaultConfigData []byte // defaultConfig holds the default configuration data. var defaultConfig AmConfig // GlobalConfig holds the global configuration. var GlobalConfig AmConfig // GlobalComputedConfig holds the computed values based on GlobalConfig. var GlobalComputedConfig AmConfigComputed // init prepares the default configuration for the application. func init() { if err := yaml.Unmarshal(defaultConfigData, &defaultConfig); err != nil { panic(err) // can't happen } } /* overlayString is a helper that takes a loaded or defaulted string and returns it. * Parameters: * loaded - The string loaded from a configuration file. * defaulted - The default value of this string. * Returns: * loaded if it's not empty, otherwise defaulted. */ func overlayString(loaded string, defaulted string) string { if loaded == "" { return defaulted } return loaded } /* overlayString is a helper that takes a loaded or defaulted string array and returns it. (It merges the two * if two different arrays are specified.) * Parameters: * loaded - The array loaded from a configuration file. * defaulted - The default value of this array. * Returns: * Merged version of the two arrays. */ func overlayStringArray(loaded, defaulted []string) []string { m := make(map[string]bool) for _, s := range defaulted { m[s] = true } for _, s := range loaded { m[s] = true } rc := make([]string, 0, len(m)) for s := range m { rc = append(rc, s) } return rc } /* overlayInt is a helper that takes a loaded or defaulted integer and returns it. * Parameters: * loaded - The integer loaded from a configuration file. * defaulted - The default value of this integer. * Returns: * loaded if it's not empty, otherwise defaulted. */ func overlayInt(loaded int, defaulted int) int { if loaded != 0 { return loaded } return defaulted } /* overlayOptionFlag is a helper that takes a loaded or defaulted option flag and returns it. * Parameters: * loaded - The option flag loaded from a configuration file. * defaulted - The default value of this option flag. * Returns: * Combined value. */ func overlayOptionFlag(loaded, defaulted bool) bool { return loaded || defaulted } /* overlayConfig takes two configuration structures and overlays them to create the third. * Parameters: * dest - Points to the destination configuration structure. * loaded - Points to the loaded configuration structure. * defaults - Points to the default configuration structure. */ func overlayConfig(dest *AmConfig, loaded *AmConfig, defaults *AmConfig) { dest.Site.BaseURL = overlayString(loaded.Site.BaseURL, defaults.Site.BaseURL) dest.Site.Title = overlayString(loaded.Site.Title, defaults.Site.Title) dest.Site.TopRefresh = overlayInt(loaded.Site.TopRefresh, defaults.Site.TopRefresh) dest.Site.LoginCookieName = overlayString(loaded.Site.LoginCookieName, defaults.Site.LoginCookieName) dest.Site.LoginCookieAge = overlayInt(loaded.Site.LoginCookieAge, defaults.Site.LoginCookieAge) dest.Site.SessionExpire = overlayString(loaded.Site.SessionExpire, defaults.Site.SessionExpire) dest.Site.UserAgreement.Title = overlayString(loaded.Site.UserAgreement.Title, defaults.Site.UserAgreement.Title) dest.Site.UserAgreement.Text = overlayString(loaded.Site.UserAgreement.Text, defaults.Site.UserAgreement.Text) dest.Site.ExternalPath = overlayString(loaded.Site.ExternalPath, defaults.Site.ExternalPath) dest.Database.Driver = overlayString(loaded.Database.Driver, defaults.Database.Driver) dest.Database.Dsn = overlayString(loaded.Database.Dsn, defaults.Database.Dsn) dest.Defaults.Language = overlayString(loaded.Defaults.Language, defaults.Defaults.Language) dest.Defaults.TimeZone = overlayString(loaded.Defaults.TimeZone, defaults.Defaults.TimeZone) dest.Email.Host = overlayString(loaded.Email.Host, defaults.Email.Host) dest.Email.Port = overlayInt(loaded.Email.Port, defaults.Email.Port) dest.Email.Tls = overlayString(loaded.Email.Tls, defaults.Email.Tls) dest.Email.AuthType = overlayString(loaded.Email.AuthType, defaults.Email.AuthType) dest.Email.User = overlayString(loaded.Email.User, defaults.Email.User) dest.Email.Password = overlayString(loaded.Email.Password, defaults.Email.Password) dest.Email.MailFromAddr = overlayString(loaded.Email.MailFromAddr, defaults.Email.MailFromAddr) dest.Email.MailFromName = overlayString(loaded.Email.MailFromName, defaults.Email.MailFromName) dest.Email.Signature = overlayString(loaded.Email.Signature, defaults.Email.Signature) dest.Email.Disclaimer = overlayString(loaded.Email.Disclaimer, defaults.Email.Disclaimer) dest.Rendering.TemplateDir = overlayString(loaded.Rendering.TemplateDir, defaults.Rendering.TemplateDir) dest.Rendering.CookieKey = overlayString(loaded.Rendering.CookieKey, defaults.Rendering.CookieKey) dest.Rendering.CountryList.Prioritize = overlayString(loaded.Rendering.CountryList.Prioritize, defaults.Rendering.CountryList.Prioritize) dest.Rendering.VeniceCompatibleImageURLs = overlayOptionFlag(loaded.Rendering.VeniceCompatibleImageURLs, defaults.Rendering.VeniceCompatibleImageURLs) dest.Posting.ExternalDictionary = overlayString(loaded.Posting.ExternalDictionary, defaults.Posting.ExternalDictionary) dest.Posting.Uploads.MaxSize = overlayString(loaded.Posting.Uploads.MaxSize, defaults.Posting.Uploads.MaxSize) dest.Posting.Uploads.NoCompressTypes = overlayStringArray(loaded.Posting.Uploads.NoCompressTypes, defaults.Posting.Uploads.NoCompressTypes) dest.Tuning.WorkerTasks = overlayInt(loaded.Tuning.WorkerTasks, defaults.Tuning.WorkerTasks) dest.Tuning.Queues.AuditWrites = overlayInt(loaded.Tuning.Queues.AuditWrites, defaults.Tuning.Queues.AuditWrites) dest.Tuning.Queues.ContextRecycle = overlayInt(loaded.Tuning.Queues.ContextRecycle, defaults.Tuning.Queues.ContextRecycle) dest.Tuning.Queues.EmailRecycle = overlayInt(loaded.Tuning.Queues.EmailRecycle, defaults.Tuning.Queues.EmailRecycle) dest.Tuning.Queues.EmailSend = overlayInt(loaded.Tuning.Queues.EmailSend, defaults.Tuning.Queues.EmailSend) dest.Tuning.Queues.IPBans = overlayInt(loaded.Tuning.Queues.IPBans, defaults.Tuning.Queues.IPBans) dest.Tuning.Queues.WorkerTasks = overlayInt(loaded.Tuning.Queues.WorkerTasks, defaults.Tuning.Queues.WorkerTasks) dest.Tuning.Caches.Ads = overlayInt(loaded.Tuning.Caches.Ads, defaults.Tuning.Caches.Ads) dest.Tuning.Caches.Communities = overlayInt(loaded.Tuning.Caches.Communities, defaults.Tuning.Caches.Communities) dest.Tuning.Caches.CommunityProps = overlayInt(loaded.Tuning.Caches.CommunityProps, defaults.Tuning.Caches.CommunityProps) dest.Tuning.Caches.Conferences = overlayInt(loaded.Tuning.Caches.Conferences, defaults.Tuning.Caches.Conferences) dest.Tuning.Caches.ConferenceProps = overlayInt(loaded.Tuning.Caches.ConferenceProps, defaults.Tuning.Caches.ConferenceProps) dest.Tuning.Caches.ContactInfo = overlayInt(loaded.Tuning.Caches.ContactInfo, defaults.Tuning.Caches.ContactInfo) dest.Tuning.Caches.Members = overlayInt(loaded.Tuning.Caches.Members, defaults.Tuning.Caches.Members) dest.Tuning.Caches.Menus = overlayInt(loaded.Tuning.Caches.Menus, defaults.Tuning.Caches.Menus) dest.Tuning.Caches.Services = overlayInt(loaded.Tuning.Caches.Services, defaults.Tuning.Caches.Services) dest.Tuning.Caches.Users = overlayInt(loaded.Tuning.Caches.Users, defaults.Tuning.Caches.Users) dest.Tuning.Caches.UserProps = overlayInt(loaded.Tuning.Caches.UserProps, defaults.Tuning.Caches.UserProps) } // parseDataSize converts the data size in bytes, kilobytes, megabytes, or gigabytes to a number value. func parseDataSize(s string) (int32, error) { re, err := regexp.Compile(`^\s*(\d+)\s*([KkMmGg]?)[Bb]?`) if err != nil { return -1, err } m := re.FindStringSubmatch(s) if m == nil { return -1, errors.New("invalid value spacified") } rc, err := strconv.Atoi(m[1]) if err != nil { return -1, err } switch m[2] { case "k", "K": rc *= 1024 case "m", "M": rc *= (1024 * 1024) case "g", "G": rc *= (1024 * 1024 * 1024) } return int32(rc), nil } // SetupConfig loads the command line arguments, loads the config file, and prepares GlobalConfig. func SetupConfig() { argparse.MustParse(&CommandLine) if CommandLine.BuggyAttachments { log.Warn("WARNING: --buggy-attachments flag set - NOT recommended for production usage") } if CommandLine.ConfigFile != "" { // load the data and use it to unmarshal the loaded configuration data, err := os.ReadFile(CommandLine.ConfigFile) if err != nil { panic(fmt.Sprintf("unable to load configuration file %s: %v", CommandLine.ConfigFile, err)) } var loadedConfig AmConfig if err = yaml.Unmarshal(data, &loadedConfig); err != nil { panic(fmt.Sprintf("unable to load configuration file %s: %v", CommandLine.ConfigFile, err)) } overlayConfig(&GlobalConfig, &loadedConfig, &defaultConfig) } else { GlobalConfig = defaultConfig // just copy over the defaults } // Compute additional values. tmp, err := parseDataSize(GlobalConfig.Posting.Uploads.MaxSize) if err != nil { panic(err.Error()) } GlobalComputedConfig.UploadMaxSize = tmp GlobalComputedConfig.UploadNoCompress = make(map[string]bool) for _, s := range GlobalConfig.Posting.Uploads.NoCompressTypes { GlobalComputedConfig.UploadNoCompress[s] = true } }