Skip to content

Commit

Permalink
Add FCM v1 API (#702)
Browse files Browse the repository at this point in the history
* Add FCM v1 API

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add Unit Tests for GCMDeviceAdmin and fix FCM v1 tests

* fixes admin test for python 3.6

---------

Co-authored-by: Tim Jahn <tim@smartfactory.ch>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 14, 2024
1 parent 7d28052 commit c23e49d
Show file tree
Hide file tree
Showing 20 changed files with 1,043 additions and 884 deletions.
56 changes: 32 additions & 24 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@ Dependencies
- For WebPush (WP), pywebpush 1.3.0+ is required (optional). py-vapid 1.3.0+ is required for generating the WebPush private key; however this
step does not need to occur on the application server.
- For Apple Push (APNS), apns2 0.3+ is required (optional).
- For FCM, firebase-admin 5+ is required (optional).

Setup
-----
You can install the library directly from pypi using pip:

.. code-block:: shell
$ pip install django-push-notifications[WP,APNS]
$ pip install django-push-notifications[WP,APNS,FCM]
Edit your settings.py file:
Expand All @@ -56,9 +57,13 @@ Edit your settings.py file:
"push_notifications"
)
# Import the firebase service
from firebase_admin import auth
# Initialize the default app (either use `GOOGLE_APPLICATION_CREDENTIALS` environment variable, or pass a firebase_admin.credentials.Certificate instance)
default_app = firebase_admin.initialize_app()
PUSH_NOTIFICATIONS_SETTINGS = {
"FCM_API_KEY": "[your api key]",
"GCM_API_KEY": "[your api key]",
"APNS_CERTIFICATE": "/path/to/your/certificate.pem",
"APNS_TOPIC": "com.example.push_test",
"WNS_PACKAGE_SECURITY_ID": "[your package security id, e.g: 'ms-app://e-3-4-6234...']",
Expand All @@ -68,7 +73,10 @@ Edit your settings.py file:
}
.. note::
If you need to support multiple mobile applications from a single Django application, see `Multiple Application Support <https://github.com/jazzband/django-push-notifications/wiki/Multiple-Application-Support>`_ for details.
To migrate from legacy FCM APIs to HTTP v1, see `docs/FCM <https://github.com/jazzband/django-push-notifications/blob/master/docs/FCM.rst>`_.

.. note::
If you need to support multiple mobile applications from a single Django application, see `Multiple Application Support <https://github.com/jazzband/django-push-notifications/wiki/Multiple-Application-Support>`_ for details.

.. note::
If you are planning on running your project with ``APNS_USE_SANDBOX=True``, then make sure you have set the
Expand All @@ -87,7 +95,6 @@ Settings list
-------------
All settings are contained in a ``PUSH_NOTIFICATIONS_SETTINGS`` dict.

In order to use FCM/GCM, you are required to include ``FCM_API_KEY`` or ``GCM_API_KEY``.
For APNS, you are required to include ``APNS_CERTIFICATE``.
For WNS, you need both the ``WNS_PACKAGE_SECURITY_KEY`` and the ``WNS_SECRET_KEY``.

Expand All @@ -109,11 +116,8 @@ For WNS, you need both the ``WNS_PACKAGE_SECURITY_KEY`` and the ``WNS_SECRET_KEY

**FCM/GCM settings**

- ``FCM_API_KEY``: Your API key for Firebase Cloud Messaging.
- ``FCM_POST_URL``: The full url that FCM notifications will be POSTed to. Defaults to https://fcm.googleapis.com/fcm/send.
- ``FIREBASE_APP``: Firebase app instance that is used to send the push notification. If not provided, the app will be using the default app instance that you've instantiated with ``firebase_admin.initialize_app()``.
- ``FCM_MAX_RECIPIENTS``: The maximum amount of recipients that can be contained per bulk message. If the ``registration_ids`` list is larger than that number, multiple bulk messages will be sent. Defaults to 1000 (the maximum amount supported by FCM).
- ``FCM_ERROR_TIMEOUT``: The timeout on FCM POSTs.
- ``GCM_API_KEY``, ``GCM_POST_URL``, ``GCM_MAX_RECIPIENTS``, ``GCM_ERROR_TIMEOUT``: Same parameters for GCM

