jschat 0.1.5 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/jschat/client.rb +1 -1
- data/lib/jschat/http/helpers/url_for.rb +38 -0
- data/lib/jschat/http/jschat.rb +172 -9
- data/lib/jschat/http/public/javascripts/app/controllers/chat_controller.js +59 -7
- data/lib/jschat/http/public/javascripts/app/protocol/change.js +2 -2
- data/lib/jschat/http/public/javascripts/app/protocol/display.js +15 -9
- data/lib/jschat/http/public/javascripts/app/ui/commands.js +2 -0
- data/lib/jschat/http/public/javascripts/init.js +1 -1
- data/lib/jschat/http/public/stylesheets/screen.css +1 -0
- data/lib/jschat/http/views/form.erb +7 -0
- data/lib/jschat/http/views/index.erb +6 -5
- data/lib/jschat/http/views/twitter.erb +6 -0
- data/lib/jschat/init.rb +19 -0
- data/lib/jschat/server.rb +46 -41
- data/lib/jschat/server_options.rb +2 -0
- data/lib/jschat/storage/mongo.rb +12 -8
- data/lib/jschat/storage/null.rb +11 -5
- data/test/test_helper.rb +7 -2
- metadata +7 -3
data/lib/jschat/client.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
# From http://github.com/emk/sinatra-url-for/blob/master/lib/sinatra/url_for.rb
|
2
|
+
module Sinatra
|
3
|
+
module UrlForHelper
|
4
|
+
# Construct a link to +url_fragment+, which should be given relative to
|
5
|
+
# the base of this Sinatra app. The mode should be either
|
6
|
+
# <code>:path_only</code>, which will generate an absolute path within
|
7
|
+
# the current domain (the default), or <code>:full</code>, which will
|
8
|
+
# include the site name and port number. (The latter is typically
|
9
|
+
# necessary for links in RSS feeds.) Example usage:
|
10
|
+
#
|
11
|
+
# url_for "/" # Returns "/myapp/"
|
12
|
+
# url_for "/foo" # Returns "/myapp/foo"
|
13
|
+
# url_for "/foo", :full # Returns "http://example.com/myapp/foo"
|
14
|
+
#--
|
15
|
+
# See README.rdoc for a list of some of the people who helped me clean
|
16
|
+
# up earlier versions of this code.
|
17
|
+
def url_for url_fragment, mode=:path_only
|
18
|
+
case mode
|
19
|
+
when :path_only
|
20
|
+
base = request.script_name
|
21
|
+
when :full
|
22
|
+
scheme = request.scheme
|
23
|
+
if (scheme == 'http' && request.port == 80 ||
|
24
|
+
scheme == 'https' && request.port == 443)
|
25
|
+
port = ""
|
26
|
+
else
|
27
|
+
port = ":#{request.port}"
|
28
|
+
end
|
29
|
+
base = "#{scheme}://#{request.host}#{port}#{request.script_name}"
|
30
|
+
else
|
31
|
+
raise TypeError, "Unknown url_for mode #{mode}"
|
32
|
+
end
|
33
|
+
"#{base}#{url_fragment}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
helpers UrlForHelper
|
38
|
+
end
|
data/lib/jschat/http/jschat.rb
CHANGED
@@ -3,13 +3,64 @@ require 'sinatra'
|
|
3
3
|
require 'sha1'
|
4
4
|
require 'json'
|
5
5
|
require 'sprockets'
|
6
|
-
require 'jschat/
|
6
|
+
require 'jschat/init'
|
7
|
+
require 'jschat/http/helpers/url_for'
|
7
8
|
|
8
9
|
set :public, File.join(File.dirname(__FILE__), 'public')
|
9
10
|
set :views, File.join(File.dirname(__FILE__), 'views')
|
11
|
+
set :sessions, true
|
12
|
+
|
13
|
+
module JsChat::Auth
|
14
|
+
end
|
15
|
+
|
16
|
+
module JsChat::Auth::Twitter
|
17
|
+
def self.template
|
18
|
+
:twitter
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.load
|
22
|
+
require 'twitter_oauth'
|
23
|
+
@loaded = true
|
24
|
+
rescue LoadError
|
25
|
+
puts 'Error: twitter_oauth gem not found'
|
26
|
+
@loaded = false
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.loaded?
|
30
|
+
@loaded
|
31
|
+
end
|
32
|
+
end
|
10
33
|
|
11
34
|
module JsChat
|
12
35
|
class ConnectionError < Exception ; end
|
36
|
+
|
37
|
+
def self.configure_authenticators
|
38
|
+
if ServerConfig['twitter']
|
39
|
+
JsChat::Auth::Twitter.load
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.init
|
44
|
+
configure_authenticators
|
45
|
+
JsChat.init_storage
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
JsChat.init
|
50
|
+
|
51
|
+
before do
|
52
|
+
if JsChat::Auth::Twitter.loaded?
|
53
|
+
@twitter = TwitterOAuth::Client.new(
|
54
|
+
:consumer_key => ServerConfig['twitter']['key'],
|
55
|
+
:consumer_secret => ServerConfig['twitter']['secret'],
|
56
|
+
:token => session[:access_token],
|
57
|
+
:secret => session[:secret_token]
|
58
|
+
)
|
59
|
+
|
60
|
+
if twitter_user?
|
61
|
+
load_twitter_user_and_set_bridge_id
|
62
|
+
end
|
63
|
+
end
|
13
64
|
end
|
14
65
|
|
15
66
|
# todo: can this be async and allow the server to have multiple threads?
|
@@ -29,8 +80,8 @@ class JsChat::Bridge
|
|
29
80
|
@cookie = response['cookie']
|
30
81
|
end
|
31
82
|
|
32
|
-
def identify(name, ip)
|
33
|
-
response = send_json({ :identify => name, :ip => ip })
|
83
|
+
def identify(name, ip, session_length = nil)
|
84
|
+
response = send_json({ :identify => name, :ip => ip, :session_length => session_length })
|
34
85
|
if response['display'] == 'error'
|
35
86
|
@identification_error = response
|
36
87
|
false
|
@@ -52,6 +103,10 @@ class JsChat::Bridge
|
|
52
103
|
send_json({ 'since' => room })['messages']
|
53
104
|
end
|
54
105
|
|
106
|
+
def room_update_times
|
107
|
+
send_json({ 'times' => 'all' })
|
108
|
+
end
|
109
|
+
|
55
110
|
def join(room)
|
56
111
|
send_json({ :join => room }, false)
|
57
112
|
end
|
@@ -119,13 +174,13 @@ helpers do
|
|
119
174
|
end
|
120
175
|
|
121
176
|
def load_bridge
|
122
|
-
@bridge = JsChat::Bridge.new
|
177
|
+
@bridge = JsChat::Bridge.new session[:jschat_id]
|
123
178
|
end
|
124
179
|
|
125
180
|
def load_and_connect
|
126
|
-
@bridge = JsChat::Bridge.new
|
181
|
+
@bridge = JsChat::Bridge.new session[:jschat_id]
|
127
182
|
@bridge.connect
|
128
|
-
|
183
|
+
session[:jschat_id] = @bridge.cookie
|
129
184
|
end
|
130
185
|
|
131
186
|
def save_last_room(room)
|
@@ -152,7 +207,44 @@ helpers do
|
|
152
207
|
|
153
208
|
def clear_cookies
|
154
209
|
response.set_cookie 'last-room', nil
|
155
|
-
|
210
|
+
session[:jschat_id] = nil
|
211
|
+
session[:request_token] = nil
|
212
|
+
session[:request_token_secret] = nil
|
213
|
+
session[:access_token] = nil
|
214
|
+
session[:secret_token] = nil
|
215
|
+
session[:twitter_name] = nil
|
216
|
+
end
|
217
|
+
|
218
|
+
def twitter_user?
|
219
|
+
session[:access_token] && session[:secret_token]
|
220
|
+
end
|
221
|
+
|
222
|
+
def save_twitter_user(options = {})
|
223
|
+
options = load_twitter_user.merge(options).merge({
|
224
|
+
'name' => nickname,
|
225
|
+
'twitter_name' => session[:twitter_name],
|
226
|
+
'access_token' => session[:access_token],
|
227
|
+
'secret_token' => session[:secret_token]
|
228
|
+
})
|
229
|
+
JsChat::Storage.driver.save_user(options)
|
230
|
+
end
|
231
|
+
|
232
|
+
def save_twitter_user_rooms
|
233
|
+
if twitter_user?
|
234
|
+
rooms = @bridge.rooms
|
235
|
+
save_twitter_user('rooms' => rooms)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def load_twitter_user
|
240
|
+
JsChat::Storage.driver.find_user({ 'twitter_name' => session[:twitter_name] }) || {}
|
241
|
+
end
|
242
|
+
|
243
|
+
def load_twitter_user_and_set_bridge_id
|
244
|
+
user = load_twitter_user
|
245
|
+
if user['jschat_id'] and user['jschat_id'].size > 0
|
246
|
+
response.set_cookie 'jschat_id', user['jschat_id']
|
247
|
+
end
|
156
248
|
end
|
157
249
|
|
158
250
|
def nickname
|
@@ -202,6 +294,13 @@ get '/messages' do
|
|
202
294
|
end
|
203
295
|
end
|
204
296
|
|
297
|
+
get '/room_update_times' do
|
298
|
+
load_bridge
|
299
|
+
if @bridge.active?
|
300
|
+
messages_js @bridge.room_update_times
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
205
304
|
get '/names' do
|
206
305
|
load_bridge
|
207
306
|
save_last_room params['room']
|
@@ -220,13 +319,14 @@ post '/join' do
|
|
220
319
|
load_bridge
|
221
320
|
@bridge.join params['room']
|
222
321
|
save_last_room params['room']
|
322
|
+
save_twitter_user_rooms
|
223
323
|
'OK'
|
224
324
|
end
|
225
325
|
|
226
326
|
get '/part' do
|
227
327
|
load_bridge
|
228
328
|
@bridge.part params['room']
|
229
|
-
|
329
|
+
save_twitter_user_rooms
|
230
330
|
if @bridge.last_error
|
231
331
|
error 500, [@bridge.last_error].to_json
|
232
332
|
else
|
@@ -270,7 +370,70 @@ end
|
|
270
370
|
|
271
371
|
get '/rooms' do
|
272
372
|
load_bridge
|
273
|
-
@bridge.rooms
|
373
|
+
rooms = @bridge.rooms
|
374
|
+
save_twitter_user('rooms' => rooms) if twitter_user?
|
375
|
+
rooms.to_json
|
376
|
+
end
|
377
|
+
|
378
|
+
get '/twitter' do
|
379
|
+
request_token = @twitter.request_token(
|
380
|
+
:oauth_callback => url_for('/twitter_auth', :full)
|
381
|
+
)
|
382
|
+
session[:request_token] = request_token.token
|
383
|
+
session[:request_token_secret] = request_token.secret
|
384
|
+
redirect request_token.authorize_url.gsub('authorize', 'authenticate')
|
385
|
+
end
|
386
|
+
|
387
|
+
get '/twitter_auth' do
|
388
|
+
# Exchange the request token for an access token.
|
389
|
+
begin
|
390
|
+
@access_token = @twitter.authorize(
|
391
|
+
session[:request_token],
|
392
|
+
session[:request_token_secret],
|
393
|
+
:oauth_verifier => params[:oauth_verifier]
|
394
|
+
)
|
395
|
+
rescue OAuth::Unauthorized => exception
|
396
|
+
puts exception
|
397
|
+
end
|
398
|
+
|
399
|
+
if @twitter.authorized?
|
400
|
+
session[:access_token] = @access_token.token
|
401
|
+
session[:secret_token] = @access_token.secret
|
402
|
+
session[:twitter_name] = @twitter.info['screen_name']
|
403
|
+
|
404
|
+
# TODO: Make this cope if someone has the same name
|
405
|
+
room = '#jschat'
|
406
|
+
save_nickname @twitter.info['screen_name']
|
407
|
+
user = load_twitter_user
|
408
|
+
session[:jschat_id] = user['jschat_id'] if user['jschat_id'] and !user['jschat_id'].empty?
|
409
|
+
save_twitter_user('twitter_name' => @twitter.info['screen_name'], 'jschat_id' => session[:jschat_id])
|
410
|
+
user = load_twitter_user
|
411
|
+
|
412
|
+
load_bridge
|
413
|
+
if @bridge.active?
|
414
|
+
if user['rooms'] and user['rooms'].any?
|
415
|
+
room = user['rooms'].first
|
416
|
+
end
|
417
|
+
else
|
418
|
+
session[:jschat_id] = nil
|
419
|
+
load_and_connect
|
420
|
+
save_twitter_user('jschat_id' => session[:jschat_id])
|
421
|
+
@bridge.identify(@twitter.info['screen_name'], request.ip, (((60 * 60) * 24) * 7))
|
422
|
+
if user['rooms']
|
423
|
+
user['rooms'].each do |room|
|
424
|
+
@bridge.join room
|
425
|
+
end
|
426
|
+
room = user['rooms'].first
|
427
|
+
else
|
428
|
+
save_last_room '#jschat'
|
429
|
+
@bridge.join '#jschat'
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
redirect "/chat/#{room}"
|
434
|
+
else
|
435
|
+
redirect '/'
|
436
|
+
end
|
274
437
|
end
|
275
438
|
|
276
439
|
# This serves the JavaScript concat'd by Sprockets
|
@@ -14,6 +14,42 @@ JsChat.ChatController = Class.create({
|
|
14
14
|
$('messages').observe('scroll', this.messagesScrolled.bindAsEventListener(this));
|
15
15
|
$$('#rooms li.join a').first().observe('click', this.joinRoomClicked.bindAsEventListener(this));
|
16
16
|
Event.observe(document, 'click', this.roomTabClick.bindAsEventListener(this));
|
17
|
+
this.allRecentMessages();
|
18
|
+
},
|
19
|
+
|
20
|
+
allRecentMessages: function() {
|
21
|
+
new Ajax.Request('/room_update_times', {
|
22
|
+
method: 'get',
|
23
|
+
onComplete: function(request) {
|
24
|
+
var times = request.responseText.evalJSON();
|
25
|
+
$H(this.lastUpdateTimes).each(function(data) {
|
26
|
+
var room = data[0],
|
27
|
+
time = data[1];
|
28
|
+
if (Date.parse(time) < Date.parse(times[room])) {
|
29
|
+
this.roomTabAlert(room);
|
30
|
+
}
|
31
|
+
}.bind(this));
|
32
|
+
this.lastUpdateTimes = times;
|
33
|
+
}.bind(this)
|
34
|
+
});
|
35
|
+
},
|
36
|
+
|
37
|
+
roomTabAlert: function(room) {
|
38
|
+
if (room === PageHelper.currentRoom()) return;
|
39
|
+
|
40
|
+
$$('ul#rooms li a').each(function(roomLink) {
|
41
|
+
if (roomLink.innerHTML === room) {
|
42
|
+
roomLink.addClassName('new');
|
43
|
+
}
|
44
|
+
});
|
45
|
+
},
|
46
|
+
|
47
|
+
clearRoomTabAlert: function(room) {
|
48
|
+
$$('ul#rooms li a').each(function(roomLink) {
|
49
|
+
if (roomLink.innerHTML === room) {
|
50
|
+
roomLink.removeClassName('new');
|
51
|
+
}
|
52
|
+
});
|
17
53
|
},
|
18
54
|
|
19
55
|
joinRoomClicked: function(e) {
|
@@ -77,7 +113,7 @@ JsChat.ChatController = Class.create({
|
|
77
113
|
if (message.match(/^\//)) {
|
78
114
|
Display.add_message('Error: Command not found. Use /help display commands.', 'error');
|
79
115
|
} else {
|
80
|
-
Display.message({ 'message': message.escapeHTML(), 'user': $('name').innerHTML },
|
116
|
+
Display.message({ 'message': message.escapeHTML(), 'user': $('name').innerHTML }, new Date());
|
81
117
|
new Ajax.Request('/message', {
|
82
118
|
method: 'post',
|
83
119
|
parameters: { 'message': message, 'to': PageHelper.currentRoom() }
|
@@ -112,8 +148,8 @@ JsChat.ChatController = Class.create({
|
|
112
148
|
});
|
113
149
|
|
114
150
|
this.createPollers();
|
151
|
+
this.getRoomList(this.addRoomAndCheckSelected);
|
115
152
|
this.joinRoom(PageHelper.currentRoom());
|
116
|
-
this.getRoomList(this.addRoomToNav);
|
117
153
|
},
|
118
154
|
|
119
155
|
getRoomList: function(callback) {
|
@@ -121,8 +157,12 @@ JsChat.ChatController = Class.create({
|
|
121
157
|
method: 'get',
|
122
158
|
parameters: { time: new Date().getTime() },
|
123
159
|
onComplete: function(response) {
|
124
|
-
response.responseText.evalJSON().each(function(roomName) {
|
125
|
-
|
160
|
+
response.responseText.evalJSON().sort().each(function(roomName) {
|
161
|
+
try {
|
162
|
+
callback.apply(this, [roomName]);
|
163
|
+
} catch (exception) {
|
164
|
+
console.log(exception);
|
165
|
+
}
|
126
166
|
}.bind(this));
|
127
167
|
}.bind(this)
|
128
168
|
});
|
@@ -138,7 +178,7 @@ JsChat.ChatController = Class.create({
|
|
138
178
|
},
|
139
179
|
onComplete: function() {
|
140
180
|
// Make the server update the last polled time
|
141
|
-
JsChat.Request.get('/messages');
|
181
|
+
JsChat.Request.get('/messages', function() {});
|
142
182
|
document.title = PageHelper.title();
|
143
183
|
UserCommands['/lastlog'].apply(this);
|
144
184
|
$('loading').hide();
|
@@ -181,6 +221,10 @@ JsChat.ChatController = Class.create({
|
|
181
221
|
$('rooms').insert({ bottom: '<li#{classAttribute}><a href="#{roomName}">#{roomName}</a></li>'.interpolate({ classAttribute: classAttribute, roomName: roomName }) });
|
182
222
|
},
|
183
223
|
|
224
|
+
addRoomAndCheckSelected: function(roomName) {
|
225
|
+
this.addRoomToNav(roomName, PageHelper.currentRoom() == roomName);
|
226
|
+
},
|
227
|
+
|
184
228
|
removeSelectedTab: function() {
|
185
229
|
$$('#rooms .selected').invoke('removeClassName', 'selected');
|
186
230
|
},
|
@@ -197,6 +241,7 @@ JsChat.ChatController = Class.create({
|
|
197
241
|
this.removeSelectedTab();
|
198
242
|
PageHelper.setCurrentRoomName(roomName);
|
199
243
|
this.joinRoom(roomName);
|
244
|
+
$('message').focus();
|
200
245
|
},
|
201
246
|
|
202
247
|
switchRoom: function(roomName) {
|
@@ -208,6 +253,8 @@ JsChat.ChatController = Class.create({
|
|
208
253
|
this.selectRoomTab(roomName);
|
209
254
|
PageHelper.setCurrentRoomName(roomName);
|
210
255
|
UserCommands['/lastlog'].apply(this);
|
256
|
+
this.clearRoomTabAlert(roomName);
|
257
|
+
$('message').focus();
|
211
258
|
},
|
212
259
|
|
213
260
|
rooms: function() {
|
@@ -263,6 +310,10 @@ JsChat.ChatController = Class.create({
|
|
263
310
|
},
|
264
311
|
|
265
312
|
updateMessages: function() {
|
313
|
+
if (this.pausePollers) {
|
314
|
+
return;
|
315
|
+
}
|
316
|
+
|
266
317
|
new Ajax.Request('/messages', {
|
267
318
|
method: 'get',
|
268
319
|
parameters: { time: new Date().getTime(), room: PageHelper.currentRoom() },
|
@@ -284,9 +335,9 @@ JsChat.ChatController = Class.create({
|
|
284
335
|
json_set.each(function(json) {
|
285
336
|
try {
|
286
337
|
if (json['change']) {
|
287
|
-
Change[json['change']](json[json['change']]);
|
338
|
+
Change[json['change']](json[json['change']], json['time']);
|
288
339
|
} else {
|
289
|
-
Display[json['display']](json[json['display']]);
|
340
|
+
Display[json['display']](json[json['display']], json['time']);
|
290
341
|
if (json['display'] !== 'error' && typeof successCallback !== 'undefined') {
|
291
342
|
successCallback();
|
292
343
|
}
|
@@ -316,5 +367,6 @@ JsChat.ChatController = Class.create({
|
|
316
367
|
this.pollers = $A();
|
317
368
|
this.pollers.push(new PeriodicalExecuter(this.updateMessages.bind(this), 3));
|
318
369
|
this.pollers.push(new PeriodicalExecuter(this.checkIdleNames.bind(this), 5));
|
370
|
+
this.pollers.push(new PeriodicalExecuter(this.allRecentMessages.bind(this), 10));
|
319
371
|
}
|
320
372
|
});
|
@@ -1,11 +1,11 @@
|
|
1
1
|
var Change = {
|
2
|
-
user: function(user) {
|
2
|
+
user: function(user, time) {
|
3
3
|
if (user['name']) {
|
4
4
|
change = $H(user['name']).toArray()[0];
|
5
5
|
var old = change[0],
|
6
6
|
new_value = change[1];
|
7
7
|
if (new_value !== PageHelper.nickname()) {
|
8
|
-
Display.add_message("#{old} is now known as #{new_value}".interpolate({ old: old, new_value: new_value }), 'server',
|
8
|
+
Display.add_message("#{old} is now known as #{new_value}".interpolate({ old: old, new_value: new_value }), 'server', time);
|
9
9
|
}
|
10
10
|
$$('#names li').each(function(element) {
|
11
11
|
if (element.innerHTML == old) element.innerHTML = new_value;
|
@@ -13,7 +13,7 @@ var Display = {
|
|
13
13
|
}.bind(this));
|
14
14
|
},
|
15
15
|
|
16
|
-
message: function(message) {
|
16
|
+
message: function(message, time) {
|
17
17
|
var name = $('name').innerHTML;
|
18
18
|
var user_class = name == message['user'] ? 'user active' : 'user';
|
19
19
|
var text = '<span class="\#{user_class}">\#{user}</span> <span class="\#{message_class}">\#{message}</span>';
|
@@ -24,9 +24,15 @@ var Display = {
|
|
24
24
|
|
25
25
|
Display.clearIdleState(message['user']);
|
26
26
|
|
27
|
-
text = text.interpolate({
|
28
|
-
|
27
|
+
text = text.interpolate({
|
28
|
+
user_class: user_class,
|
29
|
+
room: message['room'],
|
30
|
+
user: TextHelper.truncateName(message['user']),
|
31
|
+
message: TextHelper.decorateMessage(message['message']),
|
32
|
+
message_class: 'message'
|
33
|
+
});
|
29
34
|
|
35
|
+
this.add_message(text, 'message', time);
|
30
36
|
this.addImageOnLoads();
|
31
37
|
|
32
38
|
if (this.show_unread) {
|
@@ -110,9 +116,9 @@ var Display = {
|
|
110
116
|
$('room-name').title = PageHelper.currentRoom();
|
111
117
|
},
|
112
118
|
|
113
|
-
join_notice: function(join) {
|
119
|
+
join_notice: function(join, time) {
|
114
120
|
this.add_user(join['user']);
|
115
|
-
this.add_message(join['user'] + ' has joined the room', 'server',
|
121
|
+
this.add_message(join['user'] + ' has joined the room', 'server', time);
|
116
122
|
},
|
117
123
|
|
118
124
|
add_user: function(name) {
|
@@ -127,14 +133,14 @@ var Display = {
|
|
127
133
|
}
|
128
134
|
},
|
129
135
|
|
130
|
-
part_notice: function(part) {
|
136
|
+
part_notice: function(part, time) {
|
131
137
|
this.remove_user(part['user']);
|
132
|
-
this.add_message(part['user'] + ' has left the room', 'server',
|
138
|
+
this.add_message(part['user'] + ' has left the room', 'server', time);
|
133
139
|
},
|
134
140
|
|
135
|
-
quit_notice: function(quit) {
|
141
|
+
quit_notice: function(quit, time) {
|
136
142
|
this.remove_user(quit['user']);
|
137
|
-
this.add_message(quit['user'] + ' has quit', 'server',
|
143
|
+
this.add_message(quit['user'] + ' has quit', 'server', time);
|
138
144
|
},
|
139
145
|
|
140
146
|
notice: function(notice) {
|
@@ -27,11 +27,13 @@ var UserCommands = {
|
|
27
27
|
},
|
28
28
|
|
29
29
|
'/lastlog': function() {
|
30
|
+
this.pausePollers = true;
|
30
31
|
$('messages').innerHTML = '';
|
31
32
|
JsChat.Request.get('/lastlog', function(transport) {
|
32
33
|
this.displayMessages(transport.responseText);
|
33
34
|
$('names').innerHTML = '';
|
34
35
|
this.updateNames();
|
36
|
+
this.pausePollers = false;
|
35
37
|
}.bind(this));
|
36
38
|
},
|
37
39
|
|
@@ -20,6 +20,7 @@ h1 { text-align: left; margin-left: 20px }
|
|
20
20
|
.header .rooms li { float: left; margin: 0 1px 0 0; padding: 2px 6px 3px 6px; background-color: #f0f0f0; border: 1px solid #aaa; border-bottom: #fff; font-size: 90%; height: 18px }
|
21
21
|
.header .rooms li.selected { background-color: #fff; border: 1px solid #ccc; border-bottom: #fff }
|
22
22
|
.header .rooms li a { color: #777; text-decoration: none }
|
23
|
+
.header .rooms li a.new { color: #990000; font-weight: bold; }
|
23
24
|
.header .rooms li a:hover { color: #000 }
|
24
25
|
.header .rooms li.selected a { color: #444 }
|
25
26
|
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<div class="content">
|
2
|
+
<form method="post" action="/identify" id="sign-on">
|
3
|
+
Enter name: <input name="name" id="name" value="" type="text" />
|
4
|
+
and room: <input name="room" id="room" value="#jschat" type="text" />
|
5
|
+
<input type="submit" value="Go" id="sign-on-submit" />
|
6
|
+
</form>
|
7
|
+
</div>
|
@@ -5,17 +5,18 @@
|
|
5
5
|
<p>Read more on the <a href="http://blog.jschat.org">JsChat Blog</a>.</p>
|
6
6
|
<h2>Try JsChat Now</h2>
|
7
7
|
<div id="feedback" class="error" style="display: none"></div>
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
</form>
|
8
|
+
<%= erb :form, :layout => false %>
|
9
|
+
<% if JsChat::Auth::Twitter.loaded? %>
|
10
|
+
<%= erb JsChat::Auth::Twitter.template, :layout => false %>
|
11
|
+
<% end %>
|
13
12
|
<h2>Features</h2>
|
14
13
|
<ul>
|
15
14
|
<li>Simple protocol that makes it easy to implement clients and bots</li>
|
16
15
|
<li>Console client designed to look and feel like IRC clients</li>
|
17
16
|
<li>Web client auto links and displays images/YouTube videos inline</li>
|
18
17
|
<li>Protocol designed to be close to executable code, so creating clients and bots is easy</li>
|
18
|
+
<li>Optional mongodb logging</li>
|
19
|
+
<li>Optional Twitter authentication</li>
|
19
20
|
</ul>
|
20
21
|
</div>
|
21
22
|
<div class="footer">
|
@@ -0,0 +1,6 @@
|
|
1
|
+
<h2>Login with Twitter</h2>
|
2
|
+
|
3
|
+
<p>JsChat will save your rooms and keep your presence online until you click <strong>Quit</strong>.</p>
|
4
|
+
<p>This will persist even if you login on another computer.</p>
|
5
|
+
|
6
|
+
<form method="get" action="/twitter"><input type="submit" value="Login with Twitter" /></form>
|
data/lib/jschat/init.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module JsChat ; end
|
2
|
+
|
3
|
+
require 'jschat/server_options'
|
4
|
+
require 'jschat/storage/init'
|
5
|
+
|
6
|
+
module JsChat
|
7
|
+
STATELESS_TIMEOUT = 60
|
8
|
+
LASTLOG_DEFAULT = 100
|
9
|
+
|
10
|
+
def self.init_storage
|
11
|
+
if JsChat::Storage::MongoDriver.available?
|
12
|
+
JsChat::Storage.enabled = true
|
13
|
+
JsChat::Storage.driver = JsChat::Storage::MongoDriver
|
14
|
+
else
|
15
|
+
JsChat::Storage.enabled = false
|
16
|
+
JsChat::Storage.driver = JsChat::Storage::NullDriver
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/jschat/server.rb
CHANGED
@@ -5,13 +5,11 @@ require 'time'
|
|
5
5
|
require 'socket'
|
6
6
|
|
7
7
|
# JsChat libraries
|
8
|
+
require 'jschat/init'
|
8
9
|
require 'jschat/errors'
|
9
10
|
require 'jschat/flood_protection'
|
10
|
-
require 'jschat/storage/init'
|
11
11
|
|
12
12
|
module JsChat
|
13
|
-
STATELESS_TIMEOUT = 60
|
14
|
-
|
15
13
|
module Server
|
16
14
|
def self.pid_file_name
|
17
15
|
File.join(ServerConfig['tmp_files'], 'jschat.pid')
|
@@ -26,23 +24,13 @@ module JsChat
|
|
26
24
|
FileUtils.rm pid_file_name
|
27
25
|
end
|
28
26
|
|
29
|
-
def self.init_storage
|
30
|
-
if JsChat::Storage::MongoDriver.available?
|
31
|
-
JsChat::Storage.enabled = true
|
32
|
-
JsChat::Storage.driver = JsChat::Storage::MongoDriver
|
33
|
-
else
|
34
|
-
JsChat::Storage.enabled = false
|
35
|
-
JsChat::Storage.driver = JsChat::Storage::NullDriver
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
27
|
def self.stop
|
40
28
|
rm_pid_file
|
41
29
|
end
|
42
30
|
|
43
31
|
def self.run!
|
44
32
|
write_pid_file
|
45
|
-
init_storage
|
33
|
+
JsChat.init_storage
|
46
34
|
|
47
35
|
at_exit do
|
48
36
|
stop
|
@@ -58,7 +46,7 @@ module JsChat
|
|
58
46
|
include JsChat::FloodProtection
|
59
47
|
|
60
48
|
attr_accessor :name, :connection, :rooms, :last_activity,
|
61
|
-
:identified, :ip, :last_poll
|
49
|
+
:identified, :ip, :last_poll, :session_length
|
62
50
|
|
63
51
|
def initialize(connection)
|
64
52
|
@name = nil
|
@@ -68,6 +56,18 @@ module JsChat
|
|
68
56
|
@last_poll = Time.now.utc
|
69
57
|
@identified = false
|
70
58
|
@ip = ''
|
59
|
+
@expires = nil
|
60
|
+
@session_length = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def session_expired?
|
64
|
+
return true if @expires.nil?
|
65
|
+
Time.now.utc >= @expires
|
66
|
+
end
|
67
|
+
|
68
|
+
def update_session_expiration
|
69
|
+
return if @session_length.nil?
|
70
|
+
@expires = Time.now.utc + @session_length
|
71
71
|
end
|
72
72
|
|
73
73
|
def to_json
|
@@ -147,33 +147,24 @@ module JsChat
|
|
147
147
|
{ 'display' => 'messages', 'messages' => messages_since(since) }
|
148
148
|
end
|
149
149
|
|
150
|
+
def last_update_time
|
151
|
+
message = JsChat::Storage.driver.lastlog(LASTLOG_DEFAULT, name).last
|
152
|
+
message['time'] if message
|
153
|
+
end
|
154
|
+
|
150
155
|
def messages_since(since)
|
151
|
-
messages = JsChat::Storage.driver.lastlog(
|
156
|
+
messages = JsChat::Storage.driver.lastlog(LASTLOG_DEFAULT, name)
|
152
157
|
if since.nil?
|
153
158
|
messages
|
154
159
|
else
|
155
|
-
messages.select { |m|
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def message_time(message)
|
160
|
-
if message.has_key? 'display'
|
161
|
-
message[message['display']]['time']
|
162
|
-
elsif message.has_key? 'change'
|
163
|
-
message[message['change']]['time']
|
164
|
-
else
|
165
|
-
Time.now
|
160
|
+
messages.select { |m| m['time'] && m['time'] > since }
|
166
161
|
end
|
167
162
|
end
|
168
163
|
|
169
164
|
def add_to_lastlog(message)
|
170
165
|
if message
|
171
|
-
|
172
|
-
|
173
|
-
elsif message.has_key? 'change'
|
174
|
-
message[message['change']]['time'] = Time.now.utc
|
175
|
-
end
|
176
|
-
JsChat::Storage.driver.log message
|
166
|
+
message['time'] = Time.now.utc
|
167
|
+
JsChat::Storage.driver.log message, name
|
177
168
|
end
|
178
169
|
end
|
179
170
|
|
@@ -256,14 +247,16 @@ module JsChat
|
|
256
247
|
end
|
257
248
|
|
258
249
|
# {"identify":"alex"}
|
259
|
-
def identify(name, ip, options = {})
|
250
|
+
def identify(name, ip, session_length, options = {})
|
260
251
|
if @user and @user.identified
|
261
252
|
Error.new :already_identified, 'You have already identified'
|
262
253
|
elsif name_taken? name
|
263
254
|
Error.new :name_taken, 'Name already taken'
|
264
255
|
else
|
265
256
|
@user.name = name
|
266
|
-
@user.ip
|
257
|
+
@user.ip = ip
|
258
|
+
@user.session_length = session_length
|
259
|
+
@user.update_session_expiration
|
267
260
|
register_stateless_user if @stateless
|
268
261
|
{ 'display' => 'identified', 'identified' => @user }
|
269
262
|
end
|
@@ -291,9 +284,18 @@ module JsChat
|
|
291
284
|
end
|
292
285
|
end
|
293
286
|
|
287
|
+
def times(message, options = {})
|
288
|
+
times = {}
|
289
|
+
@user.rooms.each do |room|
|
290
|
+
times[room.name] = room.last_update_time
|
291
|
+
end
|
292
|
+
times
|
293
|
+
end
|
294
|
+
|
294
295
|
def ping(message, options = {})
|
295
296
|
if @user and @user.last_poll and Time.now.utc > @user.last_poll
|
296
297
|
time = Time.now.utc
|
298
|
+
@user.update_session_expiration
|
297
299
|
{ 'pong' => time }
|
298
300
|
else
|
299
301
|
# TODO: HANDLE PING OUTS
|
@@ -310,7 +312,7 @@ module JsChat
|
|
310
312
|
def room_message(message, options)
|
311
313
|
room = Room.find options['to']
|
312
314
|
if room and room.users.include? @user
|
313
|
-
room.send_message({ 'message' => message, 'user' => @user.name
|
315
|
+
room.send_message({ 'message' => message, 'user' => @user.name })
|
314
316
|
else
|
315
317
|
send_response Error.new(:not_in_room, "Please join this room first")
|
316
318
|
end
|
@@ -321,8 +323,8 @@ module JsChat
|
|
321
323
|
if user
|
322
324
|
# Return the message to the user, and send it to the other person too
|
323
325
|
now = Time.now.utc
|
324
|
-
user.private_message({ 'message' => message, 'user' => @user.name
|
325
|
-
@user.private_message({ 'message' => message, 'user' => @user.name
|
326
|
+
user.private_message({ 'message' => message, 'user' => @user.name })
|
327
|
+
@user.private_message({ 'message' => message, 'user' => @user.name })
|
326
328
|
else
|
327
329
|
Error.new(:not_online, 'User not online')
|
328
330
|
end
|
@@ -401,7 +403,9 @@ module JsChat
|
|
401
403
|
|
402
404
|
def disconnect_lagged_users
|
403
405
|
@@stateless_cookies.delete_if do |cookie|
|
404
|
-
|
406
|
+
if cookie[:user].session_expired?
|
407
|
+
lagged?(cookie[:user].last_poll) ? disconnect_user(cookie[:user]) && true : false
|
408
|
+
end
|
405
409
|
end
|
406
410
|
end
|
407
411
|
|
@@ -510,9 +514,9 @@ module JsChat
|
|
510
514
|
end
|
511
515
|
elsif input.has_key? 'identify'
|
512
516
|
input['ip'] ||= get_remote_ip
|
513
|
-
response << send_response(identify(input['identify'], input['ip']))
|
517
|
+
response << send_response(identify(input['identify'], input['ip'], input['session_length']))
|
514
518
|
else
|
515
|
-
%w{lastlog change send join names part since ping list quit}.each do |command|
|
519
|
+
%w{lastlog change send join names part since ping list quit times}.each do |command|
|
516
520
|
if @user.name.nil?
|
517
521
|
response << send_response(Error.new(:identity_required, 'Identify first'))
|
518
522
|
return response
|
@@ -545,6 +549,7 @@ module JsChat
|
|
545
549
|
puts "Data that raised exception: #{exception}"
|
546
550
|
p data
|
547
551
|
print_call_stack
|
552
|
+
raise
|
548
553
|
end
|
549
554
|
|
550
555
|
def print_call_stack(from = 0, to = 10)
|
@@ -17,6 +17,8 @@ ServerConfigDefaults = {
|
|
17
17
|
'db_name' => 'jschat',
|
18
18
|
'db_host' => 'localhost',
|
19
19
|
'db_port' => 27017
|
20
|
+
# Register your instance of JsChat here: http://twitter.com/apps/create
|
21
|
+
# 'twitter' => { 'key' => '', 'secret' => '' }
|
20
22
|
}
|
21
23
|
|
22
24
|
# Command line options will overrides these
|
data/lib/jschat/storage/mongo.rb
CHANGED
@@ -9,25 +9,29 @@ module JsChat::Storage
|
|
9
9
|
@db = Mongo::Connection.new(ServerConfig['db_host'], ServerConfig['db_port']).db(ServerConfig['db_name'])
|
10
10
|
end
|
11
11
|
|
12
|
-
def self.log(message)
|
13
|
-
message['
|
12
|
+
def self.log(message, room)
|
13
|
+
message['room'] = room
|
14
14
|
@db['events'].insert(message)
|
15
15
|
end
|
16
16
|
|
17
|
-
def self.lastlog(number)
|
18
|
-
@db['events'].find({}, { :limit => number, :sort => ['
|
17
|
+
def self.lastlog(number, room)
|
18
|
+
@db['events'].find({ :room => room }, { :limit => number, :sort => ['time', Mongo::ASCENDING] }).to_a
|
19
19
|
end
|
20
20
|
|
21
21
|
# TODO: use twitter oauth for the key
|
22
|
-
def self.find_user(
|
23
|
-
@db['users'].find_one(
|
22
|
+
def self.find_user(options)
|
23
|
+
@db['users'].find_one(options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.save_user(user)
|
27
|
+
@db['users'].save user
|
24
28
|
end
|
25
29
|
|
26
30
|
def self.set_rooms(name, rooms)
|
27
|
-
user = find_user name
|
31
|
+
user = find_user({ 'name' => name })
|
28
32
|
user ||= { 'name' => name }
|
29
33
|
user['rooms'] = rooms
|
30
|
-
|
34
|
+
save_user user
|
31
35
|
end
|
32
36
|
|
33
37
|
def self.available?
|
data/lib/jschat/storage/null.rb
CHANGED
@@ -1,17 +1,23 @@
|
|
1
1
|
module JsChat::Storage
|
2
|
+
MEMORY_MESSAGE_LIMIT = 100
|
3
|
+
|
2
4
|
module NullDriver
|
3
|
-
def self.log(message)
|
5
|
+
def self.log(message, room)
|
4
6
|
@messages ||= []
|
7
|
+
message['room'] = room
|
5
8
|
@messages.push message
|
6
|
-
@messages = @messages[-
|
9
|
+
@messages = @messages[-MEMORY_MESSAGE_LIMIT..-1] if @messages.size > MEMORY_MESSAGE_LIMIT
|
7
10
|
end
|
8
11
|
|
9
|
-
def self.lastlog(number)
|
12
|
+
def self.lastlog(number, room)
|
10
13
|
@messages ||= []
|
11
|
-
@messages[0..number]
|
14
|
+
@messages.select { |m| m['room'] == room }.reverse[0..number].reverse
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.find_user(options)
|
12
18
|
end
|
13
19
|
|
14
|
-
def self.
|
20
|
+
def self.save_user(user)
|
15
21
|
end
|
16
22
|
|
17
23
|
def self.set_rooms(name, rooms)
|
data/test/test_helper.rb
CHANGED
@@ -2,10 +2,11 @@ require 'test/unit'
|
|
2
2
|
require 'rubygems'
|
3
3
|
require 'eventmachine'
|
4
4
|
require 'json'
|
5
|
-
|
5
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
6
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'jschat', 'server.rb')
|
6
7
|
|
7
8
|
ServerConfig = {
|
8
|
-
|
9
|
+
'max_message_length' => 500
|
9
10
|
}
|
10
11
|
|
11
12
|
class JsChat::Room
|
@@ -59,3 +60,7 @@ class JsChatMock
|
|
59
60
|
room.users << user
|
60
61
|
end
|
61
62
|
end
|
63
|
+
|
64
|
+
JsChat::Storage.enabled = false
|
65
|
+
JsChat::Storage.driver = JsChat::Storage::NullDriver
|
66
|
+
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
version: 0.2.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Alex R. Young
|
@@ -106,6 +106,7 @@ files:
|
|
106
106
|
- lib/jschat/flood_protection.rb
|
107
107
|
- lib/jschat/http/config/sprockets.yml
|
108
108
|
- lib/jschat/http/config.ru
|
109
|
+
- lib/jschat/http/helpers/url_for.rb
|
109
110
|
- lib/jschat/http/jschat.rb
|
110
111
|
- lib/jschat/http/public/favicon.ico
|
111
112
|
- lib/jschat/http/public/images/emoticons/angry.gif
|
@@ -179,10 +180,13 @@ files:
|
|
179
180
|
- lib/jschat/http/public/stylesheets/screen.css
|
180
181
|
- lib/jschat/http/script/sprockets.rb
|
181
182
|
- lib/jschat/http/tmp/restart.txt
|
183
|
+
- lib/jschat/http/views/form.erb
|
182
184
|
- lib/jschat/http/views/index.erb
|
183
185
|
- lib/jschat/http/views/iphone.erb
|
184
186
|
- lib/jschat/http/views/layout.erb
|
185
187
|
- lib/jschat/http/views/message_form.erb
|
188
|
+
- lib/jschat/http/views/twitter.erb
|
189
|
+
- lib/jschat/init.rb
|
186
190
|
- lib/jschat/server.rb
|
187
191
|
- lib/jschat/server_options.rb
|
188
192
|
- lib/jschat/storage/init.rb
|