From 93a75a9464e759cf09ac4f3d204ec4d1999e0577 Mon Sep 17 00:00:00 2001 From: enki Date: Sun, 17 May 2026 16:08:09 -0700 Subject: [PATCH] chore: remove Go archive --- archive/go/Makefile | 36 -- archive/go/cmd/kindexr-cli/main.go | 95 ----- archive/go/cmd/kindexr/main.go | 117 ------ archive/go/go.mod | 51 --- archive/go/go.sum | 203 ----------- archive/go/internal/config/config.go | 280 -------------- archive/go/internal/config/config_test.go | 83 ----- archive/go/internal/db/db.go | 275 -------------- archive/go/internal/db/db_test.go | 122 ------- .../go/internal/db/migrations/001_initial.sql | 120 ------ .../migrations/002_drop_apikey_visibility.sql | 14 - archive/go/internal/db/queries.go | 343 ------------------ archive/go/internal/nostr/parser.go | 130 ------- archive/go/internal/nostr/parser_test.go | 157 -------- archive/go/internal/nostr/reader.go | 140 ------- archive/go/internal/server/server.go | 93 ----- archive/go/internal/server/server_test.go | 114 ------ archive/go/internal/torznab/caps.go | 69 ---- archive/go/internal/torznab/categories.go | 46 --- archive/go/internal/torznab/search.go | 157 -------- archive/go/internal/torznab/server.go | 69 ---- archive/go/internal/torznab/server_test.go | 175 --------- archive/go/internal/torznab/xml.go | 114 ------ 23 files changed, 3003 deletions(-) delete mode 100644 archive/go/Makefile delete mode 100644 archive/go/cmd/kindexr-cli/main.go delete mode 100644 archive/go/cmd/kindexr/main.go delete mode 100644 archive/go/go.mod delete mode 100644 archive/go/go.sum delete mode 100644 archive/go/internal/config/config.go delete mode 100644 archive/go/internal/config/config_test.go delete mode 100644 archive/go/internal/db/db.go delete mode 100644 archive/go/internal/db/db_test.go delete mode 100644 archive/go/internal/db/migrations/001_initial.sql delete mode 100644 archive/go/internal/db/migrations/002_drop_apikey_visibility.sql delete mode 100644 archive/go/internal/db/queries.go delete mode 100644 archive/go/internal/nostr/parser.go delete mode 100644 archive/go/internal/nostr/parser_test.go delete mode 100644 archive/go/internal/nostr/reader.go delete mode 100644 archive/go/internal/server/server.go delete mode 100644 archive/go/internal/server/server_test.go delete mode 100644 archive/go/internal/torznab/caps.go delete mode 100644 archive/go/internal/torznab/categories.go delete mode 100644 archive/go/internal/torznab/search.go delete mode 100644 archive/go/internal/torznab/server.go delete mode 100644 archive/go/internal/torznab/server_test.go delete mode 100644 archive/go/internal/torznab/xml.go diff --git a/archive/go/Makefile b/archive/go/Makefile deleted file mode 100644 index e40b31a..0000000 --- a/archive/go/Makefile +++ /dev/null @@ -1,36 +0,0 @@ -.PHONY: build test clean install run lint fmt vet check - -BINARY := kindexr -CLI := kindexr-cli -VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) -LDFLAGS := -ldflags "-X main.version=$(VERSION) -s -w" - -build: - go build $(LDFLAGS) -o ./bin/$(BINARY) ./cmd/kindexr - go build $(LDFLAGS) -o ./bin/$(CLI) ./cmd/kindexr-cli - -test: - go test -race ./... - -lint: - golangci-lint run - -fmt: - gofmt -w . - -vet: - go vet ./... - -check: fmt vet test - -install: build - install -D -m 0755 ./bin/$(BINARY) /usr/local/bin/$(BINARY) - install -D -m 0755 ./bin/$(CLI) /usr/local/bin/$(CLI) - install -D -m 0644 ./deploy/kindexr.service /etc/systemd/system/kindexr.service - install -D -m 0640 ./deploy/kindexr.example.yaml /etc/kindexr/config.yaml - -run: - go run ./cmd/kindexr --config ./deploy/kindexr.example.yaml - -clean: - rm -rf ./bin diff --git a/archive/go/cmd/kindexr-cli/main.go b/archive/go/cmd/kindexr-cli/main.go deleted file mode 100644 index f62a78a..0000000 --- a/archive/go/cmd/kindexr-cli/main.go +++ /dev/null @@ -1,95 +0,0 @@ -// Command kindexr-cli is the admin CLI for kindexr. -// -// TODO (Phase 3): implement the following sub-commands: -// - apikey list -// - apikey revoke -// - relay add -// - relay remove -// - relay list -// - publisher trust -// - publisher block -// - db stats -package main - -import ( - "context" - "crypto/rand" - "encoding/hex" - "flag" - "fmt" - "os" - - "git.utn.lol/enki/kindexr/internal/config" - "git.utn.lol/enki/kindexr/internal/db" -) - -func main() { - if len(os.Args) < 3 { - fmt.Fprintln(os.Stderr, "usage: kindexr-cli [flags]") - fmt.Fprintln(os.Stderr, " kindexr-cli apikey create --label ") - os.Exit(1) - } - - switch os.Args[1] { - case "apikey": - runAPIKey(os.Args[2:]) - default: - fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1]) - os.Exit(1) - } -} - -func runAPIKey(args []string) { - if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: kindexr-cli apikey create --label ") - os.Exit(1) - } - switch args[0] { - case "create": - runAPIKeyCreate(args[1:]) - default: - fmt.Fprintf(os.Stderr, "unknown apikey subcommand: %s\n", args[0]) - os.Exit(1) - } -} - -func runAPIKeyCreate(args []string) { - fs := flag.NewFlagSet("apikey create", flag.ExitOnError) - configPath := fs.String("config", "/etc/kindexr/config.yaml", "path to config file") - label := fs.String("label", "", "key label (e.g. sonarr, radarr)") - if err := fs.Parse(args); err != nil { - os.Exit(1) - } - - if *label == "" { - fmt.Fprintln(os.Stderr, "error: --label is required") - os.Exit(1) - } - - cfg, err := config.Load(*configPath) - if err != nil { - fmt.Fprintf(os.Stderr, "error loading config: %v\n", err) - os.Exit(1) - } - - database, err := db.Open(cfg.Database.Path) - if err != nil { - fmt.Fprintf(os.Stderr, "error opening database: %v\n", err) - os.Exit(1) - } - defer database.Close() - - keyBytes := make([]byte, 32) - if _, err := rand.Read(keyBytes); err != nil { - fmt.Fprintf(os.Stderr, "error generating key: %v\n", err) - os.Exit(1) - } - key := hex.EncodeToString(keyBytes) - - if err := database.CreateAPIKey(context.Background(), key, *label); err != nil { - fmt.Fprintf(os.Stderr, "error creating api key: %v\n", err) - os.Exit(1) - } - - fmt.Println(key) -} diff --git a/archive/go/cmd/kindexr/main.go b/archive/go/cmd/kindexr/main.go deleted file mode 100644 index f4eb153..0000000 --- a/archive/go/cmd/kindexr/main.go +++ /dev/null @@ -1,117 +0,0 @@ -// Command kindexr is the main daemon for the kindexr Nostr-native Torznab indexer. -package main - -import ( - "context" - "flag" - "log/slog" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "git.utn.lol/enki/kindexr/internal/config" - "git.utn.lol/enki/kindexr/internal/db" - nostrreader "git.utn.lol/enki/kindexr/internal/nostr" - "git.utn.lol/enki/kindexr/internal/server" -) - -// version is overridden at build time via -ldflags "-X main.version=". -var version = "dev" - -func main() { - configPath := flag.String("config", "/etc/kindexr/config.yaml", "path to config file") - flag.Parse() - - // Load configuration. - cfg, err := config.Load(*configPath) - if err != nil { - slog.Error("failed to load config", "error", err) - os.Exit(1) - } - - // Set up structured logging. - var logLevel slog.Level - switch cfg.Logging.Level { - case "debug": - logLevel = slog.LevelDebug - case "warn", "warning": - logLevel = slog.LevelWarn - case "error": - logLevel = slog.LevelError - default: - logLevel = slog.LevelInfo - } - - handlerOpts := &slog.HandlerOptions{Level: logLevel} - var handler slog.Handler - if cfg.Logging.Format == "json" { - handler = slog.NewJSONHandler(os.Stderr, handlerOpts) - } else { - handler = slog.NewTextHandler(os.Stderr, handlerOpts) - } - logger := slog.New(handler) - slog.SetDefault(logger) - - // Open database. - database, err := db.Open(cfg.Database.Path) - if err != nil { - slog.Error("failed to open database", "error", err, "path", cfg.Database.Path) - os.Exit(1) - } - defer database.Close() - - migrationVer, err := database.MigrationVersion(context.Background()) - if err != nil { - slog.Error("failed to query migration version", "error", err) - os.Exit(1) - } - slog.Info("database ready", "path", cfg.Database.Path, "migration_version", migrationVer) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Start Nostr relay reader. - reader := nostrreader.New(cfg, database) - reader.Start(ctx) - - // Create HTTP server. - srv := server.New(cfg, database, version) - srv.SetConnectedRelays(reader.ConnectedCount) - httpServer := &http.Server{ - Addr: cfg.Server.Listen, - Handler: srv.Handler(), - ReadTimeout: 30 * time.Second, - WriteTimeout: 30 * time.Second, - IdleTimeout: 120 * time.Second, - } - - slog.Info("starting kindexr", "version", version, "listen", cfg.Server.Listen) - - go func() { - if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - slog.Error("http server error", "error", err) - cancel() - } - }() - - // Wait for shutdown signal. - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - - select { - case sig := <-sigCh: - slog.Info("received signal, shutting down", "signal", sig) - case <-ctx.Done(): - } - - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) - defer shutdownCancel() - - if err := httpServer.Shutdown(shutdownCtx); err != nil { - slog.Error("graceful shutdown failed", "error", err) - } - - slog.Info("kindexr stopped") -} diff --git a/archive/go/go.mod b/archive/go/go.mod deleted file mode 100644 index 439981c..0000000 --- a/archive/go/go.mod +++ /dev/null @@ -1,51 +0,0 @@ -module git.utn.lol/enki/kindexr - -go 1.25.0 - -require ( - github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect - github.com/btcsuite/btcd/btcutil v1.1.5 // indirect - github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect - github.com/bytedance/sonic v1.13.1 // indirect - github.com/bytedance/sonic/loader v0.2.4 // indirect - github.com/cloudwego/base64x v0.1.5 // indirect - github.com/coder/websocket v1.8.12 // indirect - github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-chi/chi/v5 v5.2.5 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/knadh/koanf/maps v0.1.2 // indirect - github.com/knadh/koanf/parsers/yaml v1.1.0 // indirect - github.com/knadh/koanf/providers/env v1.1.0 // indirect - github.com/knadh/koanf/providers/file v1.2.1 // indirect - github.com/knadh/koanf/v2 v2.3.4 // indirect - github.com/mailru/easyjson v0.9.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/nbd-wtf/go-nostr v0.52.3 // indirect - github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/tidwall/gjson v1.18.0 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.1 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - go.yaml.in/yaml/v3 v3.0.3 // indirect - golang.org/x/arch v0.15.0 // indirect - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/sys v0.42.0 // indirect - modernc.org/libc v1.72.3 // indirect - modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.50.1 // indirect -) diff --git a/archive/go/go.sum b/archive/go/go.sum deleted file mode 100644 index d69a085..0000000 --- a/archive/go/go.sum +++ /dev/null @@ -1,203 +0,0 @@ -github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg= -github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA= -github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= -github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= -github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= -github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= -github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= -github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= -github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= -github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= -github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= -github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= -github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= -github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= -github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= -github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= -github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= -github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= -github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= -github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= -github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= -github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g= -github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= -github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= -github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= -github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= -github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= -github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= -github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= -github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= -github.com/knadh/koanf/parsers/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4= -github.com/knadh/koanf/parsers/yaml v1.1.0/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg= -github.com/knadh/koanf/providers/env v1.1.0 h1:U2VXPY0f+CsNDkvdsG8GcsnK4ah85WwWyJgef9oQMSc= -github.com/knadh/koanf/providers/env v1.1.0/go.mod h1:QhHHHZ87h9JxJAn2czdEl6pdkNnDh/JS1Vtsyt65hTY= -github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM= -github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= -github.com/knadh/koanf/v2 v2.3.4 h1:fnynNSDlujWE+v83hAp8wKr/cdoxHLO0629SN+U8Urc= -github.com/knadh/koanf/v2 v2.3.4/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/nbd-wtf/go-nostr v0.52.3 h1:Xd87pXfJEJRXHpM+fLjQQln8dBNNaoPA10V7BbyP4KI= -github.com/nbd-wtf/go-nostr v0.52.3/go.mod h1:4avYoc9mDGZ9wHsvCOhHH9vPzKucCfuYBtJUSpHTfNk= -github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= -github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= -github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= -go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= -golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= -golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= -golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= -golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU= -modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs= -modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w= -modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/archive/go/internal/config/config.go b/archive/go/internal/config/config.go deleted file mode 100644 index 8fbb824..0000000 --- a/archive/go/internal/config/config.go +++ /dev/null @@ -1,280 +0,0 @@ -// Package config provides koanf-based configuration loading for kindexr. -// Load order: defaults → YAML file → env vars (KINDEXR_ prefix). -package config - -import ( - "os" - "strings" - - "github.com/knadh/koanf/parsers/yaml" - "github.com/knadh/koanf/providers/env" - "github.com/knadh/koanf/providers/file" - "github.com/knadh/koanf/v2" -) - -// ServerConfig holds HTTP server settings. -type ServerConfig struct { - Listen string `koanf:"listen"` - BaseURL string `koanf:"base_url"` -} - -// DatabaseConfig holds SQLite settings. -type DatabaseConfig struct { - Path string `koanf:"path"` -} - -// LoggingConfig holds structured logging settings. -type LoggingConfig struct { - Level string `koanf:"level"` - Format string `koanf:"format"` -} - -// CurationConfig controls ingestion filtering. -type CurationConfig struct { - WoTOnly bool `koanf:"wot_only"` - FollowDepth int `koanf:"follow_depth"` - Allowlist []string `koanf:"allowlist"` - Blocklist []string `koanf:"blocklist"` - CurationSets []string `koanf:"curation_sets"` - OperatorPubkey string `koanf:"operator_pubkey"` -} - -// TMDBConfig controls TMDB enrichment. -type TMDBConfig struct { - Enabled bool `koanf:"enabled"` - APIKey string `koanf:"api_key"` - CacheTTL string `koanf:"cache_ttl"` -} - -// HealthConfig controls torrent health scraping. -type HealthConfig struct { - Enabled bool `koanf:"enabled"` - Method string `koanf:"method"` - RefreshInterval string `koanf:"refresh_interval"` -} - -// SignerConfig controls the Nostr signing backend. -type SignerConfig struct { - Mode string `koanf:"mode"` - BunkerURI string `koanf:"bunker_uri"` - Ncryptsec string `koanf:"ncryptsec"` - Nsec string `koanf:"nsec"` -} - -// QBittorrentConfig holds qBittorrent Web API settings. -type QBittorrentConfig struct { - URL string `koanf:"url"` - Username string `koanf:"username"` - Password string `koanf:"password"` - Category string `koanf:"category"` -} - -// WatchDirConfig holds watch-directory settings. -type WatchDirConfig struct { - Path string `koanf:"path"` -} - -// ClientConfig holds the download client settings. -type ClientConfig struct { - Type string `koanf:"type"` - QBittorrent QBittorrentConfig `koanf:"qbittorrent"` - WatchDir WatchDirConfig `koanf:"watch_dir"` -} - -// PublisherConfig controls the writer side (publishing your own events). -type PublisherConfig struct { - Enabled bool `koanf:"enabled"` - Signer SignerConfig `koanf:"signer"` - OutboxRelays []string `koanf:"outbox_relays"` - Client ClientConfig `koanf:"client"` - EnrichBeforePublish bool `koanf:"enrich_before_publish"` -} - -// Config is the top-level configuration structure. -type Config struct { - Server ServerConfig `koanf:"server"` - Database DatabaseConfig `koanf:"database"` - Logging LoggingConfig `koanf:"logging"` - Relays []string `koanf:"relays"` - NegentropyBootstrap bool `koanf:"negentropy_bootstrap"` - BackfillDays int `koanf:"backfill_days"` - Curation CurationConfig `koanf:"curation"` - TMDB TMDBConfig `koanf:"tmdb"` - Health HealthConfig `koanf:"health"` - Publisher PublisherConfig `koanf:"publisher"` -} - -// defaultRelays is the starter set of public Nostr relays. -var defaultRelays = []string{ - "wss://relay.damus.io", - "wss://nos.lol", - "wss://relay.primal.net", - "wss://nostr.mom", - "wss://relay.snort.social", - "wss://sovbit.host", -} - -// Defaults returns a Config populated with sane default values. -func Defaults() *Config { - return &Config{ - Server: ServerConfig{ - Listen: "127.0.0.1:9117", - BaseURL: "https://kindexr.example.com", - }, - Database: DatabaseConfig{ - Path: "/var/lib/kindexr/kindexr.db", - }, - Logging: LoggingConfig{ - Level: "info", - Format: "json", - }, - Relays: defaultRelays, - NegentropyBootstrap: true, - BackfillDays: 365, - Curation: CurationConfig{ - WoTOnly: false, - FollowDepth: 2, - }, - TMDB: TMDBConfig{ - Enabled: true, - CacheTTL: "168h", - }, - Health: HealthConfig{ - Enabled: false, - Method: "dht", - RefreshInterval: "30m", - }, - Publisher: PublisherConfig{ - Enabled: false, - Signer: SignerConfig{ - Mode: "bunker", - }, - OutboxRelays: []string{ - "wss://sovbit.host", - "wss://relay.damus.io", - "wss://nos.lol", - }, - Client: ClientConfig{ - Type: "qbittorrent", - QBittorrent: QBittorrentConfig{ - URL: "http://127.0.0.1:8080", - Username: "admin", - Category: "publish-nostr", - }, - WatchDir: WatchDirConfig{ - Path: "/var/lib/kindexr/watch", - }, - }, - EnrichBeforePublish: true, - }, - } -} - -// Load reads config from defaults, then optionally from a YAML file, then from -// environment variables with the KINDEXR_ prefix. If path is empty or the file -// does not exist, the file step is silently skipped. -func Load(path string) (*Config, error) { - k := koanf.New(".") - - defaults := Defaults() - - // Apply defaults via Set calls. - if err := k.Set("server.listen", defaults.Server.Listen); err != nil { - return nil, err - } - if err := k.Set("server.base_url", defaults.Server.BaseURL); err != nil { - return nil, err - } - if err := k.Set("database.path", defaults.Database.Path); err != nil { - return nil, err - } - if err := k.Set("logging.level", defaults.Logging.Level); err != nil { - return nil, err - } - if err := k.Set("logging.format", defaults.Logging.Format); err != nil { - return nil, err - } - if err := k.Set("relays", defaults.Relays); err != nil { - return nil, err - } - if err := k.Set("negentropy_bootstrap", defaults.NegentropyBootstrap); err != nil { - return nil, err - } - if err := k.Set("backfill_days", defaults.BackfillDays); err != nil { - return nil, err - } - if err := k.Set("curation.wot_only", defaults.Curation.WoTOnly); err != nil { - return nil, err - } - if err := k.Set("curation.follow_depth", defaults.Curation.FollowDepth); err != nil { - return nil, err - } - if err := k.Set("tmdb.enabled", defaults.TMDB.Enabled); err != nil { - return nil, err - } - if err := k.Set("tmdb.cache_ttl", defaults.TMDB.CacheTTL); err != nil { - return nil, err - } - if err := k.Set("health.enabled", defaults.Health.Enabled); err != nil { - return nil, err - } - if err := k.Set("health.method", defaults.Health.Method); err != nil { - return nil, err - } - if err := k.Set("health.refresh_interval", defaults.Health.RefreshInterval); err != nil { - return nil, err - } - if err := k.Set("publisher.enabled", defaults.Publisher.Enabled); err != nil { - return nil, err - } - if err := k.Set("publisher.signer.mode", defaults.Publisher.Signer.Mode); err != nil { - return nil, err - } - if err := k.Set("publisher.outbox_relays", defaults.Publisher.OutboxRelays); err != nil { - return nil, err - } - if err := k.Set("publisher.client.type", defaults.Publisher.Client.Type); err != nil { - return nil, err - } - if err := k.Set("publisher.client.qbittorrent.url", defaults.Publisher.Client.QBittorrent.URL); err != nil { - return nil, err - } - if err := k.Set("publisher.client.qbittorrent.username", defaults.Publisher.Client.QBittorrent.Username); err != nil { - return nil, err - } - if err := k.Set("publisher.client.qbittorrent.category", defaults.Publisher.Client.QBittorrent.Category); err != nil { - return nil, err - } - if err := k.Set("publisher.client.watch_dir.path", defaults.Publisher.Client.WatchDir.Path); err != nil { - return nil, err - } - if err := k.Set("publisher.enrich_before_publish", defaults.Publisher.EnrichBeforePublish); err != nil { - return nil, err - } - - // Load YAML file if path is set and file exists. - if path != "" { - if _, err := os.Stat(path); err == nil { - if err := k.Load(file.Provider(path), yaml.Parser()); err != nil { - return nil, err - } - } - // If file does not exist, skip gracefully (no error). - } - - // Load env vars with KINDEXR_ prefix; map KINDEXR_LOGGING_LEVEL -> logging.level. - // Ignore errors from env loading (no KINDEXR_ vars present is fine). - _ = k.Load(env.Provider("KINDEXR_", ".", func(s string) string { - // Strip prefix, lowercase, replace _ with . - s = strings.TrimPrefix(s, "KINDEXR_") - s = strings.ToLower(s) - s = strings.ReplaceAll(s, "_", ".") - return s - }), nil) - - cfg := &Config{} - if err := k.Unmarshal("", cfg); err != nil { - return nil, err - } - return cfg, nil -} diff --git a/archive/go/internal/config/config_test.go b/archive/go/internal/config/config_test.go deleted file mode 100644 index 913c895..0000000 --- a/archive/go/internal/config/config_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package config_test - -import ( - "os" - "path/filepath" - "testing" - - "git.utn.lol/enki/kindexr/internal/config" -) - -func TestDefaults(t *testing.T) { - cfg, err := config.Load("") - if err != nil { - t.Fatalf("Load with empty path failed: %v", err) - } - if cfg.Server.Listen != "127.0.0.1:9117" { - t.Errorf("expected default listen 127.0.0.1:9117, got %q", cfg.Server.Listen) - } - if cfg.Logging.Format != "json" { - t.Errorf("expected default log format json, got %q", cfg.Logging.Format) - } - if cfg.Logging.Level != "info" { - t.Errorf("expected default log level info, got %q", cfg.Logging.Level) - } - if cfg.Database.Path != "/var/lib/kindexr/kindexr.db" { - t.Errorf("expected default db path, got %q", cfg.Database.Path) - } -} - -func TestLoadFromFile(t *testing.T) { - dir := t.TempDir() - cfgFile := filepath.Join(dir, "config.yaml") - content := ` -server: - listen: "0.0.0.0:8888" -logging: - level: "debug" -` - if err := os.WriteFile(cfgFile, []byte(content), 0644); err != nil { - t.Fatalf("failed to write temp config: %v", err) - } - - cfg, err := config.Load(cfgFile) - if err != nil { - t.Fatalf("Load from file failed: %v", err) - } - if cfg.Server.Listen != "0.0.0.0:8888" { - t.Errorf("expected listen 0.0.0.0:8888, got %q", cfg.Server.Listen) - } - if cfg.Logging.Level != "debug" { - t.Errorf("expected log level debug, got %q", cfg.Logging.Level) - } - // Non-overridden defaults should still be present. - if cfg.Logging.Format != "json" { - t.Errorf("expected default log format json, got %q", cfg.Logging.Format) - } -} - -func TestEnvOverride(t *testing.T) { - t.Setenv("KINDEXR_LOGGING_LEVEL", "warn") - - cfg, err := config.Load("") - if err != nil { - t.Fatalf("Load failed: %v", err) - } - if cfg.Logging.Level != "warn" { - t.Errorf("expected logging level warn from env, got %q", cfg.Logging.Level) - } -} - -func TestLoadMissingFile(t *testing.T) { - dir := t.TempDir() - nonExistent := filepath.Join(dir, "does-not-exist.yaml") - - cfg, err := config.Load(nonExistent) - if err != nil { - t.Fatalf("Load with missing file should succeed, got error: %v", err) - } - // Should still have defaults. - if cfg.Server.Listen != "127.0.0.1:9117" { - t.Errorf("expected default listen, got %q", cfg.Server.Listen) - } -} diff --git a/archive/go/internal/db/db.go b/archive/go/internal/db/db.go deleted file mode 100644 index 3bdb553..0000000 --- a/archive/go/internal/db/db.go +++ /dev/null @@ -1,275 +0,0 @@ -// Package db provides SQLite database access and migration management for kindexr. -package db - -import ( - "context" - "database/sql" - "embed" - "fmt" - "os" - "path/filepath" - "sort" - "strconv" - "strings" - - _ "modernc.org/sqlite" // pure-Go SQLite driver, no CGO -) - -//go:embed migrations/*.sql -var migrationsFS embed.FS - -// DB wraps *sql.DB with kindexr-specific helpers. -type DB struct { - *sql.DB -} - -// Open opens (or creates) the SQLite database at path, applies pending -// migrations, and returns a ready-to-use *DB. The parent directory is created -// if it does not exist. -func Open(path string) (*DB, error) { - if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { - return nil, fmt.Errorf("db: create parent directory: %w", err) - } - - dsn := "file:" + path + "?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)" - sqlDB, err := sql.Open("sqlite", dsn) - if err != nil { - return nil, fmt.Errorf("db: open: %w", err) - } - - // SQLite WAL allows concurrent reads but serialises writes; one writer. - sqlDB.SetMaxOpenConns(1) - - if err := sqlDB.Ping(); err != nil { - sqlDB.Close() - return nil, fmt.Errorf("db: ping: %w", err) - } - - d := &DB{sqlDB} - if err := d.migrate(); err != nil { - sqlDB.Close() - return nil, fmt.Errorf("db: migrate: %w", err) - } - return d, nil -} - -// migrate creates the schema_migrations table and applies any unapplied -// migration files found in the embedded migrations/ directory. -func (d *DB) migrate() error { - // Ensure the migrations tracking table exists. - _, err := d.DB.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations ( - version INTEGER PRIMARY KEY, - applied_at TEXT NOT NULL - )`) - if err != nil { - return fmt.Errorf("create schema_migrations: %w", err) - } - - // Find the current applied version. - var currentVersion int - row := d.DB.QueryRow(`SELECT COALESCE(MAX(version), 0) FROM schema_migrations`) - if err := row.Scan(¤tVersion); err != nil { - return fmt.Errorf("query current version: %w", err) - } - - // Read embedded migration files. - entries, err := migrationsFS.ReadDir("migrations") - if err != nil { - return fmt.Errorf("read embedded migrations: %w", err) - } - - // Sort by filename so they apply in order. - sort.Slice(entries, func(i, j int) bool { - return entries[i].Name() < entries[j].Name() - }) - - for _, entry := range entries { - if entry.IsDir() { - continue - } - name := entry.Name() - if !strings.HasSuffix(name, ".sql") { - continue - } - - // Parse version from prefix: "001_initial.sql" → 1 - parts := strings.SplitN(name, "_", 2) - if len(parts) < 1 { - continue - } - version, err := strconv.Atoi(parts[0]) - if err != nil { - return fmt.Errorf("parse version from %q: %w", name, err) - } - - if version <= currentVersion { - continue // already applied - } - - sqlBytes, err := migrationsFS.ReadFile("migrations/" + name) - if err != nil { - return fmt.Errorf("read migration %q: %w", name, err) - } - - statements := splitStatements(string(sqlBytes)) - - tx, err := d.DB.Begin() - if err != nil { - return fmt.Errorf("begin transaction for migration %d: %w", version, err) - } - - for _, stmt := range statements { - stmt = strings.TrimSpace(stmt) - if stmt == "" { - continue - } - if _, err := tx.Exec(stmt); err != nil { - tx.Rollback() - return fmt.Errorf("migration %d, statement %q: %w", version, stmt[:min(len(stmt), 60)], err) - } - } - - _, err = tx.Exec( - `INSERT INTO schema_migrations(version, applied_at) VALUES (?, datetime('now'))`, - version, - ) - if err != nil { - tx.Rollback() - return fmt.Errorf("record migration %d: %w", version, err) - } - - if err := tx.Commit(); err != nil { - return fmt.Errorf("commit migration %d: %w", version, err) - } - } - - return nil -} - -// splitStatements splits a SQL script into individual statements, handling -// single-quoted strings, -- line comments, and BEGIN...END trigger bodies so -// that semicolons inside them are not treated as statement boundaries. -func splitStatements(sql string) []string { - var statements []string - var current strings.Builder - var word strings.Builder - inString := false - depth := 0 // tracks BEGIN...END nesting for trigger bodies - i := 0 - - flushWord := func() { - switch strings.ToUpper(word.String()) { - case "BEGIN": - depth++ - case "END": - depth-- - } - word.Reset() - } - - for i < len(sql) { - ch := sql[i] - - if inString { - current.WriteByte(ch) - if ch == '\'' { - if i+1 < len(sql) && sql[i+1] == '\'' { - current.WriteByte(sql[i+1]) - i += 2 - continue - } - inString = false - } - i++ - continue - } - - // Outside a string. - if ch == '\'' { - flushWord() - inString = true - current.WriteByte(ch) - i++ - continue - } - - // Line comment: skip to end of line. - if ch == '-' && i+1 < len(sql) && sql[i+1] == '-' { - flushWord() - for i < len(sql) && sql[i] != '\n' { - i++ - } - continue - } - - // Accumulate identifier/keyword characters. - if isWordChar(ch) { - word.WriteByte(ch) - current.WriteByte(ch) - i++ - continue - } - - // Non-word character: finalize any pending keyword. - flushWord() - - if ch == ';' { - if depth == 0 { - stmt := strings.TrimSpace(current.String()) - if stmt != "" { - statements = append(statements, stmt) - } - current.Reset() - } else { - current.WriteByte(ch) - } - i++ - continue - } - - current.WriteByte(ch) - i++ - } - - // Catch any trailing statement without a trailing semicolon. - flushWord() - if stmt := strings.TrimSpace(current.String()); stmt != "" { - statements = append(statements, stmt) - } - - return statements -} - -func isWordChar(ch byte) bool { - return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' -} - -// Stats holds aggregate statistics about the indexed content. -type Stats struct { - EventsTotal int64 - LastEventAt *int64 -} - -// GetStats returns aggregate statistics from the torrents table. -func (d *DB) GetStats(ctx context.Context) (Stats, error) { - row := d.DB.QueryRowContext(ctx, `SELECT COUNT(*), MAX(created_at) FROM torrents`) - var s Stats - var lastEventAt sql.NullInt64 - if err := row.Scan(&s.EventsTotal, &lastEventAt); err != nil { - return Stats{}, fmt.Errorf("get stats: %w", err) - } - if lastEventAt.Valid { - s.LastEventAt = &lastEventAt.Int64 - } - return s, nil -} - -// MigrationVersion returns the highest applied migration version. -func (d *DB) MigrationVersion(ctx context.Context) (int, error) { - var version int - row := d.DB.QueryRowContext(ctx, `SELECT COALESCE(MAX(version), 0) FROM schema_migrations`) - if err := row.Scan(&version); err != nil { - return 0, fmt.Errorf("migration version: %w", err) - } - return version, nil -} diff --git a/archive/go/internal/db/db_test.go b/archive/go/internal/db/db_test.go deleted file mode 100644 index 6354902..0000000 --- a/archive/go/internal/db/db_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package db_test - -import ( - "context" - "database/sql" - "path/filepath" - "testing" - - "git.utn.lol/enki/kindexr/internal/db" -) - -func tempDB(t *testing.T) *db.DB { - t.Helper() - dir := t.TempDir() - path := filepath.Join(dir, "test.db") - d, err := db.Open(path) - if err != nil { - t.Fatalf("Open failed: %v", err) - } - t.Cleanup(func() { d.Close() }) - return d -} - -func TestOpen(t *testing.T) { - dir := t.TempDir() - path := filepath.Join(dir, "test.db") - d, err := db.Open(path) - if err != nil { - t.Fatalf("Open returned error: %v", err) - } - d.Close() -} - -func TestMigrationsApplied(t *testing.T) { - d := tempDB(t) - ctx := context.Background() - version, err := d.MigrationVersion(ctx) - if err != nil { - t.Fatalf("MigrationVersion error: %v", err) - } - if version < 1 { - t.Errorf("expected at least 1 migration applied, got version %d", version) - } -} - -func TestMigrationsIdempotent(t *testing.T) { - dir := t.TempDir() - path := filepath.Join(dir, "test.db") - - d1, err := db.Open(path) - if err != nil { - t.Fatalf("first Open failed: %v", err) - } - d1.Close() - - d2, err := db.Open(path) - if err != nil { - t.Fatalf("second Open failed (idempotency): %v", err) - } - d2.Close() -} - -func TestSchema(t *testing.T) { - d := tempDB(t) - - expectedTables := []string{ - "torrents", "files", "trackers", "tags", - "publishers", "relays", "api_keys", "health", "comments", - } - - for _, table := range expectedTables { - var name string - err := d.QueryRow( - `SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table, - ).Scan(&name) - if err == sql.ErrNoRows { - t.Errorf("expected table %q to exist, but it does not", table) - } else if err != nil { - t.Errorf("querying for table %q: %v", table, err) - } - } -} - -func TestFTS5(t *testing.T) { - d := tempDB(t) - - // Insert a torrent row. - _, err := d.Exec(` - INSERT INTO torrents - (event_id, info_hash, pubkey, created_at, ingested_at, title, description, raw_event) - VALUES - ('evt1', 'aabbcc', 'pubkey1', 1715000000, 1715000001, 'test torrent title', 'test description', '{}') - `) - if err != nil { - t.Fatalf("insert torrent: %v", err) - } - - // FTS5 search. - var rowid int64 - err = d.QueryRow(`SELECT rowid FROM torrents_fts WHERE torrents_fts MATCH 'test'`).Scan(&rowid) - if err == sql.ErrNoRows { - t.Error("FTS5 search for 'test' returned no rows; trigger may not be working") - } else if err != nil { - t.Fatalf("FTS5 search error: %v", err) - } -} - -func TestGetStats(t *testing.T) { - d := tempDB(t) - ctx := context.Background() - - stats, err := d.GetStats(ctx) - if err != nil { - t.Fatalf("GetStats error: %v", err) - } - if stats.EventsTotal != 0 { - t.Errorf("expected EventsTotal=0, got %d", stats.EventsTotal) - } - if stats.LastEventAt != nil { - t.Errorf("expected LastEventAt=nil, got %v", stats.LastEventAt) - } -} diff --git a/archive/go/internal/db/migrations/001_initial.sql b/archive/go/internal/db/migrations/001_initial.sql deleted file mode 100644 index 4651734..0000000 --- a/archive/go/internal/db/migrations/001_initial.sql +++ /dev/null @@ -1,120 +0,0 @@ -CREATE TABLE torrents ( - event_id TEXT PRIMARY KEY, - info_hash TEXT NOT NULL, - pubkey TEXT NOT NULL, - created_at INTEGER NOT NULL, - ingested_at INTEGER NOT NULL, - title TEXT NOT NULL, - description TEXT, - size_bytes INTEGER, - category TEXT, - newznab_cat INTEGER, - imdb_id TEXT, - tmdb_id TEXT, - tvdb_id TEXT, - season INTEGER, - episode INTEGER, - quality TEXT, - source TEXT, - raw_event TEXT NOT NULL -); - -CREATE UNIQUE INDEX idx_torrents_event_id ON torrents(event_id); -CREATE INDEX idx_torrents_info_hash ON torrents(info_hash); -CREATE INDEX idx_torrents_pubkey ON torrents(pubkey); -CREATE INDEX idx_torrents_imdb ON torrents(imdb_id) WHERE imdb_id IS NOT NULL; -CREATE INDEX idx_torrents_tmdb ON torrents(tmdb_id) WHERE tmdb_id IS NOT NULL; -CREATE INDEX idx_torrents_tvdb ON torrents(tvdb_id) WHERE tvdb_id IS NOT NULL; -CREATE INDEX idx_torrents_created ON torrents(created_at DESC); -CREATE INDEX idx_torrents_cat ON torrents(newznab_cat); - -CREATE VIRTUAL TABLE torrents_fts USING fts5( - title, description, - content='torrents', - content_rowid='rowid', - tokenize='unicode61 remove_diacritics 2' -); - -CREATE TRIGGER torrents_ai AFTER INSERT ON torrents BEGIN - INSERT INTO torrents_fts(rowid, title, description) VALUES (new.rowid, new.title, new.description); -END; - -CREATE TRIGGER torrents_ad AFTER DELETE ON torrents BEGIN - INSERT INTO torrents_fts(torrents_fts, rowid, title, description) VALUES ('delete', old.rowid, old.title, old.description); -END; - -CREATE TRIGGER torrents_au AFTER UPDATE ON torrents BEGIN - INSERT INTO torrents_fts(torrents_fts, rowid, title, description) VALUES ('delete', old.rowid, old.title, old.description); - INSERT INTO torrents_fts(rowid, title, description) VALUES (new.rowid, new.title, new.description); -END; - -CREATE TABLE files ( - event_id TEXT NOT NULL, - idx INTEGER NOT NULL, - path TEXT NOT NULL, - size_bytes INTEGER, - PRIMARY KEY (event_id, idx), - FOREIGN KEY (event_id) REFERENCES torrents(event_id) ON DELETE CASCADE -); - -CREATE TABLE trackers ( - event_id TEXT NOT NULL, - url TEXT NOT NULL, - PRIMARY KEY (event_id, url), - FOREIGN KEY (event_id) REFERENCES torrents(event_id) ON DELETE CASCADE -); - -CREATE TABLE tags ( - event_id TEXT NOT NULL, - tag TEXT NOT NULL, - PRIMARY KEY (event_id, tag), - FOREIGN KEY (event_id) REFERENCES torrents(event_id) ON DELETE CASCADE -); - -CREATE INDEX idx_tags_tag ON tags(tag); - -CREATE TABLE publishers ( - pubkey TEXT PRIMARY KEY, - npub TEXT, - name TEXT, - trust REAL DEFAULT 0, - notes TEXT, - blocked INTEGER DEFAULT 0, - torrents_n INTEGER DEFAULT 0, - first_seen INTEGER, - last_seen INTEGER -); - -CREATE TABLE relays ( - url TEXT PRIMARY KEY, - enabled INTEGER DEFAULT 1, - last_event INTEGER, - last_sync INTEGER, - notes TEXT -); - -CREATE TABLE api_keys ( - key TEXT PRIMARY KEY, - label TEXT NOT NULL, - created_at INTEGER NOT NULL, - visibility TEXT NOT NULL DEFAULT 'all', - curation_set TEXT, - last_used INTEGER -); - -CREATE TABLE health ( - info_hash TEXT PRIMARY KEY, - seeders INTEGER, - leechers INTEGER, - checked_at INTEGER NOT NULL -); - -CREATE TABLE comments ( - event_id TEXT PRIMARY KEY, - torrent_event_id TEXT NOT NULL, - pubkey TEXT NOT NULL, - created_at INTEGER NOT NULL, - content TEXT NOT NULL, - raw_event TEXT NOT NULL, - FOREIGN KEY (torrent_event_id) REFERENCES torrents(event_id) ON DELETE CASCADE -); diff --git a/archive/go/internal/db/migrations/002_drop_apikey_visibility.sql b/archive/go/internal/db/migrations/002_drop_apikey_visibility.sql deleted file mode 100644 index 0ea6334..0000000 --- a/archive/go/internal/db/migrations/002_drop_apikey_visibility.sql +++ /dev/null @@ -1,14 +0,0 @@ --- SQLite does not support DROP COLUMN before 3.35.0; use table rebuild. -CREATE TABLE api_keys_new ( - key TEXT PRIMARY KEY, - label TEXT NOT NULL, - created_at INTEGER NOT NULL, - last_used INTEGER -); - -INSERT INTO api_keys_new (key, label, created_at, last_used) - SELECT key, label, created_at, last_used FROM api_keys; - -DROP TABLE api_keys; - -ALTER TABLE api_keys_new RENAME TO api_keys; diff --git a/archive/go/internal/db/queries.go b/archive/go/internal/db/queries.go deleted file mode 100644 index 22fea2b..0000000 --- a/archive/go/internal/db/queries.go +++ /dev/null @@ -1,343 +0,0 @@ -package db - -import ( - "context" - "database/sql" - "fmt" - "strings" - "time" -) - -// TorrentRecord holds all data for a single NIP-35 event to be stored. -type TorrentRecord struct { - EventID string - InfoHash string - Pubkey string - CreatedAt int64 - IngestedAt int64 - Title string - Description string - SizeBytes *int64 - Category string - NewznabCat *int - ImdbID string - TmdbID string - TvdbID string - Season *int - Episode *int - Quality string - Source string - RawEvent string - Trackers []string - Tags []string - Files []FileRecord -} - -// FileRecord represents a single file within a torrent. -type FileRecord struct { - Path string - SizeBytes *int64 -} - -// InsertTorrent upserts a torrent and its related rows in a single transaction. -// Duplicate event_id is silently ignored. -func (d *DB) InsertTorrent(ctx context.Context, r TorrentRecord) error { - tx, err := d.DB.BeginTx(ctx, nil) - if err != nil { - return fmt.Errorf("insert torrent: begin: %w", err) - } - defer tx.Rollback() - - _, err = tx.ExecContext(ctx, ` - INSERT OR IGNORE INTO torrents - (event_id, info_hash, pubkey, created_at, ingested_at, title, description, - size_bytes, category, newznab_cat, imdb_id, tmdb_id, tvdb_id, - season, episode, quality, source, raw_event) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - r.EventID, r.InfoHash, r.Pubkey, r.CreatedAt, r.IngestedAt, - r.Title, r.Description, nullInt64(r.SizeBytes), r.Category, nullInt(r.NewznabCat), - nullStr(r.ImdbID), nullStr(r.TmdbID), nullStr(r.TvdbID), - nullInt(r.Season), nullInt(r.Episode), nullStr(r.Quality), nullStr(r.Source), - r.RawEvent, - ) - if err != nil { - return fmt.Errorf("insert torrent row: %w", err) - } - - // Check whether the row was actually inserted (INSERT OR IGNORE skips duplicates). - var changes int64 - if err := tx.QueryRowContext(ctx, `SELECT changes()`).Scan(&changes); err != nil { - return fmt.Errorf("check changes: %w", err) - } - if changes == 0 { - return tx.Commit() // duplicate, nothing more to do - } - - for _, url := range r.Trackers { - if _, err := tx.ExecContext(ctx, - `INSERT OR IGNORE INTO trackers (event_id, url) VALUES (?, ?)`, r.EventID, url); err != nil { - return fmt.Errorf("insert tracker: %w", err) - } - } - for _, tag := range r.Tags { - if _, err := tx.ExecContext(ctx, - `INSERT OR IGNORE INTO tags (event_id, tag) VALUES (?, ?)`, r.EventID, tag); err != nil { - return fmt.Errorf("insert tag: %w", err) - } - } - for i, f := range r.Files { - if _, err := tx.ExecContext(ctx, - `INSERT OR IGNORE INTO files (event_id, idx, path, size_bytes) VALUES (?, ?, ?, ?)`, - r.EventID, i, f.Path, nullInt64(f.SizeBytes)); err != nil { - return fmt.Errorf("insert file: %w", err) - } - } - - return tx.Commit() -} - -// TorrentRow is a single result row from Search. -type TorrentRow struct { - EventID string - InfoHash string - Pubkey string - CreatedAt int64 - Title string - Description string - SizeBytes *int64 - NewznabCat *int - ImdbID string - TmdbID string - TvdbID string - Season *int - Episode *int - Quality string - Source string -} - -// SearchParams controls a Search query. -type SearchParams struct { - Query string // FTS5 query; empty returns latest events - Cats []int // newznab category IDs; empty = all categories - Limit int // 0 defaults to 50; capped at 100 - Offset int -} - -// Search runs an FTS5 query (or a latest-events scan when Query is empty) -// and returns matching torrent rows. -func (d *DB) Search(ctx context.Context, p SearchParams) ([]TorrentRow, error) { - if p.Limit <= 0 { - p.Limit = 50 - } - if p.Limit > 100 { - p.Limit = 100 - } - - catWhere, catArgs := buildCatWhere(p.Cats) - - var query string - var args []interface{} - - if p.Query != "" { - query = `SELECT t.event_id, t.info_hash, t.pubkey, t.created_at, t.title, - t.description, t.size_bytes, t.newznab_cat, t.imdb_id, - t.tmdb_id, t.tvdb_id, t.season, t.episode, t.quality, t.source - FROM torrents_fts - JOIN torrents t ON torrents_fts.rowid = t.rowid - WHERE torrents_fts MATCH ?` - args = append(args, p.Query) - if catWhere != "" { - query += " AND (" + catWhere + ")" - args = append(args, catArgs...) - } - query += " ORDER BY torrents_fts.rank LIMIT ? OFFSET ?" - } else { - query = `SELECT event_id, info_hash, pubkey, created_at, title, - description, size_bytes, newznab_cat, imdb_id, - tmdb_id, tvdb_id, season, episode, quality, source - FROM torrents` - if catWhere != "" { - query += " WHERE " + catWhere - args = append(args, catArgs...) - } - query += " ORDER BY created_at DESC LIMIT ? OFFSET ?" - } - args = append(args, p.Limit, p.Offset) - - rows, err := d.DB.QueryContext(ctx, query, args...) - if err != nil { - return nil, fmt.Errorf("search: %w", err) - } - defer rows.Close() - - var results []TorrentRow - for rows.Next() { - var r TorrentRow - var sizeBytes sql.NullInt64 - var newznabCat sql.NullInt64 - var season, episode sql.NullInt64 - var imdbID, tmdbID, tvdbID, quality, source sql.NullString - if err := rows.Scan( - &r.EventID, &r.InfoHash, &r.Pubkey, &r.CreatedAt, &r.Title, - &r.Description, &sizeBytes, &newznabCat, &imdbID, - &tmdbID, &tvdbID, &season, &episode, &quality, &source, - ); err != nil { - return nil, fmt.Errorf("search scan: %w", err) - } - if sizeBytes.Valid { - v := sizeBytes.Int64 - r.SizeBytes = &v - } - if newznabCat.Valid { - v := int(newznabCat.Int64) - r.NewznabCat = &v - } - if season.Valid { - v := int(season.Int64) - r.Season = &v - } - if episode.Valid { - v := int(episode.Int64) - r.Episode = &v - } - r.ImdbID = imdbID.String - r.TmdbID = tmdbID.String - r.TvdbID = tvdbID.String - r.Quality = quality.String - r.Source = source.String - results = append(results, r) - } - return results, rows.Err() -} - -// GetTrackers returns all tracker URLs for the given event. -func (d *DB) GetTrackers(ctx context.Context, eventID string) ([]string, error) { - rows, err := d.DB.QueryContext(ctx, `SELECT url FROM trackers WHERE event_id = ?`, eventID) - if err != nil { - return nil, fmt.Errorf("get trackers: %w", err) - } - defer rows.Close() - var urls []string - for rows.Next() { - var u string - if err := rows.Scan(&u); err != nil { - return nil, err - } - urls = append(urls, u) - } - return urls, rows.Err() -} - -// APIKey holds a validated API key record. -type APIKey struct { - Key string - Label string - CreatedAt int64 -} - -// GetAPIKey returns the key record for the given raw key string, or nil if not found. -func (d *DB) GetAPIKey(ctx context.Context, key string) (*APIKey, error) { - var ak APIKey - err := d.DB.QueryRowContext(ctx, - `SELECT key, label, created_at FROM api_keys WHERE key = ?`, key, - ).Scan(&ak.Key, &ak.Label, &ak.CreatedAt) - if err == sql.ErrNoRows { - return nil, nil - } - if err != nil { - return nil, fmt.Errorf("get api key: %w", err) - } - // Update last_used without blocking. - _, _ = d.DB.ExecContext(ctx, `UPDATE api_keys SET last_used = ? WHERE key = ?`, time.Now().Unix(), key) - return &ak, nil -} - -// CreateAPIKey inserts a new API key record. -func (d *DB) CreateAPIKey(ctx context.Context, key, label string) error { - _, err := d.DB.ExecContext(ctx, - `INSERT INTO api_keys (key, label, created_at) VALUES (?, ?, ?)`, - key, label, time.Now().Unix(), - ) - if err != nil { - return fmt.Errorf("create api key: %w", err) - } - return nil -} - -// GetRelayLastEvent returns the created_at of the most recent event seen from -// the given relay, or 0 if none has been recorded. -func (d *DB) GetRelayLastEvent(ctx context.Context, url string) (int64, error) { - var ts sql.NullInt64 - err := d.DB.QueryRowContext(ctx, - `SELECT last_event FROM relays WHERE url = ?`, url, - ).Scan(&ts) - if err == sql.ErrNoRows { - return 0, nil - } - if err != nil { - return 0, fmt.Errorf("get relay last event: %w", err) - } - return ts.Int64, nil -} - -// UpsertRelay ensures a relay URL exists in the relays table. -func (d *DB) UpsertRelay(ctx context.Context, url string) error { - _, err := d.DB.ExecContext(ctx, - `INSERT OR IGNORE INTO relays (url, enabled) VALUES (?, 1)`, url) - return err -} - -// UpdateRelaySync records the last time we successfully synced with a relay. -func (d *DB) UpdateRelaySync(ctx context.Context, url string, ts int64) error { - _, err := d.DB.ExecContext(ctx, - `UPDATE relays SET last_sync = ? WHERE url = ?`, ts, url) - return err -} - -// UpdateRelayLastEvent records the created_at of the most recent event from a relay. -func (d *DB) UpdateRelayLastEvent(ctx context.Context, url string, ts int64) error { - _, err := d.DB.ExecContext(ctx, - `UPDATE relays SET last_event = ? WHERE url = ?`, ts, url) - return err -} - -// buildCatWhere constructs the WHERE fragment and args for category filtering. -// Parent categories (multiples of 1000) match the full sub-range. -func buildCatWhere(cats []int) (string, []interface{}) { - if len(cats) == 0 { - return "", nil - } - var parts []string - var args []interface{} - for _, cat := range cats { - if cat%1000 == 0 { - parts = append(parts, "newznab_cat BETWEEN ? AND ?") - args = append(args, cat, cat+999) - } else { - parts = append(parts, "newznab_cat = ?") - args = append(args, cat) - } - } - return strings.Join(parts, " OR "), args -} - -func nullStr(s string) interface{} { - if s == "" { - return nil - } - return s -} - -func nullInt(p *int) interface{} { - if p == nil { - return nil - } - return *p -} - -func nullInt64(p *int64) interface{} { - if p == nil { - return nil - } - return *p -} diff --git a/archive/go/internal/nostr/parser.go b/archive/go/internal/nostr/parser.go deleted file mode 100644 index 6c6c1d7..0000000 --- a/archive/go/internal/nostr/parser.go +++ /dev/null @@ -1,130 +0,0 @@ -package nostr - -import ( - "fmt" - "regexp" - "strconv" - "strings" - "time" - - nostrlib "github.com/nbd-wtf/go-nostr" - - "git.utn.lol/enki/kindexr/internal/db" - "git.utn.lol/enki/kindexr/internal/torznab" -) - -var infoHashRe = regexp.MustCompile(`^[0-9a-fA-F]{40}$`) - -// Parse converts a kind 2003 Nostr event into a TorrentRecord ready for storage. -// Returns an error if the event is missing required fields. -func Parse(event *nostrlib.Event) (*db.TorrentRecord, error) { - if event.Kind != nostrlib.KindTorrent { - return nil, fmt.Errorf("wrong kind: %d", event.Kind) - } - - r := &db.TorrentRecord{ - EventID: event.ID, - Pubkey: event.PubKey, - CreatedAt: int64(event.CreatedAt), - IngestedAt: time.Now().Unix(), - RawEvent: event.String(), - } - - var totalSize int64 - hasTotalSize := false - - for _, tag := range event.Tags { - if len(tag) < 2 { - continue - } - switch tag[0] { - case "x": - hash := strings.ToLower(tag[1]) - if !infoHashRe.MatchString(hash) { - return nil, fmt.Errorf("invalid info_hash: %q", tag[1]) - } - r.InfoHash = hash - - case "title": - r.Title = tag[1] - - case "file": - path := tag[1] - rec := db.FileRecord{Path: path} - if len(tag) >= 3 { - if sz, err := strconv.ParseInt(tag[2], 10, 64); err == nil { - rec.SizeBytes = &sz - totalSize += sz - hasTotalSize = true - } - } - r.Files = append(r.Files, rec) - - case "tracker": - r.Trackers = append(r.Trackers, tag[1]) - - case "i": - parseITag(tag[1], r) - - case "t": - r.Tags = append(r.Tags, tag[1]) - } - } - - if r.InfoHash == "" { - return nil, fmt.Errorf("missing x (info_hash) tag") - } - - // Fall back to first line of content if no title tag. - if r.Title == "" { - if idx := strings.IndexByte(event.Content, '\n'); idx >= 0 { - r.Title = strings.TrimSpace(event.Content[:idx]) - } else { - r.Title = strings.TrimSpace(event.Content) - } - } - if r.Title == "" { - return nil, fmt.Errorf("event has no title") - } - - r.Description = event.Content - - if hasTotalSize { - r.SizeBytes = &totalSize - } - - return r, nil -} - -// parseITag parses a single value from an ["i", ""] tag and sets -// the appropriate fields on r. -func parseITag(value string, r *db.TorrentRecord) { - switch { - case strings.HasPrefix(value, "newznab:"): - if cat, err := strconv.Atoi(strings.TrimPrefix(value, "newznab:")); err == nil { - r.NewznabCat = &cat - } - - case strings.HasPrefix(value, "tcat:"): - // Only use tcat if newznab: wasn't already set. - if r.NewznabCat == nil { - cat := torznab.TcatToNewznab(strings.TrimPrefix(value, "tcat:")) - r.Category = strings.TrimPrefix(value, "tcat:") - r.NewznabCat = &cat - } else { - r.Category = strings.TrimPrefix(value, "tcat:") - } - - case strings.HasPrefix(value, "imdb:"): - r.ImdbID = strings.TrimPrefix(value, "imdb:") - - case strings.HasPrefix(value, "tmdb:movie:"): - r.TmdbID = "movie:" + strings.TrimPrefix(value, "tmdb:movie:") - - case strings.HasPrefix(value, "tmdb:tv:"): - r.TmdbID = "tv:" + strings.TrimPrefix(value, "tmdb:tv:") - - case strings.HasPrefix(value, "tvdb:"): - r.TvdbID = strings.TrimPrefix(value, "tvdb:") - } -} diff --git a/archive/go/internal/nostr/parser_test.go b/archive/go/internal/nostr/parser_test.go deleted file mode 100644 index fe87bef..0000000 --- a/archive/go/internal/nostr/parser_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package nostr_test - -import ( - "testing" - - nostrlib "github.com/nbd-wtf/go-nostr" - - "git.utn.lol/enki/kindexr/internal/nostr" -) - -func makeEvent(tags nostrlib.Tags, content string) *nostrlib.Event { - return &nostrlib.Event{ - ID: "aaaa111100000000000000000000000000000000000000000000000000000000", - Kind: nostrlib.KindTorrent, - PubKey: "bbbb222200000000000000000000000000000000000000000000000000000000", - CreatedAt: 1715000000, - Content: content, - Tags: tags, - } -} - -func TestParseBasic(t *testing.T) { - event := makeEvent(nostrlib.Tags{ - {"x", "abcdef1234abcdef1234abcdef1234abcdef1234"}, - {"title", "Some.Show.S01E02.1080p.WEB-DL.x265-GRP"}, - {"tracker", "udp://tracker.opentrackr.org:1337"}, - {"i", "newznab:5040"}, - {"t", "tv"}, - }, "a description") - - rec, err := nostr.Parse(event) - if err != nil { - t.Fatalf("Parse error: %v", err) - } - if rec.InfoHash != "abcdef1234abcdef1234abcdef1234abcdef1234" { - t.Errorf("wrong info_hash: %q", rec.InfoHash) - } - if rec.Title != "Some.Show.S01E02.1080p.WEB-DL.x265-GRP" { - t.Errorf("wrong title: %q", rec.Title) - } - if len(rec.Trackers) != 1 || rec.Trackers[0] != "udp://tracker.opentrackr.org:1337" { - t.Errorf("wrong trackers: %v", rec.Trackers) - } - if rec.NewznabCat == nil || *rec.NewznabCat != 5040 { - t.Errorf("wrong newznab_cat: %v", rec.NewznabCat) - } - if len(rec.Tags) != 1 || rec.Tags[0] != "tv" { - t.Errorf("wrong tags: %v", rec.Tags) - } -} - -func TestParseTitleFallback(t *testing.T) { - event := makeEvent(nostrlib.Tags{ - {"x", "abcdef1234abcdef1234abcdef1234abcdef1234"}, - }, "Title From Content\nmore description") - - rec, err := nostr.Parse(event) - if err != nil { - t.Fatalf("Parse error: %v", err) - } - if rec.Title != "Title From Content" { - t.Errorf("expected title from content first line, got %q", rec.Title) - } -} - -func TestParseMissingInfoHash(t *testing.T) { - event := makeEvent(nostrlib.Tags{ - {"title", "Some Torrent"}, - }, "") - - _, err := nostr.Parse(event) - if err == nil { - t.Fatal("expected error for missing info_hash") - } -} - -func TestParseInvalidInfoHash(t *testing.T) { - event := makeEvent(nostrlib.Tags{ - {"x", "not-a-hash"}, - {"title", "Some Torrent"}, - }, "") - - _, err := nostr.Parse(event) - if err == nil { - t.Fatal("expected error for invalid info_hash") - } -} - -func TestParseFileSizes(t *testing.T) { - event := makeEvent(nostrlib.Tags{ - {"x", "abcdef1234abcdef1234abcdef1234abcdef1234"}, - {"title", "My Torrent"}, - {"file", "file1.mkv", "1000000"}, - {"file", "file2.srt", "5000"}, - }, "") - - rec, err := nostr.Parse(event) - if err != nil { - t.Fatalf("Parse error: %v", err) - } - if rec.SizeBytes == nil { - t.Fatal("expected SizeBytes to be set") - } - if *rec.SizeBytes != 1005000 { - t.Errorf("expected SizeBytes=1005000, got %d", *rec.SizeBytes) - } - if len(rec.Files) != 2 { - t.Errorf("expected 2 files, got %d", len(rec.Files)) - } -} - -func TestParseITagIDs(t *testing.T) { - event := makeEvent(nostrlib.Tags{ - {"x", "abcdef1234abcdef1234abcdef1234abcdef1234"}, - {"title", "Breaking Bad"}, - {"i", "imdb:tt0903747"}, - {"i", "tmdb:tv:1396"}, - {"i", "tvdb:81189"}, - {"i", "tcat:video,tv"}, - }, "") - - rec, err := nostr.Parse(event) - if err != nil { - t.Fatalf("Parse error: %v", err) - } - if rec.ImdbID != "tt0903747" { - t.Errorf("wrong ImdbID: %q", rec.ImdbID) - } - if rec.TmdbID != "tv:1396" { - t.Errorf("wrong TmdbID: %q", rec.TmdbID) - } - if rec.TvdbID != "81189" { - t.Errorf("wrong TvdbID: %q", rec.TvdbID) - } - // tcat should set NewznabCat to 5000 (video,tv) - if rec.NewznabCat == nil || *rec.NewznabCat != 5000 { - t.Errorf("expected NewznabCat=5000, got %v", rec.NewznabCat) - } -} - -func TestParseNewznabOverridesTcat(t *testing.T) { - event := makeEvent(nostrlib.Tags{ - {"x", "abcdef1234abcdef1234abcdef1234abcdef1234"}, - {"title", "Some UHD Show"}, - {"i", "tcat:video,tv"}, - {"i", "newznab:5045"}, - }, "") - - rec, err := nostr.Parse(event) - if err != nil { - t.Fatalf("Parse error: %v", err) - } - // newznab: tag takes precedence over tcat: - if rec.NewznabCat == nil || *rec.NewznabCat != 5045 { - t.Errorf("expected NewznabCat=5045, got %v", rec.NewznabCat) - } -} diff --git a/archive/go/internal/nostr/reader.go b/archive/go/internal/nostr/reader.go deleted file mode 100644 index 75fbeed..0000000 --- a/archive/go/internal/nostr/reader.go +++ /dev/null @@ -1,140 +0,0 @@ -package nostr - -import ( - "context" - "log/slog" - "sync/atomic" - "time" - - nostrlib "github.com/nbd-wtf/go-nostr" - - "git.utn.lol/enki/kindexr/internal/config" - "git.utn.lol/enki/kindexr/internal/db" -) - -// Reader subscribes to Nostr relays and indexes kind 2003 events. -type Reader struct { - cfg *config.Config - db *db.DB - connected atomic.Int32 -} - -// New creates a Reader. -func New(cfg *config.Config, database *db.DB) *Reader { - return &Reader{cfg: cfg, db: database} -} - -// ConnectedCount returns the number of currently connected relays. -func (rd *Reader) ConnectedCount() int { - return int(rd.connected.Load()) -} - -// Start launches one goroutine per configured relay. It returns immediately; -// relay connections run in the background until ctx is cancelled. -func (rd *Reader) Start(ctx context.Context) { - for _, relayURL := range rd.cfg.Relays { - // Seed the relays table so we can track sync state. - _ = rd.db.UpsertRelay(ctx, relayURL) - go rd.connectLoop(ctx, relayURL) - } -} - -// connectLoop keeps re-connecting to a single relay with exponential backoff. -// Backoff resets to the base delay if the connection stayed up long enough to -// be considered successful (30 seconds), so a briefly-dropped live relay -// reconnects quickly rather than waiting minutes. -func (rd *Reader) connectLoop(ctx context.Context, relayURL string) { - const baseBackoff = 5 * time.Second - const maxBackoff = 5 * time.Minute - const successThreshold = 30 * time.Second - - backoff := baseBackoff - - for { - if ctx.Err() != nil { - return - } - - start := time.Now() - if err := rd.runRelay(ctx, relayURL); err != nil { - slog.Warn("relay disconnected", "url", relayURL, "err", err, "retry_in", backoff) - } - - if time.Since(start) >= successThreshold { - backoff = baseBackoff - } - - select { - case <-ctx.Done(): - return - case <-time.After(backoff): - if backoff < maxBackoff { - backoff *= 2 - } - } - } -} - -// runRelay connects, subscribes, and streams events until the connection drops -// or ctx is cancelled. -func (rd *Reader) runRelay(ctx context.Context, relayURL string) error { - relay, err := nostrlib.RelayConnect(ctx, relayURL) - if err != nil { - return err - } - defer relay.Close() - - rd.connected.Add(1) - defer rd.connected.Add(-1) - - _ = rd.db.UpdateRelaySync(ctx, relayURL, time.Now().Unix()) - slog.Info("relay connected", "url", relayURL) - - backfillSince := time.Now().Add(-time.Duration(rd.cfg.BackfillDays) * 24 * time.Hour).Unix() - lastEvent, _ := rd.db.GetRelayLastEvent(ctx, relayURL) - if lastEvent > backfillSince { - backfillSince = lastEvent - } - since := nostrlib.Timestamp(backfillSince) - - sub, err := relay.Subscribe(ctx, nostrlib.Filters{{ - Kinds: []int{nostrlib.KindTorrent}, - Since: &since, - }}) - if err != nil { - return err - } - - for { - select { - case <-ctx.Done(): - return nil - - case event, ok := <-sub.Events: - if !ok { - return nil - } - rd.handleEvent(ctx, relayURL, event) - - case <-sub.EndOfStoredEvents: - slog.Info("relay EOSE", "url", relayURL) - } - } -} - -// handleEvent parses and stores a single event. -func (rd *Reader) handleEvent(ctx context.Context, relayURL string, event *nostrlib.Event) { - rec, err := Parse(event) - if err != nil { - slog.Debug("skipping event", "id", event.ID, "reason", err) - return - } - - if err := rd.db.InsertTorrent(ctx, *rec); err != nil { - slog.Error("db insert failed", "id", event.ID, "err", err) - return - } - - _ = rd.db.UpdateRelayLastEvent(ctx, relayURL, int64(event.CreatedAt)) - slog.Debug("indexed event", "id", event.ID, "title", rec.Title) -} diff --git a/archive/go/internal/server/server.go b/archive/go/internal/server/server.go deleted file mode 100644 index 2e76b26..0000000 --- a/archive/go/internal/server/server.go +++ /dev/null @@ -1,93 +0,0 @@ -// Package server provides the HTTP server for kindexr, including the /health endpoint. -package server - -import ( - "encoding/json" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "git.utn.lol/enki/kindexr/internal/config" - "git.utn.lol/enki/kindexr/internal/db" - "git.utn.lol/enki/kindexr/internal/torznab" -) - -// Server holds the HTTP server state. -type Server struct { - cfg *config.Config - db *db.DB - router chi.Router - version string - connectedRelays func() int // returns live relay count; nil = always 0 -} - -// New creates a new Server with the given config, database, and version string. -func New(cfg *config.Config, database *db.DB, version string) *Server { - s := &Server{ - cfg: cfg, - db: database, - version: version, - } - s.router = s.buildRouter() - return s -} - -// SetConnectedRelays wires in a function that returns the current connected -// relay count. Call this after the reader is started. -func (s *Server) SetConnectedRelays(fn func() int) { - s.connectedRelays = fn -} - -// Handler returns the root http.Handler for the server. -func (s *Server) Handler() http.Handler { - return s.router -} - -// buildRouter wires up all routes and middleware. -func (s *Server) buildRouter() chi.Router { - r := chi.NewRouter() - r.Use(middleware.Logger) - r.Use(middleware.Recoverer) - - r.Get("/health", s.healthHandler) - - tz := torznab.New(s.cfg, s.db, s.version) - tz.Mount(r) - - return r -} - -// healthResponse is the JSON body returned by GET /health. -type healthResponse struct { - Status string `json:"status"` - Version string `json:"version"` - RelaysConnected int `json:"relays_connected"` - EventsTotal int64 `json:"events_total"` - LastEventAt *int64 `json:"last_event_at"` -} - -// healthHandler handles GET /health. -func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { - stats, err := s.db.GetStats(r.Context()) - if err != nil { - http.Error(w, `{"error":"internal server error"}`, http.StatusInternalServerError) - return - } - - relaysConnected := 0 - if s.connectedRelays != nil { - relaysConnected = s.connectedRelays() - } - - resp := healthResponse{ - Status: "ok", - Version: s.version, - RelaysConnected: relaysConnected, - EventsTotal: stats.EventsTotal, - LastEventAt: stats.LastEventAt, - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(resp) -} diff --git a/archive/go/internal/server/server_test.go b/archive/go/internal/server/server_test.go deleted file mode 100644 index 1e6b791..0000000 --- a/archive/go/internal/server/server_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package server_test - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "path/filepath" - "strings" - "testing" - - "git.utn.lol/enki/kindexr/internal/config" - "git.utn.lol/enki/kindexr/internal/db" - "git.utn.lol/enki/kindexr/internal/server" -) - -func newTestServer(t *testing.T) *httptest.Server { - t.Helper() - dir := t.TempDir() - database, err := db.Open(filepath.Join(dir, "test.db")) - if err != nil { - t.Fatalf("open test db: %v", err) - } - t.Cleanup(func() { database.Close() }) - - cfg, err := config.Load("") - if err != nil { - t.Fatalf("load config: %v", err) - } - - srv := server.New(cfg, database, "0.1.0-test") - return httptest.NewServer(srv.Handler()) -} - -func TestHealthEndpoint(t *testing.T) { - ts := newTestServer(t) - defer ts.Close() - - resp, err := http.Get(ts.URL + "/health") - if err != nil { - t.Fatalf("GET /health: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - t.Errorf("expected status 200, got %d", resp.StatusCode) - } - - var body map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { - t.Fatalf("decode JSON: %v", err) - } - - if body["status"] != "ok" { - t.Errorf("expected status=ok, got %v", body["status"]) - } - if _, ok := body["version"]; !ok { - t.Error("expected 'version' field in response") - } - if rc, ok := body["relays_connected"]; !ok { - t.Error("expected 'relays_connected' field in response") - } else if rc.(float64) != 0 { - t.Errorf("expected relays_connected=0, got %v", rc) - } - if et, ok := body["events_total"]; !ok { - t.Error("expected 'events_total' field in response") - } else if et.(float64) != 0 { - t.Errorf("expected events_total=0, got %v", et) - } - // last_event_at should be present (null is fine for empty DB). - if _, ok := body["last_event_at"]; !ok { - t.Error("expected 'last_event_at' field in response") - } -} - -func TestHealthContentType(t *testing.T) { - ts := newTestServer(t) - defer ts.Close() - - resp, err := http.Get(ts.URL + "/health") - if err != nil { - t.Fatalf("GET /health: %v", err) - } - defer resp.Body.Close() - - ct := resp.Header.Get("Content-Type") - if !strings.HasPrefix(ct, "application/json") { - t.Errorf("expected Content-Type application/json, got %q", ct) - } -} - -func TestHealthMethod(t *testing.T) { - ts := newTestServer(t) - defer ts.Close() - - // GET should return 200. - resp, err := http.Get(ts.URL + "/health") - if err != nil { - t.Fatalf("GET /health: %v", err) - } - resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Errorf("GET /health: expected 200, got %d", resp.StatusCode) - } - - // POST should return 405. - resp, err = http.Post(ts.URL+"/health", "application/json", nil) - if err != nil { - t.Fatalf("POST /health: %v", err) - } - resp.Body.Close() - if resp.StatusCode != http.StatusMethodNotAllowed { - t.Errorf("POST /health: expected 405, got %d", resp.StatusCode) - } -} diff --git a/archive/go/internal/torznab/caps.go b/archive/go/internal/torznab/caps.go deleted file mode 100644 index a9ed7bd..0000000 --- a/archive/go/internal/torznab/caps.go +++ /dev/null @@ -1,69 +0,0 @@ -package torznab - -import ( - "encoding/xml" - "net/http" -) - -func (s *Server) capsHandler(w http.ResponseWriter, r *http.Request) { - caps := Caps{ - Server: CapsServer{ - Version: s.version, - Title: "kindexr", - Strapline: "Nostr-native Torznab", - Email: "", - URL: s.cfg.Server.BaseURL + "/", - Image: "", - }, - Limits: CapsLimits{Max: 100, Default: 50}, - Searching: CapsSearching{ - Search: CapsSearch{Available: "yes", SupportedParams: "q"}, - TVSearch: CapsSearch{Available: "yes", SupportedParams: "q,season,ep,imdbid,tvdbid,tmdbid"}, - MovieSearch: CapsSearch{Available: "yes", SupportedParams: "q,imdbid,tmdbid"}, - MusicSearch: CapsSearch{Available: "yes", SupportedParams: "q,artist,album,year"}, - AudioSearch: CapsSearch{Available: "yes", SupportedParams: "q,artist,album,year"}, - BookSearch: CapsSearch{Available: "yes", SupportedParams: "q,author,title"}, - }, - Categories: CapsCategories{ - Categories: []CapsCategory{ - {ID: 2000, Name: "Movies", SubCats: []CapsSubcat{ - {ID: 2030, Name: "Movies/SD"}, - {ID: 2040, Name: "Movies/HD"}, - {ID: 2045, Name: "Movies/UHD"}, - {ID: 2050, Name: "Movies/BluRay"}, - {ID: 2060, Name: "Movies/3D"}, - }}, - {ID: 3000, Name: "Audio", SubCats: []CapsSubcat{ - {ID: 3010, Name: "Audio/MP3"}, - {ID: 3030, Name: "Audio/Audiobook"}, - {ID: 3040, Name: "Audio/Lossless"}, - }}, - {ID: 5000, Name: "TV", SubCats: []CapsSubcat{ - {ID: 5030, Name: "TV/SD"}, - {ID: 5040, Name: "TV/HD"}, - {ID: 5045, Name: "TV/UHD"}, - {ID: 5070, Name: "TV/Anime"}, - }}, - {ID: 7000, Name: "Books", SubCats: []CapsSubcat{ - {ID: 7020, Name: "Books/EBook"}, - {ID: 7030, Name: "Books/Comics"}, - }}, - {ID: 8000, Name: "Other", SubCats: nil}, - }, - }, - } - - writeXML(w, http.StatusOK, caps) -} - -func writeXML(w http.ResponseWriter, status int, v interface{}) { - out, err := xml.MarshalIndent(v, "", " ") - if err != nil { - http.Error(w, "xml marshal error", http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "application/xml; charset=utf-8") - w.WriteHeader(status) - w.Write([]byte(xml.Header)) - w.Write(out) -} diff --git a/archive/go/internal/torznab/categories.go b/archive/go/internal/torznab/categories.go deleted file mode 100644 index 85eb585..0000000 --- a/archive/go/internal/torznab/categories.go +++ /dev/null @@ -1,46 +0,0 @@ -package torznab - -// TcatToNewznab maps a tcat path (e.g. "video,tv,4k") to a newznab category ID. -// Returns 8000 (Other) for unrecognised paths. -func TcatToNewznab(tcat string) int { - switch tcat { - case "video,movie": - return 2000 - case "video,movie,sd": - return 2030 - case "video,movie,hd": - return 2040 - case "video,movie,4k", "video,movie,uhd": - return 2045 - case "video,movie,bluray", "video,movie,remux": - return 2050 - case "video,movie,3d": - return 2060 - case "video,tv": - return 5000 - case "video,tv,sd": - return 5030 - case "video,tv,hd": - return 5040 - case "video,tv,4k", "video,tv,uhd": - return 5045 - case "video,tv,anime": - return 5070 - case "audio,music": - return 3000 - case "audio,music,lossless": - return 3040 - case "audio,audiobook": - return 3030 - case "audio,music,mp3": - return 3010 - case "book": - return 7000 - case "book,ebook": - return 7020 - case "book,comic": - return 7030 - default: - return 8000 - } -} diff --git a/archive/go/internal/torznab/search.go b/archive/go/internal/torznab/search.go deleted file mode 100644 index a9dd784..0000000 --- a/archive/go/internal/torznab/search.go +++ /dev/null @@ -1,157 +0,0 @@ -package torznab - -import ( - "fmt" - "net/http" - "net/url" - "strconv" - "strings" - "time" - - "github.com/nbd-wtf/go-nostr/nip19" - - "git.utn.lol/enki/kindexr/internal/db" -) - -func (s *Server) searchHandler(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query() - - p := db.SearchParams{ - Query: q.Get("q"), - Limit: parseIntParam(q.Get("limit"), 50), - Offset: parseIntParam(q.Get("offset"), 0), - Cats: parseCats(q.Get("cat")), - } - - rows, err := s.db.Search(r.Context(), p) - if err != nil { - writeError(w, http.StatusInternalServerError, 200, "search error: "+err.Error()) - return - } - - items := make([]RSSItem, 0, len(rows)) - for _, row := range rows { - trackers, err := s.db.GetTrackers(r.Context(), row.EventID) - if err != nil { - continue - } - item := buildItem(s.cfg.Server.BaseURL, row, trackers) - items = append(items, item) - } - - feed := RSS{ - Version: "2.0", - Atom: "http://www.w3.org/2005/Atom", - Torznab: "http://torznab.com/schemas/2015/feed", - Channel: RSSChannel{ - AtomLink: RSSAtomLink{Rel: "self", Type: "application/rss+xml"}, - Title: "kindexr", - Description: "Nostr NIP-35 torrent index", - Link: s.cfg.Server.BaseURL, - Language: "en-us", - Category: "search", - Items: items, - }, - } - - writeXML(w, http.StatusOK, feed) -} - -func buildItem(baseURL string, row db.TorrentRow, trackers []string) RSSItem { - magnet := buildMagnet(row.InfoHash, row.Title, trackers) - - guid := "kindexr:" + row.EventID - if note, err := nip19.EncodeNote(row.EventID); err == nil { - guid = "nostr:" + note - } - - pubDate := time.Unix(row.CreatedAt, 0).UTC().Format(time.RFC1123Z) - - attrs := []TorznabAttr{ - {Name: "infohash", Value: row.InfoHash}, - {Name: "magneturl", Value: magnet}, - {Name: "downloadvolumefactor", Value: "0"}, - {Name: "uploadvolumefactor", Value: "1"}, - } - var sizeBytes int64 - if row.SizeBytes != nil { - sizeBytes = *row.SizeBytes - attrs = append(attrs, TorznabAttr{Name: "size", Value: fmt.Sprintf("%d", sizeBytes)}) - } - if row.NewznabCat != nil { - parent := (*row.NewznabCat / 1000) * 1000 - attrs = append(attrs, - TorznabAttr{Name: "category", Value: strconv.Itoa(*row.NewznabCat)}, - TorznabAttr{Name: "category", Value: strconv.Itoa(parent)}, - ) - } - if row.ImdbID != "" { - // Strip leading "tt" for the attr value — *arr expects bare digits. - imdb := strings.TrimPrefix(row.ImdbID, "tt") - attrs = append(attrs, TorznabAttr{Name: "imdbid", Value: imdb}) - } - if row.TmdbID != "" { - // tmdb_id stored as "movie:12345" or "tv:67890"; extract just the ID part. - parts := strings.SplitN(row.TmdbID, ":", 2) - if len(parts) == 2 { - attrs = append(attrs, TorznabAttr{Name: "tmdbid", Value: parts[1]}) - } - } - if row.TvdbID != "" { - attrs = append(attrs, TorznabAttr{Name: "tvdbid", Value: row.TvdbID}) - } - - return RSSItem{ - Title: row.Title, - GUID: RSSGUID{IsPermaLink: "false", Value: guid}, - Link: magnet, - PubDate: pubDate, - Size: sizeBytes, - Enclosure: RSSEnclosure{ - URL: magnet, - Length: sizeBytes, - Type: "application/x-bittorrent", - }, - Attrs: attrs, - } -} - -func buildMagnet(infoHash, title string, trackers []string) string { - var sb strings.Builder - sb.WriteString("magnet:?xt=urn:btih:") - sb.WriteString(infoHash) - if title != "" { - sb.WriteString("&dn=") - sb.WriteString(url.QueryEscape(title)) - } - for _, tr := range trackers { - sb.WriteString("&tr=") - sb.WriteString(url.QueryEscape(tr)) - } - return sb.String() -} - -func parseIntParam(s string, def int) int { - if s == "" { - return def - } - v, err := strconv.Atoi(s) - if err != nil || v < 0 { - return def - } - return v -} - -func parseCats(s string) []int { - if s == "" { - return nil - } - var cats []int - for _, part := range strings.Split(s, ",") { - part = strings.TrimSpace(part) - if v, err := strconv.Atoi(part); err == nil { - cats = append(cats, v) - } - } - return cats -} diff --git a/archive/go/internal/torznab/server.go b/archive/go/internal/torznab/server.go deleted file mode 100644 index 84f913c..0000000 --- a/archive/go/internal/torznab/server.go +++ /dev/null @@ -1,69 +0,0 @@ -package torznab - -import ( - "encoding/xml" - "net/http" - - "github.com/go-chi/chi/v5" - - "git.utn.lol/enki/kindexr/internal/config" - "git.utn.lol/enki/kindexr/internal/db" -) - -// Server handles Torznab API requests. -type Server struct { - cfg *config.Config - db *db.DB - version string -} - -// New creates a new Torznab Server. -func New(cfg *config.Config, database *db.DB, version string) *Server { - return &Server{cfg: cfg, db: database, version: version} -} - -// Mount registers Torznab routes under the given chi router at /api. -func (s *Server) Mount(r chi.Router) { - r.Group(func(r chi.Router) { - r.Use(s.authMiddleware) - r.Get("/api", s.apiHandler) - }) -} - -// apiHandler dispatches on the ?t= query parameter. -func (s *Server) apiHandler(w http.ResponseWriter, r *http.Request) { - switch r.URL.Query().Get("t") { - case "caps": - s.capsHandler(w, r) - case "search", "tvsearch", "movie", "music", "audio", "book": - s.searchHandler(w, r) - default: - writeError(w, http.StatusBadRequest, 202, "unknown function") - } -} - -// authMiddleware enforces the apikey query parameter. -func (s *Server) authMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - key := r.URL.Query().Get("apikey") - if key == "" { - writeError(w, http.StatusUnauthorized, 100, "missing api key") - return - } - ak, err := s.db.GetAPIKey(r.Context(), key) - if err != nil || ak == nil { - writeError(w, http.StatusUnauthorized, 100, "invalid api key") - return - } - next.ServeHTTP(w, r) - }) -} - -func writeError(w http.ResponseWriter, status, code int, descr string) { - e := ErrorResponse{Code: code, Descr: descr} - out, _ := xml.MarshalIndent(e, "", " ") - w.Header().Set("Content-Type", "application/xml; charset=utf-8") - w.WriteHeader(status) - w.Write([]byte(xml.Header)) - w.Write(out) -} diff --git a/archive/go/internal/torznab/server_test.go b/archive/go/internal/torznab/server_test.go deleted file mode 100644 index 431e082..0000000 --- a/archive/go/internal/torznab/server_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package torznab_test - -import ( - "context" - "crypto/rand" - "encoding/hex" - "encoding/xml" - "net/http" - "net/http/httptest" - "path/filepath" - "strings" - "testing" - - "git.utn.lol/enki/kindexr/internal/config" - "git.utn.lol/enki/kindexr/internal/db" - "git.utn.lol/enki/kindexr/internal/torznab" - "github.com/go-chi/chi/v5" -) - -func newTestServerWithDB(t *testing.T) (*httptest.Server, string, *db.DB) { - t.Helper() - - dir := t.TempDir() - database, err := db.Open(filepath.Join(dir, "test.db")) - if err != nil { - t.Fatalf("open db: %v", err) - } - t.Cleanup(func() { database.Close() }) - - cfg, err := config.Load("") - if err != nil { - t.Fatalf("load config: %v", err) - } - - keyBytes := make([]byte, 32) - rand.Read(keyBytes) - apiKey := hex.EncodeToString(keyBytes) - if err := database.CreateAPIKey(context.Background(), apiKey, "test"); err != nil { - t.Fatalf("create api key: %v", err) - } - - r := chi.NewRouter() - srv := torznab.New(cfg, database, "0.1.0-test") - srv.Mount(r) - - return httptest.NewServer(r), apiKey, database -} - -func newTestServer(t *testing.T) (*httptest.Server, string) { - ts, apiKey, _ := newTestServerWithDB(t) - return ts, apiKey -} - -func TestCapsEndpoint(t *testing.T) { - ts, apiKey := newTestServer(t) - defer ts.Close() - - resp, err := http.Get(ts.URL + "/api?t=caps&apikey=" + apiKey) - if err != nil { - t.Fatalf("GET /api?t=caps: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - t.Errorf("expected 200, got %d", resp.StatusCode) - } - ct := resp.Header.Get("Content-Type") - if !strings.HasPrefix(ct, "application/xml") { - t.Errorf("expected Content-Type application/xml, got %q", ct) - } - - var caps torznab.Caps - if err := xml.NewDecoder(resp.Body).Decode(&caps); err != nil { - t.Fatalf("decode caps XML: %v", err) - } - if caps.Server.Title != "kindexr" { - t.Errorf("expected server title kindexr, got %q", caps.Server.Title) - } - if len(caps.Categories.Categories) == 0 { - t.Error("expected categories to be non-empty") - } -} - -func TestCapsRequiresAuth(t *testing.T) { - ts, _ := newTestServer(t) - defer ts.Close() - - resp, err := http.Get(ts.URL + "/api?t=caps") - if err != nil { - t.Fatalf("GET /api?t=caps (no key): %v", err) - } - resp.Body.Close() - if resp.StatusCode != http.StatusUnauthorized { - t.Errorf("expected 401, got %d", resp.StatusCode) - } -} - -func TestSearchEmpty(t *testing.T) { - ts, apiKey := newTestServer(t) - defer ts.Close() - - resp, err := http.Get(ts.URL + "/api?t=search&q=&apikey=" + apiKey) - if err != nil { - t.Fatalf("GET search: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - t.Errorf("expected 200, got %d", resp.StatusCode) - } - - var rss torznab.RSS - if err := xml.NewDecoder(resp.Body).Decode(&rss); err != nil { - t.Fatalf("decode RSS: %v", err) - } - if rss.Channel.Title != "kindexr" { - t.Errorf("wrong channel title: %q", rss.Channel.Title) - } -} - -func TestSearchWithResults(t *testing.T) { - ts, apiKey, database := newTestServerWithDB(t) - defer ts.Close() - - cat := 5040 - rec := db.TorrentRecord{ - EventID: "cccc333300000000000000000000000000000000000000000000000000000000", - InfoHash: "dddd444400000000000000000000000000000000", - Pubkey: "eeee555500000000000000000000000000000000000000000000000000000000", - CreatedAt: 1715000000, - IngestedAt: 1715000001, - Title: "Breaking.Bad.S01E01.1080p.WEB-DL", - RawEvent: "{}", - NewznabCat: &cat, - Trackers: []string{"udp://tracker.opentrackr.org:1337"}, - } - if err := database.InsertTorrent(context.Background(), rec); err != nil { - t.Fatalf("insert torrent: %v", err) - } - - resp, err := http.Get(ts.URL + "/api?t=search&q=breaking&apikey=" + apiKey) - if err != nil { - t.Fatalf("GET search: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - t.Errorf("expected 200, got %d", resp.StatusCode) - } - - var rss torznab.RSS - if err := xml.NewDecoder(resp.Body).Decode(&rss); err != nil { - t.Fatalf("decode RSS: %v", err) - } - if len(rss.Channel.Items) == 0 { - t.Error("expected at least one search result") - } - if rss.Channel.Items[0].Title != "Breaking.Bad.S01E01.1080p.WEB-DL" { - t.Errorf("unexpected first result title: %q", rss.Channel.Items[0].Title) - } -} - -func TestUnknownFunction(t *testing.T) { - ts, apiKey := newTestServer(t) - defer ts.Close() - - resp, err := http.Get(ts.URL + "/api?t=unknown&apikey=" + apiKey) - if err != nil { - t.Fatalf("GET: %v", err) - } - resp.Body.Close() - if resp.StatusCode != http.StatusBadRequest { - t.Errorf("expected 400, got %d", resp.StatusCode) - } -} diff --git a/archive/go/internal/torznab/xml.go b/archive/go/internal/torznab/xml.go deleted file mode 100644 index cc97e9e..0000000 --- a/archive/go/internal/torznab/xml.go +++ /dev/null @@ -1,114 +0,0 @@ -package torznab - -import "encoding/xml" - -// Caps XML types. - -type Caps struct { - XMLName xml.Name `xml:"caps"` - Server CapsServer `xml:"server"` - Limits CapsLimits `xml:"limits"` - Searching CapsSearching `xml:"searching"` - Categories CapsCategories `xml:"categories"` -} - -type CapsServer struct { - Version string `xml:"version,attr"` - Title string `xml:"title,attr"` - Strapline string `xml:"strapline,attr"` - Email string `xml:"email,attr"` - URL string `xml:"url,attr"` - Image string `xml:"image,attr"` -} - -type CapsLimits struct { - Max int `xml:"max,attr"` - Default int `xml:"default,attr"` -} - -type CapsSearching struct { - Search CapsSearch `xml:"search"` - TVSearch CapsSearch `xml:"tv-search"` - MovieSearch CapsSearch `xml:"movie-search"` - MusicSearch CapsSearch `xml:"music-search"` - AudioSearch CapsSearch `xml:"audio-search"` - BookSearch CapsSearch `xml:"book-search"` -} - -type CapsSearch struct { - Available string `xml:"available,attr"` - SupportedParams string `xml:"supportedParams,attr"` -} - -type CapsCategories struct { - Categories []CapsCategory `xml:"category"` -} - -type CapsCategory struct { - ID int `xml:"id,attr"` - Name string `xml:"name,attr"` - SubCats []CapsSubcat `xml:"subcat"` -} - -type CapsSubcat struct { - ID int `xml:"id,attr"` - Name string `xml:"name,attr"` -} - -// RSS/Torznab search result types. - -type RSS struct { - XMLName xml.Name `xml:"rss"` - Version string `xml:"version,attr"` - Atom string `xml:"xmlns:atom,attr"` - Torznab string `xml:"xmlns:torznab,attr"` - Channel RSSChannel `xml:"channel"` -} - -type RSSChannel struct { - AtomLink RSSAtomLink `xml:"atom:link"` - Title string `xml:"title"` - Description string `xml:"description"` - Link string `xml:"link"` - Language string `xml:"language"` - Category string `xml:"category"` - Items []RSSItem `xml:"item"` -} - -type RSSAtomLink struct { - Rel string `xml:"rel,attr"` - Type string `xml:"type,attr"` -} - -type RSSItem struct { - Title string `xml:"title"` - GUID RSSGUID `xml:"guid"` - Link string `xml:"link"` - PubDate string `xml:"pubDate"` - Size int64 `xml:"size"` - Enclosure RSSEnclosure `xml:"enclosure"` - Attrs []TorznabAttr `xml:"torznab:attr"` -} - -type RSSGUID struct { - IsPermaLink string `xml:"isPermaLink,attr"` - Value string `xml:",chardata"` -} - -type RSSEnclosure struct { - URL string `xml:"url,attr"` - Length int64 `xml:"length,attr"` - Type string `xml:"type,attr"` -} - -type TorznabAttr struct { - Name string `xml:"name,attr"` - Value string `xml:"value,attr"` -} - -// ErrorResponse is the Torznab XML error envelope. -type ErrorResponse struct { - XMLName xml.Name `xml:"error"` - Code int `xml:"code,attr"` - Descr string `xml:"description,attr"` -}