summaryrefslogtreecommitdiffstats
path: root/Godeps/_workspace/src/github.com/anachronistic
diff options
context:
space:
mode:
Diffstat (limited to 'Godeps/_workspace/src/github.com/anachronistic')
-rw-r--r--Godeps/_workspace/src/github.com/anachronistic/apns/.gitignore24
-rw-r--r--Godeps/_workspace/src/github.com/anachronistic/apns/LICENSE21
-rw-r--r--Godeps/_workspace/src/github.com/anachronistic/apns/client.go167
-rw-r--r--Godeps/_workspace/src/github.com/anachronistic/apns/client_mock.go21
-rw-r--r--Godeps/_workspace/src/github.com/anachronistic/apns/client_mock_test.go24
-rw-r--r--Godeps/_workspace/src/github.com/anachronistic/apns/feedback.go102
-rw-r--r--Godeps/_workspace/src/github.com/anachronistic/apns/legacy.go15
-rw-r--r--Godeps/_workspace/src/github.com/anachronistic/apns/legacy_test.go27
-rw-r--r--Godeps/_workspace/src/github.com/anachronistic/apns/mock_feedback_server.go53
-rw-r--r--Godeps/_workspace/src/github.com/anachronistic/apns/push_notification.go175
-rw-r--r--Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_response.go36
-rw-r--r--Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_test.go111
12 files changed, 776 insertions, 0 deletions
diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/.gitignore b/Godeps/_workspace/src/github.com/anachronistic/apns/.gitignore
new file mode 100644
index 000000000..d73587219
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/anachronistic/apns/.gitignore
@@ -0,0 +1,24 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+
+*.pem \ No newline at end of file
diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/LICENSE b/Godeps/_workspace/src/github.com/anachronistic/apns/LICENSE
new file mode 100644
index 000000000..b80ffbd8d
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/anachronistic/apns/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 Alan Harris
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE. \ No newline at end of file
diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/client.go b/Godeps/_workspace/src/github.com/anachronistic/apns/client.go
new file mode 100644
index 000000000..3fc079a87
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/anachronistic/apns/client.go
@@ -0,0 +1,167 @@
+package apns
+
+import (
+ "crypto/tls"
+ "errors"
+ "net"
+ "strings"
+ "time"
+)
+
+var _ APNSClient = &Client{}
+
+// APNSClient is an APNS client.
+type APNSClient interface {
+ ConnectAndWrite(resp *PushNotificationResponse, payload []byte) (err error)
+ Send(pn *PushNotification) (resp *PushNotificationResponse)
+}
+
+// Client contains the fields necessary to communicate
+// with Apple, such as the gateway to use and your
+// certificate contents.
+//
+// You'll need to provide your own CertificateFile
+// and KeyFile to send notifications. Ideally, you'll
+// just set the CertificateFile and KeyFile fields to
+// a location on drive where the certs can be loaded,
+// but if you prefer you can use the CertificateBase64
+// and KeyBase64 fields to store the actual contents.
+type Client struct {
+ Gateway string
+ CertificateFile string
+ CertificateBase64 string
+ KeyFile string
+ KeyBase64 string
+}
+
+// BareClient can be used to set the contents of your
+// certificate and key blocks manually.
+func BareClient(gateway, certificateBase64, keyBase64 string) (c *Client) {
+ c = new(Client)
+ c.Gateway = gateway
+ c.CertificateBase64 = certificateBase64
+ c.KeyBase64 = keyBase64
+ return
+}
+
+// NewClient assumes you'll be passing in paths that
+// point to your certificate and key.
+func NewClient(gateway, certificateFile, keyFile string) (c *Client) {
+ c = new(Client)
+ c.Gateway = gateway
+ c.CertificateFile = certificateFile
+ c.KeyFile = keyFile
+ return
+}
+
+// Send connects to the APN service and sends your push notification.
+// Remember that if the submission is successful, Apple won't reply.
+func (client *Client) Send(pn *PushNotification) (resp *PushNotificationResponse) {
+ resp = new(PushNotificationResponse)
+
+ payload, err := pn.ToBytes()
+ if err != nil {
+ resp.Success = false
+ resp.Error = err
+ return
+ }
+
+ err = client.ConnectAndWrite(resp, payload)
+ if err != nil {
+ resp.Success = false
+ resp.Error = err
+ return
+ }
+
+ resp.Success = true
+ resp.Error = nil
+
+ return
+}
+
+// ConnectAndWrite establishes the connection to Apple and handles the
+// transmission of your push notification, as well as waiting for a reply.
+//
+// In lieu of a timeout (which would be available in Go 1.1)
+// we use a timeout channel pattern instead. We start two goroutines,
+// one of which just sleeps for TimeoutSeconds seconds, while the other
+// waits for a response from the Apple servers.
+//
+// Whichever channel puts data on first is the "winner". As such, it's
+// possible to get a false positive if Apple takes a long time to respond.
+// It's probably not a deal-breaker, but something to be aware of.
+func (client *Client) ConnectAndWrite(resp *PushNotificationResponse, payload []byte) (err error) {
+ var cert tls.Certificate
+
+ if len(client.CertificateBase64) == 0 && len(client.KeyBase64) == 0 {
+ // The user did not specify raw block contents, so check the filesystem.
+ cert, err = tls.LoadX509KeyPair(client.CertificateFile, client.KeyFile)
+ } else {
+ // The user provided the raw block contents, so use that.
+ cert, err = tls.X509KeyPair([]byte(client.CertificateBase64), []byte(client.KeyBase64))
+ }
+
+ if err != nil {
+ return err
+ }
+
+ gatewayParts := strings.Split(client.Gateway, ":")
+ conf := &tls.Config{
+ Certificates: []tls.Certificate{cert},
+ ServerName: gatewayParts[0],
+ }
+
+ conn, err := net.Dial("tcp", client.Gateway)
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+
+ tlsConn := tls.Client(conn, conf)
+ err = tlsConn.Handshake()
+ if err != nil {
+ return err
+ }
+ defer tlsConn.Close()
+
+ _, err = tlsConn.Write(payload)
+ if err != nil {
+ return err
+ }
+
+ // Create one channel that will serve to handle
+ // timeouts when the notification succeeds.
+ timeoutChannel := make(chan bool, 1)
+ go func() {
+ time.Sleep(time.Second * TimeoutSeconds)
+ timeoutChannel <- true
+ }()
+
+ // This channel will contain the binary response
+ // from Apple in the event of a failure.
+ responseChannel := make(chan []byte, 1)
+ go func() {
+ buffer := make([]byte, 6, 6)
+ tlsConn.Read(buffer)
+ responseChannel <- buffer
+ }()
+
+ // First one back wins!
+ // The data structure for an APN response is as follows:
+ //
+ // command -> 1 byte
+ // status -> 1 byte
+ // identifier -> 4 bytes
+ //
+ // The first byte will always be set to 8.
+ select {
+ case r := <-responseChannel:
+ resp.Success = false
+ resp.AppleResponse = ApplePushResponses[r[1]]
+ err = errors.New(resp.AppleResponse)
+ case <-timeoutChannel:
+ resp.Success = true
+ }
+
+ return err
+}
diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/client_mock.go b/Godeps/_workspace/src/github.com/anachronistic/apns/client_mock.go
new file mode 100644
index 000000000..29a1f4b23
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/anachronistic/apns/client_mock.go
@@ -0,0 +1,21 @@
+package apns
+
+import "github.com/stretchr/testify/mock"
+
+type MockClient struct {
+ mock.Mock
+}
+
+func (m *MockClient) ConnectAndWrite(resp *PushNotificationResponse, payload []byte) (err error) {
+ return m.Called(resp, payload).Error(0)
+}
+
+func (m *MockClient) Send(pn *PushNotification) (resp *PushNotificationResponse) {
+ r := m.Called(pn).Get(0)
+ if r != nil {
+ if r, ok := r.(*PushNotificationResponse); ok {
+ return r
+ }
+ }
+ return nil
+}
diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/client_mock_test.go b/Godeps/_workspace/src/github.com/anachronistic/apns/client_mock_test.go
new file mode 100644
index 000000000..86e997b5a
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/anachronistic/apns/client_mock_test.go
@@ -0,0 +1,24 @@
+package apns
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMockClientConnectAndWrite(t *testing.T) {
+ m := &MockClient{}
+ m.On("ConnectAndWrite", (*PushNotificationResponse)(nil), []byte(nil)).Return(nil)
+ assert.Nil(t, m.ConnectAndWrite(nil, nil))
+ m.On("ConnectAndWrite", &PushNotificationResponse{}, []byte{}).Return(errors.New("test"))
+ assert.Equal(t, errors.New("test"), m.ConnectAndWrite(&PushNotificationResponse{}, []byte{}))
+}
+
+func TestMockClientSend(t *testing.T) {
+ m := &MockClient{}
+ m.On("Send", (*PushNotification)(nil)).Return(nil)
+ assert.Nil(t, m.Send(nil))
+ m.On("Send", &PushNotification{}).Return(&PushNotificationResponse{})
+ assert.Equal(t, &PushNotificationResponse{}, m.Send(&PushNotification{}))
+}
diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/feedback.go b/Godeps/_workspace/src/github.com/anachronistic/apns/feedback.go
new file mode 100644
index 000000000..32e7f0f15
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/anachronistic/apns/feedback.go
@@ -0,0 +1,102 @@
+package apns
+
+import (
+ "bytes"
+ "crypto/tls"
+ "encoding/binary"
+ "encoding/hex"
+ "errors"
+ "net"
+ "strings"
+ "time"
+)
+
+// Wait at most this many seconds for feedback data from Apple.
+const FeedbackTimeoutSeconds = 5
+
+// FeedbackChannel will receive individual responses from Apple.
+var FeedbackChannel = make(chan (*FeedbackResponse))
+
+// If there's nothing to read, ShutdownChannel gets a true.
+var ShutdownChannel = make(chan bool)
+
+// FeedbackResponse represents a device token that Apple has
+// indicated should not be sent to in the future.
+type FeedbackResponse struct {
+ Timestamp uint32
+ DeviceToken string
+}
+
+// NewFeedbackResponse creates and returns a FeedbackResponse structure.
+func NewFeedbackResponse() (resp *FeedbackResponse) {
+ resp = new(FeedbackResponse)
+ return
+}
+
+// ListenForFeedback connects to the Apple Feedback Service
+// and checks for device tokens.
+//
+// Feedback consists of device tokens that should
+// not be sent to in the future; Apple *does* monitor that
+// you respect this so you should be checking it ;)
+func (client *Client) ListenForFeedback() (err error) {
+ var cert tls.Certificate
+
+ if len(client.CertificateBase64) == 0 && len(client.KeyBase64) == 0 {
+ // The user did not specify raw block contents, so check the filesystem.
+ cert, err = tls.LoadX509KeyPair(client.CertificateFile, client.KeyFile)
+ } else {
+ // The user provided the raw block contents, so use that.
+ cert, err = tls.X509KeyPair([]byte(client.CertificateBase64), []byte(client.KeyBase64))
+ }
+
+ if err != nil {
+ return err
+ }
+
+ gatewayParts := strings.Split(client.Gateway, ":")
+ conf := &tls.Config{
+ Certificates: []tls.Certificate{cert},
+ ServerName: gatewayParts[0],
+ }
+
+ conn, err := net.Dial("tcp", client.Gateway)
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+ conn.SetReadDeadline(time.Now().Add(FeedbackTimeoutSeconds * time.Second))
+
+ tlsConn := tls.Client(conn, conf)
+ err = tlsConn.Handshake()
+ if err != nil {
+ return err
+ }
+
+ var tokenLength uint16
+ buffer := make([]byte, 38, 38)
+ deviceToken := make([]byte, 32, 32)
+
+ for {
+ _, err := tlsConn.Read(buffer)
+ if err != nil {
+ ShutdownChannel <- true
+ break
+ }
+
+ resp := NewFeedbackResponse()
+
+ r := bytes.NewReader(buffer)
+ binary.Read(r, binary.BigEndian, &resp.Timestamp)
+ binary.Read(r, binary.BigEndian, &tokenLength)
+ binary.Read(r, binary.BigEndian, &deviceToken)
+ if tokenLength != 32 {
+ return errors.New("token length should be equal to 32, but isn't")
+ }
+ resp.DeviceToken = hex.EncodeToString(deviceToken)
+
+ FeedbackChannel <- resp
+ }
+
+ return nil
+}
diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/legacy.go b/Godeps/_workspace/src/github.com/anachronistic/apns/legacy.go
new file mode 100644
index 000000000..44da3dde8
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/anachronistic/apns/legacy.go
@@ -0,0 +1,15 @@
+package apns
+
+// This file exists to support backward-compatibility
+// as I gradually refactor and overhaul. Ideally, golint
+// should only complain about this file (and we should
+// try to keep its complaints to a minimum).
+
+// These variables map old identifiers to their current format.
+var (
+ APPLE_PUSH_RESPONSES = ApplePushResponses
+ FEEDBACK_TIMEOUT_SECONDS = FeedbackTimeoutSeconds
+ IDENTIFIER_UBOUND = IdentifierUbound
+ MAX_PAYLOAD_SIZE_BYTES = MaxPayloadSizeBytes
+ TIMEOUT_SECONDS = TimeoutSeconds
+)
diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/legacy_test.go b/Godeps/_workspace/src/github.com/anachronistic/apns/legacy_test.go
new file mode 100644
index 000000000..4b983c128
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/anachronistic/apns/legacy_test.go
@@ -0,0 +1,27 @@
+package apns
+
+import (
+ "reflect"
+ "testing"
+)
+
+// These identifiers were changed to resolve golint violations.
+// However, it's possible that legacy code may rely on them. This
+// will help avoid springing a breaking change on people.
+func TestLegacyConstants(t *testing.T) {
+ if !reflect.DeepEqual(APPLE_PUSH_RESPONSES, ApplePushResponses) {
+ t.Error("expected APPLE_PUSH_RESPONSES to equal ApplePushResponses")
+ }
+ if !reflect.DeepEqual(FEEDBACK_TIMEOUT_SECONDS, FeedbackTimeoutSeconds) {
+ t.Error("expected FEEDBACK_TIMEOUT_SECONDS to equal FeedbackTimeoutSeconds")
+ }
+ if !reflect.DeepEqual(IDENTIFIER_UBOUND, IdentifierUbound) {
+ t.Error("expected IDENTIFIER_UBOUND to equal IdentifierUbound")
+ }
+ if !reflect.DeepEqual(MAX_PAYLOAD_SIZE_BYTES, MaxPayloadSizeBytes) {
+ t.Error("expected MAX_PAYLOAD_SIZE_BYTES to equal MaxPayloadSizeBytes")
+ }
+ if !reflect.DeepEqual(TIMEOUT_SECONDS, TimeoutSeconds) {
+ t.Error("expected TIMEOUT_SECONDS to equal TimeoutSeconds")
+ }
+}
diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/mock_feedback_server.go b/Godeps/_workspace/src/github.com/anachronistic/apns/mock_feedback_server.go
new file mode 100644
index 000000000..d7536f261
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/anachronistic/apns/mock_feedback_server.go
@@ -0,0 +1,53 @@
+package apns
+
+import (
+ "bytes"
+ "crypto/tls"
+ "encoding/binary"
+ "log"
+ "net"
+ "time"
+)
+
+// StartMockFeedbackServer spins up a simple stand-in for the Apple
+// feedback service that can be used for testing purposes. Doesn't
+// handle many errors, etc. Just for the sake of having something "live"
+// to hit.
+func StartMockFeedbackServer(certFile, keyFile string) {
+ cert, err := tls.LoadX509KeyPair(certFile, keyFile)
+ if err != nil {
+ log.Panic(err)
+ }
+ config := tls.Config{Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAnyClientCert}
+ log.Print("- starting Mock Apple Feedback TCP server at 0.0.0.0:5555")
+
+ srv, _ := tls.Listen("tcp", "0.0.0.0:5555", &config)
+ for {
+ conn, err := srv.Accept()
+ if err != nil {
+ log.Panic(err)
+ }
+ go loop(conn)
+ }
+}
+
+// Writes binary data to the client in the same
+// manner as the Apple service would.
+//
+// [4 bytes, 2 bytes, 32 bytes] = 38 bytes total
+func loop(conn net.Conn) {
+ defer conn.Close()
+ for {
+ timeT := uint32(1368809290) // 2013-05-17 12:48:10 -0400
+ token := "abcd1234efab5678abcd1234efab5678"
+
+ buf := new(bytes.Buffer)
+ binary.Write(buf, binary.BigEndian, timeT)
+ binary.Write(buf, binary.BigEndian, uint16(len(token)))
+ binary.Write(buf, binary.BigEndian, []byte(token))
+ conn.Write(buf.Bytes())
+
+ dur, _ := time.ParseDuration("1s")
+ time.Sleep(dur)
+ }
+}
diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification.go b/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification.go
new file mode 100644
index 000000000..e6b58d575
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification.go
@@ -0,0 +1,175 @@
+package apns
+
+import (
+ "bytes"
+ "encoding/binary"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "math/rand"
+ "strconv"
+ "time"
+)
+
+// Push commands always start with command value 2.
+const pushCommandValue = 2
+
+// Your total notification payload cannot exceed 2 KB.
+const MaxPayloadSizeBytes = 2048
+
+// Every push notification gets a pseudo-unique identifier;
+// this establishes the upper boundary for it. Apple will return
+// this identifier if there is an issue sending your notification.
+const IdentifierUbound = 9999
+
+// Constants related to the payload fields and their lengths.
+const (
+ deviceTokenItemid = 1
+ payloadItemid = 2
+ notificationIdentifierItemid = 3
+ expirationDateItemid = 4
+ priorityItemid = 5
+ deviceTokenLength = 32
+ notificationIdentifierLength = 4
+ expirationDateLength = 4
+ priorityLength = 1
+)
+
+// Payload contains the notification data for your request.
+//
+// Alert is an interface here because it supports either a string
+// or a dictionary, represented within by an AlertDictionary struct.
+type Payload struct {
+ Alert interface{} `json:"alert,omitempty"`
+ Badge int `json:"badge,omitempty"`
+ Sound string `json:"sound,omitempty"`
+ ContentAvailable int `json:"content-available,omitempty"`
+ Category string `json:"category,omitempty"`
+}
+
+// NewPayload creates and returns a Payload structure.
+func NewPayload() *Payload {
+ return new(Payload)
+}
+
+// AlertDictionary is a more complex notification payload.
+//
+// From the APN docs: "Use the ... alert dictionary in general only if you absolutely need to."
+// The AlertDictionary is suitable for specific localization needs.
+type AlertDictionary struct {
+ Body string `json:"body,omitempty"`
+ ActionLocKey string `json:"action-loc-key,omitempty"`
+ LocKey string `json:"loc-key,omitempty"`
+ LocArgs []string `json:"loc-args,omitempty"`
+ LaunchImage string `json:"launch-image,omitempty"`
+}
+
+// NewAlertDictionary creates and returns an AlertDictionary structure.
+func NewAlertDictionary() *AlertDictionary {
+ return new(AlertDictionary)
+}
+
+// PushNotification is the wrapper for the Payload.
+// The length fields are computed in ToBytes() and aren't represented here.
+type PushNotification struct {
+ Identifier int32
+ Expiry uint32
+ DeviceToken string
+ payload map[string]interface{}
+ Priority uint8
+}
+
+// NewPushNotification creates and returns a PushNotification structure.
+// It also initializes the pseudo-random identifier.
+func NewPushNotification() (pn *PushNotification) {
+ pn = new(PushNotification)
+ pn.payload = make(map[string]interface{})
+ pn.Identifier = rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(IdentifierUbound)
+ pn.Priority = 10
+ return
+}
+
+// AddPayload sets the "aps" payload section of the request. It also
+// has a hack described within to deal with specific zero values.
+func (pn *PushNotification) AddPayload(p *Payload) {
+ // This deserves some explanation.
+ //
+ // Setting an exported field of type int to 0
+ // triggers the omitempty behavior if you've set it.
+ // Since the badge is optional, we should omit it if
+ // it's not set. However, we want to include it if the
+ // value is 0, so there's a hack in push_notification.go
+ // that exploits the fact that Apple treats -1 for a
+ // badge value as though it were 0 (i.e. it clears the
+ // badge but doesn't stop the notification from going
+ // through successfully.)
+ //
+ // Still a hack though :)
+ if p.Badge == 0 {
+ p.Badge = -1
+ }
+ pn.Set("aps", p)
+}
+
+// Get returns the value of a payload key, if it exists.
+func (pn *PushNotification) Get(key string) interface{} {
+ return pn.payload[key]
+}
+
+// Set defines the value of a payload key.
+func (pn *PushNotification) Set(key string, value interface{}) {
+ pn.payload[key] = value
+}
+
+// PayloadJSON returns the current payload in JSON format.
+func (pn *PushNotification) PayloadJSON() ([]byte, error) {
+ return json.Marshal(pn.payload)
+}
+
+// PayloadString returns the current payload in string format.
+func (pn *PushNotification) PayloadString() (string, error) {
+ j, err := pn.PayloadJSON()
+ return string(j), err
+}
+
+// ToBytes returns a byte array of the complete PushNotification
+// struct. This array is what should be transmitted to the APN Service.
+func (pn *PushNotification) ToBytes() ([]byte, error) {
+ token, err := hex.DecodeString(pn.DeviceToken)
+ if err != nil {
+ return nil, err
+ }
+ if len(token) != deviceTokenLength {
+ return nil, errors.New("device token has incorrect length")
+ }
+ payload, err := pn.PayloadJSON()
+ if err != nil {
+ return nil, err
+ }
+ if len(payload) > MaxPayloadSizeBytes {
+ return nil, errors.New("payload is larger than the " + strconv.Itoa(MaxPayloadSizeBytes) + " byte limit")
+ }
+
+ frameBuffer := new(bytes.Buffer)
+ binary.Write(frameBuffer, binary.BigEndian, uint8(deviceTokenItemid))
+ binary.Write(frameBuffer, binary.BigEndian, uint16(deviceTokenLength))
+ binary.Write(frameBuffer, binary.BigEndian, token)
+ binary.Write(frameBuffer, binary.BigEndian, uint8(payloadItemid))
+ binary.Write(frameBuffer, binary.BigEndian, uint16(len(payload)))
+ binary.Write(frameBuffer, binary.BigEndian, payload)
+ binary.Write(frameBuffer, binary.BigEndian, uint8(notificationIdentifierItemid))
+ binary.Write(frameBuffer, binary.BigEndian, uint16(notificationIdentifierLength))
+ binary.Write(frameBuffer, binary.BigEndian, pn.Identifier)
+ binary.Write(frameBuffer, binary.BigEndian, uint8(expirationDateItemid))
+ binary.Write(frameBuffer, binary.BigEndian, uint16(expirationDateLength))
+ binary.Write(frameBuffer, binary.BigEndian, pn.Expiry)
+ binary.Write(frameBuffer, binary.BigEndian, uint8(priorityItemid))
+ binary.Write(frameBuffer, binary.BigEndian, uint16(priorityLength))
+ binary.Write(frameBuffer, binary.BigEndian, pn.Priority)
+
+ buffer := bytes.NewBuffer([]byte{})
+ binary.Write(buffer, binary.BigEndian, uint8(pushCommandValue))
+ binary.Write(buffer, binary.BigEndian, uint32(frameBuffer.Len()))
+ binary.Write(buffer, binary.BigEndian, frameBuffer.Bytes())
+ return buffer.Bytes(), nil
+}
diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_response.go b/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_response.go
new file mode 100644
index 000000000..f08dc06e4
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_response.go
@@ -0,0 +1,36 @@
+package apns
+
+// The maximum number of seconds we're willing to wait for a response
+// from the Apple Push Notification Service.
+const TimeoutSeconds = 5
+
+// This enumerates the response codes that Apple defines
+// for push notification attempts.
+var ApplePushResponses = map[uint8]string{
+ 0: "NO_ERRORS",
+ 1: "PROCESSING_ERROR",
+ 2: "MISSING_DEVICE_TOKEN",
+ 3: "MISSING_TOPIC",
+ 4: "MISSING_PAYLOAD",
+ 5: "INVALID_TOKEN_SIZE",
+ 6: "INVALID_TOPIC_SIZE",
+ 7: "INVALID_PAYLOAD_SIZE",
+ 8: "INVALID_TOKEN",
+ 10: "SHUTDOWN",
+ 255: "UNKNOWN",
+}
+
+// PushNotificationResponse details what Apple had to say, if anything.
+type PushNotificationResponse struct {
+ Success bool
+ AppleResponse string
+ Error error
+}
+
+// NewPushNotificationResponse creates and returns a new PushNotificationResponse
+// structure; it defaults to being unsuccessful at first.
+func NewPushNotificationResponse() (resp *PushNotificationResponse) {
+ resp = new(PushNotificationResponse)
+ resp.Success = false
+ return
+}
diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_test.go b/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_test.go
new file mode 100644
index 000000000..a17b1c833
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_test.go
@@ -0,0 +1,111 @@
+package apns
+
+import (
+ "testing"
+)
+
+const testDeviceToken = "e93b7686988b4b5fd334298e60e73d90035f6d12628a80b4029bde0dec514df9"
+
+// Create a new Payload that specifies simple text,
+// a badge counter, and a custom notification sound.
+func mockPayload() (payload *Payload) {
+ payload = NewPayload()
+ payload.Alert = "You have mail!"
+ payload.Badge = 42
+ payload.Sound = "bingbong.aiff"
+ return
+}
+
+// See the commentary in push_notification.go for information
+// on why we're testing a badge of value 0.
+func mockZeroBadgePayload() (payload *Payload) {
+ payload = mockPayload()
+ payload.Badge = 0
+ return
+}
+
+// Create a new AlertDictionary. Apple recommends you not use
+// the more complex alert style unless absolutely necessary.
+func mockAlertDictionary() (dict *AlertDictionary) {
+ args := make([]string, 1)
+ args[0] = "localized args"
+
+ dict = NewAlertDictionary()
+ dict.Body = "Complex Message"
+ dict.ActionLocKey = "Play a Game!"
+ dict.LocKey = "localized key"
+ dict.LocArgs = args
+ dict.LaunchImage = "image.jpg"
+ return
+}
+
+func TestBasicAlert(t *testing.T) {
+ payload := mockPayload()
+ pn := NewPushNotification()
+
+ pn.DeviceToken = testDeviceToken
+ pn.AddPayload(payload)
+
+ bytes, _ := pn.ToBytes()
+ json, _ := pn.PayloadJSON()
+ if len(bytes) != 130 {
+ t.Error("expected 130 bytes; got", len(bytes))
+ }
+ if len(json) != 69 {
+ t.Error("expected 69 bytes; got", len(json))
+ }
+}
+
+func TestAlertDictionary(t *testing.T) {
+ dict := mockAlertDictionary()
+ payload := mockPayload()
+ payload.Alert = dict
+
+ pn := NewPushNotification()
+ pn.DeviceToken = testDeviceToken
+ pn.AddPayload(payload)
+
+ bytes, _ := pn.ToBytes()
+ json, _ := pn.PayloadJSON()
+ if len(bytes) != 255 {
+ t.Error("expected 255 bytes; got", len(bytes))
+ }
+ if len(json) != 194 {
+ t.Error("expected 194 bytes; got", len(bytes))
+ }
+}
+
+func TestCustomParameters(t *testing.T) {
+ payload := mockPayload()
+ pn := NewPushNotification()
+
+ pn.DeviceToken = testDeviceToken
+ pn.AddPayload(payload)
+ pn.Set("foo", "bar")
+
+ if pn.Get("foo") != "bar" {
+ t.Error("unable to set a custom property")
+ }
+ if pn.Get("not_set") != nil {
+ t.Error("expected a missing key to return nil")
+ }
+
+ bytes, _ := pn.ToBytes()
+ json, _ := pn.PayloadJSON()
+ if len(bytes) != 142 {
+ t.Error("expected 110 bytes; got", len(bytes))
+ }
+ if len(json) != 81 {
+ t.Error("expected 81 bytes; got", len(json))
+ }
+}
+
+func TestZeroBadgeChangesToNegativeOne(t *testing.T) {
+ payload := mockZeroBadgePayload()
+ pn := NewPushNotification()
+ pn.AddPayload(payload)
+
+ if payload.Badge != -1 {
+ t.Error("expected 0 badge value to be converted to -1; got", payload.Badge)
+ }
+}