⬆️ ⬇️

Configure Web Push Notifications using pywebpush step by step

Why another manual?



When I was given the task of making a draft of push notifications, a quick search showed that there are already a lot of articles on how to configure push notifications on Habré. Here are the most, in my opinion, fit:



How JS Works: Web Push Notifications

Web push notifications fast and easy924 /

Service Workers. Web Push and where they live



This is all great, but personally I really lacked a simple and clear guide that would allow me to immediately make everything work right away, using copy-paste practically. 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 in relation 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 still need later, so we’ll 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 running 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:



 function checkWorkerAndPushManager () { if (!('serviceWorker' in navigator)) { console.log('Workers are not supported.'); return; } if (!('PushManager' in window)) { console.log('Push notifications are not supported.'); return; } } function registerWorker () { 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; }); }); return true; } 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, ask the user for permission to display notifications.



 function requestPermission() { return new Promise(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); throw new Error('Permission not granted.'); } }); return true; } 


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.)



Then we send the received data to the server. I implemented it like this:



 function sendSubscriptionToBackEnd(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 relationship. It seems to be obvious, but suddenly someone will like it.

I have such a simple model used for saving, it will continue to be used, so I will give it:



 class UserSubscription(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 parse it.



 import json from pywebpush import webpush, WebPushException from django.conf import settings from .models import UserSubscription def push_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. This answer says that the subscription is 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!

Source: https://habr.com/ru/post/423193/



All Articles