From 29ea8a07c2a5a9473e530655553642882842c1f1 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Thu, 24 Jul 2025 07:23:29 +0530 Subject: [PATCH] push_notification: Add support to configure priority and push_type. This commit adds support to let server configure: * fcm_priority * apns_priority * apns_push_type while sending E2EE push notifications. The values of these fields will vary depending on whether the send request is to send push notification for a message or revoke an already sent notification. Since, the bouncer receives encrypted data so it can't inspect the payload to determine whether it is a removal request or not, hence can't configure priority on its own. The server needs to specify explicitly. We're not simply sending a single 'is_removal' flag because allowing the server to configure them separately will help in future to support other types of notifications with a different combination of priority and push_type, like whose aim is to notify user about information other than a new message or removal request. Fixes part of #35368. --- zerver/lib/push_notifications.py | 21 ++++++++++++++++++--- zilencer/lib/push_notifications.py | 21 +++++++++++---------- zilencer/views.py | 12 ++++++++++-- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/zerver/lib/push_notifications.py b/zerver/lib/push_notifications.py index 0782a8a542..378dfcb330 100644 --- a/zerver/lib/push_notifications.py +++ b/zerver/lib/push_notifications.py @@ -8,11 +8,11 @@ from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass from email.headerregistry import Address from functools import cache -from typing import TYPE_CHECKING, Any, Literal, Optional, TypeAlias, Union +from typing import TYPE_CHECKING, Any, Final, Literal, Optional, TypeAlias, Union import lxml.html import orjson -from aioapns.common import NotificationResult +from aioapns.common import NotificationResult, PushType from django.conf import settings from django.db import transaction from django.db.models import F, Q @@ -1451,18 +1451,33 @@ def send_push_notifications( assert push_device.bouncer_device_id is not None # for mypy device_id_to_encrypted_data[str(push_device.bouncer_device_id)] = encrypted_data + # TODO: These literals will vary when implementing notification removal. + # + # Note: The "Final" qualifier serves as a shorthand + # for declaring that a variable is effectively Literal. + fcm_priority: Final = "high" + apns_priority: Final = 10 + apns_push_type = PushType.ALERT + # Send push notification try: if settings.ZILENCER_ENABLED: from zilencer.lib.push_notifications import send_e2ee_push_notifications response_data: SendNotificationResponseData = send_e2ee_push_notifications( - device_id_to_encrypted_data, realm=user_profile.realm + device_id_to_encrypted_data, + fcm_priority=fcm_priority, + apns_priority=apns_priority, + apns_push_type=apns_push_type, + realm=user_profile.realm, ) else: post_data = { "realm_uuid": str(user_profile.realm.uuid), "device_id_to_encrypted_data": device_id_to_encrypted_data, + "fcm_priority": fcm_priority, + "apns_priority": apns_priority, + "apns_push_type": apns_push_type, } result = send_json_to_push_bouncer("POST", "push/e2ee/notify", post_data) assert isinstance(result["android_successfully_sent_count"], int) # for mypy diff --git a/zilencer/lib/push_notifications.py b/zilencer/lib/push_notifications.py index 38a4dcaf47..40bf0a6233 100644 --- a/zilencer/lib/push_notifications.py +++ b/zilencer/lib/push_notifications.py @@ -1,8 +1,9 @@ import asyncio import logging from collections.abc import Iterable +from typing import Literal, TypeAlias -from aioapns import NotificationRequest +from aioapns import NotificationRequest, PushType from django.utils.timezone import now as timezone_now from firebase_admin import exceptions as firebase_exceptions from firebase_admin import messaging as firebase_messaging @@ -19,6 +20,9 @@ from zilencer.models import RemotePushDevice, RemoteRealm logger = logging.getLogger(__name__) +FCMPriority: TypeAlias = Literal["high", "normal"] +APNsPriority: TypeAlias = Literal[10, 5, 1] + def send_e2ee_push_notification_apple( apns_requests: list[NotificationRequest], @@ -117,6 +121,9 @@ def send_e2ee_push_notification_android( def send_e2ee_push_notifications( device_id_to_encrypted_data: dict[str, str], *, + fcm_priority: FCMPriority, + apns_priority: APNsPriority, + apns_push_type: PushType, realm: Realm | None = None, remote_realm: RemoteRealm | None = None, ) -> SendNotificationResponseData: @@ -147,18 +154,12 @@ def send_e2ee_push_notifications( "alert": { "title": "New notification", }, - # TODO: Should we remove `sound` and let the clients add it. - # Then we can rename it as `apns_required_message_payload`. - "sound": "default", }, } fcm_requests = [] fcm_remote_push_devices: list[RemotePushDevice] = [] - # TODO: "normal" if remove event. - priority = "high" - for remote_push_device in remote_push_devices: message_payload = { "encrypted_data": device_id_to_encrypted_data[str(remote_push_device.device_id)], @@ -174,8 +175,8 @@ def send_e2ee_push_notifications( apns_topic=remote_push_device.ios_app_id, device_token=remote_push_device.token, message=apns_message_payload, - time_to_live=24 * 3600, - # TODO: priority + priority=apns_priority, + push_type=apns_push_type, ) ) apns_remote_push_devices.append(remote_push_device) @@ -184,7 +185,7 @@ def send_e2ee_push_notifications( firebase_messaging.Message( data=message_payload, token=remote_push_device.token, - android=firebase_messaging.AndroidConfig(priority=priority), + android=firebase_messaging.AndroidConfig(priority=fcm_priority), ) ) fcm_remote_push_devices.append(remote_push_device) diff --git a/zilencer/views.py b/zilencer/views.py index cd6e1e539d..ffcfa0be38 100644 --- a/zilencer/views.py +++ b/zilencer/views.py @@ -8,6 +8,7 @@ from uuid import UUID import orjson import requests.exceptions +from aioapns import PushType from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import URLValidator, validate_email @@ -92,7 +93,7 @@ from zilencer.auth import ( generate_registration_transfer_verification_secret, validate_registration_transfer_verification_secret, ) -from zilencer.lib.push_notifications import send_e2ee_push_notifications +from zilencer.lib.push_notifications import APNsPriority, FCMPriority, send_e2ee_push_notifications from zilencer.lib.remote_counts import MissingDataError from zilencer.models import ( RemoteInstallationCount, @@ -1807,6 +1808,9 @@ def remote_server_check_analytics(request: HttpRequest, server: RemoteZulipServe class SendE2EEPushNotificationPayload(BaseModel): realm_uuid: str device_id_to_encrypted_data: dict[str, str] + fcm_priority: FCMPriority + apns_priority: APNsPriority + apns_push_type: PushType @typed_endpoint @@ -1844,7 +1848,11 @@ def remote_server_send_e2ee_push_notification( ) response_data = send_e2ee_push_notifications( - device_id_to_encrypted_data, remote_realm=remote_realm + device_id_to_encrypted_data, + fcm_priority=payload.fcm_priority, + apns_priority=payload.apns_priority, + apns_push_type=payload.apns_push_type, + remote_realm=remote_realm, ) do_increment_logging_stat(