def send_apple_push_notification( user_identity: UserPushIndentityCompat, devices: Sequence[DeviceToken], payload_data: Dict[str, Any], remote: Optional["RemoteZulipServer"] = None, ) -> None: if not devices: return # We lazily do the APNS imports as part of optimizing Zulip's base # import time; since these are only needed in the push # notification queue worker, it's best to only import them in the # code that needs them. import aioapns import aioapns.exceptions apns_context = get_apns_context() if apns_context is None: logger.debug( "APNs: Dropping a notification because nothing configured. " "Set PUSH_NOTIFICATION_BOUNCER_URL (or APNS_CERT_FILE)." ) return if remote: assert settings.ZILENCER_ENABLED DeviceTokenClass: Type[AbstractPushDeviceToken] = RemotePushDeviceToken else: DeviceTokenClass = PushDeviceToken if remote: logger.info( "APNs: Sending notification for remote user %s:%s to %d devices", remote.uuid, user_identity, len(devices), ) else: logger.info( "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} for device in devices: # TODO obviously this should be made to actually use the async request = aioapns.NotificationRequest( device_token=device.token, message=message, time_to_live=24 * 3600 ) try: result = apns_context.loop.run_until_complete( apns_context.apns.send_notification(request) ) except aioapns.exceptions.ConnectionError: logger.error( "APNs: ConnectionError sending for user %s to device %s; check certificate expiration", user_identity, device.token, ) continue if result.is_successful: 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 ) # We remove all entries for this token (There # could be multiple for different Zulip servers). DeviceTokenClass.objects.filter(token=device.token, kind=DeviceTokenClass.APNS).delete() else: logger.warning( "APNs: Failed to send for user %s to device %s: %s", user_identity, device.token, result.description, )
def send_apple_push_notification(user_id: int, devices: List[DeviceToken], payload_data: Dict[str, Any], remote: bool = False) -> None: if not devices: return # We lazily do the APNS imports as part of optimizing Zulip's base # import time; since these are only needed in the push # notification queue worker, it's best to only import them in the # code that needs them. import aioapns import aioapns.exceptions apns_context = get_apns_context() if apns_context is None: logger.debug( "APNs: Dropping a notification because nothing configured. " "Set PUSH_NOTIFICATION_BOUNCER_URL (or APNS_CERT_FILE).") return if remote: assert settings.ZILENCER_ENABLED DeviceTokenClass = RemotePushDeviceToken else: DeviceTokenClass = PushDeviceToken logger.info("APNs: Sending notification for user %d to %d devices", user_id, len(devices)) message = {"aps": modernize_apns_payload(payload_data)} retries_left = APNS_MAX_RETRIES for device in devices: # TODO obviously this should be made to actually use the async request = aioapns.NotificationRequest(device_token=device.token, message=message, time_to_live=24 * 3600) async def attempt_send() -> Optional[str]: assert apns_context is not None try: result = await apns_context.apns.send_notification(request) return "Success" if result.is_successful else result.description except aioapns.exceptions.ConnectionClosed as e: # nocoverage logger.warning( "APNs: ConnectionClosed sending for user %d to device %s: %s", user_id, device.token, e.__class__.__name__, ) return None except aioapns.exceptions.ConnectionError as e: # nocoverage logger.warning( "APNs: ConnectionError sending for user %d to device %s: %s", user_id, device.token, e.__class__.__name__, ) return None result = apns_context.loop.run_until_complete(attempt_send()) while result is None and retries_left > 0: retries_left -= 1 result = apns_context.loop.run_until_complete(attempt_send()) if result is None: result = "HTTP error, retries exhausted" if result == "Success": logger.info("APNs: Success sending for user %d to device %s", user_id, device.token) elif result in [ "Unregistered", "BadDeviceToken", "DeviceTokenNotForTopic" ]: logger.info("APNs: Removing invalid/expired token %s (%s)", device.token, result) # We remove all entries for this token (There # could be multiple for different Zulip servers). 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, device.token, result)