Configure Web Push Notifications using pywebpush step by step

  • Tutorial

Why another manual?


When I was given the task of making a draft of push notifications, a quick search showed that there are already many articles on setting up push notifications on Habré. Here are the most useful ones , in my opinion:

How JS works: Web push notifications
Web PUSH Notifications is quick and easy 924 /
Service Workers. Web Push and where they live

This is all great, but I personally didn’t have a simple and clear guide that would allow me to make everything work right away using the copy-paste method. Well, and besides, among the manuals there is no one adapted for the python back.

Setting up notifications in the end took three days and it seems to me that this is a bit too much. I hope my article will help someone set up push notifications in three hours instead of three days.
The project on which I work is implemented on Django and I will describe the course of work with reference to this framework, but those who wish can easily adapt it to Flask or anything else.

So, drove.

We get the keys


Without the keys, naturally, we will not be allowed anywhere, so let's start with them. For key generation I used py_vapid . It is installed automatically with pywebpush , which we will still need later, so we will start with pywebpush in order not to get up two times:

> bin/pip install pywebpush
<Здесь вывод результата установки>
> bin/vapid --applicationServerKey
No private_key.pem file found.        
Do you want me to create one for you? (Y/n)Y
Generating private_key.pem
Generating public_key.pem
Application Server Key = <Мой Server Key>

The resulting value of the Application Server Key is added to the file settings.py.

NOTIFICATION_KEY = <Мой Server Key>

In addition, we will need to pass NOTIFICATION_KEY to the context. The easiest way to do this is to write your context_processor .

We do service worker


The service worker who does not know is a special file that runs in the background. We need it to display our notifications.

The easiest service worker code would look like this:

self.addEventListener('push', function(event) {
  var message = JSON.parse(event.data.text()); //
  event.waitUntil(
    self.registration.showNotification(message.title, {
      body: message.body,
    })
  );
});

And now we need to register our service worker. The process, in principle, is described here . Therefore briefly:

functioncheckWorkerAndPushManager () {
    if (!('serviceWorker'in navigator)) {
        console.log('Workers are not supported.');
        return;
    }
    if (!('PushManager'inwindow)) {
        console.log('Push notifications are not supported.');
        return;
    }
}
functionregisterWorker () {
	window.addEventListener('load', function () {
        navigator.serviceWorker.register('/static/js/sw.js').then(function (registration) {
            console.log('ServiceWorker registration successful');
        }, function (err) {
            console.log('ServiceWorker registration failed: ', err);
            return;
        });
    });
	returntrue;
}
var supported = checkWorkerAndPushManager();
if (supported){
        var worker = registerWorker ();
}

Great, you can check the work of our service worker. To do this, go to the Developer Tools in the browser, make sure that a message appears on the console about the successful registration of the box and go to the Application tab and select Service Worker on the left.

image

If the notification does not appear, check that notifications are enabled in your browser.

Well, we already know how to send notifications to ourselves. Cool, go further.

Get user permission to display notifications


After the Voker is registered, we ask the user for permission to display notifications.

functionrequestPermission() {
  returnnewPromise(function(resolve, reject) {
    const permissionResult = Notification.requestPermission(function(result) {
      // Поддержка устаревшей версии с функцией обратного вызова.
      resolve(result);
    });
    if (permissionResult) {
      permissionResult.then(resolve, reject);
    }
  })
  .then(function(permissionResult) {
    if (permissionResult !== 'granted') {
      console.log(permissionResult);
      thrownewError('Permission not granted.');
    }
  });
  returntrue;
}

There is no need for this code and comments, it just works.

Subscribe and save subscription


Subscription is also on the front. Here you can find the subscription code , but there is no urlBase64ToUint8Array function in use, so I have the code with it.


NOTIFICATION_KEY = '{{ NOTIFICATION_KEY }};
function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, '+')
    .replace(/_/g, '/')
  ;
  const rawData = window.atob(base64);
  return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
}
function subscribeUserToPush(key) {
   return navigator.serviceWorker.register('/static/coolwriter/js/sw.js')
  .then(function(registration) {
        var subscribeOptions = {
            userVisibleOnly: true,
            applicationServerKey: urlBase64ToUint8Array(key),
        };
   return registration.pushManager.subscribe(subscribeOptions)
   })
  .then(function(pushSubscription) {
    sendSubscriptionToBackEnd(pushSubscription);
  });
}

(The urlBase64ToUint8Array used here seems to be from the category of crutches and bicycles, but the attempt to recode the data with btoa did not lead to success, I don’t know why. Someone can tell.)

Next, send the data to the server. I implemented it like this:

functionsendSubscriptionToBackEnd(subscription) {
    $.post(
        SAVE_REGISTRATION_URL,
        {
           'csrfmiddlewaretoken': $('input[name=csrfmiddlewaretoken]').val(),
           //Естественно, в темплейте есть тег {% csrf_token %}.'registration_data': JSON.stringify(subscription)
        }
    );
}

Well, then save it to the server. You can directly line. Yes, do not try to make a one-to-one user-subscription connection. It seems to be obvious, but suddenly someone will like it.
I have used such a simple model for saving, it will be used further, so I will give it:

classUserSubscription(models.Model):
    subscription = models.CharField(max_length=500)
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='subscriptions')

Last step. Send a message using pywebpush


Here everything is by tutorial, no particular subtleties. Well, except that we make the message of such a structure so that our service worker can disassemble it.

import json
from pywebpush import webpush, WebPushException
from django.conf import settings
from .models import UserSubscription
defpush_notification(user_id):
    user_subscriptions = UserSubscription.objects.filter(user_id=notification.user_id)
    for subscription in user_subscriptions:
        data = json.dumps({
        'title': 'Hello',
        'body': 'there',
    })
        try:
            webpush(
                subscription_info=json.loads(subscription.subscription),
                data=data,
                vapid_private_key='./private_key.pem',
                vapid_claims={
                    'sub': 'mailto:{}'.format(settings.ADMIN_EMAIL),
                }
            )
            notification.sent = True
            notification.save()
        except WebPushException as ex:
            print('I\'m sorry, Dave, but I can\'t do that: {}'.format(repr(ex)))
            print(ex)
            # Mozilla returns additional information in the body of the response.if ex.response and ex.response.json():
                extra = ex.response.json()
                print('Remote service replied with a {}:{}, {}',
                      extra.code,
                      extra.errno,
                      extra.message
                      )

Actually, you can already call push_notification from the django shell and try to start it.
In this code, it would be good to intercept the response with the status 410. Such an answer says that the subscription has been canceled, and it would be good to delete such subscriptions from the database.

Finally


Actually there is another great django-webpush library . Perhaps those who work with Django should start with it.

PS All Happy Programmer's Day!

Also popular now: