From d676fb8000b00532933e92dcc643782d01a25789 Mon Sep 17 00:00:00 2001 From: morikuni Date: Tue, 28 Jan 2025 14:18:13 +0900 Subject: [PATCH 1/2] Support push for Live Activity --- messaging/messaging.go | 84 +++++++++++++++++++++++++++++++++++-- messaging/messaging_test.go | 44 +++++++++++++++++++ 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/messaging/messaging.go b/messaging/messaging.go index dcd412c3..8d27bbe0 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -620,9 +620,10 @@ type WebpushFCMOptions struct { // See https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html // for more details on supported headers and payload keys. type APNSConfig struct { - Headers map[string]string `json:"headers,omitempty"` - Payload *APNSPayload `json:"payload,omitempty"` - FCMOptions *APNSFCMOptions `json:"fcm_options,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Payload *APNSPayload `json:"payload,omitempty"` + FCMOptions *APNSFCMOptions `json:"fcm_options,omitempty"` + LiveActivityToken string `json:"live_activity_token,omitempty"` } // APNSPayload is the payload that can be included in an APNS message. @@ -685,6 +686,13 @@ type Aps struct { MutableContent bool `json:"-"` Category string `json:"category,omitempty"` ThreadID string `json:"thread-id,omitempty"` + StaleDate *time.Time `json:"-"` + ContentState map[string]interface{} `json:"content-state,omitempty"` + Timestamp *time.Time `json:"-"` + Event LiveActivityEvent `json:"-"` + DismissalDate *time.Time `json:"-"` + AttributesType string `json:"attributes-type,omitempty"` + Attributes map[string]interface{} `json:"attributes,omitempty"` CustomData map[string]interface{} `json:"-"` } @@ -716,6 +724,32 @@ func (a *Aps) standardFields() map[string]interface{} { if a.ThreadID != "" { m["thread-id"] = a.ThreadID } + if a.StaleDate != nil { + m["stale-date"] = a.StaleDate.Unix() + } + if a.ContentState != nil { + m["content-state"] = a.ContentState + } + if a.Timestamp != nil { + m["timestamp"] = a.Timestamp.Unix() + } + if a.Event != liveActivityEventUnspecified { + events := map[LiveActivityEvent]string{ + LiveActivityEventStart: "start", + LiveActivityEventUpdate: "update", + LiveActivityEventEnd: "end", + } + m["event"] = events[a.Event] + } + if a.DismissalDate != nil { + m["dismissal-date"] = a.DismissalDate.Unix() + } + if a.AttributesType != "" { + m["attributes-type"] = a.AttributesType + } + if a.Attributes != nil { + m["attributes"] = a.Attributes + } return m } @@ -736,6 +770,10 @@ func (a *Aps) UnmarshalJSON(b []byte) error { SoundObject *json.RawMessage `json:"sound,omitempty"` ContentAvailableInt int `json:"content-available,omitempty"` MutableContentInt int `json:"mutable-content,omitempty"` + StaleDate *int64 `json:"stale-date,omitempty"` + Timestamp *int64 `json:"timestamp,omitempty"` + Event string `json:"event,omitempty"` + DismissalDate *int64 `json:"dismissal-date,omitempty"` *apsInternal }{ apsInternal: (*apsInternal)(a), @@ -761,6 +799,30 @@ func (a *Aps) UnmarshalJSON(b []byte) error { } } } + if temp.StaleDate != nil { + staleDate := time.Unix(*temp.StaleDate, 0) + a.StaleDate = &staleDate + } + if temp.Timestamp != nil { + timestamp := time.Unix(*temp.Timestamp, 0) + a.Timestamp = ×tamp + } + if temp.Event != "" { + events := map[string]LiveActivityEvent{ + "start": LiveActivityEventStart, + "update": LiveActivityEventUpdate, + "end": LiveActivityEventEnd, + } + if event, ok := events[temp.Event]; ok { + a.Event = event + } else { + return fmt.Errorf("unknown event value: %q", temp.Event) + } + } + if temp.DismissalDate != nil { + dismissalDate := time.Unix(*temp.DismissalDate, 0) + a.DismissalDate = &dismissalDate + } allFields := make(map[string]interface{}) if err := json.Unmarshal(b, &allFields); err != nil { @@ -775,6 +837,22 @@ func (a *Aps) UnmarshalJSON(b []byte) error { return nil } +// LiveActivityEvent represents an event type for Live Activity in push notification payloads. +type LiveActivityEvent int + +const ( + liveActivityEventUnspecified LiveActivityEvent = iota + + // LiveActivityEventStart starts a Live Activity. + LiveActivityEventStart + + // LiveActivityEventUpdate updates a Live Activity. + LiveActivityEventUpdate + + // LiveActivityEventEnd ends a Live Activity. + LiveActivityEventEnd +) + // CriticalSound is the sound payload that can be included in an Aps. type CriticalSound struct { Critical bool `json:"-"` diff --git a/messaging/messaging_test.go b/messaging/messaging_test.go index dceed542..1e3762a0 100644 --- a/messaging/messaging_test.go +++ b/messaging/messaging_test.go @@ -48,6 +48,7 @@ var ( badgeZero = 0 timestampMillis = int64(12345) timestamp = time.Unix(0, 1546304523123*1000000).UTC() + unixTime = time.Unix(1546304, 0) ) var validMessages = []struct { @@ -588,6 +589,49 @@ var validMessages = []struct { "topic": "test-topic", }, }, + { + name: "APNSLiveActivity", + req: &Message{ + Token: "test-token", + APNS: &APNSConfig{ + LiveActivityToken: "live-activity-token", + Payload: &APNSPayload{ + Aps: &Aps{ + StaleDate: &unixTime, + ContentState: map[string]interface{}{ + "s1": "v", + "s2": float64(1), + }, + Timestamp: &unixTime, + Event: LiveActivityEventStart, + DismissalDate: &unixTime, + AttributesType: "TestAttributes", + Attributes: map[string]interface{}{ + "a1": "v", + "a2": float64(1), + }, + }, + }, + }, + }, + want: map[string]interface{}{ + "token": "test-token", + "apns": map[string]interface{}{ + "live_activity_token": "live-activity-token", + "payload": map[string]interface{}{ + "aps": map[string]interface{}{ + "stale-date": float64(unixTime.Unix()), + "content-state": map[string]interface{}{"s1": "v", "s2": float64(1)}, + "timestamp": float64(unixTime.Unix()), + "event": "start", + "dismissal-date": float64(unixTime.Unix()), + "attributes-type": "TestAttributes", + "attributes": map[string]interface{}{"a1": "v", "a2": float64(1)}, + }, + }, + }, + }, + }, { name: "AndroidNotificationPriorityMin", req: &Message{ From 548a22d0d4972847a6b4195b0794fd9b0f59d2e3 Mon Sep 17 00:00:00 2001 From: morikuni Date: Tue, 3 Jun 2025 09:20:14 +0900 Subject: [PATCH 2/2] Limit Live Activity support to LiveActivityToken field only As per review feedback, removed all extra Live Activity fields from Aps struct: - Removed StaleDate, ContentState, Timestamp, Event, DismissalDate, AttributesType, and Attributes fields - Removed LiveActivityEvent type and constants - Updated tests to only test LiveActivityToken field - Kept only the essential LiveActivityToken field in APNSConfig as requested --- messaging/messaging.go | 77 ------------------------------------- messaging/messaging_test.go | 29 -------------- 2 files changed, 106 deletions(-) diff --git a/messaging/messaging.go b/messaging/messaging.go index 8d27bbe0..dcdaafec 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -686,13 +686,6 @@ type Aps struct { MutableContent bool `json:"-"` Category string `json:"category,omitempty"` ThreadID string `json:"thread-id,omitempty"` - StaleDate *time.Time `json:"-"` - ContentState map[string]interface{} `json:"content-state,omitempty"` - Timestamp *time.Time `json:"-"` - Event LiveActivityEvent `json:"-"` - DismissalDate *time.Time `json:"-"` - AttributesType string `json:"attributes-type,omitempty"` - Attributes map[string]interface{} `json:"attributes,omitempty"` CustomData map[string]interface{} `json:"-"` } @@ -724,32 +717,6 @@ func (a *Aps) standardFields() map[string]interface{} { if a.ThreadID != "" { m["thread-id"] = a.ThreadID } - if a.StaleDate != nil { - m["stale-date"] = a.StaleDate.Unix() - } - if a.ContentState != nil { - m["content-state"] = a.ContentState - } - if a.Timestamp != nil { - m["timestamp"] = a.Timestamp.Unix() - } - if a.Event != liveActivityEventUnspecified { - events := map[LiveActivityEvent]string{ - LiveActivityEventStart: "start", - LiveActivityEventUpdate: "update", - LiveActivityEventEnd: "end", - } - m["event"] = events[a.Event] - } - if a.DismissalDate != nil { - m["dismissal-date"] = a.DismissalDate.Unix() - } - if a.AttributesType != "" { - m["attributes-type"] = a.AttributesType - } - if a.Attributes != nil { - m["attributes"] = a.Attributes - } return m } @@ -770,10 +737,6 @@ func (a *Aps) UnmarshalJSON(b []byte) error { SoundObject *json.RawMessage `json:"sound,omitempty"` ContentAvailableInt int `json:"content-available,omitempty"` MutableContentInt int `json:"mutable-content,omitempty"` - StaleDate *int64 `json:"stale-date,omitempty"` - Timestamp *int64 `json:"timestamp,omitempty"` - Event string `json:"event,omitempty"` - DismissalDate *int64 `json:"dismissal-date,omitempty"` *apsInternal }{ apsInternal: (*apsInternal)(a), @@ -799,30 +762,6 @@ func (a *Aps) UnmarshalJSON(b []byte) error { } } } - if temp.StaleDate != nil { - staleDate := time.Unix(*temp.StaleDate, 0) - a.StaleDate = &staleDate - } - if temp.Timestamp != nil { - timestamp := time.Unix(*temp.Timestamp, 0) - a.Timestamp = ×tamp - } - if temp.Event != "" { - events := map[string]LiveActivityEvent{ - "start": LiveActivityEventStart, - "update": LiveActivityEventUpdate, - "end": LiveActivityEventEnd, - } - if event, ok := events[temp.Event]; ok { - a.Event = event - } else { - return fmt.Errorf("unknown event value: %q", temp.Event) - } - } - if temp.DismissalDate != nil { - dismissalDate := time.Unix(*temp.DismissalDate, 0) - a.DismissalDate = &dismissalDate - } allFields := make(map[string]interface{}) if err := json.Unmarshal(b, &allFields); err != nil { @@ -837,22 +776,6 @@ func (a *Aps) UnmarshalJSON(b []byte) error { return nil } -// LiveActivityEvent represents an event type for Live Activity in push notification payloads. -type LiveActivityEvent int - -const ( - liveActivityEventUnspecified LiveActivityEvent = iota - - // LiveActivityEventStart starts a Live Activity. - LiveActivityEventStart - - // LiveActivityEventUpdate updates a Live Activity. - LiveActivityEventUpdate - - // LiveActivityEventEnd ends a Live Activity. - LiveActivityEventEnd -) - // CriticalSound is the sound payload that can be included in an Aps. type CriticalSound struct { Critical bool `json:"-"` diff --git a/messaging/messaging_test.go b/messaging/messaging_test.go index 1e3762a0..b17a800e 100644 --- a/messaging/messaging_test.go +++ b/messaging/messaging_test.go @@ -48,7 +48,6 @@ var ( badgeZero = 0 timestampMillis = int64(12345) timestamp = time.Unix(0, 1546304523123*1000000).UTC() - unixTime = time.Unix(1546304, 0) ) var validMessages = []struct { @@ -595,40 +594,12 @@ var validMessages = []struct { Token: "test-token", APNS: &APNSConfig{ LiveActivityToken: "live-activity-token", - Payload: &APNSPayload{ - Aps: &Aps{ - StaleDate: &unixTime, - ContentState: map[string]interface{}{ - "s1": "v", - "s2": float64(1), - }, - Timestamp: &unixTime, - Event: LiveActivityEventStart, - DismissalDate: &unixTime, - AttributesType: "TestAttributes", - Attributes: map[string]interface{}{ - "a1": "v", - "a2": float64(1), - }, - }, - }, }, }, want: map[string]interface{}{ "token": "test-token", "apns": map[string]interface{}{ "live_activity_token": "live-activity-token", - "payload": map[string]interface{}{ - "aps": map[string]interface{}{ - "stale-date": float64(unixTime.Unix()), - "content-state": map[string]interface{}{"s1": "v", "s2": float64(1)}, - "timestamp": float64(unixTime.Unix()), - "event": "start", - "dismissal-date": float64(unixTime.Unix()), - "attributes-type": "TestAttributes", - "attributes": map[string]interface{}{"a1": "v", "a2": float64(1)}, - }, - }, }, }, },