diff --git a/meshcentral.js b/meshcentral.js index dfd93845..bce1e2e7 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -2302,7 +2302,7 @@ function CreateMeshCentralServer(config, args) { const domainId = meshSplit[1]; if (obj.config.domains[domainId] == null) return; const mailserver = obj.config.domains[domainId].mailserver; - if (mailserver == null) return; + if ((mailserver == null) && (obj.msgserver == null)) return; // Get the device group for this device const mesh = obj.webserver.meshes[meshid]; @@ -2335,7 +2335,8 @@ function CreateMeshCentralServer(config, args) { if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; } } - if ((notify & 48) != 0) { + // Email notifications + if ((mailserver != null) && ((notify & 48) != 0)) { if (stateSet == true) { if ((notify & 16) != 0) { mailserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo); @@ -2351,6 +2352,24 @@ function CreateMeshCentralServer(config, args) { } } } + + // Messaging notifications + if ((obj.msgserver != null) && ((notify & 384) != 0)) { + if (stateSet == true) { + if ((notify & 128) != 0) { + obj.msgserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo); + } else { + obj.msgserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo); + } + } + else if (stateSet == false) { + if ((notify & 256) != 0) { + obj.msgserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo); + } else { + obj.msgserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo); + } + } + } } } } diff --git a/meshmessaging.js b/meshmessaging.js index 3be55582..0b997bc0 100644 --- a/meshmessaging.js +++ b/meshmessaging.js @@ -122,6 +122,7 @@ module.exports.CreateServer = function (parent) { obj.callMeBotClient = null; obj.pushoverClient = null; obj.zulipClient = null; + const sortCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }) // Telegram client setup if (parent.config.messaging.telegram) { @@ -347,7 +348,7 @@ module.exports.CreateServer = function (parent) { if (func != null) { func(true); } } else { // No providers found - func(false, "No messaging providers found for this message."); + if (func != null) { func(false, "No messaging providers found for this message."); } } } @@ -436,6 +437,210 @@ module.exports.CreateServer = function (parent) { obj.sendMessage(to, sms, domain, func); }; + // Send device state change notification + obj.sendDeviceNotify = function (domain, username, to, connections, disconnections, lang) { + if (to == null) return; + parent.debug('email', "Sending device state change message to " + to); + + var sms = []; + if (connections.length > 0) { sms.push('Connections: ' + connections.join(', ')); } // TODO: Translate 'Connections: ' + if (disconnections.length > 0) { sms.push('Disconnections: ' + disconnections.join(', ')); } // TODO: Translate 'Disconnections: ' + if (sms.length == 0) return; + sms = sms.join(' - '); + if (sms.length > 1000) { sms = sms.substring(0, 997) + '...'; } // Limit messages to 1000 characters + + // Send the message + obj.sendMessage(to, sms, domain, null); + }; + + + // + // Device connetion and disconnection notifications + // + + obj.deviceNotifications = {}; // UserId --> { timer, nodes: nodeid --> connectType } + + // A device connected and a user needs to be notified about it. + obj.notifyDeviceConnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) { + const mesh = parent.webserver.meshes[meshid]; + if (mesh == null) return; + + // Add the user and start a timer + if (obj.deviceNotifications[user._id] == null) { + obj.deviceNotifications[user._id] = { nodes: {} }; + obj.deviceNotifications[user._id].timer = setTimeout(function () { sendDeviceNotifications(user._id); }, 1 * 60 * 1000); // 1 minute before message is sent + } + + // Add the device + if (obj.deviceNotifications[user._id].nodes[nodeid] == null) { + obj.deviceNotifications[user._id].nodes[nodeid] = { c: connectType }; // This device connection need to be added + } else { + const info = obj.deviceNotifications[user._id].nodes[nodeid]; + if ((info.d != null) && ((info.d & connectType) != 0)) { + info.d -= connectType; // This device disconnect cancels out a device connection + if (((info.c == null) || (info.c == 0)) && ((info.d == null) || (info.d == 0))) { + // This device no longer needs a notification + delete obj.deviceNotifications[user._id].nodes[nodeid]; + if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) { + // This user no longer needs a notification + clearTimeout(obj.deviceNotifications[user._id].timer); + delete obj.deviceNotifications[user._id]; + } + return; + } + } else { + if (info.c != null) { + info.c |= connectType; // This device disconnect needs to be added + } else { + info.c = connectType; // This device disconnect needs to be added + } + } + } + + // Set the device group name + if ((extraInfo != null) && (extraInfo.name != null)) { obj.deviceNotifications[user._id].nodes[nodeid].nn = extraInfo.name; } + obj.deviceNotifications[user._id].nodes[nodeid].mn = mesh.name; + } + + // Cancel a device disconnect notification + obj.cancelNotifyDeviceDisconnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) { + const mesh = parent.webserver.meshes[meshid]; + if (mesh == null) return; + + if ((obj.deviceNotifications[user._id] != null) && (obj.deviceNotifications[user._id].nodes[nodeid] != null)) { + const info = obj.deviceNotifications[user._id].nodes[nodeid]; + if ((info.d != null) && ((info.d & connectType) != 0)) { + info.d -= connectType; // This device disconnect cancels out a device connection + if (((info.c == null) || (info.c == 0)) && ((info.d == null) || (info.d == 0))) { + // This device no longer needs a notification + delete obj.deviceNotifications[user._id].nodes[nodeid]; + if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) { + // This user no longer needs a notification + clearTimeout(obj.deviceNotifications[user._id].timer); + delete obj.deviceNotifications[user._id]; + } + } + } + } + } + + // A device disconnected and a user needs to be notified about it. + obj.notifyDeviceDisconnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) { + const mesh = parent.webserver.meshes[meshid]; + if (mesh == null) return; + + // Add the user and start a timer + if (obj.deviceNotifications[user._id] == null) { + obj.deviceNotifications[user._id] = { nodes: {} }; + obj.deviceNotifications[user._id].timer = setTimeout(function () { sendDeviceNotifications(user._id); }, 1 * 60 * 1000); // 1 minute before message is sent + } + + // Add the device + if (obj.deviceNotifications[user._id].nodes[nodeid] == null) { + obj.deviceNotifications[user._id].nodes[nodeid] = { d: connectType }; // This device disconnect need to be added + } else { + const info = obj.deviceNotifications[user._id].nodes[nodeid]; + if ((info.c != null) && ((info.c & connectType) != 0)) { + info.c -= connectType; // This device disconnect cancels out a device connection + if (((info.d == null) || (info.d == 0)) && ((info.c == null) || (info.c == 0))) { + // This device no longer needs a notification + delete obj.deviceNotifications[user._id].nodes[nodeid]; + if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) { + // This user no longer needs a notification + clearTimeout(obj.deviceNotifications[user._id].timer); + delete obj.deviceNotifications[user._id]; + } + return; + } + } else { + if (info.d != null) { + info.d |= connectType; // This device disconnect needs to be added + } else { + info.d = connectType; // This device disconnect needs to be added + } + } + } + + // Set the device group name + if ((extraInfo != null) && (extraInfo.name != null)) { obj.deviceNotifications[user._id].nodes[nodeid].nn = extraInfo.name; } + obj.deviceNotifications[user._id].nodes[nodeid].mn = mesh.name; + } + + // Cancel a device connect notification + obj.cancelNotifyDeviceConnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) { + const mesh = parent.webserver.meshes[meshid]; + if (mesh == null) return; + + if ((obj.deviceNotifications[user._id] != null) && (obj.deviceNotifications[user._id].nodes[nodeid] != null)) { + const info = obj.deviceNotifications[user._id].nodes[nodeid]; + if ((info.c != null) && ((info.c & connectType) != 0)) { + info.c -= connectType; // This device disconnect cancels out a device connection + if (((info.d == null) || (info.d == 0)) && ((info.c == null) || (info.c == 0))) { + // This device no longer needs a notification + delete obj.deviceNotifications[user._id].nodes[nodeid]; + if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) { + // This user no longer needs a notification + clearTimeout(obj.deviceNotifications[user._id].timer); + delete obj.deviceNotifications[user._id]; + } + } + } + } + } + + // Send a notification about device connections and disconnections to a user + function sendDeviceNotifications(userid) { + if (obj.deviceNotifications[userid] == null) return; + clearTimeout(obj.deviceNotifications[userid].timer); + + var connections = []; + var disconnections = []; + + for (var nodeid in obj.deviceNotifications[userid].nodes) { + var info = obj.deviceNotifications[userid].nodes[nodeid]; + if ((info.c != null) && (info.c > 0) && (info.nn != null) && (info.mn != null)) { + /* + var c = []; + if (info.c & 1) { c.push("Agent"); } + if (info.c & 2) { c.push("CIRA"); } + if (info.c & 4) { c.push("AMT"); } + if (info.c & 8) { c.push("AMT-Relay"); } + if (info.c & 16) { c.push("MQTT"); } + connections.push(info.mn + ', ' + info.nn + ': ' + c.join(', ')); + */ + if (info.c & 1) { connections.push(info.nn); } + } + if ((info.d != null) && (info.d > 0) && (info.nn != null) && (info.mn != null)) { + /* + var d = []; + if (info.d & 1) { d.push("Agent"); } + if (info.d & 2) { d.push("CIRA"); } + if (info.d & 4) { d.push("AMT"); } + if (info.d & 8) { d.push("AMT-Relay"); } + if (info.d & 16) { d.push("MQTT"); } + disconnections.push(info.mn + ', ' + info.nn + ': ' + d.join(', ')); + */ + if (info.d & 1) { disconnections.push(info.nn); } + } + } + + // Sort the notifications + connections.sort(sortCollator.compare); + disconnections.sort(sortCollator.compare); + + // Get the user and domain + const user = parent.webserver.users[userid]; + if ((user == null) || (user.email == null) || (user.emailVerified !== true)) return; + const domain = obj.parent.config.domains[user.domain]; + if (domain == null) return; + + // Send the message + obj.sendDeviceNotify(domain, user.name, user.msghandle, connections, disconnections, user.llang); + + // Clean up + delete obj.deviceNotifications[userid]; + } + return obj; }; diff --git a/meshuser.js b/meshuser.js index 40f035af..cd7b361c 100644 --- a/meshuser.js +++ b/meshuser.js @@ -1717,12 +1717,15 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use { if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here. - // 2 = WebPage device connections - // 4 = WebPage device disconnections - // 8 = WebPage device desktop and serial events - // 16 = Email device connections - // 32 = Email device disconnections - // 64 = Email device help request + // 2 = WebPage device connections + // 4 = WebPage device disconnections + // 8 = WebPage device desktop and serial events + // 16 = Email device connections + // 32 = Email device disconnections + // 64 = Email device help request + // 128 = Messaging device connections + // 256 = Messaging device disconnections + // 512 = Messaging device help request var err = null; try { @@ -1767,12 +1770,15 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use { if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here. - // 2 = WebPage device connections - // 4 = WebPage device disconnections - // 8 = WebPage device desktop and serial events - // 16 = Email device connections - // 32 = Email device disconnections - // 64 = Email device help request + // 2 = WebPage device connections + // 4 = WebPage device disconnections + // 8 = WebPage device desktop and serial events + // 16 = Email device connections + // 32 = Email device disconnections + // 64 = Email device help request + // 128 = Messaging device connections + // 256 = Messaging device disconnections + // 512 = Messaging device help request var err = null; try { @@ -5312,6 +5318,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use 'dupagents': [serverUserCommandDupAgents, ""], 'email': [serverUserCommandEmail, ""], 'emailnotifications': [serverUserCommandEmailNotifications, ""], + 'msgnotifications': [serverUserCommandMessageNotifications, ""], 'firebase': [serverUserCommandFirebase, ""], 'heapdump': [serverUserCommandHeapDump, ""], 'heapdump2': [serverUserCommandHeapDump2, ""], @@ -6892,7 +6899,23 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use x += ' ' + info.mn + ', ' + info.nn + ', c:' + (info.c ? info.c : 0) + ', d:' + (info.d ? info.d : 0) + '\r\n'; } } - cmdData.result = ((x == '')?'None':x); + cmdData.result = ((x == '') ? 'None' : x); + } + } + + function serverUserCommandMessageNotifications(cmdData) { + if (parent.parent.msgserver == null) { + cmdData.result = "No messaging service enabled."; + } else { + var x = ''; + for (var userid in parent.parent.msgserver.deviceNotifications) { + x += userid + '\r\n'; + for (var nodeid in parent.parent.msgserver.deviceNotifications[userid].nodes) { + const info = parent.parent.msgserver.deviceNotifications[userid].nodes[nodeid]; + x += ' ' + info.mn + ', ' + info.nn + ', c:' + (info.c ? info.c : 0) + ', d:' + (info.d ? info.d : 0) + '\r\n'; + } + } + cmdData.result = ((x == '') ? 'None' : x); } }