Bläddra i källkod

[add] live history update
[add][cli] live update

B Thibault 8 år sedan
förälder
incheckning
ee2314b815
9 ändrade filer med 207 tillägg och 158 borttagningar
  1. 5 4
      Makefile
  2. 15 2
      cli/data.js
  3. 4 0
      cli/ui.js
  4. 17 0
      cli/workflow.js
  5. 7 7
      srv/public/slack.min.js
  6. 23 14
      srv/src/httpServ.js
  7. 64 9
      srv/src/slack.js
  8. 14 82
      srv/src/slackData.js
  9. 58 40
      srv/src/slackHistory.js

+ 5 - 4
Makefile

@@ -1,8 +1,9 @@
 
-SRC=		srv/src/slackData.js\
-			cli/resources.js	\
-			cli/ui.js			\
-			cli/data.js			\
+SRC=		srv/src/slackData.js	\
+			srv/src/slackHistory.js	\
+			cli/resources.js		\
+			cli/ui.js				\
+			cli/data.js				\
 			cli/workflow.js
 
 OUTPUT=		srv/public/slack.min.js

+ 15 - 2
cli/data.js

@@ -3,7 +3,8 @@ var
     /**
      * @type SlackWrapper
     **/
-    SLACK;
+    SLACK
+;
 
 /**
  * @constructor
@@ -14,6 +15,9 @@ function SlackWrapper() {
 
     /** @type {SlackData} */
     this.context = new SlackData(null);
+
+    /** @type {!Object.<string, SlackHistory>} **/
+    this.history = {};
 }
 
 SlackWrapper.prototype.update = function(data) {
@@ -24,7 +28,16 @@ SlackWrapper.prototype.update = function(data) {
         onContextUpdated();
     }
     if (data["live"]) {
-        console.log("updated LIVE");
+        for (var i in data["live"]) {
+            var history = this.history[i];
+            if (!history)
+                this.history[i] = new SlackHistory(i, data["live"][i]);
+            else
+                history.pushAll(data["live"][i]);
+        }
+        if (SELECTED_ROOM && data["live"][SELECTED_ROOM.id]) {
+            onRoomUpdated();
+        }
     }
     console.log(this);
 };

+ 4 - 0
cli/ui.js

@@ -68,6 +68,10 @@ function onRoomSelected() {
     document.getElementById(R.id.currentRoom.title).textContent = name;
 }
 
