Add Arrtrix runtime, config, onboarding, and webhook support
- Implement runtime package for bridge startup, config loading, and env overrides - Add onboarding package for management room welcome messages - Add matrixcmd package for command processing and help - Add webhook package with Radarr webhook support and validation - Extend connector config for webhooks and validation - Update default config and example config for new options - Add tests for new packages and config validation - Change database type default to sqlite3-fk-wal
This commit is contained in:
parent
eeedb5268a
commit
fe627f3aab
19 changed files with 1855 additions and 35 deletions
206
packages/arrtrix/pkg/runtime/envconfig.go
Normal file
206
packages/arrtrix/pkg/runtime/envconfig.go
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
package runtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const fileEnvPrefix = "READFILE:"
|
||||
|
||||
func updateConfigFromEnv(cfg, networkData any, prefix string) error {
|
||||
if prefix == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
cfgVal := reflect.ValueOf(cfg)
|
||||
networkVal := reflect.ValueOf(networkData)
|
||||
|
||||
for _, env := range os.Environ() {
|
||||
if !strings.HasPrefix(env, prefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
keyValue := strings.SplitN(env, "=", 2)
|
||||
if len(keyValue) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimPrefix(keyValue[0], prefix)
|
||||
value := keyValue[1]
|
||||
if strings.HasSuffix(key, "_FILE") {
|
||||
key = strings.TrimSuffix(key, "_FILE")
|
||||
value = fileEnvPrefix + value
|
||||
}
|
||||
|
||||
key = strings.ToLower(key)
|
||||
if !strings.ContainsRune(key, '.') {
|
||||
key = strings.ReplaceAll(key, "__", ".")
|
||||
}
|
||||
|
||||
path := strings.Split(key, ".")
|
||||
field, ok := reflectGetFromMainOrNetwork(cfgVal, networkVal, path)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s not found", formatKey(path))
|
||||
}
|
||||
|
||||
if strings.HasPrefix(value, fileEnvPrefix) {
|
||||
filePath := strings.TrimPrefix(value, fileEnvPrefix)
|
||||
fileData, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file %s for %s: %w", filePath, formatKey(path), err)
|
||||
}
|
||||
value = strings.TrimSpace(string(fileData))
|
||||
}
|
||||
|
||||
if err := setReflectedValue(field, path, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type reflectedField struct {
|
||||
value reflect.Value
|
||||
valueKind reflect.Kind
|
||||
remainingPath []string
|
||||
}
|
||||
|
||||
func formatKey(path []string) string {
|
||||
return strings.Join(path, "->")
|
||||
}
|
||||
|
||||
func reflectGetFromMainOrNetwork(main, network reflect.Value, path []string) (*reflectedField, bool) {
|
||||
if len(path) > 0 && path[0] == "network" {
|
||||
return reflectGetYAML(network, path[1:])
|
||||
}
|
||||
return reflectGetYAML(main, path)
|
||||
}
|
||||
|
||||
func reflectGetYAML(value reflect.Value, path []string) (*reflectedField, bool) {
|
||||
if len(path) == 0 {
|
||||
return &reflectedField{value: value, valueKind: value.Kind()}, true
|
||||
}
|
||||
if value.Kind() == reflect.Ptr {
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
switch value.Kind() {
|
||||
case reflect.Map:
|
||||
return &reflectedField{
|
||||
value: value,
|
||||
valueKind: value.Type().Elem().Kind(),
|
||||
remainingPath: path,
|
||||
}, true
|
||||
case reflect.Struct:
|
||||
fields := reflect.VisibleFields(value.Type())
|
||||
for _, field := range fields {
|
||||
if yamlFieldName(field) != path[0] {
|
||||
continue
|
||||
}
|
||||
return reflectGetYAML(value.FieldByIndex(field.Index), path[1:])
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func yamlFieldName(field reflect.StructField) string {
|
||||
parts := strings.SplitN(field.Tag.Get("yaml"), ",", 2)
|
||||
switch name := parts[0]; {
|
||||
case name == "-" && len(parts) == 1:
|
||||
return ""
|
||||
case name == "":
|
||||
return strings.ToLower(field.Name)
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
func setReflectedValue(field *reflectedField, path []string, raw string) error {
|
||||
parsed, err := parseValue(field.valueKind, raw, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value := field.value
|
||||
if value.Kind() == reflect.Ptr {
|
||||
if value.IsNil() {
|
||||
value.Set(reflect.New(value.Type().Elem()))
|
||||
}
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
if value.Kind() == reflect.Map {
|
||||
if value.Type().Key().Kind() != reflect.String {
|
||||
return fmt.Errorf("unsupported map key type %s in %s", value.Type().Key().Kind(), formatKey(path))
|
||||
}
|
||||
key := strings.Join(field.remainingPath, ".")
|
||||
value.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(parsed))
|
||||
return nil
|
||||
}
|
||||
|
||||
value.Set(reflect.ValueOf(parsed))
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseValue(kind reflect.Kind, raw string, path []string) (any, error) {
|
||||
switch kind {
|
||||
case reflect.String:
|
||||
return raw, nil
|
||||
case reflect.Bool:
|
||||
parsed, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid value for %s: %w", formatKey(path), err)
|
||||
}
|
||||
return parsed, nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
parsed, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid value for %s: %w", formatKey(path), err)
|
||||
}
|
||||
switch kind {
|
||||
case reflect.Int8:
|
||||
return int8(parsed), nil
|
||||
case reflect.Int16:
|
||||
return int16(parsed), nil
|
||||
case reflect.Int32:
|
||||
return int32(parsed), nil
|
||||
case reflect.Int64:
|
||||
return parsed, nil
|
||||
default:
|
||||
return int(parsed), nil
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
parsed, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid value for %s: %w", formatKey(path), err)
|
||||
}
|
||||
switch kind {
|
||||
case reflect.Uint8:
|
||||
return uint8(parsed), nil
|
||||
case reflect.Uint16:
|
||||
return uint16(parsed), nil
|
||||
case reflect.Uint32:
|
||||
return uint32(parsed), nil
|
||||
case reflect.Uint64:
|
||||
return parsed, nil
|
||||
default:
|
||||
return uint(parsed), nil
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
parsed, err := strconv.ParseFloat(raw, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid value for %s: %w", formatKey(path), err)
|
||||
}
|
||||
if kind == reflect.Float32 {
|
||||
return float32(parsed), nil
|
||||
}
|
||||
return parsed, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type %s in %s", kind, formatKey(path))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue