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