jschat 0.1.2 → 0.1.5

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/README.textile CHANGED
@@ -43,7 +43,7 @@ The web app must be run alongside the server. The web app must be started in pr
43
43
 
44
44
  The web app currently has no database dependencies, it's a wrapper that links cookies to JsChat server proxies. You can run it on port 80 by configuring Rack or an Apache proxy. I have Apache set up this way on "jschat.org":http://jschat.org.
45
45
 
46
- h3. Configuration files
46
+ h3. Configuration Files
47
47
 
48
48
  These are the default locations of the configuration files. You can override them with <code>--config=PATH</code>:
49
49
 
@@ -58,6 +58,16 @@ The file format is JSON, like this:
58
58
  { "port": 3001 }
59
59
  </pre>
60
60
 
61
+ h3. Server Configuration Options
62
+
63
+ <pre>
64
+ {
65
+ "port": integer,
66
+ "ip": "string: IP address to bind to",
67
+ "tmp_files": "string: path to tmp files (including PID file)"
68
+ }
69
+ </pre>
70
+
61
71
  h3. Client Commands
62
72
 
63
73
  * Change name or identify: <code>/nick name</code>
data/bin/jschat-server CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  require 'logger'
4
4
  require 'jschat/server'
5
- require 'jschat/server-options'
5
+ require 'jschat/server_options'
6
6
 
7
- EM.run do
8
- EM.start_server ServerConfig['ip'], ServerConfig['port'], JsChat
9
- end
7
+ JsChat::Server.run!
@@ -3,7 +3,7 @@ require 'sinatra'
3
3
  require 'sha1'
4
4
  require 'json'
5
5
  require 'sprockets'
6
- require 'jschat/server-options'
6
+ require 'jschat/server_options'
7
7
 
8
8
  set :public, File.join(File.dirname(__FILE__), 'public')
9
9
  set :views, File.join(File.dirname(__FILE__), 'views')
@@ -39,6 +39,10 @@ class JsChat::Bridge
39
39
  end
40
40
  end
41
41
 
42
+ def rooms
43
+ send_json({ :list => 'rooms' })
44
+ end
45
+
42
46
  def lastlog(room)
43
47
  response = send_json({ :lastlog => room })
44
48
  response['messages']
@@ -52,6 +56,10 @@ class JsChat::Bridge
52
56
  send_json({ :join => room }, false)
53
57
  end
54
58
 
59
+ def part(room)
60
+ send_json({ :part => room })
61
+ end
62
+
55
63
  def send_message(message, to)
56
64
  send_json({ :send => message, :to => to }, false)
57
65
  end
@@ -212,7 +220,18 @@ post '/join' do
212
220
  load_bridge
213
221
  @bridge.join params['room']
214
222
  save_last_room params['room']
215
- "Request OK"
223
+ 'OK'
224
+ end
225
+
226
+ get '/part' do
227
+ load_bridge
228
+ @bridge.part params['room']
229
+
230
+ if @bridge.last_error
231
+ error 500, [@bridge.last_error].to_json
232
+ else
233
+ 'OK'
234
+ end
216
235
  end
217
236
 
218
237
  get '/chat/' do
@@ -249,6 +268,11 @@ get '/quit' do
249
268
  redirect '/'
250
269
  end
251
270
 
271
+ get '/rooms' do
272
+ load_bridge
273
+ @bridge.rooms.to_json
274
+ end
275
+
252
276
  # This serves the JavaScript concat'd by Sprockets
253
277
  # run script/sprocket.rb to cache this
254
278
  get '/javascripts/all.js' do
