diff --git a/.env.example b/.env.example index fb16ddf..0a5715b 100644 --- a/.env.example +++ b/.env.example @@ -39,4 +39,5 @@ WC_FOOTBALL_DATA_TOKEN= # PORT — defaults to 8080 (internal health server) # TELEGRAM_WEBHOOK_SECRET — long polling has no webhook # GOLD_VNAPP_API_KEY — gold module auto-fetches + caches the key to Mongo -# STOCK/COIN/GOLD *_API_URL overrides — modules use their coded default providers +# Stock/coin/gold URL env overrides are not supported; modules use coded +# default providers. diff --git a/cmd/server/main.go b/cmd/server/main.go index 74d2197..3997319 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -86,13 +86,6 @@ func main() { if cfg.TelegramBotToken == "" { log.Fatal("missing required env", "key", "TELEGRAM_BOT_TOKEN") } - exportOptionalEnv("GOLD_PRICE_API_URL", cfg.GoldPriceAPIURL) - exportOptionalEnv("GOLD_FX_API_URL", cfg.GoldFXAPIURL) - exportOptionalEnv("GOLD_VNAPP_API_URL", cfg.GoldVNAppAPIURL) - exportOptionalEnv("GOLD_VNAPP_API_KEY", cfg.GoldVNAppAPIKey) - exportOptionalEnv("COIN_BINANCE_API_URL", cfg.CoinBinanceAPIURL) - exportOptionalEnv("COIN_COINBASE_API_URL", cfg.CoinCoinbaseAPIURL) - exportOptionalEnv("COIN_COINGECKO_API_URL", cfg.CoinCoinGeckoAPIURL) // Periodic metrics flush. Cancels with rootCtx and emits one final // flush on shutdown so the trailing window isn't lost. @@ -264,23 +257,16 @@ func buildProvider(ctx context.Context, cfg config) (storage.Provider, func(), e } type config struct { - Port string - TelegramBotToken string - SourceCommit string // Coolify-injected commit SHA (runtime env) for deploynotify - GeminiAPIKey string - GoldPriceAPIURL string - GoldFXAPIURL string - GoldVNAppAPIURL string - GoldVNAppAPIKey string - CoinBinanceAPIURL string - CoinCoinbaseAPIURL string - CoinCoinGeckoAPIURL string - Modules []string - BotOwnerID int64 - AdminUserIDs map[int64]bool - KVProvider string // empty = auto-detect; or "memory"|"mongodb" - MongoURL string // required when KVProvider=mongodb (Atlas SRV connection string; SECRET — never log) - MongoDatabase string // required when KVProvider=mongodb + Port string + TelegramBotToken string + SourceCommit string // Coolify-injected commit SHA (runtime env) for deploynotify + GeminiAPIKey string + Modules []string + BotOwnerID int64 + AdminUserIDs map[int64]bool + KVProvider string // empty = auto-detect; or "memory"|"mongodb" + MongoURL string // required when KVProvider=mongodb (Atlas SRV connection string; SECRET — never log) + MongoDatabase string // required when KVProvider=mongodb } func loadConfig() config { @@ -301,32 +287,16 @@ func loadConfig() config { log.Fatal("invalid PORT", "value", port) } return config{ - Port: port, - TelegramBotToken: envMap["TELEGRAM_BOT_TOKEN"], - SourceCommit: envMap["SOURCE_COMMIT"], - GeminiAPIKey: envMap["GEMINI_API_KEY"], - GoldPriceAPIURL: envMap["GOLD_PRICE_API_URL"], - GoldFXAPIURL: envMap["GOLD_FX_API_URL"], - GoldVNAppAPIURL: envMap["GOLD_VNAPP_API_URL"], - GoldVNAppAPIKey: envMap["GOLD_VNAPP_API_KEY"], - CoinBinanceAPIURL: envMap["COIN_BINANCE_API_URL"], - CoinCoinbaseAPIURL: envMap["COIN_COINBASE_API_URL"], - CoinCoinGeckoAPIURL: envMap["COIN_COINGECKO_API_URL"], - Modules: splitCSV(envMap["MODULES"]), - BotOwnerID: parseInt64(envMap["OWNER_ID"]), - AdminUserIDs: parseInt64Set(envMap["ADMIN_IDS"]), - KVProvider: envMap["KV_PROVIDER"], - MongoURL: envMap["MONGO_URL"], - MongoDatabase: envMap["MONGO_DATABASE"], - } -} - -func exportOptionalEnv(key, value string) { - if strings.TrimSpace(value) == "" { - return - } - if err := os.Setenv(key, value); err != nil { - log.Warn("could not export optional env", "key", key, "err", err) + Port: port, + TelegramBotToken: envMap["TELEGRAM_BOT_TOKEN"], + SourceCommit: envMap["SOURCE_COMMIT"], + GeminiAPIKey: envMap["GEMINI_API_KEY"], + Modules: splitCSV(envMap["MODULES"]), + BotOwnerID: parseInt64(envMap["OWNER_ID"]), + AdminUserIDs: parseInt64Set(envMap["ADMIN_IDS"]), + KVProvider: envMap["KV_PROVIDER"], + MongoURL: envMap["MONGO_URL"], + MongoDatabase: envMap["MONGO_DATABASE"], } } diff --git a/compose.yml b/compose.yml index fbc8bdc..61eb3e6 100644 --- a/compose.yml +++ b/compose.yml @@ -24,9 +24,10 @@ services: # PORT defaults to 8080 (internal health server) — omit unless overriding. # Long polling = no TELEGRAM_WEBHOOK_SECRET, no /webhook, no public domain. # Cron is in-process only — there is no /cron HTTP route and no secret. - # No stock/coin/gold *_API_URL overrides — modules use their coded default - # providers (stock: SSI/VCI/KBS; coin: Binance→Coinbase→CoinGecko; - # gold: VNAppMob→spot). GOLD_VNAPP_API_KEY auto-fetches + caches to Mongo. + # No stock/coin/gold URL env overrides — modules use their coded default + # providers (stock: SSI/VCI/KBS; coin: Binance->Coinbase->CoinGecko; + # gold: VNAppMob). If GOLD_VNAPP_API_KEY is unset, the bot auto-fetches + # and caches a key to Mongo. # Long polling is outbound-only: nothing inbound to route, so no published # ports and no public domain. `expose` keeps :8080 reachable inside the # Coolify network for the container health monitor against GET / only. diff --git a/docs/deploy-coolify-selfhosted.md b/docs/deploy-coolify-selfhosted.md index b307b32..a6025a6 100644 --- a/docs/deploy-coolify-selfhosted.md +++ b/docs/deploy-coolify-selfhosted.md @@ -37,8 +37,8 @@ Copy [`.env.example`](../.env.example) → `.env` (gitignored) and fill in. | `WC_FOOTBALL_DATA_TOKEN` | optional | football-data.org token for the `wc` module | **Leave UNSET on self-host:** `KV_PROVIDER`, `PORT`, -`TELEGRAM_WEBHOOK_SECRET`, `GOLD_VNAPP_API_KEY`, and the `STOCK/COIN/GOLD -*_API_URL` overrides (modules use coded defaults). +`TELEGRAM_WEBHOOK_SECRET`, and `GOLD_VNAPP_API_KEY`. Stock, coin, and gold URL +overrides are not supported in runtime env; modules use coded defaults. > Cron runs in-process (`internal/cron`) — there is no `/cron` HTTP route and no > `CRON_SHARED_SECRET`. The scheduler is the sole trigger; nothing inbound. diff --git a/internal/modules/coin/helpers.go b/internal/modules/coin/helpers.go index b8cfeec..5bd5ffe 100644 --- a/internal/modules/coin/helpers.go +++ b/internal/modules/coin/helpers.go @@ -28,7 +28,7 @@ type state struct { } func newState(store Store) *state { - return &state{store: store, prices: NewPriceClientFromEnv()} + return &state{store: store, prices: NewPriceClient()} } func (s *state) now() time.Time { diff --git a/internal/modules/coin/prices.go b/internal/modules/coin/prices.go index 5fcfe21..0b63f9f 100644 --- a/internal/modules/coin/prices.go +++ b/internal/modules/coin/prices.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "net/http" - "os" "sync" "time" ) @@ -47,13 +46,13 @@ type cachedPrice struct { expiry time.Time } -func NewPriceClientFromEnv() *PriceClient { +func NewPriceClient() *PriceClient { httpClient := &http.Client{Timeout: coinHTTPTimeout} return &PriceClient{ Providers: []PriceProvider{ - &BinanceProvider{HTTP: httpClient, URL: os.Getenv("COIN_BINANCE_API_URL")}, - &CoinbaseProvider{HTTP: httpClient, URL: os.Getenv("COIN_COINBASE_API_URL")}, - &CoinGeckoProvider{HTTP: httpClient, URL: os.Getenv("COIN_COINGECKO_API_URL")}, + &BinanceProvider{HTTP: httpClient}, + &CoinbaseProvider{HTTP: httpClient}, + &CoinGeckoProvider{HTTP: httpClient}, }, CacheTTL: coinPriceCacheTTL, } diff --git a/internal/modules/gold/composite_prices.go b/internal/modules/gold/composite_prices.go index 3528cbb..df6fc76 100644 --- a/internal/modules/gold/composite_prices.go +++ b/internal/modules/gold/composite_prices.go @@ -18,9 +18,9 @@ type compositePriceFetcher struct { vnappmob sjcPriceFetcher } -// NewCompositePriceFetcherFromEnv builds the production price fetcher using -// only the env-driven VNAppMob client. -func NewCompositePriceFetcherFromEnv(coll storage.Collection) priceFetcher { +// NewCompositePriceFetcher builds the production price fetcher using the coded +// VNAppMob endpoint. +func NewCompositePriceFetcher(coll storage.Collection) priceFetcher { return &compositePriceFetcher{ vnappmob: NewVNAppMobClientFromEnv(coll), } diff --git a/internal/modules/gold/composite_prices_test.go b/internal/modules/gold/composite_prices_test.go index 7682a2c..fa6534e 100644 --- a/internal/modules/gold/composite_prices_test.go +++ b/internal/modules/gold/composite_prices_test.go @@ -91,8 +91,8 @@ func TestGoldPriceLines(t *testing.T) { } } -func TestNewCompositePriceFetcherFromEnv(t *testing.T) { - f, ok := NewCompositePriceFetcherFromEnv(storage.NewMemoryProvider().Collection("gold")).(*compositePriceFetcher) +func TestNewCompositePriceFetcher(t *testing.T) { + f, ok := NewCompositePriceFetcher(storage.NewMemoryProvider().Collection("gold")).(*compositePriceFetcher) if !ok { t.Fatalf("expected *compositePriceFetcher, got %T", f) } diff --git a/internal/modules/gold/helpers.go b/internal/modules/gold/helpers.go index 5c89f2b..6d40b4c 100644 --- a/internal/modules/gold/helpers.go +++ b/internal/modules/gold/helpers.go @@ -33,7 +33,7 @@ type state struct { func newState(coll storage.Collection) *state { return &state{ store: storage.Typed[Portfolio](coll), - prices: NewCompositePriceFetcherFromEnv(coll), + prices: NewCompositePriceFetcher(coll), } } diff --git a/internal/modules/gold/prices.go b/internal/modules/gold/prices.go index 8525862..4a3375f 100644 --- a/internal/modules/gold/prices.go +++ b/internal/modules/gold/prices.go @@ -7,7 +7,6 @@ import ( "fmt" "math" "net/http" - "os" "strings" "sync" "time" @@ -42,7 +41,8 @@ type SJCPrice struct { type GoldPriceClient struct { HTTP *http.Client - // Per-provider URL overrides; empty means the provider default. + // URL fields are explicit injection points for tests; production uses the + // provider defaults. GoldURL string // primary: gold-api.com SwissquoteURL string NBPURL string @@ -57,13 +57,6 @@ type GoldPriceClient struct { fxExpiry time.Time } -func NewGoldPriceClientFromEnv() *GoldPriceClient { - return &GoldPriceClient{ - GoldURL: strings.TrimSpace(os.Getenv("GOLD_PRICE_API_URL")), - FXURL: strings.TrimSpace(os.Getenv("GOLD_FX_API_URL")), - } -} - func (c *GoldPriceClient) FetchPrice(ctx context.Context) (GoldPrice, error) { xauUSD, err := c.fetchXAUUSD(ctx) if err != nil { diff --git a/internal/modules/gold/vnappmob_client.go b/internal/modules/gold/vnappmob_client.go index 93c3344..899f2bd 100644 --- a/internal/modules/gold/vnappmob_client.go +++ b/internal/modules/gold/vnappmob_client.go @@ -36,7 +36,7 @@ type apiKeyCache struct { // refreshing it before expiry or when the SJC endpoint returns 403. type VNAppMobClient struct { HTTP *http.Client - BaseURL string // optional override; default https://api.vnappmob.com + BaseURL string // explicit test override; production uses https://api.vnappmob.com Token string // optional env override (GOLD_VNAPP_API_KEY) cache storage.DocStore[apiKeyCache] // module-scoped typed cache store @@ -44,14 +44,13 @@ type VNAppMobClient struct { mu sync.Mutex } -// NewVNAppMobClientFromEnv creates a client reading GOLD_VNAPP_API_URL and -// GOLD_VNAPP_API_KEY from the environment. When the key is empty, the client -// refreshes it automatically via the VNAppMob refresh endpoint. +// NewVNAppMobClientFromEnv creates a client reading only GOLD_VNAPP_API_KEY +// from the environment. When the key is empty, the client refreshes it +// automatically via the coded VNAppMob endpoint. func NewVNAppMobClientFromEnv(coll storage.Collection) *VNAppMobClient { return &VNAppMobClient{ - BaseURL: strings.TrimSpace(os.Getenv("GOLD_VNAPP_API_URL")), - Token: strings.TrimSpace(os.Getenv("GOLD_VNAPP_API_KEY")), - cache: storage.Typed[apiKeyCache](coll), + Token: strings.TrimSpace(os.Getenv("GOLD_VNAPP_API_KEY")), + cache: storage.Typed[apiKeyCache](coll), } }