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