**WNS settings**

Expand Down Expand Up @@ -147,6 +151,15 @@ FCM/GCM and APNS services have slightly different semantics. The app tries to of
# but for more complex nested collections the extras dict will be sent via
# the bulk message api.
device.send_message(None, extra={"foo": "bar"})
# You may also pass a Firebase message object.
device.send_message(messaging.Message(
notification=messaging.Notification(
title='Hello World',
body='What a beautiful day.'
),
))
# If you want to use gcm.send_message directly, you will have to use messaging.Message.
device = APNSDevice.objects.get(registration_id=apns_token)
device.send_message("You've got mail") # Alert message may only be sent as text.
Expand Down Expand Up @@ -215,19 +228,17 @@ value per user. Assuming User model has a method get_badge returning badge count
badge=lambda token: APNSDevice.objects.get(registration_id=token).user.get_badge()
)
Firebase vs Google Cloud Messaging
Firebase
----------------------------------

``django-push-notifications`` supports both Google Cloud Messaging and Firebase Cloud Messaging (which is now the officially supported messaging platform from Google). When registering a device, you must pass the ``cloud_message_type`` parameter to set the cloud type that matches the device needs.
This is currently defaulting to ``'GCM'``, but may change to ``'FCM'`` at some point. You are encouraged to use the `officially supported library <https://developers.google.com/cloud-messaging/faq>`_.
``django-push-notifications`` supports Firebase Cloud Messaging v1.

When using FCM, ``django-push-notifications`` will automatically use the `notification and data messages format <https://firebase.google.com/docs/cloud-messaging/concept-options#notifications_and_data_messages>`_ to be conveniently handled by Firebase devices. You may want to check the payload to see if it matches your needs, and review your notification statuses in `FCM Diagnostic console <https://support.google.com/googleplay/android-developer/answer/2663268?hl=en>`_.


.. code-block:: python
# Create a FCM device
fcm_device = GCMDevice.objects.create(registration_id="token", cloud_message_type="FCM", user=the_user)
fcm_device = GCMDevice.objects.create(registration_id="token", user=the_user)
# Send a notification message
fcm_device.send_message("This is a message")
Expand All @@ -247,14 +258,10 @@ When using FCM, ``django-push-notifications`` will automatically use the `notifi
# Send a data message only
fcm_device.send_message(None, extra={"other": "content", "misc": "data"})
You can disable this default behaviour by setting ``use_fcm_notifications`` to ``False``.

.. code-block:: python
fcm_device = GCMDevice.objects.create(registration_id="token", cloud_message_type="FCM", user=the_user)
# Send a data message with classic format
fcm_device.send_message("This is a message", use_fcm_notifications=False)
Behind the scenes, a `Firebase Message <https://firebase.google.com/docs/reference/admin/dotnet/class/firebase-admin/messaging/message>`_ will be created.
You can also create this yourself and pass it to the ``send_message`` method instead.


Sending FCM/GCM messages to topic members
Expand All @@ -264,18 +271,19 @@ Note: gcm_send_bulk_message must be used when sending messages to topic subscrib

.. code-block:: python
from push_notifications.gcm import send_message
from push_notifications.gcm import send_message, dict_to_fcm_message
# First param is "None" because no Registration_id is needed, the message will be sent to all devices subscribed to the topic.
send_message(None, {"body": "Hello members of my_topic!"}, cloud_type="FCM", to="/topics/my_topic")
# Create message object from dictonary. You can also directly create a messaging.Message object.
message = dict_to_fcm_message({"body": "Hello members of my_topic!"})
# First param is "None" because no Registration_id is needed, the message will be sent to all devices subscribed to the topic.
send_message(None, message, to="/topics/my_topic")
Reference: `FCM Documentation <https://firebase.google.com/docs/cloud-messaging/android/topic-messaging>`_

Exceptions
----------

