diff options
| -rw-r--r-- | README.md | 25 | ||||
| -rw-r--r-- | api/nostr/v1/nostrv1connect/nostr.connect.go | 420 | ||||
| -rw-r--r-- | buf.gen.yaml | 4 | ||||
| -rw-r--r-- | cmd/relay/main.go | 18 | ||||
| -rw-r--r-- | go.mod | 7 | ||||
| -rw-r--r-- | go.sum | 10 | ||||
| -rw-r--r-- | internal/handler/connect/handler.go | 101 |
7 files changed, 575 insertions, 10 deletions
| @@ -28,8 +28,10 @@ make build-all # Build both | |||
| 28 | ``` | 28 | ``` |
| 29 | 29 | ||
| 30 | The relay will start: | 30 | The relay will start: |
| 31 | - **gRPC** on `:50051` | 31 | - **gRPC** (native) on `:50051` |
| 32 | - **WebSocket** (Nostr) on `:8080` | 32 | - **HTTP** server on `:8080`: |
| 33 | - **Connect** (gRPC over HTTP/JSON) at `/nostr.v1.NostrRelay/*` | ||
| 34 | - **WebSocket** (Nostr protocol) at `/` | ||
| 33 | 35 | ||
| 34 | ### Test with Client | 36 | ### Test with Client |
| 35 | 37 | ||
| @@ -64,6 +66,16 @@ nak req -k 1 --limit 10 ws://localhost:8080 | |||
| 64 | echo '{"kind":1,"content":"hello","tags":[]}' | nak event --sec <nsec> | nak publish ws://localhost:8080 | 66 | echo '{"kind":1,"content":"hello","tags":[]}' | nak event --sec <nsec> | nak publish ws://localhost:8080 |
| 65 | ``` | 67 | ``` |
| 66 | 68 | ||
| 69 | **With Connect (HTTP/JSON):** | ||
| 70 | ```bash | ||
| 71 | # Call gRPC methods over HTTP with JSON | ||
| 72 | curl -X POST http://localhost:8080/nostr.v1.NostrRelay/PublishEvent \ | ||
| 73 | -H "Content-Type: application/json" \ | ||
| 74 | -d '{"event": {...}}' | ||
| 75 | |||
| 76 | # Works from browsers, curl, fetch(), etc. | ||
| 77 | ``` | ||
| 78 | |||
| 67 | ## gRPC API | 79 | ## gRPC API |
| 68 | 80 | ||
| 69 | See [proto/nostr/v1/nostr.proto](proto/nostr/v1/nostr.proto) for the full API. | 81 | See [proto/nostr/v1/nostr.proto](proto/nostr/v1/nostr.proto) for the full API. |
| @@ -82,13 +94,16 @@ See [proto/nostr/v1/nostr.proto](proto/nostr/v1/nostr.proto) for the full API. | |||
| 82 | **Phase 1: Complete** ✅ | 94 | **Phase 1: Complete** ✅ |
| 83 | - ✅ SQLite storage with binary-first design | 95 | - ✅ SQLite storage with binary-first design |
| 84 | - ✅ Event validation (ID, signature) | 96 | - ✅ Event validation (ID, signature) |
| 85 | - ✅ gRPC publish/query API | 97 | - ✅ **Triple protocol support:** |
| 98 | - **gRPC** (native binary protocol) | ||
| 99 | - **Connect** (gRPC over HTTP/JSON - browser compatible!) | ||
| 100 | - **WebSocket** (NIP-01 - standard Nostr protocol) | ||
| 86 | - ✅ Subscribe/streaming (real-time event delivery) | 101 | - ✅ Subscribe/streaming (real-time event delivery) |
| 87 | - ✅ Subscription management (filter matching, fan-out) | 102 | - ✅ Subscription management (filter matching, fan-out) |
| 88 | - ✅ **WebSocket server (NIP-01) - standard Nostr clients work!** | ||
| 89 | 103 | ||
| 90 | **Compatible with:** | 104 | **Compatible with:** |
| 91 | - Any gRPC client (custom or generated) | 105 | - Any gRPC client (Go, Python, JS, etc.) |
| 106 | - Any HTTP client (curl, fetch, browsers) | ||
| 92 | - Any Nostr client (Damus, Amethyst, Snort, Iris, Gossip, etc.) | 107 | - Any Nostr client (Damus, Amethyst, Snort, Iris, Gossip, etc.) |
| 93 | - nak CLI for testing | 108 | - nak CLI for testing |
| 94 | 109 | ||
diff --git a/api/nostr/v1/nostrv1connect/nostr.connect.go b/api/nostr/v1/nostrv1connect/nostr.connect.go new file mode 100644 index 0000000..fd98039 --- /dev/null +++ b/api/nostr/v1/nostrv1connect/nostr.connect.go | |||
| @@ -0,0 +1,420 @@ | |||
| 1 | // Code generated by protoc-gen-connect-go. DO NOT EDIT. | ||
| 2 | // | ||
| 3 | // Source: nostr/v1/nostr.proto | ||
| 4 | |||
| 5 | package nostrv1connect | ||
| 6 | |||
| 7 | import ( | ||
| 8 | connect "connectrpc.com/connect" | ||
| 9 | context "context" | ||
| 10 | errors "errors" | ||
| 11 | http "net/http" | ||
| 12 | v1 "northwest.io/nostr-grpc/api/nostr/v1" | ||
| 13 | strings "strings" | ||
| 14 | ) | ||
| 15 | |||
| 16 | // This is a compile-time assertion to ensure that this generated file and the connect package are | ||
| 17 | // compatible. If you get a compiler error that this constant is not defined, this code was | ||
| 18 | // generated with a version of connect newer than the one compiled into your binary. You can fix the | ||
| 19 | // problem by either regenerating this code with an older version of connect or updating the connect | ||
| 20 | // version compiled into your binary. | ||
| 21 | const _ = connect.IsAtLeastVersion1_13_0 | ||
| 22 | |||
| 23 | const ( | ||
| 24 | // NostrRelayName is the fully-qualified name of the NostrRelay service. | ||
| 25 | NostrRelayName = "nostr.v1.NostrRelay" | ||
| 26 | // RelayAdminName is the fully-qualified name of the RelayAdmin service. | ||
| 27 | RelayAdminName = "nostr.v1.RelayAdmin" | ||
| 28 | ) | ||
| 29 | |||
| 30 | // These constants are the fully-qualified names of the RPCs defined in this package. They're | ||
| 31 | // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. | ||
| 32 | // | ||
| 33 | // Note that these are different from the fully-qualified method names used by | ||
| 34 | // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to | ||
| 35 | // reflection-formatted method names, remove the leading slash and convert the remaining slash to a | ||
| 36 | // period. | ||
| 37 | const ( | ||
| 38 | // NostrRelayPublishEventProcedure is the fully-qualified name of the NostrRelay's PublishEvent RPC. | ||
| 39 | NostrRelayPublishEventProcedure = "/nostr.v1.NostrRelay/PublishEvent" | ||
| 40 | // NostrRelaySubscribeProcedure is the fully-qualified name of the NostrRelay's Subscribe RPC. | ||
| 41 | NostrRelaySubscribeProcedure = "/nostr.v1.NostrRelay/Subscribe" | ||
| 42 | // NostrRelayUnsubscribeProcedure is the fully-qualified name of the NostrRelay's Unsubscribe RPC. | ||
| 43 | NostrRelayUnsubscribeProcedure = "/nostr.v1.NostrRelay/Unsubscribe" | ||
| 44 | // NostrRelayPublishBatchProcedure is the fully-qualified name of the NostrRelay's PublishBatch RPC. | ||
| 45 | NostrRelayPublishBatchProcedure = "/nostr.v1.NostrRelay/PublishBatch" | ||
| 46 | // NostrRelayQueryEventsProcedure is the fully-qualified name of the NostrRelay's QueryEvents RPC. | ||
| 47 | NostrRelayQueryEventsProcedure = "/nostr.v1.NostrRelay/QueryEvents" | ||
| 48 | // NostrRelayCountEventsProcedure is the fully-qualified name of the NostrRelay's CountEvents RPC. | ||
| 49 | NostrRelayCountEventsProcedure = "/nostr.v1.NostrRelay/CountEvents" | ||
| 50 | // RelayAdminGetStatsProcedure is the fully-qualified name of the RelayAdmin's GetStats RPC. | ||
| 51 | RelayAdminGetStatsProcedure = "/nostr.v1.RelayAdmin/GetStats" | ||
| 52 | // RelayAdminGetConnectionsProcedure is the fully-qualified name of the RelayAdmin's GetConnections | ||
| 53 | // RPC. | ||
| 54 | RelayAdminGetConnectionsProcedure = "/nostr.v1.RelayAdmin/GetConnections" | ||
| 55 | // RelayAdminBanPublicKeyProcedure is the fully-qualified name of the RelayAdmin's BanPublicKey RPC. | ||
| 56 | RelayAdminBanPublicKeyProcedure = "/nostr.v1.RelayAdmin/BanPublicKey" | ||
| 57 | // RelayAdminGetStorageInfoProcedure is the fully-qualified name of the RelayAdmin's GetStorageInfo | ||
| 58 | // RPC. | ||
| 59 | RelayAdminGetStorageInfoProcedure = "/nostr.v1.RelayAdmin/GetStorageInfo" | ||
| 60 | ) | ||
| 61 | |||
| 62 | // NostrRelayClient is a client for the nostr.v1.NostrRelay service. | ||
| 63 | type NostrRelayClient interface { | ||
| 64 | // Publish a single event | ||
| 65 | PublishEvent(context.Context, *connect.Request[v1.PublishEventRequest]) (*connect.Response[v1.PublishEventResponse], error) | ||
| 66 | // Subscribe to events matching filters (streaming) | ||
| 67 | Subscribe(context.Context, *connect.Request[v1.SubscribeRequest]) (*connect.ServerStreamForClient[v1.Event], error) | ||
| 68 | // Unsubscribe from an active subscription | ||
| 69 | Unsubscribe(context.Context, *connect.Request[v1.UnsubscribeRequest]) (*connect.Response[v1.Empty], error) | ||
| 70 | // gRPC-specific: batch publish | ||
| 71 | PublishBatch(context.Context, *connect.Request[v1.PublishBatchRequest]) (*connect.Response[v1.PublishBatchResponse], error) | ||
| 72 | // gRPC-specific: paginated query (non-streaming) | ||
| 73 | QueryEvents(context.Context, *connect.Request[v1.QueryRequest]) (*connect.Response[v1.QueryResponse], error) | ||
| 74 | // Event counts (NIP-45) | ||
| 75 | CountEvents(context.Context, *connect.Request[v1.CountRequest]) (*connect.Response[v1.CountResponse], error) | ||
| 76 | } | ||
| 77 | |||
| 78 | // NewNostrRelayClient constructs a client for the nostr.v1.NostrRelay service. By default, it uses | ||
| 79 | // the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends | ||
| 80 | // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or | ||
| 81 | // connect.WithGRPCWeb() options. | ||
| 82 | // | ||
| 83 | // The URL supplied here should be the base URL for the Connect or gRPC server (for example, | ||
| 84 | // http://api.acme.com or https://acme.com/grpc). | ||
| 85 | func NewNostrRelayClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) NostrRelayClient { | ||
| 86 | baseURL = strings.TrimRight(baseURL, "/") | ||
| 87 | nostrRelayMethods := v1.File_nostr_v1_nostr_proto.Services().ByName("NostrRelay").Methods() | ||
| 88 | return &nostrRelayClient{ | ||
| 89 | publishEvent: connect.NewClient[v1.PublishEventRequest, v1.PublishEventResponse]( | ||
| 90 | httpClient, | ||
| 91 | baseURL+NostrRelayPublishEventProcedure, | ||
| 92 | connect.WithSchema(nostrRelayMethods.ByName("PublishEvent")), | ||
| 93 | connect.WithClientOptions(opts...), | ||
| 94 | ), | ||
| 95 | subscribe: connect.NewClient[v1.SubscribeRequest, v1.Event]( | ||
| 96 | httpClient, | ||
| 97 | baseURL+NostrRelaySubscribeProcedure, | ||
| 98 | connect.WithSchema(nostrRelayMethods.ByName("Subscribe")), | ||
| 99 | connect.WithClientOptions(opts...), | ||
| 100 | ), | ||
| 101 | unsubscribe: connect.NewClient[v1.UnsubscribeRequest, v1.Empty]( | ||
| 102 | httpClient, | ||
| 103 | baseURL+NostrRelayUnsubscribeProcedure, | ||
| 104 | connect.WithSchema(nostrRelayMethods.ByName("Unsubscribe")), | ||
| 105 | connect.WithClientOptions(opts...), | ||
| 106 | ), | ||
| 107 | publishBatch: connect.NewClient[v1.PublishBatchRequest, v1.PublishBatchResponse]( | ||
| 108 | httpClient, | ||
| 109 | baseURL+NostrRelayPublishBatchProcedure, | ||
| 110 | connect.WithSchema(nostrRelayMethods.ByName("PublishBatch")), | ||
| 111 | connect.WithClientOptions(opts...), | ||
| 112 | ), | ||
| 113 | queryEvents: connect.NewClient[v1.QueryRequest, v1.QueryResponse]( | ||
| 114 | httpClient, | ||
| 115 | baseURL+NostrRelayQueryEventsProcedure, | ||
| 116 | connect.WithSchema(nostrRelayMethods.ByName("QueryEvents")), | ||
| 117 | connect.WithClientOptions(opts...), | ||
| 118 | ), | ||
| 119 | countEvents: connect.NewClient[v1.CountRequest, v1.CountResponse]( | ||
| 120 | httpClient, | ||
| 121 | baseURL+NostrRelayCountEventsProcedure, | ||
| 122 | connect.WithSchema(nostrRelayMethods.ByName("CountEvents")), | ||
| 123 | connect.WithClientOptions(opts...), | ||
| 124 | ), | ||
| 125 | } | ||
| 126 | } | ||
| 127 | |||
| 128 | // nostrRelayClient implements NostrRelayClient. | ||
| 129 | type nostrRelayClient struct { | ||
| 130 | publishEvent *connect.Client[v1.PublishEventRequest, v1.PublishEventResponse] | ||
| 131 | subscribe *connect.Client[v1.SubscribeRequest, v1.Event] | ||
| 132 | unsubscribe *connect.Client[v1.UnsubscribeRequest, v1.Empty] | ||
| 133 | publishBatch *connect.Client[v1.PublishBatchRequest, v1.PublishBatchResponse] | ||
| 134 | queryEvents *connect.Client[v1.QueryRequest, v1.QueryResponse] | ||
| 135 | countEvents *connect.Client[v1.CountRequest, v1.CountResponse] | ||
| 136 | } | ||
| 137 | |||
| 138 | // PublishEvent calls nostr.v1.NostrRelay.PublishEvent. | ||
| 139 | func (c *nostrRelayClient) PublishEvent(ctx context.Context, req *connect.Request[v1.PublishEventRequest]) (*connect.Response[v1.PublishEventResponse], error) { | ||
| 140 | return c.publishEvent.CallUnary(ctx, req) | ||
| 141 | } | ||
| 142 | |||
| 143 | // Subscribe calls nostr.v1.NostrRelay.Subscribe. | ||
| 144 | func (c *nostrRelayClient) Subscribe(ctx context.Context, req *connect.Request[v1.SubscribeRequest]) (*connect.ServerStreamForClient[v1.Event], error) { | ||
| 145 | return c.subscribe.CallServerStream(ctx, req) | ||
| 146 | } | ||
| 147 | |||
| 148 | // Unsubscribe calls nostr.v1.NostrRelay.Unsubscribe. | ||
| 149 | func (c *nostrRelayClient) Unsubscribe(ctx context.Context, req *connect.Request[v1.UnsubscribeRequest]) (*connect.Response[v1.Empty], error) { | ||
| 150 | return c.unsubscribe.CallUnary(ctx, req) | ||
| 151 | } | ||
| 152 | |||
| 153 | // PublishBatch calls nostr.v1.NostrRelay.PublishBatch. | ||
| 154 | func (c *nostrRelayClient) PublishBatch(ctx context.Context, req *connect.Request[v1.PublishBatchRequest]) (*connect.Response[v1.PublishBatchResponse], error) { | ||
| 155 | return c.publishBatch.CallUnary(ctx, req) | ||
| 156 | } | ||
| 157 | |||
| 158 | // QueryEvents calls nostr.v1.NostrRelay.QueryEvents. | ||
| 159 | func (c *nostrRelayClient) QueryEvents(ctx context.Context, req *connect.Request[v1.QueryRequest]) (*connect.Response[v1.QueryResponse], error) { | ||
| 160 | return c.queryEvents.CallUnary(ctx, req) | ||
| 161 | } | ||
| 162 | |||
| 163 | // CountEvents calls nostr.v1.NostrRelay.CountEvents. | ||
| 164 | func (c *nostrRelayClient) CountEvents(ctx context.Context, req *connect.Request[v1.CountRequest]) (*connect.Response[v1.CountResponse], error) { | ||
| 165 | return c.countEvents.CallUnary(ctx, req) | ||
| 166 | } | ||
| 167 | |||
| 168 | // NostrRelayHandler is an implementation of the nostr.v1.NostrRelay service. | ||
| 169 | type NostrRelayHandler interface { | ||
| 170 | // Publish a single event | ||
| 171 | PublishEvent(context.Context, *connect.Request[v1.PublishEventRequest]) (*connect.Response[v1.PublishEventResponse], error) | ||
| 172 | // Subscribe to events matching filters (streaming) | ||
| 173 | Subscribe(context.Context, *connect.Request[v1.SubscribeRequest], *connect.ServerStream[v1.Event]) error | ||
| 174 | // Unsubscribe from an active subscription | ||
| 175 | Unsubscribe(context.Context, *connect.Request[v1.UnsubscribeRequest]) (*connect.Response[v1.Empty], error) | ||
| 176 | // gRPC-specific: batch publish | ||
| 177 | PublishBatch(context.Context, *connect.Request[v1.PublishBatchRequest]) (*connect.Response[v1.PublishBatchResponse], error) | ||
| 178 | // gRPC-specific: paginated query (non-streaming) | ||
| 179 | QueryEvents(context.Context, *connect.Request[v1.QueryRequest]) (*connect.Response[v1.QueryResponse], error) | ||
| 180 | // Event counts (NIP-45) | ||
| 181 | CountEvents(context.Context, *connect.Request[v1.CountRequest]) (*connect.Response[v1.CountResponse], error) | ||
| 182 | } | ||
| 183 | |||
| 184 | // NewNostrRelayHandler builds an HTTP handler from the service implementation. It returns the path | ||
| 185 | // on which to mount the handler and the handler itself. | ||
| 186 | // | ||
| 187 | // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf | ||
| 188 | // and JSON codecs. They also support gzip compression. | ||
| 189 | func NewNostrRelayHandler(svc NostrRelayHandler, opts ...connect.HandlerOption) (string, http.Handler) { | ||
| 190 | nostrRelayMethods := v1.File_nostr_v1_nostr_proto.Services().ByName("NostrRelay").Methods() | ||
| 191 | nostrRelayPublishEventHandler := connect.NewUnaryHandler( | ||
| 192 | NostrRelayPublishEventProcedure, | ||
| 193 | svc.PublishEvent, | ||
| 194 | connect.WithSchema(nostrRelayMethods.ByName("PublishEvent")), | ||
| 195 | connect.WithHandlerOptions(opts...), | ||
| 196 | ) | ||
| 197 | nostrRelaySubscribeHandler := connect.NewServerStreamHandler( | ||
| 198 | NostrRelaySubscribeProcedure, | ||
| 199 | svc.Subscribe, | ||
| 200 | connect.WithSchema(nostrRelayMethods.ByName("Subscribe")), | ||
| 201 | connect.WithHandlerOptions(opts...), | ||
| 202 | ) | ||
| 203 | nostrRelayUnsubscribeHandler := connect.NewUnaryHandler( | ||
| 204 | NostrRelayUnsubscribeProcedure, | ||
| 205 | svc.Unsubscribe, | ||
| 206 | connect.WithSchema(nostrRelayMethods.ByName("Unsubscribe")), | ||
| 207 | connect.WithHandlerOptions(opts...), | ||
| 208 | ) | ||
| 209 | nostrRelayPublishBatchHandler := connect.NewUnaryHandler( | ||
| 210 | NostrRelayPublishBatchProcedure, | ||
| 211 | svc.PublishBatch, | ||
| 212 | connect.WithSchema(nostrRelayMethods.ByName("PublishBatch")), | ||
| 213 | connect.WithHandlerOptions(opts...), | ||
| 214 | ) | ||
| 215 | nostrRelayQueryEventsHandler := connect.NewUnaryHandler( | ||
| 216 | NostrRelayQueryEventsProcedure, | ||
| 217 | svc.QueryEvents, | ||
| 218 | connect.WithSchema(nostrRelayMethods.ByName("QueryEvents")), | ||
| 219 | connect.WithHandlerOptions(opts...), | ||
| 220 | ) | ||
| 221 | nostrRelayCountEventsHandler := connect.NewUnaryHandler( | ||
| 222 | NostrRelayCountEventsProcedure, | ||
| 223 | svc.CountEvents, | ||
| 224 | connect.WithSchema(nostrRelayMethods.ByName("CountEvents")), | ||
| 225 | connect.WithHandlerOptions(opts...), | ||
| 226 | ) | ||
| 227 | return "/nostr.v1.NostrRelay/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 228 | switch r.URL.Path { | ||
| 229 | case NostrRelayPublishEventProcedure: | ||
| 230 | nostrRelayPublishEventHandler.ServeHTTP(w, r) | ||
| 231 | case NostrRelaySubscribeProcedure: | ||
| 232 | nostrRelaySubscribeHandler.ServeHTTP(w, r) | ||
| 233 | case NostrRelayUnsubscribeProcedure: | ||
| 234 | nostrRelayUnsubscribeHandler.ServeHTTP(w, r) | ||
| 235 | case NostrRelayPublishBatchProcedure: | ||
| 236 | nostrRelayPublishBatchHandler.ServeHTTP(w, r) | ||
| 237 | case NostrRelayQueryEventsProcedure: | ||
| 238 | nostrRelayQueryEventsHandler.ServeHTTP(w, r) | ||
| 239 | case NostrRelayCountEventsProcedure: | ||
| 240 | nostrRelayCountEventsHandler.ServeHTTP(w, r) | ||
| 241 | default: | ||
| 242 | http.NotFound(w, r) | ||
| 243 | } | ||
| 244 | }) | ||
| 245 | } | ||
| 246 | |||
| 247 | // UnimplementedNostrRelayHandler returns CodeUnimplemented from all methods. | ||
| 248 | type UnimplementedNostrRelayHandler struct{} | ||
| 249 | |||
| 250 | func (UnimplementedNostrRelayHandler) PublishEvent(context.Context, *connect.Request[v1.PublishEventRequest]) (*connect.Response[v1.PublishEventResponse], error) { | ||
| 251 | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("nostr.v1.NostrRelay.PublishEvent is not implemented")) | ||
| 252 | } | ||
| 253 | |||
| 254 | func (UnimplementedNostrRelayHandler) Subscribe(context.Context, *connect.Request[v1.SubscribeRequest], *connect.ServerStream[v1.Event]) error { | ||
| 255 | return connect.NewError(connect.CodeUnimplemented, errors.New("nostr.v1.NostrRelay.Subscribe is not implemented")) | ||
| 256 | } | ||
| 257 | |||
| 258 | func (UnimplementedNostrRelayHandler) Unsubscribe(context.Context, *connect.Request[v1.UnsubscribeRequest]) (*connect.Response[v1.Empty], error) { | ||
| 259 | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("nostr.v1.NostrRelay.Unsubscribe is not implemented")) | ||
| 260 | } | ||
| 261 | |||
| 262 | func (UnimplementedNostrRelayHandler) PublishBatch(context.Context, *connect.Request[v1.PublishBatchRequest]) (*connect.Response[v1.PublishBatchResponse], error) { | ||
| 263 | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("nostr.v1.NostrRelay.PublishBatch is not implemented")) | ||
| 264 | } | ||
| 265 | |||
| 266 | func (UnimplementedNostrRelayHandler) QueryEvents(context.Context, *connect.Request[v1.QueryRequest]) (*connect.Response[v1.QueryResponse], error) { | ||
| 267 | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("nostr.v1.NostrRelay.QueryEvents is not implemented")) | ||
| 268 | } | ||
| 269 | |||
| 270 | func (UnimplementedNostrRelayHandler) CountEvents(context.Context, *connect.Request[v1.CountRequest]) (*connect.Response[v1.CountResponse], error) { | ||
| 271 | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("nostr.v1.NostrRelay.CountEvents is not implemented")) | ||
| 272 | } | ||
| 273 | |||
| 274 | // RelayAdminClient is a client for the nostr.v1.RelayAdmin service. | ||
| 275 | type RelayAdminClient interface { | ||
| 276 | GetStats(context.Context, *connect.Request[v1.Empty]) (*connect.Response[v1.RelayStats], error) | ||
| 277 | GetConnections(context.Context, *connect.Request[v1.Empty]) (*connect.Response[v1.ConnectionList], error) | ||
| 278 | BanPublicKey(context.Context, *connect.Request[v1.BanRequest]) (*connect.Response[v1.Empty], error) | ||
| 279 | GetStorageInfo(context.Context, *connect.Request[v1.Empty]) (*connect.Response[v1.StorageStats], error) | ||
| 280 | } | ||
| 281 | |||
| 282 | // NewRelayAdminClient constructs a client for the nostr.v1.RelayAdmin service. By default, it uses | ||
| 283 | // the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends | ||
| 284 | // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or | ||
| 285 | // connect.WithGRPCWeb() options. | ||
| 286 | // | ||
| 287 | // The URL supplied here should be the base URL for the Connect or gRPC server (for example, | ||
| 288 | // http://api.acme.com or https://acme.com/grpc). | ||
| 289 | func NewRelayAdminClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) RelayAdminClient { | ||
| 290 | baseURL = strings.TrimRight(baseURL, "/") | ||
| 291 | relayAdminMethods := v1.File_nostr_v1_nostr_proto.Services().ByName("RelayAdmin").Methods() | ||
| 292 | return &relayAdminClient{ | ||
| 293 | getStats: connect.NewClient[v1.Empty, v1.RelayStats]( | ||
| 294 | httpClient, | ||
| 295 | baseURL+RelayAdminGetStatsProcedure, | ||
| 296 | connect.WithSchema(relayAdminMethods.ByName("GetStats")), | ||
| 297 | connect.WithClientOptions(opts...), | ||
| 298 | ), | ||
| 299 | getConnections: connect.NewClient[v1.Empty, v1.ConnectionList]( | ||
| 300 | httpClient, | ||
| 301 | baseURL+RelayAdminGetConnectionsProcedure, | ||
| 302 | connect.WithSchema(relayAdminMethods.ByName("GetConnections")), | ||
| 303 | connect.WithClientOptions(opts...), | ||
| 304 | ), | ||
| 305 | banPublicKey: connect.NewClient[v1.BanRequest, v1.Empty]( | ||
| 306 | httpClient, | ||
| 307 | baseURL+RelayAdminBanPublicKeyProcedure, | ||
| 308 | connect.WithSchema(relayAdminMethods.ByName("BanPublicKey")), | ||
| 309 | connect.WithClientOptions(opts...), | ||
| 310 | ), | ||
| 311 | getStorageInfo: connect.NewClient[v1.Empty, v1.StorageStats]( | ||
| 312 | httpClient, | ||
| 313 | baseURL+RelayAdminGetStorageInfoProcedure, | ||
| 314 | connect.WithSchema(relayAdminMethods.ByName("GetStorageInfo")), | ||
| 315 | connect.WithClientOptions(opts...), | ||
| 316 | ), | ||
| 317 | } | ||
| 318 | } | ||
| 319 | |||
| 320 | // relayAdminClient implements RelayAdminClient. | ||
| 321 | type relayAdminClient struct { | ||
| 322 | getStats *connect.Client[v1.Empty, v1.RelayStats] | ||
| 323 | getConnections *connect.Client[v1.Empty, v1.ConnectionList] | ||
| 324 | banPublicKey *connect.Client[v1.BanRequest, v1.Empty] | ||
| 325 | getStorageInfo *connect.Client[v1.Empty, v1.StorageStats] | ||
| 326 | } | ||
| 327 | |||
| 328 | // GetStats calls nostr.v1.RelayAdmin.GetStats. | ||
| 329 | func (c *relayAdminClient) GetStats(ctx context.Context, req *connect.Request[v1.Empty]) (*connect.Response[v1.RelayStats], error) { | ||
| 330 | return c.getStats.CallUnary(ctx, req) | ||
| 331 | } | ||
| 332 | |||
| 333 | // GetConnections calls nostr.v1.RelayAdmin.GetConnections. | ||
| 334 | func (c *relayAdminClient) GetConnections(ctx context.Context, req *connect.Request[v1.Empty]) (*connect.Response[v1.ConnectionList], error) { | ||
| 335 | return c.getConnections.CallUnary(ctx, req) | ||
| 336 | } | ||
| 337 | |||
| 338 | // BanPublicKey calls nostr.v1.RelayAdmin.BanPublicKey. | ||
| 339 | func (c *relayAdminClient) BanPublicKey(ctx context.Context, req *connect.Request[v1.BanRequest]) (*connect.Response[v1.Empty], error) { | ||
| 340 | return c.banPublicKey.CallUnary(ctx, req) | ||
| 341 | } | ||
| 342 | |||
| 343 | // GetStorageInfo calls nostr.v1.RelayAdmin.GetStorageInfo. | ||
| 344 | func (c *relayAdminClient) GetStorageInfo(ctx context.Context, req *connect.Request[v1.Empty]) (*connect.Response[v1.StorageStats], error) { | ||
| 345 | return c.getStorageInfo.CallUnary(ctx, req) | ||
| 346 | } | ||
| 347 | |||
| 348 | // RelayAdminHandler is an implementation of the nostr.v1.RelayAdmin service. | ||
| 349 | type RelayAdminHandler interface { | ||
| 350 | GetStats(context.Context, *connect.Request[v1.Empty]) (*connect.Response[v1.RelayStats], error) | ||
| 351 | GetConnections(context.Context, *connect.Request[v1.Empty]) (*connect.Response[v1.ConnectionList], error) | ||
| 352 | BanPublicKey(context.Context, *connect.Request[v1.BanRequest]) (*connect.Response[v1.Empty], error) | ||
| 353 | GetStorageInfo(context.Context, *connect.Request[v1.Empty]) (*connect.Response[v1.StorageStats], error) | ||
| 354 | } | ||
| 355 | |||
| 356 | // NewRelayAdminHandler builds an HTTP handler from the service implementation. It returns the path | ||
| 357 | // on which to mount the handler and the handler itself. | ||
| 358 | // | ||
| 359 | // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf | ||
| 360 | // and JSON codecs. They also support gzip compression. | ||
| 361 | func NewRelayAdminHandler(svc RelayAdminHandler, opts ...connect.HandlerOption) (string, http.Handler) { | ||
| 362 | relayAdminMethods := v1.File_nostr_v1_nostr_proto.Services().ByName("RelayAdmin").Methods() | ||
| 363 | relayAdminGetStatsHandler := connect.NewUnaryHandler( | ||
| 364 | RelayAdminGetStatsProcedure, | ||
| 365 | svc.GetStats, | ||
| 366 | connect.WithSchema(relayAdminMethods.ByName("GetStats")), | ||
| 367 | connect.WithHandlerOptions(opts...), | ||
| 368 | ) | ||
| 369 | relayAdminGetConnectionsHandler := connect.NewUnaryHandler( | ||
| 370 | RelayAdminGetConnectionsProcedure, | ||
| 371 | svc.GetConnections, | ||
| 372 | connect.WithSchema(relayAdminMethods.ByName("GetConnections")), | ||
| 373 | connect.WithHandlerOptions(opts...), | ||
| 374 | ) | ||
| 375 | relayAdminBanPublicKeyHandler := connect.NewUnaryHandler( | ||
| 376 | RelayAdminBanPublicKeyProcedure, | ||
| 377 | svc.BanPublicKey, | ||
| 378 | connect.WithSchema(relayAdminMethods.ByName("BanPublicKey")), | ||
| 379 | connect.WithHandlerOptions(opts...), | ||
| 380 | ) | ||
| 381 | relayAdminGetStorageInfoHandler := connect.NewUnaryHandler( | ||
| 382 | RelayAdminGetStorageInfoProcedure, | ||
| 383 | svc.GetStorageInfo, | ||
| 384 | connect.WithSchema(relayAdminMethods.ByName("GetStorageInfo")), | ||
| 385 | connect.WithHandlerOptions(opts...), | ||
| 386 | ) | ||
| 387 | return "/nostr.v1.RelayAdmin/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 388 | switch r.URL.Path { | ||
| 389 | case RelayAdminGetStatsProcedure: | ||
| 390 | relayAdminGetStatsHandler.ServeHTTP(w, r) | ||
| 391 | case RelayAdminGetConnectionsProcedure: | ||
| 392 | relayAdminGetConnectionsHandler.ServeHTTP(w, r) | ||
| 393 | case RelayAdminBanPublicKeyProcedure: | ||
| 394 | relayAdminBanPublicKeyHandler.ServeHTTP(w, r) | ||
| 395 | case RelayAdminGetStorageInfoProcedure: | ||
| 396 | relayAdminGetStorageInfoHandler.ServeHTTP(w, r) | ||
| 397 | default: | ||
| 398 | http.NotFound(w, r) | ||
| 399 | } | ||
| 400 | }) | ||
| 401 | } | ||
| 402 | |||
| 403 | // UnimplementedRelayAdminHandler returns CodeUnimplemented from all methods. | ||
| 404 | type UnimplementedRelayAdminHandler struct{} | ||
| 405 | |||
| 406 | func (UnimplementedRelayAdminHandler) GetStats(context.Context, *connect.Request[v1.Empty]) (*connect.Response[v1.RelayStats], error) { | ||
| 407 | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("nostr.v1.RelayAdmin.GetStats is not implemented")) | ||
| 408 | } | ||
| 409 | |||
| 410 | func (UnimplementedRelayAdminHandler) GetConnections(context.Context, *connect.Request[v1.Empty]) (*connect.Response[v1.ConnectionList], error) { | ||
| 411 | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("nostr.v1.RelayAdmin.GetConnections is not implemented")) | ||
| 412 | } | ||
| 413 | |||
| 414 | func (UnimplementedRelayAdminHandler) BanPublicKey(context.Context, *connect.Request[v1.BanRequest]) (*connect.Response[v1.Empty], error) { | ||
| 415 | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("nostr.v1.RelayAdmin.BanPublicKey is not implemented")) | ||
| 416 | } | ||
| 417 | |||
| 418 | func (UnimplementedRelayAdminHandler) GetStorageInfo(context.Context, *connect.Request[v1.Empty]) (*connect.Response[v1.StorageStats], error) { | ||
| 419 | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("nostr.v1.RelayAdmin.GetStorageInfo is not implemented")) | ||
| 420 | } | ||
diff --git a/buf.gen.yaml b/buf.gen.yaml index e6f485a..01fea00 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml | |||
| @@ -13,3 +13,7 @@ plugins: | |||
| 13 | out: api | 13 | out: api |
| 14 | opt: | 14 | opt: |
| 15 | - paths=source_relative | 15 | - paths=source_relative |
| 16 | - remote: buf.build/connectrpc/go | ||
| 17 | out: api | ||
| 18 | opt: | ||
| 19 | - paths=source_relative | ||
diff --git a/cmd/relay/main.go b/cmd/relay/main.go index 53296b9..9cf6ad6 100644 --- a/cmd/relay/main.go +++ b/cmd/relay/main.go | |||
| @@ -11,9 +11,14 @@ import ( | |||
| 11 | 11 | ||
| 12 | "context" | 12 | "context" |
| 13 | 13 | ||
| 14 | "connectrpc.com/connect" | ||
| 15 | "golang.org/x/net/http2" | ||
| 16 | "golang.org/x/net/http2/h2c" | ||
| 14 | "google.golang.org/grpc" | 17 | "google.golang.org/grpc" |
| 15 | 18 | ||
| 16 | pb "northwest.io/nostr-grpc/api/nostr/v1" | 19 | pb "northwest.io/nostr-grpc/api/nostr/v1" |
| 20 | "northwest.io/nostr-grpc/api/nostr/v1/nostrv1connect" | ||
| 21 | connecthandler "northwest.io/nostr-grpc/internal/handler/connect" | ||
| 17 | grpchandler "northwest.io/nostr-grpc/internal/handler/grpc" | 22 | grpchandler "northwest.io/nostr-grpc/internal/handler/grpc" |
| 18 | wshandler "northwest.io/nostr-grpc/internal/handler/websocket" | 23 | wshandler "northwest.io/nostr-grpc/internal/handler/websocket" |
| 19 | "northwest.io/nostr-grpc/internal/storage" | 24 | "northwest.io/nostr-grpc/internal/storage" |
| @@ -39,7 +44,14 @@ func main() { | |||
| 39 | grpcHandler := grpchandler.NewServer(store) | 44 | grpcHandler := grpchandler.NewServer(store) |
| 40 | grpcHandler.SetSubscriptionManager(subManager) | 45 | grpcHandler.SetSubscriptionManager(subManager) |
| 41 | 46 | ||
| 47 | connectHandler := connecthandler.NewHandler(grpcHandler) | ||
| 48 | |||
| 49 | mux := http.NewServeMux() | ||
| 50 | path, handler := nostrv1connect.NewNostrRelayHandler(connectHandler, connect.WithInterceptors()) | ||
| 51 | mux.Handle(path, handler) | ||
| 52 | |||
| 42 | wsHandler := wshandler.NewHandler(store, subManager) | 53 | wsHandler := wshandler.NewHandler(store, subManager) |
| 54 | mux.Handle("/", wsHandler) | ||
| 43 | 55 | ||
| 44 | grpcLis, err := net.Listen("tcp", *grpcAddr) | 56 | grpcLis, err := net.Listen("tcp", *grpcAddr) |
| 45 | if err != nil { | 57 | if err != nil { |
| @@ -51,11 +63,13 @@ func main() { | |||
| 51 | 63 | ||
| 52 | httpServer := &http.Server{ | 64 | httpServer := &http.Server{ |
| 53 | Addr: *wsAddr, | 65 | Addr: *wsAddr, |
| 54 | Handler: wsHandler, | 66 | Handler: h2c.NewHandler(mux, &http2.Server{}), |
| 55 | } | 67 | } |
| 56 | 68 | ||
| 57 | log.Printf("gRPC server listening on %s", *grpcAddr) | 69 | log.Printf("gRPC server listening on %s", *grpcAddr) |
| 58 | log.Printf("WebSocket server listening on %s", *wsAddr) | 70 | log.Printf("HTTP server listening on %s", *wsAddr) |
| 71 | log.Printf(" - Connect (gRPC-Web) at %s/nostr.v1.NostrRelay/*", *wsAddr) | ||
| 72 | log.Printf(" - WebSocket (Nostr) at %s/", *wsAddr) | ||
| 59 | log.Printf("Database: %s", *dbPath) | 73 | log.Printf("Database: %s", *dbPath) |
| 60 | 74 | ||
| 61 | sigChan := make(chan os.Signal, 1) | 75 | sigChan := make(chan os.Signal, 1) |
| @@ -11,6 +11,7 @@ require ( | |||
| 11 | ) | 11 | ) |
| 12 | 12 | ||
| 13 | require ( | 13 | require ( |
| 14 | connectrpc.com/connect v1.19.1 // indirect | ||
| 14 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect | 15 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect |
| 15 | github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect | 16 | github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect |
| 16 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect | 17 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect |
| @@ -20,9 +21,9 @@ require ( | |||
| 20 | github.com/ncruces/go-strftime v1.0.0 // indirect | 21 | github.com/ncruces/go-strftime v1.0.0 // indirect |
| 21 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect | 22 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect |
| 22 | golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect | 23 | golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect |
| 23 | golang.org/x/net v0.48.0 // indirect | 24 | golang.org/x/net v0.50.0 // indirect |
| 24 | golang.org/x/sys v0.39.0 // indirect | 25 | golang.org/x/sys v0.41.0 // indirect |
| 25 | golang.org/x/text v0.32.0 // indirect | 26 | golang.org/x/text v0.34.0 // indirect |
| 26 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect | 27 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect |
| 27 | modernc.org/libc v1.67.6 // indirect | 28 | modernc.org/libc v1.67.6 // indirect |
| 28 | modernc.org/mathutil v1.7.1 // indirect | 29 | modernc.org/mathutil v1.7.1 // indirect |
| @@ -1,3 +1,5 @@ | |||
| 1 | connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= | ||
| 2 | connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= | ||
| 1 | github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= | 3 | github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= |
| 2 | github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= | 4 | github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= |
| 3 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= | 5 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= |
| @@ -50,17 +52,25 @@ golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2 | |||
| 50 | golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= | 52 | golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= |
| 51 | golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= | 53 | golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= |
| 52 | golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= | 54 | golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= |
| 55 | golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= | ||
| 53 | golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= | 56 | golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= |
| 54 | golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= | 57 | golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= |
| 58 | golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= | ||
| 59 | golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= | ||
| 55 | golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= | 60 | golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= |
| 56 | golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= | 61 | golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= |
| 57 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | 62 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
| 58 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= | 63 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= |
| 59 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | 64 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= |
| 65 | golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= | ||
| 66 | golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | ||
| 60 | golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= | 67 | golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= |
| 61 | golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= | 68 | golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= |
| 69 | golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= | ||
| 70 | golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= | ||
| 62 | golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= | 71 | golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= |
| 63 | golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= | 72 | golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= |
| 73 | golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= | ||
| 64 | gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= | 74 | gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= |
| 65 | gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= | 75 | gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= |
| 66 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= | 76 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= |
diff --git a/internal/handler/connect/handler.go b/internal/handler/connect/handler.go new file mode 100644 index 0000000..f33e4fc --- /dev/null +++ b/internal/handler/connect/handler.go | |||
| @@ -0,0 +1,101 @@ | |||
| 1 | package connect | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | |||
| 6 | "connectrpc.com/connect" | ||
| 7 | "google.golang.org/grpc/metadata" | ||
| 8 | |||
| 9 | pb "northwest.io/nostr-grpc/api/nostr/v1" | ||
| 10 | "northwest.io/nostr-grpc/api/nostr/v1/nostrv1connect" | ||
| 11 | grpchandler "northwest.io/nostr-grpc/internal/handler/grpc" | ||
| 12 | ) | ||
| 13 | |||
| 14 | type Handler struct { | ||
| 15 | grpcServer *grpchandler.Server | ||
| 16 | } | ||
| 17 | |||
| 18 | func NewHandler(grpcServer *grpchandler.Server) *Handler { | ||
| 19 | return &Handler{grpcServer: grpcServer} | ||
| 20 | } | ||
| 21 | |||
| 22 | func (h *Handler) PublishEvent(ctx context.Context, req *connect.Request[pb.PublishEventRequest]) (*connect.Response[pb.PublishEventResponse], error) { | ||
| 23 | resp, err := h.grpcServer.PublishEvent(ctx, req.Msg) | ||
| 24 | if err != nil { | ||
| 25 | return nil, err | ||
| 26 | } | ||
| 27 | return connect.NewResponse(resp), nil | ||
| 28 | } | ||
| 29 | |||
| 30 | func (h *Handler) Subscribe(ctx context.Context, req *connect.Request[pb.SubscribeRequest], stream *connect.ServerStream[pb.Event]) error { | ||
| 31 | return h.grpcServer.Subscribe(req.Msg, &subscribeStreamAdapter{stream: stream, ctx: ctx}) | ||
| 32 | } | ||
| 33 | |||
| 34 | func (h *Handler) Unsubscribe(ctx context.Context, req *connect.Request[pb.UnsubscribeRequest]) (*connect.Response[pb.Empty], error) { | ||
| 35 | resp, err := h.grpcServer.Unsubscribe(ctx, req.Msg) | ||
| 36 | if err != nil { | ||
| 37 | return nil, err | ||
| 38 | } | ||
| 39 | return connect.NewResponse(resp), nil | ||
| 40 | } | ||
| 41 | |||
| 42 | func (h *Handler) PublishBatch(ctx context.Context, req *connect.Request[pb.PublishBatchRequest]) (*connect.Response[pb.PublishBatchResponse], error) { | ||
| 43 | resp, err := h.grpcServer.PublishBatch(ctx, req.Msg) | ||
| 44 | if err != nil { | ||
| 45 | return nil, err | ||
| 46 | } | ||
| 47 | return connect.NewResponse(resp), nil | ||
| 48 | } | ||
| 49 | |||
| 50 | func (h *Handler) QueryEvents(ctx context.Context, req *connect.Request[pb.QueryRequest]) (*connect.Response[pb.QueryResponse], error) { | ||
| 51 | resp, err := h.grpcServer.QueryEvents(ctx, req.Msg) | ||
| 52 | if err != nil { | ||
| 53 | return nil, err | ||
| 54 | } | ||
| 55 | return connect.NewResponse(resp), nil | ||
| 56 | } | ||
| 57 | |||
| 58 | func (h *Handler) CountEvents(ctx context.Context, req *connect.Request[pb.CountRequest]) (*connect.Response[pb.CountResponse], error) { | ||
| 59 | resp, err := h.grpcServer.CountEvents(ctx, req.Msg) | ||
| 60 | if err != nil { | ||
| 61 | return nil, err | ||
| 62 | } | ||
| 63 | return connect.NewResponse(resp), nil | ||
| 64 | } | ||
| 65 | |||
| 66 | type subscribeStreamAdapter struct { | ||
| 67 | stream *connect.ServerStream[pb.Event] | ||
| 68 | ctx context.Context | ||
| 69 | } | ||
| 70 | |||
| 71 | func (s *subscribeStreamAdapter) Send(event *pb.Event) error { | ||
| 72 | return s.stream.Send(event) | ||
| 73 | } | ||
| 74 | |||
| 75 | func (s *subscribeStreamAdapter) Context() context.Context { | ||
| 76 | return s.ctx | ||
| 77 | } | ||
| 78 | |||
| 79 | func (s *subscribeStreamAdapter) SetHeader(md metadata.MD) error { | ||
| 80 | return nil | ||
| 81 | } | ||
| 82 | |||
| 83 | func (s *subscribeStreamAdapter) SendHeader(md metadata.MD) error { | ||
| 84 | return nil | ||
| 85 | } | ||
| 86 | |||
| 87 | func (s *subscribeStreamAdapter) SetTrailer(md metadata.MD) { | ||
| 88 | } | ||
| 89 | |||
| 90 | func (s *subscribeStreamAdapter) SendMsg(m any) error { | ||
| 91 | if event, ok := m.(*pb.Event); ok { | ||
| 92 | return s.Send(event) | ||
| 93 | } | ||
| 94 | return nil | ||
| 95 | } | ||
| 96 | |||
| 97 | func (s *subscribeStreamAdapter) RecvMsg(m any) error { | ||
| 98 | return nil | ||
| 99 | } | ||
| 100 | |||
| 101 | var _ nostrv1connect.NostrRelayHandler = (*Handler)(nil) | ||
