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 CHANGED
@@ -130,7 +130,7 @@ module JsChat
130
130
  command = get_command json
131
131
 
132
132
  if command
133
- time = get_time json[json[command]]
133
+ time = get_time json
134
134
  response = send(protocol_method_name(command, json[command]), json[json[command]])
135
135
  case response
136
136
  when Array
@@ -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
@@ -3,13 +3,64 @@ require 'sinatra'
3
3
  require 'sha1'
4
4
  require 'json'
5
5
  require 'sprockets'
6
- require 'jschat/server_options'
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 request.cookies['jschat-id']
177
+ @bridge = JsChat::Bridge.new session[:jschat_id]
123
178
  end
124
179
 
125
180
  def load_and_connect
126
- @bridge = JsChat::Bridge.new request.cookies['jschat-id']
181
+ @bridge = JsChat::Bridge.new session[:jschat_id]
127
182
  @bridge.connect
128
- response.set_cookie 'jschat-id', @bridge.cookie
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
- response.set_cookie 'jschat-id', nil
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.to_json
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 }, true);
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
- callback(roomName);
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', user['time']);
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({ user_class: user_class, room: message['room'], user: TextHelper.truncateName(message['user']), message: TextHelper.decorateMessage(message['message']), message_class: 'message' });
28
- this.add_message(text, 'message', message['time']);
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', join['time']);
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', part['time']);
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', quit['time']);
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
 
@@ -1,7 +1,7 @@
1
1
  var JsChat = {};
2
2
 
3
3
  document.observe('dom:loaded', function() {
4
- if ($('post_message')) {
4
+ if ($('post_message')) {
5
5
  var chatController = new JsChat.ChatController();
6
6
  }
7
7
 
@@ -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
- <form method="post" action="/identify" id="sign-on">
9
- Enter name: <input name="name" id="name" value="" type="text" />
10
- and room: <input name="room" id="room" value="#jschat" type="text" />
11
- <input type="submit" value="Go" id="sign-on-submit" />
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>
@@ -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(100)
156
+ messages = JsChat::Storage.driver.lastlog(LASTLOG_DEFAULT, name)
152
157
  if since.nil?
153
158
  messages
154
159
  else
155
- messages.select { |m| message_time(m) > since }
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
- if message.has_key? 'display'
172
- message[message['display']]['time'] = Time.now.utc
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 = 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, 'time' => Time.now.utc })
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, 'time' => now })
325
- @user.private_message({ 'message' => message, 'user' => @user.name, 'time' => now })
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
- lagged?(cookie[:user].last_poll) ? disconnect_user(cookie[:user]) && true : false
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
@@ -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['time_index'] = Time.now.to_i
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 => ['time_index', Mongo::ASCENDING] }).to_a
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(name)
23
- @db['users'].find_one('name' => name)
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
- @db['users'].save user
34
+ save_user user
31
35
  end
32
36
 
33
37
  def self.available?
@@ -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[-100..-1] if @messages.size > 100
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.find_user(name)
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
- require File.join(File.dirname(__FILE__), '../', 'jschat.rb')
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
- :max_message_length => 500
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
- - 1
8
- - 5
9
- version: 0.1.5
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