diff --git a/README.markdown b/README.markdown index a42c3ad..f346e40 100644 --- a/README.markdown +++ b/README.markdown @@ -36,6 +36,23 @@ frame.add_item('b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b87', paylo apns.gateway_server.send_notification_multiple(frame) ``` +## HTTP/2 sample usage + +The legacy binary interface above is deprecated. The `apns2.py` module provides a +minimal client that uses Apple's modern HTTP/2 provider API. + +```python +from apns2 import APNsHTTP2 +from apns import Payload +import json + +client = APNsHTTP2(use_sandbox=True, cert_file='cert.pem', key_file='key.pem') +payload = Payload(alert="Hello HTTP/2") +client.send_notification( + 'device_token_hex', json.loads(payload.json()), topic='com.example.App' +) +``` + Apple recommends to query the feedback service daily to get the list of device tokens. You need to create a new connection to APNS to see all the tokens that have failed since you only receive that information upon connection. Remember, once you have viewed the list of tokens, Apple will clear the list from their servers. Use the timestamp to verify that the device tokens haven’t been reregistered since the feedback entry was generated. For each device that has not been reregistered, stop sending notifications. By using this information to stop sending push notifications that will fail to be delivered, you reduce unnecessary message overhead and improve overall system performance. ``` diff --git a/apns-send-http2 b/apns-send-http2 new file mode 100755 index 0000000..3cb7a82 --- /dev/null +++ b/apns-send-http2 @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +"""Command line script for sending a push via HTTP/2""" + +import optparse +import json +from apns2 import APNsHTTP2 +from apns import Payload # reuse existing Payload class + +parser = optparse.OptionParser() + +parser.add_option("-c", "--certificate-file", dest="certificate_file", help="Path to .pem certificate file") +parser.add_option("-k", "--key-file", dest="key_file", help="Path to .pem key file") +parser.add_option("-p", "--push-token", dest="push_token", help="Push token") +parser.add_option("-t", "--topic", dest="topic", help="Topic (usually the app bundle id)") +parser.add_option("-m", "--message", dest="message", help="Message") +parser.add_option("-s", "--sandbox", action="store_true", dest="sandbox", default=False, help="Use apple sandbox") + +options, args = parser.parse_args() + +if not options.certificate_file or not options.key_file: + parser.error('Must provide certificate and key files') +if not options.push_token: + parser.error('Must provide --push-token') +if not options.message: + parser.error('Must provide --message') +if not options.topic: + parser.error('Must provide --topic') + +apns = APNsHTTP2(use_sandbox=options.sandbox, cert_file=options.certificate_file, key_file=options.key_file) + +payload = Payload(alert=options.message, sound="default", badge=1) +resp = apns.send_notification(options.push_token, json.loads(payload.json()), topic=options.topic) +print("Status:", resp.status_code) diff --git a/apns.py b/apns.py index 33655b1..0b2b878 100644 --- a/apns.py +++ b/apns.py @@ -321,6 +321,10 @@ def __init__(self, alert=None, badge=None, sound=None, category=None, custom=Non def dict(self): """Returns the payload as a regular Python dictionary""" d = {} + if self.sound: + d['sound'] = self.sound + if self.badge is not None: + d['badge'] = int(self.badge) if self.alert: # Alert can be either a string or a PayloadAlert # object @@ -328,10 +332,6 @@ def dict(self): 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) if self.category: d['category'] = self.category @@ -347,7 +347,7 @@ def dict(self): return d def json(self): - return json.dumps(self.dict(), separators=(',',':'), ensure_ascii=False).encode('utf-8') + return json.dumps(self.dict(), separators=(',', ':'), ensure_ascii=False).encode('utf-8') def _check_size(self): payload_length = len(self.json()) diff --git a/apns2.py b/apns2.py new file mode 100644 index 0000000..4104bb9 --- /dev/null +++ b/apns2.py @@ -0,0 +1,30 @@ +"""HTTP/2 APNs client using the modern provider API.""" + +import json +import httpx +from typing import Optional + +class APNsHTTP2: + """Simple APNs client that uses Apple's HTTP/2 provider API.""" + + def __init__(self, *, use_sandbox: bool = False, cert_file: Optional[str] = None, key_file: Optional[str] = None, timeout: float = 10.0): + self.use_sandbox = use_sandbox + self.cert_file = cert_file + self.key_file = key_file + self.timeout = timeout + self._client = httpx.Client(http2=True, verify=True, cert=(self.cert_file, self.key_file)) + self.host = ("api.push.apple.com", "api.sandbox.push.apple.com")[self.use_sandbox] + + def _endpoint(self, token: str) -> str: + return f"https://{self.host}/3/device/{token}" + + def send_notification(self, token: str, payload: dict, topic: str, priority: int = 10) -> httpx.Response: + headers = { + "apns-topic": topic, + "apns-priority": str(priority), + } + data = json.dumps(payload).encode("utf-8") + url = self._endpoint(token) + response = self._client.post(url, headers=headers, content=data, timeout=self.timeout) + response.raise_for_status() + return response diff --git a/setup.py b/setup.py index 1d66940..3200d08 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,8 @@ download_url = 'https://github.com/djacobs/PyAPNs', license = 'unlicense.org', name = 'apns', - py_modules = ['apns'], - scripts = ['apns-send'], + py_modules = ['apns', 'apns2'], + scripts = ['apns-send', 'apns-send-http2'], url = 'http://29.io/', version = '2.0.1', )