Updated server for Express 1.0.0; reorganized modules; updated README
* Server now works with Express 1.0.0 (beta or beta2) and Connect * `AjaxIM` is now `Hub` and functions as both manager and session middleware * Authentication directory structure neatened * Development controllers and views separated into `dev/` folder * Client-side content moved to `client/` folder * Updated installation instructions in README
@@ -1,4 +1,3 @@
|
||||
local/*
|
||||
*.local.*
|
||||
*.local/
|
||||
public/
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2005 - 2010 Joshua Gross, http://ajaxim.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -1,19 +1,22 @@
|
||||
# Ajax IM
|
||||
|
||||
Ajax IM ("Ajax Instant Messenger") is a browser-centric instant messaging framework.
|
||||
Ajax IM ("Ajax Instant Messenger") is a browser-centric instant
|
||||
messaging framework.
|
||||
|
||||
|
||||
## What is Ajax IM?
|
||||
|
||||
It uses AJAX to create a real-time (or near real-time) IM environment that can be used in conjunction with existing community and commercial software, or simply as a stand-alone product.
|
||||
It uses AJAX to create a real-time (or near real-time) IM environment that
|
||||
can be used in conjunction with existing community and commercial software,
|
||||
or simply as a stand-alone product.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Install `Node.js` (known to work with v0.1.98+):
|
||||
wget http://nodejs.org/dist/node-v0.1.100.tar.gz
|
||||
tar xzf node-v0.1.100.tar.gz
|
||||
cd node-v0.1.100
|
||||
wget http://nodejs.org/dist/node-v0.1.101.tar.gz
|
||||
tar xzf node-v0.1.101.tar.gz
|
||||
cd node-v0.1.101
|
||||
./configure
|
||||
make
|
||||
make install
|
||||
@@ -21,5 +24,48 @@ Install `Node.js` (known to work with v0.1.98+):
|
||||
Install Node Package Manager (`npm`):
|
||||
See instructions at http://github.com/isaacs/npm.
|
||||
|
||||
Install `Connect`:
|
||||
npm install connect
|
||||
|
||||
Install `Express.js`:
|
||||
npm install express
|
||||
|
||||
## Installation for Development
|
||||
|
||||
If you will want to test Ajax IM standalone for development, you will want to
|
||||
install [`Jade`](http://github.com/visionmedia/jade) as well.
|
||||
|
||||
To install `Jade`:
|
||||
npm install jade
|
||||
|
||||
## Starting up the server
|
||||
|
||||
Starting the server in _development_ mode is as simple as:
|
||||
node server/app.js
|
||||
|
||||
To start the server in _production_ mode:
|
||||
EXPRESS_ENV=production node server/app.js
|
||||
|
||||
## License
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2010 [Joshua Gross](http://www.unwieldy.net)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
@@ -25,7 +25,6 @@
|
||||
// and connecting with the server. It does //not// handle registration or
|
||||
// account management.
|
||||
|
||||
var AjaxIM;
|
||||
(function($) {
|
||||
// Cookies API
|
||||
var cookies = {
|
||||
@@ -75,7 +74,7 @@ var AjaxIM;
|
||||
self.cookies.set(name, '', -1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Storage API
|
||||
var store = (function(){
|
||||
var api = {},
|
||||
@@ -84,12 +83,12 @@ var AjaxIM;
|
||||
localStorageName = 'localStorage',
|
||||
globalStorageName = 'globalStorage',
|
||||
storage
|
||||
|
||||
|
||||
api.set = function(key, value) {}
|
||||
api.get = function(key) {}
|
||||
api.remove = function(key) {}
|
||||
api.clear = function() {}
|
||||
|
||||
|
||||
function serialize(value) {
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
@@ -97,21 +96,21 @@ var AjaxIM;
|
||||
if (typeof value != 'string') { return undefined }
|
||||
return JSON.parse(value)
|
||||
}
|
||||
|
||||
|
||||
if (localStorageName in win && win[localStorageName]) {
|
||||
storage = win[localStorageName]
|
||||
api.set = function(key, val) { storage[key] = serialize(val) }
|
||||
api.get = function(key) { return deserialize(storage[key]) }
|
||||
api.remove = function(key) { delete storage[key] }
|
||||
api.clear = function() { storage.clear() }
|
||||
|
||||
|
||||
} else if (globalStorageName in win && win[globalStorageName]) {
|
||||
storage = win[globalStorageName][win.location.hostname]
|
||||
api.set = function(key, val) { storage[key] = serialize(val) }
|
||||
api.get = function(key) { return deserialize(storage[key] && storage[key].value) }
|
||||
api.remove = function(key) { delete storage[key] }
|
||||
api.clear = function() { for (var key in storage ) { delete storage[key] } }
|
||||
|
||||
|
||||
} else if (doc.documentElement.addBehavior) {
|
||||
function getStorage() {
|
||||
if (storage) { return storage; }
|
||||
@@ -147,7 +146,7 @@ var AjaxIM;
|
||||
storage.save(localStorageName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return api
|
||||
})();
|
||||
|
||||
@@ -288,34 +287,44 @@ var AjaxIM;
|
||||
|
||||
// Setup and hide the scrollers
|
||||
$('.imjs-scroll').css('display', 'none');
|
||||
$('#imjs-scroll-left').live('click', function() {
|
||||
var hiddenTab = $('#imjs-bar li.imjs-tab:visible').slice(-1)
|
||||
.next('#imjs-bar li.imjs-tab:hidden')
|
||||
$('#imjs-scroll-right').live('click', function() {
|
||||
var hiddenTab = $(this)
|
||||
.prevAll('#imjs-bar li.imjs-tab:hidden')
|
||||
.filter(function() {
|
||||
return $(this).data('state') != 'closed'
|
||||
return (
|
||||
$(this).data('state') != 'closed' &&
|
||||
$(this).prev('#imjs-bar li.imjs-tab:visible').length
|
||||
);
|
||||
})
|
||||
.not('.imjs-default').slice(-1).css('display', '');
|
||||
.not('.imjs-default')
|
||||
.slice(-1)
|
||||
.css('display', '');
|
||||
|
||||
if(hiddenTab.length) {
|
||||
$('#imjs-bar li.imjs-tab:visible').eq(0).css('display', 'none');
|
||||
$(this).html(parseInt($(this).html()) - 1);
|
||||
$('#imjs-scroll-right').html(parseInt($('#imjs-scroll-right').html()) + 1);
|
||||
$('#imjs-scroll-left').html(parseInt($('#imjs-scroll-left').html()) + 1);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
$('#imjs-scroll-right').live('click', function() {
|
||||
var hiddenTab = $('#imjs-bar li.imjs-tab:visible').eq(0)
|
||||
.prev('#imjs-bar li.imjs-tab:hidden')
|
||||
$('#imjs-scroll-left').live('click', function() {
|
||||
var hiddenTab = $(this)
|
||||
.nextAll('#imjs-bar li.imjs-tab:hidden')
|
||||
.filter(function() {
|
||||
return $(this).data('state') != 'closed'
|
||||
return (
|
||||
$(this).data('state') != 'closed' &&
|
||||
$(this).next('#imjs-bar li.imjs-tab:visible').length
|
||||
);
|
||||
})
|
||||
.not('.imjs-default').slice(-1).css('display', '');
|
||||
|
||||
.not('.imjs-default')
|
||||
.slice(-1)
|
||||
.css('display', '');
|
||||
console.log(hiddenTab)
|
||||
if(hiddenTab.length) {
|
||||
$('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none');
|
||||
$(this).html(parseInt($(this).html()) - 1);
|
||||
$('#imjs-scroll-left').html(parseInt($('#imjs-scroll-left').html()) + 1);
|
||||
$('#imjs-scroll-right').html(parseInt($('#imjs-scroll-right').html()) + 1);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -483,7 +492,7 @@ var AjaxIM;
|
||||
self.addFriend(friend[0], friend[1], 'Friends');
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
case 'message':
|
||||
self.incoming(message.user, message.body);
|
||||
break;
|
||||
@@ -614,12 +623,12 @@ var AjaxIM;
|
||||
// format of {{{#imjs-[md5 of username]}}}.
|
||||
_createChatbox: function(username, no_stamp) {
|
||||
var chatbox_id = 'imjs-' + $.md5(username);
|
||||
if(!(chatbox = $('#' + chatbox_id)).size()) {
|
||||
if(!(chatbox = $('#' + chatbox_id)).length) {
|
||||
// add a tab
|
||||
var tab = this.bar.addTab(username, '#' + chatbox_id);
|
||||
var chatbox = tab.find('.imjs-chatbox');
|
||||
chatbox.attr('id', chatbox_id);
|
||||
|
||||
chatbox.attr('id', chatbox_id);
|
||||
chatbox.data('tab', tab);
|
||||
|
||||
// remove default items from the message log
|
||||
@@ -639,24 +648,20 @@ var AjaxIM;
|
||||
this.chats[username] = chatbox;
|
||||
chatbox.data('username', username);
|
||||
|
||||
// did this chatbox fall down?
|
||||
this.bar._scrollers();
|
||||
|
||||
if(username in this.friends) {
|
||||
status = this.friends[username].status;
|
||||
tab.addClass('imjs-' + status);
|
||||
}
|
||||
|
||||
// store inputbox height
|
||||
//var input = chatbox.find('.imjs-input');
|
||||
//input.data('height', input.height());
|
||||
setTimeout(this.bar._scrollers, 0);
|
||||
} else if(chatbox.data('tab').data('state') == 'closed') {
|
||||
chatbox.find('.imjs-msglog > *').addClass('imjs-msg-old');
|
||||
|
||||
var tab = chatbox.data('tab');
|
||||
if(tab.css('display') == 'none')
|
||||
tab.css('display', '').removeClass('imjs-selected')
|
||||
.appendTo('#imjs-bar');
|
||||
.insertAfter('#imjs-scroll-left')
|
||||
.data('state', 'minimized');
|
||||
|
||||
if(!no_stamp) {
|
||||
// possibly add a date stamp
|
||||
@@ -669,6 +674,8 @@ var AjaxIM;
|
||||
} else {
|
||||
this.bar.notification(tab);
|
||||
}
|
||||
|
||||
setTimeout(this.bar._scrollers, 0);
|
||||
}
|
||||
|
||||
return chatbox;
|
||||
@@ -705,7 +712,7 @@ var AjaxIM;
|
||||
|
||||
chatbox.data('lastDateStamp', formatted_date);
|
||||
date_stamp.appendTo(message_log);
|
||||
|
||||
|
||||
return jQuery('<div>').append(date_stamp.clone()).html();
|
||||
} else {
|
||||
//$('<div></div>').appendTo(message_log);
|
||||
@@ -743,7 +750,7 @@ var AjaxIM;
|
||||
error_item.appendTo(message_log);
|
||||
|
||||
message_log[0].scrollTop = message_log[0].scrollHeight;
|
||||
|
||||
|
||||
return jQuery('<div>').append(error_item.clone()).html();
|
||||
},
|
||||
|
||||
@@ -934,7 +941,7 @@ var AjaxIM;
|
||||
|
||||
AjaxIM.post(
|
||||
this.actions.send,
|
||||
{'username': username, 'body': body},
|
||||
{to: username, body: body},
|
||||
function(result) {
|
||||
if(result.type == 'success' && result.success == 'sent') {
|
||||
$(self).trigger('sendMessageSuccessful',
|
||||
@@ -984,7 +991,7 @@ var AjaxIM;
|
||||
|
||||
AjaxIM.post(
|
||||
this.actions.status,
|
||||
{'status': this.statuses[s], 'message': message},
|
||||
{status: this.statuses[s], message: message},
|
||||
function(result) {
|
||||
switch(result.r) {
|
||||
case 'ok':
|
||||
@@ -1192,7 +1199,7 @@ var AjaxIM;
|
||||
// //Note:// New tabs are given an automatically generated ID
|
||||
// in the format of {{{#imjs-tab-[md5 of label]}}}.
|
||||
addTab: function(label, action, closable) {
|
||||
var tab = $('.imjs-tab.imjs-default').clone().insertAfter('#imjs-scroll-right');
|
||||
var tab = $('.imjs-tab.imjs-default').clone().insertAfter('#imjs-scroll-left');
|
||||
tab.removeClass('imjs-default')
|
||||
.attr('id', 'imjs-tab-' + $.md5(label))
|
||||
.html(tab.html().replace('{label}', label))
|
||||
@@ -1240,20 +1247,20 @@ var AjaxIM;
|
||||
// Document me!
|
||||
_scrollers: function() {
|
||||
var needScrollers = false;
|
||||
$('.imjs-tab').filter(function() {
|
||||
return $(this).data('state') != 'closed'
|
||||
}).css('display', '');
|
||||
$('#imjs-scroll-left').nextAll('.imjs-tab')
|
||||
.filter(function() {
|
||||
return $(this).data('state') != 'closed';
|
||||
})
|
||||
.each(function(i, tab) {
|
||||
tab = $(tab).css('display', '');
|
||||
|
||||
$.each(self.chats, function(username, chatbox) {
|
||||
var tab = chatbox.data('tab');
|
||||
if(tab.data('state') == 'closed') return true;
|
||||
|
||||
if(tab.position().top > $('#imjs-bar').height()) {
|
||||
var tab_pos = tab.position();
|
||||
if(tab_pos.top >= $('#imjs-bar').height() ||
|
||||
tab_pos.left < 0 ||
|
||||
tab_pos.right > $(document).width()) {
|
||||
$('.imjs-scroll').css('display', '');
|
||||
tab.css('display', 'none');
|
||||
needScrollers = true;
|
||||
} else {
|
||||
tab.css('display', '');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1262,18 +1269,25 @@ var AjaxIM;
|
||||
}
|
||||
|
||||
if($('#imjs-scroll-left').css('display') != 'none' &&
|
||||
$('#imjs-scroll-left').position().top > $('#imjs-bar').height()) {
|
||||
$('#imjs-scroll-right').position().top >= $('#imjs-bar').height()) {
|
||||
$('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none');
|
||||
}
|
||||
|
||||
var hiddenLeft = $('#imjs-bar li.imjs-tab:visible').slice(-1)
|
||||
while($('.imjs-selected').css('display') == 'none')
|
||||
$('#imjs-scroll-right').click();
|
||||
|
||||
self.bar._scrollerIndex();
|
||||
},
|
||||
|
||||
_scrollerIndex: function() {
|
||||
var hiddenRight = $('#imjs-bar li.imjs-tab:visible').slice(-1)
|
||||
.nextAll('#imjs-bar li.imjs-tab:hidden')
|
||||
.not('.imjs-default')
|
||||
.filter(function() {
|
||||
return $(this).data('state') != 'closed'
|
||||
}).length;
|
||||
|
||||
var hiddenRight = $('#imjs-bar li.imjs-tab:visible').eq(0)
|
||||
var hiddenLeft = $('#imjs-bar li.imjs-tab:visible').eq(0)
|
||||
.prevAll('#imjs-bar li.imjs-tab:hidden')
|
||||
.not('.imjs-default')
|
||||
.filter(function() {
|
||||
@@ -1288,7 +1302,7 @@ var AjaxIM;
|
||||
|
||||
self.bar.initialize();
|
||||
self.bar._scrollers();
|
||||
|
||||
|
||||
self.listen();
|
||||
};
|
||||
|
||||
@@ -1340,7 +1354,7 @@ var AjaxIM;
|
||||
|
||||
AjaxIM.get = function(url, data, successFunc, failureFunc) {
|
||||
AjaxIM.request(url, 'GET', data, successFunc, failureFunc);
|
||||
};
|
||||
};
|
||||
|
||||
AjaxIM.request = function(url, type, data, successFunc, failureFunc) {
|
||||
if(typeof failureFunc != 'function');
|
||||
@@ -1537,7 +1551,7 @@ var AjaxIM;
|
||||
if(str in AjaxIM.l10n) return AjaxIM.l10n[str];
|
||||
return str;
|
||||
};
|
||||
|
||||
|
||||
AjaxIM.l10n = {
|
||||
dayNames: [
|
||||
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
|
||||
|
Antes Largura: | Altura: | Tamanho: 868 B Depois Largura: | Altura: | Tamanho: 868 B |
|
Antes Largura: | Altura: | Tamanho: 814 B Depois Largura: | Altura: | Tamanho: 814 B |
|
Antes Largura: | Altura: | Tamanho: 853 B Depois Largura: | Altura: | Tamanho: 853 B |
|
Antes Largura: | Altura: | Tamanho: 853 B Depois Largura: | Altura: | Tamanho: 853 B |
|
Antes Largura: | Altura: | Tamanho: 850 B Depois Largura: | Altura: | Tamanho: 850 B |
|
Antes Largura: | Altura: | Tamanho: 843 B Depois Largura: | Altura: | Tamanho: 843 B |
|
Antes Largura: | Altura: | Tamanho: 860 B Depois Largura: | Altura: | Tamanho: 860 B |
|
Antes Largura: | Altura: | Tamanho: 846 B Depois Largura: | Altura: | Tamanho: 846 B |
|
Antes Largura: | Altura: | Tamanho: 854 B Depois Largura: | Altura: | Tamanho: 854 B |
|
Antes Largura: | Altura: | Tamanho: 121 B Depois Largura: | Altura: | Tamanho: 121 B |
|
Antes Largura: | Altura: | Tamanho: 176 B Depois Largura: | Altura: | Tamanho: 176 B |
|
Antes Largura: | Altura: | Tamanho: 552 B Depois Largura: | Altura: | Tamanho: 552 B |
|
Antes Largura: | Altura: | Tamanho: 212 B Depois Largura: | Altura: | Tamanho: 212 B |
|
Antes Largura: | Altura: | Tamanho: 706 B Depois Largura: | Altura: | Tamanho: 706 B |
|
Antes Largura: | Altura: | Tamanho: 142 B Depois Largura: | Altura: | Tamanho: 142 B |
|
Antes Largura: | Altura: | Tamanho: 141 B Depois Largura: | Altura: | Tamanho: 141 B |
|
Antes Largura: | Altura: | Tamanho: 234 B Depois Largura: | Altura: | Tamanho: 234 B |
|
Antes Largura: | Altura: | Tamanho: 109 B Depois Largura: | Altura: | Tamanho: 109 B |
@@ -158,15 +158,14 @@ ul#imjs-bar {
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 95%;
|
||||
right: 5%;
|
||||
background: #f0efed url(images/w.png) top left repeat-x;
|
||||
height: 25px;
|
||||
border: solid #cfceca;
|
||||
border-width: 1px 1px 0 1px;
|
||||
border-width: 1px 1px 0 0;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0 0 0 -47.5%;
|
||||
margin: 0;
|
||||
font: 12px/25px Helvetica Neue, Helvetica, Arial, Calibri, Tahoma, Verdana, sans-serif;
|
||||
color: #222;
|
||||
}
|
||||
@@ -174,7 +173,7 @@ ul#imjs-bar {
|
||||
/* [begin] Generic bar item */
|
||||
ul#imjs-bar > li {
|
||||
position: relative;
|
||||
float: right;
|
||||
float: left;
|
||||
width: 161px;
|
||||
border-left: 1px solid #cecece;
|
||||
padding: 0 10px;
|
||||
@@ -273,12 +272,14 @@ ul#imjs-bar {
|
||||
padding: 1px 6px 0 4px;
|
||||
background-image: url(images/sl.png);
|
||||
background-position: 5px center;
|
||||
float: left;
|
||||
}
|
||||
|
||||
ul#imjs-bar li#imjs-scroll-right {
|
||||
padding: 1px 5px 0;
|
||||
background-image: url(images/sr.png);
|
||||
background-position: 20px center;
|
||||
float: right;
|
||||
}
|
||||
/* [end] Bar scrolling buttons */
|
||||
|
||||
@@ -326,6 +327,10 @@ ul#imjs-bar {
|
||||
/* [end] Tooltip */
|
||||
|
||||
/* [begin] Friends list button */
|
||||
ul#imjs-bar > li#imjs-friends {
|
||||
float: right;
|
||||
}
|
||||
|
||||
ul#imjs-bar > li#imjs-friends.imjs-not-connected,
|
||||
ul#imjs-bar > li#imjs-friends.not-connected:hover {
|
||||
width: 10px;
|
||||
@@ -20,7 +20,7 @@
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
<li id="imjs-scroll-right" class="imjs-scroll">{count}</li>
|
||||
<li id="imjs-scroll-left" class="imjs-scroll">{count}</li>
|
||||
<li class="imjs-tab imjs-default">
|
||||
<span class="imjs-tab-text"><strong>•</strong> {label} <a href="#" class="imjs-close">x</a></span>
|
||||
<span class="imjs-notification">{count}</span>
|
||||
@@ -73,7 +73,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
<li id="imjs-scroll-left" class="imjs-scroll">{count}</li>
|
||||
<li id="imjs-scroll-right" class="imjs-scroll">{count}</li>
|
||||
</ul>
|
||||
<span class="imjs-tooltip"><p>{tip}</p></span>
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
var store = (function(){
|
||||
var api = {},
|
||||
win = window,
|
||||
doc = win.document,
|
||||
localStorageName = 'localStorage',
|
||||
globalStorageName = 'globalStorage',
|
||||
storage
|
||||
|
||||
api.set = function(key, value) {}
|
||||
api.get = function(key) {}
|
||||
api.remove = function(key) {}
|
||||
api.clear = function() {}
|
||||
|
||||
function serialize(value) {
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
function deserialize(value) {
|
||||
if (typeof value != 'string') { return undefined }
|
||||
return JSON.parse(value)
|
||||
}
|
||||
|
||||
if (localStorageName in win && win[localStorageName]) {
|
||||
storage = win[localStorageName]
|
||||
api.set = function(key, val) { storage[key] = serialize(val) }
|
||||
api.get = function(key) { return deserialize(storage[key]) }
|
||||
api.remove = function(key) { delete storage[key] }
|
||||
api.clear = function() { storage.clear() }
|
||||
|
||||
} else if (globalStorageName in win && win[globalStorageName]) {
|
||||
storage = win[globalStorageName][win.location.hostname]
|
||||
api.set = function(key, val) { storage[key] = serialize(val) }
|
||||
api.get = function(key) { return deserialize(storage[key] && storage[key].value) }
|
||||
api.remove = function(key) { delete storage[key] }
|
||||
api.clear = function() { for (var key in storage ) { delete storage[key] } }
|
||||
|
||||
} else if (doc.documentElement.addBehavior) {
|
||||
function getStorage() {
|
||||
if (storage) { return storage; }
|
||||
storage = doc.body.appendChild(doc.createElement('div'))
|
||||
storage.style.display = 'none'
|
||||
// See http://msdn.microsoft.com/en-us/library/ms531081(v=VS.85).aspx
|
||||
// and http://msdn.microsoft.com/en-us/library/ms531424(v=VS.85).aspx
|
||||
storage.addBehavior('#default#userData')
|
||||
storage.load(localStorageName)
|
||||
return storage;
|
||||
}
|
||||
api.set = function(key, val) {
|
||||
var storage = getStorage()
|
||||
storage.setAttribute(key, serialize(val))
|
||||
storage.save(localStorageName)
|
||||
}
|
||||
api.get = function(key) {
|
||||
var storage = getStorage()
|
||||
return deserialize(storage.getAttribute(key))
|
||||
}
|
||||
api.remove = function(key) {
|
||||
var storage = getStorage()
|
||||
storage.removeAttribute(key)
|
||||
storage.save(localStorageName)
|
||||
}
|
||||
api.clear = function() {
|
||||
var storage = getStorage()
|
||||
var attributes = storage.XMLDocument.documentElement.attributes;
|
||||
storage.load(localStorageName)
|
||||
for (var i=0, attr; attr = attributes[i]; i++) {
|
||||
storage.removeAttribute(attr.name)
|
||||
}
|
||||
storage.save(localStorageName)
|
||||
}
|
||||
}
|
||||
|
||||
return api
|
||||
})()
|
||||
@@ -1,95 +1,76 @@
|
||||
// = Ajax IM =
|
||||
//
|
||||
// **Copyright © 2005 – 2010 Joshua Gross**\\
|
||||
// //MIT Licensed//
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
// == Node.js Server ==
|
||||
//
|
||||
var sys = require('sys');
|
||||
|
||||
require.paths.unshift(require.paths[0] + '/express');
|
||||
require('express');
|
||||
require('express/plugins');
|
||||
var sys = require('sys'),
|
||||
connect = require('connect'),
|
||||
express = require('express'),
|
||||
packages = require('./libs/packages');
|
||||
Object.merge(global, require('ext'));
|
||||
Object.merge(global, require('./session')); // Ugly.
|
||||
|
||||
Object.merge(global, require('./settings'));
|
||||
try { Object.merge(global, require('./settings.local')); } catch(e) {}
|
||||
|
||||
var chat = require('./chat');
|
||||
var app = express.createServer(
|
||||
connect.methodOverride(),
|
||||
connect.cookieDecoder(),
|
||||
connect.bodyDecoder(),
|
||||
require('./middleware/im')({
|
||||
lifetime: (15).minutes,
|
||||
reapInterval: (1).minute,
|
||||
authentication: require('./libs/authentication/' + AUTH_LIBRARY)
|
||||
})
|
||||
);
|
||||
|
||||
configure('development', function() {
|
||||
use(Logger);
|
||||
use(Static);
|
||||
app.set('root', __dirname);
|
||||
|
||||
app.configure('development', function() {
|
||||
app.set('view engine', 'jade');
|
||||
app.set('views', __dirname + '/dev/views');
|
||||
|
||||
app.use(connect.logger());
|
||||
app.use('/dev', connect.router(require('./dev/app')));
|
||||
app.use(connect.staticProvider(__dirname + '/dev/public'));
|
||||
app.use(connect.errorHandler({dumpExceptions: true, showStack: true}));
|
||||
});
|
||||
|
||||
configure(function() {
|
||||
use(MethodOverride);
|
||||
use(Cookie);
|
||||
use(Session.IM, {lifetime: (15).minutes,
|
||||
reapInterval: (1).minute,
|
||||
authentication:
|
||||
require('./libs/authenticate/' + AUTH_LIBRARY)
|
||||
});
|
||||
set('root', __dirname);
|
||||
app.listen(APP_PORT, APP_HOST);
|
||||
|
||||
// Listener endpoint; handled in middleware
|
||||
app.get('/listen', function(){});
|
||||
|
||||
app.post('/message', function(req, res) {
|
||||
res.find(req.body['to'], function(user) {
|
||||
if(!user)
|
||||
return res.send(new packages.Error('not online'));
|
||||
|
||||
res.message(user, new packages.Message(
|
||||
req.session.data('username'),
|
||||
req.body.body
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
get('/listen', function() {
|
||||
// Do nothing.
|
||||
});
|
||||
app.post('/message/typing', function(req, res) {
|
||||
if(-~packages.TYPING_STATES.indexOf('typing' + req.body['state'])) {
|
||||
res.find(req.body['to'], function(user) {
|
||||
if(user) {
|
||||
res.message(user, new packages.Status(
|
||||
req.session.data('username'),
|
||||
'typing' + req.body.state
|
||||
));
|
||||
}
|
||||
|
||||
get('/message/:username', function(username) {
|
||||
chat.AjaxIM.messageUser(this.session, username,
|
||||
new chat.Message(
|
||||
this.session,
|
||||
this.param('body') || ''
|
||||
));
|
||||
});
|
||||
|
||||
post('/message', function() {
|
||||
// if(!('username' in this.params.post))...
|
||||
chat.AjaxIM.messageUser(this.session, this.params.post['username'],
|
||||
new chat.Message(
|
||||
this.session,
|
||||
this.params.post['body'] || ''
|
||||
));
|
||||
});
|
||||
|
||||
post('/message/typing', function(username) {
|
||||
if('state' in this.params.post &&
|
||||
'username' in this.params.post &&
|
||||
-~chat.TYPING_STATES.indexOf('typing' + this.params.post.state)) {
|
||||
chat.AjaxIM.messageUser(this.session, this.params.post['username'],
|
||||
new chat.Status(
|
||||
this.session,
|
||||
'typing' + this.params.post.state
|
||||
));
|
||||
// Typing updates do not receive confirmations,
|
||||
// as they are not important enough.
|
||||
res.send('');
|
||||
});
|
||||
} else {
|
||||
res.send(new packages.Error('invalid state'));
|
||||
}
|
||||
});
|
||||
|
||||
post('/status', function() {
|
||||
if('status' in this.params.post &&
|
||||
-~chat.STATUSES.indexOf(this.params.post.status)) {
|
||||
this.session.status = this.params.post.status;
|
||||
app.post('/status', function(req, res) {
|
||||
if(~packages.STATUSES.indexOf(req.body['status'])) {
|
||||
res.status(req.body.status);
|
||||
res.send(new packages.Success('status updated'));
|
||||
} else {
|
||||
res.send(new packages.Error('invalid status'));
|
||||
}
|
||||
});
|
||||
|
||||
run(APP_PORT, APP_HOST);
|
||||
});
|
||||
@@ -1,179 +0,0 @@
|
||||
var utils = require('express/utils'),
|
||||
events = require('events'),
|
||||
sys = require('sys');
|
||||
|
||||
exports.AjaxIM = AjaxIM = new (new Class({
|
||||
constructor: function() {
|
||||
this.users = [];
|
||||
this.events = new events.EventEmitter();
|
||||
this.events.addListener('update', (function(package) {
|
||||
if(package.constructor === exports.Offline) {
|
||||
for(var i = 0, l = this.users.length; i < l; i++) {
|
||||
if(this.users[i].get('username') == package.user)
|
||||
this.users.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}).bind(this));
|
||||
},
|
||||
|
||||
messageUser: function(session, user, package) {
|
||||
if(!(user in session.convos)) {
|
||||
try {
|
||||
var user_id = this.findUser(user).id;
|
||||
|
||||
session.convos[user] =
|
||||
new exports.Conversation(session.id, user_id);
|
||||
} catch(e) {
|
||||
session.respond(new exports.Error('user offline'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
session.convos[user].send(package);
|
||||
session.respond(new exports.Success('sent'));
|
||||
} catch(e) {
|
||||
session.respond(new exports.Error(e.description));
|
||||
}
|
||||
},
|
||||
|
||||
findUser: function(username) {
|
||||
return this.users.find(function(e) {
|
||||
return e.get('username') == username;
|
||||
});
|
||||
},
|
||||
|
||||
// === {{{ AjaxIM.online() }}} ===
|
||||
//
|
||||
// Return a list of currently signed in users and their statuses.
|
||||
online: function() {
|
||||
},
|
||||
|
||||
// === {{{ AjaxIM.onlineTotal() }}} ===
|
||||
//
|
||||
// Return a count of the number of online users.
|
||||
onlineTotal: function() {
|
||||
}
|
||||
}));
|
||||
|
||||
var Package = new Class({
|
||||
_sanitize: function(content) {
|
||||
// strip HTML
|
||||
return content.replace(/<(.|\n)*?>/g, '');
|
||||
}
|
||||
});
|
||||
|
||||
exports.Error = Package.extend({
|
||||
constructor: function(error) {
|
||||
this.error = error;
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return JSON.encode({
|
||||
type: 'error',
|
||||
error: this.error
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.Success = Package.extend({
|
||||
constructor: function(success) {
|
||||
this.success = success;
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return JSON.encode({
|
||||
type: 'success',
|
||||
success: this.success
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.Message = Package.extend({
|
||||
constructor: function(user, body) {
|
||||
this.user = user;
|
||||
this.body = body;
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return JSON.encode({
|
||||
type: 'message',
|
||||
user: this.user.get('username'),
|
||||
room: this.room,
|
||||
body: this._sanitize(this.body)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.Notice = Package.extend({
|
||||
constructor: function(user, info) {
|
||||
this.user = user;
|
||||
this.info = info;
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return JSON.encode({
|
||||
type: 'notice',
|
||||
user: this.user.get('username'),
|
||||
room: this.room,
|
||||
info: this.info
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.TYPING_STATES = ['typing+', 'typing~', 'typing-'];
|
||||
exports.STATUSES = ['available', 'away', 'idle'];
|
||||
exports.Status = Package.extend({
|
||||
constructor: function(user, status, message) {
|
||||
var statuses = exports.STATUSES + exports.TYPING_STATES;
|
||||
|
||||
this.user = user;
|
||||
this.status = -~statuses.indexOf(status) ? status : statuses[0];
|
||||
this.message = message;
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return JSON.encode({
|
||||
type: 'status',
|
||||
user: this.user.get('username'),
|
||||
status: this.status,
|
||||
message: this._sanitize(this.message || '')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.Offline = Package.extend({
|
||||
constructor: function(user) {
|
||||
this.user = user;
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
// A special type of status
|
||||
return JSON.encode({
|
||||
type: 'status',
|
||||
user: this.user.get('username'),
|
||||
status: 'offline',
|
||||
message: ''
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.Conversation = new Class({
|
||||
constructor: function(you, them) {
|
||||
this.you = you;
|
||||
this.them = them;
|
||||
this.last_updated = Date.now();
|
||||
},
|
||||
|
||||
send: function(package) {
|
||||
this.touch();
|
||||
if(user = Session.IM.get(this.them))
|
||||
user.notify(package);
|
||||
else
|
||||
throw new Error('user not online');
|
||||
},
|
||||
|
||||
touch: function() {
|
||||
this.last_updated = Date.now();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
var utils = require('connect/utils');
|
||||
|
||||
module.exports = function(app) {
|
||||
app.get('/', function(req, res) {
|
||||
res.render('chat', {
|
||||
locals: {}
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/cookie', function(req, res) {
|
||||
res.send('cookie set', {
|
||||
'Set-Cookie': utils.serializeCookie('sessionid', utils.uid())
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
../../../client/js/
|
||||
@@ -0,0 +1 @@
|
||||
../../../client/themes/
|
||||
@@ -0,0 +1,13 @@
|
||||
!!! 5
|
||||
html
|
||||
head
|
||||
title Ajax IM
|
||||
script(src: '/js/jquery-1.4.1.min.js', type: 'text/javascript')
|
||||
script(src: '/js/jquery.jsonp-1.1.3.min.js', type: 'text/javascript')
|
||||
script(src: '/js/jquery.md5.js', type: 'text/javascript')
|
||||
script(src: '/js/store.js', type: 'text/javascript')
|
||||
script(src: '/js/im.js', type: 'text/javascript')
|
||||
script(type: 'text/javascript')
|
||||
| $(function(){var im = AjaxIM.init({theme: "/themes/default"});});
|
||||
|
||||
body Hello.
|
||||
@@ -0,0 +1 @@
|
||||
#{body}
|
||||
@@ -1,9 +1,9 @@
|
||||
exports.cookie = 'ajaxim_session';
|
||||
|
||||
exports.authenticate = function(request, callback) {
|
||||
exports.authenticate = function(session_id, callback) {
|
||||
// Verify user based on request.
|
||||
// On failure, redirect user to auth form
|
||||
|
||||
|
||||
callback({
|
||||
username: 'username',
|
||||
displayname: 'John Smith',
|
||||
@@ -11,9 +11,9 @@ exports.authenticate = function(request, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
exports.friends = function(user, callback) {
|
||||
exports.friends = function(session_id, data, callback) {
|
||||
// Create a friends list based on given user data
|
||||
|
||||
|
||||
callback([
|
||||
'username1',
|
||||
'username2',
|
||||
@@ -0,0 +1,88 @@
|
||||
var sys = require('sys');
|
||||
|
||||
var Package = function() {};
|
||||
Package.prototype._sanitize = function(content) {
|
||||
// strip HTML
|
||||
return content.replace(/<(.|\n)*?>/g, '');
|
||||
};
|
||||
|
||||
var Error = exports.Error = function(error) {
|
||||
this.error = error;
|
||||
};
|
||||
sys.inherits(Error, Package);
|
||||
Error.prototype.toJSON = function() {
|
||||
return {
|
||||
type: 'error',
|
||||
error: this.error
|
||||
};
|
||||
};
|
||||
|
||||
var Success = exports.Success = function(success) {
|
||||
this.success = success;
|
||||
};
|
||||
sys.inherits(Success, Package);
|
||||
Success.prototype.toJSON = function() {
|
||||
return {
|
||||
type: 'success',
|
||||
success: this.success
|
||||
};
|
||||
};
|
||||
|
||||
var Message = exports.Message = function(from, body) {
|
||||
this.from = from;
|
||||
this.body = body;
|
||||
};
|
||||
sys.inherits(Message, Package);
|
||||
Message.prototype.toJSON = function() {
|
||||
return {
|
||||
type: 'message',
|
||||
user: this.from,
|
||||
body: this._sanitize(this.body)
|
||||
};
|
||||
};
|
||||
|
||||
var Notice = exports.Notice = function(username, info) {
|
||||
this.username = username;
|
||||
this.info = info;
|
||||
};
|
||||
sys.inherits(Notice, Package);
|
||||
Notice.prototype.toJSON = function() {
|
||||
return {
|
||||
type: 'notice',
|
||||
user: this.username,
|
||||
info: this.info
|
||||
};
|
||||
};
|
||||
|
||||
exports.TYPING_STATES = ['typing+', 'typing~', 'typing-'];
|
||||
exports.STATUSES = ['available', 'away', 'idle'];
|
||||
var Status = exports.Status = function(username, status, message) {
|
||||
var statuses = exports.STATUSES + exports.TYPING_STATES;
|
||||
|
||||
this.username = username;
|
||||
this.status = -~statuses.indexOf(status) ? status : statuses[0];
|
||||
this.message = message;
|
||||
};
|
||||
sys.inherits(Status, Package);
|
||||
Status.prototype.toJSON = function() {
|
||||
return {
|
||||
type: 'status',
|
||||
user: this.username,
|
||||
status: this.status,
|
||||
message: this._sanitize(this.message || '')
|
||||
};
|
||||
};
|
||||
|
||||
var Offline = exports.Offline = function(username) {
|
||||
this.username = username;
|
||||
};
|
||||
sys.inherits(Offline, Package);
|
||||
Offline.prototype.toJSON = function() {
|
||||
// A special type of status
|
||||
return {
|
||||
type: 'status',
|
||||
user: this.user.get('username'),
|
||||
status: 'offline',
|
||||
message: ''
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
var url = require('url'),
|
||||
Hub = require('./im/hub');
|
||||
|
||||
module.exports = function setupHub(options) {
|
||||
options = options || {};
|
||||
|
||||
store = new Hub(options);
|
||||
|
||||
return function session(req, res, next) {
|
||||
req.sessionStore = store;
|
||||
req.sessionID = req.cookies[options.authentication.cookie];
|
||||
|
||||
if(!req.cookies) {
|
||||
next(new Error('session requires cookieDecoder to work properly'));
|
||||
return;
|
||||
}
|
||||
|
||||
if(req.sessionID) {
|
||||
store.get(req.sessionID, function(err, sess) {
|
||||
if(err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!sess) {
|
||||
next(new Error(JSON.stringify({
|
||||
type: 'error',
|
||||
error: 'not authenticated'})));
|
||||
return;
|
||||
}
|
||||
|
||||
sess.touch();
|
||||
if(url.parse(req.url).pathname === '/listen') {
|
||||
req.connection.setTimeout((5).minutes);
|
||||
sess.listener(res);
|
||||
store.set(req.sessionID, sess);
|
||||
|
||||
if(msg = sess.message_queue.shift())
|
||||
sess._send.apply(sess, msg);
|
||||
} else {
|
||||
sess.connection = res;
|
||||
}
|
||||
|
||||
req.session = sess;
|
||||
res.find = store.find.bind(store);
|
||||
res.message = function(to, package) {
|
||||
store.message(req.session, to, package);
|
||||
};
|
||||
res.status = function(status) {
|
||||
req.session.status = status;
|
||||
};
|
||||
res.signOff = function() { store.signOff(req.sessionID); };
|
||||
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
next(new Error(JSON.stringify({
|
||||
type: 'error',
|
||||
error: 'not authenticated'})));
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
var events = require('events'),
|
||||
sys = require('sys'),
|
||||
packages = require('../../libs/packages'),
|
||||
User = require('./user');
|
||||
|
||||
var Hub = module.exports = function Hub(options) {
|
||||
this.events = new events.EventEmitter();
|
||||
this.auth = options.authentication;
|
||||
this.sessions = {};
|
||||
|
||||
this.maxAge = options.maxAge || 14400000;
|
||||
this.reapInterval = options.reapInterval || 60000;
|
||||
|
||||
if(this.reapInterval !== -1) {
|
||||
setInterval(function(self) {
|
||||
self.reap(self.maxAge);
|
||||
}, this.reapInterval, this);
|
||||
}
|
||||
|
||||
this.events.addListener('update', (function(package) {
|
||||
if(package.constructor === exports.Offline) {
|
||||
for(var i = 0, l = this.users.length; i < l; i++) {
|
||||
if(this.users[i].get('username') == package.user)
|
||||
this.users.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}).bind(this));
|
||||
};
|
||||
|
||||
Hub.prototype.destroy = function(sid, fn) {
|
||||
this.set(sid, null, fn);
|
||||
};
|
||||
|
||||
Hub.prototype.reap = function(ms) {
|
||||
var threshold = +new Date - ms,
|
||||
sids = Object.keys(this.sessions);
|
||||
for(var i = 0, len = sids.length; i < len; ++i) {
|
||||
var sid = sids[i], sess = this.sessions[sid];
|
||||
if(sess.lastAccess < threshold) {
|
||||
sess.signoff((function() {
|
||||
delete this.sessions[sid];
|
||||
}).bind(this));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Hub.prototype.get = function(sid, fn) {
|
||||
if(this.sessions[sid]) {
|
||||
fn(null, this.sessions[sid]);
|
||||
} else {
|
||||
this.auth.authenticate(sid, (function(data) {
|
||||
if(data) {
|
||||
var session = new User(sid, data);
|
||||
this.set(sid, session);
|
||||
|
||||
this.auth.friends(sid, data, (function(friends) {
|
||||
var friends_copy = friends.slice();
|
||||
Object.values(this.sessions).filter(function(friend) {
|
||||
return ~friends.indexOf(friend.data('username'));
|
||||
}).each(function(friend) {
|
||||
var username = friend.data('username');
|
||||
friends_copy[friends_copy.indexOf(username)] =
|
||||
[username, friend.data('status')];
|
||||
}, this);
|
||||
|
||||
session._friends(friends_copy);
|
||||
session.events.addListener('status', (function(value) {
|
||||
this.events.emit(
|
||||
'update',
|
||||
new packages.Status(session.data('username'), value)
|
||||
);
|
||||
}).bind(this));
|
||||
this.events.addListener('update',
|
||||
session.receivedUpdate.bind(session));
|
||||
this.set(sid, session);
|
||||
fn(null, session);
|
||||
}).bind(this));
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
}).bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
Hub.prototype.set = function(sid, sess, fn) {
|
||||
this.sessions[sid] = sess;
|
||||
fn && fn();
|
||||
};
|
||||
|
||||
Hub.prototype.find = function(username, fn) {
|
||||
for(var sid in this.sessions) {
|
||||
var session = this.sessions[sid],
|
||||
sess_username = session.data('username');
|
||||
if(sess_username == username) {
|
||||
fn(session);
|
||||
return;
|
||||
}
|
||||
}
|
||||
fn(false);
|
||||
};
|
||||
|
||||
Hub.prototype.message = function(from, to, package) {
|
||||
try {
|
||||
package.user = from;
|
||||
to.send(package);
|
||||
from.respond(new packages.Success('sent'));
|
||||
} catch(e) {
|
||||
from.respond(new packages.Error(e.description));
|
||||
}
|
||||
};
|
||||
|
||||
Hub.prototype.signOff = function(sid) {
|
||||
this.get(sid, function(session) {
|
||||
if(session) this.events.emit('update', new packages.Offline(this));
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
var events = require('events'),
|
||||
packages = require('../../libs/packages');
|
||||
|
||||
var User = module.exports = function(id, data) {
|
||||
this.id = id;
|
||||
this.connection = null;
|
||||
this.listeners = [];
|
||||
this.message_queue = [];
|
||||
this.convos = {};
|
||||
this._data = data;
|
||||
|
||||
this.events = new events.EventEmitter();
|
||||
this.status = packages.STATUSES[0];
|
||||
|
||||
setInterval(this._expireConns.bind(this), 500);
|
||||
};
|
||||
|
||||
User.prototype.receivedUpdate = function(package) {
|
||||
if(this.friends.indexOf(package.user))
|
||||
this.send(package);
|
||||
};
|
||||
|
||||
User.prototype._friends = function(friends) {
|
||||
this.friends = friends;
|
||||
this.send(JSON.stringify({type: 'hello', friends: friends}));
|
||||
};
|
||||
|
||||
User.prototype._expireConns = function() {
|
||||
var conn,
|
||||
noop = JSON.stringify({type: 'noop'});
|
||||
for(var i = 0; i < this.listeners.length; i++) {
|
||||
conn = this.listeners[i].connection;
|
||||
if((Date.now() - conn._idleStart) >= conn._idleTimeout - 2000) {
|
||||
this.listeners[i].writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': noop.length
|
||||
});
|
||||
this.listeners[i].end(noop);
|
||||
this.listeners.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
User.prototype.listener = function(conn) {
|
||||
this.listeners.push(conn);
|
||||
};
|
||||
|
||||
User.prototype.respond = function(code, message, callback) {
|
||||
this._send('connection', code, message, callback);
|
||||
};
|
||||
|
||||
User.prototype.send = function(code, message, callback) {
|
||||
this._send('listener', code, message, callback);
|
||||
};
|
||||
|
||||
User.prototype._send = function(type, code, message, callback) {
|
||||
if(!message && typeof code != 'number') {
|
||||
callback = message;
|
||||
message = code;
|
||||
code = 200;
|
||||
}
|
||||
|
||||
if(typeof message != 'string')
|
||||
message = JSON.stringify(message);
|
||||
|
||||
if(type == 'connection' && this.connection) {
|
||||
this.connection.writeHead(code || 200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': message.length
|
||||
});
|
||||
this.connection.end(message);
|
||||
} else {
|
||||
if(!this.listeners.length)
|
||||
return this.message_queue.push(arguments);
|
||||
|
||||
var notify_run, cx = this.listeners.slice();
|
||||
this.listeners = [];
|
||||
(notify_run = function(conn) {
|
||||
return function() {
|
||||
if(!conn) {
|
||||
if(callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
conn.writeHead(code || 200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': message.length
|
||||
});
|
||||
conn.end(message);
|
||||
notify_run(cx.shift());
|
||||
};
|
||||
})(cx.shift())();
|
||||
}
|
||||
};
|
||||
|
||||
User.prototype.data = function(key, def) {
|
||||
if(key == 'id') return this.id;
|
||||
return this._data[key] || this['_' + key] ||
|
||||
(typeof this[key] != 'function' && this[key]) ||
|
||||
def || false;
|
||||
};
|
||||
|
||||
User.prototype.touch = function() {
|
||||
this.lastAccess = +new Date;
|
||||
};
|
||||
|
||||
Object.defineProperty(User.prototype, 'status', {
|
||||
get: function() {
|
||||
return this._status;
|
||||
},
|
||||
set: function(value) {
|
||||
this._status = value;
|
||||
this.events.emit('status', value);
|
||||
}
|
||||
});
|
||||
@@ -1,235 +0,0 @@
|
||||
var utils = require('express/utils'),
|
||||
events = require('events'),
|
||||
chat = require('./chat'),
|
||||
sys = require('sys');
|
||||
|
||||
var User = Base.extend({
|
||||
constructor: function(id, data) {
|
||||
this.id = id;
|
||||
this.connection = null;
|
||||
this.listeners = [];
|
||||
this.message_queue = [];
|
||||
this.convos = {};
|
||||
this._data = data;
|
||||
|
||||
this.events = new events.EventEmitter();
|
||||
this.events.addListener('status', (function(value) {
|
||||
chat.AjaxIM.events.emit('update', new chat.Status(this, value));
|
||||
}).bind(this));
|
||||
|
||||
chat.AjaxIM.users.push(this);
|
||||
|
||||
this.status = chat.STATUSES[0];
|
||||
|
||||
Session.IM.authentication.friends(data.username, (function(friends) {
|
||||
friends.push(data.username);
|
||||
this.friends = friends;
|
||||
|
||||
var f_s = friends.slice();
|
||||
|
||||
chat.AjaxIM.users.filter(function(friend) {
|
||||
return ~~friends.indexOf(friend.get('username'));
|
||||
}).each(function(friend) {
|
||||
var username = friend.get('username');
|
||||
sys.puts(friend.get('status'));
|
||||
f_s[f_s.indexOf(username)] = [username, friend.get('status')];
|
||||
}, this);
|
||||
|
||||
this.notify(JSON.stringify({type: 'hello', friends: f_s}));
|
||||
}).bind(this));
|
||||
|
||||
chat.AjaxIM.events.addListener('update', (function(package) {
|
||||
if(this.friends.indexOf(package.user))
|
||||
this.notify(package);
|
||||
}).bind(this));
|
||||
|
||||
setInterval(this._expireConns.bind(this), 500);
|
||||
},
|
||||
|
||||
_expireConns: function() {
|
||||
var conn;
|
||||
for(var i = 0; i < this.listeners.length; i++) {
|
||||
conn = this.listeners[i].connection;
|
||||
if((Date.now() - conn._idleStart) >= conn._idleTimeout - 2000) {
|
||||
this.listeners[i].respond(200, JSON.encode({type: 'noop'}));
|
||||
this.listeners.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
connected: function(conn) {
|
||||
this.connection = conn;
|
||||
},
|
||||
|
||||
listener: function(conn) {
|
||||
this.listeners.push(conn);
|
||||
},
|
||||
|
||||
respond: function(code, message, callback) {
|
||||
this._send('connection', code, message, callback);
|
||||
},
|
||||
|
||||
notify: function(code, message, callback) {
|
||||
this._send('listener', code, message, callback);
|
||||
},
|
||||
|
||||
_send: function(type, code, message, callback) {
|
||||
if(!message && typeof code != 'number') {
|
||||
callback = message;
|
||||
message = code;
|
||||
code = 200;
|
||||
}
|
||||
|
||||
if(typeof message != 'string')
|
||||
message = message.toString();
|
||||
|
||||
if(type == 'connection' && this.connection) {
|
||||
this.connection.respond(code, message, 'UTF-8');
|
||||
} else {
|
||||
if(!this.listeners.length)
|
||||
return this.message_queue.push(arguments);
|
||||
|
||||
var notify_run, cx = this.listeners.slice();
|
||||
this.listeners = [];
|
||||
(notify_run = function(conn) {
|
||||
return function() {
|
||||
if(!conn) {
|
||||
if(callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
conn.respond(code, message, 'UTF-8',
|
||||
notify_run(cx.shift()));
|
||||
};
|
||||
})(cx.shift())();
|
||||
}
|
||||
},
|
||||
|
||||
signoff: function(callback) {
|
||||
chat.AjaxIM.events.emit('update', new chat.Offline(this));
|
||||
|
||||
if(callback) callback();
|
||||
},
|
||||
|
||||
get: function(key, def) {
|
||||
if(key == 'id') return this.id;
|
||||
else if(key in this._data)
|
||||
return this._data[key];
|
||||
else
|
||||
return this['_' + key] || def || false;
|
||||
},
|
||||
|
||||
get status() {
|
||||
return this._status;
|
||||
},
|
||||
|
||||
set status(value) {
|
||||
this._status = value;
|
||||
this.events.emit('status', value);
|
||||
}
|
||||
});
|
||||
|
||||
Store.Memory.IM = Store.Memory.extend({
|
||||
name: 'Memory.IM',
|
||||
|
||||
constructor: function(options) {
|
||||
Store.Memory.call(this);
|
||||
this.auth = options.authentication;
|
||||
},
|
||||
|
||||
fetch: function(req, callback) {
|
||||
var sid = req.cookie(this.auth.cookie),
|
||||
self = this;
|
||||
|
||||
if(sid && this.store[sid]) {
|
||||
callback(null, this.store[sid]);
|
||||
} else {
|
||||
this.generate(sid, req, function(err, session) {
|
||||
self.commit(session);
|
||||
callback(err, session);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
reap: function(ms) {
|
||||
var threshold = +new Date(Date.now() - ms),
|
||||
sids = Object.keys(this.store);
|
||||
for(var i = 0, len = sids.length; i < len; ++i) {
|
||||
if(this.store[sids[i]].lastAccess < threshold) {
|
||||
this.store[sids[i]].signoff((function() {
|
||||
this.destroy(sids[i]);
|
||||
}).bind(this));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
generate: function(sid, req, callback) {
|
||||
this.auth.authenticate(req, function(data) {
|
||||
if(data) {
|
||||
callback(null, new User(sid, data));
|
||||
} else {
|
||||
callback(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Session.IM = Plugin.extend({
|
||||
extend: {
|
||||
init: function(options) {
|
||||
this.cookie = {};
|
||||
Object.merge(this, options);
|
||||
this.store = new (this.dataStore || Store.Memory.IM)(options);
|
||||
this.startReaper();
|
||||
},
|
||||
|
||||
startReaper: function() {
|
||||
setInterval(function(self) {
|
||||
self.store.reap(self.lifetime || (1).day);
|
||||
}, this.reapInterval || this.reapEvery || (1).hour, this);
|
||||
},
|
||||
|
||||
get: function(session_id) {
|
||||
return this.store.store[session_id] || false;
|
||||
}
|
||||
},
|
||||
|
||||
on: {
|
||||
request: function(event, callback) {
|
||||
if(event.request.url.pathname === '/favicon.ico')
|
||||
return;
|
||||
|
||||
Session.IM.store.fetch(event.request, function(err, session) {
|
||||
if(err) return callback(err);
|
||||
|
||||
event.request.session = session;
|
||||
event.request.session.touch();
|
||||
|
||||
if(event.request.url.pathname == '/listen') {
|
||||
event.request.connection.setTimeout((5).minutes);
|
||||
session.listener(event.request);
|
||||
|
||||
Session.IM.store.commit(event.request.session);
|
||||
|
||||
if(msg = session.message_queue.shift())
|
||||
session._send.apply(session, msg);
|
||||
} else {
|
||||
session.connection = event.request;
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
response: function(event, callback) {
|
||||
if(event.request.session)
|
||||
return Session.IM.store.commit(
|
||||
event.request.session,
|
||||
callback),
|
||||
true;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,22 +1,15 @@
|
||||
// == Node.js Server Configuration ==
|
||||
// == Server Configuration ==
|
||||
//
|
||||
// This is the configuration file for the Node.js Ajax IM server. Here, you
|
||||
// can set which ports will be used for the public and internal servers,
|
||||
// as well as other settings such as the session cookie name and expiration.
|
||||
|
||||
// === {{{ host and port }}} ===
|
||||
// === Host and Port ===
|
||||
//
|
||||
// Define the host and port that Ajax IM will run on.
|
||||
APP_HOST = 'localhost';
|
||||
APP_PORT = 8000;
|
||||
|
||||
// === API Key ===
|
||||
//
|
||||
// This is the **private** API key that is used for any REST calls to the
|
||||
// server. Please change this key to something long and random. You should
|
||||
// never use this key on the client side!
|
||||
API_KEY = 'FG34tbNW$n5aw4E6Y&U&6inBFDs';
|
||||
|
||||
// === Authentication Library ===
|
||||
//
|
||||
// This is the library (from libs/authenticate/) that we will use to
|
||||
|
||||