-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add endpoint for registering alby go notifications #128
base: main
Are you sure you want to change the base?
Changes from 4 commits
8fc78f1
8b18359
a275855
b99700a
572b9d8
c2c9ca3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
package nostr | ||
|
||
import ( | ||
"net/http" | ||
"time" | ||
|
||
"github.com/labstack/echo/v4" | ||
"github.com/nbd-wtf/go-nostr" | ||
expo "github.com/oliveroneill/exponent-server-sdk-golang/sdk" | ||
"github.com/sirupsen/logrus" | ||
) | ||
|
||
func (svc *Service) NIP47ExpoNotificationHandler(c echo.Context) error { | ||
var requestData NIP47ExpoNotificationRequest | ||
if err := c.Bind(&requestData); err != nil { | ||
return c.JSON(http.StatusBadRequest, ErrorResponse{ | ||
Message: "Error decoding notification request", | ||
Error: err.Error(), | ||
}) | ||
} | ||
|
||
if (requestData.PushToken == "") { | ||
return c.JSON(http.StatusBadRequest, ErrorResponse{ | ||
Message: "push token is empty", | ||
Error: "no push token in request data", | ||
}) | ||
} | ||
|
||
_, err := expo.NewExponentPushToken(requestData.PushToken) | ||
if err != nil { | ||
return c.JSON(http.StatusBadRequest, ErrorResponse{ | ||
Message: "invalid push token", | ||
Error: "invalid push token in request data", | ||
}) | ||
} | ||
|
||
if (requestData.WalletPubkey == "") { | ||
return c.JSON(http.StatusBadRequest, ErrorResponse{ | ||
Message: "wallet pubkey is empty", | ||
Error: "no wallet pubkey in request data", | ||
}) | ||
} | ||
|
||
if (requestData.ConnPubkey == "") { | ||
return c.JSON(http.StatusBadRequest, ErrorResponse{ | ||
Message: "connection pubkey is empty", | ||
Error: "no connection pubkey in request data", | ||
}) | ||
} | ||
|
||
var existingSubscriptions []Subscription | ||
if err := svc.db.Where("push_token = ? AND open = ?", requestData.PushToken, true).Find(&existingSubscriptions).Error; err != nil { | ||
svc.Logger.WithError(err).WithFields(logrus.Fields{ | ||
"push_token": requestData.PushToken, | ||
}).Error("Failed to check existing subscriptions") | ||
return c.JSON(http.StatusInternalServerError, ErrorResponse{ | ||
Message: "internal server error", | ||
Error: err.Error(), | ||
}) | ||
} | ||
|
||
for _, existingSubscription := range existingSubscriptions { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could the DB query above be improved instead of having to do a for loop after (more filtering?) |
||
existingWalletPubkey := (*existingSubscription.Authors)[0] | ||
existingConnPubkey := (*existingSubscription.Tags)["p"][0] | ||
|
||
if existingWalletPubkey == requestData.WalletPubkey && existingConnPubkey == requestData.ConnPubkey { | ||
svc.Logger.WithFields(logrus.Fields{ | ||
"wallet_pubkey": requestData.WalletPubkey, | ||
"relay_url": requestData.RelayUrl, | ||
"push_token": requestData.PushToken, | ||
}).Debug("Subscription already started") | ||
return c.JSON(http.StatusOK, ExpoSubscriptionResponse{ | ||
SubscriptionId: existingSubscription.Uuid, | ||
PushToken: requestData.PushToken, | ||
WalletPubkey: requestData.WalletPubkey, | ||
AppPubkey: requestData.ConnPubkey, | ||
}) | ||
} | ||
} | ||
|
||
svc.Logger.WithFields(logrus.Fields{ | ||
"wallet_pubkey": requestData.WalletPubkey, | ||
"relay_url": requestData.RelayUrl, | ||
"push_token": requestData.PushToken, | ||
}).Debug("Subscribing to send push notifications") | ||
|
||
subscription := Subscription{ | ||
RelayUrl: requestData.RelayUrl, | ||
PushToken: requestData.PushToken, | ||
Open: true, | ||
Since: time.Now(), | ||
Authors: &[]string{requestData.WalletPubkey}, | ||
Kinds: &[]int{NIP_47_NOTIFICATION_KIND}, | ||
} | ||
|
||
tags := make(nostr.TagMap) | ||
(tags)["p"] = []string{requestData.ConnPubkey} | ||
subscription.Tags = &tags | ||
|
||
err = svc.db.Create(&subscription).Error | ||
if err != nil { | ||
svc.Logger.WithError(err).WithFields(logrus.Fields{ | ||
"wallet_pubkey": requestData.WalletPubkey, | ||
"relay_url": requestData.RelayUrl, | ||
"push_token": requestData.PushToken, | ||
}).Error("Failed to store subscription") | ||
return c.JSON(http.StatusBadRequest, ErrorResponse{ | ||
Message: "Failed to store subscription", | ||
Error: err.Error(), | ||
}) | ||
} | ||
|
||
go svc.startSubscription(svc.Ctx, &subscription, nil, svc.handleSubscribedExpoNotification) | ||
|
||
return c.JSON(http.StatusOK, ExpoSubscriptionResponse{ | ||
SubscriptionId: subscription.Uuid, | ||
PushToken: requestData.PushToken, | ||
WalletPubkey: requestData.WalletPubkey, | ||
AppPubkey: requestData.ConnPubkey, | ||
}) | ||
} | ||
|
||
func (svc *Service) handleSubscribedExpoNotification(event *nostr.Event, subscription *Subscription) { | ||
svc.Logger.WithFields(logrus.Fields{ | ||
"event_id": event.ID, | ||
"event_kind": event.Kind, | ||
"subscription_id": subscription.ID, | ||
"relay_url": subscription.RelayUrl, | ||
}).Debug("Received subscribed notification") | ||
|
||
pushToken, _ := expo.NewExponentPushToken(subscription.PushToken) | ||
|
||
response, err := svc.client.Publish( | ||
&expo.PushMessage{ | ||
To: []expo.ExponentPushToken{pushToken}, | ||
Title: "New event", | ||
Body: "", | ||
Data: map[string]string{ | ||
"content": event.Content, | ||
"appPubkey": event.Tags.GetFirst([]string{"p", ""}).Value(), | ||
}, | ||
Priority: expo.DefaultPriority, | ||
}, | ||
) | ||
if err != nil { | ||
svc.Logger.WithError(err).WithFields(logrus.Fields{ | ||
"push_token": subscription.PushToken, | ||
}).Error("Failed to send expo notification") | ||
return | ||
} | ||
|
||
err = response.ValidateResponse() | ||
if err != nil { | ||
svc.Logger.WithError(err).WithFields(logrus.Fields{ | ||
"push_token": subscription.PushToken, | ||
}).Error("Failed to valid expo publish response") | ||
return | ||
} | ||
|
||
svc.Logger.WithFields(logrus.Fields{ | ||
"event_id": event.ID, | ||
"push_token": subscription.PushToken, | ||
}).Debug("Push notification sent successfully") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,7 @@ import ( | |
"gorm.io/gorm" | ||
|
||
"github.com/jackc/pgx/v5/stdlib" | ||
expo "github.com/oliveroneill/exponent-server-sdk-golang/sdk" | ||
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql" | ||
gormtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/gorm.io/gorm.v1" | ||
) | ||
|
@@ -49,6 +50,7 @@ type Service struct { | |
subscriptions map[string]*nostr.Subscription | ||
subscriptionsMutex sync.Mutex | ||
relayMutex sync.Mutex | ||
client *expo.PushClient | ||
} | ||
|
||
func NewService(ctx context.Context) (*Service, error) { | ||
|
@@ -116,6 +118,12 @@ func NewService(ctx context.Context) (*Service, error) { | |
|
||
subscriptions := make(map[string]*nostr.Subscription) | ||
|
||
// TODO: Check limits | ||
client := expo.NewPushClient(&expo.ClientConfig{ | ||
Host: "https://api.expo.dev", | ||
APIURL: "/v2", | ||
}) | ||
|
||
im-adithya marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var wg sync.WaitGroup | ||
svc := &Service{ | ||
Cfg: cfg, | ||
|
@@ -125,6 +133,7 @@ func NewService(ctx context.Context) (*Service, error) { | |
Logger: logger, | ||
Relay: relay, | ||
subscriptions: subscriptions, | ||
client: client, | ||
} | ||
|
||
logger.Info("Starting all open subscriptions...") | ||
|
@@ -139,7 +148,11 @@ func NewService(ctx context.Context) (*Service, error) { | |
// Create a copy of the loop variable to | ||
// avoid passing address of the same variable | ||
subscription := sub | ||
go svc.startSubscription(svc.Ctx, &subscription, nil, svc.handleSubscribedEvent) | ||
handleEvent := svc.handleSubscribedEvent | ||
if sub.PushToken != "" { | ||
handleEvent = svc.handleSubscribedExpoNotification | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can the naming be improved here? we are still handling a subscribed event, right - we're just notifying in a different way? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed to be simple - There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's still inconsistent with |
||
} | ||
go svc.startSubscription(svc.Ctx, &subscription, nil, handleEvent) | ||
} | ||
|
||
return svc, nil | ||
|
@@ -928,6 +941,7 @@ func (svc *Service) postEventToWebhook(event *nostr.Event, webhookURL string) { | |
"event_kind": event.Kind, | ||
"webhook_url": webhookURL, | ||
}).Error("Failed to post event to webhook") | ||
return | ||
} | ||
|
||
svc.Logger.WithFields(logrus.Fields{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,4 +28,4 @@ var _202404031539_add_indexes = &gormigrate.Migration{ | |
} | ||
return nil | ||
}, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package migrations | ||
|
||
import ( | ||
"github.com/go-gormigrate/gormigrate/v2" | ||
"gorm.io/gorm" | ||
) | ||
|
||
// Add push_token column to subscriptions table | ||
var _202411071013_add_push_token_to_subscriptions = &gormigrate.Migration{ | ||
ID: "202411071013_add_push_token_to_subscriptions", | ||
Migrate: func(tx *gorm.DB) error { | ||
if err := tx.Exec("ALTER TABLE subscriptions ADD COLUMN push_token TEXT").Error; err != nil { | ||
return err | ||
} | ||
if err := tx.Exec("CREATE INDEX IF NOT EXISTS subscriptions_push_token ON subscriptions (push_token)").Error; err != nil { | ||
return err | ||
} | ||
return nil | ||
}, | ||
Rollback: func(tx *gorm.DB) error { | ||
if err := tx.Exec("DROP INDEX IF EXISTS subscriptions_push_token").Error; err != nil { | ||
return err | ||
} | ||
if err := tx.Exec("ALTER TABLE subscriptions DROP COLUMN push_token").Error; err != nil { | ||
return err | ||
} | ||
return nil | ||
}, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't use app specific names here.
You implement an universal API here that can also be used by other applications.
(e.g.
/nip47/notifications/expo
)Also try to be consistent.
maybe we move the webhook endpoin then to:
/nip47/notifications/webhook
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done 👍
Regarding webhook, it's a one-time notification endpoint, and it would also be a breaking change. Should we do that?