PyAPNs sending push notification to more than one device token not working

google-app-engine, push-notification, pyapns, python

Solution

Increasing the expiry time did the magic for me.

def send_notifications(self, tokens, payload):
    for token in tokens:
        try :
            logging.info("Sending Notification to Token: %s" % (token))
            self.send_notification(token, payload, identifier=None, expiry = (datetime.utcnow() + timedelta(300)))                
        except Exception, e:
            self._disconnect()
            logging.info("Exception: %s" % (str(e)))
            logging.info("Token: %s" % (token))

Problem

I am using PyAPNs for sending iOS Push Notification. I also merged the fixes for following known issues https://github.com/djacobs/PyAPNs/issues/13 Now, the code is working fine If I send notification to an individual device. But I have a list for device tokens and I have to send notification to all of them one by one. For this purpose I am simple looping over the single notification call like this: ``` def send_notifications(self, tokens, payload): for token in tokens: try : logging.info("Sending Notification to Token: %s" % (token)) self.send_notification(token, payload) except Exception, e: self._disconnect() logging.info("Exception: %s" % (str(e))) logging.info("Token: %s" % (token)) ``` But the problem is that above code is not working. The device token which was working fine for individual push, is not working using above code. For example device token 45183e79de216ea05e3d6e83083476ebeb64caf733188bb77b0b1d268526c815 is working fine individually but failing in case of bulk send. For reference I am putting the apns file and partial server logs: apns.py ``` # PyAPNs was developed by Simon Whitaker <simon@goosoftware.co.uk> # Source available at https://github.com/simonwhitaker/PyAPNs # # PyAPNs is distributed under the terms of the MIT license. # # Copyright (c) 2011 Goo Software Ltd # # 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. from binascii import a2b_hex, b2a_hex from datetime import datetime, timedelta from time import mktime from socket import socket, AF_INET, SOCK_STREAM, timeout from struct import pack, unpack import select try: from ssl import wrap_socket from ssl import SSLError, SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE except ImportError: from socket import ssl as wrap_socket try: import json except ImportError: import simplejson as json from apnserrors import * import logging import StringIO MAX_PAYLOAD_LENGTH = 256 TIMEOUT = 60 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, enhanced=True): """ Set use_sandbox to True to use the sandbox (test) APNs servers. Default is False. """ super(APNs, self).__init__() 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_uchar_big_endian(byte): """ Returns an unsigned char from a packed big-endian (network) byte """ return unpack('>B', byte)[0] @staticmethod def packed_ushort_big_endian(num): """ Returns an unsigned short in packed big-endian (network) form """ return pack('>H', num) @staticmethod def unpacked_ushort_big_endian(bytes): """ Returns an unsigned short from a packed big-endian (network) byte array """ return unpack('>H', bytes)[0] @staticmethod def packed_uint_big_endian(num): """ Returns an unsigned int in packed big-endian (network) form """ return pack('>I', num) @staticmethod def unpacked_uint_big_endian(bytes): """ Returns an unsigned int from a packed big-endian (network) byte array """ return unpack('>I', bytes)[0] @property def feedback_server(self): if not self._feedback_connection: self._feedback_connection = FeedbackConnection( use_sandbox = self.use_sandbox, cert_file = self.cert_file, key_file = self.key_file ) return self._feedback_connection @property def gateway_server(self): if not self._gateway_connection: self._gateway_connection = GatewayConnection( use_sandbox = self.use_sandbox, cert_file = self.cert_file, key_file = self.key_file, enhanced = self.enhanced ) return self._gateway_connection class APNsConnection(object): """ A generic connection class for communicating with the APNs """ def __init__(self, cert_file=None, key_file=None, enhanced=True): super(APNsConnection, self).__init__() self.cert_file = cert_file self.key_file = key_file self.enhanced = enhanced self._socket = None self._ssl = None def __del__(self): self._disconnect(); def _connect(self): # Establish an SSL connection self._socket = socket(AF_INET, SOCK_STREAM) self._socket.connect((self.server, self.port)) if self.enhanced: self._ssl = wrap_socket(self._socket, StringIO.StringIO(self.key_file), StringIO.StringIO(self.cert_file), do_handshake_on_connect=False) self._ssl.setblocking(0) while True: try: self._ssl.do_handshake() break except SSLError, err: if SSL_ERROR_WANT_READ == err.args[0]: select.select([self._ssl], [], []) elif SSL_ERROR_WANT_WRITE == err.args[0]: select.select([], [self._ssl], []) else: raise else: self._ssl = wrap_socket(self._socket, StringIO.StringIO(self.key_file), StringIO.StringIO(self.cert_file)) def _disconnect(self): if self._socket: self._socket.close() self._ssl = None def _connection(self): if not self._ssl: self._connect() return self._ssl def read(self, n=None): return self._connection().recv(n) def recvall(self, n): data = "" while True: more = self._connection().recv(n - len(data)) data += more if len(data) >= n: break rlist, _, _ = select.select([self._connection()], [], [], TIMEOUT) if not rlist: raise timeout return data def write(self, string): if self.enhanced: # nonblocking socket rlist, _, _ = select.select([self._connection()], [], [], 0) if rlist: # there's error response from APNs buff = self.recvall(ERROR_RESPONSE_LENGTH) if len(buff) != ERROR_RESPONSE_LENGTH: return None command = APNs.unpacked_uchar_big_endian(buff[0]) if 8 != command: self._disconnect() raise UnknownError(0) status = APNs.unpacked_uchar_big_endian(buff[1]) identifier = APNs.unpacked_uint_big_endian(buff[2:6]) self._disconnect() raise { 1: ProcessingError, 2: MissingDeviceTokenError, 3: MissingTopicError, 4: MissingPayloadError, 5: InvalidTokenSizeError, 6: InvalidTopicSizeError, 7: InvalidPayloadSizeError, 8: InvalidTokenError }.get(status, UnknownError)(identifier) _, wlist, _ = select.select([], [self._connection()], [], TIMEOUT) if wlist: return self._connection().sendall(string) else: self._disconnect() raise timeout else: # not-enhanced format using blocking socket return self._connection().sendall(string) class PayloadAlert(object): def __init__(self, body, action_loc_key=None, loc_key=None, loc_args=None, launch_image=None): super(PayloadAlert, self).__init__() self.body = body self.action_loc_key = action_loc_key self.loc_key = loc_key self.loc_args = loc_args self.launch_image = launch_image def dict(self): d = { 'body': self.body } if self.action_loc_key: d['action-loc-key'] = self.action_loc_key if self.loc_key: d['loc-key'] = self.loc_key if self.loc_args: d['loc-args'] = self.loc_args if self.launch_image: d['launch-image'] = self.launch_image return d class Payload(object): """A class representing an APNs message payload""" def __init__(self, alert=None, badge=None, sound=None, custom={}): super(Payload, self).__init__() self.alert = alert self.badge = badge self.sound = sound self.custom = custom self._check_size() def dict(self): """Returns the payload as a regular Python dictionary""" d = {} if self.alert: # Alert can be either a string or a PayloadAlert # object if isinstance(self.alert, PayloadAlert): d['alert'] = self.alert.dict() else: d['alert'] = self.alert if self.sound: d['sound'] = self.sound if self.badge is not None: d['badge'] = int(self.badge) d = { 'aps': d } d.update(self.custom) return d def json(self): return json.dumps(self.dict(), separators=(',',':'), ensure_ascii=False).encode('utf-8') def _check_size(self): if len(self.json()) > MAX_PAYLOAD_LENGTH: raise PayloadTooLargeError() def __repr__(self): attrs = ("alert", "badge", "sound", "custom") args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs]) return "%s(%s)" % (self.__class__.__name__, args) class FeedbackConnection(APNsConnection): """ A class representing a connection to the APNs Feedback server """ def __init__(self, use_sandbox=False, **kwargs): super(FeedbackConnection, self).__init__(**kwargs) self.server = ( 'feedback.push.apple.com', 'feedback.sandbox.push.apple.com')[use_sandbox] self.port = 2196 def _chunks(self): BUF_SIZE = 4096 while 1: data = self.read(BUF_SIZE) yield data if not data: break def items(self): """ A generator that yields (token_hex, fail_time) pairs retrieved from the APNs feedback server """ buff = '' for chunk in self._chunks(): buff += chunk # Quit if there's no more data to read if not buff: break # Sanity check: after a socket read we should always have at least # 6 bytes in the buffer if len(buff) < 6: break while len(buff) > 6: token_length = APNs.unpacked_ushort_big_endian(buff[4:6]) bytes_to_read = 6 + token_length if len(buff) >= bytes_to_read: fail_time_unix = APNs.unpacked_uint_big_endian(buff[0:4]) fail_time = datetime.utcfromtimestamp(fail_time_unix) token = b2a_hex(buff[6:bytes_to_read]) yield (token, fail_time) # Remove data for current token from buffer buff = buff[bytes_to_read:] else: # break out of inner while loop - i.e. go and fetch # some more data and append to buffer break class GatewayConnection(APNsConnection): """ A class that represents a connection to the APNs gateway server """ def __init__(self, use_sandbox=False, **kwargs): super(GatewayConnection, self).__init__(**kwargs) self.server = ( 'gateway.push.apple.com', 'gateway.sandbox.push.apple.com')[use_sandbox] self.port = 2195 def _get_notification(self, token_hex, payload): """ Takes a token as a hex string and a payload as a Python dict and sends the notification """ 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)) notification = ('\0' + token_length_bin + token_bin + payload_length_bin + payload_json) return notification def _get_enhanced_notification(self, token_hex, payload, identifier, expiry): """ Takes a token as a hex string and a payload as a Python dict and sends the notification in the 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(int(mktime(expiry.timetuple()))) 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=None, expiry=None): if self.enhanced: if not expiry: # by default, undelivered notification expires after 30 seconds expiry = datetime.utcnow() + timedelta(30) if not identifier: identifier = 0 logging.info("self.write(self._get_enhanced_notification())") self.write(self._get_enhanced_notification(token_hex, payload, identifier, expiry)) else: logging.info("self.write(self._get_notification(token_hex, payload))") self.write(self._get_notification(token_hex, payload)) def send_notifications(self, tokens, payload): for token in tokens: try : logging.info("Sending Notification to Token: %s" % (token)) self.send_notification(token, payload) except Exception, e: self._disconnect() logging.info("Exception: %s" % (str(e))) logging.info("Token: %s" % (token)) ``` Server Logs: ``` Sending Notification to Token: 99f65209a76ed41ce50c73198d72048f94085dd2a2dde0245110dccccda86fd0 I 2014-05-20 05:18:24.029 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:24.437 Sending Notification to Token: 2230c2421e3b83cd6b16a69c6ba528230b11d29183b0bfb73b159816237b17ce I 2014-05-20 05:18:24.437 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:24.442 . . . Sending Notification to Token: 6bd80feb5158a8f92537955c93acd1661242c007dcffebe55e77bb38cafef0ba I 2014-05-20 05:18:24.986 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:24.991 Sending Notification to Token: 2230c2421e3b83cd6b16a69c6ba528230b11d29183b0bfb73b159816237b17ce I 2014-05-20 05:18:24.991 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:24.996 Sending Notification to Token: 6bd80feb5158a8f92537955c93acd1661242c007dcffebe55e77bb38cafef0ba I 2014-05-20 05:18:24.996 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:25.004 Sending Notification to Token: 1bacfcb6b80868493b236ec6131bed11918c935752734701b89b060045e6b006 I 2014-05-20 05:18:25.004 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:25.021 Sending Notification to Token: 35bd8dda849e30a85b12b2a0e274b9507db7c7f365aa5a27f3fbda316052246e I 2014-05-20 05:18:25.021 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:25.054 Sending Notification to Token: 6bd80feb5158a8f92537955c93acd1661242c007dcffebe55e77bb38cafef0ba I 2014-05-20 05:18:25.054 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:25.059 Sending Notification to Token: 6bd80feb5158a8f92537955c93acd1661242c007dcffebe55e77bb38cafef0ba I 2014-05-20 05:18:25.059 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:25.064 Sending Notification to Token: 1bacfcb6b80868493b236ec6131bed11918c935752734701b89b060045e6b006 I 2014-05-20 05:18:25.064 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:25.068 Sending Notification to Token: 6bd80feb5158a8f92537955c93acd1661242c007dcffebe55e77bb38cafef0ba I 2014-05-20 05:18:25.069 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:25.073 Sending Notification to Token: d25a34a1fd031abf3fbfb5916af415206048fb6343586b91b96d0506eb28cb54 I 2014-05-20 05:18:25.073 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:25.078 . . . Sending Notification to Token: 45183e79de216ea05e3d6e83083476ebeb64caf733188bb77b0b1d268526c815 I 2014-05-20 05:18:30.145 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:30.152 Sending Notification to Token: b57b2d96a4b4db552137bcea4fd58f3ce53393fbe7c828b617306df2922dbfd3 I 2014-05-20 05:18:30.152 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:30.159 Sending Notification to Token: 82acbf3dc5da893d2f4d551df10c129c8c192efe335cc608d291dc922e947615 I 2014-05-20 05:18:30.159 self.write(self._get_enhanced_notification()) I 2014-05-20 05:18:30.166 feedback token_hex: 0cf58d47f435f170473b63e1852b637c11935b6e38d41321fe98911eaf898301 I 2014-05-20 05:18:31.754 feedback token_hex: 0d344046d62f808c30bc5670cbb7dc478cca0a9798830d22f8f6ed27c76923c6 I 2014-05-20 05:18:31.754 feedback token_hex: 2230c2421e3b83cd6b16a69c6ba528230b11d29183b0bfb73b159816237b17ce I 2014-05-20 05:18:31.754 feedback token_hex: 349c54d18bb1ee014dc84f7b7b60c4a2eef1b9d3cf51c12daab93261d5e09e7c I 2014-05-20 05:18:31.754 feedback token_hex: 3980924c6cd4e752f2a02b8d28f7ce11d7a3eba5f41628166733cda4e621bfcf I 2014-05-20 05:18:31.755 feedback token_hex: 6bd80feb5158a8f92537955c93acd1661242c007dcffebe55e77bb38cafef0ba I 2014-05-20 05:18:31.755 feedback token_hex: b96e27adab644f0a18e8f4dfe19786aab82b69e1ef46c580b887e6779964c55f I 2014-05-20 05:18:31.755 feedback token_hex: e5ee1848342d2e4789cfa07baae3ac754785d78ccb50dc5b5f10044053843115 I 2014-05-20 05:18:31.755 feedback token_hex: f339e53e44efa03996dffc24b5c9419609018fd8dd5d1953230a4bd8c5cabc78 I 2014-05-20 05:18:31.760 feedback fail_count: 9 ```

Original source