Improve config parsing and intialization

This commit is contained in:
Jon Staab
2025-09-29 13:37:57 -07:00
parent 001771cb6c
commit 54a55da390
8 changed files with 166 additions and 146 deletions
+4 -3
View File
@@ -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.
+12 -3
View File
@@ -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
+1 -12
View File
@@ -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{
+12 -4
View File
@@ -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 {
+2 -1
View File
@@ -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)
-17
View File
@@ -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)
}
+13 -106
View File
@@ -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
+122
View File
@@ -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)
}
}
}