diff --git a/alerts/config.go b/alerts/config.go index b6f833465..9437c6a5f 100644 --- a/alerts/config.go +++ b/alerts/config.go @@ -269,3 +269,11 @@ type Repository interface { EnsureIndexes() error } + +// Note gathers information necessary for sending an alert notification. +type Note struct { + // Message communicates the alert to the recipient. + Message string + RecipientUserID string + FollowedUserID string +} diff --git a/data/service/service/standard.go b/data/service/service/standard.go index fcda95a2b..065f642ff 100644 --- a/data/service/service/standard.go +++ b/data/service/service/standard.go @@ -4,6 +4,7 @@ import ( "context" "github.com/Shopify/sarama" + "github.com/kelseyhightower/envconfig" eventsCommon "github.com/tidepool-org/go-common/events" "github.com/tidepool-org/platform/application" @@ -17,6 +18,7 @@ import ( dataSourceStoreStructured "github.com/tidepool-org/platform/data/source/store/structured" dataSourceStoreStructuredMongo "github.com/tidepool-org/platform/data/source/store/structured/mongo" dataStoreMongo "github.com/tidepool-org/platform/data/store/mongo" + "github.com/tidepool-org/platform/devicetokens" "github.com/tidepool-org/platform/errors" "github.com/tidepool-org/platform/events" "github.com/tidepool-org/platform/log" @@ -24,6 +26,7 @@ import ( "github.com/tidepool-org/platform/permission" permissionClient "github.com/tidepool-org/platform/permission/client" "github.com/tidepool-org/platform/platform" + "github.com/tidepool-org/platform/push" "github.com/tidepool-org/platform/service/server" "github.com/tidepool-org/platform/service/service" storeStructuredMongo "github.com/tidepool-org/platform/store/structured/mongo" @@ -41,6 +44,7 @@ type Standard struct { dataClient *Client clinicsClient *clinics.Client dataSourceClient *dataSourceServiceClient.Client + pusher Pusher userEventsHandler events.Runner api *api.Standard server *server.Standard @@ -87,6 +91,9 @@ func (s *Standard) Initialize(provider application.Provider) error { if err := s.initializeSaramaLogger(); err != nil { return err } + if err := s.initializePusher(); err != nil { + return err + } if err := s.initializeUserEventsHandler(); err != nil { return err } @@ -426,3 +433,34 @@ func (s *Standard) initializeSaramaLogger() error { sarama.Logger = log.NewSarama(s.Logger()) return nil } + +// Pusher is a service-agnostic interface for sending push notifications. +type Pusher interface { + // Push a notification to a device. + Push(context.Context, *devicetokens.DeviceToken, *push.Notification) error +} + +func (s *Standard) initializePusher() error { + var err error + + apns2Config := &struct { + SigningKey []byte `envconfig:"TIDEPOOL_DATA_SERVICE_PUSHER_APNS_SIGNING_KEY"` + KeyID string `envconfig:"TIDEPOOL_DATA_SERVICE_PUSHER_APNS_KEY_ID"` + BundleID string `envconfig:"TIDEPOOL_DATA_SERVICE_PUSHER_APNS_BUNDLE_ID"` + TeamID string `envconfig:"TIDEPOOL_DATA_SERVICE_PUSHER_APNS_TEAM_ID"` + }{} + if err := envconfig.Process("", apns2Config); err != nil { + return errors.Wrap(err, "Unable to process APNs pusher config") + } + + var pusher Pusher + pusher, err = push.NewAPNSPusherFromKeyData(apns2Config.SigningKey, apns2Config.KeyID, + apns2Config.TeamID, apns2Config.BundleID) + if err != nil { + s.Logger().WithError(err).Warn("falling back to logging of push notifications") + pusher = push.NewLogPusher(s.Logger()) + } + s.pusher = pusher + + return nil +} diff --git a/push/logpush.go b/push/logpush.go new file mode 100644 index 000000000..a313806a8 --- /dev/null +++ b/push/logpush.go @@ -0,0 +1,39 @@ +package push + +import ( + "context" + "os" + + "github.com/tidepool-org/platform/devicetokens" + "github.com/tidepool-org/platform/log" + logjson "github.com/tidepool-org/platform/log/json" + lognull "github.com/tidepool-org/platform/log/null" +) + +// LogPusher logs notifications instead of sending push notifications. +// +// Useful for dev or testing situations. +type LogPusher struct { + log.Logger +} + +// NewLogPusher uses a [log.Logger] instead of pushing via APNs. +func NewLogPusher(l log.Logger) *LogPusher { + if l == nil { + var err error + l, err = logjson.NewLogger(os.Stderr, log.DefaultLevelRanks(), log.DefaultLevel()) + if err != nil { + l = lognull.NewLogger() + } + } + return &LogPusher{Logger: l} +} + +// Push implements [service.Pusher]. +func (p *LogPusher) Push(ctx context.Context, deviceToken *devicetokens.DeviceToken, note *Notification) error { + p.Logger.WithFields(log.Fields{ + "deviceToken": deviceToken, + "note": note, + }).Info("logging push notification") + return nil +} diff --git a/push/push.go b/push/push.go index 419cd395b..bca2d4598 100644 --- a/push/push.go +++ b/push/push.go @@ -11,6 +11,7 @@ import ( "github.com/sideshow/apns2/payload" "github.com/sideshow/apns2/token" + "github.com/tidepool-org/platform/alerts" "github.com/tidepool-org/platform/devicetokens" "github.com/tidepool-org/platform/errors" "github.com/tidepool-org/platform/log" @@ -21,6 +22,17 @@ type Notification struct { Message string } +// String implements fmt.Stringer. +func (n Notification) String() string { + return n.Message +} + +func FromNote(note *alerts.Note) *Notification { + return &Notification{ + Message: note.Message, + } +} + // APNSPusher implements push notifications via Apple APNs. type APNSPusher struct { BundleID string @@ -47,6 +59,22 @@ func NewAPNSPusher(client APNS2Client, bundleID string) *APNSPusher { // // https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns func NewAPNSPusherFromKeyData(signingKey []byte, keyID, teamID, bundleID string) (*APNSPusher, error) { + if len(signingKey) == 0 { + return nil, errors.New("Unable to build APNSPusher: APNs signing key is blank") + } + + if bundleID == "" { + return nil, errors.New("Unable to build APNSPusher: bundleID is blank") + } + + if keyID == "" { + return nil, errors.New("Unable to build APNSPusher: keyID is blank") + } + + if teamID == "" { + return nil, errors.New("Unable to build APNSPusher: teamID is blank") + } + authKey, err := token.AuthKeyFromBytes(signingKey) if err != nil { return nil, err