├── img └── jabberPresence.png ├── public └── home │ ├── favicon.ico │ ├── static │ └── css │ │ ├── main.65027555.css │ │ └── main.65027555.css.map │ ├── asset-manifest.json │ ├── manifest.json │ ├── index.html │ └── service-worker.js ├── users ├── user.js ├── verifyToken.js ├── authController.js └── userController.js ├── space └── space.js ├── .eslintrc.json ├── endpoints ├── endpoints.js └── endpointController.js ├── model ├── db.js ├── space.js ├── appController.js └── cart.js ├── myutils ├── changelog.js ├── tpxml.js ├── pollCUCM.js ├── excel.js ├── myutils.js ├── help.js ├── ping.js ├── tpXapi.js └── netTools.js ├── gulpfile.js ├── LICENSE.md ├── endpointScripts ├── AutoDialByTOD.js ├── dialpad Macro.js ├── SpeakerTrackDiagnosticMode.js ├── RoomAvailability Macro.js ├── proximityControl.js └── in-room-controls.xml ├── .gitignore ├── certs ├── cert.pem └── key.pem ├── package.json ├── svrConfig └── logger.js ├── status └── statusController.js ├── broadcast └── broadcastController.js ├── server.js ├── flintConversations ├── conversationNewCart.js ├── conversations.js └── conversationFunctions.js ├── flintServer └── flintConfig.js └── README.md /img/jabberPresence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voipnorm/telehealthPresence/HEAD/img/jabberPresence.png -------------------------------------------------------------------------------- /public/home/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voipnorm/telehealthPresence/HEAD/public/home/favicon.ico -------------------------------------------------------------------------------- /public/home/static/css/main.65027555.css: -------------------------------------------------------------------------------- 1 | body{margin:0;padding:0;font-family:sans-serif} 2 | /*# sourceMappingURL=main.65027555.css.map*/ -------------------------------------------------------------------------------- /public/home/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "main.css": "static/css/main.65027555.css", 3 | "main.css.map": "static/css/main.65027555.css.map", 4 | "main.js": "static/js/main.ca670417.js", 5 | "main.js.map": "static/js/main.ca670417.js.map" 6 | } -------------------------------------------------------------------------------- /users/user.js: -------------------------------------------------------------------------------- 1 | //user DB schema 2 | 3 | var mongoose = require('mongoose'); 4 | 5 | var UserSchema = new mongoose.Schema({ 6 | name: String, 7 | email: String, 8 | password: String 9 | }); 10 | 11 | mongoose.model('User', UserSchema); 12 | 13 | module.exports = mongoose.model('User'); 14 | -------------------------------------------------------------------------------- /public/home/static/css/main.65027555.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["index.css"],"names":[],"mappings":"AAAA,KACE,SACA,UACA,sBAAwB","file":"static/css/main.65027555.css","sourcesContent":["body {\n margin: 0;\n padding: 0;\n font-family: sans-serif;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/index.css"],"sourceRoot":""} -------------------------------------------------------------------------------- /space/space.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var SpaceSchema = new mongoose.Schema({ 3 | "spaceId" : String, 4 | "spaceActive" : String, 5 | "setup" : String, 6 | "dnsServer" : String, 7 | "conversationState" : {"conversation": String}, 8 | "webUrls": String 9 | 10 | }); 11 | mongoose.model('Space', SpaceSchema); 12 | 13 | module.exports = mongoose.model('Space'); -------------------------------------------------------------------------------- /public/home/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:import/errors", 6 | "plugin:import/warnings" 7 | ], 8 | "parserOptions": { 9 | "ecmaVersion": 7, 10 | "sourceType": "module" 11 | }, 12 | "env": { 13 | "browser": true, 14 | "node": true, 15 | "mocha": true 16 | }, 17 | "rules": { 18 | "no-console": 0 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /endpoints/endpoints.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var EndpointSchema = new mongoose.Schema({ 3 | cartName: String, 4 | xmppJID: String, 5 | xmppServer: String, 6 | cartIP: String, 7 | mac: String, 8 | peopleTest: String, 9 | location: String, 10 | version: String, 11 | 12 | }); 13 | mongoose.model('Endpoint', EndpointSchema); 14 | 15 | module.exports = mongoose.model('Endpoint'); 16 | -------------------------------------------------------------------------------- /model/db.js: -------------------------------------------------------------------------------- 1 | var log = require('../svrConfig/logger'); 2 | var myutils = require('../myutils/myutils'); 3 | var mongoose = require('mongoose'); 4 | var db = mongoose.connection; 5 | mongoose.connect('mongodb://localhost/presenceDB'); 6 | 7 | 8 | db.on('error', function(err){ 9 | log.error("DB Connection Failure: "+err); 10 | myutils.sparkPost("DB Connection Failure: "+err, process.env.SPARK_ROOM_ID) 11 | }); 12 | db.once('open', function() { 13 | log.info('Successfully connected to MongoDB!!'); 14 | }); -------------------------------------------------------------------------------- /public/home/index.html: -------------------------------------------------------------------------------- 1 | React App
-------------------------------------------------------------------------------- /myutils/changelog.js: -------------------------------------------------------------------------------- 1 | var changeLog = 2 | "1-Jan-2018 App is created.
"+ 3 | "26-June-2018 API with JOSN Web Tokens.
"+ 4 | "26-June-2018 Mongo DB support.
"+ 5 | "26-June-2018 Webex Teams Chat bot support.
"+ 6 | "26-June-2018 Support for People presence and People count.
"+ 7 | "26-June-2018 CSV upload via chatbot support.
"+ 8 | "26-June-2018 Web admin interface.
"+ 9 | "end." 10 | 11 | exports.printChangeLog = function(callback){ 12 | callback(changeLog) 13 | }; 14 | 15 | var roadMap = 16 | "2018 - Undefined "; 17 | 18 | exports.printRoadmap = function(callback){ 19 | callback(roadMap) 20 | }; -------------------------------------------------------------------------------- /myutils/tpxml.js: -------------------------------------------------------------------------------- 1 | //communication with video TP units though XML API. 2 | const log = require('../svrConfig/logger'); 3 | const jsxapi = require('jsxapi'); 4 | 5 | exports.broadcastMessage = function(cart, title, text , duration, cb) { 6 | const xapi = jsxapi.connect('ssh://'+cart.ipAddress, { 7 | username: cart.username, 8 | password: cart.password 9 | }); 10 | xapi.on('error', (err) => { 11 | return log.error(err); 12 | 13 | }); 14 | return xapi.command('UserInterface Message Alert Display', { 15 | Title: title, 16 | Text: text, 17 | Duration: duration, 18 | }) 19 | .then(()=> { 20 | log.info("xapi session closed."); 21 | return xapi.close(); 22 | }) 23 | .catch(err=> cb(err, null)); 24 | }; 25 | -------------------------------------------------------------------------------- /users/verifyToken.js: -------------------------------------------------------------------------------- 1 | //verify tokens being processed by API 2 | 3 | var jwt = require('jsonwebtoken'); 4 | var secret = process.env.SECRETTOKEN; 5 | var log = require('../svrConfig/logger'); 6 | 7 | function verifyToken(req, res, next) { 8 | log.info("Header Token "+req.headers['x-access-token']); 9 | var token = req.headers['x-access-token']; 10 | if (!token) 11 | return res.status(403).send({ auth: false, message: 'No token provided.' }); 12 | jwt.verify(token, secret, function(err, decoded) { 13 | if (err) 14 | return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' }); 15 | // if everything good, save to request for use in other routes 16 | req.userId = decoded.id; 17 | next(); 18 | }); 19 | } 20 | 21 | module.exports = verifyToken; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var nodemon = require('gulp-nodemon'); 3 | var gulpMocha = require('gulp-mocha'); 4 | var eslint = require('gulp-eslint'); 5 | var env = require('gulp-env'); 6 | 7 | gulp.task('default',['lint','test'], function(){ 8 | nodemon({ 9 | exec: 'DEBUG=flint*,sparky* node', 10 | script: 'server.js', 11 | ext: 'js', 12 | env: { 13 | PORT: 8080 14 | }, 15 | ignore: ['./node_modules/'] 16 | }) 17 | .on('restart',['test'], function(){ 18 | console.log('We have restarted'); 19 | }) 20 | }); 21 | 22 | gulp.task('test', function(){ 23 | env({vars:{ENV:'Test'}}); 24 | gulp.src(['Tests/singleFunctionTest.js','Tests/cityUpdateTest.js','Tests/activeUpdateTest.js'/*,'Tests/unitUpdateTest.js'*/]) 25 | .pipe(gulpMocha({reporter: 'nyan'})); 26 | }); 27 | 28 | gulp.task('lint', function () { 29 | return gulp.src(['**/*.js','!node_modules/**','!code graveyard/**','!Tests/**','!public/**']) 30 | .pipe(eslint()) 31 | .pipe(eslint.format()); 32 | }); 33 | 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /endpointScripts/AutoDialByTOD.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wakes the system up from standby at 8.00 in the morning 3 | * on weekdays and auto dials another endpoint. Other endpoint needs to be setup to Auto Answer. 4 | */ 5 | 6 | // library for communicating with video system 7 | const xapi = require('xapi'); 8 | 9 | // how often to check time 10 | const intervalSec = 60; 11 | 12 | // Standard javascript built-ins such as date and timers are included 13 | function checkTime() { 14 | const now = new Date(); 15 | const weekday = now.getDay() > 0 && now.getDay() < 6; 16 | const wakeupNow = now.getHours() === 8 && now.getMinutes() < 2 && weekday; 17 | const sleepNow = now.getHours() === 17 && now.getMinutes() < 2 && weekday; 18 | 19 | if (wakeupNow) {xapi.command('standby deactivate'); 20 | xapi.command("dial", {Number:"OTHERENDPOINT@YOURDOMAIN.COM"}); 21 | console.log("Call Connecting"); 22 | } 23 | 24 | if(sleepNow){ 25 | xapi.command("call disconnect"); 26 | xapi.command('standby activate'); 27 | console.log("Call disconnecting"); 28 | } 29 | 30 | } 31 | 32 | setInterval(checkTime, intervalSec * 1000); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cloud 9 files 2 | .c9/ 3 | # Space date 4 | oldCode/space.json 5 | cart.json 6 | #client websocket file 7 | device.json 8 | #access log 9 | access.log 10 | 11 | certs 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Typescript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | -------------------------------------------------------------------------------- /certs/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID2zCCAsOgAwIBAgIJAM3n4ZcnPnYUMA0GCSqGSIb3DQEBCwUAMIGDMQswCQYD 3 | VQQGEwJVUzEPMA0GA1UECAwGUmVudG9uMQ8wDQYDVQQHDAZSZW50b24xDTALBgNV 4 | BAoMBGhvbWUxDDAKBgNVBAsMA2hvbTESMBAGA1UEAwwJbG9jYWxob3N0MSEwHwYJ 5 | KoZIhvcNAQkBFhJjaHJpc3Rub0BjaXNjby5jb20wHhcNMTgwNTI0MjE1MTQzWhcN 6 | MjEwMjE2MjE1MTQzWjCBgzELMAkGA1UEBhMCVVMxDzANBgNVBAgMBlJlbnRvbjEP 7 | MA0GA1UEBwwGUmVudG9uMQ0wCwYDVQQKDARob21lMQwwCgYDVQQLDANob20xEjAQ 8 | BgNVBAMMCWxvY2FsaG9zdDEhMB8GCSqGSIb3DQEJARYSY2hyaXN0bm9AY2lzY28u 9 | Y29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwQHZSlwL19Wia/OR 10 | CHtnKIWBha0UhFfXlxi1pCs9XB4FT8v/XamZZ2erPoDXwiGx2npphQWkNFkLLfR5 11 | q2dxy1F/7ar1dbeBaG5OkBgGrhlKLMreWODlhHx0SHFtvlFkfFS1M+ti07Fb9yWx 12 | JAJgW3LjthUIDkMTYMwm6952FT0EzULK54Ra+ttF8/hfJoM0WdXn7CzjWnTdy3LL 13 | oOWS2oGiub31I5h3/gGQ3dieoffv0z0UF6/gE4COInVgFwI5H2pYW4VRcJA9hAzY 14 | cz5A2f8PKtd7BY6//6cnYckaq/M9wmhoAhFE3zhaESTrBJfo6c9jitmmxGFtWoTi 15 | xo/W3QIDAQABo1AwTjAdBgNVHQ4EFgQU3DCh164ASer0z75481YKShOeiGkwHwYD 16 | VR0jBBgwFoAU3DCh164ASer0z75481YKShOeiGkwDAYDVR0TBAUwAwEB/zANBgkq 17 | hkiG9w0BAQsFAAOCAQEAE07lgvqC0fhuhrm5cwO8M9B6JFqrKjkPzhrPWjEk99ca 18 | CYrpsxCssWDJgBwwFCtsrb/wXl1riDWfpyr6BYSVoQQWleq/LLfXcFD3EwHS7qwc 19 | yqXqbm7JsxWntudSoL8H/gF93ZzQnvE9wyyJ2E1GKVBfdhQoeZlqvPWeWw7i3cAm 20 | M2/NPFvUkkYRXNeG3A5isZPRlbvC/Pshz5Eo43lq6BtNmO/TT8mTddKSf4pfPM5b 21 | AmmHSkNwNro+E6lCxiw0eRdaL86ZZaKS/lTRaAw2FRO7xzCNs98ahrtBuVfhlx7j 22 | nC3qbcM1vy3KzYfof4DenADiYNov/LDKbx1NAc2bvA== 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /endpointScripts/dialpad Macro.js: -------------------------------------------------------------------------------- 1 | const xapi = require('xapi'); 2 | 3 | var did = ""; 4 | function listenToGui() { 5 | xapi.event.on('UserInterface Extensions Widget Action', (event) => { 6 | 7 | if (event.Type === 'pressed') { 8 | console.log('Unknown button pressed', event); 9 | if(event.WidgetId === "dialButton"){ 10 | xapi.command("dial", {Number:did}); 11 | did=""; 12 | textBoxUpdate(did); 13 | }else if(event.WidgetId==="backSpace"){ 14 | backSpaceString(did); 15 | }else{ 16 | const newdigit = event.Value; 17 | did = did+newdigit; 18 | console.log("DID new Value :"+did); 19 | textBoxUpdate(did); 20 | resetButton(event.WidgetId); 21 | } 22 | } 23 | if(event.Type === "released"){ 24 | resetButton(event.WidgetId); 25 | console.log("release triggered"); 26 | } 27 | }); 28 | } 29 | function resetButton(widgetID){ 30 | xapi.command('UserInterface Extensions Widget UnsetValue', { 31 | WidgetId: widgetID, 32 | }); 33 | 34 | console.log("button reset done"); 35 | } 36 | function backSpaceString(string){ 37 | did = string.slice(0, -1); 38 | textBoxUpdate(did); 39 | } 40 | 41 | function textBoxUpdate(stringValue){ 42 | xapi.command('UserInterface Extensions Widget SetValue', { 43 | WidgetId: "textBox", 44 | Value: stringValue, 45 | }); 46 | } 47 | 48 | listenToGui(); -------------------------------------------------------------------------------- /myutils/pollCUCM.js: -------------------------------------------------------------------------------- 1 | 2 | require('dotenv').config(); 3 | const util = require('util'); 4 | const EventEmitter = require('events').EventEmitter; 5 | const request = require('request'); 6 | const log = require('../svrConfig/logger'); 7 | const ris = require('cucm-risdevice-query').RisQuery; 8 | const cucmIp = process.env.CUCMIPADDRESS; 9 | const cucmAdmin = process.env.CUCMPRESENCEACCOUNT; 10 | const cucmPwd = process.env.CUCMPRESENCEPWD; 11 | 12 | function CUCMPoll(mac){ 13 | this.mac = mac; 14 | } 15 | util.inherits(CUCMPoll,EventEmitter); 16 | 17 | CUCMPoll.prototype.checkIP = function(callback){ 18 | var self = this; 19 | const devices = [this.mac]; 20 | const risReqXml = ris.createRisDoc({ 21 | version: process.env.CUCMVERSION, 22 | query: devices 23 | }); 24 | log.info('Processing Mac: '+JSON.stringify(risReqXml)); 25 | const url = `https://${cucmIp}:8443` + ris.risPath; 26 | request({ 27 | url: url, 28 | method: 'POST', 29 | body: risReqXml, 30 | headers: { 31 | 'Content-Type': 'text/xml' 32 | }, 33 | auth: { 34 | username: cucmAdmin, 35 | password: cucmPwd 36 | }, 37 | strictSSL: false 38 | }, (err, resp, body) => { 39 | if(err) return log.error("CUCM Error : "+err); 40 | log.info(JSON.stringify(body)) 41 | const parsedResponse = ris.parseResponse(body); 42 | log.info(JSON.stringify(parsedResponse)); 43 | return callback(parsedResponse); 44 | }); 45 | }; 46 | 47 | 48 | module.exports = CUCMPoll; 49 | -------------------------------------------------------------------------------- /endpointScripts/SpeakerTrackDiagnosticMode.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Macro companion to the SpeakerTrack Diagnostic mode 4 | */ 5 | 6 | const xapi = require('xapi'); 7 | const stdID = 'STDOnOff'; 8 | let stdStatus = "Stop"; 9 | 10 | function listenToGui() { 11 | xapi.event.on('UserInterface Extensions Widget Action', (event) => { 12 | //console.log(event); 13 | if (event.WidgetId === stdID) { 14 | //console.log('Unknown togglebutton', event); 15 | if(event.Value === "on"){ 16 | stdStatus="Start"; 17 | textBoxUpdate(stdStatus); 18 | setSTD(stdStatus); 19 | 20 | }else if(event.Value === "off"){ 21 | stdStatus="Stop"; 22 | textBoxUpdate(stdStatus); 23 | setSTD(stdStatus); 24 | 25 | }else{ 26 | console.log("Macro error"); 27 | } 28 | } 29 | 30 | }); 31 | } 32 | 33 | function setSTD(status){ 34 | console.log(status); 35 | if(status==="Start"){ 36 | xapi.command('Cameras SpeakerTrack Diagnostics Start') 37 | .catch((error) => { console.error(error); }); 38 | }else{ 39 | xapi.command('Cameras SpeakerTrack Diagnostics Stop') 40 | .catch((error) => { console.error(error); }); 41 | } 42 | 43 | } 44 | 45 | function textBoxUpdate(stringValue){ 46 | xapi.command('UserInterface Extensions Widget SetValue', { 47 | WidgetId: "textBoxSTD", 48 | Value: "Speaker Track Diagnostics Mode: "+stringValue, 49 | }); 50 | } 51 | 52 | 53 | 54 | listenToGui(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node_mon", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "author": "Chris Norman", 7 | "dependencies": { 8 | "apiai": "^4.0.3", 9 | "assert": "^1.4.1", 10 | "async": "~0.2.8", 11 | "bcryptjs": "^2.4.3", 12 | "bluebird": "^3.5.3", 13 | "body-parser": "^1.18.3", 14 | "bottleneck": "^1.15.1", 15 | "ciscospark-websocket-events": "^1.2.0", 16 | "cors": "^2.8.5", 17 | "cucm-risdevice-query": "^1.1.1", 18 | "debug": "^2.2.0", 19 | "dotenv": "^4.0.0", 20 | "exceljs": "^0.8.5", 21 | "express": "^4.16.4", 22 | "express-range": "^2.0.1", 23 | "fwsp-hydra-express": "^1.0.0", 24 | "http-server": "^0.9.0", 25 | "hydra-express": "^1.7.0", 26 | "jsonwebtoken": "^8.4.0", 27 | "jsxapi": "^4.2.0", 28 | "lodash": "^4.17.11", 29 | "mongoose": "^5.3.12", 30 | "node-flint": "^4.7.0", 31 | "node-schedule": "^1.2.5", 32 | "node-sparkclient": "^0.1.4", 33 | "nodemon": "^1.18.6", 34 | "portscanner": "^2.2.0", 35 | "prettyjson": "^1.2.1", 36 | "react-admin": "^2.4.2", 37 | "request": "^2.88.0", 38 | "simple-xmpp": "^1.3.0", 39 | "tz-lookup": "^6.1.8", 40 | "valid-url": "^1.0.9", 41 | "winston": "^3.1.0" 42 | }, 43 | "devDependencies": { 44 | "eslint": "^4.19.1", 45 | "eslint-plugin-import": "^2.14.0", 46 | "gulp": "^3.9.1", 47 | "gulp-env": "^0.4.0", 48 | "gulp-eslint": "^4.0.0", 49 | "gulp-mocha": "^4.3.1", 50 | "gulp-nodemon": "^2.4.1", 51 | "should": "^13.2.3", 52 | "supertest": "^3.3.0" 53 | }, 54 | "license": "ISC" 55 | } 56 | -------------------------------------------------------------------------------- /svrConfig/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | //Logging configuration 3 | const winston = require('winston'); 4 | const env = process.env.NODE_ENV; 5 | const {createLogger, format, transports} = require('winston'); 6 | const {combine, timestamp, label, printf, colorize} = format; 7 | 8 | 9 | const myFormat = printf(info => { 10 | return `${info.timestamp} ${info.level}: ${info.message}`; 11 | }); 12 | 13 | 14 | const logger = 15 | winston.createLogger({ 16 | format: combine( 17 | format.splat(), 18 | colorize({ all: true }), 19 | timestamp(), 20 | myFormat 21 | ), 22 | transports: [ 23 | // 24 | // - Write to all logs with level `info` and below to `combined.log` 25 | // - Write all logs error (and below) to `error.log`. 26 | // 27 | new winston.transports.File({ filename: './logs/error.log', level: 'error' }), 28 | new winston.transports.File({ filename: './logs/combined.log' }) 29 | ], 30 | exceptionHandlers: [ 31 | new winston.transports.File( { 32 | filename: 'logs/exceptions.log' 33 | } ), 34 | new winston.transports.Console( { 35 | colorize: true 36 | } ), 37 | ] 38 | }); 39 | 40 | // 41 | // If we're not in production then log to the `console` with the format: 42 | // `${info.level}: ${info.message} JSON.stringify({ ...rest }) ` 43 | // 44 | if (env !== 'production') { 45 | logger.add(new winston.transports.Console()); 46 | } 47 | 48 | //logger.info('Hello, this is a logging event with a custom pretty print', { 'foo': 'bar' }); 49 | //logger.info('Hello, this is a logging event with a custom pretty print2', { 'foo': 'bar' }); 50 | 51 | module.exports = logger; 52 | 53 | -------------------------------------------------------------------------------- /status/statusController.js: -------------------------------------------------------------------------------- 1 | //rest api for admin interface or general use 2 | var crud = require('../model/appController'); 3 | var log = require('../svrConfig/logger'); 4 | var carts = crud.cartDataObj; 5 | var range = require('express-range'); 6 | var VerifyToken = require('../users/VerifyToken'); 7 | var express = require('express'); 8 | var router = express.Router(); 9 | var bodyParser = require('body-parser'); 10 | var cors = require('cors'); 11 | router.use(bodyParser.urlencoded({ extended: false })); 12 | router.use(bodyParser.json()); 13 | router.use(range({ 14 | accept: 'endpoints', 15 | limit: 10, 16 | })); 17 | router.use(cors({ 18 | origin: '*', 19 | credentials: false, 20 | exposedHeaders: 'content-range', 21 | 22 | })); 23 | 24 | //get status of all endpoints 25 | router.get("/",VerifyToken, function(req, res) { 26 | let endpoints = []; 27 | let num = carts.length; 28 | res.header("Access-Control-Allow-Origin", "*"); 29 | log.info("Carts API request : "+carts.length); 30 | if (isFinite(num) && num > 0) { 31 | for (var i = 0; i <= num - 1; i++) { 32 | endpoints.push({ 33 | _id: i, 34 | cartName: carts[i].cartName, 35 | cartIP: carts[i].cartIP, 36 | status: carts[i].cartStatus, 37 | location: carts[i].location, 38 | version: carts[i].version 39 | }); 40 | } 41 | res.range({ 42 | first: req.range.first, 43 | last: req.range.last, 44 | length: endpoints.length 45 | }); 46 | res.json({data:endpoints.slice(req.range.first, req.range.last + 1),total:endpoints.length}); 47 | 48 | }else { 49 | res.status(400).send({ message: 'invalid number supplied' }); 50 | } 51 | }); 52 | 53 | module.exports = router; -------------------------------------------------------------------------------- /certs/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIHx5wltr22M0CAggA 3 | MBQGCCqGSIb3DQMHBAgmKaS79F6tvgSCBMiY1waLjBefyIV5L5E3HXbrl0RoUxbS 4 | amKVZyAluJd/dGhTz79Z+Z/NYSVgt3nxCA3s+tpVewmm/qgAYR8//esoFdfRVCvy 5 | x+rlygrnI9hvEu2V1emCZYNwBv8vhs6YWQ+gZRR2wtx5v/jESYHm55DS/2lkeEC/ 6 | qH8zkPepIq/5zWrOeUQKqjr/b5AtMW5lYEdvKYRByku+g4ahzzQqAiQoHC88CQg7 7 | JnFJuXpxfwcteT1OpuA8+m5imHLfP6jqFjzuQOFREtyIU3aEHwQRSnAxlI1qZgqo 8 | DKJoVi/uzjC4OwGQcw1woeo5Sp3wBg5GmXi+SUaVqDh2hu4jCDdpl3u0oZTi8Hqr 9 | GgY8p/AfRb+GuB2afPspa2v127rYLO7jK/+OUWjzJMUX50KFXsXSCczmTMoyyS7A 10 | FCW+kCoHrVujbPhnuOZwkrVx3y8OdzjAUw1JUnEQwKLq/RZfAdLMY2ARmikPw/EG 11 | ccQpALF63Ng++y5Q9JFDZc2tTG7+/pi72zzzMFP6A/cm+n1hNn61H2VMq7c77HCP 12 | 6mUFL3X2kGH1UndX83hoix4Oz6OcXwXSISagdeYHzdNVffFArv5eAFzukA85QN8X 13 | jDm+vCC4sQXCk7JlzClb3RpE5FT+5cS1r3DToj6vKJ4VK6ZbpVSDyw/aurryEiUo 14 | wiAqtqhYfXqGd7f8D52YkQAb3n9r/B9cIkZUUEnlwJhZ9xjWyb+xONbGx3CpFz7D 15 | hFKaPSzmdqcyFwAb7nnOM2tCFgtzCg3scSl8CdfaU49nf1CI+nOFxqCZedg8N2np 16 | 2+dfrArHH70tHeHWL/b/4nhEC8h8+6ZIPhh1tVbi0WhqBqkeRBXbB50SGI9K2JSH 17 | ZA+bubBPf9BIep2Pd50HNmyXRUH3NXUuyKt1set4pNIppsbv7xRJ11JKSlRwixf7 18 | 4OOKC6Li2TX2QaOGud9jP8Kd4AwtU11tanwRDZOE2YVNssz219g69hoWXCrVzANP 19 | 5e03GIPeoRUugQOXbRnpds7nC6KgHUVNwX5OqX8QwM94xSiO8TGAYtWJUcCLo8DM 20 | 1JKFoY7sGrEENsx6U/LQLfpyNiUeGeECee67YX2hh4G5Rj2m3Ognozh9JtejAd6M 21 | GenBNIJePtdalIVLUc4fHzSJ7GinKXniQIFiojLUJFqI4DYVHXFLqM03JTrNnK6z 22 | 7jZAx8ip/8dgEe+/bzBU26SXFYV8FqufHlBUuKRMlPD5RQuL+djLrQYDyVoFQp45 23 | EPDEgqN0k7++Q47tdvcxBdcret0EU4h8HQ8fXvq4vZIAbIKAEoYAGc/wXVHdb3ag 24 | EWUvIZGHebZBs9agyu/gvXM/M8GwLscU/OLmij7Fr3hM1isu1aBcgFsDd7EDZusE 25 | DIT5Lq/dppl3wAAZO2DY7QVfBsiXVU+Wh1b+D89GkX1UPT7GosEjerg3tIPXA7fE 26 | s80F2jeeMyo0aMgQCV+SjLBq72HcHqOc15cd6rIEz/DVUYxyMloMlO8H5oaGBsa+ 27 | 2TBpsQAC0pM7wgkJBU9IB523S6iVvknlyC35bs2TRXfC9q2yLgMlFb6hH19IlVvL 28 | g7zsiruOfQtkDCNv3vKek2ZZzTKbddE6YVBMwZVquEG7TFKf+34iHhX+gYVs39Rf 29 | Y0M= 30 | -----END ENCRYPTED PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /model/space.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var crud = require('./appController'); 4 | var log = require('../svrConfig/logger'); 5 | //pass in object versus single values 6 | function Space(data){ 7 | //space ID 8 | this.spaceId = data.spaceId; 9 | //Disable space but not delete on bot removal 10 | this.spaceActive = data.spaceActive; 11 | //Setup currently not in use 12 | this.setup = data.setup; 13 | //setting not in use 14 | this.dnsServer = null; 15 | //setting not in use 16 | this.conversationState = data.conversationState; 17 | } 18 | 19 | util.inherits(Space,EventEmitter); 20 | 21 | Space.prototype.updateConversationState = function(obj){ 22 | var self = this; 23 | self.conversationState = { 24 | conversation: obj.conversation, 25 | state: obj.state, 26 | cartName: null, 27 | ipAddress: null 28 | }; 29 | return self; 30 | }; 31 | 32 | 33 | 34 | Space.prototype.updatednsServer = function(param){ 35 | var self = this; 36 | self.dnsServer = param; 37 | return self; 38 | }; 39 | 40 | Space.prototype.updateActive = function(param){ 41 | var self = this; 42 | self.spaceActive = param; 43 | return self; 44 | }; 45 | 46 | Space.prototype.updateSetup = function(param){ 47 | var self = this; 48 | self.spaceSetup = param; 49 | return self; 50 | }; 51 | Space.prototype.writeToFile = function(){ 52 | var self = this; 53 | crud.writeToJSON(function(){ 54 | log.info("spaceObj.Writing to file......"); 55 | self.emit('writeComplete'); 56 | }); 57 | return; 58 | }; 59 | Space.prototype.writeCartToFile = function(){ 60 | var self = this; 61 | crud.writeCartToJSON(function(){ 62 | log.info("spaceObj.Writing to file......"); 63 | self.emit('writeComplete'); 64 | }); 65 | return; 66 | }; 67 | 68 | module.exports = Space; -------------------------------------------------------------------------------- /broadcast/broadcastController.js: -------------------------------------------------------------------------------- 1 | //API for sending broadcast messages to all endpoints. 2 | 3 | //rest api for admin interface or general use 4 | var crud = require('../model/appController'); 5 | var log = require('../svrConfig/logger'); 6 | var carts = crud.cartDataObj; 7 | var range = require('express-range'); 8 | var VerifyToken = require('../users/VerifyToken'); 9 | var express = require('express'); 10 | var router = express.Router(); 11 | var bodyParser = require('body-parser'); 12 | var cors = require('cors'); 13 | var tpxml = require('../myutils/tpxml'); 14 | var _= require('lodash'); 15 | router.use(bodyParser.urlencoded({ extended: false })); 16 | router.use(bodyParser.json()); 17 | router.use(range({ 18 | accept: 'endpoints', 19 | limit: 10, 20 | })); 21 | router.use(cors({ 22 | origin: '*', 23 | credentials: false, 24 | exposedHeaders: 'content-range', 25 | 26 | })); 27 | 28 | //broadcast message to all endpoints. 29 | router.post("/",VerifyToken, function(req, res) { 30 | 31 | res.header("Access-Control-Allow-Origin", "*"); 32 | log.info("Carts API request : "+carts.length); 33 | _.forEach(crud.cartDataObj, function(cart) { 34 | if(cart.cartStatus === "online"){ 35 | let message = req.body.message; 36 | var cartObj = { 37 | "username":process.env.TPADMIN, 38 | "password":cart.endpointPwd, 39 | "ipAddress":cart.cartIP 40 | }; 41 | tpxml.broadcastMessage(cartObj,"Important",message,"20", function(err, message){ 42 | if(err)log.error("conversationFunction.broadcast: error "+err); 43 | log.info("conversationFunctions.broadcast: broadcast to endpoints success") 44 | }); 45 | }else{ 46 | log.info('Broadcast failed endpoint offline: '+cart.cartIP); 47 | } 48 | }) 49 | return res.status(200).send("Broadcast complete."); 50 | }); 51 | 52 | module.exports = router; -------------------------------------------------------------------------------- /myutils/excel.js: -------------------------------------------------------------------------------- 1 | //module for reading CSV file downloaded from Spark for uploading bulk TP endpoints - needs work on adding validy of CSV format 2 | 3 | var Excel = require('exceljs'); 4 | var fs = require('fs'); 5 | var workbook = new Excel.Workbook(); 6 | var log = require('../svrConfig/logger'); 7 | var crud = require('../model/appController'); 8 | 9 | function readcsv(filename, callback){ 10 | workbook.csv.readFile(filename) 11 | .then(function(worksheet) { 12 | 13 | for(var i = 0; i { 56 | if(err) return log.error("CUCM Error : "+err); 57 | const parsedResponse = ris.parseResponse(body); 58 | log.info(JSON.stringify(parsedResponse)); 59 | return callback(parsedResponse); 60 | }); 61 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | var flint = require('./flintServer/flintConfig'); 3 | var webhook = require('node-flint/webhook'); 4 | var express = require('express'); 5 | var https = require('https'); 6 | var path = require('path'); 7 | var log = require('./svrConfig/logger'); 8 | var crud = require('./model/appController'); 9 | var bodyParser = require('body-parser'); 10 | var db = require('./model/db'); 11 | var AuthController = require('./users/authController'); 12 | var endpointController = require('./endpoints/endpointController'); 13 | var userController = require('./users/userController'); 14 | var statusController = require('./status/statusController'); 15 | var broadcastController = require('./broadcast/broadcastController'); 16 | var fs = require('fs'); 17 | 18 | var app = express(); 19 | 20 | app.use(bodyParser.json()); 21 | 22 | // define express path for incoming webhooks 23 | app.post('/flint',webhook(flint)); 24 | //CORS Configuration 25 | app.use(function(req, res, next) { 26 | res.header("Access-Control-Allow-Origin", "*"); 27 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 28 | next(); 29 | }); 30 | 31 | //rest api configuration 32 | app.use('/api/auth', AuthController); 33 | app.use('/api/endpoints', endpointController); 34 | app.use('/api/users',userController); 35 | app.use('/api/status', statusController); 36 | app.use('/api/broadcast', broadcastController); 37 | //Serve up admin interface 38 | app.use('/', express.static(__dirname + '/public/home', { index: 'index.html' })); 39 | 40 | 41 | var sslOptions = { 42 | key: fs.readFileSync('./certs/key.pem'), 43 | cert: fs.readFileSync('./certs/cert.pem'), 44 | passphrase: process.env.SECRETTOKEN 45 | }; 46 | crud.startUp(); 47 | 48 | https.createServer(sslOptions, app).listen(process.env.SECUREWEBPORT); 49 | 50 | var server = app.listen(process.env.WEBPORT, function () { 51 | log.info('server : Chatbot listening on port %s', process.env.WEBPORT); 52 | }); 53 | 54 | 55 | 56 | // gracefully shutdown (ctrl-c) 57 | process.on('SIGINT', function() { 58 | log.info('server : stoppping...'); 59 | server.close(); 60 | flint.stop().then(function() { 61 | process.exit(); 62 | }); 63 | }); 64 | 65 | module.exports = app; -------------------------------------------------------------------------------- /flintConversations/conversationNewCart.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var log = require('../svrConfig/logger'); 4 | var crud = require('../model/appController'); 5 | //setup conversation file 6 | module.exports = { 7 | //Welcome conversation flow 8 | convo1: function(request, bot, trigger, spData){ 9 | bot.say("Please enter the IP Address of the cart you would like presence updates for."); 10 | spData.conversationState.conversation = "newCart"; 11 | spData.conversationState.state = "1"; 12 | spData.conversationState.cartName = request; 13 | return; 14 | }, 15 | convo2: function(request, bot , trigger, spData){ 16 | bot.say("Please enter the JID address of the cart."); 17 | spData.conversationState.state="2"; 18 | spData.conversationState.ipAddress = request; 19 | return; 20 | }, 21 | convo3: function(request, bot , trigger, spData){ 22 | 23 | spData.conversationState.state="3"; 24 | spData.conversationState.JID = request; 25 | 26 | return bot.say({markdown:"Your setup is nearly complete. If you would like to commit these changes just type **yes**, if not type **no**."}); 27 | }, 28 | convo4: function(request, bot , trigger, spData){ 29 | log.info("conversationSetup.setupConvo4 :"+ JSON.stringify(spData.conversationState)); 30 | 31 | if(request === "yes") { 32 | var cart = 33 | { 34 | cartName:spData.conversationState.cartName, 35 | cartIP: spData.conversationState.ipAddress, 36 | JID: spData.conversationState.JID 37 | }; 38 | crud.createCart(cart, function(){ 39 | spData.updateConversationState({conversation: "commands", state:""}); 40 | return bot.say({markdown: "Thank you, your setup is now complete."}); 41 | }) 42 | }else if(request === "no"){ 43 | spData.updateConversationState({conversation: "commands", state:""}); 44 | return bot.say({markdown: "Your new cart has been cancelled. To start over please enter **/newCart**."}); 45 | }else { 46 | bot.say("Oh no, something went wrong. Lets try this again." + 47 | " Please enter yes or no"); 48 | return; 49 | } 50 | } 51 | }; -------------------------------------------------------------------------------- /endpointScripts/RoomAvailability Macro.js: -------------------------------------------------------------------------------- 1 | const xapi = require('xapi'); 2 | 3 | 4 | var dnd = "Room Available"; 5 | var tpDNDStatus = "Inactive"; 6 | var tpAAStatus = "On"; 7 | 8 | 9 | function listenToGui() { 10 | xapi.status.on('Conference DoNotDisturb', DNDChanged); 11 | xapi.event.on('UserInterface Extensions Widget Action', (event) => { 12 | //console.log(event); 13 | if (event.WidgetId === 'DNDToggle') { 14 | //console.log('Unknown togglebutton', event); 15 | if(event.Value === "on"){ 16 | dnd="Room Occupied"; 17 | tpDNDStatus = "Activate"; 18 | tpAAStatus = "Off"; 19 | 20 | textBoxUpdate(dnd); 21 | setDND(tpDNDStatus); 22 | 23 | 24 | setAutoAnswer(tpAAStatus); 25 | }else if(event.Value === "off"){ 26 | dnd="Room Available"; 27 | textBoxUpdate(dnd); 28 | tpDNDStatus = "Inactive"; 29 | setDND(tpDNDStatus); 30 | tpAAStatus = "On"; 31 | setAutoAnswer(tpAAStatus); 32 | }else{ 33 | console.log("Macro error"); 34 | } 35 | } 36 | 37 | }); 38 | } 39 | 40 | function setDND(status){ 41 | console.log(status); 42 | if(status==="Activate"){ 43 | xapi.command('Conference DoNotDisturb activate') 44 | .catch((error) => { console.error(error); }); 45 | }else{ 46 | xapi.command('Conference DoNotDisturb deactivate') 47 | .catch((error) => { console.error(error); }); 48 | } 49 | 50 | } 51 | function toggleUpdate(status){ 52 | xapi.command('UserInterface Extensions Widget SetValue', { 53 | WidgetId: "DNDToggle", 54 | Value: status, 55 | }); 56 | } 57 | 58 | function setAutoAnswer(status){ 59 | //console.log(status); 60 | xapi.config.set('Conference AutoAnswer Mode', status); 61 | } 62 | 63 | function textBoxUpdate(stringValue){ 64 | xapi.command('UserInterface Extensions Widget SetValue', { 65 | WidgetId: "textBoxDND", 66 | Value: stringValue, 67 | }); 68 | } 69 | 70 | function DNDChanged(status){ 71 | console.log("This is the status: "+status); 72 | if(status === "Active"){ 73 | dnd="Room Occupied"; 74 | tpDNDStatus = "Activate"; 75 | tpAAStatus = "Off"; 76 | toggleUpdate('on'); 77 | textBoxUpdate(dnd); 78 | }else{ 79 | dnd="Room Available"; 80 | tpDNDStatus = "Inactive"; 81 | tpAAStatus = "on"; 82 | toggleUpdate('off'); 83 | textBoxUpdate(dnd) 84 | } 85 | 86 | } 87 | 88 | listenToGui(); -------------------------------------------------------------------------------- /endpointScripts/proximityControl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Macro companion to the Ultrasound Control 3 | * - lets users toggle Proximity Mode to On/Off 4 | * - displays the current MaxVolume level 5 | */ 6 | 7 | const xapi = require('xapi') 8 | const proximityID = 'ProximityOnOff'; 9 | const textBoxId = 'textBoxProximity'; 10 | 11 | // Change proximity mode to "On" or "Off" 12 | function switchProximityMode(mode) { 13 | console.debug(`switching proximity mode to: ${mode}`) 14 | 15 | xapi.config.set('Proximity Mode', mode) 16 | .then(() => { 17 | console.info(`turned proximity mode: ${mode}`) 18 | }) 19 | .catch((err) => { 20 | console.error(`could not turn proximity mode: ${mode} ${err}`) 21 | }) 22 | } 23 | 24 | // React to UI events 25 | function onGui(event) { 26 | // Proximity Mode Switch 27 | if ((event.Type === 'changed') && (event.WidgetId === proximityID)) { 28 | switchProximityMode(event.Value) 29 | return; 30 | } 31 | } 32 | xapi.event.on('UserInterface Extensions Widget Action', onGui); 33 | 34 | 35 | // 36 | // Proximity Services Availability 37 | // 38 | 39 | // Update Toogle if proximity mode changes 40 | function updateProximityToggle(mode) { 41 | console.debug(`switching toggle to ${mode}`) 42 | 43 | xapi.command("UserInterface Extensions Widget SetValue", { 44 | WidgetId: proximityID, 45 | Value: mode 46 | }) 47 | } 48 | xapi.config.on("Proximity Mode", mode => { 49 | console.log(`proximity mode changed to: ${mode}`) 50 | 51 | // Update toggle 52 | // [WORKAROUND] Configuration is On or Off, needs to be turned to lowercase 53 | updateProximityToggle(mode.toLowerCase()); 54 | textBoxUpdate(mode); 55 | }) 56 | 57 | // Refresh Toggle state 58 | function refreshProximityToggle() { 59 | xapi.status.get("Proximity Services Availability") 60 | .then(availability => { 61 | console.debug(`current proximity mode is ${availability}`) 62 | switch (availability) { 63 | case 'Available': 64 | updateProximityToggle('on'); 65 | textBoxUpdate('On'); 66 | return; 67 | 68 | case 'Disabled': 69 | default: 70 | updateProximityToggle('off'); 71 | textBoxUpdate('Off'); 72 | return; 73 | } 74 | }) 75 | .catch((err) => { 76 | console.error(`could not read current proximity mode, err: ${err.message}`) 77 | }) 78 | } 79 | 80 | function textBoxUpdate(stringValue){ 81 | xapi.command('UserInterface Extensions Widget SetValue', { 82 | WidgetId: textBoxId, 83 | Value: "Proximity "+stringValue, 84 | }); 85 | } 86 | // Initialize at widget deployment 87 | refreshProximityToggle(); -------------------------------------------------------------------------------- /users/authController.js: -------------------------------------------------------------------------------- 1 | /* 2 | Generate web token, register users and enable login to admin web interface 3 | */ 4 | var jwt = require('jsonwebtoken'); 5 | var bcrypt = require('bcryptjs'); 6 | var User = require('./user'); 7 | var secret = process.env.SECRETTOKEN; 8 | var express = require('express'); 9 | var router = express.Router(); 10 | var bodyParser = require('body-parser'); 11 | var log = require('../svrConfig/logger'); 12 | var cors = require('cors'); 13 | router.use(bodyParser.urlencoded({ extended: false })); 14 | router.use(bodyParser.json()); 15 | router.use(cors({ 16 | origin: '*', 17 | credentials: false, 18 | exposedHeaders: 'content-range', 19 | 20 | })); 21 | 22 | //register users. Admin account token is only allowed to register users using this method. 23 | 24 | router.post('/register', function(req, res) { 25 | var token = req.headers['x-access-token']; 26 | if (!token) return res.status(401).send({ auth: false, message: 'No token provided.' }); 27 | if(token === process.env.WEBADMINTOKEN){ 28 | var hashedPassword = bcrypt.hashSync(req.body.password, 8); 29 | var newUser = { 30 | name : req.body.name, 31 | email : req.body.email, 32 | password : hashedPassword 33 | }; 34 | User.create(newUser, function (err, user) { 35 | if (err) return res.status(500).send("There was a problem registering the user."); 36 | // create a token 37 | var token = jwt.sign({ id: user._id }, secret, { 38 | expiresIn: 86400 // expires in 24 hours 39 | }); 40 | res.status(200).send({ auth: true, token: token }); 41 | }); 42 | } 43 | 44 | }); 45 | //Returns user details based on token 46 | router.get('/me', function(req, res) { 47 | var token = req.headers['x-access-token']; 48 | log.info("Header sent"+req.headers['x-access-token']); 49 | if (!token) return res.status(401).send({ auth: false, message: 'No token provided.' }); 50 | 51 | jwt.verify(token, secret, function(err, decoded) { 52 | if (err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' }); 53 | 54 | res.status(200).send(decoded); 55 | }); 56 | }); 57 | //User login 58 | router.post('/login', function(req, res) { 59 | log.info("Login request from "+req.body.email); 60 | User.findOne({ email: req.body.email }, function (err, user) { 61 | if (err) return res.status(500).send('Error on the server.'); 62 | if (!user) return res.status(404).send('No user found.'); 63 | var passwordIsValid = bcrypt.compareSync(req.body.password, user.password); 64 | if (!passwordIsValid) return res.status(401).send({ auth: false, token: null }); 65 | var token = jwt.sign({ id: user._id }, secret, { 66 | expiresIn: 86400 // expires in 24 hours 67 | }); 68 | res.status(200).send({ auth: true, token: token }); 69 | }); 70 | }); 71 | 72 | module.exports = router; -------------------------------------------------------------------------------- /public/home/service-worker.js: -------------------------------------------------------------------------------- 1 | "use strict";var precacheConfig=[["/index.html","1e2b910f185de5c6962dab7a066eecfe"],["/static/css/main.65027555.css","41e5e45b9b5d9ecaa09b72c11eed3386"],["/static/js/main.ca670417.js","cefe6e20ec1914a717e21246341e20b2"]],cacheName="sw-precache-v3-sw-precache-webpack-plugin-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(t){return t.redirected?("body"in t?Promise.resolve(t.body):t.blob()).then(function(e){return new Response(e,{headers:t.headers,status:t.status,statusText:t.statusText})}):Promise.resolve(t)},createCacheKey=function(e,t,n,r){var a=new URL(e);return r&&a.pathname.match(r)||(a.search+=(a.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,n){var t=new URL(e);return t.hash="",t.search=t.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(t){return n.every(function(e){return!e.test(t[0])})}).map(function(e){return e.join("=")}).join("&"),t.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],r=new URL(t,self.location),a=createCacheKey(r,hashParamName,n,/\.\w{8}\./);return[r.toString(),a]}));function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(r){return setOfCachedUrls(r).then(function(n){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(t){if(!n.has(t)){var e=new Request(t,{credentials:"same-origin"});return fetch(e).then(function(e){if(!e.ok)throw new Error("Request for "+t+" returned a response with status "+e.status);return cleanResponse(e).then(function(e){return r.put(t,e)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var n=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(t){return t.keys().then(function(e){return Promise.all(e.map(function(e){if(!n.has(e.url))return t.delete(e)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(t){if("GET"===t.request.method){var e,n=stripIgnoredUrlParameters(t.request.url,ignoreUrlParametersMatching),r="index.html";(e=urlsToCacheKeys.has(n))||(n=addDirectoryIndex(n,r),e=urlsToCacheKeys.has(n));var a="/index.html";!e&&"navigate"===t.request.mode&&isPathWhitelisted(["^(?!\\/__).*"],t.request.url)&&(n=new URL(a,self.location).toString(),e=urlsToCacheKeys.has(n)),e&&t.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(e){return console.warn('Couldn\'t serve response for "%s" from cache: %O',t.request.url,e),fetch(t.request)}))}}); -------------------------------------------------------------------------------- /flintServer/flintConfig.js: -------------------------------------------------------------------------------- 1 | //Flint server configuration 2 | require('dotenv').config(); 3 | var SparkWebSocket = require('ciscospark-websocket-events'); 4 | var accessToken = process.env.SPARK_BOT; 5 | var webHookUrl = "http://localhost:8080/flint"; 6 | var Flint = require('node-flint'); 7 | var log = require('../svrConfig/logger'); 8 | var crud = require('../model/appController'); 9 | var conversation = require('../flintConversations/conversations'); 10 | 11 | 12 | // Spark Websocket Intialization - websocket support is limited for bots 13 | var sparkwebsocket = new SparkWebSocket(accessToken); 14 | sparkwebsocket.connect(function(err,res){ 15 | if (!err){ 16 | log.info('flintConfig.websockets : '+res); 17 | sparkwebsocket.setWebHookURL(webHookUrl); 18 | 19 | }else{ 20 | log.error("flintConfig.websockets Startup: "+err); 21 | } 22 | }); 23 | 24 | // flint options 25 | var config = { 26 | token: accessToken, 27 | port: process.env.WEBPORT, 28 | removeWebhooksOnStart: true, 29 | requeueMinTime: 500, 30 | requeueMaxRetry: 6, 31 | maxConcurrent: 5, 32 | minTime: 50 33 | }; 34 | 35 | // init flint 36 | var flint = new Flint(config); 37 | flint.start(); 38 | 39 | //control authorization to the bot 40 | function myAuthorizer(bot, trigger) { 41 | if(trigger.personEmail === process.env.APP_ADMIN) { 42 | return true; 43 | } 44 | else { 45 | bot.say("You are not authorized for use of this bot. I am outta here."); 46 | log.info("flintConfig.flint Access Info - unauthorized access : "+trigger.personEmail); 47 | bot.exit(); 48 | return false; 49 | } 50 | } 51 | 52 | flint.setAuthorizer(myAuthorizer); 53 | 54 | // add flint event listeners 55 | flint.on('message', function(bot, trigger) { 56 | log.info('flintConfig : "%s" said "%s" in room "%s"', trigger.personEmail, trigger.text, trigger.roomTitle); 57 | log.info("flintConfig : This is the room ID:"+ trigger.roomId); 58 | }); 59 | 60 | flint.on('initialized', function() { 61 | log.info('flintConfig : initialized %s rooms', flint.bots.length); 62 | 63 | 64 | }); 65 | flint.on('spawn', function(bot) { 66 | log.info('new bot spawned in room: %s', bot.room.id); 67 | var spaceId = bot.room.id; 68 | crud.findSpace(spaceId, function(err,spDetails){ 69 | if(err){ 70 | //creates new room and writes new room to JOSN 71 | crud.createSpace(spaceId, function(data){ 72 | log.info('flintConfig : Room created: '+ JSON.stringify(data)); 73 | return bot.say({markdown:"This is a personal bot monitor tool. Please remove if you are not authorized."}); 74 | });} 75 | if(spDetails.spaceActive === 'true') { 76 | return log.info("flintConfig.spawn: Room active."); 77 | }}); 78 | bot.repeat; 79 | }); 80 | flint.on('despawn', function(bot){ 81 | log.info('flintConfig : Bot removed room: '+bot.room.id); 82 | var spaceId = bot.room.id; 83 | crud.findSpace(spaceId, function(err,spDetails){ 84 | if(err) return log.error("flintConfig.despawn :"+err); 85 | crud.deleteSpace(spaceId, function(err, indexNo){ 86 | if(err) return log.error("flint.despawn : "+err); 87 | log.info("flint.despawn : "+indexNo) 88 | return spDetails.writeToFile(); 89 | 90 | }) 91 | 92 | }); 93 | }); 94 | conversation(flint); 95 | 96 | module.exports = flint; -------------------------------------------------------------------------------- /users/userController.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var bodyParser = require('body-parser'); 4 | var range = require('express-range'); 5 | var verifyToken = require('./verifyToken'); 6 | var log= require('../svrConfig/logger'); 7 | var bcrypt = require('bcryptjs'); 8 | router.use(bodyParser.urlencoded({ extended: true })); 9 | router.use(bodyParser.json()); 10 | var User = require('./user'); 11 | var cors = require('cors'); 12 | router.use(range({ 13 | accept: 'users', 14 | limit: 10, 15 | })); 16 | 17 | router.use(cors({ 18 | origin: '*', 19 | credentials: false, 20 | exposedHeaders: 'content-range', 21 | 22 | })); 23 | 24 | 25 | router.route('/') 26 | //AUTH USER 27 | .all((req, res, next) => verifyToken(req, res, next)) 28 | // CREATES A NEW USER 29 | .post((req, res) => { 30 | log.info("User Post:" + JSON.stringify(req.body)); 31 | var hashedPassword = bcrypt.hashSync(req.body.password, 8); 32 | var newUser = { 33 | name: req.body.name, 34 | email: req.body.email, 35 | password: hashedPassword, 36 | }; 37 | User.create(newUser, function (err, user) { 38 | if (err) return res.status(500).send("There was a problem adding the information to the database."); 39 | res.status(200).send(user); 40 | }); 41 | }) 42 | // RETURNS ALL THE USERS IN THE DATABASE 43 | .get((req, res) => { 44 | log.info("User GET:" + JSON.stringify(req.body)); 45 | User.find({}, function (err, users) { 46 | if (err) return res.status(500).send("There was a problem finding the users."); 47 | res.range({ 48 | first: req.range.first, 49 | last: req.range.last, 50 | length: users.length 51 | }); 52 | res.json({data: users.slice(req.range.first, req.range.last + 1), total: users.length}); 53 | }); 54 | }); 55 | 56 | router.route('/:id') 57 | //AUTH USER 58 | .all((req, res, next) => verifyToken(req, res, next)) 59 | // GETS A SINGLE USER FROM THE DATABASE 60 | .get((req, res) => { 61 | log.info("User GET/:ID:" + JSON.stringify(req.params.id)); 62 | User.find({_id: req.params.id}, function (err, user) { 63 | if (err) return res.status(500).send("There was a problem finding the user."); 64 | if (!user) return res.status(404).send("No user found."); 65 | res.status(200).json({data: user}); 66 | }); 67 | }) 68 | 69 | // DELETES A USER FROM THE DATABASE 70 | .delete((req, res) => { 71 | log.info("User DELETE/:ID:" + JSON.stringify(req.body)); 72 | User.findOneAndDelete({_id: req.params.id}, function (err, user) { 73 | if (err) return res.status(500).send("There was a problem deleting the user."); 74 | log.info("User: " + user.name + " was deleted."); 75 | res.status(200).send("User: " + user.name + " was deleted."); 76 | }); 77 | }) 78 | 79 | // UPDATES A SINGLE USER IN THE DATABASE --- TBD 80 | .put((req, res) => { 81 | log.info("User PUT/:ID:" + JSON.stringify(req.body)); 82 | log.info("URL ID: " + req.params.id); 83 | delete req.body.id; 84 | log.info("User PUT/:ID:" + JSON.stringify(req.body)); 85 | 86 | User.findById(req.params.id, function (err, user) { 87 | var userPassword = user.password; 88 | if (userPassword != req.body.password) { 89 | userPassword = bcrypt.hashSync(req.body.password, 8) 90 | } 91 | if (err) return res.status(500).send("There was a problem updating the user."); 92 | user.name = req.body.name; 93 | user.email = req.body.email; 94 | user.password = userPassword; 95 | user.save(function (err) { 96 | if (err) { 97 | return res.status(500).send("There was a problem updating the user."); 98 | } 99 | log.info("Update success: " + JSON.stringify(user)); 100 | return res.status(200).json({data: user}); 101 | }) 102 | 103 | }); 104 | }); 105 | 106 | module.exports = router; 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telehealth Presence Application 2 | 3 | A mix of Webex Teams chatbot and XMPP application, TPA allows additional XMPP presence states for Cisco video codecs inside of a Jabber XMPP environment. 4 | 5 | There are a number of components to this application: 6 | * XMPP account registration for telehealth carts 7 | * Webex Teams chat bot to do reporting and control of application(bulk uploads and application troubleshooting) 8 | * Tracking availability of video endpoint administration pages to verify endpoint availability for enhanced presence status 9 | * Poll video endpoints API for people presence and other information such as DND. 10 | * Automatically update IP changes from CUCM using MAC information. 11 | * MongoDB backend holds user, endpoint and space collections. 12 | * Web admin interface for adding and removing users and endpoints. 13 | * REST API service that allows administration and publish endpoint status. 14 | All pieces work together to create an application that can add additional presence states for video endpoints similar to Cisco Movi. 15 | 16 | [![Jabber Endpoint presence](/img/jabberPresence.png?raw=true)] 17 | 18 | ## Getting Started 19 | 20 | The following applications and hardware are required: 21 | 22 | * Cisco Unified Communications Manager 23 | * Cisco Presence Server 24 | * Cisco Video endpoint 25 | * Cisco Webex Teams 26 | * Nodejs 27 | * MongoDb 28 | 29 | ### Prerequisites 30 | 31 | Configuration required: 32 | 33 | * Video endpoint registration in CUCM 34 | * CUCM end user account enabled for Presence Server for each video endpoint 35 | * Endpoint and end user accounts paired with each other within CUCM 36 | * Cisco Webex Teams Bot account 37 | * Cisco Webex Teams Space ID to send application messages. 38 | * DNS FQDN videoPresence.:3001 eith via DNS record or host record. 39 | * install and configure Nodejs and MongoDb 40 | * Optional: MongoDb Compass 41 | https://www.mongodb.com/download-center#compass 42 | 43 | ### Installing 44 | 45 | #### Via Git 46 | ```bash 47 | mkdir myproj 48 | cd myproj 49 | git clone https://github.com/voipnorm/telehealthPresence.git 50 | npm install 51 | ``` 52 | 53 | Set the following environment variables... 54 | 55 | ``` 56 | SPARK_ROOM_ID= 57 | SPARK_BOT= 58 | WEBPORT=8080 59 | NODE_ENV=development 60 | SPARK_BOT_STRING= 61 | ALLOW_DOMAIN= 62 | APP_ADMIN= 63 | XMPPSERVER= 64 | XMPPCARTPWD=