diff options
| author | bndw <ben@bdw.to> | 2026-02-13 17:48:36 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-13 17:48:36 -0800 |
| commit | 62d31434ddbadff18580826576e1169f539e23f0 (patch) | |
| tree | a3e56da30e33ddf0dbfdea407df6724c9640a7b1 /internal/handler/grpc/server.go | |
| parent | 5fcf5bd1fa5b2e707cea82c4652ea65c3c113c1a (diff) | |
feat: add gRPC handler with event validation and publishing
Handler implementation:
- EventStore interface (consumer-side)
- Server with PublishEvent, QueryEvents, CountEvents, PublishBatch
- pb.Event <-> nostr.Event conversion helpers
- Signature and ID validation using existing nostr package
- Canonical JSON generation for storage
9 tests passing
Diffstat (limited to 'internal/handler/grpc/server.go')
| -rw-r--r-- | internal/handler/grpc/server.go | 128 |
1 files changed, 128 insertions, 0 deletions
diff --git a/internal/handler/grpc/server.go b/internal/handler/grpc/server.go new file mode 100644 index 0000000..a3a3175 --- /dev/null +++ b/internal/handler/grpc/server.go | |||
| @@ -0,0 +1,128 @@ | |||
| 1 | package grpc | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | "fmt" | ||
| 6 | |||
| 7 | pb "northwest.io/nostr-grpc/api/nostr/v1" | ||
| 8 | "northwest.io/nostr-grpc/internal/storage" | ||
| 9 | ) | ||
| 10 | |||
| 11 | type EventStore interface { | ||
| 12 | StoreEvent(context.Context, *storage.EventData) error | ||
| 13 | QueryEvents(context.Context, []*pb.Filter, *storage.QueryOptions) ([]*pb.Event, error) | ||
| 14 | } | ||
| 15 | |||
| 16 | type Server struct { | ||
| 17 | pb.UnimplementedNostrRelayServer | ||
| 18 | store EventStore | ||
| 19 | } | ||
| 20 | |||
| 21 | func NewServer(store EventStore) *Server { | ||
| 22 | return &Server{store: store} | ||
| 23 | } | ||
| 24 | |||
| 25 | func (s *Server) PublishEvent(ctx context.Context, req *pb.PublishEventRequest) (*pb.PublishEventResponse, error) { | ||
| 26 | if req.Event == nil { | ||
| 27 | return &pb.PublishEventResponse{ | ||
| 28 | Accepted: false, | ||
| 29 | Message: "event is required", | ||
| 30 | }, nil | ||
| 31 | } | ||
| 32 | |||
| 33 | nostrEvent := PBToNostr(req.Event) | ||
| 34 | |||
| 35 | if !nostrEvent.CheckID() { | ||
| 36 | return &pb.PublishEventResponse{ | ||
| 37 | Accepted: false, | ||
| 38 | Message: "invalid event ID", | ||
| 39 | }, nil | ||
| 40 | } | ||
| 41 | |||
| 42 | if !nostrEvent.Verify() { | ||
| 43 | return &pb.PublishEventResponse{ | ||
| 44 | Accepted: false, | ||
| 45 | Message: "invalid signature", | ||
| 46 | }, nil | ||
| 47 | } | ||
| 48 | |||
| 49 | canonicalJSON := nostrEvent.Serialize() | ||
| 50 | |||
| 51 | eventData := &storage.EventData{ | ||
| 52 | Event: req.Event, | ||
| 53 | CanonicalJSON: canonicalJSON, | ||
| 54 | } | ||
| 55 | |||
| 56 | err := s.store.StoreEvent(ctx, eventData) | ||
| 57 | if err == storage.ErrEventExists { | ||
| 58 | return &pb.PublishEventResponse{ | ||
| 59 | Accepted: false, | ||
| 60 | Message: "duplicate: event already exists", | ||
| 61 | CanonicalJson: canonicalJSON, | ||
| 62 | }, nil | ||
| 63 | } | ||
| 64 | if err != nil { | ||
| 65 | return nil, fmt.Errorf("failed to store event: %w", err) | ||
| 66 | } | ||
| 67 | |||
| 68 | return &pb.PublishEventResponse{ | ||
| 69 | Accepted: true, | ||
| 70 | Message: "success", | ||
| 71 | CanonicalJson: canonicalJSON, | ||
| 72 | }, nil | ||
| 73 | } | ||
| 74 | |||
| 75 | func (s *Server) QueryEvents(ctx context.Context, req *pb.QueryRequest) (*pb.QueryResponse, error) { | ||
| 76 | opts := &storage.QueryOptions{ | ||
| 77 | IncludeCanonical: req.IncludeCanonicalJson, | ||
| 78 | Limit: req.PageSize, | ||
| 79 | } | ||
| 80 | |||
| 81 | if opts.Limit == 0 { | ||
| 82 | opts.Limit = 100 | ||
| 83 | } | ||
| 84 | |||
| 85 | events, err := s.store.QueryEvents(ctx, req.Filters, opts) | ||
| 86 | if err != nil { | ||
| 87 | return nil, fmt.Errorf("query failed: %w", err) | ||
| 88 | } | ||
| 89 | |||
| 90 | return &pb.QueryResponse{ | ||
| 91 | Events: events, | ||
| 92 | }, nil | ||
| 93 | } | ||
| 94 | |||
| 95 | func (s *Server) CountEvents(ctx context.Context, req *pb.CountRequest) (*pb.CountResponse, error) { | ||
| 96 | events, err := s.store.QueryEvents(ctx, req.Filters, &storage.QueryOptions{Limit: 0}) | ||
| 97 | if err != nil { | ||
| 98 | return nil, fmt.Errorf("count failed: %w", err) | ||
| 99 | } | ||
| 100 | |||
| 101 | return &pb.CountResponse{ | ||
| 102 | Count: int64(len(events)), | ||
| 103 | }, nil | ||
| 104 | } | ||
| 105 | |||
| 106 | func (s *Server) PublishBatch(ctx context.Context, req *pb.PublishBatchRequest) (*pb.PublishBatchResponse, error) { | ||
| 107 | results := make([]*pb.PublishEventResponse, len(req.Events)) | ||
| 108 | |||
| 109 | for i, event := range req.Events { | ||
| 110 | resp, err := s.PublishEvent(ctx, &pb.PublishEventRequest{Event: event}) | ||
| 111 | if err != nil { | ||
| 112 | return nil, err | ||
| 113 | } | ||
| 114 | results[i] = resp | ||
| 115 | } | ||
| 116 | |||
| 117 | return &pb.PublishBatchResponse{ | ||
| 118 | Results: results, | ||
| 119 | }, nil | ||
| 120 | } | ||
| 121 | |||
| 122 | func (s *Server) Subscribe(req *pb.SubscribeRequest, stream pb.NostrRelay_SubscribeServer) error { | ||
| 123 | return fmt.Errorf("not implemented yet") | ||
| 124 | } | ||
| 125 | |||
| 126 | func (s *Server) Unsubscribe(ctx context.Context, req *pb.UnsubscribeRequest) (*pb.Empty, error) { | ||
| 127 | return nil, fmt.Errorf("not implemented yet") | ||
| 128 | } | ||
