Skip to content

Commit

Permalink
Merge pull request #128 from getAlby/task-expo-notifs
Browse files Browse the repository at this point in the history
feat: add endpoint for registering alby go notifications
  • Loading branch information
im-adithya authored Dec 19, 2024
2 parents 12d12b1 + d71df15 commit c973253
Show file tree
Hide file tree
Showing 11 changed files with 413 additions and 53 deletions.
1 change: 1 addition & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func main() {
e.POST("/nip47", svc.NIP47Handler)
e.POST("/nip47/webhook", svc.NIP47WebhookHandler)
e.POST("/nip47/notifications", svc.NIP47NotificationHandler)
e.POST("/nip47/notifications/push", svc.NIP47PushNotificationHandler)
e.POST("/publish", svc.PublishHandler)
e.POST("/subscriptions", svc.SubscriptionHandler)
e.DELETE("/subscriptions/:id", svc.StopSubscriptionHandler)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module http-nostr
go 1.21.3

require (
github.com/getAlby/exponent-server-sdk-golang/sdk v0.0.0-20241113053439-fb024e3a89b1
github.com/getsentry/sentry-go v0.28.1
github.com/jackc/pgx/v5 v5.6.0
github.com/joho/godotenv v1.5.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4/go.mod h1:I5sHm0Y
github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/getAlby/exponent-server-sdk-golang/sdk v0.0.0-20241113053439-fb024e3a89b1 h1:u1rDPykjuG3DkUAeHlGby4frrynzjJAfZbuK/jLlu6k=
github.com/getAlby/exponent-server-sdk-golang/sdk v0.0.0-20241113053439-fb024e3a89b1/go.mod h1:EK6N2J42WZk795IUD9GGbKL8XAK5UjfUEvxh4d9hobY=
github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k=
github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
Expand Down
98 changes: 49 additions & 49 deletions internal/nostr/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type Subscription struct {
ID uint
RelayUrl string
WebhookUrl string
PushToken string
IsIOS bool
Open bool
Ids *[]string `gorm:"-"`
Kinds *[]int `gorm:"-"`
Expand All @@ -43,82 +45,65 @@ type Subscription struct {
EventChan chan *nostr.Event `gorm:"-"`
RequestEvent *RequestEvent `gorm:"-"`

// TODO: fix an elegant solution to store datatypes
IdsString string
KindsString string
AuthorsString string
TagsString string
IdsJson json.RawMessage `gorm:"type:jsonb"`
KindsJson json.RawMessage `gorm:"type:jsonb"`
AuthorsJson json.RawMessage `gorm:"type:jsonb"`
TagsJson json.RawMessage `gorm:"type:jsonb"`
}

func (s *Subscription) BeforeSave(tx *gorm.DB) error {
var err error
if s.Ids != nil {
var idsJson []byte
idsJson, err = json.Marshal(s.Ids)
if err != nil {
return err
}
s.IdsString = string(idsJson)
if s.IdsJson, err = json.Marshal(s.Ids); err != nil {
return err
}
}

if s.Kinds != nil {
var kindsJson []byte
kindsJson, err = json.Marshal(s.Kinds)
if err != nil {
return err
}
s.KindsString = string(kindsJson)
if s.KindsJson, err = json.Marshal(s.Kinds); err != nil {
return err
}
}

if s.Authors != nil {
var authorsJson []byte
authorsJson, err = json.Marshal(s.Authors)
if err != nil {
return err
}
s.AuthorsString = string(authorsJson)
if s.AuthorsJson, err = json.Marshal(s.Authors); err != nil {
return err
}
}

if s.Tags != nil {
var tagsJson []byte
tagsJson, err = json.Marshal(s.Tags)
if err != nil {
return err
}
s.TagsString = string(tagsJson)
if s.TagsJson, err = json.Marshal(s.Tags); err != nil {
return err
}
}

return nil
}

func (s *Subscription) AfterFind(tx *gorm.DB) error {
var err error
if s.IdsString != "" {
err = json.Unmarshal([]byte(s.IdsString), &s.Ids)
if err != nil {
return err
}
if len(s.IdsJson) > 0 {
if err = json.Unmarshal(s.IdsJson, &s.Ids); err != nil {
return err
}
}

if s.KindsString != "" {
err = json.Unmarshal([]byte(s.KindsString), &s.Kinds)
if err != nil {
return err
}
if len(s.KindsJson) > 0 {
if err = json.Unmarshal(s.KindsJson, &s.Kinds); err != nil {
return err
}
}

if s.AuthorsString != "" {
err = json.Unmarshal([]byte(s.AuthorsString), &s.Authors)
if err != nil {
return err
}
if len(s.AuthorsJson) > 0 {
if err = json.Unmarshal(s.AuthorsJson, &s.Authors); err != nil {
return err
}
}

if s.TagsString != "" {
err = json.Unmarshal([]byte(s.TagsString), &s.Tags)
if err != nil {
return err
}
if len(s.TagsJson) > 0 {
if err = json.Unmarshal(s.TagsJson, &s.Tags); err != nil {
return err
}
}

return nil
Expand Down Expand Up @@ -185,6 +170,14 @@ type NIP47NotificationRequest struct {
ConnPubkey string `json:"connectionPubkey"`
}

type NIP47PushNotificationRequest struct {
RelayUrl string `json:"relayUrl"`
PushToken string `json:"pushToken"`
WalletPubkey string `json:"walletPubkey"`
ConnPubkey string `json:"connectionPubkey"`
IsIOS bool `json:"isIOS"`
}

type NIP47Response struct {
Event *nostr.Event `json:"event,omitempty"`
State string `json:"state"`
Expand Down Expand Up @@ -212,6 +205,13 @@ type SubscriptionResponse struct {
WebhookUrl string `json:"webhookUrl"`
}

type PushSubscriptionResponse struct {
SubscriptionId string `json:"subscriptionId"`
PushToken string `json:"pushToken"`
WalletPubkey string `json:"walletPubkey"`
AppPubkey string `json:"appPubkey"`
}

type StopSubscriptionResponse struct {
Message string `json:"message"`
State string `json:"state"`
Expand Down
15 changes: 14 additions & 1 deletion internal/nostr/nostr.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"gorm.io/driver/postgres"
"gorm.io/gorm"

expo "github.com/getAlby/exponent-server-sdk-golang/sdk"
"github.com/jackc/pgx/v5/stdlib"
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
gormtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/gorm.io/gorm.v1"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -116,6 +118,11 @@ func NewService(ctx context.Context) (*Service, error) {

subscriptions := make(map[string]*nostr.Subscription)

client := expo.NewPushClient(&expo.ClientConfig{
Host: "https://api.expo.dev",
APIURL: "/v2",
})

var wg sync.WaitGroup
svc := &Service{
Cfg: cfg,
Expand All @@ -125,6 +132,7 @@ func NewService(ctx context.Context) (*Service, error) {
Logger: logger,
Relay: relay,
subscriptions: subscriptions,
client: client,
}

logger.Info("Starting all open subscriptions...")
Expand All @@ -139,7 +147,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.handleSubscribedEventForPushNotification
}
go svc.startSubscription(svc.Ctx, &subscription, nil, handleEvent)
}

return svc, nil
Expand Down Expand Up @@ -928,6 +940,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{
Expand Down
163 changes: 163 additions & 0 deletions internal/nostr/push.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package nostr

import (
"net/http"
"time"

expo "github.com/getAlby/exponent-server-sdk-golang/sdk"
"github.com/labstack/echo/v4"
"github.com/nbd-wtf/go-nostr"
"github.com/sirupsen/logrus"
)

func (svc *Service) NIP47PushNotificationHandler(c echo.Context) error {
var requestData NIP47PushNotificationRequest
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 = ? AND authors_json->>0 = ? AND tags_json->'p'->>0 = ?", requestData.PushToken, true, requestData.WalletPubkey, requestData.ConnPubkey).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(),
})
}

if len(existingSubscriptions) > 0 {
existingSubscription := existingSubscriptions[0]
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, PushSubscriptionResponse{
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,
IsIOS: requestData.IsIOS,
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.handleSubscribedEventForPushNotification)

return c.JSON(http.StatusOK, PushSubscriptionResponse{
SubscriptionId: subscription.Uuid,
PushToken: requestData.PushToken,
WalletPubkey: requestData.WalletPubkey,
AppPubkey: requestData.ConnPubkey,
})
}

func (svc *Service) handleSubscribedEventForPushNotification(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 push notification")

pushToken, _ := expo.NewExponentPushToken(subscription.PushToken)

pushMessage := &expo.PushMessage{
To: []expo.ExponentPushToken{pushToken},
Data: map[string]string{
"content": event.Content,
"appPubkey": event.Tags.GetFirst([]string{"p", ""}).Value(),
},
}

if subscription.IsIOS {
pushMessage.Title = "Received notification"
pushMessage.MutableContent = true
}

response, err := svc.client.Publish(pushMessage)
if err != nil {
svc.Logger.WithError(err).WithFields(logrus.Fields{
"push_token": subscription.PushToken,
}).Error("Failed to send push notification")
return
}

err = response.ValidateResponse()
if err != nil {
svc.Logger.WithError(err).WithFields(logrus.Fields{
"push_token": subscription.PushToken,
}).Error("Failed to validate expo publish response")
return
}

svc.Logger.WithFields(logrus.Fields{
"event_id": event.ID,
"push_token": subscription.PushToken,
}).Debug("Push notification sent successfully")
}
Loading

0 comments on commit c973253

Please sign in to comment.