push_notification: Send a list of push requests.

Earlier, we were passing a map `device_id_to_encrypted_data`
and http headers as separate fields to bouncer.

The downside of that approach is it restricts the bouncer to
process only one type of notice i.e. either notification for
a new message or removal of sent notification, because it
used to receive a fixed priority and push_type for all the
entries in the map.

Also, using map restricts the bouncer to receive only one
request per device_id. Server can't send multiple notices
to a device in a single call to bouncer.

Currently, the server isn't modelled in a way to make a
single call to the bouncer with:
* Both send-notification & remove-notification request data.
* Multiple send-notification request data to the same device.

This commit replaces the old protocol of sending data with
a list of objects where each object has the required data
for bouncer to send it to FCM or APNs.

This makes things a lot flexible and opens possibility for
server to batch requests in a different way if we'd like to.
This commit is contained in:
Prakhar Pratyush
2025-07-25 16:08:28 +05:30
committed by Tim Abbott
parent 3d3f4d5e62
commit 6ab6df96c8
4 changed files with 156 additions and 75 deletions

View File

@@ -1,15 +1,17 @@
import asyncio
import logging
from collections.abc import Iterable
from typing import Literal, TypeAlias
from dataclasses import asdict
from aioapns import NotificationRequest, PushType
from aioapns import NotificationRequest
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
from firebase_admin.messaging import UnregisteredError as FCMUnregisteredError
from zerver.lib.push_notifications import (
APNsPushRequest,
FCMPushRequest,
SendNotificationResponseData,
fcm_app,
get_apns_context,
@@ -20,9 +22,6 @@ 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],
@@ -119,11 +118,8 @@ def send_e2ee_push_notification_android(
def send_e2ee_push_notifications(
device_id_to_encrypted_data: dict[str, str],
push_requests: list[APNsPushRequest | FCMPushRequest],
*,
fcm_priority: FCMPriority,
apns_priority: APNsPriority,
apns_push_type: PushType,
realm: Realm | None = None,
remote_realm: RemoteRealm | None = None,
) -> SendNotificationResponseData:
@@ -131,13 +127,15 @@ def send_e2ee_push_notifications(
import aioapns
device_ids = [int(device_id_str) for device_id_str in device_id_to_encrypted_data]
device_ids = {push_request.device_id for push_request in push_requests}
remote_push_devices = RemotePushDevice.objects.filter(
device_id__in=device_ids, expired_time__isnull=True, realm=realm, remote_realm=remote_realm
)
unexpired_remote_push_device_ids = {
remote_push_device.device_id for remote_push_device in remote_push_devices
device_id_to_remote_push_device = {
remote_push_device.device_id: remote_push_device
for remote_push_device in remote_push_devices
}
unexpired_remote_push_device_ids = set(device_id_to_remote_push_device.keys())
# Device IDs which should be deleted on server.
# Either the device ID is invalid or the token
@@ -148,44 +146,35 @@ def send_e2ee_push_notifications(
apns_requests = []
apns_remote_push_devices: list[RemotePushDevice] = []
apns_base_message_payload = {
"aps": {
"mutable-content": 1,
"alert": {
"title": "New notification",
},
},
}
fcm_requests = []
fcm_remote_push_devices: list[RemotePushDevice] = []
for remote_push_device in remote_push_devices:
message_payload = {
"encrypted_data": device_id_to_encrypted_data[str(remote_push_device.device_id)],
"push_account_id": remote_push_device.push_account_id,
}
for push_request in push_requests:
device_id = push_request.device_id
if device_id not in unexpired_remote_push_device_ids:
continue
remote_push_device = device_id_to_remote_push_device[device_id]
if remote_push_device.token_kind == RemotePushDevice.TokenKind.APNS:
apns_message_payload = {
**apns_base_message_payload,
**message_payload,
}
assert isinstance(push_request, APNsPushRequest)
apns_requests.append(
aioapns.NotificationRequest(
apns_topic=remote_push_device.ios_app_id,
device_token=remote_push_device.token,
message=apns_message_payload,
priority=apns_priority,
push_type=apns_push_type,
message=asdict(push_request.payload),
priority=push_request.http_headers.apns_priority,
push_type=push_request.http_headers.apns_push_type,
)
)
apns_remote_push_devices.append(remote_push_device)
else:
assert isinstance(push_request, FCMPushRequest)
fcm_requests.append(
firebase_messaging.Message(
data=message_payload,
data=asdict(push_request.payload),
token=remote_push_device.token,
android=firebase_messaging.AndroidConfig(priority=fcm_priority),
android=firebase_messaging.AndroidConfig(priority=push_request.fcm_priority),
)
)
fcm_remote_push_devices.append(remote_push_device)