Skip to content

Commit

Permalink
Merge branch 'sideshow:master' into feat/jwt-token-client-manager
Browse files Browse the repository at this point in the history
  • Loading branch information
defgenx authored Feb 17, 2025
2 parents 99b2710 + 22443bc commit 81c1f88
Show file tree
Hide file tree
Showing 11 changed files with 272 additions and 28 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
go: [1.16.x, 1.17.x, 1.18.x]
go: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x, 1.21.x, 1.22.x]
steps:
- uses: actions/checkout@v2

Expand All @@ -33,4 +33,4 @@ jobs:
- name: Update coverage
env:
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: goveralls -coverprofile=covprofile -service=github
run: goveralls -ignore=apns2/main.go -coverprofile=covprofile -service=github
6 changes: 3 additions & 3 deletions certificate/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"crypto/x509"
"encoding/pem"
"errors"
"io/ioutil"
"os"
"strings"

"golang.org/x/crypto/pkcs12"
Expand All @@ -29,7 +29,7 @@ var (
// Use "" as the password argument if the PKCS#12 certificate is not password
// protected.
func FromP12File(filename string, password string) (tls.Certificate, error) {
p12bytes, err := ioutil.ReadFile(filename)
p12bytes, err := os.ReadFile(filename)
if err != nil {
return tls.Certificate{}, err
}
Expand Down Expand Up @@ -62,7 +62,7 @@ func FromP12Bytes(bytes []byte, password string) (tls.Certificate, error) {
// Use "" as the password argument if the PEM certificate is not password
// protected.
func FromPemFile(filename string, password string) (tls.Certificate, error) {
bytes, err := ioutil.ReadFile(filename)
bytes, err := os.ReadFile(filename)
if err != nil {
return tls.Certificate{}, err
}
Expand Down
8 changes: 4 additions & 4 deletions certificate/certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package certificate_test
import (
"crypto/tls"
"errors"
"io/ioutil"
"os"
"testing"

"github.com/sideshow/apns2/certificate"
Expand All @@ -19,7 +19,7 @@ func TestValidCertificateFromP12File(t *testing.T) {
}

func TestValidCertificateFromP12Bytes(t *testing.T) {
bytes, _ := ioutil.ReadFile("_fixtures/certificate-valid.p12")
bytes, _ := os.ReadFile("_fixtures/certificate-valid.p12")
cer, err := certificate.FromP12Bytes(bytes, "")
assert.NoError(t, err)
assert.NotEqual(t, tls.Certificate{}, cer)
Expand Down Expand Up @@ -52,7 +52,7 @@ func TestValidCertificateFromPemFile(t *testing.T) {
}

func TestValidCertificateFromPemBytes(t *testing.T) {
bytes, _ := ioutil.ReadFile("_fixtures/certificate-valid.pem")
bytes, _ := os.ReadFile("_fixtures/certificate-valid.pem")
cer, err := certificate.FromPemBytes(bytes, "")
assert.NoError(t, err)
assert.NotEqual(t, tls.Certificate{}, cer)
Expand All @@ -65,7 +65,7 @@ func TestValidCertificateFromPemFileWithPKCS8PrivateKey(t *testing.T) {
}

func TestValidCertificateFromPemBytesWithPKCS8PrivateKey(t *testing.T) {
bytes, _ := ioutil.ReadFile("_fixtures/certificate-valid-pkcs8.pem")
bytes, _ := os.ReadFile("_fixtures/certificate-valid-pkcs8.pem")
cer, err := certificate.FromPemBytes(bytes, "")
assert.NoError(t, err)
assert.NotEqual(t, tls.Certificate{}, cer)
Expand Down
4 changes: 2 additions & 2 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ func (c *Client) PushWithContext(ctx Context, n *Notification) (*Response, error
r := &Response{}
r.StatusCode = response.StatusCode
r.ApnsID = response.Header.Get("apns-id")
r.ApnsUniqueID = response.Header.Get("apns-unique-id")

decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(r); err != nil && err != io.EOF {
Expand Down Expand Up @@ -226,13 +227,12 @@ func setHeaders(r *http.Request, n *Notification) {
if n.Priority > 0 {
r.Header.Set("apns-priority", strconv.Itoa(n.Priority))
}
if !n.Expiration.IsZero() {
if n.Expiration.After(time.Unix(0, 0)) {
r.Header.Set("apns-expiration", strconv.FormatInt(n.Expiration.Unix(), 10))
}
if n.PushType != "" {
r.Header.Set("apns-push-type", string(n.PushType))
} else {
r.Header.Set("apns-push-type", string(PushTypeAlert))
}

}
48 changes: 46 additions & 2 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"io"
"net"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -245,6 +245,25 @@ func TestHeaders(t *testing.T) {
assert.NoError(t, err)
}

func TestExpirationHeader(t *testing.T) {
n := mockNotification()
n.ApnsID = "84DB694F-464F-49BD-960A-D6DB028335C9"
n.CollapseID = "game1.start.identifier"
n.Topic = "com.testapp"
n.Priority = 10
n.Expiration = time.Unix(0, 0)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, n.ApnsID, r.Header.Get("apns-id"))
assert.Equal(t, n.CollapseID, r.Header.Get("apns-collapse-id"))
assert.Equal(t, "10", r.Header.Get("apns-priority"))
assert.Equal(t, n.Topic, r.Header.Get("apns-topic"))
assert.Equal(t, "", r.Header.Get("apns-expiration"))
}))
defer server.Close()
_, err := mockClient(server.URL).Push(n)
assert.NoError(t, err)
}

func TestPushTypeAlertHeader(t *testing.T) {
n := mockNotification()
n.PushType = apns.PushTypeAlert
Expand Down Expand Up @@ -322,6 +341,28 @@ func TestPushTypeMDMHeader(t *testing.T) {
assert.NoError(t, err)
}

func TestPushTypeLiveActivityHeader(t *testing.T) {
n := mockNotification()
n.PushType = apns.PushTypeLiveActivity
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "liveactivity", r.Header.Get("apns-push-type"))
}))
defer server.Close()
_, err := mockClient(server.URL).Push(n)
assert.NoError(t, err)
}

func TestPushTypePushToTalkHeader(t *testing.T) {
n := mockNotification()
n.PushType = apns.PushTypePushToTalk
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "pushtotalk", r.Header.Get("apns-push-type"))
}))
defer server.Close()
_, err := mockClient(server.URL).Push(n)
assert.NoError(t, err)
}

