From 0743f1b1639f6170f91309b3ae73b30020b3b83c Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Sun, 26 Feb 2017 17:26:15 -0700 Subject: [PATCH 01/26] working --- index.js | 2 +- lib/command_factory.js | 129 ----- lib/slack_monkey_patch.js | 38 -- scripts/stackstorm.js | 444 ------------------ src/lib/command_executor.js | 146 ++++++ src/lib/command_factory.js | 103 ++++ {lib => src/lib}/format_command.js | 2 +- {lib => src/lib}/format_data.js | 0 .../lib/messaging_handler.js | 86 +++- src/lib/stackstorm_api.js | 209 +++++++++ {lib => src/lib}/utils.js | 37 -- src/stackstorm.js | 87 ++++ 12 files changed, 613 insertions(+), 670 deletions(-) delete mode 100644 lib/command_factory.js delete mode 100644 lib/slack_monkey_patch.js delete mode 100644 scripts/stackstorm.js create mode 100644 src/lib/command_executor.js create mode 100644 src/lib/command_factory.js rename {lib => src/lib}/format_command.js (94%) rename {lib => src/lib}/format_data.js (100%) rename lib/post_data.js => src/lib/messaging_handler.js (72%) create mode 100644 src/lib/stackstorm_api.js rename {lib => src/lib}/utils.js (71%) create mode 100644 src/stackstorm.js diff --git a/index.js b/index.js index 19b2d29..02175eb 100644 --- a/index.js +++ b/index.js @@ -18,7 +18,7 @@ limitations under the License. var fs = require('fs'); var path = require('path'); -var SCRIPTS_PATH = path.resolve(__dirname, 'scripts'); +var SCRIPTS_PATH = path.resolve(__dirname, 'src'); module.exports = function(robot, scripts) { diff --git a/lib/command_factory.js b/lib/command_factory.js deleted file mode 100644 index 1466adc..0000000 --- a/lib/command_factory.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - Licensed to the StackStorm, Inc ('StackStorm') under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and -limitations under the License. -*/ - -"use strict"; - -var _ = require('lodash'); -var utils = require('./utils.js'); - - -/** - * Manage command for stackstorm, storing them in robot and providing - * lookups given command literals. - */ -function CommandFactory(robot) { - this.robot = robot; - - // Stores a list of hubot command strings - this.st2_hubot_commands = []; - - // Maps command name (pk) to the action alias object - this.st2_commands_name_map = {}; - - // Maps command format to the action alias object - this.st2_commands_format_map = {}; - - // Maps command format to a compiled regex for that format - this.st2_commands_regex_map = {}; -} - -CommandFactory.prototype.getRegexForFormatString = function(format) { - var extra_params, regex_str, regex; - - // Note: We replace format parameters with ([\s\S]+?) and allow arbitrary - // number of key=value pairs at the end of the string - // Format parameters with default value {{param=value}} are allowed to - // be skipped. - // Note that we use "[\s\S]" instead of "." to allow multi-line values - // and multi-line commands in general. - extra_params = '(\\s+(\\S+)\\s*=("([\\s\\S]*?)"|\'([\\s\\S]*?)\'|({[\\s\\S]*?})|(\\S+))\\s*)*'; - regex_str = format.replace(/(\s*){{\s*\S+\s*=\s*(?:({.+?}|.+?))\s*}}(\s*)/g, '\\s*($1([\\s\\S]+?)$3)?\\s*'); - regex_str = regex_str.replace(/\s*{{.+?}}\s*/g, '\\s*([\\s\\S]+?)\\s*'); - regex = new RegExp('^\\s*' + regex_str + extra_params + '\\s*$', 'i'); - return regex; -}; - -CommandFactory.prototype.addCommand = function(command, name, format, action_alias, flag) { - var compiled_template, context, command_string, regex; - - if (!format) { - this.robot.logger.error('Skipped empty command.'); - return; - } - - context = { - robotName: this.robot.name, - command: command - }; - - compiled_template = _.template('hubot ${command}'); - command_string = compiled_template(context); - - if (!flag || flag === utils.REPRESENTATION) { - this.st2_hubot_commands.push(command_string); - this.st2_commands_name_map[name] = action_alias; - this.st2_commands_format_map[format] = action_alias; - regex = this.getRegexForFormatString(format); - this.st2_commands_regex_map[format] = regex; - } - if ((!flag || flag === utils.DISPLAY) && this.robot.commands.indexOf(command_string) === -1) { - this.robot.commands.push(command_string); - } - - this.robot.logger.debug('Added command: ' + command); -}; - -CommandFactory.prototype.removeCommands = function() { - var i, command, array_index; - - for (i = 0; i < this.st2_hubot_commands.length; i++) { - command = this.st2_hubot_commands[i]; - array_index = this.robot.commands.indexOf(command); - - if (array_index !== -1) { - this.robot.commands.splice(array_index, 1); - } - } - - this.st2_hubot_commands = []; - this.st2_commands_name_map = {}; - this.st2_commands_format_map = {}; - this.st2_commands_regex_map = {}; -}; - -CommandFactory.prototype.getMatchingCommand = function(command) { - var result, common_prefix, command_name, command_arguments, format_strings, - i, format_string, regex, action_alias; - - // 1. Try to use regex search - this works for commands with a format string - format_strings = Object.keys(this.st2_commands_regex_map); - - for (i = 0; i < format_strings.length; i++) { - format_string = format_strings[i]; - regex = this.st2_commands_regex_map[format_string]; - if (regex.test(command)) { - action_alias = this.st2_commands_format_map[format_string]; - command_name = action_alias.name; - - return [command_name, format_string, action_alias]; - } - } - - return null; -}; - -module.exports = CommandFactory; diff --git a/lib/slack_monkey_patch.js b/lib/slack_monkey_patch.js deleted file mode 100644 index 360bf5d..0000000 --- a/lib/slack_monkey_patch.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - Licensed to the StackStorm, Inc ('StackStorm') under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -function sendMessageRaw(message) { - /*jshint validthis:true */ - message['channel'] = this.id; - message['parse'] = 'none'; - this._client._send(message); -} - -function patchSendMessage(robot) { - // We monkey patch sendMessage function to send "parse" argument with the message so the text is not - // formatted and parsed on the server side. - // NOTE / TODO: We can get rid of this nasty patch once our node-slack-client and hubot-slack pull - // requests are merged. - if (robot.adapter && robot.adapter.constructor && robot.adapter.constructor.name === 'SlackBot') { - for (var channel in robot.adapter.client.channels) { - robot.adapter.client.channels[channel].sendMessage = sendMessageRaw.bind(robot.adapter.client.channels[channel]); - } - } -} - - -exports.patchSendMessage = patchSendMessage; diff --git a/scripts/stackstorm.js b/scripts/stackstorm.js deleted file mode 100644 index 9d83c8d..0000000 --- a/scripts/stackstorm.js +++ /dev/null @@ -1,444 +0,0 @@ -// Licensed to the StackStorm, Inc ('StackStorm') under one or more -// contributor license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright ownership. -// The ASF licenses this file to You under the Apache License, Version 2.0 -// (the "License"); you may not use this file except in compliance with -// the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Description: -// StackStorm hubot integration -// -// Dependencies: -// -// -// Configuration: -// ST2_API - FQDN + port to StackStorm endpoint -// ST2_ROUTE - StackStorm notification route name -// ST2_COMMANDS_RELOAD_INTERVAL - Reload interval for commands -// -// Notes: -// Command list is automatically generated from StackStorm ChatOps metadata -// - -"use strict"; - -var _ = require('lodash'), - util = require('util'), - env = _.clone(process.env), - Promise = require('rsvp').Promise, - utils = require('../lib/utils.js'), - slack_monkey_patch = require('../lib/slack_monkey_patch.js'), - formatCommand = require('../lib/format_command.js'), - formatData = require('../lib/format_data.js'), - postData = require('../lib/post_data.js'), - CommandFactory = require('../lib/command_factory.js'), - st2client = require('st2client'), - uuid = require('node-uuid') - ; - -// Setup the Environment -env.ST2_API = env.ST2_API || 'http://localhost:9101'; -env.ST2_ROUTE = env.ST2_ROUTE || null; -env.ST2_WEBUI_URL = env.ST2_WEBUI_URL || null; - -// Optional authentication info -env.ST2_AUTH_USERNAME = env.ST2_AUTH_USERNAME || null; -env.ST2_AUTH_PASSWORD = env.ST2_AUTH_PASSWORD || null; - -// Optional authentication token -env.ST2_AUTH_TOKEN = env.ST2_AUTH_TOKEN || null; - -// Optional API key -env.ST2_API_KEY = env.ST2_API_KEY || null; - -// slack attachment colors -env.ST2_SLACK_SUCCESS_COLOR = env.ST2_SLACK_SUCCESS_COLOR || 'dfdfdf'; -env.ST2_SLACK_FAIL_COLOR = env.ST2_SLACK_FAIL_COLOR || 'danger'; - -// Optional, if not provided, we infer it from the API URL -env.ST2_AUTH_URL = env.ST2_AUTH_URL || null; - -// Command reload interval in seconds -env.ST2_COMMANDS_RELOAD_INTERVAL = parseInt(env.ST2_COMMANDS_RELOAD_INTERVAL || 120, 10); - -// Cap message length to a certain number of characters. -env.ST2_MAX_MESSAGE_LENGTH = parseInt(env.ST2_MAX_MESSAGE_LENGTH || 500, 10); - -// Constants -// Fun human-friendly commands. Use %s for payload output. -var START_MESSAGES = [ - "I'll take it from here! Your execution ID for reference is %s", - "Got it! Remember %s as your execution ID", - "I'm on it! Your execution ID is %s", - "Let me get right on that. Remember %s as your execution ID", - "Always something with you. :) I'll take care of that. Your ID is %s", - "I have it covered. Your execution ID is %s", - "Let me start up the machine! Your execution ID is %s", - "I'll throw that task in the oven and get cookin'! Your execution ID is %s", - "Want me to take that off your hand? You got it! Don't forget your execution ID: %s", - "River Tam will get it done with her psychic powers. Your execution ID is %s" -]; - -var ERROR_MESSAGES = [ - "I'm sorry, Dave. I'm afraid I can't do that. {~} %s" -]; - -var TWOFACTOR_MESSAGE = "This action requires two-factor auth! Waiting for your confirmation."; - - -module.exports = function(robot) { - slack_monkey_patch.patchSendMessage(robot); - - var self = this; - - var promise = Promise.resolve(); - - var url = utils.parseUrl(env.ST2_API); - - var opts = { - protocol: url.protocol, - host: url.hostname, - port: url.port, - prefix: url.path, - rejectUnauthorized: false - }; - - var api = st2client(opts); - - if (env.ST2_API_KEY) { - api.setKey({ key: env.ST2_API_KEY }); - } else if (env.ST2_AUTH_TOKEN) { - api.setToken({ token: env.ST2_AUTH_TOKEN }); - } - - function authenticate() { - api.removeListener('expiry', authenticate); - - // API key gets precedence 1 - if (env.ST2_API_KEY) { - robot.logger.info('Using ST2_API_KEY as authentication. Expiry will lead to bot exit.'); - return Promise.resolve(); - } - // Auth token gets precedence 2 - if (env.ST2_AUTH_TOKEN) { - robot.logger.info('Using ST2_AUTH_TOKEN as authentication. Expiry will lead to bot exit.'); - return Promise.resolve(); - } - - robot.logger.info('Requesting a token...'); - - var url = utils.parseUrl(env.ST2_AUTH_URL); - - var client = st2client({ - auth: { - protocol: url.protocol, - host: url.hostname, - port: url.port, - prefix: url.path - } - }); - - return client.authenticate(env.ST2_AUTH_USERNAME, env.ST2_AUTH_PASSWORD) - .then(function (token) { - robot.logger.info('Token received. Expiring ' + token.expiry); - api.setToken(token); - client.on('expiry', authenticate); - }) - .catch(function (err) { - robot.logger.error('Failed to authenticate: ' + err.message); - - throw err; - }); - } - - if (env.ST2_API_KEY || env.ST2_AUTH_TOKEN || env.ST2_AUTH_USERNAME || env.ST2_AUTH_PASSWORD) { - // If using username and password then all are required. - if ((env.ST2_AUTH_USERNAME || env.ST2_AUTH_PASSWORD) && - !(env.ST2_AUTH_USERNAME && env.ST2_AUTH_PASSWORD && env.ST2_AUTH_URL)) { - throw new Error('Env variables ST2_AUTH_USERNAME, ST2_AUTH_PASSWORD and ST2_AUTH_URL should only be used together.'); - } - promise = authenticate(); - } - - // Pending 2-factor auth commands - if (env.HUBOT_2FA) { - var twofactor = {}; - robot.logger.info('Two-factor auth is enabled'); - } - - // factory to manage commands - var command_factory = new CommandFactory(robot); - - // formatter to manage per adapter message formatting. - var formatter = formatData.getFormatter(robot.adapterName, robot); - - // handler to manage per adapter message post-ing. - var postDataHandler = postData.getDataPostHandler(robot.adapterName, robot, formatter); - - var loadCommands = function() { - robot.logger.info('Loading commands....'); - - api.actionAlias.list() - .then(function (aliases) { - // Remove all the existing commands - command_factory.removeCommands(); - - _.each(aliases, function (alias) { - var name = alias.name; - var formats = alias.formats; - var description = alias.description; - - if (alias.enabled === false) { - return; - } - - if (!formats || formats.length === 0) { - robot.logger.error('No formats specified for command: ' + name); - return; - } - - _.each(formats, function (format) { - var command = formatCommand(robot.logger, name, format.display || format, description); - command_factory.addCommand(command, name, format.display || format, alias, - format.display ? utils.DISPLAY : false); - - _.each(format.representation, function (representation) { - command = formatCommand(robot.logger, name, representation, description); - command_factory.addCommand(command, name, representation, alias, utils.REPRESENTATION); - }); - }); - }); - - robot.logger.info(command_factory.st2_hubot_commands.length + ' commands are loaded'); - }) - .catch(function (err) { - var error_msg = 'Failed to retrieve commands from "%s": %s'; - robot.logger.error(util.format(error_msg, env.ST2_API, err.message)); - }); - }; - - var sendAck = function (msg, res) { - var history_url = utils.getExecutionHistoryUrl(res.execution); - var history = history_url ? util.format(' (details available at %s)', history_url) : ''; - - if (res.actionalias && res.actionalias.ack) { - if (res.actionalias.ack.enabled === false) { - return; - } else if (res.actionalias.ack.append_url === false) { - history = ''; - } - } - - if (res.message) { - return msg.send(res.message + history); - } - - var message = util.format(_.sample(START_MESSAGES), res.execution.id); - return msg.send(message + history); - }; - - var createExecution = function (msg, payload) { - robot.logger.debug('Sending command payload:', JSON.stringify(payload)); - - api.aliasExecution.create(payload) - .then(function (res) { sendAck(msg, res); }) - .catch(function (err) { - // Compatibility with older StackStorm versions - if (err.status === 200) { - return sendAck(msg, { execution: { id: err.message } }); - } - robot.logger.error('Failed to create an alias execution:', err); - var addressee = utils.normalizeAddressee(msg, robot.adapterName); - var message = util.format(_.sample(ERROR_MESSAGES), err.message); - if (err.requestId) { - message = util.format( - message, - util.format('; Use request ID %s to grep st2 api logs.', err.requestId)); - } - postDataHandler.postData({ - whisper: false, - user: addressee.name, - channel: addressee.room, - message: message, - extra: { - color: '#F35A00' - } - }); - throw err; - }); - }; - - var executeCommand = function(msg, command_name, format_string, command, action_alias) { - var addressee = utils.normalizeAddressee(msg, robot.adapterName); - var payload = { - 'name': command_name, - 'format': format_string, - 'command': command, - 'user': addressee.name, - 'source_channel': addressee.room, - 'notification_route': env.ST2_ROUTE || 'hubot' - }; - - if (utils.enable2FA(action_alias)) { - var twofactor_id = uuid.v4(); - robot.logger.debug('Requested an action that requires 2FA. Guid: ' + twofactor_id); - msg.send(TWOFACTOR_MESSAGE); - api.executions.create({ - 'action': env.HUBOT_2FA, - 'parameters': { - 'uuid': twofactor_id, - 'user': addressee.name, - 'channel': addressee.room, - 'hint': action_alias.description - } - }); - twofactor[twofactor_id] = { - 'msg': msg, - 'payload': payload - }; - } else { - createExecution(msg, payload); - } - - - }; - - robot.respond(/([\s\S]+?)$/i, function(msg) { - var command, result, command_name, format_string, action_alias; - - // Normalize the command and remove special handling provided by the chat service. - // e.g. slack replace quote marks with left double quote which would break behavior. - command = formatter.normalizeCommand(msg.match[1]); - - result = command_factory.getMatchingCommand(command); - - if (!result) { - // No command found - return; - } - - command_name = result[0]; - format_string = result[1]; - action_alias = result[2]; - - executeCommand(msg, command_name, format_string, command, action_alias); - }); - - robot.router.post('/hubot/st2', function(req, res) { - var data; - - try { - if (req.body.payload) { - data = JSON.parse(req.body.payload); - } else { - data = req.body; - } - // Special handler to try and figure out when a hipchat message - // is a whisper: - if (robot.adapterName === 'hipchat' && !data.whisper && data.channel.indexOf('@') > -1 ) { - data.whisper = true; - robot.logger.debug('Set whisper to true for hipchat message'); - } - - postDataHandler.postData(data); - - res.send('{"status": "completed", "msg": "Message posted successfully"}'); - } catch (e) { - robot.logger.error("Unable to decode JSON: " + e); - robot.logger.error(e.stack); - res.send('{"status": "failed", "msg": "An error occurred trying to post the message: ' + e + '"}'); - } - }); - - var commands_load_interval; - - function start() { - api.stream.listen().catch(function (err) { - robot.logger.error('Unable to connect to stream:', err); - }).then(function (source) { - source.onerror = function (err) { - // TODO: squeeze a little bit more info out of evensource.js - robot.logger.error('Stream error:', err); - }; - source.addEventListener('st2.announcement__chatops', function (e) { - var data; - - robot.logger.debug('Chatops message received:', e.data); - - if (e.data) { - data = JSON.parse(e.data).payload; - } else { - data = e.data; - } - - // Special handler to try and figure out when a hipchat message - // is a whisper: - if (robot.adapterName === 'hipchat' && !data.whisper && data.channel.indexOf('@') > -1 ) { - data.whisper = true; - robot.logger.debug('Set whisper to true for hipchat message'); - } - - postDataHandler.postData(data); - - }); - - if (env.HUBOT_2FA) { - source.addEventListener('st2.announcement__2fa', function (e) { - var data; - - robot.logger.debug('Successfull two-factor auth:', e.data); - - if (e.data) { - data = JSON.parse(e.data).payload; - } else { - data = e.data; - } - - var executionData = twofactor[data.uuid]; - createExecution(executionData.msg, executionData.payload); - delete twofactor[data.uuid]; - - }); - } - - }); - - // Add an interval which tries to re-load the commands - commands_load_interval = setInterval(loadCommands.bind(self), (env.ST2_COMMANDS_RELOAD_INTERVAL * 1000)); - - // Initial command loading - loadCommands(); - - // Install SIGUSR2 handler which reloads the command - install_sigusr2_handler(); - } - - function stop() { - clearInterval(commands_load_interval); - api.stream.listen().then(function (source) { - source.removeAllListeners(); - source.close(); - }); - } - - function install_sigusr2_handler() { - process.on('SIGUSR2', function() { - loadCommands(); - }); - } - - // Authenticate with StackStorm backend and then call start. - // On a failure to authenticate log the error but do not quit. - return promise.then(function () { - start(); - return stop; - }); -}; diff --git a/src/lib/command_executor.js b/src/lib/command_executor.js new file mode 100644 index 0000000..4dd413d --- /dev/null +++ b/src/lib/command_executor.js @@ -0,0 +1,146 @@ +"use strict"; + +var _ = require('lodash'), + env = _.clone(process.env), + utils = require('./utils.js'), + util = require('util'), + postData = require('./post_data.js'); + + +// Constants +// Fun human-friendly commands. Use %s for payload output. +var START_MESSAGES = [ + "I'll take it from here! Your execution ID for reference is %s", + "Got it! Remember %s as your execution ID", + "I'm on it! Your execution ID is %s", + "Let me get right on that. Remember %s as your execution ID", + "Always something with you. :) I'll take care of that. Your ID is %s", + "I have it covered. Your execution ID is %s", + "Let me start up the machine! Your execution ID is %s", + "I'll throw that task in the oven and get cookin'! Your execution ID is %s", + "Want me to take that off your hand? You got it! Don't forget your execution ID: %s", + "River Tam will get it done with her psychic powers. Your execution ID is %s" +]; + +var ERROR_MESSAGES = [ + "I'm sorry, Dave. I'm afraid I can't do that. {~} %s" +]; + + +var setup_postback_route = function (robot) { + robot.router.post('/hubot/st2', function (req, res) { + var data; + + try { + if (req.body.payload) { + data = JSON.parse(req.body.payload); + } else { + data = req.body; + } + // Special handler to try and figure out when a hipchat message + // is a whisper: + if (robot.adapterName === 'hipchat' && !data.whisper && data.channel.indexOf('@') > -1) { + data.whisper = true; + robot.logger.debug('Set whisper to true for hipchat message'); + } + + postDataHandler.postData(data); + + res.send('{"status": "completed", "msg": "Message posted successfully"}'); + } catch (e) { + robot.logger.error("Unable to decode JSON: " + e); + robot.logger.error(e.stack); + res.send('{"status": "failed", "msg": "An error occurred trying to post the message: ' + e + '"}'); + } + }); +} + +function CommandExecutor(robot) { + this.robot = robot; + this.st2_action_aliases = []; + setup_postback_route(robot); +} + +CommandExecutor.prototype.sendAck = function (msg, res) { + var history_url = utils.getExecutionHistoryUrl(res.execution); + var history = history_url ? util.format(' (details available at %s)', history_url) : ''; + + if (res.actionalias && res.actionalias.ack) { + if (res.actionalias.ack.enabled === false) { + return; + } else if (res.actionalias.ack.append_url === false) { + history = ''; + } + } + + if (res.message) { + return msg.send(res.message + history); + } + + var message = util.format(_.sample(START_MESSAGES), res.execution.id); + return msg.send(message + history); +}; + +CommandExecutor.prototype.createExecution = function (msg, payload) { + this.robot.logger.debug('Sending command payload:', JSON.stringify(payload)); + + api.aliasExecution.create(payload) + .then(function (res) { this.sendAck(msg, res); }) + .catch(function (err) { + // Compatibility with older StackStorm versions + if (err.status === 200) { + return this.sendAck(msg, { execution: { id: err.message } }); + } + this.robot.logger.error('Failed to create an alias execution:', err); + var addressee = utils.normalizeAddressee(msg, robot.adapterName); + var message = util.format(_.sample(ERROR_MESSAGES), err.message); + if (err.requestId) { + message = util.format( + message, + util.format('; Use request ID %s to grep st2 api logs.', err.requestId)); + } + postDataHandler.postData({ + whisper: false, + user: addressee.name, + channel: addressee.room, + message: message, + extra: { + color: '#F35A00' + } + }); + throw err; + }); +}; + +CommandExecutor.prototype.execute = function (msg, command_name, format_string, command, action_alias) { + var addressee = utils.normalizeAddressee(msg, this.robot.adapterName); + var payload = { + 'name': command_name, + 'format': format_string, + 'command': command, + 'user': addressee.name, + 'source_channel': addressee.room, + 'notification_route': env.ST2_ROUTE || 'hubot' + }; + + if (utils.enable2FA(action_alias)) { + var twofactor_id = uuid.v4(); + this.robot.logger.debug('Requested an action that requires 2FA. Guid: ' + twofactor_id); + msg.send(TWOFACTOR_MESSAGE); + api.executions.create({ + 'action': env.HUBOT_2FA, + 'parameters': { + 'uuid': twofactor_id, + 'user': addressee.name, + 'channel': addressee.room, + 'hint': action_alias.description + } + }); + twofactor[twofactor_id] = { + 'msg': msg, + 'payload': payload + }; + } else { + this.createExecution(msg, payload); + } +} \ No newline at end of file diff --git a/src/lib/command_factory.js b/src/lib/command_factory.js new file mode 100644 index 0000000..db65ae3 --- /dev/null +++ b/src/lib/command_factory.js @@ -0,0 +1,103 @@ +/* + Licensed to the StackStorm, Inc ('StackStorm') under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +var _ = require('lodash'), + utils = require('./utils.js'), + formatCommand = require('./format_command.js'), + CommandExecutor = require('./command_executor'); + + +var getRegexForFormatString = function (format) { + var extra_params, regex_str, regex; + + // Note: We replace format parameters with ([\s\S]+?) and allow arbitrary + // number of key=value pairs at the end of the string + // Format parameters with default value {{param=value}} are allowed to + // be skipped. + // Note that we use "[\s\S]" instead of "." to allow multi-line values + // and multi-line commands in general. + extra_params = '(\\s+(\\S+)\\s*=("([\\s\\S]*?)"|\'([\\s\\S]*?)\'|({[\\s\\S]*?})|(\\S+))\\s*)*'; + regex_str = format.replace(/(\s*){{\s*\S+\s*=\s*(?:({.+?}|.+?))\s*}}(\s*)/g, '\\s*($1([\\s\\S]+?)$3)?\\s*'); + regex_str = regex_str.replace(/\s*{{.+?}}\s*/g, '\\s*([\\s\\S]+?)\\s*'); + regex = new RegExp('^\\s*' + regex_str + extra_params + '\\s*$', 'i'); + return regex; +}; + + +/** + * Manage command for stackstorm, storing them in robot and providing + * lookups given command literals. + */ +function CommandFactory(robot) { + this.robot = robot; + this.st2_action_aliases = []; + this.command_executor = new CommandExecutor(robot); +} + +CommandFactory.prototype.addCommand = function (action_alias, formatter) { + if (action_alias.enabled === false) { + return; + } + + if (!action_alias.formats || action_alias.formats.length === 0) { + this.robot.logger.error('No formats specified for command: ' + action_alias.name); + return; + } + + var regexes = []; + + _.each(action_alias.formats, function (format) { + action_alias.formatted_command = formatCommand(action_alias.name, format.display || format, action_alias.description); + + var context = { + robotName: this.robot.name, + command: action_alias.formatted_command + }; + + var compiled_template = _.template('hubot ${command}'); + action_alias.command_string = compiled_template(context); + + this.robot.commands.push(action_alias.formatted_command); + if (format.display) { + _.each(format.representation, function (representation) { + regexes.push(getRegexForFormatString(representation)); + }); + } else { + regexes.push(getRegexForFormatString(format)); + } + }); + + action_alias.regexes = regexes; + this.st2_action_aliases.push(action_alias); + + this.robot.listen(function (msg) { + var command = formatter.normalizeCommand(msg.text); + _.each(regexes, function (regex) { + if (regex.test(command)) { + return true; + } + }); + return false; + }, { id: action_alias.name }, function (response) { + this.command_executor.execute(response, ); + executeCommand(msg, command_name, format_string, command, action_alias); + }); +}; + +module.exports = CommandFactory; diff --git a/lib/format_command.js b/src/lib/format_command.js similarity index 94% rename from lib/format_command.js rename to src/lib/format_command.js index ff8fd92..833c574 100644 --- a/lib/format_command.js +++ b/src/lib/format_command.js @@ -19,7 +19,7 @@ limitations under the License. var _ = require('lodash'); -module.exports = function(logger, name, format, description) { +module.exports = function(name, format, description) { var context, template_str, compiled_template, command; if (!format) { diff --git a/lib/format_data.js b/src/lib/format_data.js similarity index 100% rename from lib/format_data.js rename to src/lib/format_data.js diff --git a/lib/post_data.js b/src/lib/messaging_handler.js similarity index 72% rename from lib/post_data.js rename to src/lib/messaging_handler.js index 6089d55..fd70781 100644 --- a/lib/post_data.js +++ b/src/lib/messaging_handler.js @@ -22,14 +22,27 @@ var env = process.env, utils = require('./utils.js'); /* - SlackDataPostHandler. + SlackMessagingHandler. */ -function SlackDataPostHandler(robot, formatter) { +function SlackMessagingHandler(robot, formatter) { this.robot = robot; this.formatter = formatter; + + var sendMessageRaw = function(message) { + /*jshint validthis:true */ + message['channel'] = this.id; + message['parse'] = 'none'; + this._client._send(message); + }; + + if (robot.adapter && robot.adapter.constructor && robot.adapter.constructor.name === 'SlackBot') { + for (var channel in robot.adapter.client.channels) { + robot.adapter.client.channels[channel].sendMessage = sendMessageRaw.bind(robot.adapter.client.channels[channel]); + } + } } -SlackDataPostHandler.prototype.postData = function(data) { +SlackMessagingHandler.prototype.postData = function(data) { var recipient, attachment_color, split_message, attachment, pretext = ""; @@ -80,15 +93,22 @@ SlackDataPostHandler.prototype.postData = function(data) { } }; +SlackMessagingHandler.normalizeAddressee = function(msg) { + return { + name: msg.message.user.name, + room: msg.message.room + }; +}; + /* - HipchatDataPostHandler. + HipchatMessagingHandler. */ -function HipchatDataPostHandler(robot, formatter) { +function HipchatMessagingHandler(robot, formatter) { this.robot = robot; this.formatter = formatter; } -HipchatDataPostHandler.prototype.postData = function(data) { +HipchatMessagingHandler.prototype.postData = function(data) { var recipient, split_message, formatted_message, pretext = ""; @@ -123,15 +143,27 @@ HipchatDataPostHandler.prototype.postData = function(data) { } }; +SlackMessagingHandler.normalizeAddressee = function(msg) { + var name = msg.message.user.name; + var room = msg.message.room; + if (room === undefined) { + room = msg.message.user.jid; + } + return { + name: name, + room: room + }; +}; + /* - Yammer Handler. + YammerMessagingHandler. */ -function YammerDataPostHandler(robot, formatter) { +function YammerMessagingHandler(robot, formatter) { this.robot = robot; this.formatter = formatter; } -YammerDataPostHandler.prototype.postData = function(data) { +YammerMessagingHandler.prototype.postData = function(data) { var recipient, split_message, formatted_message, text = ""; @@ -156,15 +188,22 @@ YammerDataPostHandler.prototype.postData = function(data) { this.robot.send.call(this.robot, recipient, formatted_message); }; +YammerMessagingHandler.normalizeAddressee = function(msg) { + return { + name: String(msg.message.user.thread_id), + room: msg.message.room + }; +}; + /* - DefaultDataPostHandler. + DefaultMessagingHandler. */ -function DefaultFormatter(robot, formatter) { +function DefaultMessagingHandler(robot, formatter) { this.robot = robot; this.formatter = formatter; } -DefaultFormatter.prototype.postData = function(data) { +DefaultMessagingHandler.prototype.postData = function(data) { var recipient, split_message, formatted_message, text = ""; @@ -189,20 +228,27 @@ DefaultFormatter.prototype.postData = function(data) { this.robot.messageRoom.call(this.robot, recipient, formatted_message); }; -var dataPostHandlers = { - 'slack': SlackDataPostHandler, - 'hipchat': HipchatDataPostHandler, - 'yammer': YammerDataPostHandler, - 'default': DefaultFormatter +DefaultMessagingHandler.normalizeAddressee = function(msg) { + return { + room: msg.message.user.room, + name: msg.message.user.name + }; +}; + +var messagingHandlers = { + 'slack': SlackMessagingHandler, + 'hipchat': HipchatMessagingHandler, + 'yammer': YammerMessagingHandler, + 'default': DefaultMessagingHandler }; -module.exports.getDataPostHandler = function(adapterName, robot, formatter) { - if (!(adapterName in dataPostHandlers)) { +module.exports.getMessagingHandler = function(adapterName, robot, formatter) { + if (!(adapterName in messagingHandlers)) { robot.logger.warning( util.format('No post handler found for %s. Using DefaultFormatter.', adapterName)); adapterName = 'default'; } robot.logger.debug( util.format('Using %s post data handler.', adapterName)); - return new dataPostHandlers[adapterName](robot, formatter); + return new messagingHandlers[adapterName](robot, formatter); }; diff --git a/src/lib/stackstorm_api.js b/src/lib/stackstorm_api.js new file mode 100644 index 0000000..cece29f --- /dev/null +++ b/src/lib/stackstorm_api.js @@ -0,0 +1,209 @@ +"use strict"; + +var _ = require('lodash'), + util = require('util'), + env = _.clone(process.env), + Promise = require('rsvp').Promise, + utils = require('./utils.js'), + st2client = require('st2client'), + EventEmitter = require('events').EventEmitter + ; + +// Setup the Environment +env.ST2_API = env.ST2_API || 'http://localhost:9101'; +env.ST2_ROUTE = env.ST2_ROUTE || null; +env.ST2_ROUTE = env.ST2_WEBUI_URL || null; + +// Optional authentication info +env.ST2_AUTH_USERNAME = env.ST2_AUTH_USERNAME || null; +env.ST2_AUTH_PASSWORD = env.ST2_AUTH_PASSWORD || null; + +// Optional authentication token +env.ST2_AUTH_TOKEN = env.ST2_AUTH_TOKEN || null; + +// Optional API key +env.ST2_API_KEY = env.ST2_API_KEY || null; + +// Optional, if not provided, we infer it from the API URL +env.ST2_AUTH_URL = env.ST2_AUTH_URL || null; + +function StackStormApi(logger) { + this.logger = logger; + var url = utils.parseUrl(env.ST2_API); + + var opts = { + protocol: url.protocol, + host: url.hostname, + port: url.port, + prefix: url.path, + rejectUnauthorized: false + }; + + this.api = st2client(opts); + if (env.ST2_API_KEY) { + this.api.setKey({ key: env.ST2_API_KEY }); + } else if (env.ST2_AUTH_TOKEN) { + this.api.setToken({ token: env.ST2_AUTH_TOKEN }); + } + + if (env.ST2_API_KEY || env.ST2_AUTH_TOKEN || env.ST2_AUTH_USERNAME || env.ST2_AUTH_PASSWORD) { + // If using username and password then all are required. + if ((env.ST2_AUTH_USERNAME || env.ST2_AUTH_PASSWORD) && + !(env.ST2_AUTH_USERNAME && env.ST2_AUTH_PASSWORD && env.ST2_AUTH_URL)) { + throw new Error('Env variables ST2_AUTH_USERNAME, ST2_AUTH_PASSWORD and ST2_AUTH_URL should only be used together.'); + } + } + + EventEmitter.call(this); +} + +util.inherits(StackStormApi, EventEmitter); + +StackStormApi.prototype.start = function start() { + var self = this; + self.api.stream.listen().catch(function (err) { + self.logger.error('Unable to connect to stream:', err); + }).then(function (source) { + source.onerror = function (err) { + // TODO: squeeze a little bit more info out of evensource.js + self.logger.error('Stream error:', err); + }; + source.addEventListener('st2.announcement__chatops', function (e) { + var data; + + self.logger.debug('Chatops message received:', e.data); + + if (e.data) { + data = JSON.parse(e.data).payload; + } else { + data = e.data; + } + + self.emit('chatops_announcement', data); + + // Special handler to try and figure out when a hipchat message + // is a whisper: + + // TODO: move to postdata logic or in the event listener + // if (this.robot.adapterName === 'hipchat' && !data.whisper && data.channel.indexOf('@') > -1) { + // data.whisper = true; + // this.robot.logger.debug('Set whisper to true for hipchat message'); + // } + }); + }); +}; + +StackStormApi.prototype.getAliases = function () { + this.logger.info('Loading commands....'); + return this.api.actionAlias.list() + .catch(function (err) { + var error_msg = 'Failed to retrieve commands from "%s": %s'; + this.logger.error(util.format(error_msg, env.ST2_API, err.message)); + return []; + }); +}; + +StackStormApi.prototype.sendAck = function (msg, res) { + var history_url = utils.getExecutionHistoryUrl(res.execution); + var history = history_url ? util.format(' (details available at %s)', history_url) : ''; + + if (res.actionalias && res.actionalias.ack) { + if (res.actionalias.ack.enabled === false) { + return; + } else if (res.actionalias.ack.append_url === false) { + history = ''; + } + } + + if (res.message) { + return msg.send(res.message + history); + } + + var message = util.format(_.sample(START_MESSAGES), res.execution.id); + return msg.send(message + history); +}; + +StackStormApi.prototype.createExecution = function (msg, payload) { + this.logger.debug('Sending command payload:', JSON.stringify(payload)); + + return this.api.aliasExecution.create(payload) + .then(function (res) { this.sendAck(msg, res); }) + .catch(function (err) { + // Compatibility with older StackStorm versions + if (err.status === 200) { + return this.sendAck(msg, { execution: { id: err.message } }); + } + this.logger.error('Failed to create an alias execution:', err); + var addressee = utils.normalizeAddressee(msg, robot.adapterName); + var message = util.format(_.sample(ERROR_MESSAGES), err.message); + if (err.requestId) { + message = util.format( + message, + util.format('; Use request ID %s to grep st2 api logs.', err.requestId)); + } + this.postDataHandler.postData({ + whisper: false, + user: addressee.name, + channel: addressee.room, + message: message, + extra: { + color: '#F35A00' + } + }); + throw err; + }); +}; + +StackStormApi.prototype.executeCommand = function (msg, command_name, format_string, command, action_alias) { + var addressee = utils.normalizeAddressee(msg, this.robot.adapterName); + var payload = { + 'name': command_name, + 'format': format_string, + 'command': command, + 'user': addressee.name, + 'source_channel': addressee.room, + 'notification_route': env.ST2_ROUTE || 'hubot' + }; + + return this.createExecution(msg, payload); +}; + +StackStormApi.prototype.authenticate = function authenticate() { + this.api.removeListener('expiry', authenticate); + + // API key gets precedence 1 + if (env.ST2_API_KEY) { + this.logger.info('Using ST2_API_KEY as authentication. Expiry will lead to bot exit.'); + return Promise.resolve(); + } + // Auth token gets precedence 2 + if (env.ST2_AUTH_TOKEN) { + this.logger.info('Using ST2_AUTH_TOKEN as authentication. Expiry will lead to bot exit.'); + return Promise.resolve(); + } + + this.logger.info('Requesting a token...'); + + var url = utils.parseUrl(env.ST2_AUTH_URL); + + var client = st2client({ + auth: { + protocol: url.protocol, + host: url.hostname, + port: url.port, + prefix: url.path + } + }); + + return client.authenticate(env.ST2_AUTH_USERNAME, env.ST2_AUTH_PASSWORD) + .then(function (token) { + this.logger.info('Token received. Expiring ' + token.expiry); + this.api.setToken(token); + client.on('expiry', this.authenticate); + }) + .catch(function (err) { + this.logger.error('Failed to authenticate: ' + err.message); + + throw err; + }); +}; \ No newline at end of file diff --git a/lib/utils.js b/src/lib/utils.js similarity index 71% rename from lib/utils.js rename to src/lib/utils.js index f5a7bbb..a8383ce 100644 --- a/lib/utils.js +++ b/src/lib/utils.js @@ -87,44 +87,7 @@ function splitMessage(message) { }; } -function enable2FA(action_alias) { - return env.HUBOT_2FA && - action_alias.extra && action_alias.extra.security && - action_alias.extra.security.twofactor !== undefined; -} - -function normalizeAddressee(msg, adapter) { - var name = msg.message.user.name; - if (adapter === "hipchat") { - // Hipchat users aren't pinged by name, they're - // pinged by mention_name - name = msg.message.user.mention_name; - } - var room = msg.message.room; - if (room === undefined) { - if (adapter === "hipchat") { - room = msg.message.user.jid; - } - } - if (adapter === "yammer") { - room = String(msg.message.user.thread_id); - name = msg.message.user.name[0]; - } - if (adapter === "spark") { - room = msg.message.user.room; - name = msg.message.user.name; - } - return { - name: name, - room: room - }; -} - exports.isNull = isNull; exports.getExecutionHistoryUrl = getExecutionHistoryUrl; exports.parseUrl = parseUrl; exports.splitMessage = splitMessage; -exports.enable2FA = enable2FA; -exports.normalizeAddressee = normalizeAddressee; -exports.DISPLAY = DISPLAY; -exports.REPRESENTATION = REPRESENTATION; diff --git a/src/stackstorm.js b/src/stackstorm.js new file mode 100644 index 0000000..7431e4e --- /dev/null +++ b/src/stackstorm.js @@ -0,0 +1,87 @@ +// Licensed to the StackStorm, Inc ('StackStorm') under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Description: +// StackStorm hubot integration +// +// Dependencies: +// +// +// Configuration: +// ST2_API - FQDN + port to StackStorm endpoint +// ST2_ROUTE - StackStorm notification route name +// ST2_COMMANDS_RELOAD_INTERVAL - Reload interval for commands +// +// Notes: +// Command list is automatically generated from StackStorm ChatOps metadata +// + +"use strict"; + +var _ = require('lodash'), + util = require('util'), + env = _.clone(process.env), + Promise = require('rsvp').Promise, + utils = require('../lib/utils'), + slack_monkey_patch = require('./lib/slack_monkey_patch'), + formatCommand = require('./lib/format_command'), + formatData = require('./lib/format_data'), + postData = require('./lib/post_data'), + CommandFactory = require('./lib/command_factory'), + StackStormApi = require('./lib/stackstorm_api'), + st2client = require('st2client'), + uuid = require('node-uuid') + ; + + + + + +module.exports = function(robot) { + slack_monkey_patch.patchSendMessage(robot); + + var self = this; + + var promise = Promise.resolve(); + + // factory to manage commands + var command_factory = new CommandFactory(robot); + + // formatter to manage per adapter message formatting. + var formatter = formatData.getFormatter(robot.adapterName, robot); + + // handler to manage per adapter message post-ing. + var postDataHandler = postData.getDataPostHandler(robot.adapterName, robot, formatter); + + var stackstorm_api = new StackStormApi(robot.logger) + + + + + function stop() { + clearInterval(commands_load_interval); + api.stream.listen().then(function (source) { + source.removeAllListeners(); + source.close(); + }); + } + + // Authenticate with StackStorm backend and then call start. + // On a failure to authenticate log the error but do not quit. + return promise.then(function () { + start(); + return stop; + }); +}; From 0fca33b9a7475746fe678ab4669b57486dd8f2da Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Sun, 26 Feb 2017 17:26:44 -0700 Subject: [PATCH 02/26] working --- src/lib/stackstorm_api.js | 24 +++++++++++++++++++-- src/stackstorm.js | 45 ++++++++++++--------------------------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/lib/stackstorm_api.js b/src/lib/stackstorm_api.js index cece29f..d1675f6 100644 --- a/src/lib/stackstorm_api.js +++ b/src/lib/stackstorm_api.js @@ -27,6 +27,24 @@ env.ST2_API_KEY = env.ST2_API_KEY || null; // Optional, if not provided, we infer it from the API URL env.ST2_AUTH_URL = env.ST2_AUTH_URL || null; +var START_MESSAGES = [ + "I'll take it from here! Your execution ID for reference is %s", + "Got it! Remember %s as your execution ID", + "I'm on it! Your execution ID is %s", + "Let me get right on that. Remember %s as your execution ID", + "Always something with you. :) I'll take care of that. Your ID is %s", + "I have it covered. Your execution ID is %s", + "Let me start up the machine! Your execution ID is %s", + "I'll throw that task in the oven and get cookin'! Your execution ID is %s", + "Want me to take that off your hand? You got it! Don't forget your execution ID: %s", + "River Tam will get it done with her psychic powers. Your execution ID is %s" +]; + +var ERROR_MESSAGES = [ + "I'm sorry, Dave. I'm afraid I can't do that. {~} %s" +]; + + function StackStormApi(logger) { this.logger = logger; var url = utils.parseUrl(env.ST2_API); @@ -154,7 +172,7 @@ StackStormApi.prototype.createExecution = function (msg, payload) { }); }; -StackStormApi.prototype.executeCommand = function (msg, command_name, format_string, command, action_alias) { +StackStormApi.prototype.executeCommand = function (msg, command_name, format_string, command, addressee) { var addressee = utils.normalizeAddressee(msg, this.robot.adapterName); var payload = { 'name': command_name, @@ -165,7 +183,9 @@ StackStormApi.prototype.executeCommand = function (msg, command_name, format_str 'notification_route': env.ST2_ROUTE || 'hubot' }; - return this.createExecution(msg, payload); + this.logger.debug('Sending command payload:', JSON.stringify(payload)); + + return this.api.aliasExecution.create(payload); }; StackStormApi.prototype.authenticate = function authenticate() { diff --git a/src/stackstorm.js b/src/stackstorm.js index 7431e4e..d413fb4 100644 --- a/src/stackstorm.js +++ b/src/stackstorm.js @@ -30,28 +30,20 @@ "use strict"; -var _ = require('lodash'), - util = require('util'), - env = _.clone(process.env), - Promise = require('rsvp').Promise, - utils = require('../lib/utils'), - slack_monkey_patch = require('./lib/slack_monkey_patch'), - formatCommand = require('./lib/format_command'), - formatData = require('./lib/format_data'), - postData = require('./lib/post_data'), - CommandFactory = require('./lib/command_factory'), - StackStormApi = require('./lib/stackstorm_api'), - st2client = require('st2client'), - uuid = require('node-uuid') - ; - - - - +var _ = require('lodash'); +var util = require('util'); +var env = _.clone(process.env); +var Promise = require('rsvp').Promise; +var utils = require('../lib/utils'); +var formatCommand = require('./lib/format_command'); +var formatData = require('./lib/format_data'); +var messaging_handler = require('./lib/messaging_handler'); +var CommandFactory = require('./lib/command_factory'); +var StackStormApi = require('./lib/stackstorm_api'); +var st2client = require('st2client'); +var uuid = require('node-uuid'); module.exports = function(robot) { - slack_monkey_patch.patchSendMessage(robot); - var self = this; var promise = Promise.resolve(); @@ -63,20 +55,11 @@ module.exports = function(robot) { var formatter = formatData.getFormatter(robot.adapterName, robot); // handler to manage per adapter message post-ing. - var postDataHandler = postData.getDataPostHandler(robot.adapterName, robot, formatter); - - var stackstorm_api = new StackStormApi(robot.logger) - + var messagingHandler = messaging_handler.getMessagingHandler(robot.adapterName, robot, formatter); + var stackstorm_api = new StackStormApi(robot.logger); - function stop() { - clearInterval(commands_load_interval); - api.stream.listen().then(function (source) { - source.removeAllListeners(); - source.close(); - }); - } // Authenticate with StackStorm backend and then call start. // On a failure to authenticate log the error but do not quit. From 606d89f3862236e55f981d36b1b164e4843c06df Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Sun, 26 Feb 2017 17:54:45 -0700 Subject: [PATCH 03/26] working --- src/lib/command_executor.js | 146 ----------------------------- src/lib/command_factory.js | 19 ++-- src/lib/format_data.js | 126 ------------------------- src/lib/messaging_handler.js | 173 ++++++++++++++++++++++++----------- src/lib/stackstorm_api.js | 15 ++- src/lib/utils.js | 2 - 6 files changed, 134 insertions(+), 347 deletions(-) delete mode 100644 src/lib/command_executor.js delete mode 100644 src/lib/format_data.js diff --git a/src/lib/command_executor.js b/src/lib/command_executor.js deleted file mode 100644 index 4dd413d..0000000 --- a/src/lib/command_executor.js +++ /dev/null @@ -1,146 +0,0 @@ -"use strict"; - -var _ = require('lodash'), - env = _.clone(process.env), - utils = require('./utils.js'), - util = require('util'), - postData = require('./post_data.js'); - - -// Constants -// Fun human-friendly commands. Use %s for payload output. -var START_MESSAGES = [ - "I'll take it from here! Your execution ID for reference is %s", - "Got it! Remember %s as your execution ID", - "I'm on it! Your execution ID is %s", - "Let me get right on that. Remember %s as your execution ID", - "Always something with you. :) I'll take care of that. Your ID is %s", - "I have it covered. Your execution ID is %s", - "Let me start up the machine! Your execution ID is %s", - "I'll throw that task in the oven and get cookin'! Your execution ID is %s", - "Want me to take that off your hand? You got it! Don't forget your execution ID: %s", - "River Tam will get it done with her psychic powers. Your execution ID is %s" -]; - -var ERROR_MESSAGES = [ - "I'm sorry, Dave. I'm afraid I can't do that. {~} %s" -]; - - -var setup_postback_route = function (robot) { - robot.router.post('/hubot/st2', function (req, res) { - var data; - - try { - if (req.body.payload) { - data = JSON.parse(req.body.payload); - } else { - data = req.body; - } - // Special handler to try and figure out when a hipchat message - // is a whisper: - if (robot.adapterName === 'hipchat' && !data.whisper && data.channel.indexOf('@') > -1) { - data.whisper = true; - robot.logger.debug('Set whisper to true for hipchat message'); - } - - postDataHandler.postData(data); - - res.send('{"status": "completed", "msg": "Message posted successfully"}'); - } catch (e) { - robot.logger.error("Unable to decode JSON: " + e); - robot.logger.error(e.stack); - res.send('{"status": "failed", "msg": "An error occurred trying to post the message: ' + e + '"}'); - } - }); -} - -function CommandExecutor(robot) { - this.robot = robot; - this.st2_action_aliases = []; - setup_postback_route(robot); -} - -CommandExecutor.prototype.sendAck = function (msg, res) { - var history_url = utils.getExecutionHistoryUrl(res.execution); - var history = history_url ? util.format(' (details available at %s)', history_url) : ''; - - if (res.actionalias && res.actionalias.ack) { - if (res.actionalias.ack.enabled === false) { - return; - } else if (res.actionalias.ack.append_url === false) { - history = ''; - } - } - - if (res.message) { - return msg.send(res.message + history); - } - - var message = util.format(_.sample(START_MESSAGES), res.execution.id); - return msg.send(message + history); -}; - -CommandExecutor.prototype.createExecution = function (msg, payload) { - this.robot.logger.debug('Sending command payload:', JSON.stringify(payload)); - - api.aliasExecution.create(payload) - .then(function (res) { this.sendAck(msg, res); }) - .catch(function (err) { - // Compatibility with older StackStorm versions - if (err.status === 200) { - return this.sendAck(msg, { execution: { id: err.message } }); - } - this.robot.logger.error('Failed to create an alias execution:', err); - var addressee = utils.normalizeAddressee(msg, robot.adapterName); - var message = util.format(_.sample(ERROR_MESSAGES), err.message); - if (err.requestId) { - message = util.format( - message, - util.format('; Use request ID %s to grep st2 api logs.', err.requestId)); - } - postDataHandler.postData({ - whisper: false, - user: addressee.name, - channel: addressee.room, - message: message, - extra: { - color: '#F35A00' - } - }); - throw err; - }); -}; - -CommandExecutor.prototype.execute = function (msg, command_name, format_string, command, action_alias) { - var addressee = utils.normalizeAddressee(msg, this.robot.adapterName); - var payload = { - 'name': command_name, - 'format': format_string, - 'command': command, - 'user': addressee.name, - 'source_channel': addressee.room, - 'notification_route': env.ST2_ROUTE || 'hubot' - }; - - if (utils.enable2FA(action_alias)) { - var twofactor_id = uuid.v4(); - this.robot.logger.debug('Requested an action that requires 2FA. Guid: ' + twofactor_id); - msg.send(TWOFACTOR_MESSAGE); - api.executions.create({ - 'action': env.HUBOT_2FA, - 'parameters': { - 'uuid': twofactor_id, - 'user': addressee.name, - 'channel': addressee.room, - 'hint': action_alias.description - } - }); - twofactor[twofactor_id] = { - 'msg': msg, - 'payload': payload - }; - } else { - this.createExecution(msg, payload); - } -} \ No newline at end of file diff --git a/src/lib/command_factory.js b/src/lib/command_factory.js index db65ae3..735d747 100644 --- a/src/lib/command_factory.js +++ b/src/lib/command_factory.js @@ -17,10 +17,10 @@ limitations under the License. "use strict"; -var _ = require('lodash'), - utils = require('./utils.js'), - formatCommand = require('./format_command.js'), - CommandExecutor = require('./command_executor'); +var _ = require('lodash'); +var utils = require('./utils.js'); +var formatCommand = require('./format_command.js'); +var CommandExecutor = require('./command_executor'); var getRegexForFormatString = function (format) { @@ -39,15 +39,9 @@ var getRegexForFormatString = function (format) { return regex; }; - -/** - * Manage command for stackstorm, storing them in robot and providing - * lookups given command literals. - */ function CommandFactory(robot) { this.robot = robot; this.st2_action_aliases = []; - this.command_executor = new CommandExecutor(robot); } CommandFactory.prototype.addCommand = function (action_alias, formatter) { @@ -94,9 +88,8 @@ CommandFactory.prototype.addCommand = function (action_alias, formatter) { } }); return false; - }, { id: action_alias.name }, function (response) { - this.command_executor.execute(response, ); - executeCommand(msg, command_name, format_string, command, action_alias); + }, { id: action_alias.name }, function (msg) { + executeCommand(msg, command_name, format_string, command, addressee); }); }; diff --git a/src/lib/format_data.js b/src/lib/format_data.js deleted file mode 100644 index 6fbfbb9..0000000 --- a/src/lib/format_data.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - Licensed to the StackStorm, Inc ('StackStorm') under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and -limitations under the License. -*/ - -"use strict"; - -var _ = require('lodash'), - truncate = require('truncate'), - util = require('util'), - utils = require('./utils.js'); - -var env = process.env; - -/* - SlackFormatter. -*/ -function SlackFormatter(robot) { - this.robot = robot; -} - -SlackFormatter.prototype.formatData = function(data) { - if (utils.isNull(data)) { - return ""; - } - // For slack we do not truncate or format the result. This is because - // data is posted to slack as a message attachment. - return data; -}; - -SlackFormatter.prototype.formatRecepient = function(recepient) { - return recepient; -}; - -SlackFormatter.prototype.normalizeCommand = function(command) { - // replace left double quote with regular quote - command = command.replace(/\u201c/g, '\u0022'); - // replace right double quote with regular quote - command = command.replace(/\u201d/g, '\u0022'); - // replace left single quote with regular apostrophe - command = command.replace(/\u2018/g, '\u0027'); - // replace right single quote with regular apostrophe - command = command.replace(/\u2019/g, '\u0027'); - return command; -}; - -/* - HipChatFormatter. -*/ -function HipChatFormatter(robot) { - this.robot = robot; -} - -HipChatFormatter.prototype.formatData = function(data) { - if (utils.isNull(data)) { - return ""; - } - // HipChat has "show more" capability in messages so no truncation. - return '/code ' + data; -}; - -HipChatFormatter.prototype.formatRecepient = function(recepient) { - var robot_name = env.HUBOT_HIPCHAT_JID.split("_")[0]; - var hipchat_domain = (env.HUBOT_HIPCHAT_XMPP_DOMAIN === 'btf.hipchat.com') ? - 'conf.btf.hipchat.com' : 'conf.hipchat.com'; - return util.format('%s_%s@%s', robot_name, recepient, hipchat_domain); -}; - -HipChatFormatter.prototype.normalizeCommand = function(command) { - return command; -}; - -/* - DefaultFormatter. -*/ -function DefaultFormatter(robot) { - this.robot = robot; - - // Limit the size of a message. - this.truncate_length = env.ST2_MAX_MESSAGE_LENGTH; -} - -DefaultFormatter.prototype.formatData = function(data) { - if (utils.isNull(data)) { - return ""; - } - if (this.truncate_length > 0) { - data = truncate(data, this.truncate_length); - } - return data; -}; - -DefaultFormatter.prototype.formatRecepient = function(recepient) { - return recepient; -}; - -DefaultFormatter.prototype.normalizeCommand = function(command) { - return command; -}; - -var formatters = { - 'slack': SlackFormatter, - 'hipchat': HipChatFormatter, - 'default': DefaultFormatter -}; - -module.exports.getFormatter = function(adapterName, robot) { - if (!(adapterName in formatters)) { - robot.logger.warning( - util.format('No supported formatter found for %s. Using DefaultFormatter.', adapterName)); - adapterName = 'default'; - } - return new formatters[adapterName](robot); -}; diff --git a/src/lib/messaging_handler.js b/src/lib/messaging_handler.js index fd70781..0dc8c55 100644 --- a/src/lib/messaging_handler.js +++ b/src/lib/messaging_handler.js @@ -17,16 +17,76 @@ limitations under the License. "use strict"; -var env = process.env, - util = require('util'), - utils = require('./utils.js'); +var _ = require('lodash'); +var env = process.env; +var util = require('util'); +var utils = require('./utils.js'); +var truncate = require('truncate'); /* - SlackMessagingHandler. + DefaultMessagingHandler. */ -function SlackMessagingHandler(robot, formatter) { +function DefaultMessagingHandler(robot, formatter) { this.robot = robot; this.formatter = formatter; + this.truncate_length = env.ST2_MAX_MESSAGE_LENGTH; +} + +DefaultMessagingHandler.prototype.postData = function(data) { + var recipient, split_message, formatted_message, + text = ""; + + if (data.whisper && data.user) { + recipient = data.user; + } else { + recipient = data.channel; + text = (data.user && !data.whisper) ? util.format('%s: ', data.user) : ""; + } + + recipient = this.formatter.formatRecepient(recipient); + text += this.formatter.formatData(data.message); + + // Ignore the delimiter in the default formatter and just concat parts. + split_message = utils.splitMessage(text); + if (split_message.pretext && split_message.text) { + formatted_message = util.format("%s\n%s", split_message.pretext, split_message.text); + } else { + formatted_message = split_message.pretext || split_message.text; + } + + this.robot.messageRoom.call(this.robot, recipient, formatted_message); +}; + +DefaultMessagingHandler.normalizeAddressee = function(msg) { + return { + room: msg.message.user.room, + name: msg.message.user.name + }; +}; + +DefaultMessagingHandler.prototype.formatData = function(data) { + if (utils.isNull(data)) { + return ""; + } + if (this.truncate_length > 0) { + data = truncate(data, this.truncate_length); + } + return data; +}; + +DefaultMessagingHandler.prototype.formatRecepient = function(recepient) { + return recepient; +}; + +DefaultMessagingHandler.prototype.normalizeCommand = function(command) { + return command; +}; + +/* + SlackMessagingHandler. +*/ +function SlackMessagingHandler(robot, formatter) { + DefaultMessagingHandler.call(this, robot, formatter); var sendMessageRaw = function(message) { /*jshint validthis:true */ @@ -42,6 +102,8 @@ function SlackMessagingHandler(robot, formatter) { } } +util.inherits(SlackMessagingHandler, DefaultMessagingHandler); + SlackMessagingHandler.prototype.postData = function(data) { var recipient, attachment_color, split_message, attachment, pretext = ""; @@ -93,21 +155,47 @@ SlackMessagingHandler.prototype.postData = function(data) { } }; -SlackMessagingHandler.normalizeAddressee = function(msg) { +SlackMessagingHandler.prototype.normalizeAddressee = function(msg) { return { name: msg.message.user.name, room: msg.message.room }; }; +SlackMessagingHandler.prototype.formatData = function(data) { + if (utils.isNull(data)) { + return ""; + } + // For slack we do not truncate or format the result. This is because + // data is posted to slack as a message attachment. + return data; +}; + +SlackMessagingHandler.prototype.formatRecepient = function(recepient) { + return recepient; +}; + +SlackMessagingHandler.prototype.normalizeCommand = function(command) { + // replace left double quote with regular quote + command = command.replace(/\u201c/g, '\u0022'); + // replace right double quote with regular quote + command = command.replace(/\u201d/g, '\u0022'); + // replace left single quote with regular apostrophe + command = command.replace(/\u2018/g, '\u0027'); + // replace right single quote with regular apostrophe + command = command.replace(/\u2019/g, '\u0027'); + return command; +}; + /* HipchatMessagingHandler. */ function HipchatMessagingHandler(robot, formatter) { - this.robot = robot; - this.formatter = formatter; + DefaultMessagingHandler.call(this, robot, formatter); } +util.inherits(HipchatMessagingHandler, DefaultMessagingHandler); + HipchatMessagingHandler.prototype.postData = function(data) { var recipient, split_message, formatted_message, pretext = ""; @@ -143,7 +231,7 @@ HipchatMessagingHandler.prototype.postData = function(data) { } }; -SlackMessagingHandler.normalizeAddressee = function(msg) { +HipchatMessagingHandler.prototype.normalizeAddressee = function(msg) { var name = msg.message.user.name; var room = msg.message.room; if (room === undefined) { @@ -155,14 +243,35 @@ SlackMessagingHandler.normalizeAddressee = function(msg) { }; }; +HipchatMessagingHandler.prototype.formatData = function(data) { + if (utils.isNull(data)) { + return ""; + } + // HipChat has "show more" capability in messages so no truncation. + return '/code ' + data; +}; + +HipchatMessagingHandler.prototype.formatRecepient = function(recepient) { + var robot_name = env.HUBOT_HIPCHAT_JID.split("_")[0]; + var hipchat_domain = (env.HUBOT_HIPCHAT_XMPP_DOMAIN === 'btf.hipchat.com') ? + 'conf.btf.hipchat.com' : 'conf.hipchat.com'; + return util.format('%s_%s@%s', robot_name, recepient, hipchat_domain); +}; + +HipchatMessagingHandler.prototype.normalizeCommand = function(command) { + return command; +}; + + /* YammerMessagingHandler. */ function YammerMessagingHandler(robot, formatter) { - this.robot = robot; - this.formatter = formatter; + DefaultMessagingHandler.call(this, robot, formatter); } +util.inherits(YammerMessagingHandler, DefaultMessagingHandler); + YammerMessagingHandler.prototype.postData = function(data) { var recipient, split_message, formatted_message, text = ""; @@ -188,53 +297,13 @@ YammerMessagingHandler.prototype.postData = function(data) { this.robot.send.call(this.robot, recipient, formatted_message); }; -YammerMessagingHandler.normalizeAddressee = function(msg) { +YammerMessagingHandler.prototype.normalizeAddressee = function(msg) { return { name: String(msg.message.user.thread_id), room: msg.message.room }; }; -/* - DefaultMessagingHandler. -*/ -function DefaultMessagingHandler(robot, formatter) { - this.robot = robot; - this.formatter = formatter; -} - -DefaultMessagingHandler.prototype.postData = function(data) { - var recipient, split_message, formatted_message, - text = ""; - - if (data.whisper && data.user) { - recipient = data.user; - } else { - recipient = data.channel; - text = (data.user && !data.whisper) ? util.format('%s: ', data.user) : ""; - } - - recipient = this.formatter.formatRecepient(recipient); - text += this.formatter.formatData(data.message); - - // Ignore the delimiter in the default formatter and just concat parts. - split_message = utils.splitMessage(text); - if (split_message.pretext && split_message.text) { - formatted_message = util.format("%s\n%s", split_message.pretext, split_message.text); - } else { - formatted_message = split_message.pretext || split_message.text; - } - - this.robot.messageRoom.call(this.robot, recipient, formatted_message); -}; - -DefaultMessagingHandler.normalizeAddressee = function(msg) { - return { - room: msg.message.user.room, - name: msg.message.user.name - }; -}; - var messagingHandlers = { 'slack': SlackMessagingHandler, 'hipchat': HipchatMessagingHandler, diff --git a/src/lib/stackstorm_api.js b/src/lib/stackstorm_api.js index d1675f6..088220b 100644 --- a/src/lib/stackstorm_api.js +++ b/src/lib/stackstorm_api.js @@ -1,13 +1,12 @@ "use strict"; -var _ = require('lodash'), - util = require('util'), - env = _.clone(process.env), - Promise = require('rsvp').Promise, - utils = require('./utils.js'), - st2client = require('st2client'), - EventEmitter = require('events').EventEmitter - ; +var _ = require('lodash'); +var util = require('util'); +var env = _.clone(process.env); +var Promise = require('rsvp').Promise; +var utils = require('./utils.js'); +var st2client = require('st2client'); +var EventEmitter = require('events').EventEmitter; // Setup the Environment env.ST2_API = env.ST2_API || 'http://localhost:9101'; diff --git a/src/lib/utils.js b/src/lib/utils.js index a8383ce..1168743 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -25,8 +25,6 @@ var WEBUI_EXECUTION_HISTORY_URL = '%s/#/history/%s/general'; var MESSAGE_EXECUTION_ID_REGEX = new RegExp('.*execution: (.+).*'); var CLI_EXECUTION_GET_CMD = 'st2 execution get %s'; var PRETEXT_DELIMITER = '{~}'; -var DISPLAY = 1; -var REPRESENTATION = 2; function isNull(value) { From 8e427943258c69bf63b25ef6f9ddde80b47e9282 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Tue, 28 Feb 2017 08:29:17 -0700 Subject: [PATCH 04/26] main refactor --- jshint.json => .jshintrc | 0 src/lib/command_factory.js | 75 +++++-- src/lib/format_command.js | 39 ---- src/lib/messaging_handler.js | 323 --------------------------- src/lib/messaging_handler/default.js | 65 ++++++ src/lib/messaging_handler/hipchat.js | 82 +++++++ src/lib/messaging_handler/index.js | 31 +++ src/lib/messaging_handler/slack.js | 112 ++++++++++ src/lib/messaging_handler/yammer.js | 47 ++++ src/lib/stackstorm_api.js | 100 ++++----- src/stackstorm.js | 78 +++++-- 11 files changed, 495 insertions(+), 457 deletions(-) rename jshint.json => .jshintrc (100%) delete mode 100644 src/lib/format_command.js delete mode 100644 src/lib/messaging_handler.js create mode 100644 src/lib/messaging_handler/default.js create mode 100644 src/lib/messaging_handler/hipchat.js create mode 100644 src/lib/messaging_handler/index.js create mode 100644 src/lib/messaging_handler/slack.js create mode 100644 src/lib/messaging_handler/yammer.js diff --git a/jshint.json b/.jshintrc similarity index 100% rename from jshint.json rename to .jshintrc diff --git a/src/lib/command_factory.js b/src/lib/command_factory.js index 735d747..01b51ef 100644 --- a/src/lib/command_factory.js +++ b/src/lib/command_factory.js @@ -19,9 +19,27 @@ limitations under the License. var _ = require('lodash'); var utils = require('./utils.js'); -var formatCommand = require('./format_command.js'); -var CommandExecutor = require('./command_executor'); +var util = require('util'); +var EventEmitter = require('events').EventEmitter; +var formatCommand = function(name, format, description) { + var context, template_str, compiled_template, command; + + if (!format) { + throw (Error('format should be non-empty.')); + } + + context = { + 'format': format, + 'description': description + }; + + template_str = '${format} - ${description}'; + compiled_template = _.template(template_str); + command = compiled_template(context); + + return command; +}; var getRegexForFormatString = function (format) { var extra_params, regex_str, regex; @@ -41,55 +59,64 @@ var getRegexForFormatString = function (format) { function CommandFactory(robot) { this.robot = robot; - this.st2_action_aliases = []; + EventEmitter.call(this); } -CommandFactory.prototype.addCommand = function (action_alias, formatter) { +util.inherits(CommandFactory, EventEmitter); + +CommandFactory.prototype.addCommand = function (action_alias, messaging_handler) { + var self = this; + if (action_alias.enabled === false) { return; } if (!action_alias.formats || action_alias.formats.length === 0) { - this.robot.logger.error('No formats specified for command: ' + action_alias.name); + self.robot.logger.error('No formats specified for command: ' + action_alias.name); return; } var regexes = []; + var commands_regex_map = {}; + var action_alias_name = action_alias.name; _.each(action_alias.formats, function (format) { - action_alias.formatted_command = formatCommand(action_alias.name, format.display || format, action_alias.description); - - var context = { - robotName: this.robot.name, - command: action_alias.formatted_command - }; - + var formatted_string = formatCommand(action_alias.name, format.display || format, action_alias.description); var compiled_template = _.template('hubot ${command}'); - action_alias.command_string = compiled_template(context); + self.robot.commands.push(compiled_template({ robotName: self.robot.name, command: formatted_string })); - this.robot.commands.push(action_alias.formatted_command); if (format.display) { _.each(format.representation, function (representation) { - regexes.push(getRegexForFormatString(representation)); + commands_regex_map[formatted_string] = getRegexForFormatString(representation); }); } else { - regexes.push(getRegexForFormatString(format)); + commands_regex_map[formatted_string] = getRegexForFormatString(format); } }); - action_alias.regexes = regexes; - this.st2_action_aliases.push(action_alias); + var format_strings = Object.keys(commands_regex_map); - this.robot.listen(function (msg) { - var command = formatter.normalizeCommand(msg.text); - _.each(regexes, function (regex) { + self.robot.listen(function (msg) { + var i, format_string, regex; + var command = messaging_handler.normalizeCommand(msg.text); + for (i = 0; i < format_strings.length; i++) { + format_string = format_strings[i]; + regex = commands_regex_map[format_string]; if (regex.test(command)) { + msg['st2_command_format_string'] = format_string; + msg['normalized_command'] = command; return true; } - }); + } return false; - }, { id: action_alias.name }, function (msg) { - executeCommand(msg, command_name, format_string, command, addressee); + }, { id: action_alias_name }, function (msg) { + self.emit('st2.command_match', { + msg: msg, + alias_name: action_alias_name, + command_format_string: msg.message['st2_command_format_string'], + command: msg.message['normalized_command'], + addressee: messaging_handler.normalizeAddressee(msg) + }); }); }; diff --git a/src/lib/format_command.js b/src/lib/format_command.js deleted file mode 100644 index 833c574..0000000 --- a/src/lib/format_command.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - Licensed to the StackStorm, Inc ('StackStorm') under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and -limitations under the License. -*/ - -"use strict"; - -var _ = require('lodash'); - -module.exports = function(name, format, description) { - var context, template_str, compiled_template, command; - - if (!format) { - throw (Error('format should be non-empty.')); - } - - context = { - 'format': format, - 'description': description - }; - - template_str = '${format} - ${description}'; - compiled_template = _.template(template_str); - command = compiled_template(context); - - return command; -}; diff --git a/src/lib/messaging_handler.js b/src/lib/messaging_handler.js deleted file mode 100644 index 0dc8c55..0000000 --- a/src/lib/messaging_handler.js +++ /dev/null @@ -1,323 +0,0 @@ -/* - Licensed to the StackStorm, Inc ('StackStorm') under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and -limitations under the License. -*/ - -"use strict"; - -var _ = require('lodash'); -var env = process.env; -var util = require('util'); -var utils = require('./utils.js'); -var truncate = require('truncate'); - -/* - DefaultMessagingHandler. -*/ -function DefaultMessagingHandler(robot, formatter) { - this.robot = robot; - this.formatter = formatter; - this.truncate_length = env.ST2_MAX_MESSAGE_LENGTH; -} - -DefaultMessagingHandler.prototype.postData = function(data) { - var recipient, split_message, formatted_message, - text = ""; - - if (data.whisper && data.user) { - recipient = data.user; - } else { - recipient = data.channel; - text = (data.user && !data.whisper) ? util.format('%s: ', data.user) : ""; - } - - recipient = this.formatter.formatRecepient(recipient); - text += this.formatter.formatData(data.message); - - // Ignore the delimiter in the default formatter and just concat parts. - split_message = utils.splitMessage(text); - if (split_message.pretext && split_message.text) { - formatted_message = util.format("%s\n%s", split_message.pretext, split_message.text); - } else { - formatted_message = split_message.pretext || split_message.text; - } - - this.robot.messageRoom.call(this.robot, recipient, formatted_message); -}; - -DefaultMessagingHandler.normalizeAddressee = function(msg) { - return { - room: msg.message.user.room, - name: msg.message.user.name - }; -}; - -DefaultMessagingHandler.prototype.formatData = function(data) { - if (utils.isNull(data)) { - return ""; - } - if (this.truncate_length > 0) { - data = truncate(data, this.truncate_length); - } - return data; -}; - -DefaultMessagingHandler.prototype.formatRecepient = function(recepient) { - return recepient; -}; - -DefaultMessagingHandler.prototype.normalizeCommand = function(command) { - return command; -}; - -/* - SlackMessagingHandler. -*/ -function SlackMessagingHandler(robot, formatter) { - DefaultMessagingHandler.call(this, robot, formatter); - - var sendMessageRaw = function(message) { - /*jshint validthis:true */ - message['channel'] = this.id; - message['parse'] = 'none'; - this._client._send(message); - }; - - if (robot.adapter && robot.adapter.constructor && robot.adapter.constructor.name === 'SlackBot') { - for (var channel in robot.adapter.client.channels) { - robot.adapter.client.channels[channel].sendMessage = sendMessageRaw.bind(robot.adapter.client.channels[channel]); - } - } -} - -util.inherits(SlackMessagingHandler, DefaultMessagingHandler); - -SlackMessagingHandler.prototype.postData = function(data) { - var recipient, attachment_color, split_message, - attachment, pretext = ""; - - if (data.whisper && data.user) { - recipient = data.user; - } else { - recipient = data.channel; - pretext = (data.user && !data.whisper) ? util.format('@%s: ', data.user) : ""; - } - - if (data.extra && data.extra.color) { - attachment_color = data.extra.color; - } else { - attachment_color = env.ST2_SLACK_SUCCESS_COLOR; - if (data.message.indexOf("status : failed") > -1) { - attachment_color = env.ST2_SLACK_FAIL_COLOR; - } - } - - split_message = utils.splitMessage(this.formatter.formatData(data.message)); - - if (split_message.text) { - var content = { - color: attachment_color, - "mrkdwn_in": ["text", "pretext"], - }; - if (data.extra && data.extra.slack) { - for (var attrname in data.extra.slack) { content[attrname] = data.extra.slack[attrname]; } - } - var robot = this.robot; - var chunks = split_message.text.match(/[\s\S]{1,7900}/g); - var sendChunk = function (i) { - content.text = chunks[i]; - content.fallback = chunks[i]; - attachment = { - channel: recipient, - content: content, - text: i === 0 ? pretext + split_message.pretext : null - }; - robot.emit('slack-attachment', attachment); - if (chunks.length > ++i) { - setTimeout(function(){ sendChunk(i); }, 300); - } - }; - sendChunk(0); - } else { - this.robot.messageRoom.call(this.robot, recipient, pretext + split_message.pretext); - } -}; - -SlackMessagingHandler.prototype.normalizeAddressee = function(msg) { - return { - name: msg.message.user.name, - room: msg.message.room - }; -}; - -SlackMessagingHandler.prototype.formatData = function(data) { - if (utils.isNull(data)) { - return ""; - } - // For slack we do not truncate or format the result. This is because - // data is posted to slack as a message attachment. - return data; -}; - -SlackMessagingHandler.prototype.formatRecepient = function(recepient) { - return recepient; -}; - -SlackMessagingHandler.prototype.normalizeCommand = function(command) { - // replace left double quote with regular quote - command = command.replace(/\u201c/g, '\u0022'); - // replace right double quote with regular quote - command = command.replace(/\u201d/g, '\u0022'); - // replace left single quote with regular apostrophe - command = command.replace(/\u2018/g, '\u0027'); - // replace right single quote with regular apostrophe - command = command.replace(/\u2019/g, '\u0027'); - return command; -}; - -/* - HipchatMessagingHandler. -*/ -function HipchatMessagingHandler(robot, formatter) { - DefaultMessagingHandler.call(this, robot, formatter); -} - -util.inherits(HipchatMessagingHandler, DefaultMessagingHandler); - -HipchatMessagingHandler.prototype.postData = function(data) { - var recipient, split_message, formatted_message, - pretext = ""; - - recipient = data.channel; - if (data.user && !data.whisper) { - pretext = util.format('@%s: ', data.user); - } - - if (recipient.indexOf('@') === -1 ) { - recipient = this.formatter.formatRecepient(recipient); - } - split_message = utils.splitMessage(data.message); - if (pretext) { - split_message.pretext = pretext + split_message.pretext; - } - - /* Hipchat is unable to render text and code in the - same message, so split them */ - if (split_message.pretext) { - if (data.whisper) { - this.robot.send.call(this.robot, data.channel, split_message.pretext); - } else { - this.robot.messageRoom.call(this.robot, recipient, split_message.pretext); - } - } - if (split_message.text) { - if (data.whisper) { - this.robot.send.call(this.robot, data.channel, this.formatter.formatData(split_message.text)); - } else { - this.robot.messageRoom.call(this.robot, recipient, this.formatter.formatData(split_message.text)); - } - } -}; - -HipchatMessagingHandler.prototype.normalizeAddressee = function(msg) { - var name = msg.message.user.name; - var room = msg.message.room; - if (room === undefined) { - room = msg.message.user.jid; - } - return { - name: name, - room: room - }; -}; - -HipchatMessagingHandler.prototype.formatData = function(data) { - if (utils.isNull(data)) { - return ""; - } - // HipChat has "show more" capability in messages so no truncation. - return '/code ' + data; -}; - -HipchatMessagingHandler.prototype.formatRecepient = function(recepient) { - var robot_name = env.HUBOT_HIPCHAT_JID.split("_")[0]; - var hipchat_domain = (env.HUBOT_HIPCHAT_XMPP_DOMAIN === 'btf.hipchat.com') ? - 'conf.btf.hipchat.com' : 'conf.hipchat.com'; - return util.format('%s_%s@%s', robot_name, recepient, hipchat_domain); -}; - -HipchatMessagingHandler.prototype.normalizeCommand = function(command) { - return command; -}; - - -/* - YammerMessagingHandler. -*/ -function YammerMessagingHandler(robot, formatter) { - DefaultMessagingHandler.call(this, robot, formatter); -} - -util.inherits(YammerMessagingHandler, DefaultMessagingHandler); - -YammerMessagingHandler.prototype.postData = function(data) { - var recipient, split_message, formatted_message, - text = ""; - - if (data.whisper && data.user) { - recipient = { name: data.user, thread_id: data.channel }; - } else { - recipient = { name: data.user, thread_id: data.channel }; - text = (data.user && !data.whisper) ? util.format('@%s: ', data.user) : ""; - } - - recipient = this.formatter.formatRecepient(recipient); - text += this.formatter.formatData(data.message); - - // Ignore the delimiter in the default formatter and just concat parts. - split_message = utils.splitMessage(text); - if (split_message.pretext && split_message.text) { - formatted_message = util.format("%s\n%s", split_message.pretext, split_message.text); - } else { - formatted_message = split_message.pretext || split_message.text; - } - - this.robot.send.call(this.robot, recipient, formatted_message); -}; - -YammerMessagingHandler.prototype.normalizeAddressee = function(msg) { - return { - name: String(msg.message.user.thread_id), - room: msg.message.room - }; -}; - -var messagingHandlers = { - 'slack': SlackMessagingHandler, - 'hipchat': HipchatMessagingHandler, - 'yammer': YammerMessagingHandler, - 'default': DefaultMessagingHandler -}; - -module.exports.getMessagingHandler = function(adapterName, robot, formatter) { - if (!(adapterName in messagingHandlers)) { - robot.logger.warning( - util.format('No post handler found for %s. Using DefaultFormatter.', adapterName)); - adapterName = 'default'; - } - robot.logger.debug( - util.format('Using %s post data handler.', adapterName)); - return new messagingHandlers[adapterName](robot, formatter); -}; diff --git a/src/lib/messaging_handler/default.js b/src/lib/messaging_handler/default.js new file mode 100644 index 0000000..e6ae634 --- /dev/null +++ b/src/lib/messaging_handler/default.js @@ -0,0 +1,65 @@ +"use strict"; + +var _ = require('lodash'); +var env = process.env; +var util = require('util'); +var utils = require('./../utils'); +var truncate = require('truncate'); +var hubot_alias = env.HUBOT_ALIAS; + +function DefaultMessagingHandler(robot) { + this.robot = robot; + this.truncate_length = env.ST2_MAX_MESSAGE_LENGTH; +} + +DefaultMessagingHandler.prototype.postData = function(data) { + var recipient, split_message, formatted_message, + text = ""; + + if (data.whisper && data.user) { + recipient = data.user; + } else { + recipient = data.channel; + text = (data.user && !data.whisper) ? util.format('%s: ', data.user) : ""; + } + + recipient = this.formatRecepient(recipient); + text += this.formatData(data.message); + + // Ignore the delimiter in the default formatter and just concat parts. + split_message = utils.splitMessage(text); + if (split_message.pretext && split_message.text) { + formatted_message = util.format("%s\n%s", split_message.pretext, split_message.text); + } else { + formatted_message = split_message.pretext || split_message.text; + } + + this.robot.messageRoom.call(this.robot, recipient, formatted_message); +}; + +DefaultMessagingHandler.normalizeAddressee = function(msg) { + return { + room: msg.message.user.room, + name: msg.message.user.name + }; +}; + +DefaultMessagingHandler.prototype.formatData = function(data) { + if (utils.isNull(data)) { + return ""; + } + if (this.truncate_length > 0) { + data = truncate(data, this.truncate_length); + } + return data; +}; + +DefaultMessagingHandler.prototype.formatRecepient = function(recepient) { + return recepient; +}; + +DefaultMessagingHandler.prototype.normalizeCommand = function(command) { + return command.replace(/^${hubot_alias}/, "").trim(); +}; + +module.exports = DefaultMessagingHandler; \ No newline at end of file diff --git a/src/lib/messaging_handler/hipchat.js b/src/lib/messaging_handler/hipchat.js new file mode 100644 index 0000000..bb590f3 --- /dev/null +++ b/src/lib/messaging_handler/hipchat.js @@ -0,0 +1,82 @@ +"use strict"; + +var _ = require('lodash'); +var env = process.env; +var util = require('util'); +var utils = require('./../utils'); +var DefaultMessagingHandler = require('./default'); + + +function HipchatMessagingHandler(robot) { + DefaultMessagingHandler.call(this, robot); +} + +util.inherits(HipchatMessagingHandler, DefaultMessagingHandler); + +HipchatMessagingHandler.prototype.postData = function(data) { + var recipient, split_message, formatted_message, + pretext = ""; + + recipient = data.channel; + if (data.user && !data.whisper) { + pretext = util.format('@%s: ', data.user); + } + + if (recipient.indexOf('@') === -1 ) { + recipient = this.formatRecepient(recipient); + } + split_message = utils.splitMessage(data.message); + if (pretext) { + split_message.pretext = pretext + split_message.pretext; + } + + /* Hipchat is unable to render text and code in the + same message, so split them */ + if (split_message.pretext) { + if (data.whisper) { + this.robot.send.call(this.robot, data.channel, split_message.pretext); + } else { + this.robot.messageRoom.call(this.robot, recipient, split_message.pretext); + } + } + if (split_message.text) { + if (data.whisper) { + this.robot.send.call(this.robot, data.channel, this.formatData(split_message.text)); + } else { + this.robot.messageRoom.call(this.robot, recipient, this.formatData(split_message.text)); + } + } +}; + +HipchatMessagingHandler.prototype.normalizeAddressee = function(msg) { + var name = msg.message.user.name; + var room = msg.message.room; + if (room === undefined) { + room = msg.message.user.jid; + } + return { + name: name, + room: room + }; +}; + +HipchatMessagingHandler.prototype.formatData = function(data) { + if (utils.isNull(data)) { + return ""; + } + // HipChat has "show more" capability in messages so no truncation. + return '/code ' + data; +}; + +HipchatMessagingHandler.prototype.formatRecepient = function(recepient) { + var robot_name = env.HUBOT_HIPCHAT_JID.split("_")[0]; + var hipchat_domain = (env.HUBOT_HIPCHAT_XMPP_DOMAIN === 'btf.hipchat.com') ? + 'conf.btf.hipchat.com' : 'conf.hipchat.com'; + return util.format('%s_%s@%s', robot_name, recepient, hipchat_domain); +}; + +HipchatMessagingHandler.prototype.normalizeCommand = function(command) { + return HipchatMessagingHandler.super_.prototype.normalizeCommand.call(this, command); +}; + +module.exports = HipchatMessagingHandler; diff --git a/src/lib/messaging_handler/index.js b/src/lib/messaging_handler/index.js new file mode 100644 index 0000000..64d65e8 --- /dev/null +++ b/src/lib/messaging_handler/index.js @@ -0,0 +1,31 @@ +"use strict"; + +var _ = require('lodash'); +var env = process.env; +var util = require('util'); +var fs = require('fs'); +var path = require('path'); + +var filenames = fs.readdirSync('./'); + +var messagingHandlers = {}; + +filenames.forEach(function(filename) { + if (filename === 'index.js') { + return; + } + + var message_handler = filename.replace(/\.[^/.]+$/, ""); + messagingHandlers[message_handler] = require('./' + filename); +}); + +module.exports.getMessagingHandler = function(adapterName, robot) { + if (!(adapterName in messagingHandlers)) { + robot.logger.warning( + util.format('No post handler found for %s. Using DefaultFormatter.', adapterName)); + adapterName = 'default'; + } + robot.logger.debug( + util.format('Using %s post data handler.', adapterName)); + return new messagingHandlers[adapterName](robot); +}; \ No newline at end of file diff --git a/src/lib/messaging_handler/slack.js b/src/lib/messaging_handler/slack.js new file mode 100644 index 0000000..cad17a4 --- /dev/null +++ b/src/lib/messaging_handler/slack.js @@ -0,0 +1,112 @@ +"use strict"; + +var _ = require('lodash'); +var env = process.env; +var util = require('util'); +var utils = require('./../utils'); +var DefaultMessagingHandler = require('./default'); + +function SlackMessagingHandler(robot) { + DefaultMessagingHandler.call(this, robot); + + var sendMessageRaw = function(message) { + /*jshint validthis:true */ + message['channel'] = this.id; + message['parse'] = 'none'; + this._client._send(message); + }; + + if (robot.adapter && robot.adapter.constructor && robot.adapter.constructor.name === 'SlackBot') { + for (var channel in robot.adapter.client.channels) { + robot.adapter.client.channels[channel].sendMessage = sendMessageRaw.bind(robot.adapter.client.channels[channel]); + } + } +} + +util.inherits(SlackMessagingHandler, DefaultMessagingHandler); + +SlackMessagingHandler.prototype.postData = function(data) { + var recipient, attachment_color, split_message, + attachment, pretext = ""; + + if (data.whisper && data.user) { + recipient = data.user; + } else { + recipient = data.channel; + pretext = (data.user && !data.whisper) ? util.format('@%s: ', data.user) : ""; + } + + if (data.extra && data.extra.color) { + attachment_color = data.extra.color; + } else { + attachment_color = env.ST2_SLACK_SUCCESS_COLOR; + if (data.message.indexOf("status : failed") > -1) { + attachment_color = env.ST2_SLACK_FAIL_COLOR; + } + } + + split_message = utils.splitMessage(this.formatData(data.message)); + + if (split_message.text) { + var content = { + color: attachment_color, + "mrkdwn_in": ["text", "pretext"], + }; + if (data.extra && data.extra.slack) { + for (var attrname in data.extra.slack) { content[attrname] = data.extra.slack[attrname]; } + } + var robot = this.robot; + var chunks = split_message.text.match(/[\s\S]{1,7900}/g); + var sendChunk = function (i) { + content.text = chunks[i]; + content.fallback = chunks[i]; + attachment = { + channel: recipient, + content: content, + text: i === 0 ? pretext + split_message.pretext : null + }; + robot.emit('slack-attachment', attachment); + if (chunks.length > ++i) { + setTimeout(function(){ sendChunk(i); }, 300); + } + }; + sendChunk(0); + } else { + this.robot.messageRoom.call(this.robot, recipient, pretext + split_message.pretext); + } +}; + +SlackMessagingHandler.prototype.normalizeAddressee = function(msg) { + return { + name: msg.message.user.name, + room: msg.message.room + }; +}; + +SlackMessagingHandler.prototype.formatData = function(data) { + if (utils.isNull(data)) { + return ""; + } + // For slack we do not truncate or format the result. This is because + // data is posted to slack as a message attachment. + return data; +}; + +SlackMessagingHandler.prototype.formatRecepient = function(recepient) { + return recepient; +}; + +SlackMessagingHandler.prototype.normalizeCommand = function(command) { + command = SlackMessagingHandler.super_.prototype.normalizeCommand.call(this, command); + // replace left double quote with regular quote + command = command.replace(/\u201c/g, '\u0022'); + // replace right double quote with regular quote + command = command.replace(/\u201d/g, '\u0022'); + // replace left single quote with regular apostrophe + command = command.replace(/\u2018/g, '\u0027'); + // replace right single quote with regular apostrophe + command = command.replace(/\u2019/g, '\u0027'); + return command; +}; + +module.exports = SlackMessagingHandler; \ No newline at end of file diff --git a/src/lib/messaging_handler/yammer.js b/src/lib/messaging_handler/yammer.js new file mode 100644 index 0000000..ec59687 --- /dev/null +++ b/src/lib/messaging_handler/yammer.js @@ -0,0 +1,47 @@ +"use strict"; + +var _ = require('lodash'); +var env = process.env; +var util = require('util'); +var utils = require('./../utils'); +var DefaultMessagingHandler = require('./default'); + +function YammerMessagingHandler(robot) { + DefaultMessagingHandler.call(this, robot); +} + +util.inherits(YammerMessagingHandler, DefaultMessagingHandler); + +YammerMessagingHandler.prototype.postData = function(data) { + var recipient, split_message, formatted_message, + text = ""; + + if (data.whisper && data.user) { + recipient = { name: data.user, thread_id: data.channel }; + } else { + recipient = { name: data.user, thread_id: data.channel }; + text = (data.user && !data.whisper) ? util.format('@%s: ', data.user) : ""; + } + + recipient = this.formatRecepient(recipient); + text += this.formatData(data.message); + + // Ignore the delimiter in the default formatter and just concat parts. + split_message = utils.splitMessage(text); + if (split_message.pretext && split_message.text) { + formatted_message = util.format("%s\n%s", split_message.pretext, split_message.text); + } else { + formatted_message = split_message.pretext || split_message.text; + } + + this.robot.send.call(this.robot, recipient, formatted_message); +}; + +YammerMessagingHandler.prototype.normalizeAddressee = function(msg) { + return { + name: String(msg.message.user.thread_id), + room: msg.message.room + }; +}; + +module.exports = YammerMessagingHandler; \ No newline at end of file diff --git a/src/lib/stackstorm_api.js b/src/lib/stackstorm_api.js index 088220b..3408846 100644 --- a/src/lib/stackstorm_api.js +++ b/src/lib/stackstorm_api.js @@ -76,38 +76,36 @@ function StackStormApi(logger) { util.inherits(StackStormApi, EventEmitter); -StackStormApi.prototype.start = function start() { +StackStormApi.prototype.startListener = function start() { var self = this; - self.api.stream.listen().catch(function (err) { - self.logger.error('Unable to connect to stream:', err); - }).then(function (source) { - source.onerror = function (err) { - // TODO: squeeze a little bit more info out of evensource.js - self.logger.error('Stream error:', err); - }; - source.addEventListener('st2.announcement__chatops', function (e) { - var data; - - self.logger.debug('Chatops message received:', e.data); - - if (e.data) { - data = JSON.parse(e.data).payload; - } else { - data = e.data; - } - - self.emit('chatops_announcement', data); - - // Special handler to try and figure out when a hipchat message - // is a whisper: + return self.api.stream.listen() + .catch(function (err) { + self.logger.error('Unable to connect to stream:', err); + }) + .then(function (source) { + source.onerror = function (err) { + // TODO: squeeze a little bit more info out of evensource.js + self.logger.error('Stream error:', err); + }; + source.addEventListener('st2.announcement__chatops', function (e) { + var data; + + self.logger.debug('Chatops message received:', e.data); + + if (e.data) { + data = JSON.parse(e.data).payload; + } else { + data = e.data; + } - // TODO: move to postdata logic or in the event listener - // if (this.robot.adapterName === 'hipchat' && !data.whisper && data.channel.indexOf('@') > -1) { - // data.whisper = true; - // this.robot.logger.debug('Set whisper to true for hipchat message'); - // } + self.emit('st2.chatops_announcement', data); + }); + return source; + }) + .then(function (source) { + // source.removeAllListeners(); + // source.close(); }); - }); }; StackStormApi.prototype.getAliases = function () { @@ -140,53 +138,44 @@ StackStormApi.prototype.sendAck = function (msg, res) { return msg.send(message + history); }; -StackStormApi.prototype.createExecution = function (msg, payload) { +// TODO: decouple the msg object from stackstorm api, this should use an event emitter +StackStormApi.prototype.executeCommand = function (msg, alias_name, format_string, command, addressee) { + var self = this; + var payload = { + 'name': alias_name, + 'format': format_string, + 'command': command, + 'user': addressee.name, + 'source_channel': addressee.room, + 'notification_route': env.ST2_ROUTE || 'hubot' + }; + this.logger.debug('Sending command payload:', JSON.stringify(payload)); return this.api.aliasExecution.create(payload) .then(function (res) { this.sendAck(msg, res); }) .catch(function (err) { - // Compatibility with older StackStorm versions if (err.status === 200) { return this.sendAck(msg, { execution: { id: err.message } }); } this.logger.error('Failed to create an alias execution:', err); - var addressee = utils.normalizeAddressee(msg, robot.adapterName); var message = util.format(_.sample(ERROR_MESSAGES), err.message); if (err.requestId) { message = util.format( message, util.format('; Use request ID %s to grep st2 api logs.', err.requestId)); } - this.postDataHandler.postData({ - whisper: false, - user: addressee.name, - channel: addressee.room, + self.emit('st2.execution_error', { + name: alias_name, + format_string: format_string, message: message, - extra: { - color: '#F35A00' - } + addressee: addressee, + command: command }); throw err; }); }; -StackStormApi.prototype.executeCommand = function (msg, command_name, format_string, command, addressee) { - var addressee = utils.normalizeAddressee(msg, this.robot.adapterName); - var payload = { - 'name': command_name, - 'format': format_string, - 'command': command, - 'user': addressee.name, - 'source_channel': addressee.room, - 'notification_route': env.ST2_ROUTE || 'hubot' - }; - - this.logger.debug('Sending command payload:', JSON.stringify(payload)); - - return this.api.aliasExecution.create(payload); -}; - StackStormApi.prototype.authenticate = function authenticate() { this.api.removeListener('expiry', authenticate); @@ -222,7 +211,6 @@ StackStormApi.prototype.authenticate = function authenticate() { }) .catch(function (err) { this.logger.error('Failed to authenticate: ' + err.message); - throw err; }); }; \ No newline at end of file diff --git a/src/stackstorm.js b/src/stackstorm.js index d413fb4..30861c7 100644 --- a/src/stackstorm.js +++ b/src/stackstorm.js @@ -40,31 +40,79 @@ var formatData = require('./lib/format_data'); var messaging_handler = require('./lib/messaging_handler'); var CommandFactory = require('./lib/command_factory'); var StackStormApi = require('./lib/stackstorm_api'); -var st2client = require('st2client'); var uuid = require('node-uuid'); -module.exports = function(robot) { +module.exports = function (robot) { var self = this; - var promise = Promise.resolve(); + var stackstormApi = new StackStormApi(robot.logger); + var commandFactory = new CommandFactory(robot); + var messagingHandler = messaging_handler.getMessagingHandler(robot.adapterName, robot); - // factory to manage commands - var command_factory = new CommandFactory(robot); + robot.router.post('/hubot/st2', function (req, res) { + var data; - // formatter to manage per adapter message formatting. - var formatter = formatData.getFormatter(robot.adapterName, robot); + try { + if (req.body.payload) { + data = JSON.parse(req.body.payload); + } else { + data = req.body; + } + // Special handler to try and figure out when a hipchat message + // is a whisper: + // TODO: move this logic to the messaging handler + if (robot.adapterName === 'hipchat' && !data.whisper && data.channel.indexOf('@') > -1) { + data.whisper = true; + robot.logger.debug('Set whisper to true for hipchat message'); + } - // handler to manage per adapter message post-ing. - var messagingHandler = messaging_handler.getMessagingHandler(robot.adapterName, robot, formatter); + messagingHandler.postData(data); - var stackstorm_api = new StackStormApi(robot.logger); + res.send('{"status": "completed", "msg": "Message posted successfully"}'); + } catch (e) { + robot.logger.error("Unable to decode JSON: " + e); + robot.logger.error(e.stack); + res.send('{"status": "failed", "msg": "An error occurred trying to post the message: ' + e + '"}'); + } + }); + + stackstormApi.on('st2.chatops_announcement', function (data) { + // TODO: move this logic to the messaging handler + if (robot.adapterName === 'hipchat' && !data.whisper && data.channel.indexOf('@') > -1) { + data.whisper = true; + robot.logger.debug('Set whisper to true for hipchat message'); + } + messagingHandler.postData(data); + }); + stackstormApi.on('st2.execution_error', function (data) { + messagingHandler.postData({ + whisper: false, + user: data.addressee.name, + channel: data.addressee.room, + message: data.message, + extra: { + color: '#F35A00' + } + }); + }); - // Authenticate with StackStorm backend and then call start. - // On a failure to authenticate log the error but do not quit. - return promise.then(function () { - start(); - return stop; + commandFactory.on('st2.command_match', function (data) { + stackstormApi.executeCommand(data.msg, data.alias_name, data.format_string, data.command, data.addressee); }); + + return stackstormApi.authenticate() + .then(function () { + return stackstormApi.getAliases(); + }) + .then(function (aliases) { + _.each(aliases, function (alias) { + commandFactory.addCommand(alias, messagingHandler); + }); + }) + .then(function () { + return stackstormApi.startListener(); + }); + }; From 0b269cb5d10a5801ac43b0b4549f15013211bd1f Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Tue, 28 Feb 2017 08:33:59 -0700 Subject: [PATCH 05/26] fixing import typo --- src/lib/command_factory.js | 1 + src/stackstorm.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/command_factory.js b/src/lib/command_factory.js index 01b51ef..c9cc281 100644 --- a/src/lib/command_factory.js +++ b/src/lib/command_factory.js @@ -64,6 +64,7 @@ function CommandFactory(robot) { util.inherits(CommandFactory, EventEmitter); +// TODO: decouple messaging_handler from command factory CommandFactory.prototype.addCommand = function (action_alias, messaging_handler) { var self = this; diff --git a/src/stackstorm.js b/src/stackstorm.js index 30861c7..5938d2c 100644 --- a/src/stackstorm.js +++ b/src/stackstorm.js @@ -34,7 +34,7 @@ var _ = require('lodash'); var util = require('util'); var env = _.clone(process.env); var Promise = require('rsvp').Promise; -var utils = require('../lib/utils'); +var utils = require('./lib/utils'); var formatCommand = require('./lib/format_command'); var formatData = require('./lib/format_data'); var messaging_handler = require('./lib/messaging_handler'); From 6a47bd4039c92e23c062c5e6588a7804ab632db4 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Wed, 1 Mar 2017 13:53:24 -0700 Subject: [PATCH 06/26] finished refactor and fixing some bugs --- src/lib/command_factory.js | 17 ++++++---- src/lib/messaging_handler/default.js | 23 ++++++++----- src/lib/messaging_handler/hipchat.js | 17 ++++++---- src/lib/messaging_handler/index.js | 4 +-- src/lib/messaging_handler/slack.js | 12 ++++--- src/lib/messaging_handler/yammer.js | 10 +++--- src/lib/stackstorm_api.js | 49 +++++++++++++++------------- src/stackstorm.js | 5 ++- 8 files changed, 79 insertions(+), 58 deletions(-) diff --git a/src/lib/command_factory.js b/src/lib/command_factory.js index c9cc281..8b16f6f 100644 --- a/src/lib/command_factory.js +++ b/src/lib/command_factory.js @@ -22,7 +22,7 @@ var utils = require('./utils.js'); var util = require('util'); var EventEmitter = require('events').EventEmitter; -var formatCommand = function(name, format, description) { +var formatHelpCommand = function(name, format, description) { var context, template_str, compiled_template, command; if (!format) { @@ -34,7 +34,7 @@ var formatCommand = function(name, format, description) { 'description': description }; - template_str = '${format} - ${description}'; + template_str = 'hubot ${format} - ${description}'; compiled_template = _.template(template_str); command = compiled_template(context); @@ -58,7 +58,8 @@ var getRegexForFormatString = function (format) { }; function CommandFactory(robot) { - this.robot = robot; + var self = this; + self.robot = robot; EventEmitter.call(this); } @@ -82,9 +83,8 @@ CommandFactory.prototype.addCommand = function (action_alias, messaging_handler) var action_alias_name = action_alias.name; _.each(action_alias.formats, function (format) { - var formatted_string = formatCommand(action_alias.name, format.display || format, action_alias.description); - var compiled_template = _.template('hubot ${command}'); - self.robot.commands.push(compiled_template({ robotName: self.robot.name, command: formatted_string })); + var formatted_string = format.display || format; + self.robot.commands.push(formatHelpCommand(action_alias.name, formatted_string, action_alias.description)); if (format.display) { _.each(format.representation, function (representation) { @@ -99,6 +99,9 @@ CommandFactory.prototype.addCommand = function (action_alias, messaging_handler) self.robot.listen(function (msg) { var i, format_string, regex; + if (!msg.text) { + return false; + } var command = messaging_handler.normalizeCommand(msg.text); for (i = 0; i < format_strings.length; i++) { format_string = format_strings[i]; @@ -110,7 +113,7 @@ CommandFactory.prototype.addCommand = function (action_alias, messaging_handler) } } return false; - }, { id: action_alias_name }, function (msg) { + }, { id: 'st2.' + action_alias_name }, function (msg) { self.emit('st2.command_match', { msg: msg, alias_name: action_alias_name, diff --git a/src/lib/messaging_handler/default.js b/src/lib/messaging_handler/default.js index e6ae634..401ba29 100644 --- a/src/lib/messaging_handler/default.js +++ b/src/lib/messaging_handler/default.js @@ -5,14 +5,17 @@ var env = process.env; var util = require('util'); var utils = require('./../utils'); var truncate = require('truncate'); -var hubot_alias = env.HUBOT_ALIAS; + +var hubot_alias_regex = new RegExp('^' + env.HUBOT_ALIAS); function DefaultMessagingHandler(robot) { - this.robot = robot; - this.truncate_length = env.ST2_MAX_MESSAGE_LENGTH; + var self = this; + self.robot = robot; + self.truncate_length = env.ST2_MAX_MESSAGE_LENGTH; } DefaultMessagingHandler.prototype.postData = function(data) { + var self = this; var recipient, split_message, formatted_message, text = ""; @@ -23,8 +26,8 @@ DefaultMessagingHandler.prototype.postData = function(data) { text = (data.user && !data.whisper) ? util.format('%s: ', data.user) : ""; } - recipient = this.formatRecepient(recipient); - text += this.formatData(data.message); + recipient = self.formatRecepient(recipient); + text += self.formatData(data.message); // Ignore the delimiter in the default formatter and just concat parts. split_message = utils.splitMessage(text); @@ -34,7 +37,7 @@ DefaultMessagingHandler.prototype.postData = function(data) { formatted_message = split_message.pretext || split_message.text; } - this.robot.messageRoom.call(this.robot, recipient, formatted_message); + self.robot.messageRoom.call(self.robot, recipient, formatted_message); }; DefaultMessagingHandler.normalizeAddressee = function(msg) { @@ -45,11 +48,13 @@ DefaultMessagingHandler.normalizeAddressee = function(msg) { }; DefaultMessagingHandler.prototype.formatData = function(data) { + var self = this; + if (utils.isNull(data)) { return ""; } - if (this.truncate_length > 0) { - data = truncate(data, this.truncate_length); + if (self.truncate_length > 0) { + data = truncate(data, self.truncate_length); } return data; }; @@ -59,7 +64,7 @@ DefaultMessagingHandler.prototype.formatRecepient = function(recepient) { }; DefaultMessagingHandler.prototype.normalizeCommand = function(command) { - return command.replace(/^${hubot_alias}/, "").trim(); + return command.replace(hubot_alias_regex, "").trim(); }; module.exports = DefaultMessagingHandler; \ No newline at end of file diff --git a/src/lib/messaging_handler/hipchat.js b/src/lib/messaging_handler/hipchat.js index bb590f3..fe8a042 100644 --- a/src/lib/messaging_handler/hipchat.js +++ b/src/lib/messaging_handler/hipchat.js @@ -8,12 +8,14 @@ var DefaultMessagingHandler = require('./default'); function HipchatMessagingHandler(robot) { - DefaultMessagingHandler.call(this, robot); + var self = this; + DefaultMessagingHandler.call(self, robot); } util.inherits(HipchatMessagingHandler, DefaultMessagingHandler); HipchatMessagingHandler.prototype.postData = function(data) { + var self = this; var recipient, split_message, formatted_message, pretext = ""; @@ -23,7 +25,7 @@ HipchatMessagingHandler.prototype.postData = function(data) { } if (recipient.indexOf('@') === -1 ) { - recipient = this.formatRecepient(recipient); + recipient = self.formatRecepient(recipient); } split_message = utils.splitMessage(data.message); if (pretext) { @@ -34,16 +36,16 @@ HipchatMessagingHandler.prototype.postData = function(data) { same message, so split them */ if (split_message.pretext) { if (data.whisper) { - this.robot.send.call(this.robot, data.channel, split_message.pretext); + self.robot.send.call(self.robot, data.channel, split_message.pretext); } else { - this.robot.messageRoom.call(this.robot, recipient, split_message.pretext); + self.robot.messageRoom.call(self.robot, recipient, split_message.pretext); } } if (split_message.text) { if (data.whisper) { - this.robot.send.call(this.robot, data.channel, this.formatData(split_message.text)); + self.robot.send.call(self.robot, data.channel, self.formatData(split_message.text)); } else { - this.robot.messageRoom.call(this.robot, recipient, this.formatData(split_message.text)); + self.robot.messageRoom.call(self.robot, recipient, self.formatData(split_message.text)); } } }; @@ -76,7 +78,8 @@ HipchatMessagingHandler.prototype.formatRecepient = function(recepient) { }; HipchatMessagingHandler.prototype.normalizeCommand = function(command) { - return HipchatMessagingHandler.super_.prototype.normalizeCommand.call(this, command); + var self = this; + return HipchatMessagingHandler.super_.prototype.normalizeCommand.call(self, command); }; module.exports = HipchatMessagingHandler; diff --git a/src/lib/messaging_handler/index.js b/src/lib/messaging_handler/index.js index 64d65e8..995d6ea 100644 --- a/src/lib/messaging_handler/index.js +++ b/src/lib/messaging_handler/index.js @@ -6,7 +6,7 @@ var util = require('util'); var fs = require('fs'); var path = require('path'); -var filenames = fs.readdirSync('./'); +var filenames = fs.readdirSync(__dirname); var messagingHandlers = {}; @@ -16,7 +16,7 @@ filenames.forEach(function(filename) { } var message_handler = filename.replace(/\.[^/.]+$/, ""); - messagingHandlers[message_handler] = require('./' + filename); + messagingHandlers[message_handler] = require(path.join(__dirname, filename)); }); module.exports.getMessagingHandler = function(adapterName, robot) { diff --git a/src/lib/messaging_handler/slack.js b/src/lib/messaging_handler/slack.js index cad17a4..4d8bd5e 100644 --- a/src/lib/messaging_handler/slack.js +++ b/src/lib/messaging_handler/slack.js @@ -7,6 +7,7 @@ var utils = require('./../utils'); var DefaultMessagingHandler = require('./default'); function SlackMessagingHandler(robot) { + var self = this; DefaultMessagingHandler.call(this, robot); var sendMessageRaw = function(message) { @@ -26,6 +27,8 @@ function SlackMessagingHandler(robot) { util.inherits(SlackMessagingHandler, DefaultMessagingHandler); SlackMessagingHandler.prototype.postData = function(data) { + var self = this; + var recipient, attachment_color, split_message, attachment, pretext = ""; @@ -45,7 +48,7 @@ SlackMessagingHandler.prototype.postData = function(data) { } } - split_message = utils.splitMessage(this.formatData(data.message)); + split_message = utils.splitMessage(self.formatData(data.message)); if (split_message.text) { var content = { @@ -55,7 +58,7 @@ SlackMessagingHandler.prototype.postData = function(data) { if (data.extra && data.extra.slack) { for (var attrname in data.extra.slack) { content[attrname] = data.extra.slack[attrname]; } } - var robot = this.robot; + var robot = self.robot; var chunks = split_message.text.match(/[\s\S]{1,7900}/g); var sendChunk = function (i) { content.text = chunks[i]; @@ -72,7 +75,7 @@ SlackMessagingHandler.prototype.postData = function(data) { }; sendChunk(0); } else { - this.robot.messageRoom.call(this.robot, recipient, pretext + split_message.pretext); + self.robot.messageRoom.call(self.robot, recipient, pretext + split_message.pretext); } }; @@ -97,7 +100,8 @@ SlackMessagingHandler.prototype.formatRecepient = function(recepient) { }; SlackMessagingHandler.prototype.normalizeCommand = function(command) { - command = SlackMessagingHandler.super_.prototype.normalizeCommand.call(this, command); + var self = this; + command = SlackMessagingHandler.super_.prototype.normalizeCommand.call(self, command); // replace left double quote with regular quote command = command.replace(/\u201c/g, '\u0022'); // replace right double quote with regular quote diff --git a/src/lib/messaging_handler/yammer.js b/src/lib/messaging_handler/yammer.js index ec59687..99faa7c 100644 --- a/src/lib/messaging_handler/yammer.js +++ b/src/lib/messaging_handler/yammer.js @@ -7,12 +7,14 @@ var utils = require('./../utils'); var DefaultMessagingHandler = require('./default'); function YammerMessagingHandler(robot) { - DefaultMessagingHandler.call(this, robot); + var self = this; + DefaultMessagingHandler.call(self, robot); } util.inherits(YammerMessagingHandler, DefaultMessagingHandler); YammerMessagingHandler.prototype.postData = function(data) { + var self = this; var recipient, split_message, formatted_message, text = ""; @@ -23,8 +25,8 @@ YammerMessagingHandler.prototype.postData = function(data) { text = (data.user && !data.whisper) ? util.format('@%s: ', data.user) : ""; } - recipient = this.formatRecepient(recipient); - text += this.formatData(data.message); + recipient = self.formatRecepient(recipient); + text += self.formatData(data.message); // Ignore the delimiter in the default formatter and just concat parts. split_message = utils.splitMessage(text); @@ -34,7 +36,7 @@ YammerMessagingHandler.prototype.postData = function(data) { formatted_message = split_message.pretext || split_message.text; } - this.robot.send.call(this.robot, recipient, formatted_message); + self.robot.send.call(self.robot, recipient, formatted_message); }; YammerMessagingHandler.prototype.normalizeAddressee = function(msg) { diff --git a/src/lib/stackstorm_api.js b/src/lib/stackstorm_api.js index 3408846..ee5c635 100644 --- a/src/lib/stackstorm_api.js +++ b/src/lib/stackstorm_api.js @@ -11,7 +11,6 @@ var EventEmitter = require('events').EventEmitter; // Setup the Environment env.ST2_API = env.ST2_API || 'http://localhost:9101'; env.ST2_ROUTE = env.ST2_ROUTE || null; -env.ST2_ROUTE = env.ST2_WEBUI_URL || null; // Optional authentication info env.ST2_AUTH_USERNAME = env.ST2_AUTH_USERNAME || null; @@ -45,7 +44,8 @@ var ERROR_MESSAGES = [ function StackStormApi(logger) { - this.logger = logger; + var self = this; + self.logger = logger; var url = utils.parseUrl(env.ST2_API); var opts = { @@ -56,11 +56,11 @@ function StackStormApi(logger) { rejectUnauthorized: false }; - this.api = st2client(opts); + self.api = st2client(opts); if (env.ST2_API_KEY) { - this.api.setKey({ key: env.ST2_API_KEY }); + self.api.setKey({ key: env.ST2_API_KEY }); } else if (env.ST2_AUTH_TOKEN) { - this.api.setToken({ token: env.ST2_AUTH_TOKEN }); + self.api.setToken({ token: env.ST2_AUTH_TOKEN }); } if (env.ST2_API_KEY || env.ST2_AUTH_TOKEN || env.ST2_AUTH_USERNAME || env.ST2_AUTH_PASSWORD) { @@ -109,11 +109,13 @@ StackStormApi.prototype.startListener = function start() { }; StackStormApi.prototype.getAliases = function () { - this.logger.info('Loading commands....'); - return this.api.actionAlias.list() + var self = this; + + self.logger.info('Getting Action Aliases....'); + return self.api.actionAlias.list() .catch(function (err) { var error_msg = 'Failed to retrieve commands from "%s": %s'; - this.logger.error(util.format(error_msg, env.ST2_API, err.message)); + self.logger.error(util.format(error_msg, env.ST2_API, err.message)); return []; }); }; @@ -150,15 +152,15 @@ StackStormApi.prototype.executeCommand = function (msg, alias_name, format_strin 'notification_route': env.ST2_ROUTE || 'hubot' }; - this.logger.debug('Sending command payload:', JSON.stringify(payload)); + self.logger.debug('Sending command payload:', JSON.stringify(payload)); - return this.api.aliasExecution.create(payload) - .then(function (res) { this.sendAck(msg, res); }) + return self.api.aliasExecution.create(payload) + .then(function (res) { self.sendAck(msg, res); }) .catch(function (err) { if (err.status === 200) { - return this.sendAck(msg, { execution: { id: err.message } }); + return self.sendAck(msg, { execution: { id: err.message } }); } - this.logger.error('Failed to create an alias execution:', err); + self.logger.error('Failed to create an alias execution:', err); var message = util.format(_.sample(ERROR_MESSAGES), err.message); if (err.requestId) { message = util.format( @@ -177,20 +179,21 @@ StackStormApi.prototype.executeCommand = function (msg, alias_name, format_strin }; StackStormApi.prototype.authenticate = function authenticate() { - this.api.removeListener('expiry', authenticate); + var self = this; + self.api.removeListener('expiry', authenticate); // API key gets precedence 1 if (env.ST2_API_KEY) { - this.logger.info('Using ST2_API_KEY as authentication. Expiry will lead to bot exit.'); + self.logger.info('Using ST2_API_KEY as authentication. Expiry will lead to bot exit.'); return Promise.resolve(); } // Auth token gets precedence 2 if (env.ST2_AUTH_TOKEN) { - this.logger.info('Using ST2_AUTH_TOKEN as authentication. Expiry will lead to bot exit.'); + self.logger.info('Using ST2_AUTH_TOKEN as authentication. Expiry will lead to bot exit.'); return Promise.resolve(); } - this.logger.info('Requesting a token...'); + self.logger.info('Requesting a token...'); var url = utils.parseUrl(env.ST2_AUTH_URL); @@ -205,12 +208,14 @@ StackStormApi.prototype.authenticate = function authenticate() { return client.authenticate(env.ST2_AUTH_USERNAME, env.ST2_AUTH_PASSWORD) .then(function (token) { - this.logger.info('Token received. Expiring ' + token.expiry); - this.api.setToken(token); - client.on('expiry', this.authenticate); + self.logger.info('Token received. Expiring ' + token.expiry); + self.api.setToken(token); + client.on('expiry', self.authenticate); }) .catch(function (err) { - this.logger.error('Failed to authenticate: ' + err.message); + self.logger.error('Failed to authenticate: ' + err.message); throw err; }); -}; \ No newline at end of file +}; + +module.exports = StackStormApi; \ No newline at end of file diff --git a/src/stackstorm.js b/src/stackstorm.js index 5938d2c..973c164 100644 --- a/src/stackstorm.js +++ b/src/stackstorm.js @@ -35,8 +35,6 @@ var util = require('util'); var env = _.clone(process.env); var Promise = require('rsvp').Promise; var utils = require('./lib/utils'); -var formatCommand = require('./lib/format_command'); -var formatData = require('./lib/format_data'); var messaging_handler = require('./lib/messaging_handler'); var CommandFactory = require('./lib/command_factory'); var StackStormApi = require('./lib/stackstorm_api'); @@ -99,7 +97,7 @@ module.exports = function (robot) { }); commandFactory.on('st2.command_match', function (data) { - stackstormApi.executeCommand(data.msg, data.alias_name, data.format_string, data.command, data.addressee); + stackstormApi.executeCommand(data.msg, data.alias_name, data.command_format_string, data.command, data.addressee); }); return stackstormApi.authenticate() @@ -110,6 +108,7 @@ module.exports = function (robot) { _.each(aliases, function (alias) { commandFactory.addCommand(alias, messagingHandler); }); + return aliases; }) .then(function () { return stackstormApi.startListener(); From 8aaa99c75323967bfb6f29d7372950c6754bdc94 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Wed, 1 Mar 2017 16:11:27 -0700 Subject: [PATCH 07/26] updating to support weburl for acknowlegement messages --- src/lib/stackstorm_api.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/stackstorm_api.js b/src/lib/stackstorm_api.js index ee5c635..9d125ac 100644 --- a/src/lib/stackstorm_api.js +++ b/src/lib/stackstorm_api.js @@ -11,6 +11,7 @@ var EventEmitter = require('events').EventEmitter; // Setup the Environment env.ST2_API = env.ST2_API || 'http://localhost:9101'; env.ST2_ROUTE = env.ST2_ROUTE || null; +env.ST2_WEBUI_URL = env.ST2_WEBUI_URL || null; // Optional authentication info env.ST2_AUTH_USERNAME = env.ST2_AUTH_USERNAME || null; @@ -121,6 +122,7 @@ StackStormApi.prototype.getAliases = function () { }; StackStormApi.prototype.sendAck = function (msg, res) { + res.execution.web_url = env.ST2_WEBUI_URL; var history_url = utils.getExecutionHistoryUrl(res.execution); var history = history_url ? util.format(' (details available at %s)', history_url) : ''; @@ -141,11 +143,11 @@ StackStormApi.prototype.sendAck = function (msg, res) { }; // TODO: decouple the msg object from stackstorm api, this should use an event emitter -StackStormApi.prototype.executeCommand = function (msg, alias_name, format_string, command, addressee) { +StackStormApi.prototype.executeCommand = function (msg, alias_name, command_format_string, command, addressee) { var self = this; var payload = { 'name': alias_name, - 'format': format_string, + 'format': command_format_string, 'command': command, 'user': addressee.name, 'source_channel': addressee.room, @@ -169,7 +171,7 @@ StackStormApi.prototype.executeCommand = function (msg, alias_name, format_strin } self.emit('st2.execution_error', { name: alias_name, - format_string: format_string, + format_string: command_format_string, message: message, addressee: addressee, command: command From 5f51dff830f06d529da7c45ba88e95a41e069fe5 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Wed, 1 Mar 2017 16:12:47 -0700 Subject: [PATCH 08/26] Revert "updating to support weburl for acknowlegement messages" This reverts commit 8aaa99c75323967bfb6f29d7372950c6754bdc94. --- src/lib/stackstorm_api.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/lib/stackstorm_api.js b/src/lib/stackstorm_api.js index 9d125ac..ee5c635 100644 --- a/src/lib/stackstorm_api.js +++ b/src/lib/stackstorm_api.js @@ -11,7 +11,6 @@ var EventEmitter = require('events').EventEmitter; // Setup the Environment env.ST2_API = env.ST2_API || 'http://localhost:9101'; env.ST2_ROUTE = env.ST2_ROUTE || null; -env.ST2_WEBUI_URL = env.ST2_WEBUI_URL || null; // Optional authentication info env.ST2_AUTH_USERNAME = env.ST2_AUTH_USERNAME || null; @@ -122,7 +121,6 @@ StackStormApi.prototype.getAliases = function () { }; StackStormApi.prototype.sendAck = function (msg, res) { - res.execution.web_url = env.ST2_WEBUI_URL; var history_url = utils.getExecutionHistoryUrl(res.execution); var history = history_url ? util.format(' (details available at %s)', history_url) : ''; @@ -143,11 +141,11 @@ StackStormApi.prototype.sendAck = function (msg, res) { }; // TODO: decouple the msg object from stackstorm api, this should use an event emitter -StackStormApi.prototype.executeCommand = function (msg, alias_name, command_format_string, command, addressee) { +StackStormApi.prototype.executeCommand = function (msg, alias_name, format_string, command, addressee) { var self = this; var payload = { 'name': alias_name, - 'format': command_format_string, + 'format': format_string, 'command': command, 'user': addressee.name, 'source_channel': addressee.room, @@ -171,7 +169,7 @@ StackStormApi.prototype.executeCommand = function (msg, alias_name, command_form } self.emit('st2.execution_error', { name: alias_name, - format_string: command_format_string, + format_string: format_string, message: message, addressee: addressee, command: command From 08d8caf52e2f4a4c055afe22bcfb4f6cc99a6b33 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Sat, 4 Mar 2017 16:40:16 -0700 Subject: [PATCH 09/26] moving hipchat logic to messaging handler --- src/lib/messaging_handler/hipchat.js | 6 ++++++ src/lib/stackstorm_api.js | 22 ++++++++++++++++++---- src/stackstorm.js | 22 ++++------------------ 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/lib/messaging_handler/hipchat.js b/src/lib/messaging_handler/hipchat.js index fe8a042..7f5116c 100644 --- a/src/lib/messaging_handler/hipchat.js +++ b/src/lib/messaging_handler/hipchat.js @@ -16,6 +16,12 @@ util.inherits(HipchatMessagingHandler, DefaultMessagingHandler); HipchatMessagingHandler.prototype.postData = function(data) { var self = this; + + if (!data.whisper && data.channel.indexOf('@') > -1) { + data.whisper = true; + self.robot.logger.debug('Set whisper to true for hipchat message'); + } + var recipient, split_message, formatted_message, pretext = ""; diff --git a/src/lib/stackstorm_api.js b/src/lib/stackstorm_api.js index ee5c635..30e3728 100644 --- a/src/lib/stackstorm_api.js +++ b/src/lib/stackstorm_api.js @@ -25,6 +25,9 @@ env.ST2_API_KEY = env.ST2_API_KEY || null; // Optional, if not provided, we infer it from the API URL env.ST2_AUTH_URL = env.ST2_AUTH_URL || null; +env.ST2_ALIAS_PACK_RESTRICTION = env.ST2_ALIAS_PACK_RESTRICTION || null; + + var START_MESSAGES = [ "I'll take it from here! Your execution ID for reference is %s", "Got it! Remember %s as your execution ID", @@ -102,10 +105,6 @@ StackStormApi.prototype.startListener = function start() { }); return source; }) - .then(function (source) { - // source.removeAllListeners(); - // source.close(); - }); }; StackStormApi.prototype.getAliases = function () { @@ -113,6 +112,21 @@ StackStormApi.prototype.getAliases = function () { self.logger.info('Getting Action Aliases....'); return self.api.actionAlias.list() + .then(function (aliases) { + if (env.ST2_ALIAS_PACK_RESTRICTION) { + self.logger.info('Alias Restrictions are in place, only retrieving aliases from the following pack(s): ' + env.ST2_ALIAS_PACK_RESTRICTION); + var return_aliases = []; + var restrictions = env.ST2_ALIAS_PACK_RESTRICTION.split(','); + _.each(aliases, function(alias) { + if (restrictions.indexOf(alias.pack) > -1) { + return_aliases.push(alias); + } + }); + return return_aliases; + } else { + return aliases; + } + }) .catch(function (err) { var error_msg = 'Failed to retrieve commands from "%s": %s'; self.logger.error(util.format(error_msg, env.ST2_API, err.message)); diff --git a/src/stackstorm.js b/src/stackstorm.js index 973c164..d6a94c2 100644 --- a/src/stackstorm.js +++ b/src/stackstorm.js @@ -49,23 +49,13 @@ module.exports = function (robot) { robot.router.post('/hubot/st2', function (req, res) { var data; - try { if (req.body.payload) { data = JSON.parse(req.body.payload); } else { data = req.body; } - // Special handler to try and figure out when a hipchat message - // is a whisper: - // TODO: move this logic to the messaging handler - if (robot.adapterName === 'hipchat' && !data.whisper && data.channel.indexOf('@') > -1) { - data.whisper = true; - robot.logger.debug('Set whisper to true for hipchat message'); - } - messagingHandler.postData(data); - res.send('{"status": "completed", "msg": "Message posted successfully"}'); } catch (e) { robot.logger.error("Unable to decode JSON: " + e); @@ -75,12 +65,6 @@ module.exports = function (robot) { }); stackstormApi.on('st2.chatops_announcement', function (data) { - // TODO: move this logic to the messaging handler - if (robot.adapterName === 'hipchat' && !data.whisper && data.channel.indexOf('@') > -1) { - data.whisper = true; - robot.logger.debug('Set whisper to true for hipchat message'); - } - messagingHandler.postData(data); }); @@ -108,10 +92,12 @@ module.exports = function (robot) { _.each(aliases, function (alias) { commandFactory.addCommand(alias, messagingHandler); }); - return aliases; }) .then(function () { return stackstormApi.startListener(); }); - + // .then(function (source) { + // source.removeAllListeners(); + // source.close(); + // }); }; From 2afb8aed95b0081c97cabf5fc924f9443afc33a1 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Sat, 4 Mar 2017 18:02:53 -0700 Subject: [PATCH 10/26] updates to support hubot-middleware-auth-ext --- src/lib/command_factory.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/lib/command_factory.js b/src/lib/command_factory.js index 8b16f6f..762916d 100644 --- a/src/lib/command_factory.js +++ b/src/lib/command_factory.js @@ -97,6 +97,17 @@ CommandFactory.prototype.addCommand = function (action_alias, messaging_handler) var format_strings = Object.keys(commands_regex_map); + var listener_opts = { + source: 'st2', + id: 'st2.' + action_alias_name + } + + if (action_alias.extra && action_alias.extra.hubot_auth) { + listener_opts.auth = true; + listener_opts.roles = action_alias.extra.hubot_auth.roles; + listener_opts.rooms = action_alias.extra.hubot_auth.rooms; + } + self.robot.listen(function (msg) { var i, format_string, regex; if (!msg.text) { @@ -113,7 +124,7 @@ CommandFactory.prototype.addCommand = function (action_alias, messaging_handler) } } return false; - }, { id: 'st2.' + action_alias_name }, function (msg) { + }, listener_opts, function (msg) { self.emit('st2.command_match', { msg: msg, alias_name: action_alias_name, From 4280b5b01cd5656d696f5fc6dca11547c537b6fe Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Sat, 4 Mar 2017 18:18:02 -0700 Subject: [PATCH 11/26] fixing the auth by putting quotes around true --- src/lib/command_factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/command_factory.js b/src/lib/command_factory.js index 762916d..f0d16d9 100644 --- a/src/lib/command_factory.js +++ b/src/lib/command_factory.js @@ -103,7 +103,7 @@ CommandFactory.prototype.addCommand = function (action_alias, messaging_handler) } if (action_alias.extra && action_alias.extra.hubot_auth) { - listener_opts.auth = true; + listener_opts.auth = 'true'; listener_opts.roles = action_alias.extra.hubot_auth.roles; listener_opts.rooms = action_alias.extra.hubot_auth.rooms; } From 5ea29cc1c77d85e68783769340a2766b4100a690 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Sun, 5 Mar 2017 19:55:17 -0700 Subject: [PATCH 12/26] testing brain storage --- src/stackstorm.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stackstorm.js b/src/stackstorm.js index d6a94c2..19c40f1 100644 --- a/src/stackstorm.js +++ b/src/stackstorm.js @@ -43,6 +43,7 @@ var uuid = require('node-uuid'); module.exports = function (robot) { var self = this; + robot.brain.set('foo', 'bar'); var stackstormApi = new StackStormApi(robot.logger); var commandFactory = new CommandFactory(robot); var messagingHandler = messaging_handler.getMessagingHandler(robot.adapterName, robot); From 710388fd042aad1192377075ca609b975eaa0787 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Sun, 5 Mar 2017 19:59:20 -0700 Subject: [PATCH 13/26] tring hubot brain test again --- src/stackstorm.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stackstorm.js b/src/stackstorm.js index 19c40f1..658e64b 100644 --- a/src/stackstorm.js +++ b/src/stackstorm.js @@ -44,6 +44,7 @@ module.exports = function (robot) { var self = this; robot.brain.set('foo', 'bar'); + robot.brain.save(); var stackstormApi = new StackStormApi(robot.logger); var commandFactory = new CommandFactory(robot); var messagingHandler = messaging_handler.getMessagingHandler(robot.adapterName, robot); From 81475c2dfec422977af7a4a4a1757a4a58c75d67 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Sun, 5 Mar 2017 20:55:57 -0700 Subject: [PATCH 14/26] removing log entries --- src/stackstorm.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/stackstorm.js b/src/stackstorm.js index 658e64b..d6a94c2 100644 --- a/src/stackstorm.js +++ b/src/stackstorm.js @@ -43,8 +43,6 @@ var uuid = require('node-uuid'); module.exports = function (robot) { var self = this; - robot.brain.set('foo', 'bar'); - robot.brain.save(); var stackstormApi = new StackStormApi(robot.logger); var commandFactory = new CommandFactory(robot); var messagingHandler = messaging_handler.getMessagingHandler(robot.adapterName, robot); From b0928a9daf5945eb2f3212651ed316e8d10ec7b1 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Sun, 5 Mar 2017 21:19:12 -0700 Subject: [PATCH 15/26] adding mongodb brain --- package.json | 4 ++- src/lib/mongodb_brain.js | 58 ++++++++++++++++++++++++++++++++++++++++ src/stackstorm.js | 4 +++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 src/lib/mongodb_brain.js diff --git a/package.json b/package.json index 71215c5..ca757f0 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "rsvp": "3.0.14", "st2client": "^0.4.3", "truncate": "^1.0.4", - "node-uuid": "~1.4.0" + "node-uuid": "~1.4.0", + "mongodb": "^2.0.52", + "mongodb-url": "^1.0.3" }, "peerDependencies": { "hubot": "2.x" diff --git a/src/lib/mongodb_brain.js b/src/lib/mongodb_brain.js new file mode 100644 index 0000000..58ba5b2 --- /dev/null +++ b/src/lib/mongodb_brain.js @@ -0,0 +1,58 @@ + +var mongodb = require('mongodb'); +var url = require('mongodb-url').get(); + +module.exports = function(robot) { + robot.logger.info('mongodb: Connecting to `%s`...', url); + var db; + + mongodb.connect(url, function(err, _db) { + if (err) { + robot.logger.error('mongodb: Connection failed: `%s`', err.message); + robot.logger.error(err.stack); + return; + } + db = _db; + + db.collection('hubot').findOne({ + _id: 'brain' + }, function(_err, doc) { + if (_err) { + robot.logger.error('mongodb: Lookup failed'); + robot.logger.error(_err.stack); + return; + } + if (doc) { + robot.logger.info('mongodb: loaded brain from previous document'); + robot.brain.mergeData(doc); + } else { + robot.logger.info('mongodb: Initializing...'); + robot.brain.mergeData({}); + } + robot.brain.resetSaveInterval(10); + robot.brain.setAutoSave(true); + robot.logger.info('mongodb: Ready.'); + }); + }); + + robot.brain.on('save', function(data) { + data = data || {}; + data._id = 'brain'; + robot.logger.info('mongodb: saving...'); + db.collection('hubot').save(data, function(err) { + if (err) { + robot.logger.error('mongodb: Save failed: `%s`', err.message); + return; + } + robot.logger.info('mongodb: Saved!'); + }); + }); + + robot.brain.on('close', function() { + if (db) { + robot.logger.info('mongodb: Closing client. Goodbye.'); + db.close(); + } + }); + robot.brain.setAutoSave(false); +}; \ No newline at end of file diff --git a/src/stackstorm.js b/src/stackstorm.js index d6a94c2..ff1227a 100644 --- a/src/stackstorm.js +++ b/src/stackstorm.js @@ -39,14 +39,18 @@ var messaging_handler = require('./lib/messaging_handler'); var CommandFactory = require('./lib/command_factory'); var StackStormApi = require('./lib/stackstorm_api'); var uuid = require('node-uuid'); +var mongodb_brain = require('./lib/mongodb_brain'); module.exports = function (robot) { var self = this; + mongodb_brain(robot); + var stackstormApi = new StackStormApi(robot.logger); var commandFactory = new CommandFactory(robot); var messagingHandler = messaging_handler.getMessagingHandler(robot.adapterName, robot); + robot.router.post('/hubot/st2', function (req, res) { var data; try { From bb21df5bde44cd66499fa049a91a135756081a36 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Mon, 20 Mar 2017 15:30:49 -0600 Subject: [PATCH 16/26] adding the hubot alias as a requirement to execute command --- src/lib/command_factory.js | 3 ++- src/lib/messaging_handler/default.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/command_factory.js b/src/lib/command_factory.js index f0d16d9..fe6329e 100644 --- a/src/lib/command_factory.js +++ b/src/lib/command_factory.js @@ -18,6 +18,7 @@ limitations under the License. "use strict"; var _ = require('lodash'); +var env = process.env; var utils = require('./utils.js'); var util = require('util'); var EventEmitter = require('events').EventEmitter; @@ -53,7 +54,7 @@ var getRegexForFormatString = function (format) { extra_params = '(\\s+(\\S+)\\s*=("([\\s\\S]*?)"|\'([\\s\\S]*?)\'|({[\\s\\S]*?})|(\\S+))\\s*)*'; regex_str = format.replace(/(\s*){{\s*\S+\s*=\s*(?:({.+?}|.+?))\s*}}(\s*)/g, '\\s*($1([\\s\\S]+?)$3)?\\s*'); regex_str = regex_str.replace(/\s*{{.+?}}\s*/g, '\\s*([\\s\\S]+?)\\s*'); - regex = new RegExp('^\\s*' + regex_str + extra_params + '\\s*$', 'i'); + regex = new RegExp('^\\' + env.HUBOT_ALIAS + '\\s*' + regex_str + extra_params + '\\s*$', 'i'); return regex; }; diff --git a/src/lib/messaging_handler/default.js b/src/lib/messaging_handler/default.js index 401ba29..b5b48b6 100644 --- a/src/lib/messaging_handler/default.js +++ b/src/lib/messaging_handler/default.js @@ -64,7 +64,8 @@ DefaultMessagingHandler.prototype.formatRecepient = function(recepient) { }; DefaultMessagingHandler.prototype.normalizeCommand = function(command) { - return command.replace(hubot_alias_regex, "").trim(); + return command; + // return command.replace(hubot_alias_regex, "").trim(); }; module.exports = DefaultMessagingHandler; \ No newline at end of file From 68bafaa48aeb10bed275d7d084d42c2f9dcd3a1d Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Mon, 20 Mar 2017 15:35:12 -0600 Subject: [PATCH 17/26] fixing bug --- src/lib/command_factory.js | 2 +- src/lib/messaging_handler/default.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/command_factory.js b/src/lib/command_factory.js index fe6329e..cf5297d 100644 --- a/src/lib/command_factory.js +++ b/src/lib/command_factory.js @@ -118,7 +118,7 @@ CommandFactory.prototype.addCommand = function (action_alias, messaging_handler) for (i = 0; i < format_strings.length; i++) { format_string = format_strings[i]; regex = commands_regex_map[format_string]; - if (regex.test(command)) { + if (regex.test(msg.text)) { msg['st2_command_format_string'] = format_string; msg['normalized_command'] = command; return true; diff --git a/src/lib/messaging_handler/default.js b/src/lib/messaging_handler/default.js index b5b48b6..401ba29 100644 --- a/src/lib/messaging_handler/default.js +++ b/src/lib/messaging_handler/default.js @@ -64,8 +64,7 @@ DefaultMessagingHandler.prototype.formatRecepient = function(recepient) { }; DefaultMessagingHandler.prototype.normalizeCommand = function(command) { - return command; - // return command.replace(hubot_alias_regex, "").trim(); + return command.replace(hubot_alias_regex, "").trim(); }; module.exports = DefaultMessagingHandler; \ No newline at end of file From f633ff12ae2261482545776110f1a4d14e93f1ce Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Mon, 20 Mar 2017 17:10:27 -0600 Subject: [PATCH 18/26] creating a separate package --- package.json | 13 +++++++------ src/stackstorm.js | 4 ---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index ca757f0..ced449c 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { - "name": "hubot-stackstorm", - "description": "A hubot plugin for integrating with StackStorm event-driven infrastructure automation platform.", - "version": "0.4.5", - "author": "StackStorm, Inc. ", + "name": "hubot-stackstorm-auth", + "description": "A hubot plugin for integrating with StackStorm event-driven infrastructure automation platform with role based auth.", + "version": "0.1.0", + "author": "Silver Blueprints, LLC. ", "license": "Apache-2.0", "keywords": [ "hubot", "hubot-scripts", + "auth", "stackstorm", "st2", "automation", @@ -15,10 +16,10 @@ ], "repository": { "type": "git", - "url": "git://github.com/StackStorm/hubot-stackstorm.git" + "url": "git@github.com:silverbp/hubot-stackstorm-auth.git" }, "bugs": { - "url": "https://github.com/StackStorm/hubot-stackstorm/issues" + "url": "https://github.com/silverbp/hubot-stackstorm-auth/issues" }, "dependencies": { "lodash": "~3.8.0", diff --git a/src/stackstorm.js b/src/stackstorm.js index ff1227a..9dba1f8 100644 --- a/src/stackstorm.js +++ b/src/stackstorm.js @@ -100,8 +100,4 @@ module.exports = function (robot) { .then(function () { return stackstormApi.startListener(); }); - // .then(function (source) { - // source.removeAllListeners(); - // source.close(); - // }); }; From 4947ee25e441ec1047b1d28617041092c5fd8a09 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Mon, 20 Mar 2017 17:24:40 -0600 Subject: [PATCH 19/26] disabling the mongobrain, will use redis instead --- package.json | 2 +- src/stackstorm.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ced449c..7c56743 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hubot-stackstorm-auth", "description": "A hubot plugin for integrating with StackStorm event-driven infrastructure automation platform with role based auth.", - "version": "0.1.0", + "version": "0.1.1", "author": "Silver Blueprints, LLC. ", "license": "Apache-2.0", "keywords": [ diff --git a/src/stackstorm.js b/src/stackstorm.js index 9dba1f8..5773d13 100644 --- a/src/stackstorm.js +++ b/src/stackstorm.js @@ -39,12 +39,12 @@ var messaging_handler = require('./lib/messaging_handler'); var CommandFactory = require('./lib/command_factory'); var StackStormApi = require('./lib/stackstorm_api'); var uuid = require('node-uuid'); -var mongodb_brain = require('./lib/mongodb_brain'); +// var mongodb_brain = require('./lib/mongodb_brain'); module.exports = function (robot) { var self = this; - mongodb_brain(robot); + // mongodb_brain(robot); var stackstormApi = new StackStormApi(robot.logger); var commandFactory = new CommandFactory(robot); From faacf1302d163789834d51ad262adba383c0789a Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Mon, 20 Mar 2017 17:40:18 -0600 Subject: [PATCH 20/26] removing mongdb brain since no longer using it --- src/lib/mongodb_brain.js | 58 ---------------------------------------- src/stackstorm.js | 3 --- 2 files changed, 61 deletions(-) delete mode 100644 src/lib/mongodb_brain.js diff --git a/src/lib/mongodb_brain.js b/src/lib/mongodb_brain.js deleted file mode 100644 index 58ba5b2..0000000 --- a/src/lib/mongodb_brain.js +++ /dev/null @@ -1,58 +0,0 @@ - -var mongodb = require('mongodb'); -var url = require('mongodb-url').get(); - -module.exports = function(robot) { - robot.logger.info('mongodb: Connecting to `%s`...', url); - var db; - - mongodb.connect(url, function(err, _db) { - if (err) { - robot.logger.error('mongodb: Connection failed: `%s`', err.message); - robot.logger.error(err.stack); - return; - } - db = _db; - - db.collection('hubot').findOne({ - _id: 'brain' - }, function(_err, doc) { - if (_err) { - robot.logger.error('mongodb: Lookup failed'); - robot.logger.error(_err.stack); - return; - } - if (doc) { - robot.logger.info('mongodb: loaded brain from previous document'); - robot.brain.mergeData(doc); - } else { - robot.logger.info('mongodb: Initializing...'); - robot.brain.mergeData({}); - } - robot.brain.resetSaveInterval(10); - robot.brain.setAutoSave(true); - robot.logger.info('mongodb: Ready.'); - }); - }); - - robot.brain.on('save', function(data) { - data = data || {}; - data._id = 'brain'; - robot.logger.info('mongodb: saving...'); - db.collection('hubot').save(data, function(err) { - if (err) { - robot.logger.error('mongodb: Save failed: `%s`', err.message); - return; - } - robot.logger.info('mongodb: Saved!'); - }); - }); - - robot.brain.on('close', function() { - if (db) { - robot.logger.info('mongodb: Closing client. Goodbye.'); - db.close(); - } - }); - robot.brain.setAutoSave(false); -}; \ No newline at end of file diff --git a/src/stackstorm.js b/src/stackstorm.js index 5773d13..f5be6da 100644 --- a/src/stackstorm.js +++ b/src/stackstorm.js @@ -39,13 +39,10 @@ var messaging_handler = require('./lib/messaging_handler'); var CommandFactory = require('./lib/command_factory'); var StackStormApi = require('./lib/stackstorm_api'); var uuid = require('node-uuid'); -// var mongodb_brain = require('./lib/mongodb_brain'); module.exports = function (robot) { var self = this; - // mongodb_brain(robot); - var stackstormApi = new StackStormApi(robot.logger); var commandFactory = new CommandFactory(robot); var messagingHandler = messaging_handler.getMessagingHandler(robot.adapterName, robot); From b3121c8f44a7de7cb7c857d083fa735cfc021ce1 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Mon, 27 Mar 2017 23:13:12 -0600 Subject: [PATCH 21/26] updating with env auth --- src/lib/command_factory.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/command_factory.js b/src/lib/command_factory.js index cf5297d..781bc5c 100644 --- a/src/lib/command_factory.js +++ b/src/lib/command_factory.js @@ -107,6 +107,9 @@ CommandFactory.prototype.addCommand = function (action_alias, messaging_handler) listener_opts.auth = 'true'; listener_opts.roles = action_alias.extra.hubot_auth.roles; listener_opts.rooms = action_alias.extra.hubot_auth.rooms; + if (action_alias.extra.hubot_auth.env) { + listener_opts.env = action_alias.extra.hubot_auth.env + } } self.robot.listen(function (msg) { From 64f80bfc771b060d19b18c4317b0e2fd406e973d Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Mon, 27 Mar 2017 23:13:42 -0600 Subject: [PATCH 22/26] updating version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7c56743..40f88b3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hubot-stackstorm-auth", "description": "A hubot plugin for integrating with StackStorm event-driven infrastructure automation platform with role based auth.", - "version": "0.1.1", + "version": "0.1.2", "author": "Silver Blueprints, LLC. ", "license": "Apache-2.0", "keywords": [ From 67466495f18dbc067205b8ff67d3cef44efcced7 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Sun, 2 Apr 2017 16:42:34 -0600 Subject: [PATCH 23/26] fixing to support the new hubot-slack plugin --- package.json | 6 ++---- src/lib/messaging_handler/slack.js | 16 ++-------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 40f88b3..edc3687 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hubot-stackstorm-auth", "description": "A hubot plugin for integrating with StackStorm event-driven infrastructure automation platform with role based auth.", - "version": "0.1.2", + "version": "1.0.1", "author": "Silver Blueprints, LLC. ", "license": "Apache-2.0", "keywords": [ @@ -27,9 +27,7 @@ "rsvp": "3.0.14", "st2client": "^0.4.3", "truncate": "^1.0.4", - "node-uuid": "~1.4.0", - "mongodb": "^2.0.52", - "mongodb-url": "^1.0.3" + "node-uuid": "~1.4.0" }, "peerDependencies": { "hubot": "2.x" diff --git a/src/lib/messaging_handler/slack.js b/src/lib/messaging_handler/slack.js index 4d8bd5e..7705e8f 100644 --- a/src/lib/messaging_handler/slack.js +++ b/src/lib/messaging_handler/slack.js @@ -9,19 +9,6 @@ var DefaultMessagingHandler = require('./default'); function SlackMessagingHandler(robot) { var self = this; DefaultMessagingHandler.call(this, robot); - - var sendMessageRaw = function(message) { - /*jshint validthis:true */ - message['channel'] = this.id; - message['parse'] = 'none'; - this._client._send(message); - }; - - if (robot.adapter && robot.adapter.constructor && robot.adapter.constructor.name === 'SlackBot') { - for (var channel in robot.adapter.client.channels) { - robot.adapter.client.channels[channel].sendMessage = sendMessageRaw.bind(robot.adapter.client.channels[channel]); - } - } } util.inherits(SlackMessagingHandler, DefaultMessagingHandler); @@ -80,9 +67,10 @@ SlackMessagingHandler.prototype.postData = function(data) { }; SlackMessagingHandler.prototype.normalizeAddressee = function(msg) { + var room_name = robot.adapter.client.rtm.dataStore.getChannelById(msg.message.room).name return { name: msg.message.user.name, - room: msg.message.room + room: room_name }; }; From 4a2ba195d3fb73d039c3a331aa603b938b3add71 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Sun, 2 Apr 2017 16:48:41 -0600 Subject: [PATCH 24/26] Revert "fixing to support the new hubot-slack plugin" This reverts commit 67466495f18dbc067205b8ff67d3cef44efcced7. --- package.json | 6 ++++-- src/lib/messaging_handler/slack.js | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index edc3687..40f88b3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hubot-stackstorm-auth", "description": "A hubot plugin for integrating with StackStorm event-driven infrastructure automation platform with role based auth.", - "version": "1.0.1", + "version": "0.1.2", "author": "Silver Blueprints, LLC. ", "license": "Apache-2.0", "keywords": [ @@ -27,7 +27,9 @@ "rsvp": "3.0.14", "st2client": "^0.4.3", "truncate": "^1.0.4", - "node-uuid": "~1.4.0" + "node-uuid": "~1.4.0", + "mongodb": "^2.0.52", + "mongodb-url": "^1.0.3" }, "peerDependencies": { "hubot": "2.x" diff --git a/src/lib/messaging_handler/slack.js b/src/lib/messaging_handler/slack.js index 7705e8f..4d8bd5e 100644 --- a/src/lib/messaging_handler/slack.js +++ b/src/lib/messaging_handler/slack.js @@ -9,6 +9,19 @@ var DefaultMessagingHandler = require('./default'); function SlackMessagingHandler(robot) { var self = this; DefaultMessagingHandler.call(this, robot); + + var sendMessageRaw = function(message) { + /*jshint validthis:true */ + message['channel'] = this.id; + message['parse'] = 'none'; + this._client._send(message); + }; + + if (robot.adapter && robot.adapter.constructor && robot.adapter.constructor.name === 'SlackBot') { + for (var channel in robot.adapter.client.channels) { + robot.adapter.client.channels[channel].sendMessage = sendMessageRaw.bind(robot.adapter.client.channels[channel]); + } + } } util.inherits(SlackMessagingHandler, DefaultMessagingHandler); @@ -67,10 +80,9 @@ SlackMessagingHandler.prototype.postData = function(data) { }; SlackMessagingHandler.prototype.normalizeAddressee = function(msg) { - var room_name = robot.adapter.client.rtm.dataStore.getChannelById(msg.message.room).name return { name: msg.message.user.name, - room: room_name + room: msg.message.room }; }; From 74724819253b9d6df4c411f56c02a16d874035bb Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Sun, 2 Apr 2017 16:49:20 -0600 Subject: [PATCH 25/26] removing mongo --- package.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 40f88b3..b652f1a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hubot-stackstorm-auth", "description": "A hubot plugin for integrating with StackStorm event-driven infrastructure automation platform with role based auth.", - "version": "0.1.2", + "version": "1.0.2", "author": "Silver Blueprints, LLC. ", "license": "Apache-2.0", "keywords": [ @@ -27,9 +27,7 @@ "rsvp": "3.0.14", "st2client": "^0.4.3", "truncate": "^1.0.4", - "node-uuid": "~1.4.0", - "mongodb": "^2.0.52", - "mongodb-url": "^1.0.3" + "node-uuid": "~1.4.0" }, "peerDependencies": { "hubot": "2.x" From 96d6d3b4dedf056791704f561378c40a185a0ab0 Mon Sep 17 00:00:00 2001 From: Casey Entzi Date: Mon, 3 Apr 2017 10:35:35 -0600 Subject: [PATCH 26/26] fixing representation but where format representations weren't working, also testing whether you can hide a command by not specifying display and just specifying representation --- package.json | 2 +- src/lib/command_factory.js | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index b652f1a..5758eeb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hubot-stackstorm-auth", "description": "A hubot plugin for integrating with StackStorm event-driven infrastructure automation platform with role based auth.", - "version": "1.0.2", + "version": "1.1.5", "author": "Silver Blueprints, LLC. ", "license": "Apache-2.0", "keywords": [ diff --git a/src/lib/command_factory.js b/src/lib/command_factory.js index 781bc5c..cfd97b0 100644 --- a/src/lib/command_factory.js +++ b/src/lib/command_factory.js @@ -84,15 +84,16 @@ CommandFactory.prototype.addCommand = function (action_alias, messaging_handler) var action_alias_name = action_alias.name; _.each(action_alias.formats, function (format) { - var formatted_string = format.display || format; - self.robot.commands.push(formatHelpCommand(action_alias.name, formatted_string, action_alias.description)); - - if (format.display) { + if (typeof format === 'string') { + self.robot.commands.push(formatHelpCommand(action_alias.name, format, action_alias.description)); + commands_regex_map[format] = getRegexForFormatString(format); + } else { + if (format.display) { + self.robot.commands.push(formatHelpCommand(action_alias.name, format.display, action_alias.description)); + } _.each(format.representation, function (representation) { - commands_regex_map[formatted_string] = getRegexForFormatString(representation); + commands_regex_map[representation] = getRegexForFormatString(representation); }); - } else { - commands_regex_map[formatted_string] = getRegexForFormatString(format); } });