FILE / ScuroNeko/Laniakea
bot_opts_loader.go
Исходный файл и его история в репозитории.
(fix): harden concurrent lifecycle (tests): add regression coverage (doc): update v1.2 guidance
214 lines
6.4 KiB
Go
214 lines
6.4 KiB
Go
package laniakea
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
|
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
|
"git.scuroneko.dev/scuroneko/laniakea/utils"
|
|
)
|
|
|
|
// ConfigVersion is the current version of the built-in JSON BotOpts file format.
|
|
const ConfigVersion = 1
|
|
|
|
// ErrConfigVersionMismatch reports that a config file declares a newer version
|
|
// than this library knows how to decode.
|
|
var ErrConfigVersionMismatch = fmt.Errorf("config version mismatch: expected %d", ConfigVersion)
|
|
|
|
type botOptsFileJSONLogger struct {
|
|
LoggerBasePath string `json:"base_path"`
|
|
UseRequestLogger bool `json:"use_request_logger"`
|
|
WriteToFile bool `json:"write_to_file"`
|
|
LogFormat utils.LogFormat `json:"log_format"`
|
|
}
|
|
type botOptsFileJSONAPI struct {
|
|
UseTestServer bool `json:"use_test_server"`
|
|
APIURL string `json:"url"`
|
|
RateLimit int `json:"rate_limit"`
|
|
PollTimeout int `json:"poll_timeout"`
|
|
DropRLOverflow bool `json:"drop_overflow"`
|
|
}
|
|
|
|
// BotOptsFileJSON is the JSON file representation of BotOpts.
|
|
type BotOptsFileJSON struct {
|
|
// Version identifies the JSON configuration format version.
|
|
Version int `json:"version"`
|
|
// Token is the Telegram bot token.
|
|
Token string `json:"token"`
|
|
// UpdateTypes limits the update kinds requested from Telegram.
|
|
UpdateTypes []tgapi.UpdateType `json:"update_types"`
|
|
// Debug enables debug logging.
|
|
Debug bool `json:"debug"`
|
|
// ErrorTemplate formats user-facing handler errors.
|
|
ErrorTemplate string `json:"error_template"`
|
|
// Prefixes contains accepted command prefixes.
|
|
Prefixes []string `json:"prefixes"`
|
|
// Logger contains file logging options.
|
|
Logger botOptsFileJSONLogger `json:"logger"`
|
|
// API contains Telegram client and rate-limit options.
|
|
API botOptsFileJSONAPI `json:"api"`
|
|
// StrictPayloadType requires callback payloads to use the configured encoding.
|
|
StrictPayloadType bool `json:"strict_payload_type"`
|
|
// MaxWorkers limits concurrent update handlers.
|
|
MaxWorkers int `json:"max_workers"`
|
|
}
|
|
|
|
// BotOptsFileJSONCodec encodes and decodes BotOpts using BotOptsFileJSON.
|
|
type BotOptsFileJSONCodec struct{}
|
|
|
|
// FromBytes decodes BotOpts from JSON file bytes.
|
|
func (codec BotOptsFileJSONCodec) FromBytes(data []byte) (*BotOpts, error) {
|
|
fileOpts := new(BotOptsFileJSON)
|
|
err := json.Unmarshal(data, fileOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if fileOpts.Version > ConfigVersion {
|
|
return nil, ErrConfigVersionMismatch
|
|
}
|
|
opts := &BotOpts{
|
|
Token: fileOpts.Token,
|
|
UpdateTypes: fileOpts.UpdateTypes,
|
|
Debug: fileOpts.Debug,
|
|
ErrorTemplate: fileOpts.ErrorTemplate,
|
|
Prefixes: fileOpts.Prefixes,
|
|
|
|
LoggerBasePath: fileOpts.Logger.LoggerBasePath,
|
|
UseRequestLogger: fileOpts.Logger.UseRequestLogger,
|
|
WriteToFile: fileOpts.Logger.WriteToFile,
|
|
LogFormat: fileOpts.Logger.LogFormat,
|
|
|
|
UseTestServer: fileOpts.API.UseTestServer,
|
|
APIURL: fileOpts.API.APIURL,
|
|
RateLimit: fileOpts.API.RateLimit,
|
|
PollTimeout: fileOpts.API.PollTimeout,
|
|
DropRateLimitOverflow: fileOpts.API.DropRLOverflow,
|
|
|
|
StrictPayloadType: fileOpts.StrictPayloadType,
|
|
MaxWorkers: fileOpts.MaxWorkers,
|
|
|
|
FileConfigVersion: fileOpts.Version,
|
|
}
|
|
return opts, nil
|
|
}
|
|
|
|
// ToBytes encodes BotOpts into JSON file bytes.
|
|
func (codec BotOptsFileJSONCodec) ToBytes(opts *BotOpts) ([]byte, error) {
|
|
if opts == nil {
|
|
return nil, ErrOptsIsNil
|
|
}
|
|
fileOpts := &BotOptsFileJSON{
|
|
Version: ConfigVersion,
|
|
Token: opts.Token,
|
|
UpdateTypes: opts.UpdateTypes,
|
|
Debug: opts.Debug,
|
|
ErrorTemplate: opts.ErrorTemplate,
|
|
Prefixes: opts.Prefixes,
|
|
Logger: botOptsFileJSONLogger{
|
|
LoggerBasePath: opts.LoggerBasePath,
|
|
UseRequestLogger: opts.UseRequestLogger,
|
|
WriteToFile: opts.WriteToFile,
|
|
LogFormat: opts.LogFormat,
|
|
},
|
|
API: botOptsFileJSONAPI{
|
|
UseTestServer: opts.UseTestServer,
|
|
APIURL: opts.APIURL,
|
|
RateLimit: opts.RateLimit,
|
|
PollTimeout: opts.PollTimeout,
|
|
DropRLOverflow: opts.DropRateLimitOverflow,
|
|
},
|
|
StrictPayloadType: opts.StrictPayloadType,
|
|
MaxWorkers: opts.MaxWorkers,
|
|
}
|
|
data, err := json.Marshal(fileOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// Load reads BotOpts from a JSON config file.
|
|
func (codec BotOptsFileJSONCodec) Load(filename string) (*BotOpts, error) {
|
|
return LoadBotOptsFile(codec, filename)
|
|
}
|
|
|
|
// Save writes BotOpts to a JSON config file.
|
|
func (codec BotOptsFileJSONCodec) Save(filename string, opts *BotOpts) error {
|
|
return SaveBotOptsFile(codec, filename, opts)
|
|
}
|
|
|
|
// EscapeEnv escapes an environment value for use inside a JSON string.
|
|
func (codec BotOptsFileJSONCodec) EscapeEnv(s string) string {
|
|
data, _ := json.Marshal(s)
|
|
return string(data[1 : len(data)-1])
|
|
}
|
|
|
|
var envParameterRegex = regexp.MustCompile(`\{\{\s*(\w+)\s*}}`)
|
|
|
|
type botOptsFileEnvEscaper interface {
|
|
EscapeEnv(string) string
|
|
}
|
|
|
|
// BotOptsFileCodec decodes and encodes BotOpts file formats.
|
|
type BotOptsFileCodec interface {
|
|
FromBytes([]byte) (*BotOpts, error)
|
|
ToBytes(*BotOpts) ([]byte, error)
|
|
Load(filename string) (*BotOpts, error)
|
|
Save(filename string, opts *BotOpts) error
|
|
}
|
|
|
|
// LoadBotOptsFile reads a config file, expands env placeholders, and decodes BotOpts.
|
|
func LoadBotOptsFile(codec BotOptsFileCodec, filename string) (*BotOpts, error) {
|
|
if isNilValue(codec) {
|
|
return nil, ErrCodecIsNil
|
|
}
|
|
f, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
data, err := io.ReadAll(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data = expandEnvPlaceholdersInFile(codec, data)
|
|
return codec.FromBytes(data)
|
|
}
|
|
|
|
// SaveBotOptsFile encodes BotOpts with codec and writes the result to filename.
|
|
func SaveBotOptsFile(codec BotOptsFileCodec, filename string, opts *BotOpts) error {
|
|
if isNilValue(codec) {
|
|
return ErrCodecIsNil
|
|
}
|
|
if opts == nil {
|
|
return ErrOptsIsNil
|
|
}
|
|
data, err := codec.ToBytes(opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.WriteFile(filename, data, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func expandEnvPlaceholdersInFile(codec BotOptsFileCodec, data []byte) []byte {
|
|
return envParameterRegex.ReplaceAllFunc(data, func(match []byte) []byte {
|
|
group := envParameterRegex.FindSubmatch(match)
|
|
if len(group) != 2 {
|
|
return match
|
|
}
|
|
key := group[1]
|
|
value := os.Getenv(string(key))
|
|
if escaper, ok := codec.(botOptsFileEnvEscaper); ok {
|
|
value = escaper.EscapeEnv(value)
|
|
}
|
|
return []byte(value)
|
|
})
|
|
}
|