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 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