UNPKG

smartslack

Version:

SmartSlack is a node.js module for Slack's Real Time Messaging API

457 lines (379 loc) 12.6 kB
'use strict'; /** * SmartSlack is a node.js module for Slack's Real Time Messaging API * @copyright Phillip J. Henslee II 2015 <ph2@ph2.us> * @module smartslack */ var EventEmitter = require('events').EventEmitter; var bole = require('bole'); var dns = require('dns'); var https = require('https'); var _ = require('lodash'); var util = require('util'); var WebSocket = require('ws'); var Attachment = require('./slack/attachment'); var Cache = require('./cache'); var errors = require('./errors'); var logging = require('./logging'); var slack = require('./slack/'); var common = require('./common'); /** * Slack RTM API Client * @constructor * @param {object} options required configuration options */ function SmartSlack(options) { // Must provide a valid options argument if (!_.isObject(options)) { throw new Error(errors.missing_options_arg); } var accessToken = options.token; // Need a valid Slack token to authenticate // Updated on July 29, 2019 to accommodate new token format. // Reference: https://api.slack.com/docs/oauth if (typeof accessToken !== 'string' || !accessToken.match(/^([a-z]{4})-([0-9]{11,12})-([0-9a-zA-Z]{24})$/) && !accessToken.match(/^([a-z]{4})-([0-9]{10,11})-([0-9]{11,12})-([0-9a-zA-Z]{24})$/)) { throw new Error(errors.invalid_token); } // Save the cache data Cache.defaults(options); Cache.add({hostname: 'slack.com'}); // API references this.channels = slack.channels; this.chat = slack.chat; this.emoji = slack.emoji; this.groups = slack.groups; this.im = slack.im; this.reactions = slack.reactions; this.test = slack.test; this.users = slack.users; // Logger this.log = bole('SmartSlack'); this._autoReconnect = options.autoReconnect || true; this._lastPong = null; this._messageId = 0; this._socketUrl = null; this._webSocket = null; } util.inherits(SmartSlack, EventEmitter); /** * Create a new attachment object * @param {string} text The text of the attachment * @returns {object} Attachment The attachment instance */ SmartSlack.prototype.createAttachment = function (text) { return new Attachment(text); }; /** * Start the RTM session */ SmartSlack.prototype.start = function () { var _this = this; slack.rtm.start(function (err, result) { if (err) { throw err; } _this.log.info('Slack RTM session started...'); _this.log.debug(result); _this._startTime = _.now(); _this.name = result.self.name; _this.id = result.self.id; // Cache the Slack session data Cache.data.user = result.self; Cache.data.users = result.users; Cache.data.channels = result.channels; Cache.data.groups = result.groups; Cache.data.ims = result.ims; Cache.data.team = result.team; // Valid for thirty seconds... _this._socketUrl = result.url; // Connect web socket _this._connectSocket(); }); }; /** * Post a direct message via the API * @description Convenience function so you can call * client.postDirectMessage instead of client.chat.postDirectMessage; * @param {string} user The user id, name, or email address * @param {string} text The message text * @param {object} options Additional argument options for the message * @param {function} callback(err,result) */ SmartSlack.prototype.postDirectMessage = function (user, text, options, callback) { if (typeof options === 'function') { callback = options; options = null; } callback = (_.isFunction(callback)) ? callback : _.noop; if (!_.isString(user) && !_.isString(text)) { return callback(new Error(errors.missing_required_arg)); } return slack.chat.postDirectMessage(user, text, options, callback); }; /** * Post a message via the API * @description Convenience function so you can call * client.postMessage instead of client.chat.postMessage; * @param {string} channel The channel name or id * @param {string} text The message text * @param {object} options Additional argument options for the message * @param {function} callback(err,result) */ SmartSlack.prototype.postMessage = function (channel, text, options, callback) { if (typeof options === 'function') { callback = options; options = null; } callback = (_.isFunction(callback)) ? callback : _.noop; if (!_.isString(channel) && !_.isString(text)) { return callback(new Error(errors.missing_required_arg)); } return slack.chat.postMessage(channel, text, options, callback); }; /** * Send RTM message to channel * @param {string} channel The channel name (i.e. general) * @param {string} text The message text * @param {function} callback(err,result) */ SmartSlack.prototype.sendToChannel = function (channel, text, callback) { callback = (_.isFunction(callback)) ? callback : _.noop; if (!_.isString(channel) && !_.isString(text)) { return callback(new Error(errors.missing_required_arg)); } if (channel.match(/^([C]0)/)) { return this.send(channel, text); } return this._sendToType(slack.types.CHANNEL, channel, text, callback); }; /** * Send RTM message to group * @param {string} group The private group name * @param {string} text The message text * @param {function} callback(err,result) */ SmartSlack.prototype.sendToGroup = function (group, text, callback) { callback = (_.isFunction(callback)) ? callback : _.noop; if (!_.isString(group) && !_.isString(text)) { return callback(new Error(errors.missing_required_arg)); } if (group.match(/^([G]0)/)) { return this.send(group, text); } return this._sendToType(slack.types.GROUP, group, text, callback); }; /** * Send RTM message to user * @param {string} user The user id, name, email address * @param {string} text The message text * @param {function} callback(err,result) */ SmartSlack.prototype.sendToUser = function (user, text, callback) { callback = (_.isFunction(callback)) ? callback : _.noop; if (!_.isString(user) && !_.isString(text)) { return callback(new Error(errors.missing_required_arg)); } if (user.match(/^([D]0)/)) { return this.send(user, text); } return this._sendToType(slack.types.USER, user, text, callback); }; /** * Used for reconnection, attempts to resolve * slack.com calls login() if resolved */ SmartSlack.prototype._canResolve = function () { var _this = this; this.log.debug('Attempting to resolve ' + Cache.data.hostname); dns.resolve4(Cache.data.hostname, function (err, addresses) { if (err) { _this.log.error('DNS resolution failed. Error Code: ' + err.code); } else { _this.log.debug('Resolved ' + Cache.data.hostname + ' (' + addresses[0] + ')'); if (_this._reconnecting) { clearInterval(_this._reconnecting); _this.start(); } } }); }; /** * Connect webSocket * @access private */ SmartSlack.prototype._connectSocket = function () { var _this = this; var output; this._webSocket = new WebSocket(this._socketUrl); this._webSocket.on('open', function (result) { _this.emit('open', result); _this._lastPong = _.now(); }); this._webSocket.on('close', function (result) { _this.emit('close', result); if (_this._autoReconnect) { _this._reconnect(); } }); this._webSocket.on('error', function (result) { _this.emit('error', result); }); this._webSocket.on('message', function (result) { try { output = JSON.parse(result); _this._onRtmEvent(output); } catch (error) { _this.emit('error', error); } }); }; /** * Handle slack RTM event message * @param {object} message The Slack RTM JSON event message * @access private */ SmartSlack.prototype._onRtmEvent = function (message) { var _this = this; this.emit('eventmessage', message); this.log.debug(message); switch (message.type) { case 'hello': // Received the hello event message this.log.info('Slack hello received, socket connected...'); this.emit('connected'); this._lastPong = _.now(); // Start pings every five seconds this._pingInterval = setInterval(function () { _this._ping(); }, 5000); break; case 'message': this.emit('message', message); break; case 'pong': this._lastPong = _.now(); this.log.debug('Last pong latency: ' + (this._lastPong - message.time) + 'ms'); break; case 'presence_change': _.find(Cache.data.users, {id: message.user}).presence = message.presence; this.log.info('User presence changed, updating user presence...'); break; case 'team_join event': this.log.info('New member joined the team, updating user list...'); this.users.getList(function (err, result) { if (err) { _this.log.error(err); } else { Cache.data.users = result.members; } }); break; case 'user_change': this.log.info('User information has changed, updating user list...'); this.users.getList(function (err, result) { if (err) { _this.log.error(err); } else { Cache.data.users = result.members; } }); break; } }; /** * Sends ping message over the RTM * @access private */ SmartSlack.prototype._ping = function () { var _this = this; var result; var ping = {type: "ping"}; ping.id = _this._messageId; ping.time = _.now(); result = JSON.stringify(ping); _this._webSocket.send(result); _this._messageId += 1; }; /** * Reconnects to Slack RTM, called from canResolve() * @access private */ SmartSlack.prototype._reconnect = function () { var _this = this; // Connection lost stop pinging and reset clearInterval(_this._pingInterval); _this._lastPong = null; _this._startTime = null; // Attempt to resolve slack.com and call login when available _this._reconnecting = setInterval(function () { _this._canResolve(); }, 5000); _this.log.info('Connection lost, waiting to reconnect...'); }; /** * Sends a message via the RTM socket * @param {string} channel The channel id * @param {string} text The message text * @access private */ SmartSlack.prototype.send = function (channel, text) { var _this = this; var message; var data; text = common.escape(text); message = { id: this._messageId, type: 'message', channel: channel, text: text }; try { data = JSON.stringify(message); } catch (error) { this.log.error(error); } _this._webSocket.send(data); _this._messageId += 1; }; /** * Send a RTM socket message to channel, group, or user * @param slackType {string} enum slackTypes.CHANNEL * @param typeName {string} the entity's name * @param text {string} the message text * @param {function} callback(err,result) */ SmartSlack.prototype._sendToType = function (slackType, typeName, text, callback) { var entityId; callback = (_.isFunction(callback)) ? callback : _.noop; switch (slackType) { case slack.types.CHANNEL: this.channels.getChannel(typeName, function (err, result) { if (!err) { entityId = result.id; } }); break; case slack.types.GROUP: this.groups.getGroup(typeName, function (err, result) { if (!err) { entityId = result.id; } }); break; case slack.types.USER: this.users.getImChannel(typeName, function (err, result) { if (!err) { entityId = result; } }); break; default: entityId = null; } if (entityId) { return this.send(entityId, text); } callback(new TypeError(errors.invalid_entity_type)); }; module.exports = SmartSlack;