diff options
| author | bndw <ben@bdw.to> | 2026-02-13 19:12:28 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-13 19:12:28 -0800 |
| commit | 656748ea286ff7eac6cbe1b241ad31212892ba61 (patch) | |
| tree | e9685b4a585809463bdf51a4d1ecb7f7c5efaf70 /internal/handler/websocket | |
| parent | 83876eae868bd1e4fb6b9a823a6e8173919f290d (diff) | |
feat: implement NIP-09 (deletions) and NIP-11 (relay info)
NIP-11 (Relay Information Document):
- Serves relay metadata at GET / with Accept: application/nostr+json
- Returns name, description, supported NIPs, limitations
- CORS headers for browser compatibility
NIP-09 (Event Deletion):
- Kind 5 events delete events referenced in 'e' tags
- Only authors can delete their own events
- Soft delete (marks deleted=1)
- Records deletion in deletions table
- Works across all protocols (gRPC, Connect, WebSocket)
Fixed deletions schema:
- deleted_event_id as PRIMARY KEY (not deletion_event_id)
- Allows one deletion event to delete multiple events
3 new tests, 44 total tests passing
Supported NIPs now: 1, 9, 11
Diffstat (limited to 'internal/handler/websocket')
| -rw-r--r-- | internal/handler/websocket/handler.go | 13 | ||||
| -rw-r--r-- | internal/handler/websocket/nip11.go | 60 |
2 files changed, 73 insertions, 0 deletions
diff --git a/internal/handler/websocket/handler.go b/internal/handler/websocket/handler.go index cef83dd..4a7db0d 100644 --- a/internal/handler/websocket/handler.go +++ b/internal/handler/websocket/handler.go | |||
| @@ -17,6 +17,7 @@ import ( | |||
| 17 | type EventStore interface { | 17 | type EventStore interface { |
| 18 | StoreEvent(context.Context, *storage.EventData) error | 18 | StoreEvent(context.Context, *storage.EventData) error |
| 19 | QueryEvents(context.Context, []*pb.Filter, *storage.QueryOptions) ([]*pb.Event, error) | 19 | QueryEvents(context.Context, []*pb.Filter, *storage.QueryOptions) ([]*pb.Event, error) |
| 20 | ProcessDeletion(context.Context, *pb.Event) error | ||
| 20 | } | 21 | } |
| 21 | 22 | ||
| 22 | type Handler struct { | 23 | type Handler struct { |
| @@ -32,6 +33,11 @@ func NewHandler(store EventStore, subs *subscription.Manager) *Handler { | |||
| 32 | } | 33 | } |
| 33 | 34 | ||
| 34 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | 35 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| 36 | if r.Method == "GET" && r.Header.Get("Accept") == "application/nostr+json" { | ||
| 37 | h.ServeNIP11(w, r) | ||
| 38 | return | ||
| 39 | } | ||
| 40 | |||
| 35 | conn, err := websocket.Accept(w, r) | 41 | conn, err := websocket.Accept(w, r) |
| 36 | if err != nil { | 42 | if err != nil { |
| 37 | log.Printf("WebSocket accept failed: %v", err) | 43 | log.Printf("WebSocket accept failed: %v", err) |
| @@ -125,6 +131,13 @@ func (h *Handler) handleEvent(ctx context.Context, conn *websocket.Conn, raw []j | |||
| 125 | return nil | 131 | return nil |
| 126 | } | 132 | } |
| 127 | 133 | ||
| 134 | if pbEvent.Kind == 5 { | ||
| 135 | if err := h.store.ProcessDeletion(ctx, pbEvent); err != nil { | ||
| 136 | h.sendOK(ctx, conn, event.ID, false, fmt.Sprintf("deletion failed: %v", err)) | ||
| 137 | return nil | ||
| 138 | } | ||
| 139 | } | ||
| 140 | |||
| 128 | h.subs.MatchAndFan(pbEvent) | 141 | h.subs.MatchAndFan(pbEvent) |
| 129 | 142 | ||
| 130 | h.sendOK(ctx, conn, event.ID, true, "") | 143 | h.sendOK(ctx, conn, event.ID, true, "") |
diff --git a/internal/handler/websocket/nip11.go b/internal/handler/websocket/nip11.go new file mode 100644 index 0000000..a5bb9ca --- /dev/null +++ b/internal/handler/websocket/nip11.go | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | package websocket | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "encoding/json" | ||
| 5 | "net/http" | ||
| 6 | ) | ||
| 7 | |||
| 8 | type RelayInfo struct { | ||
| 9 | Name string `json:"name"` | ||
| 10 | Description string `json:"description"` | ||
| 11 | Pubkey string `json:"pubkey,omitempty"` | ||
| 12 | Contact string `json:"contact,omitempty"` | ||
| 13 | SupportedNIPs []int `json:"supported_nips"` | ||
| 14 | Software string `json:"software"` | ||
| 15 | Version string `json:"version"` | ||
| 16 | Limitation *Limits `json:"limitation,omitempty"` | ||
| 17 | } | ||
| 18 | |||
| 19 | type Limits struct { | ||
| 20 | MaxMessageLength int `json:"max_message_length,omitempty"` | ||
| 21 | MaxSubscriptions int `json:"max_subscriptions,omitempty"` | ||
| 22 | MaxFilters int `json:"max_filters,omitempty"` | ||
| 23 | MaxLimit int `json:"max_limit,omitempty"` | ||
| 24 | MaxSubidLength int `json:"max_subid_length,omitempty"` | ||
| 25 | MaxEventTags int `json:"max_event_tags,omitempty"` | ||
| 26 | MaxContentLength int `json:"max_content_length,omitempty"` | ||
| 27 | MinPowDifficulty int `json:"min_pow_difficulty,omitempty"` | ||
| 28 | AuthRequired bool `json:"auth_required"` | ||
| 29 | PaymentRequired bool `json:"payment_required"` | ||
| 30 | RestrictedWrites bool `json:"restricted_writes"` | ||
| 31 | } | ||
| 32 | |||
| 33 | func (h *Handler) ServeNIP11(w http.ResponseWriter, r *http.Request) { | ||
| 34 | info := RelayInfo{ | ||
| 35 | Name: "nostr-grpc relay", | ||
| 36 | Description: "High-performance Nostr relay with gRPC, Connect, and WebSocket support", | ||
| 37 | SupportedNIPs: []int{1, 9, 11}, | ||
| 38 | Software: "northwest.io/nostr-grpc", | ||
| 39 | Version: "0.1.0", | ||
| 40 | Limitation: &Limits{ | ||
| 41 | MaxMessageLength: 65536, | ||
| 42 | MaxSubscriptions: 20, | ||
| 43 | MaxFilters: 10, | ||
| 44 | MaxLimit: 5000, | ||
| 45 | MaxSubidLength: 64, | ||
| 46 | MaxEventTags: 2000, | ||
| 47 | MaxContentLength: 65536, | ||
| 48 | AuthRequired: false, | ||
| 49 | PaymentRequired: false, | ||
| 50 | RestrictedWrites: false, | ||
| 51 | }, | ||
| 52 | } | ||
| 53 | |||
| 54 | w.Header().Set("Content-Type", "application/nostr+json") | ||
| 55 | w.Header().Set("Access-Control-Allow-Origin", "*") | ||
| 56 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept") | ||
| 57 | w.Header().Set("Access-Control-Allow-Methods", "GET") | ||
| 58 | |||
| 59 | json.NewEncoder(w).Encode(info) | ||
| 60 | } | ||