func TestAuthorizationHeader(t *testing.T) {
n := mockNotification()
token := mockToken()
Expand All @@ -340,7 +381,7 @@ func TestAuthorizationHeader(t *testing.T) {
func TestPayload(t *testing.T) {
n := mockNotification()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.Equal(t, n.Payload, body)
}))
Expand All @@ -359,16 +400,19 @@ func TestBadPayload(t *testing.T) {
func Test200SuccessResponse(t *testing.T) {
n := mockNotification()
var apnsID = "02ABC856-EF8D-4E49-8F15-7B8A61D978D6"
var apnsUniqueID = "A6739D99-D92A-424B-A91E-BF012365BD4E"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("apns-id", apnsID)
w.Header().Set("apns-unique-id", apnsUniqueID)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
res, err := mockClient(server.URL).Push(n)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, apnsID, res.ApnsID)
assert.Equal(t, apnsUniqueID, res.ApnsUniqueID)
assert.Equal(t, true, res.Sent())
}

Expand Down
14 changes: 14 additions & 0 deletions notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ const (
// contact the MDM server. If you set this push type, you must use the topic
// from the UID attribute in the subject of your MDM push certificate.
PushTypeMDM EPushType = "mdm"

// PushTypeLiveActivity is used for Live Activities that display various
// real-time information. If you set this push type, the topic field must
// use your app’s bundle ID with `push-type.liveactivity` appended to the end.
// The live activity push supports only token-based authentication. This
// push type is recommended for iOS. It is not available on macOS, tvOS,
// watchOS and iPadOS.
PushTypeLiveActivity EPushType = "liveactivity"

// PushTypePushToTalk is used for notifications that provide information about the
// push to talk. If you set this push type, the apns-topic header field
// must use your app’s bundle ID with.voip-ptt appended to the end.
// The pushtotalk push type isn’t available on watchOS, macOS, and tvOS. It’s recommended on iOS and iPadOS.
PushTypePushToTalk EPushType = "pushtotalk"
)

