mirror of https://github.com/zulip/zulip.git
push notifs: Implement APNs with new API.
And it works! A couple of things still to do: * When a device token is no longer active, we'll get HTTP status 410. We should then remove the token from the database so we don't keep trying to push to it. This is fairly urgent. * The library we're using has a nice asynchronous API, but this version doesn't use it. This is OK now, but async will be essential at scale.
This commit is contained in:
parent
35db1b2f11
commit
613d093d7d
|
@ -9,6 +9,8 @@ import time
|
||||||
import random
|
import random
|
||||||
from typing import Any, Dict, List, Optional, SupportsInt, Text, Union, Type
|
from typing import Any, Dict, List, Optional, SupportsInt, Text, Union, Type
|
||||||
|
|
||||||
|
from apns2.client import APNsClient
|
||||||
|
from apns2.payload import Payload as APNsPayload
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
@ -49,16 +51,42 @@ def hex_to_b64(data):
|
||||||
# Sending to APNs, for iOS
|
# Sending to APNs, for iOS
|
||||||
#
|
#
|
||||||
|
|
||||||
# `APNS_SANDBOX` should be a bool
|
_apns_client = None # type: APNsClient
|
||||||
assert isinstance(settings.APNS_SANDBOX, bool)
|
|
||||||
|
def get_apns_client():
|
||||||
|
global _apns_client
|
||||||
|
if _apns_client is None:
|
||||||
|
# NB if called concurrently, this will make excess connections.
|
||||||
|
# That's a little sloppy, but harmless unless a server gets
|
||||||
|
# hammered with a ton of these all at once after startup.
|
||||||
|
_apns_client = APNsClient(credentials=settings.APNS_CERT_FILE,
|
||||||
|
use_sandbox=settings.APNS_SANDBOX)
|
||||||
|
return _apns_client
|
||||||
|
|
||||||
@statsd_increment("apple_push_notification")
|
@statsd_increment("apple_push_notification")
|
||||||
def send_apple_push_notification(user_id, devices, **extra_data):
|
def send_apple_push_notification(user_id, devices, payload_data):
|
||||||
# type: (int, List[DeviceToken], **Any) -> None
|
# type: (int, List[DeviceToken], Dict[str, Any]) -> None
|
||||||
if not devices:
|
if not devices:
|
||||||
return
|
return
|
||||||
logging.warn("APNs unimplemented. Dropping notification for user %d with %d devices.",
|
logging.info("APNs: Sending notification for user %d to %d devices",
|
||||||
user_id, len(devices))
|
user_id, len(devices))
|
||||||
|
payload = APNsPayload(**payload_data)
|
||||||
|
expiration = int(time.time() + 24 * 3600)
|
||||||
|
client = get_apns_client()
|
||||||
|
for device in devices:
|
||||||
|
# TODO obviously this should be made to actually use the async
|
||||||
|
stream_id = client.send_notification_async(
|
||||||
|
device.token, payload, topic='org.zulip.Zulip',
|
||||||
|
expiration=expiration)
|
||||||
|
result = client.get_notification_result(stream_id)
|
||||||
|
if result == 'Success':
|
||||||
|
logging.info("APNs: Success sending for user %d to device %s",
|
||||||
|
user_id, device.token)
|
||||||
|
else:
|
||||||
|
logging.warn("APNs: Failed to send for user %d to device %s: %s",
|
||||||
|
user_id, device.token, result)
|
||||||
|
# TODO delete token if status 410 (and timestamp isn't before
|
||||||
|
# the token we have)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sending to GCM, for Android
|
# Sending to GCM, for Android
|
||||||
|
@ -293,7 +321,13 @@ def get_apns_payload(message):
|
||||||
# type: (Message) -> Dict[str, Any]
|
# type: (Message) -> Dict[str, Any]
|
||||||
return {
|
return {
|
||||||
'alert': get_alert_from_message(message),
|
'alert': get_alert_from_message(message),
|
||||||
'message_ids': [message.id],
|
# TODO: set badge count in a better way
|
||||||
|
'badge': 1,
|
||||||
|
'custom': {
|
||||||
|
'zulip': {
|
||||||
|
'message_ids': [message.id],
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_gcm_payload(user_profile, message):
|
def get_gcm_payload(user_profile, message):
|
||||||
|
@ -369,10 +403,9 @@ def handle_push_notification(user_profile_id, missed_message):
|
||||||
apple_devices = list(PushDeviceToken.objects.filter(user=user_profile,
|
apple_devices = list(PushDeviceToken.objects.filter(user=user_profile,
|
||||||
kind=PushDeviceToken.APNS))
|
kind=PushDeviceToken.APNS))
|
||||||
|
|
||||||
# TODO: set badge count in a better way
|
|
||||||
if apple_devices:
|
if apple_devices:
|
||||||
send_apple_push_notification(user_profile.id, apple_devices,
|
send_apple_push_notification(user_profile.id, apple_devices,
|
||||||
badge=1, zulip=apns_payload)
|
apns_payload)
|
||||||
|
|
||||||
if android_devices:
|
if android_devices:
|
||||||
send_android_push_notification(android_devices, gcm_payload)
|
send_android_push_notification(android_devices, gcm_payload)
|
||||||
|
|
|
@ -306,6 +306,7 @@ class HandlePushNotificationTest(PushNotificationTest):
|
||||||
mock.patch('zerver.lib.push_notifications.requests.request',
|
mock.patch('zerver.lib.push_notifications.requests.request',
|
||||||
side_effect=self.bounce_request), \
|
side_effect=self.bounce_request), \
|
||||||
mock.patch('zerver.lib.push_notifications.gcm') as mock_gcm, \
|
mock.patch('zerver.lib.push_notifications.gcm') as mock_gcm, \
|
||||||
|
mock.patch('zerver.lib.push_notifications._apns_client') as mock_apns, \
|
||||||
mock.patch('logging.info') as mock_info, \
|
mock.patch('logging.info') as mock_info, \
|
||||||
mock.patch('logging.warn') as mock_warn:
|
mock.patch('logging.warn') as mock_warn:
|
||||||
apns_devices = [
|
apns_devices = [
|
||||||
|
@ -320,10 +321,12 @@ class HandlePushNotificationTest(PushNotificationTest):
|
||||||
]
|
]
|
||||||
mock_gcm.json_request.return_value = {
|
mock_gcm.json_request.return_value = {
|
||||||
'success': {gcm_devices[0][2]: message.id}}
|
'success': {gcm_devices[0][2]: message.id}}
|
||||||
|
mock_apns.get_notification_result.return_value = 'Success'
|
||||||
apn.handle_push_notification(self.user_profile.id, missed_message)
|
apn.handle_push_notification(self.user_profile.id, missed_message)
|
||||||
mock_warn.assert_called_with(
|
for _, _, token in apns_devices:
|
||||||
"APNs unimplemented. Dropping notification for user %d with %d devices.",
|
mock_info.assert_any_call(
|
||||||
self.user_profile.id, len(apns_devices))
|
"APNs: Success sending for user %d to device %s",
|
||||||
|
self.user_profile.id, token)
|
||||||
for _, _, token in gcm_devices:
|
for _, _, token in gcm_devices:
|
||||||
mock_info.assert_any_call(
|
mock_info.assert_any_call(
|
||||||
"GCM: Sent %s as %s" % (token, message.id))
|
"GCM: Sent %s as %s" % (token, message.id))
|
||||||
|
@ -455,8 +458,7 @@ class HandlePushNotificationTest(PushNotificationTest):
|
||||||
apn.handle_push_notification(self.user_profile.id, missed_message)
|
apn.handle_push_notification(self.user_profile.id, missed_message)
|
||||||
mock_send_apple.assert_called_with(self.user_profile.id,
|
mock_send_apple.assert_called_with(self.user_profile.id,
|
||||||
apple_devices,
|
apple_devices,
|
||||||
badge=1,
|
{'apns': True})
|
||||||
zulip={'apns': True})
|
|
||||||
mock_send_android.assert_called_with(android_devices,
|
mock_send_android.assert_called_with(android_devices,
|
||||||
{'gcm': True})
|
{'gcm': True})
|
||||||
|
|
||||||
|
@ -488,8 +490,13 @@ class TestGetAPNsPayload(PushNotificationTest):
|
||||||
message = self.get_message(Recipient.HUDDLE)
|
message = self.get_message(Recipient.HUDDLE)
|
||||||
payload = apn.get_apns_payload(message)
|
payload = apn.get_apns_payload(message)
|
||||||
expected = {
|
expected = {
|
||||||
"alert": "New private group message from King Hamlet",
|
'alert': "New private group message from King Hamlet",
|
||||||
"message_ids": [message.id],
|
'badge': 1,
|
||||||
|
'custom': {
|
||||||
|
'zulip': {
|
||||||
|
'message_ids': [message.id],
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.assertDictEqual(payload, expected)
|
self.assertDictEqual(payload, expected)
|
||||||
|
|
||||||
|
|
|
@ -102,9 +102,7 @@ def remote_server_notify_push(request, # type: HttpRequest
|
||||||
if android_devices:
|
if android_devices:
|
||||||
send_android_push_notification(android_devices, gcm_payload, remote=True)
|
send_android_push_notification(android_devices, gcm_payload, remote=True)
|
||||||
|
|
||||||
# TODO: set badge count in a better way
|
|
||||||
if apple_devices:
|
if apple_devices:
|
||||||
send_apple_push_notification(user_id, apple_devices,
|
send_apple_push_notification(user_id, apple_devices, apns_payload)
|
||||||
badge=1, zulip=apns_payload)
|
|
||||||
|
|
||||||
return json_success()
|
return json_success()
|
||||||
|
|
Loading…
Reference in New Issue