diff --git a/go.mod b/go.mod index cbd3b4f84c2ecda77836cd87f631d34326485e8e..750797553056cdcff5b0b42a02dda39b51ee945b 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( firebase.google.com/go v3.12.0+incompatible github.com/jonahh-yeti/apns v0.0.1 github.com/pkg/errors v0.9.1 + github.com/sideshow/apns2 v0.20.0 github.com/spf13/cobra v1.0.0 github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/viper v1.7.0 diff --git a/go.sum b/go.sum index 811e5c87ec25d3606cc5c1717155751d99d76db7..f9d445a30cf8a5acb448cae35f5aac20bb159091 100644 --- a/go.sum +++ b/go.sum @@ -327,6 +327,8 @@ github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9Nz github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sideshow/apns2 v0.20.0 h1:5Lzk4DUq+waVc6/BkKzpDTpQjtk/BZOP0YsayBpY1NE= +github.com/sideshow/apns2 v0.20.0/go.mod h1:f7dArLPLbiZ3qPdzzrZXdCSlMp8FD0p6z7tHssDOLvk= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= diff --git a/notifications/apns/apns.go b/notifications/apns/apns.go new file mode 100644 index 0000000000000000000000000000000000000000..2e3f8e15bfba4ef4ca0815040a1f865247922be4 --- /dev/null +++ b/notifications/apns/apns.go @@ -0,0 +1,19 @@ +package apns + +import "github.com/sideshow/apns2" + +type ApnsComm struct { + *apns2.Client + topic string +} + +func NewApnsComm(cl *apns2.Client, topic string) *ApnsComm { + return &ApnsComm{ + Client: cl, + topic: topic, + } +} + +func (c *ApnsComm) GetTopic() string { + return c.topic +} diff --git a/firebase/fcm.go b/notifications/firebase/fcm.go similarity index 96% rename from firebase/fcm.go rename to notifications/firebase/fcm.go index cbe26d45b50b1c02c0accdcd4bce7e31ea832f2f..94bbfb4d0eb0a0af00c0cd7f7badc82bee0ad183 100644 --- a/firebase/fcm.go +++ b/notifications/firebase/fcm.go @@ -21,13 +21,13 @@ import ( ) // function types for use in notificationsbot struct -type SetupFunc func(string) (*messaging.Client, context.Context, error) type SendFunc func(FBSender, string, *mixmessages.NotificationData) (string, error) // FirebaseComm is a struct which holds the functions to setup the messaging app and sending notifications // Using a struct in this manner allows us to properly unit test the NotifyUser function type FirebaseComm struct { SendNotification SendFunc + *messaging.Client } // FBSender is an interface which matches the send function in the messaging app, allowing us to unit test sendNotification @@ -36,9 +36,10 @@ type FBSender interface { } // NewFirebaseComm create a *FirebaseComm object with the proper setup and send functions -func NewFirebaseComm() *FirebaseComm { +func NewFirebaseComm(cl *messaging.Client) *FirebaseComm { return &FirebaseComm{ SendNotification: sendNotification, + Client: cl, } } diff --git a/firebase/fcm_test.go b/notifications/firebase/fcm_test.go similarity index 98% rename from firebase/fcm_test.go rename to notifications/firebase/fcm_test.go index 70741aec18f7f8077661c80ced17cd17af636fd2..08650bbdc49e3a9401a13a0debe9c0383695235a 100644 --- a/firebase/fcm_test.go +++ b/notifications/firebase/fcm_test.go @@ -37,7 +37,7 @@ func TestSendNotification(t *testing.T) { // Unit test the NewFirebaseComm method func TestNewFirebaseComm(t *testing.T) { - comm := NewFirebaseComm() + comm := NewFirebaseComm(nil) if comm.SendNotification == nil { t.Error("Failed to set functions in comm") } diff --git a/notifications/notifications.go b/notifications/notifications.go index c7a0aa2fe307a2ccd17cb4540d399e04a5ee6726..f500602d2104a653fbef6857860d24ba5a2e3a77 100644 --- a/notifications/notifications.go +++ b/notifications/notifications.go @@ -10,16 +10,20 @@ package notifications import ( "encoding/base64" - "firebase.google.com/go/messaging" - "github.com/jonahh-yeti/apns" + "gitlab.com/elixxir/notifications-bot/notifications/apns" + + // "github.com/jonahh-yeti/apns" "github.com/pkg/errors" + "github.com/sideshow/apns2" + "github.com/sideshow/apns2/payload" + apnstoken "github.com/sideshow/apns2/token" jww "github.com/spf13/jwalterweatherman" pb "gitlab.com/elixxir/comms/mixmessages" "gitlab.com/elixxir/comms/network" "gitlab.com/elixxir/comms/notificationBot" "gitlab.com/elixxir/crypto/hash" "gitlab.com/elixxir/crypto/registration" - "gitlab.com/elixxir/notifications-bot/firebase" + "gitlab.com/elixxir/notifications-bot/notifications/firebase" "gitlab.com/elixxir/notifications-bot/storage" "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/crypto/signature/rsa" @@ -34,10 +38,7 @@ import ( ) // Function type definitions for the main operations (poll and notify) -type NotifyFunc func(*pb.NotificationData, ApnsSender, *messaging.Client, *firebase.FirebaseComm, *storage.Storage) error -type ApnsSender interface { - Send(token string, p apns.Payload, opts ...apns.SendOption) (*apns.Response, error) -} +type NotifyFunc func(*pb.NotificationData, *apns.ApnsComm, *firebase.FirebaseComm, *storage.Storage) error // Params struct holds info passed in for configuration type Params struct { @@ -61,8 +62,8 @@ type Impl struct { Storage *storage.Storage inst *network.Instance notifyFunc NotifyFunc - fcm *messaging.Client - apnsClient *apns.Client + fcm *firebase.FirebaseComm + apnsClient *apns.ApnsComm receivedNdf *uint32 ndfStopper Stopper @@ -92,18 +93,19 @@ func StartNotifications(params Params, noTLS, noFirebase bool) (*Impl, error) { } // Set up firebase messaging client - var app *messaging.Client + var fbComm *firebase.FirebaseComm if !noFirebase { - app, err = firebase.SetupMessagingApp(params.FBCreds) + app, err := firebase.SetupMessagingApp(params.FBCreds) if err != nil { return nil, errors.Wrap(err, "Failed to setup firebase messaging app") } + fbComm = firebase.NewFirebaseComm(app) } receivedNdf := uint32(0) impl := &Impl{ notifyFunc: notifyUser, - fcm: app, + fcm: fbComm, receivedNdf: &receivedNdf, } @@ -113,27 +115,27 @@ func StartNotifications(params Params, noTLS, noFirebase bool) (*Impl, error) { if params.APNS.KeyID == "" || params.APNS.Issuer == "" || params.APNS.BundleID == "" { return nil, errors.WithMessagef(err, "APNS not properly configured: %+v", params.APNS) } - apnsKey, err := utils.ReadFile(params.APNS.KeyPath) + + authKey, err := apnstoken.AuthKeyFromFile(params.APNS.KeyPath) if err != nil { - return nil, errors.WithMessage(err, "Failed to read APNS key") + return nil, errors.WithMessage(err, "Failed to load auth key from file") + } + token := &apnstoken.Token{ + AuthKey: authKey, + // KeyID from developer account (Certificates, Identifiers & Profiles -> Keys) + KeyID: params.APNS.KeyID, + // TeamID from developer account (View Account -> Membership) + TeamID: params.APNS.Issuer, } - var endpoint apns.ClientOption + apnsClient := apns2.NewTokenClient(token) if params.APNS.Dev { - jww.INFO.Println("") - endpoint = apns.WithEndpoint(apns.DevelopmentGateway) + jww.INFO.Printf("Running with dev apns gateway") + apnsClient.Development() } else { - endpoint = apns.WithEndpoint(apns.ProductionGateway) + apnsClient.Production() } - apnsClient, err := apns.NewClient( - apns.WithJWT(apnsKey, params.APNS.KeyID, params.APNS.Issuer), - apns.WithBundleID(params.APNS.BundleID), - apns.WithMaxIdleConnections(100), - apns.WithTimeout(5*time.Second), - endpoint) - if err != nil { - return nil, errors.WithMessage(err, "Failed to setup apns client") - } - impl.apnsClient = apnsClient + + impl.apnsClient = apns.NewApnsComm(apnsClient, params.APNS.BundleID) } // Start notification comms server @@ -171,7 +173,7 @@ func NewImplementation(instance *Impl) *notificationBot.Implementation { // NotifyUser accepts a UID and service key file path. // It handles the logic involved in retrieving a user's token and sending the notification -func notifyUser(data *pb.NotificationData, apnsClient ApnsSender, fcm *messaging.Client, fc *firebase.FirebaseComm, db *storage.Storage) error { +func notifyUser(data *pb.NotificationData, apnsClient *apns.ApnsComm, fc *firebase.FirebaseComm, db *storage.Storage) error { elist, err := db.GetEphemeral(data.EphemeralID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -188,25 +190,23 @@ func notifyUser(data *pb.NotificationData, apnsClient ApnsSender, fcm *messaging } isAPNS := !strings.Contains(u.Token, ":") - mutableContent := 1 + // mutableContent := 1 if isAPNS { jww.INFO.Printf("Notifying ephemeral ID %+v via APNS to token %+v", data.EphemeralID, u.Token) - resp, err := apnsClient.Send(u.Token, apns.Payload{ - APS: apns.APS{ - Alert: apns.Alert{ - Title: "Privacy: protected!", - Body: "Some notifications are not for you to ensure privacy; we hope to remove this notification soon", - }, - MutableContent: &mutableContent, - }, - CustomValues: map[string]interface{}{ - "messagehash": base64.StdEncoding.EncodeToString(data.MessageHash), - "identityfingerprint": base64.StdEncoding.EncodeToString(data.IdentityFP), - }, - }, apns.WithExpiration(604800), // 1 week - apns.WithPriority(10), - apns.WithCollapseID(base64.StdEncoding.EncodeToString(u.TransmissionRSAHash)), - apns.WithPushType("alert")) + notifPayload := payload.NewPayload().AlertTitle("Privacy: protected!").AlertBody( + "Some notifications are not for you to ensure privacy; we hope to remove this notification soon").MutableContent().Custom( + "messagehash", base64.StdEncoding.EncodeToString(data.MessageHash)).Custom( + "identityfingerprint", base64.StdEncoding.EncodeToString(data.IdentityFP)) + notif := &apns2.Notification{ + CollapseID: base64.StdEncoding.EncodeToString(u.TransmissionRSAHash), + DeviceToken: u.Token, + Expiration: time.Now().Add(time.Hour * 24 * 7), + Priority: apns2.PriorityHigh, + Payload: notifPayload, + PushType: apns2.PushTypeAlert, + Topic: apnsClient.GetTopic(), + } + resp, err := apnsClient.Push(notif) if err != nil { jww.ERROR.Printf("Failed to send notification via APNS: %+v: %+v", resp, err) // TODO : Should be re-enabled for specific error cases? deep dive on apns docs may be helpful @@ -218,7 +218,7 @@ func notifyUser(data *pb.NotificationData, apnsClient ApnsSender, fcm *messaging jww.INFO.Printf("Notified ephemeral ID %+v [%+v] and received response %+v", data.EphemeralID, u.Token, resp) } } else { - resp, err := fc.SendNotification(fcm, u.Token, data) + resp, err := fc.SendNotification(fc.Client, u.Token, data) if err != nil { // Catch two firebase errors that we don't want to crash on // 400 and 404 indicate that the token stored is incorrect @@ -335,9 +335,8 @@ func (nb *Impl) ReceiveNotificationBatch(notifBatch *pb.NotificationBatch, auth jww.INFO.Printf("Received notification batch for round %+v", notifBatch.RoundID) - fbComm := firebase.NewFirebaseComm() for _, notifData := range notifBatch.GetNotifications() { - err := nb.notifyFunc(notifData, nb.apnsClient, nb.fcm, fbComm, nb.Storage) + err := nb.notifyFunc(notifData, nb.apnsClient, nb.fcm, nb.Storage) if err != nil { return err } diff --git a/notifications/notifications_test.go b/notifications/notifications_test.go index c7fc27518ebf6f4b080a69b9a9bd3c086a3ed0d0..b2d9de848ced32e0be43f6cad0ee17d3c09bf80e 100644 --- a/notifications/notifications_test.go +++ b/notifications/notifications_test.go @@ -6,14 +6,14 @@ package notifications import ( - "firebase.google.com/go/messaging" "fmt" - "github.com/jonahh-yeti/apns" "github.com/pkg/errors" + "github.com/sideshow/apns2" pb "gitlab.com/elixxir/comms/mixmessages" "gitlab.com/elixxir/crypto/hash" "gitlab.com/elixxir/crypto/registration" - "gitlab.com/elixxir/notifications-bot/firebase" + "gitlab.com/elixxir/notifications-bot/notifications/apns" + "gitlab.com/elixxir/notifications-bot/notifications/firebase" "gitlab.com/elixxir/notifications-bot/storage" "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/crypto/csprng" @@ -29,12 +29,6 @@ import ( var port = 4200 -type MockApns struct{} - -func (m *MockApns) Send(token string, p apns.Payload, opts ...apns.SendOption) (*apns.Response, error) { - return nil, nil -} - // Test notificationbot's notifyuser function // this mocks the setup and send functions, and only tests the core logic of this function func TestNotifyUser(t *testing.T) { @@ -68,11 +62,13 @@ func TestNotifyUser(t *testing.T) { if err != nil { t.Errorf("Failed to add latest ephemeral: %+v", err) } + + ac := apns.NewApnsComm(apns2.NewTokenClient(nil), "") err = notifyUser(&pb.NotificationData{ EphemeralID: eph.EphemeralId, IdentityFP: nil, MessageHash: nil, - }, &MockApns{}, nil, fcBadSend, s) + }, ac, fcBadSend, s) if err == nil { t.Errorf("Should have returned an error") } @@ -81,7 +77,7 @@ func TestNotifyUser(t *testing.T) { EphemeralID: eph.EphemeralId, IdentityFP: nil, MessageHash: nil, - }, &MockApns{}, nil, fc, s) + }, ac, fc, s) if err != nil { t.Errorf("Failed to notify user properly") } @@ -292,7 +288,7 @@ func TestImpl_UnregisterForNotifications(t *testing.T) { func TestImpl_ReceiveNotificationBatch(t *testing.T) { impl := getNewImpl() dataChan := make(chan *pb.NotificationData) - impl.notifyFunc = func(data *pb.NotificationData, apns ApnsSender, f *messaging.Client, fc *firebase.FirebaseComm, s *storage.Storage) error { + impl.notifyFunc = func(data *pb.NotificationData, apns *apns.ApnsComm, fc *firebase.FirebaseComm, s *storage.Storage) error { go func() { dataChan <- data }() return nil }