Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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 <[email protected]>
# 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
like image 951
gmuhammad Avatar asked May 20 '14 12:05

gmuhammad


People also ask

What is device token in push notification?

The push notification networks identify each device by device token. A device token is not a device IMEI but an ID to identify a certain mobile app on a certain device. It is issued by calling libraries of FCM, JPush, or APNs.

How do push tokens work?

A Push Token is an advanced technology for an easy-to-use and secure multifactor authentication (MFA): When a user tries to access protected content or initiates a transaction, a push notification is sent to the users registered mobile device, for instance a smartphone.

What is APNs token?

Apple Push Notification service (APNs) must know the address of a user's device before it can send notifications to that device. This address takes the form of a device token unique to both the device and your app.


1 Answers

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))
like image 189
gmuhammad Avatar answered Oct 16 '22 18:10

gmuhammad