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