client.go raw
1 // Package grpc provides a gRPC client that implements the acl.I interface.
2 // This allows the relay to use a remote ACL server via gRPC.
3 package grpc
4
5 import (
6 "context"
7 "time"
8
9 "google.golang.org/grpc"
10 "google.golang.org/grpc/credentials/insecure"
11 "next.orly.dev/pkg/lol/chk"
12 "next.orly.dev/pkg/lol/log"
13
14 "next.orly.dev/pkg/nostr/encoders/event"
15 acliface "next.orly.dev/pkg/interfaces/acl"
16 orlyaclv1 "next.orly.dev/pkg/proto/orlyacl/v1"
17 orlydbv1 "next.orly.dev/pkg/proto/orlydb/v1"
18 )
19
20 // Client implements the acl.I interface via gRPC.
21 type Client struct {
22 conn *grpc.ClientConn
23 client orlyaclv1.ACLServiceClient
24 ready chan struct{}
25 mode string
26 }
27
28 // Verify Client implements acl.I at compile time.
29 var _ acliface.I = (*Client)(nil)
30
31 // Verify Client implements acl.PolicyChecker at compile time.
32 var _ acliface.PolicyChecker = (*Client)(nil)
33
34 // ClientConfig holds configuration for the gRPC ACL client.
35 type ClientConfig struct {
36 ServerAddress string
37 ConnectTimeout time.Duration
38 }
39
40 // New creates a new gRPC ACL client.
41 func New(ctx context.Context, cfg *ClientConfig) (*Client, error) {
42 timeout := cfg.ConnectTimeout
43 if timeout == 0 {
44 timeout = 10 * time.Second
45 }
46
47 dialCtx, cancel := context.WithTimeout(ctx, timeout)
48 defer cancel()
49
50 conn, err := grpc.DialContext(dialCtx, cfg.ServerAddress,
51 grpc.WithTransportCredentials(insecure.NewCredentials()),
52 grpc.WithDefaultCallOptions(
53 grpc.MaxCallRecvMsgSize(16<<20), // 16MB
54 grpc.MaxCallSendMsgSize(16<<20), // 16MB
55 ),
56 )
57 if err != nil {
58 return nil, err
59 }
60
61 c := &Client{
62 conn: conn,
63 client: orlyaclv1.NewACLServiceClient(conn),
64 ready: make(chan struct{}),
65 }
66
67 // Check if server is ready and get mode
68 go c.waitForReady(ctx)
69
70 return c, nil
71 }
72
73 func (c *Client) waitForReady(ctx context.Context) {
74 for {
75 select {
76 case <-ctx.Done():
77 return
78 default:
79 resp, err := c.client.Ready(ctx, &orlyaclv1.Empty{})
80 if err == nil && resp.Ready {
81 // Get mode from server
82 modeResp, err := c.client.GetMode(ctx, &orlyaclv1.Empty{})
83 if err == nil {
84 c.mode = modeResp.Mode
85 }
86 close(c.ready)
87 log.I.F("gRPC ACL client connected and ready, mode: %s", c.mode)
88 return
89 }
90 time.Sleep(100 * time.Millisecond)
91 }
92 }
93 }
94
95 // Close closes the gRPC connection.
96 func (c *Client) Close() error {
97 if c.conn != nil {
98 return c.conn.Close()
99 }
100 return nil
101 }
102
103 // Ready returns a channel that closes when the client is ready.
104 func (c *Client) Ready() <-chan struct{} {
105 return c.ready
106 }
107
108 // === acl.I Interface Implementation ===
109
110 func (c *Client) Configure(cfg ...any) error {
111 // Configuration is done on the server side
112 // The client just passes through to the server
113 return nil
114 }
115
116 func (c *Client) GetAccessLevel(pub []byte, address string) string {
117 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
118 defer cancel()
119
120 resp, err := c.client.GetAccessLevel(ctx, &orlyaclv1.AccessLevelRequest{
121 Pubkey: pub,
122 Address: address,
123 })
124 if chk.E(err) {
125 return "none"
126 }
127 return resp.Level
128 }
129
130 func (c *Client) GetACLInfo() (name, description, documentation string) {
131 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
132 defer cancel()
133
134 resp, err := c.client.GetACLInfo(ctx, &orlyaclv1.Empty{})
135 if chk.E(err) {
136 return "", "", ""
137 }
138 return resp.Name, resp.Description, resp.Documentation
139 }
140
141 func (c *Client) Syncer() {
142 // The syncer runs on the ACL server, not the client
143 // This is a no-op for the gRPC client
144 }
145
146 func (c *Client) Type() string {
147 return c.mode
148 }
149
150 // === acl.PolicyChecker Interface Implementation ===
151
152 func (c *Client) CheckPolicy(ev *event.E) (bool, error) {
153 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
154 defer cancel()
155
156 resp, err := c.client.CheckPolicy(ctx, &orlyaclv1.PolicyCheckRequest{
157 Event: orlydbv1.EventToProto(ev),
158 })
159 if err != nil {
160 return false, err
161 }
162 if resp.Error != "" {
163 return resp.Allowed, &policyError{msg: resp.Error}
164 }
165 return resp.Allowed, nil
166 }
167
168 // policyError is a simple error type for policy check failures
169 type policyError struct {
170 msg string
171 }
172
173 func (e *policyError) Error() string {
174 return e.msg
175 }
176
177 // === Follows ACL Methods ===
178
179 // GetThrottleDelay returns the progressive throttle delay for a pubkey.
180 func (c *Client) GetThrottleDelay(pubkey []byte, ip string) time.Duration {
181 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
182 defer cancel()
183
184 resp, err := c.client.GetThrottleDelay(ctx, &orlyaclv1.ThrottleDelayRequest{
185 Pubkey: pubkey,
186 Ip: ip,
187 })
188 if chk.E(err) {
189 return 0
190 }
191 return time.Duration(resp.DelayMs) * time.Millisecond
192 }
193
194 // AddFollow adds a pubkey to the followed list.
195 func (c *Client) AddFollow(pubkey []byte) error {
196 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
197 defer cancel()
198
199 _, err := c.client.AddFollow(ctx, &orlyaclv1.AddFollowRequest{
200 Pubkey: pubkey,
201 })
202 return err
203 }
204
205 // GetFollowedPubkeys returns all followed pubkeys.
206 func (c *Client) GetFollowedPubkeys() [][]byte {
207 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
208 defer cancel()
209
210 resp, err := c.client.GetFollowedPubkeys(ctx, &orlyaclv1.Empty{})
211 if chk.E(err) {
212 return nil
213 }
214 return resp.Pubkeys
215 }
216
217 // GetAdminRelays returns the admin relay URLs.
218 func (c *Client) GetAdminRelays() []string {
219 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
220 defer cancel()
221
222 resp, err := c.client.GetAdminRelays(ctx, &orlyaclv1.Empty{})
223 if chk.E(err) {
224 return nil
225 }
226 return resp.Urls
227 }
228
229 // === Managed ACL Methods ===
230
231 // BanPubkey adds a pubkey to the ban list.
232 func (c *Client) BanPubkey(pubkey, reason string) error {
233 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
234 defer cancel()
235
236 _, err := c.client.BanPubkey(ctx, &orlyaclv1.BanPubkeyRequest{
237 Pubkey: pubkey,
238 Reason: reason,
239 })
240 return err
241 }
242
243 // UnbanPubkey removes a pubkey from the ban list.
244 func (c *Client) UnbanPubkey(pubkey string) error {
245 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
246 defer cancel()
247
248 _, err := c.client.UnbanPubkey(ctx, &orlyaclv1.PubkeyRequest{
249 Pubkey: pubkey,
250 })
251 return err
252 }
253
254 // AllowPubkey adds a pubkey to the allow list.
255 func (c *Client) AllowPubkey(pubkey, reason string) error {
256 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
257 defer cancel()
258
259 _, err := c.client.AllowPubkey(ctx, &orlyaclv1.AllowPubkeyRequest{
260 Pubkey: pubkey,
261 Reason: reason,
262 })
263 return err
264 }
265
266 // DisallowPubkey removes a pubkey from the allow list.
267 func (c *Client) DisallowPubkey(pubkey string) error {
268 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
269 defer cancel()
270
271 _, err := c.client.DisallowPubkey(ctx, &orlyaclv1.PubkeyRequest{
272 Pubkey: pubkey,
273 })
274 return err
275 }
276
277 // BlockIP adds an IP to the block list.
278 func (c *Client) BlockIP(ip, reason string) error {
279 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
280 defer cancel()
281
282 _, err := c.client.BlockIP(ctx, &orlyaclv1.BlockIPRequest{
283 Ip: ip,
284 Reason: reason,
285 })
286 return err
287 }
288
289 // UnblockIP removes an IP from the block list.
290 func (c *Client) UnblockIP(ip string) error {
291 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
292 defer cancel()
293
294 _, err := c.client.UnblockIP(ctx, &orlyaclv1.IPRequest{
295 Ip: ip,
296 })
297 return err
298 }
299
300 // UpdatePeerAdmins updates the peer relay identity pubkeys.
301 func (c *Client) UpdatePeerAdmins(peerPubkeys [][]byte) error {
302 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
303 defer cancel()
304
305 _, err := c.client.UpdatePeerAdmins(ctx, &orlyaclv1.UpdatePeerAdminsRequest{
306 PeerPubkeys: peerPubkeys,
307 })
308 return err
309 }
310
311 // === Curating ACL Methods ===
312
313 // TrustPubkey adds a pubkey to the trusted list.
314 func (c *Client) TrustPubkey(pubkey, note string) error {
315 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
316 defer cancel()
317
318 _, err := c.client.TrustPubkey(ctx, &orlyaclv1.TrustPubkeyRequest{
319 Pubkey: pubkey,
320 Note: note,
321 })
322 return err
323 }
324
325 // UntrustPubkey removes a pubkey from the trusted list.
326 func (c *Client) UntrustPubkey(pubkey string) error {
327 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
328 defer cancel()
329
330 _, err := c.client.UntrustPubkey(ctx, &orlyaclv1.PubkeyRequest{
331 Pubkey: pubkey,
332 })
333 return err
334 }
335
336 // BlacklistPubkey adds a pubkey to the blacklist.
337 func (c *Client) BlacklistPubkey(pubkey, reason string) error {
338 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
339 defer cancel()
340
341 _, err := c.client.BlacklistPubkey(ctx, &orlyaclv1.BlacklistPubkeyRequest{
342 Pubkey: pubkey,
343 Reason: reason,
344 })
345 return err
346 }
347
348 // UnblacklistPubkey removes a pubkey from the blacklist.
349 func (c *Client) UnblacklistPubkey(pubkey string) error {
350 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
351 defer cancel()
352
353 _, err := c.client.UnblacklistPubkey(ctx, &orlyaclv1.PubkeyRequest{
354 Pubkey: pubkey,
355 })
356 return err
357 }
358
359 // RateLimitCheck checks if a pubkey/IP can publish.
360 func (c *Client) RateLimitCheck(pubkey, ip string) (allowed bool, message string, err error) {
361 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
362 defer cancel()
363
364 resp, err := c.client.RateLimitCheck(ctx, &orlyaclv1.RateLimitCheckRequest{
365 Pubkey: pubkey,
366 Ip: ip,
367 })
368 if err != nil {
369 return false, "", err
370 }
371 return resp.Allowed, resp.Message, nil
372 }
373
374 // IsCuratingConfigured checks if curating mode is configured.
375 func (c *Client) IsCuratingConfigured() (bool, error) {
376 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
377 defer cancel()
378
379 resp, err := c.client.IsCuratingConfigured(ctx, &orlyaclv1.Empty{})
380 if err != nil {
381 return false, err
382 }
383 return resp.Value, nil
384 }
385
386 // === Paid ACL Methods ===
387
388 // SubscriptionInfo holds subscription details returned by GetSubscription.
389 type SubscriptionInfo struct {
390 PubkeyHex string
391 Alias string
392 ExpiresAt time.Time
393 CreatedAt time.Time
394 HasAlias bool
395 }
396
397 // SubscribePubkey activates a subscription for a pubkey.
398 func (c *Client) SubscribePubkey(pubkey string, expiresAt time.Time, invoiceHash, alias string) error {
399 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
400 defer cancel()
401
402 _, err := c.client.SubscribePubkey(ctx, &orlyaclv1.SubscribeRequest{
403 Pubkey: pubkey,
404 ExpiresAt: expiresAt.Unix(),
405 InvoiceHash: invoiceHash,
406 Alias: alias,
407 })
408 return err
409 }
410
411 // UnsubscribePubkey removes a subscription.
412 func (c *Client) UnsubscribePubkey(pubkey string) error {
413 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
414 defer cancel()
415
416 _, err := c.client.UnsubscribePubkey(ctx, &orlyaclv1.PubkeyRequest{
417 Pubkey: pubkey,
418 })
419 return err
420 }
421
422 // IsSubscribedPaid checks if a pubkey has an active paid subscription.
423 func (c *Client) IsSubscribedPaid(pubkey string) (bool, error) {
424 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
425 defer cancel()
426
427 resp, err := c.client.IsSubscribed(ctx, &orlyaclv1.PubkeyRequest{
428 Pubkey: pubkey,
429 })
430 if err != nil {
431 return false, err
432 }
433 return resp.Value, nil
434 }
435
436 // GetSubscription returns subscription details for a pubkey.
437 func (c *Client) GetSubscription(pubkey string) (*SubscriptionInfo, error) {
438 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
439 defer cancel()
440
441 resp, err := c.client.GetSubscription(ctx, &orlyaclv1.PubkeyRequest{
442 Pubkey: pubkey,
443 })
444 if err != nil {
445 return nil, err
446 }
447 return &SubscriptionInfo{
448 PubkeyHex: resp.Pubkey,
449 Alias: resp.Alias,
450 ExpiresAt: time.Unix(resp.ExpiresAt, 0),
451 CreatedAt: time.Unix(resp.CreatedAt, 0),
452 HasAlias: resp.HasAlias,
453 }, nil
454 }
455
456 // ClaimAlias claims an email alias for a pubkey.
457 func (c *Client) ClaimAlias(alias, pubkey string) error {
458 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
459 defer cancel()
460
461 _, err := c.client.ClaimAlias(ctx, &orlyaclv1.ClaimAliasRequest{
462 Alias: alias,
463 Pubkey: pubkey,
464 })
465 return err
466 }
467
468 // GetAliasByPubkey returns the alias for a pubkey, or "" if none.
469 func (c *Client) GetAliasByPubkey(pubkey string) (string, error) {
470 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
471 defer cancel()
472
473 resp, err := c.client.GetAliasByPubkey(ctx, &orlyaclv1.PubkeyRequest{
474 Pubkey: pubkey,
475 })
476 if err != nil {
477 return "", err
478 }
479 return resp.Alias, nil
480 }
481
482 // GetPubkeyByAlias returns the pubkey for an alias, or "" if not found.
483 func (c *Client) GetPubkeyByAlias(alias string) (string, error) {
484 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
485 defer cancel()
486
487 resp, err := c.client.GetPubkeyByAlias(ctx, &orlyaclv1.AliasRequest{
488 Alias: alias,
489 })
490 if err != nil {
491 return "", err
492 }
493 return resp.Pubkey, nil
494 }
495
496 // IsAliasTaken checks if an alias is already claimed.
497 func (c *Client) IsAliasTaken(alias string) (bool, error) {
498 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
499 defer cancel()
500
501 resp, err := c.client.IsAliasTaken(ctx, &orlyaclv1.AliasRequest{
502 Alias: alias,
503 })
504 if err != nil {
505 return false, err
506 }
507 return resp.Value, nil
508 }
509