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
|
||||
from django.conf import settings
|
||||
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.translation import gettext as _
|
||||
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()
|
||||
|
||||
|
||||
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
|
||||
#
|
||||
|
@ -134,7 +177,7 @@ APNS_MAX_RETRIES = 3
|
|||
|
||||
@statsd_increment("apple_push_notification")
|
||||
def send_apple_push_notification(
|
||||
user_id: int,
|
||||
user_identity: UserPushIndentityCompat,
|
||||
devices: Sequence[DeviceToken],
|
||||
payload_data: Dict[str, Any],
|
||||
remote: Optional["RemoteZulipServer"] = None,
|
||||
|
@ -164,14 +207,16 @@ def send_apple_push_notification(
|
|||
|
||||
if remote:
|
||||
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,
|
||||
user_id,
|
||||
user_identity,
|
||||
len(devices),
|
||||
)
|
||||
else:
|
||||
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()
|
||||
message = {**payload_data.pop("custom", {}), "aps": payload_data}
|
||||
|
@ -187,15 +232,17 @@ def send_apple_push_notification(
|
|||
)
|
||||
except aioapns.exceptions.ConnectionError as e:
|
||||
logger.warning(
|
||||
"APNs: ConnectionError sending for user %d to device %s: %s",
|
||||
user_id,
|
||||
"APNs: ConnectionError sending for user %s to device %s: %s",
|
||||
user_identity,
|
||||
device.token,
|
||||
e.__class__.__name__,
|
||||
)
|
||||
continue
|
||||
|
||||
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"]:
|
||||
logger.info(
|
||||
"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()
|
||||
else:
|
||||
logger.warning(
|
||||
"APNs: Failed to send for user %d to device %s: %s",
|
||||
user_id,
|
||||
"APNs: Failed to send for user %s to device %s: %s",
|
||||
user_identity,
|
||||
device.token,
|
||||
result.description,
|
||||
)
|
||||
|
@ -256,7 +303,9 @@ def send_android_push_notification_to_user(
|
|||
user_profile: UserProfile, data: Dict[str, Any], options: Dict[str, Any]
|
||||
) -> None:
|
||||
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:
|
||||
|
@ -306,7 +355,7 @@ def parse_gcm_options(options: Dict[str, Any], data: Dict[str, Any]) -> str:
|
|||
|
||||
@statsd_increment("android_push_notification")
|
||||
def send_android_push_notification(
|
||||
user_id: int,
|
||||
user_identity: UserPushIndentityCompat,
|
||||
devices: Sequence[DeviceToken],
|
||||
data: Dict[str, Any],
|
||||
options: Dict[str, Any],
|
||||
|
@ -334,14 +383,14 @@ def send_android_push_notification(
|
|||
|
||||
if remote:
|
||||
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,
|
||||
user_id,
|
||||
user_identity,
|
||||
len(devices),
|
||||
)
|
||||
else:
|
||||
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]
|
||||
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():
|
||||
send_notifications_to_bouncer(user_profile_id, apns_payload, gcm_payload, gcm_options)
|
||||
else:
|
||||
user_identity = UserPushIndentityCompat(user_id=user_profile_id)
|
||||
android_devices = list(
|
||||
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)
|
||||
)
|
||||
if android_devices:
|
||||
send_android_push_notification(
|
||||
user_profile_id, android_devices, gcm_payload, gcm_options
|
||||
)
|
||||
send_android_push_notification(user_identity, android_devices, gcm_payload, gcm_options)
|
||||
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
|
||||
# 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(apple_devices),
|
||||
)
|
||||
send_apple_push_notification(user_profile.id, apple_devices, apns_payload)
|
||||
send_android_push_notification(user_profile.id, android_devices, gcm_payload, gcm_options)
|
||||
user_identity = UserPushIndentityCompat(user_id=user_profile.id)
|
||||
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
|
||||
from django.conf import settings
|
||||
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.test import override_settings
|
||||
from django.utils.crypto import get_random_string
|
||||
|
@ -34,6 +34,7 @@ from zerver.lib.exceptions import JsonableError
|
|||
from zerver.lib.push_notifications import (
|
||||
APNsContext,
|
||||
DeviceToken,
|
||||
UserPushIndentityCompat,
|
||||
b64_to_hex,
|
||||
get_apns_badge_count,
|
||||
get_apns_badge_count_future,
|
||||
|
@ -192,7 +193,13 @@ class PushBouncerNotificationTest(BouncerTestCase):
|
|||
result = self.uuid_post(
|
||||
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(
|
||||
self.server_uuid, endpoint, {"user_id": user_id, "token": token, "token_kind": 17}
|
||||
)
|
||||
|
@ -367,12 +374,14 @@ class PushBouncerNotificationTest(BouncerTestCase):
|
|||
logger.output,
|
||||
[
|
||||
"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"
|
||||
],
|
||||
)
|
||||
|
||||
user_identity = UserPushIndentityCompat(user_id=hamlet.id)
|
||||
apple_push.assert_called_once_with(
|
||||
hamlet.id,
|
||||
user_identity,
|
||||
[apple_token],
|
||||
{
|
||||
"badge": 0,
|
||||
|
@ -386,7 +395,7 @@ class PushBouncerNotificationTest(BouncerTestCase):
|
|||
remote=server,
|
||||
)
|
||||
android_push.assert_called_once_with(
|
||||
hamlet.id,
|
||||
user_identity,
|
||||
list(reversed(android_tokens)),
|
||||
{"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)
|
||||
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.is_successful = True
|
||||
apns_context.apns.send_notification.return_value = asyncio.Future(
|
||||
|
@ -974,14 +985,14 @@ class HandlePushNotificationTest(PushNotificationTest):
|
|||
views_logger.output,
|
||||
[
|
||||
"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"
|
||||
],
|
||||
)
|
||||
for _, _, token in apns_devices:
|
||||
self.assertIn(
|
||||
"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,
|
||||
)
|
||||
for _, _, token in gcm_devices:
|
||||
|
@ -1035,7 +1046,7 @@ class HandlePushNotificationTest(PushNotificationTest):
|
|||
views_logger.output,
|
||||
[
|
||||
"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"
|
||||
],
|
||||
)
|
||||
|
@ -1249,10 +1260,9 @@ class HandlePushNotificationTest(PushNotificationTest):
|
|||
) as mock_push_notifications:
|
||||
|
||||
handle_push_notification(self.user_profile.id, missed_message)
|
||||
mock_send_apple.assert_called_with(self.user_profile.id, apple_devices, {"apns": True})
|
||||
mock_send_android.assert_called_with(
|
||||
self.user_profile.id, android_devices, {"gcm": True}, {}
|
||||
)
|
||||
user_identity = UserPushIndentityCompat(user_id=self.user_profile.id)
|
||||
mock_send_apple.assert_called_with(user_identity, apple_devices, {"apns": True})
|
||||
mock_send_android.assert_called_with(user_identity, android_devices, {"gcm": True}, {})
|
||||
mock_push_notifications.assert_called_once()
|
||||
|
||||
def test_send_remove_notifications_to_bouncer(self) -> None:
|
||||
|
@ -1324,8 +1334,9 @@ class HandlePushNotificationTest(PushNotificationTest):
|
|||
) as mock_send_apple:
|
||||
handle_remove_push_notification(self.user_profile.id, [message.id])
|
||||
mock_push_notifications.assert_called_once()
|
||||
user_identity = UserPushIndentityCompat(user_id=self.user_profile.id)
|
||||
mock_send_android.assert_called_with(
|
||||
self.user_profile.id,
|
||||
user_identity,
|
||||
android_devices,
|
||||
{
|
||||
"server": "testserver",
|
||||
|
@ -1339,7 +1350,7 @@ class HandlePushNotificationTest(PushNotificationTest):
|
|||
{"priority": "normal"},
|
||||
)
|
||||
mock_send_apple.assert_called_with(
|
||||
self.user_profile.id,
|
||||
user_identity,
|
||||
apple_devices,
|
||||
{
|
||||
"badge": 0,
|
||||
|
@ -1451,10 +1462,9 @@ class HandlePushNotificationTest(PushNotificationTest):
|
|||
) as mock_push_notifications:
|
||||
handle_push_notification(self.user_profile.id, missed_message)
|
||||
mock_logger.assert_not_called()
|
||||
mock_send_apple.assert_called_with(self.user_profile.id, apple_devices, {"apns": True})
|
||||
mock_send_android.assert_called_with(
|
||||
self.user_profile.id, android_devices, {"gcm": True}, {}
|
||||
)
|
||||
user_identity = UserPushIndentityCompat(user_id=self.user_profile.id)
|
||||
mock_send_apple.assert_called_with(user_identity, apple_devices, {"apns": True})
|
||||
mock_send_android.assert_called_with(user_identity, android_devices, {"gcm": True}, {})
|
||||
mock_push_notifications.assert_called_once()
|
||||
|
||||
@mock.patch("zerver.lib.push_notifications.logger.info")
|
||||
|
@ -1492,7 +1502,9 @@ class TestAPNs(PushNotificationTest):
|
|||
payload_data: Dict[str, Any] = {},
|
||||
) -> None:
|
||||
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:
|
||||
|
@ -1559,7 +1571,7 @@ class TestAPNs(PushNotificationTest):
|
|||
self.send()
|
||||
for device in self.devices():
|
||||
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,
|
||||
)
|
||||
|
||||
|
@ -1576,7 +1588,7 @@ class TestAPNs(PushNotificationTest):
|
|||
)
|
||||
self.send(devices=self.devices()[0:1])
|
||||
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,
|
||||
)
|
||||
|
||||
|
@ -1594,7 +1606,7 @@ class TestAPNs(PushNotificationTest):
|
|||
apns_context.apns.send_notification.return_value.set_result(result)
|
||||
self.send(devices=self.devices()[0:1])
|
||||
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,
|
||||
)
|
||||
|
||||
|
@ -2340,7 +2352,7 @@ class GCMSendTest(PushNotificationTest):
|
|||
with self.assertLogs("zerver.lib.push_notifications", level="INFO") as logger:
|
||||
send_android_push_notification_to_user(self.user_profile, data, {})
|
||||
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_msg3 = f"INFO:zerver.lib.push_notifications:GCM: Sent {2222} as {1}"
|
||||
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:
|
||||
send_android_push_notification_to_user(self.user_profile, data, {})
|
||||
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],
|
||||
)
|
||||
self.assertEqual(
|
||||
|
@ -2427,7 +2439,7 @@ class GCMSendTest(PushNotificationTest):
|
|||
with self.assertLogs("zerver.lib.push_notifications", level="INFO") as logger:
|
||||
send_android_push_notification_to_user(self.user_profile, data, {})
|
||||
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],
|
||||
)
|
||||
self.assertEqual(
|
||||
|
@ -2668,3 +2680,24 @@ class PushBouncerSignupTest(ZulipTestCase):
|
|||
self.assert_json_error(
|
||||
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)
|
||||
# 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:
|
||||
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:
|
||||
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.lib.exceptions import JsonableError
|
||||
from zerver.lib.push_notifications import (
|
||||
UserPushIndentityCompat,
|
||||
send_android_push_notification,
|
||||
send_apple_push_notification,
|
||||
)
|
||||
|
@ -153,17 +154,28 @@ def register_remote_server(
|
|||
def register_remote_push_device(
|
||||
request: HttpRequest,
|
||||
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_kind: int = REQ(json_validator=check_int),
|
||||
ios_app_id: Optional[str] = None,
|
||||
) -> HttpResponse:
|
||||
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:
|
||||
with transaction.atomic():
|
||||
RemotePushDeviceToken.objects.create(
|
||||
# Exactly one of these two user identity fields will be None.
|
||||
user_id=user_id,
|
||||
user_uuid=user_uuid,
|
||||
server=server,
|
||||
kind=token_kind,
|
||||
token=token,
|
||||
|
@ -183,12 +195,15 @@ def unregister_remote_push_device(
|
|||
entity: Union[UserProfile, RemoteZulipServer],
|
||||
token: str = REQ(),
|
||||
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,
|
||||
) -> HttpResponse:
|
||||
server = validate_bouncer_token_request(entity, token, token_kind)
|
||||
user_identity = UserPushIndentityCompat(user_id=user_id, user_uuid=user_uuid)
|
||||
|
||||
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()
|
||||
if deleted[0] == 0:
|
||||
raise JsonableError(err_("Token does not exist"))
|
||||
|
@ -200,10 +215,13 @@ def unregister_remote_push_device(
|
|||
def unregister_all_remote_push_devices(
|
||||
request: HttpRequest,
|
||||
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:
|
||||
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)
|
||||
|
||||
|
||||
|
@ -215,14 +233,15 @@ def remote_server_notify_push(
|
|||
) -> HttpResponse:
|
||||
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"]
|
||||
apns_payload = payload["apns_payload"]
|
||||
gcm_options = payload.get("gcm_options", {})
|
||||
|
||||
android_devices = list(
|
||||
RemotePushDeviceToken.objects.filter(
|
||||
user_id=user_id,
|
||||
user_identity.filter_q(),
|
||||
kind=RemotePushDeviceToken.GCM,
|
||||
server=server,
|
||||
)
|
||||
|
@ -230,7 +249,7 @@ def remote_server_notify_push(
|
|||
|
||||
apple_devices = list(
|
||||
RemotePushDeviceToken.objects.filter(
|
||||
user_id=user_id,
|
||||
user_identity.filter_q(),
|
||||
kind=RemotePushDeviceToken.APNS,
|
||||
server=server,
|
||||
)
|
||||
|
@ -239,7 +258,7 @@ def remote_server_notify_push(
|
|||
logger.info(
|
||||
"Sending mobile push notifications for remote user %s:%s: %s via FCM devices, %s via APNs devices",
|
||||
server.uuid,
|
||||
user_id,
|
||||
user_identity,
|
||||
len(android_devices),
|
||||
len(apple_devices),
|
||||
)
|
||||
|
@ -265,14 +284,14 @@ def remote_server_notify_push(
|
|||
|
||||
gcm_payload = truncate_payload(gcm_payload)
|
||||
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(
|
||||
apns_payload["custom"].get("zulip"), dict
|
||||
):
|
||||
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(
|
||||
request,
|
||||
|
|
Loading…
Reference in New Issue