Skip to content

Instantly share code, notes, and snippets.

@meulta
Last active April 15, 2017 19:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save meulta/8feb9ae9bc4e7a448361393a45c28f44 to your computer and use it in GitHub Desktop.
Save meulta/8feb9ae9bc4e7a448361393a45c28f44 to your computer and use it in GitHub Desktop.
Push notifications
const webPush = require('web-push');
const restify = require('restify');
const builder = require('botbuilder');
const fs = require('fs');
const vapidKeyFilePath = "./vapidKey.json";
var vapidKeys = {};
if (fs.existsSync(vapidKeyFilePath)) {
//if the vapid file exists, then we try to parse its content
//to retrieve the public and private key
//more tests might be necessary here
try {
vapidKeys = JSON.parse(fs.readFileSync(vapidKeyFilePath));
}
catch (e) {
console.error("There is an error with the vapid key file. Log: " + e.message);
process.exit(-1);
}
}
else {
//if the file did not exists, we use the web-push module to create keys
//and store them in the file for future use
//you should copy the public key in the index.js file
vapidKeys = webPush.generateVAPIDKeys();
fs.writeFileSync(vapidKeyFilePath, JSON.stringify(vapidKeys));
console.log("No vapid key file found. One was generated. Here is the public key: " + vapidKeys.publicKey);
}
//here we setup the web-push module for it to be able to
//send push notification using our public and private keys
webPush.setVapidDetails(
'mailto:example@yourdomain.org',
vapidKeys.publicKey,
vapidKeys.privateKey);
//standard way of creating a rest API for a bot
var server = restify.createServer();
server.use(restify.bodyParser());
server.listen(process.env.port || process.env.PORT || 3000, function () {
console.log('%s listening to %s', server.name, server.url);
});
// Create chat bot
var connector = new builder.ChatConnector({
appId: process.env.MICROSOFT_APP_ID,
appPassword: process.env.MICROSOFT_APP_PASSWORD
});
var bot = new builder.UniversalBot(connector);
server.post('/api/messages', connector.listen());
//=========================================================
// Bots Dialogs
//=========================================================
//this is a 'dictionary' in which we store every push subscription per user
var pushPerUser = [];
//this event handler is looking for every message sent by the bot
//we look into the dictionary to see if we have push subscription info
//for this user and we send a push notif with the bot message
//this will keep send the message to the user through the channel
bot.on("outgoing", function (message) {
if (pushPerUser && pushPerUser[message.address.user.id]) {
var pushsub = pushPerUser[message.address.user.id];
webPush.sendNotification({
endpoint: pushsub.endpoint,
TTL: "1",
keys: {
p256dh: pushsub.key,
auth: pushsub.authSecret
}
}, message.text);
}
});
//here we handle incoming "events" looking for the one which might come from
//the backchannel. we add or replace the subscription info for this specific user
bot.on("event", function (message) {
if (message.name === "pushsubscriptionadded") {
pushPerUser[message.user.id] = message.value;
}
});
//here is a classic way of greeting a new user and explaining how things work
bot.on('conversationUpdate', function (message) {
if (message.membersAdded) {
message.membersAdded.forEach(function (identity) {
if (identity.id === message.address.bot.id) {
var reply = new builder.Message()
.address(message.address)
.text("Howdy! I am a [demo bot](https://github.com/meulta/webchat-pushnotifications) using the [WebChat control](https://github.com/Microsoft/BotFramework-WebChat) and with Push Notifications! Say **hello** and I will send a message every 10 seconds. If you accepted notifications, you will get one! If you close the tab but leave the browser opened, you will get a notification when I talk. Oh and say **stop** to shut me off :)");
bot.send(reply);
}
});
}
});
//this part is a very simple way of simulating proactive dialogs
//we only send a message with a integer that we increment
//every 5 seconds. this is for push notifications test purposes and should not
//be use as base to create any real life bot :)
var loop = false;
var count = 1;
bot.dialog('/', function (session) {
if (session.message.text === "stop") {
session.send("Stopping loop");
loop = false;
}
else if (!loop) {
loop = true;
count = 1
proactiveEmulation(session);
}
});
var proactiveEmulation = (session) => {
if (loop) {
session.send(`Hello World of web push! :) (${count++})`);
setTimeout(() => proactiveEmulation(session), 5000);
}
};
//we use this to serve the webchat webpage
server.get(/\/web\/?.*/, restify.serveStatic({
directory: __dirname
}));
(function () {
const DIRECTLINE_SECRET = ""; //you get that from the direct line channel at dev.botframework.com
const VAPID_PUBLICKEY = ""; //you get that from the server, which will generate a vapidKey.json file
var startChat = function () {
let botConnection;
if (getParameterByName("isback") === 'y') {
//if we are resuming an existing conversation, we get back the conversationid from LocalStorage
botConnection = new DirectLine.DirectLine({
secret: DIRECTLINE_SECRET,
conversationId: localStorage.getItem("pushsample.botConnection.conversationId"),
webSocket: false
});
} else {
//if it is a brand new conversation, we create a fresh one
botConnection = new DirectLine.DirectLine({
secret: DIRECTLINE_SECRET,
webSocket: false
});
}
botConnection.connectionStatus$
.filter(s => s === 2) //when the status is 'connected' (2)
.subscribe(c => {
//everything is setup in DirectLine, we can create the Chatbot control
BotChat.App({
botConnection: botConnection,
user: { id: botConnection.conversationId}, //you could define you own userid here
resize: 'detect'
}, document.getElementById("bot"));
//we setup push notifications (including service worker registration)
setupPush((subscriptionInfo) => {
//once push notifications are setup, we get the subscription info back in this callback
//we use the backchannel to send this info back to the bot using an 'event' activity
botConnection
.postActivity({
type: "event",
name: "pushsubscriptionadded",
value: subscriptionInfo,
from: { id: botConnection.conversationId } //you could define your own userId here
})
.subscribe(id => {
//we store the conversation id which we get back from postActivity(...) in the LocalStorage
//we will need this in case of conversation resuming
localStorage.setItem("pushsample.botConnection.conversationId", botConnection.conversationId);
});
});
});
botConnection.activity$.subscribe(c => {
//here is were you can get each activity's watermark
//we do not do anything in this sample, but you can use it if you need
//to restore history at resuming at a specific moment in the conversation
console.log(botConnection.watermark);
});
};
// Push
var setupPush = function (done) {
//first step is registering the service worker file
navigator.serviceWorker.register('service-worker.js')
.then(function (registration) {
//once the sw is registered, we try to get an existing push subscription
return registration.pushManager.getSubscription()
.then(function (subscription) {
//if the subscription exists, then we pass is to the next chained .then function using return
if (subscription) {
return subscription;
}
//if the subscription does not exists, we wrap the VAPID public key and create a new one
//we pass this new once to the next chaind .then function using return
const convertedVapidKey = urlBase64ToUint8Array(VAPID_PUBLICKEY);
return registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey
});
});
})
.then(function (subscription) {
//wrapping the key and secret
const rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
const key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
const rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
const authSecret = rawAuthSecret ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';
const endpoint = subscription.endpoint;
//we call back the code that asked to register push notification with the subscription information
done({
endpoint: subscription.endpoint,
key: key,
authSecret: authSecret
});
});
}
// Helpers
function getParameterByName(name, url) {
if (!url) {
url = window.location.href;
}
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
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;
}
//everything is defined, let's start the chat
startChat();
})();
var baseurl = ""; //replace that with you website baseurl. you could handle this differently but it was simplier in this sample
self.addEventListener('push', function (event) {
//creating the notification message (we should never be in the "no message" case)
var payload = event.data ? event.data.text() : 'No message...';
//we show a notification to the user with the text message
//and an icon which is hosted as a resource on the website
event.waitUntil(
self.registration.showNotification('Chat bot!', {
body: payload,
icon: '/web/img/thinking_morphi.png'
})
);
});
self.addEventListener('notificationclick', function (event) {
// Android doesn't close the notification when you click on it
// See: http://crbug.com/463146
event.notification.close();
// This looks to see if the current is already open and
// focuses if it is
event.waitUntil(
//searching for all clients / tab opened in the browser
clients.matchAll({
type: "window"
})
.then(function (clientList) {
//going through the list of clients/tab and trying to find our website
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i];
//if we find it, we put focus back on the tab
if ((client.url.toLowerCase() == baseurl + '/web/index.html' || client.url.toLowerCase() == baseurl + '/web/index.html?isback=y') && 'focus' in client)
return client.focus();
}
if (clients.openWindow) {
//if we did not find it, then we re-open it with the isback=y parameter
//to ensure that we resume the conversation using the conversationid
return clients.openWindow('/web/index.html?isback=y');
}
})
);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment