Skip to content
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

See https://developers.mattermost.com/contribute/mobile/push-notifications/service/

## VoIP push notifications (iOS Calls)

The proxy delivers PushKit / VoIP pushes for iOS calls. Dispatch is driven by the `transport` field on the incoming notification: when `transport=voip`, the proxy emits a VoIP-shaped APNs request (`apns-push-type: voip`, topic `<ApplePushTopic>.voip`, minimal payload) using the existing `apple_rn` / `apple_rnbeta` `ApplePushSettings` entry indicated by the message's `platform`. No extra configuration block is required; the same APNs key is reused for both standard and VoIP pushes.

Operator prerequisites:

- The iOS app bundle must declare the `voip` background mode and ship with an entitlement granting the `<bundle>.voip` APNs topic.

No changes to the standard `apple_rn` / `apple_rnbeta` entries are required.


# How to Release

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattermost/logr/v2 v2.0.22 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 // indirect
Expand Down
14 changes: 7 additions & 7 deletions server/android_notification_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (me *AndroidNotificationServer) SendNotification(msg *PushNotification) Pus
}

if me.metrics != nil {
me.metrics.incrementNotificationTotal(PushNotifyAndroid, pushType)
me.metrics.incrementNotificationTotal(PushNotifyAndroid, pushType, "")
}
fcmMsg := &messaging.Message{
Token: msg.DeviceID,
Expand All @@ -187,7 +187,7 @@ func (me *AndroidNotificationServer) SendNotification(msg *PushNotification) Pus
me.logger.Error(
"Failed to send FCM push",
mlog.String("sid", msg.ServerID),
mlog.String("did", msg.DeviceID),
mlog.String("did", RedactToken(msg.DeviceID)),
mlog.Err(err),
mlog.String("type", me.AndroidPushSettings.Type),
mlog.String("errorCode", errorCode),
Expand All @@ -196,7 +196,7 @@ func (me *AndroidNotificationServer) SendNotification(msg *PushNotification) Pus
if messaging.IsUnregistered(err) || messaging.IsSenderIDMismatch(err) {
me.logger.Info("Android response failure sending remove code", mlog.String("type", me.AndroidPushSettings.Type))
if me.metrics != nil {
me.metrics.incrementRemoval(PushNotifyAndroid, pushType, unregistered)
me.metrics.incrementRemoval(PushNotifyAndroid, pushType, "", unregistered)
}
return NewRemovePushResponse()
}
Expand All @@ -218,17 +218,17 @@ func (me *AndroidNotificationServer) SendNotification(msg *PushNotification) Pus

}
if me.metrics != nil {
me.metrics.incrementFailure(PushNotifyAndroid, pushType, reason)
me.metrics.incrementFailure(PushNotifyAndroid, pushType, "", reason)
}

return NewErrorPushResponse(err.Error())
}

if me.metrics != nil {
if msg.AckID != "" {
me.metrics.incrementSuccessWithAck(PushNotifyAndroid, pushType)
me.metrics.incrementSuccessWithAck(PushNotifyAndroid, pushType, "")
} else {
me.metrics.incrementSuccess(PushNotifyAndroid, pushType)
me.metrics.incrementSuccess(PushNotifyAndroid, pushType, "")
}
}
return NewOkPushResponse()
Expand All @@ -238,7 +238,7 @@ func (me *AndroidNotificationServer) SendNotificationWithRetry(fcmMsg *messaging
var err error
waitTime := time.Second

logger := me.logger.With(mlog.String("did", fcmMsg.Token))
logger := me.logger.With(mlog.String("did", RedactToken(fcmMsg.Token)))

// Keep a general context to make sure the whole retry
// doesn't take longer than the timeout.
Expand Down
166 changes: 120 additions & 46 deletions server/apple_notification_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ func (me *AppleNotificationServer) Initialize() error {
}

func (me *AppleNotificationServer) SendNotification(msg *PushNotification) PushResponse {
if msg.Transport == PushTransportVoIP {
return me.sendVoIPNotification(msg)
}

data := payload.NewPayload()
if msg.Badge == 0 && msg.Type == PushTypeClear && msg.AppVersion > 1 {
Expand Down Expand Up @@ -169,7 +172,7 @@ func (me *AppleNotificationServer) SendNotification(msg *PushNotification) PushR
}
}
if me.metrics != nil {
me.metrics.incrementNotificationTotal(PushNotifyApple, pushType)
me.metrics.incrementNotificationTotal(PushNotifyApple, pushType, "")
}
data.Custom("type", pushType)
data.Custom("sub_type", msg.SubType)
Expand Down Expand Up @@ -229,67 +232,138 @@ func (me *AppleNotificationServer) SendNotification(msg *PushNotification) PushR
data.Custom("from_webhook", msg.FromWebhook)
}

