def _ping_client(client):
        client_type = client.client_type
        if client_type in ClientType.FCM_CLIENTS:
            from models.notifications.ping import PingNotification
            notification = PingNotification()

            from models.notifications.requests.fcm_request import FCMRequest
            fcm_request = FCMRequest(firebase_app,
                                     notification,
                                     tokens=[client.messaging_id])
            logging.info('Ping - {}'.format(str(fcm_request)))

            batch_response = fcm_request.send()
            if batch_response.failure_count > 0:
                response = batch_response.responses[0]
                logging.info('Error Sending Ping - {}'.format(
                    response.exception))
                return False
            else:
                logging.info('Ping Sent')
        elif client_type == ClientType.OS_ANDROID:
            # Send old notifications to Android
            from notifications.ping import PingNotification
            notification = PingNotification()
            notification.send({client_type: [client.messaging_id]})
        else:
            raise Exception(
                'Unsupported FCM client type: {}'.format(client_type))

        return True
 def test_send_failed(self):
     batch_response = messaging.BatchResponse(
         [messaging.SendResponse(None, 'a')])
     request = FCMRequest(app=self.app,
                          notification=MockNotification(),
                          tokens=['abc', 'def'])
     with patch.object(
             messaging, 'send_multicast',
             return_value=batch_response) as mock_send, patch.object(
                 request, 'defer_track_notification') as mock_track:
         response = request.send()
     mock_send.assert_called_once()
     mock_track.assert_not_called()
     self.assertEqual(response, batch_response)
    def _send_fcm(cls, clients, notification, backoff_iteration=0):
        # Only send to FCM clients if notifications are enabled
        if not cls._notifications_enabled():
            return 1

        # Only allow so many retries
        backoff_time = 2**backoff_iteration
        if backoff_time > MAXIMUM_BACKOFF:
            return 2

        # Make sure we're only sending to FCM clients
        clients = [
            client for client in clients
            if client.client_type in ClientType.FCM_CLIENTS
        ]

        from models.notifications.requests.fcm_request import FCMRequest, MAXIMUM_TOKENS
        # We can only send to so many FCM clients at a time - send to our clients across several requests
        for subclients in [
                clients[i:i + MAXIMUM_TOKENS]
                for i in range(0, len(clients), MAXIMUM_TOKENS)
        ]:
            fcm_request = FCMRequest(
                firebase_app,
                notification,
                tokens=[client.messaging_id for client in subclients])
            logging.info(str(fcm_request))

            batch_response = fcm_request.send()
            retry_clients = []

            # Handle our failed sends - this might include logging/alerting, removing old clients, or retrying sends
            from firebase_admin.exceptions import InvalidArgumentError, InternalError, UnavailableError
            from firebase_admin.messaging import QuotaExceededError, SenderIdMismatchError, ThirdPartyAuthError, UnregisteredError
            for index, response in enumerate([
                    response for response in batch_response.responses
                    if not response.success
            ]):
                client = subclients[index]
                if isinstance(response.exception, UnregisteredError):
                    logging.info(
                        'Deleting unregistered client with ID: {}'.format(
                            client.messaging_id))
                    MobileClient.delete_for_messaging_id(client.messaging_id)
                elif isinstance(response.exception, SenderIdMismatchError):
                    logging.info(
                        'Deleting mismatched client with ID: {}'.format(
                            client.messaging_id))
                    MobileClient.delete_for_messaging_id(client.messaging_id)
                elif isinstance(response.exception, QuotaExceededError):
                    logging.error('Qutoa exceeded - retrying client...')
                    retry_clients.append(client)
                elif isinstance(response.exception, ThirdPartyAuthError):
                    logging.critical(
                        'Third party error sending to FCM - {}'.format(
                            response.exception))
                elif isinstance(response.exception, InvalidArgumentError):
                    logging.critical(
                        'Invalid argument when sending to FCM - {}'.format(
                            response.exception))
                elif isinstance(response.exception, InternalError):
                    logging.error('Interal FCM error - retrying client...')
                    retry_clients.append(client)
                elif isinstance(response.exception, UnavailableError):
                    logging.error('FCM unavailable - retrying client...')
                    retry_clients.append(client)
                else:
                    debug_string = cls._debug_string(response.exception)
                    logging.error('Unhandled FCM error for {} - {}'.format(
                        client.messaging_id, debug_string))

            if retry_clients:
                # Try again, with exponential backoff
                deferred.defer(cls._send_fcm,
                               retry_clients,
                               notification,
                               backoff_iteration + 1,
                               _countdown=backoff_time,
                               _target='backend-tasks',
                               _queue='push-notifications',
                               _url='/_ah/queue/deferred_notification_send')

        return 0