package config import ( "fmt" "github.com/pkg/errors" "io/ioutil" "github.com/santhosh-tekuri/jsonschema" "gopkg.in/yaml.v2" ) // Config is for top-level struct for config type Config struct { Telegram TelegramConfig `yaml:":telegram"` XMPP XMPPConfig `yaml:":xmpp"` } // XMPPConfig is for :xmpp: subtree type XMPPConfig struct { Loglevel string `yaml:":loglevel"` Jid string `yaml:":jid"` Host string `yaml:":host"` Port string `yaml:":port"` Password string `yaml:":password"` Db string `yaml:":db"` } // TelegramConfig is for :telegram: subtree type TelegramConfig struct { Loglevel string `yaml:":loglevel"` Content TelegramContentConfig `yaml:":content"` Verbosity uint8 `yaml:":tdlib_verbosity"` Tdlib TelegramTdlibConfig `yaml:":tdlib"` } // TelegramContentConfig is for :content: subtree type TelegramContentConfig struct { Path string `yaml:":path"` Link string `yaml:":link"` Upload string `yaml:":upload"` User string `yaml:":user"` } // TelegramTdlibConfig is for :tdlib: subtree type TelegramTdlibConfig struct { Datadir string `yaml:":datadir"` Client TelegramTdlibClientConfig `yaml:":client"` } // TelegramTdlibClientConfig is for :client: subtree type TelegramTdlibClientConfig struct { APIID string `yaml:":api_id"` APIHash string `yaml:":api_hash"` DeviceModel string `yaml:":device_model"` ApplicationVersion string `yaml:":application_version"` UseChatInfoDatabase bool `yaml:":use_chat_info_database"` UseSecretChats bool `yaml:":use_secret_chats"` CatchTimeout int64 `yaml:":catch_timeout"` } // ReadConfig reads the specified config file, validates it and returns a struct func ReadConfig(path string, schemaPath string) (Config, error) { var config Config file, err := ioutil.ReadFile(path) if err != nil { return config, errors.Wrap(err, "Can't open config file") } err = yaml.Unmarshal(file, &config) if err != nil { return config, errors.Wrap(err, "Error parsing config") } err = validateConfig(file, schemaPath) if err != nil { return config, errors.Wrap(err, "Validation error") } return config, nil } func validateConfig(file []byte, schemaPath string) error { schema, err := jsonschema.Compile(schemaPath) if err != nil { return errors.Wrap(err, "Corrupted JSON schema") } var configGeneric interface{} err = yaml.Unmarshal(file, &configGeneric) if err != nil { return errors.Wrap(err, "Error re-parsing config") } configGeneric, err = convertToStringKeysRecursive(configGeneric, "") if err != nil { return errors.Wrap(err, "Config conversion error") } err = schema.ValidateInterface(configGeneric) if err != nil { return errors.Wrap(err, "Config validation error") } return nil } // copied and adapted from https://github.com/docker/docker-ce/blob/de14285fad39e215ea9763b8b404a37686811b3f/components/cli/cli/compose/loader/loader.go#L330 func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) { if mapping, ok := value.(map[interface{}]interface{}); ok { dict := make(map[string]interface{}) for key, entry := range mapping { str, ok := key.(string) if !ok { return nil, formatInvalidKeyError(keyPrefix, key) } var newKeyPrefix string if keyPrefix == "" { newKeyPrefix = str } else { newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str) } convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) if err != nil { return nil, err } dict[str] = convertedEntry } return dict, nil } if list, ok := value.([]interface{}); ok { var convertedList []interface{} for index, entry := range list { newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index) convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) if err != nil { return nil, err } convertedList = append(convertedList, convertedEntry) } return convertedList, nil } return value, nil } func formatInvalidKeyError(keyPrefix string, key interface{}) error { var location string if keyPrefix == "" { location = "at top level" } else { location = fmt.Sprintf("in %s", keyPrefix) } return errors.Errorf("Non-string key %s: %#v", location, key) }