fcm-django
=========================


.. image:: https://badge.fury.io/py/fcm-django.svg
    :target: https://badge.fury.io/py/fcm-django


Django app for Firebase Cloud Messaging. Used as an unified platform for sending push notifications to mobile devices & browsers (android / ios / chrome / firefox / ...).

Supports Firebase Cloud Messaging HTTP v1 API. If you're looking for the legacy API, use ``fcm-django<1``!

FCMDevice model fields
 - *registration_id* (required - is FCM token)
 - *name* (optional)
 - *active* (default: true)
 - *user* (optional)
 - *device_id* (optional - can be used to uniquely identify devices)
 - *type* ('android', 'web', 'ios')

Functionality:
 - all necessary migrations
 - model admins for django admin
 - admin actions for testing single and bulk notification sending
 - automatic device pruning: devices to which notifications fail to send are marked as inactive
 - devices marked as inactive will not be sent notifications
 - Django rest framework viewsets

Demo javascript client project
------------------------------
Unsure how to use this project? Check out the demo at:
https://github.com/xtrinch/fcm-django-web-demo

Migration to v1.0
-----------------

We've replaced Python package ``pyfcm`` for Firebase's own package ``firebase-admin``.
Thus, we no longer use an API key. Instead, you'll need an environment variable
``GOOGLE_APPLICATION_CREDENTIALS`` which is a path pointing to your JSON-file stored
credentials. To learn more or view other options to input credentials, visit the
`Google Cloud docs <https://cloud.google.com/docs/authentication/getting-started>`_.

Finally, in your ``settings.py`` (or whatever imported file), add:

.. code-block:: python

    from firebase_admin import initialize_app
    FIREBASE_APP = initialize_app()
    # Or just
    initialize_app()

The API for sending messages is now under the ``firebase-admin`` package; hence,
we removed the methods ``send_data_message`` from the QuerySet and class instance
methods. Instead, everything is under a single method: ``send_message``

.. code-block:: python

    from firebase_admin.messaging import Message, Notification
    FCMDevice.objects.send_message(Message(data=dict()))
    # Note: You can also combine the data and notification kwarg
    FCMDevice.objects.send_message(
        Message(notification=Notification(title="title", body="body", image="image_url"))
    )
    device = FCMDevice.objects.first()
    device.send_message(Message(...))

Additionally, we've added Firebase's new Topic API, allowing for easier sending
of bulk messages.

.. code-block:: python

    from firebase_admin.messaging import Message, Notification
    topic = "A topic"
    FCMDevice.objects.handle_subscription(True, topic)
    FCMDevice.send_topic_message(Message(data={...}), "TOPIC NAME")

There are two additional parameters to both methods:
``skip_registration_id_lookup`` and ``additional_registration_ids``.
Visit `Sending Messages <https://github.com/xtrinch/fcm-django#sending-messages>`_ to learn more.

Note: ``registration_ids`` is actually incorrect terminology as it
should actually be called ``registration tokens``. However, to be
consistent with ``django-push-notifications``, we've refrained from
switching to stay backwards compatible in the docs and with the
sister package.

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

.. code-block:: console

    pip install fcm-django


Edit your settings.py file:

.. code-block:: python

    from firebase_admin import initialize_app

    INSTALLED_APPS = (
        ...
        "fcm_django"
        ...
    )

    # Optional ONLY IF you have initialized a firebase app already:
    # Visit https://firebase.google.com/docs/admin/setup/#python
    # for more options for the following:
    # Store an environment variable called GOOGLE_APPLICATION_CREDENTIALS
    # which is a path that point to a json file with your credentials.
    # Additional arguments are available: credentials, options, name
    FIREBASE_APP = initialize_app()
    # To learn more, visit the docs here:
    # https://cloud.google.com/docs/authentication/getting-started>

    FCM_DJANGO_SETTINGS = {
         # an instance of firebase_admin.App to be used as default for all fcm-django requests
         # default: None (the default Firebase app)
        "DEFAULT_FIREBASE_APP": None,
         # default: _('FCM Django')
        "APP_VERBOSE_NAME": "[string for AppConfig's verbose_name]",
         # true if you want to have only one active device per registered user at a time
         # default: False
        "ONE_DEVICE_PER_USER": True/False,
         # devices to which notifications cannot be sent,
         # are deleted upon receiving error response from FCM
         # default: False
        "DELETE_INACTIVE_DEVICES": True/False,
        # Transform create of an existing Device (based on registration id) into
		    # an update. See the section
        # "Update of device with duplicate registration ID" for more details.
        # default: False
        "UPDATE_ON_DUPLICATE_REG_ID": True/False,
    }

