Federated via graft
0

Configure Feed

Select the types of activity you want to include in your feed.

1// Package config loads and validates the sync daemon's configuration. 2package config 3 4import ( 5 "fmt" 6 "os" 7 "time" 8 9 "gopkg.in/yaml.v3" 10) 11 12// Config is the top-level configuration for the sync daemon. 13type Config struct { 14 SyncInterval time.Duration `yaml:"sync_interval"` 15 StateDB string `yaml:"state_db"` 16 Repos []RepoPair `yaml:"repos"` 17} 18 19// RepoPair links one Forgejo repository to one Radicle repository and 20// declares which content types are kept in sync between them. 21type RepoPair struct { 22 Name string `yaml:"name"` 23 Forgejo ForgejoTarget `yaml:"forgejo"` 24 Radicle RadicleTarget `yaml:"radicle"` 25 Sync SyncScope `yaml:"sync"` 26} 27 28// ForgejoTarget identifies a repository on a Forgejo instance and where to 29// read its access token from. The token is never stored inline in the 30// config file so the file itself can be handled as non-sensitive. 31type ForgejoTarget struct { 32 BaseURL string `yaml:"base_url"` 33 Owner string `yaml:"owner"` 34 Repo string `yaml:"repo"` 35 TokenFile string `yaml:"token_file"` 36} 37 38// RadicleTarget identifies a repository on a Radicle node. HTTPBaseURL is 39// that node's radicle-httpd endpoint (e.g. https://r1.cyberwild.org), used 40// for reads; RadHome is the local RAD_HOME whose keys the "rad" CLI uses 41// for writes — the two are only the same node when the daemon runs 42// alongside it, which is not assumed here. 43type RadicleTarget struct { 44 RID string `yaml:"rid"` 45 HTTPBaseURL string `yaml:"http_base_url"` 46 RadHome string `yaml:"rad_home"` 47} 48 49// SyncScope selects which content types are synchronized for a repo pair. 50type SyncScope struct { 51 Git bool `yaml:"git"` 52 Issues bool `yaml:"issues"` 53 Patches bool `yaml:"patches"` 54} 55 56// Load reads and validates the configuration file at path. 57func Load(path string) (*Config, error) { 58 data, err := os.ReadFile(path) 59 if err != nil { 60 return nil, fmt.Errorf("read config: %w", err) 61 } 62 63 var cfg Config 64 if err := yaml.Unmarshal(data, &cfg); err != nil { 65 return nil, fmt.Errorf("parse config: %w", err) 66 } 67 68 if cfg.SyncInterval <= 0 { 69 cfg.SyncInterval = 5 * time.Minute 70 } 71 if cfg.StateDB == "" { 72 return nil, fmt.Errorf("state_db must be set") 73 } 74 if len(cfg.Repos) == 0 { 75 return nil, fmt.Errorf("no repos configured") 76 } 77 for i, r := range cfg.Repos { 78 if err := r.validate(); err != nil { 79 return nil, fmt.Errorf("repos[%d] (%s): %w", i, r.Name, err) 80 } 81 } 82 83 return &cfg, nil 84} 85 86func (r RepoPair) validate() error { 87 if r.Name == "" { 88 return fmt.Errorf("name is required") 89 } 90 if r.Forgejo.BaseURL == "" || r.Forgejo.Owner == "" || r.Forgejo.Repo == "" { 91 return fmt.Errorf("forgejo.base_url, owner and repo are required") 92 } 93 if r.Forgejo.TokenFile == "" { 94 return fmt.Errorf("forgejo.token_file is required") 95 } 96 if r.Radicle.RID == "" { 97 return fmt.Errorf("radicle.rid is required") 98 } 99 if r.Radicle.HTTPBaseURL == "" { 100 return fmt.Errorf("radicle.http_base_url is required") 101 } 102 if r.Radicle.RadHome == "" { 103 return fmt.Errorf("radicle.rad_home is required") 104 } 105 if !r.Sync.Git && !r.Sync.Issues && !r.Sync.Patches { 106 return fmt.Errorf("at least one of sync.git, sync.issues, sync.patches must be true") 107 } 108 return nil 109} 110 111// ReadToken reads a token from a file, trimming surrounding whitespace. 112// Token files are expected to be chmod 600 and owned by the service user. 113func ReadToken(path string) (string, error) { 114 data, err := os.ReadFile(path) 115 if err != nil { 116 return "", fmt.Errorf("read token file %s: %w", path, err) 117 } 118 token := string(data) 119 for len(token) > 0 && (token[len(token)-1] == '\n' || token[len(token)-1] == '\r' || token[len(token)-1] == ' ') { 120 token = token[:len(token)-1] 121 } 122 if token == "" { 123 return "", fmt.Errorf("token file %s is empty", path) 124 } 125 return token, nil 126}