slack.js 8.7 KB

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