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
Esse commit está contido em:
Joshua Gross
2010-07-25 17:27:50 -07:00
commit b7b5a874c4
45 arquivos alterados com 609 adições e 664 exclusões
-1
Ver Arquivo
@@ -1,4 +1,3 @@
local/*
*.local.*
*.local/
public/
-19
Ver Arquivo
@@ -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.
+51 -5
Ver Arquivo
@@ -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.
+66 -52
Ver Arquivo
@@ -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",
Ver Arquivo
Ver Arquivo

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>&bull;</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>
-73
Ver Arquivo
@@ -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
})()
+61 -80
Ver Arquivo
@@ -1,95 +1,76 @@
// = Ajax IM =
//
// **Copyright &copy; 2005 &ndash; 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);
});
-179
Ver Arquivo
@@ -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();
}
});
+15
Ver Arquivo
@@ -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())
});
});
};
Link simbólico
+1
Ver Arquivo
@@ -0,0 +1 @@
../../../client/js/
+1
Ver Arquivo
@@ -0,0 +1 @@
../../../client/themes/
+13
Ver Arquivo
@@ -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.
+1
Ver Arquivo
@@ -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',
+88
Ver Arquivo
@@ -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: ''
};
};
+62
Ver Arquivo
@@ -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'})));
}
};
};
+116
Ver Arquivo
@@ -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));
});
};
+116
Ver Arquivo
@@ -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);
}
});
-235
Ver Arquivo
@@ -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;
}
}
});
+2 -9
Ver Arquivo
@@ -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