Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added v1 endpoints for pyFCm in new v1 package. #319

Merged
merged 4 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ jobs:
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
export FCM_TEST_API_KEY=AAA
export FCM_TEST_PROJECT_ID=test
pip install . ".[test]"
python -m pytest .
Empty file added pyfcm/v1/__init__.py
Empty file.
200 changes: 200 additions & 0 deletions pyfcm/v1/baseapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import os
import threading

from pyfcm.baseapi import BaseAPI

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

from pyfcm.errors import InvalidDataError


class BaseAPI(BaseAPI):
FCM_END_POINT = "https://fcm.googleapis.com/v1/projects"

def __init__(self, service_account_file_path: str, project_id: str, proxy_dict=None, env=None, json_encoder=None,
adapter=None):
"""
Override existing init function to give ability to use v1 endpoints of Firebase Cloud Messaging API
Attributes:
service_account_file_path (str): path to service account JSON file
project_id (str): project ID of Google account
proxy_dict (dict): proxy settings dictionary, use proxy (keys: `http`, `https`)
env (dict): environment settings dictionary, for example "app_engine"
json_encoder (BaseJSONEncoder): JSON encoder
adapter (BaseAdapter): adapter instance
"""
self.service_account_file = service_account_file_path
self.project_id = project_id
self.FCM_END_POINT = self.FCM_END_POINT + f"/{self.project_id}/messages:send"
self.FCM_REQ_PROXIES = None
self.custom_adapter = adapter
self.thread_local = threading.local()

if proxy_dict and isinstance(proxy_dict, dict) and (('http' in proxy_dict) or ('https' in proxy_dict)):
self.FCM_REQ_PROXIES = proxy_dict
self.requests_session.proxies.update(proxy_dict)
self.send_request_responses = []

if env == 'app_engine':
try:
from requests_toolbelt.adapters import appengine
appengine.monkeypatch()
except ModuleNotFoundError:
pass

self.json_encoder = json_encoder

def _get_access_token(self):
"""
Generates access from refresh token that contains in the service_account_file.
If token expires then new access token is generated.
Returns:
str: Access token
"""
# get OAuth 2.0 access token
try:
credentials = service_account.Credentials.from_service_account_file(
self.service_account_file,
scopes=['https://www.googleapis.com/auth/firebase.messaging'])
request = google.auth.transport.requests.Request()
credentials.refresh(request)
return credentials.token
except Exception as e:
raise InvalidDataError(e)

def request_headers(self):
"""
Generates request headers including Content-Type and Authorization of Bearer token

Returns:
dict: request headers
"""
return {
"Content-Type": self.CONTENT_TYPE,
"Authorization": "Bearer " + self._get_access_token(),
}

def parse_payload(self,
registration_ids=None,
topic_name=None,
message_body=None,
message_title=None,
message_icon=None,
sound=None,
condition=None,
collapse_key=None,
delay_while_idle=False,
time_to_live=None,
restricted_package_name=None,
low_priority=False,
dry_run=False,
data_message=None,
click_action=None,
badge=None,
color=None,
tag=None,
body_loc_key=None,
body_loc_args=None,
title_loc_key=None,
title_loc_args=None,
content_available=None,
remove_notification=False,
**extra_kwargs):

"""

:rtype: json
"""
fcm_payload = dict()
if registration_ids:
if len(registration_ids) > 1:
fcm_payload['registration_ids'] = registration_ids
else:
fcm_payload['token'] = registration_ids[0]
if condition:
fcm_payload['condition'] = condition
else:
# In the `to` reference at: https://firebase.google.com/docs/cloud-messaging/http-server-ref#send-downstream
# We have `Do not set this field (to) when sending to multiple topics`
# Which is why it's in the `else` block since `condition` is used when multiple topics are being targeted
if topic_name:
fcm_payload['to'] = '/topics/%s' % topic_name
# if low_priority:
# fcm_payload['priority'] = self.FCM_LOW_PRIORITY
# else:
# fcm_payload['priority'] = self.FCM_HIGH_PRIORITY

if delay_while_idle:
fcm_payload['delay_while_idle'] = delay_while_idle
if collapse_key:
fcm_payload['collapse_key'] = collapse_key
if time_to_live:
if isinstance(time_to_live, int):
fcm_payload['time_to_live'] = time_to_live
else:
raise InvalidDataError("Provided time_to_live is not an integer")
if restricted_package_name:
fcm_payload['restricted_package_name'] = restricted_package_name
if dry_run:
fcm_payload['dry_run'] = dry_run

