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.
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