nip47_notifier.go raw

   1  package notifications
   2  
   3  import (
   4  	"context"
   5  	"encoding/json"
   6  	"errors"
   7  
   8  	"github.com/getAlby/go-nostr"
   9  	"github.com/getAlby/hub/config"
  10  	"github.com/getAlby/hub/constants"
  11  	"github.com/getAlby/hub/db"
  12  	"github.com/getAlby/hub/events"
  13  	"github.com/getAlby/hub/logger"
  14  	"github.com/getAlby/hub/nip47/cipher"
  15  	"github.com/getAlby/hub/nip47/models"
  16  	"github.com/getAlby/hub/nip47/permissions"
  17  	nostrmodels "github.com/getAlby/hub/nostr/models"
  18  	"github.com/getAlby/hub/service/keys"
  19  	"github.com/sirupsen/logrus"
  20  	"gorm.io/gorm"
  21  )
  22  
  23  type Nip47Notifier struct {
  24  	pool           nostrmodels.SimplePool
  25  	cfg            config.Config
  26  	keys           keys.Keys
  27  	db             *gorm.DB
  28  	permissionsSvc permissions.PermissionsService
  29  }
  30  
  31  func NewNip47Notifier(pool nostrmodels.SimplePool, db *gorm.DB, cfg config.Config, keys keys.Keys, permissionsSvc permissions.PermissionsService) *Nip47Notifier {
  32  	return &Nip47Notifier{
  33  		pool:           pool,
  34  		cfg:            cfg,
  35  		db:             db,
  36  		permissionsSvc: permissionsSvc,
  37  		keys:           keys,
  38  	}
  39  }
  40  
  41  func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.Event) error {
  42  	switch event.Event {
  43  	case "nwc_payment_received":
  44  		transaction, ok := event.Properties.(*db.Transaction)
  45  		if !ok {
  46  			logger.Logger.WithField("event", event).Error("Failed to cast event")
  47  			return errors.New("failed to cast event")
  48  		}
  49  
  50  		notification := PaymentReceivedNotification{
  51  			Transaction: *models.ToNip47Transaction(transaction),
  52  		}
  53  
  54  		notifier.notifySubscribers(ctx, &Notification{
  55  			Notification:     notification,
  56  			NotificationType: PAYMENT_RECEIVED_NOTIFICATION,
  57  		}, nostr.Tags{}, transaction.AppId)
  58  
  59  	case "nwc_payment_sent":
  60  		transaction, ok := event.Properties.(*db.Transaction)
  61  		if !ok {
  62  			logger.Logger.WithField("event", event).Error("Failed to cast event")
  63  			return errors.New("failed to cast event")
  64  		}
  65  
  66  		notification := PaymentSentNotification{
  67  			Transaction: *models.ToNip47Transaction(transaction),
  68  		}
  69  
  70  		notifier.notifySubscribers(ctx, &Notification{
  71  			Notification:     notification,
  72  			NotificationType: PAYMENT_SENT_NOTIFICATION,
  73  		}, nostr.Tags{}, transaction.AppId)
  74  
  75  	case "nwc_hold_invoice_accepted":
  76  		dbTransaction, ok := event.Properties.(*db.Transaction)
  77  		if !ok {
  78  			logger.Logger.WithField("event", event).Error("Failed to cast event properties to db.Transaction for hold invoice accepted")
  79  			return errors.New("failed to cast event")
  80  		}
  81  
  82  		nip47Transaction := models.ToNip47Transaction(dbTransaction)
  83  
  84  		notification := HoldInvoiceAcceptedNotification{
  85  			Transaction: *nip47Transaction,
  86  		}
  87  
  88  		notifier.notifySubscribers(ctx, &Notification{
  89  			Notification:     notification,
  90  			NotificationType: HOLD_INVOICE_ACCEPTED_NOTIFICATION,
  91  		}, nostr.Tags{}, dbTransaction.AppId)
  92  	}
  93  	return nil
  94  }
  95  
  96  func (notifier *Nip47Notifier) notifySubscribers(ctx context.Context, notification *Notification, tags nostr.Tags, appId *uint) error {
  97  	apps := []db.App{}
  98  
  99  	// TODO: join apps and permissions
 100  	err := notifier.db.Find(&apps).Error
 101  	if err != nil {
 102  		logger.Logger.WithError(err).Error("Failed to list apps")
 103  		return errors.New("failed to list apps")
 104  	}
 105  
 106  	for _, app := range apps {
 107  		if app.Isolated && (appId == nil || app.ID != *appId) {
 108  			continue
 109  		}
 110  
 111  		hasPermission, _, _ := notifier.permissionsSvc.HasPermission(&app, constants.NOTIFICATIONS_SCOPE)
 112  		if !hasPermission {
 113  			continue
 114  		}
 115  
 116  		appWalletPrivKey := notifier.keys.GetNostrSecretKey()
 117  		if app.WalletPubkey != nil {
 118  			appWalletPrivKey, err = notifier.keys.GetAppWalletKey(app.ID)
 119  			if err != nil {
 120  				logger.Logger.WithFields(logrus.Fields{
 121  					"notification": notification,
 122  					"appId":        app.ID,
 123  				}).WithError(err).Error("error deriving child key")
 124  				return errors.New("failed to derive child key")
 125  			}
 126  		}
 127  
 128  		appWalletPubKey, err := nostr.GetPublicKey(appWalletPrivKey)
 129  		if err != nil {
 130  			logger.Logger.WithFields(logrus.Fields{
 131  				"notification": notification,
 132  				"appId":        app.ID,
 133  			}).WithError(err).Error("Failed to calculate app wallet pub key")
 134  			return errors.New("failed to calculate app wallet pubkey")
 135  		}
 136  
 137  		err = notifier.notifySubscriber(ctx, &app, notification, tags, appWalletPubKey, appWalletPrivKey, constants.ENCRYPTION_TYPE_NIP04)
 138  		if err != nil {
 139  			logger.Logger.WithError(err).Error("failed to notify subscriber (NIP-04)")
 140  			return err
 141  		}
 142  		err = notifier.notifySubscriber(ctx, &app, notification, tags, appWalletPubKey, appWalletPrivKey, constants.ENCRYPTION_TYPE_NIP44_V2)
 143  		if err != nil {
 144  			logger.Logger.WithError(err).Error("failed to notify subscriber (NIP-44)")
 145  			return err
 146  		}
 147  	}
 148  	return nil
 149  }
 150  
 151  func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App, notification *Notification, tags nostr.Tags, appWalletPubKey, appWalletPrivKey string, encryption string) error {
 152  	logger.Logger.WithFields(logrus.Fields{
 153  		"notification": notification,
 154  		"appId":        app.ID,
 155  		"encryption":   encryption,
 156  	}).Debug("Notifying subscriber")
 157  
 158  	var err error
 159  
 160  	payloadBytes, err := json.Marshal(notification)
 161  	if err != nil {
 162  		logger.Logger.WithFields(logrus.Fields{
 163  			"notification": notification,
 164  			"appId":        app.ID,
 165  			"encryption":   encryption,
 166  		}).WithError(err).Error("Failed to stringify notification")
 167  		return err
 168  	}
 169  
 170  	nip47Cipher, err := cipher.NewNip47Cipher(encryption, app.AppPubkey, appWalletPrivKey)
 171  	if err != nil {
 172  		logger.Logger.WithFields(logrus.Fields{
 173  			"notification": notification,
 174  			"appId":        app.ID,
 175  			"encryption":   encryption,
 176  		}).WithError(err).Error("Failed to initialize cipher")
 177  		return err
 178  	}
 179  
 180  	msg, err := nip47Cipher.Encrypt(string(payloadBytes))
 181  	if err != nil {
 182  		logger.Logger.WithFields(logrus.Fields{
 183  			"notification": notification,
 184  			"appId":        app.ID,
 185  			"encryption":   encryption,
 186  		}).WithError(err).Error("Failed to encrypt notification payload")
 187  		return err
 188  	}
 189  
 190  	allTags := nostr.Tags{[]string{"p", app.AppPubkey}}
 191  	allTags = append(allTags, tags...)
 192  
 193  	event := &nostr.Event{
 194  		PubKey:    appWalletPubKey,
 195  		CreatedAt: nostr.Now(),
 196  		Kind:      models.NOTIFICATION_KIND,
 197  		Tags:      allTags,
 198  		Content:   msg,
 199  	}
 200  
 201  	if encryption == constants.ENCRYPTION_TYPE_NIP04 {
 202  		event.Kind = models.LEGACY_NOTIFICATION_KIND
 203  	}
 204  
 205  	err = event.Sign(appWalletPrivKey)
 206  	if err != nil {
 207  		logger.Logger.WithFields(logrus.Fields{
 208  			"notification": notification,
 209  			"appId":        app.ID,
 210  			"encryption":   encryption,
 211  		}).WithError(err).Error("Failed to sign event")
 212  		return err
 213  	}
 214  
 215  	publishResultChannel := notifier.pool.PublishMany(ctx, notifier.cfg.GetRelayUrls(), *event)
 216  
 217  	publishSuccessful := false
 218  	for result := range publishResultChannel {
 219  		if result.Error == nil {
 220  			publishSuccessful = true
 221  		} else {
 222  			logger.Logger.WithFields(logrus.Fields{
 223  				"notification": notification,
 224  				"appId":        app.ID,
 225  				"relay":        result.RelayURL,
 226  			}).WithError(result.Error).Error("failed to publish notification to relay")
 227  		}
 228  	}
 229  
 230  	if !publishSuccessful {
 231  		logger.Logger.WithFields(logrus.Fields{
 232  			"notification": notification,
 233  			"appId":        app.ID,
 234  			"encryption":   encryption,
 235  		}).WithError(err).Error("Failed to publish notification")
 236  		return err
 237  	}
 238  	logger.Logger.WithFields(logrus.Fields{
 239  		"appId":      app.ID,
 240  		"encryption": encryption,
 241  	}).Debug("Published notification event")
 242  	return nil
 243  }
 244