2014-05-20 407 views
7

我使用PyAPNs發送iOS推送通知。我還合併了修復PyAPN發送推送通知到多個設備令牌不起作用

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

服務器日誌:

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 

回答

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)) 
相關問題