const (
Expand Down
127 changes: 116 additions & 11 deletions payload/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,41 @@ const (
InterruptionLevelCritical EInterruptionLevel = "critical"
)

// LiveActivityEvent defines the value for the payload aps event
type ELiveActivityEvent string

const (
// LiveActivityEventUpdate is used to update an live activity.
LiveActivityEventUpdate ELiveActivityEvent = "update"

// LiveActivityEventEnd is used to end an live activity.
LiveActivityEventEnd ELiveActivityEvent = "end"
)

// Payload represents a notification which holds the content that will be
// marshalled as JSON.
type Payload struct {
content map[string]interface{}
}

type aps struct {
Alert interface{} `json:"alert,omitempty"`
Badge interface{} `json:"badge,omitempty"`
Category string `json:"category,omitempty"`
ContentAvailable int `json:"content-available,omitempty"`
InterruptionLevel EInterruptionLevel `json:"interruption-level,omitempty"`
MutableContent int `json:"mutable-content,omitempty"`
RelevanceScore interface{} `json:"relevance-score,omitempty"`
Sound interface{} `json:"sound,omitempty"`
ThreadID string `json:"thread-id,omitempty"`
URLArgs []string `json:"url-args,omitempty"`
Alert interface{} `json:"alert,omitempty"`
Badge interface{} `json:"badge,omitempty"`
Category string `json:"category,omitempty"`
ContentAvailable int `json:"content-available,omitempty"`
InterruptionLevel EInterruptionLevel `json:"interruption-level,omitempty"`
MutableContent int `json:"mutable-content,omitempty"`
RelevanceScore interface{} `json:"relevance-score,omitempty"`
Sound interface{} `json:"sound,omitempty"`
ThreadID string `json:"thread-id,omitempty"`
URLArgs []string `json:"url-args,omitempty"`
ContentState map[string]interface{} `json:"content-state,omitempty"`
DismissalDate int64 `json:"dismissal-date,omitempty"`
StaleDate int64 `json:"stale-date,omitempty"`
Event ELiveActivityEvent `json:"event,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
AttributesType string `json:"attributes-type,omitempty"`
Attributes map[string]interface{} `json:"attributes,omitempty"`
}

type alert struct {
Expand All @@ -53,6 +71,8 @@ type alert struct {
Subtitle string `json:"subtitle,omitempty"`
TitleLocArgs []string `json:"title-loc-args,omitempty"`
TitleLocKey string `json:"title-loc-key,omitempty"`
SubtitleLocArgs []string `json:"subtitle-loc-args,omitempty"`
SubtitleLocKey string `json:"subtitle-loc-key,omitempty"`
SummaryArg string `json:"summary-arg,omitempty"`
SummaryArgCount int `json:"summary-arg-count,omitempty"`
}
Expand Down Expand Up @@ -81,6 +101,69 @@ func (p *Payload) Alert(alert interface{}) *Payload {
return p
}

// SetContentState sets the aps content-state on the payload.
// This will update content-state of live activity widget.
//
// {"aps":{"content-state": {} }}`
func (p *Payload) SetContentState(contentState map[string]interface{}) *Payload {
p.aps().ContentState = contentState
return p
}

// SetDismissalDate sets the aps dismissal-date on the payload.
// This will remove the live activity from the user's UI at the given timestamp.
//
// {"aps":{"dismissal-date": DismissalDate }}`
func (p *Payload) SetDismissalDate(dismissalDate int64) *Payload {
p.aps().DismissalDate = dismissalDate
return p
}

// SetStaleDate sets the aps stale-date on the payload.
// This will mark this live activity update as outdated at the given timestamp.
//
// {"aps":{"stale-date": StaleDate }}`
func (p *Payload) SetStaleDate(staleDate int64) *Payload {
p.aps().StaleDate = staleDate
return p
}

// SetEvent sets the aps event type on the payload.
// This can either be `LiveActivityEventUpdate` or `LiveActivityEventEnd`
//
// {"aps":{"event": Event }}`
func (p *Payload) SetEvent(event ELiveActivityEvent) *Payload {
p.aps().Event = event
return p
}

// SetTimestamp sets the aps timestamp on the payload.
// This will let live activity know when to update the stuff.
//
// {"aps":{"timestamp": Timestamp }}`
func (p *Payload) SetTimestamp(timestamp int64) *Payload {
p.aps().Timestamp = timestamp
return p
}

// SetAttributesType sets the aps attributes-type field on the payload.
// This is used for push-to-start live activities
//
// {"aps":{"attributes-type": attributesType }}`
func (p *Payload) SetAttributesType(attributesType string) *Payload {
p.aps().AttributesType = attributesType
return p
}

// SetAttributes sets the aps attributes field on the payload.
// This is used for push-to-start live activities
//
// {"aps":{"attributes": attributes }}`
func (p *Payload) SetAttributes(attributes map[string]interface{}) *Payload {
p.aps().Attributes = attributes
return p
}

// Badge sets the aps badge on the payload.
// This will display a numeric badge on the app icon.
//
Expand Down Expand Up @@ -193,6 +276,28 @@ func (p *Payload) AlertSubtitle(subtitle string) *Payload {
return p
}

// AlertSubtitleLocKey sets the aps alert subtitle localization key on the payload.
// This is the key to a subtitle string in the Localizable.strings file for the
// current localization. See Localized Formatted Strings in Apple documentation
// for more information.
//
// {"aps":{"alert":{"subtitle-loc-key":key}}}
func (p *Payload) AlertSubtitleLocKey(key string) *Payload {
p.aps().alert().SubtitleLocKey = key
return p
}

// AlertSubtitleLocArgs sets the aps alert subtitle localization args on the payload.
// These are the variable string values to appear in place of the format
// specifiers in subtitle-loc-key. See Localized Formatted Strings in Apple
// documentation for more information.
//
// {"aps":{"alert":{"title-loc-args":args}}}
func (p *Payload) AlertSubtitleLocArgs(args []string) *Payload {
p.aps().alert().SubtitleLocArgs = args
return p
}

// AlertBody sets the aps alert body on the payload.
// This is the text of the alert message.
//
Expand All @@ -218,7 +323,7 @@ func (p *Payload) AlertLaunchImage(image string) *Payload {
// specifiers in loc-key. See Localized Formatted Strings in Apple
// documentation for more information.
//
// {"aps":{"alert":{"loc-args":args}}}
// {"aps":{"alert":{"loc-args":args}}}
func (p *Payload) AlertLocArgs(args []string) *Payload {
p.aps().alert().LocArgs = args
return p
Expand Down
Loading

0 comments on commit 81c1f88

Please sign in to comment.