jschat 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|