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