slack.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. const
  2. WebSocket = require('ws'),
  3. https = require('https'),
  4. sleep = require("sleep").sleep
  5. ,SlackData = require("./slackData.js").SlackData
  6. ,SlackHistory = require("./slackHistory.js").SlackHistory
  7. ,config = require("../config.js")
  8. ;
  9. const SLACK_ENDPOINT = "https://slack.com/api/"
  10. ,SLACK_HOSTNAME = "slack.com"
  11. ,SLACK_ENDPOINT_PATH = "/api/"
  12. ,GETAPI = {
  13. rtmStart: "rtm.start"
  14. ,oauth: "oauth.access"
  15. ,channelHistory: "channels.history"
  16. ,directHistory: "im.history"
  17. ,groupHistory: "groups.history"
  18. ,postMsg: "chat.postMessage"
  19. ,postMeMsg: "chat.meMessage"
  20. ,editMsg: "chat.update"
  21. ,removeMsg: "chat.delete"
  22. ,postFile: "files.upload"
  23. ,setActive: "users.setActive"
  24. ,emojiList: "emoji.list"
  25. ,slashList: "commands.list"
  26. ,slashExec: "chat.command"
  27. ,addReaction: "reactions.add"
  28. ,removeReaction: "reactions.remove"
  29. ,sendAction: "chat.attachmentAction"
  30. ,read: {
  31. group: "groups.mark"
  32. ,im: "im.mark"
  33. ,group: "mpim.mark"
  34. ,channel: "channels.mark"
  35. }
  36. }
  37. ,HISTORY_LENGTH = 35
  38. ,UPDATE_LIVE = [
  39. "message"
  40. ,"pin_added"
  41. ,"pin_removed"
  42. ,"reaction_added"
  43. ,"reaction_removed"
  44. ,"star_added"
  45. ,"star_removed"
  46. ] // Message type that affect live history
  47. ;
  48. function Slack(sess, manager) {
  49. this.token = sess.slackToken;
  50. this.sessId = sess.id;
  51. this.manager = manager;
  52. this.rtm = null;
  53. this.data = new SlackData(this);
  54. this.history = {};
  55. this.pendingRtm = {};
  56. this.connected = false;
  57. this.closing = false;
  58. }
  59. Slack.prototype.onRequest = function(knownVersion, cb) {
  60. this.lastCb = cb;
  61. if (this.connected === false) {
  62. this.connect(knownVersion);
  63. } else {
  64. this.waitForEvent(knownVersion);
  65. }
  66. };
  67. function httpsRequest(url, cb) {
  68. try {
  69. https.get(url, (res) => {
  70. if (res.statusCode !== 200) {
  71. cb(res.statusCode, null);
  72. return;
  73. }
  74. var body = null;
  75. res.on('data', (chunk) => {
  76. body = body ? Buffer.concat([body, chunk], body.length +chunk.length) : Buffer.from(chunk);
  77. });
  78. res.on('end', () => {
  79. try {
  80. body = JSON.parse(body.toString("utf8"));
  81. } catch (e) {}
  82. cb && cb(res.statusCode, body);
  83. });
  84. });
  85. }
  86. catch (e) {
  87. console.error("Error in https request: ", e);
  88. cb && cb(0, null);
  89. }
  90. }
  91. Slack.prototype.connect = function(knownVersion) {
  92. var _this = this;
  93. this.connected = undefined;
  94. httpsRequest(SLACK_ENDPOINT +GETAPI.rtmStart +"?token=" +this.token, (status, body) => {
  95. if (status !== 200) {
  96. _this.error = body.error;
  97. _this.connected = false;
  98. console.error("Slack api responded " +status);
  99. _this.lastCb(_this);
  100. return;
  101. }
  102. if (!body) {
  103. _this.error = "Slack API error";
  104. _this.connected = false;
  105. _this.lastCb(_this);
  106. return;
  107. }
  108. if (!body.ok) {
  109. _this.error = body.error;
  110. _this.connected = false;
  111. console.error("Slack api responded !ok with ", body);
  112. _this.lastCb(_this);
  113. return;
  114. }
  115. _this.getEmojis((emojis) => {
  116. _this.getSlashCommands((commands) => {
  117. body.emojis = emojis;
  118. body.commands = commands;
  119. _this.data.updateStatic(body, Date.now());
  120. _this.connectRtm(body.url);
  121. _this.waitForEvent(knownVersion);
  122. });
  123. });
  124. });
  125. };
  126. Slack.prototype.sendCommand = function(room, cmd, arg) {
  127. httpsRequest(
  128. SLACK_ENDPOINT
  129. +GETAPI.slashExec
  130. +"?token=" +this.token
  131. +"&command=" +encodeURIComponent(cmd.name)
  132. +"&disp=" +encodeURIComponent(cmd.name)
  133. +"&channel=" +room.id
  134. +"&text=" +arg);
  135. }
  136. Slack.prototype.sendTyping = function(room) {
  137. this.rtm.send('{"id":' +this.rtmId++ +',"type":"typing","channel":"' +room.id +'"}');
  138. }
  139. Slack.prototype.getSlashCommands = function(cb) {
  140. httpsRequest(SLACK_ENDPOINT +GETAPI.slashList +"?token=" +this.token, (status, body) => {
  141. if (!status || !body || !body.ok)
  142. cb(null);
  143. else
  144. cb(body.commands || {});
  145. });
  146. };
  147. Slack.prototype.getEmojis = function(cb) {
  148. httpsRequest(SLACK_ENDPOINT +GETAPI.emojiList +"?token=" +this.token, (status, body) => {
  149. if (!status || !body || !body.ok)
  150. cb(null);
  151. else
  152. cb(body.emoji || {});
  153. });
  154. };
  155. Slack.prototype.waitForEvent = function(knownVersion) {
  156. var tick = 0
  157. ,_this = this
  158. ,interval;
  159. interval = setInterval(() => {
  160. tick++;
  161. if (!_this.lastCb) {
  162. clearInterval(interval);
  163. return;
  164. }
  165. if (_this.connected) {
  166. var updatedCtx = _this.data.getUpdates(knownVersion, Date.now())
  167. ,updatedLive = _this.getLiveUpdates(knownVersion)
  168. ,updated;
  169. if (updatedCtx || updatedLive) {
  170. updated = {};
  171. updated["static"] = updatedCtx;
  172. updated["live"] = updatedLive;
  173. updated["v"] = Math.max(_this.data.liveV, _this.data.staticV);
  174. }
  175. if (updated) {
  176. clearInterval(interval);
  177. _this.lastCb(_this, updated);
  178. return;
  179. }
  180. }
  181. if (tick >= 55) { // < 1 minute timeout
  182. clearInterval(interval);
  183. _this.lastCb(_this, { v: knownVersion });
  184. return;
  185. }
  186. }, 1000);
  187. };
  188. /** @return {Object|undefined} */
  189. Slack.prototype.getLiveUpdates = function(knownVersion) {
  190. var result = {};
  191. for (var roomId in this.history) {
  192. var history = this.history[roomId];
  193. if (history.isNew) {
  194. result[roomId] = history.toStatic(0);
  195. history.isNew = false;
  196. }
  197. else {
  198. var roomData = history.toStatic(knownVersion);
  199. if (roomData.length)
  200. result[roomId] = roomData;
  201. }
  202. }
  203. for (var roomId in result) {
  204. return result;
  205. }
  206. return undefined;
  207. };
  208. Slack.prototype.onMessage = function(msg, t) {
  209. if (msg["ok"] === true && msg["reply_to"] && this.pendingRtm[msg["reply_to"]]) {
  210. var ts = msg["ts"]
  211. ,rtmId = msg["reply_to"];
  212. msg = this.pendingRtm[rtmId];
  213. msg["ts"] = ts;
  214. delete this.pendingRtm[rtmId];
  215. }
  216. this.data.onMessage(msg, t);
  217. if ((msg["channel"] || msg["channel_id"] || (msg["item"] && msg["item"]["channel"])) && msg["type"] && UPDATE_LIVE.indexOf(msg["type"]) !== -1) {
  218. var channelId = msg["channel"] || msg["channel_id"] || msg["item"]["channel"]
  219. ,channel = this.data.channels[msg["channel"]]
  220. ,histo = this.history[channelId];
  221. if (histo) {
  222. var lastMsg = histo.push(msg, t);
  223. if (lastMsg)
  224. this.data.liveV = t;
  225. histo.resort();
  226. if (channel)
  227. channel.setLastMsg(lastMsg, t);
  228. } else if (channel) {
  229. this.fetchHistory(this.data.channels[this.data.team.id +'|' +msg["channel"]]);
  230. }
  231. }
  232. };
  233. /**
  234. * @param {SlackChan|SlackGroup|SlackIms} chan
  235. * @param {number} ts
  236. **/
  237. Slack.prototype.markRead = function(chan, ts) {
  238. var apiURI;
  239. if (chan.id[0] === 'C')
  240. apiURI = SLACK_ENDPOINT+GETAPI.read.channel;
  241. else if (chan.id[0] === 'G')
  242. apiURI = SLACK_ENDPOINT+GETAPI.read.group;
  243. else if (chan.id[0] === 'D')
  244. apiURI = SLACK_ENDPOINT+GETAPI.read.im;
  245. httpsRequest(apiURI
  246. +"?token=" +this.token
  247. +"&channel="+chan.id
  248. +"&ts="+ts);
  249. };
  250. Slack.prototype.connectRtm = function(url, cb) {
  251. var _this = this;
  252. this.rtmId = 0;
  253. this.rtm = new WebSocket(url);
  254. this.rtm.on("message", function(msg) {
  255. if (!_this.connected && cb) {
  256. cb();
  257. }
  258. if (!_this.connected) {
  259. _this.connected = true;
  260. }
  261. _this.onMessage(JSON.parse(msg), Date.now());
  262. });
  263. this.rtm.once("error", function(e) {
  264. _this.connected = false;
  265. console.error(e);
  266. _this.close();
  267. });
  268. this.rtm.once("end", function() {
  269. console.error("RTM hang up");
  270. _this.onClose();
  271. });
  272. };
  273. Slack.prototype.onClose = function() {
  274. this.manager.suicide(this);
  275. };
  276. Slack.prototype.ping = function() {
  277. httpsRequest(SLACK_ENDPOINT+GETAPI.setActive
  278. +"?token=" +this.token);
  279. };
  280. Slack.prototype.close = function() {
  281. if (!this.closing) {
  282. this.closing = true;
  283. if (this.rtm)
  284. this.rtm.close();
  285. this.onClose();
  286. }
  287. };
  288. Slack.getOauthToken = function(code, redirectUri, cb) {
  289. httpsRequest(SLACK_ENDPOINT+GETAPI.oauth
  290. +"?client_id=" +config.clientId
  291. +"&client_secret=" +config.clientSecret
  292. +"&redirect_uri=" +redirectUri
  293. +"&code=" +code,
  294. (status, resp) => {
  295. if (status === 200 && resp.ok) {
  296. cb(resp);
  297. } else {
  298. cb(null);
  299. }
  300. });
  301. };
  302. /**
  303. * @param {SlackChan|SlackGroup|SlackIms} channel
  304. * @param {string} contentType
  305. * @param {function(string|null)} callback
  306. **/
  307. Slack.prototype.openUploadFileStream = function(channel, contentType, callback) {
  308. var req = https.request({
  309. hostname: SLACK_HOSTNAME
  310. ,method: 'POST'
  311. ,path: SLACK_ENDPOINT_PATH +GETAPI.postFile
  312. +"?token=" +this.token
  313. +"&channels=" +channel.id
  314. ,headers: {
  315. "Content-Type": contentType
  316. }
  317. }, (res) => {
  318. var errorJson;
  319. res.on("data", (chunk) => {
  320. errorJson = errorJson ? Buffer.concat([errorJson, chunk], errorJson.length +chunk.length) : Buffer.from(chunk);
  321. });
  322. res.once("end", () => {
  323. if (res.statusCode === 200) {
  324. callback(null);
  325. } else {
  326. try {
  327. errorJson = JSON.parse(errorJson.toString());
  328. } catch(e) {
  329. callback("error");
  330. return;
  331. }
  332. callback(errorJson["error"] || "error");
  333. }
  334. });
  335. });
  336. return req;
  337. };
  338. /**
  339. * @param {SlackChan|SlackGroup|SlackIms} channel
  340. * @param {string} contentType
  341. * @param {function(string|null)} callback
  342. **/
  343. Slack.prototype.sendAction = function(contentType, payloadStream, callback) {
  344. var req = https.request({
  345. hostname: SLACK_HOSTNAME
  346. ,method: 'POST'
  347. ,path: SLACK_ENDPOINT_PATH +GETAPI.sendAction
  348. +"?token=" +this.token
  349. ,headers: {
  350. "Content-Type": contentType
  351. }
  352. }, (res) => {
  353. var resp;
  354. res.on("data", (chunk) => {
  355. resp = resp ? Buffer.concat([resp, chunk], resp.length +chunk.length) : Buffer.from(chunk);
  356. });
  357. res.once("end", () => {
  358. callback(resp);
  359. });
  360. });
  361. payloadStream.pipe(req, { end: true });
  362. };
  363. /**
  364. * @param {SlackChan|SlackGroup|SlackIms} channel
  365. * @param {string} contentType
  366. * @param {function(string|null)} callback
  367. **/
  368. Slack.prototype.openUploadFileStream = function(channel, contentType, callback) {
  369. var req = https.request({
  370. hostname: SLACK_HOSTNAME
  371. ,method: 'POST'
  372. ,path: SLACK_ENDPOINT_PATH +GETAPI.postFile
  373. +"?token=" +this.token
  374. +"&channels=" +channel.id
  375. ,headers: {
  376. "Content-Type": contentType
  377. }
  378. }, (res) => {
  379. var errorJson;
  380. res.on("data", (chunk) => {
  381. errorJson = errorJson ? Buffer.concat([errorJson, chunk], errorJson.length +chunk.length) : Buffer.from(chunk);
  382. });
  383. res.once("end", () => {
  384. if (res.statusCode === 200) {
  385. callback(null);
  386. } else {
  387. try {
  388. errorJson = JSON.parse(errorJson.toString());
  389. } catch(e) {
  390. callback("error");
  391. return;
  392. }
  393. callback(errorJson["error"] || "error");
  394. }
  395. });
  396. });
  397. return req;
  398. };
  399. /**
  400. * @param {SlackChan|SlackGroup|SlackIms} channel
  401. * @param {string} msgId
  402. * @param {string} reaction
  403. **/
  404. Slack.prototype.addReaction = function(channel, msgId, reaction) {
  405. httpsRequest(SLACK_ENDPOINT +GETAPI.addReaction
  406. +"?token=" +this.token
  407. +"&name=" +reaction
  408. +"&channel="+channel.id
  409. +"&timestamp="+msgId);
  410. }
  411. /**
  412. * @param {SlackChan|SlackGroup|SlackIms} channel
  413. * @param {string} msgId
  414. * @param {string} reaction
  415. **/
  416. Slack.prototype.removeReaction = function(channel, msgId, reaction) {
  417. httpsRequest(SLACK_ENDPOINT +GETAPI.removeReaction
  418. +"?token=" +this.token
  419. +"&name=" +reaction
  420. +"&channel="+channel.id
  421. +"&timestamp="+msgId);
  422. }
  423. /**
  424. * @param {SlackChan|SlackGroup|SlackIms} channel
  425. * @param {Array.<string>} text
  426. **/
  427. Slack.prototype.sendMeMsg = function(channel, text) {
  428. httpsRequest(SLACK_ENDPOINT +GETAPI.postMeMsg
  429. +"?token=" +this.token
  430. +"&channel=" +channel.id
  431. +"&text=" +text.join("\n")
  432. +"&as_user=true");
  433. };
  434. /**
  435. * @param {SlackChan|SlackGroup|SlackIms} channel
  436. * @param {Array.<string>} text
  437. * @param {Array.<Object>=} attachments
  438. **/
  439. Slack.prototype.sendMsg = function(channel, text, attachments) {
  440. if (attachments) {
  441. httpsRequest(SLACK_ENDPOINT +GETAPI.postMsg
  442. +"?token=" +this.token
  443. +"&channel=" +channel.id
  444. +"&text=" +text.join("\n")
  445. + (attachments ? ("&attachments=" +encodeURIComponent(JSON.stringify(attachments))) : "")
  446. +"&as_user=true");
  447. } else {
  448. var decodedText = [];
  449. text.forEach(function(i) {
  450. decodedText.push(decodeURIComponent(i));
  451. });
  452. var fullDecodedText = decodedText.join("\n");
  453. this.pendingRtm[this.rtmId] = {
  454. type: 'message',
  455. channel: channel.id,
  456. user: this.data.self.id,
  457. text: fullDecodedText
  458. };
  459. this.rtm.send('{"id":' +this.rtmId++ +',"type":"message","channel":"' +channel.id +'", "text":' +JSON.stringify(fullDecodedText) +'}');
  460. }
  461. };
  462. /**
  463. * @param {SlackChan|SlackGroup|SlackIms} channel
  464. * @param {string} msgId
  465. **/
  466. Slack.prototype.removeMsg = function(channel, msgId) {
  467. httpsRequest(SLACK_ENDPOINT +GETAPI.removeMsg
  468. +"?token=" +this.token
  469. +"&channel=" +channel.id
  470. +"&ts=" +msgId
  471. +"&as_user=true");
  472. };
  473. /**
  474. * @param {SlackChan|SlackGroup|SlackIms} channel
  475. * @param {string} msgId
  476. * @param {string} text
  477. **/
  478. Slack.prototype.editMsg = function(channel, msgId, text) {
  479. httpsRequest(SLACK_ENDPOINT +GETAPI.editMsg
  480. +"?token=" +this.token
  481. +"&channel=" +channel.id
  482. +"&ts=" +msgId
  483. +"&text=" +text.join("\n")
  484. +"&as_user=true");
  485. };
  486. /**
  487. * @param {SlackChan|SlackGroup|SlackIms} target
  488. **/
  489. Slack.prototype.fetchHistory = function(target) {
  490. var _this = this
  491. ,baseUrl = ""
  492. ,targetId = target.remoteId;
  493. if (targetId[0] === 'D') {
  494. baseUrl = SLACK_ENDPOINT +GETAPI.directHistory;
  495. } else if (targetId[0] === 'C') {
  496. baseUrl = SLACK_ENDPOINT +GETAPI.channelHistory;
  497. } else if (targetId[0] === 'G') {
  498. baseUrl = SLACK_ENDPOINT +GETAPI.groupHistory;
  499. }
  500. httpsRequest(baseUrl
  501. +"?token="+this.token
  502. +"&channel=" +targetId
  503. +"&count=" +HISTORY_LENGTH,
  504. (status, resp) => {
  505. if (status === 200 && resp && resp.ok) {
  506. var history = _this.history[target.id]
  507. ,now = Date.now();
  508. if (!history) {
  509. history = _this.history[target.id] = new SlackHistory(target.remoteId, target.id, this.data.team.id +'|', HISTORY_LENGTH);
  510. history.isNew = true;
  511. }
  512. var now = Date.now();
  513. if (history.pushAll(resp.messages, now))
  514. this.data.liveV = now;
  515. }
  516. });
  517. };
  518. module.exports.Slack = Slack;