jschat 0.1.1
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/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
|