- ``NotificationError(Exception)``: Base exception for all notification-related errors.
- ``gcm.GCMError(NotificationError)``: An error was returned by GCM. This is never raised when using bulk notifications.
- ``apns.APNSError(NotificationError)``: Something went wrong upon sending APNS notifications.
- ``apns.APNSDataOverflow(APNSError)``: The APNS payload exceeds its maximum size and cannot be sent.

Expand Down
111 changes: 111 additions & 0 deletions docs/FCM.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
Generate service account private key file
------------------------------

Migrating to FCM v1 API
------------------------------

- GCM and legacy FCM API support have been removed. (GCM is off since 2019, FCM legacy will be turned off in june 2024)
- Firebase-Admin SDK has been added


Authentication does not work with an access token anymore.
Follow the `official docs <https://firebase.google.com/docs/admin/setup/#initialize_the_sdk_in_non-google_environments>`_ to generate a service account private key file.

Then, either define an environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` with the path to the service account private key file, or pass the path to the file explicitly when initializing the SDK.

Initialize the firebase admin in your ``settings.py`` file.

.. code-block:: python
# Import the firebase service
from firebase_admin import auth
# Initialize the default app
default_app = firebase_admin.initialize_app()
This will do the trick.


Multiple Application Support
------------------------------

Removed settings:

- ``API_KEY``
- ``POST_URL``
- ``ERROR_TIMEOUT``

Added setting:

- ``FIREBASE_APP``: initialise your firebase app and set it here.


.. code-block:: python
# Before
PUSH_NOTIFICATIONS_SETTINGS = {
# Load and process all PUSH_NOTIFICATIONS_SETTINGS using the AppConfig manager.
"CONFIG": "push_notifications.conf.AppConfig",
# collection of all defined applications
"APPLICATIONS": {
"my_fcm_app": {
# PLATFORM (required) determines what additional settings are required.
"PLATFORM": "FCM",
# required FCM setting
"API_KEY": "[your api key]",
},
"my_ios_app": {
# PLATFORM (required) determines what additional settings are required.
"PLATFORM": "APNS",
# required APNS setting
"CERTIFICATE": "/path/to/your/certificate.pem",
},
"my_wns_app": {
# PLATFORM (required) determines what additional settings are required.
"PLATFORM": "WNS",
# required WNS settings
"PACKAGE_SECURITY_ID": "[your package security id, e.g: 'ms-app://e-3-4-6234...']",
"SECRET_KEY": "[your app secret key, e.g.: 'KDiejnLKDUWodsjmewuSZkk']",
},
}
}
# After
firebase_app = firebase_admin.initialize_app()
PUSH_NOTIFICATIONS_SETTINGS = {
# Load and process all PUSH_NOTIFICATIONS_SETTINGS using the AppConfig manager.
"CONFIG": "push_notifications.conf.AppConfig",
# collection of all defined applications
"APPLICATIONS": {
"my_fcm_app": {
# PLATFORM (required) determines what additional settings are required.
"PLATFORM": "FCM",
# FCM settings
"FIREBASE_APP": firebase_app,
},
"my_ios_app": {
# PLATFORM (required) determines what additional settings are required.
"PLATFORM": "APNS",
# required APNS setting
"CERTIFICATE": "/path/to/your/certificate.pem",
},
"my_wns_app": {
# PLATFORM (required) determines what additional settings are required.
"PLATFORM": "WNS",
# required WNS settings
"PACKAGE_SECURITY_ID": "[your package security id, e.g: 'ms-app://e-3-4-6234...']",
"SECRET_KEY": "[your app secret key, e.g.: 'KDiejnLKDUWodsjmewuSZkk']",
},
}
}
31 changes: 31 additions & 0 deletions push_notifications/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,37 @@ class GCMDeviceAdmin(DeviceAdmin):
)
list_filter = ("active", "cloud_message_type")

def send_messages(self, request, queryset, bulk=False):
"""
Provides error handling for DeviceAdmin send_message and send_bulk_message methods.
"""
results = []
errors = []

if bulk:
results.append(queryset.send_message("Test bulk notification"))
else:
for device in queryset:
result = device.send_message("Test single notification")
if result:
results.append(result)

for batch in results:
for response in batch.responses:
if response.exception:
errors.append(repr(response.exception))

if errors:
self.message_user(
request, _("Some messages could not be processed: %s") % (", ".join(errors)),
level=messages.ERROR
)
else:
self.message_user(
request, _("All messages were sent."),
level=messages.SUCCESS
)


class WebPushDeviceAdmin(DeviceAdmin):
list_display = ("__str__", "browser", "user", "active", "date_created")
Expand Down
2 changes: 1 addition & 1 deletion push_notifications/conf/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from django.utils.module_loading import import_string

from ..settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS # noqa: I001
from .app import AppConfig # noqa: F401
from .appmodel import AppModelConfig # noqa: F401
from .legacy import LegacyConfig # noqa: F401
from ..settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS # noqa: I001


manager = None
Expand Down
43 changes: 9 additions & 34 deletions push_notifications/conf/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
PLATFORMS = [
"APNS",
"FCM",
"GCM",
"WNS",
"WP",
]
Expand Down Expand Up @@ -55,9 +54,9 @@
"USE_SANDBOX", "USE_ALTERNATIVE_PORT", "TOPIC"
]

FCM_REQUIRED_SETTINGS = GCM_REQUIRED_SETTINGS = ["API_KEY"]
FCM_OPTIONAL_SETTINGS = GCM_OPTIONAL_SETTINGS = [
"POST_URL", "MAX_RECIPIENTS", "ERROR_TIMEOUT"
FCM_REQUIRED_SETTINGS = []
FCM_OPTIONAL_SETTINGS = [
"MAX_RECIPIENTS", "FIREBASE_APP"
]

WNS_REQUIRED_SETTINGS = ["PACKAGE_SECURITY_ID", "SECRET_KEY"]
Expand Down Expand Up @@ -189,23 +188,8 @@ def _validate_fcm_config(self, application_id, application_config):
application_id, application_config, FCM_REQUIRED_SETTINGS
)

application_config.setdefault("POST_URL", "https://fcm.googleapis.com/fcm/send")
application_config.setdefault("FIREBASE_APP", None)
application_config.setdefault("MAX_RECIPIENTS", 1000)
application_config.setdefault("ERROR_TIMEOUT", None)

def _validate_gcm_config(self, application_id, application_config):
allowed = (
REQUIRED_SETTINGS + OPTIONAL_SETTINGS + GCM_REQUIRED_SETTINGS + GCM_OPTIONAL_SETTINGS
)

self._validate_allowed_settings(application_id, application_config, allowed)
self._validate_required_settings(
application_id, application_config, GCM_REQUIRED_SETTINGS
)

application_config.setdefault("POST_URL", "https://android.googleapis.com/gcm/send")
application_config.setdefault("MAX_RECIPIENTS", 1000)
application_config.setdefault("ERROR_TIMEOUT", None)

def _validate_wns_config(self, application_id, application_config):
allowed = (
Expand Down Expand Up @@ -303,23 +287,14 @@ def _get_application_settings(self, application_id, platform, settings_key):

return app_config.get(settings_key)

def get_firebase_app(self, application_id=None):
return self._get_application_settings(application_id, "FCM", "FIREBASE_APP")

def has_auth_token_creds(self, application_id=None):
return self.has_token_creds

def get_gcm_api_key(self, application_id=None):
return self._get_application_settings(application_id, "GCM", "API_KEY")

def get_fcm_api_key(self, application_id=None):
return self._get_application_settings(application_id, "FCM", "API_KEY")

def get_post_url(self, cloud_type, application_id=None):
return self._get_application_settings(application_id, cloud_type, "POST_URL")

def get_error_timeout(self, cloud_type, application_id=None):
return self._get_application_settings(application_id, cloud_type, "ERROR_TIMEOUT")

def get_max_recipients(self, cloud_type, application_id=None):
return self._get_application_settings(application_id, cloud_type, "MAX_RECIPIENTS")
def get_max_recipients(self, application_id=None):
return self._get_application_settings(application_id, "FCM", "MAX_RECIPIENTS")

def get_apns_certificate(self, application_id=None):
r = self._get_application_settings(application_id, "APNS", "CERTIFICATE")
Expand Down
Loading

0 comments on commit c23e49d

Please sign in to comment.