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
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.
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.
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.
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))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With