Federated via graft
0

Configure Feed

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

graft-source / cmd / sync / main.go
3.9 kB 164 lines
1// Command sync runs the Forgejo <-> Radicle sync daemon: on an interval, it 2// mirrors git content, issues and patches for every repo pair listed in the 3// config file, then sleeps until the next pass. 4package main 5 6import ( 7 "flag" 8 "log/slog" 9 "net/http" 10 "net/url" 11 "os" 12 "strings" 13 "time" 14 15 "graft/internal/activitypub" 16 "graft/internal/config" 17 "graft/internal/state" 18 "graft/internal/status" 19 "graft/internal/sync" 20) 21 22func main() { 23 configPath := flag.String("config", "/etc/graft/config.yaml", "path to config.yaml") 24 once := flag.Bool("once", false, "run a single pass and exit, instead of looping") 25 listen := flag.String("listen", ":8090", "address to serve /healthz and / (status) on; empty disables it") 26 flag.Parse() 27 28 log := slog.New(slog.NewJSONHandler(os.Stdout, nil)) 29 30 cfg, err := config.Load(*configPath) 31 if err != nil { 32 log.Error("load config", "err", err) 33 os.Exit(1) 34 } 35 36 st, err := state.Open(cfg.StateDB) 37 if err != nil { 38 log.Error("open state db", "err", err) 39 os.Exit(1) 40 } 41 defer st.Close() 42 43 stateDir := cfg.StateDB 44 if idx := lastSlash(stateDir); idx >= 0 { 45 stateDir = stateDir[:idx] 46 } 47 48 syncers := make([]*sync.RepoSyncer, 0, len(cfg.Repos)) 49 for _, pair := range cfg.Repos { 50 rs, err := sync.New(pair, st, sync.WorkDirFor(stateDir, pair.Name)) 51 if err != nil { 52 log.Error("set up repo pair", "pair", pair.Name, "err", err) 53 os.Exit(1) 54 } 55 syncers = append(syncers, rs) 56 } 57 58 topology := buildTopology(cfg) 59 60 tracker := status.NewTracker(st, cfg.SourceURL) 61 tracker.SetTopology(topology) 62 tracker.SetPublicHost(cfg.PublicHost) 63 64 var apHandler *activitypub.Handler 65 if cfg.PublicHost != "" { 66 apHandler = activitypub.NewHandler(st, cfg.PublicHost, log, 67 func(series string) string { 68 if refs := topology[series]; len(refs) > 0 { 69 return refs[0].URL 70 } 71 return "" 72 }, 73 func(series string) bool { 74 _, ok := topology[series] 75 return ok 76 }, 77 ) 78 } 79 80 runAll := func() { 81 for _, rs := range syncers { 82 gitErr, issuesErr, patchErr := rs.Run(log) 83 tracker.Record(rs.Name(), rs.Series(), gitErr, issuesErr, patchErr) 84 } 85 if apHandler != nil { 86 apHandler.DeliverNewActivity() 87 } 88 } 89 90 if *listen != "" { 91 mux := http.NewServeMux() 92 if apHandler != nil { 93 apHandler.Register(mux) 94 } 95 mux.Handle("/", tracker.Handler()) 96 97 go func() { 98 if err := http.ListenAndServe(*listen, mux); err != nil { 99 log.Error("status server stopped", "err", err) 100 } 101 }() 102 } 103 104 runAll() 105 if *once { 106 return 107 } 108 109 ticker := time.NewTicker(cfg.SyncInterval) 110 defer ticker.Stop() 111 for range ticker.C { 112 runAll() 113 } 114} 115 116// buildTopology declares every mirror target configured for each series 117// (its Forgejo side and its Radicle side), so the dashboard can show a 118// side that's never produced an event yet — not just sides the activity 119// log happens to mention. 120func buildTopology(cfg *config.Config) map[string][]status.ServerRef { 121 topology := map[string][]status.ServerRef{} 122 seen := map[string]bool{} 123 add := func(series, host, repoURL string) { 124 if host == "" { 125 return 126 } 127 key := series + "|" + host 128 if seen[key] { 129 return 130 } 131 seen[key] = true 132 topology[series] = append(topology[series], status.ServerRef{Label: host, URL: repoURL}) 133 } 134 135 for _, pair := range cfg.Repos { 136 series := pair.Series 137 if series == "" { 138 series = pair.Name 139 } 140 141 fHost := "" 142 if u, err := url.Parse(pair.Forgejo.BaseURL); err == nil { 143 fHost = u.Host 144 } 145 fURL := strings.TrimRight(pair.Forgejo.BaseURL, "/") + "/" + pair.Forgejo.Owner + "/" + pair.Forgejo.Repo 146 add(series, fHost, fURL) 147 148 rHost := "" 149 if u, err := url.Parse(pair.Radicle.HTTPBaseURL); err == nil { 150 rHost = u.Host 151 } 152 add(series, rHost, sync.RadicleExplorerLink(pair.Radicle)) 153 } 154 return topology 155} 156 157func lastSlash(s string) int { 158 for i := len(s) - 1; i >= 0; i-- { 159 if s[i] == '/' { 160 return i 161 } 162 } 163 return -1 164}