slack.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  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. }
  30. ,HISTORY_LENGTH = 35
  31. ,INACTIVE_DELAY = 120000
  32. ,UPDATE_LIVE = [
  33. "message"
  34. ,"pin_added"
  35. ,"pin_removed"
  36. ,"reaction_added"
  37. ,"reaction_removed"
  38. ,"star_added"
  39. ,"star_removed"
  40. ] // Message type that affect live history
  41. ;
  42. var
  43. SLACK_SESSIONS = []
  44. ;
  45. setInterval(function() {
  46. var t = Date.now();
  47. SLACK_SESSIONS.forEach(function(slackInst) {
  48. if (!slackInst.closeIfInnactive(t) && slackInst.active)
  49. slackInst.sendActive();
  50. });
  51. }, 60000);
  52. function Slack(sess) {
  53. this.token = sess.slackToken;
  54. this.rtm = null;
  55. this.data = new SlackData(this);
  56. this.history = {};
  57. this.connected = false;
  58. this.active = true;
  59. this.lastPing = Date.now();
  60. }
  61. Slack.prototype.onUserInterract = function(t) {
  62. this.lastPing = Math.max(t, this.lastPing);
  63. }
  64. Slack.prototype.onRequest = function(knownVersion, cb) {
  65. this.lastCb = cb;
  66. if (this.connected === false) {
  67. this.connect(knownVersion);
  68. } else {
  69. this.waitForEvent(knownVersion);
  70. }
  71. };
  72. function httpsRequest(url, cb) {
  73. https.get(url, (res) => {
  74. if (res.statusCode !== 200) {
  75. cb(res.statusCode, null);
  76. return;
  77. }
  78. var body = null;
  79. res.on('data', (chunk) => {
  80. body = body ? Buffer.concat([body, chunk], body.length +chunk.length) : Buffer.from(chunk);
  81. });
  82. res.on('end', () => {
  83. try {
  84. body = JSON.parse(body.toString("utf8"));
  85. } catch (e) {}
  86. cb && cb(res.statusCode, body);
  87. });
  88. });
  89. }
  90. Slack.prototype.connect = function(knownVersion) {
  91. var _this = this;
  92. this.connected = undefined;
  93. httpsRequest(SLACK_ENDPOINT +GETAPI.rtmStart +"?token=" +this.token, (status, body) => {
  94. if (status !== 200) {
  95. _this.error = body.error;
  96. _this.connected = false;
  97. console.error("Slack api responded " +status);
  98. _this.lastCb(_this);
  99. return;
  100. }
  101. if (!body) {
  102. _this.error = "Slack API error";
  103. _this.connected = false;
  104. _this.lastCb(_this);
  105. return;
  106. }
  107. if (!body.ok) {
  108. _this.error = body.error;
  109. _this.connected = false;
  110. console.error("Slack api responded !ok with ", body);
  111. _this.lastCb(_this);
  112. return;
  113. }
  114. _this.getEmojis((emojis) => {
  115. _this.getSlashCommands((commands) => {
  116. body.emojis = emojis;
  117. body.commands = commands;
  118. _this.data.updateStatic(body, Date.now());
  119. _this.connectRtm(body.url);
  120. _this.waitForEvent(knownVersion);
  121. });
  122. });
  123. });
  124. };
  125. Slack.prototype.sendCommand = function(room, cmd, arg) {
  126. httpsRequest(
  127. SLACK_ENDPOINT
  128. +GETAPI.slashExec
  129. +"?token=" +this.token
  130. +"&command=" +encodeURIComponent(cmd.name)
  131. +"&disp=" +encodeURIComponent(cmd.name)
  132. +"&channel=" +room.id
  133. +"&text=" +arg);
  134. }
  135. Slack.prototype.sendTyping = function(room) {
  136. this.rtm.send('{"id":' +this.rtmId++ +',"type":"typing","channel":"' +room.id +'"}');
  137. }
  138. Slack.prototype.getSlashCommands = function(cb) {
  139. httpsRequest(SLACK_ENDPOINT +GETAPI.slashList +"?token=" +this.token, (status, body) => {
  140. if (!status || !body || !body.ok)
  141. cb(null);
  142. else
  143. cb(body.commands || {});
  144. });
  145. };
  146. Slack.prototype.getEmojis = function(cb) {
  147. httpsRequest(SLACK_ENDPOINT +GETAPI.emojiList +"?token=" +this.token, (status, body) => {
  148. if (!status || !body || !body.ok)
  149. cb(null);
  150. else
  151. cb(body.emoji || {});
  152. });
  153. };
  154. Slack.prototype.waitForEvent = function(knownVersion) {
  155. var tick = 0
  156. ,_this = this
  157. ,interval;
  158. interval = setInterval(() => {
  159. tick++;
  160. if (!_this.lastCb) {
  161. clearInterval(interval);
  162. return;
  163. }
  164. if (_this.connected) {
  165. var updatedCtx = _this.data.getUpdates(knownVersion, Date.now())
  166. ,updatedLive = _this.getLiveUpdates(knownVersion)
  167. ,updated;
  168. if (updatedCtx || updatedLive) {
  169. updated = {};
  170. updated["static"] = updatedCtx;
  171. updated["live"] = updatedLive;
  172. updated["v"] = Math.max(_this.data.liveV, _this.data.staticV);
  173. }
  174. if (updated) {
  175. clearInterval(interval);
  176. _this.lastCb(_this, updated);
  177. return;
  178. }
  179. }
  180. if (tick >= 55) { // < 1 minute timeout
  181. clearInterval(interval);
  182. _this.lastCb(_this, { v: knownVersion });
  183. return;
  184. }
  185. }, 1000);
  186. };
  187. /** @return {Object|undefined} */
  188. Slack.prototype.getLiveUpdates = function(knownVersion) {
  189. var result = {};
  190. for (var roomId in this.history) {
  191. var history = this.history[roomId];
  192. if (history.isNew) {
  193. result[roomId] = history.toStatic(0);
  194. history.isNew = false;
  195. }
  196. else {
  197. var roomData = history.toStatic(knownVersion);
  198. if (roomData.length)
  199. result[roomId] = roomData;
  200. }
  201. }
  202. for (var roomId in result) {
  203. return result;
  204. }
  205. return undefined;
  206. };
  207. Slack.prototype.onMessage = function(msg, t) {
  208. this.data.onMessage(msg, t);
  209. if ((msg["channel"] || msg["channel_id"] || (msg["item"] && msg["item"]["channel"])) && msg["type"] && UPDATE_LIVE.indexOf(msg["type"]) !== -1) {
  210. var channelId = msg["channel"] || msg["channel_id"] || msg["item"]["channel"]
  211. ,channel = this.data.getChannel(msg["channel"])
  212. ,histo = this.history[channelId];
  213. if (histo) {
  214. this.data.liveV = Math.max(this.data.liveV, histo.push(msg, t));
  215. histo.resort();
  216. if (channel)
  217. channel.lastMsg = Math.max(this.data.liveV, channel.lastMsg);
  218. } else if (channel) {
  219. this.fetchHistory(msg["channel"]);
  220. }
  221. }
  222. };
  223. Slack.prototype.connectRtm = function(url, cb) {
  224. var _this = this;
  225. this.rtmId = 0;
  226. this.rtm = new WebSocket(url);
  227. this.rtm.on("message", function(msg) {
  228. if (!_this.connected && cb) {
  229. cb();
  230. }
  231. if (!_this.connected) {
  232. _this.connected = true;
  233. SLACK_SESSIONS.push(_this);
  234. }
  235. _this.onMessage(JSON.parse(msg), Date.now());
  236. });
  237. this.rtm.once("error", function(e) {
  238. _this.connected = false;
  239. console.error(e);
  240. _this.close();
  241. var arrIndex = SLACK_SESSIONS.indexOf(_this);
  242. if (arrIndex >= 0) SLACK_SESSIONS.splice(arrIndex, 1);
  243. });
  244. this.rtm.once("end", function() {
  245. console.error("RTM hang up");
  246. _this.onClose();
  247. });
  248. };
  249. Slack.prototype.onClose = function() {
  250. var arrIndex = SLACK_SESSIONS.indexOf(this);
  251. this.connected = false;
  252. if (arrIndex >= 0) SLACK_SESSIONS.splice(arrIndex, 1);
  253. };
  254. /**
  255. * @return {boolean} true if innactive and closeding
  256. **/
  257. Slack.prototype.closeIfInnactive = function(t) {
  258. if (t - this.lastPing >INACTIVE_DELAY) {
  259. this.close();
  260. return true;
  261. }
  262. return false;
  263. };
  264. Slack.prototype.sendActive = function() {
  265. httpsRequest(SLACK_ENDPOINT+GETAPI.setActive
  266. +"?token=" +this.token);
  267. };
  268. Slack.prototype.close = function() {
  269. this.rtm.close();
  270. this.onClose();
  271. };
  272. Slack.getOauthToken = function(code, redirectUri, cb) {
  273. httpsRequest(SLACK_ENDPOINT+GETAPI.oauth
  274. +"?client_id=" +config.clientId
  275. +"&client_secret=" +config.clientSecret
  276. +"&redirect_uri=" +redirectUri
  277. +"&code=" +code,
  278. (status, resp) => {
  279. if (status === 200 && resp.ok) {
  280. cb(resp);
  281. } else {
  282. cb(null);
  283. }
  284. });
  285. };
  286. /**
  287. * @param {SlackChan|SlackGroup|SlackIms} channel
  288. * @param {string} contentType
  289. * @param {function(string|null)} callback
  290. **/
  291. Slack.prototype.openUploadFileStream = function(channel, contentType, callback) {
  292. var req = https.request({
  293. hostname: SLACK_HOSTNAME
  294. ,method: 'POST'
  295. ,path: SLACK_ENDPOINT_PATH +GETAPI.postFile
  296. +"?token=" +this.token
  297. +"&channels=" +channel.id
  298. ,headers: {
  299. "Content-Type": contentType
  300. }
  301. }, (res) => {
  302. var errorJson;
  303. res.on("data", (chunk) => {
  304. errorJson = errorJson ? Buffer.concat([errorJson, chunk], errorJson.length +chunk.length) : Buffer.from(chunk);
  305. });
  306. res.once("end", () => {
  307. if (res.statusCode === 200) {
  308. callback(null);
  309. } else {
  310. try {
  311. errorJson = JSON.parse(errorJson.toString());
  312. } catch(e) {
  313. callback("error");
  314. return;
  315. }
  316. callback(errorJson["error"] || "error");
  317. }
  318. });
  319. });
  320. return req;
  321. };
  322. /**
  323. * @param {SlackChan|SlackGroup|SlackIms} channel
  324. * @param {string} msgId
  325. * @param {string} reaction
  326. **/
  327. Slack.prototype.addReaction = function(channel, msgId, reaction) {
  328. httpsRequest(SLACK_ENDPOINT +GETAPI.addReaction
  329. +"?token=" +this.token
  330. +"&name=" +reaction
  331. +"&channel="+channel.id
  332. +"&timestamp="+msgId);
  333. }
  334. /**
  335. * @param {SlackChan|SlackGroup|SlackIms} channel
  336. * @param {string} msgId
  337. * @param {string} reaction
  338. **/
  339. Slack.prototype.removeReaction = function(channel, msgId, reaction) {
  340. httpsRequest(SLACK_ENDPOINT +GETAPI.removeReaction
  341. +"?token=" +this.token
  342. +"&name=" +reaction
  343. +"&channel="+channel.id
  344. +"&timestamp="+msgId);
  345. }
  346. /**
  347. * @param {SlackChan|SlackGroup|SlackIms} channel
  348. * @param {Array.<string>} text
  349. **/
  350. Slack.prototype.sendMeMsg = function(channel, text) {
  351. httpsRequest(SLACK_ENDPOINT +GETAPI.postMeMsg
  352. +"?token=" +this.token
  353. +"&channel=" +channel.id
  354. +"&text=" +text.join("\n")
  355. +"&as_user=true");
  356. };
  357. /**
  358. * @param {SlackChan|SlackGroup|SlackIms} channel
  359. * @param {Array.<string>} text
  360. * @param {Array.<Object>=} attachments
  361. **/
  362. Slack.prototype.sendMsg = function(channel, text, attachments) {
  363. httpsRequest(SLACK_ENDPOINT +GETAPI.postMsg
  364. +"?token=" +this.token
  365. +"&channel=" +channel.id
  366. +"&text=" +text.join("\n")
  367. + (attachments ? ("&attachments=" +encodeURIComponent(JSON.stringify(attachments))) : "")
  368. +"&as_user=true");
  369. };
  370. /**
  371. * @param {SlackChan|SlackGroup|SlackIms} channel
  372. * @param {string} msgId
  373. **/
  374. Slack.prototype.removeMsg = function(channel, msgId) {
  375. httpsRequest(SLACK_ENDPOINT +GETAPI.removeMsg
  376. +"?token=" +this.token
  377. +"&channel=" +channel.id
  378. +"&ts=" +msgId
  379. +"&as_user=true");
  380. };
  381. /**
  382. * @param {SlackChan|SlackGroup|SlackIms} channel
  383. * @param {string} msgId
  384. * @param {string} text
  385. **/
  386. Slack.prototype.editMsg = function(channel, msgId, text) {
  387. httpsRequest(SLACK_ENDPOINT +GETAPI.editMsg
  388. +"?token=" +this.token
  389. +"&channel=" +channel.id
  390. +"&ts=" +msgId
  391. +"&text=" +text.join("\n")
  392. +"&as_user=true");
  393. };
  394. Slack.prototype.fetchHistory = function(targetId) {
  395. var _this = this
  396. ,baseUrl = "";
  397. if (targetId[0] === 'D') {
  398. baseUrl = SLACK_ENDPOINT +GETAPI.directHistory;
  399. } else if (targetId[0] === 'C') {
  400. baseUrl = SLACK_ENDPOINT +GETAPI.channelHistory;
  401. } else if (targetId[0] === 'G') {
  402. baseUrl = SLACK_ENDPOINT +GETAPI.groupHistory;
  403. }
  404. httpsRequest(baseUrl
  405. +"?token="+this.token
  406. +"&channel=" +targetId
  407. +"&count=" +HISTORY_LENGTH,
  408. (status, resp) => {
  409. if (status === 200 && resp && resp.ok) {
  410. var history = _this.history[targetId]
  411. ,now = Date.now();
  412. if (!history) {
  413. history = _this.history[targetId] = new SlackHistory(targetId, HISTORY_LENGTH);
  414. history.isNew = true;
  415. }
  416. this.data.liveV = Math.max(history.pushAll(resp.messages, Date.now()), this.data.liveV);
  417. }
  418. });
  419. };
  420. module.exports.Slack = Slack;