const WebSocket = require('ws'), https = require('https'), sleep = require("sleep").sleep ,SlackData = require("./slackData.js").SlackData ,SlackHistory = require("./slackHistory.js").SlackHistory ,config = require("../config.js") ; const SLACK_ENDPOINT = "https://slack.com/api/" ,SLACK_HOSTNAME = "slack.com" ,SLACK_ENDPOINT_PATH = "/api/" ,GETAPI = { rtmStart: "rtm.start" ,oauth: "oauth.access" ,channelHistory: "channels.history" ,directHistory: "im.history" ,groupHistory: "groups.history" ,postMsg: "chat.postMessage" ,postMeMsg: "chat.meMessage" ,editMsg: "chat.update" ,removeMsg: "chat.delete" ,postFile: "files.upload" ,setActive: "users.setActive" ,emojiList: "emoji.list" ,slashList: "commands.list" ,slashExec: "chat.command" ,addReaction: "reactions.add" ,removeReaction: "reactions.remove" ,sendAction: "chat.attachmentAction" ,read: { group: "groups.mark" ,im: "im.mark" ,group: "mpim.mark" ,channel: "channels.mark" } } ,HISTORY_LENGTH = 35 ,UPDATE_LIVE = [ "message" ,"pin_added" ,"pin_removed" ,"reaction_added" ,"reaction_removed" ,"star_added" ,"star_removed" ] // Message type that affect live history ; function Slack(sess, manager) { this.token = sess.slackToken; this.sessId = sess.id; this.manager = manager; this.rtm = null; this.data = new SlackData(this); this.history = {}; this.pendingRtm = {}; this.connected = false; this.closing = false; } Slack.prototype.onRequest = function(knownVersion, cb) { this.lastCb = cb; if (this.connected === false) { this.connect(knownVersion); } else { this.waitForEvent(knownVersion); } }; function httpsRequest(url, cb) { try { https.get(url, (res) => { if (res.statusCode !== 200) { cb(res.statusCode, null); return; } var body = null; res.on('data', (chunk) => { body = body ? Buffer.concat([body, chunk], body.length +chunk.length) : Buffer.from(chunk); }); res.on('end', () => { try { body = JSON.parse(body.toString("utf8")); } catch (e) {} cb && cb(res.statusCode, body); }); }); } catch (e) { console.error("Error in https request: ", e); cb && cb(0, null); } } Slack.prototype.connect = function(knownVersion) { var _this = this; this.connected = undefined; httpsRequest(SLACK_ENDPOINT +GETAPI.rtmStart +"?token=" +this.token, (status, body) => { if (status !== 200) { _this.error = body.error; _this.connected = false; console.error("Slack api responded " +status); _this.lastCb(_this); return; } if (!body) { _this.error = "Slack API error"; _this.connected = false; _this.lastCb(_this); return; } if (!body.ok) { _this.error = body.error; _this.connected = false; console.error("Slack api responded !ok with ", body); _this.lastCb(_this); return; } _this.getEmojis((emojis) => { _this.getSlashCommands((commands) => { body.emojis = emojis; body.commands = commands; _this.data.updateStatic(body, Date.now()); _this.connectRtm(body.url); _this.waitForEvent(knownVersion); }); }); }); }; Slack.prototype.sendCommand = function(room, cmd, arg) { httpsRequest( SLACK_ENDPOINT +GETAPI.slashExec +"?token=" +this.token +"&command=" +encodeURIComponent(cmd.name) +"&disp=" +encodeURIComponent(cmd.name) +"&channel=" +room.remoteId +"&text=" +arg); } Slack.prototype.sendTyping = function(room) { this.rtm.send('{"id":' +this.rtmId++ +',"type":"typing","channel":"' +room.remoteId +'"}'); } Slack.prototype.getSlashCommands = function(cb) { httpsRequest(SLACK_ENDPOINT +GETAPI.slashList +"?token=" +this.token, (status, body) => { if (!status || !body || !body.ok) cb(null); else cb(body.commands || {}); }); }; Slack.prototype.getEmojis = function(cb) { httpsRequest(SLACK_ENDPOINT +GETAPI.emojiList +"?token=" +this.token, (status, body) => { if (!status || !body || !body.ok) cb(null); else cb(body.emoji || {}); }); }; Slack.prototype.waitForEvent = function(knownVersion) { var tick = 0 ,_this = this ,interval; interval = setInterval(() => { tick++; if (!_this.lastCb) { clearInterval(interval); return; } if (_this.connected) { var updatedCtx = _this.data.getUpdates(knownVersion, Date.now()) ,updatedLive = _this.getLiveUpdates(knownVersion) ,updated; if (updatedCtx || updatedLive) { updated = {}; updated["static"] = updatedCtx; updated["live"] = updatedLive; updated["v"] = Math.max(_this.data.liveV, _this.data.staticV); } if (updated) { clearInterval(interval); _this.lastCb(_this, updated); return; } } if (tick >= 55) { // < 1 minute timeout clearInterval(interval); _this.lastCb(_this, { v: knownVersion }); return; } }, 1000); }; /** @return {Object|undefined} */ Slack.prototype.getLiveUpdates = function(knownVersion) { var result = {}; for (var roomId in this.history) { var history = this.history[roomId]; if (history.isNew) { result[roomId] = history.toStatic(0); history.isNew = false; } else { var roomData = history.toStatic(knownVersion); if (roomData.length) result[roomId] = roomData; } } for (var roomId in result) { return result; } return undefined; }; Slack.prototype.onMessage = function(msg, t) { if (msg["ok"] === true && msg["reply_to"] && this.pendingRtm[msg["reply_to"]]) { var ts = msg["ts"] ,rtmId = msg["reply_to"]; msg = this.pendingRtm[rtmId]; msg["ts"] = ts; delete this.pendingRtm[rtmId]; } this.data.onMessage(msg, t); if ((msg["channel"] || msg["channel_id"] || (msg["item"] && msg["item"]["channel"])) && msg["type"] && UPDATE_LIVE.indexOf(msg["type"]) !== -1) { var channelId = this.data.team.id +'|' +(msg["channel"] || msg["channel_id"] || msg["item"]["channel"]) ,channel = this.data.channels[channelId] ,histo = this.history[channelId]; if (histo) { var lastMsg = histo.push(msg, t); if (lastMsg) this.data.liveV = t; histo.resort(); if (channel) channel.setLastMsg(lastMsg, t); } else if (channel) { this.fetchHistory(channel); } } }; /** * @param {SlackChan|SlackGroup|SlackIms} chan * @param {number} ts **/ Slack.prototype.markRead = function(chan, ts) { var apiURI; if (chan.remoteId[0] === 'C') apiURI = SLACK_ENDPOINT+GETAPI.read.channel; else if (chan.remoteId[0] === 'G') apiURI = SLACK_ENDPOINT+GETAPI.read.group; else if (chan.remoteId[0] === 'D') apiURI = SLACK_ENDPOINT+GETAPI.read.im; httpsRequest(apiURI +"?token=" +this.token +"&channel="+chan.remoteId +"&ts="+ts); }; Slack.prototype.connectRtm = function(url, cb) { var _this = this; this.rtmId = 0; this.rtm = new WebSocket(url); this.rtm.on("message", function(msg) { if (!_this.connected && cb) { cb(); } if (!_this.connected) { _this.connected = true; } _this.onMessage(JSON.parse(msg), Date.now()); }); this.rtm.once("error", function(e) { _this.connected = false; console.error(e); _this.close(); }); this.rtm.once("end", function() { console.error("RTM hang up"); _this.onClose(); }); }; Slack.prototype.onClose = function() { this.manager.suicide(this); }; Slack.prototype.ping = function() { httpsRequest(SLACK_ENDPOINT+GETAPI.setActive +"?token=" +this.token); }; Slack.prototype.close = function() { if (!this.closing) { this.closing = true; if (this.rtm) this.rtm.close(); this.onClose(); } }; Slack.getOauthToken = function(code, redirectUri, cb) { httpsRequest(SLACK_ENDPOINT+GETAPI.oauth +"?client_id=" +config.clientId +"&client_secret=" +config.clientSecret +"&redirect_uri=" +redirectUri +"&code=" +code, (status, resp) => { if (status === 200 && resp.ok) { cb(resp); } else { cb(null); } }); }; /** * @param {SlackChan|SlackGroup|SlackIms} channel * @param {string} contentType * @param {function(string|null)} callback **/ Slack.prototype.openUploadFileStream = function(channel, contentType, callback) { var req = https.request({ hostname: SLACK_HOSTNAME ,method: 'POST' ,path: SLACK_ENDPOINT_PATH +GETAPI.postFile +"?token=" +this.token +"&channels=" +channel.remoteId ,headers: { "Content-Type": contentType } }, (res) => { var errorJson; res.on("data", (chunk) => { errorJson = errorJson ? Buffer.concat([errorJson, chunk], errorJson.length +chunk.length) : Buffer.from(chunk); }); res.once("end", () => { if (res.statusCode === 200) { callback(null); } else { try { errorJson = JSON.parse(errorJson.toString()); } catch(e) { callback("error"); return; } callback(errorJson["error"] || "error"); } }); }); return req; }; /** * @param {SlackChan|SlackGroup|SlackIms} channel * @param {string} contentType * @param {function(string|null)} callback **/ Slack.prototype.sendAction = function(contentType, payloadStream, callback) { var req = https.request({ hostname: SLACK_HOSTNAME ,method: 'POST' ,path: SLACK_ENDPOINT_PATH +GETAPI.sendAction +"?token=" +this.token ,headers: { "Content-Type": contentType } }, (res) => { var resp; res.on("data", (chunk) => { resp = resp ? Buffer.concat([resp, chunk], resp.length +chunk.length) : Buffer.from(chunk); }); res.once("end", () => { callback(resp); }); }); payloadStream.pipe(req, { end: true }); }; /** * @param {SlackChan|SlackGroup|SlackIms} channel * @param {string} contentType * @param {function(string|null)} callback **/ Slack.prototype.openUploadFileStream = function(channel, contentType, callback) { var req = https.request({ hostname: SLACK_HOSTNAME ,method: 'POST' ,path: SLACK_ENDPOINT_PATH +GETAPI.postFile +"?token=" +this.token +"&channels=" +channel.remoteId ,headers: { "Content-Type": contentType } }, (res) => { var errorJson; res.on("data", (chunk) => { errorJson = errorJson ? Buffer.concat([errorJson, chunk], errorJson.length +chunk.length) : Buffer.from(chunk); }); res.once("end", () => { if (res.statusCode === 200) { callback(null); } else { try { errorJson = JSON.parse(errorJson.toString()); } catch(e) { callback("error"); return; } callback(errorJson["error"] || "error"); } }); }); return req; }; /** * @param {SlackChan|SlackGroup|SlackIms} channel * @param {string} msgId * @param {string} reaction **/ Slack.prototype.addReaction = function(channel, msgId, reaction) { httpsRequest(SLACK_ENDPOINT +GETAPI.addReaction +"?token=" +this.token +"&name=" +reaction +"&channel="+channel.remoteId +"×tamp="+msgId); } /** * @param {SlackChan|SlackGroup|SlackIms} channel * @param {string} msgId * @param {string} reaction **/ Slack.prototype.removeReaction = function(channel, msgId, reaction) { httpsRequest(SLACK_ENDPOINT +GETAPI.removeReaction +"?token=" +this.token +"&name=" +reaction +"&channel="+channel.remoteId +"×tamp="+msgId); } /** * @param {SlackChan|SlackGroup|SlackIms} channel * @param {Array.} text **/ Slack.prototype.sendMeMsg = function(channel, text) { httpsRequest(SLACK_ENDPOINT +GETAPI.postMeMsg +"?token=" +this.token +"&channel=" +channel.remoteId +"&text=" +text.join("\n") +"&as_user=true"); }; /** * @param {SlackChan|SlackGroup|SlackIms} channel * @param {Array.} text * @param {Array.=} attachments **/ Slack.prototype.sendMsg = function(channel, text, attachments) { if (attachments) { httpsRequest(SLACK_ENDPOINT +GETAPI.postMsg +"?token=" +this.token +"&channel=" +channel.remoteId +"&text=" +text.join("\n") + (attachments ? ("&attachments=" +encodeURIComponent(JSON.stringify(attachments))) : "") +"&as_user=true"); } else { var decodedText = []; text.forEach(function(i) { decodedText.push(decodeURIComponent(i)); }); var fullDecodedText = decodedText.join("\n"); this.pendingRtm[this.rtmId] = { type: 'message', channel: channel.remoteId, user: this.data.self.remoteId, text: fullDecodedText }; this.rtm.send('{"id":' +this.rtmId++ +',"type":"message","channel":"' +channel.remoteId +'", "text":' +JSON.stringify(fullDecodedText) +'}'); } }; /** * @param {SlackChan|SlackGroup|SlackIms} channel * @param {string} msgId **/ Slack.prototype.removeMsg = function(channel, msgId) { httpsRequest(SLACK_ENDPOINT +GETAPI.removeMsg +"?token=" +this.token +"&channel=" +channel.remoteId +"&ts=" +msgId +"&as_user=true"); }; /** * @param {SlackChan|SlackGroup|SlackIms} channel * @param {string} msgId * @param {string} text **/ Slack.prototype.editMsg = function(channel, msgId, text) { httpsRequest(SLACK_ENDPOINT +GETAPI.editMsg +"?token=" +this.token +"&channel=" +channel.remoteId +"&ts=" +msgId +"&text=" +text.join("\n") +"&as_user=true"); }; /** * @param {SlackChan|SlackGroup|SlackIms} target **/ Slack.prototype.fetchHistory = function(target) { var _this = this ,baseUrl = "" ,targetId = target.remoteId; if (targetId[0] === 'D') { baseUrl = SLACK_ENDPOINT +GETAPI.directHistory; } else if (targetId[0] === 'C') { baseUrl = SLACK_ENDPOINT +GETAPI.channelHistory; } else if (targetId[0] === 'G') { baseUrl = SLACK_ENDPOINT +GETAPI.groupHistory; } httpsRequest(baseUrl +"?token="+this.token +"&channel=" +targetId +"&count=" +HISTORY_LENGTH, (status, resp) => { if (status === 200 && resp && resp.ok) { var history = _this.history[target.id] ,now = Date.now(); if (!history) { history = _this.history[target.id] = new SlackHistory(target.remoteId, target.id, this.data.team.id +'|', HISTORY_LENGTH); history.isNew = true; } var now = Date.now(); if (history.pushAll(resp.messages, now)) this.data.liveV = now; } }); }; module.exports.Slack = Slack;