Native Django migrations are in use. ``manage.py migrate`` will install and migrate all models.

Messages
--------

You can read more about different types of messages here_.

.. _here: https://firebase.google.com/docs/cloud-messaging/concept-options

In short, there are two types: notifications and data messages.

Notification:

.. code-block:: python

    from firebase_admin.messaging import Message, Notification
    Message(
        notification=Notification(title="title", body="text", image="url"),
        topic="Optional topic parameter: Whatever you want",
    )

Data message:

.. code-block:: python

    from firebase_admin.messaging import Message
    Message(
        data={
            "Nick" : "Mario",
            "body" : "great match!",
            "Room" : "PortugalVSDenmark"
       },
       topic="Optional topic parameter: Whatever you want",
    )

As in the following example, you can send either a notification, a data message, or both.
You can also customize the Android, iOS, and Web configuration along with additional
FCM conditions. Visit ``firebase_admin.messaging.Message`` to learn more about those
configurations.

Sending messages
----------------

Additional parameters are ``additional_registration_ids`` and
``skip_registration_id_lookup``. View the "Additional Parameters"
section for more information.

.. code-block:: python

    from firebase_admin.messaging import Message
    from fcm_django.models import FCMDevice

    # You can still use .filter() or any methods that return QuerySet (from the chain)
    device = FCMDevice.objects.all().first()
    # send_message parameters include: message, dry_run, app
    device.send_message(Message(data={...}))

Sending messages in bulk
------------------------

.. code-block:: python

    from firebase_admin.messaging import Message
    from fcm_django.models import FCMDevice

    # You can still use .filter() or any methods that return QuerySet (from the chain)
    devices = FCMDevice.objects.all()
    devices.send_message(Message(data={...}))
    # Or (send_message parameters include: messages, dry_run, app)
    FCMDevice.objects.send_message(Message(...))

Subscribing or Unsubscribing Users to topic
-------------------------------------------

.. code-block:: python

    from fcm_django.models import FCMDevice

    # Subscribing
    FCMDevice.objects.all().handle_topic_subscription(True, topic="TOPIC NAME"))
    device = FCMDevice.objects.all().first()
    device.handle_topic_subscription(True, topic="TOPIC NAME"))

    # Finally you can send a message to that topic
    from firebase_admin.messaging import Message
    message = Message(..., topic="A topic")
    # You can still use .filter() or any methods that return QuerySet (from the chain)
    FCMDevice.objects.send_message(message)

    # Unsubscribing
    FCMDevice.objects.all().handle_topic_subscription(False, topic="TOPIC NAME"))
    device = FCMDevice.objects.all().first()
    device.handle_topic_subscription(False, topic="TOPIC NAME"))

Sending messages to topic
-------------------------

.. code-block:: python

    from fcm_django.models import FCMDevice

    FCMDevice.send_topic_message(Message(data={...}), "TOPIC NAME")

Additional Parameters
---------------------

You can add additional_registration_ids (Sequence) for manually
sending registration IDs. It will append these IDs to the queryset
lookup's returned registration IDs.

You can also add skip_registration_id_lookup (bool) to skip database
lookup that goes along with your query.

.. code-block:: python

    from firebase_admin.messaging import Message
    from fcm_django.models import FCMDevice
    FCMDevice.objects.send_message(Message(...), False, ["registration_ids"])

Using multiple FCM apps
-----------------------

By default the message will be sent using the default FCM ``firebase_admin.App`` (we initialized this in our settings),
or the one specified with the ``DEFAULT_FIREBASE_APP`` setting.

This default can be overridden by specifying an app when calling send_message. This can be used to send messages using different firebase projects.

.. code-block:: python

    from firebase_app import App
    from firebase_app.messaging import Notification
    from fcm_django.models import FCMDevice

    device = FCMDevice.objects.all().first()
    device.send_message(notification=Notification(...), app=App(...))

Setting a default Firebase app for FCM
--------------------------------------

If you want to use a specific Firebase app for all fcm-django requests, you can create an instance of
``firebase_admin.App`` and pass it to fcm-django with the ``DEFAULT_FIREBASE_APP`` setting.

The ``DEFAULT_FIREBASE_APP`` will be used for all send / subscribe / unsubscribe requests, include ``FCMDevice``'s
admin actions.

