forked from coracle/zooid
Improve config parsing and intialization
This commit is contained in:
@@ -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.
|
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`.
|
- `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`.
|
- `CONFIG` - where to store relay configuration files. Defaults to `./config`.
|
||||||
- `CONF` - the location of the directory for storing relay configuration files. Defaults to `./conf`.
|
- `MEDIA` - where to store blossom media files. Defaults to `./media`.
|
||||||
|
- `DATA` - where to store databse files. Defaults to `./data`.
|
||||||
|
|
||||||
## Configuration
|
## 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.
|
- `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.
|
- `schema` - a string that identifies this relay. This cannot be changed, and must be usable as a sqlite identifier.
|
||||||
|
|||||||
+12
-3
@@ -20,8 +20,17 @@ func main() {
|
|||||||
|
|
||||||
port := zooid.Env("PORT")
|
port := zooid.Env("PORT")
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: fmt.Sprintf(":%s", port),
|
Addr: fmt.Sprintf(":%s", port),
|
||||||
Handler: http.HandlerFunc(zooid.ServeHTTP),
|
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() {
|
go func() {
|
||||||
@@ -31,7 +40,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go zooid.MonitorInstances()
|
go zooid.Start()
|
||||||
|
|
||||||
<-shutdown
|
<-shutdown
|
||||||
|
|
||||||
|
|||||||
+1
-12
@@ -5,7 +5,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/eventstore"
|
"fiatjaf.com/nostr/eventstore"
|
||||||
@@ -18,19 +17,9 @@ type BlossomStore struct {
|
|||||||
Events eventstore.Store
|
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) {
|
func (bl *BlossomStore) Enable(instance *Instance) {
|
||||||
|
dir := Env("MEDIA")
|
||||||
fs := afero.NewOsFs()
|
fs := afero.NewOsFs()
|
||||||
dir := Env("DATA") + "/media"
|
|
||||||
backend := blossom.New(instance.Relay, "https://"+bl.Config.Host)
|
backend := blossom.New(instance.Relay, "https://"+bl.Config.Host)
|
||||||
|
|
||||||
backend.Store = blossom.EventStoreBlobIndexWrapper{
|
backend.Store = blossom.EventStoreBlobIndexWrapper{
|
||||||
|
|||||||
+12
-4
@@ -51,12 +51,20 @@ type Config struct {
|
|||||||
secret nostr.SecretKey
|
secret nostr.SecretKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(hostname string) (*Config, error) {
|
func LoadConfig(filename string) (*Config, error) {
|
||||||
path := filepath.Join("config", hostname)
|
path := filepath.Join(Env("CONFIG"), filename)
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
if _, err := toml.DecodeFile(path, &config); err != nil {
|
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)
|
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 {
|
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 {
|
func (config *Config) GetRolesForPubkey(pubkey nostr.PubKey) []Role {
|
||||||
|
|||||||
+2
-1
@@ -17,7 +17,8 @@ func Env(k string, fallback ...string) (v string) {
|
|||||||
|
|
||||||
env["PORT"] = "3334"
|
env["PORT"] = "3334"
|
||||||
env["DATA"] = "./data"
|
env["DATA"] = "./data"
|
||||||
env["CONF"] = "./conf"
|
env["MEDIA"] = "./media"
|
||||||
|
env["CONFIG"] = "./config"
|
||||||
|
|
||||||
for _, item := range os.Environ() {
|
for _, item := range os.Environ() {
|
||||||
parts := strings.SplitN(item, "=", 2)
|
parts := strings.SplitN(item, "=", 2)
|
||||||
|
|||||||
@@ -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
@@ -5,102 +5,16 @@ import (
|
|||||||
"iter"
|
"iter"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/eventstore"
|
"fiatjaf.com/nostr/eventstore"
|
||||||
"fiatjaf.com/nostr/khatru"
|
"fiatjaf.com/nostr/khatru"
|
||||||
"fiatjaf.com/nostr/nip29"
|
"fiatjaf.com/nostr/nip29"
|
||||||
"github.com/fsnotify/fsnotify"
|
|
||||||
"github.com/gosimple/slug"
|
"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 {
|
type Instance struct {
|
||||||
Config *Config
|
Config *Config
|
||||||
Events eventstore.Store
|
Events eventstore.Store
|
||||||
@@ -109,13 +23,8 @@ type Instance struct {
|
|||||||
Relay *khatru.Relay
|
Relay *khatru.Relay
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeInstance(hostname string) (*Instance, error) {
|
func MakeInstance(filename string) (*Instance, error) {
|
||||||
config, err := LoadConfig(hostname)
|
config, err := LoadConfig(filename)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pubkey, err := nostr.PubKeyFromHex(config.Info.Pubkey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -148,12 +57,20 @@ func MakeInstance(hostname string) (*Instance, error) {
|
|||||||
instance.Relay.Negentropy = true
|
instance.Relay.Negentropy = true
|
||||||
instance.Relay.Info.Name = config.Info.Name
|
instance.Relay.Info.Name = config.Info.Name
|
||||||
instance.Relay.Info.Icon = config.Info.Icon
|
instance.Relay.Info.Icon = config.Info.Icon
|
||||||
instance.Relay.Info.PubKey = &pubkey
|
|
||||||
instance.Relay.Info.Description = config.Info.Description
|
instance.Relay.Info.Description = config.Info.Description
|
||||||
// instance.Relay.Info.Self = nostr.GetPublicKey(secret)
|
// instance.Relay.Info.Self = nostr.GetPublicKey(secret)
|
||||||
instance.Relay.Info.Software = "https://github.com/coracle-social/zooid"
|
instance.Relay.Info.Software = "https://github.com/coracle-social/zooid"
|
||||||
instance.Relay.Info.Version = "v0.1.0"
|
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.UseEventstore(instance.Events, 400)
|
||||||
|
|
||||||
instance.Relay.OnConnect = instance.OnConnect
|
instance.Relay.OnConnect = instance.OnConnect
|
||||||
@@ -171,11 +88,7 @@ func MakeInstance(hostname string) (*Instance, error) {
|
|||||||
// Initialize stuff
|
// Initialize stuff
|
||||||
|
|
||||||
if err := instance.Events.Init(); err != nil {
|
if err := instance.Events.Init(); err != nil {
|
||||||
log.Fatal("Failed to initialize event store:", err)
|
log.Fatal("Failed to initialize event store: ", err)
|
||||||
}
|
|
||||||
|
|
||||||
if err := instance.Blossom.Init(); err != nil {
|
|
||||||
log.Fatal("Failed to initialize blossom store:", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Blossom.Enabled {
|
if config.Blossom.Enabled {
|
||||||
@@ -189,14 +102,8 @@ func MakeInstance(hostname string) (*Instance, error) {
|
|||||||
return instance, nil
|
return instance, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instance *Instance) Cleanup() bool {
|
func (instance *Instance) Cleanup() {
|
||||||
// Close the event store
|
|
||||||
instance.Events.Close()
|
instance.Events.Close()
|
||||||
|
|
||||||
// Remove from instances map
|
|
||||||
delete(instances, instance.Config.Host)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility methods
|
// Utility methods
|
||||||
|
|||||||
+122
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user