first commit
This commit is contained in:
59
cmd/nip05api/cleanup.go
Normal file
59
cmd/nip05api/cleanup.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/noderunners/nip05api/internal/db"
|
||||
)
|
||||
|
||||
// cleanupWorker prunes old delivered/dead outbox rows hourly so the DB stays
|
||||
// small. Delivered: 7 days. Dead: 30 days (kept longer for forensic value).
|
||||
type cleanupWorker struct {
|
||||
db *db.DB
|
||||
}
|
||||
|
||||
func newCleanupWorker(d *db.DB) *cleanupWorker { return &cleanupWorker{db: d} }
|
||||
|
||||
func (c *cleanupWorker) Run(ctx context.Context) {
|
||||
t := time.NewTicker(1 * time.Hour)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
c.prune(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cleanupWorker) prune(ctx context.Context) {
|
||||
queries := []struct {
|
||||
label string
|
||||
query string
|
||||
args []any
|
||||
}{
|
||||
{"webhook delivered", `DELETE FROM webhook_outbox WHERE status = 'delivered' AND created_at < ?`,
|
||||
[]any{time.Now().UTC().Add(-7 * 24 * time.Hour).Format(time.RFC3339)}},
|
||||
{"webhook dead", `DELETE FROM webhook_outbox WHERE status = 'dead' AND created_at < ?`,
|
||||
[]any{time.Now().UTC().Add(-30 * 24 * time.Hour).Format(time.RFC3339)}},
|
||||
{"dm delivered", `DELETE FROM dm_outbox WHERE status = 'delivered' AND created_at < ?`,
|
||||
[]any{time.Now().UTC().Add(-7 * 24 * time.Hour).Format(time.RFC3339)}},
|
||||
{"dm dead", `DELETE FROM dm_outbox WHERE status = 'dead' AND created_at < ?`,
|
||||
[]any{time.Now().UTC().Add(-30 * 24 * time.Hour).Format(time.RFC3339)}},
|
||||
{"audit", `DELETE FROM audit_log WHERE created_at < ?`,
|
||||
[]any{time.Now().UTC().Add(-180 * 24 * time.Hour).Format(time.RFC3339)}},
|
||||
}
|
||||
for _, q := range queries {
|
||||
res, err := c.db.ExecContext(ctx, q.query, q.args...)
|
||||
if err != nil {
|
||||
slog.Warn("cleanup", "label", q.label, "err", err)
|
||||
continue
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n > 0 {
|
||||
slog.Info("cleanup", "label", q.label, "rows", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
175
cmd/nip05api/main.go
Normal file
175
cmd/nip05api/main.go
Normal file
@@ -0,0 +1,175 @@
|
||||
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,
|
||||
}, 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)
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user