diff --git a/changelog.md b/changelog.md index a61037f8..f899f958 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,8 @@ 9. Slack task fail/abort messages can now - optionally - use same templating concept that was previously available for emails. Better looking Slack alert messages thus! ([#120](https://github.com/ptarmiganlabs/butler/issues/120)). 10. Added rate limiting option for task fail/aborts sent to Slack and MS Teams. ([#119](https://github.com/ptarmiganlabs/butler/issues/119)). 11. Add MQTT message containing all available info about failed/aborted tasks. Format is stringified JSON. ([#128](https://github.com/ptarmiganlabs/butler/issues/128)). +12. Add exclude list to user activity events sent to Slack and MS Teams. Using this feature it's possible to prevent session start/stop etc notifications for specific users to be sent to Teams and Slack. Useful for example if a system account is used to do API calls - that probably should not be reported as user activity. ([#132](https://github.com/ptarmiganlabs/butler/issues/132)). +13. Improvements around handling of inbound MQTT messages. The root MQTT topic that Butler subscribes to is now configurable ([#137](https://github.com/ptarmiganlabs/butler/issues/137)). The topic through which Sense tasks can be started is now also configurable ([#136](https://github.com/ptarmiganlabs/butler/issues/136)) rather than hard coded. ### Fixes and patches @@ -37,7 +39,7 @@ 3. **BREAKING**: Similar to 1 above, all MS Teams configuration has been moved from `Butler.teamsConfig.*` into the larger/more generic `Butler.teamsNotification.*` section of the config file. -4. UDP message format for user session start/stop log appender has changed to follow same principles as other log appender UDP messages sent to Butler. ([#126](https://github.com/ptarmiganlabs/butler/issues/126)). +4. UDP message format for user session start/stop log appender has changed to follow same principles as other log appender UDP messages sent to Butler. ([#126](https://github.com/ptarmiganlabs/butler/issues/126), [#134](https://github.com/ptarmiganlabs/butler/issues/134)). ## 4.3.0 diff --git a/docs/log4net_task-failed/scheduler/LocalLogConfig.xml b/docs/log4net_task-failed/scheduler/LocalLogConfig.xml index 03d47585..53b8d9c4 100644 --- a/docs/log4net_task-failed/scheduler/LocalLogConfig.xml +++ b/docs/log4net_task-failed/scheduler/LocalLogConfig.xml @@ -1,6 +1,6 @@ - + @@ -13,7 +13,7 @@ - + @@ -31,7 +31,7 @@ - + @@ -44,7 +44,7 @@ - + @@ -55,11 +55,12 @@ - + + - + @@ -74,8 +75,8 @@ - - + + diff --git a/docs/log4net_user-audit-event/LocalLogConfig.xml b/docs/log4net_user-audit-event/LocalLogConfig.xml index e76bc1c9..303d38da 100644 --- a/docs/log4net_user-audit-event/LocalLogConfig.xml +++ b/docs/log4net_user-audit-event/LocalLogConfig.xml @@ -1,24 +1,48 @@ - + - + + - + - + + - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/sense_apps/Butler 4.3 demo app.qvf b/docs/sense_apps/Butler 4.3 demo app.qvf deleted file mode 100644 index ce684b53..00000000 Binary files a/docs/sense_apps/Butler 4.3 demo app.qvf and /dev/null differ diff --git a/docs/sense_apps/Butler 5.0 - demo app (many APIs!).qvf b/docs/sense_apps/Butler 5.0 - demo app (many APIs!).qvf new file mode 100644 index 00000000..28b98d02 Binary files /dev/null and b/docs/sense_apps/Butler 5.0 - demo app (many APIs!).qvf differ diff --git a/docs/sense_apps/Butler 5.0 - partial reload demo (step 1).qvf b/docs/sense_apps/Butler 5.0 - partial reload demo (step 1).qvf new file mode 100644 index 00000000..1734b700 Binary files /dev/null and b/docs/sense_apps/Butler 5.0 - partial reload demo (step 1).qvf differ diff --git a/docs/sense_apps/Butler 5.0 - partial reload demo (step 2).qvf b/docs/sense_apps/Butler 5.0 - partial reload demo (step 2).qvf new file mode 100644 index 00000000..4a00e436 Binary files /dev/null and b/docs/sense_apps/Butler 5.0 - partial reload demo (step 2).qvf differ diff --git a/docs/sense_apps/Butler 5.0 - post message to Slack.qvf b/docs/sense_apps/Butler 5.0 - post message to Slack.qvf new file mode 100644 index 00000000..cf99946b Binary files /dev/null and b/docs/sense_apps/Butler 5.0 - post message to Slack.qvf differ diff --git a/docs/sense_apps/Butler 5.0 - publish messages to MQTT.qvf b/docs/sense_apps/Butler 5.0 - publish messages to MQTT.qvf new file mode 100644 index 00000000..b12604bc Binary files /dev/null and b/docs/sense_apps/Butler 5.0 - publish messages to MQTT.qvf differ diff --git a/docs/sense_script/butler_subs.qvs b/docs/sense_script/butler_subs.qvs index 9cd65f92..b98ba78d 100644 --- a/docs/sense_script/butler_subs.qvs +++ b/docs/sense_script/butler_subs.qvs @@ -1,11 +1,16 @@ // ------------------------ // INSTRUCTIONS // -// All of thee subs below that deal with Butler's API assume two variables exist: +// All of thee subs below deal with Butler's API. It isassumed that two variables exist: // vButlerHost : The host, fully-qualified-domain-name or IP of the server where Butler is running // vButlerPort : The port on which Butler's REST API is exposed // -// Both the settings above are set in Butler's main config file. +// If unsure what the proper values for those variables are, check Butler's YAML config file, +// or the log messages printed when Butler starts. Look for a log line like this one: +// +// MAIN: REST server listening on http://192.168.1.168:8080 +// +// In this case vButlerHost should be set to 'http://192.168.1.168' and vButlerPort to '8080' // ------------------------ @@ -54,6 +59,116 @@ end sub +// ------------------------------------------------------------ +// ** List enabled API endpoints ** +// +// Parameters: +// vResultTable : Name of table to which names of enabled API +// endpoints will be added +// ------------------------------------------------------------ +sub GetEnabledButlerAPIEndpoints(vResultTable) + LIB CONNECT TO 'Butler_GET'; + + RestConnectorMasterTable: + SQL SELECT + "@Value" + FROM JSON (wrap on) "root" ArrayValueAlias "@Value" + WITH CONNECTION ( + Url "$(vButlerHost):$(vButlerPort)/v4/configfile/endpointsenabled", + ); + + [$(vResultTable)]: + NoConcatenate LOAD + [@Value] as "Endpoint name" + RESIDENT RestConnectorMasterTable; + + set vResultTable=; + DROP TABLE RestConnectorMasterTable; +end sub + + + +// ------------------------------------------------------------ +// ** Reload QSEoW apps ** +// +// NOTE: The reload is done directly in the engine, i.e. no reload +// tasks are used for the reload. +// This means that the reload cannot be triggered using QMC tasks. +// +// The options for starting the reload are using the sub below or +// via direct calls to Butler's API. +// +// Parameters: +// vAppId: ID of QSEoW app that should be reloaded. +// vPartialReload: true/false +// vReloadMode: 0, 1, 2. +// vStartQSEoWTaskOnSuccess: Array of Sense task IDs. Will be started if the reload completes successfully. +// Format is '["taskid1","taskid2","taskid3"]' +// +// vStartQSEoWTaskOnFailure: Array of Sense task IDs. Will be started if the reload fails. +// Format is '["taskid1","taskid2","taskid3"]' +// +// ------------------------------------------------------------ +sub ReloadSenseApp(vAppId, vPartialReload, vReloadMode, vStartQSEoWTaskOnSuccess, vStartQSEoWTaskOnFailure) + // Make task IDs double double-quoted + let vStartQSEoWTaskOnSuccess = Replace('$(vStartQSEoWTaskOnSuccess)', '"', '""'); + let vStartQSEoWTaskOnFailure = Replace('$(vStartQSEoWTaskOnFailure)', '"', '""'); + + // Build request body + let vRequestBody = '{""partialReload"":""$(vPartialReload)"", ""reloadMode"":$(vReloadMode), ""startQSEoWTaskOnSuccess"":$(vStartQSEoWTaskOnSuccess), ""startQSEoWTaskOnFailure"":$(vStartQSEoWTaskOnFailure)}'; + + LIB CONNECT TO 'Butler_POST'; + + RestConnectorMasterTable: + SQL SELECT + "appId" + FROM JSON (wrap on) "root" + WITH CONNECTION ( + Url "$(vButlerHost):$(vButlerPort)/v4/app/$(vAppId)/reload", + BODY "$(vRequestBody)", + HTTPHEADER "X-HTTP-Method-Override" "PUT" + ); + + set vAppId=; + set vPartialReload=; + set vReloadMode=; + set vStartQSEoWTaskOnSuccess=; + set vStartQSEoWTaskOnFailure=; + set vRequestBody=; + DROP TABLE RestConnectorMasterTable; +end sub + + + +// ------------------------------------------------------------ +// ** Start QSEoW task ** +// +// Use Butler to start the task identified by the ID in parameter vTaskId. +// +// Paramaters: +// vTaskId : ID of task to be started. Can be found in QMC task view. +// ------------------------------------------------------------ +sub StartTask(vTaskId) + LIB CONNECT TO 'Butler_POST'; + + RestConnectorMasterTable: + SQL SELECT + "taskId" + FROM JSON (wrap on) "root" + WITH CONNECTION ( + Url "$(vButlerHost):$(vButlerPort)/v4/reloadtask/$(vTaskId)/start", + HTTPHEADER "X-HTTP-Method-Override" "PUT" + ); + + set vTaskId=; + DROP TABLE RestConnectorMasterTable; +end sub + + + + + + // ------------------------------------------------------------ // ** Copy files/directories ** // @@ -85,7 +200,7 @@ sub CopyFile(vSource, vDest, vOverwrite, vPreserveTimestamp) "preserveTimestamp" FROM JSON (wrap on) "root" WITH CONNECTION ( - Url "http://$(vButlerHost):$(vButlerPort)/v4/filecopy", + Url "$(vButlerHost):$(vButlerPort)/v4/filecopy", BODY "$(vRequestBody)", HTTPHEADER "X-HTTP-Method-Override" "PUT" ); @@ -94,7 +209,7 @@ sub CopyFile(vSource, vDest, vOverwrite, vPreserveTimestamp) set vDest=; set vOverwrite=; set vPreserveTimestamp=; - + set vRequestBody=; DROP TABLE RestConnectorMasterTable; end sub @@ -130,7 +245,7 @@ sub MoveFile(vSource, vDest, vOverwrite) "overwrite" FROM JSON (wrap on) "root" WITH CONNECTION ( - Url "http://$(vButlerHost):$(vButlerPort)/v4/filemove", + Url "$(vButlerHost):$(vButlerPort)/v4/filemove", BODY "$(vRequestBody)", HTTPHEADER "X-HTTP-Method-Override" "PUT" ); @@ -138,7 +253,7 @@ sub MoveFile(vSource, vDest, vOverwrite) set vSource=; set vDest=; set vOverwrite=; - + set vRequestBody=; DROP TABLE RestConnectorMasterTable; end sub @@ -165,13 +280,13 @@ sub DeleteFile(vFile) "vFile" FROM JSON (wrap on) "root" WITH CONNECTION ( - Url "http://$(vButlerHost):$(vButlerPort)/v4/filedelete", + Url "$(vButlerHost):$(vButlerPort)/v4/filedelete", BODY "$(vRequestBody)", HTTPHEADER "X-HTTP-Method-Override" "DELETE" ); set vFile=; - + set vRequestBody=; DROP TABLE RestConnectorMasterTable; end sub @@ -200,11 +315,12 @@ sub CreateDir(vDir) "directory" FROM JSON (wrap on) "root" WITH CONNECTION ( - Url "http://$(vButlerHost):$(vButlerPort)/v4/createdir", + Url "$(vButlerHost):$(vButlerPort)/v4/createdir", BODY "$(vRequestBody)", HTTPHEADER "Content-Type" "application/json" ); + set vDir=; set vRequestBody=; DROP TABLE RestConnectorMasterTable; end sub @@ -231,44 +347,18 @@ sub CreateDirQVD(vDir) "directory" FROM JSON (wrap on) "root" WITH CONNECTION ( - Url "http://$(vButlerHost):$(vButlerPort)/v4/createdirqvd", + Url "$(vButlerHost):$(vButlerPort)/v4/createdirqvd", BODY "$(vRequestBody)", HTTPHEADER "Content-Type" "application/json" ); + set vDir=; set vRequestBody=; DROP TABLE RestConnectorMasterTable; end sub -// ------------------------------------------------------------ -// ** List enabled API endpoints ** -// -// Parameters: -// vResultTable : Name of table to which names of enabled API -// endpoints will be added -// ------------------------------------------------------------ -sub GetEnabledButlerAPIEndpoints(vResultTable) - LIB CONNECT TO 'Butler_GET'; - - RestConnectorMasterTable: - SQL SELECT - "@Value" - FROM JSON (wrap on) "root" ArrayValueAlias "@Value" - WITH CONNECTION ( - Url "http://$(vButlerHost):$(vButlerPort)/v4/configfile/endpointsenabled", - ); - - [$(vResultTable)]: - NoConcatenate LOAD - [@Value] as "Endpoint name" - RESIDENT RestConnectorMasterTable; - - set vResultTable=; - DROP TABLE RestConnectorMasterTable; -end sub - // ------------------------------------------------------------ @@ -301,7 +391,7 @@ sub AddKeyValue(vNamespace, vKey, vValue, vTimeToLive) "ttl" FROM JSON (wrap on) "root" WITH CONNECTION ( - Url "http://$(vButlerHost):$(vButlerPort)/v4/keyvalues/$(vNamespace)", + Url "$(vButlerHost):$(vButlerPort)/v4/keyvalues/$(vNamespace)", BODY "$(vRequestBody)", HTTPHEADER "Content-Type" "application/json" ); @@ -310,6 +400,7 @@ sub AddKeyValue(vNamespace, vKey, vValue, vTimeToLive) set vKey=; set vValue=; set vTimeToLive=; + set vRequestBody=; DROP TABLE RestConnectorMasterTable; end sub @@ -332,7 +423,7 @@ sub GetKeyValue(vNamespace, vKey, vResultVarName) "value" FROM JSON (wrap on) "root" WITH CONNECTION ( - Url "http://$(vButlerHost):$(vButlerPort)/v4/keyvalues/$(vNamespace)?key=$(vKey)" + Url "$(vButlerHost):$(vButlerPort)/v4/keyvalues/$(vNamespace)?key=$(vKey)" ); let $(vResultVarName) = Peek('value', 0, 'RestConnectorMasterTable'); @@ -340,6 +431,7 @@ sub GetKeyValue(vNamespace, vKey, vResultVarName) set vResultVarName=; set vNamespace=; set vKey=; + set vRequestBody=; DROP TABLE RestConnectorMasterTable; end sub @@ -359,7 +451,7 @@ sub DeleteKeyValue(vNamespace, vKey) SQL SELECT FROM JSON (wrap on) "root" WITH CONNECTION ( - Url "http://$(vButlerHost):$(vButlerPort)/v4/keyvalues/$(vNamespace)/$(vKey)", + Url "$(vButlerHost):$(vButlerPort)/v4/keyvalues/$(vNamespace)/$(vKey)", HTTPHEADER "X-HTTP-Method-Override" "DELETE" ); @@ -370,6 +462,14 @@ end sub +// ------------------------------------------------------------ +// ** Checks if a key exists in a particular namespace ** +// +// Paramaters: +// vNamespace : Namespace in which to look for key +// vKey : Key name +// vResultVarName : Name of variable in wich value will be placed +// ------------------------------------------------------------ sub ExistsKeyValue(vNamespace, vKey, vResultVarName) LIB CONNECT TO 'Butler_GET'; @@ -386,7 +486,7 @@ sub ExistsKeyValue(vNamespace, vKey, vResultVarName) FROM "keyValue" FK "__FK_keyValue") FROM JSON (wrap on) "root" PK "__KEY_root" WITH CONNECTION ( - Url "http://$(vButlerHost):$(vButlerPort)/v4/keyvalues/$(vNamespace)/keyexists?key=$(vKey)", + Url "$(vButlerHost):$(vButlerPort)/v4/keyvalues/$(vNamespace)/keyexists?key=$(vKey)", ); [keyValue]: @@ -416,7 +516,12 @@ end sub - +// ------------------------------------------------------------ +// ** Delete a namespace and all key-value pairs in it. ** +// +// Paramaters: +// vNamespace : Namespace to be deleted +// ------------------------------------------------------------ sub DeleteKeyValueNamespace(vNamespace) LIB CONNECT TO 'Butler_POST'; @@ -424,7 +529,7 @@ sub DeleteKeyValueNamespace(vNamespace) SQL SELECT FROM JSON (wrap on) "root" WITH CONNECTION ( - Url "http://$(vButlerHost):$(vButlerPort)/v4/keyvalues/$(vNamespace)", + Url "$(vButlerHost):$(vButlerPort)/v4/keyvalues/$(vNamespace)", HTTPHEADER "X-HTTP-Method-Override" "DELETE" ); @@ -433,6 +538,13 @@ sub DeleteKeyValueNamespace(vNamespace) end sub + +// ------------------------------------------------------------ +// ** Get a list of all currently defined namespaces. ** +// +// Paramaters: +// vResultTableName : Name of table in which namespaces will be returned +// ------------------------------------------------------------ sub GetKeyValueNamespaces(vResultTableName) LIB CONNECT TO 'Butler_GET'; @@ -441,7 +553,7 @@ sub GetKeyValueNamespaces(vResultTableName) "@Value" FROM JSON (wrap on) "root" ArrayValueAlias "@Value" WITH CONNECTION ( - Url "http://$(vButlerHost):$(vButlerPort)/v4/keyvaluesnamespaces", + Url "$(vButlerHost):$(vButlerPort)/v4/keyvaluesnamespaces", ); [$(vResultTableName)]: @@ -456,30 +568,88 @@ end sub + // ------------------------------------------------------------ -// ** Start Sense task ** +// ** Publish message to MQTT topic ** // -// Use Butler to start the task identified by the ID in parameter vTaskId. +// Publishes a message to a MQTT topic. +// The address of the MQTT server is configured in the Butler config file. // // Paramaters: -// vTaskId : +// vTopic : MQTT topic +// vMessage : MQTT message // ------------------------------------------------------------ -sub StartTask(vTaskId) - LIB CONNECT TO 'Butler_POST'; +sub PostToMQTT(vTopic, vMessage) + // URL encode the parameters passed to Butler, to ensure spaces, international characters etc are handled correctly. + // I.e. call ButlerInit before calling this Sub... + let vRequestBody = '{""topic"":""$(vTopic)"", ""message"":""$(vMessage)""}'; + + LIB CONNECT TO 'Butler_POST'; RestConnectorMasterTable: SQL SELECT - "taskId" + "directory" FROM JSON (wrap on) "root" WITH CONNECTION ( - Url "http://$(vButlerHost):$(vButlerPort)/v4/reloadtask/$(vTaskId)/start", + Url "$(vButlerHost):$(vButlerPort)/v4/mqttpublishmessage", + BODY "$(vRequestBody)", + HTTPHEADER "Content-Type" "application/json", HTTPHEADER "X-HTTP-Method-Override" "PUT" ); - set vTaskId=; + set vTopic=; + set vMessage=; + set vRequestBody=; + DROP TABLE RestConnectorMasterTable; +end sub - DROP TABLE RestConnectorMasterTable; + + +// ------------------------------------------------------------ +// ** Publish message to Slack ** +// +// Publishes a basic message to Slack. +// The Slack API access URL is configured in the Butler config file. +// +// No advanced layout can be used, available formatting options for messages: https://api.slack.com/docs/formatting +// Available emojis: http://www.emoji-cheat-sheet.com/ +// Slack API docs: https://api.slack.com/incoming-webhooks +// +// Paramaters: +// vSlackChannel : Channel to which message will be posted +// vFromUser : Name of user that will appear as sender of message +// vMessage : Message to send +// vEmoji : Emoji to show next to message in Slack +// ------------------------------------------------------------ +sub PostToSlack(vToSlackChannel, vFromUser, vMessage, vEmoji) + // URL encode the parameters passed to Butler, to ensure spaces, international characters etc are handled correctly. + // If neither # nor @ is specified as first character of the Slack channel name, # will be added. + if ( (left('$(vToSlackChannel)', 1) <> '@') and (left('$(vToSlackChannel)', 1) <> '#') ) then + let vToSlackChannel = '#$(vToSlackChannel)'; + endif + + // Build request body + let vRequestBody = '{""channel"":""$(vToSlackChannel)"", ""from_user"":""$(vFromUser)"", ""msg"":""$(vMessage)"", ""emoji"":""$(vEmoji)""}'; + + LIB CONNECT TO 'Butler_POST'; + + RestConnectorMasterTable: + SQL SELECT + "directory" + FROM JSON (wrap on) "root" + WITH CONNECTION ( + Url "$(vButlerHost):$(vButlerPort)/v4/slackpostmessage", + BODY "$(vRequestBody)", + HTTPHEADER "Content-Type" "application/json", + HTTPHEADER "X-HTTP-Method-Override" "PUT" + ); + + set vToSlackChannel=; + set vFromUser=; + set vMessage=; + set vEmoji=; + set vRequestBody=; + drop table RestConnectorMasterTable; end sub - diff --git a/docs/sense_script/post_to_mqtt.qvs b/docs/sense_script/post_to_mqtt.qvs deleted file mode 100644 index 16901091..00000000 --- a/docs/sense_script/post_to_mqtt.qvs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------ -// ** Publish message to MQTT topic ** -// -// Publishes a message to a MQTT topic. -// The address of the MQTT server is configured in Butler. -// -// ------------------------------------------------------------ - -Sub PostToMQTT(vTopic, vMessage) - // URL encode the parameters passed to Butler, to ensure spaces, international characters etc are handled correctly. - let vTopic = MapSubstring('URL_EncodingReferenceMap', '$(vTopic)'); - let vMessage = MapSubstring('URL_EncodingReferenceMap', '$(vMessage)'); - - tmpMQTT: - LOAD - * - FROM [http://localhost:8080/mqttPublishMessage?topic=$(vTopic)&message=$(vMessage)] - (txt, codepage is 1252, embedded labels, delimiter is '\t', msq); - - drop table tmpMQTT; -end sub diff --git a/docs/sense_script/post_to_slack.qvs b/docs/sense_script/post_to_slack.qvs deleted file mode 100644 index 97b89c9b..00000000 --- a/docs/sense_script/post_to_slack.qvs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------ -// ** Publish message to a Slack channel ** -// -// Publishes a message to Slack. -// The Slack API access URL is hard coded in Butler -// -// Formatting options for messages: https://api.slack.com/docs/formatting -// Available emojis: http://www.emoji-cheat-sheet.com/ -// Slack API docs: https://api.slack.com/incoming-webhooks -// -// ------------------------------------------------------------ - -Sub PostToSlack(vToSlackChannel, vFromUser, vMessage, vEmoji) - // URL encode the parameters passed to Butler, to ensure spaces, international characters etc are handled correctly. - // If neither # nor @ is specified as first character of the Slack channel name, # will be added. - - if ( (left('$(vToSlackChannel)', 1) <> '@') and (left('$(vToSlackChannel)', 1) <> '#') ) then - let vToSlackChannel = '#$(vToSlackChannel)'; - endif - - let vToSlackChannel2 = '$(vToSlackChannel)'; - let vToSlackChannel = MapSubstring('URL_EncodingReferenceMap', '$(vToSlackChannel)'); - let vFromUser2 = '$(vFromUser)'; - let vFromUser = MapSubstring('URL_EncodingReferenceMap', '$(vFromUser)'); - let vMessage2 = '$(vMessage)'; - let vMessage = MapSubstring('URL_EncodingReferenceMap', '$(vMessage)'); - - tmpSlack: - LOAD - * - FROM [http://localhost:8080/v2/slackPostMessage?channel=$(vToSlackChannel)&from_user=$(vFromUser)&msg=$(vMessage)&emoji=$(vEmoji)] - (txt, codepage is 1252, embedded labels, delimiter is '\t', msq); - - drop table tmpSlack; - - let vToSlackChannel = '$(vToSlackChannel2)'; - let vFromUser = '$(vFromUser2)'; - let vMessage = '$(vMessage2)'; -end sub diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml index 3bc6c44b..5b84700b 100644 --- a/src/config/production_template.yaml +++ b/src/config/production_template.yaml @@ -51,43 +51,53 @@ Butler: # Settings for notifications and messages sent to MS Teams teamsNotification: - enable: true + enable: false userSessionEvents: - enable: true + enable: false webhookURL: + excludeUser: + - directory: + userId: + - directory: + userId: reloadTaskFailure: - enable: true + enable: false webhookURL: messageType: formatted # formatted / basic. Formatted means that template file below will be used to create the message. basicMsgTemplate: 'Qlik Sense reload failed: "{{taskName}}"' # Only needed if message type = basic - rateLimit: 15 # Min seconds between emails for a given taskID. Defaults to 5 minutes. + rateLimit: 300 # Min seconds between emails for a given taskID. Defaults to 5 minutes. headScriptLogLines: 10 tailScriptLogLines: 10 templateFile: /path/to/teams/template/directory/failed-reload.handlebars reloadTaskAborted: - enable: true + enable: false webhookURL: messageType: formatted # formatted / basic. Formatted means that template file below will be used to create the message. basicMsgTemplate: 'Qlik Sense reload aborted: "{{taskName}}"' # Only needed if message type = basic - rateLimit: 15 # Min seconds between emails for a given taskID. Defaults to 5 minutes. + rateLimit: 300 # Min seconds between emails for a given taskID. Defaults to 5 minutes. headScriptLogLines: 10 tailScriptLogLines: 10 templateFile: /path/to/teams/template/directory/aborted-reload.handlebars # Settings for notifications and messages sent to Slack slackNotification: - enable: true + enable: false restMessage: - enable: true + enable: false webhookURL: userSessionEvents: - enable: true + enable: false webhookURL: channel: sense-user-activity # Slack channel to which user activity data is sent fromUser: Qlik Sense iconEmoji: ':thumbsup:' + excludeUser: + - directory: + userId: + - directory: + userId: reloadTaskFailure: - enable: true + enable: false webhookURL: channel: sense-task-failure # Slack channel to which task failure notifications are sent messageType: formatted # formatted / basic. Formatted means that template file below will be used to create the message. @@ -99,7 +109,7 @@ Butler: fromUser: Qlik Sense iconEmoji: ':ghost:' reloadTaskAborted: - enable: true + enable: false webhookURL: channel: sense-task-aborted # Slack channel to which task stopped notifications are sent messageType: formatted # formatted / basic. Formatted means that template file below will be used to create the message. @@ -134,14 +144,14 @@ Butler: - directory: userId: rateLimit: 600 # Min seconds between emails for a given taskID. Defaults to 5 minutes. - headScriptLogLines: 15 # # of lines from start of script to include in email - tailScriptLogLines: 15 # # of lines from end of script to include in email + headScriptLogLines: 15 # Number of lines from start of script to include in email + tailScriptLogLines: 15 # Number of lines from end of script to include in email priority: high # high/normal/low subject: 'Qlik Sense reload aborted: "{{taskName}}"' # Email subject. Can use template fields bodyFileDirectory: config/email_templates # Directory where email body template files are stored htmlTemplateFile: aborted-reload # Name of email body template file to use fromAdress: Qlik Sense (no-reply) - toAdress: # Array of email addresses to which the notification email will be sent + recipients: # Array of email addresses to which the notification email will be sent - - reloadTaskFailure: @@ -163,14 +173,14 @@ Butler: - directory: userId: rateLimit: 600 # Min seconds between emails for a given taskID. Defaults to 5 minutes. - headScriptLogLines: 15 # # of lines from start of script to include in email - tailScriptLogLines: 15 # # of lines from end of script to include in email + headScriptLogLines: 15 # Number of lines from start of script to include in email + tailScriptLogLines: 15 # Number of lines from end of script to include in email priority: high # high/normal/low subject: 'Qlik Sense reload failed: "{{taskName}}"' # Email subject. Can use template fields bodyFileDirectory: config/email_templates # Directory where email body template files are stored htmlTemplateFile: failed-reload # Name of email body template file to use fromAdress: Qlik Sense (no-reply) - toAdress: # Array of email addresses to which the notification email will be sent + recipients: # Array of email addresses to which the notification email will be sent - - smtp: # Email server settings. See https://nodemailer.com/smtp/ for details on the meaning of these fields. @@ -189,29 +199,29 @@ Butler: # Settings for notifications and messages sent using outgoing webhooks webhookNotification: - enable: true + enable: false reloadTaskFailure: rateLimit: 300 # Min seconds between outgoing webhook calls for a given taskID. Defaults to 5 minutes. webhooks: - - description: 'This outgoing webhook is used to...' + - description: 'This outgoing webhook is used to...' # Informational only webhookURL: http://host.my.domain:port/some/path # outgoing webhook that Butler will call httpMethod: POST # GET/POST/PUT - - description: 'This outgoing webhook is used to...' + - description: 'This outgoing webhook is used to...' # Informational only webhookURL: http://host.my.domain:port/some/path # outgoing webhook that Butler will call httpMethod: PUT # GET/POST/PUT. - - description: 'This outgoing webhook is used to...' + - description: 'This outgoing webhook is used to...' # Informational only webhookURL: http://host.my.domain:port/some/path # outgoing webhook that Butler will call httpMethod: GET # GET/POST/PUT reloadTaskAborted: rateLimit: 300 # Min seconds between outgoing webhook calls for a given taskID. Defaults to 5 minutes. webhooks: - - description: 'This outgoing webhook is used to...' + - description: 'This outgoing webhook is used to...' # Informational only webhookURL: http://host.my.domain:port/some/path # outgoing webhook that Butler will call httpMethod: PUT # GET/POST/PUT - - description: 'This outgoing webhook is used to...' + - description: 'This outgoing webhook is used to...' # Informational only webhookURL: http://host.my.domain:port/some/path # outgoing webhook that Butler will call httpMethod: POST # GET/POST/PUT - - description: 'This outgoing webhook is used to...' + - description: 'This outgoing webhook is used to...' # Informational only webhookURL: http://host.my.domain:port/some/path # outgoing webhook that Butler will call httpMethod: GET # GET/POST/PUT @@ -231,6 +241,8 @@ Butler: brokerPort: 1883 taskFailureSendFull: true taskAbortedSendFull: true + subscriptionRootTopic: qliksense/# # Topic that Butler will subscribe to + taskStartTopic: qliksense/start_task # Topic for incoming messages used to start Sense tasks. Should be subtopic to subscriptionRootTopic taskFailureTopic: qliksense/task_failure taskFailureFullTopic: qliksense/task_failure_full taskFailureServerStatusTopic: qliksense/butler/task_failure_server diff --git a/src/globals.js b/src/globals.js index 571e4eae..3c6b3c23 100644 --- a/src/globals.js +++ b/src/globals.js @@ -59,8 +59,6 @@ const getLoggingLevel = () => { }).level; }; -// Load our own libs -// var qrsUtil = require('./qrsUtil'); // Helper function to read the contents of the certificate files: const readCert = filename => fs.readFileSync(filename); @@ -330,7 +328,6 @@ function initInfluxDB() { module.exports = { config, - // qrsUtil, configEngine, configQRS, teamsTaskFailureObj, diff --git a/src/lib/logRESTCall.js b/src/lib/logRESTCall.js index 841b4544..29b2447b 100644 --- a/src/lib/logRESTCall.js +++ b/src/lib/logRESTCall.js @@ -9,6 +9,7 @@ var globals = require('../globals'); // Function for logging info about REST call module.exports.logRESTCall = function (req) { globals.logger.info(`${req.url} called from ${req.client.remoteAddress}`); - globals.logger.verbose(`Query: ${JSON.stringify(req.query, null, 2)}`); - globals.logger.verbose(`Headers: ${JSON.stringify(req.headers, null, 2)}`); + globals.logger.debug(`Query: ${JSON.stringify(req.query, null, 2)}`); + globals.logger.debug(`Body: ${JSON.stringify(req.body, null, 2)}`); + globals.logger.debug(`Headers: ${JSON.stringify(req.headers, null, 2)}`); }; diff --git a/src/lib/smtp.js b/src/lib/smtp.js index 7410bea0..731c5af4 100644 --- a/src/lib/smtp.js +++ b/src/lib/smtp.js @@ -79,8 +79,8 @@ function isEmailReloadFailedNotificationConfigOk() { !globals.config.has('Butler.emailNotification.reloadTaskFailure.bodyFileDirectory') || !globals.config.has('Butler.emailNotification.reloadTaskFailure.htmlTemplateFile') || !globals.config.has('Butler.emailNotification.reloadTaskFailure.fromAdress') || - !globals.config.has('Butler.emailNotification.reloadTaskFailure.toAdress') || - globals.config.get('Butler.emailNotification.reloadTaskFailure.toAdress').length == 0 + !globals.config.has('Butler.emailNotification.reloadTaskFailure.recipients') || + globals.config.get('Butler.emailNotification.reloadTaskFailure.recipients').length == 0 ) { // Not enough info in config file globals.logger.error('SMTP: Reload failure email config info missing in Butler config file'); @@ -114,8 +114,8 @@ function isEmailReloadAbortedNotificationConfigOk() { !globals.config.has('Butler.emailNotification.reloadTaskAborted.bodyFileDirectory') || !globals.config.has('Butler.emailNotification.reloadTaskAborted.htmlTemplateFile') || !globals.config.has('Butler.emailNotification.reloadTaskAborted.fromAdress') || - !globals.config.has('Butler.emailNotification.reloadTaskAborted.toAdress') || - globals.config.get('Butler.emailNotification.reloadTaskAborted.toAdress').length == 0 + !globals.config.has('Butler.emailNotification.reloadTaskAborted.recipients') || + globals.config.get('Butler.emailNotification.reloadTaskAborted.recipients').length == 0 ) { // Not enough info in config file globals.logger.error('SMTP: Reload aborted email config info missing in Butler config file'); @@ -321,7 +321,7 @@ function sendReloadTaskFailureNotificationEmail(reloadParams) { sendEmail( globals.config.get('Butler.emailNotification.reloadTaskFailure.fromAdress'), - recipientEmails.concat(globals.config.get('Butler.emailNotification.reloadTaskFailure.toAdress')), + recipientEmails.concat(globals.config.get('Butler.emailNotification.reloadTaskFailure.recipients')), globals.config.get('Butler.emailNotification.reloadTaskFailure.priority'), globals.config.get('Butler.emailNotification.reloadTaskFailure.subject'), globals.config.get('Butler.emailNotification.reloadTaskFailure.bodyFileDirectory'), @@ -424,10 +424,10 @@ function sendReloadTaskAbortedNotificationEmail(reloadParams) { } } - // Note: Butler.emailNotification.reloadTaskAborted.toAdress is an array, sendEmail() should send individual emails to everyone in that array + // Note: Butler.emailNotification.reloadTaskAborted.recipients is an array, sendEmail() should send individual emails to everyone in that array sendEmail( globals.config.get('Butler.emailNotification.reloadTaskAborted.fromAdress'), - recipientEmails.concat(globals.config.get('Butler.emailNotification.reloadTaskAborted.toAdress')), + recipientEmails.concat(globals.config.get('Butler.emailNotification.reloadTaskAborted.recipients')), globals.config.get('Butler.emailNotification.reloadTaskAborted.priority'), globals.config.get('Butler.emailNotification.reloadTaskAborted.subject'), globals.config.get('Butler.emailNotification.reloadTaskAborted.bodyFileDirectory'), diff --git a/src/mqtt/mqtt_handlers.js b/src/mqtt/mqtt_handlers.js index 76f5272b..7307e6b0 100644 --- a/src/mqtt/mqtt_handlers.js +++ b/src/mqtt/mqtt_handlers.js @@ -7,6 +7,8 @@ var dict = require('dict'); // Load global variables and functions var globals = require('../globals'); +var qrsUtil = require('../qrs_util'); + module.exports.mqttInitHandlers = function () { // Handler for MQTT connect messages. Called when connection to MQTT broker has been established @@ -26,22 +28,20 @@ module.exports.mqttInitHandlers = function () { )} with client ID ${globals.mqttClient.options.clientId}`, ); - // Have Butler listen to all messages in the qliksense/ subtree - globals.mqttClient.subscribe('qliksense/#'); + // Have Butler listen to all messages in the topic subtree specified in the config file + globals.mqttClient.subscribe(globals.config.get('Butler.mqttConfig.subscriptionRootTopic')); }); // Handler for MQTT messages matching the previously set up subscription globals.mqttClient.on('message', function (topic, message) { try { - globals.logger.info('MQTT message received'); - globals.logger.info(topic.toString()); - globals.logger.info(message.toString()); + globals.logger.verbose(`MQTT message received. Topic=${topic.toString()}, Message=${message.toString()}`); // **MQTT message dispatch** // Start Sense task - if (topic == 'qliksense/start_task') { - globals.logger.verbose(`MQTT: Starting task ID ${message.toString()}.`); - globals.qrsUtil.senseStartTask.senseStartTask(message.toString()); + if (topic == globals.config.get('Butler.mqttConfig.taskStartTopic')) { + globals.logger.info(`MQTT: Starting task ID ${message.toString()}.`); + qrsUtil.senseStartTask.senseStartTask(message.toString()); } var array1, array2, serverName, userName; @@ -62,8 +62,7 @@ module.exports.mqttInitHandlers = function () { // directoryName = array2[0]; userName = array2[1]; - globals.logger.info('MQTT: Adding active user'); - // console.info('Adding active user'); + globals.logger.verbose(`MQTT: Adding active user: ${userName} on server ${serverName}`); globals.currentUsers.set(userName, serverName); // Add user as active // Build JSON of all active users @@ -86,7 +85,7 @@ module.exports.mqttInitHandlers = function () { // Send active user count messages to MQTT, one for each proxy node globals.currentUsersPerServer.forEach(function (value, key) { - globals.logger.info('MQTT: server:' + key + ', users:' + JSON.stringify(value)); + globals.logger.verbose('MQTT: server:' + key + ', users:' + JSON.stringify(value)); // console.info('server:' + key + ', users:' + JSON.stringify(value)); // console.log('========='); // console.log('server:' + key + ', # of users=' + globals.currentUsersPerServer.size); @@ -120,7 +119,7 @@ module.exports.mqttInitHandlers = function () { // directoryName = array2[0]; userName = array2[1]; - globals.logger.info('MQTT: Removing active user'); + globals.logger.verbose(`MQTT: Removing active user: ${userName}`); globals.currentUsers.delete(userName); // Remove user from list of active users // Build JSON of all active users diff --git a/src/package-lock.json b/src/package-lock.json index 884ff858..958d5396 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -16,7 +16,7 @@ "dict": "^1.4.0", "email-validator": "^2.0.4", "enigma.js": "^2.7.2", - "express-handlebars": "^5.2.0", + "express-handlebars": "^5.2.1", "fs-extra": "^9.1.0", "handlebars": "^4.7.7", "influx": "^5.7.0", @@ -25,7 +25,7 @@ "keyv": "^4.0.3", "keyv-file": "^0.2.0", "later": "^1.2.0", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "luxon": "^1.26.0", "mkdirp": "^1.0.4", "moment": "^2.29.1", @@ -48,7 +48,7 @@ "ws": "^7.4.3" }, "devDependencies": { - "eslint": "7.19.0", + "eslint": "7.20.0", "eslint-config-google": "0.14.0" }, "engines": { @@ -56,12 +56,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "dependencies": { - "@babel/highlight": "^7.12.13" + "@babel/highlight": "^7.10.4" } }, "node_modules/@babel/helper-validator-identifier": { @@ -90,6 +90,9 @@ "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" } }, "node_modules/@dabh/diagnostics": { @@ -209,6 +212,9 @@ "dev": true, "dependencies": { "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" } }, "node_modules/any-base": { @@ -881,15 +887,18 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.8.0" + } }, "node_modules/eslint": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.19.0.tgz", - "integrity": "sha512-CGlMgJY56JZ9ZSYhJuhow61lMPPjUzWmChFya71Z/jilVos7mR/jPgaEfVGgMBY5DshbKdG8Ezb8FDCHcoMEMg==", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.20.0.tgz", + "integrity": "sha512-qGi0CTcOGP2OtCQBgWZlQjcTuP0XkIpYFj25XtRTQSHC+umNnp7UMshr2G8SLsRFYDdAPFeHOsiteadmMH02Yw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.0.0", + "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.3.0", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -901,7 +910,7 @@ "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^2.0.0", "espree": "^7.3.1", - "esquery": "^1.2.0", + "esquery": "^1.4.0", "esutils": "^2.0.2", "file-entry-cache": "^6.0.0", "functional-red-black-tree": "^1.0.1", @@ -926,6 +935,15 @@ "table": "^6.0.4", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-config-google": { @@ -1121,13 +1139,13 @@ } }, "node_modules/express-handlebars": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-5.2.0.tgz", - "integrity": "sha512-kkty9fsldSuqDI/5ohU+EWNOWaPhJOPmVUZewMH+7522atj8QF8WbXzjZKBRloafxRcsPDG68jmfW7MGCYT/1g==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-5.2.1.tgz", + "integrity": "sha512-PTp5YZDcuJPqE01Qv5VAAS1DevNJLn6vwglerCwPKQC7wfLH6zr2uh1gwgTlWCqIjqJLLzMgOEE9YA5o0nBCmA==", "dependencies": { "glob": "^7.1.6", - "graceful-fs": "^4.2.4", - "handlebars": "^4.7.6" + "graceful-fs": "^4.2.6", + "handlebars": "^4.7.7" }, "engines": { "node": ">=10" @@ -1375,9 +1393,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" }, "node_modules/handle-thing": { "version": "2.0.0", @@ -1408,7 +1426,10 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "dev": true, + "engines": { + "node": ">=4" + } }, "node_modules/help-me": { "version": "1.1.0", @@ -1852,9 +1873,9 @@ } }, "node_modules/lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.get": { "version": "4.4.2", @@ -2914,6 +2935,9 @@ "dev": true, "dependencies": { "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, "node_modules/swagger-jsdoc": { @@ -3380,12 +3404,12 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "requires": { - "@babel/highlight": "^7.12.13" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-validator-identifier": { @@ -4165,12 +4189,12 @@ "dev": true }, "eslint": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.19.0.tgz", - "integrity": "sha512-CGlMgJY56JZ9ZSYhJuhow61lMPPjUzWmChFya71Z/jilVos7mR/jPgaEfVGgMBY5DshbKdG8Ezb8FDCHcoMEMg==", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.20.0.tgz", + "integrity": "sha512-qGi0CTcOGP2OtCQBgWZlQjcTuP0XkIpYFj25XtRTQSHC+umNnp7UMshr2G8SLsRFYDdAPFeHOsiteadmMH02Yw==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", + "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.3.0", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -4182,7 +4206,7 @@ "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^2.0.0", "espree": "^7.3.1", - "esquery": "^1.2.0", + "esquery": "^1.4.0", "esutils": "^2.0.2", "file-entry-cache": "^6.0.0", "functional-red-black-tree": "^1.0.1", @@ -4396,13 +4420,13 @@ "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" }, "express-handlebars": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-5.2.0.tgz", - "integrity": "sha512-kkty9fsldSuqDI/5ohU+EWNOWaPhJOPmVUZewMH+7522atj8QF8WbXzjZKBRloafxRcsPDG68jmfW7MGCYT/1g==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-5.2.1.tgz", + "integrity": "sha512-PTp5YZDcuJPqE01Qv5VAAS1DevNJLn6vwglerCwPKQC7wfLH6zr2uh1gwgTlWCqIjqJLLzMgOEE9YA5o0nBCmA==", "requires": { "glob": "^7.1.6", - "graceful-fs": "^4.2.4", - "handlebars": "^4.7.6" + "graceful-fs": "^4.2.6", + "handlebars": "^4.7.7" } }, "extend": { @@ -4630,9 +4654,9 @@ } }, "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" }, "handle-thing": { "version": "2.0.0", @@ -5054,9 +5078,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.get": { "version": "4.4.2", diff --git a/src/package.json b/src/package.json index 496354c6..65b47ce6 100644 --- a/src/package.json +++ b/src/package.json @@ -10,7 +10,7 @@ "dict": "^1.4.0", "email-validator": "^2.0.4", "enigma.js": "^2.7.2", - "express-handlebars": "^5.2.0", + "express-handlebars": "^5.2.1", "fs-extra": "^9.1.0", "handlebars": "^4.7.7", "influx": "^5.7.0", @@ -19,7 +19,7 @@ "keyv": "^4.0.3", "keyv-file": "^0.2.0", "later": "^1.2.0", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "luxon": "^1.26.0", "mkdirp": "^1.0.4", "moment": "^2.29.1", @@ -42,7 +42,7 @@ "ws": "^7.4.3" }, "devDependencies": { - "eslint": "7.19.0", + "eslint": "7.20.0", "eslint-config-google": "0.14.0" }, "homepage": "https://github.com/ptarmiganlabs/butler", diff --git a/src/udp/udp_handlers.js b/src/udp/udp_handlers.js index b5e089a9..b0cf293f 100644 --- a/src/udp/udp_handlers.js +++ b/src/udp/udp_handlers.js @@ -488,10 +488,22 @@ module.exports.udpInitSessionConnectionServer = function () { // Main handler for UDP messages relating to session and connection events globals.udpServerSessionConnectionSocket.on('message', async function (message, remote) { try { - var msg = message.toString().split(';'); - globals.logger.info(`SESSIONS: ${msg[0]}: ${msg[1]} for user ${msg[2]}/${msg[3]}`); - - // Send notification to MS Teams, if enabled + // Message parts + // 0: Message type. Possible values are /proxy-connection/, /proxy-session/ + // 1: Host + // 2: Command + // 3: User directory + // 4: user ID + // 5: Origin + // 6: Context + // 7: Message. Can contain single quotes and semicolon - handle with care + + var msgTmp1 = message.toString().split(';'), + msg = msgTmp1.slice(0, 7); + + globals.logger.info(`SESSIONS: ${msg[1]}: ${msg[2]} for user ${msg[3]}/${msg[4]}`); + + // Send notification to Slack, if enabled if ( globals.config.has('Butler.slackNotification.enable') && globals.config.has('Butler.slackNotification.userSessionEvents.enable') && @@ -505,7 +517,7 @@ module.exports.udpInitSessionConnectionServer = function () { type: 'section', text: { type: 'plain_text', - text: msg[1] + ', user: ' + msg[2] + '/' + msg[3] + ' on server ' + msg[0], + text: msg[2] + ', user: ' + msg[3] + '/' + msg[4] + ' on server ' + msg[1], }, }, ], @@ -517,7 +529,23 @@ module.exports.udpInitSessionConnectionServer = function () { webhookUrl: globals.config.get('Butler.slackNotification.userSessionEvents.webhookURL'), }; - let res = slackApi.slackSend(slackConfig, globals.logger); + // Is the user referenced by this event on the exclude list? If so don't sent a notification + // msg[3] = user directory + // msg[4] = userId + let sendMsg = true; + if (globals.config.has('Butler.slackNotification.userSessionEvents.excludeUser')) { + let excludeUsers = globals.config.get('Butler.slackNotification.userSessionEvents.excludeUser'); + if ( excludeUsers.some(item => { + return (item.directory == msg[3] && item.userId == msg[4]); + })) { + // User found in exclude list + sendMsg = false; + } + } + + if (sendMsg) { + let res = slackApi.slackSend(slackConfig, globals.logger); + } } // Send notification to MS Teams, if enabled @@ -527,33 +555,42 @@ module.exports.udpInitSessionConnectionServer = function () { globals.config.get('Butler.teamsNotification.enable') == true && globals.config.get('Butler.teamsNotification.userSessionEvents.enable') == true ) { - await globals.teamsUserSessionObj.send( - JSON.stringify({ - '@type': 'MessageCard', - '@context': 'https://schema.org/extensions', - summary: msg[1] + ' for user ' + msg[2] + '/' + msg[3], - themeColor: '0078D7', - title: msg[1] + ' for user ' + msg[2] + '/' + msg[3] + ' on server ' + msg[0], - }), - ); - } - - // Handle session events - if (msg[1] == 'Start session') { - globals.mqttClient.publish(globals.config.get('Butler.mqttConfig.sessionStartTopic'), msg[0] + ': ' + msg[2] + '/' + msg[3]); - } - - if (msg[1] == 'Stop session') { - globals.mqttClient.publish(globals.config.get('Butler.mqttConfig.sessionStopTopic'), msg[0] + ': ' + msg[2] + '/' + msg[3]); - } - - // Handle connection events - if (msg[1] == 'Open connection') { - globals.mqttClient.publish(globals.config.get('Butler.mqttConfig.connectionOpenTopic'), msg[0] + ': ' + msg[2] + '/' + msg[3]); + // Is the user referenced by this event on the exclude list? If so don't sent a notification + // msg[3] = user directory + // msg[4] = userId + let sendMsg = true; + if (globals.config.has('Butler.teamsNotification.userSessionEvents.excludeUser')) { + let excludeUsers = globals.config.get('Butler.teamsNotification.userSessionEvents.excludeUser'); + if ( excludeUsers.some(item => { + return (item.directory == msg[3] && item.userId == msg[4]); + })) { + // User found in exclude list + sendMsg = false; + } + } + + if (sendMsg) { + await globals.teamsUserSessionObj.send( + JSON.stringify({ + '@type': 'MessageCard', + '@context': 'https://schema.org/extensions', + summary: msg[2] + ' for user ' + msg[3] + '/' + msg[4], + themeColor: '0078D7', + title: msg[2] + ' for user ' + msg[3] + '/' + msg[4] + ' on server ' + msg[1], + }), + ); + } } - if (msg[1] == 'Close connection') { - globals.mqttClient.publish(globals.config.get('Butler.mqttConfig.connectionCloseTopic'), msg[0] + ': ' + msg[2] + '/' + msg[3]); + // Send MQTT messages + if (msg[2] == 'Start session') { + globals.mqttClient.publish(globals.config.get('Butler.mqttConfig.sessionStartTopic'), msg[1] + ': ' + msg[3] + '/' + msg[4]); + } else if (msg[2] == 'Stop session') { + globals.mqttClient.publish(globals.config.get('Butler.mqttConfig.sessionStopTopic'), msg[1] + ': ' + msg[3] + '/' + msg[4]); + } else if (msg[2] == 'Open connection') { + globals.mqttClient.publish(globals.config.get('Butler.mqttConfig.connectionOpenTopic'), msg[1] + ': ' + msg[3] + '/' + msg[4]); + } else if (msg[2] == 'Close connection') { + globals.mqttClient.publish(globals.config.get('Butler.mqttConfig.connectionCloseTopic'), msg[1] + ': ' + msg[3] + '/' + msg[4]); } } catch (err) { globals.logger.error(`SESSIONS: Error processing user session event: ${err}`);