164 lines
6 KiB
Python
164 lines
6 KiB
Python
import json
|
|
import logging
|
|
import typing
|
|
|
|
import requests
|
|
from django.conf import settings
|
|
from firebase_admin.exceptions import FirebaseError
|
|
from firebase_admin.messaging import AndroidConfig, APNSConfig, APNSPayload, Message, SendResponse, UnregisteredError
|
|
from requests import HTTPError
|
|
from rest_framework import status
|
|
|
|
from apps.base.utils import live_settings
|
|
from apps.mobile_app.types import FCMMessageData, MessageType
|
|
from common.api_helpers.utils import create_engine_url
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from apps.mobile_app.models import FCMDevice
|
|
from apps.user_management.models import Organization
|
|
|
|
|
|
MAX_RETRIES = 1 if settings.DEBUG else 10
|
|
|
|
# UnregisteredError
|
|
# App instance was unregistered from FCM. This usually means that the token used is no longer valid and a
|
|
# new one must be used.
|
|
#
|
|
# In other words, this error occurs outside of our control and retrying will never fix it, therefore we should skip
|
|
FIREBASE_ERRORS_TO_NOT_RETRY = (UnregisteredError,)
|
|
logger = logging.getLogger(__name__)
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
|
|
def _send_push_notification_to_fcm_relay(message: Message) -> requests.Response:
|
|
"""
|
|
Send push notification to FCM relay on cloud instance: apps.mobile_app.fcm_relay.FCMRelayView
|
|
"""
|
|
url = create_engine_url("mobile_app/v1/fcm_relay", override_base=settings.GRAFANA_CLOUD_ONCALL_API_URL)
|
|
|
|
response = requests.post(
|
|
url, headers={"Authorization": live_settings.GRAFANA_CLOUD_ONCALL_TOKEN}, json=json.loads(str(message))
|
|
)
|
|
response.raise_for_status()
|
|
|
|
return response
|
|
|
|
|
|
def send_message_to_fcm_device(device: "FCMDevice", message: Message) -> bool:
|
|
"""
|
|
https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream
|
|
"""
|
|
response = device.send_message(message)
|
|
|
|
if isinstance(response, SendResponse):
|
|
logger.debug(
|
|
f"FCM response: success={response.success} message_id={response.message_id} exception={response.exception}"
|
|
)
|
|
elif isinstance(response, FirebaseError):
|
|
logger.exception(
|
|
f"FCM error occured in mobile_app.utils.send_message_to_fcm_device fcm_device_info={device} "
|
|
f"firebase_error_code={response._code} firebase_error_cause={response._cause} "
|
|
f"firebase_error_http_response={response._http_response}"
|
|
)
|
|
|
|
if isinstance(response, FIREBASE_ERRORS_TO_NOT_RETRY):
|
|
logger.warning(f"FCM error {response} is not being retried as we explicitly do not want to retry it")
|
|
return False
|
|
|
|
raise response
|
|
else:
|
|
logger.debug(f"FCM response: {response}")
|
|
|
|
return True
|
|
|
|
|
|
def send_push_notification(
|
|
device_to_notify: "FCMDevice", message: Message, error_cb: typing.Optional[typing.Callable[..., None]] = None
|
|
) -> bool:
|
|
logger.debug(f"Sending push notification to device type {device_to_notify.type} with message: {message}")
|
|
|
|
def _error_cb():
|
|
if error_cb:
|
|
error_cb()
|
|
|
|
if settings.IS_OPEN_SOURCE:
|
|
# FCM relay uses cloud connection to send push notifications
|
|
from apps.oss_installation.models import CloudConnector
|
|
|
|
if not CloudConnector.objects.exists():
|
|
_error_cb()
|
|
logger.error("Error while sending a mobile push notification: not connected to cloud")
|
|
return False
|
|
|
|
try:
|
|
response = _send_push_notification_to_fcm_relay(message)
|
|
logger.debug(f"FCM relay response: {response}")
|
|
except HTTPError as e:
|
|
if status.HTTP_400_BAD_REQUEST <= e.response.status_code < status.HTTP_500_INTERNAL_SERVER_ERROR:
|
|
# do not retry on HTTP client errors (4xx errors)
|
|
_error_cb()
|
|
logger.error(
|
|
f"Error while sending a mobile push notification: HTTP client error {e.response.status_code}"
|
|
)
|
|
return False
|
|
else:
|
|
raise
|
|
else:
|
|
succeeded = send_message_to_fcm_device(device_to_notify, message)
|
|
if not succeeded:
|
|
_error_cb()
|
|
return False
|
|
|
|
# notification succeeded (otherwise raised exception before)
|
|
return True
|
|
|
|
|
|
def construct_fcm_message(
|
|
message_type: MessageType,
|
|
device_to_notify: "FCMDevice",
|
|
thread_id: str,
|
|
data: FCMMessageData,
|
|
apns_payload: typing.Optional[APNSPayload] = None,
|
|
) -> Message:
|
|
apns_config_kwargs = {}
|
|
|
|
if apns_payload is not None:
|
|
apns_config_kwargs["payload"] = apns_payload
|
|
|
|
return Message(
|
|
token=device_to_notify.registration_id,
|
|
data={
|
|
# from the docs..
|
|
# A dictionary of data fields (optional). All keys and values in the dictionary must be strings
|
|
**data,
|
|
"type": message_type,
|
|
"thread_id": thread_id,
|
|
},
|
|
android=AndroidConfig(
|
|
# from the docs
|
|
# https://firebase.google.com/docs/cloud-messaging/concept-options#setting-the-priority-of-a-message
|
|
#
|
|
# Normal priority.
|
|
# Normal priority messages are delivered immediately when the app is in the foreground.
|
|
# For backgrounded apps, delivery may be delayed. For less time-sensitive messages, such as notifications
|
|
# of new email, keeping your UI in sync, or syncing app data in the background, choose normal delivery
|
|
# priority.
|
|
#
|
|
# High priority.
|
|
# FCM attempts to deliver high priority messages immediately even if the device is in Doze mode.
|
|
# High priority messages are for time-sensitive, user visible content.
|
|
priority="high",
|
|
),
|
|
apns=APNSConfig(
|
|
**apns_config_kwargs,
|
|
headers={
|
|
# From the docs
|
|
# https://firebase.google.com/docs/cloud-messaging/concept-options#setting-the-priority-of-a-message
|
|
"apns-priority": "10",
|
|
},
|
|
),
|
|
)
|
|
|
|
|
|
def add_stack_slug_to_message_title(title: str, organization: "Organization") -> str:
|
|
return f"[{organization.stack_slug}] {title}"
|