Skip to content

gonzalo123/push

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Sending cross-Browser Push Notifications with Python

Today we're going to implement a complete Web Push Notification system using Python for the backend and service worker for the frontend.

The system consists of two main components:

  • Frontend: A Service Worker that receives and displays notifications
  • Backend: A Python script that sends notifications to subscribed clients

It's important to understand how the Web Push Notification system works. The client (browser) subscribes to notifications and receives a unique endpoint URL. This URL is used by the server to send notifications to the client. Each browser has its own endpoint URL, so the server needs to keep track of these URLs for each client. Even that each browser has its own endpoint URL, they use same standard for the Web Push Notification system. The server will send notifications to the endpoint URL using the VAPID keys for authentication.

First we need to create our VAPID keys. VAPID (Voluntary Application Server Identification) is a protocol that allows the server to identify itself to the push service. It consists of a public key and a private key. The public key is sent to the client during the subscription process, and the private key is used by the server to sign the notifications. I'm using a node js package called vapid to generate the keys. You can install it using npm:

npm install -g web-push

Then run the following command to generate the keys:

npx web-push generate-vapid-keys 

The frontend in this example is a simple HTML page with a button to subscribe to notifications. When the button is clicked, the browser will ask for permission to send notifications. If the user accepts, the browser will generate a subscription object that contains the endpoint URL and the VAPID keys. The subscription object is then sent to the server, which stores it in a database (in this case, a simple JSON file). The server can then use this subscription object to send notifications to the client.

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Push demo</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            max-width: 500px;
            margin: 0 auto;
            padding: 1rem;
            line-height: 1.5;
        }
        button {
            background-color: #4CAF50;
            border: none;
            color: white;
            padding: 10px 20px;
            text-align: center;
            font-size: 16px;
            margin: 10px 0;
            cursor: pointer;
            border-radius: 4px;
            width: 100%;
        }
        #unsubscribeButton {
            background-color: #f44336;
            display: none;
        }
        #subscriptionData {
            background-color: #f5f5f5;
            padding: 10px;
            border-radius: 4px;
            margin-top: 20px;
            word-break: break-all;
            display: none;
        }
    </style>
</head>
<body>
    <button id="subscribeButton">Subscribe</button>
    <button id="unsubscribeButton">Cancel subscription</button>

    <div id="subscriptionData"></div>

    <p id="status"></p>

    <script src="main.js"></script>
</body>
</html>

The main.js file contains the logic for subscribing and unsubscribing from notifications. It also handles the display of the subscription data and the status messages.

const publicVapidKey = 'BD7XE2RJieJ_FMejZibmBI_Tf0d7l04uDBIIVazO1sn_aB9_6ZIDHT7NGI2KyU-GtrS3W_cY1htmcMoBP0x0UTs';

const subscribeButton = document.getElementById('subscribeButton');
const unsubscribeButton = document.getElementById('unsubscribeButton');
const statusElement = document.getElementById('status');
const subscriptionDataElement = document.getElementById('subscriptionData');

if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
  statusElement.textContent = 'Your browser does not support push notifications 😒';
  subscribeButton.disabled = true;
} else {
  (async () => {
    try {
      await navigator.serviceWorker.register('sw.js');
      statusElement.textContent = 'Service Worker registered successfully βœ…';
      await checkSubscription();
    } catch (error) {
      statusElement.textContent = `Error registering Service Worker: ${error}`;
    }
  })();
}

async function checkSubscription() {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.getSubscription();
  
  if (subscription) {
    subscribeButton.style.display = 'none';
    unsubscribeButton.style.display = 'block';
    displaySubscription(subscription);
  } else {
    subscribeButton.style.display = 'block';
    unsubscribeButton.style.display = 'none';
    subscriptionDataElement.style.display = 'none';
  }
}

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/-/g, '+')
    .replace(/_/g, '/');

  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);

  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

subscribeButton.addEventListener('click', async () => {
  try {
    const permission = await Notification.requestPermission();
    
    if (permission !== 'granted') {
      throw new Error('Notification permission denied');
    }
    
    const registration = await navigator.serviceWorker.ready;
    const subscription = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
    });
    
    statusElement.textContent = 'Subscribed to push notifications βœ…';
    displaySubscription(subscription);

    subscribeButton.style.display = 'none';
    unsubscribeButton.style.display = 'block';
  } catch (error) {
    statusElement.textContent = `Error subscribing: ${error}`;
  }
});

