Skip to content
Browse files

support enhanced notification message with non-blocking socket and dr…

…op support for python prior to 2.6

stop using socket.ssl and simplejson
  • Loading branch information...
1 parent 54068eb commit 007bcbd121620465e54ed4379f999295bc3aec38 Josh Chung committed Jun 3, 2012
Showing with 128 additions and 22 deletions.
  1. +55 −4 README.markdown
  2. +73 −18 apns.py
View
59 README.markdown
@@ -1,16 +1,26 @@
# PyAPNs
A Python library for interacting with the Apple Push Notification service
-(APNs)
+(APNs) including enhanced notification format with non-blocking SSL connection.
+
+The original version of PyAPNs is written and maintained by Simon Whitaker (https://github.com/simonwhitaker/PyAPNs), which can be installed using easy_install
+
+ $ easy_install apns
+
+Enhanced notification format support using non-blocking SSL connection is added and maintained by Josh Ha-Nyung Chung
## Installation
-Either download the source from GitHub or use easy_install:
+Download the source from GitHub:
- $ easy_install apns
+ $ git clone git://github.com/minorblend/PyAPNs.git
+ $ cd PyAPNs
+ $ sudo python setup.py
## Sample usage
+### normal format
+
```python
from apns import APNs, Payload
@@ -41,13 +51,54 @@ of the Payload constructor.
payload = Payload(alert="Hello World!", custom={'sekrit_number':123})
```
+### enhanced format
+
+```python
+from apns import APNs, Payload
+
+apns = APNs(use_sandbox=True, cert_file='cert.pem', key_file='key.pem', enhanced=True)
+
+# Send a notification
+token_hex = 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b87'
+payload = Payload(alert="Hello World!", sound="default", badge=1)
+response = apns.gateway_server.send_notification(token_hex, payload)
+if response:
+ (status, identifier) = response
+ # do something to handle error
+ # response DOES NOT correspond to the notification message which is just to be sent.
+ # corresponding notification message can be matched by returned identifier
+ # APN connection is closed when APNs returns an error response. PyAPNs does not try to reconnect.
+
+# Get feedback messages
+for (token_hex, fail_time) in apns.feedback_server.items():
+ # do stuff with token_hex and fail_time
+```
+
+For more complicated alerts including custom buttons etc, use the PayloadAlert
+class. Example:
+
+```python
+alert = PayloadAlert("Hello world!", action_loc_key="Click me")
+payload = Payload(alert=alert, sound="default")
+```
+
+To send custom payload arguments, pass a dictionary to the custom kwarg
+of the Payload constructor.
+
+```python
+payload = Payload(alert="Hello World!", custom={'sekrit_number':123})
+```
+
## Further Info
[iOS Reference Library: Local and Push Notification Programming Guide][a1]
## Credits
-Written and maintained by Simon Whitaker at [Goo Software Ltd][goo].
+Originally written and maintained by Simon Whitaker at [Goo Software Ltd][goo].
+
+Enhanced format support is added and maintained by Josh Ha-Nyung Chung at [Sunnyloft][sunnyloft].
[a1]:http://developer.apple.com/iphone/library/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008194-CH1-SW1
[goo]:http://www.goosoftware.co.uk/
+[sunnyloft]:http://sunnyloft.com/
View
91 apns.py
@@ -28,22 +28,17 @@
from socket import socket, AF_INET, SOCK_STREAM
from struct import pack, unpack
-try:
- from ssl import wrap_socket
-except ImportError:
- from socket import ssl
-
-try:
- import json
-except ImportError:
- import simplejson as json
+import select
+import ssl
+import json
MAX_PAYLOAD_LENGTH = 256
+ERROR_RESPONSE_LENGTH = 6
class APNs(object):
"""A class representing an Apple Push Notification service connection"""
- def __init__(self, use_sandbox=False, cert_file=None, key_file=None):
+ def __init__(self, use_sandbox=False, cert_file=None, key_file=None, enhanced=False):
"""
Set use_sandbox to True to use the sandbox (test) APNs servers.
Default is False.
@@ -52,10 +47,18 @@ def __init__(self, use_sandbox=False, cert_file=None, key_file=None):
self.use_sandbox = use_sandbox
self.cert_file = cert_file
self.key_file = key_file
+ self.enhanced = enhanced
self._feedback_connection = None
self._gateway_connection = None
@staticmethod
+ def unpacked_byte_bigendian(byte):
+ """
+ Returns a byte from a packed big-endian (network) form
+ """
+ return unpack('>b', byte)[0]
+
+ @staticmethod
def packed_ushort_big_endian(num):
"""
Returns an unsigned short in packed big-endian (network) form
@@ -100,7 +103,8 @@ def gateway_server(self):
self._gateway_connection = GatewayConnection(
use_sandbox = self.use_sandbox,
cert_file = self.cert_file,
- key_file = self.key_file
+ key_file = self.key_file,
+ enhanced = self.enhanced
)
return self._gateway_connection
@@ -109,10 +113,11 @@ class APNsConnection(object):
"""
A generic connection class for communicating with the APNs
"""
- def __init__(self, cert_file=None, key_file=None):
+ def __init__(self, cert_file=None, key_file=None, enhanced=False):
super(APNsConnection, self).__init__()
self.cert_file = cert_file
self.key_file = key_file
+ self.enhanced = enhanced
self._socket = None
self._ssl = None
@@ -123,10 +128,23 @@ def _connect(self):
# Establish an SSL connection
self._socket = socket(AF_INET, SOCK_STREAM)
self._socket.connect((self.server, self.port))
- if wrap_socket:
- self._ssl = wrap_socket(self._socket, self.key_file, self.cert_file)
+ if self.enhanced:
+ self._socket.setblocking(0)
+ self._ssl = ssl.wrap_socket(self._socket, self.key_file, self.cert_file,
+ do_handshake_on_connect=False)
+ while True:
+ try:
+ self._ssl.do_handshake()
+ break
+ except ssl.SSLError, err:
+ if ssl.SSL_ERROR_WANT_READ == err.args[0]:
+ select.select([self._ssl], [], [])
+ elif ssl.SSL_ERROR_WANT_WRITE == err.args[0]:
+ select.select([], [self._ssl], [])
+ else:
+ raise
else:
- self._ssl = ssl(self._socket, self.key_file, self.cert_file)
+ self._ssl = ssl.wrap_socket(self._socket, self.key_file, self.cert_file)
def _disconnect(self):
if self._socket:
@@ -138,10 +156,27 @@ def _connection(self):
return self._ssl
def read(self, n=None):
+ if self.enhanced:
+ select.select([], [self._connection()], [])
+ return self._connection().read(n)
return self._connection().read(n)
def write(self, string):
- return self._connection().write(string)
+ if self.enhanced: # nonblocking socket
+ rlist, wlist, _ = select.select([self._connection()], [self._connection()], [])
+ if len(rlist) > 0: # there's error response from APNs
+ buff = self.read(ERROR_RESPONSE_LENGTH)
+ command = APNs.unpacked_byte_bigendian(buff[0])
+ if 8 != command: # not error response
+ return None
+ status = APNs.unpacked_byte_bigendian(buff[1])
+ identifier = APNs.unpacked_uint_big_endian(buff[2:])
+
+ return (status, identifier)
+ if len(wlist) > 0:
+ self._connection().write(string)
+ else: # blocking socket
+ return self._connection().write(string)
class PayloadAlert(object):
@@ -292,6 +327,26 @@ def _get_notification(self, token_hex, payload):
return notification
- def send_notification(self, token_hex, payload):
- self.write(self._get_notification(token_hex, payload))
+ def _get_enhanced_notification(self, token_hex, payload, identifier, expiry):
+ """
+ form notification data in an enhanced format
+ """
+ token_bin = a2b_hex(token_hex)
+ token_length_bin = APNs.packed_ushort_big_endian(len(token_bin))
+ payload_json = payload.json()
+ payload_length_bin = APNs.packed_ushort_big_endian(len(payload_json))
+ identifier_bin = APNs.packed_uint_big_endian(identifier)
+ expiry_bin = APNs.packed_uint_big_endian(expiry)
+ notification = ('\1' + identifier_bin + expiry_bin + token_length_bin + token_bin + payload_length_bin +
+ payload_json)
+ return notification
+
+ def send_notification(self, token_hex, payload, identifier=0, expiry=0):
+ """
+ in enhanced mode, send_notification may return error response from APNs if any
+ """
+ if self.enhanced:
+ return self.write(self._get_enhanced_notification(token_hex, payload, identifier, expiry))
+ else:
+ self.write(self._get_notification(token_hex, payload))

0 comments on commit 007bcbd

Please sign in to comment.
Something went wrong with that request. Please try again.