jschat 0.1.2 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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