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