| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- 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.id
- +"&text=" +arg);
- }
- Slack.prototype.sendTyping = function(room) {
- this.rtm.send('{"id":' +this.rtmId++ +',"type":"typing","channel":"' +room.id +'"}');
- }
- 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 = msg["channel"] || msg["channel_id"] || msg["item"]["channel"]
- ,channel = this.data.channels[msg["channel"]]
- ,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(this.data.channels[this.data.team.id +'|' +msg["channel"]]);
- }
- }
- };
- /**
- * @param {SlackChan|SlackGroup|SlackIms} chan
- * @param {number} ts
- **/
- Slack.prototype.markRead = function(chan, ts) {
- var apiURI;
- if (chan.id[0] === 'C')
- apiURI = SLACK_ENDPOINT+GETAPI.read.channel;
- else if (chan.id[0] === 'G')
- apiURI = SLACK_ENDPOINT+GETAPI.read.group;
- else if (chan.id[0] === 'D')
- apiURI = SLACK_ENDPOINT+GETAPI.read.im;
- httpsRequest(apiURI
- +"?token=" +this.token
- +"&channel="+chan.id
- +"&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.id
- ,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.id
- ,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.id
- +"×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.id
- +"×tamp="+msgId);
- }
- /**
- * @param {SlackChan|SlackGroup|SlackIms} channel
- * @param {Array.<string>} text
- **/
- Slack.prototype.sendMeMsg = function(channel, text) {
- httpsRequest(SLACK_ENDPOINT +GETAPI.postMeMsg
- +"?token=" +this.token
- +"&channel=" +channel.id
- +"&text=" +text.join("\n")
- +"&as_user=true");
- };
- /**
- * @param {SlackChan|SlackGroup|SlackIms} channel
- * @param {Array.<string>} text
- * @param {Array.<Object>=} attachments
- **/
- Slack.prototype.sendMsg = function(channel, text, attachments) {
- if (attachments) {
- httpsRequest(SLACK_ENDPOINT +GETAPI.postMsg
- +"?token=" +this.token
- +"&channel=" +channel.id
- +"&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.id,
- user: this.data.self.id,
- text: fullDecodedText
- };
- this.rtm.send('{"id":' +this.rtmId++ +',"type":"message","channel":"' +channel.id +'", "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.id
- +"&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.id
- +"&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;
|