2013-10-21 19:47:08 +02:00
|
|
|
from __future__ import absolute_import
|
|
|
|
|
2013-12-09 23:17:16 +01:00
|
|
|
from zerver.models import PushDeviceToken
|
2013-10-21 19:47:08 +02:00
|
|
|
from zerver.lib.timestamp import timestamp_to_datetime
|
|
|
|
from zerver.decorator import statsd_increment
|
|
|
|
|
2013-11-04 17:22:58 +01:00
|
|
|
from apnsclient import Session, Message, APNs
|
2013-12-09 23:19:59 +01:00
|
|
|
import gcmclient
|
2013-10-21 19:47:08 +02:00
|
|
|
|
|
|
|
from django.conf import settings
|
|
|
|
|
2013-10-24 21:50:43 +02:00
|
|
|
import base64, binascii, logging, os
|
2013-10-21 19:47:08 +02:00
|
|
|
|
|
|
|
# Maintain a long-lived Session object to avoid having to re-SSL-handshake
|
|
|
|
# for each request
|
|
|
|
session = Session()
|
2013-10-24 21:50:43 +02:00
|
|
|
connection = None
|
2013-11-10 17:36:33 +01:00
|
|
|
if settings.APNS_CERT_FILE is not None and os.path.exists(settings.APNS_CERT_FILE):
|
2013-10-24 21:50:43 +02:00
|
|
|
connection = session.get_connection(settings.APNS_SANDBOX, cert_file=settings.APNS_CERT_FILE)
|
2013-10-21 19:47:08 +02:00
|
|
|
|
2013-12-09 23:17:16 +01:00
|
|
|
def num_push_devices_for_user(user_profile, kind = None):
|
|
|
|
if kind is None:
|
|
|
|
return PushDeviceToken.objects.filter(user=user_profile).count()
|
|
|
|
else:
|
|
|
|
return PushDeviceToken.objects.filter(user=user_profile, kind=kind).count()
|
2013-10-21 19:47:08 +02:00
|
|
|
|
|
|
|
# We store the token as b64, but apns-client wants hex strings
|
|
|
|
def b64_to_hex(data):
|
|
|
|
return binascii.hexlify(base64.b64decode(data))
|
|
|
|
|
|
|
|
def hex_to_b64(data):
|
|
|
|
return base64.b64encode(binascii.unhexlify(data))
|
|
|
|
|
|
|
|
# Send a push notification to the desired clients
|
|
|
|
# extra_data is a dict that will be passed to the
|
|
|
|
# mobile app
|
|
|
|
@statsd_increment("apple_push_notification")
|
|
|
|
def send_apple_push_notification(user, alert, **extra_data):
|
2013-11-26 20:32:59 +01:00
|
|
|
if not connection:
|
|
|
|
logging.error("Attempting to send push notification, but no connection was found. This may be because we could not find the APNS Certificate file.")
|
2013-12-09 23:19:59 +01:00
|
|
|
return
|
2013-12-09 23:17:16 +01:00
|
|
|
|
2014-01-22 17:57:09 +01:00
|
|
|
b64_tokens = [device.token for device in PushDeviceToken.objects.filter(user=user, kind=PushDeviceToken.APNS)]
|
|
|
|
tokens = [b64_to_hex(token) for token in b64_tokens]
|
2013-10-21 19:47:08 +02:00
|
|
|
|
2014-01-22 17:57:09 +01:00
|
|
|
logging.info("APNS: Sending apple push notification to devices: %s" % (b64_tokens,))
|
2013-10-21 19:47:08 +02:00
|
|
|
message = Message(tokens, alert=alert, **extra_data)
|
|
|
|
|
|
|
|
apns_client = APNs(connection)
|
|
|
|
ret = apns_client.send(message)
|
|
|
|
if not ret:
|
2014-01-22 17:57:09 +01:00
|
|
|
logging.warning("APNS: Failed to send push notification for clients %s" % (b64_tokens,))
|
2013-10-21 19:47:08 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
for token, reason in ret.failed.items():
|
|
|
|
code, errmsg = reason
|
2014-01-22 17:57:09 +01:00
|
|
|
b64_token = hex_to_b64(token)
|
|
|
|
logging.warning("APNS: Failed to deliver APNS notification to %s, reason: %s" % (b64_token, errmsg))
|
2014-01-21 21:22:33 +01:00
|
|
|
if code == 8:
|
|
|
|
# Invalid Token, remove from our database
|
|
|
|
logging.warning("APNS: Removing token from database due to above failure")
|
2014-01-22 17:57:09 +01:00
|
|
|
PushDeviceToken.objects.get(user=user, token=b64_token).delete()
|
2013-10-21 19:47:08 +02:00
|
|
|
|
|
|
|
# Check failures not related to devices.
|
|
|
|
for code, errmsg in ret.errors:
|
2014-01-21 21:15:28 +01:00
|
|
|
logging.warning("APNS: Unknown error when delivering APNS: %s" % (errmsg,))
|
2013-10-21 19:47:08 +02:00
|
|
|
|
|
|
|
if ret.needs_retry():
|
2014-01-21 22:07:30 +01:00
|
|
|
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,))
|
|
|
|
|
2013-10-21 19:47:08 +02:00
|
|
|
|
|
|
|
# NOTE: This is used by the check_apns_tokens manage.py command. Do not call it otherwise, as the
|
|
|
|
# feedback() call can take up to 15s
|
|
|
|
def check_apns_feedback():
|
2014-01-22 19:45:43 +01:00
|
|
|
feedback_connection = session.get_connection(settings.APNS_FEEDBACK, cert_file=settings.APNS_CERT_FILE)
|
|
|
|
apns_client = APNs(feedback_connection, tail_timeout=20)
|
2013-10-21 19:47:08 +02:00
|
|
|
|
|
|
|
for token, since in apns_client.feedback():
|
|
|
|
since_date = timestamp_to_datetime(since)
|
|
|
|
logging.info("Found unavailable token %s, unavailable since %s" % (token, since_date))
|
|
|
|
|
2013-12-09 23:17:16 +01:00
|
|
|
PushDeviceToken.objects.filter(token=hex_to_b64(token), last_updates__lt=since_date, type=PushDeviceToken.APNS).delete()
|
2013-10-21 19:47:08 +02:00
|
|
|
logging.info("Finished checking feedback for stale tokens")
|
2013-12-09 23:19:59 +01:00
|
|
|
|
|
|
|
|
|
|
|
if settings.ANDROID_GCM_API_KEY:
|
|
|
|
gcm = gcmclient.GCM(settings.ANDROID_GCM_API_KEY)
|
|
|
|
else:
|
|
|
|
gcm = None
|
|
|
|
|
|
|
|
@statsd_increment("android_push_notification")
|
|
|
|
def send_android_push_notification(user, data):
|
|
|
|
if not gcm:
|
|
|
|
logging.error("Attempting to send a GCM push notification, but no API key was configured")
|
|
|
|
return
|
|
|
|
|
|
|
|
reg_ids = [device.token for device in
|
|
|
|
PushDeviceToken.objects.filter(user=user, kind=PushDeviceToken.GCM)]
|
|
|
|
|
|
|
|
msg = gcmclient.JSONMessage(reg_ids, data)
|
|
|
|
res = gcm.send(msg)
|
|
|
|
|
|
|
|
for reg_id, msg_id in res.success.items():
|
|
|
|
logging.info("GCM: Sent %s as %s" % (reg_id, msg_id))
|
|
|
|
|
|
|
|
for reg_id, new_reg_id in res.canonical.items():
|
|
|
|
logging.info("GCM: Updating registration %s with %s" % (reg_id, new_reg_id))
|
|
|
|
|
|
|
|
device = PushDeviceToken.objects.get(token=reg_id, kind=PushDeviceToken.GCM)
|
|
|
|
device.token = new_reg_id
|
|
|
|
device.save(update_fields=['token'])
|
|
|
|
|
|
|
|
for reg_id in res.not_registered:
|
|
|
|
logging.info("GCM: Removing %s" % (reg_id,))
|
|
|
|
|
|
|
|
device = PushDeviceToken.objects.get(token=reg_id, kind=PushDeviceToken.GCM)
|
|
|
|
device.delete()
|
|
|
|
|
|
|
|
for reg_id, err_code in res.failed.items():
|
|
|
|
logging.warning("GCM: Delivery to %s failed: %s" % (reg_id, err_code))
|
|
|
|
|
|
|
|
if res.needs_retry():
|
|
|
|
# TODO
|
|
|
|
logging.warning("GCM: delivery needs a retry but ignoring")
|