Files
Nip-05-api/cmd/nip05api/main.go
Michilis 43d78862e3 Add configurable LNbits invoice memos and pubkey metadata
Read INVOICE_MEMO_YEARLY and INVOICE_MEMO_LIFETIME from the environment
and pass the user pubkey in LNbits payment extra for invoice creation.
2026-05-06 19:52:07 +00:00

178 lines
4.8 KiB
Go

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)
}()
}