jschat 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. data/MIT-LICENSE +23 -0
  2. data/README.textile +71 -0
  3. data/bin/jschat-client +3 -0
  4. data/bin/jschat-server +18 -0
  5. data/bin/jschat-web +7 -0
  6. data/lib/jschat/client.rb +745 -0
  7. data/lib/jschat/errors.rb +41 -0
  8. data/lib/jschat/flood_protection.rb +39 -0
  9. data/lib/jschat/http/config/sprockets.yml +7 -0
  10. data/lib/jschat/http/config.ru +12 -0
  11. data/lib/jschat/http/jschat.rb +264 -0
  12. data/lib/jschat/http/public/favicon.ico +0 -0
  13. data/lib/jschat/http/public/images/emoticons/angry.gif +0 -0
  14. data/lib/jschat/http/public/images/emoticons/arr.gif +0 -0
  15. data/lib/jschat/http/public/images/emoticons/blink.gif +0 -0
  16. data/lib/jschat/http/public/images/emoticons/blush.gif +0 -0
  17. data/lib/jschat/http/public/images/emoticons/brucelee.gif +0 -0
  18. data/lib/jschat/http/public/images/emoticons/btw.gif +0 -0
  19. data/lib/jschat/http/public/images/emoticons/chuckle.gif +0 -0
  20. data/lib/jschat/http/public/images/emoticons/clap.gif +0 -0
  21. data/lib/jschat/http/public/images/emoticons/cool.gif +0 -0
  22. data/lib/jschat/http/public/images/emoticons/drool.gif +0 -0
  23. data/lib/jschat/http/public/images/emoticons/drunk.gif +0 -0
  24. data/lib/jschat/http/public/images/emoticons/dry.gif +0 -0
  25. data/lib/jschat/http/public/images/emoticons/eek.gif +0 -0
  26. data/lib/jschat/http/public/images/emoticons/flex.gif +0 -0
  27. data/lib/jschat/http/public/images/emoticons/happy.gif +0 -0
  28. data/lib/jschat/http/public/images/emoticons/holmes.gif +0 -0
  29. data/lib/jschat/http/public/images/emoticons/huh.gif +0 -0
  30. data/lib/jschat/http/public/images/emoticons/laugh.gif +0 -0
  31. data/lib/jschat/http/public/images/emoticons/lol.gif +0 -0
  32. data/lib/jschat/http/public/images/emoticons/mad.gif +0 -0
  33. data/lib/jschat/http/public/images/emoticons/mellow.gif +0 -0
  34. data/lib/jschat/http/public/images/emoticons/noclue.gif +0 -0
  35. data/lib/jschat/http/public/images/emoticons/oh.gif +0 -0
  36. data/lib/jschat/http/public/images/emoticons/ohmy.gif +0 -0
  37. data/lib/jschat/http/public/images/emoticons/ph34r.gif +0 -0
  38. data/lib/jschat/http/public/images/emoticons/pimp.gif +0 -0
  39. data/lib/jschat/http/public/images/emoticons/punch.gif +0 -0
  40. data/lib/jschat/http/public/images/emoticons/realmad.gif +0 -0
  41. data/lib/jschat/http/public/images/emoticons/rock.gif +0 -0
  42. data/lib/jschat/http/public/images/emoticons/rofl.gif +0 -0
  43. data/lib/jschat/http/public/images/emoticons/rolleyes.gif +0 -0
  44. data/lib/jschat/http/public/images/emoticons/sad.gif +0 -0
  45. data/lib/jschat/http/public/images/emoticons/scratch.gif +0 -0
  46. data/lib/jschat/http/public/images/emoticons/shifty.gif +0 -0
  47. data/lib/jschat/http/public/images/emoticons/shock.gif +0 -0
  48. data/lib/jschat/http/public/images/emoticons/shrug.gif +0 -0
  49. data/lib/jschat/http/public/images/emoticons/sleep.gif +0 -0
  50. data/lib/jschat/http/public/images/emoticons/sleeping.gif +0 -0
  51. data/lib/jschat/http/public/images/emoticons/smile.gif +0 -0
  52. data/lib/jschat/http/public/images/emoticons/suicide.gif +0 -0
  53. data/lib/jschat/http/public/images/emoticons/sweat.gif +0 -0
  54. data/lib/jschat/http/public/images/emoticons/thumbs.gif +0 -0
  55. data/lib/jschat/http/public/images/emoticons/tongue.gif +0 -0
  56. data/lib/jschat/http/public/images/emoticons/unsure.gif +0 -0
  57. data/lib/jschat/http/public/images/emoticons/w00t.gif +0 -0
  58. data/lib/jschat/http/public/images/emoticons/wacko.gif +0 -0
  59. data/lib/jschat/http/public/images/emoticons/whistling.gif +0 -0
  60. data/lib/jschat/http/public/images/emoticons/wink.gif +0 -0
  61. data/lib/jschat/http/public/images/emoticons/worship.gif +0 -0
  62. data/lib/jschat/http/public/images/emoticons/yucky.gif +0 -0
  63. data/lib/jschat/http/public/images/jschat.gif +0 -0
  64. data/lib/jschat/http/public/images/shadow.png +0 -0
  65. data/lib/jschat/http/public/javascripts/app/controllers/chat_controller.js +191 -0
  66. data/lib/jschat/http/public/javascripts/app/controllers/signon_controller.js +56 -0
  67. data/lib/jschat/http/public/javascripts/app/helpers/emote_helper.js +23 -0
  68. data/lib/jschat/http/public/javascripts/app/helpers/form_helpers.js +37 -0
  69. data/lib/jschat/http/public/javascripts/app/helpers/link_helper.js +47 -0
  70. data/lib/jschat/http/public/javascripts/app/helpers/page_helper.js +27 -0
  71. data/lib/jschat/http/public/javascripts/app/helpers/text_helper.js +92 -0
  72. data/lib/jschat/http/public/javascripts/app/lib/split.js +78 -0
  73. data/lib/jschat/http/public/javascripts/app/models/cookie.js +27 -0
  74. data/lib/jschat/http/public/javascripts/app/protocol/change.js +15 -0
  75. data/lib/jschat/http/public/javascripts/app/protocol/chat_request.js +13 -0
  76. data/lib/jschat/http/public/javascripts/app/protocol/display.js +147 -0
  77. data/lib/jschat/http/public/javascripts/app/ui/commands.js +55 -0
  78. data/lib/jschat/http/public/javascripts/app/ui/tab_completion.js +122 -0
  79. data/lib/jschat/http/public/javascripts/init.js +19 -0
  80. data/lib/jschat/http/public/stylesheets/iphone.css +3 -0
  81. data/lib/jschat/http/public/stylesheets/screen.css +68 -0
  82. data/lib/jschat/http/script/sprockets.rb +14 -0
  83. data/lib/jschat/http/tmp/restart.txt +0 -0
  84. data/lib/jschat/http/views/index.erb +23 -0
  85. data/lib/jschat/http/views/iphone.erb +29 -0
  86. data/lib/jschat/http/views/layout.erb +29 -0
  87. data/lib/jschat/http/views/message_form.erb +15 -0
  88. data/lib/jschat/server.rb +503 -0
  89. data/test/server_test.rb +175 -0
  90. data/test/stateless_test.rb +33 -0
  91. data/test/test_helper.rb +61 -0
  92. metadata +223 -0