+function onRoomUpdated() {
+    console.log("Updated room", SLACK.history[SELECTED_ROOM.id]);
+}
+
 function onChanClick(e) {
     while (e.target !== e.currentTarget && e.target) {
         if (e.target.classList.contains(R.klass.chatList.entry)) {

+ 17 - 0
cli/workflow.js

@@ -11,6 +11,16 @@ var
     ,SELECTED_ROOM = null
 ;
 
+/**
+ * @param {SlackChan|SlackIms|SlackGroup} room
+ * @param {function(boolean)} cb
+**/
+function fetchHistory(room, cb) {
+    var xhr = new XMLHttpRequest();
+    xhr.open('GET', 'api?room=' +room.id, true);
+    xhr.send(null);
+}
+
 function poll(callback) {
     var xhr = new XMLHttpRequest();
     xhr.timeout = 1000 * 60 * 1; // 3 min timeout
@@ -72,6 +82,13 @@ function selectRoom(room) {
     document.body.classList.remove(R.klass.noRoomSelected);
     SELECTED_ROOM = room;
     onRoomSelected();
+    if (SELECTED_ROOM.lastRead && !SLACK.history[SELECTED_ROOM.id]) {
+        fetchHistory(SELECTED_ROOM, function(success) {
+            if (!success) {
+                // TODO handle error
+            }
+        });
+    }
 }
 
 function unselectRoom() {

+ 7 - 7
srv/public/slack.min.js

@@ -1,7 +1,7 @@
-function e(b,a){this.id=b.id;this.name=b.name;this.a={};if(b.members)for(var d=0,c=b.members.length;d<c;d++){var f=h(a,b.members[d]);this.a[f.id]=f;f.c[this.id]=this}}function k(b,a){var d=[];this.id=a.id;this.a={};for(var c=0,f=a.members.length;c<f;c++){var g=h(b,a.members[c]);this.a[a.members[c]]=g;g.c[this.id]=this;d.push(g.name)}this.name=d.join(", ")}function l(b,a){this.id=a.id;this.b=b}function m(b){this.id=b.id;this.name=b.name;this.status=b.status;this.c={};this.a=null}
-function n(b){this.id=b.id;this.name=b.name;this.c={};this.a=null}function p(){this.c={};this.b={};this.g={};this.a={};this.f=null;this.h={}}function h(b,a){return b.a[a]||b.h[a]||null}"undefined"!==typeof module&&(module.j.i=p);function q(){var b=document.createDocumentFragment(),a=r.a.f?Object.keys(r.a.f.c):[];a.sort(function(b,c){return b[0]!==c[0]?b[0]-c[0]:(r.a.c[b]||r.a.b[b]).name.localeCompare((r.a.c[c]||r.a.b[c]).name)});a.forEach(function(a){a=r.a.c[a]||r.a.b[a];var c=document.createElement("li");c.id=a.id;c.className="slack-context-room";c.textContent=a.name;c&&b.appendChild(c)});a=r.a.a?Object.keys(r.a.a):[];a.sort(function(b,a){return r.a.a[b].name.localeCompare(r.a.a[a].name)});a.forEach(function(a){a=r.a.a[a].a;
-var c=document.createElement("li");c.id=a.id;c.className="slack-context-room";c.textContent=a.b.name;c&&b.appendChild(c)});document.getElementById("chanList").textContent="";document.getElementById("chanList").appendChild(b)}
-function t(b){for(;b.target!==b.currentTarget&&b.target;){if(b.target.classList.contains("slack-context-room")){if((b=r.a.c[b.target.id]||r.a.g[b.target.id]||r.a.b[b.target.id])&&b!==u){u&&document.getElementById(u.id).classList.remove("selected");document.getElementById(b.id).classList.add("selected");document.body.classList.remove("no-room-selected");u=b;b=void 0;var a=u.name||(u.b?u.b.name:void 0);if(!a){a=[];for(b in u.a)a.push(u.a[b].name);a=a.join(", ")}document.getElementById("currentRoomTitle").textContent=
-a}break}b.target=b.target.parentElement}}document.addEventListener("DOMContentLoaded",function(){document.getElementById("chatList").addEventListener("click",t);v()});var r;function w(){this.b=0;this.a=new p}
-w.prototype.update=function(b){b.v&&(this.b=b.v);if(b["static"]){for(var a=this.a,d=b["static"],c=0,f=d.bots.length;c<f;c++)a.h[d.bots[c].id]=new n(d.bots[c]);c=0;for(f=d.users.length;c<f;c++)a.a[d.users[c].id]=new m(d.users[c]);c=0;for(f=d.ims.length;c<f;c++){var g=h(a,d.ims[c].user);g&&(g.a=new l(g,d.ims[c]),a.g[g.a.id]=g.a)}c=0;for(f=d.channels.length;c<f;c++)a.c[d.channels[c].id]=new e(d.channels[c],a);c=0;for(f=d.groups.length;c<f;c++)a.b[d.groups[c].id]=new k(a,d.groups[c]);a.f=h(a,d.self.id);
-q()}b.live&&console.log("updated LIVE");console.log(this)};r=new w;var x=5,u=null;function y(b){var a=new XMLHttpRequest;a.timeout=6E4;a.onreadystatechange=function(){if(4===a.readyState)if(a.status){var d=null,c=2===Math.floor(a.status/100);if(c){x=5;d=a.response;try{d=JSON.parse(d)}catch(f){d=null}}else x+=Math.floor(x/2),x=Math.min(60,x);b(c,d)}else y(b),x=5};a.open("GET","api?v="+r.b,!0);a.send(null)}function z(b,a){b?(a&&r.update(a),v()):setTimeout(v,1E3*x)}function v(){y(z)};
+function f(b,a){this.id=b.id;this.name=b.name;this.b=parseFloat(b.last_read);this.a={};if(b.members)for(var g=0,c=b.members.length;g<c;g++){var e=h(a,b.members[g]);this.a[e.id]=e;e.f[this.id]=this}}function l(b,a){var g=[];this.id=a.id;this.a={};for(var c=0,e=a.members.length;c<e;c++){var d=h(b,a.members[c]);this.a[a.members[c]]=d;d.f[this.id]=this;g.push(d.name)}this.name=g.join(", ");this.b=parseFloat(a.last_read)}function p(b,a){this.id=a.id;this.c=b;this.b=parseFloat(a.last_read)}
+function q(b){this.id=b.id;this.name=b.name;this.status=b.status;this.f={};this.a=null}function r(b){this.id=b.id;this.name=b.name;this.f={};this.a=null}function t(){this.f={};this.b={};this.g={};this.a={};this.c=null;this.h={}}function h(b,a){return b.a[a]||b.h[a]||null}"undefined"!==typeof module&&(module.i.j=t);function u(){}function v(b,a){this.id="string"===typeof b?b:b.id;this.a=[];a&&w(this,a)}function w(b,a){for(a.forEach(function(){this.a.push(new u)}.bind(b));500<b.a.length;)b.a.shift()}v.prototype.push=function(){for(this.a.push(new u);500<this.a.length;)this.a.shift()};"undefined"!==typeof module&&(module.i.l=v);function x(){var b=document.createDocumentFragment(),a=y.a.c?Object.keys(y.a.c.f):[];a.sort(function(b,a){return b[0]!==a[0]?b[0]-a[0]:(y.a.f[b]||y.a.b[b]).name.localeCompare((y.a.f[a]||y.a.b[a]).name)});a.forEach(function(a){a=y.a.f[a]||y.a.b[a];var c=document.createElement("li");c.id=a.id;c.className="slack-context-room";c.textContent=a.name;c&&b.appendChild(c)});a=y.a.a?Object.keys(y.a.a):[];a.sort(function(b,a){return y.a.a[b].name.localeCompare(y.a.a[a].name)});a.forEach(function(a){a=y.a.a[a].a;
+var c=document.createElement("li");c.id=a.id;c.className="slack-context-room";c.textContent=a.c.name;c&&b.appendChild(c)});document.getElementById("chanList").textContent="";document.getElementById("chanList").appendChild(b)}
+function z(b){for(;b.target!==b.currentTarget&&b.target;){if(b.target.classList.contains("slack-context-room")){if((b=y.a.f[b.target.id]||y.a.g[b.target.id]||y.a.b[b.target.id])&&b!==A){A&&document.getElementById(A.id).classList.remove("selected");document.getElementById(b.id).classList.add("selected");document.body.classList.remove("no-room-selected");A=b;b=void 0;var a=A.name||(A.c?A.c.name:void 0);if(!a){a=[];for(b in A.a)a.push(A.a[b].name);a=a.join(", ")}document.getElementById("currentRoomTitle").textContent=
+a;A.b&&!y.b[A.id]&&(b=new XMLHttpRequest,b.open("GET","api?room="+A.id,!0),b.send(null))}break}b.target=b.target.parentElement}}document.addEventListener("DOMContentLoaded",function(){document.getElementById("chatList").addEventListener("click",z);B()});var y;y=new function(){this.c=0;this.a=new t;this.b={}};var C=5,A=null;function D(b){var a=new XMLHttpRequest;a.timeout=6E4;a.onreadystatechange=function(){if(4===a.readyState)if(a.status){var g=null,c=2===Math.floor(a.status/100);if(c){C=5;g=a.response;try{g=JSON.parse(g)}catch(e){g=null}}else C+=Math.floor(C/2),C=Math.min(60,C);b(c,g)}else D(b),C=5};a.open("GET","api?v="+y.c,!0);a.send(null)}
+function E(b,a){if(b){if(a){var g=y;a.v&&(g.c=a.v);if(a["static"]){for(var c=g.a,e=a["static"],d=0,k=e.bots.length;d<k;d++)c.h[e.bots[d].id]=new r(e.bots[d]);d=0;for(k=e.users.length;d<k;d++)c.a[e.users[d].id]=new q(e.users[d]);d=0;for(k=e.ims.length;d<k;d++){var m=h(c,e.ims[d].user);m&&(m.a=new p(m,e.ims[d]),c.g[m.a.id]=m.a)}d=0;for(k=e.channels.length;d<k;d++)c.f[e.channels[d].id]=new f(e.channels[d],c);d=0;for(k=e.groups.length;d<k;d++)c.b[e.groups[d].id]=new l(c,e.groups[d]);c.c=h(c,e.self.id);
+x()}if(a.live){for(var n in a.live)(c=g.b[n])?w(c,a.live[n]):g.b[n]=new v(n,a.live[n]);A&&a.live[A.id]&&console.log("Updated room",y.b[A.id])}console.log(g)}B()}else setTimeout(B,1E3*C)}function B(){D(E)};

+ 23 - 14
srv/src/httpServ.js

@@ -103,22 +103,31 @@ Server.prototype.onRequest = function(req, res) {
         var apiSuccess = false;
 
         res.slack = slackManager.lazyGet(req.session);
-        res.slack.onRequest(req.urlObj.queryTokens.v || 0, (slack, newData) => {
-            console.log("write handler");
-            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));
-            }
+        if (req.urlObj.queryTokens.room) {
+            req.urlObj.queryTokens.room.forEach(function(targetId) {
+                res.slack.fetchHistory(targetId);
+            });
             sessionManager.saveSession(req.session);
+            res.writeHeader("204", "No Content");
             res.end();
-        });
+        } else {
+            res.slack.onRequest(req.urlObj.queryTokens.v || 0, (slack, newData) => {
+                console.log("write handler");
+                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();
+            });
+        }
     }
 };
 

+ 64 - 9
srv/src/slack.js

@@ -5,6 +5,7 @@ const
     sleep = require("sleep").sleep
 
     ,SlackData = require("./slackData.js").SlackData
+    ,SlackHistory = require("./slackHistory.js").SlackHistory
     ,config = require("../config.js")
 ;
 
@@ -12,13 +13,16 @@ const SLACK_ENDPOINT = "https://slack.com/api/"
 ,GETAPI = {
     rtmStart: "rtm.start"
     ,oauth: "oauth.access"
-    ,history: "channels.history"
+    ,channelHistory: "channels.history"
+    ,directHistory: "im.history"
+    ,groupHistory: "groups.history"
 };
 
 function Slack(sess) {
     this.token = sess.slackToken;
     this.rtm = null;
     this.data = new SlackData(this);
+    this.history = {};
     this.connected = false;
 }
 
@@ -93,9 +97,16 @@ Slack.prototype.waitForEvent = function(knownVersion) {
             return;
         }
         if (_this.connected) {
-            var updated = _this.data.getUpdates(knownVersion);
-            if (updated["static"] || updated["live"]) {
-                updated["v"] = Math.max(_this.data.liveV, _this.data.staticV);
+            var updatedCtx = _this.data.getUpdates(knownVersion)
+                ,updatedLive = _this.getLiveUpdates(knownVersion);
+
+            if (updatedCtx || updatedLive) {
+                var updated = {
+                    "static": updatedCtx
+                    ,"live": updatedLive
+                    ,"v": Math.max(_this.data.liveV, _this.data.staticV)
+                    // TODO "typing" stream
+                };
                 clearInterval(interval);
                 _this.lastCb(_this, updated);
                 return;
@@ -109,6 +120,33 @@ Slack.prototype.waitForEvent = function(knownVersion) {
     }, 1000);
 };
 
+/** @return {Object|undefined} */
+Slack.prototype.getLiveUpdates = function(knownVersion) {
+    var result = {};
+
+    for (var roomId in this.history) {
+        var history = this.history[roomId];
+        if (history.isNew) {
+            result[roomId] = history.toStatic(0);
+            history.isNew = false;
+        }
+        else {
+            var roomData = history.toStatic(knownVersion);
+            if (roomData.length)
+                result[roomId] = roomData;
+        }
+    }
+    for (var roomId in result) {
+        return result;
+    }
+    return undefined;
+};
+
+Slack.prototype.onMessage = function(msg) {
+    //TODO handle live
+    this.data.onMessage(msg);
+};
+
 Slack.prototype.connectRtm = function(url, cb) {
     var _this = this;
 
@@ -118,8 +156,7 @@ Slack.prototype.connectRtm = function(url, cb) {
             cb();
         }
         _this.connected = true;
-        msg = JSON.parse(msg);
-        console.log("RTM >>> ", msg);
+        _this.onMessage(JSON.parse(msg));
     });
     this.rtm.once("error", function(e) {
         _this.connected = false;
@@ -150,13 +187,31 @@ Slack.getOauthToken = function(code, cb) {
     });
 };
 
-Slack.prototype.fetchHistory = function(targetId, cb) {
-    httpsRequest(SLACK_ENDPOINT +GETAPI.history
+Slack.prototype.fetchHistory = function(targetId) {
+    var _this = this
+        ,baseUrl = "";
+
+    if (targetId[0] === 'D') {
+        baseUrl = SLACK_ENDPOINT +GETAPI.directHistory;
+    } else if (targetId[0] === 'C') {
+        baseUrl = SLACK_ENDPOINT +GETAPI.channelHistory;
+    } else if (targetId[0] === 'G') {
+        baseUrl = SLACK_ENDPOINT +GETAPI.groupHistory;
+    }
+    httpsRequest(baseUrl
         +"?token="+this.token
         +"&channel=" +targetId
         +"&count=25",
     (status, resp) => {
-        cb(targetId, status === 200 && resp && resp.ok ? resp.messages : null);
+        if (status === 200 && resp && resp.ok) {
+            var history = _this.history[targetId];
+            if (history)
+                history.pushAll(resp.messages);
+            else {
+                history = _this.history[targetId] = new SlackHistory(targetId, resp.messages);
+                history.isNew = true;
+            }
+        }
     });
 };
 

+ 14 - 82
srv/src/slackData.js

@@ -49,7 +49,7 @@ function SlackChan(chanData, slackData) {
     /** @type {boolean} */
     this.isMember = chanData["is_member"];
     /** @type {number} */
-    this.lastRead = chanData["last_read"];
+    this.lastRead = parseFloat(chanData["last_read"]);
     /** @type {Object.<string, SlackBot|SlackUser>} */
     this.members = {};
     if (chanData["members"]) for (var i =0, nbMembers = chanData["members"].length; i < nbMembers; i++) {
@@ -108,7 +108,7 @@ function SlackGroup(slack, groupData) {
     /** @type {boolean} */
     this.archived = groupData["is_archived"];
     /** @type {number} */
-    this.lastRead = groupData["last_read"];
+    this.lastRead = parseFloat(groupData["last_read"]);
     /** @type {string|undefined} */
     this.topic;
     /** @type {number|undefined} */
@@ -146,7 +146,7 @@ function SlackIms(user, imsData) {
     /** @type {SlackUser|SlackBot} */
     this.user = user;
     /** @type {number} */
-    this.lastRead = imsData["last_read"];
+    this.lastRead = parseFloat(imsData["last_read"]);
 }
 
 /**
@@ -210,16 +210,6 @@ function SlackBot(botData) {
     this.ims = null;
 }
 
-/**
- * @constructor
-**/
-function SlackHistory(target) {
-    this.id = target.id;
-    this.target = target;
-    this.v = 0;
-    this.messages = [];
-}
-
 /**
  * @constructor
 **/
@@ -238,8 +228,6 @@ function SlackData(slack) {
     this.self = null;
     /** @type {Object.<string, SlackBot>} */
     this.bots = {};
-    // channel/ims id -> array of events
-    this.history = {};
 
     /**
      * Node serv handler
@@ -272,14 +260,6 @@ SlackTeam.prototype.toStatic = function() {
     };
 };
 
-SlackHistory.prototype.update = function(messages) {
-    // TODO
-};
-
-SlackHistory.prototype.getUpdates = function(knownVersion) {
-    // TODO
-};
-
 /**
  * @return {Object}
 **/
@@ -412,42 +392,6 @@ SlackData.prototype.updateStatic = function(data) {
     if (!this.slack) {
         return;
     }
-    /*
-    var fetchHistory = {}
-        ,doFetch = false;
-    for (var i in this.channels)
-        if (!this.history[i]) {
-            doFetch = true;
-            fetchHistory[i] = this.channels[i];
-        }
-    for (var i in this.users)
-        if (!this.history[this.users[i].ims.id]) {
-            doFetch = true;
-            fetchHistory[i] = this.users[i].ims.id;
-        }
-    //TODO fetch group history
-    if (!doFetch)
-        callback();
-    else for (var i in fetchHistory) {
-        var _this = this;
-        this.slack.fetchHistory(i, function(currentUpdated, data) {
-            if (data != null) {
-                _this.setHistory(fetchHistory[currentUpdated], data);
-                fetchHistory[currentUpdated] = null;
-                for (var i in fetchHistory) {
-                    if (fetchHistory[i])
-                        return;
-                }
-            }
-        });
-    }
-    */
-};
-
-SlackData.prototype.setHistory = function(target, data) {
-    if (!this.history[target.id])
-        this.history[target.id] = new SlackHistory(target);
-    this.history[target.id].update(data);
 };
 
 /**
@@ -473,19 +417,6 @@ SlackData.prototype.getMember = function(mId) {
     return this.users[mId] || this.bots[mId] || null;
 };
 
-/**
- * @param {number} knownVersion
- * @return {Object}
-**/
-SlackData.prototype.buildLive = function(knownVersion) {
-    var res = {};
-    //TODO include presence/typing stream
-    for (var i in this.history)
-        if (this.history[i].v > knownVersion)
-            res[i] = this.history[i].getUpdates(knownVersion);
-    return res;
-};
-
 /** @return {Object} */
 SlackData.prototype.buildStatic = function() {
     var res = {
@@ -515,20 +446,21 @@ SlackData.prototype.buildStatic = function() {
     return res;
 };
 
+/**
+ * @param {Object} msg
+ * @return {boolean}
+**/
+SlackData.prototype.onMessage = function(msg) {
+    //TODO
+    return false;
+};
+
 /**
  * @param {number} knownVersion
- * @return {{live: (Object|undefined), static: (Object|undefined)}}
+ * @return {Object|undefined}
 **/
 SlackData.prototype.getUpdates = function(knownVersion) {
-    var res = {};
-
-    if (this.liveV > knownVersion) {
-        res["live"] = this.buildLive(knownVersion);
-    }
-    if (this.staticV > knownVersion) {
-        res["static"] = this.buildStatic();
-    }
-    return res;
+    return (this.staticV > knownVersion) ? this.buildStatic() : undefined;
 };
 
 /** @suppress {undefinedVars,checkTypes} */

+ 58 - 40
srv/src/slackHistory.js

@@ -2,50 +2,66 @@
 /**
  * @constructor
 **/
-function SlackHistory(target) {
-    this.id = target.id;
-    this.target = target;
-    this.v = 0;
+function SlackMessage(e) {
+    /** @type {string} **/
+    this.userId = e["user"];
+
+    /** @type {string} **/
+    this.ts = e["ts"];
+
+    /** @type {string} **/
+    this.message = e["text"];
+}
+
+/**
+ * @constructor
+ * @param {SlackChan|SlackGroup|SlackIms|string} room or roomId
+ * @param {Array|undefined} evts
+**/
+function SlackHistory(room, evts) {
+    /** @type {string} */
+    this.id = typeof room === "string" ? room : room.id;
+    /** @type {Array.<SlackMessage>} */
     this.messages = [];
+    /** @type number */
+    this.v = 0;
+
+    if (evts) this.pushAll(evts);
 }
 
-SlackData.prototype.setHistory = function(target, data) {
-    if (!this.history[target.id])
-        this.history[target.id] = new SlackHistory(target);
-    this.history[target.id].update(data);
-};
+SlackMessage.prototype.toStatic = function() {
+    return {
+        "ts": this.ts
+        ,"user": this.userId
+        ,"text": this.message
+    };
+}
+
+/**
+ * @param {Array} evts
+**/
+SlackHistory.prototype.pushAll = function(evts) {
+    evts.forEach(function(e) {
+        this.messages.push(new SlackMessage(e));
+    }.bind(this));
+    while (this.messages.length > 500)
+        this.messages.shift();
+}
+
+SlackHistory.prototype.push = function(ev) {
+    this.messages.push(new SlackMessage(ev));
+    while (this.messages.length > 500)
+        this.messages.shift();
+}
 
+SlackHistory.prototype.toStatic = function(knownVersion) {
+    var result = [];
 
-    /*
-    var fetchHistory = {}
-        ,doFetch = false;
-    for (var i in this.channels)
-        if (!this.history[i]) {
-            doFetch = true;
-            fetchHistory[i] = this.channels[i];
-        }
-    for (var i in this.users)
-        if (!this.history[this.users[i].ims.id]) {
-            doFetch = true;
-            fetchHistory[i] = this.users[i].ims.id;
-        }
-    //TODO fetch group history
-    if (!doFetch)
-        callback();
-    else for (var i in fetchHistory) {
-        var _this = this;
-        this.slack.fetchHistory(i, function(currentUpdated, data) {
-            if (data != null) {
-                _this.setHistory(fetchHistory[currentUpdated], data);
-                fetchHistory[currentUpdated] = null;
-                for (var i in fetchHistory) {
-                    if (fetchHistory[i])
-                        return;
-                }
-            }
-        });
+    for (var i = this.messages.length -1; i >= 0 && this.messages[i].ts > knownVersion; i--) {
+        result.push(this.messages[i].toStatic());
     }
-    */
+    return result;
+}
 
 SlackHistory.prototype.update = function(messages) {
     // TODO
@@ -55,6 +71,8 @@ SlackHistory.prototype.getUpdates = function(knownVersion) {
     // TODO
 };
 
-
-module.exports.SlackHistory = SlackHistory;
+/** @suppress {undefinedVars,checkTypes} */
+(function() {
+    if (typeof module !== "undefined") module.exports.SlackHistory = SlackHistory;
+})();