Browse Source

[quickfix] compile with minified option
[add] chat group
[add] sort chans by type (public > groups > direct) then by name
[add] JSdoc
[bugfix] can IMS with bots

B Thibault 8 years ago
parent
commit
2bb0aa33cd
4 changed files with 181 additions and 41 deletions
  1. 1 1
      Makefile
  2. 17 6
      cli/ui.js
  3. 7 23
      srv/public/slack.min.js
  4. 156 11
      srv/src/slackData.js

+ 1 - 1
Makefile

@@ -10,7 +10,7 @@ OUTPUT=		srv/public/slack.min.js
 CLOSURE=	cli/closure-compiler-v20170218.jar
 
 all:
-	java -jar ${CLOSURE} --compilation_level SIMPLE --language_in=ECMASCRIPT5_STRICT --warning_level=VERBOSE --js_output_file ${OUTPUT} ${SRC}
+	java -jar ${CLOSURE} --compilation_level ADVANCED --language_in=ECMASCRIPT5_STRICT --warning_level=VERBOSE --js_output_file ${OUTPUT} ${SRC}
 
 debug:
 	java -jar ${CLOSURE} --compilation_level WHITESPACE_ONLY --language_in=ECMASCRIPT5_STRICT --js_output_file ${OUTPUT} ${SRC}

+ 17 - 6
cli/ui.js

