[manual] Backend support for Android GCM push notifications

This adds a dependency on gcmclient:
http://gcm-client.readthedocs.org/en/latest/gcmclient.html

pip install gcm-client

or

apt-get install python-gcm-client

(imported from commit 9f1fbf1f793e4a27baed85c6f1aa7a7b03106a10)
This commit is contained in:
Kevin Mehall 2013-12-09 17:19:59 -05:00
parent e4589700b6
commit 662edc2558
8 changed files with 132 additions and 5 deletions

View File

@ -54,6 +54,8 @@ class zulip::app_frontend {
"python-diff-match-patch",
# Needed for iOS
"python-apns-client",
# Needed for Android push
"python-gcm-client",
# Needed for avatar image resizing
"python-imaging",
# Needed for LDAP support

View File

@ -14,6 +14,7 @@ from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity,
get_stream_cache_key, to_dict_cache_key_id, is_super_user, \
UserActivityInterval, get_active_user_dicts_in_realm, RealmAlias, \
ScheduledJob
from zerver.lib.avatar import get_avatar_url
from django.db import transaction, IntegrityError
from django.db.models import F, Q
@ -44,7 +45,8 @@ from zerver.lib.utils import log_statsd_event, statsd
from zerver.lib.html_diff import highlight_html_differences
from zerver.lib.alert_words import user_alert_words, add_user_alert_words, \
remove_user_alert_words, set_user_alert_words
from zerver.lib.push_notifications import num_push_devices_for_user, send_apple_push_notification
from zerver.lib.push_notifications import num_push_devices_for_user, \
send_apple_push_notification, send_android_push_notification
from zerver import tornado_callbacks
@ -2020,7 +2022,10 @@ def handle_push_notification(user_profile_id, missed_message):
return
sender_str = message.sender.full_name
if user_profile.enable_offline_push_notifications and num_push_devices_for_user(user_profile):
apple = num_push_devices_for_user(user_profile, kind=PushDeviceToken.APNS)
android = num_push_devices_for_user(user_profile, kind=PushDeviceToken.GCM)
if apple or android:
#TODO: set badge count in a better way
# Determine what alert string to display based on the missed messages
if message.recipient.type == Recipient.HUDDLE:
@ -2031,12 +2036,41 @@ def handle_push_notification(user_profile_id, missed_message):
alert = "New mention from %s" % (sender_str,)
else:
alert = "New Zulip mentions and private messages from %s" % (sender_str,)
extra_data = {'message_ids': [message.id]}
send_apple_push_notification(user_profile, alert, badge=1, zulip=extra_data)
if apple:
apple_extra_data = {'message_ids': [message.id]}
send_apple_push_notification(user_profile, alert, badge=1, zulip=apple_extra_data)
if android:
content = message.content
content_truncated = (len(content) > 200)
if content_truncated:
content = content[:200] + "..."
android_data = {
'user': user_profile.email,
'event': 'message',
'alert': alert,
'zulip_message_id': message.id, # message_id is reserved for CCS
'time': datetime_to_timestamp(message.pub_date),
'content': content,
'content_truncated': content_truncated,
'sender_email': message.sender.email,
'sender_full_name': message.sender.full_name,
'sender_avatar_url': get_avatar_url(message.sender.avatar_source, message.sender.email),
}
if message.recipient.type == Recipient.STREAM:
android_data['recipient_type'] = "stream"
android_data['stream'] = get_display_recipient(message.recipient)
android_data['topic'] = message.subject
elif message.recipient.type in (Recipient.HUDDLE, Recipient.PERSONAL):
android_data['recipient_type'] = "private"
send_android_push_notification(user_profile, android_data)
except UserMessage.DoesNotExist:
logging.error("Could not find UserMessage with message_id %s" %(missed_message['message_id'],))
return
def handle_missedmessage_emails(user_profile_id, missed_email_events):
message_ids = [event.get('message_id') for event in missed_email_events]

View File

@ -5,6 +5,7 @@ from zerver.lib.timestamp import timestamp_to_datetime
from zerver.decorator import statsd_increment
from apnsclient import Session, Message, APNs
import gcmclient
from django.conf import settings
@ -37,6 +38,7 @@ def hex_to_b64(data):
def send_apple_push_notification(user, alert, **extra_data):
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.")
return
tokens = [b64_to_hex(device.token) for device in
PushDeviceToken.objects.filter(user=user, kind=PushDeviceToken.APNS)]
@ -74,3 +76,44 @@ def check_apns_feedback():
PushDeviceToken.objects.filter(token=hex_to_b64(token), last_updates__lt=since_date, type=PushDeviceToken.APNS).delete()
logging.info("Finished checking feedback for stale tokens")
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")

View File

@ -4798,6 +4798,36 @@ class APNSTokenTests(AuthedTestCase):
result = self.client.delete('/json/users/me/apns_device_token', urllib.urlencode({'token': token}))
self.assert_json_success(result)
class GCMTokenTests(AuthedTestCase):
def test_add_token(self):
email = "cordelia@zulip.com"
self.login(email)
result = self.client.post('/json/users/me/apns_device_token', {'token': "test_token"})
self.assert_json_success(result)
def test_delete_token(self):
email = "cordelia@zulip.com"
self.login(email)
token = "test_token"
result = self.client.post('/json/users/me/android_gcm_reg_id', {'token':token})
self.assert_json_success(result)
result = self.client.delete('/json/users/me/android_gcm_reg_id', urllib.urlencode({'token': token}))
self.assert_json_success(result)
def test_change_user(self):
token = "test_token"
self.login("cordelia@zulip.com")
result = self.client.post('/json/users/me/android_gcm_reg_id', {'token':token})
self.assert_json_success(result)
self.login("hamlet@zulip.com")
result = self.client.post('/json/users/me/android_gcm_reg_id', {'token':token})
self.assert_json_success(result)
def full_test_name(test):
test_class = test.__class__.__name__
test_method = test._testMethodName

View File

@ -2397,6 +2397,10 @@ def add_push_device_token(request, user_profile, token, kind):
def add_apns_device_token(request, user_profile, token=REQ):
return add_push_device_token(request, user_profile, token, PushDeviceToken.APNS)
@has_request_variables
def add_android_reg_id(request, user_profile, token=REQ):
return add_push_device_token(request, user_profile, token, PushDeviceToken.GCM)
def remove_push_device_token(request, user_profile, token, kind):
if token == '' or len(token) > 4096:
return json_error('Empty or invalid length token')
@ -2413,6 +2417,11 @@ def remove_push_device_token(request, user_profile, token, kind):
def remove_apns_device_token(request, user_profile, token=REQ):
return remove_push_device_token(request, user_profile, token, PushDeviceToken.APNS)
@has_request_variables
def remove_android_reg_id(request, user_profile, token=REQ):
return remove_push_device_token(request, user_profile, token, PushDeviceToken.GCM)
def generate_204(request):
return HttpResponse(content=None, status=204)

View File

@ -136,6 +136,11 @@ else:
APNS_FEEDBACK = "feedback_sandbox"
APNS_CERT_FILE = "/etc/ssl/django-private/apns-dev.pem"
if DEPLOYED:
ANDROID_GCM_API_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
else:
ANDROID_GCM_API_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Administrator domain for this install
ADMIN_DOMAIN = "zulip.com"

View File

@ -258,6 +258,7 @@ DEFAULT_SETTINGS = {'TWITTER_CONSUMER_KEY': '',
'NAGIOS_STAGING_SEND_BOT': None,
'NAGIOS_STAGING_RECEIVE_BOT': None,
'APNS_CERT_FILE': None,
'ANDROID_GCM_API_KEY': None,
'INITIAL_PASSWORD_SALT': None,
'FEEDBACK_BOT': 'feedback@zulip.com',
'FEEDBACK_BOT_NAME': 'Zulip Feedback Bot',

View File

@ -217,6 +217,9 @@ v1_api_and_json_patterns = patterns('zerver.views',
url(r'^users/me/apns_device_token$', 'rest_dispatch',
{'POST' : 'add_apns_device_token',
'DELETE': 'remove_apns_device_token'}),
url(r'^users/me/android_gcm_reg_id$', 'rest_dispatch',
{'POST': 'add_android_reg_id',
'DELETE': 'remove_android_reg_id'}),
url(r'^users/(?P<email>.*)/reactivate$', 'rest_dispatch',
{'POST': 'reactivate_user_backend'}),
url(r'^users/(?P<email>.*)$', 'rest_dispatch',