In your ``settings.py``:

.. code-block:: python

    from firebase_admin import initialize_app, credentials
    from google.auth import load_credentials_from_file
    from google.oauth2.service_account import Credentials

    # create a custom Credentials class to load a non-default google service account JSON
    class CustomFirebaseCredentials(credentials.ApplicationDefault):
        def __init__(self, account_file_path: str):
            super().__init__()
            self._account_file_path = account_file_path

        def _load_credential(self):
            if not self._g_credential:
                self._g_credential, self._project_id = load_credentials_from_file(self._account_file_path,
                                                                                  scopes=credentials._scopes)

    # init default firebase app
    # this loads the default google service account with GOOGLE_APPLICATION_CREDENTIALS env variable
    FIREBASE_APP = initialize_app()

    # init second firebase app for fcm-django
    # the environment variable contains a path to the custom google service account JSON
    custom_credentials = CustomFirebaseCredentials(os.getenv('CUSTOM_GOOGLE_APPLICATION_CREDENTIALS'))
    FIREBASE_MESSAGING_APP = initialize_app(custom_credentials, name='messaging')

    FCM_DJANGO_SETTINGS = {
        "DEFAULT_FIREBASE_APP": FIREBASE_MESSAGING_APP,
        # [...] your other settings
    }


Django REST Framework (DRF) support
-----------------------------------

Viewsets come in two different varieties:

- ``FCMDeviceViewSet``

    - Permissions as specified in settings (``AllowAny`` by default, which is not recommended)
    - A device may be registered without associating it with a user
    - Will not allow duplicate registration_id's

- ``FCMDeviceAuthorizedViewSet``

    - Permissions are ``IsAuthenticated`` and custom permission ``IsOwner``, which will only allow the ``request.user`` to get and update devices that belong to that user
    - Requires a user to be authenticated, so all devices will be associated with a user
    - Will allow duplicate registration_id's for different users, so you are responsible for cleanup (if you do not want duplicate registration id's, use the ``UPDATE_ON_DUPLICATE_REG_ID`` flag)

Routes can be added one of two ways:

- `Routers`_ (include all views)

.. _Routers: http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers#using-routers

.. code-block:: python

    from fcm_django.api.rest_framework import FCMDeviceAuthorizedViewSet

    from rest_framework.routers import DefaultRouter

    router = DefaultRouter()

    router.register('devices', FCMDeviceAuthorizedViewSet)

    urlpatterns = [
        # URLs will show up at <api_root>/devices
        # DRF browsable API which lists all available endpoints
        path('', include(router.urls)),
        # ...
    ]

- Using `as_view`_ (specify which views to include)

.. _as_view: http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers#binding-viewsets-to-urls-explicitly

.. code-block:: python

    from fcm_django.api.rest_framework import FCMDeviceAuthorizedViewSet

    urlpatterns = [
        # Only allow creation of devices by authenticated users
        path('devices', FCMDeviceAuthorizedViewSet.as_view({'post': 'create'}), name='create_fcm_device'),
        # ...
    ]

Update of device with duplicate registration ID
-----------------------------------------------

The DRF viewset enforce the uniqueness of the registration ID. In some use case it
may cause an issue: If an already registered mobile device changes its user, then
it will fail to register because the registration ID already exist.

When option ``UPDATE_ON_DUPLICATE_REG_ID`` is set to True, then any creation of
device with an already existing registration ID will be transformed into an update.

The ``UPDATE_ON_DUPLICATE_REG_ID`` only works with DRF.

Python 3 support
----------------
``fcm-django`` is fully compatible with Python 3.6+

Django version compatibility
----------------------------
Compatible with Django versions 3.0+.
For Django version 2.2, use version ``fcm-django < 2``.
For lower django versions, use version ``fcm-django < 1``.

Acknowledgements
----------------
Library relies on firebase-admin-sdk for sending notifications, for more info about all the possible fields, see:
https://github.com/firebase/firebase-admin-python

Migration from v0 to v1 was done by `Andrew-Chen-Wang <https://github.com/Andrew-Chen-Wang>`_

Need help, have any questions, suggestions?
-------------------------------------------
Submit an issue/PR on this project. Please do not send me emails, as then the community has no chance to see your questions / provide answers.

Contributing
------------

To setup the development environment, simply do ``pip install -r requirements.txt``
To manually run the pre-commit hook, run `pre-commit run --all-files`.
