slack-robot
Version:
Simple robot for your slack integration
664 lines (535 loc) • 17.9 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _lodash = require('lodash');
var _async = require('async');
var _async2 = _interopRequireDefault(_async);
var _fs = require('fs');
var _fs2 = _interopRequireDefault(_fs);
var _request = require('request');
var _request2 = _interopRequireDefault(_request);
var _bluebird = require('bluebird');
var _bluebird2 = _interopRequireDefault(_bluebird);
var _eventemitter = require('eventemitter3');
var _eventemitter2 = _interopRequireDefault(_eventemitter);
var _slackClient = require('slack-client');
var _Events = require('./Events');
var _util = require('./util');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var USER_PREFIX = 'user__';
var MPIM_PREFIX = 'mpim__';
var DEFAUT_POST_MESSAGE_OPTS = {
as_user: true,
parse: 'full'
};
var TASK_TYPES = {
TEXT: 'text',
ATTACHMENT: 'attachments',
UPLOAD: 'file',
REACTION: 'reaction'
};
var Response = function (_EventEmitter) {
_inherits(Response, _EventEmitter);
/**
* @constructor
* @param {WebClient} api
* @param {SlackDataStore} dataStore
* @param {Request} request
* @param {number} concurrency (defaults to 1 to allow serial response sending)
*/
function Response(slackToken, dataStore, request) {
var concurrency = arguments.length <= 3 || arguments[3] === undefined ? 1 : arguments[3];
_classCallCheck(this, Response);
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Response).call(this));
_this._dataStore = dataStore;
_this._defaultTarget = [request.to.id];
_this._messageTimestamp = request.message.timestamp;
concurrency = parseInt(concurrency, 10);
/**
* We use new instance of WebClient instead of passing from robot
* to allow different concurrency option
*
* @type {WebClient}
*/
_this._api = new _slackClient.WebClient(slackToken, { maxRequestConcurrency: concurrency });
/**
* This is where we queue our response before actually sending them
* by default every new item added the queue will not be processed
* automatically (the queue will be paused) until the user explicitly
* call .send()
*
* @type {AsyncQueue}
*/
_this._queue = _async2.default.queue((0, _lodash.bind)(_this._send, _this), concurrency);
return _this;
}
/**
* Change default target, only used internally in robot.to() method.
* Because robot.to is supposed to used by human, it's possible
* that given array of target contain user name or channel name
* and not an id, so we need to convert them first
*
* @internal
* @param {Array.<string>} defaultTarget
*/
_createClass(Response, [{
key: 'setDefaultTarget',
value: function setDefaultTarget(defaultTarget) {
this._defaultTarget = this._mapTargetToId(defaultTarget);
}
/**
* Send basic text message
*
* @public
* @param {string} text
* @param {=Array.<string>|string} optTargets
* @return {Response}
*/
}, {
key: 'text',
value: function text(_text) {
for (var _len = arguments.length, optTargets = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
optTargets[_key - 1] = arguments[_key];
}
var targets = this._mapTargetToId(optTargets);
var base = {
type: TASK_TYPES.TEXT,
value: _text
};
// do not send until told otherwise
this._queue.pause();
this._addToQueues(base, targets);
return this;
}
/**
* Send message with attachment
*
* @public
* @param {string} text
* @param {Array.<Object>|Object} attachment
* @param {=Array.<string>|string} optTargets
* @see https://api.slack.com/docs/attachments
*
* Also support sending attachment without text with two params
* @param {Array.<Object>|Object} attachment
* @param {=Array.<string>|string} optTargets
*/
}, {
key: 'attachment',
value: function attachment() {
for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
var text = void 0,
attachments = void 0,
optTargets = void 0;
if (typeof args[0] === 'string') {
text = args[0];
attachments = args[1];
if (args.length > 2) {
optTargets = args.splice(2);
}
} else {
attachments = args[0];
if (arguments.length > 1) {
optTargets = args.splice(1);
}
}
var targets = this._mapTargetToId(optTargets);
var base = {
type: TASK_TYPES.ATTACHMENT,
value: {
text: text,
attachments: [attachments]
}
};
if (attachments.length) {
base.value.attachments = attachments;
}
// do not send until told otherwise
this._queue.pause();
this._addToQueues(base, targets);
return this;
}
/**
* Send a file from a string or stream
*
* @public
* @param {string} filename
* @param {string|ReadStream} content
* @param {=Array.<string>|string} optTargets
* @see https://nodejs.org/api/fs.html
*/
}, {
key: 'upload',
value: function upload(filename, content) {
for (var _len3 = arguments.length, optTargets = Array(_len3 > 2 ? _len3 - 2 : 0), _key3 = 2; _key3 < _len3; _key3++) {
optTargets[_key3 - 2] = arguments[_key3];
}
var targets = this._mapTargetToId(optTargets);
var base = {
type: TASK_TYPES.UPLOAD,
value: {
filename: filename,
content: content
}
};
// do not send until told otherwise
this._queue.pause();
this._addToQueues(base, targets);
return this;
}
/**
* Add reaction to sent message
*
* @public
* @param {string} emoji
*/
}, {
key: 'reaction',
value: function reaction(emoji) {
var task = {
type: TASK_TYPES.REACTION,
// also include target prop to prevent error when checking targetId
target: this._defaultTarget,
value: {
emoji: (0, _util.stripEmoji)(emoji),
channel: this._defaultTarget[0],
timestamp: this._messageTimestamp
}
};
this._queue.pause();
this._addToQueue(task);
return this;
}
/**
* Wrap asynchronous task
* @param {function} asyncTaskFn
*/
}, {
key: 'async',
value: function async(asyncTaskFn) {
var _this2 = this;
var superPromise = new _bluebird2.default(function (resolve, reject) {
asyncTaskFn(function (err) {
if (err) {
return reject(err);
}
return resolve();
});
});
// add shortcut to send all pending queues
superPromise.send = function () {
return superPromise.then(function () {
return _this2.send();
});
};
return superPromise;
}
/**
* Start queue processing
*
*/
}, {
key: 'send',
value: function send() {
var _this3 = this;
this._queue.resume();
return new _bluebird2.default(function (resolve) {
_this3._queue.drain = function () {
return resolve();
};
});
}
/**
* Add response to queue for all target
*
* @param {Object} base response object
* @param {Array.<string>} targets list of channel
*/
}, {
key: '_addToQueues',
value: function _addToQueues(base, targets) {
var _this4 = this;
targets.forEach(function (target) {
var task = _extends({ target: target }, base);
_this4._addToQueue(task);
});
}
/**
* Add task to queue, emit error events if task
* failed to finish
*
* @private
* @param {Object} task
*/
}, {
key: '_addToQueue',
value: function _addToQueue(task) {
var _this5 = this;
this._queue.push(task, function (err, data) {
if (err) {
return _this5.emit(_Events.RESPONSE_EVENTS.TASK_ERROR, err);
}
_this5.emit(_Events.RESPONSE_EVENTS.TASK_FINISHED, task, data);
});
}
/**
* Send response to correct target
*
* @private
* @param {Object} task
* @param {function} callback
*/
}, {
key: '_send',
value: function _send(task, callback) {
var _this6 = this;
if (task.target.indexOf(USER_PREFIX) > -1) {
var userId = task.target.replace(USER_PREFIX, '');
return this._api.dm.open(userId, function (err, data) {
if (err) {
return callback(err);
}
if (!data.ok) {
return callback(new Error(data.error));
}
task.target = data.channel.id;
_this6._sendResponse(task, callback);
});
} else if (task.target.indexOf(MPIM_PREFIX) > -1) {
var userIds = task.target.replace(MPIM_PREFIX, '');
return this._api.mpim.open(userIds, function (err, data) {
if (err) {
return callback(err);
}
if (!data.ok) {
return callback(new Error(data.error));
}
task.target = data.group.id;
_this6._sendResponse(task, callback);
});
}
this._sendResponse(task, callback);
}
/**
* Send response based on response type
*
* @private
* @param {Object} task
* @param {function} callback
*/
}, {
key: '_sendResponse',
value: function _sendResponse(task, callback) {
switch (task.type) {
case TASK_TYPES.TEXT:
this._sendTextResponse(task.target, task.value, callback);
break;
case TASK_TYPES.ATTACHMENT:
this._sendAttachmentResponse(task.target, task.value, callback);
break;
case TASK_TYPES.UPLOAD:
this._sendFileResponse(task.target, task.value, callback);
break;
case TASK_TYPES.REACTION:
this._sendReactionResponse(task.value, callback);
break;
default:
callback(null, { message: 'Unknown task type ' + task.type });
}
}
/**
* @private
* @param {string} id channel id
* @param {string} text
* @param {function} callback
*/
}, {
key: '_sendTextResponse',
value: function _sendTextResponse(id, text, callback) {
this._api.chat.postMessage(id, text, DEFAUT_POST_MESSAGE_OPTS, function (err, res) {
if (err) {
return callback(err);
}
callback(null, res);
});
}
/**
* @private
* @param {string} id channel id
* @param {object} attachment
* @param {function} callback
*/
}, {
key: '_sendAttachmentResponse',
value: function _sendAttachmentResponse(id, attachment, callback) {
var text = attachment.text;
var attachments = attachment.attachments;
var opts = _extends({}, DEFAUT_POST_MESSAGE_OPTS, {
attachments: JSON.stringify(attachments)
});
this._api.chat.postMessage(id, text, opts, function (err, res) {
if (err) {
return callback(err);
}
callback(null, res);
});
}
/**
* @private
* @param {object} reaction
* @param {function} callback
*/
}, {
key: '_sendReactionResponse',
value: function _sendReactionResponse(reaction, callback) {
var opts = {
channel: reaction.channel,
timestamp: reaction.timestamp
};
this._api.reactions.add(reaction.emoji, opts, function (err, res) {
if (err) {
return callback(err);
}
callback(null, res);
});
}
/**
* Instead of using WebClient, use "request" with multipart support
* for uploading binary
* TODO use WebClient when this is fixed
*
* @private
* @param {string} id channel id
* @param {object} file
* @param {function} callback
*/
}, {
key: '_sendFileResponse',
value: function _sendFileResponse(id, file, callback) {
var url = 'https://slack.com/api/files.upload';
var r = _request2.default.post(url, function (err, res, body) {
if (err) {
return callback(err);
}
var data = JSON.parse(body);
if (!data.ok) {
return callback(new Error(data.error));
}
callback(null, data);
});
var form = r.form();
form.append('token', this._api._token);
form.append('channels', id);
form.append('filename', file.filename);
form.append('filetype', (0, _util.getFileExtension)(file.filename));
/**
* Slack API expect one of two fields, file or content.
* file is used when sending multipart/form-data, content
* is used when sending urlencodedform
* @see https://api.slack.com/methods/files.upload
*/
if (file.content instanceof _fs2.default.ReadStream) {
form.append('file', file.content);
} else {
form.append('content', file.content);
}
}
/**
* Convert given array of target into array of id.
* If no target is specified, use defaultTarget
*
* @private
* @param {=Array.<string>} targets
* @return {Array.<string>}
*/
}, {
key: '_mapTargetToId',
value: function _mapTargetToId(optTargets) {
var _this7 = this;
var targets = optTargets && optTargets.length > 0 ? optTargets : this._defaultTarget;
var idFormat = ['C', 'G', 'D'];
return targets.map(function (target) {
if (Array.isArray(target)) {
return _this7._getMpimTarget(target);
}
// skip mapping if already a pending id
if (target.indexOf(USER_PREFIX) === 0 || target.indexOf(MPIM_PREFIX) === 0) {
return target;
}
// skip mapping if already an id
if (idFormat.indexOf(target.substring(0, 1)) > -1) {
return target;
}
var channel = _this7._dataStore.getChannelOrGroupByName(target);
if (!channel) {
// not a channel or group, use user id
// prefix with u__ to mark that we need to "open im" first
// before we can send message
var user = _this7._dataStore.getUserByName(target.replace('@', ''));
if (!user) {
return null;
}
return USER_PREFIX + user.id;
}
return channel.id;
}).filter(function (target) {
return target !== null;
});
}
/**
* MPIM target is marked by specifying array of target,
* we need to get list of user id (if not already),
* and exclude invalid target (channel, group, etc)
*
* @param {Array.<string>} users
* @return {string}
*/
}, {
key: '_getMpimTarget',
value: function _getMpimTarget(users) {
var _this8 = this;
var userIds = users.map(function (t) {
var mark = t.substring(0, 1);
switch (mark) {
// direct message, we need to get the user id
case 'D':
{
var dm = _this8._dataStore.getDMById(t);
if (!dm) {
return null;
}
return dm.user;
}
case 'U':
return t;
// invalid input
case 'C':
case 'G':
return null;
// treat other target as user name
default:
{
var user = _this8._dataStore.getUserByName(t.replace('@', ''));
if (!user) {
return null;
}
return user.id;
}
}
}).filter(function (user) {
return user !== null;
});
return MPIM_PREFIX + userIds.join(',');
}
}]);
return Response;
}(_eventemitter2.default);
exports.default = Response;