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