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