[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", "python-diff-match-patch",
# Needed for iOS # Needed for iOS
"python-apns-client", "python-apns-client",
# Needed for Android push
"python-gcm-client",
# Needed for avatar image resizing # Needed for avatar image resizing
"python-imaging", "python-imaging",
# Needed for LDAP support # 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, \ get_stream_cache_key, to_dict_cache_key_id, is_super_user, \
UserActivityInterval, get_active_user_dicts_in_realm, RealmAlias, \ UserActivityInterval, get_active_user_dicts_in_realm, RealmAlias, \
ScheduledJob ScheduledJob
from zerver.lib.avatar import get_avatar_url
from django.db import transaction, IntegrityError from django.db import transaction, IntegrityError
from django.db.models import F, Q 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.html_diff import highlight_html_differences
from zerver.lib.alert_words import user_alert_words, add_user_alert_words, \ from zerver.lib.alert_words import user_alert_words, add_user_alert_words, \
remove_user_alert_words, set_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 from zerver import tornado_callbacks
@ -2020,7 +2022,10 @@ def handle_push_notification(user_profile_id, missed_message):
return return
sender_str = message.sender.full_name 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 #TODO: set badge count in a better way
# Determine what alert string to display based on the missed messages # Determine what alert string to display based on the missed messages
if message.recipient.type == Recipient.HUDDLE: 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,) alert = "New mention from %s" % (sender_str,)
else: else:
alert = "New Zulip mentions and private messages from %s" % (sender_str,) 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: except UserMessage.DoesNotExist:
logging.error("Could not find UserMessage with message_id %s" %(missed_message['message_id'],)) 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): def handle_missedmessage_emails(user_profile_id, missed_email_events):
message_ids = [event.get('message_id') for event in 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 zerver.decorator import statsd_increment
from apnsclient import Session, Message, APNs from apnsclient import Session, Message, APNs
import gcmclient
from django.conf import settings from django.conf import settings
@ -37,6 +38,7 @@ def hex_to_b64(data):
def send_apple_push_notification(user, alert, **extra_data): def send_apple_push_notification(user, alert, **extra_data):
if not connection: 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.") 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 tokens = [b64_to_hex(device.token) for device in
PushDeviceToken.objects.filter(user=user, kind=PushDeviceToken.APNS)] 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() 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") 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})) result = self.client.delete('/json/users/me/apns_device_token', urllib.urlencode({'token': token}))
self.assert_json_success(result) 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): def full_test_name(test):
test_class = test.__class__.__name__ test_class = test.__class__.__name__
test_method = test._testMethodName 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): def add_apns_device_token(request, user_profile, token=REQ):
return add_push_device_token(request, user_profile, token, PushDeviceToken.APNS) 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): def remove_push_device_token(request, user_profile, token, kind):
if token == '' or len(token) > 4096: if token == '' or len(token) > 4096:
return json_error('Empty or invalid length token') 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): def remove_apns_device_token(request, user_profile, token=REQ):
return remove_push_device_token(request, user_profile, token, PushDeviceToken.APNS) 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): def generate_204(request):
return HttpResponse(content=None, status=204) return HttpResponse(content=None, status=204)

View File

@ -136,6 +136,11 @@ else:
APNS_FEEDBACK = "feedback_sandbox" APNS_FEEDBACK = "feedback_sandbox"
APNS_CERT_FILE = "/etc/ssl/django-private/apns-dev.pem" 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 # Administrator domain for this install
ADMIN_DOMAIN = "zulip.com" ADMIN_DOMAIN = "zulip.com"

View File

@ -258,6 +258,7 @@ DEFAULT_SETTINGS = {'TWITTER_CONSUMER_KEY': '',
'NAGIOS_STAGING_SEND_BOT': None, 'NAGIOS_STAGING_SEND_BOT': None,
'NAGIOS_STAGING_RECEIVE_BOT': None, 'NAGIOS_STAGING_RECEIVE_BOT': None,
'APNS_CERT_FILE': None, 'APNS_CERT_FILE': None,
'ANDROID_GCM_API_KEY': None,
'INITIAL_PASSWORD_SALT': None, 'INITIAL_PASSWORD_SALT': None,
'FEEDBACK_BOT': 'feedback@zulip.com', 'FEEDBACK_BOT': 'feedback@zulip.com',
'FEEDBACK_BOT_NAME': 'Zulip Feedback Bot', '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', url(r'^users/me/apns_device_token$', 'rest_dispatch',
{'POST' : 'add_apns_device_token', {'POST' : 'add_apns_device_token',
'DELETE': 'remove_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', url(r'^users/(?P<email>.*)/reactivate$', 'rest_dispatch',
{'POST': 'reactivate_user_backend'}), {'POST': 'reactivate_user_backend'}),
url(r'^users/(?P<email>.*)$', 'rest_dispatch', url(r'^users/(?P<email>.*)$', 'rest_dispatch',