package laniakea import ( "os" "strconv" "strings" "git.scuroneko.dev/scuroneko/laniakea/tgapi" "git.scuroneko.dev/scuroneko/laniakea/utils" "git.scuroneko.dev/scuroneko/sneklog/v2" ) // BotOpts holds configuration options for initializing a Bot. // // Values are loaded from environment variables via LoadOptsFromEnv(). // Use &BotOpts{} to create a value and set fields manually. type BotOpts struct { // Token is the Telegram bot token (required). Token string // UpdateTypes is a list of update types to listen for. // Example: "["message", "edited_message", "callback_query"]" // Defaults to empty (Telegram will return all types). UpdateTypes []tgapi.UpdateType // Debug enables debug-level logging. Debug bool // ErrorTemplate is the format string used to wrap error messages sent to users. // Use "%s" to insert the actual error. Example: "❌ Error: %s" ErrorTemplate string // Prefixes is a list of command prefixes (e.g., ["/", "!"]). // Defaults to ["/"] if not set via environment. Prefixes []string // LoggerBasePath is the directory where log files are written. // Defaults to "./". LoggerBasePath string // UseRequestLogger enables detailed logging of all Telegram API requests. UseRequestLogger bool // WriteToFile enables writing logs to files (main.log and requests.log). WriteToFile bool // UseTestServer uses Telegram's test server (https://api.test.telegram.org). UseTestServer bool // APIURL overrides the default Telegram API endpoint (useful for proxies or self-hosted). APIURL string // RateLimit is the maximum number of API requests per second. // Telegram allows up to 30 req/s for most bots. Defaults to 30. RateLimit int // DropRateLimitOverflow rejects outgoing Telegram API requests immediately when // rate-limit capacity is unavailable instead of waiting for capacity. DropRateLimitOverflow bool // StrictPayloadType disables callback payload fallback decoding. // When enabled, the bot accepts only the configured default payload type. StrictPayloadType bool // MaxWorkers is the maximum number of update handlers that may run concurrently. MaxWorkers int // PollTimeout is the long-polling timeout in seconds for getUpdates. // Defaults to 30. Telegram allows 0..50; values outside that range are accepted // by the bot but rejected by Telegram at runtime. PollTimeout int // FileConfigVersion stores the version declared by the config file used to // load these options. // // It is zero when the options were not loaded from a versioned file. FileConfigVersion int // LogFormat selects text or JSON output for bot-managed loggers. LogFormat utils.LogFormat // LogFormatter customizes bot-managed logger writers when supported. LogFormatter *sneklog.Formatter } // LoadOptsFromEnv loads BotOpts from environment variables. // // Environment variables: // - TG_TOKEN: Bot token (required) // - UPDATE_TYPES: semicolon-separated update types (e.g., "message;callback_query") // - DEBUG: "true" to enable debug logging // - ERROR_TEMPLATE: format string for error messages (e.g., "❌ %s") // - PREFIXES: semicolon-separated prefixes (e.g., "/;!bot") // - LOGGER_BASE_PATH: directory for log files (default: "./") // - USE_REQ_LOG: "true" to enable request logging // - WRITE_TO_FILE: "true" to write logs to files // - USE_TEST_SERVER: "true" to use Telegram test server // - API_URL: custom API endpoint // - RATE_LIMIT: max requests per second (default: 30) // - DROP_RL_OVERFLOW: "true" to reject rate-limited API requests instead of waiting // - STRICT_PAYLOAD_TYPE: "true" to reject callback payloads encoded in a different format // - MAX_WORKERS: maximum number of concurrent update handlers (default: 32) // - POLL_TIMEOUT: long-polling timeout in seconds for getUpdates (default: 30) // - LOG_FORMAT: logger output format, "text" or "json" (default: "text") // // Returns a populated BotOpts. // NewBot validates required fields and returns ErrTokenRequired when TG_TOKEN is missing. func LoadOptsFromEnv() *BotOpts { rateLimit := 30 maxWorkers := 32 pollTimeout := 30 stringUpdateTypes := splitEnvList(os.Getenv("UPDATE_TYPES")) updateTypes := make([]tgapi.UpdateType, 0, len(stringUpdateTypes)) for _, updateType := range stringUpdateTypes { updateTypes = append(updateTypes, tgapi.UpdateType(updateType)) } if rl := os.Getenv("RATE_LIMIT"); rl != "" { if n, err := strconv.Atoi(rl); err == nil { rateLimit = n } } if mw := os.Getenv("MAX_WORKERS"); mw != "" { if n, err := strconv.Atoi(mw); err == nil { maxWorkers = n } } if pt := os.Getenv("POLL_TIMEOUT"); pt != "" { if n, err := strconv.Atoi(pt); err == nil { pollTimeout = n } } return &BotOpts{ Token: os.Getenv("TG_TOKEN"), UpdateTypes: updateTypes, Debug: os.Getenv("DEBUG") == "true", ErrorTemplate: os.Getenv("ERROR_TEMPLATE"), Prefixes: LoadPrefixesFromEnv(), LoggerBasePath: os.Getenv("LOGGER_BASE_PATH"), UseRequestLogger: os.Getenv("USE_REQ_LOG") == "true", WriteToFile: os.Getenv("WRITE_TO_FILE") == "true", UseTestServer: os.Getenv("USE_TEST_SERVER") == "true", APIURL: os.Getenv("API_URL"), RateLimit: rateLimit, DropRateLimitOverflow: os.Getenv("DROP_RL_OVERFLOW") == "true", StrictPayloadType: os.Getenv("STRICT_PAYLOAD_TYPE") == "true", MaxWorkers: maxWorkers, PollTimeout: pollTimeout, FileConfigVersion: 0, LogFormat: utils.LogFormat(os.Getenv("LOG_FORMAT")), } } // SetToken sets the Telegram bot token (required). func (opts *BotOpts) SetToken(token string) *BotOpts { opts.Token = token return opts } // SetUpdateTypes sets the list of update types to listen for. // If empty (default), Telegram will return all update types. // Example: opts.SetUpdateTypes("message", "callback_query"). func (opts *BotOpts) SetUpdateTypes(types ...tgapi.UpdateType) *BotOpts { opts.UpdateTypes = types return opts } // SetDebug enables or disables debug-level logging. // Default is false. func (opts *BotOpts) SetDebug(debug bool) *BotOpts { opts.Debug = debug return opts } // SetErrorTemplate sets the format string for error messages sent to users. // Use "%s" to insert the actual error. Example: "❌ Error: %s" // If not set, defaults to "%s". func (opts *BotOpts) SetErrorTemplate(tpl string) *BotOpts { opts.ErrorTemplate = tpl return opts } // SetPrefixes sets the command prefixes (e.g., "/", "!"). // If not set via environment, defaults to ["/"]. func (opts *BotOpts) SetPrefixes(prefixes ...string) *BotOpts { opts.Prefixes = prefixes return opts } // SetLoggerBasePath sets the directory where log files are written. // Defaults to "./". func (opts *BotOpts) SetLoggerBasePath(path string) *BotOpts { opts.LoggerBasePath = path return opts } // SetUseRequestLogger enables detailed logging of all Telegram API requests. // Default is false. func (opts *BotOpts) SetUseRequestLogger(use bool) *BotOpts { opts.UseRequestLogger = use return opts } // SetWriteToFile enables writing logs to files (main.log and requests.log). // Default is false. func (opts *BotOpts) SetWriteToFile(write bool) *BotOpts { opts.WriteToFile = write return opts } // SetUseTestServer enables using Telegram's test server (https://api.telegram.org/bot/test). // Default is false. func (opts *BotOpts) SetUseTestServer(use bool) *BotOpts { opts.UseTestServer = use return opts } // SetAPIURL overrides the default Telegram API endpoint (useful for proxies or self-hosted). // If not set, defaults to "https://api.telegram.org". func (opts *BotOpts) SetAPIURL(url string) *BotOpts { opts.APIURL = url return opts } // SetRateLimit sets the maximum number of API requests per second. // Telegram allows up to 30 req/s for most bots. Defaults to 30. func (opts *BotOpts) SetRateLimit(limit int) *BotOpts { opts.RateLimit = limit return opts } // SetDropRateLimitOverflow configures outgoing Telegram API requests to fail // immediately when rate-limit capacity is unavailable. Default is false. func (opts *BotOpts) SetDropRateLimitOverflow(drop bool) *BotOpts { opts.DropRateLimitOverflow = drop return opts } // SetStrictPayloadType enables or disables strict callback payload decoding. // When enabled, the bot accepts only the configured default payload type. func (opts *BotOpts) SetStrictPayloadType(strict bool) *BotOpts { opts.StrictPayloadType = strict return opts } // SetMaxWorkers sets the maximum number of concurrent update handlers. // Must be called before NewBot, as the value is captured during bot creation. // // The optimal value depends on your bot's workload: // - For I/O-bound handlers (e.g., database queries, external API calls), you may // need more workers, but be mindful of downstream service limits. // - For CPU-bound handlers, keep workers close to the number of CPU cores. // // Recommended starting points (adjust based on profiling and monitoring): // - Small to medium bots with fast handlers: 16–32 // - Medium to large bots with fast handlers: 32–64 // - Large bots with heavy I/O: 64–128 (ensure your infrastructure can handle it) // // The default is 32. Monitor queue length and processing latency to fine-tune. func (opts *BotOpts) SetMaxWorkers(workers int) *BotOpts { opts.MaxWorkers = workers return opts } // SetPollTimeout sets the long-polling timeout in seconds for getUpdates. // Defaults to 30. Telegram accepts 0..50. func (opts *BotOpts) SetPollTimeout(seconds int) *BotOpts { opts.PollTimeout = seconds return opts } // SetLogFormat sets the output format used by bot-managed loggers. func (opts *BotOpts) SetLogFormat(format utils.LogFormat) *BotOpts { opts.LogFormat = format return opts } // SetLogFormatter sets the formatter used by bot-managed logger writers. func (opts *BotOpts) SetLogFormatter(formatter *sneklog.Formatter) *BotOpts { opts.LogFormatter = formatter return opts } // LoadPrefixesFromEnv returns the PREFIXES environment variable split by semicolon. // Defaults to ["/"] if not set. func LoadPrefixesFromEnv() []string { prefixesS, exists := os.LookupEnv("PREFIXES") if !exists { return []string{"/"} } prefixes := splitEnvList(prefixesS) if len(prefixes) == 0 { return []string{"/"} } return prefixes } func splitEnvList(value string) []string { if value == "" { return nil } parts := strings.Split(value, ";") out := make([]string, 0, len(parts)) for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } out = append(out, part) } return out }