publish_nip47_info.go raw

   1  package nip47
   2  
   3  import (
   4  	"context"
   5  	"errors"
   6  	"strconv"
   7  	"strings"
   8  
   9  	"github.com/getAlby/go-nostr"
  10  	"github.com/getAlby/hub/db"
  11  	"github.com/getAlby/hub/lnclient"
  12  	"github.com/getAlby/hub/logger"
  13  	"github.com/getAlby/hub/nip47/cipher"
  14  	"github.com/getAlby/hub/nip47/models"
  15  	nostrmodels "github.com/getAlby/hub/nostr/models"
  16  	"github.com/sirupsen/logrus"
  17  )
  18  
  19  type Nip47InfoPublishRequest struct {
  20  	AppId            uint
  21  	AppWalletPubKey  string
  22  	AppWalletPrivKey string
  23  	RelayUrl         string
  24  	Attempt          uint32
  25  }
  26  
  27  type nip47InfoPublishQueue struct {
  28  	channel chan *Nip47InfoPublishRequest
  29  }
  30  
  31  func NewNip47InfoPublishQueue() *nip47InfoPublishQueue {
  32  	return &nip47InfoPublishQueue{
  33  		channel: make(chan *Nip47InfoPublishRequest),
  34  	}
  35  }
  36  
  37  func (q *nip47InfoPublishQueue) AddToQueue(req *Nip47InfoPublishRequest) {
  38  	// thread will be blocked if the channel is full, so execute in a separate goroutine
  39  	go func() {
  40  		q.channel <- req
  41  	}()
  42  }
  43  
  44  func (q *nip47InfoPublishQueue) Channel() <-chan *Nip47InfoPublishRequest {
  45  	return q.channel
  46  }
  47  
  48  func (svc *nip47Service) GetNip47Info(ctx context.Context, pool nostrmodels.SimplePool, appWalletPubKey string) (*nostr.Event, error) {
  49  	filter := nostr.Filter{
  50  		Kinds:   []int{models.INFO_EVENT_KIND},
  51  		Authors: []string{appWalletPubKey},
  52  		Limit:   1,
  53  	}
  54  
  55  	relayEvent := pool.QuerySingle(ctx, svc.cfg.GetRelayUrls(), filter)
  56  	if relayEvent == nil {
  57  		return nil, nil
  58  	}
  59  
  60  	return relayEvent.Event, nil
  61  }
  62  
  63  func (svc *nip47Service) PublishNip47Info(ctx context.Context, pool nostrmodels.SimplePool, appId uint, appWalletPubKey string, appWalletPrivKey string, relayUrl string, lnClient lnclient.LNClient) (*nostr.Event, error) {
  64  	var capabilities []string
  65  	var permitsNotifications bool
  66  	tags := nostr.Tags{[]string{"encryption", cipher.SUPPORTED_ENCRYPTIONS}}
  67  
  68  	if svc.keys.GetNostrPublicKey() == appWalletPubKey {
  69  		// legacy app, so return lnClient.GetSupportedNIP47Methods()
  70  		capabilities = lnClient.GetSupportedNIP47Methods()
  71  		permitsNotifications = true
  72  	} else {
  73  		app := db.App{}
  74  		err := svc.db.First(&app, appId).Error
  75  		if err != nil {
  76  			logger.Logger.WithFields(logrus.Fields{
  77  				"walletPubKey": appWalletPubKey,
  78  			}).WithError(err).Error("Failed to find app for wallet pubkey")
  79  			return nil, err
  80  		}
  81  		capabilities = svc.permissionsService.GetPermittedMethods(&app, lnClient)
  82  		permitsNotifications = svc.permissionsService.PermitsNotifications(&app)
  83  
  84  		// NWA: associate the info event with the app so that the app can receive the wallet pubkey
  85  		tags = append(tags, []string{"p", app.AppPubkey})
  86  	}
  87  	if permitsNotifications && len(lnClient.GetSupportedNIP47NotificationTypes()) > 0 {
  88  		capabilities = append(capabilities, "notifications")
  89  		tags = append(tags, []string{"notifications", strings.Join(lnClient.GetSupportedNIP47NotificationTypes(), " ")})
  90  	}
  91  
  92  	ev := &nostr.Event{}
  93  	ev.Kind = models.INFO_EVENT_KIND
  94  	ev.Content = strings.Join(capabilities, " ")
  95  	ev.CreatedAt = nostr.Now()
  96  	ev.PubKey = appWalletPubKey
  97  	ev.Tags = tags
  98  	err := ev.Sign(appWalletPrivKey)
  99  	if err != nil {
 100  		return nil, err
 101  	}
 102  
 103  	// publish to a single relay so that we can requeue failed publishes on a relay level
 104  	publishResultChannel := pool.PublishMany(ctx, []string{relayUrl}, *ev)
 105  
 106  	publishSuccessful := false
 107  	for result := range publishResultChannel {
 108  		if result.Error == nil {
 109  			publishSuccessful = true
 110  		} else {
 111  			logger.Logger.WithFields(logrus.Fields{
 112  				"appId": appId,
 113  				"relay": result.RelayURL,
 114  			}).WithError(result.Error).Error("failed to publish nip47 info to relay")
 115  		}
 116  	}
 117  	if !publishSuccessful {
 118  		return nil, errors.New("failed to publish nostr info event to all relays")
 119  	}
 120  	logger.Logger.WithField("wallet_pubkey", appWalletPubKey).Debug("published info event")
 121  	return ev, nil
 122  }
 123  
 124  func (svc *nip47Service) PublishNip47InfoDeletion(ctx context.Context, pool nostrmodels.SimplePool, appWalletPubKey string, appWalletPrivKey string, infoEventId string) error {
 125  	ev := &nostr.Event{}
 126  	ev.Kind = nostr.KindDeletion
 127  	ev.Content = "deleting nip47 info since app connection for this key was deleted"
 128  	ev.Tags = nostr.Tags{[]string{"e", infoEventId}, []string{"k", strconv.Itoa(models.INFO_EVENT_KIND)}}
 129  	ev.CreatedAt = nostr.Now()
 130  	ev.PubKey = appWalletPubKey
 131  	err := ev.Sign(appWalletPrivKey)
 132  	if err != nil {
 133  		return err
 134  	}
 135  	publishResultChannel := pool.PublishMany(ctx, svc.cfg.GetRelayUrls(), *ev)
 136  
 137  	publishSuccessful := false
 138  	for result := range publishResultChannel {
 139  		if result.Error == nil {
 140  			publishSuccessful = true
 141  		} else {
 142  			logger.Logger.WithFields(logrus.Fields{
 143  				"wallet_pubkey": appWalletPubKey,
 144  				"relay":         result.RelayURL,
 145  			}).WithError(result.Error).Error("failed to publish info event deletion to relay")
 146  		}
 147  	}
 148  
 149  	if !publishSuccessful {
 150  		return errors.New("failed to publish info event deletion to all relays")
 151  	}
 152  	return nil
 153  }
 154