package main import ( "context" "log/slog" "os" "os/signal" "sync" "syscall" "github.com/noderunners/nip05api/internal/audit" "github.com/noderunners/nip05api/internal/config" "github.com/noderunners/nip05api/internal/db" "github.com/noderunners/nip05api/internal/dm" "github.com/noderunners/nip05api/internal/expiry" httpapi "github.com/noderunners/nip05api/internal/http" "github.com/noderunners/nip05api/internal/invoice" applog "github.com/noderunners/nip05api/internal/log" "github.com/noderunners/nip05api/internal/messages" "github.com/noderunners/nip05api/internal/nostr" "github.com/noderunners/nip05api/internal/payments" syncw "github.com/noderunners/nip05api/internal/sync" "github.com/noderunners/nip05api/internal/user" "github.com/noderunners/nip05api/internal/webhook" ) // version is overridden at build time via -ldflags "-X main.version=..." var version = "dev" func main() { if err := run(); err != nil { slog.Error("fatal", "err", err) os.Exit(1) } } func run() error { cfg, err := config.Load() if err != nil { applog.Setup("info") return err } applog.Setup(cfg.LogLevel) slog.Info("starting", "version", version, "domain", cfg.Domain, "port", cfg.Port, "lightning", cfg.Lightning.Enabled, "dm", cfg.DM.Enabled, "sync", cfg.Nostr.UsernameSyncEnabled, "webhook", cfg.Webhook.URL != "", ) database, err := db.Open(cfg.DatabasePath) if err != nil { return err } defer database.Close() ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer cancel() if err := database.Migrate(ctx); err != nil { return err } tmpls, err := messages.Load(cfg.DM.MessagesFile) if err != nil { return err } userRepo := user.NewRepo(database) userSvc := user.NewService(userRepo, cfg.ReservedUsernames) auditLogger := audit.New(database) pool := nostr.NewPool(cfg.Nostr.Relays) defer pool.Close() hookRepo := webhook.NewRepo(database) hookSvc := webhook.NewService(hookRepo, cfg.Domain, cfg.Webhook.URL != "") hookWorker := webhook.NewWorker(hookRepo, cfg.Webhook.URL, cfg.Webhook.Secret, cfg.Webhook.TimeoutSecs, cfg.Webhook.MaxRetries) dmRepo := dm.NewRepo(database) dmSvc := dm.NewService(dmRepo, tmpls, cfg.DM.Enabled) var dmWorker *dm.Worker if cfg.DM.Enabled { dmWorker, err = dm.NewWorker(dmRepo, pool, cfg.DM.Nsec, cfg.DM.Kind) if err != nil { return err } } var invSvc *invoice.Service var lnClient *invoice.LNbitsClient if cfg.Lightning.Enabled { lnClient = invoice.NewLNbits(cfg.Lightning.LNbitsURL, cfg.Lightning.LNbitsInvoiceKey) invRepo := invoice.NewRepo(database) invSvc = invoice.NewService(invRepo, userSvc, lnClient, invoice.Pricing{ YearlySats: cfg.Lightning.PriceYearlySats, LifetimeSats: cfg.Lightning.PriceLifetimeSats, ExpiryMins: cfg.Lightning.InvoiceExpiryMins, MemoYearly: cfg.Lightning.InvoiceMemoYearly, MemoLifetime: cfg.Lightning.InvoiceMemoLifetime, }, cfg.Domain) } payWorker := payments.NewWorker(invSvc, userSvc, lnClient, dmSvc, hookSvc, auditLogger, cfg.Domain, cfg.FrontendURL, cfg.Lightning.Enabled && invSvc != nil) syncWorker := syncw.NewWorker(userSvc, pool, cfg.Nostr.SyncIntervalMins, cfg.Nostr.UsernameSyncEnabled, cfg.Domain, cfg.ReservedUsernames) expiryWorker := expiry.NewWorker(userSvc, dmSvc, hookSvc, auditLogger, cfg.Domain, cfg.FrontendURL, cfg.Expiry.GraceDays, cfg.Expiry.ReminderDays, cfg.Expiry.CronHourUTC) cleanupWorker := newCleanupWorker(database) srv := httpapi.NewServer(httpapi.Deps{ Cfg: cfg, DB: database, Users: userSvc, Invoices: invSvc, DMs: dmSvc, Hooks: hookSvc, Audit: auditLogger, Version: version, }) var wg sync.WaitGroup startWorker(&wg, ctx, "webhook", hookWorker.Run) if dmWorker != nil { startWorker(&wg, ctx, "dm", dmWorker.Run) } startWorker(&wg, ctx, "payments", payWorker.Run) startWorker(&wg, ctx, "sync", syncWorker.Run) startWorker(&wg, ctx, "expiry", expiryWorker.Run) startWorker(&wg, ctx, "cleanup", cleanupWorker.Run) serverErr := make(chan error, 1) go func() { slog.Info("http listening", "addr", cfg.Addr()) if err := srv.ListenAndServe(); err != nil && err.Error() != "http: Server closed" { serverErr <- err } close(serverErr) }() select { case <-ctx.Done(): slog.Info("shutdown signal received") case err := <-serverErr: if err != nil { cancel() wg.Wait() return err } } if err := httpapi.Shutdown(context.Background(), srv); err != nil { slog.Error("server shutdown", "err", err) } wg.Wait() slog.Info("shutdown complete") return nil } func startWorker(wg *sync.WaitGroup, ctx context.Context, name string, run func(context.Context)) { wg.Add(1) go func() { defer wg.Done() defer slog.Info("worker stopped", "name", name) slog.Info("worker started", "name", name) run(ctx) }() }