mirror of
https://github.com/zulip/zulip.git
synced 2025-11-03 05:23:35 +00:00
Switch to PyAPNS for sending push notifications.
Switch to [PyAPNS](https://github.com/djacobs/PyAPNs). Fixes #538.
This commit is contained in:
@@ -6,28 +6,103 @@ from typing import Any, Dict, Optional
|
||||
from zerver.models import PushDeviceToken, UserProfile
|
||||
from zerver.lib.timestamp import timestamp_to_datetime
|
||||
from zerver.decorator import statsd_increment
|
||||
from zerver.lib.utils import generate_random_token
|
||||
from zerver.lib.redis_utils import get_redis_client
|
||||
|
||||
from apnsclient import Session, Message, APNs
|
||||
from apnsclient.apns import Connection
|
||||
from apns import APNs, Frame, Payload, SENT_BUFFER_QTY
|
||||
import gcmclient
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
import base64, binascii, logging, os
|
||||
import base64, binascii, logging, os, time
|
||||
from collections import OrderedDict
|
||||
from functools import partial
|
||||
|
||||
# APNS error codes
|
||||
ERROR_CODES = {
|
||||
1: 'Processing error',
|
||||
2: 'Missing device token', # looks like token was empty?
|
||||
3: 'Missing topic', # topic is encoded in the certificate, looks like certificate is wrong. bail out.
|
||||
4: 'Missing payload', # bail out, our message looks like empty
|
||||
5: 'Invalid token size', # current token has wrong size, skip it and retry
|
||||
6: 'Invalid topic size', # can not happen, we do not send topic, it is part of certificate. bail out.
|
||||
7: 'Invalid payload size', # our payload is probably too big. bail out.
|
||||
8: 'Invalid token', # our device token is broken, skipt it and retry
|
||||
10: 'Shutdown', # server went into maintenance mode. reported token is the last success, skip it and retry.
|
||||
None: 'Unknown', # unknown error, for sure we try again, but user should limit number of retries
|
||||
}
|
||||
|
||||
redis_client = get_redis_client()
|
||||
|
||||
# Maintain a long-lived Session object to avoid having to re-SSL-handshake
|
||||
# for each request
|
||||
session = Session()
|
||||
connection = None
|
||||
if settings.APNS_CERT_FILE is not None and os.path.exists(settings.APNS_CERT_FILE):
|
||||
connection = session.get_connection(settings.APNS_SANDBOX, cert_file=settings.APNS_CERT_FILE)
|
||||
|
||||
# We maintain an additional APNS connection for pushing to Zulip apps that have been signed
|
||||
# by the Dropbox certs (and have an app id of com.dropbox.zulip)
|
||||
dbx_session = Session()
|
||||
dbx_connection = None
|
||||
|
||||
def get_apns_key(token):
|
||||
return 'apns:' + token
|
||||
|
||||
class APNsMessage(object):
|
||||
def __init__(self, user, tokens, alert=None, badge=None, sound=None,
|
||||
category=None, **kwargs):
|
||||
self.frame = Frame()
|
||||
self.tokens = tokens
|
||||
expiry = time.time() + 24 * 3600
|
||||
priority = 10
|
||||
payload = Payload(alert=alert, badge=badge, sound=sound,
|
||||
category=category, custom=kwargs)
|
||||
for token in tokens:
|
||||
data = {'token': token, 'user': user}
|
||||
identifier = generate_random_token(32)
|
||||
key = get_apns_key(identifier)
|
||||
redis_client.hmset(key, data)
|
||||
redis_client.expire(key, expiry)
|
||||
self.frame.add_item(token, payload, identifier, expiry, priority)
|
||||
|
||||
def get_frame(self):
|
||||
return self.frame
|
||||
|
||||
def response_listener(error_response, connection):
|
||||
identifier = error_response['identifier']
|
||||
key = get_apns_key(identifier)
|
||||
if not redis_client.exists(key):
|
||||
logging.warn("APNs key, {}, doesn't not exist.".format(key))
|
||||
return
|
||||
|
||||
code = error_response['status']
|
||||
errmsg = ERROR_CODES[code]
|
||||
data = redis_client.hgetall(key)
|
||||
token = data['token']
|
||||
user = data['user']
|
||||
b64_token = hex_to_b64(token)
|
||||
|
||||
logging.warning("APNS: Failed to deliver APNS notification to %s, reason: %s" % (b64_token, errmsg))
|
||||
if code == 8:
|
||||
# Invalid Token, remove from our database
|
||||
logging.warning("APNS: Removing token from database due to above failure")
|
||||
try:
|
||||
PushDeviceToken.objects.get(user=user, token=b64_token).delete()
|
||||
except PushDeviceToken.DoesNotExist:
|
||||
pass
|
||||
|
||||
if settings.APNS_CERT_FILE is not None and os.path.exists(settings.APNS_CERT_FILE):
|
||||
connection = APNs(use_sandbox=settings.APNS_SANDBOX,
|
||||
cert_file=settings.APNS_CERT_FILE,
|
||||
key_file=settings.APNS_KEY_FILE,
|
||||
enhanced=True)
|
||||
connection.gateway_server.register_response_listener(
|
||||
partial(response_listener, connection=connection))
|
||||
|
||||
if settings.DBX_APNS_CERT_FILE is not None and os.path.exists(settings.DBX_APNS_CERT_FILE):
|
||||
dbx_connection = session.get_connection(settings.APNS_SANDBOX, cert_file=settings.DBX_APNS_CERT_FILE)
|
||||
dbx_connection = APNs(use_sandbox=settings.APNS_SANDBOX,
|
||||
cert_file=settings.DBX_APNS_CERT_FILE,
|
||||
key_file=settings.DBX_APNS_KEY_FILE,
|
||||
enhanced=True)
|
||||
dbx_connection.gateway_server.register_response_listener(
|
||||
partial(response_listener, connection=dbx_connection))
|
||||
|
||||
def num_push_devices_for_user(user_profile, kind = None):
|
||||
# type: (UserProfile, Optional[int]) -> PushDeviceToken
|
||||
@@ -46,37 +121,13 @@ def hex_to_b64(data):
|
||||
return base64.b64encode(binascii.unhexlify(data.encode('utf-8')))
|
||||
|
||||
def _do_push_to_apns_service(user, message, apns_connection):
|
||||
# type: (UserProfile, Message, Connection) -> None
|
||||
# type: (UserProfile, APNsMessage, APNs) -> None
|
||||
if not apns_connection:
|
||||
logging.info("Not delivering APNS message %s to user %s due to missing connection" % (message, user))
|
||||
return
|
||||
|
||||
apns_client = APNs(apns_connection)
|
||||
ret = apns_client.send(message)
|
||||
if not ret:
|
||||
logging.warning("APNS: Failed to send push notification for clients %s" % (message.tokens,))
|
||||
return
|
||||
|
||||
for token, reason in ret.failed.items():
|
||||
code, errmsg = reason
|
||||
b64_token = hex_to_b64(token)
|
||||
logging.warning("APNS: Failed to deliver APNS notification to %s, reason: %s" % (b64_token, errmsg))
|
||||
if code == 8:
|
||||
# Invalid Token, remove from our database
|
||||
logging.warning("APNS: Removing token from database due to above failure")
|
||||
PushDeviceToken.objects.get(user=user, token=b64_token).delete()
|
||||
|
||||
# Check failures not related to devices.
|
||||
for code, errmsg in ret.errors:
|
||||
logging.warning("APNS: Unknown error when delivering APNS: %s" % (errmsg,))
|
||||
|
||||
if ret.needs_retry():
|
||||
logging.warning("APNS: delivery needs a retry, trying again")
|
||||
retry_msg = ret.retry()
|
||||
ret = apns_client.send(retry_msg)
|
||||
for code, errmsg in ret.errors:
|
||||
logging.warning("APNS: Unknown error when delivering APNS: %s" % (errmsg,))
|
||||
|
||||
frame = message.get_frame()
|
||||
apns_connection.gateway_server.send_notification_multiple(frame)
|
||||
|
||||
# Send a push notification to the desired clients
|
||||
# extra_data is a dict that will be passed to the
|
||||
@@ -94,10 +145,13 @@ def send_apple_push_notification(user, alert, **extra_data):
|
||||
tokens = [(b64_to_hex(device.token), device.ios_app_id, device.token) for device in devices]
|
||||
|
||||
logging.info("APNS: Sending apple push notification to devices: %s" % (tokens,))
|
||||
zulip_message = Message([token[0] for token in tokens if token[1] in (settings.ZULIP_IOS_APP_ID, None)],
|
||||
alert=alert, **extra_data)
|
||||
dbx_message = Message([token[0] for token in tokens if token[1] in (settings.DBX_IOS_APP_ID,)],
|
||||
alert=alert, **extra_data)
|
||||
zulip_message = APNsMessage(user,
|
||||
[token[0] for token in tokens if token[1] in (settings.ZULIP_IOS_APP_ID, None)],
|
||||
alert=alert, **extra_data)
|
||||
|
||||
dbx_message = APNsMessage(user,
|
||||
[token[0] for token in tokens if token[1] in (settings.DBX_IOS_APP_ID,)],
|
||||
alert=alert, **extra_data)
|
||||
|
||||
_do_push_to_apns_service(user, zulip_message, connection)
|
||||
_do_push_to_apns_service(user, dbx_message, dbx_connection)
|
||||
@@ -106,10 +160,11 @@ def send_apple_push_notification(user, alert, **extra_data):
|
||||
# feedback() call can take up to 15s
|
||||
def check_apns_feedback():
|
||||
# type: () -> None
|
||||
feedback_connection = session.get_connection(settings.APNS_FEEDBACK, cert_file=settings.APNS_CERT_FILE)
|
||||
apns_client = APNs(feedback_connection, tail_timeout=20)
|
||||
feedback_connection = APNs(use_sandbox=settings.APNS_SANDBOX,
|
||||
cert_file=settings.APNS_CERT_FILE,
|
||||
key_file=settings.APNS_KEY_FILE)
|
||||
|
||||
for token, since in apns_client.feedback():
|
||||
for token, since in feedback_connection.feedback_server.items():
|
||||
since_date = timestamp_to_datetime(since)
|
||||
logging.info("Found unavailable token %s, unavailable since %s" % (token, since_date))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user