Merge pull request #1124 from thelounge/xpaw/push-notifications
Implement push notifications
This commit is contained in:
commit
fcd9782eb7
@ -10,7 +10,7 @@ Modern web IRC client designed for self-hosting.
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
* **Modern features brought to IRC.** Link previews, new message markers, ~~push notifications~~ [*(soon!)*](https://github.com/thelounge/lounge/pull/1124), and more bring IRC to the 21st century.
|
* **Modern features brought to IRC.** Push notifications, link previews, new message markers, and more bring IRC to the 21st century.
|
||||||
* **Always connected.** Remains connected to IRC servers while you are offline.
|
* **Always connected.** Remains connected to IRC servers while you are offline.
|
||||||
* **Cross platform.** It doesn't matter what OS you use, it just works wherever Node.js runs.
|
* **Cross platform.** It doesn't matter what OS you use, it just works wherever Node.js runs.
|
||||||
* **Responsive interface.** The client works smoothly on every desktop, smartphone and tablet.
|
* **Responsive interface.** The client works smoothly on every desktop, smartphone and tablet.
|
||||||
|
@ -688,7 +688,8 @@ kbd {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#windows label {
|
#windows label,
|
||||||
|
#settings .error {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,20 +293,36 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#unless public}}
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<h2>Notifications</h2>
|
<h2>Push Notifications</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<button type="button" class="btn" id="pushNotifications" disabled data-text-alternate="Unsubscribe from push notifications">Subscribe to push notifications</button>
|
||||||
|
<div class="error" id="pushNotificationsHttps">
|
||||||
|
<strong>Warning</strong>:
|
||||||
|
Push notifications are only supported over HTTPS connections.
|
||||||
|
</div>
|
||||||
|
<div class="error" id="pushNotificationsUnsupported">
|
||||||
|
<strong>Warning</strong>:
|
||||||
|
<span>Push notifications are not supported by your browser.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<h2>Browser Notifications</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label class="opt">
|
<label class="opt">
|
||||||
<input id="desktopNotifications" type="checkbox" name="desktopNotifications">
|
<input id="desktopNotifications" type="checkbox" name="desktopNotifications">
|
||||||
Enable desktop notifications<br>
|
Enable browser notifications<br>
|
||||||
<div class="error" id="warnUnsupportedDesktopNotifications">
|
<div class="error" id="warnUnsupportedDesktopNotifications">
|
||||||
<strong>Warning</strong>:
|
<strong>Warning</strong>:
|
||||||
Desktop notifications are not supported by your browser.
|
Notifications are not supported by your browser.
|
||||||
</div>
|
</div>
|
||||||
<div class="error" id="warnBlockedDesktopNotifications">
|
<div class="error" id="warnBlockedDesktopNotifications">
|
||||||
<strong>Warning</strong>:
|
<strong>Warning</strong>:
|
||||||
Desktop notifications are blocked by your browser.
|
Notifications are blocked by your browser.
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,6 +22,7 @@ require("./socket-events");
|
|||||||
const constants = require("./constants");
|
const constants = require("./constants");
|
||||||
const storage = require("./localStorage");
|
const storage = require("./localStorage");
|
||||||
const utils = require("./utils");
|
const utils = require("./utils");
|
||||||
|
require("./webpush");
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
var sidebar = $("#sidebar, #footer");
|
var sidebar = $("#sidebar, #footer");
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
const $ = require("jquery");
|
const $ = require("jquery");
|
||||||
const socket = require("../socket");
|
const socket = require("../socket");
|
||||||
const render = require("../render");
|
const render = require("../render");
|
||||||
|
const webpush = require("../webpush");
|
||||||
const sidebar = $("#sidebar");
|
const sidebar = $("#sidebar");
|
||||||
const storage = require("../localStorage");
|
const storage = require("../localStorage");
|
||||||
|
|
||||||
@ -21,6 +22,8 @@ socket.on("init", function(data) {
|
|||||||
storage.set("token", data.token);
|
storage.set("token", data.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
webpush.configurePushNotifications(data.pushSubscription, data.applicationServerKey);
|
||||||
|
|
||||||
$("body").removeClass("signed-out");
|
$("body").removeClass("signed-out");
|
||||||
$("#loading").remove();
|
$("#loading").remove();
|
||||||
$("#sign-in").remove();
|
$("#sign-in").remove();
|
||||||
|
127
client/js/webpush.js
Normal file
127
client/js/webpush.js
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const $ = require("jquery");
|
||||||
|
const storage = require("./localStorage");
|
||||||
|
const socket = require("./socket");
|
||||||
|
|
||||||
|
const pushNotificationsButton = $("#pushNotifications");
|
||||||
|
let clientSubscribed = null;
|
||||||
|
let applicationServerKey;
|
||||||
|
|
||||||
|
module.exports.configurePushNotifications = (subscribedOnServer, key) => {
|
||||||
|
applicationServerKey = key;
|
||||||
|
|
||||||
|
// If client has push registration but the server knows nothing about it,
|
||||||
|
// this subscription is broken and client has to register again
|
||||||
|
if (clientSubscribed === true && subscribedOnServer === false) {
|
||||||
|
pushNotificationsButton.attr("disabled", true);
|
||||||
|
|
||||||
|
navigator.serviceWorker.register("service-worker.js")
|
||||||
|
.then((registration) => registration.pushManager.getSubscription())
|
||||||
|
.then((subscription) => subscription && subscription.unsubscribe())
|
||||||
|
.then((successful) => {
|
||||||
|
if (successful) {
|
||||||
|
alternatePushButton().removeAttr("disabled");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isAllowedServiceWorkersHost()) {
|
||||||
|
$("#pushNotificationsHttps").hide();
|
||||||
|
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
navigator.serviceWorker.register("service-worker.js").then((registration) => {
|
||||||
|
if (!registration.pushManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return registration.pushManager.getSubscription().then((subscription) => {
|
||||||
|
$("#pushNotificationsUnsupported").hide();
|
||||||
|
|
||||||
|
pushNotificationsButton
|
||||||
|
.removeAttr("disabled")
|
||||||
|
.on("click", onPushButton);
|
||||||
|
|
||||||
|
clientSubscribed = !!subscription;
|
||||||
|
|
||||||
|
if (clientSubscribed) {
|
||||||
|
alternatePushButton();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
$("#pushNotificationsUnsupported span").text(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPushButton() {
|
||||||
|
pushNotificationsButton.attr("disabled", true);
|
||||||
|
|
||||||
|
navigator.serviceWorker.register("service-worker.js").then((registration) => {
|
||||||
|
registration.pushManager.getSubscription().then((existingSubscription) => {
|
||||||
|
if (existingSubscription) {
|
||||||
|
socket.emit("push:unregister");
|
||||||
|
|
||||||
|
return existingSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
return registration.pushManager.subscribe({
|
||||||
|
applicationServerKey: urlBase64ToUint8Array(applicationServerKey),
|
||||||
|
userVisibleOnly: true
|
||||||
|
}).then((subscription) => {
|
||||||
|
const rawKey = subscription.getKey ? subscription.getKey("p256dh") : "";
|
||||||
|
const key = rawKey ? window.btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : "";
|
||||||
|
const rawAuthSecret = subscription.getKey ? subscription.getKey("auth") : "";
|
||||||
|
const authSecret = rawAuthSecret ? window.btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : "";
|
||||||
|
|
||||||
|
socket.emit("push:register", {
|
||||||
|
token: storage.get("token"),
|
||||||
|
endpoint: subscription.endpoint,
|
||||||
|
keys: {
|
||||||
|
p256dh: key,
|
||||||
|
auth: authSecret
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}).then((successful) => {
|
||||||
|
if (successful) {
|
||||||
|
alternatePushButton().removeAttr("disabled");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
$("#pushNotificationsUnsupported span").text(err).show();
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function alternatePushButton() {
|
||||||
|
const text = pushNotificationsButton.text();
|
||||||
|
|
||||||
|
return pushNotificationsButton
|
||||||
|
.text(pushNotificationsButton.data("text-alternate"))
|
||||||
|
.data("text-alternate", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAllowedServiceWorkersHost() {
|
||||||
|
return location.protocol === "https:" || location.hostname === "localhost" || location.hostname === "127.0.0.1";
|
||||||
|
}
|
41
client/service-worker.js
Normal file
41
client/service-worker.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// The Lounge - https://github.com/thelounge/lounge
|
||||||
|
/* global clients */
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
self.addEventListener("push", function(event) {
|
||||||
|
if (!event.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = event.data.json();
|
||||||
|
|
||||||
|
if (payload.type === "notification") {
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification(payload.title, {
|
||||||
|
badge: "img/logo-64.png",
|
||||||
|
icon: "img/touch-icon-192x192.png",
|
||||||
|
body: payload.body,
|
||||||
|
timestamp: payload.timestamp,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("notificationclick", function(event) {
|
||||||
|
event.notification.close();
|
||||||
|
|
||||||
|
event.waitUntil(clients.matchAll({
|
||||||
|
type: "window"
|
||||||
|
}).then(function(clientList) {
|
||||||
|
for (var i = 0; i < clientList.length; i++) {
|
||||||
|
var client = clientList[i];
|
||||||
|
if ("focus" in client) {
|
||||||
|
return client.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clients.openWindow) {
|
||||||
|
return clients.openWindow(".");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
@ -58,7 +58,8 @@
|
|||||||
"socket.io": "1.7.4",
|
"socket.io": "1.7.4",
|
||||||
"spdy": "3.4.7",
|
"spdy": "3.4.7",
|
||||||
"ua-parser-js": "0.7.14",
|
"ua-parser-js": "0.7.14",
|
||||||
"urijs": "1.18.12"
|
"urijs": "1.18.12",
|
||||||
|
"web-push": "3.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "6.26.0",
|
"babel-core": "6.26.0",
|
||||||
|
@ -94,11 +94,21 @@ function Client(manager, name, config) {
|
|||||||
client.config.sessions = {};
|
client.config.sessions = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_.forOwn(client.config.sessions, (session) => {
|
||||||
|
if (session.pushSubscription) {
|
||||||
|
this.registerPushSubscription(session, session.pushSubscription, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (client.name) {
|
if (client.name) {
|
||||||
log.info(`User ${colors.bold(client.name)} loaded`);
|
log.info(`User ${colors.bold(client.name)} loaded`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Client.prototype.isRegistered = function() {
|
||||||
|
return this.name.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
Client.prototype.emit = function(event, data) {
|
Client.prototype.emit = function(event, data) {
|
||||||
if (this.sockets !== null) {
|
if (this.sockets !== null) {
|
||||||
this.sockets.in(this.id).emit(event, data);
|
this.sockets.in(this.id).emit(event, data);
|
||||||
@ -310,11 +320,11 @@ Client.prototype.updateSession = function(token, ip, request) {
|
|||||||
friendlyAgent += ` on ${agent.os.name} ${agent.os.version}`;
|
friendlyAgent += ` on ${agent.os.name} ${agent.os.version}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
client.config.sessions[token] = {
|
client.config.sessions[token] = _.assign({
|
||||||
lastUse: Date.now(),
|
lastUse: Date.now(),
|
||||||
ip: ip,
|
ip: ip,
|
||||||
agent: friendlyAgent,
|
agent: friendlyAgent,
|
||||||
};
|
}, client.config.sessions[token]);
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.setPassword = function(hash, callback) {
|
Client.prototype.setPassword = function(hash, callback) {
|
||||||
@ -410,7 +420,7 @@ Client.prototype.more = function(data) {
|
|||||||
Client.prototype.open = function(socketId, target) {
|
Client.prototype.open = function(socketId, target) {
|
||||||
// Opening a window like settings
|
// Opening a window like settings
|
||||||
if (target === null) {
|
if (target === null) {
|
||||||
this.attachedClients[socketId] = -1;
|
this.attachedClients[socketId].openChannel = -1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -423,7 +433,7 @@ Client.prototype.open = function(socketId, target) {
|
|||||||
target.chan.unread = 0;
|
target.chan.unread = 0;
|
||||||
target.chan.highlight = false;
|
target.chan.highlight = false;
|
||||||
|
|
||||||
this.attachedClients[socketId] = target.chan.id;
|
this.attachedClients[socketId].openChannel = target.chan.id;
|
||||||
this.lastActiveChannel = target.chan.id;
|
this.lastActiveChannel = target.chan.id;
|
||||||
|
|
||||||
this.emit("open", target.chan.id);
|
this.emit("open", target.chan.id);
|
||||||
@ -493,7 +503,7 @@ Client.prototype.quit = function() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.clientAttach = function(socketId) {
|
Client.prototype.clientAttach = function(socketId, token) {
|
||||||
var client = this;
|
var client = this;
|
||||||
var save = false;
|
var save = false;
|
||||||
|
|
||||||
@ -507,7 +517,10 @@ Client.prototype.clientAttach = function(socketId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
client.attachedClients[socketId] = client.lastActiveChannel;
|
client.attachedClients[socketId] = {
|
||||||
|
token: token,
|
||||||
|
openChannel: client.lastActiveChannel
|
||||||
|
};
|
||||||
|
|
||||||
// Update old networks to store ip and hostmask
|
// Update old networks to store ip and hostmask
|
||||||
client.networks.forEach((network) => {
|
client.networks.forEach((network) => {
|
||||||
@ -547,6 +560,40 @@ Client.prototype.clientDetach = function(socketId) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Client.prototype.registerPushSubscription = function(session, subscription, noSave) {
|
||||||
|
if (!_.isPlainObject(subscription) || !_.isPlainObject(subscription.keys)
|
||||||
|
|| typeof subscription.endpoint !== "string" || !/^https?:\/\//.test(subscription.endpoint)
|
||||||
|
|| typeof subscription.keys.p256dh !== "string" || typeof subscription.keys.auth !== "string") {
|
||||||
|
session.pushSubscription = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
endpoint: subscription.endpoint,
|
||||||
|
keys: {
|
||||||
|
p256dh: subscription.keys.p256dh,
|
||||||
|
auth: subscription.keys.auth
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
session.pushSubscription = data;
|
||||||
|
|
||||||
|
if (!noSave) {
|
||||||
|
this.manager.updateUser(this.name, {
|
||||||
|
sessions: this.config.sessions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
Client.prototype.unregisterPushSubscription = function(token) {
|
||||||
|
this.config.sessions[token].pushSubscription = null;
|
||||||
|
this.manager.updateUser(this.name, {
|
||||||
|
sessions: this.config.sessions
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
Client.prototype.save = _.debounce(function SaveClient() {
|
Client.prototype.save = _.debounce(function SaveClient() {
|
||||||
if (Helper.config.public) {
|
if (Helper.config.public) {
|
||||||
return;
|
return;
|
||||||
|
@ -5,6 +5,7 @@ var colors = require("colors/safe");
|
|||||||
var fs = require("fs");
|
var fs = require("fs");
|
||||||
var Client = require("./client");
|
var Client = require("./client");
|
||||||
var Helper = require("./helper");
|
var Helper = require("./helper");
|
||||||
|
const WebPush = require("./plugins/webpush");
|
||||||
|
|
||||||
module.exports = ClientManager;
|
module.exports = ClientManager;
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ function ClientManager() {
|
|||||||
ClientManager.prototype.init = function(identHandler, sockets) {
|
ClientManager.prototype.init = function(identHandler, sockets) {
|
||||||
this.sockets = sockets;
|
this.sockets = sockets;
|
||||||
this.identHandler = identHandler;
|
this.identHandler = identHandler;
|
||||||
|
this.webPush = new WebPush();
|
||||||
|
|
||||||
if (!Helper.config.public) {
|
if (!Helper.config.public) {
|
||||||
if ("autoload" in Helper.config) {
|
if ("autoload" in Helper.config) {
|
||||||
|
@ -19,6 +19,7 @@ var Helper = {
|
|||||||
getVersion: getVersion,
|
getVersion: getVersion,
|
||||||
getGitCommit: getGitCommit,
|
getGitCommit: getGitCommit,
|
||||||
ip2hex: ip2hex,
|
ip2hex: ip2hex,
|
||||||
|
cleanIrcMessage: cleanIrcMessage,
|
||||||
|
|
||||||
password: {
|
password: {
|
||||||
hash: passwordHash,
|
hash: passwordHash,
|
||||||
@ -121,6 +122,11 @@ function expandHome(shortenedPath) {
|
|||||||
return path.resolve(shortenedPath.replace(/^~($|\/|\\)/, home + "$1"));
|
return path.resolve(shortenedPath.replace(/^~($|\/|\\)/, home + "$1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanIrcMessage(message) {
|
||||||
|
// TODO: This does not strip hex based colours
|
||||||
|
return message.replace(/\x02|\x1D|\x1F|\x16|\x0F|\x03(?:[0-9]{1,2}(?:,[0-9]{1,2})?)?/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
function passwordRequiresUpdate(password) {
|
function passwordRequiresUpdate(password) {
|
||||||
return bcrypt.getRounds(password) !== 11;
|
return bcrypt.getRounds(password) !== 11;
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ Chan.prototype.pushMessage = function(client, msg, increasesUnread) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// If this channel is open in any of the clients, do not increase unread counter
|
// If this channel is open in any of the clients, do not increase unread counter
|
||||||
var isOpen = _.includes(client.attachedClients, this.id);
|
const isOpen = _.find(client.attachedClients, {openChannel: this.id}) !== undefined;
|
||||||
|
|
||||||
if ((increasesUnread || msg.highlight) && !isOpen) {
|
if ((increasesUnread || msg.highlight) && !isOpen) {
|
||||||
obj.unread = ++this.unread;
|
obj.unread = ++this.unread;
|
||||||
|
@ -16,7 +16,7 @@ module.exports = function(client, chan, msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove all IRC formatting characters before searching for links
|
// Remove all IRC formatting characters before searching for links
|
||||||
const cleanText = msg.text.replace(/\x02|\x1D|\x1F|\x16|\x0F|\x03(?:[0-9]{1,2}(?:,[0-9]{1,2})?)?/g, "");
|
const cleanText = Helper.cleanIrcMessage(msg.text);
|
||||||
|
|
||||||
// We will only try to prefetch http(s) links
|
// We will only try to prefetch http(s) links
|
||||||
const links = findLinks(cleanText).filter((w) => /^https?:\/\//.test(w.link));
|
const links = findLinks(cleanText).filter((w) => /^https?:\/\//.test(w.link));
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
const Chan = require("../../models/chan");
|
const Chan = require("../../models/chan");
|
||||||
const Msg = require("../../models/msg");
|
const Msg = require("../../models/msg");
|
||||||
const LinkPrefetch = require("./link");
|
const LinkPrefetch = require("./link");
|
||||||
|
const Helper = require("../../helper");
|
||||||
|
|
||||||
module.exports = function(irc, network) {
|
module.exports = function(irc, network) {
|
||||||
var client = this;
|
var client = this;
|
||||||
@ -102,5 +103,24 @@ module.exports = function(irc, network) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
chan.pushMessage(client, msg, !self);
|
chan.pushMessage(client, msg, !self);
|
||||||
|
|
||||||
|
// Do not send notifications for messages older than 15 minutes (znc buffer for example)
|
||||||
|
if (highlight && (!data.time || data.time > Date.now() - 900000)) {
|
||||||
|
let title = data.nick;
|
||||||
|
|
||||||
|
if (chan.type !== Chan.Type.QUERY) {
|
||||||
|
title += ` (${chan.name}) mentioned you`;
|
||||||
|
} else {
|
||||||
|
title += " sent you a message";
|
||||||
|
}
|
||||||
|
|
||||||
|
client.manager.webPush.push(client, {
|
||||||
|
type: "notification",
|
||||||
|
chanId: chan.id,
|
||||||
|
timestamp: data.time || Date.now(),
|
||||||
|
title: `The Lounge: ${title}`,
|
||||||
|
body: Helper.cleanIrcMessage(data.message)
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
73
src/plugins/webpush.js
Normal file
73
src/plugins/webpush.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const _ = require("lodash");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const WebPushAPI = require("web-push");
|
||||||
|
const Helper = require("../helper");
|
||||||
|
|
||||||
|
class WebPush {
|
||||||
|
constructor() {
|
||||||
|
const vapidPath = path.join(Helper.HOME, "vapid.json");
|
||||||
|
|
||||||
|
if (fs.existsSync(vapidPath)) {
|
||||||
|
const data = fs.readFileSync(vapidPath, "utf-8");
|
||||||
|
const parsedData = JSON.parse(data);
|
||||||
|
|
||||||
|
if (typeof parsedData.publicKey === "string" && typeof parsedData.privateKey === "string") {
|
||||||
|
this.vapidKeys = {
|
||||||
|
publicKey: parsedData.publicKey,
|
||||||
|
privateKey: parsedData.privateKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.vapidKeys) {
|
||||||
|
this.vapidKeys = WebPushAPI.generateVAPIDKeys();
|
||||||
|
|
||||||
|
fs.writeFileSync(vapidPath, JSON.stringify(this.vapidKeys, null, "\t"));
|
||||||
|
|
||||||
|
log.info("New VAPID key pair has been generated for use with push subscription.");
|
||||||
|
}
|
||||||
|
|
||||||
|
WebPushAPI.setVapidDetails(
|
||||||
|
"https://github.com/thelounge/lounge",
|
||||||
|
this.vapidKeys.publicKey,
|
||||||
|
this.vapidKeys.privateKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
push(client, payload, onlyToOffline) {
|
||||||
|
_.forOwn(client.config.sessions, (session, token) => {
|
||||||
|
if (session.pushSubscription) {
|
||||||
|
if (onlyToOffline && _.find(client.attachedClients, {token: token}) !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pushSingle(client, session.pushSubscription, payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pushSingle(client, subscription, payload) {
|
||||||
|
WebPushAPI
|
||||||
|
.sendNotification(subscription, JSON.stringify(payload))
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.statusCode >= 400 && error.statusCode < 500) {
|
||||||
|
log.warn(`WebPush subscription for ${client.name} returned an error (${error.statusCode}), removing subscription`);
|
||||||
|
|
||||||
|
_.forOwn(client.config.sessions, (session, token) => {
|
||||||
|
if (session.pushSubscription && session.pushSubscription.endpoint === subscription.endpoint) {
|
||||||
|
client.unregisterPushSubscription(token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.error("WebPush Error", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WebPush;
|
@ -180,7 +180,7 @@ function initializeClient(socket, client, generateToken, token) {
|
|||||||
socket.on("disconnect", function() {
|
socket.on("disconnect", function() {
|
||||||
client.clientDetach(socket.id);
|
client.clientDetach(socket.id);
|
||||||
});
|
});
|
||||||
client.clientAttach(socket.id);
|
client.clientAttach(socket.id, token);
|
||||||
|
|
||||||
socket.on(
|
socket.on(
|
||||||
"input",
|
"input",
|
||||||
@ -296,6 +296,31 @@ function initializeClient(socket, client, generateToken, token) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("push:register", (subscription) => {
|
||||||
|
if (!client.isRegistered() || !client.config.sessions[token]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registration = client.registerPushSubscription(client.config.sessions[token], subscription);
|
||||||
|
|
||||||
|
if (registration) {
|
||||||
|
client.manager.webPush.pushSingle(client, registration, {
|
||||||
|
type: "notification",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
title: "The Lounge",
|
||||||
|
body: "🚀 Push notifications have been enabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("push:unregister", () => {
|
||||||
|
if (!client.isRegistered()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.unregisterPushSubscription(token);
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("sign-out", () => {
|
socket.on("sign-out", () => {
|
||||||
delete client.config.sessions[token];
|
delete client.config.sessions[token];
|
||||||
|
|
||||||
@ -314,6 +339,8 @@ function initializeClient(socket, client, generateToken, token) {
|
|||||||
|
|
||||||
const sendInitEvent = (tokenToSend) => {
|
const sendInitEvent = (tokenToSend) => {
|
||||||
socket.emit("init", {
|
socket.emit("init", {
|
||||||
|
applicationServerKey: manager.webPush.vapidKeys.publicKey,
|
||||||
|
pushSubscription: client.config.sessions[token],
|
||||||
active: client.lastActiveChannel,
|
active: client.lastActiveChannel,
|
||||||
networks: client.networks,
|
networks: client.networks,
|
||||||
token: tokenToSend
|
token: tokenToSend
|
||||||
|
48
test/tests/cleanircmessages.js
Normal file
48
test/tests/cleanircmessages.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const expect = require("chai").expect;
|
||||||
|
const Helper = require("../../src/helper");
|
||||||
|
|
||||||
|
describe("Clean IRC messages", function() {
|
||||||
|
it("should remove all formatting", function() {
|
||||||
|
const testCases = [{
|
||||||
|
input: "\x0303",
|
||||||
|
expected: ""
|
||||||
|
}, {
|
||||||
|
input: "\x02bold",
|
||||||
|
expected: "bold"
|
||||||
|
}, {
|
||||||
|
input: "\x038yellowText",
|
||||||
|
expected: "yellowText"
|
||||||
|
}, {
|
||||||
|
input: "\x030,0white,white",
|
||||||
|
expected: "white,white"
|
||||||
|
}, {
|
||||||
|
input: "\x034,8yellowBGredText",
|
||||||
|
expected: "yellowBGredText"
|
||||||
|
}, {
|
||||||
|
input: "\x1ditalic",
|
||||||
|
expected: "italic"
|
||||||
|
}, {
|
||||||
|
input: "\x1funderline",
|
||||||
|
expected: "underline"
|
||||||
|
}, {
|
||||||
|
input: "\x02bold\x038yellow\x02nonBold\x03default",
|
||||||
|
expected: "boldyellownonBolddefault"
|
||||||
|
}, {
|
||||||
|
input: "\x02bold\x02 \x02bold\x02",
|
||||||
|
expected: "bold bold"
|
||||||
|
}, {
|
||||||
|
input: "\x02irc\x0f://\x1dfreenode.net\x0f/\x034,8thelounge",
|
||||||
|
expected: "irc://freenode.net/thelounge"
|
||||||
|
}, {
|
||||||
|
input: "\x02#\x038,9thelounge",
|
||||||
|
expected: "#thelounge"
|
||||||
|
}];
|
||||||
|
|
||||||
|
const actual = testCases.map((testCase) => Helper.cleanIrcMessage(testCase.input));
|
||||||
|
const expected = testCases.map((testCase) => testCase.expected);
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user