slack.js 8.2 KB

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