if data_message:
if isinstance(data_message, dict):
fcm_payload['message'] = data_message
else:
raise InvalidDataError("Provided data_message is in the wrong format")

fcm_payload['notification'] = {}
if message_icon:
fcm_payload['notification']['icon'] = message_icon
# If body is present, use it
if message_body:
fcm_payload['notification']['body'] = message_body
# Else use body_loc_key and body_loc_args for body
else:
if body_loc_key:
fcm_payload['notification']['body_loc_key'] = body_loc_key
if body_loc_args:
if isinstance(body_loc_args, list):
fcm_payload['notification']['body_loc_args'] = body_loc_args
else:
raise InvalidDataError('body_loc_args should be an array')
# If title is present, use it
if message_title:
fcm_payload['notification']['title'] = message_title
# Else use title_loc_key and title_loc_args for title
else:
if title_loc_key:
fcm_payload['notification']['title_loc_key'] = title_loc_key
if title_loc_args:
if isinstance(title_loc_args, list):
fcm_payload['notification']['title_loc_args'] = title_loc_args
else:
raise InvalidDataError('title_loc_args should be an array')

# This is needed for iOS when we are sending only custom data messages
if content_available and isinstance(content_available, bool):
fcm_payload['content_available'] = content_available

if click_action:
fcm_payload['notification']['click_action'] = click_action
if badge:
fcm_payload['notification']['badge'] = badge
if color:
fcm_payload['notification']['color'] = color
if tag:
fcm_payload['notification']['tag'] = tag
# only add the 'sound' key if sound is not None
# otherwise a default sound will play -- even with empty string args.
if sound:
fcm_payload['notification']['sound'] = sound

if extra_kwargs:
fcm_payload['notification'].update(extra_kwargs)

# Do this if you only want to send a data message.
if remove_notification:
del fcm_payload['notification']

return self.json_dumps({"message": fcm_payload})
6 changes: 6 additions & 0 deletions pyfcm/v1/fcm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pyfcm import FCMNotification
from pyfcm.v1.baseapi import BaseAPI

class FCMNotification(FCMNotification, BaseAPI):
def __init__(self, *args, **kwargs):
super(BaseAPI).__init__(*args, **kwargs)
8 changes: 7 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
requests>=2.6.0
aiohttp>=3.6.2
cachetools==5.3.3
google-auth==2.29.0
pyasn1==0.6.0
pyasn1-modules==0.4.0
rsa==4.9
requests>=2.6.0
urllib3==1.26.5

73 changes: 73 additions & 0 deletions tests/test_v1_baseapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import os
import json
import pytest

from pyfcm import errors
from pyfcm.v1.baseapi import BaseAPI


@pytest.fixture(scope="module")
def base_api():
service_account_file_path = "service_account.json"
project_id = os.getenv("FCM_TEST_PROJECT_ID", None)
assert project_id, "Please set the environment variables for testing according to CONTRIBUTING.rst"

return BaseAPI(service_account_file_path=service_account_file_path, project_id=project_id)


def test_parse_payload(base_api):
json_string = base_api.parse_payload(
registration_ids=["Test"],
message_body="Test",
message_title="Test",
message_icon="Test",
sound="Test",
collapse_key="Test",
delay_while_idle=False,
time_to_live=0,
restricted_package_name="Test",
low_priority=False,
dry_run=False,
data_message={"test": "test"},
click_action="Test",
badge="Test",
color="Test",
tag="Test",
body_loc_key="Test",
body_loc_args="Test",
title_loc_key="Test",
title_loc_args="Test",
content_available="Test",
android_channel_id="Test",
timeout=5,
extra_notification_kwargs={},
extra_kwargs={}
)

data = json.loads(json_string.decode("utf-8"))
assert data["message"]["notification"] == {
"android_channel_id": "Test",
"badge": "Test", "body": "Test",
"click_action": "Test",
"color": "Test",
"extra_kwargs": {},
"extra_notification_kwargs": {},
"icon": "Test",
"sound": "Test",
"tag": "Test",
"timeout": 5,
"title": "Test"
}


def test_parse_responses(base_api):
response = base_api.parse_responses()

assert response == {
"multicast_ids": [],
"success": 0,
"failure": 0,
"canonical_ids": 0,
"results": [],
"topic_message_id": None
}