slack.js 16 KB

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