unsubscribeButton.addEventListener('click', async () => {
  try {
    const registration = await navigator.serviceWorker.ready;
    const subscription = await registration.pushManager.getSubscription();
    
    if (subscription) {
      await subscription.unsubscribe();
    }
    
    statusElement.textContent = 'Subscription canceled βœ…';
    subscribeButton.style.display = 'block';
    unsubscribeButton.style.display = 'none';
    subscriptionDataElement.style.display = 'none';
  } catch (error) {
    statusElement.textContent = `Error canceling subscription: ${error}`;
  }
});

function displaySubscription(subscription) {
  subscriptionDataElement.textContent = JSON.stringify(subscription.toJSON(), null, 2);
  subscriptionDataElement.style.display = 'block';
}

And finally, the service worker (sw.js) that handles the push notifications. It listens for push events and displays the notifications.

self.addEventListener('push', event => {
  try {
    const data = event.data.json();

    const title = data.title || 'Notification';
    const options = {
      body: data.body || 'You have received a new notification',
      icon: data.icon || '/icon-192x192.png',
      badge: data.badge || '/badge-72x72.png',
      data: {
        url: data.url || '/'
      }
    };

    event.waitUntil(
      self.registration.showNotification(title, options)
    );
  } catch (error) {
    event.waitUntil(
      self.registration.showNotification('New notification', {
        body: 'You have received a notification',
        icon: '/icon-192x192.png'
      })
    );
  }
});

self.addEventListener('notificationclick', event => {
  event.notification.close();
  
  const urlToOpen = event.notification.data && event.notification.data.url
    ? event.notification.data.url
    : '/';

  event.waitUntil(
    clients.matchAll({type: 'window'}).then(windowClients => {
      for (let client of windowClients) {
        if (client.url === urlToOpen && 'focus' in client) {
          return client.focus();
        }
      }
      if (clients.openWindow) {
        return clients.openWindow(urlToOpen);
      }
    })
  );
});

Now, when we open the browser and click the "Subscribe" button, we should see a notification asking for permission to send notifications. If we accept, we will be subscribed to notifications and we will see the subscription data displayed on the page. We can also unsubscribe from notifications by clicking the "Cancel subscription" button. The browser will show us a json object with the subscription data, which includes the endpoint URL and the VAPID keys. This data is sent to the server when we subscribe to notifications. The server will use this data to send notifications to the client. We need to copy this json object and save it in a file called subscription.json. This file will be used by the server to send notifications to the client. In a real life application, we would send this data to the server using a http request.

{
  "endpoint": "https://fcm.googleapis.com/fcm/send/e1...",
  "expirationTime": null,
  "keys": {
    "p256dh": "BGoNCkIqKDXvR6qRYTaSUGxz7bASa-RCmXoysU8B3IW24aGXE51GX2wXzfEshU4--sL26iI3iwZOrMCjkbWoZJ0",
    "auth": "LtMQyIB2_RoeqULPaWSd6ACq5SU2C9A-q8XoDlRQBtc"
  }
}

The server will use this data to send notifications to the client. The server will use the VAPID keys to authenticate the notifications. The server will also use the endpoint URL to send the notifications to the client.

import json
import logging
from pathlib import Path

from pywebpush import webpush, WebPushException

logging.basicConfig(
    format='%(asctime)s [%(levelname)s] %(message)s',
    level='INFO',
    datefmt='%d/%m/%Y %X')

logger = logging.getLogger(__name__)

VAPID_PRIVATE_KEY = "RWbdOcw9sh1vC7L14iAkRhLE_3ZmVU0q0cQ2xqQnFn8"
VAPID_CLAIMS = {
    "sub": "mailto:gonzalo123@gmail.com",
    "aud": "https://fcm.googleapis.com"
}

BASE_DIR = Path(__file__).resolve().parent


def send_push_notification(subscription_info, message_data):
    try:
        webpush(
            subscription_info=subscription_info,
            data=json.dumps(message_data),
            vapid_private_key=VAPID_PRIVATE_KEY,
            vapid_claims=VAPID_CLAIMS
        )
        logger.info("Notification sent!")
    except WebPushException as ex:
        logger.error(f"Error sending push notification: {ex}")
        if ex.response and ex.response.json():
            logger.error(ex.response.json())


if __name__ == "__main__":
    with open(BASE_DIR / "subscription.json", 'r') as f:
        subscription_info = json.load(f)

    title = 'Title'
    body = 'Body'
    url = 'https://gonzalo123.dom'
    icon = None
    message_data = {
        "title": title,
        "body": body,
        "url": url,
        "icon": icon
    }

    send_push_notification(subscription_info, message_data)

About

Sending cross-Browser Push Notifications with Python

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors