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,41 @@
1
+ module JsChat
2
+ class Error < RuntimeError
3
+ def initialize(code_key, message)
4
+ @message = message
5
+ @code = JsChat::Errors::Codes.invert[code_key]
6
+ end
7
+
8
+ # Note: This shouldn't really include 'display' directives
9
+ def to_json
10
+ { 'display' => 'error', 'error' => { 'message' => @message, 'code' => @code } }.to_json
11
+ end
12
+ end
13
+
14
+ module Errors
15
+ class InvalidName < JsChat::Error ; end
16
+ class MessageTooLong < JsChat::Error ; end
17
+ class InvalidCookie < JsChat::Error ; end
18
+
19
+ Codes = {
20
+ # 1xx: User errors
21
+ 100 => :name_taken,
22
+ 101 => :invalid_name,
23
+ 104 => :not_online,
24
+ 105 => :identity_required,
25
+ 106 => :already_identified,
26
+ 107 => :invalid_cookie,
27
+ # 2xx: Room errors
28
+ 200 => :already_joined,
29
+ 201 => :invalid_room,
30
+ 202 => :not_in_room,
31
+ 204 => :room_not_available,
32
+ # 3xx: Message errors
33
+ 300 => :to_required,
34
+ 301 => :message_too_long,
35
+ # 5xx: Other errors
36
+ 500 => :invalid_request,
37
+ 501 => :flooding,
38
+ 502 => :ping_out
39
+ }
40
+ end
41
+ end
@@ -0,0 +1,39 @@
1
+ module JsChat
2
+ module Errors
3
+ class Flooding < JsChat::Error ; end
4
+ class StillFlooding < Exception ; end
5
+ end
6
+
7
+ module FloodProtection
8
+ def seen!
9
+ @activity_log ||= []
10
+ @activity_log << Time.now.utc
11
+ @activity_log.shift if @activity_log.size > 50
12
+ remove_old_activity_logs
13
+ detect_flooding
14
+
15
+ if flooding?
16
+ if @still_flooding
17
+ raise JsChat::Errors::StillFlooding
18
+ else
19
+ @still_flooding = true
20
+ raise JsChat::Errors::Flooding.new(:flooding, 'Please wait a few seconds before responding')
21
+ end
22
+ elsif @still_flooding
23
+ @still_flooding = false
24
+ end
25
+ end
26
+
27
+ def detect_flooding
28
+ @flooding = @activity_log.size > 10 and @activity_log.sort.inject { |i, sum| sum.to_i - i.to_i } - @activity_log.first.to_i < 0.5
29
+ end
30
+
31
+ def flooding?
32
+ @flooding
33
+ end
34
+
35
+ def remove_old_activity_logs
36
+ @activity_log.delete_if { |l| l + 5 < Time.now.utc }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,7 @@
1
+ :load_path:
2
+ - public/javascripts
3
+ - public/javascripts/all
4
+ :source_files:
5
+ - public/javascripts/init.js
6
+ - public/javascripts/app/**/*.js
7
+ :output_file: public/javascripts/all.js
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'sinatra'
3
+
4
+ set :environment, :production
5
+
6
+ # You could log like this:
7
+ # log = File.new(File.join(File.dirname(__FILE__), 'sinatra.log'), 'a')
8
+ # $stdout.reopen(log)
9
+ # $stderr.reopen(log)
10
+
11
+ require 'jschat.rb'
12
+ run Sinatra::Application
@@ -0,0 +1,264 @@
1
+ require 'rubygems'
2
+ require 'sinatra'
3
+ require 'sha1'
4
+ require 'json'
5
+ require 'sprockets'
6
+
7
+ set :public, File.join(File.dirname(__FILE__), 'public')
8
+ set :views, File.join(File.dirname(__FILE__), 'views')
9
+
10
+ module JsChat
11
+ Config = {
12
+ :ip => '0.0.0.0',
13
+ :port => 6789
14
+ }
15
+
16
+ class ConnectionError < Exception ; end
17
+ end
18
+
19
+ # todo: can this be async and allow the server to have multiple threads?
20
+ class JsChat::Bridge
21
+ attr_reader :cookie, :identification_error, :last_error
22
+
23
+ def initialize(cookie = nil)
24
+ @cookie = cookie
25
+ end
26
+
27
+ def cookie_set?
28
+ !(@cookie.nil? or @cookie.empty?)
29
+ end
30
+
31
+ def connect
32
+ response = send_json({ :protocol => 'stateless' })
33
+ @cookie = response['cookie']
34
+ end
35
+
36
+ def identify(name, ip)
37
+ response = send_json({ :identify => name, :ip => ip })
38
+ if response['display'] == 'error'
39
+ @identification_error = response
40
+ false
41
+ else
42
+ true
43
+ end
44
+ end
45
+
46
+ def lastlog(room)
47
+ response = send_json({ :lastlog => room })
48
+ response['messages']
49
+ end
50
+
51
+ def recent_messages(room)
52
+ send_json({ 'since' => room })['messages']
53
+ end
54
+
55
+ def join(room)
56
+ send_json({ :join => room }, false)
57
+ end
58
+
59
+ def send_message(message, to)
60
+ send_json({ :send => message, :to => to }, false)
61
+ end
62
+
63
+ def active?
64
+ return false unless cookie_set?
65
+ response = ping
66
+ if response.nil? or response['display'] == 'error'
67
+ @last_error = response
68
+ false
69
+ else
70
+ true
71
+ end
72
+ end
73
+
74
+ def ping
75
+ send_json({ 'ping' => Time.now.utc })
76
+ end
77
+
78
+ def change(change_type, data)
79
+ send_json({ 'change' => change_type, change_type => data })
80
+ end
81
+
82
+ def names(room)
83
+ send_json({'names' => room})
84
+ end
85
+
86
+ def send_quit(name)
87
+ send_json({'quit' => name })
88
+ end
89
+
90
+ def send_json(h, get_results = true)
91
+ response = nil
92
+ h[:cookie] = @cookie if cookie_set?
93
+ c = TCPSocket.open(JsChat::Config[:ip], JsChat::Config[:port])
94
+ c.send(h.to_json + "\n", 0)
95
+ if get_results
96
+ response = c.gets
97
+ response = JSON.parse(response)
98
+ end
99
+ ensure
100
+ c.close
101
+ response
102
+ end
103
+ end
104
+
105
+ helpers do
106
+ include Rack::Utils
107
+ alias_method :h, :escape_html
108
+
109
+ def detected_layout
110
+ iphone_user_agent? ? :iphone : :layout
111
+ end
112
+
113
+ def iphone_user_agent?
114
+ request.env["HTTP_USER_AGENT"] && request.env["HTTP_USER_AGENT"][/(Mobile\/.+Safari)/]
115
+ end
116
+
117
+ def load_bridge
118
+ @bridge = JsChat::Bridge.new request.cookies['jschat-id']
119
+ end
120
+
121
+ def load_and_connect
122
+ @bridge = JsChat::Bridge.new request.cookies['jschat-id']
123
+ @bridge.connect
124
+ response.set_cookie 'jschat-id', @bridge.cookie
125
+ end
126
+
127
+ def save_last_room(room)
128
+ response.set_cookie 'last-room', room
129
+ end
130
+
131
+ def last_room
132
+ request.cookies['last-room']
133
+ end
134
+
135
+ def save_nickname(name)
136
+ response.set_cookie 'jschat-name', name
137
+ end
138
+
139
+ def messages_js(messages)
140
+ messages ||= []
141
+ messages.to_json
142
+ end
143
+
144
+ def remove_my_messages(messages)
145
+ return if messages.nil?
146
+ messages.delete_if { |message| message['message'] and message['message']['user'] == nickname }
147
+ end
148
+
149
+ def clear_cookies
150
+ response.set_cookie 'last-room', nil
151
+ response.set_cookie 'jschat-id', nil
152
+ end
153
+
154
+ def nickname
155
+ request.cookies['jschat-name']
156
+ end
157
+ end
158
+
159
+ # Identify
160
+ get '/' do
161
+ load_bridge
162
+
163
+ if @bridge.active? and last_room
164
+ redirect "/chat/#{last_room}"
165
+ else
166
+ clear_cookies
167
+ erb :index, :layout => detected_layout
168
+ end
169
+ end
170
+
171
+ post '/identify' do
172
+ load_and_connect
173
+ save_last_room params['room']
174
+ save_nickname params['name']
175
+ if @bridge.identify params['name'], request.ip
176
+ { 'action' => 'redirect', 'to' => "/chat/#{params['room']}" }.to_json
177
+ else
178
+ @bridge.identification_error.to_json
179
+ end
180
+ end
181
+
182
+ post '/change-name' do
183
+ load_bridge
184
+ [@bridge.change('user', { 'name' => params['name'] })].to_json
185
+ end
186
+
187
+ get '/messages' do
188
+ load_bridge
189
+ if @bridge.active?
190
+ save_last_room params['room']
191
+ messages_js remove_my_messages(@bridge.recent_messages(params['room']))
192
+ else
193
+ if @bridge.last_error and @bridge.last_error['error']['code'] == 107
194
+ error 500, [@bridge.last_error].to_json
195
+ else
196
+ [@bridge.last_error].to_json
197
+ end
198
+ end
199
+ end
200
+
201
+ get '/names' do
202
+ load_bridge
203
+ save_last_room params['room']
204
+ [@bridge.names(params['room'])].to_json
205
+ end
206
+
207
+ get '/lastlog' do
208
+ load_bridge
209
+ if @bridge.active?
210
+ save_last_room params['room']
211
+ messages_js @bridge.lastlog(params['room'])
212
+ end
213
+ end
214
+
215
+ post '/join' do
216
+ load_bridge
217
+ @bridge.join params['room']
218
+ save_last_room params['room']
219
+ "Request OK"
220
+ end
221
+
222
+ get '/chat/' do
223
+ load_bridge
224
+ if @bridge and @bridge.active?
225
+ erb :message_form, :layout => detected_layout
226
+ else
227
+ erb :index, :layout => detected_layout
228
+ end
229
+ end
230
+
231
+ post '/message' do
232
+ load_bridge
233
+ save_last_room params['room']
234
+ @bridge.send_message params['message'], params['to']
235
+ 'OK'
236
+ end
237
+
238
+ get '/user/name' do
239
+ load_bridge
240
+ nickname
241
+ end
242
+
243
+ get '/ping' do
244
+ load_bridge
245
+ @bridge.ping.to_json
246
+ end
247
+
248
+ get '/quit' do
249
+ load_bridge
250
+ @bridge.send_quit nickname
251
+ load_bridge
252
+ clear_cookies
253
+ redirect '/'
254
+ end
255
+
256
+ # This serves the JavaScript concat'd by Sprockets
257
+ # run script/sprocket.rb to cache this
258
+ get '/javascripts/all.js' do
259
+ root = File.join(File.dirname(File.expand_path(__FILE__)))
260
+ sprockets_config = YAML.load(IO.read(File.join(root, 'config', 'sprockets.yml')))
261
+ secretary = Sprockets::Secretary.new(sprockets_config.merge(:root => root))
262
+ content_type 'text/javascript'
263
+ secretary.concatenation.to_s
264
+ end
Binary file