notifications: Dedupe APNs tokens case-insensitively.

Fixes zulip/zulip-flutter#1617.

It turns out that an APNs token (which is a hex string) is equally
valid in lower or upper case.  The old app would send the server
the lower-case form of the token, but the new app sends the
upper-case form.

Because we've been treating tokens case-sensitively, if the user
upgrades from the old app to the new, that results in the server
and bouncer each having two copies of the token (one lower-case and
one upper-case), and therefore sending that device two copies of
each notification: zulip/zulip-flutter#1617.

To fix that immediately, have the bouncer drop duplicate tokens
before sending the notifications to APNs.

Work is also in progress on fixing this in a better-structured way,
by having the database correctly treat tokens as the same when they
differ only in case.
This commit is contained in:
Greg Price
2025-07-01 00:11:29 -04:00
committed by Tim Abbott
parent 9b15dce1b2
commit aaeabeda44

View File

@@ -241,6 +241,20 @@ def modernize_apns_payload(data: Mapping[str, Any]) -> Mapping[str, Any]:
APNS_MAX_RETRIES = 3
def dedupe_device_tokens(
devices: Sequence[DeviceToken],
) -> Sequence[DeviceToken]:
device_tokens: set[str] = set()
result: list[DeviceToken] = []
for device in devices:
lower_token = device.token.lower()
if lower_token in device_tokens: # nocoverage
continue
device_tokens.add(lower_token)
result.append(device)
return result
def send_apple_push_notification(
user_identity: UserPushIdentityCompat,
devices: Sequence[DeviceToken],
@@ -270,18 +284,24 @@ def send_apple_push_notification(
else:
DeviceTokenClass = PushDeviceToken
orig_devices = devices
devices = dedupe_device_tokens(devices)
num_duplicate_tokens = len(orig_devices) - len(devices)
if remote:
logger.info(
"APNs: Sending notification for remote user %s:%s to %d devices",
"APNs: Sending notification for remote user %s:%s to %d devices (skipped %d duplicates)",
remote.uuid,
user_identity,
len(devices),
num_duplicate_tokens,
)
else:
logger.info(
"APNs: Sending notification for local user %s to %d devices",
"APNs: Sending notification for local user %s to %d devices (skipped %d duplicates)",
user_identity,
len(devices),
num_duplicate_tokens,
)
payload_data = dict(modernize_apns_payload(payload_data))
message = {**payload_data.pop("custom", {}), "aps": payload_data}