django-push-notifications

repository·master·Indexed 25 days ago

https://github.com/jazzband/django-push-notifications

A minimal Django application providing models to manage and send push notifications across multiple platforms, including APNS (Apple), FCM (Google), WNS (Windows), and WebPush. It supports the Firebase Admin SDK for FCM v1 API, VAPID key generation for Web Push, and provides integration options via Django Rest Framework for device registration.

Tokens
7.3K
Snippets
13
Records
24
Agent score
81%

What's inside django-push-notifications

  1. Understand Device models and attributes

    master

    The app provides four specific models for different push services: GCMDevice, APNSDevice, WNSDevice, and WebPushDevice.

    All these models share the following attributes:

    • name (optional): A name for the device.
    • active (default True): Boolean determining if the device receives notifications.
    • user (optional): Foreign key to auth.User to link the device to a specific user.
    • device_id (optional): A UUID obtained from Android/iOS/Windows APIs to uniquely identify the device.
    • registration_id (required): The FCM/GCM registration ID or the APNS token for the device.
  2. Integrate with Django REST Framework (DRF)

    master

    The library provides ViewSets for managing devices via an API.

    ViewSet Types

    • APNSDeviceViewSet / GCMDeviceViewSet: Uses permissions defined in settings (defaults to AllowAny). Devices can be registered without an associated user.
    • APNSDeviceAuthorizedViewSet / GCMDeviceAuthorizedViewSet: Uses IsAuthenticated and IsOwner permissions. Requires authentication and ensures devices are associated with the requesting user.

    Handling Duplicate Registration IDs

    By default, the DRF viewset enforces unique registration IDs. If a device changes users and attempts to re-register, it will fail. To allow updating an existing device instead of failing, set UPDATE_ON_DUPLICATE_REG_ID = True in your settings. This only applies to DRF.

  3. Subscribe users to WebPush in the browser

    master

    To allow users to receive notifications, you must implement client-side logic to request permission and subscribe to the PushManager. This logic should ideally be triggered by a user action (like a button click) to avoid browser permission denials.

    Steps:

    1. Convert your VAPID Public Key from Base64 to a Uint8Array using a utility function.
    2. Use navigator.serviceWorker.ready to access the pushManager.
    3. Call subscribe() with userVisibleOnly: true and your applicationServerKey.
    4. On successful subscription, extract the endpoint (as registration_id), p256dh, and auth values.
    5. POST this data to your Django server to create a WebPushDevice.
    // Utils functions:
    function urlBase64ToUint8Array (base64String) {
      var padding = '='.repeat((4 - base64String.length % 4) % 4)
      var base64 = (base64String + padding)
        .replace(/\-/g, '+')
        .replace(/_/g, '/')
    
      var rawData = window.atob(base64)
      var outputArray = new Uint8Array(rawData.length)
    
      for (var i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i)
      }
      return outputArray;
    }
    
    var applicationServerKey = '<Your Public Key>';
    
    function subscribeUser() {
      if ('Notification' in window && 'serviceWorker' in navigator) {
        navigator.serviceWorker.ready.then(function (reg) {
          reg.pushManager
            .subscribe({
              userVisibleOnly: true,
              applicationServerKey: urlBase64ToUint8Array(
                applicationServerKey
              ),
            })
            .then(function (sub) {
              var registration_id = sub.endpoint;
              var data = {
                p256dh: btoa(
                  String.fromCharCode.apply(
                    null,
                    new Uint8Array(sub.getKey('p256dh'))
                  )
                ),
                auth: btoa(
                  String.fromCharCode.apply(
                    null,
                    new Uint8Array(sub.getKey('auth'))
                  )
                ),
                registration_id: registration_id,
              }
              requestPOSTToServer(data)
            })
            .catch(function (e) {
              if (Notification.permission === 'denied') {
                console.warn('Permission for notifications was denied')
              } else {
                console.error('Unable to subscribe to push', e)
              }
            })
        })
      }
    }
    
    // Send the subscription data to your server
    function requestPOSTToServer (data) {
      const headers = new Headers();
      headers.set('Content-Type', 'application/json');
      const requestOptions = {
        method: 'POST',
        headers,
        body: JSON.stringify(data),
      };
    
      return (
        fetch(
          '<your endpoint url>',
          requestOptions
        )
      ).then((response) => response.json())
    }
  4. Generate VAPID keys for Web Push

    master

    To implement Web Push, you must configure VAPID keys (a private and public key pair) for signing push requests.

    Due to known compatibility issues between py-vapid and newer cryptography versions, it is recommended to generate keys using a standalone script with the ecdsa library.

    1. Install the dependency: pip install ecdsa
    2. Create a script named vapid_keygen.py with the provided implementation.
    3. Run the script: python vapid_keygen.py.

    Usage of generated keys:

    • The Private Key must be assigned to the Django setting WP_PRIVATE_KEY.
    • The Public Key is used in your client-side JavaScript as the applicationServerKey.
    # vapid_keygen.py
    import base64
    import ecdsa
    
    def generate_vapid_keypair():
        """
        Generate a new set of encoded key-pair for VAPID
        """
        pk = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p)
        vk = pk.get_verifying_key()
    
        return {
            'private_key': base64.urlsafe_b64encode(pk.to_string()).strip(b"="),
            'public_key': base64.urlsafe_b64encode(b"\x04" + vk.to_string()).strip(b"=")
        }
    
    keys = generate_vapid_keypair()
    
    print("\nPrivate key (use for WP_PRIVATE_KEY setting):\n")
    print(keys["private_key"].decode())
    print("\nPublic key (use as Application Server Key in client JavaScript):\n")
    print(keys["public_key"].decode())
    print()
  5. Install django-push-notifications

    master

    You can install the library via pip. It is recommended to install the extra dependencies for the specific push services you intend to use (WebPush, APNS, or FCM).

    Dependencies

    • Python 3.7+
    • Django 2.2+
    • Django REST Framework 3.7+ (required if using the API module)
    • pywebpush 1.3.0+ (optional, for WebPush)
    • py-vapid 1.3.0+ (optional, for generating WebPush private keys)
    • apns2 0.3+ (optional, for APNS)
    • aioapns 3.1+ (optional, for async APNS; overrides apns2 for Python 3.10+ support)
    • firebase-admin 6.2+ (optional, for FCM)
    $ pip install django-push-notifications[WP,apns-async,FCM]
  6. Implement a Service Worker to display Web Push notifications

    master

    A Service Worker is required to listen for the push event and display the notification to the user.

    Key capabilities:

    • Handling JSON payloads: If the push data is JSON, you can extract title, message, and tag. If it is plain text, it falls back to a default title.
    • Showing Notifications: Uses self.registration.showNotification().
    • Communication: You can use client.postMessage() to send data back to your main JavaScript application.
    • Interaction: The notificationclick event can be used to focus an existing tab or open a new one when the user clicks the notification.
    // Example navigatorPush.service.js file
    
    var getTitle = function (title) {
      if (title === "") {
        title = "TITLE DEFAULT";
      }
      return title;
    };
    var getNotificationOptions = function (message, message_tag) {
      var options = {
        body: message,
        icon: '/img/icon_120.png',
        tag: message_tag,
        vibrate: [200, 100, 200, 100, 200, 100, 200, 100, 200]
      };
      return options;
    };
    
    self.addEventListener('install', function (event) {
      self.skipWaiting();
    });
    
    self.addEventListener('push', function(event) {
      try {
        // Push is a JSON
        var response_json = event.data.json();
        var title = response_json.title;
        var message = response_json.message;
        var message_tag = response_json.tag;
      } catch (err) {
        // Push is a simple text
        var title = "";
        var message = event.data.text();
        var message_tag = "";
      }
      self.registration.showNotification(getTitle(title), getNotificationOptions(message, message_tag));
      // Optional: Comunicating with our js application. Send a signal
      self.clients.matchAll({includeUncontrolled: true, type: 'window'}).then(function (clients) {
        clients.forEach(function (client) {
          client.postMessage({
            "data": message_tag,
            "data_title": title,
            "data_body": message});
        });
      });
    });
    
    // Optional: Added to that the browser opens when you click on the notification push web.
    self.addEventListener('notificationclick', function(event) {
      // Android doesn't close the notification when you click it
      // See http://crbug.com/463146
      event.notification.close();
      // Check if there's already a tab open with this URL.
      // If yes, focus on the tab. If no, open a tab with the URL.
      event.waitUntil(clients.matchAll({type: 'window', includeUncontrolled: true}).then(function(windowClients) {
          for (var i = 0; i < windowClients.length; i++) {
            var client = windowClients[i];
            if ('focus' in client) {
              return client.focus();
            }
          }
        })
      );
    });
  7. Generate an APNS PEM file for django-push-notifications

    master

    The APNS_CERTIFICATE setting requires a path to a .pem file containing both a certificate and a private key pair. This allows a secure connection to Apple's push gateway.

    1. Generate the Apple Push Certificate

    Use the Apple Developer site to generate a push notification certificate for either development or production.

    Important: To avoid confusion between sandbox and production certificates, do not generate the certificate from the top-level Certificates section. Instead, navigate through your specific app's configuration: Identifiers -> App IDs -> [Your App] -> Edit -> Push Notifications Section -> Create Certificate.

    The end result should be an exported .p12 file containing the certificate and private key.

    2. Convert the .p12 certificate to PEM format

    Follow these steps using openssl to prepare the file for the library:

    1. Extract the certificate:
      openssl pkcs12 -clcerts -nokeys -out aps-cert.pem -in Certificates.p12
    2. Extract the key:
      openssl pkcs12 -nocerts -out aps-key.pem -in Certificates.p12
    3. Remove the passphrase from the key:
      openssl rsa -in aps-key.pem -out aps-key-noenc.pem
    4. Combine certificate and key into a single file:
      cat aps-cert.pem aps-key-noenc.pem > aps.pem

    3. Verify connectivity

    Test the validity of your certificate and connectivity to the APNS gateway:

    Production:

    openssl s_client -connect gateway.push.apple.com:2195 -cert aps-cert.pem -key aps-key-noenc.pem

    Sandbox:

    openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert aps-cert.pem -key aps-key-noenc.pem

    If the connection opens and remains open, the certificate is valid. If it closes or displays an error, the certificate or key is invalid.

    $ openssl pkcs12 -clcerts -nokeys -out aps-cert.pem -in Certificates.p12
    $ openssl pkcs12 -nocerts -out aps-key.pem -in Certificates.p12
    $ openssl rsa -in aps-key.pem -out aps-key-noenc.pem
    $ cat aps-cert.pem aps-key-noenc.pem > aps.pem
  8. Configure django-push-notifications in settings.py

    master

    To set up the library, add push_notifications to your INSTALLED_APPS. You must also initialize the Firebase app if you are using FCM, and define your configuration in the PUSH_NOTIFICATIONS_SETTINGS dictionary.

    Note: If using APNS with APNS_USE_SANDBOX=True, ensure you are using a development certificate.

    INSTALLED_APPS = (
        ...
        "push_notifications"
    )
    
    # Import the firebase service
    from firebase_admin import auth
    
    # Initialize the default app (either use `GOOGLE_APPLICATION_CREDENTIALS` env var, or pass a firebase_admin.credentials.Certificate instance)
    # You can also pass options like httpTimeout: This sets the timeout (in seconds) for outgoing HTTP connections.
    import firebase_admin
    default_app = firebase_admin.initialize_app()
    
    PUSH_NOTIFICATIONS_SETTINGS = {
        "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...']",
        "WNS_SECRET_KEY": "[your app secret key, e.g.: 'KDiejnLKDUWodsjmewuSZkk']",
        "WP_PRIVATE_KEY": "/path/to/your/private.pem",
        "WP_CLAIMS": {'sub': "mailto:development@example.com"}
    }
  9. Migrate to FCM v1 API and Firebase Admin SDK

    master

    Legacy FCM and GCM support have been removed. You must now use the firebase-admin SDK for FCM functionality. Authentication no longer works with an access token; instead, you must use a service account private key file.

    To set up authentication, you can either:

    1. Define the GOOGLE_APPLICATION_CREDENTIALS environment variable pointing to the path of your service account private key file.
    2. Pass the path to the file explicitly when initializing the Firebase Admin SDK.

    In your settings.py, initialize the Firebase Admin SDK as follows:

    # Import the firebase service
    import firebase_admin
    
    # Initialize the default app
    default_app = firebase_admin.initialize_app()
  10. Create WebPushDevice on the server

    master

    Your Django server needs an endpoint to receive subscription data (p256dh, auth, and registration_id) and associate it with a user via a WebPushDevice object.

    Option 1: Using Django Rest Framework (DRF) You can use the built-in WebPushDeviceViewSet from push_notifications.api.rest_framework to handle this automatically.

    Option 2: Using a custom Function View You can manually parse the JSON body and create the device:

    WebPushDevice.objects.create(user=request.user, **data)
    # Using DRF
    from rest_framework.routers import SimpleRouter
    from push_notifications.api.rest_framework import WebPushDeviceViewSet
    
    api_router = SimpleRouter()
    api_router.register(r'push/web', WebPushDeviceViewSet, basename='web_push')
    
    # In urlpatterns
    re_path('api/v1/', include(api_router.urls))
    
    # OR Using a manual view
    import json
    from push_notifications.models import WebPushDevice
    
    def register_webpush(request):
        data = json.loads(request.body)
        WebPushDevice.objects.create(
            user=request.user,
            **data
        )
  11. Configure push notification applications in PUSH_NOTIFICATIONS_SETTINGS

    master

    The AppConfig class manages multiple push notification applications via the PUSH_NOTIFICATIONS_SETTINGS dictionary. Each application must be identified by a unique key in the APPLICATIONS sub-dictionary and must specify a PLATFORM.

    Supported platforms are:

    • APNS (Apple Push Notification service)
    • FCM (Firebase Cloud Messaging)
    • WNS (Windows Push Notification Services)
    • WP (Web Push)

    Each platform has specific required settings. If a setting is missing or invalid for the chosen platform, an ImproperlyConfigured exception will be raised during initialization.