2013-10-21 19:47:08 +02:00
|
|
|
from __future__ import absolute_import
|
2016-06-15 08:28:52 +02:00
|
|
|
|
2016-08-03 12:20:12 +02:00
|
|
|
import random
|
2016-10-27 23:55:31 +02:00
|
|
|
import requests
|
2017-05-09 12:15:11 +02:00
|
|
|
from typing import Any, Dict, List, Optional, SupportsInt, Text, Union, Type
|
2013-10-21 19:47:08 +02:00
|
|
|
|
2016-10-27 23:55:31 +02:00
|
|
|
from version import ZULIP_VERSION
|
2017-03-06 03:05:04 +01:00
|
|
|
from zerver.models import PushDeviceToken, Message, Recipient, UserProfile, \
|
|
|
|
UserMessage, get_display_recipient, receives_offline_notifications, \
|
|
|
|
receives_online_notifications
|
2016-08-03 12:47:12 +02:00
|
|
|
from zerver.models import get_user_profile_by_id
|
2017-06-15 07:44:00 +02:00
|
|
|
from zerver.lib.avatar import absolute_avatar_url
|
2017-03-06 04:11:13 +01:00
|
|
|
from zerver.lib.request import JsonableError
|
2017-03-06 03:05:04 +01:00
|
|
|
from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime
|
2013-10-21 19:47:08 +02:00
|
|
|
from zerver.decorator import statsd_increment
|
2016-06-08 12:32:59 +02:00
|
|
|
from zerver.lib.utils import generate_random_token
|
|
|
|
from zerver.lib.redis_utils import get_redis_client
|
2017-08-18 09:04:52 +02:00
|
|
|
from zerver.lib.queue import retry_event
|
2013-10-21 19:47:08 +02:00
|
|
|
|
2016-06-08 12:32:59 +02:00
|
|
|
from apns import APNs, Frame, Payload, SENT_BUFFER_QTY
|
2016-12-13 08:41:48 +01:00
|
|
|
from gcm import GCM
|
2013-10-21 19:47:08 +02:00
|
|
|
|
|
|
|
from django.conf import settings
|
2017-04-15 04:03:56 +02:00
|
|
|
from django.utils.timezone import now as timezone_now
|
2017-03-06 03:57:31 +01:00
|
|
|
from django.utils.translation import ugettext as _
|
2016-10-27 23:55:31 +02:00
|
|
|
from six.moves import urllib
|
2013-10-21 19:47:08 +02:00
|
|
|
|
2017-01-24 06:36:39 +01:00
|
|
|
import base64
|
|
|
|
import binascii
|
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import time
|
2016-10-27 23:55:31 +02:00
|
|
|
import ujson
|
2016-06-08 12:32:59 +02:00
|
|
|
from functools import partial
|
|
|
|
|
2017-05-09 12:15:11 +02:00
|
|
|
if settings.ZILENCER_ENABLED:
|
|
|
|
from zilencer.models import RemotePushDeviceToken
|
2017-05-12 10:12:38 +02:00
|
|
|
else: # nocoverage -- Not convenient to add test for this.
|
2017-05-09 12:15:11 +02:00
|
|
|
from mock import Mock
|
|
|
|
RemotePushDeviceToken = Mock() # type: ignore # https://github.com/JukkaL/mypy/issues/1188
|
|
|
|
|
|
|
|
DeviceToken = Union[PushDeviceToken, RemotePushDeviceToken]
|
|
|
|
|
2016-06-08 12:32:59 +02:00
|
|
|
# APNS error codes
|
|
|
|
ERROR_CODES = {
|
|
|
|
1: 'Processing error',
|
2017-05-07 17:08:46 +02:00
|
|
|
2: 'Missing device token', # looks like token was empty?
|
|
|
|
3: 'Missing topic', # topic is encoded in the certificate, looks like certificate is wrong. bail out.
|
|
|
|
4: 'Missing payload', # bail out, our message looks like empty
|
|
|
|
5: 'Invalid token size', # current token has wrong size, skip it and retry
|
|
|
|
6: 'Invalid topic size', # can not happen, we do not send topic, it is part of certificate. bail out.
|
|
|
|
7: 'Invalid payload size', # our payload is probably too big. bail out.
|
|
|
|
8: 'Invalid token', # our device token is broken, skipt it and retry
|
|
|
|
10: 'Shutdown', # server went into maintenance mode. reported token is the last success, skip it and retry.
|
|
|
|
None: 'Unknown', # unknown error, for sure we try again, but user should limit number of retries
|
2016-06-08 12:32:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
redis_client = get_redis_client()
|
2013-10-21 19:47:08 +02:00
|
|
|
|
|
|
|
# Maintain a long-lived Session object to avoid having to re-SSL-handshake
|
|
|
|
# for each request
|
2013-10-24 21:50:43 +02:00
|
|
|
connection = None
|
2013-10-21 19:47:08 +02:00
|
|
|
|
2016-08-03 10:41:39 +02:00
|
|
|
# `APNS_SANDBOX` should be a bool
|
|
|
|
assert isinstance(settings.APNS_SANDBOX, bool)
|
|
|
|
|
2017-05-08 13:47:18 +02:00
|
|
|
def uses_notification_bouncer():
|
|
|
|
# type: () -> bool
|
|
|
|
return settings.PUSH_NOTIFICATION_BOUNCER_URL is not None
|
|
|
|
|
2016-08-08 10:40:45 +02:00
|
|
|
def get_apns_key(identifer):
|
2016-08-08 10:50:10 +02:00
|
|
|
# type: (SupportsInt) -> str
|
2016-08-08 10:40:45 +02:00
|
|
|
return 'apns:' + str(identifer)
|
2016-06-08 12:32:59 +02:00
|
|
|
|
|
|
|
class APNsMessage(object):
|
2017-03-06 03:22:17 +01:00
|
|
|
def __init__(self, user_id, tokens, alert=None, badge=None, sound=None,
|
2016-12-03 00:04:17 +01:00
|
|
|
category=None, **kwargs):
|
2017-03-06 03:22:17 +01:00
|
|
|
# type: (int, List[Text], Text, int, Text, Text, **Any) -> None
|
2016-06-08 12:32:59 +02:00
|
|
|
self.frame = Frame()
|
|
|
|
self.tokens = tokens
|
2016-08-03 11:54:00 +02:00
|
|
|
expiry = int(time.time() + 24 * 3600)
|
2016-06-08 12:32:59 +02:00
|
|
|
priority = 10
|
|
|
|
payload = Payload(alert=alert, badge=badge, sound=sound,
|
|
|
|
category=category, custom=kwargs)
|
|
|
|
for token in tokens:
|
2017-03-06 03:22:17 +01:00
|
|
|
data = {'token': token, 'user_id': user_id}
|
2016-08-03 12:20:12 +02:00
|
|
|
identifier = random.getrandbits(32)
|
2016-06-08 12:32:59 +02:00
|
|
|
key = get_apns_key(identifier)
|
|
|
|
redis_client.hmset(key, data)
|
|
|
|
redis_client.expire(key, expiry)
|
|
|
|
self.frame.add_item(token, payload, identifier, expiry, priority)
|
|
|
|
|
|
|
|
def get_frame(self):
|
2016-08-08 10:50:10 +02:00
|
|
|
# type: () -> Frame
|
2016-06-08 12:32:59 +02:00
|
|
|
return self.frame
|
|
|
|
|
2016-08-04 10:16:07 +02:00
|
|
|
def response_listener(error_response):
|
2016-08-08 10:50:10 +02:00
|
|
|
# type: (Dict[str, SupportsInt]) -> None
|
2016-06-08 12:32:59 +02:00
|
|
|
identifier = error_response['identifier']
|
|
|
|
key = get_apns_key(identifier)
|
|
|
|
if not redis_client.exists(key):
|
|
|
|
logging.warn("APNs key, {}, doesn't not exist.".format(key))
|
|
|
|
return
|
|
|
|
|
|
|
|
code = error_response['status']
|
2016-08-04 13:00:04 +02:00
|
|
|
assert isinstance(code, int)
|
|
|
|
|
2016-06-08 12:32:59 +02:00
|
|
|
errmsg = ERROR_CODES[code]
|
|
|
|
data = redis_client.hgetall(key)
|
|
|
|
token = data['token']
|
2017-06-10 15:07:37 +02:00
|
|
|
user_id = int(data['user_id'])
|
2016-06-08 12:32:59 +02:00
|
|
|
b64_token = hex_to_b64(token)
|
|
|
|
|
2016-08-04 10:49:19 +02:00
|
|
|
logging.warn("APNS: Failed to deliver APNS notification to %s, reason: %s" % (b64_token, errmsg))
|
2016-06-08 12:32:59 +02:00
|
|
|
if code == 8:
|
|
|
|
# Invalid Token, remove from our database
|
2016-08-04 10:49:19 +02:00
|
|
|
logging.warn("APNS: Removing token from database due to above failure")
|
2016-06-08 12:32:59 +02:00
|
|
|
try:
|
2017-06-10 15:07:37 +02:00
|
|
|
PushDeviceToken.objects.get(user_id=user_id, token=b64_token).delete()
|
2017-05-09 12:29:50 +02:00
|
|
|
return # No need to check RemotePushDeviceToken
|
2016-06-08 12:32:59 +02:00
|
|
|
except PushDeviceToken.DoesNotExist:
|
|
|
|
pass
|
|
|
|
|
2017-05-09 12:29:50 +02:00
|
|
|
if settings.ZILENCER_ENABLED:
|
|
|
|
# Trying to delete from both models is a bit inefficient than
|
|
|
|
# deleting from only one model but this method is very simple.
|
|
|
|
try:
|
2017-06-10 15:07:37 +02:00
|
|
|
RemotePushDeviceToken.objects.get(user_id=user_id,
|
2017-05-09 12:29:50 +02:00
|
|
|
token=b64_token).delete()
|
|
|
|
except RemotePushDeviceToken.DoesNotExist:
|
|
|
|
pass
|
|
|
|
|
2016-08-04 09:59:15 +02:00
|
|
|
def get_connection(cert_file, key_file):
|
2016-08-08 10:50:10 +02:00
|
|
|
# type: (str, str) -> APNs
|
2016-06-08 12:32:59 +02:00
|
|
|
connection = APNs(use_sandbox=settings.APNS_SANDBOX,
|
2016-08-04 09:59:15 +02:00
|
|
|
cert_file=cert_file,
|
|
|
|
key_file=key_file,
|
2016-06-08 12:32:59 +02:00
|
|
|
enhanced=True)
|
2016-08-04 10:16:07 +02:00
|
|
|
connection.gateway_server.register_response_listener(response_listener)
|
2016-08-04 09:59:15 +02:00
|
|
|
return connection
|
|
|
|
|
2017-05-12 10:12:38 +02:00
|
|
|
if settings.APNS_CERT_FILE is not None and os.path.exists(settings.APNS_CERT_FILE): # nocoverage
|
2016-08-04 09:59:15 +02:00
|
|
|
connection = get_connection(settings.APNS_CERT_FILE,
|
|
|
|
settings.APNS_KEY_FILE)
|
|
|
|
|
2013-12-09 23:17:16 +01:00
|
|
|
def num_push_devices_for_user(user_profile, kind = None):
|
2016-06-04 01:51:51 +02:00
|
|
|
# type: (UserProfile, Optional[int]) -> PushDeviceToken
|
2013-12-09 23:17:16 +01:00
|
|
|
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):
|
2016-12-21 13:17:53 +01:00
|
|
|
# type: (bytes) -> Text
|
2016-06-15 08:28:52 +02:00
|
|
|
return binascii.hexlify(base64.b64decode(data)).decode('utf-8')
|
2013-10-21 19:47:08 +02:00
|
|
|
|
|
|
|
def hex_to_b64(data):
|
2016-12-21 13:17:53 +01:00
|
|
|
# type: (Text) -> bytes
|
2016-06-15 08:28:52 +02:00
|
|
|
return base64.b64encode(binascii.unhexlify(data.encode('utf-8')))
|
2013-10-21 19:47:08 +02:00
|
|
|
|
2017-03-06 03:24:50 +01:00
|
|
|
def _do_push_to_apns_service(user_id, message, apns_connection):
|
|
|
|
# type: (int, APNsMessage, APNs) -> None
|
2017-05-16 18:23:10 +02:00
|
|
|
if not apns_connection: # nocoverage
|
2017-03-06 03:24:50 +01:00
|
|
|
logging.info("Not delivering APNS message %s to user %s due to missing connection" % (message, user_id))
|
2015-02-11 08:20:51 +01:00
|
|
|
return
|
|
|
|
|
2016-06-08 12:32:59 +02:00
|
|
|
frame = message.get_frame()
|
|
|
|
apns_connection.gateway_server.send_notification_multiple(frame)
|
2013-10-21 19:47:08 +02:00
|
|
|
|
2017-03-06 03:19:40 +01:00
|
|
|
def send_apple_push_notification_to_user(user, alert, **extra_data):
|
|
|
|
# type: (UserProfile, Text, **Any) -> None
|
|
|
|
devices = PushDeviceToken.objects.filter(user=user, kind=PushDeviceToken.APNS)
|
2017-03-06 03:30:56 +01:00
|
|
|
send_apple_push_notification(user.id, devices, zulip=dict(alert=alert),
|
|
|
|
**extra_data)
|
2017-03-06 03:19:40 +01:00
|
|
|
|
2015-02-10 08:08:47 +01:00
|
|
|
# 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")
|
2017-03-06 03:30:56 +01:00
|
|
|
def send_apple_push_notification(user_id, devices, **extra_data):
|
2017-05-09 12:29:50 +02:00
|
|
|
# type: (int, List[DeviceToken], **Any) -> None
|
2017-05-16 18:23:10 +02:00
|
|
|
if not connection:
|
2017-04-25 22:51:27 +02:00
|
|
|
logging.warning("Attempting to send push notification, but no connection was found. "
|
|
|
|
"This may be because we could not find the APNS Certificate file.")
|
2015-02-10 08:08:47 +01:00
|
|
|
return
|
|
|
|
|
|
|
|
# Plain b64 token kept for debugging purposes
|
2016-08-04 13:00:26 +02:00
|
|
|
tokens = [(b64_to_hex(device.token), device.ios_app_id, device.token)
|
|
|
|
for device in devices]
|
|
|
|
|
2017-05-16 18:23:10 +02:00
|
|
|
valid_devices = [device for device in tokens if device[1] in [settings.ZULIP_IOS_APP_ID, None]]
|
|
|
|
valid_tokens = [device[0] for device in valid_devices]
|
|
|
|
if valid_tokens:
|
|
|
|
logging.info("APNS: Sending apple push notification "
|
|
|
|
"to devices: %s" % (valid_devices,))
|
|
|
|
zulip_message = APNsMessage(user_id, valid_tokens,
|
|
|
|
alert=extra_data['zulip']['alert'],
|
|
|
|
**extra_data)
|
|
|
|
_do_push_to_apns_service(user_id, zulip_message, connection)
|
|
|
|
else: # nocoverage
|
|
|
|
logging.warn("APNS: Not sending notification because "
|
|
|
|
"tokens didn't match devices: %s/%s" % (tokens, settings.ZULIP_IOS_APP_ID,))
|
2015-02-10 08:08:47 +01:00
|
|
|
|
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():
|
2016-06-04 01:51:51 +02:00
|
|
|
# type: () -> None
|
2016-06-08 12:32:59 +02:00
|
|
|
feedback_connection = APNs(use_sandbox=settings.APNS_SANDBOX,
|
|
|
|
cert_file=settings.APNS_CERT_FILE,
|
|
|
|
key_file=settings.APNS_KEY_FILE)
|
2013-10-21 19:47:08 +02:00
|
|
|
|
2016-06-08 12:32:59 +02:00
|
|
|
for token, since in feedback_connection.feedback_server.items():
|
2013-10-21 19:47:08 +02:00
|
|
|
since_date = timestamp_to_datetime(since)
|
|
|
|
logging.info("Found unavailable token %s, unavailable since %s" % (token, since_date))
|
|
|
|
|
2016-08-04 13:15:49 +02:00
|
|
|
PushDeviceToken.objects.filter(token=hex_to_b64(token), last_updated__lt=since_date,
|
|
|
|
kind=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
|
|
|
|
|
|
|
|
2017-05-12 10:12:38 +02:00
|
|
|
if settings.ANDROID_GCM_API_KEY: # nocoverage
|
2016-12-13 08:41:48 +01:00
|
|
|
gcm = GCM(settings.ANDROID_GCM_API_KEY)
|
2013-12-09 23:19:59 +01:00
|
|
|
else:
|
|
|
|
gcm = None
|
|
|
|
|
2017-03-06 03:11:44 +01:00
|
|
|
def send_android_push_notification_to_user(user_profile, data):
|
2016-06-15 08:28:52 +02:00
|
|
|
# type: (UserProfile, Dict[str, Any]) -> None
|
2017-03-06 03:11:44 +01:00
|
|
|
devices = list(PushDeviceToken.objects.filter(user=user_profile,
|
|
|
|
kind=PushDeviceToken.GCM))
|
|
|
|
send_android_push_notification(devices, data)
|
|
|
|
|
|
|
|
@statsd_increment("android_push_notification")
|
2017-05-16 21:15:45 +02:00
|
|
|
def send_android_push_notification(devices, data, remote=False):
|
|
|
|
# type: (List[DeviceToken], Dict[str, Any], bool) -> None
|
2013-12-09 23:19:59 +01:00
|
|
|
if not gcm:
|
2017-04-25 22:51:27 +02:00
|
|
|
logging.warning("Attempting to send a GCM push notification, but no API key was configured")
|
2013-12-09 23:19:59 +01:00
|
|
|
return
|
2017-03-06 03:11:44 +01:00
|
|
|
reg_ids = [device.token for device in devices]
|
2013-12-09 23:19:59 +01:00
|
|
|
|
2017-05-16 21:15:45 +02:00
|
|
|
if remote:
|
2017-05-17 07:16:20 +02:00
|
|
|
DeviceTokenClass = RemotePushDeviceToken
|
2017-05-16 21:15:45 +02:00
|
|
|
else:
|
|
|
|
DeviceTokenClass = PushDeviceToken
|
2017-05-09 12:15:11 +02:00
|
|
|
|
2017-05-17 09:58:27 +02:00
|
|
|
try:
|
2017-05-17 10:00:23 +02:00
|
|
|
res = gcm.json_request(registration_ids=reg_ids, data=data, retries=10)
|
2017-05-17 09:58:27 +02:00
|
|
|
except IOError as e:
|
|
|
|
logging.warning(str(e))
|
|
|
|
return
|
2013-12-09 23:19:59 +01:00
|
|
|
|
2016-12-13 08:41:48 +01:00
|
|
|
if res and 'success' in res:
|
|
|
|
for reg_id, msg_id in res['success'].items():
|
|
|
|
logging.info("GCM: Sent %s as %s" % (reg_id, msg_id))
|
2013-12-09 23:19:59 +01:00
|
|
|
|
2014-01-22 22:42:23 +01:00
|
|
|
# res.canonical will contain results when there are duplicate registrations for the same
|
|
|
|
# device. The "canonical" registration is the latest registration made by the device.
|
|
|
|
# Ref: http://developer.android.com/google/gcm/adv.html#canonical
|
2016-12-13 08:41:48 +01:00
|
|
|
if 'canonical' in res:
|
|
|
|
for reg_id, new_reg_id in res['canonical'].items():
|
|
|
|
if reg_id == new_reg_id:
|
|
|
|
# I'm not sure if this should happen. In any case, not really actionable.
|
|
|
|
logging.warning("GCM: Got canonical ref but it already matches our ID %s!" % (reg_id,))
|
2017-05-09 12:15:11 +02:00
|
|
|
elif not DeviceTokenClass.objects.filter(token=new_reg_id,
|
|
|
|
kind=DeviceTokenClass.GCM).count():
|
2016-12-13 08:41:48 +01:00
|
|
|
# This case shouldn't happen; any time we get a canonical ref it should have been
|
|
|
|
# previously registered in our system.
|
|
|
|
#
|
|
|
|
# That said, recovery is easy: just update the current PDT object to use the new ID.
|
|
|
|
logging.warning(
|
2017-01-24 07:06:13 +01:00
|
|
|
"GCM: Got canonical ref %s replacing %s but new ID not registered! Updating." %
|
|
|
|
(new_reg_id, reg_id))
|
2017-05-09 12:15:11 +02:00
|
|
|
DeviceTokenClass.objects.filter(
|
|
|
|
token=reg_id, kind=DeviceTokenClass.GCM).update(token=new_reg_id)
|
2016-12-13 08:41:48 +01:00
|
|
|
else:
|
|
|
|
# Since we know the new ID is registered in our system we can just drop the old one.
|
|
|
|
logging.info("GCM: Got canonical ref %s, dropping %s" % (new_reg_id, reg_id))
|
|
|
|
|
2017-05-09 12:15:11 +02:00
|
|
|
DeviceTokenClass.objects.filter(token=reg_id, kind=DeviceTokenClass.GCM).delete()
|
2016-12-13 08:41:48 +01:00
|
|
|
|
|
|
|
if 'errors' in res:
|
|
|
|
for error, reg_ids in res['errors'].items():
|
|
|
|
if error in ['NotRegistered', 'InvalidRegistration']:
|
|
|
|
for reg_id in reg_ids:
|
|
|
|
logging.info("GCM: Removing %s" % (reg_id,))
|
|
|
|
|
2017-05-09 12:15:11 +02:00
|
|
|
device = DeviceTokenClass.objects.get(token=reg_id, kind=DeviceTokenClass.GCM)
|
2016-12-13 08:41:48 +01:00
|
|
|
device.delete()
|
|
|
|
else:
|
|
|
|
for reg_id in reg_ids:
|
|
|
|
logging.warning("GCM: Delivery to %s failed: %s" % (reg_id, error))
|
|
|
|
|
|
|
|
# python-gcm handles retrying of the unsent messages.
|
|
|
|
# Ref: https://github.com/geeknam/python-gcm/blob/master/gcm/gcm.py#L497
|
2017-03-06 03:05:04 +01:00
|
|
|
|
2017-05-08 12:58:11 +02:00
|
|
|
def get_alert_from_message(message):
|
|
|
|
# type: (Message) -> Text
|
|
|
|
"""
|
|
|
|
Determine what alert string to display based on the missed messages.
|
|
|
|
"""
|
|
|
|
sender_str = message.sender.full_name
|
|
|
|
if message.recipient.type == Recipient.HUDDLE:
|
|
|
|
return "New private group message from %s" % (sender_str,)
|
|
|
|
elif message.recipient.type == Recipient.PERSONAL:
|
|
|
|
return "New private message from %s" % (sender_str,)
|
|
|
|
elif message.recipient.type == Recipient.STREAM:
|
|
|
|
return "New mention from %s" % (sender_str,)
|
|
|
|
else:
|
|
|
|
return "New Zulip mentions and private messages from %s" % (sender_str,)
|
|
|
|
|
2017-05-08 13:21:56 +02:00
|
|
|
def get_apns_payload(message):
|
|
|
|
# type: (Message) -> Dict[str, Any]
|
|
|
|
return {
|
|
|
|
'alert': get_alert_from_message(message),
|
|
|
|
'message_ids': [message.id],
|
|
|
|
}
|
|
|
|
|
2017-05-08 13:26:01 +02:00
|
|
|
def get_gcm_payload(user_profile, message):
|
|
|
|
# type: (UserProfile, Message) -> Dict[str, Any]
|
|
|
|
content = message.content
|
|
|
|
content_truncated = (len(content) > 200)
|
|
|
|
if content_truncated:
|
|
|
|
content = content[:200] + "..."
|
|
|
|
|
|
|
|
android_data = {
|
|
|
|
'user': user_profile.email,
|
|
|
|
'event': 'message',
|
|
|
|
'alert': get_alert_from_message(message),
|
2017-05-31 21:25:43 +02:00
|
|
|
'zulip_message_id': message.id, # message_id is reserved for CCS
|
2017-05-08 13:26:01 +02:00
|
|
|
'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,
|
2017-06-15 07:44:00 +02:00
|
|
|
'sender_avatar_url': absolute_avatar_url(message.sender),
|
2017-05-08 13:26:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
|
|
return android_data
|
|
|
|
|
2017-03-06 03:05:04 +01:00
|
|
|
@statsd_increment("push_notifications")
|
|
|
|
def handle_push_notification(user_profile_id, missed_message):
|
|
|
|
# type: (int, Dict[str, Any]) -> None
|
2017-08-18 09:04:52 +02:00
|
|
|
"""
|
|
|
|
missed_message is the event received by the
|
|
|
|
zerver.worker.queue_processors.PushNotificationWorker.consume function.
|
|
|
|
"""
|
2017-03-06 03:05:04 +01:00
|
|
|
try:
|
|
|
|
user_profile = get_user_profile_by_id(user_profile_id)
|
|
|
|
if not (receives_offline_notifications(user_profile) or receives_online_notifications(user_profile)):
|
|
|
|
return
|
|
|
|
|
|
|
|
umessage = UserMessage.objects.get(user_profile=user_profile,
|
|
|
|
message__id=missed_message['message_id'])
|
|
|
|
message = umessage.message
|
|
|
|
if umessage.flags.read:
|
|
|
|
return
|
|
|
|
|
2017-05-08 13:21:56 +02:00
|
|
|
apns_payload = get_apns_payload(message)
|
2017-05-08 13:26:01 +02:00
|
|
|
gcm_payload = get_gcm_payload(user_profile, message)
|
2017-05-08 13:21:56 +02:00
|
|
|
|
2017-05-08 13:48:16 +02:00
|
|
|
if uses_notification_bouncer():
|
2017-08-18 09:04:52 +02:00
|
|
|
try:
|
|
|
|
send_notifications_to_bouncer(user_profile_id,
|
|
|
|
apns_payload,
|
|
|
|
gcm_payload)
|
|
|
|
except requests.ConnectionError:
|
|
|
|
if 'failed_tries' not in missed_message:
|
|
|
|
missed_message['failed_tries'] = 0
|
|
|
|
|
|
|
|
def failure_processor(event):
|
|
|
|
# type: (Dict[str, Any]) -> None
|
|
|
|
logging.warning("Maximum retries exceeded for trigger:%s event:push_notification" % (event['user_profile_id']))
|
|
|
|
retry_event('missedmessage_mobile_notifications', missed_message,
|
|
|
|
failure_processor)
|
|
|
|
|
2017-05-08 13:48:16 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
android_devices = list(PushDeviceToken.objects.filter(user=user_profile,
|
|
|
|
kind=PushDeviceToken.GCM))
|
|
|
|
|
|
|
|
apple_devices = list(PushDeviceToken.objects.filter(user=user_profile,
|
|
|
|
kind=PushDeviceToken.APNS))
|
|
|
|
|
2017-05-12 07:50:18 +02:00
|
|
|
# TODO: set badge count in a better way
|
|
|
|
if apple_devices:
|
|
|
|
send_apple_push_notification(user_profile.id, apple_devices,
|
|
|
|
badge=1, zulip=apns_payload)
|
|
|
|
|
|
|
|
if android_devices:
|
|
|
|
send_android_push_notification(android_devices, gcm_payload)
|
2017-03-06 03:05:04 +01:00
|
|
|
|
|
|
|
except UserMessage.DoesNotExist:
|
|
|
|
logging.error("Could not find UserMessage with message_id %s" % (missed_message['message_id'],))
|
2017-03-06 03:57:31 +01:00
|
|
|
|
2017-05-08 13:48:16 +02:00
|
|
|
def send_notifications_to_bouncer(user_profile_id, apns_payload, gcm_payload):
|
|
|
|
# type: (int, Dict[str, Any], Dict[str, Any]) -> None
|
|
|
|
post_data = {
|
|
|
|
'user_id': user_profile_id,
|
|
|
|
'apns_payload': apns_payload,
|
|
|
|
'gcm_payload': gcm_payload,
|
|
|
|
}
|
|
|
|
send_json_to_push_bouncer('POST', 'notify', post_data)
|
|
|
|
|
2017-03-06 03:57:31 +01:00
|
|
|
def add_push_device_token(user_profile, token_str, kind, ios_app_id=None):
|
2017-07-07 19:54:37 +02:00
|
|
|
# type: (UserProfile, bytes, int, Optional[str]) -> None
|
2017-03-06 03:57:31 +01:00
|
|
|
|
2016-10-27 23:55:31 +02:00
|
|
|
# If we're sending things to the push notification bouncer
|
|
|
|
# register this user with them here
|
2017-05-08 13:47:18 +02:00
|
|
|
if uses_notification_bouncer():
|
2016-10-27 23:55:31 +02:00
|
|
|
post_data = {
|
|
|
|
'server_uuid': settings.ZULIP_ORG_ID,
|
|
|
|
'user_id': user_profile.id,
|
|
|
|
'token': token_str,
|
|
|
|
'token_kind': kind,
|
|
|
|
}
|
|
|
|
|
|
|
|
if kind == PushDeviceToken.APNS:
|
|
|
|
post_data['ios_app_id'] = ios_app_id
|
|
|
|
|
|
|
|
send_to_push_bouncer('POST', 'register', post_data)
|
|
|
|
return
|
|
|
|
|
2017-03-06 03:57:31 +01:00
|
|
|
# If another user was previously logged in on the same device and didn't
|
|
|
|
# properly log out, the token will still be registered to the wrong account
|
|
|
|
PushDeviceToken.objects.filter(token=token_str).exclude(user=user_profile).delete()
|
|
|
|
|
|
|
|
# Overwrite with the latest value
|
|
|
|
token, created = PushDeviceToken.objects.get_or_create(user=user_profile,
|
|
|
|
token=token_str,
|
|
|
|
defaults=dict(
|
|
|
|
kind=kind,
|
|
|
|
ios_app_id=ios_app_id))
|
|
|
|
if not created:
|
2017-04-15 04:03:56 +02:00
|
|
|
token.last_updated = timezone_now()
|
2017-03-06 03:57:31 +01:00
|
|
|
token.save(update_fields=['last_updated'])
|
2017-03-06 04:11:13 +01:00
|
|
|
|
2017-03-06 07:01:28 +01:00
|
|
|
def remove_push_device_token(user_profile, token_str, kind):
|
2017-07-07 19:54:37 +02:00
|
|
|
# type: (UserProfile, bytes, int) -> None
|
2016-10-27 23:55:31 +02:00
|
|
|
|
|
|
|
# If we're sending things to the push notification bouncer
|
|
|
|
# register this user with them here
|
2017-05-08 13:47:18 +02:00
|
|
|
if uses_notification_bouncer():
|
2016-10-27 23:55:31 +02:00
|
|
|
# TODO: Make this a remove item
|
|
|
|
post_data = {
|
|
|
|
'server_uuid': settings.ZULIP_ORG_ID,
|
|
|
|
'user_id': user_profile.id,
|
|
|
|
'token': token_str,
|
|
|
|
'token_kind': kind,
|
|
|
|
}
|
|
|
|
send_to_push_bouncer("POST", "unregister", post_data)
|
|
|
|
return
|
|
|
|
|
2017-03-06 04:11:13 +01:00
|
|
|
try:
|
|
|
|
token = PushDeviceToken.objects.get(token=token_str, kind=kind)
|
|
|
|
token.delete()
|
|
|
|
except PushDeviceToken.DoesNotExist:
|
|
|
|
raise JsonableError(_("Token does not exist"))
|
2016-10-27 23:55:31 +02:00
|
|
|
|
2017-05-08 13:48:16 +02:00
|
|
|
def send_json_to_push_bouncer(method, endpoint, post_data):
|
2016-10-27 23:55:31 +02:00
|
|
|
# type: (str, str, Dict[str, Any]) -> None
|
2017-05-08 13:48:16 +02:00
|
|
|
send_to_push_bouncer(
|
|
|
|
method,
|
|
|
|
endpoint,
|
|
|
|
ujson.dumps(post_data),
|
|
|
|
extra_headers={"Content-type": "application/json"},
|
|
|
|
)
|
|
|
|
|
|
|
|
def send_to_push_bouncer(method, endpoint, post_data, extra_headers=None):
|
|
|
|
# type: (str, str, Union[Text, Dict[str, Any]], Optional[Dict[str, Any]]) -> None
|
2016-10-27 23:55:31 +02:00
|
|
|
url = urllib.parse.urljoin(settings.PUSH_NOTIFICATION_BOUNCER_URL,
|
|
|
|
'/api/v1/remotes/push/' + endpoint)
|
|
|
|
api_auth = requests.auth.HTTPBasicAuth(settings.ZULIP_ORG_ID,
|
|
|
|
settings.ZULIP_ORG_KEY)
|
|
|
|
|
2017-05-08 13:48:16 +02:00
|
|
|
headers = {"User-agent": "ZulipServer/%s" % (ZULIP_VERSION,)}
|
|
|
|
if extra_headers is not None:
|
|
|
|
headers.update(extra_headers)
|
|
|
|
|
2016-10-27 23:55:31 +02:00
|
|
|
res = requests.request(method,
|
|
|
|
url,
|
2017-05-08 13:48:16 +02:00
|
|
|
data=post_data,
|
2016-10-27 23:55:31 +02:00
|
|
|
auth=api_auth,
|
|
|
|
timeout=30,
|
|
|
|
verify=True,
|
2017-05-08 13:48:16 +02:00
|
|
|
headers=headers)
|
2016-10-27 23:55:31 +02:00
|
|
|
|
|
|
|
# TODO: Think more carefully about how this error hanlding should work.
|
|
|
|
if res.status_code >= 500:
|
|
|
|
raise JsonableError(_("Error received from push notification bouncer"))
|
|
|
|
elif res.status_code >= 400:
|
|
|
|
try:
|
|
|
|
msg = ujson.loads(res.content)['msg']
|
|
|
|
except Exception:
|
|
|
|
raise JsonableError(_("Error received from push notification bouncer"))
|
|
|
|
raise JsonableError(msg)
|
|
|
|
elif res.status_code != 200:
|
|
|
|
raise JsonableError(_("Error received from push notification bouncer"))
|
|
|
|
|
|
|
|
# If we don't throw an exception, it's a successful bounce!
|