Skip to content

Commit

Permalink
feat: Send Notification to firebase
Browse files Browse the repository at this point in the history
- Trigger function on version and deleted docuemtn  insertion
- Send FCM notification if right document
- FirebaseNotification class wrapper around FCM requests
  • Loading branch information
marination committed Jan 5, 2024
1 parent c1c4848 commit 82d93dc
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 32 deletions.
66 changes: 66 additions & 0 deletions landa/firebase_connector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import json

import google.auth.transport.requests
import requests
from google.oauth2 import service_account


class FirebaseNotification:
def __init__(self, cert, project_id):
self.cert = cert
self.url = f"https://fcm.googleapis.com/v1/projects/{project_id}/messages:send"
self.token = self._get_access_token()

def _get_access_token(self) -> str:
"""Retrieve a valid access token to authorize requests.
:return: Access token.
"""
credentials = service_account.Credentials.from_service_account_file(
self.cert,
scopes=[
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/firebase",
],
)
request = google.auth.transport.requests.Request()
credentials.refresh(request)
return credentials.token

@property
def headers(self):
"""Get headers for authorized requests."""
return {
"Authorization": "Bearer " + self.token,
"Content-Type": "application/json; UTF-8",
}

def send_to_topic(self, topic: str, data: dict = None) -> requests.Response:
"""Send a message to a topic."""
return requests.post(
self.url,
headers=self.headers,
data=json.dumps(
{
"message": {
"topic": topic,
"data": data,
}
}
),
)

def send_to_token(self, token: str, data: dict = None) -> requests.Response:
"""Send a message to a token."""
return requests.post(
self.url,
headers=self.headers,
data=json.dumps(
{
"message": {
"token": token,
"data": data,
}
}
),
)
6 changes: 6 additions & 0 deletions landa/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,12 @@
"Water Body Management Local Organization": {
"after_insert": "landa.water_body_management.utils.create_version_log",
},
"Version": {
"after_insert": "landa.water_body_management.utils.create_firebase_notification",
},
"Deleted Document": {
"after_insert": "landa.water_body_management.utils.create_firebase_notification",
},
}

# Scheduled Tasks
Expand Down
4 changes: 3 additions & 1 deletion landa/water_body_management/change_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ def _get_version_log_query(self, from_datetime: str):
)

def _get_deleted_document_query(self, from_datetime: str):
# Deleted Document DocType is not joined with File DocType
# because the file does not exist anymore
deleted_document = frappe.qb.DocType("Deleted Document")
return (
frappe.qb.from_(deleted_document)
Expand Down Expand Up @@ -170,7 +172,7 @@ def _build_dependency_change_log(self, entry, changed_data):
and changed_data.attached_to_doctype != "Water Body"
):
# Filter deleted documents that are not attached to water body
return []
return None

if entry.doctype == "Water Body Management Local Organization":
if cint(entry.deleted):
Expand Down
98 changes: 68 additions & 30 deletions landa/water_body_management/utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import json

import firebase_admin
import frappe
from firebase_admin import credentials, messaging
from frappe import _

from landa.firebase_connector import FirebaseNotification
from landa.water_body_management.change_log import ChangeLog

VALID_DOCTYPES = [
"Water Body",
"Fish Species",
"File",
"Water Body Management Local Organization",
"Water Body Rules",
]


def create_version_log(doc, event):
"""Called via hooks.py to create Version Log of document creation"""
Expand All @@ -24,36 +31,67 @@ def create_version_log(doc, event):


def create_firebase_notification(doc, event):
"""Enqueue this on hooks.py to send firebase notification on document creation"""
enabled, file_path = frappe.get_value(
"Water Body Management Settings", None, ["enable_firebase_notifications", "api_file_path"]
)
"""Enqueue this on hooks.py to send firebase notification on doc event"""
if not doc_eligible(doc):
return

enabled, file_path, topic, project_id = get_firebase_settings()
if not enabled:
return

# doc must have keys same as `ChangeLog()._get_changed_data` query
change_log = ChangeLog().format_change(doc)

cred = credentials.Certificate(file_path)
firebase_admin.initialize_app(cred)

# `create_firebase_notification` must be triggered on
# Version Doc creation:
# for: "Water Body", "Fish Species", "File","Water Body Management Local Organization", "Water Body Rules",
# File if: file's ref docs is Water Body
# Deleted Document Doc creation:
# for: "Water Body", "Fish Species", "File","Water Body Management Local Organization"
# Format and send notification
message = messaging.Message(
notification=messaging.Notification(
title="Water Body" + "Created/Updated/Deleted",
body=json.dumps(change_log),
),
token="<TOKEN>",
)
formatted_doc = format_doc_for_change_log(doc)
change_log = ChangeLog().format_change(formatted_doc)
if not change_log:
return

# Send notification to topic
try:
fcm = FirebaseNotification(file_path, project_id)
response = fcm.send_to_topic(topic, change_log)
response.raise_for_status()
except Exception:
frappe.log_error(message=frappe.get_traceback(), title="Firebase Notification Error")


def doc_eligible(doc):

Check notice

Code scanning / CodeQL

Explicit returns mixed with implicit (fall through) returns Note

Mixing implicit and explicit returns may indicate an error as implicit returns always return None.
"""
Check if document is eligible for firebase notification
"""
if doc.doctype == "Version":
if doc.ref_doctype not in VALID_DOCTYPES:
return False

# Send a message to the device corresponding to the provided
# registration token.
response = messaging.send(message)
# Response is a message ID string.
print("Successfully sent message:", response)
if doc.ref_doctype == "File":
return frappe.db.get_value("File", doc.docname, "attached_to_doctype") == "Water Body"

return True

if doc.doctype == "Deleted Document":
return doc.deleted_doctype in VALID_DOCTYPES[:-1]


def format_doc_for_change_log(doc):
"""Format doc for change log"""
doc.attached_to_name = None

if doc.doctype == "Version":
if doc.ref_doctype == "File":
doc.attached_to_name = frappe.db.get_value("File", doc.docname, "attached_to_name")
doc.doctype = doc.ref_doctype
doc.deleted = 0
elif doc.doctype == "Deleted Document":
doc.doctype = doc.deleted_doctype
doc.docname = doc.deleted_name
doc.deleted = 1

return doc


def get_firebase_settings():
"""Get Firebase Settings"""
return frappe.get_cached_value(
"Water Body Management Settings",
None,
["enable_firebase_notifications", "api_file_path", "firebase_topic", "project_id"],
)
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ readme = "README.md"
dynamic = ["version"]
dependencies = [
"thefuzz",
"firebase-admin",
]

[build-system]
Expand Down

0 comments on commit 82d93dc

Please sign in to comment.