Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WebPush support for Safari #674

Merged
merged 9 commits into from
Oct 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 3 additions & 270 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Edit your settings.py file:
"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"}
"WP_CLAIMS": {'sub': "mailto:development@example.com"}
}

.. note::
Expand Down Expand Up @@ -122,278 +122,11 @@ For WNS, you need both the ``WNS_PACKAGE_SECURITY_KEY`` and the ``WNS_SECRET_KEY

**WP settings**

- Install:

.. code-block:: python

pip install pywebpush
pip install py-vapid (Only for generating key)

- Getting keys:

- Create file (claim.json) like this:

.. code-block:: bash

{
"sub": "mailto: development@example.com",
"aud": "https://android.googleapis.com"
}

- Generate public and private keys:

.. code-block:: bash

vapid --sign claim.json

No private_key.pem file found.
Do you want me to create one for you? (Y/n)Y
Do you want me to create one for you? (Y/n)Y
Generating private_key.pem
Generating public_key.pem
Include the following headers in your request:

Crypto-Key: p256ecdsa=BEFuGfKKEFp-kEBMxAIw7ng8HeH_QwnH5_h55ijKD4FRvgdJU1GVlDo8K5U5ak4cMZdQTUJlkA34llWF0xHya70

Authorization: WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2FuZHJvaWQuZ29vZ2xlYXBpcy5jb20iLCJleHAiOiIxNTA4NDkwODM2Iiwic3ViIjoibWFpbHRvOiBkZXZlbG9wbWVudEBleGFtcGxlLmNvbSJ9.r5CYMs86X3JZ4AEs76pXY5PxsnEhIFJ-0ckbibmFHZuyzfIpf1ZGIJbSI7knA4ufu7Hm8RFfEg5wWN1Yf-dR2A

- Generate client public key (applicationServerKey)

.. code-block:: bash

vapid --applicationServerKey

Application Server Key = BEFuGfKKEFp-kEBMxAIw7ng8HeH_QwnH5_h55ijKD4FRvgdJU1GVlDo8K5U5ak4cMZdQTUJlkA34llWF0xHya70


- Configure settings:

- ``WP_PRIVATE_KEY``: Absolute path to your private certificate file: os.path.join(BASE_DIR, "private_key.pem")
- ``WP_CLAIMS``: Dictionary with the same sub info like claims file: {'sub': "mailto: development@example.com"}
- ``WP_CLAIMS``: Dictionary with default value for the sub, (subject), sent to the webpush service, This would be used by the service if they needed to reach out to you (the sender). Could be a url or mailto e.g. {'sub': "mailto:development@example.com"}.
- ``WP_ERROR_TIMEOUT``: The timeout on WebPush POSTs. (Optional)
- ``WP_POST_URL``: A dictionary (key per browser supported) with the full url that webpush notifications will be POSTed to. (Optional)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this line removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This MR is essentially deprectating the old approach. Instead the full endpoint that the browser returns is stored in the registration_id of the model, instead of just the token, and the code uses exactly what the browser gave us when we send the message. Rather than composing a url based on what is in WP_POST_URL.

As a result I thought it was sensible to remove the WP_POST_URL from the docs as it would confuse new users.



- Configure client (javascript):

.. code-block:: javascript

// 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;
}

function loadVersionBrowser () {
if ("userAgentData" in navigator) {
// navigator.userAgentData is not available in
// Firefox and Safari
const uaData = navigator.userAgentData;
// Outputs of navigator.userAgentData.brands[n].brand are e.g.
// Chrome: 'Google Chrome'
// Edge: 'Microsoft Edge'
// Opera: 'Opera'
let browsername;
let browserversion;
let chromeVersion = null;
for (var i = 0; i < uaData.brands.length; i++) {
let brand = uaData.brands[i].brand;
browserversion = uaData.brands[i].version;
if (brand.match(/opera|chrome|edge|safari|firefox|msie|trident/i) !== null) {
// If we have a chrome match, save the match, but try to find another match
// E.g. Edge can also produce a false Chrome match.
if (brand.match(/chrome/i) !== null) {
chromeVersion = browserversion;
}
// If this is not a chrome match return immediately
else {
browsername = brand.substr(brand.indexOf(' ')+1);
return {
name: browsername,
version: browserversion
}
}
}
}
// No non-Chrome match was found. If we have a chrome match, return it.
if (chromeVersion !== null) {
return {
name: "chrome",
version: chromeVersion
}
}
}
// If no userAgentData is not present, or if no match via userAgentData was found,
// try to extract the browser name and version from userAgent
const userAgent = navigator.userAgent;
var ua = userAgent, tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
if (/trident/i.test(M[1])) {
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
return {name: 'IE', version: (tem[1] || '')};
}
if (M[1] === 'Chrome') {
tem = ua.match(/\bOPR\/(\d+)/);
if (tem != null) {
return {name: 'Opera', version: tem[1]};
}
}
M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
if ((tem = ua.match(/version\/(\d+)/i)) != null) {
M.splice(1, 1, tem[1]);
}
return {
name: M[0],
version: M[1]
};
};
var applicationServerKey = "BEFuGfKKEFp-kEBMxAIw7ng8HeH_QwnH5_h55ijKD4FRvgdJU1GVlDo8K5U5ak4cMZdQTUJlkA34llWF0xHya70";
....

// In your ready listener
if ('serviceWorker' in navigator) {
// The service worker has to store in the root of the app
// http://stackoverflow.com/questions/29874068/navigator-serviceworker-is-never-ready
var browser = loadVersionBrowser(navigator.userAgent);
navigator.serviceWorker.register('navigatorPush.service.js?version=1.0.0').then(function (reg) {
reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(applicationServerKey)
}).then(function (sub) {
var endpointParts = sub.endpoint.split('/');
var registration_id = endpointParts[endpointParts.length - 1];
var data = {
'browser': browser.name.toUpperCase(),
'p256dh': btoa(String.fromCharCode.apply(null, new Uint8Array(sub.getKey('p256dh')))),
'auth': btoa(String.fromCharCode.apply(null, new Uint8Array(sub.getKey('auth')))),
'name': 'XXXXX',
'registration_id': registration_id
};
requestPOSTToServer(data);
})
}).catch(function (err) {
console.log(':^(', err);
});




// 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]
};
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();
}
}
})
);
});

The above code makes a call to ``requestPOSTToServer()``, which is not implemented. This is where you should make a call to your Django app to register the Web Push device. Your implementation will vary depending on the design of your web app, here's an example using a Django view and an Ajax call:

Example Django view to handle registration:

.. code-block:: python

from django.http import JsonResponse
from push_notifications.models import WebPushDevice

def register_wp_notifications(request):
WebPushDevice.objects.create(
registration_id=request.GET.get('registration_id'),
p256dh=request.GET.get('p256dh'),
auth=request.GET.get('auth'),
browser=request.GET.get('browser'),
user=request.user,
)
data = {
'result': True
}
return JsonResponse(data)

Example JavaScript to send registration (requires jQuery):

.. code-block:: javascript

function requestPOSTToServer ( data ) {
$.ajax({
url: '/PATH/DEFINED/IN/URLS.PY/',
data: {
'browser': data.browser,
'p256dh': data.p256dh,
'auth': data.auth,
'registration_id': data.registration_id
},
dataType: 'json',
success: function (data) {
}
});
}
For more information about how to configure WebPush, see `docs/WebPush <https://github.com/jazzband/django-push-notifications/blob/master/docs/WebPush.rst>`_.


Sending messages
Expand Down
Loading
Loading