mirror of https://github.com/zulip/zulip.git
[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:
parent
e4589700b6
commit
662edc2558
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in New Issue