@@ -1,6 +1,6 @@
 
 /**
- * @param {SlackChan} chan
+ * @param {SlackChan|SlackGroup} chan
  * @return {Element}
 **/
 function createChanListItem(chan) {
@@ -26,21 +26,32 @@ function createImsListItem(ims) {
 function onContextUpdated() {
     var chanListFram = document.createDocumentFragment();
 
-    for (var chanId in SLACK.context.self.channels) {
-        var chan = SLACK.context.channels[chanId]
+    var sortedChans = SLACK.context.self ? Object.keys(SLACK.context.self.channels) : [];
+    sortedChans.sort(function(a, b) {
+        if (a[0] !== b[0]) {
+            return a[0] - b[0];
+        }
+        return (SLACK.context.channels[a] || SLACK.context.groups[a]).name.localeCompare((SLACK.context.channels[b] || SLACK.context.groups[b]).name);
+    });
+    sortedChans.forEach(function(chanId) {
+        var chan = SLACK.context.channels[chanId] || SLACK.context.groups[chanId]
             ,chanListItem = createChanListItem(chan);
         if (chanListItem) {
             chanListFram.appendChild(chanListItem);
         }
-    }
-    for (var userId in SLACK.context.users) {
+    });
+    var sortedUsers = SLACK.context.users ? Object.keys(SLACK.context.users) : [];
+    sortedUsers.sort(function(a, b) {
+        return SLACK.context.users[a].name.localeCompare(SLACK.context.users[b].name);
+    });
+    sortedUsers.forEach(function(userId) {
         var ims = SLACK.context.users[userId].ims
             ,imsListItem = createImsListItem(ims);
 
         if (imsListItem) {
             chanListFram.appendChild(imsListItem);
         }
-    }
+    });
     document.getElementById(R.id.chanList).textContent = "";
     document.getElementById(R.id.chanList).appendChild(chanListFram);
 }

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

@@ -1,23 +1,7 @@
-function SlackTeam(teamData){this.id=teamData["id"];this.name=teamData["name"];this.domain=teamData["domain"];this.callApp=teamData["prefs"]["calling_app_id"];this.callAppName=teamData["prefs"]["calling_app_name"];this.fileUploadPermission=teamData["prefs"]["disable_file_uploads"];this.fileEditPermission=teamData["prefs"]["disable_file_editing"];this.fileDeletePermission=teamData["prefs"]["disable_file_deleting"];this.icons={image_34:teamData["icon"]["image_34"],image_44:teamData["icon"]["image_44"],
-image_68:teamData["icon"]["image_68"],image_88:teamData["icon"]["image_88"],image_102:teamData["icon"]["image_102"],image_132:teamData["icon"]["image_132"],image_230:teamData["icon"]["image_230"],image_default:teamData["icon"]["image_default"]}}
-function SlackChan(chanData,slackData){this.id=chanData["id"];this.name=chanData["name"];this.created=chanData["created"];this.creator=slackData.getMember(chanData["creator"]);this.archived=chanData["is_archived"];this.isMember=chanData["is_member"];this.lastRead=chanData["last_read"];this.members={};if(chanData["members"])for(var i=0,nbMembers=chanData["members"].length;i<nbMembers;i++){var member=slackData.getMember(chanData["members"][i]);this.members[member.id]=member;member.channels[this.id]=
-this}if(chanData["topic"]){this.topic=chanData["topic"]["value"];this.topicCreator=slackData.getMember(chanData["topic"]["creator"]);this.topicTs=chanData["topic"]["last_set"]}if(chanData["purpose"]){this.purpose=chanData["purpose"]["value"];this.purposeCreator=slackData.getMember(chanData["purpose"]["creator"]);this.purposeTs=chanData["purpose"]["last_set"]}}function SlackGroup(groupData){}
-function SlackIms(user,imsData){this.id=imsData["id"];this.created=imsData["created"];this.user=user;this.lastRead=imsData["last_read"]}
-function SlackUser(userData){this.id=userData["id"];this.name=userData["name"];this.deleted=userData["deleted"];this.status=userData["status"];this.realName=userData["real_name"]||userData["profile"]["real_name"];this.presence=userData["presence"]!=="away";this.icons={image_24:userData["profile"]["image_24"],image_32:userData["profile"]["image_32"],image_48:userData["profile"]["image_48"],image_72:userData["profile"]["image_72"],image_192:userData["profile"]["image_192"],image_512:userData["profile"]["image_512"]};
-this.email=userData["profile"]["email"];this.firstName=userData["profile"]["first_name"];this.lastName=userData["profile"]["last_name"];this.channels={};this.ims=null}function SlackBot(botData){this.id=botData["id"];this.deleted=botData["deleted"];this.name=botData["name"];this.appId=botData["app_id"];this.icons={image_36:botData["icons"]["image_36"],image_48:botData["icons"]["image_48"],image_72:botData["icons"]["image_72"]};this.channels={}}
-function SlackHistory(target){this.id=target.id;this.target=target;this.v=0;this.messages=[]}function SlackData(slack){this.team=null;this.channels={};this.groups=[];this.ims={};this.users={};this.self=null;this.bots={};this.history={};this.slack=slack;this.staticV=0;this.liveV=0}
-SlackTeam.prototype.toStatic=function(){return{"id":this.id,"name":this.name,"domain":this.domain,"prefs":{"calling_app_id":this.callApp,"calling_app_name":this.callAppName,"disable_file_uploads":this.fileUploadPermission,"disable_file_editing":this.fileEditPermission,"disable_file_deleting":this.fileDeletePermission},"icon":this.icons}};SlackHistory.prototype.update=function(messages){};SlackHistory.prototype.getUpdates=function(knownVersion){};
-SlackChan.prototype.toStatic=function(){var res={"id":this.id,"name":this.name,"created":this.created,"creator":this.creator.id,"is_archived":this.archived,"is_member":this.isMember,"last_read":this.lastRead};if(this.isMember){res["members"]=Object.keys(this.members);res["topic"]={"value":this.topic,"creator":this.topicCreator?this.topicCreator.id:null,"last_set":this.topicTs};res["purpose"]={"value":this.purpose,"creator":this.purposeCreator?this.purposeCreator.id:null,"last_set":this.purposeTs}}return res};
-SlackUser.prototype.toStatic=function(){return{"id":this.id,"name":this.name,"deleted":this.deleted,"status":this.status,"real_name":this.realName,"presence":this.presence?"present":"away","profile":{"email":this.email,"first_name":this.firstName,"last_name":this.lastName,"image_24":this.icons.image_24,"image_32":this.icons.image_32,"image_48":this.icons.image_48,"image_72":this.icons.image_72,"image_192":this.icons.image_192,"image_512":this.icons.image_512}}};
-SlackIms.prototype.toStatic=function(){return{"id":this.id,"created":this.created,"user":this.user.id,"last_read":this.lastRead}};SlackBot.prototype.toStatic=function(){return{"id":this.id,"deleted":this.deleted,"name":this.name,"app_id":this.appId,"icons":{"image_36":this.icons.image_36,"image_48":this.icons.image_48,"image_72":this.icons.image_72}}};
-SlackData.prototype.updateStatic=function(data){for(var i=0,nbBots=data["bots"].length;i<nbBots;i++)this.bots[data.bots[i].id]=new SlackBot(data["bots"][i]);for(var i=0,nbUsers=data["users"].length;i<nbUsers;i++)this.users[data["users"][i].id]=new SlackUser(data["users"][i]);for(var i=0,nbIms=data["ims"].length;i<nbIms;i++){var user=this.getMember(data["ims"][i]["user"]);user.ims=new SlackIms(user,data["ims"][i]);this.ims[user.ims.id]=user.ims}for(var i=0,nbChan=data["channels"].length;i<nbChan;i++)this.channels[data["channels"][i].id]=
-new SlackChan(data["channels"][i],this);this.team=new SlackTeam(data["team"]);this.staticV=parseFloat(data["latest_event_ts"]);this.self=this.getMember(data["self"]["id"]);if(!this.slack)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)};
-SlackData.prototype.getBotsByAppId=function(appId){var bots=[];for(var botId in this.bots)if(this.bots[botId].appId===appId)bots.push(this.bots[botId]);return bots};SlackData.prototype.getMember=function(mId){return this.users[mId]||this.bots[mId]||null};SlackData.prototype.buildLive=function(knownVersion){var res={};for(var i in this.history)if(this.history[i].v>knownVersion)res[i]=this.history[i].getUpdates(knownVersion);return res};
-SlackData.prototype.buildStatic=function(){var res={"team":this.team.toStatic(),"channels":[],"groups":[],"ims":[],"users":[],"bots":[],"self":{"id":this.self.id}};for(var chanId in this.channels)res["channels"].push(this.channels[chanId].toStatic());for(var userId in this.users){res["users"].push(this.users[userId].toStatic());res["ims"].push(this.users[userId].ims.toStatic())}for(var botId in this.bots)res["bots"].push(this.bots[botId].toStatic());return res};
-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};(function(){if(typeof module!=="undefined")module.exports.SlackData=SlackData})();var R={id:{chanList:"chanList",chatList:"chatList",currentRoom:{title:"currentRoomTitle"}},klass:{selected:"selected",noRoomSelected:"no-room-selected",chatList:{entry:"slack-context-room"}}};function createChanListItem(chan){var dom=document.createElement("li");dom.id=chan.id;dom.className=R.klass.chatList.entry;dom.textContent=chan.name;return dom}function createImsListItem(ims){var dom=document.createElement("li");dom.id=ims.id;dom.className=R.klass.chatList.entry;dom.textContent=ims.user.name;return dom}
-function onContextUpdated(){var chanListFram=document.createDocumentFragment();for(var chanId in SLACK.context.self.channels){var chan=SLACK.context.channels[chanId],chanListItem=createChanListItem(chan);if(chanListItem)chanListFram.appendChild(chanListItem)}for(var userId in SLACK.context.users){var ims=SLACK.context.users[userId].ims,imsListItem=createImsListItem(ims);if(imsListItem)chanListFram.appendChild(imsListItem)}document.getElementById(R.id.chanList).textContent="";document.getElementById(R.id.chanList).appendChild(chanListFram)}
-function onRoomSelected(){var name=SELECTED_ROOM.name||(SELECTED_ROOM.user?SELECTED_ROOM.user.name:undefined);if(!name){var members=[];for(var i in SELECTED_ROOM.members)members.push(SELECTED_ROOM.members[i].name);name=members.join(", ")}document.getElementById(R.id.currentRoom.title).textContent=name}
-function onChanClick(e){while(e.target!==e.currentTarget&&e.target){if(e.target.classList.contains(R.klass.chatList.entry)){var room=SLACK.context.channels[e.target.id]||SLACK.context.ims[e.target.id]||SLACK.context.groups[e.target.id];if(room&&room!==SELECTED_ROOM)selectRoom(room);return}e.target=e.target.parentElement}}document.addEventListener("DOMContentLoaded",function(){document.getElementById(R.id.chatList).addEventListener("click",onChanClick);startPolling()});var SLACK;function SlackWrapper(){this.lastServerVersion=0;this.context=new SlackData(null)}SlackWrapper.prototype.update=function(data){if(data.v)this.lastServerVersion=data.v;if(data["static"]){this.context.updateStatic(data["static"]);onContextUpdated()}if(data.live)console.log("updated LIVE");console.log(this)};SLACK=new SlackWrapper;var NEXT_RETRY=5,SELECTED_ROOM=null;
-function poll(callback){var xhr=new XMLHttpRequest;xhr.timeout=1E3*60*1;xhr.onreadystatechange=function(e){if(xhr.readyState===4){if(xhr.status===0){poll(callback);NEXT_RETRY=5;return}var resp=null,success=Math.floor(xhr.status/100)===2;if(success){NEXT_RETRY=5;resp=xhr.response;try{resp=JSON.parse((resp))}catch(e){resp=null}}else{NEXT_RETRY+=Math.floor(NEXT_RETRY/2);NEXT_RETRY=Math.min(60,NEXT_RETRY)}callback(success,resp)}};xhr.open("GET","api?v="+SLACK.lastServerVersion,true);xhr.send(null)}
-function onPollResponse(success,response){if(success){if(response)SLACK.update(response);startPolling()}else setTimeout(startPolling,NEXT_RETRY*1E3)}function startPolling(){poll(onPollResponse)}function selectRoom(room){if(SELECTED_ROOM)unselectRoom();document.getElementById(room.id).classList.add(R.klass.selected);document.body.classList.remove(R.klass.noRoomSelected);SELECTED_ROOM=room;onRoomSelected()}
-function unselectRoom(){document.getElementById(SELECTED_ROOM.id).classList.remove(R.klass.selected)};
+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.h={};this.a={};this.g=null;this.f={}}function h(b,a){return b.a[a]||b.f[a]||null}"undefined"!==typeof module&&(module.m.l=p);function q(){var b=document.createDocumentFragment(),a=r.a.g?Object.keys(r.a.g.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.h[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.j&&(this.b=b.j);if(b.i){for(var a=this.a,d=b.i,c=0,f=d.bots.length;c<f;c++)a.f[d.f[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.h[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.g=h(a,d.self.id);q()}b.o&&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)};

+ 156 - 11
srv/src/slackData.js

@@ -3,14 +3,23 @@
  * @constructor
 **/
 function SlackTeam(teamData) {
+    /** @type {string} */
     this.id = teamData["id"];
+    /** @type {string} */
     this.name = teamData["name"];
+    /** @type {string} */
     this.domain = teamData["domain"];
+    /** @type {string} */
     this.callApp = teamData["prefs"]["calling_app_id"];
+    /** @type {string} */
     this.callAppName = teamData["prefs"]["calling_app_name"];
+    /** @type {boolean} */
     this.fileUploadPermission = teamData["prefs"]["disable_file_uploads"];
+    /** @type {boolean} */
     this.fileEditPermission = teamData["prefs"]["disable_file_editing"];
+    /** @type {boolean} */
     this.fileDeletePermission = teamData["prefs"]["disable_file_deleting"];
+    /** @type {Object.<string, string>} */
     this.icons = {
         image_34: teamData["icon"]["image_34"]
         ,image_44: teamData["icon"]["image_44"]
@@ -27,19 +36,39 @@ function SlackTeam(teamData) {
  * @constructor
 **/
 function SlackChan(chanData, slackData) {
+    /** @type {string} */
     this.id = chanData["id"];
+    /** @type {string} */
     this.name = chanData["name"];
+    /** @type {string} */
     this.created = chanData["created"];
+    /** @type {SlackUser|SlackBot} */
     this.creator = slackData.getMember(chanData["creator"]);
+    /** @type {boolean} */
     this.archived = chanData["is_archived"];
+    /** @type {boolean} */
     this.isMember = chanData["is_member"];
+    /** @type {number} */
     this.lastRead = chanData["last_read"];
+    /** @type {Object.<string, SlackBot|SlackUser>} */
     this.members = {};
     if (chanData["members"]) for (var i =0, nbMembers = chanData["members"].length; i < nbMembers; i++) {
         var member = slackData.getMember(chanData["members"][i]);
         this.members[member.id] = member;
         member.channels[this.id] = this;
     }
+    /** @type {string|undefined} */
+    this.topic;
+    /** @type {number|undefined} */
+    this.topicTs;
+    /** @type {SlackUser|SlackBot|undefined} */
+    this.topicCreator;
+    /** @type {string|undefined} */
+    this.purpose;
+    /** @type {number|undefined} */
+    this.purposeTs;
+    /** @type {SlackUser|SlackBot|undefined} */
+    this.purposeCreator;
     if (chanData["topic"]) {
         this.topic = chanData["topic"]["value"];
         this.topicCreator = slackData.getMember(chanData["topic"]["creator"]);
@@ -54,20 +83,69 @@ function SlackChan(chanData, slackData) {
 
 /**
  * @constructor
+ * @param {SlackData} slack
+ * @param {*} groupData
 **/
-function SlackGroup(groupData) {
-    // TODO
+function SlackGroup(slack, groupData) {
+    var memberNames = [];
+
+    /** @type {string} */
+    this.id = groupData["id"];
+    /** @type {Object.<string, SlackUser|SlackBot>} */
+    this.members = {};
+    for (var i =0, nbMembers = groupData["members"].length; i < nbMembers; i++) {
+        var member = slack.getMember(groupData["members"][i]);
+        this.members[groupData["members"][i]] = member;
+        member.channels[this.id] = this;
+        memberNames.push(member.name);
+    }
+    /** @type {string} */
+    this.name = memberNames.join(", ");
+    /** @type {number} */
+    this.created = groupData["created"];
+    /** @type {SlackUser|SlackBot} */
+    this.creator = slack.getMember(groupData["creator"]);
+    /** @type {boolean} */
+    this.archived = groupData["is_archived"];
+    /** @type {number} */
+    this.lastRead = groupData["last_read"];
+    /** @type {string|undefined} */
+    this.topic;
+    /** @type {number|undefined} */
+    this.topicTs;
+    /** @type {SlackUser|SlackBot|undefined} */
+    this.topicCreator;
+    /** @type {string|undefined} */
+    this.purpose;
+    /** @type {number|undefined} */
+    this.purposeTs;
+    /** @type {SlackUser|SlackBot|undefined} */
+    this.purposeCreator;
+    if (groupData["topic"]) {
+        this.topic = groupData["topic"]["value"];
+        this.topicCreator = slack.getMember(groupData["topic"]["creator"]);
+        this.topicTs = groupData["topic"]["last_set"];
+    }
+    if (groupData["purpose"]) {
+        this.purpose = groupData["purpose"]["value"];
+        this.purposeCreator = slack.getMember(groupData["purpose"]["creator"]);
+        this.purposeTs = groupData["purpose"]["last_set"];
+    }
 }
 
 /**
  * @constructor
- * @param {SlackUser} user
+ * @param {SlackUser|SlackBot} user
  * @param {*} imsData
 **/
 function SlackIms(user, imsData) {
+    /** @type {string} */
     this.id = imsData["id"];
+    /** @type {number} */
     this.created = imsData["created"];
+    /** @type {SlackUser|SlackBot} */
     this.user = user;
+    /** @type {number} */
     this.lastRead = imsData["last_read"];
 }
 
@@ -75,12 +153,19 @@ function SlackIms(user, imsData) {
  * @constructor
 **/
 function SlackUser(userData) {
+    /** @type {string} */
     this.id = userData["id"];
+    /** @type {string} */
     this.name = userData["name"];
+    /** @type {boolean} */
     this.deleted = userData["deleted"];
+    /** @type {string} */
     this.status = userData["status"];
+    /** @type {string} */
     this.realName = userData["real_name"] || userData["profile"]["real_name"];
+    /** @type {boolean} */
     this.presence = userData["presence"] !== 'away';
+    /** @type {Object.<string, string>} */
     this.icons = {
         image_24: userData["profile"]["image_24"]
         ,image_32: userData["profile"]["image_32"]
@@ -89,10 +174,15 @@ function SlackUser(userData) {
         ,image_192: userData["profile"]["image_192"]
         ,image_512: userData["profile"]["image_512"]
     };
+    /** @type {string} */
     this.email = userData["profile"]["email"];
+    /** @type {string} */
     this.firstName = userData["profile"]["first_name"];
+    /** @type {string} */
     this.lastName = userData["profile"]["last_name"];
+    /** @type {!Object.<string, SlackChan|SlackGroup>} */
     this.channels = {};
+    /** @type {SlackIms} */
     this.ims = null;
 }
 
@@ -100,16 +190,24 @@ function SlackUser(userData) {
  * @constructor
 **/
 function SlackBot(botData) {
+    /** @type {string} */
     this.id = botData["id"];
+    /** @type {boolean} */
     this.deleted = botData["deleted"];
+    /** @type {string} */
     this.name = botData["name"];
+    /** @type {string} */
     this.appId = botData["app_id"];
+    /** @type {Object.<string, string>} */
     this.icons = {
         image_36: botData["icons"]["image_36"]
         ,image_48: botData["icons"]["image_48"]
         ,image_72: botData["icons"]["image_72"]
     };
+    /** @type {!Object.<string, SlackGroup|SlackChan>} */
     this.channels = {};
+    /** @type {SlackIms|null} */
+    this.ims = null;
 }
 
 /**
@@ -126,15 +224,27 @@ function SlackHistory(target) {
  * @constructor
 **/
 function SlackData(slack) {
+    /** @type {SlackTeam|null} */
     this.team = null;
+    /** @type {Object.<string, SlackChan>} */
     this.channels = {};
-    this.groups = [];
+    /** @type {Object.<string, SlackGroup>} */
+    this.groups = {};
+    /** @type {Object.<string, SlackIms>} */
     this.ims = {};
+    /** @type {Object.<string, SlackUser>} */
     this.users = {};
+    /** @type {SlackUser|SlackBot} */
     this.self = null;
+    /** @type {Object.<string, SlackBot>} */
     this.bots = {};
     // channel/ims id -> array of events
     this.history = {};
+
+    /**
+     * Node serv handler
+     * @type {*}
+    **/
     this.slack = slack;
 
     /** @type {number} */
@@ -184,7 +294,7 @@ SlackChan.prototype.toStatic = function() {
         ,"last_read": this.lastRead
     };
     if (this.isMember) {
-        res["members"] = Object.keys(this.members);
+        res["members"] = this.members ? Object.keys(this.members) : [];
         res["topic"] = {
             "value": this.topic
             ,"creator": this.topicCreator ? this.topicCreator.id : null
@@ -209,7 +319,6 @@ SlackUser.prototype.toStatic = function() {
         ,"deleted": this.deleted
         ,"status": this.status
         ,"real_name": this.realName
-        ,"presence": this.presence ? "present" : "away"
         ,"profile": {
             "email": this.email
             ,"first_name": this.firstName
@@ -225,6 +334,35 @@ SlackUser.prototype.toStatic = function() {
     };
 };
 
+SlackGroup.prototype.toStatic = function() {
+    var res = {
+        "id": this.id
+        ,"members": []
+        ,"created": this.created
+        ,"creator": this.creator.id
+        ,"is_archived": this.archived
+        ,"last_read": this.lastRead
+    };
+    for (var mId in this.members) {
+        res["members"].push(mId);
+    }
+    if (this.topic) {
+        res["topic"] = {
+            "value": this.topic
+            ,"creator": this.topicCreator.id
+            ,"last_set": this.topicTs
+        };
+    }
+    if (this.purpose) {
+        res["purpose"] = {
+            "value": this.purpose
+            ,"creator": this.purposeCreator.id
+            ,"last_set": this.purposeTs
+        };
+    }
+    return res;
+};
+
 SlackIms.prototype.toStatic = function() {
     return {
         "id": this.id
@@ -258,12 +396,15 @@ SlackData.prototype.updateStatic = function(data) {
         this.users[data["users"][i].id] = new SlackUser(data["users"][i]);
     for (var i =0, nbIms = data["ims"].length; i < nbIms; i++) {
         var user = this.getMember(data["ims"][i]["user"]);
-        user.ims = new SlackIms(user, data["ims"][i]);
-        this.ims[user.ims.id] = user.ims;
+        if (user) {
+            user.ims = new SlackIms(user, data["ims"][i]);
+            this.ims[user.ims.id] = user.ims;
+        }
     }
     for (var i =0, nbChan = data["channels"].length; i < nbChan; i++)
         this.channels[data["channels"][i].id] = new SlackChan(data["channels"][i], this);
-    //this.groups.push(new SlackGroup(data.groups)); TODO
+    for (var i =0, nbGroups = data["groups"].length; i < nbGroups; i++)
+        this.groups[data["groups"][i]["id"]] = new SlackGroup(this, data["groups"][i]);
     this.team = new SlackTeam(data["team"]);
     this.staticV = parseFloat(data["latest_event_ts"]);
     this.self = this.getMember(data["self"]["id"]);
@@ -326,7 +467,7 @@ SlackData.prototype.getBotsByAppId = function(appId) {
 
 /**
  * @param {string} mId
- * @return {SlackUser|null}
+ * @return {SlackUser|SlackBot|null}
 **/
 SlackData.prototype.getMember = function(mId) {
     return this.users[mId] || this.bots[mId] || null;
@@ -338,6 +479,7 @@ SlackData.prototype.getMember = function(mId) {
 **/
 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);
@@ -349,7 +491,7 @@ SlackData.prototype.buildStatic = function() {
     var res = {
         "team": this.team.toStatic()
         ,"channels": []
-        ,"groups": [] //TODO
+        ,"groups": []
         ,"ims": []
         ,"users": []
         ,"bots": []
@@ -367,6 +509,9 @@ SlackData.prototype.buildStatic = function() {
     for (var botId in this.bots) {
         res["bots"].push(this.bots[botId].toStatic());
     }
+    for (var groupId in this.groups) {
+        res["groups"].push(this.groups[groupId].toStatic());
+    }
     return res;
 };