slack.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  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. ,removeMsg: "chat.delete"
  20. ,postFile: "files.upload"
  21. ,emojiList: "emoji.list"
  22. ,addReaction: "reactions.add"
  23. ,removeReaction: "reactions.remove"
  24. }
  25. ,HISTORY_LENGTH = 35
  26. ,UPDATE_LIVE = [
  27. "message"
  28. ,"pin_added"
  29. ,"pin_removed"
  30. ,"reaction_added"
  31. ,"reaction_removed"
  32. ,"star_added"
  33. ,"star_removed"
  34. ] // Message type that affect live history
  35. ;
  36. var
  37. SLACK_SESSIONS = []
  38. ;
  39. setInterval(function() {
  40. SLACK_SESSIONS.forEach(function(slackInst) {
  41. slackInst.closeIfInnactive();
  42. });
  43. }, 60000);
  44. function Slack(sess) {
  45. this.token = sess.slackToken;
  46. this.rtm = null;
  47. this.data = new SlackData(this);
  48. this.history = {};
  49. this.connected = false;
  50. }
  51. Slack.prototype.onRequest = function(knownVersion, cb) {
  52. this.lastCb = cb;
  53. if (this.connected === false) {
  54. this.connect(knownVersion);
  55. } else {
  56. this.waitForEvent(knownVersion);
  57. }
  58. };
  59. function httpsRequest(url, cb) {
  60. https.get(url, (res) => {
  61. if (res.statusCode !== 200) {
  62. cb(res.statusCode, null);
  63. return;
  64. }
  65. var body = null;
  66. res.on('data', (chunk) => {
  67. body = body ? Buffer.concat([body, chunk], body.length +chunk.length) : Buffer.from(chunk);
  68. });
  69. res.on('end', () => {
  70. try {
  71. body = JSON.parse(body.toString("utf8"));
  72. } catch (e) {}
  73. cb && cb(res.statusCode, body);
  74. });
  75. });
  76. }
  77. Slack.prototype.connect = function(knownVersion) {
  78. var _this = this;
  79. this.connected = undefined;
  80. httpsRequest(SLACK_ENDPOINT +GETAPI.rtmStart +"?token=" +this.token, (status, body) => {
  81. if (status !== 200) {
  82. _this.error = body.error;
  83. _this.connected = false;
  84. console.error("Slack api responded " +status);
  85. _this.lastCb(_this);
  86. return;
  87. }
  88. if (!body) {
  89. _this.error = "Slack API error";
  90. _this.connected = false;
  91. _this.lastCb(_this);
  92. return;
  93. }
  94. if (!body.ok) {
  95. _this.error = body.error;
  96. _this.connected = false;
  97. console.error("Slack api responded !ok with ", body);
  98. _this.lastCb(_this);
  99. return;
  100. }
  101. _this.getEmojis((emojis) => {
  102. body.emojis = emojis;
  103. _this.data.updateStatic(body);
  104. _this.connectRtm(body.url);
  105. _this.waitForEvent(knownVersion);
  106. });
  107. });
  108. };
  109. Slack.prototype.getEmojis = function(cb) {
  110. httpsRequest(SLACK_ENDPOINT +GETAPI.emojiList +"?token=" +this.token, (status, body) => {
  111. if (!status || !body || !body.ok)
  112. cb(null);
  113. else
  114. cb(body.emoji || {});
  115. });
  116. };
  117. Slack.prototype.waitForEvent = function(knownVersion) {
  118. var tick = 0
  119. ,_this = this
  120. ,interval;
  121. interval = setInterval(() => {
  122. tick++;
  123. if (!_this.lastCb) {
  124. clearInterval(interval);
  125. return;
  126. }
  127. if (_this.connected) {
  128. var updatedCtx = _this.data.getUpdates(knownVersion)
  129. ,updatedLive = _this.getLiveUpdates(knownVersion);
  130. if (updatedCtx || updatedLive) {
  131. var updated = {
  132. "static": updatedCtx
  133. ,"live": updatedLive
  134. ,"v": Math.max(_this.data.liveV, _this.data.staticV)
  135. // TODO "typing" stream
  136. };
  137. clearInterval(interval);
  138. _this.lastCb(_this, updated);
  139. return;
  140. }
  141. }
  142. if (tick >= 55) { // < 1 minute timeout
  143. clearInterval(interval);
  144. _this.lastCb(_this, { v: knownVersion });
  145. return;
  146. }
  147. }, 1000);
  148. };
  149. /** @return {Object|undefined} */
  150. Slack.prototype.getLiveUpdates = function(knownVersion) {
  151. var result = {};
  152. for (var roomId in this.history) {
  153. var history = this.history[roomId];
  154. if (history.isNew) {
  155. result[roomId] = history.toStatic(0);
  156. history.isNew = false;
  157. }
  158. else {
  159. var roomData = history.toStatic(knownVersion);
  160. if (roomData.length)
  161. result[roomId] = roomData;
  162. }
  163. }
  164. for (var roomId in result) {
  165. return result;
  166. }
  167. return undefined;
  168. };
  169. Slack.prototype.onMessage = function(msg) {
  170. this.data.onMessage(msg);
  171. if ((msg["channel"] || msg["channel_id"] || (msg["item"] && msg["item"]["channel"])) && msg["type"] && UPDATE_LIVE.indexOf(msg["type"]) !== -1) {
  172. var channelId = msg["channel"] || msg["channel_id"] || msg["item"]["channel"]
  173. ,histo = this.history[channelId];
  174. if (histo) {
  175. this.data.liveV = Math.max(this.data.liveV, histo.push(msg));
  176. histo.resort();
  177. } else if (this.data.getChannel(msg["channel"])) {
  178. this.fetchHistory(msg["channel"]);
  179. }
  180. }
  181. };
  182. Slack.prototype.connectRtm = function(url, cb) {
  183. var _this = this;
  184. this.rtm = new WebSocket(url);
  185. this.rtm.on("message", function(msg) {
  186. if (!_this.connected && cb) {
  187. cb();
  188. }
  189. _this.connected = true;
  190. _this.onMessage(JSON.parse(msg));
  191. SLACK_SESSIONS.push(_this);
  192. });
  193. this.rtm.once("error", function(e) {
  194. _this.connected = false;
  195. console.error(e);
  196. _this.close();
  197. });
  198. this.rtm.once("end", function() {
  199. _this.connected = false;
  200. console.error("RTM hang up");
  201. _this.close();
  202. });
  203. };
  204. Slack.prototype.closeIfInnactive = function() {
  205. //TODO
  206. };
  207. Slack.prototype.close = function() {
  208. };
  209. Slack.getOauthToken = function(code, redirectUri, cb) {
  210. httpsRequest(SLACK_ENDPOINT+GETAPI.oauth
  211. +"?client_id=" +config.clientId
  212. +"&client_secret=" +config.clientSecret
  213. +"&redirect_uri=" +redirectUri
  214. +"&code=" +code,
  215. (status, resp) => {
  216. if (status === 200 && resp.ok) {
  217. cb(resp);
  218. } else {
  219. cb(null);
  220. }
  221. });
  222. };
  223. /**
  224. * @param {SlackChan|SlackGroup|SlackIms} channel
  225. * @param {string} contentType
  226. * @param {function(string|null)} callback
  227. **/
  228. Slack.prototype.openUploadFileStream = function(channel, contentType, callback) {
  229. var req = https.request({
  230. hostname: SLACK_HOSTNAME
  231. ,method: 'POST'
  232. ,path: SLACK_ENDPOINT_PATH +GETAPI.postFile
  233. +"?token=" +this.token
  234. +"&channels=" +channel.id
  235. ,headers: {
  236. "Content-Type": contentType
  237. }
  238. }, (res) => {
  239. var errorJson;
  240. res.on("data", (chunk) => {
  241. errorJson = errorJson ? Buffer.concat([errorJson, chunk], errorJson.length +chunk.length) : Buffer.from(chunk);
  242. });
  243. res.once("end", () => {
  244. if (res.statusCode === 200) {
  245. callback(null);
  246. } else {
  247. try {
  248. errorJson = JSON.parse(errorJson.toString());
  249. } catch(e) {
  250. callback("error");
  251. return;
  252. }
  253. callback(errorJson["error"] || "error");
  254. }
  255. });
  256. });
  257. return req;
  258. };
  259. /**
  260. * @param {SlackChan|SlackGroup|SlackIms} channel
  261. * @param {string} msgId
  262. * @param {string} reaction
  263. **/
  264. Slack.prototype.addReaction = function(channel, msgId, reaction) {
  265. httpsRequest(SLACK_ENDPOINT +GETAPI.addReaction
  266. +"?token=" +this.token
  267. +"&name=" +reaction
  268. +"&channel="+channel.id
  269. +"&timestamp="+msgId);
  270. }
  271. /**
  272. * @param {SlackChan|SlackGroup|SlackIms} channel
  273. * @param {string} msgId
  274. * @param {string} reaction
  275. **/
  276. Slack.prototype.removeReaction = function(channel, msgId, reaction) {
  277. httpsRequest(SLACK_ENDPOINT +GETAPI.removeReaction
  278. +"?token=" +this.token
  279. +"&name=" +reaction
  280. +"&channel="+channel.id
  281. +"&timestamp="+msgId);
  282. }
  283. /**
  284. * @param {SlackChan|SlackGroup|SlackIms} channel
  285. * @param {Array.<string>} text
  286. * @param {Array.<Object>=} attachments
  287. **/
  288. Slack.prototype.sendMsg = function(channel, text, attachments) {
  289. httpsRequest(SLACK_ENDPOINT +GETAPI.postMsg
  290. +"?token=" +this.token
  291. +"&channel=" +channel.id
  292. +"&text=" +text.join("\n")
  293. + (attachments ? ("&attachments=" +encodeURIComponent(JSON.stringify(attachments))) : "")
  294. +"&as_user=true");
  295. };
  296. /**
  297. * @param {SlackChan|SlackGroup|SlackIms} channel
  298. * @param {string} msgId
  299. **/
  300. Slack.prototype.removeMsg = function(channel, msgId) {
  301. httpsRequest(SLACK_ENDPOINT +GETAPI.removeMsg
  302. +"?token=" +this.token
  303. +"&channel=" +channel.id
  304. +"&ts=" +msgId
  305. +"&as_user=true");
  306. };
  307. Slack.prototype.fetchHistory = function(targetId) {
  308. var _this = this
  309. ,baseUrl = "";
  310. if (targetId[0] === 'D') {
  311. baseUrl = SLACK_ENDPOINT +GETAPI.directHistory;
  312. } else if (targetId[0] === 'C') {
  313. baseUrl = SLACK_ENDPOINT +GETAPI.channelHistory;
  314. } else if (targetId[0] === 'G') {
  315. baseUrl = SLACK_ENDPOINT +GETAPI.groupHistory;
  316. }
  317. httpsRequest(baseUrl
  318. +"?token="+this.token
  319. +"&channel=" +targetId
  320. +"&count=" +HISTORY_LENGTH,
  321. (status, resp) => {
  322. if (status === 200 && resp && resp.ok) {
  323. var history = _this.history[targetId];
  324. if (!history) {
  325. history = _this.history[targetId] = new SlackHistory(targetId, HISTORY_LENGTH);
  326. history.isNew = true;
  327. }
  328. this.data.liveV = Math.max(history.pushAll(resp.messages), this.data.liveV);
  329. }
  330. });
  331. };
  332. module.exports.Slack = Slack;