From 231c0f45a30112137d75009c22f422db6b630873 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Wed, 11 Jan 2023 11:42:01 +0000 Subject: [PATCH] Re-implement FCM relay after introducing firebase (#1121) This PR changes how `FCMRelayView` handles push notifications from OSS instances, also changing how the mobile app backend sends push notifications. --- engine/apps/mobile_app/fcm_relay.py | 73 +++++++++++++++++++++++------ engine/apps/mobile_app/tasks.py | 33 ++++++++++--- 2 files changed, 84 insertions(+), 22 deletions(-) diff --git a/engine/apps/mobile_app/fcm_relay.py b/engine/apps/mobile_app/fcm_relay.py index f50f2434..8fdeb716 100644 --- a/engine/apps/mobile_app/fcm_relay.py +++ b/engine/apps/mobile_app/fcm_relay.py @@ -1,29 +1,72 @@ -# from firebase_admin.messaging import Message -# from fcm_django.models import FCMDevice +import logging + +from fcm_django.models import FCMDevice +from firebase_admin.messaging import APNSConfig, APNSPayload, Aps, ApsAlert, CriticalSound, Message from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView -REQUIRED_FIELDS = {"registration_ids", "notification", "data"} +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) -# TODO: update thie class FCMRelayView(APIView): + # TODO: use public API authentication (then it would be required to connect to a cloud instance to use the app) + authentication_classes = [] + permission_classes = [] + def post(self, request): """ - This view accepts requests from OSS instances of Grafana OnCall and forwards these requests to FCM. - Requests will be sent with the FCM_API_KEY configured in server settings - (see PUSH_NOTIFICATIONS_SETTINGS in settings/base.py) + This view accepts push notifications from OSS instances and forwards these requests to FCM. + Requests to this endpoint come from OSS instances: apps.mobile_app.tasks.send_push_notification_to_fcm_relay """ - if not REQUIRED_FIELDS.issubset(request.data.keys()): + try: + token = request.data["token"] + data = request.data["data"] + except KeyError: return Response(status=status.HTTP_400_BAD_REQUEST) - # registration_ids = request.data["registration_ids"] - # data = { - # **request.data["data"], - # **request.data["notification"], - # } + message = Message(token=token, data=data, apns=get_apns(request.data)) - # return FCMDevice.objects.send_message(Message(), False, ["registration_ids"]) - return "TODO:" + logger.debug(f"Sending message to FCM: {message}") + result = FCMDevice(registration_id=token).send_message(message) + logger.debug(f"FCM response: {result}") + + return Response(status=status.HTTP_200_OK) + + +def get_apns(data): + """ + Create APNSConfig object from JSON payload from OSS instance. + """ + aps = data.get("apns", {}).get("payload", {}).get("aps", {}) + if not aps: + return None + + thread_id = aps.get("thread-id") + badge = aps.get("badge") + + alert = aps.get("alert") + if isinstance(alert, dict): + alert = ApsAlert(**alert) + + sound = aps.get("sound") + if isinstance(sound, dict): + sound = CriticalSound(**sound) + + # remove all keys from "aps" so it can be used for custom_data + for key in ["thread-id", "badge", "alert", "sound"]: + aps.pop(key, None) + + return APNSConfig( + payload=APNSPayload( + aps=Aps( + thread_id=thread_id, + badge=badge, + alert=alert, + sound=sound, + custom_data=aps, + ) + ) + ) diff --git a/engine/apps/mobile_app/tasks.py b/engine/apps/mobile_app/tasks.py index 04bbe5b7..d3198adb 100644 --- a/engine/apps/mobile_app/tasks.py +++ b/engine/apps/mobile_app/tasks.py @@ -1,3 +1,7 @@ +import json +import logging + +import requests from celery.utils.log import get_task_logger from django.conf import settings from fcm_django.models import FCMDevice @@ -6,10 +10,12 @@ from firebase_admin.messaging import APNSConfig, APNSPayload, Aps, ApsAlert, Cri from apps.alerts.models import AlertGroup from apps.mobile_app.alert_rendering import get_push_notification_message from apps.user_management.models import User +from common.api_helpers.utils import create_engine_url from common.custom_celery_tasks import shared_dedicated_queue_retry_task MAX_RETRIES = 1 if settings.DEBUG else 10 logger = get_task_logger(__name__) +logger.setLevel(logging.DEBUG) @shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=MAX_RETRIES) @@ -71,8 +77,6 @@ def notify_user_async(user_pk, alert_group_pk, notification_policy_pk, critical) alert_body = f"Status: {status_verbose}, alerts: {alerts_count_str}" - # TODO: we should update this to check if FCM_RELAY is set and conditionally make a call here.. - message = Message( token=device_to_notify.registration_id, data={ @@ -109,10 +113,25 @@ def notify_user_async(user_pk, alert_group_pk, notification_policy_pk, critical) ), ) - logger.info(f"Sending push notification with message: {message}; thread-id: {thread_id};") + logger.debug(f"Sending push notification with message: {message}; thread-id: {thread_id};") - fcm_response = device_to_notify.send_message(message) + if settings.LICENSE == settings.OPEN_SOURCE_LICENSE_NAME: + response = send_push_notification_to_fcm_relay(message) + logger.debug(f"FCM relay response: {response}") + else: + response = device_to_notify.send_message(message) + # NOTE: we may want to further handle the response from FCM, but for now lets simply log it out + # https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream + logger.debug(f"FCM response: {response}") - # NOTE: we may want to further handle the response from FCM, but for now lets simply log it out - # https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream - logger.info(f"FCM response was: {fcm_response}") + +def send_push_notification_to_fcm_relay(message): + """ + 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, json=json.loads(str(message))) + response.raise_for_status() + + return response