From 54a55da3907628698c449a17b1ef1f8e898776e4 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Mon, 29 Sep 2025 13:37:57 -0700 Subject: [PATCH] Improve config parsing and intialization --- README.md | 7 +-- cmd/relay/main.go | 15 ++++-- zooid/blossom.go | 13 +---- zooid/config.go | 16 ++++-- zooid/env.go | 3 +- zooid/http.go | 17 ------- zooid/instance.go | 119 +++++--------------------------------------- zooid/lib.go | 122 ++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 166 insertions(+), 146 deletions(-) delete mode 100644 zooid/http.go create mode 100644 zooid/lib.go diff --git a/README.md b/README.md index a3a9713..235af2e 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,13 @@ A single zooid instance can run any number of "virtual" relays. The `config` dir Zooid supports a few environment variables, which configure shared resources like the web server or sqlite database. - `PORT` - the port the server will listen on for all requests. Defaults to `3334`. -- `DATA` - the location of the directory for storing database files and media. Defaults to `./data`. -- `CONF` - the location of the directory for storing relay configuration files. Defaults to `./conf`. +- `CONFIG` - where to store relay configuration files. Defaults to `./config`. +- `MEDIA` - where to store blossom media files. Defaults to `./media`. +- `DATA` - where to store databse files. Defaults to `./data`. ## Configuration -Configuration files are written using [toml](https://toml.io) files placed in the `CONF` directory. Top level configuration options are required: +Configuration files are written using [toml](https://toml.io). Top level configuration options are required: - `host` - a hostname to serve this relay on. - `schema` - a string that identifies this relay. This cannot be changed, and must be usable as a sqlite identifier. diff --git a/cmd/relay/main.go b/cmd/relay/main.go index b35fed3..0ed3bbc 100644 --- a/cmd/relay/main.go +++ b/cmd/relay/main.go @@ -20,8 +20,17 @@ func main() { port := zooid.Env("PORT") srv := &http.Server{ - Addr: fmt.Sprintf(":%s", port), - Handler: http.HandlerFunc(zooid.ServeHTTP), + Addr: fmt.Sprintf(":%s", port), + Handler: http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + instance, exists := zooid.Dispatch(r.Host) + if exists { + instance.Relay.ServeHTTP(w, r) + } else { + http.Error(w, "Not Found", http.StatusNotFound) + } + }, + ), } go func() { @@ -31,7 +40,7 @@ func main() { } }() - go zooid.MonitorInstances() + go zooid.Start() <-shutdown diff --git a/zooid/blossom.go b/zooid/blossom.go index 1712091..449a565 100644 --- a/zooid/blossom.go +++ b/zooid/blossom.go @@ -5,7 +5,6 @@ import ( "context" "io" "net/url" - "os" "fiatjaf.com/nostr" "fiatjaf.com/nostr/eventstore" @@ -18,19 +17,9 @@ type BlossomStore struct { Events eventstore.Store } -func (bl *BlossomStore) Init() error { - dir := Env("DATA") + "/media" - - if err := os.MkdirAll(dir, 0755); err != nil { - return err - } - - return nil -} - func (bl *BlossomStore) Enable(instance *Instance) { + dir := Env("MEDIA") fs := afero.NewOsFs() - dir := Env("DATA") + "/media" backend := blossom.New(instance.Relay, "https://"+bl.Config.Host) backend.Store = blossom.EventStoreBlobIndexWrapper{ diff --git a/zooid/config.go b/zooid/config.go index 4fbe649..9e9c49e 100644 --- a/zooid/config.go +++ b/zooid/config.go @@ -51,12 +51,20 @@ type Config struct { secret nostr.SecretKey } -func LoadConfig(hostname string) (*Config, error) { - path := filepath.Join("config", hostname) +func LoadConfig(filename string) (*Config, error) { + path := filepath.Join(Env("CONFIG"), filename) var config Config if _, err := toml.DecodeFile(path, &config); err != nil { - return nil, fmt.Errorf("failed to parse config file %s: %w", path, err) + return nil, fmt.Errorf("Failed to parse config file %s: %w", path, err) + } + + if config.Host == "" { + return nil, fmt.Errorf("host is required") + } + + if config.Schema == "" { + return nil, fmt.Errorf("schema is required") } secret, err := nostr.SecretKeyFromHex(config.Secret) @@ -84,7 +92,7 @@ func (config *Config) Sign(event *nostr.Event) error { } func (config *Config) IsOwner(pubkey nostr.PubKey) bool { - return pubkey == nostr.MustPubKeyFromHex(config.Info.Pubkey) + return pubkey.Hex() == config.Info.Pubkey } func (config *Config) GetRolesForPubkey(pubkey nostr.PubKey) []Role { diff --git a/zooid/env.go b/zooid/env.go index b109c2e..f145e31 100644 --- a/zooid/env.go +++ b/zooid/env.go @@ -17,7 +17,8 @@ func Env(k string, fallback ...string) (v string) { env["PORT"] = "3334" env["DATA"] = "./data" - env["CONF"] = "./conf" + env["MEDIA"] = "./media" + env["CONFIG"] = "./config" for _, item := range os.Environ() { parts := strings.SplitN(item, "=", 2) diff --git a/zooid/http.go b/zooid/http.go deleted file mode 100644 index e5d66e6..0000000 --- a/zooid/http.go +++ /dev/null @@ -1,17 +0,0 @@ -package zooid - -import ( - "log" - "net/http" -) - -func ServeHTTP(w http.ResponseWriter, r *http.Request) { - instance, err := GetInstance(r.Host) - if err != nil { - log.Printf("Failed to load config for hostname %s: %v", r.Host, err) - http.Error(w, "Not Found", http.StatusNotFound) - return - } - - instance.Relay.ServeHTTP(w, r) -} diff --git a/zooid/instance.go b/zooid/instance.go index a9a5f1a..866fe85 100644 --- a/zooid/instance.go +++ b/zooid/instance.go @@ -5,102 +5,16 @@ import ( "iter" "log" "net/http" - "os" - "path/filepath" "slices" "strings" - "sync" "fiatjaf.com/nostr" "fiatjaf.com/nostr/eventstore" "fiatjaf.com/nostr/khatru" "fiatjaf.com/nostr/nip29" - "github.com/fsnotify/fsnotify" "github.com/gosimple/slug" ) -// Top level instance creation/destruction - -var ( - instances map[string]*Instance - instancesOnce sync.Once - instancesMux sync.RWMutex -) - -func GetInstance(hostname string) (*Instance, error) { - instancesMux.RLock() - defer instancesMux.RUnlock() - - instancesOnce.Do(func() { - instances = make(map[string]*Instance) - }) - - instance, exists := instances[hostname] - - if !exists { - newInstance, err := MakeInstance(hostname) - if err != nil { - return nil, err - } - - instances[hostname] = newInstance - instance = newInstance - } - - return instance, nil -} - -func MonitorInstances() { - dir := Env("CONF") - - if err := os.MkdirAll(dir, 0755); err != nil { - log.Fatal("Failed to create config directory: %v", err) - } - - watcher, err := fsnotify.NewWatcher() - if err != nil { - log.Printf("Failed to create file watcher: %v", err) - return - } - defer watcher.Close() - - if err := watcher.Add(dir); err != nil { - log.Printf("Failed to watch config directory: %v", err) - return - } - - log.Printf("Watching config directory for changes") - - for { - select { - case event, ok := <-watcher.Events: - if !ok { - return - } - - hostname := filepath.Base(event.Name) - - if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) || event.Has(fsnotify.Remove) { - log.Printf("Config file changed/deleted: %s", event.Name) - - instancesMux.Lock() - if instance, exists := instances[hostname]; exists { - instance.Cleanup() - } - instancesMux.Unlock() - } - - case err, ok := <-watcher.Errors: - if !ok { - return - } - log.Printf("File watcher error: %v", err) - } - } -} - -// Instance struct - type Instance struct { Config *Config Events eventstore.Store @@ -109,13 +23,8 @@ type Instance struct { Relay *khatru.Relay } -func MakeInstance(hostname string) (*Instance, error) { - config, err := LoadConfig(hostname) - if err != nil { - return nil, err - } - - pubkey, err := nostr.PubKeyFromHex(config.Info.Pubkey) +func MakeInstance(filename string) (*Instance, error) { + config, err := LoadConfig(filename) if err != nil { return nil, err } @@ -148,12 +57,20 @@ func MakeInstance(hostname string) (*Instance, error) { instance.Relay.Negentropy = true instance.Relay.Info.Name = config.Info.Name instance.Relay.Info.Icon = config.Info.Icon - instance.Relay.Info.PubKey = &pubkey instance.Relay.Info.Description = config.Info.Description // instance.Relay.Info.Self = nostr.GetPublicKey(secret) instance.Relay.Info.Software = "https://github.com/coracle-social/zooid" instance.Relay.Info.Version = "v0.1.0" + if config.Info.Pubkey != "" { + pubkey, err := nostr.PubKeyFromHex(config.Info.Pubkey) + if err != nil { + return nil, err + } + + instance.Relay.Info.PubKey = &pubkey + } + instance.Relay.UseEventstore(instance.Events, 400) instance.Relay.OnConnect = instance.OnConnect @@ -171,11 +88,7 @@ func MakeInstance(hostname string) (*Instance, error) { // Initialize stuff if err := instance.Events.Init(); err != nil { - log.Fatal("Failed to initialize event store:", err) - } - - if err := instance.Blossom.Init(); err != nil { - log.Fatal("Failed to initialize blossom store:", err) + log.Fatal("Failed to initialize event store: ", err) } if config.Blossom.Enabled { @@ -189,14 +102,8 @@ func MakeInstance(hostname string) (*Instance, error) { return instance, nil } -func (instance *Instance) Cleanup() bool { - // Close the event store +func (instance *Instance) Cleanup() { instance.Events.Close() - - // Remove from instances map - delete(instances, instance.Config.Host) - - return true } // Utility methods diff --git a/zooid/lib.go b/zooid/lib.go new file mode 100644 index 0000000..eed3feb --- /dev/null +++ b/zooid/lib.go @@ -0,0 +1,122 @@ +package zooid + +import ( + "log" + "os" + "path/filepath" + "sync" + + "github.com/fsnotify/fsnotify" +) + +var ( + instancesByHost map[string]*Instance + instancesByName map[string]*Instance + instancesOnce sync.Once + instancesMux sync.RWMutex +) + +func Dispatch(hostname string) (*Instance, bool) { + instancesMux.RLock() + defer instancesMux.RUnlock() + + instance, exists := instancesByHost[hostname] + + return instance, exists +} + +func Start() { + dataDir := Env("DATA") + if err := os.MkdirAll(dataDir, 0755); err != nil { + log.Fatal("Failed to create data directory: %v", err) + } + + mediaDir := Env("MEDIA") + if err := os.MkdirAll(mediaDir, 0755); err != nil { + log.Fatal("Failed to create media directory: %v", err) + } + + configDir := Env("CONFIG") + if err := os.MkdirAll(configDir, 0755); err != nil { + log.Fatal("Failed to create config directory: %v", err) + } + + instancesOnce.Do(func() { + instancesByHost = make(map[string]*Instance) + instancesByName = make(map[string]*Instance) + }) + + entries, err := os.ReadDir(configDir) + if err != nil { + log.Fatal("Failed to scan config directory: %v", err) + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + instance, err := MakeInstance(entry.Name()) + + if err != nil { + log.Printf("Failed to make instance for %s: %v", entry.Name(), err) + } else { + instancesByHost[instance.Config.Host] = instance + instancesByName[entry.Name()] = instance + log.Printf("Loaded %s", entry.Name()) + } + } + + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Printf("Failed to create file watcher: %v", err) + return + } + + defer watcher.Close() + + if err := watcher.Add(configDir); err != nil { + log.Printf("Failed to watch config directory: %v", err) + return + } + + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + + filename := filepath.Base(event.Name) + + if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) || event.Has(fsnotify.Remove) { + instancesMux.Lock() + + if instance, exists := instancesByName[filename]; exists { + instance.Cleanup() + + delete(instancesByHost, instance.Config.Host) + delete(instancesByName, filename) + } + + instance, err := MakeInstance(filename) + if err != nil { + log.Printf("Failed to reload %s: %v", filename, err) + } else { + instancesByHost[instance.Config.Host] = instance + instancesByName[filename] = instance + log.Printf("Reloaded %v", filename) + } + + instancesMux.Unlock() + } + + case err, ok := <-watcher.Errors: + if !ok { + return + } + + log.Printf("File watcher error: %v", err) + } + } +}