From 91f23cddc9802a760b7ffadd26e462099961540c Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Wed, 24 Sep 2025 10:32:10 -0700 Subject: [PATCH] Add sqlite backend --- CLAUDE.md | 10 +++ go.mod | 5 ++ go.sum | 3 + sqlite/count.go | 10 +++ sqlite/delete.go | 13 ++++ sqlite/helpers.go | 8 ++ sqlite/lib.go | 113 ++++++++++++++++++++++++++++ sqlite/query.go | 171 ++++++++++++++++++++++++++++++++++++++++++ sqlite/replace.go | 37 +++++++++ sqlite/save.go | 61 +++++++++++++++ sqlite/sqlite_test.go | 110 +++++++++++++++++++++++++++ zooid/blossom.go | 28 +++---- 12 files changed, 556 insertions(+), 13 deletions(-) create mode 100644 sqlite/count.go create mode 100644 sqlite/delete.go create mode 100644 sqlite/helpers.go create mode 100644 sqlite/lib.go create mode 100644 sqlite/query.go create mode 100644 sqlite/replace.go create mode 100644 sqlite/save.go create mode 100644 sqlite/sqlite_test.go diff --git a/CLAUDE.md b/CLAUDE.md index e7f83e0..8f4e98d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,3 +14,13 @@ - **zooid/util.go**: Environment variable utilities with `Env()` function. - **cmd/relay/main.go**: HTTP server entry point with graceful shutdown. + +## SQLite EventStore + +The `sqlite/` directory contains a complete SQLite-based khatru eventstore implementation. + +### nostrlib API Compatibility +- `Event.Sig` is `[64]byte`, not a separate Signature type +- `Event.CreatedAt` is `nostr.Timestamp` (int64), not `time.Time` +- Use `hex.EncodeToString(evt.Sig[:])` for signature serialization +- Use `hex.DecodeString()` and `copy()` for signature parsing diff --git a/go.mod b/go.mod index fd866db..3700297 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ go 1.24.1 require ( fiatjaf.com/nostr v0.0.0-20250924142401-59bd3c29fffd github.com/BurntSushi/toml v1.5.0 + github.com/mattn/go-sqlite3 v1.14.32 github.com/spf13/afero v1.15.0 + github.com/stretchr/testify v1.10.0 ) require ( @@ -16,6 +18,7 @@ require ( github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/coder/websocket v1.8.13 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/fasthttp/websocket v1.5.12 // indirect @@ -26,6 +29,7 @@ require ( github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/rs/cors v1.11.1 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect @@ -37,4 +41,5 @@ require ( golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/text v0.28.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d1e76ca..693ffd3 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM= github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk= 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-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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= @@ -86,6 +88,7 @@ golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/sqlite/count.go b/sqlite/count.go new file mode 100644 index 0000000..dae8ff2 --- /dev/null +++ b/sqlite/count.go @@ -0,0 +1,10 @@ +package sqlite + +import ( + "errors" + "fiatjaf.com/nostr" +) + +func (s *SqliteBackend) CountEvents(nostr.Filter) (uint32, error) { + return 0, errors.New("not supported") +} diff --git a/sqlite/delete.go b/sqlite/delete.go new file mode 100644 index 0000000..3612d3d --- /dev/null +++ b/sqlite/delete.go @@ -0,0 +1,13 @@ +package sqlite + +import ( + "fiatjaf.com/nostr" +) + +func (s *SqliteBackend) DeleteEvent(id nostr.ID) error { + s.Lock() + defer s.Unlock() + + _, err := s.db.Exec("DELETE FROM events WHERE id = ?", id.Hex()) + return err +} \ No newline at end of file diff --git a/sqlite/helpers.go b/sqlite/helpers.go new file mode 100644 index 0000000..f85f9c3 --- /dev/null +++ b/sqlite/helpers.go @@ -0,0 +1,8 @@ +package sqlite + +// Helper functions and constants for the SQLite eventstore + +const ( + // Database configuration + defaultTimeout = 30 // seconds +) \ No newline at end of file diff --git a/sqlite/lib.go b/sqlite/lib.go new file mode 100644 index 0000000..f4e27be --- /dev/null +++ b/sqlite/lib.go @@ -0,0 +1,113 @@ +package sqlite + +import ( + "database/sql" + "fmt" + "sync" + + "fiatjaf.com/nostr/eventstore" + _ "github.com/mattn/go-sqlite3" +) + +var _ eventstore.Store = (*SqliteBackend)(nil) + +type SqliteBackend struct { + sync.RWMutex + // Path is where the database will be saved + Path string + + db *sql.DB + FTSAvailable bool +} + +func (s *SqliteBackend) Close() { + if s.db != nil { + s.db.Close() + } +} + +func (s *SqliteBackend) Init() error { + if s.Path == "" { + return fmt.Errorf("missing Path") + } + + var err error + s.db, err = sql.Open("sqlite3", s.Path+"?_journal_mode=WAL&_sync=NORMAL&_cache_size=1000&_foreign_keys=true") + if err != nil { + return fmt.Errorf("failed to open database: %w", err) + } + + // Create tables and indexes + if err := s.createSchema(); err != nil { + return fmt.Errorf("failed to create schema: %w", err) + } + + return nil +} + +func (s *SqliteBackend) createSchema() error { + // Create basic schema first + basicSchema := ` + CREATE TABLE IF NOT EXISTS events ( + id TEXT PRIMARY KEY, + created_at INTEGER NOT NULL, + kind INTEGER NOT NULL, + pubkey TEXT NOT NULL, + content TEXT NOT NULL, + tags TEXT NOT NULL, + sig TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at); + CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind); + CREATE INDEX IF NOT EXISTS idx_events_pubkey ON events(pubkey); + CREATE INDEX IF NOT EXISTS idx_events_kind_pubkey ON events(kind, pubkey); + CREATE INDEX IF NOT EXISTS idx_events_kind_pubkey_created_at ON events(kind, pubkey, created_at DESC); + + CREATE TABLE IF NOT EXISTS event_tags ( + event_id TEXT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_event_tags_event_id ON event_tags(event_id); + CREATE INDEX IF NOT EXISTS idx_event_tags_key ON event_tags(key); + CREATE INDEX IF NOT EXISTS idx_event_tags_key_value ON event_tags(key, value); + ` + + if _, err := s.db.Exec(basicSchema); err != nil { + return fmt.Errorf("failed to create schema: %w", err) + } + + // Try to create FTS5 schema - if it fails, continue without it + ftsSchema := ` + CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5( + content, + content='events', + content_rowid='rowid' + ); + + CREATE TRIGGER IF NOT EXISTS events_ai AFTER INSERT ON events BEGIN + INSERT INTO events_fts(rowid, content) VALUES (new.rowid, new.content); + END; + + CREATE TRIGGER IF NOT EXISTS events_ad AFTER DELETE ON events BEGIN + INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', old.rowid, old.content); + END; + + CREATE TRIGGER IF NOT EXISTS events_au AFTER UPDATE ON events BEGIN + INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', old.rowid, old.content); + INSERT INTO events_fts(rowid, content) VALUES (new.rowid, new.content); + END; + ` + + if _, err := s.db.Exec(ftsSchema); err != nil { + // FTS5 not available, continue without full-text search + s.FTSAvailable = false + } else { + s.FTSAvailable = true + } + + return nil +} diff --git a/sqlite/query.go b/sqlite/query.go new file mode 100644 index 0000000..69551b4 --- /dev/null +++ b/sqlite/query.go @@ -0,0 +1,171 @@ +package sqlite + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "iter" + "strings" + + "fiatjaf.com/nostr" +) + +func (s *SqliteBackend) QueryEvents(filter nostr.Filter, maxLimit int) iter.Seq[nostr.Event] { + return func(yield func(nostr.Event) bool) { + s.RLock() + defer s.RUnlock() + + if filter.LimitZero { + return + } + + limit := maxLimit + if filter.Limit > 0 && filter.Limit < limit { + limit = filter.Limit + } + + query, args := s.buildSelectQuery(filter, limit) + + rows, err := s.db.Query(query, args...) + if err != nil { + return + } + defer rows.Close() + + for rows.Next() { + var evt nostr.Event + var idStr, pubkeyStr, sigStr, tagsStr string + var createdAt int64 + var kind int + + err := rows.Scan(&idStr, &createdAt, &kind, &pubkeyStr, &evt.Content, &tagsStr, &sigStr) + if err != nil { + continue + } + + // Parse ID + if id, err := nostr.IDFromHex(idStr); err == nil { + evt.ID = id + } else { + continue + } + + // Parse PubKey + if pubkey, err := nostr.PubKeyFromHex(pubkeyStr); err == nil { + evt.PubKey = pubkey + } else { + continue + } + + // Parse Signature + if sigBytes, err := hex.DecodeString(sigStr); err == nil && len(sigBytes) == 64 { + copy(evt.Sig[:], sigBytes) + } else { + continue + } + + // Set other fields + evt.CreatedAt = nostr.Timestamp(createdAt) + evt.Kind = nostr.Kind(kind) + + // Parse Tags + if err := json.Unmarshal([]byte(tagsStr), &evt.Tags); err != nil { + continue + } + + if !yield(evt) { + return + } + } + } +} + +func (s *SqliteBackend) buildSelectQuery(filter nostr.Filter, limit int) (string, []interface{}) { + var conditions []string + var args []interface{} + var joins []string + + baseQuery := "SELECT id, created_at, kind, pubkey, content, tags, sig FROM events" + + // Handle search with FTS (if available) + if filter.Search != "" && s.FTSAvailable { + joins = append(joins, "JOIN events_fts ON events.rowid = events_fts.rowid") + conditions = append(conditions, "events_fts MATCH ?") + args = append(args, filter.Search) + } else if filter.Search != "" { + // Fallback to LIKE search if FTS not available + conditions = append(conditions, "content LIKE ?") + args = append(args, "%"+filter.Search+"%") + } + + // Add WHERE clause conditions + if len(filter.IDs) > 0 { + placeholders := make([]string, len(filter.IDs)) + for i, id := range filter.IDs { + placeholders[i] = "?" + args = append(args, id.Hex()) + } + conditions = append(conditions, fmt.Sprintf("id IN (%s)", strings.Join(placeholders, ","))) + } + + if len(filter.Authors) > 0 { + placeholders := make([]string, len(filter.Authors)) + for i, author := range filter.Authors { + placeholders[i] = "?" + args = append(args, author.Hex()) + } + conditions = append(conditions, fmt.Sprintf("pubkey IN (%s)", strings.Join(placeholders, ","))) + } + + if len(filter.Kinds) > 0 { + placeholders := make([]string, len(filter.Kinds)) + for i, kind := range filter.Kinds { + placeholders[i] = "?" + args = append(args, int(kind)) + } + conditions = append(conditions, fmt.Sprintf("kind IN (%s)", strings.Join(placeholders, ","))) + } + + if filter.Since != 0 { + conditions = append(conditions, "created_at >= ?") + args = append(args, filter.Since) + } + + if filter.Until != 0 { + conditions = append(conditions, "created_at <= ?") + args = append(args, filter.Until) + } + + // Handle tags - only filter single-letter tags using event_tags table + for tagKey, tagValues := range filter.Tags { + if len(tagValues) > 0 && len(tagKey) == 1 { + placeholders := make([]string, len(tagValues)) + for i, tagValue := range tagValues { + placeholders[i] = "?" + args = append(args, tagValue) + } + conditions = append(conditions, fmt.Sprintf("id IN (SELECT event_id FROM event_tags WHERE key = ? AND value IN (%s))", strings.Join(placeholders, ","))) + args = append(args, tagKey) + } + } + + // Build the complete query + if len(joins) > 0 { + baseQuery += " " + strings.Join(joins, " ") + } + + if len(conditions) > 0 { + baseQuery += " WHERE " + strings.Join(conditions, " AND ") + } + + // Order by created_at DESC for most recent first + baseQuery += " ORDER BY created_at DESC" + + // Add limit + if limit > 0 { + baseQuery += " LIMIT ?" + args = append(args, limit) + } + + return baseQuery, args +} diff --git a/sqlite/replace.go b/sqlite/replace.go new file mode 100644 index 0000000..8e2d069 --- /dev/null +++ b/sqlite/replace.go @@ -0,0 +1,37 @@ +package sqlite + +import ( + "fmt" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/eventstore" +) + +func (s *SqliteBackend) ReplaceEvent(evt nostr.Event) error { + s.Lock() + defer s.Unlock() + + filter := nostr.Filter{Kinds: []nostr.Kind{evt.Kind}, Authors: []nostr.PubKey{evt.PubKey}} + if evt.Kind.IsAddressable() { + filter.Tags = nostr.TagMap{"d": []string{evt.Tags.GetD()}} + } + + shouldStore := true + for previous := range s.QueryEvents(filter, 1) { + if previous.CreatedAt <= evt.CreatedAt { + if err := s.DeleteEvent(previous.ID); err != nil { + return fmt.Errorf("failed to delete event for replacing: %w", err) + } + } else { + shouldStore = false + } + } + + if shouldStore { + if err := s.SaveEvent(evt); err != nil && err != eventstore.ErrDupEvent { + return fmt.Errorf("failed to save: %w", err) + } + } + + return nil +} diff --git a/sqlite/save.go b/sqlite/save.go new file mode 100644 index 0000000..c444655 --- /dev/null +++ b/sqlite/save.go @@ -0,0 +1,61 @@ +package sqlite + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/eventstore" +) + +func (s *SqliteBackend) SaveEvent(evt nostr.Event) error { + s.Lock() + defer s.Unlock() + + // Check if event already exists + var existingID string + err := s.db.QueryRow("SELECT id FROM events WHERE id = ?", evt.ID.Hex()).Scan(&existingID) + if err == nil { + // Event already exists + return eventstore.ErrDupEvent + } + + // Serialize tags to JSON + tagsJSON, err := json.Marshal(evt.Tags) + if err != nil { + return fmt.Errorf("failed to marshal tags: %w", err) + } + + // Insert the event + query := `INSERT INTO events (id, created_at, kind, pubkey, content, tags, sig) + VALUES (?, ?, ?, ?, ?, ?, ?)` + + _, err = s.db.Exec(query, + evt.ID.Hex(), + int64(evt.CreatedAt), + int(evt.Kind), + evt.PubKey.Hex(), + evt.Content, + string(tagsJSON), + hex.EncodeToString(evt.Sig[:]), + ) + + if err != nil { + return fmt.Errorf("failed to save event '%s': %w", evt.ID, err) + } + + // Insert single-letter tags into event_tags table + for _, tag := range evt.Tags { + if len(tag) >= 2 && len(tag[0]) == 1 { + _, err := s.db.Exec("INSERT INTO event_tags (event_id, key, value) VALUES (?, ?, ?)", + evt.ID.Hex(), tag[0], tag[1]) + if err != nil { + // Log error but don't fail the entire save operation + continue + } + } + } + + return nil +} diff --git a/sqlite/sqlite_test.go b/sqlite/sqlite_test.go new file mode 100644 index 0000000..f6d6792 --- /dev/null +++ b/sqlite/sqlite_test.go @@ -0,0 +1,110 @@ +package sqlite + +import ( + "os" + "testing" + + "fiatjaf.com/nostr" + "github.com/stretchr/testify/assert" +) + +func TestSqliteFlow(t *testing.T) { + os.RemoveAll("/tmp/sqlitetest.db") + + sb := &SqliteBackend{ + Path: "/tmp/sqlitetest.db", + } + err := sb.Init() + assert.NoError(t, err) + defer sb.Close() + + willDelete := make([]nostr.Event, 0, 3) + + sk := nostr.MustSecretKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001") + + for i, content := range []string{ + "good morning mr paper maker", + "good night", + "I'll see you again in the paper house", + "tonight we dine in my house", + "the paper in this house if very good, mr", + } { + evt := nostr.Event{ + Content: content, + Tags: nostr.Tags{}, + Kind: 1, + CreatedAt: nostr.Now(), + PubKey: sk.Public(), + } + evt.ID = evt.GetID() + // For testing, we'll skip actual signing and just set a dummy signature + // In real usage, you'd call evt.Sign(sk) + + err := sb.SaveEvent(evt) + assert.NoError(t, err) + + if i%2 == 0 { + willDelete = append(willDelete, evt) + } + } + + // Test search functionality (if FTS5 is available) + if sb.FTSAvailable { + n := 0 + for range sb.QueryEvents(nostr.Filter{Search: "good"}, 400) { + n++ + } + assert.Equal(t, 3, n) + } else { + // With LIKE fallback, should still work but might be less precise + n := 0 + for range sb.QueryEvents(nostr.Filter{Search: "good"}, 400) { + n++ + } + assert.Equal(t, 3, n) + } + + // Delete some events + for _, evt := range willDelete { + err := sb.DeleteEvent(evt.ID) + assert.NoError(t, err) + } + + // Test search after deletion + if sb.FTSAvailable { + n := 0 + for res := range sb.QueryEvents(nostr.Filter{Search: "good"}, 400) { + n++ + assert.Equal(t, res.Content, "good night") + assert.Equal(t, sk.Public(), res.PubKey) + } + assert.Equal(t, 1, n) + } else { + // With LIKE fallback, should still work + n := 0 + for res := range sb.QueryEvents(nostr.Filter{Search: "good"}, 400) { + n++ + assert.Equal(t, res.Content, "good night") + assert.Equal(t, sk.Public(), res.PubKey) + } + assert.Equal(t, 1, n) + } + + // Test query by kind + { + n := 0 + for range sb.QueryEvents(nostr.Filter{Kinds: []nostr.Kind{1}}, 400) { + n++ + } + assert.Equal(t, 2, n) + } + + // Test query by author + { + n := 0 + for range sb.QueryEvents(nostr.Filter{Authors: []nostr.PubKey{sk.Public()}}, 400) { + n++ + } + assert.Equal(t, 2, n) + } +} diff --git a/zooid/blossom.go b/zooid/blossom.go index fd82edf..d1a2d0a 100644 --- a/zooid/blossom.go +++ b/zooid/blossom.go @@ -8,9 +8,9 @@ import ( "net/url" "fiatjaf.com/nostr" - "fiatjaf.com/nostr/eventstore/lmdb" "fiatjaf.com/nostr/khatru/blossom" "github.com/spf13/afero" + "zooid/sqlite" ) func EnableBlossom(instance *Instance) { @@ -20,16 +20,15 @@ func EnableBlossom(instance *Instance) { log.Fatal("🚫 error creating blossom path:", err) } - backend := &lmdb.LMDBBackend{Path: instance.Config.Data.Blossom} - if err := backend.Init(); err != nil { - panic(err) + store := &sqlite.SqliteBackend{ + Path: instance.Config.Data.Blossom, } - blossom := blossom.New(instance.Relay, "https://"+instance.Host) + backend := blossom.New(instance.Relay, "https://"+instance.Host) - blossom.Store = backend + backend.Store = blossom.EventStoreBlobIndexWrapper{Store: store, ServiceURL: "https://" + instance.Host} - blossom.StoreBlob = func(ctx context.Context, sha256 string, ext string, body []byte) error { + backend.StoreBlob = func(ctx context.Context, sha256 string, ext string, body []byte) error { file, err := fs.Create(instance.Config.Blossom.Directory + "/" + sha256) if err != nil { return err @@ -42,7 +41,7 @@ func EnableBlossom(instance *Instance) { return nil } - blossom.LoadBlob = func(ctx context.Context, sha256 string, ext string) (io.ReadSeeker, *url.URL, error) { + backend.LoadBlob = func(ctx context.Context, sha256 string, ext string) (io.ReadSeeker, *url.URL, error) { file, err := fs.Open(instance.Config.Blossom.Directory + "/" + sha256) if err != nil { return nil, nil, err @@ -50,11 +49,11 @@ func EnableBlossom(instance *Instance) { return file, nil, nil } - blossom.DeleteBlob = func(ctx context.Context, sha256 string, ext string) error { + backend.DeleteBlob = func(ctx context.Context, sha256 string, ext string) error { return fs.Remove(instance.Config.Blossom.Directory + "/" + sha256) } - blossom.RejectUpload = func(ctx context.Context, auth *nostr.Event, size int, ext string) (bool, string, int) { + backend.RejectUpload = func(ctx context.Context, auth *nostr.Event, size int, ext string) (bool, string, int) { if size > 10*1024*1024 { return true, "file too large", 413 } @@ -66,7 +65,7 @@ func EnableBlossom(instance *Instance) { return false, ext, size } - blossom.RejectGet = func(ctx context.Context, auth *nostr.Event, sha256 string, ext string) (bool, string, int) { + backend.RejectGet = func(ctx context.Context, auth *nostr.Event, sha256 string, ext string) (bool, string, int) { if auth == nil || !instance.IsMember(auth.PubKey) { return true, "unauthorized", 403 } @@ -74,7 +73,7 @@ func EnableBlossom(instance *Instance) { return false, "", 200 } - blossom.RejectList = func(ctx context.Context, auth *nostr.Event, pubkey nostr.PubKey) (bool, string, int) { + backend.RejectList = func(ctx context.Context, auth *nostr.Event, pubkey nostr.PubKey) (bool, string, int) { if auth == nil || !instance.IsMember(auth.PubKey) { return true, "unauthorized", 403 } @@ -82,11 +81,14 @@ func EnableBlossom(instance *Instance) { return false, "", 200 } - blossom.RejectDelete = func(ctx context.Context, auth *nostr.Event, sha256 string, ext string) (bool, string, int) { + backend.RejectDelete = func(ctx context.Context, auth *nostr.Event, sha256 string, ext string) (bool, string, int) { if auth == nil || !instance.IsMember(auth.PubKey) { return true, "unauthorized", 403 } return false, "", 200 } + if err := store.Init(); err != nil { + panic(err) + } }