jschat 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +23 -0
- data/README.textile +71 -0
- data/bin/jschat-client +3 -0
- data/bin/jschat-server +18 -0
- data/bin/jschat-web +7 -0
- data/lib/jschat/client.rb +745 -0
- data/lib/jschat/errors.rb +41 -0
- data/lib/jschat/flood_protection.rb +39 -0
- data/lib/jschat/http/config/sprockets.yml +7 -0
- data/lib/jschat/http/config.ru +12 -0
- data/lib/jschat/http/jschat.rb +264 -0
- data/lib/jschat/http/public/favicon.ico +0 -0
- data/lib/jschat/http/public/images/emoticons/angry.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/arr.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/blink.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/blush.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/brucelee.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/btw.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/chuckle.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/clap.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/cool.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/drool.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/drunk.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/dry.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/eek.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/flex.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/happy.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/holmes.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/huh.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/laugh.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/lol.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/mad.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/mellow.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/noclue.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/oh.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/ohmy.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/ph34r.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/pimp.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/punch.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/realmad.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/rock.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/rofl.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/rolleyes.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/sad.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/scratch.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/shifty.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/shock.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/shrug.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/sleep.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/sleeping.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/smile.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/suicide.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/sweat.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/thumbs.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/tongue.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/unsure.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/w00t.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/wacko.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/whistling.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/wink.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/worship.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/yucky.gif +0 -0
- data/lib/jschat/http/public/images/jschat.gif +0 -0
- data/lib/jschat/http/public/images/shadow.png +0 -0
- data/lib/jschat/http/public/javascripts/app/controllers/chat_controller.js +191 -0
- data/lib/jschat/http/public/javascripts/app/controllers/signon_controller.js +56 -0
- data/lib/jschat/http/public/javascripts/app/helpers/emote_helper.js +23 -0
- data/lib/jschat/http/public/javascripts/app/helpers/form_helpers.js +37 -0
- data/lib/jschat/http/public/javascripts/app/helpers/link_helper.js +47 -0
- data/lib/jschat/http/public/javascripts/app/helpers/page_helper.js +27 -0
- data/lib/jschat/http/public/javascripts/app/helpers/text_helper.js +92 -0
- data/lib/jschat/http/public/javascripts/app/lib/split.js +78 -0
- data/lib/jschat/http/public/javascripts/app/models/cookie.js +27 -0
- data/lib/jschat/http/public/javascripts/app/protocol/change.js +15 -0
- data/lib/jschat/http/public/javascripts/app/protocol/chat_request.js +13 -0
- data/lib/jschat/http/public/javascripts/app/protocol/display.js +147 -0
- data/lib/jschat/http/public/javascripts/app/ui/commands.js +55 -0
- data/lib/jschat/http/public/javascripts/app/ui/tab_completion.js +122 -0
- data/lib/jschat/http/public/javascripts/init.js +19 -0
- data/lib/jschat/http/public/stylesheets/iphone.css +3 -0
- data/lib/jschat/http/public/stylesheets/screen.css +68 -0
- data/lib/jschat/http/script/sprockets.rb +14 -0
- data/lib/jschat/http/tmp/restart.txt +0 -0
- data/lib/jschat/http/views/index.erb +23 -0
- data/lib/jschat/http/views/iphone.erb +29 -0
- data/lib/jschat/http/views/layout.erb +29 -0
- data/lib/jschat/http/views/message_form.erb +15 -0
- data/lib/jschat/server.rb +503 -0
- data/test/server_test.rb +175 -0
- data/test/stateless_test.rb +33 -0
- data/test/test_helper.rb +61 -0
- metadata +223 -0
@@ -0,0 +1,503 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'json'
|
4
|
+
require 'time'
|
5
|
+
require 'socket'
|
6
|
+
|
7
|
+
# JsChat libraries
|
8
|
+
require 'jschat/errors'
|
9
|
+
require 'jschat/flood_protection'
|
10
|
+
|
11
|
+
module JsChat
|
12
|
+
STATELESS_TIMEOUT = 60
|
13
|
+
|
14
|
+
class User
|
15
|
+
include JsChat::FloodProtection
|
16
|
+
|
17
|
+
attr_accessor :name, :connection, :rooms, :last_activity,
|
18
|
+
:identified, :ip, :last_poll
|
19
|
+
|
20
|
+
def initialize(connection)
|
21
|
+
@name = nil
|
22
|
+
@connection = connection
|
23
|
+
@rooms = []
|
24
|
+
@last_activity = Time.now.utc
|
25
|
+
@last_poll = Time.now.utc
|
26
|
+
@identified = false
|
27
|
+
@ip = ''
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_json
|
31
|
+
{ 'name' => @name, 'last_activity' => @last_activity }.to_json
|
32
|
+
end
|
33
|
+
|
34
|
+
def name=(name)
|
35
|
+
if @connection and @connection.name_taken? name
|
36
|
+
raise JsChat::Errors::InvalidName.new(:name_taken, 'Name taken')
|
37
|
+
elsif User.valid_name?(name)
|
38
|
+
@identified = true
|
39
|
+
@name = name
|
40
|
+
else
|
41
|
+
raise JsChat::Errors::InvalidName.new(:invalid_name, 'Invalid name')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.valid_name?(name)
|
46
|
+
not name.match /[^[:alnum:]._\-\[\]^C]/ and name.size > 0
|
47
|
+
end
|
48
|
+
|
49
|
+
def private_message(message)
|
50
|
+
response = { 'display' => 'message', 'message' => message }
|
51
|
+
@connection.send_response response
|
52
|
+
end
|
53
|
+
|
54
|
+
def change(params)
|
55
|
+
# Valid options for change
|
56
|
+
['name'].each do |field|
|
57
|
+
if params[field]
|
58
|
+
old_value = send(field)
|
59
|
+
send "#{field}=", params[field]
|
60
|
+
@rooms.each do |room|
|
61
|
+
response = { 'change' => 'user',
|
62
|
+
'room' => room.name,
|
63
|
+
'user' => { field => { old_value => params[field] } } }
|
64
|
+
room.change_notice self, response
|
65
|
+
return [field, params[field]]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class Room
|
73
|
+
attr_accessor :name, :users
|
74
|
+
|
75
|
+
def initialize(name)
|
76
|
+
@name = name
|
77
|
+
@users = []
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.valid_name?(name)
|
81
|
+
User.valid_name?(name[1..-1]) and name[0].chr == '#'
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.find(item)
|
85
|
+
@@rooms ||= []
|
86
|
+
|
87
|
+
if item.kind_of? String
|
88
|
+
@@rooms.find { |room| room.name.downcase == item.downcase if room.name }
|
89
|
+
elsif item.kind_of? User
|
90
|
+
@@rooms.find_all { |room| room.users.include? item }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.find_or_create(room_name)
|
95
|
+
room = find room_name
|
96
|
+
if room.nil?
|
97
|
+
room = new(room_name)
|
98
|
+
@@rooms << room
|
99
|
+
end
|
100
|
+
room
|
101
|
+
end
|
102
|
+
|
103
|
+
def lastlog(since = nil)
|
104
|
+
{ 'display' => 'messages', 'messages' => messages_since(since) }
|
105
|
+
end
|
106
|
+
|
107
|
+
def messages_since(since)
|
108
|
+
if since.nil?
|
109
|
+
@messages
|
110
|
+
else
|
111
|
+
@messages.select { |m| message_time(m) > since }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def message_time(message)
|
116
|
+
if message.has_key? 'display'
|
117
|
+
message[message['display']]['time']
|
118
|
+
elsif message.has_key? 'change'
|
119
|
+
message[message['change']]['time']
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def add_to_lastlog(message)
|
124
|
+
@messages ||= []
|
125
|
+
if message
|
126
|
+
if message.has_key? 'display'
|
127
|
+
message[message['display']]['time'] = Time.now.utc
|
128
|
+
elsif message.has_key? 'change'
|
129
|
+
message[message['change']]['time'] = Time.now.utc
|
130
|
+
end
|
131
|
+
@messages.push message
|
132
|
+
@messages = @messages[-100..-1] if @messages.size > 100
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def join(user)
|
137
|
+
if @users.include? user
|
138
|
+
Error.new(:already_joined, 'Already in that room')
|
139
|
+
else
|
140
|
+
@users << user
|
141
|
+
user.rooms << self
|
142
|
+
join_notice user
|
143
|
+
{ 'display' => 'join', 'join' => { 'user' => user.name, 'room' => @name } }
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def part(user)
|
148
|
+
if not @users.include?(user)
|
149
|
+
Error.new(:not_in_room, 'Not in that room')
|
150
|
+
else
|
151
|
+
user.rooms.delete_if { |r| r == self }
|
152
|
+
@users.delete_if { |u| u == user }
|
153
|
+
part_notice user
|
154
|
+
{ 'display' => 'part', 'part' => { 'user' => user.name, 'room' => @name } }
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def send_message(message)
|
159
|
+
message['room'] = name
|
160
|
+
response = { 'display' => 'message', 'message' => message }
|
161
|
+
|
162
|
+
add_to_lastlog response
|
163
|
+
|
164
|
+
@users.each do |user|
|
165
|
+
user.connection.send_response response
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def member_names
|
170
|
+
@users.collect { |user| user.name }
|
171
|
+
end
|
172
|
+
|
173
|
+
def to_json
|
174
|
+
{ 'name' => @name, 'members' => member_names }.to_json
|
175
|
+
end
|
176
|
+
|
177
|
+
def notice(user, message, all = false)
|
178
|
+
add_to_lastlog message
|
179
|
+
|
180
|
+
@users.each do |u|
|
181
|
+
if (u != user and !all) or all
|
182
|
+
u.connection.send_response(message)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def change_notice(user, response)
|
188
|
+
notice(user, response, true)
|
189
|
+
end
|
190
|
+
|
191
|
+
def join_notice(user)
|
192
|
+
notice(user, { 'display' => 'join_notice', 'join_notice' => { 'user' => user.name, 'room' => @name } })
|
193
|
+
end
|
194
|
+
|
195
|
+
def part_notice(user)
|
196
|
+
notice(user, { 'display' => 'part_notice', 'part_notice' => { 'user' => user.name, 'room' => @name } })
|
197
|
+
end
|
198
|
+
|
199
|
+
def quit_notice(user)
|
200
|
+
notice(user, { 'display' => 'quit_notice', 'quit_notice' => { 'user' => user.name, 'room' => @name } })
|
201
|
+
@users.delete_if { |u| u == user }
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# User initially has a nil name
|
206
|
+
def users_with_names
|
207
|
+
@@users.find_all { |u| u.name }
|
208
|
+
end
|
209
|
+
|
210
|
+
def name_taken?(name)
|
211
|
+
users_with_names.find { |user| user.name.downcase == name.downcase }
|
212
|
+
end
|
213
|
+
|
214
|
+
# {"identify":"alex"}
|
215
|
+
def identify(name, ip, options = {})
|
216
|
+
if @user and @user.identified
|
217
|
+
Error.new :already_identified, 'You have already identified'
|
218
|
+
elsif name_taken? name
|
219
|
+
Error.new :name_taken, 'Name already taken'
|
220
|
+
else
|
221
|
+
@user.name = name
|
222
|
+
@user.ip = ip
|
223
|
+
register_stateless_user if @stateless
|
224
|
+
{ 'display' => 'identified', 'identified' => @user }
|
225
|
+
end
|
226
|
+
rescue JsChat::Errors::InvalidName => exception
|
227
|
+
exception
|
228
|
+
end
|
229
|
+
|
230
|
+
def lastlog(room, options = {})
|
231
|
+
room = Room.find room
|
232
|
+
if room and room.users.include? @user
|
233
|
+
room.lastlog
|
234
|
+
else
|
235
|
+
Error.new(:not_in_room, "Please join this room first")
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def since(room, options = {})
|
240
|
+
room = Room.find room
|
241
|
+
if room and room.users.include? @user
|
242
|
+
response = room.lastlog(@user.last_poll)
|
243
|
+
@user.last_poll = Time.now.utc
|
244
|
+
response
|
245
|
+
else
|
246
|
+
Error.new(:not_in_room, "Please join this room first")
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def ping(message, options = {})
|
251
|
+
if @user and @user.last_poll and Time.now.utc > @user.last_poll
|
252
|
+
time = Time.now.utc
|
253
|
+
{ 'pong' => time }
|
254
|
+
else
|
255
|
+
# TODO: HANDLE PING OUTS
|
256
|
+
Error.new(:ping_out, 'Your connection has been lost')
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def quit(message, options = {})
|
261
|
+
if @user
|
262
|
+
disconnect_user @user
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def room_message(message, options)
|
267
|
+
room = Room.find options['to']
|
268
|
+
if room and room.users.include? @user
|
269
|
+
room.send_message({ 'message' => message, 'user' => @user.name, 'time' => Time.now.utc })
|
270
|
+
else
|
271
|
+
send_response Error.new(:not_in_room, "Please join this room first")
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def private_message(message, options)
|
276
|
+
user = users_with_names.find { |u| u.name.downcase == options['to'].downcase }
|
277
|
+
if user
|
278
|
+
# Return the message to the user, and send it to the other person too
|
279
|
+
now = Time.now.utc
|
280
|
+
user.private_message({ 'message' => message, 'user' => @user.name, 'time' => now })
|
281
|
+
@user.private_message({ 'message' => message, 'user' => @user.name, 'time' => now })
|
282
|
+
else
|
283
|
+
Error.new(:not_online, 'User not online')
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def send_message(message, options)
|
288
|
+
if options['to'].nil?
|
289
|
+
send_response Error.new(:to_required, 'Please specify who to send the message to or join a channel')
|
290
|
+
elsif options['to'][0].chr == '#'
|
291
|
+
room_message message, options
|
292
|
+
else
|
293
|
+
private_message message, options
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def join(room_name, options = {})
|
298
|
+
if Room.valid_name? room_name
|
299
|
+
room = Room.find_or_create(room_name)
|
300
|
+
room.join @user
|
301
|
+
else
|
302
|
+
Error.new(:invalid_room, 'Invalid room name')
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def part(room_name, options = {})
|
307
|
+
room = @user.rooms.find { |r| r.name == room_name }
|
308
|
+
if room
|
309
|
+
room.part @user
|
310
|
+
else
|
311
|
+
Error.new(:not_in_room, "You are not in that room")
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def names(room_name, options = {})
|
316
|
+
room = Room.find(room_name)
|
317
|
+
if room
|
318
|
+
{ 'display' => 'names', 'names' => room.users, 'room' => room.name }
|
319
|
+
else
|
320
|
+
Error.new(:room_not_available, 'No such room')
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def new_cookie
|
325
|
+
chars = ("a".."z").to_a + ("1".."9").to_a
|
326
|
+
Array.new(8, '').collect { chars[rand(chars.size)] }.join
|
327
|
+
end
|
328
|
+
|
329
|
+
def register_stateless_cient
|
330
|
+
@stateless_cookie = new_cookie
|
331
|
+
user = User.new(self)
|
332
|
+
@@stateless_cookies << { :cookie => @stateless_cookie, :user => user }
|
333
|
+
@@users << user
|
334
|
+
{ 'cookie' => @stateless_cookie }
|
335
|
+
end
|
336
|
+
|
337
|
+
def current_stateless_client
|
338
|
+
@@stateless_cookies.find { |c| c[:cookie] == @stateless_cookie }
|
339
|
+
end
|
340
|
+
|
341
|
+
def register_stateless_user
|
342
|
+
current_stateless_client[:user] = @user
|
343
|
+
end
|
344
|
+
|
345
|
+
def valid_stateless_user?
|
346
|
+
current_stateless_client
|
347
|
+
end
|
348
|
+
|
349
|
+
def load_stateless_user
|
350
|
+
if client = current_stateless_client
|
351
|
+
@user = client[:user]
|
352
|
+
@stateless = true
|
353
|
+
else
|
354
|
+
raise JsChat::Errors::InvalidCookie.new(:invalid_cookie, 'Invalid cookie')
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def disconnect_lagged_users
|
359
|
+
@@stateless_cookies.delete_if do |cookie|
|
360
|
+
lagged?(cookie[:user].last_poll) ? disconnect_user(cookie[:user]) && true : false
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
def lagged?(time)
|
365
|
+
Time.now.utc - time > STATELESS_TIMEOUT
|
366
|
+
end
|
367
|
+
|
368
|
+
def unbind
|
369
|
+
return if @stateless
|
370
|
+
disconnect_user(@user)
|
371
|
+
@user = nil
|
372
|
+
end
|
373
|
+
|
374
|
+
def disconnect_user(user)
|
375
|
+
log :info, "Removing a connection"
|
376
|
+
Room.find(user).each do |room|
|
377
|
+
room.quit_notice user
|
378
|
+
end
|
379
|
+
|
380
|
+
@@users.delete_if { |u| u == user }
|
381
|
+
end
|
382
|
+
|
383
|
+
def post_init
|
384
|
+
@@users ||= []
|
385
|
+
@@stateless_cookies ||= []
|
386
|
+
@user = User.new(self)
|
387
|
+
@@users << @user
|
388
|
+
end
|
389
|
+
|
390
|
+
def log(level, message)
|
391
|
+
if Object.const_defined? :ServerConfig and ServerConfig[:logger]
|
392
|
+
if @user
|
393
|
+
message = "#{@user.name} (#{@user.ip}): #{message}"
|
394
|
+
end
|
395
|
+
ServerConfig[:logger].send level, message
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
def change(change, options = {})
|
400
|
+
if change == 'user'
|
401
|
+
field, value = @user.send :change, options[change]
|
402
|
+
{ 'display' => 'notice', 'notice' => "Your #{field} has been changed to: #{value}" }
|
403
|
+
else
|
404
|
+
Error.new(:invalid_request, "Invalid change request")
|
405
|
+
end
|
406
|
+
rescue JsChat::Errors::InvalidName => exception
|
407
|
+
exception
|
408
|
+
end
|
409
|
+
|
410
|
+
def send_response(data)
|
411
|
+
response = ''
|
412
|
+
case data
|
413
|
+
when String
|
414
|
+
response = data
|
415
|
+
when Error
|
416
|
+
response = data.to_json + "\n"
|
417
|
+
log :error, data.message
|
418
|
+
else
|
419
|
+
# Other objects should be safe for to_json
|
420
|
+
response = data.to_json + "\n"
|
421
|
+
log :info, response.strip
|
422
|
+
end
|
423
|
+
|
424
|
+
send_data response
|
425
|
+
end
|
426
|
+
|
427
|
+
include EM::Protocols::LineText2
|
428
|
+
|
429
|
+
def get_remote_ip
|
430
|
+
Socket.unpack_sockaddr_in(get_peername)[1]
|
431
|
+
end
|
432
|
+
|
433
|
+
def receive_line(data)
|
434
|
+
response = ''
|
435
|
+
disconnect_lagged_users
|
436
|
+
|
437
|
+
if data and data.size > ServerConfig[:max_message_length]
|
438
|
+
raise JsChat::Errors::MessageTooLong.new(:message_too_long, 'Message too long')
|
439
|
+
end
|
440
|
+
|
441
|
+
data.chomp.split("\n").each do |line|
|
442
|
+
# Receive the identify request
|
443
|
+
input = JSON.parse line
|
444
|
+
|
445
|
+
@user.seen!
|
446
|
+
|
447
|
+
# Unbind when a stateless connection doesn't match the cookie
|
448
|
+
if input.has_key?('cookie')
|
449
|
+
@stateless_cookie = input['cookie']
|
450
|
+
load_stateless_user
|
451
|
+
end
|
452
|
+
|
453
|
+
if input.has_key? 'protocol'
|
454
|
+
if input['protocol'] == 'stateless'
|
455
|
+
@stateless = true
|
456
|
+
response << send_response(register_stateless_cient)
|
457
|
+
end
|
458
|
+
elsif input.has_key? 'identify'
|
459
|
+
input['ip'] ||= get_remote_ip
|
460
|
+
response << send_response(identify(input['identify'], input['ip']))
|
461
|
+
else
|
462
|
+
['lastlog', 'change', 'send', 'join', 'names', 'part', 'since', 'ping', 'quit'].each do |command|
|
463
|
+
if @user.name.nil?
|
464
|
+
response << send_response(Error.new(:identity_required, "Identify first"))
|
465
|
+
return response
|
466
|
+
end
|
467
|
+
|
468
|
+
if input.has_key? command
|
469
|
+
if command == 'send'
|
470
|
+
@user.last_activity = Time.now.utc
|
471
|
+
message_result = send('send_message', input[command], input)
|
472
|
+
response << message_result if message_result.kind_of? String
|
473
|
+
else
|
474
|
+
result = send_response(send(command, input[command], input))
|
475
|
+
response << result if result.kind_of? String
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
response
|
483
|
+
rescue JsChat::Errors::StillFlooding
|
484
|
+
""
|
485
|
+
rescue JsChat::Errors::Flooding => exception
|
486
|
+
send_response exception
|
487
|
+
rescue JsChat::Errors::MessageTooLong => exception
|
488
|
+
send_response exception
|
489
|
+
rescue JsChat::Errors::InvalidCookie => exception
|
490
|
+
send_response exception
|
491
|
+
rescue Exception => exception
|
492
|
+
puts "Data that raised exception: #{exception}"
|
493
|
+
p data
|
494
|
+
print_call_stack
|
495
|
+
end
|
496
|
+
|
497
|
+
def print_call_stack(from = 2, to = 5)
|
498
|
+
puts "Stack:"
|
499
|
+
(from..to).each do |index|
|
500
|
+
puts "\t#{caller[index]}"
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
data/test/server_test.rb
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TestJsChat < Test::Unit::TestCase
|
4
|
+
include JsChatHelpers
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@jschat = JsChatMock.new
|
8
|
+
@jschat.post_init
|
9
|
+
end
|
10
|
+
|
11
|
+
def teardown
|
12
|
+
@jschat.reset
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_identify
|
16
|
+
response = JSON.parse @jschat.receive_line({ 'identify' => 'alex' }.to_json)
|
17
|
+
assert_equal 'identified', response['display']
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_invalid_identify
|
21
|
+
response = JSON.parse @jschat.receive_line({ 'identify' => '@lex' }.to_json)
|
22
|
+
assert_equal JsChat::Errors::Codes.invert[:invalid_name], response['error']['code']
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_identify_twice_fails
|
26
|
+
identify_as 'alex'
|
27
|
+
result = JSON.parse identify_as('alex')
|
28
|
+
assert_equal 106, result['error']['code']
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_ensure_nicks_are_longer_than_0
|
32
|
+
result = JSON.parse identify_as('')
|
33
|
+
assert result['error']
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_ensure_nicks_are_unique
|
37
|
+
identify_as 'alex'
|
38
|
+
|
39
|
+
# Obvious duplicate
|
40
|
+
result = identify_as 'alex'
|
41
|
+
assert result['error']
|
42
|
+
|
43
|
+
# Case
|
44
|
+
result = identify_as 'Alex'
|
45
|
+
assert result['error']
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_invalid_room_name
|
49
|
+
identify_as 'bob'
|
50
|
+
response = JSON.parse @jschat.receive_line({ 'join' => 'oublinet' }.to_json)
|
51
|
+
assert_equal 'Invalid room name', response['error']['message']
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_join
|
55
|
+
identify_as 'bob'
|
56
|
+
expected = { 'display' => 'join', 'join' => { 'user' => 'bob', 'room' => '#oublinet' } }.to_json + "\n"
|
57
|
+
assert_equal expected, @jschat.receive_line({ 'join' => '#oublinet' }.to_json)
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_join_without_identifying
|
61
|
+
response = JSON.parse @jschat.receive_line({ 'join' => '#oublinet' }.to_json)
|
62
|
+
assert response['error']
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_join_more_than_once
|
66
|
+
identify_as 'bob'
|
67
|
+
|
68
|
+
expected = { 'display' => 'error', 'error' => { 'message' => 'Already in that room' } }.to_json + "\n"
|
69
|
+
@jschat.receive_line({ 'join' => '#oublinet' }.to_json)
|
70
|
+
response = JSON.parse @jschat.receive_line({ 'join' => '#oublinet' }.to_json)
|
71
|
+
assert_equal JsChat::Errors::Codes.invert[:already_joined], response['error']['code']
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_names
|
75
|
+
identify_as 'nick', '#oublinet'
|
76
|
+
|
77
|
+
# Add a user
|
78
|
+
@jschat.add_user 'alex', '#oublinet'
|
79
|
+
|
80
|
+
response = JSON.parse(@jschat.receive_line({ 'names' => '#oublinet' }.to_json))
|
81
|
+
assert response['names']
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_valid_names
|
85
|
+
user = JsChat::User.new nil
|
86
|
+
['alex*', "alex\n"].each do |name|
|
87
|
+
assert_raises JsChat::Errors::InvalidName do
|
88
|
+
user.name = name
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_message_not_in_room
|
94
|
+
identify_as 'nick', '#oublinet'
|
95
|
+
@jschat.add_user 'alex', '#oublinet'
|
96
|
+
response = JSON.parse @jschat.receive_line({ 'send' => 'hello', 'to' => '#merk' }.to_json)
|
97
|
+
assert_equal 'Please join this room first', response['error']['message']
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_message
|
101
|
+
identify_as 'nick', '#oublinet'
|
102
|
+
@jschat.add_user 'alex', '#oublinet'
|
103
|
+
assert @jschat.receive_line({ 'send' => 'hello', 'to' => '#oublinet' }.to_json)
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_message_ignores_case
|
107
|
+
identify_as 'nick', '#oublinet'
|
108
|
+
@jschat.add_user 'alex', '#oublinet'
|
109
|
+
response = @jschat.receive_line({ 'send' => 'hello', 'to' => '#Oublinet' }.to_json)
|
110
|
+
assert response
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_part
|
114
|
+
identify_as 'nick', '#oublinet'
|
115
|
+
@jschat.add_user 'alex', '#oublinet'
|
116
|
+
response = JSON.parse @jschat.receive_line({ 'part' => '#oublinet'}.to_json)
|
117
|
+
assert_equal '#oublinet', response['part']['room']
|
118
|
+
end
|
119
|
+
|
120
|
+
def test_private_message
|
121
|
+
identify_as 'nick'
|
122
|
+
@jschat.add_user 'alex', '#oublinet'
|
123
|
+
response = JSON.parse @jschat.receive_line({ 'send' => 'hello', 'to' => 'alex' }.to_json)
|
124
|
+
assert_equal 'hello', response['message']['message']
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_private_message_ignores_case
|
128
|
+
identify_as 'nick'
|
129
|
+
@jschat.add_user 'alex', '#oublinet'
|
130
|
+
response = JSON.parse @jschat.receive_line({ 'send' => 'hello', 'to' => 'Alex' }.to_json)
|
131
|
+
assert_equal 'hello', response['message']['message']
|
132
|
+
end
|
133
|
+
|
134
|
+
def test_log_request
|
135
|
+
identify_as 'nick', '#oublinet'
|
136
|
+
@jschat.receive_line({ 'send' => 'hello', 'to' => '#oublinet' }.to_json)
|
137
|
+
response = JSON.parse @jschat.receive_line({ 'lastlog' => '#oublinet' }.to_json)
|
138
|
+
assert_equal 'hello', response['messages'].last['message']['message']
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_name_change
|
142
|
+
identify_as 'nick', '#oublinet'
|
143
|
+
@jschat.add_user 'alex', '#oublinet'
|
144
|
+
response = JSON.parse @jschat.receive_line({ 'change' => 'user', 'user' => { 'name' => 'bob' }}.to_json)
|
145
|
+
assert_equal 'notice', response['display']
|
146
|
+
end
|
147
|
+
|
148
|
+
def test_name_change_duplicate
|
149
|
+
identify_as 'nick', '#oublinet'
|
150
|
+
@jschat.add_user 'alex', '#oublinet'
|
151
|
+
response = JSON.parse @jschat.receive_line({ 'change' => 'user', 'user' => { 'name' => 'alex' }}.to_json)
|
152
|
+
assert_equal 'error', response['display']
|
153
|
+
end
|
154
|
+
|
155
|
+
def test_max_message_length
|
156
|
+
identify_as 'nick', '#oublinet'
|
157
|
+
response = JSON.parse @jschat.receive_line({ 'send' => 'a' * 1000, 'to' => '#oublinet' }.to_json)
|
158
|
+
assert response['error']
|
159
|
+
end
|
160
|
+
|
161
|
+
def test_flood_protection
|
162
|
+
identify_as 'nick', '#oublinet'
|
163
|
+
response = ''
|
164
|
+
# simulate a flood and extract the error response
|
165
|
+
(1..50).detect do
|
166
|
+
response = @jschat.receive_line({ 'send' => 'a' * 10, 'to' => '#oublinet' }.to_json)
|
167
|
+
response.match /error/
|
168
|
+
end
|
169
|
+
response = JSON.parse response
|
170
|
+
assert response['error']
|
171
|
+
assert_match /wait a few seconds/i, response['error']['message']
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TestJsChat < Test::Unit::TestCase
|
4
|
+
include JsChatHelpers
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@jschat = JsChatMock.new
|
8
|
+
@jschat.post_init
|
9
|
+
@cookie = get_cookie
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_identify
|
13
|
+
response = send_to_jschat({ 'identify' => 'alex', 'cookie' => @cookie })
|
14
|
+
assert_equal 'identified', response['display']
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_join
|
18
|
+
response = identify_as 'alex2', '#jschat'
|
19
|
+
assert JSON.parse(response)['join']
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_message
|
23
|
+
response = identify_as 'nick', '#jschat'
|
24
|
+
assert send_to_jschat({ 'cookie' => @cookie, 'send' => 'hello', 'to' => '#jschat' }, false)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def get_cookie
|
30
|
+
JSON.parse(@jschat.receive_line({ 'protocol' => 'stateless' }.to_json))['cookie']
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|