@@ -12,6 +12,24 @@ JsChat.ChatController = Class.create({
12
12
 
13
13
  $('post_message').observe('submit', this.postMessageFormEvent.bindAsEventListener(this));
14
14
  $('messages').observe('scroll', this.messagesScrolled.bindAsEventListener(this));
15
+ $$('#rooms li.join a').first().observe('click', this.joinRoomClicked.bindAsEventListener(this));
16
+ Event.observe(document, 'click', this.roomTabClick.bindAsEventListener(this));
17
+ },
18
+
19
+ joinRoomClicked: function(e) {
20
+ this.addRoomPrompt(e);
21
+ Event.stop(e);
22
+ return false;
23
+ },
24
+
25
+ roomTabClick: function(e) {
26
+ var element = Event.element(e);
27
+
28
+ if (element.tagName == 'A' && element.up('#rooms') && !element.up('li').hasClassName('join')) {
29
+ this.switchRoom(element.innerHTML);
30
+ Event.stop(e);
31
+ return false;
32
+ }
15
33
  },
16
34
 
17
35
  messagesScrolled: function() {
@@ -80,8 +98,7 @@ JsChat.ChatController = Class.create({
80
98
  Display.show_unread = false;
81
99
  Display.ignore_notices = false;
82
100
 
83
- $('room-name').innerHTML = TextHelper.truncateRoomName(PageHelper.currentRoom());
84
- $('room-name').title = PageHelper.currentRoom();
101
+ PageHelper.setCurrentRoomName(window.location.hash);
85
102
  $('message').activate();
86
103
  $$('.header .navigation li').invoke('hide');
87
104
  $('quit-nav').show();
@@ -95,13 +112,26 @@ JsChat.ChatController = Class.create({
95
112
  });
96
113
 
97
114
  this.createPollers();
98
- this.joinRoom();
115
+ this.joinRoom(PageHelper.currentRoom());
116
+ this.getRoomList(this.addRoomToNav);
117
+ },
118
+
119
+ getRoomList: function(callback) {
120
+ new Ajax.Request('/rooms', {
121
+ method: 'get',
122
+ parameters: { time: new Date().getTime() },
123
+ onComplete: function(response) {
124
+ response.responseText.evalJSON().each(function(roomName) {
125
+ callback(roomName);
126
+ }.bind(this));
127
+ }.bind(this)
128
+ });
99
129
  },
100
130
 
101
- joinRoom: function() {
131
+ joinRoom: function(roomName) {
102
132
  new Ajax.Request('/join', {
103
133
  method: 'post',
104
- parameters: { time: new Date().getTime(), room: PageHelper.currentRoom() },
134
+ parameters: { time: new Date().getTime(), room: roomName },
105
135
  onFailure: function() {
106
136
  Display.add_message("Error: Couldn't join channel", 'server');
107
137
  $('loading').hide();
@@ -112,10 +142,109 @@ JsChat.ChatController = Class.create({
112
142
  document.title = PageHelper.title();
113
143
  UserCommands['/lastlog'].apply(this);
114
144
  $('loading').hide();
145
+ $('rooms').show();
146
+ this.addRoomToNav(roomName, true);
115
147
  }.bind(this)
116
148
  });
117
149
  },
118
150
 
151
+ isValidRoom: function(roomName) {
152
+ if (PageHelper.allRoomNames().include(roomName)) {
153
+ return false;
154
+ }
155
+ return true;
156
+ },
157
+
158
+ validateAndJoinRoom: function(roomName) {
159
+ if (roomName === null || roomName.length == 0) {
160
+ return;
161
+ }
162
+
163
+ if (!roomName.match(/^#/)) {
164
+ roomName = '#' + roomName;
165
+ }
166
+
167
+ if (this.isValidRoom(roomName)) {
168
+ this.joinRoomInTab(roomName);
169
+ }
170
+ },
171
+
172
+ addRoomPrompt: function() {
173
+ var roomName = prompt('Enter a room name:');
174
+ this.validateAndJoinRoom(roomName);
175
+ },
176
+
177
+ addRoomToNav: function(roomName, selected) {
178
+ if (PageHelper.allRoomNames().include(roomName)) return;
179
+
180
+ var classAttribute = selected ? ' class="selected"' : '';
181
+ $('rooms').insert({ bottom: '<li#{classAttribute}><a href="#{roomName}">#{roomName}</a></li>'.interpolate({ classAttribute: classAttribute, roomName: roomName }) });
182
+ },
183
+
184
+ removeSelectedTab: function() {
185
+ $$('#rooms .selected').invoke('removeClassName', 'selected');
186
+ },
187
+
188
+ selectRoomTab: function(roomName) {
189
+ $$('#rooms a').each(function(a) {
190
+ if (a.innerHTML == roomName) {
191
+ a.up('li').addClassName('selected');
192
+ }
193
+ });
194
+ },
195
+
196
+ joinRoomInTab: function(roomName) {
197
+ this.removeSelectedTab();
198
+ PageHelper.setCurrentRoomName(roomName);
199
+ this.joinRoom(roomName);
200
+ },
201
+
202
+ switchRoom: function(roomName) {
203
+ if (PageHelper.currentRoom() == roomName) {
204
+ return;
205
+ }
206
+
207
+ this.removeSelectedTab();
208
+ this.selectRoomTab(roomName);
209
+ PageHelper.setCurrentRoomName(roomName);
210
+ UserCommands['/lastlog'].apply(this);
211
+ },
212
+
213
+ rooms: function() {
214
+ return $$('#rooms li a').slice(1).collect(function(element) {
215
+ return element.innerHTML;
216
+ });
217
+ },
218
+
219
+ partRoom: function(roomName) {
220
+ if (this.rooms().length == 1) {
221
+ return UserCommands['/quit']();
222
+ }
223
+
224
+ new Ajax.Request('/part', {
225
+ method: 'get',
226
+ parameters: { room: roomName },
227
+ onSuccess: function(request) {
228
+ this.removeTab(roomName);
229
+ }.bind(this),
230
+ onFailure: function(request) {
231
+ Display.add_message('Error: ' + request.responseText, 'server');
232
+ }
233
+ });
234
+ },
235
+
236
+ removeTab: function(roomName) {
237
+ $$('#rooms li').each(function(element) {
238
+ if (element.down('a').innerHTML == roomName) {
239
+ element.remove();
240
+
241
+ if (roomName == PageHelper.currentRoom()) {
242
+ this.switchRoom($$('#rooms li a')[1].innerHTML);
243
+ }
244
+ }
245
+ }.bind(this));
246
+ },
247
+
119
248
  updateNames: function() {
120
249
  UserCommands['/names'].apply(this);
121
250
  },
@@ -3,6 +3,19 @@ var PageHelper = {
3
3
  return window.location.hash;
4
4
  },
5
5
 
6
+ setCurrentRoomName: function(roomName) {
7
+ window.location.hash = roomName;
8
+ $('room-name').innerHTML = TextHelper.truncateRoomName(PageHelper.currentRoom());
9
+ $('room-name').title = PageHelper.currentRoom();
10
+ document.title = PageHelper.title();
11
+ },
12
+
13
+ allRoomNames: function() {
14
+ return $$('#rooms li a').collect(function(link) {
15
+ return link.innerHTML;
16
+ });
17
+ },
18
+
6
19
  nickname: function() {
7
20
  return Cookie.find('jschat-name');
8
21
  },
@@ -9,9 +9,12 @@ var UserCommands = {
9
9
  var help = [];
10
10
  Display.add_message('<strong>JsChat Help</strong> &mdash; Type the following commands into the message field:', 'help')
11
11
  help.push(['/clear', 'Clears messages']);
12
+ help.push(['/join #room_name', 'Joins a room']);
13
+ help.push(['/part #room_name', 'Leaves a room. Leave room_name blank for the current room']);
12
14
  help.push(['/lastlog', 'Shows recent activity']);
13
15
  help.push(['/names', 'Refreshes the names list']);
14
16
  help.push(['/name new_name', 'Changes your name']);
17
+ help.push(['/quit', 'Quit']);
15
18
  help.push(['/emotes', 'Shows available emotes']);
16
19
  $A(help).each(function(options) {
17
20
  var help_text = '<span class="command">#{command}</span><span class="command_help">#{text}</span>'.interpolate({ command: options[0], text: options[1]});
@@ -51,5 +54,23 @@ var UserCommands = {
51
54
 
52
55
  '/names': function() {
53
56
  JsChat.Request.get('/names', function(t) { this.displayMessages(t.responseText); }.bind(this));
57
+ },
58
+
59
+ '/(join)\\s+(.*)': function() {
60
+ var room = arguments[0][2];
61
+ this.validateAndJoinRoom(room);
62
+ },
63
+
64
+ '/(part|leave)': function() {
65
+ this.partRoom(PageHelper.currentRoom());
66
+ },
67
+
68
+ '/(part|leave)\\s+(.*)': function() {
69
+ var room = arguments[0][2];
70
+ this.partRoom(room);
71
+ },
72
+
73
+ '/quit': function() {
74
+ window.location = '/quit';
54
75
  }
55
76
  };
@@ -1,3 +1,5 @@
1
1
  body { font-size: 200% }
2
2
  input { font-size: 150% }
3
3
  #info { display: none }
4
+ .header .rooms { top: 6px; }
5
+ .header .rooms li { padding-bottom: 4px; height: 1.25em; font-size: 120% }
@@ -16,6 +16,13 @@ h1 { text-align: left; margin-left: 20px }
16
16
  .header .navigation li#quit-nav a:hover { color: #990000; background-color: #fff }
17
17
  .header-shadow { width: 100%; height: 6px; position: absolute; top: 59px; left: 0; background-image: url('/images/shadow.png'); background-repeat: repeat-x }
18
18
 
19
+ .header .rooms { position: absolute; left: 200px; top: 35px; list-style-type: none; margin: 0; padding: 0 }
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
+ .header .rooms li.selected { background-color: #fff; border: 1px solid #ccc; border-bottom: #fff }
22
+ .header .rooms li a { color: #777; text-decoration: none }
23
+ .header .rooms li a:hover { color: #000 }
24
+ .header .rooms li.selected a { color: #444 }
25
+
19
26
  .page { margin-top: 60px }
20
27
 
21
28
  #messages { width: 500px; height: 300px; margin: 0 20px 10px 20px; padding: 0; overflow: auto; background-color: #fff; float: left; text-align: left; display: inline; }
@@ -14,6 +14,9 @@
14
14
  <div id="loading" style="display: none">Loading...</div>
15
15
  <div class="header">
16
16
  <h1><a href="/"><img src="/images/jschat.gif" alt="JsChat" /></a></h1>
17
+ <ul id="rooms" class="rooms" style="display: none">
18
+ <li class="join"><a href="#">+</a></li>
19
+ </ul>
17
20
  <ul class="navigation">
18
21
  <li><a href="/">Home</a></li>
19
22
  <li><a href="http://github.com/alexyoung/jschat">Download</a>
@@ -13,6 +13,9 @@
13
13
  <div id="loading" style="display: none">Loading...</div>
14
14
  <div class="header">
15
15
  <h1><a href="/"><img src="/images/jschat.gif" alt="JsChat" /></a></h1>
16
+ <ul id="rooms" class="rooms" style="display: none">
17
+ <li class="join"><a href="#">+</a></li>
18
+ </ul>
16
19
  <ul class="navigation">
17
20
  <li><a href="/">Home</a></li>
18
21
  <li><a href="http://github.com/alexyoung/jschat">Download</a>
data/lib/jschat/server.rb CHANGED
@@ -7,10 +7,53 @@ require 'socket'
7
7
  # JsChat libraries
8
8
  require 'jschat/errors'
9
9
  require 'jschat/flood_protection'
10
+ require 'jschat/storage/init'
10
11
 
11
12
  module JsChat
12
13
  STATELESS_TIMEOUT = 60
13
14
 
15
+ module Server
16
+ def self.pid_file_name
17
+ File.join(ServerConfig['tmp_files'], 'jschat.pid')
18
+ end
19
+
20
+ def self.write_pid_file
21
+ return unless ServerConfig['use_tmp_files']
22
+ File.open(pid_file_name, 'w') { |f| f << Process.pid }
23
+ end
24
+
25
+ def self.rm_pid_file
26
+ FileUtils.rm pid_file_name
27
+ end
28
+
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
+ def self.stop
40
+ rm_pid_file
41
+ end
42
+
43
+ def self.run!
44
+ write_pid_file
45
+ init_storage
46
+
47
+ at_exit do
48
+ stop
49
+ end
50
+
51
+ EM.run do
52
+ EM.start_server ServerConfig['ip'], ServerConfig['port'], JsChat
53
+ end
54
+ end
55
+ end
56
+
14
57
  class User
15
58
  include JsChat::FloodProtection
16
59
 
@@ -105,10 +148,11 @@ module JsChat
105
148
  end
106
149
 
107
150
  def messages_since(since)
151
+ messages = JsChat::Storage.driver.lastlog(100)
108
152
  if since.nil?
109
- @messages
153
+ messages
110
154
  else
111
- @messages.select { |m| message_time(m) > since }
155
+ messages.select { |m| message_time(m) > since }
112
156
  end
113
157
  end
114
158
 
@@ -117,19 +161,19 @@ module JsChat
117
161
  message[message['display']]['time']
118
162
  elsif message.has_key? 'change'
119
163
  message[message['change']]['time']
164
+ else
165
+ Time.now
120
166
  end
121
167
  end
122
168
 
123
169
  def add_to_lastlog(message)
124
- @messages ||= []
125
170
  if message
126
171
  if message.has_key? 'display'
127
172
  message[message['display']]['time'] = Time.now.utc
128
173
  elsif message.has_key? 'change'
129
174
  message[message['change']]['time'] = Time.now.utc
130
175
  end
131
- @messages.push message
132
- @messages = @messages[-100..-1] if @messages.size > 100
176
+ JsChat::Storage.driver.log message
133
177
  end
134
178
  end
135
179
 
@@ -401,12 +445,21 @@ module JsChat
401
445
  field, value = @user.send :change, options[change]
402
446
  { 'display' => 'notice', 'notice' => "Your #{field} has been changed to: #{value}" }
403
447
  else
404
- Error.new(:invalid_request, "Invalid change request")
448
+ Error.new(:invalid_request, 'Invalid change request')
405
449
  end
406
450
  rescue JsChat::Errors::InvalidName => exception
407
451
  exception
408
452
  end
409
453
 
454
+ def list(list, options = {})
455
+ case list
456
+ when 'rooms'
457
+ @user.rooms.collect { |room| room.name }
458
+ else
459
+ Error.new(:invalid_request, 'Invalid list command')
460
+ end
461
+ end
462
+
410
463
  def send_response(data)
411
464
  response = ''
412
465
  case data
@@ -459,9 +512,9 @@ module JsChat
459
512
  input['ip'] ||= get_remote_ip
460
513
  response << send_response(identify(input['identify'], input['ip']))
461
514
  else
462
- ['lastlog', 'change', 'send', 'join', 'names', 'part', 'since', 'ping', 'quit'].each do |command|
515
+ %w{lastlog change send join names part since ping list quit}.each do |command|
463
516
  if @user.name.nil?
464
- response << send_response(Error.new(:identity_required, "Identify first"))
517
+ response << send_response(Error.new(:identity_required, 'Identify first'))
465
518
  return response
466
519
  end
467
520
 
@@ -494,7 +547,7 @@ module JsChat
494
547
  print_call_stack
495
548
  end
496
549
 
497
- def print_call_stack(from = 2, to = 5)
550
+ def print_call_stack(from = 0, to = 10)
498
551
  puts "Stack:"
499
552
  (from..to).each do |index|
500
553
  puts "\t#{caller[index]}"
@@ -1,4 +1,5 @@
1
1
  require 'optparse'
2
+ require 'tmpdir'
2
3
 
3
4
  logger = nil
4
5
 
@@ -11,7 +12,11 @@ ServerConfigDefaults = {
11
12
  'port' => 6789,
12
13
  'ip' => '0.0.0.0',
13
14
  'logger' => logger,
14
- 'max_message_length' => 500
15
+ 'max_message_length' => 500,
16
+ 'tmp_files' => File.join(Dir::tmpdir, 'jschat'),
17
+ 'db_name' => 'jschat',
18
+ 'db_host' => 'localhost',
19
+ 'db_port' => 27017
15
20
  }
16
21
 
17
22
  # Command line options will overrides these
@@ -24,6 +29,17 @@ def load_options(path)
24
29
  end
25
30
  end
26
31
 
32
+ def make_tmp_files
33
+ ServerConfig['use_tmp_files'] = false
34
+ if File.exists? ServerConfig['tmp_files']
35
+ ServerConfig['use_tmp_files'] = true
36
+ else
37
+ if Dir.mkdir ServerConfig['tmp_files']
38
+ ServerConfig['use_tmp_files'] = true
39
+ end
40
+ end
41
+ end
42
+
27
43
  options = {}
28
44
  default_config_file = '/etc/jschat/config.json'
29
45
 
@@ -34,7 +50,8 @@ ARGV.clone.options do |opts|
34
50
  opts.separator ""
35
51
 
36
52
  opts.on("-c", "--config=PATH", String, "Configuration file location (#{default_config_file}") { |o| options['config'] = o }
37
- opts.on("-p", "--port=port", String, "Port number") { |o| options['port'] = o }
53
+ opts.on("-p", "--port=PORT", String, "Port number") { |o| options['port'] = o }
54
+ opts.on("-t", "--tmp_files=PATH", String, "Temporary files location (including pid file)") { |o| options['tmp_files'] = o }
38
55
  opts.on("--help", "-H", "This text") { puts opts; exit 0 }
39
56
 
40
57
  opts.parse!
@@ -43,3 +60,4 @@ end
43
60
  options = load_options(options['config'] || default_config_file).merge options
44
61
 
45
62
  ServerConfig = ServerConfigDefaults.merge options
63
+ make_tmp_files
@@ -0,0 +1,19 @@
1
+ require File.join(File.dirname(__FILE__), 'mongo')
2
+ require File.join(File.dirname(__FILE__), 'null')
3
+
4
+ module JsChat::Storage
5
+ def self.driver=(driver)
6
+ @driver = driver
7
+ end
8
+
9
+ def self.driver ; @driver ; end
10
+
11
+ def self.enabled=(enabled)
12
+ @enabled = enabled
13
+ end
14
+
15
+ def self.enabled?
16
+ @enabled
17
+ end
18
+ end
19
+
@@ -0,0 +1,41 @@
1
+ begin
2
+ require 'mongo'
3
+ rescue LoadError
4
+ end
5
+
6
+ module JsChat::Storage
7
+ module MongoDriver
8
+ def self.connect!
9
+ @db = Mongo::Connection.new(ServerConfig['db_host'], ServerConfig['db_port']).db(ServerConfig['db_name'])
10
+ end
11
+
12
+ def self.log(message)
13
+ message['time_index'] = Time.now.to_i
14
+ @db['events'].insert(message)
15
+ end
16
+
17
+ def self.lastlog(number)
18
+ @db['events'].find({}, { :limit => number, :sort => ['time_index', Mongo::ASCENDING] }).to_a
19
+ end
20
+
21
+ # TODO: use twitter oauth for the key
22
+ def self.find_user(name)
23
+ @db['users'].find_one('name' => name)
24
+ end
25
+
26
+ def self.set_rooms(name, rooms)
27
+ user = find_user name
28
+ user ||= { 'name' => name }
29
+ user['rooms'] = rooms
30
+ @db['users'].save user
31
+ end
32
+
33
+ def self.available?
34
+ return unless Object.const_defined?(:Mongo)
35
+ connect!
36
+ rescue
37
+ puts 'Failed to connect to mongo'
38
+ false
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,21 @@
1
+ module JsChat::Storage
2
+ module NullDriver
3
+ def self.log(message)
4
+ @messages ||= []
5
+ @messages.push message
6
+ @messages = @messages[-100..-1] if @messages.size > 100
7
+ end
8
+
9
+ def self.lastlog(number)
10
+ @messages ||= []
11
+ @messages[0..number]
12
+ end
13
+
14
+ def self.find_user(name)
15
+ end
16
+
17
+ def self.set_rooms(name, rooms)
18
+ end
19
+ end
20
+ end
21
+
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 1
8
- - 2
9
- version: 0.1.2
8
+ - 5
9
+ version: 0.1.5
10
10
  platform: ruby
11
11
  authors:
12
12
  - Alex R. Young
@@ -183,8 +183,11 @@ files:
183
183
  - lib/jschat/http/views/iphone.erb
184
184
  - lib/jschat/http/views/layout.erb
185
185
  - lib/jschat/http/views/message_form.erb
186
- - lib/jschat/server-options.rb
187
186
  - lib/jschat/server.rb
187
+ - lib/jschat/server_options.rb
188
+ - lib/jschat/storage/init.rb
189
+ - lib/jschat/storage/mongo.rb
190
+ - lib/jschat/storage/null.rb
188
191
  - test/server_test.rb
189
192
  - test/stateless_test.rb
190
193
  - test/test_helper.rb