@@ -0,0 +1,745 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+ require 'json'
4
+ require 'ncurses'
5
+ require 'optparse'
6
+ require 'time'
7
+
8
+ options = {}
9
+
10
+ ARGV.clone.options do |opts|
11
+ script_name = File.basename($0)
12
+ opts.banner = "Usage: #{$0} [options]"
13
+
14
+ opts.separator ""
15
+
16
+ opts.on("-h", "--hostname=host", String, "JsChat server hostname") { |o| options['hostname'] = o }
17
+ opts.on("-p", "--port=port", String, "JsChat server port number") { |o| options['port'] = o }
18
+ opts.on("-r", "--room=#room", String, "Channel to auto-join: remember to escape the hash") { |o| options['room'] = o }
19
+ opts.on("-n", "--nick=name", String, "Your name") { |o| options['nick'] = o }
20
+ opts.on("--help", "-H", "This text") { puts opts; exit 0 }
21
+
22
+ opts.parse!
23
+ end
24
+
25
+ ClientConfig = {
26
+ :port => options['port'] || '6789',
27
+ :ip => options['hostname'] || '0.0.0.0',
28
+ :name => options['nick'] || ENV['LOGNAME'],
29
+ :auto_join => options['room'] || '#jschat'
30
+ }
31
+
32
+ module Ncurses
33
+ KEY_DELETE = ?\C-h
34
+ KEY_TAB = 9
35
+ end
36
+
37
+ module JsChat
38
+ class Protocol
39
+ class Response
40
+ attr_reader :message, :time
41
+ def initialize(message, time)
42
+ @message, @time = message, time
43
+ end
44
+ end
45
+
46
+ def initialize(connection)
47
+ @connection = connection
48
+ end
49
+
50
+ def legal_commands
51
+ %w(messages message joined quit error join names part identified part_notice quit_notice join_notice)
52
+ end
53
+
54
+ def legal_change_commands
55
+ %w(user)
56
+ end
57
+
58
+ def legal?(command)
59
+ legal_commands.include? command
60
+ end
61
+
62
+ def legal_change?(command)
63
+ legal_change_commands.include? command
64
+ end
65
+
66
+ def identified? ; @identified ; end
67
+
68
+ def change_user(json)
69
+ original_name = json['name'].keys().first
70
+ new_name = json['name'].values.first
71
+ @connection.names.delete original_name
72
+ @connection.names << new_name
73
+ "* User #{original_name} is now known as #{new_name}"
74
+ end
75
+
76
+ def message(json)
77
+ if json['room']
78
+ "[#{json['room']}] <#{json['user']}> #{json['message']}"
79
+ else
80
+ "PRIVATE <#{json['user']}> #{json['message']}"
81
+ end
82
+ end
83
+
84
+ def messages(messages)
85
+ results = []
86
+ messages.each do |json|
87
+ results << process_message(json)
88
+ end
89
+ results
90
+ end
91
+
92
+ def get_command(json)
93
+ if json.has_key? 'display' and legal? json['display']
94
+ 'display'
95
+ elsif json.has_key? 'change' and legal_change? json['change']
96
+ 'change'
97
+ end
98
+ end
99
+
100
+ def get_time(json)
101
+ if json.kind_of? Hash and json.has_key? 'time'
102
+ Time.parse(json['time']).getlocal
103
+ else
104
+ Time.now.localtime
105
+ end
106
+ end
107
+
108
+ def protocol_method_name(command, method_name)
109
+ if command == 'display'
110
+ method_name
111
+ elsif command == 'change'
112
+ "change_#{method_name}"
113
+ end
114
+ end
115
+
116
+ def process_message(json)
117
+ command = get_command json
118
+
119
+ if command
120
+ time = get_time json[json[command]]
121
+ response = send(protocol_method_name(command, json[command]), json[json[command]])
122
+ case response
123
+ when Array
124
+ response
125
+ when String
126
+ Response.new(response, time)
127
+ end
128
+ end
129
+ end
130
+
131
+ def join(json)
132
+ @connection.send_names json['room']
133
+ "* User #{json['user']} joined #{json['room']}"
134
+ end
135
+
136
+ def join_notice(json)
137
+ @connection.names << json['user']
138
+ "* User #{json['user']} joined #{json['room']}"
139
+ end
140
+
141
+ def part(json)
142
+ "* You left #{json['room']}"
143
+ end
144
+
145
+ def part_notice(json)
146
+ @connection.names.delete json['user']
147
+ "* #{json['user']} left #{json['room']}"
148
+ end
149
+
150
+ def quit(json)
151
+ @connection.names.delete json['user']
152
+ "* User #{json['user']} left #{json['room']}"
153
+ end
154
+
155
+ def names(json)
156
+ @connection.names = json.collect { |u| u['name'] }
157
+ "* In this room: #{@connection.names.join(', ')}"
158
+ end
159
+
160
+ def identified(json)
161
+ if ClientConfig[:auto_join] and !@auto_joined
162
+ @connection.send_join ClientConfig[:auto_join]
163
+ @auto_joined = true
164
+ end
165
+
166
+ @identified = true
167
+
168
+ "* You are now known as #{json['name']}"
169
+ end
170
+
171
+ def error(json)
172
+ "* [ERROR] #{json['message']}"
173
+ end
174
+
175
+ alias_method :quit_notice, :quit
176
+ end
177
+ end
178
+
179
+ module JsClient
180
+ class TabComplete
181
+ attr_accessor :matched, :index
182
+
183
+ def initialize
184
+ end
185
+
186
+ def run(names, field, form, cursor_x)
187
+ form.form_driver Ncurses::Form::REQ_BEG_LINE
188
+ text = field.field_buffer(0).dup.strip
189
+ if text.size > 0 and cursor_x > 0
190
+ if @matched.nil?
191
+ source = text.slice(0, cursor_x)
192
+ source = text.split(' ').last if text.match(' ')
193
+ @index = 0
194
+ else
195
+ source = @matched
196
+ @index += 1
197
+ end
198
+ names = names.sort.find_all { |name| name.match /^#{source}/i }
199
+ @index = 0 if @index >= names.size
200
+ name = names[@index]
201
+ if name and (@matched or source.size == text.size)
202
+ @matched = source
203
+ field.set_field_buffer(0, "#{name}: ")
204
+ form.form_driver Ncurses::Form::REQ_END_LINE
205
+ form.form_driver Ncurses::Form::REQ_NEXT_CHAR
206
+ else
207
+ @matched = nil
208
+ @index = 0
209
+ (0..cursor_x - 1).each do
210
+ form.form_driver Ncurses::Form::REQ_NEXT_CHAR
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ def match(input)
217
+ end
218
+
219
+ def reset
220
+ @matched = nil
221
+ @index = 0
222
+ end
223
+ end
224
+
225
+ def keyboard=(keyboard)
226
+ @keyboard = keyboard
227
+ end
228
+
229
+ # This should take room into account
230
+ def names=(names)
231
+ @names = names
232
+ end
233
+
234
+ def names
235
+ @names
236
+ end
237
+
238
+ module KeyboardInput
239
+ def setup_screen
240
+ Ncurses.initscr
241
+ @windows = {}
242
+
243
+ Ncurses.raw
244
+ Ncurses.start_color
245
+ Ncurses.noecho
246
+ Ncurses.use_default_colors
247
+ Ncurses.init_pair 2, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLUE
248
+ Ncurses.init_pair 3, Ncurses::COLOR_CYAN, -1
249
+ Ncurses.init_pair 4, Ncurses::COLOR_YELLOW, -1
250
+ Ncurses.init_pair 5, Ncurses::COLOR_BLACK, -1
251
+ Ncurses.init_pair 6, Ncurses::COLOR_RED, -1
252
+
253
+ @history_position = 0
254
+ @history = []
255
+ @lastlog = []
256
+ @room_name = ''
257
+
258
+ setup_windows
259
+ end
260
+
261
+ def room_name=(room_name)
262
+ @room_name = room_name
263
+ end
264
+
265
+ def setup_windows
266
+ Ncurses.refresh
267
+
268
+ display_windows
269
+
270
+ Thread.new do
271
+ loop do
272
+ display_time
273
+ sleep 60 - Time.now.sec
274
+ end
275
+ end
276
+
277
+ Signal.trap('SIGWINCH') do
278
+ resize
279
+ end
280
+ end
281
+
282
+ def remove_windows_and_forms
283
+ remove_windows
284
+ remove_forms
285
+ end
286
+
287
+ def remove_windows
288
+ @windows.each do |name, window|
289
+ window.delete
290
+ end
291
+ end
292
+
293
+ def remove_forms
294
+ @input_field.free_field
295
+ @input_form.free_form
296
+ end
297
+
298
+ def update_windows
299
+ rows, cols = get_window_size
300
+ remove_windows_and_forms
301
+ display_windows
302
+ display_room_name
303
+ end
304
+
305
+ def display_windows
306
+ rows, cols = get_window_size
307
+ @windows[:text] = Ncurses.newwin(rows - 1, cols, 0, 0)
308
+ @windows[:info] = Ncurses.newwin(rows - 1, cols, rows - 2, 0)
309
+ @windows[:input] = Ncurses.newwin(rows, cols, rows - 1, 0)
310
+ @windows[:text].scrollok(true)
311
+ @windows[:info].bkgd Ncurses.COLOR_PAIR(2)
312
+ @windows[:input].keypad(true)
313
+ @windows[:input].nodelay(true)
314
+
315
+ @windows[:text].refresh
316
+ @windows[:info].refresh
317
+ @windows[:input].refresh
318
+ display_input
319
+ end
320
+
321
+ def display_input
322
+ offset = @room_name.size > 0 ? @room_name.size + 3 : 0
323
+ @input_field = Ncurses::Form::FIELD.new(1, Ncurses.COLS - offset, 0, offset, 0, 0)
324
+ Ncurses::Form.field_opts_off(@input_field, Ncurses::Form::O_AUTOSKIP)
325
+ Ncurses::Form.field_opts_off(@input_field, Ncurses::Form::O_STATIC)
326
+ @input_form = Ncurses::Form::FORM.new([@input_field])
327
+ @input_form.set_form_win @windows[:input]
328
+ @input_form.post_form
329
+ @input_field.set_field_buffer 0, ''
330
+ end
331
+
332
+ def display_room_name
333
+ if @room_name
334
+ display_input
335
+ @windows[:input].mvprintw(0, 0, "[#{@room_name}] ")
336
+ @windows[:input].refresh
337
+ end
338
+ end
339
+
340
+ def display_time
341
+ @windows[:info].move 0, 0
342
+ @windows[:info].addstr "[#{Time.now.strftime('%H:%M')}]\n"
343
+ @windows[:info].refresh
344
+ @windows[:input].refresh
345
+ end
346
+
347
+ def resize
348
+ # Save the user input
349
+ @input_form.form_driver Ncurses::Form::REQ_BEG_LINE
350
+ input = @input_field.field_buffer(0)
351
+ input.rstrip!
352
+
353
+ Ncurses.def_prog_mode
354
+ Ncurses.endwin
355
+ Ncurses.reset_prog_mode
356
+
357
+ update_windows
358
+
359
+ cols = get_window_size[0]
360
+ if @lastlog.size > 0
361
+ lastlog_start = cols > @lastlog.size ? @lastlog.size : cols
362
+ @lastlog[-lastlog_start..-1].each do |message|
363
+ display_text message[0], message[1]
364
+ end
365
+ end
366
+
367
+ @input_field.set_field_buffer(0, input)
368
+ @windows[:input].addstr input
369
+ @input_form.form_driver Ncurses::Form::REQ_END_LINE
370
+
371
+ Ncurses.refresh
372
+ rescue Exception => exception
373
+ puts exception
374
+ end
375
+
376
+ # FIXME: This doesn't work after resize
377
+ # I've tried other ruby ncurses programs and they don't either
378
+ def get_window_size
379
+ Ncurses.refresh
380
+ cols, rows = [], []
381
+ Ncurses.stdscr.getmaxyx rows, cols
382
+ [rows.first, cols.first]
383
+ end
384
+
385
+ def get_history_text(offset = 1)
386
+ offset_position = @history_position + offset
387
+ if offset_position >= 0 and offset_position < @history.size
388
+ @history_position = offset_position
389
+ @history[@history_position]
390
+ else
391
+ if @history_position > -1 and @history_position < @history.size
392
+ @history_position = offset_position
393
+ end
394
+ ''
395
+ end
396
+ end
397
+
398
+ def arrow_keys(data)
399
+ c = data[0]
400
+ if @sequence
401
+ @sequence << c
402
+ history_text = ''
403
+
404
+ if data == 'A'
405
+ @sequence = nil
406
+ history_text = get_history_text(-1)
407
+ elsif data == 'B'
408
+ @sequence = nil
409
+ history_text = get_history_text
410
+ elsif data == 'O'
411
+ return true
412
+ elsif data == 'D'
413
+ # left
414
+ @sequence = nil
415
+ @input_form.form_driver Ncurses::Form::REQ_PREV_CHAR
416
+ @windows[:input].refresh
417
+ return true
418
+ elsif data == 'C'
419
+ # right
420
+ @input_form.form_driver Ncurses::Form::REQ_NEXT_CHAR
421
+ @windows[:input].refresh
422
+ @sequence = nil
423
+ return true
424
+ else
425
+ @sequence = nil
426
+ return true
427
+ end
428
+
429
+ begin
430
+ @input_form.form_driver Ncurses::Form::REQ_CLR_FIELD
431
+ @input_field.set_field_buffer(0, history_text)
432
+ @windows[:input].addstr history_text
433
+ @windows[:input].refresh
434
+ @input_form.form_driver Ncurses::Form::REQ_END_LINE
435
+ rescue Exception => exception
436
+ end
437
+
438
+ return true
439
+ elsif c == 27
440
+ @sequence = c
441
+ return true
442
+ end
443
+ end
444
+
445
+ def cursor_position
446
+ cursor_position = Ncurses.getcurx(@windows[:input]) - 10
447
+ end
448
+
449
+ def receive_data(data)
450
+ @clipboard ||= ''
451
+ c = data[0]
452
+
453
+ if arrow_keys data
454
+ return
455
+ end
456
+
457
+ if c != Ncurses::KEY_TAB
458
+ @tab_completion.reset
459
+ end
460
+
461
+ case c
462
+ when -1
463
+ # Return
464
+ when Ncurses::KEY_TAB
465
+ @tab_completion.run @connection.names, @input_field, @input_form, cursor_position
466
+ when Ncurses::KEY_ENTER, ?\n, ?\r
467
+ @input_form.form_driver Ncurses::Form::REQ_BEG_LINE
468
+ line = @input_field.field_buffer(0)
469
+ line.rstrip!
470
+
471
+ if !line.empty? and line.length > 0
472
+ @history << line.dup
473
+ @history_position = @history.size
474
+ manage_commands line
475
+ end
476
+ @input_form.form_driver Ncurses::Form::REQ_CLR_FIELD
477
+ when ?\C-l
478
+ # Refresh
479
+ resize
480
+ when Ncurses::KEY_BACKSPACE, ?\C-h, 127
481
+ # Backspace
482
+ @input_form.form_driver Ncurses::Form::REQ_DEL_PREV
483
+ @input_form.form_driver Ncurses::Form::REQ_CLR_EOL
484
+ when ?\C-d
485
+ @input_form.form_driver Ncurses::Form::REQ_DEL_CHAR
486
+ when Ncurses::KEY_LEFT, ?\C-b
487
+ @input_form.form_driver Ncurses::Form::REQ_PREV_CHAR
488
+ when Ncurses::KEY_RIGHT, ?\C-f
489
+ @input_form.form_driver Ncurses::Form::REQ_NEXT_CHAR
490
+ when ?\C-a
491
+ @input_form.form_driver Ncurses::Form::REQ_BEG_LINE
492
+ when ?\C-e
493
+ @input_form.form_driver Ncurses::Form::REQ_END_LINE
494
+ when ?\C-k
495
+ @input_form.form_driver Ncurses::Form::REQ_CLR_EOL
496
+ when ?\C-u
497
+ @input_form.form_driver Ncurses::Form::REQ_BEG_LINE
498
+ @clipboard = @input_field.field_buffer(0)
499
+ @input_form.form_driver Ncurses::Form::REQ_CLR_FIELD
500
+ when ?\C-y
501
+ unless @clipboard.empty?
502
+ cursor_position = Ncurses.getcurx(@windows[:input])
503
+
504
+ text = @input_field.field_buffer(0).insert(cursor_position - 9, @clipboard)
505
+ @input_field.set_field_buffer(0, text)
506
+
507
+ @windows[:text].addstr "#{cursor_position}\n #{text}"
508
+ @windows[:text].refresh
509
+ end
510
+ when ?\C-c
511
+ quit
512
+ when ?\C-w
513
+ @input_form.form_driver Ncurses::Form::REQ_PREV_CHAR
514
+ @input_form.form_driver Ncurses::Form::REQ_DEL_WORD
515
+ else
516
+ @input_form.form_driver c
517
+ end
518
+ @windows[:input].refresh
519
+ end
520
+
521
+ def show_message(messages, time = Time.now.localtime)
522
+ messages.split("\n").each do |message|
523
+ @lastlog << [message.dup, time]
524
+ @lastlog.shift if @lastlog.size > 250
525
+ display_text message, time
526
+ end
527
+ end
528
+
529
+ def display_text(message, time = Time.now.localtime)
530
+ message = message.dup
531
+ @windows[:text].addstr "#{time.strftime('%H:%M')} "
532
+
533
+ if message.match /^\*/
534
+ @windows[:text].attrset(Ncurses.COLOR_PAIR(3))
535
+ end
536
+
537
+ if message.match /^\* \[ERROR\]/
538
+ @windows[:text].attrset(Ncurses.COLOR_PAIR(6))
539
+ elsif message.match /^\[/
540
+ channel_info = message.split(']').first.sub /\[/, ''
541
+ message.sub! "[#{channel_info}]", ''
542
+
543
+ @windows[:text].attrset(Ncurses.COLOR_PAIR(5))
544
+ @windows[:text].addstr "["
545
+ @windows[:text].attrset(Ncurses.COLOR_PAIR(4))
546
+ @windows[:text].addstr "#{channel_info}"
547
+ @windows[:text].attrset(Ncurses.COLOR_PAIR(5))
548
+ @windows[:text].addstr "] "
549
+
550
+ name = message.split('>').first.sub(/</, '').strip
551
+ message.sub!("<#{name}> ", '')
552
+ @windows[:text].attrset(Ncurses.COLOR_PAIR(5))
553
+ @windows[:text].addstr '<'
554
+ @windows[:text].attrset(Ncurses.COLOR_PAIR(0))
555
+ @windows[:text].addstr "#{name}"
556
+ @windows[:text].attrset(Ncurses.COLOR_PAIR(5))
557
+ @windows[:text].addstr '>'
558
+ @windows[:text].attrset(Ncurses.COLOR_PAIR(0))
559
+ end
560
+
561
+ @windows[:text].addstr "#{message}\n"
562
+ @windows[:text].refresh
563
+ @windows[:input].refresh
564
+
565
+ display_time
566
+
567
+ if message.match /^\*/
568
+ @windows[:text].attrset(Ncurses.COLOR_PAIR(0))
569
+ end
570
+ end
571
+
572
+ def quit
573
+ Ncurses.endwin
574
+ exit
575
+ end
576
+
577
+ def help_text
578
+ <<-TEXT
579
+ *** JsChat Help ***
580
+ Commands start with a forward slash. Parameters in square brackets are optional.
581
+ /name new_name - Change your name. Alias: /nick
582
+ /names [room name] - List the people in a room.
583
+ /join #room - Join a room. Alias: /j
584
+ /switch #room - Speak in a different room. Alias: /s
585
+ /part #room - Leave a room. Alias: /p
586
+ /message person - Send a private message. Alias: /m
587
+ /lastlog - Display the last 100 messages for a room.
588
+ /quit - Quit JsChat
589
+ *** End Help ***
590
+
591
+ TEXT
592
+ end
593
+
594
+ def manage_commands(line)
595
+ operand = strip_command line
596
+ case line
597
+ when %r{^/switch}, %r{^/s}
598
+ @connection.switch_room operand
599
+ when %r{^/names}
600
+ @connection.send_names operand
601
+ when %r{^/name}, %r{^/nick}
602
+ @connection.send_change_name operand
603
+ when %r{^/quit}
604
+ quit
605
+ when %r{^/join}, %r{^/j}
606
+ @connection.send_join operand
607
+ when %r{^/part}, %r{^/p}
608
+ @connection.send_part operand
609
+ when %r{^/lastlog}
610
+ @connection.send_lastlog
611
+ when %r{^/clear}
612
+ @windows[:text].clear
613
+ @windows[:text].refresh
614
+ display_time
615
+ when %r{^/message}, %r{^/m}
616
+ if operand and operand.size > 0
617
+ message = operand.match(/([^ ]*)\s+(.*)/)
618
+ if message
619
+ @connection.send_private_message message[1], message[2]
620
+ end
621
+ end
622
+ when %r{^/help}
623
+ help_text.split("\n").each do |message|
624
+ display_text message
625
+ end
626
+ when %r{^/}
627
+ display_text '* Command not found. Try using /help'
628
+ else
629
+ @connection.send_message(line)
630
+ end
631
+ end
632
+
633
+ def strip_command(line)
634
+ matches = line.match(%r{/[a-zA-Z][^ ]*(.*)})
635
+ if matches
636
+ matches[1].strip
637
+ else
638
+ line
639
+ end
640
+ end
641
+
642
+ def connection=(connection)
643
+ @connection = connection
644
+ @tab_completion = JsClient::TabComplete.new
645
+ end
646
+ end
647
+
648
+ include EM::Protocols::LineText2
649
+
650
+ def receive_line(data)
651
+ data.split("\n").each do |line|
652
+ json = JSON.parse(line.strip)
653
+ # Execute the json
654
+ protocol_response = @protocol.process_message json
655
+ if protocol_response
656
+ if protocol_response.kind_of? Array
657
+ protocol_response.each do |response|
658
+ @keyboard.show_message response.message, response.time
659
+ end
660
+ else
661
+ @keyboard.show_message protocol_response.message, protocol_response.time
662
+ end
663
+ elsif json.has_key? 'notice'
664
+ @keyboard.show_message "* #{json['notice']}"
665
+ else
666
+ @keyboard.show_message "* [SERVER] #{line}"
667
+ end
668
+ end
669
+ rescue Exception => exception
670
+ @keyboard.show_message "* [CLIENT ERROR] #{exception}"
671
+ end
672
+
673
+ def send_join(room)
674
+ @current_room = room
675
+ @keyboard.room_name = room
676
+ @keyboard.display_room_name
677
+ send_data({ 'join' => room }.to_json + "\n")
678
+ end
679
+
680
+ def switch_room(room)
681
+ @current_room = room
682
+ @keyboard.room_name = room
683
+ @keyboard.display_room_name
684
+ @keyboard.show_message "* Switched room to: #{room}"
685
+ end
686
+
687
+ def send_part(room = nil)
688
+ room = @current_room if room.nil?
689
+ send_data({ 'part' => room }.to_json + "\n")
690
+ end
691
+
692
+ def send_names(room = nil)
693
+ room = @current_room if room.nil? or room.strip.empty?
694
+ send_data({ 'names' => room }.to_json + "\n")
695
+ end
696
+
697
+ def send_lastlog(room = nil)
698
+ room = @current_room if room.nil? or room.strip.empty?
699
+ send_data({ 'lastlog' => room }.to_json + "\n")
700
+ end
701
+
702
+ def send_message(line)
703
+ send_data({ 'to' => @current_room, 'send' => line }.to_json + "\n")
704
+ end
705
+
706
+ def send_private_message(user, message)
707
+ send_data({ 'to' => user, 'send' => message }.to_json + "\n")
708
+ end
709
+
710
+ def send_identify(username)
711
+ send_data({ 'identify' => username }.to_json + "\n")
712
+ end
713
+
714
+ def send_change_name(username)
715
+ if @protocol.identified?
716
+ send_data({ 'change' => 'user', 'user' => { 'name' => username } }.to_json + "\n")
717
+ else
718
+ send_identify username
719
+ end
720
+ end
721
+
722
+ def unbind
723
+ Ncurses.endwin
724
+ Ncurses.clear
725
+ puts "Disconnected from server"
726
+ exit
727
+ end
728
+
729
+ def post_init
730
+ # When connected
731
+ @protocol = JsChat::Protocol.new self
732
+ send_identify ClientConfig[:name]
733
+ end
734
+ end
735
+
736
+ EM.run do
737
+ puts "Connecting to: #{ClientConfig[:ip]}"
738
+ connection = EM.connect ClientConfig[:ip], ClientConfig[:port], JsClient
739
+
740
+ EM.open_keyboard(JsClient::KeyboardInput) do |keyboard|
741
+ keyboard.connection = connection
742
+ keyboard.setup_screen
743
+ connection.keyboard = keyboard
744
+ end
745
+ end