Skip to content

Commit b719521

Browse files
blighjJames Blighcuylerstuweazmeuknlittlejohns
authored
Add WebPush support for Safari (#674)
* Add WebPush support for Safari * Update webpush.py based on review Declare results variable at the start of the block * Fix typo in warning Co-authored-by: Cuyler Stuwe <[email protected]> * Update README.rst Co-authored-by: James Bligh <[email protected]> * Fix mailto: space * Expanded documentation for Web Push (#558) • Added example code to register WP device • Fixed issue where call to extract UserAgent didn't include UA • Added examples on how to send a Web Push message * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Attempt to fix tests * Update README.rst --------- Co-authored-by: James Bligh <[email protected]> Co-authored-by: Cuyler Stuwe <[email protected]> Co-authored-by: Éloi Rivard <[email protected]> Co-authored-by: Neil Littlejohns <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent b69e5e4 commit b719521

File tree

10 files changed

+370
-296
lines changed

10 files changed

+370
-296
lines changed

README.rst

Lines changed: 3 additions & 270 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Edit your settings.py file:
6464
"WNS_PACKAGE_SECURITY_ID": "[your package security id, e.g: 'ms-app://e-3-4-6234...']",
6565
"WNS_SECRET_KEY": "[your app secret key, e.g.: 'KDiejnLKDUWodsjmewuSZkk']",
6666
"WP_PRIVATE_KEY": "/path/to/your/private.pem",
67-
"WP_CLAIMS": {'sub': "mailto: [email protected]"}
67+
"WP_CLAIMS": {'sub': "mailto:[email protected]"}
6868
}
6969
7070
.. note::
@@ -122,278 +122,11 @@ For WNS, you need both the ``WNS_PACKAGE_SECURITY_KEY`` and the ``WNS_SECRET_KEY
122122

123123
**WP settings**
124124

125-
- Install:
126-
127-
.. code-block:: python
128-
129-
pip install pywebpush
130-
pip install py-vapid (Only for generating key)
131-
132-
- Getting keys:
133-
134-
- Create file (claim.json) like this:
135-
136-
.. code-block:: bash
137-
138-
{
139-
"sub": "mailto: [email protected]",
140-
"aud": "https://android.googleapis.com"
141-
}
142-
143-
- Generate public and private keys:
144-
145-
.. code-block:: bash
146-
147-
vapid --sign claim.json
148-
149-
No private_key.pem file found.
150-
Do you want me to create one for you? (Y/n)Y
151-
Do you want me to create one for you? (Y/n)Y
152-
Generating private_key.pem
153-
Generating public_key.pem
154-
Include the following headers in your request:
155-
156-
Crypto-Key: p256ecdsa=BEFuGfKKEFp-kEBMxAIw7ng8HeH_QwnH5_h55ijKD4FRvgdJU1GVlDo8K5U5ak4cMZdQTUJlkA34llWF0xHya70
157-
158-
Authorization: WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2FuZHJvaWQuZ29vZ2xlYXBpcy5jb20iLCJleHAiOiIxNTA4NDkwODM2Iiwic3ViIjoibWFpbHRvOiBkZXZlbG9wbWVudEBleGFtcGxlLmNvbSJ9.r5CYMs86X3JZ4AEs76pXY5PxsnEhIFJ-0ckbibmFHZuyzfIpf1ZGIJbSI7knA4ufu7Hm8RFfEg5wWN1Yf-dR2A
159-
160-
- Generate client public key (applicationServerKey)
161-
162-
.. code-block:: bash
163-
164-
vapid --applicationServerKey
165-
166-
Application Server Key = BEFuGfKKEFp-kEBMxAIw7ng8HeH_QwnH5_h55ijKD4FRvgdJU1GVlDo8K5U5ak4cMZdQTUJlkA34llWF0xHya70
167-
168-
169-
- Configure settings:
170-
171125
- ``WP_PRIVATE_KEY``: Absolute path to your private certificate file: os.path.join(BASE_DIR, "private_key.pem")
172-
- ``WP_CLAIMS``: Dictionary with the same sub info like claims file: {'sub': "mailto: [email protected]"}
126+
- ``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:[email protected]"}.
173127
- ``WP_ERROR_TIMEOUT``: The timeout on WebPush POSTs. (Optional)
174-
- ``WP_POST_URL``: A dictionary (key per browser supported) with the full url that webpush notifications will be POSTed to. (Optional)
175-
176-
177-
- Configure client (javascript):
178-
179-
.. code-block:: javascript
180-
181-
// Utils functions:
182-
183-
function urlBase64ToUint8Array (base64String) {
184-
var padding = '='.repeat((4 - base64String.length % 4) % 4)
185-
var base64 = (base64String + padding)
186-
.replace(/\-/g, '+')
187-
.replace(/_/g, '/')
188-
189-
var rawData = window.atob(base64)
190-
var outputArray = new Uint8Array(rawData.length)
191128

192-
for (var i = 0; i < rawData.length; ++i) {
193-
outputArray[i] = rawData.charCodeAt(i)
194-
}
195-
return outputArray;
196-
}
197-
198-
function loadVersionBrowser () {
199-
if ("userAgentData" in navigator) {
200-
// navigator.userAgentData is not available in
201-
// Firefox and Safari
202-
const uaData = navigator.userAgentData;
203-
// Outputs of navigator.userAgentData.brands[n].brand are e.g.
204-
// Chrome: 'Google Chrome'
205-
// Edge: 'Microsoft Edge'
206-
// Opera: 'Opera'
207-
let browsername;
208-
let browserversion;
209-
let chromeVersion = null;
210-
for (var i = 0; i < uaData.brands.length; i++) {
211-
let brand = uaData.brands[i].brand;
212-
browserversion = uaData.brands[i].version;
213-
if (brand.match(/opera|chrome|edge|safari|firefox|msie|trident/i) !== null) {
214-
// If we have a chrome match, save the match, but try to find another match
215-
// E.g. Edge can also produce a false Chrome match.
216-
if (brand.match(/chrome/i) !== null) {
217-
chromeVersion = browserversion;
218-
}
219-
// If this is not a chrome match return immediately
220-
else {
221-
browsername = brand.substr(brand.indexOf(' ')+1);
222-
return {
223-
name: browsername,
224-
version: browserversion
225-
}
226-
}
227-
}
228-
}
229-
// No non-Chrome match was found. If we have a chrome match, return it.
230-
if (chromeVersion !== null) {
231-
return {
232-
name: "chrome",
233-
version: chromeVersion
234-
}
235-
}
236-
}
237-
// If no userAgentData is not present, or if no match via userAgentData was found,
238-
// try to extract the browser name and version from userAgent
239-
const userAgent = navigator.userAgent;
240-
var ua = userAgent, tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
241-
if (/trident/i.test(M[1])) {
242-
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
243-
return {name: 'IE', version: (tem[1] || '')};
244-
}
245-
if (M[1] === 'Chrome') {
246-
tem = ua.match(/\bOPR\/(\d+)/);
247-
if (tem != null) {
248-
return {name: 'Opera', version: tem[1]};
249-
}
250-
}
251-
M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
252-
if ((tem = ua.match(/version\/(\d+)/i)) != null) {
253-
M.splice(1, 1, tem[1]);
254-
}
255-
return {
256-
name: M[0],
257-
version: M[1]
258-
};
259-
};
260-
var applicationServerKey = "BEFuGfKKEFp-kEBMxAIw7ng8HeH_QwnH5_h55ijKD4FRvgdJU1GVlDo8K5U5ak4cMZdQTUJlkA34llWF0xHya70";
261-
....
262-
263-
// In your ready listener
264-
if ('serviceWorker' in navigator) {
265-
// The service worker has to store in the root of the app
266-
// http://stackoverflow.com/questions/29874068/navigator-serviceworker-is-never-ready
267-
var browser = loadVersionBrowser(navigator.userAgent);
268-
navigator.serviceWorker.register('navigatorPush.service.js?version=1.0.0').then(function (reg) {
269-
reg.pushManager.subscribe({
270-
userVisibleOnly: true,
271-
applicationServerKey: urlBase64ToUint8Array(applicationServerKey)
272-
}).then(function (sub) {
273-
var endpointParts = sub.endpoint.split('/');
274-
var registration_id = endpointParts[endpointParts.length - 1];
275-
var data = {
276-
'browser': browser.name.toUpperCase(),
277-
'p256dh': btoa(String.fromCharCode.apply(null, new Uint8Array(sub.getKey('p256dh')))),
278-
'auth': btoa(String.fromCharCode.apply(null, new Uint8Array(sub.getKey('auth')))),
279-
'name': 'XXXXX',
280-
'registration_id': registration_id
281-
};
282-
requestPOSTToServer(data);
283-
})
284-
}).catch(function (err) {
285-
console.log(':^(', err);
286-
});
287-
288-
289-
290-
291-
// Example navigatorPush.service.js file
292-
293-
var getTitle = function (title) {
294-
if (title === "") {
295-
title = "TITLE DEFAULT";
296-
}
297-
return title;
298-
};
299-
var getNotificationOptions = function (message, message_tag) {
300-
var options = {
301-
body: message,
302-
icon: '/img/icon_120.png',
303-
tag: message_tag,
304-
vibrate: [200, 100, 200, 100, 200, 100, 200]
305-
};
306-
return options;
307-
};
308-
309-
self.addEventListener('install', function (event) {
310-
self.skipWaiting();
311-
});
312-
313-
self.addEventListener('push', function(event) {
314-
try {
315-
// Push is a JSON
316-
var response_json = event.data.json();
317-
var title = response_json.title;
318-
var message = response_json.message;
319-
var message_tag = response_json.tag;
320-
} catch (err) {
321-
// Push is a simple text
322-
var title = "";
323-
var message = event.data.text();
324-
var message_tag = "";
325-
}
326-
self.registration.showNotification(getTitle(title), getNotificationOptions(message, message_tag));
327-
// Optional: Comunicating with our js application. Send a signal
328-
self.clients.matchAll({includeUncontrolled: true, type: 'window'}).then(function (clients) {
329-
clients.forEach(function (client) {
330-
client.postMessage({
331-
"data": message_tag,
332-
"data_title": title,
333-
"data_body": message});
334-
});
335-
});
336-
});
337-
338-
// Optional: Added to that the browser opens when you click on the notification push web.
339-
self.addEventListener('notificationclick', function(event) {
340-
// Android doesn't close the notification when you click it
341-
// See http://crbug.com/463146
342-
event.notification.close();
343-
// Check if there's already a tab open with this URL.
344-
// If yes: focus on the tab.
345-
// If no: open a tab with the URL.
346-
event.waitUntil(clients.matchAll({type: 'window', includeUncontrolled: true}).then(function(windowClients) {
347-
for (var i = 0; i < windowClients.length; i++) {
348-
var client = windowClients[i];
349-
if ('focus' in client) {
350-
return client.focus();
351-
}
352-
}
353-
})
354-
);
355-
});
356-
357-
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:
358-
359-
Example Django view to handle registration:
360-
361-
.. code-block:: python
362-
363-
from django.http import JsonResponse
364-
from push_notifications.models import WebPushDevice
365-
366-
def register_wp_notifications(request):
367-
WebPushDevice.objects.create(
368-
registration_id=request.GET.get('registration_id'),
369-
p256dh=request.GET.get('p256dh'),
370-
auth=request.GET.get('auth'),
371-
browser=request.GET.get('browser'),
372-
user=request.user,
373-
)
374-
data = {
375-
'result': True
376-
}
377-
return JsonResponse(data)
378-
379-
Example JavaScript to send registration (requires jQuery):
380-
381-
.. code-block:: javascript
382-
383-
function requestPOSTToServer ( data ) {
384-
$.ajax({
385-
url: '/PATH/DEFINED/IN/URLS.PY/',
386-
data: {
387-
'browser': data.browser,
388-
'p256dh': data.p256dh,
389-
'auth': data.auth,
390-
'registration_id': data.registration_id
391-
},
392-
dataType: 'json',
393-
success: function (data) {
394-
}
395-
});
396-
}
129+
For more information about how to configure WebPush, see `docs/WebPush <https://github.com/jazzband/django-push-notifications/blob/master/docs/WebPush.rst>`_.
397130

398131

399132
Sending messages

0 commit comments

Comments
 (0)