if me.AppleClient != nil {
me.logger.Info(
"Sending apple push notification",
mlog.String("device", me.ApplePushSettings.Type),
mlog.String("type", msg.Type),
mlog.String("ack_id", msg.AckID),
)
return me.dispatchAndHandleResponse(notification, msg, pushType, "")
}

res, err := me.SendNotificationWithRetry(notification)
if err != nil {
me.logger.Error(
"Failed to send apple push",
mlog.String("sid", msg.ServerID),
mlog.String("did", msg.DeviceID),
mlog.Err(err),
mlog.String("type", me.ApplePushSettings.Type),
)
if me.metrics != nil {
me.metrics.incrementFailure(PushNotifyApple, pushType, "RequestError")
}
return NewErrorPushResponse("unknown transport error")
}
func (me *AppleNotificationServer) dispatchAndHandleResponse(notification *apns.Notification, msg *PushNotification, pushType, transport string) PushResponse {
if me.AppleClient == nil {
return NewOkPushResponse()
}

if !res.Sent() {
if res.Reason == apns.ReasonBadDeviceToken || res.Reason == apns.ReasonUnregistered || res.Reason == apns.ReasonMissingDeviceToken || res.Reason == apns.ReasonDeviceTokenNotForTopic {
me.logger.Info(
"Failed to send apple push sending remove code res",
mlog.String("ApnsID", res.ApnsID),
mlog.String("reason", res.Reason),
mlog.Int("code", res.StatusCode),
mlog.String("type", me.ApplePushSettings.Type),
)
if me.metrics != nil {
me.metrics.incrementRemoval(PushNotifyApple, pushType, res.Reason)
}
return NewRemovePushResponse()
}
logFields := []mlog.Field{
mlog.String("device", me.ApplePushSettings.Type),
mlog.String("type", msg.Type),
mlog.String("ack_id", msg.AckID),
}
if transport != "" {
logFields = append(logFields, mlog.String("transport", transport))
}
me.logger.Info("Sending apple push notification", logFields...)

me.logger.Error(
"Failed to send apple push with res",
res, err := me.SendNotificationWithRetry(notification)
if err != nil {
errFields := []mlog.Field{
mlog.String("sid", msg.ServerID),
mlog.String("did", RedactToken(msg.DeviceID)),
mlog.Err(err),
mlog.String("type", me.ApplePushSettings.Type),
}
if transport != "" {
errFields = append(errFields, mlog.String("transport", transport))
}
me.logger.Error("Failed to send apple push", errFields...)
if me.metrics != nil {
me.metrics.incrementFailure(PushNotifyApple, pushType, transport, "RequestError")
}
return NewErrorPushResponse("unknown transport error")
}

if !res.Sent() {
if res.Reason == apns.ReasonBadDeviceToken || res.Reason == apns.ReasonUnregistered || res.Reason == apns.ReasonMissingDeviceToken || res.Reason == apns.ReasonDeviceTokenNotForTopic {
me.logger.Info(
"Failed to send apple push sending remove code res",
mlog.String("ApnsID", res.ApnsID),
mlog.String("reason", res.Reason),
mlog.Int("code", res.StatusCode),
mlog.String("type", me.ApplePushSettings.Type),
)
if me.metrics != nil {
me.metrics.incrementFailure(PushNotifyApple, pushType, res.Reason)
me.metrics.incrementRemoval(PushNotifyApple, pushType, transport, res.Reason)
}
return NewErrorPushResponse("unknown send response error")
return NewRemovePushResponse()
}

me.logger.Error(
"Failed to send apple push with res",
mlog.String("ApnsID", res.ApnsID),
mlog.String("reason", res.Reason),
mlog.Int("code", res.StatusCode),
mlog.String("type", me.ApplePushSettings.Type),
)
if me.metrics != nil {
me.metrics.incrementFailure(PushNotifyApple, pushType, transport, res.Reason)
}
return NewErrorPushResponse("unknown send response error")
}

if me.metrics != nil {
if msg.AckID != "" {
me.metrics.incrementSuccessWithAck(PushNotifyApple, pushType)
me.metrics.incrementSuccessWithAck(PushNotifyApple, pushType, transport)
} else {
me.metrics.incrementSuccess(PushNotifyApple, pushType)
me.metrics.incrementSuccess(PushNotifyApple, pushType, transport)
}
}
return NewOkPushResponse()
}

// sendVoIPNotification dispatches a PushKit VoIP push using the same APNs key
// configured for the standard target. The payload carries the routing fields
// the mobile client needs to wake the call UI; the canonical Call state
// (callID, hostID, participants, etc.) is fetched via the existing
// GET /calls REST roundtrip once the app foregrounds and reconnects its
// WebSocket.
func (me *AppleNotificationServer) sendVoIPNotification(msg *PushNotification) PushResponse {
notification := me.buildVoIPNotification(msg)

if me.metrics != nil {
me.metrics.incrementNotificationTotal(PushNotifyApple, msg.Type, PushTransportVoIP)
}
Comment on lines +318 to +320

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have metrics for this new VoIP path? (Do we need to key off SubType somewhere?)


return me.dispatchAndHandleResponse(notification, msg, msg.Type, PushTransportVoIP)
}

func (me *AppleNotificationServer) buildVoIPNotification(msg *PushNotification) *apns.Notification {
data := payload.NewPayload().
ContentAvailable().
Custom("type", msg.Type).
Custom("sub_type", msg.SubType).
Custom("channel_id", msg.ChannelID).
Custom("server_id", msg.ServerID).
Custom("post_id", msg.PostID).
Custom("thread_id", msg.RootID).
Custom("sender_id", msg.SenderID).
Custom("id_loaded", msg.IsIDLoaded)

// sender_name and channel_name are only populated by the server when
// PushNotificationContents is FullNotification or GenericNotification —
// for IdLoadedNotification they're omitted on purpose and the device
// fetches them via the ack-receipt round-trip.
if msg.SenderName != "" {
data.Custom("sender_name", msg.SenderName)
}
if msg.ChannelName != "" {
data.Custom("channel_name", msg.ChannelName)
}

if msg.AckID != "" {
data.Custom("ack_id", msg.AckID)
}

if msg.Signature == "" {
data.Custom("signature", "NO_SIGNATURE")
} else {
data.Custom("signature", msg.Signature)
}

return &apns.Notification{
DeviceToken: msg.DeviceID,
Payload: data,
Topic: me.ApplePushSettings.ApplePushTopic + ".voip",
Priority: apns.PriorityHigh,
PushType: apns.PushTypeVOIP,
}
}

func (me *AppleNotificationServer) SendNotificationWithRetry(notification *apns.Notification) (*apns.Response, error) {
var res *apns.Response
var err error
Expand All @@ -300,7 +374,7 @@ func (me *AppleNotificationServer) SendNotificationWithRetry(notification *apns.
generalContext, cancelGeneralContext := context.WithTimeout(context.Background(), me.sendTimeout)
defer cancelGeneralContext()

for retries := 0; retries < MAX_RETRIES; retries++ {
for retries := range MAX_RETRIES {
start := time.Now()

retryContext, cancelRetryContext := context.WithTimeout(generalContext, me.retryTimeout)
Expand All @@ -316,13 +390,13 @@ func (me *AppleNotificationServer) SendNotificationWithRetry(notification *apns.

me.logger.Error(
"Failed to send apple push",
mlog.String("did", notification.DeviceToken),
mlog.String("did", RedactToken(notification.DeviceToken)),
mlog.Int("retry", retries),
mlog.Err(err),
)

if retries == MAX_RETRIES-1 {
me.logger.Error("Max retries reached", mlog.String("did", notification.DeviceToken))
me.logger.Error("Max retries reached", mlog.String("did", RedactToken(notification.DeviceToken)))
break
}

Expand All @@ -334,7 +408,7 @@ func (me *AppleNotificationServer) SendNotificationWithRetry(notification *apns.
if generalContext.Err() != nil {
me.logger.Info(
"Not retrying because context error",
mlog.String("did", notification.DeviceToken),
mlog.String("did", RedactToken(notification.DeviceToken)),
mlog.Int("retry", retries),
mlog.Err(generalContext.Err()),
)
Expand Down
Loading
Loading