From c1988a14a71b6be747f0b3d5a379bad3cb3fded6 Mon Sep 17 00:00:00 2001 From: Mateusz Mandera Date: Mon, 11 Dec 2023 22:21:48 +0100 Subject: [PATCH] zilencer: Return can_push info at the push/notify endpoint. This provides the remote server this information to refresh it on its Realm attributes whenever it sends a push notification. Fixes #27483. --- corporate/lib/stripe.py | 23 ++++++++++++++++++ zerver/lib/push_notifications.py | 15 ++++++++++++ zerver/tests/test_push_notifications.py | 27 +++++++++++++++++++-- zilencer/views.py | 31 ++++++------------------- 4 files changed, 70 insertions(+), 26 deletions(-) diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index cb65a4e7a4..5f437c7725 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -46,6 +46,7 @@ from zerver.lib.send_email import ( send_email_to_billing_admins_and_realm_owners, ) from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime +from zerver.lib.types import RemoteRealmDictValue from zerver.lib.url_encoding import append_url_query_string from zerver.lib.utils import assert_is_not_none from zerver.models import ( @@ -3244,6 +3245,28 @@ class RemoteRealmBillingSession(BillingSession): # nocoverage if plan is None: return + def get_push_service_validity_dict(self) -> RemoteRealmDictValue: + customer = self.get_customer() + if customer is None: + return {"can_push": True, "expected_end_timestamp": None} + + current_plan = get_current_plan_by_customer(customer) + if current_plan is None: + return {"can_push": True, "expected_end_timestamp": None} + + expected_end_timestamp = None + if current_plan.status in [ + CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE, + CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL, + ]: + expected_end_timestamp = datetime_to_timestamp( + self.get_next_billing_cycle(current_plan) + ) + return { + "can_push": True, + "expected_end_timestamp": expected_end_timestamp, + } + class RemoteServerBillingSession(BillingSession): # nocoverage """Billing session for pre-8.0 servers that do not yet support diff --git a/zerver/lib/push_notifications.py b/zerver/lib/push_notifications.py index 3f3bc18221..a8766a5573 100644 --- a/zerver/lib/push_notifications.py +++ b/zerver/lib/push_notifications.py @@ -641,6 +641,21 @@ def send_notifications_to_bouncer( increment=total_android_devices + total_apple_devices, ) + remote_realm_dict = response_data.get("realm") + if remote_realm_dict is not None: + # The server may have updated our understanding of whether + # push notifications will work. + assert isinstance(remote_realm_dict, dict) + do_set_realm_property( + user_profile.realm, + "push_notifications_enabled", + remote_realm_dict["can_push"], + acting_user=None, + ) + do_set_push_notifications_enabled_end_timestamp( + user_profile.realm, remote_realm_dict["expected_end_timestamp"], acting_user=None + ) + return total_android_devices, total_apple_devices diff --git a/zerver/tests/test_push_notifications.py b/zerver/tests/test_push_notifications.py index 7269375dc4..1094d68942 100644 --- a/zerver/tests/test_push_notifications.py +++ b/zerver/tests/test_push_notifications.py @@ -591,6 +591,7 @@ class PushBouncerNotificationTest(BouncerTestCase): payload = { "user_id": hamlet.id, "user_uuid": str(hamlet.uuid), + "realm_uuid": str(hamlet.realm.uuid), "gcm_payload": {"event": "remove", "zulip_message_ids": many_ids}, "apns_payload": { "badge": 0, @@ -619,6 +620,7 @@ class PushBouncerNotificationTest(BouncerTestCase): "total_android_devices": 2, "total_apple_devices": 1, "deleted_devices": {"android_devices": [], "apple_devices": []}, + "realm": {"can_push": True, "expected_end_timestamp": None}, }, data, ) @@ -1813,7 +1815,9 @@ class AnalyticsBouncerTest(BouncerTestCase): with mock.patch( "zilencer.views.RemoteRealmBillingSession.get_customer", return_value=dummy_customer ): - with mock.patch("zilencer.views.get_current_plan_by_customer", return_value=None) as m: + with mock.patch( + "corporate.lib.stripe.get_current_plan_by_customer", return_value=None + ) as m: send_server_data_to_push_bouncer(consider_usage_statistics=False) m.assert_called() realms = Realm.objects.all() @@ -1828,7 +1832,8 @@ class AnalyticsBouncerTest(BouncerTestCase): "zilencer.views.RemoteRealmBillingSession.get_customer", return_value=dummy_customer ): with mock.patch( - "zilencer.views.get_current_plan_by_customer", return_value=dummy_customer_plan + "corporate.lib.stripe.get_current_plan_by_customer", + return_value=dummy_customer_plan, ): with mock.patch( "zilencer.views.RemoteRealmBillingSession.get_next_billing_cycle", @@ -3614,6 +3619,7 @@ class TestSendNotificationsToBouncer(PushNotificationTest): android_devices=[device.token for device in android_devices], apple_devices=[device.token for device in apple_devices], ), + "realm": {"can_push": True, "expected_end_timestamp": None}, } total_android_devices, total_apple_devices = send_notifications_to_bouncer( user, {"apns": True}, {"gcm": True}, {}, list(android_devices), list(apple_devices) @@ -3650,6 +3656,23 @@ class TestSendNotificationsToBouncer(PushNotificationTest): self.assertEqual(PushDeviceToken.objects.filter(kind=PushDeviceToken.APNS).count(), 0) self.assertEqual(PushDeviceToken.objects.filter(kind=PushDeviceToken.GCM).count(), 0) + # Now simulating getting "can_push" as False from the bouncer and verify + # that we update the realm value. + mock_send.return_value = { + "total_android_devices": 1, + "total_apple_devices": 3, + "realm": {"can_push": False, "expected_end_timestamp": None}, + "deleted_devices": DevicesToCleanUpDict( + android_devices=[], + apple_devices=[], + ), + } + total_android_devices, total_apple_devices = send_notifications_to_bouncer( + user, {"apns": True}, {"gcm": True}, {}, list(android_devices), list(apple_devices) + ) + user.realm.refresh_from_db() + self.assertEqual(user.realm.push_notifications_enabled, False) + @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") class TestSendToPushBouncer(ZulipTestCase): diff --git a/zilencer/views.py b/zilencer/views.py index b98299827d..e68678e641 100644 --- a/zilencer/views.py +++ b/zilencer/views.py @@ -47,7 +47,7 @@ from zerver.lib.remote_server import ( ) from zerver.lib.request import REQ, has_request_variables from zerver.lib.response import json_success -from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime +from zerver.lib.timestamp import timestamp_to_datetime from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint from zerver.lib.types import RemoteRealmDictValue from zerver.lib.validator import check_capped_string, check_int, check_string_fixed_length @@ -514,6 +514,8 @@ def remote_server_notify_push( timezone_now(), increment=android_successfully_delivered + apple_successfully_delivered, ) + + remote_realm_dict: Optional[RemoteRealmDictValue] = None if remote_realm is not None: do_increment_logging_stat( remote_realm, @@ -522,6 +524,8 @@ def remote_server_notify_push( timezone_now(), increment=android_successfully_delivered + apple_successfully_delivered, ) + billing_session = RemoteRealmBillingSession(remote_realm) + remote_realm_dict = billing_session.get_push_service_validity_dict() deleted_devices = get_deleted_devices( user_identity, @@ -536,6 +540,7 @@ def remote_server_notify_push( "total_android_devices": len(android_devices), "total_apple_devices": len(apple_devices), "deleted_devices": deleted_devices, + "realm": remote_realm_dict, }, ) @@ -968,29 +973,7 @@ def remote_server_post_analytics( for remote_realm in remote_realms: uuid = str(remote_realm.uuid) billing_session = RemoteRealmBillingSession(remote_realm) - - customer = billing_session.get_customer() - if customer is None: - remote_realm_dict[uuid] = {"can_push": True, "expected_end_timestamp": None} - continue - - current_plan = get_current_plan_by_customer(customer) - if current_plan is None: - remote_realm_dict[uuid] = {"can_push": True, "expected_end_timestamp": None} - continue - - expected_end_timestamp = None - if current_plan.status in [ - CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE, - CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL, - ]: - expected_end_timestamp = datetime_to_timestamp( - billing_session.get_next_billing_cycle(current_plan) - ) - remote_realm_dict[uuid] = { - "can_push": True, - "expected_end_timestamp": expected_end_timestamp, - } + remote_realm_dict[uuid] = billing_session.get_push_service_validity_dict() return json_success(request, data={"realms": remote_realm_dict})