mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	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:
		
				
					committed by
					
						
						Tim Abbott
					
				
			
			
				
	
			
			
			
						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)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								zilencer/migrations/0024_remotepushdevicetoken_user_uuid.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								zilencer/migrations/0024_remotepushdevicetoken_user_uuid.py
									
									
									
									
									
										Normal file
									
								
							@@ -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,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user