diff --git a/service/adapters/apns/apns.go b/service/adapters/apns/apns.go index e0a39d1..5885728 100644 --- a/service/adapters/apns/apns.go +++ b/service/adapters/apns/apns.go @@ -116,6 +116,39 @@ func (a *APNS) SendFollowChangeNotification(followChange domain.FollowChangeBatc return nil } +func (a *APNS) SendSilentFollowChangeNotification(followChange domain.FollowChangeBatch, apnsToken domain.APNSToken) error { + if apnsToken.Hex() == "" { + return errors.New("invalid APNs token") + } + n, err := a.buildSilentFollowChangeNotification(followChange, apnsToken) + if err != nil { + return err + } + resp, err := a.client.Push(n) + //a.metrics.ReportCallToAPNS(resp.StatusCode, err) + if err != nil { + return errors.Wrap(err, "error pushing the silent follow change notification") + } + + if resp.StatusCode == 200 { + a.logger.Debug(). + WithField("uuid", n.ApnsID). + WithField("response.reason", resp.Reason). + WithField("response.statusCode", resp.StatusCode). + WithField("host", a.client.Host). + Message("sent a silent follow change notification") + } else { + a.logger.Error(). + WithField("uuid", n.ApnsID). + WithField("response.reason", resp.Reason). + WithField("response.statusCode", resp.StatusCode). + WithField("host", a.client.Host). + Message("failed to send a silent follow change notification") + } + + return nil +} + func (a *APNS) buildFollowChangeNotification(followChange domain.FollowChangeBatch, apnsToken domain.APNSToken) (*apns2.Notification, error) { payload, err := FollowChangePayload(followChange) if err != nil { @@ -134,6 +167,24 @@ func (a *APNS) buildFollowChangeNotification(followChange domain.FollowChangeBat return n, nil } +func (a *APNS) buildSilentFollowChangeNotification(followChange domain.FollowChangeBatch, apnsToken domain.APNSToken) (*apns2.Notification, error) { + payload, err := SilentFollowChangePayload(followChange) + if err != nil { + return nil, errors.Wrap(err, "error creating a payload") + } + + n := &apns2.Notification{ + PushType: apns2.PushTypeAlert, + ApnsID: uuid.New().String(), + DeviceToken: apnsToken.Hex(), + Topic: a.cfg.APNSTopic(), + Payload: payload, + Priority: apns2.PriorityLow, + } + + return n, nil +} + func FollowChangePayload(followChange domain.FollowChangeBatch) ([]byte, error) { return FollowChangePayloadWithValidation(followChange, true) } @@ -203,6 +254,53 @@ func FollowChangePayloadWithValidation(followChange domain.FollowChangeBatch, va return payloadBytes, nil } +func SilentFollowChangePayload(followChange domain.FollowChangeBatch) ([]byte, error) { + return SilentFollowChangePayloadWithValidation(followChange, true) +} + +func SilentFollowChangePayloadWithValidation(followChange domain.FollowChangeBatch, validate bool) ([]byte, error) { + totalNpubs := len(followChange.Follows) + if validate && totalNpubs > MAX_TOTAL_NPUBS { + return nil, errors.New("FollowChangeBatch for followee " + followChange.Followee.Hex() + " has too many npubs (" + fmt.Sprint(totalNpubs) + "). MAX_TOTAL_NPUBS is " + fmt.Sprint(MAX_TOTAL_NPUBS)) + } + + singleChange := totalNpubs == 1 + + npubFollows, error := pubkeysToNpubs(followChange.Follows) + if error != nil { + return nil, errors.Wrap(error, "error encoding follow npubs") + } + + // See https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification + + var data map[string]interface{} + + if singleChange { + data = map[string]interface{}{ + "follows": npubFollows, + "friendlyFollower": followChange.FriendlyFollower, + } + } else { + data = map[string]interface{}{ + "follows": npubFollows, + } + } + + payload := map[string]interface{}{ + "aps": map[string]interface{}{ + "content-available": 1, + }, + "data": data, + } + + payloadBytes, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + return payloadBytes, nil +} + func pubkeysToNpubs(pubkeys []domain.PublicKey) ([]string, error) { npubs := make([]string, len(pubkeys)) for i, pubkey := range pubkeys { diff --git a/service/adapters/apns/apns_mock.go b/service/adapters/apns/apns_mock.go index 1fec1df..07fd31f 100644 --- a/service/adapters/apns/apns_mock.go +++ b/service/adapters/apns/apns_mock.go @@ -41,6 +41,12 @@ func (a *APNSMock) SendFollowChangeNotification(followChange domain.FollowChange return a.SendNotification(notification) } +func (a *APNSMock) SendSilentFollowChangeNotification(followChange domain.FollowChangeBatch, token domain.APNSToken) error { + notification := notifications.Notification{} + + return a.SendNotification(notification) +} + func (a *APNSMock) SentNotifications() []notifications.Notification { a.sentNotificationsLock.Lock() defer a.sentNotificationsLock.Unlock() diff --git a/service/adapters/apns/apns_test.go b/service/adapters/apns/apns_test.go index 95e715f..a1971a7 100644 --- a/service/adapters/apns/apns_test.go +++ b/service/adapters/apns/apns_test.go @@ -174,6 +174,34 @@ func TestFollowChangePayload_BatchedFollow_WithNoFriendlyFollower(t *testing.T) require.Equal(t, expectedPayload, actualPayload) } + +func TestSilentFollowChangePayload_BatchedFollow_WithNoFriendlyFollower(t *testing.T) { + pk1, _ := fixtures.PublicKeyAndNpub() + pk2, pk2Npub := fixtures.PublicKeyAndNpub() + pk3, pk3Npub := fixtures.PublicKeyAndNpub() + + batch := domain.FollowChangeBatch{ + Followee: pk1, + Follows: []domain.PublicKey{pk2, pk3}, + } + + payload, err := apns.SilentFollowChangePayload(batch) + require.NoError(t, err) + + expectedPayload := map[string]interface{}{ + "aps": map[string]interface{}{ + "content-available": float64(1), + }, + "data": map[string]interface{}{ + "follows": []interface{}{pk2Npub, pk3Npub}, + }, + } + + var actualPayload map[string]interface{} + err = json.Unmarshal(payload, &actualPayload) + require.NoError(t, err) + require.Equal(t, expectedPayload, actualPayload) +} func TestFollowChangePayload_Exceeds4096Bytes_With60TotalNpubs(t *testing.T) { pk1, _ := fixtures.PublicKeyAndNpub() diff --git a/service/app/app.go b/service/app/app.go index 1880b6b..7efeffc 100644 --- a/service/app/app.go +++ b/service/app/app.go @@ -83,6 +83,7 @@ type Queries struct { type APNS interface { SendNotification(notification notifications.Notification) error SendFollowChangeNotification(followChange domain.FollowChangeBatch, apnsToken domain.APNSToken) error + SendSilentFollowChangeNotification(followChange domain.FollowChangeBatch, apnsToken domain.APNSToken) error } type EventOrError struct { diff --git a/service/app/follow_change_puller.go b/service/app/follow_change_puller.go index 1d89f79..d14598a 100644 --- a/service/app/follow_change_puller.go +++ b/service/app/follow_change_puller.go @@ -79,6 +79,15 @@ func (f *FollowChangePuller) Run(ctx context.Context) error { Message("error sending follow change notification") continue } + + if err := f.apns.SendSilentFollowChangeNotification(*followChangeAggregate, token); err != nil { + f.logger.Error(). + WithField("token", token.Hex()). + WithField("followee", followChangeAggregate.Followee.Hex()). + WithError(err). + Message("error sending silent follow change notification") + continue + } } f.counter += 1