Added device state notifications to messaging applications.

Ylian Saint-Hilaire committed Nov 1, 2022 at 22:39 UTC 1a6e78efeda27053e08d438012d7008e9e467233
3 files changed +263 -16
meshcentral.js
+21 -2
@@ -2302,7 +2302,7 @@ function CreateMeshCentralServer(config, args) {
2302 const domainId = meshSplit[1];
2303 if (obj.config.domains[domainId] == null) return;
2304 const mailserver = obj.config.domains[domainId].mailserver;
2305 - if (mailserver == null) return;
2305 + if ((mailserver == null) && (obj.msgserver == null)) return;
2306
2307 // Get the device group for this device
2308 const mesh = obj.webserver.meshes[meshid];
@@ -2335,7 +2335,8 @@ function CreateMeshCentralServer(config, args) {
2335 if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
2336 }
2337
2338 - if ((notify & 48) != 0) {
2338 + // Email notifications
2339 + if ((mailserver != null) && ((notify & 48) != 0)) {
2340 if (stateSet == true) {
2341 if ((notify & 16) != 0) {
2342 mailserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
@@ -2351,6 +2352,24 @@ function CreateMeshCentralServer(config, args) {
2352 }
2353 }
2354 }
2355 +
2356 + // Messaging notifications
2357 + if ((obj.msgserver != null) && ((notify & 384) != 0)) {
2358 + if (stateSet == true) {
2359 + if ((notify & 128) != 0) {
2360 + obj.msgserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2361 + } else {
2362 + obj.msgserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2363 + }
2364 + }
2365 + else if (stateSet == false) {
2366 + if ((notify & 256) != 0) {
2367 + obj.msgserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2368 + } else {
2369 + obj.msgserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2370 + }
2371 + }
2372 + }
2373 }
2374 }
2375 }
meshmessaging.js
+206 -1
@@ -122,6 +122,7 @@ module.exports.CreateServer = function (parent) {
122 obj.callMeBotClient = null;
123 obj.pushoverClient = null;
124 obj.zulipClient = null;
125 + const sortCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
126
127 // Telegram client setup
128 if (parent.config.messaging.telegram) {
@@ -347,7 +348,7 @@ module.exports.CreateServer = function (parent) {
348 if (func != null) { func(true); }
349 } else {
350 // No providers found
350 - func(false, "No messaging providers found for this message.");
351 + if (func != null) { func(false, "No messaging providers found for this message."); }
352 }
353 }
354
@@ -436,6 +437,210 @@ module.exports.CreateServer = function (parent) {
437 obj.sendMessage(to, sms, domain, func);
438 };
439
440 + // Send device state change notification
441 + obj.sendDeviceNotify = function (domain, username, to, connections, disconnections, lang) {
442 + if (to == null) return;
443 + parent.debug('email', "Sending device state change message to " + to);
444 +
445 + var sms = [];
446 + if (connections.length > 0) { sms.push('Connections: ' + connections.join(', ')); } // TODO: Translate 'Connections: '
447 + if (disconnections.length > 0) { sms.push('Disconnections: ' + disconnections.join(', ')); } // TODO: Translate 'Disconnections: '
448 + if (sms.length == 0) return;
449 + sms = sms.join(' - ');
450 + if (sms.length > 1000) { sms = sms.substring(0, 997) + '...'; } // Limit messages to 1000 characters
451 +
452 + // Send the message
453 + obj.sendMessage(to, sms, domain, null);
454 + };
455 +
456 +
457 + //
458 + // Device connetion and disconnection notifications
459 + //
460 +
461 + obj.deviceNotifications = {}; // UserId --> { timer, nodes: nodeid --> connectType }
462 +
463 + // A device connected and a user needs to be notified about it.
464 + obj.notifyDeviceConnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
465 + const mesh = parent.webserver.meshes[meshid];
466 + if (mesh == null) return;
467 +
468 + // Add the user and start a timer
469 + if (obj.deviceNotifications[user._id] == null) {
470 + obj.deviceNotifications[user._id] = { nodes: {} };
471 + obj.deviceNotifications[user._id].timer = setTimeout(function () { sendDeviceNotifications(user._id); }, 1 * 60 * 1000); // 1 minute before message is sent
472 + }
473 +
474 + // Add the device
475 + if (obj.deviceNotifications[user._id].nodes[nodeid] == null) {
476 + obj.deviceNotifications[user._id].nodes[nodeid] = { c: connectType }; // This device connection need to be added
477 + } else {
478 + const info = obj.deviceNotifications[user._id].nodes[nodeid];
479 + if ((info.d != null) && ((info.d & connectType) != 0)) {
480 + info.d -= connectType; // This device disconnect cancels out a device connection
481 + if (((info.c == null) || (info.c == 0)) && ((info.d == null) || (info.d == 0))) {
482 + // This device no longer needs a notification
483 + delete obj.deviceNotifications[user._id].nodes[nodeid];
484 + if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
485 + // This user no longer needs a notification
486 + clearTimeout(obj.deviceNotifications[user._id].timer);
487 + delete obj.deviceNotifications[user._id];
488 + }
489 + return;
490 + }
491 + } else {
492 + if (info.c != null) {
493 + info.c |= connectType; // This device disconnect needs to be added
494 + } else {
495 + info.c = connectType; // This device disconnect needs to be added
496 + }
497 + }
498 + }
499 +
500 + // Set the device group name
501 + if ((extraInfo != null) && (extraInfo.name != null)) { obj.deviceNotifications[user._id].nodes[nodeid].nn = extraInfo.name; }
502 + obj.deviceNotifications[user._id].nodes[nodeid].mn = mesh.name;
503 + }
504 +
505 + // Cancel a device disconnect notification
506 + obj.cancelNotifyDeviceDisconnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
507 + const mesh = parent.webserver.meshes[meshid];
508 + if (mesh == null) return;
509 +
510 + if ((obj.deviceNotifications[user._id] != null) && (obj.deviceNotifications[user._id].nodes[nodeid] != null)) {
511 + const info = obj.deviceNotifications[user._id].nodes[nodeid];
512 + if ((info.d != null) && ((info.d & connectType) != 0)) {
513 + info.d -= connectType; // This device disconnect cancels out a device connection
514 + if (((info.c == null) || (info.c == 0)) && ((info.d == null) || (info.d == 0))) {
515 + // This device no longer needs a notification
516 + delete obj.deviceNotifications[user._id].nodes[nodeid];
517 + if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
518 + // This user no longer needs a notification
519 + clearTimeout(obj.deviceNotifications[user._id].timer);
520 + delete obj.deviceNotifications[user._id];
521 + }
522 + }
523 + }
524 + }
525 + }
526 +
527 + // A device disconnected and a user needs to be notified about it.
528 + obj.notifyDeviceDisconnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
529 + const mesh = parent.webserver.meshes[meshid];
530 + if (mesh == null) return;
531 +
532 + // Add the user and start a timer
533 + if (obj.deviceNotifications[user._id] == null) {
534 + obj.deviceNotifications[user._id] = { nodes: {} };
535 + obj.deviceNotifications[user._id].timer = setTimeout(function () { sendDeviceNotifications(user._id); }, 1 * 60 * 1000); // 1 minute before message is sent
536 + }
537 +
538 + // Add the device
539 + if (obj.deviceNotifications[user._id].nodes[nodeid] == null) {
540 + obj.deviceNotifications[user._id].nodes[nodeid] = { d: connectType }; // This device disconnect need to be added
541 + } else {
542 + const info = obj.deviceNotifications[user._id].nodes[nodeid];
543 + if ((info.c != null) && ((info.c & connectType) != 0)) {
544 + info.c -= connectType; // This device disconnect cancels out a device connection
545 + if (((info.d == null) || (info.d == 0)) && ((info.c == null) || (info.c == 0))) {
546 + // This device no longer needs a notification
547 + delete obj.deviceNotifications[user._id].nodes[nodeid];
548 + if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
549 + // This user no longer needs a notification
550 + clearTimeout(obj.deviceNotifications[user._id].timer);
551 + delete obj.deviceNotifications[user._id];
552 + }
553 + return;
554 + }
555 + } else {
556 + if (info.d != null) {
557 + info.d |= connectType; // This device disconnect needs to be added
558 + } else {
559 + info.d = connectType; // This device disconnect needs to be added
560 + }
561 + }
562 + }
563 +
564 + // Set the device group name
565 + if ((extraInfo != null) && (extraInfo.name != null)) { obj.deviceNotifications[user._id].nodes[nodeid].nn = extraInfo.name; }
566 + obj.deviceNotifications[user._id].nodes[nodeid].mn = mesh.name;
567 + }
568 +
569 + // Cancel a device connect notification
570 + obj.cancelNotifyDeviceConnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
571 + const mesh = parent.webserver.meshes[meshid];
572 + if (mesh == null) return;
573 +
574 + if ((obj.deviceNotifications[user._id] != null) && (obj.deviceNotifications[user._id].nodes[nodeid] != null)) {
575 + const info = obj.deviceNotifications[user._id].nodes[nodeid];
576 + if ((info.c != null) && ((info.c & connectType) != 0)) {
577 + info.c -= connectType; // This device disconnect cancels out a device connection
578 + if (((info.d == null) || (info.d == 0)) && ((info.c == null) || (info.c == 0))) {
579 + // This device no longer needs a notification
580 + delete obj.deviceNotifications[user._id].nodes[nodeid];
581 + if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
582 + // This user no longer needs a notification
583 + clearTimeout(obj.deviceNotifications[user._id].timer);
584 + delete obj.deviceNotifications[user._id];
585 + }
586 + }
587 + }
588 + }
589 + }
590 +
591 + // Send a notification about device connections and disconnections to a user
592 + function sendDeviceNotifications(userid) {
593 + if (obj.deviceNotifications[userid] == null) return;
594 + clearTimeout(obj.deviceNotifications[userid].timer);
595 +
596 + var connections = [];
597 + var disconnections = [];
598 +
599 + for (var nodeid in obj.deviceNotifications[userid].nodes) {
600 + var info = obj.deviceNotifications[userid].nodes[nodeid];
601 + if ((info.c != null) && (info.c > 0) && (info.nn != null) && (info.mn != null)) {
602 + /*
603 + var c = [];
604 + if (info.c & 1) { c.push("Agent"); }
605 + if (info.c & 2) { c.push("CIRA"); }
606 + if (info.c & 4) { c.push("AMT"); }
607 + if (info.c & 8) { c.push("AMT-Relay"); }
608 + if (info.c & 16) { c.push("MQTT"); }
609 + connections.push(info.mn + ', ' + info.nn + ': ' + c.join(', '));
610 + */
611 + if (info.c & 1) { connections.push(info.nn); }
612 + }
613 + if ((info.d != null) && (info.d > 0) && (info.nn != null) && (info.mn != null)) {
614 + /*
615 + var d = [];
616 + if (info.d & 1) { d.push("Agent"); }
617 + if (info.d & 2) { d.push("CIRA"); }
618 + if (info.d & 4) { d.push("AMT"); }
619 + if (info.d & 8) { d.push("AMT-Relay"); }
620 + if (info.d & 16) { d.push("MQTT"); }
621 + disconnections.push(info.mn + ', ' + info.nn + ': ' + d.join(', '));
622 + */
623 + if (info.d & 1) { disconnections.push(info.nn); }
624 + }
625 + }
626 +
627 + // Sort the notifications
628 + connections.sort(sortCollator.compare);
629 + disconnections.sort(sortCollator.compare);
630 +
631 + // Get the user and domain
632 + const user = parent.webserver.users[userid];
633 + if ((user == null) || (user.email == null) || (user.emailVerified !== true)) return;
634 + const domain = obj.parent.config.domains[user.domain];
635 + if (domain == null) return;
636 +
637 + // Send the message
638 + obj.sendDeviceNotify(domain, user.name, user.msghandle, connections, disconnections, user.llang);
639 +
640 + // Clean up
641 + delete obj.deviceNotifications[userid];
642 + }
643 +
644 return obj;
645 };
646
meshuser.js
+36 -13
@@ -1717,12 +1717,15 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
1717 {
1718 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
1719
1720 - // 2 = WebPage device connections
1721 - // 4 = WebPage device disconnections
1722 - // 8 = WebPage device desktop and serial events
1723 - // 16 = Email device connections
1724 - // 32 = Email device disconnections
1725 - // 64 = Email device help request
1720 + // 2 = WebPage device connections
1721 + // 4 = WebPage device disconnections
1722 + // 8 = WebPage device desktop and serial events
1723 + // 16 = Email device connections
1724 + // 32 = Email device disconnections
1725 + // 64 = Email device help request
1726 + // 128 = Messaging device connections
1727 + // 256 = Messaging device disconnections
1728 + // 512 = Messaging device help request
1729
1730 var err = null;
1731 try {
@@ -1767,12 +1770,15 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
1770 {
1771 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
1772
1770 - // 2 = WebPage device connections
1771 - // 4 = WebPage device disconnections
1772 - // 8 = WebPage device desktop and serial events
1773 - // 16 = Email device connections
1774 - // 32 = Email device disconnections
1775 - // 64 = Email device help request
1773 + // 2 = WebPage device connections
1774 + // 4 = WebPage device disconnections
1775 + // 8 = WebPage device desktop and serial events
1776 + // 16 = Email device connections
1777 + // 32 = Email device disconnections
1778 + // 64 = Email device help request
1779 + // 128 = Messaging device connections
1780 + // 256 = Messaging device disconnections
1781 + // 512 = Messaging device help request
1782
1783 var err = null;
1784 try {
@@ -5312,6 +5318,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
5318 'dupagents': [serverUserCommandDupAgents, ""],
5319 'email': [serverUserCommandEmail, ""],
5320 'emailnotifications': [serverUserCommandEmailNotifications, ""],
5321 + 'msgnotifications': [serverUserCommandMessageNotifications, ""],
5322 'firebase': [serverUserCommandFirebase, ""],
5323 'heapdump': [serverUserCommandHeapDump, ""],
5324 'heapdump2': [serverUserCommandHeapDump2, ""],
@@ -6892,7 +6899,23 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
6899 x += ' ' + info.mn + ', ' + info.nn + ', c:' + (info.c ? info.c : 0) + ', d:' + (info.d ? info.d : 0) + '\r\n';
6900 }
6901 }
6895 - cmdData.result = ((x == '')?'None':x);
6902 + cmdData.result = ((x == '') ? 'None' : x);
6903 + }
6904 + }
6905 +
6906 + function serverUserCommandMessageNotifications(cmdData) {
6907 + if (parent.parent.msgserver == null) {
6908 + cmdData.result = "No messaging service enabled.";
6909 + } else {
6910 + var x = '';
6911 + for (var userid in parent.parent.msgserver.deviceNotifications) {
6912 + x += userid + '\r\n';
6913 + for (var nodeid in parent.parent.msgserver.deviceNotifications[userid].nodes) {
6914 + const info = parent.parent.msgserver.deviceNotifications[userid].nodes[nodeid];
6915 + x += ' ' + info.mn + ', ' + info.nn + ', c:' + (info.c ? info.c : 0) + ', d:' + (info.d ? info.d : 0) + '\r\n';
6916 + }
6917 + }
6918 + cmdData.result = ((x == '') ? 'None' : x);
6919 }
6920 }
6921