Firebase cloud messaging for Django with original Google SDK.
- 2023-03-23: Version
0.2.0- Overhaul
FCMDeviceto be customizable - Remove
FCM_USER_MODELand addFCM_DEVICE_MODELconfig settings (see below)
- Overhaul
- 2023-03-10: Version
0.1.0- First beta release
- Standard push notifications
- Localized push notifications
- Sending in foreground and background with celery
- Requirements
- Add
firebase_pushto yourrequirements.txt/Pipfile/pyproject.toml - Import the default settings at the end of your
settings.py:from firebase_push.conf.settings import *
- Override default settings if needed (see next section)
- URLs
- Add urls to your
urlpatternsinurls.pyfrom firebase_push.conf.urls import urlpatterns as firebase_push_urlpatterns urlpatterns += firebase_push_urlpatterns
- Application
- Add
firebase_pushandrest_frameworkto yourINSTALLED_APPSINSTALLED_APPS = [ "firebase_push", "rest_framework", "admin_extra_buttons", ... ]
- Add a
FCMHistoryandFCMDeviceclass to your application:
from firebase_push.models import FCMHistoryBase, FCMDeviceBase
class FCMHistory(FCMHistoryBase):
pass
class FCMDevice(FCMDeviceBase):
pass- Point the setting
FCM_PUSH_HISTORY_MODELandFCM_DEVICE_MODELto that class:
FCM_PUSH_HISTORY_MODEL = "demo.FCMHistory"
FCM_DEVICE_MODEL = "demo.FCMDevice"- Run
manage.py makemigrationsandmanage.py migrate - Do not forget to configure REST-Framework authentication (or supply CSRF Tokens when calling the API :S)
Set environment variable GOOGLE_APPLICATION_CREDENTIALS to the path to your
service account JSON file:
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account.jsonAlternatively you can use the django setting FCM_CREDENTIALS_FILE to provide the path.
- In the Firebase console, open Settings > Service Accounts.
- Click Generate New Private Key, then confirm by clicking Generate Key.
- Securely store the JSON file containing the key.
FCM_FETCH_USER_FUNCTION: (path) path to the function to call in your code to fetch a user id to attach to the request. Will be called with the Djangorequestas single parameter, expected to return an id to a DB model instance of the model used in yourFCMDeviceclass.
If you send your push notifications without sync=True you will need a running
celery worker.
To configure celery you will need at least the following in your settings.py
# Celery broker URL for redis transport
CELERY_BROKER_URL = f"redis://localhost:6379/1"To start the celery worker user something like this:
export GOOGLE_APPLICATION_CREDENTIALS=serviceaccount.json # Point this to the google service account
export DJANGO_SETTINGS_MODULE=demo.settings.native # Point this to your config
celery --app demo worker # demo here stands as placeholder for your applicationTo send Push Notifications manually there is an extra button in the Django admin for the FCMHistory class:
firebase-push/: registration endpoint, call this on app-activation
Payload:
{
"registration_id": "<fcm_token>",
"topics": [ "default" ],
"platform": "ios",
"app_version": "2.0"
}registration_id: FCM Token from Firebase SDKtopics: List of topics to subscribe to. If left out defaults todefaultplatform: app platform, one ofandroid,ios,web, if left out defaults tounknownapp_version: app version string, if left out defaults to empty string
Reply:
{
"registration_id": "<FCM token>",
"topics": [
"default"
],
"platform": "ios",
"app_version": "2.0",
"created_at": "2023-01-30T15:20:44.265191",
"updated_at": "2023-01-30T15:20:44.265208"
}You may POST new values, autentication for the user is handled by REST-Framework. If the user of the registration
changes, the old registration is removed and a new one is created. This is done to avoid receiving notifications of
other users when frequently switching accounts while testing the app.
To update for example the subscribed topics you may call PATCH on the endpoint with appended registration ID (like
firebase-push/<bla>) and only specify the changed values in the payload. If the registration is currently recorded
for a different user, the old registration will be removed as with POST. Calling the endpoint with PATCH is possible
but the utility of this is limited, better stick to POST and include all values to make sure everything is recorded
in the DB correctly.
If you call the endpoint with GET you will get a list of all registrations of the current user.
If you call the endpoint with DELETE and appended registration ID (like firebase-push/<bla>) the push registration
will be deleted from the server if the current user owns it and you will receive a 204 No Content response.
There are 3 Models of which one is an abstract model.
FCMDeviceakaFCMDeviceBase: The device registration, contains FCM tokens and some metadata about the device. You can override theuserfield or add your own fields to modify this class.FCMTopic: A topic for which a device can register. Can be used to filter which messages to send to which devicesFCMHistoryakaFCMHistoryBase: This is the abstract model for the push notification history. You can add your own fields to this to save additional information about a message.
registration_idthe FCM Tokenuseruser to which this device belongs, devices will cascade delete with this usertopicstopics which the device subscribes toplatformplatform as reported by the device on registration, one ofandroid,ios,web,unknownapp_versionstringified application version as reported by devicecreated_at,updated_at,disabled_atsome dates used by the cleanup scripts
message_idinternal UUID to identify messages that were sent in one batchmessage_dataJSON data that was sent to firebasedevicedevice this message was sent to (will be set toNoneif the device is removed)userthe user that this message was sent to (will cascade delete the history if removed)topicoptional: topic this message was sent tostatusone ofpending,sent,failederror_messageifstatusis failed this contains the error messagecreated_at,updated_atsome dates used by the cleanup scripts
if you override the history class to add custom data to it, it is probably a good idea to override the
PushMessage classes too to hook into history item creation:
# Data model
from django.db import models
from firebase_push.models import FCMHistoryBase
class FCMHistory(FCMHistoryBase):
foobar = models.CharField(max_length=255, null=False, blank=False)
# Push message override
from typing import Any
from firebase_push.message import PushMessage
class MyPushMessage(PushMessage):
def __init__(self, title: str = "", body: str = "", link: Optional[str]=None, foobar: str = "nothing"):
super().__init(title, body, link=link)
self.foobar = foobar
def serialize(self) -> dict[str, Any]:
result = super().serialize()
result.update(foobar=self.foobar)
return result
def deserialize(self, data: dict[str, Any]):
super().deserialize(data)
self.foobar = data['foobar']
def create_history_entries(self, *args, **kwargs) -> list[FCMHistory]:
entries = super().create_history_entries(*args, **kwargs)
for entry in entries:
entry.foobar = self.foobar
return entriesTo send a message use one of the PushMessageBase subclasses like provided:
PushMessage: Basic push message class, no i18n logicLocalizedPushMessage: Push message with i18n logic
Basic interface is like this:
from firebase_push.message import PushMessage
msg = PushMessage("Title", "body text", link="http://google.com")
msg.add_user(some_user)
msg.add_topic('test')
msg.send()This will send the message to all devices registered to the user some_user that subscribe the test topic.
Sending will be performed in the background via Celery task. The celery task will update the automatically
created FCMHistory object once it has been processed. If you send to a topic that does not exist it is created on the
spot, but will then of course reach no device.
There are optional additional attributes you may set for a message:
Common attributes:
collapse_id: If multiple messages with this ID are sent they are collapsedbadge_count: Badge count to display on app icon, may not work for all android devices, set to 0 to remove badgedata_available: Set toTrueto trigger the app to be launched in background for a data download.sound: Play a sound for the notification, set todefaultto play default sound or to name of sound file in app-bundle otherwise.data: Custom dictionary of strings to append as data to the message
Android specific:
android_icon: Icon for the notificationcolor: CSS-Style Hex color for the notificationexpiration: Date until which the message is valid to be deliveredis_priority: Set toTrueto make it a priority message
Web specific:
web_actions: Actions for the push notifications, is a tuple:("title", "action", "icon")web_icon: Icon for the notification
To send a localizable push message you can use Android style format strings and replacement parameters. Web-Push notifications do not have a local stored translation table so they will be sent by using Django's translation facilities, so make sure to set the correct language environment before sending a message.
To create a localization for the Web-Push strings convert them to python .format() style strings and add
them to the gettext .po file.
Basic interface works like this:
from firebase_push.message import PushMessage
msg = PushMessage(
"Title with %s",
"body number %d, string: %s",
link="http://google.com"
title_args=["placeholders"],
body_args=[10, 'a string']
)
msg.add_user(some_user)
msg.add_topic('test')
msg.send()For testing you can use msg.send(sync=True) to send the message immediately without involving celery
This will send a translateable message. The clients have to have translation tables.
- Android needs just
Title with %sandbody number %d, string: %sin the strings - iOS will use something like
Title with %@andbody number %@, string: %@ - For Django/Web you will need
Title with {:s}andbody number {:d}, string: {:s}
It is possible to add positioning hints:
- Android
%1$sand%2$dto declare parameters as first and second - iOS will have it replaced with
%1$@and%2$@ - For Django/Web use
{1:s}and{2:s}
There are some additional attributes:
Apple specific:
action_loc: Identifier for the action item that is displayed (optional, will not display an action if undefined)
Web specific:
web_actions:actionandtitleshould be translatable strings or translation identifiers
Automatic cleanup of outdated device registrations and push notification history works by calling management commands. By default history is removed after 6 months, device registrations are disabled after 2 months as per recommendation from google. Disabled devices are removed after them being disabled for 2 months.
python manage.py age_devices [-s <days>]python manage.py cleanup_devices [-s <days>]python manage.py cleanup_history [-s <days>]
Attention: As firebase_push does not control what is saved in the push notification history the cleanup_history
command may fail on unknown database constraints. Please duplicate the management command if that may happen with your
implementation.
