PyAPN向多个设备令牌发送推送通知不起作用

时间:2014-05-20 12:33:00

标签: python google-app-engine push-notification pyapns

我正在使用PyAPNs发送iOS推送通知。我还合并了修复以解决已知问题

https://github.com/djacobs/PyAPNs/issues/13

现在,代码工作正常如果我向单个设备发送通知。但我有一个设备令牌列表,我必须逐个向所有人发送通知。为此,我简单地循环遍历单个通知调用,如下所示:

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))

但问题是上面的代码不起作用。对于单独推送工作正常的设备令牌,使用上面的代码无法正常工作。例如,设备令牌45183e79de216ea05e3d6e83083476ebeb64caf733188bb77b0b1d268526c815单独工作正常但在批量发送时失败。作为参考,我将放置apns文件和部分服务器日志:

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))

服务器日志:

    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

1 个答案:

答案 0 :(得分:5)

增加到期时间对我来说是神奇的。

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))