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