mirror of https://github.com/zulip/zulip.git
zilencer: Change push bouncer API to accept uuids as user identifier.
This is the first step to making the full switch to self-hosted servers use user uuids, per issue #18017. The old id format is still supported of course, for backward compatibility. This commit is separate in order to allow deploying *just* the bouncer API change to production first.
This commit is contained in:
parent
75f7426e21
commit
0677c90170
|
@ -13,7 +13,7 @@ import lxml.html
|
||||||
import orjson
|
import orjson
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import IntegrityError, transaction
|
from django.db import IntegrityError, transaction
|
||||||
from django.db.models import F
|
from django.db.models import F, Q
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.utils.translation import override as override_language
|
from django.utils.translation import override as override_language
|
||||||
|
@ -58,6 +58,49 @@ def hex_to_b64(data: str) -> str:
|
||||||
return base64.b64encode(bytes.fromhex(data)).decode()
|
return base64.b64encode(bytes.fromhex(data)).decode()
|
||||||
|
|
||||||
|
|
||||||
|
class UserPushIndentityCompat:
|
||||||
|
"""Compatibility class for supporting the transition from remote servers
|
||||||
|
sending their UserProfile ids to the bouncer to sending UserProfile uuids instead.
|
||||||
|
|
||||||
|
Until we can drop support for receiving user_id, we need this
|
||||||
|
class, because a user's identity in the push notification context
|
||||||
|
may be represented either by an id or uuid.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, user_id: Optional[int] = None, user_uuid: Optional[str] = None) -> None:
|
||||||
|
assert user_id is not None or user_uuid is not None
|
||||||
|
self.user_id = user_id
|
||||||
|
self.user_uuid = user_uuid
|
||||||
|
|
||||||
|
def filter_q(self) -> Q:
|
||||||
|
"""
|
||||||
|
This aims to support correctly querying for RemotePushDeviceToken.
|
||||||
|
If only one of (user_id, user_uuid) is provided, the situation is trivial,
|
||||||
|
If both are provided, we want to query for tokens matching EITHER the
|
||||||
|
uuid or the id - because the user may have devices with old registrations,
|
||||||
|
so user_id-based, as well as new registration with uuid. Notifications
|
||||||
|
naturally should be sent to both.
|
||||||
|
"""
|
||||||
|
if self.user_id is not None and self.user_uuid is None:
|
||||||
|
return Q(user_id=self.user_id)
|
||||||
|
elif self.user_uuid is not None and self.user_id is None:
|
||||||
|
return Q(user_uuid=self.user_uuid)
|
||||||
|
else:
|
||||||
|
assert self.user_id is not None and self.user_uuid is not None
|
||||||
|
return Q(user_uuid=self.user_uuid) | Q(user_id=self.user_id)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if self.user_uuid is not None:
|
||||||
|
return f"uuid:{self.user_uuid}"
|
||||||
|
|
||||||
|
return f"id:{self.user_id}"
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
if isinstance(other, UserPushIndentityCompat):
|
||||||
|
return self.user_id == other.user_id and self.user_uuid == other.user_uuid
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sending to APNs, for iOS
|
# Sending to APNs, for iOS
|
||||||
#
|
#
|
||||||
|
@ -134,7 +177,7 @@ APNS_MAX_RETRIES = 3
|
||||||
|
|
||||||
@statsd_increment("apple_push_notification")
|
@statsd_increment("apple_push_notification")
|
||||||
def send_apple_push_notification(
|
def send_apple_push_notification(
|
||||||
user_id: int,
|
user_identity: UserPushIndentityCompat,
|
||||||
devices: Sequence[DeviceToken],
|
devices: Sequence[DeviceToken],
|
||||||
payload_data: Dict[str, Any],
|
payload_data: Dict[str, Any],
|
||||||
remote: Optional["RemoteZulipServer"] = None,
|
remote: Optional["RemoteZulipServer"] = None,
|
||||||
|
@ -164,14 +207,16 @@ def send_apple_push_notification(
|
||||||
|
|
||||||
if remote:
|
if remote:
|
||||||
logger.info(
|
logger.info(
|
||||||
"APNs: Sending notification for remote user %s:%d to %d devices",
|
"APNs: Sending notification for remote user %s:%s to %d devices",
|
||||||
remote.uuid,
|
remote.uuid,
|
||||||
user_id,
|
user_identity,
|
||||||
len(devices),
|
len(devices),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
"APNs: Sending notification for local user %d to %d devices", user_id, len(devices)
|
"APNs: Sending notification for local user %s to %d devices",
|
||||||
|
user_identity,
|
||||||
|
len(devices),
|
||||||
)
|
)
|
||||||
payload_data = modernize_apns_payload(payload_data).copy()
|
payload_data = modernize_apns_payload(payload_data).copy()
|
||||||
message = {**payload_data.pop("custom", {}), "aps": payload_data}
|
message = {**payload_data.pop("custom", {}), "aps": payload_data}
|
||||||
|
@ -187,15 +232,17 @@ def send_apple_push_notification(
|
||||||
)
|
)
|
||||||
except aioapns.exceptions.ConnectionError as e:
|
except aioapns.exceptions.ConnectionError as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"APNs: ConnectionError sending for user %d to device %s: %s",
|
"APNs: ConnectionError sending for user %s to device %s: %s",
|
||||||
user_id,
|
user_identity,
|
||||||
device.token,
|
device.token,
|
||||||
e.__class__.__name__,
|
e.__class__.__name__,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if result.is_successful:
|
if result.is_successful:
|
||||||
logger.info("APNs: Success sending for user %d to device %s", user_id, device.token)
|
logger.info(
|
||||||
|
"APNs: Success sending for user %s to device %s", user_identity, device.token
|
||||||
|
)
|
||||||
elif result.description in ["Unregistered", "BadDeviceToken", "DeviceTokenNotForTopic"]:
|
elif result.description in ["Unregistered", "BadDeviceToken", "DeviceTokenNotForTopic"]:
|
||||||
logger.info(
|
logger.info(
|
||||||
"APNs: Removing invalid/expired token %s (%s)", device.token, result.description
|
"APNs: Removing invalid/expired token %s (%s)", device.token, result.description
|
||||||
|
@ -205,8 +252,8 @@ def send_apple_push_notification(
|
||||||
DeviceTokenClass.objects.filter(token=device.token, kind=DeviceTokenClass.APNS).delete()
|
DeviceTokenClass.objects.filter(token=device.token, kind=DeviceTokenClass.APNS).delete()
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"APNs: Failed to send for user %d to device %s: %s",
|
"APNs: Failed to send for user %s to device %s: %s",
|
||||||
user_id,
|
user_identity,
|
||||||
device.token,
|
device.token,
|
||||||
result.description,
|
result.description,
|
||||||
)
|
)
|
||||||
|
@ -256,7 +303,9 @@ def send_android_push_notification_to_user(
|
||||||
user_profile: UserProfile, data: Dict[str, Any], options: Dict[str, Any]
|
user_profile: UserProfile, data: Dict[str, Any], options: Dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
devices = list(PushDeviceToken.objects.filter(user=user_profile, kind=PushDeviceToken.GCM))
|
devices = list(PushDeviceToken.objects.filter(user=user_profile, kind=PushDeviceToken.GCM))
|
||||||
send_android_push_notification(user_profile.id, devices, data, options)
|
send_android_push_notification(
|
||||||
|
UserPushIndentityCompat(user_id=user_profile.id), devices, data, options
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_gcm_options(options: Dict[str, Any], data: Dict[str, Any]) -> str:
|
def parse_gcm_options(options: Dict[str, Any], data: Dict[str, Any]) -> str:
|
||||||
|
@ -306,7 +355,7 @@ def parse_gcm_options(options: Dict[str, Any], data: Dict[str, Any]) -> str:
|
||||||
|
|
||||||
@statsd_increment("android_push_notification")
|
@statsd_increment("android_push_notification")
|
||||||
def send_android_push_notification(
|
def send_android_push_notification(
|
||||||
user_id: int,
|
user_identity: UserPushIndentityCompat,
|
||||||
devices: Sequence[DeviceToken],
|
devices: Sequence[DeviceToken],
|
||||||
data: Dict[str, Any],
|
data: Dict[str, Any],
|
||||||
options: Dict[str, Any],
|
options: Dict[str, Any],
|
||||||
|
@ -334,14 +383,14 @@ def send_android_push_notification(
|
||||||
|
|
||||||
if remote:
|
if remote:
|
||||||
logger.info(
|
logger.info(
|
||||||
"GCM: Sending notification for remote user %s:%d to %d devices",
|
"GCM: Sending notification for remote user %s:%s to %d devices",
|
||||||
remote.uuid,
|
remote.uuid,
|
||||||
user_id,
|
user_identity,
|
||||||
len(devices),
|
len(devices),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
"GCM: Sending notification for local user %d to %d devices", user_id, len(devices)
|
"GCM: Sending notification for local user %s to %d devices", user_identity, len(devices)
|
||||||
)
|
)
|
||||||
reg_ids = [device.token for device in devices]
|
reg_ids = [device.token for device in devices]
|
||||||
priority = parse_gcm_options(options, data)
|
priority = parse_gcm_options(options, data)
|
||||||
|
@ -947,6 +996,7 @@ def handle_remove_push_notification(user_profile_id: int, message_ids: List[int]
|
||||||
if uses_notification_bouncer():
|
if uses_notification_bouncer():
|
||||||
send_notifications_to_bouncer(user_profile_id, apns_payload, gcm_payload, gcm_options)
|
send_notifications_to_bouncer(user_profile_id, apns_payload, gcm_payload, gcm_options)
|
||||||
else:
|
else:
|
||||||
|
user_identity = UserPushIndentityCompat(user_id=user_profile_id)
|
||||||
android_devices = list(
|
android_devices = list(
|
||||||
PushDeviceToken.objects.filter(user=user_profile, kind=PushDeviceToken.GCM)
|
PushDeviceToken.objects.filter(user=user_profile, kind=PushDeviceToken.GCM)
|
||||||
)
|
)
|
||||||
|
@ -954,11 +1004,9 @@ def handle_remove_push_notification(user_profile_id: int, message_ids: List[int]
|
||||||
PushDeviceToken.objects.filter(user=user_profile, kind=PushDeviceToken.APNS)
|
PushDeviceToken.objects.filter(user=user_profile, kind=PushDeviceToken.APNS)
|
||||||
)
|
)
|
||||||
if android_devices:
|
if android_devices:
|
||||||
send_android_push_notification(
|
send_android_push_notification(user_identity, android_devices, gcm_payload, gcm_options)
|
||||||
user_profile_id, android_devices, gcm_payload, gcm_options
|
|
||||||
)
|
|
||||||
if apple_devices:
|
if apple_devices:
|
||||||
send_apple_push_notification(user_profile_id, apple_devices, apns_payload)
|
send_apple_push_notification(user_identity, apple_devices, apns_payload)
|
||||||
|
|
||||||
# We intentionally use the non-truncated message_ids here. We are
|
# We intentionally use the non-truncated message_ids here. We are
|
||||||
# assuming in this very rare case that the user has manually
|
# assuming in this very rare case that the user has manually
|
||||||
|
@ -1077,5 +1125,6 @@ def handle_push_notification(user_profile_id: int, missed_message: Dict[str, Any
|
||||||
len(android_devices),
|
len(android_devices),
|
||||||
len(apple_devices),
|
len(apple_devices),
|
||||||
)
|
)
|
||||||
send_apple_push_notification(user_profile.id, apple_devices, apns_payload)
|
user_identity = UserPushIndentityCompat(user_id=user_profile.id)
|
||||||
send_android_push_notification(user_profile.id, android_devices, gcm_payload, gcm_options)
|
send_apple_push_notification(user_identity, apple_devices, apns_payload)
|
||||||
|
send_android_push_notification(user_identity, android_devices, gcm_payload, gcm_options)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import orjson
|
||||||
import responses
|
import responses
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import F
|
from django.db.models import F, Q
|
||||||
from django.http.response import ResponseHeaders
|
from django.http.response import ResponseHeaders
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
|
@ -34,6 +34,7 @@ from zerver.lib.exceptions import JsonableError
|
||||||
from zerver.lib.push_notifications import (
|
from zerver.lib.push_notifications import (
|
||||||
APNsContext,
|
APNsContext,
|
||||||
DeviceToken,
|
DeviceToken,
|
||||||
|
UserPushIndentityCompat,
|
||||||
b64_to_hex,
|
b64_to_hex,
|
||||||
get_apns_badge_count,
|
get_apns_badge_count,
|
||||||
get_apns_badge_count_future,
|
get_apns_badge_count_future,
|
||||||
|
@ -192,7 +193,13 @@ class PushBouncerNotificationTest(BouncerTestCase):
|
||||||
result = self.uuid_post(
|
result = self.uuid_post(
|
||||||
self.server_uuid, endpoint, {"token": token, "token_kind": token_kind}
|
self.server_uuid, endpoint, {"token": token, "token_kind": token_kind}
|
||||||
)
|
)
|
||||||
self.assert_json_error(result, "Missing 'user_id' argument")
|
self.assert_json_error(result, "Missing user_id or user_uuid")
|
||||||
|
result = self.uuid_post(
|
||||||
|
self.server_uuid,
|
||||||
|
endpoint,
|
||||||
|
{"user_id": user_id, "user_uuid": "xxx", "token": token, "token_kind": token_kind},
|
||||||
|
)
|
||||||
|
self.assert_json_error(result, "Specify only one of user_id or user_uuid")
|
||||||
result = self.uuid_post(
|
result = self.uuid_post(
|
||||||
self.server_uuid, endpoint, {"user_id": user_id, "token": token, "token_kind": 17}
|
self.server_uuid, endpoint, {"user_id": user_id, "token": token, "token_kind": 17}
|
||||||
)
|
)
|
||||||
|
@ -367,12 +374,14 @@ class PushBouncerNotificationTest(BouncerTestCase):
|
||||||
logger.output,
|
logger.output,
|
||||||
[
|
[
|
||||||
"INFO:zilencer.views:"
|
"INFO:zilencer.views:"
|
||||||
f"Sending mobile push notifications for remote user 6cde5f7a-1f7e-4978-9716-49f69ebfc9fe:{hamlet.id}: "
|
f"Sending mobile push notifications for remote user 6cde5f7a-1f7e-4978-9716-49f69ebfc9fe:id:{hamlet.id}: "
|
||||||
"2 via FCM devices, 1 via APNs devices"
|
"2 via FCM devices, 1 via APNs devices"
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user_identity = UserPushIndentityCompat(user_id=hamlet.id)
|
||||||
apple_push.assert_called_once_with(
|
apple_push.assert_called_once_with(
|
||||||
hamlet.id,
|
user_identity,
|
||||||
[apple_token],
|
[apple_token],
|
||||||
{
|
{
|
||||||
"badge": 0,
|
"badge": 0,
|
||||||
|
@ -386,7 +395,7 @@ class PushBouncerNotificationTest(BouncerTestCase):
|
||||||
remote=server,
|
remote=server,
|
||||||
)
|
)
|
||||||
android_push.assert_called_once_with(
|
android_push.assert_called_once_with(
|
||||||
hamlet.id,
|
user_identity,
|
||||||
list(reversed(android_tokens)),
|
list(reversed(android_tokens)),
|
||||||
{"event": "remove", "zulip_message_ids": ",".join(str(i) for i in range(50, 250))},
|
{"event": "remove", "zulip_message_ids": ",".join(str(i) for i in range(50, 250))},
|
||||||
{},
|
{},
|
||||||
|
@ -962,7 +971,9 @@ class HandlePushNotificationTest(PushNotificationTest):
|
||||||
(b64_to_hex(device.token), device.ios_app_id, device.token)
|
(b64_to_hex(device.token), device.ios_app_id, device.token)
|
||||||
for device in RemotePushDeviceToken.objects.filter(kind=PushDeviceToken.GCM)
|
for device in RemotePushDeviceToken.objects.filter(kind=PushDeviceToken.GCM)
|
||||||
]
|
]
|
||||||
mock_gcm.json_request.return_value = {"success": {gcm_devices[0][2]: message.id}}
|
mock_gcm.json_request.return_value = {
|
||||||
|
"success": {device[2]: message.id for device in gcm_devices}
|
||||||
|
}
|
||||||
result = mock.Mock()
|
result = mock.Mock()
|
||||||
result.is_successful = True
|
result.is_successful = True
|
||||||
apns_context.apns.send_notification.return_value = asyncio.Future(
|
apns_context.apns.send_notification.return_value = asyncio.Future(
|
||||||
|
@ -974,14 +985,14 @@ class HandlePushNotificationTest(PushNotificationTest):
|
||||||
views_logger.output,
|
views_logger.output,
|
||||||
[
|
[
|
||||||
"INFO:zilencer.views:"
|
"INFO:zilencer.views:"
|
||||||
f"Sending mobile push notifications for remote user 6cde5f7a-1f7e-4978-9716-49f69ebfc9fe:{self.user_profile.id}: "
|
f"Sending mobile push notifications for remote user 6cde5f7a-1f7e-4978-9716-49f69ebfc9fe:id:{self.user_profile.id}: "
|
||||||
f"{len(gcm_devices)} via FCM devices, {len(apns_devices)} via APNs devices"
|
f"{len(gcm_devices)} via FCM devices, {len(apns_devices)} via APNs devices"
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
for _, _, token in apns_devices:
|
for _, _, token in apns_devices:
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"INFO:zerver.lib.push_notifications:"
|
"INFO:zerver.lib.push_notifications:"
|
||||||
f"APNs: Success sending for user {self.user_profile.id} to device {token}",
|
f"APNs: Success sending for user id:{self.user_profile.id} to device {token}",
|
||||||
pn_logger.output,
|
pn_logger.output,
|
||||||
)
|
)
|
||||||
for _, _, token in gcm_devices:
|
for _, _, token in gcm_devices:
|
||||||
|
@ -1035,7 +1046,7 @@ class HandlePushNotificationTest(PushNotificationTest):
|
||||||
views_logger.output,
|
views_logger.output,
|
||||||
[
|
[
|
||||||
"INFO:zilencer.views:"
|
"INFO:zilencer.views:"
|
||||||
f"Sending mobile push notifications for remote user 6cde5f7a-1f7e-4978-9716-49f69ebfc9fe:{self.user_profile.id}: "
|
f"Sending mobile push notifications for remote user 6cde5f7a-1f7e-4978-9716-49f69ebfc9fe:id:{self.user_profile.id}: "
|
||||||
f"{len(gcm_devices)} via FCM devices, {len(apns_devices)} via APNs devices"
|
f"{len(gcm_devices)} via FCM devices, {len(apns_devices)} via APNs devices"
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -1249,10 +1260,9 @@ class HandlePushNotificationTest(PushNotificationTest):
|
||||||
) as mock_push_notifications:
|
) as mock_push_notifications:
|
||||||
|
|
||||||
handle_push_notification(self.user_profile.id, missed_message)
|
handle_push_notification(self.user_profile.id, missed_message)
|
||||||
mock_send_apple.assert_called_with(self.user_profile.id, apple_devices, {"apns": True})
|
user_identity = UserPushIndentityCompat(user_id=self.user_profile.id)
|
||||||
mock_send_android.assert_called_with(
|
mock_send_apple.assert_called_with(user_identity, apple_devices, {"apns": True})
|
||||||
self.user_profile.id, android_devices, {"gcm": True}, {}
|
mock_send_android.assert_called_with(user_identity, android_devices, {"gcm": True}, {})
|
||||||
)
|
|
||||||
mock_push_notifications.assert_called_once()
|
mock_push_notifications.assert_called_once()
|
||||||
|
|
||||||
def test_send_remove_notifications_to_bouncer(self) -> None:
|
def test_send_remove_notifications_to_bouncer(self) -> None:
|
||||||
|
@ -1324,8 +1334,9 @@ class HandlePushNotificationTest(PushNotificationTest):
|
||||||
) as mock_send_apple:
|
) as mock_send_apple:
|
||||||
handle_remove_push_notification(self.user_profile.id, [message.id])
|
handle_remove_push_notification(self.user_profile.id, [message.id])
|
||||||
mock_push_notifications.assert_called_once()
|
mock_push_notifications.assert_called_once()
|
||||||
|
user_identity = UserPushIndentityCompat(user_id=self.user_profile.id)
|
||||||
mock_send_android.assert_called_with(
|
mock_send_android.assert_called_with(
|
||||||
self.user_profile.id,
|
user_identity,
|
||||||
android_devices,
|
android_devices,
|
||||||
{
|
{
|
||||||
"server": "testserver",
|
"server": "testserver",
|
||||||
|
@ -1339,7 +1350,7 @@ class HandlePushNotificationTest(PushNotificationTest):
|
||||||
{"priority": "normal"},
|
{"priority": "normal"},
|
||||||
)
|
)
|
||||||
mock_send_apple.assert_called_with(
|
mock_send_apple.assert_called_with(
|
||||||
self.user_profile.id,
|
user_identity,
|
||||||
apple_devices,
|
apple_devices,
|
||||||
{
|
{
|
||||||
"badge": 0,
|
"badge": 0,
|
||||||
|
@ -1451,10 +1462,9 @@ class HandlePushNotificationTest(PushNotificationTest):
|
||||||
) as mock_push_notifications:
|
) as mock_push_notifications:
|
||||||
handle_push_notification(self.user_profile.id, missed_message)
|
handle_push_notification(self.user_profile.id, missed_message)
|
||||||
mock_logger.assert_not_called()
|
mock_logger.assert_not_called()
|
||||||
mock_send_apple.assert_called_with(self.user_profile.id, apple_devices, {"apns": True})
|
user_identity = UserPushIndentityCompat(user_id=self.user_profile.id)
|
||||||
mock_send_android.assert_called_with(
|
mock_send_apple.assert_called_with(user_identity, apple_devices, {"apns": True})
|
||||||
self.user_profile.id, android_devices, {"gcm": True}, {}
|
mock_send_android.assert_called_with(user_identity, android_devices, {"gcm": True}, {})
|
||||||
)
|
|
||||||
mock_push_notifications.assert_called_once()
|
mock_push_notifications.assert_called_once()
|
||||||
|
|
||||||
@mock.patch("zerver.lib.push_notifications.logger.info")
|
@mock.patch("zerver.lib.push_notifications.logger.info")
|
||||||
|
@ -1492,7 +1502,9 @@ class TestAPNs(PushNotificationTest):
|
||||||
payload_data: Dict[str, Any] = {},
|
payload_data: Dict[str, Any] = {},
|
||||||
) -> None:
|
) -> None:
|
||||||
send_apple_push_notification(
|
send_apple_push_notification(
|
||||||
self.user_profile.id, devices if devices is not None else self.devices(), payload_data
|
UserPushIndentityCompat(user_id=self.user_profile.id),
|
||||||
|
devices if devices is not None else self.devices(),
|
||||||
|
payload_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_apns_context(self) -> None:
|
def test_get_apns_context(self) -> None:
|
||||||
|
@ -1559,7 +1571,7 @@ class TestAPNs(PushNotificationTest):
|
||||||
self.send()
|
self.send()
|
||||||
for device in self.devices():
|
for device in self.devices():
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
f"INFO:zerver.lib.push_notifications:APNs: Success sending for user {self.user_profile.id} to device {device.token}",
|
f"INFO:zerver.lib.push_notifications:APNs: Success sending for user id:{self.user_profile.id} to device {device.token}",
|
||||||
logger.output,
|
logger.output,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1576,7 +1588,7 @@ class TestAPNs(PushNotificationTest):
|
||||||
)
|
)
|
||||||
self.send(devices=self.devices()[0:1])
|
self.send(devices=self.devices()[0:1])
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
f"WARNING:zerver.lib.push_notifications:APNs: ConnectionError sending for user {self.user_profile.id} to device {self.devices()[0].token}: ConnectionError",
|
f"WARNING:zerver.lib.push_notifications:APNs: ConnectionError sending for user id:{self.user_profile.id} to device {self.devices()[0].token}: ConnectionError",
|
||||||
logger.output,
|
logger.output,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1594,7 +1606,7 @@ class TestAPNs(PushNotificationTest):
|
||||||
apns_context.apns.send_notification.return_value.set_result(result)
|
apns_context.apns.send_notification.return_value.set_result(result)
|
||||||
self.send(devices=self.devices()[0:1])
|
self.send(devices=self.devices()[0:1])
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
f"WARNING:zerver.lib.push_notifications:APNs: Failed to send for user {self.user_profile.id} to device {self.devices()[0].token}: InternalServerError",
|
f"WARNING:zerver.lib.push_notifications:APNs: Failed to send for user id:{self.user_profile.id} to device {self.devices()[0].token}: InternalServerError",
|
||||||
logger.output,
|
logger.output,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2340,7 +2352,7 @@ class GCMSendTest(PushNotificationTest):
|
||||||
with self.assertLogs("zerver.lib.push_notifications", level="INFO") as logger:
|
with self.assertLogs("zerver.lib.push_notifications", level="INFO") as logger:
|
||||||
send_android_push_notification_to_user(self.user_profile, data, {})
|
send_android_push_notification_to_user(self.user_profile, data, {})
|
||||||
self.assert_length(logger.output, 3)
|
self.assert_length(logger.output, 3)
|
||||||
log_msg1 = f"INFO:zerver.lib.push_notifications:GCM: Sending notification for local user {self.user_profile.id} to 2 devices"
|
log_msg1 = f"INFO:zerver.lib.push_notifications:GCM: Sending notification for local user id:{self.user_profile.id} to 2 devices"
|
||||||
log_msg2 = f"INFO:zerver.lib.push_notifications:GCM: Sent {1111} as {0}"
|
log_msg2 = f"INFO:zerver.lib.push_notifications:GCM: Sent {1111} as {0}"
|
||||||
log_msg3 = f"INFO:zerver.lib.push_notifications:GCM: Sent {2222} as {1}"
|
log_msg3 = f"INFO:zerver.lib.push_notifications:GCM: Sent {2222} as {1}"
|
||||||
self.assertEqual([log_msg1, log_msg2, log_msg3], logger.output)
|
self.assertEqual([log_msg1, log_msg2, log_msg3], logger.output)
|
||||||
|
@ -2400,7 +2412,7 @@ class GCMSendTest(PushNotificationTest):
|
||||||
with self.assertLogs("zerver.lib.push_notifications", level="INFO") as logger:
|
with self.assertLogs("zerver.lib.push_notifications", level="INFO") as logger:
|
||||||
send_android_push_notification_to_user(self.user_profile, data, {})
|
send_android_push_notification_to_user(self.user_profile, data, {})
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
f"INFO:zerver.lib.push_notifications:GCM: Sending notification for local user {self.user_profile.id} to 2 devices",
|
f"INFO:zerver.lib.push_notifications:GCM: Sending notification for local user id:{self.user_profile.id} to 2 devices",
|
||||||
logger.output[0],
|
logger.output[0],
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -2427,7 +2439,7 @@ class GCMSendTest(PushNotificationTest):
|
||||||
with self.assertLogs("zerver.lib.push_notifications", level="INFO") as logger:
|
with self.assertLogs("zerver.lib.push_notifications", level="INFO") as logger:
|
||||||
send_android_push_notification_to_user(self.user_profile, data, {})
|
send_android_push_notification_to_user(self.user_profile, data, {})
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
f"INFO:zerver.lib.push_notifications:GCM: Sending notification for local user {self.user_profile.id} to 2 devices",
|
f"INFO:zerver.lib.push_notifications:GCM: Sending notification for local user id:{self.user_profile.id} to 2 devices",
|
||||||
logger.output[0],
|
logger.output[0],
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -2668,3 +2680,24 @@ class PushBouncerSignupTest(ZulipTestCase):
|
||||||
self.assert_json_error(
|
self.assert_json_error(
|
||||||
result, f"Zulip server auth failure: key does not match role {zulip_org_id}"
|
result, f"Zulip server auth failure: key does not match role {zulip_org_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserPushIndentityCompat(ZulipTestCase):
|
||||||
|
def test_filter_q(self) -> None:
|
||||||
|
user_identity_id = UserPushIndentityCompat(user_id=1)
|
||||||
|
user_identity_uuid = UserPushIndentityCompat(user_uuid="aaaa")
|
||||||
|
user_identity_both = UserPushIndentityCompat(user_id=1, user_uuid="aaaa")
|
||||||
|
|
||||||
|
self.assertEqual(user_identity_id.filter_q(), Q(user_id=1))
|
||||||
|
self.assertEqual(user_identity_uuid.filter_q(), Q(user_uuid="aaaa"))
|
||||||
|
self.assertEqual(user_identity_both.filter_q(), Q(user_uuid="aaaa") | Q(user_id=1))
|
||||||
|
|
||||||
|
def test_eq(self) -> None:
|
||||||
|
user_identity_a = UserPushIndentityCompat(user_id=1)
|
||||||
|
user_identity_b = UserPushIndentityCompat(user_id=1)
|
||||||
|
user_identity_c = UserPushIndentityCompat(user_id=2)
|
||||||
|
self.assertEqual(user_identity_a, user_identity_b)
|
||||||
|
self.assertNotEqual(user_identity_a, user_identity_c)
|
||||||
|
|
||||||
|
# An integer can't be equal to an instance of the class.
|
||||||
|
self.assertNotEqual(user_identity_a, 1)
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 3.2.9 on 2021-12-27 21:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("zilencer", "0023_remotezulipserver_deactivated"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="remotepushdevicetoken",
|
||||||
|
name="user_id",
|
||||||
|
field=models.BigIntegerField(db_index=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="remotepushdevicetoken",
|
||||||
|
name="user_uuid",
|
||||||
|
field=models.UUIDField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="remotepushdevicetoken",
|
||||||
|
unique_together={
|
||||||
|
("server", "user_uuid", "kind", "token"),
|
||||||
|
("server", "user_id", "kind", "token"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -63,10 +63,18 @@ class RemotePushDeviceToken(AbstractPushDeviceToken):
|
||||||
|
|
||||||
server: RemoteZulipServer = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE)
|
server: RemoteZulipServer = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE)
|
||||||
# The user id on the remote server for this device
|
# The user id on the remote server for this device
|
||||||
user_id: int = models.BigIntegerField(db_index=True)
|
user_id: int = models.BigIntegerField(db_index=True, null=True)
|
||||||
|
user_uuid: UUID = models.UUIDField(null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ("server", "user_id", "kind", "token")
|
unique_together = [
|
||||||
|
# These indexes rely on the property that in Postgres,
|
||||||
|
# NULL != NULL in the context of unique indexes, so multiple
|
||||||
|
# rows with the same values in these columns can exist
|
||||||
|
# if one of them is NULL.
|
||||||
|
("server", "user_id", "kind", "token"),
|
||||||
|
("server", "user_uuid", "kind", "token"),
|
||||||
|
]
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"<RemotePushDeviceToken {self.server} {self.user_id}>"
|
return f"<RemotePushDeviceToken {self.server} {self.user_id}>"
|
||||||
|
|
|
@ -17,6 +17,7 @@ from corporate.lib.stripe import do_deactivate_remote_server
|
||||||
from zerver.decorator import InvalidZulipServerKeyError, require_post
|
from zerver.decorator import InvalidZulipServerKeyError, require_post
|
||||||
from zerver.lib.exceptions import JsonableError
|
from zerver.lib.exceptions import JsonableError
|
||||||
from zerver.lib.push_notifications import (
|
from zerver.lib.push_notifications import (
|
||||||
|
UserPushIndentityCompat,
|
||||||
send_android_push_notification,
|
send_android_push_notification,
|
||||||
send_apple_push_notification,
|
send_apple_push_notification,
|
||||||
)
|
)
|
||||||
|
@ -153,17 +154,28 @@ def register_remote_server(
|
||||||
def register_remote_push_device(
|
def register_remote_push_device(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
entity: Union[UserProfile, RemoteZulipServer],
|
entity: Union[UserProfile, RemoteZulipServer],
|
||||||
user_id: int = REQ(json_validator=check_int),
|
user_id: Optional[int] = REQ(json_validator=check_int, default=None),
|
||||||
|
user_uuid: Optional[str] = REQ(default=None),
|
||||||
token: str = REQ(),
|
token: str = REQ(),
|
||||||
token_kind: int = REQ(json_validator=check_int),
|
token_kind: int = REQ(json_validator=check_int),
|
||||||
ios_app_id: Optional[str] = None,
|
ios_app_id: Optional[str] = None,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
server = validate_bouncer_token_request(entity, token, token_kind)
|
server = validate_bouncer_token_request(entity, token, token_kind)
|
||||||
|
|
||||||
|
if user_id is None and user_uuid is None:
|
||||||
|
raise JsonableError(_("Missing user_id or user_uuid"))
|
||||||
|
if user_id is not None and user_uuid is not None:
|
||||||
|
# We don't want "hybrid" registrations with both.
|
||||||
|
# Our RemotePushDeviceToken should be either in the new uuid format
|
||||||
|
# or the legacy id one.
|
||||||
|
raise JsonableError(_("Specify only one of user_id or user_uuid"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
RemotePushDeviceToken.objects.create(
|
RemotePushDeviceToken.objects.create(
|
||||||
|
# Exactly one of these two user identity fields will be None.
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
user_uuid=user_uuid,
|
||||||
server=server,
|
server=server,
|
||||||
kind=token_kind,
|
kind=token_kind,
|
||||||
token=token,
|
token=token,
|
||||||
|
@ -183,12 +195,15 @@ def unregister_remote_push_device(
|
||||||
entity: Union[UserProfile, RemoteZulipServer],
|
entity: Union[UserProfile, RemoteZulipServer],
|
||||||
token: str = REQ(),
|
token: str = REQ(),
|
||||||
token_kind: int = REQ(json_validator=check_int),
|
token_kind: int = REQ(json_validator=check_int),
|
||||||
user_id: int = REQ(json_validator=check_int),
|
user_id: Optional[int] = REQ(json_validator=check_int, default=None),
|
||||||
|
user_uuid: Optional[str] = REQ(default=None),
|
||||||
ios_app_id: Optional[str] = None,
|
ios_app_id: Optional[str] = None,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
server = validate_bouncer_token_request(entity, token, token_kind)
|
server = validate_bouncer_token_request(entity, token, token_kind)
|
||||||
|
user_identity = UserPushIndentityCompat(user_id=user_id, user_uuid=user_uuid)
|
||||||
|
|
||||||
deleted = RemotePushDeviceToken.objects.filter(
|
deleted = RemotePushDeviceToken.objects.filter(
|
||||||
token=token, kind=token_kind, user_id=user_id, server=server
|
user_identity.filter_q(), token=token, kind=token_kind, server=server
|
||||||
).delete()
|
).delete()
|
||||||
if deleted[0] == 0:
|
if deleted[0] == 0:
|
||||||
raise JsonableError(err_("Token does not exist"))
|
raise JsonableError(err_("Token does not exist"))
|
||||||
|
@ -200,10 +215,13 @@ def unregister_remote_push_device(
|
||||||
def unregister_all_remote_push_devices(
|
def unregister_all_remote_push_devices(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
entity: Union[UserProfile, RemoteZulipServer],
|
entity: Union[UserProfile, RemoteZulipServer],
|
||||||
user_id: int = REQ(json_validator=check_int),
|
user_id: Optional[int] = REQ(json_validator=check_int, default=None),
|
||||||
|
user_uuid: Optional[str] = REQ(default=None),
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
server = validate_entity(entity)
|
server = validate_entity(entity)
|
||||||
RemotePushDeviceToken.objects.filter(user_id=user_id, server=server).delete()
|
user_identity = UserPushIndentityCompat(user_id=user_id, user_uuid=user_uuid)
|
||||||
|
|
||||||
|
RemotePushDeviceToken.objects.filter(user_identity.filter_q(), server=server).delete()
|
||||||
return json_success(request)
|
return json_success(request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -215,14 +233,15 @@ def remote_server_notify_push(
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
server = validate_entity(entity)
|
server = validate_entity(entity)
|
||||||
|
|
||||||
user_id = payload["user_id"]
|
user_identity = UserPushIndentityCompat(payload.get("user_id"), payload.get("user_uuid"))
|
||||||
|
|
||||||
gcm_payload = payload["gcm_payload"]
|
gcm_payload = payload["gcm_payload"]
|
||||||
apns_payload = payload["apns_payload"]
|
apns_payload = payload["apns_payload"]
|
||||||
gcm_options = payload.get("gcm_options", {})
|
gcm_options = payload.get("gcm_options", {})
|
||||||
|
|
||||||
android_devices = list(
|
android_devices = list(
|
||||||
RemotePushDeviceToken.objects.filter(
|
RemotePushDeviceToken.objects.filter(
|
||||||
user_id=user_id,
|
user_identity.filter_q(),
|
||||||
kind=RemotePushDeviceToken.GCM,
|
kind=RemotePushDeviceToken.GCM,
|
||||||
server=server,
|
server=server,
|
||||||
)
|
)
|
||||||
|
@ -230,7 +249,7 @@ def remote_server_notify_push(
|
||||||
|
|
||||||
apple_devices = list(
|
apple_devices = list(
|
||||||
RemotePushDeviceToken.objects.filter(
|
RemotePushDeviceToken.objects.filter(
|
||||||
user_id=user_id,
|
user_identity.filter_q(),
|
||||||
kind=RemotePushDeviceToken.APNS,
|
kind=RemotePushDeviceToken.APNS,
|
||||||
server=server,
|
server=server,
|
||||||
)
|
)
|
||||||
|
@ -239,7 +258,7 @@ def remote_server_notify_push(
|
||||||
logger.info(
|
logger.info(
|
||||||
"Sending mobile push notifications for remote user %s:%s: %s via FCM devices, %s via APNs devices",
|
"Sending mobile push notifications for remote user %s:%s: %s via FCM devices, %s via APNs devices",
|
||||||
server.uuid,
|
server.uuid,
|
||||||
user_id,
|
user_identity,
|
||||||
len(android_devices),
|
len(android_devices),
|
||||||
len(apple_devices),
|
len(apple_devices),
|
||||||
)
|
)
|
||||||
|
@ -265,14 +284,14 @@ def remote_server_notify_push(
|
||||||
|
|
||||||
gcm_payload = truncate_payload(gcm_payload)
|
gcm_payload = truncate_payload(gcm_payload)
|
||||||
send_android_push_notification(
|
send_android_push_notification(
|
||||||
user_id, android_devices, gcm_payload, gcm_options, remote=server
|
user_identity, android_devices, gcm_payload, gcm_options, remote=server
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(apns_payload.get("custom"), dict) and isinstance(
|
if isinstance(apns_payload.get("custom"), dict) and isinstance(
|
||||||
apns_payload["custom"].get("zulip"), dict
|
apns_payload["custom"].get("zulip"), dict
|
||||||
):
|
):
|
||||||
apns_payload["custom"]["zulip"] = truncate_payload(apns_payload["custom"]["zulip"])
|
apns_payload["custom"]["zulip"] = truncate_payload(apns_payload["custom"]["zulip"])
|
||||||
send_apple_push_notification(user_id, apple_devices, apns_payload, remote=server)
|
send_apple_push_notification(user_identity, apple_devices, apns_payload, remote=server)
|
||||||
|
|
||||||
return json_success(
|
return json_success(
|
||||||
request,
|
request,
|
||||||
|
|
Loading…
Reference in New Issue