Browse Source

[add] slack rtm handshake
[add] slack oauth
[wip] long polling timeout after 1 minutes, to test / improve
[wip] parse slack "static" data

B Thibault 8 years ago
parent
commit
a5fe710fe4
10 changed files with 195 additions and 32 deletions
  1. 1 0
      .gitignore
  2. 2 3
      cli/workflow.js
  3. 1 0
      package.json
  4. 0 6
      src/config.js
  5. 7 0
      src/config.js.exple
  6. 1 1
      src/public/slack.min.js
  7. 41 18
      src/src/httpServ.js
  8. 4 1
      src/src/session.js
  9. 137 1
      src/src/slack.js
  10. 1 2
      src/src/slackManager.js

+ 1 - 0
.gitignore

@@ -1,6 +1,7 @@
 *.swp
 *.bak
 cli/closure-compiler-v*.jar
+/src/config.js
 
 # cache
 store.json

+ 2 - 3
cli/workflow.js

@@ -8,8 +8,7 @@ var
 
 function poll(callback) {
     var xhr = new XMLHttpRequest();
-    xhr.timeout = 18000; // 2 min timeout
-    xhr.timeout = 1800;
+    xhr.timeout = 1000 * 60 * 1; // 3 min timeout
     xhr.onreadystatechange = function(e) {
         if (xhr.readyState === 4) {
             if (xhr.status === 0) {
@@ -35,7 +34,7 @@ function poll(callback) {
             callback(success, resp);
         }
     };
-    xhr.open('GET', 'api?t=' +SLACK.lastServerVersion, true);
+    xhr.open('GET', 'api?v=' +SLACK.lastServerVersion, true);
     xhr.send(null);
 }
 

+ 1 - 0
package.json

@@ -1,6 +1,7 @@
 {
   "name": "knackigate-slack",
   "dependencies": [
+    "ws",
     "http",
     "client-sessions",
     "crypto",

+ 0 - 6
src/config.js

@@ -1,6 +0,0 @@
-
-module.exports = {
-    clientId: "secret client id"
-    ,rootUrl: "local url"
-    ,isDebug: false
-};

+ 7 - 0
src/config.js.exple

@@ -0,0 +1,7 @@
+
+module.exports = {
+    clientId: "slack_client_id"
+    ,clientSecret: "slack_client_secret"
+    ,rootUrl: "redirect_url"
+    ,isDebug: false
+};

+ 1 - 1
src/public/slack.min.js

@@ -1,2 +1,2 @@
-document.addEventListener("DOMContentLoaded",function(){startPolling()});var SLACK;function Slack(){this.lastServerVersion=0}Slack.prototype.update=function(b){console.log(b)};SLACK=new Slack;var NEXT_RETRY=5;function poll(b){var a=new XMLHttpRequest;a.timeout=18E3;a.timeout=1800;a.onreadystatechange=function(c){if(4===a.readyState)if(0===a.status)poll(b),NEXT_RETRY=5;else{c=null;var d=2===Math.floor(a.status/100);if(d){NEXT_RETRY=5;c=a.response;try{c=JSON.parse(c)}catch(e){c=null}}else NEXT_RETRY+=Math.floor(NEXT_RETRY/2),NEXT_RETRY=Math.min(60,NEXT_RETRY);b(d,c)}};a.open("GET","api?t="+SLACK.lastServerVersion,!0);a.send(null)}
+document.addEventListener("DOMContentLoaded",function(){startPolling()});var SLACK;function Slack(){this.lastServerVersion=0}Slack.prototype.update=function(b){console.log(b)};SLACK=new Slack;var NEXT_RETRY=5;function poll(b){var a=new XMLHttpRequest;a.timeout=6E4;a.onreadystatechange=function(c){if(4===a.readyState)if(0===a.status)poll(b),NEXT_RETRY=5;else{c=null;var d=2===Math.floor(a.status/100);if(d){NEXT_RETRY=5;c=a.response;try{c=JSON.parse(c)}catch(e){c=null}}else NEXT_RETRY+=Math.floor(NEXT_RETRY/2),NEXT_RETRY=Math.min(60,NEXT_RETRY);b(d,c)}};a.open("GET","api?v="+SLACK.lastServerVersion,!0);a.send(null)}
 function onPollResponse(b,a){b?(a&&SLACK.update(a),startPolling()):setTimeout(startPolling,1E3*NEXT_RETRY)}function startPolling(){poll(onPollResponse)};

+ 41 - 18
src/src/httpServ.js

@@ -1,10 +1,10 @@
 var http = require("http")
     ,clientSession = require("client-sessions")
-    ,sleep = require("sleep").sleep
 
     ,Url = require("./url.js").Url
     ,config = require("../config.js")
     ,sessionManager = require("./session.js").SessionManager
+    ,Slack = require("./slack.js").Slack
     ,slackManager = require("./slackManager.js").SlackManager;
 
 function Server(port) {
@@ -56,6 +56,16 @@ Server.prototype.onListen = function() {
     console.log("onListen");
 }
 
+function redirectToSlackAuth(res) {
+    res.writeHeader("302", {
+        Location: "https://slack.com/oauth/authorize"
+        +"?client_id=" +config.clientId
+        +"&scope=" +slackManager.getScope().join(",")
+        +"&redirect=" +config.rootUrl
+    });
+    res.end();
+}
+
 Server.prototype.onRequest = function(req, res) {
     req.reqT = Date.now();
     req.cookies = Server.parseCookies(req);
@@ -64,19 +74,22 @@ Server.prototype.onRequest = function(req, res) {
 
     if (!req.session || !req.session.slackToken) {
         if (req.urlObj.queryTokens.code) {
-        req.session = sessionManager.lazyForRequest(req);
-        req.session.setSlackToken(req.reqT, req.urlObj.queryTokens.code[0]);
-            res.writeHeader("302", {
-                Location: config.rootUrl
-            ,"Set-Cookie": "sessID="+req.session.sessId
+            Slack.getOauthToken(req.urlObj.queryTokens.code, (token) => {
+                if (token) {
+                    req.session = sessionManager.lazyForRequest(req);
+                    req.session.setSlackToken(req.reqT, token);
+                    res.writeHeader("302", {
+                        Location: config.rootUrl
+                    ,"Set-Cookie": "sessID="+req.session.sessId
+                    });
+                    sessionManager.saveSession(req.session);
+                    res.end();
+                } else {
+                    redirectToSlackAuth(res);
+                }
             });
         } else {
-            res.writeHeader("302", {
-                Location: "https://slack.com/oauth/authorize"
-                +"?client_id=" +config.clientId
-                +"&scope=" +slackManager.getScope().join(" ")
-                +"&redirect=" +config.rootUrl
-            });
+            redirectToSlackAuth(res);
         }
     } else if (req.urlObj.isPublic()) {
         if (!config.isDebug)
@@ -87,15 +100,25 @@ Server.prototype.onRequest = function(req, res) {
         sessionManager.saveSession(req.session);
         return; // async pipe will close when finished
     } else {
+        var apiSuccess = false;
+
         res.slack = slackManager.lazyGet(req.session);
-        //TODO stuff
-        res.writeHeader("200", {
-            "Content-Type": "application/json"
+        res.slack.onRequest(req.urlObj.queryTokens.v || 0, (slack, newData) => {
+            if (!slack.connected) {
+                res.writeHeader("403", {
+                    "Content-Type": "application/json"
+                });
+                res.write(slack.error);
+            } else {
+                res.writeHeader("200", {
+                    "Content-Type": "application/json"
+                });
+                res.write(JSON.stringify(newData));
+            }
+            sessionManager.saveSession(req.session);
+            res.end();
         });
-        res.write('"' +Math.floor(Date.now() /1000) +'"');
     }
-    sessionManager.saveSession(req.session);
-    res.end();
 }
 
 module.exports.HttpServ = Server;

+ 4 - 1
src/src/session.js

@@ -44,7 +44,10 @@ SessionManager.prototype.saveSession = function(session) {
 }
 
 Session.prototype.setSlackToken = function(reqT, slackToken) {
-    this.slackToken = slackToken;
+    this.slackUserId = slackToken["user_id"];
+    this.slackTeamName = slackToken["team_name"];
+    this.slackTeamId = slackToken["team_name"];
+    this.slackToken = slackToken["access_token"];
     this.modifiedAt = reqT;
 }
 

+ 137 - 1
src/src/slack.js

@@ -1,10 +1,146 @@
 
+const
+    WebSocket = require('ws'),
+    https = require('https'),
+    sleep = require("sleep").sleep
+
+    ,SlackData = require("./slackData.js").SlackData
+    ,config = require("../config.js")
+;
+
+const SLACK_ENDPOINT = "https://slack.com/api/"
+,GETAPI = {
+    rtmStart: "rtm.start"
+    ,oauth: "oauth.access"
+};
+
 function Slack(sess) {
-    console.log("creating new slack RTM for ", sess);
+    this.token = sess.slackToken;
+    this.rtm = null;
+    this.data = new SlackData();
+    this.connected = false;
+}
+
+Slack.prototype.onRequest = function(knownVersion, cb) {
+    this.lastCb = cb;
+    if (this.connected === false) {
+        this.connect(knownVersion);
+    } else {
+        this.waitForEvent(knownVersion);
+    }
+}
+
+function httpsRequest(url, cb) {
+    https.get(url, (res) => {
+        if (res.statusCode !== 200) {
+            cb(res.statusCode, null);
+            return;
+        }
+        var body = null;
+        res.on('data', (chunk) => {
+            body = body ? Buffer.concat([body, chunk], body.length +chunk.length) : Buffer.from(chunk);
+        });
+        res.on('end', () => {
+            try {
+                body = JSON.parse(body.toString("utf8"));
+            } catch (e) {}
+            cb(res.statusCode, body);
+        });
+    });
+}
+
+Slack.prototype.connect = function(knownVersion) {
+    var _this = this;
+
+    this.connected = undefined;
+    httpsRequest(SLACK_ENDPOINT +GETAPI.rtmStart +"?token=" +this.token, (status, body) => {
+        if (status !== 200) {
+            _this.error = body.error;
+            _this.connected = false;
+            console.error("Slack api responded " +status);
+            _this.lastCb(_this);
+            return;
+        }
+        if (!body) {
+            _this.error = "Slack API error";
+            _this.connected = false;
+            _this.lastCb(_this);
+            return;
+        }
+        if (!body.ok) {
+            _this.error = body.error;
+            _this.connected = false;
+            console.error("Slack api responded !ok with ", body);
+            _this.lastCb(_this);
+            return;
+        }
+        _this.data.updateStatic(body);
+        _this.connectRtm(body.url);
+        _this.waitForEvent(knownVersion);
+    });
+}
+
+Slack.prototype.waitForEvent = function(knownVersion) {
+    var tick = 0
+        ,_this = this
+        ,interval;
+
+    interval = setInterval(() => {
+        tick++;
+        var updated = this.data.getUpdates(knownVersion);
+        if (!_this.lastCb) {
+            clearInterval(interval);
+        }
+        if (updated.length > 0) {
+            clearInterval(interval);
+            _this.lastCb(_this, { v: knownVersion, u: updated });
+        } else if (tick === 55) { // < 1 minute timeout
+            clearInterval(interval);
+            _this.lastCb(_this, { v: knownVersion });
+        }
+    }, 1000);
+}
+
+Slack.prototype.connectRtm = function(url, cb) {
+    var _this = this;
+
+    this.rtm = new WebSocket(url);
+    this.rtm.on("message", function(msg) {
+        if (!_this.connected && cb) {
+            cb();
+        }
+        _this.connected = true;
+        msg = JSON.parse(msg);
+        console.log("RTM >>> ", msg);
+    });
+    this.rtm.once("error", function(e) {
+        _this.connected = false;
+        console.error(e);
+        _this.close();
+    });
+    this.rtm.once("end", function() {
+        _this.connected = false;
+        console.error("RTM hang up");
+        _this.close();
+    });
 }
 
 Slack.prototype.close = function() {
 }
 
+Slack.getOauthToken = function(code, cb) {
+    httpsRequest(SLACK_ENDPOINT+GETAPI.oauth
+        +"?client_id=" +config.clientId
+        +"&client_secret=" +config.clientSecret
+        +"&code=" +code,
+    (status, resp) => {
+        if (status === 200 && resp.ok) {
+            cb(resp);
+        } else {
+            cb(null);
+        }
+    });
+}
+
 module.exports.Slack = Slack;
 

+ 1 - 2
src/src/slackManager.js

@@ -9,8 +9,7 @@ var instance = new SlackManager();
 
 SlackManager.prototype.getScope = function() {
     return [
-        "channels:read"
-        ,"chat:write:user"
+        "client"
     ];
 }