goshrine_bot 0.1.0
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/History.txt +0 -0
- data/Manifest.txt +0 -0
- data/README.rdoc +56 -0
- data/Rakefile +22 -0
- data/TODO.txt +2 -0
- data/bin/goshrine_bot +10 -0
- data/goshrine_bot.gemspec +33 -0
- data/goshrine_bot.yml +5 -0
- data/lib/goshrine_bot/client.rb +180 -0
- data/lib/goshrine_bot/core_ext/hash.rb +53 -0
- data/lib/goshrine_bot/faye/channel.rb +143 -0
- data/lib/goshrine_bot/faye/client.rb +283 -0
- data/lib/goshrine_bot/faye/connection.rb +122 -0
- data/lib/goshrine_bot/faye/error.rb +44 -0
- data/lib/goshrine_bot/faye/grammar.rb +58 -0
- data/lib/goshrine_bot/faye/namespace.rb +20 -0
- data/lib/goshrine_bot/faye/rack_adapter.rb +115 -0
- data/lib/goshrine_bot/faye/server.rb +266 -0
- data/lib/goshrine_bot/faye/timeouts.rb +21 -0
- data/lib/goshrine_bot/faye/transport.rb +123 -0
- data/lib/goshrine_bot/faye.rb +36 -0
- data/lib/goshrine_bot/game.rb +252 -0
- data/lib/goshrine_bot/gtp_stdio_client.rb +130 -0
- data/lib/goshrine_bot/httpclient.rb +288 -0
- data/lib/goshrine_bot/runner.rb +91 -0
- data/lib/goshrine_bot.rb +25 -0
- metadata +104 -0
@@ -0,0 +1,283 @@
|
|
1
|
+
module Faye
|
2
|
+
class Client
|
3
|
+
include EventMachine::Deferrable
|
4
|
+
include Timeouts
|
5
|
+
|
6
|
+
UNCONNECTED = 1
|
7
|
+
CONNECTING = 2
|
8
|
+
CONNECTED = 3
|
9
|
+
DISCONNECTED = 4
|
10
|
+
|
11
|
+
HANDSHAKE = 'handshake'
|
12
|
+
RETRY = 'retry'
|
13
|
+
NONE = 'none'
|
14
|
+
|
15
|
+
CONNECTION_TIMEOUT = 60.0
|
16
|
+
|
17
|
+
attr_reader :endpoint, :namespace, :cookie
|
18
|
+
|
19
|
+
def initialize(endpoint = nil, options = {})
|
20
|
+
@endpoint = endpoint || RackAdapter::DEFAULT_ENDPOINT
|
21
|
+
@options = options
|
22
|
+
@timeout = @options[:timeout] || CONNECTION_TIMEOUT
|
23
|
+
@cookie = @options[:cookie]
|
24
|
+
|
25
|
+
@transport = Transport.get(self)
|
26
|
+
@state = UNCONNECTED
|
27
|
+
@namespace = Namespace.new
|
28
|
+
@outbox = []
|
29
|
+
@channels = Channel::Tree.new
|
30
|
+
@current_id = 0
|
31
|
+
|
32
|
+
@advice = {'reconnect' => RETRY, 'interval' => Connection::INTERVAL}
|
33
|
+
end
|
34
|
+
|
35
|
+
# Request
|
36
|
+
# MUST include: * channel
|
37
|
+
# * version
|
38
|
+
# * supportedConnectionTypes
|
39
|
+
# MAY include: * minimumVersion
|
40
|
+
# * ext
|
41
|
+
# * id
|
42
|
+
#
|
43
|
+
# Success Response Failed Response
|
44
|
+
# MUST include: * channel MUST include: * channel
|
45
|
+
# * version * successful
|
46
|
+
# * supportedConnectionTypes * error
|
47
|
+
# * clientId MAY include: * supportedConnectionTypes
|
48
|
+
# * successful * advice
|
49
|
+
# MAY include: * minimumVersion * version
|
50
|
+
# * advice * minimumVersion
|
51
|
+
# * ext * ext
|
52
|
+
# * id * id
|
53
|
+
# * authSuccessful
|
54
|
+
def handshake(&block)
|
55
|
+
return if @advice['reconnect'] == NONE
|
56
|
+
return if @state != UNCONNECTED
|
57
|
+
|
58
|
+
@state = CONNECTING
|
59
|
+
@transport.send({
|
60
|
+
'channel' => Channel::HANDSHAKE,
|
61
|
+
'version' => BAYEUX_VERSION,
|
62
|
+
'supportedConnectionTypes' => Transport.supported_connection_types
|
63
|
+
|
64
|
+
}) do |response|
|
65
|
+
unless response['successful']
|
66
|
+
EventMachine.add_timer(@advice['interval'] / 1000.0) { handshake(&block) }
|
67
|
+
return @state = UNCONNECTED
|
68
|
+
end
|
69
|
+
|
70
|
+
@state = CONNECTED
|
71
|
+
@client_id = response['clientId']
|
72
|
+
@transport = Transport.get(self, response['supportedConnectionTypes'])
|
73
|
+
|
74
|
+
block.call if block_given?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Request Response
|
79
|
+
# MUST include: * channel MUST include: * channel
|
80
|
+
# * clientId * successful
|
81
|
+
# * connectionType * clientId
|
82
|
+
# MAY include: * ext MAY include: * error
|
83
|
+
# * id * advice
|
84
|
+
# * ext
|
85
|
+
# * id
|
86
|
+
# * timestamp
|
87
|
+
def connect(&block)
|
88
|
+
|
89
|
+
return if @advice['reconnect'] == NONE or
|
90
|
+
@state == DISCONNECTED
|
91
|
+
|
92
|
+
if @advice['reconnect'] == HANDSHAKE or @state == UNCONNECTED
|
93
|
+
begin_reconnect_timeout
|
94
|
+
return handshake { connect(&block) }
|
95
|
+
end
|
96
|
+
|
97
|
+
return callback(&block) if @state == CONNECTING
|
98
|
+
return unless @state == CONNECTED
|
99
|
+
|
100
|
+
set_deferred_status(:succeeded)
|
101
|
+
set_deferred_status(:deferred)
|
102
|
+
block.call if block_given?
|
103
|
+
|
104
|
+
return unless @connection_id.nil?
|
105
|
+
|
106
|
+
@connection_id = @namespace.generate
|
107
|
+
@transport.send({
|
108
|
+
'channel' => Channel::CONNECT,
|
109
|
+
'clientId' => @client_id,
|
110
|
+
'connectionType' => @transport.connection_type,
|
111
|
+
'id' => @connection_id
|
112
|
+
|
113
|
+
}, &verify_client_id { |response|
|
114
|
+
@namespace.release(@connection_id) if @connection_id
|
115
|
+
@connection_id = nil
|
116
|
+
remove_timeout(:reconnect)
|
117
|
+
EventMachine.add_timer(@advice['interval'] / 1000.0) { connect }
|
118
|
+
})
|
119
|
+
|
120
|
+
begin_reconnect_timeout
|
121
|
+
end
|
122
|
+
|
123
|
+
# Request Response
|
124
|
+
# MUST include: * channel MUST include: * channel
|
125
|
+
# * clientId * successful
|
126
|
+
# MAY include: * ext * clientId
|
127
|
+
# * id MAY include: * error
|
128
|
+
# * ext
|
129
|
+
# * id
|
130
|
+
def disconnect
|
131
|
+
return unless @state == CONNECTED
|
132
|
+
@state = DISCONNECTED
|
133
|
+
|
134
|
+
@transport.send({
|
135
|
+
'channel' => Channel::DISCONNECT,
|
136
|
+
'clientId' => @client_id
|
137
|
+
})
|
138
|
+
|
139
|
+
@channels = Channel::Tree.new
|
140
|
+
remove_timeout(:reconnect)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Request Response
|
144
|
+
# MUST include: * channel MUST include: * channel
|
145
|
+
# * clientId * successful
|
146
|
+
# * subscription * clientId
|
147
|
+
# MAY include: * ext * subscription
|
148
|
+
# * id MAY include: * error
|
149
|
+
# * advice
|
150
|
+
# * ext
|
151
|
+
# * id
|
152
|
+
# * timestamp
|
153
|
+
def subscribe(channels, &block)
|
154
|
+
connect {
|
155
|
+
channels = [channels].flatten
|
156
|
+
validate_channels(channels)
|
157
|
+
|
158
|
+
@transport.send({
|
159
|
+
'channel' => Channel::SUBSCRIBE,
|
160
|
+
'clientId' => @client_id,
|
161
|
+
'subscription' => channels
|
162
|
+
|
163
|
+
}, &verify_client_id { |response|
|
164
|
+
if response['successful'] and block
|
165
|
+
channels = [response['subscription']].flatten
|
166
|
+
channels.each { |channel| @channels[channel] = block }
|
167
|
+
end
|
168
|
+
})
|
169
|
+
}
|
170
|
+
end
|
171
|
+
|
172
|
+
# Request Response
|
173
|
+
# MUST include: * channel MUST include: * channel
|
174
|
+
# * clientId * successful
|
175
|
+
# * subscription * clientId
|
176
|
+
# MAY include: * ext * subscription
|
177
|
+
# * id MAY include: * error
|
178
|
+
# * advice
|
179
|
+
# * ext
|
180
|
+
# * id
|
181
|
+
# * timestamp
|
182
|
+
def unsubscribe(channels, &block)
|
183
|
+
connect {
|
184
|
+
channels = [channels].flatten
|
185
|
+
validate_channels(channels)
|
186
|
+
|
187
|
+
@transport.send({
|
188
|
+
'channel' => Channel::UNSUBSCRIBE,
|
189
|
+
'clientId' => @client_id,
|
190
|
+
'subscription' => channels
|
191
|
+
|
192
|
+
}, &verify_client_id { |response|
|
193
|
+
if response['successful']
|
194
|
+
channels = [response['subscription']].flatten
|
195
|
+
channels.each { |channel| @channels[channel] = nil }
|
196
|
+
end
|
197
|
+
})
|
198
|
+
}
|
199
|
+
end
|
200
|
+
|
201
|
+
# Request Response
|
202
|
+
# MUST include: * channel MUST include: * channel
|
203
|
+
# * data * successful
|
204
|
+
# MAY include: * clientId MAY include: * id
|
205
|
+
# * id * error
|
206
|
+
# * ext * ext
|
207
|
+
def publish(channel, data, id=nil)
|
208
|
+
connect {
|
209
|
+
validate_channels([channel])
|
210
|
+
|
211
|
+
enqueue({
|
212
|
+
'id' => (id || next_id),
|
213
|
+
'channel' => channel,
|
214
|
+
'data' => data,
|
215
|
+
'clientId' => @client_id
|
216
|
+
})
|
217
|
+
|
218
|
+
add_timeout(:publish, Connection::MAX_DELAY) { flush! }
|
219
|
+
}
|
220
|
+
end
|
221
|
+
|
222
|
+
def handle_error(msg_id, error)
|
223
|
+
puts "Message #{msg_id} Error #{error}"
|
224
|
+
end
|
225
|
+
|
226
|
+
def handle_advice(advice)
|
227
|
+
@advice.update(advice)
|
228
|
+
@client_id = nil if @advice['reconnect'] == HANDSHAKE
|
229
|
+
end
|
230
|
+
|
231
|
+
def deliver_messages(messages)
|
232
|
+
messages.each do |message|
|
233
|
+
channels = @channels.glob(message['channel'])
|
234
|
+
channels.each { |callback| callback.call(message['data']) }
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
def begin_reconnect_timeout
|
241
|
+
add_timeout(:reconnect, @timeout) do
|
242
|
+
@namespace.release(@connection_id) if @connection_id
|
243
|
+
@connection_id = nil
|
244
|
+
@client_id = nil
|
245
|
+
@state = UNCONNECTED
|
246
|
+
subscribe(@channels.keys)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def enqueue(message)
|
251
|
+
@outbox << message
|
252
|
+
end
|
253
|
+
|
254
|
+
def flush!
|
255
|
+
@transport.send(@outbox)
|
256
|
+
@outbox = []
|
257
|
+
end
|
258
|
+
|
259
|
+
def next_id
|
260
|
+
@current_id += 1
|
261
|
+
end
|
262
|
+
|
263
|
+
def validate_channels(channels)
|
264
|
+
channels.each do |channel|
|
265
|
+
raise "'#{ channel }' is not a valid channel name" unless Channel.valid?(channel)
|
266
|
+
raise "Clients may not subscribe to channel '#{ channel }'" unless Channel.subscribable?(channel)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def verify_client_id(&block)
|
271
|
+
lambda do |response|
|
272
|
+
if response['clientId'] != @client_id
|
273
|
+
false
|
274
|
+
else
|
275
|
+
block.call(response)
|
276
|
+
true
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Faye
|
2
|
+
class Connection
|
3
|
+
include EventMachine::Deferrable
|
4
|
+
include Observable
|
5
|
+
include Timeouts
|
6
|
+
|
7
|
+
MAX_DELAY = 0.1
|
8
|
+
INTERVAL = 0.4
|
9
|
+
TIMEOUT = 60.0
|
10
|
+
|
11
|
+
attr_reader :id
|
12
|
+
|
13
|
+
def initialize(id, options = {})
|
14
|
+
@id = id
|
15
|
+
@options = options
|
16
|
+
@timeout = @options[:timeout] || TIMEOUT
|
17
|
+
@channels = Set.new
|
18
|
+
@inbox = Set.new
|
19
|
+
@connected = false
|
20
|
+
@connection_listener = options[:connection_listener]
|
21
|
+
|
22
|
+
# This is for clients that subscribe, but never re-connect to get messages
|
23
|
+
# (prevents a memory leak)
|
24
|
+
add_timeout(:deletion, 60 * INTERVAL) do
|
25
|
+
puts "Deleting orphaned connection: #{@id}"
|
26
|
+
delete
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def update(message, event)
|
32
|
+
return unless message == :message
|
33
|
+
@inbox.add(event)
|
34
|
+
begin_delivery_timeout!
|
35
|
+
end
|
36
|
+
|
37
|
+
def subscribe(channel)
|
38
|
+
if @channels.add?(channel)
|
39
|
+
channel.add_observer(self)
|
40
|
+
if @options[:subscription_listener]
|
41
|
+
@options[:subscription_listener].subscribed(self, channel)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def unsubscribe(channel)
|
47
|
+
return @channels.each(&method(:unsubscribe)) if channel == :all
|
48
|
+
return unless @channels.member?(channel)
|
49
|
+
@channels.delete(channel)
|
50
|
+
channel.delete_observer(self)
|
51
|
+
if @options[:subscription_listener]
|
52
|
+
@options[:subscription_listener].unsubscribed(self, channel)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def connect(transport_authentication, &block)
|
57
|
+
callback(&block)
|
58
|
+
return if @connected
|
59
|
+
|
60
|
+
@connected = true
|
61
|
+
|
62
|
+
if @connection_listener
|
63
|
+
@connection_listener.connected(id, transport_authentication)
|
64
|
+
end
|
65
|
+
|
66
|
+
remove_timeout(:deletion)
|
67
|
+
|
68
|
+
begin_delivery_timeout!
|
69
|
+
begin_connection_timeout!
|
70
|
+
end
|
71
|
+
|
72
|
+
def flush!
|
73
|
+
return unless @connected
|
74
|
+
release_connection!
|
75
|
+
|
76
|
+
events = @inbox.entries
|
77
|
+
@inbox = Set.new
|
78
|
+
|
79
|
+
set_deferred_status(:succeeded, events)
|
80
|
+
set_deferred_status(:deferred)
|
81
|
+
end
|
82
|
+
|
83
|
+
def disconnect!
|
84
|
+
unsubscribe(:all)
|
85
|
+
flush!
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def begin_delivery_timeout!
|
91
|
+
if @delivery_timeout.nil? && @connected && (!@inbox.empty? || @channels.empty?)
|
92
|
+
add_timeout(:delivery, MAX_DELAY) { flush! }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def begin_connection_timeout!
|
97
|
+
return unless @connection_timeout.nil? and @connected
|
98
|
+
add_timeout(:connection, @timeout) { flush! }
|
99
|
+
end
|
100
|
+
|
101
|
+
def delete
|
102
|
+
unsubscribe(:all)
|
103
|
+
if @connection_listener
|
104
|
+
@connection_listener.disconnected(id)
|
105
|
+
end
|
106
|
+
changed(true)
|
107
|
+
notify_observers(:stale_client, self)
|
108
|
+
end
|
109
|
+
|
110
|
+
def release_connection!
|
111
|
+
remove_timeout(:connection)
|
112
|
+
remove_timeout(:delivery)
|
113
|
+
@connected = false
|
114
|
+
|
115
|
+
add_timeout(:deletion, 10 * INTERVAL) do
|
116
|
+
delete
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Faye
|
2
|
+
class Error
|
3
|
+
|
4
|
+
def self.parse(string)
|
5
|
+
return nil unless Grammar::ERROR =~ string
|
6
|
+
parts = string.split(':')
|
7
|
+
args = parts[1].split(',')
|
8
|
+
new(parts[0].to_i, args, parts[2])
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.method_missing(type, *args)
|
12
|
+
code = const_get(type.to_s.upcase)
|
13
|
+
new(code[0], args, code[1]).to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :code, :args, :message
|
17
|
+
|
18
|
+
def initialize(code, args, message)
|
19
|
+
@code = code
|
20
|
+
@args = args
|
21
|
+
@message = message
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
"#{ @code }:#{ @args * ',' }:#{ @message }"
|
26
|
+
end
|
27
|
+
|
28
|
+
# http://code.google.com/p/cometd/wiki/BayeuxCodes
|
29
|
+
VERSION_MISMATCH = [300, 'Version mismatch']
|
30
|
+
CONNTYPE_MISMATCH = [301, 'Connection types not supported']
|
31
|
+
EXT_MISMATCH = [302, 'Extension mismatch']
|
32
|
+
BAD_REQUEST = [400, 'Bad request']
|
33
|
+
CLIENT_UNKNOWN = [401, 'Unknown client']
|
34
|
+
PARAMETER_MISSING = [402, 'Missing required parameter']
|
35
|
+
CHANNEL_FORBIDDEN = [403, 'Forbidden channel']
|
36
|
+
CHANNEL_UNKNOWN = [404, 'Unknown channel']
|
37
|
+
CHANNEL_INVALID = [405, 'Invalid channel']
|
38
|
+
EXT_UNKNOWN = [406, 'Unknown extension']
|
39
|
+
PUBLISH_FAILED = [407, 'Failed to publish']
|
40
|
+
SERVER_ERROR = [500, 'Internal server error']
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Faye
|
2
|
+
module Grammar
|
3
|
+
|
4
|
+
def self.rule(&block)
|
5
|
+
source = instance_eval(&block)
|
6
|
+
%r{^#{string(source)}$}
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.choice(*list)
|
10
|
+
'(' + list.map(&method(:string)) * '|' + ')'
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.repeat(*pattern)
|
14
|
+
'(' + string(pattern) + ')*'
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.oneormore(*pattern)
|
18
|
+
'(' + string(pattern) + ')+'
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.string(item)
|
22
|
+
return item.map(&method(:string)) * '' if Array === item
|
23
|
+
String === item ? item : item.source.gsub(/^\^/, '').gsub(/\$$/, '')
|
24
|
+
end
|
25
|
+
|
26
|
+
LOWALPHA = rule {[ '[a-z]' ]}
|
27
|
+
UPALPHA = rule {[ '[A-Z]' ]}
|
28
|
+
ALPHA = rule {[ choice(LOWALPHA, UPALPHA) ]}
|
29
|
+
DIGIT = rule {[ '[0-9]' ]}
|
30
|
+
ALPHANUM = rule {[ choice(ALPHA, DIGIT) ]}
|
31
|
+
MARK = rule {[ choice(*%w[\\- \\_ \\! \\~ \\( \\) \\$ \\@]) ]}
|
32
|
+
STRING = rule {[ repeat(choice(ALPHANUM, MARK, ' ', '\\/', '\\*', '\\.')) ]}
|
33
|
+
TOKEN = rule {[ oneormore(choice(ALPHANUM, MARK)) ]}
|
34
|
+
INTEGER = rule {[ oneormore(DIGIT) ]}
|
35
|
+
|
36
|
+
CHANNEL_SEGMENT = rule {[ TOKEN ]}
|
37
|
+
CHANNEL_SEGMENTS = rule {[ CHANNEL_SEGMENT, repeat('\\/', CHANNEL_SEGMENT) ]}
|
38
|
+
CHANNEL_NAME = rule {[ '\\/', CHANNEL_SEGMENTS ]}
|
39
|
+
|
40
|
+
WILD_CARD = rule {[ '\\*{1,2}' ]}
|
41
|
+
CHANNEL_PATTERN = rule {[ repeat('\\/', CHANNEL_SEGMENT), '\\/', WILD_CARD ]}
|
42
|
+
|
43
|
+
VERSION_ELEMENT = rule {[ ALPHANUM, repeat(choice(ALPHANUM, '\\-', '\\_')) ]}
|
44
|
+
VERSION = rule {[ INTEGER, repeat('\\.', VERSION_ELEMENT) ]}
|
45
|
+
|
46
|
+
CLIENT_ID = rule {[ oneormore(ALPHANUM) ]}
|
47
|
+
|
48
|
+
ID = rule {[ oneormore(ALPHANUM) ]}
|
49
|
+
|
50
|
+
ERROR_MESSAGE = rule {[ STRING ]}
|
51
|
+
ERROR_ARGS = rule {[ STRING, repeat(',', STRING) ]}
|
52
|
+
ERROR_CODE = rule {[ DIGIT, DIGIT, DIGIT ]}
|
53
|
+
ERROR = rule {[ choice(string([ERROR_CODE, ':', ERROR_ARGS, ':', ERROR_MESSAGE]),
|
54
|
+
string([ERROR_CODE, ':', ':', ERROR_MESSAGE])) ]}
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Faye
|
2
|
+
class Namespace
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@used = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def generate
|
9
|
+
name = Faye.random
|
10
|
+
name = Faye.random while @used.has_key?(name)
|
11
|
+
@used[name] = name
|
12
|
+
end
|
13
|
+
|
14
|
+
def release(name)
|
15
|
+
@used.delete(name)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rack'
|
3
|
+
require 'thin'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Faye
|
7
|
+
class AlwaysEmptyHash
|
8
|
+
def initialize
|
9
|
+
@h = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing(m, *args)
|
13
|
+
@h.send(m, *args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def []=(key,value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class RackAdapter
|
21
|
+
|
22
|
+
# Only supported under Thin
|
23
|
+
ASYNC_RESPONSE = [-1, AlwaysEmptyHash.new, []].freeze
|
24
|
+
|
25
|
+
DEFAULT_ENDPOINT = '/bayeux'
|
26
|
+
SCRIPT_PATH = File.join(ROOT, 'faye-client-min.js')
|
27
|
+
|
28
|
+
TYPE_JSON = {'Content-Type' => 'application/json'}
|
29
|
+
TYPE_SCRIPT = {'Content-Type' => 'text/javascript'}
|
30
|
+
TYPE_TEXT = {'Content-Type' => 'text/plain'}
|
31
|
+
|
32
|
+
def initialize(app = nil, options = nil)
|
33
|
+
@app = app if app.respond_to?(:call)
|
34
|
+
@options = [app, options].grep(Hash).first || {}
|
35
|
+
|
36
|
+
@endpoint = @options[:mount] || DEFAULT_ENDPOINT
|
37
|
+
@endpoint_re = Regexp.new('^' + @endpoint + '(/[^/]+)*(\\.js)?$')
|
38
|
+
@server = Server.new(@options)
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_client
|
42
|
+
@client ||= Client.new(@server)
|
43
|
+
end
|
44
|
+
|
45
|
+
def run(port)
|
46
|
+
handler = Rack::Handler.get('thin')
|
47
|
+
handler.run(self, :Port => port)
|
48
|
+
end
|
49
|
+
|
50
|
+
def call(env)
|
51
|
+
ensure_reactor_running!
|
52
|
+
request = Rack::Request.new(env)
|
53
|
+
|
54
|
+
unless request.path_info =~ @endpoint_re
|
55
|
+
env['faye.client'] = get_client
|
56
|
+
return @app ? @app.call(env) :
|
57
|
+
[404, TYPE_TEXT, ["Sure you're not looking for #{@endpoint} ?"]]
|
58
|
+
end
|
59
|
+
|
60
|
+
if request.path_info =~ /\.js$/
|
61
|
+
return [200, TYPE_SCRIPT, File.new(SCRIPT_PATH)]
|
62
|
+
end
|
63
|
+
|
64
|
+
begin
|
65
|
+
json_msg = request.post? ? request.body.read : request.params['message']
|
66
|
+
message = JSON.parse(json_msg)
|
67
|
+
jsonp = request.params['jsonp'] || JSONP_CALLBACK
|
68
|
+
|
69
|
+
@server.flush_connection(message) if request.get?
|
70
|
+
|
71
|
+
on_response(env, message) do |replies|
|
72
|
+
response = JSON.unparse(replies)
|
73
|
+
response = "#{ jsonp }(#{ response });" if request.get?
|
74
|
+
response
|
75
|
+
end
|
76
|
+
rescue Exception => e
|
77
|
+
puts "Caught #{e.message}: " + e.backtrace.join("\n")
|
78
|
+
[400, TYPE_TEXT, 'Bad request']
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def on_response(env, message, &block)
|
85
|
+
request = Rack::Request.new(env)
|
86
|
+
type = request.get? ? TYPE_SCRIPT : TYPE_JSON
|
87
|
+
callback = env['async.callback']
|
88
|
+
transport_authentication = env['faye.transport_authentication']
|
89
|
+
|
90
|
+
if callback
|
91
|
+
body = DeferredBody.new
|
92
|
+
callback.call [200, type, body]
|
93
|
+
@server.process(message, transport_authentication, false) { |r| body.succeed block.call(r) }
|
94
|
+
return ASYNC_RESPONSE
|
95
|
+
end
|
96
|
+
|
97
|
+
response = nil
|
98
|
+
@server.process(message, transport_authentication, false) { |r| response = block.call(r) }
|
99
|
+
sleep(0.1) while response.nil?
|
100
|
+
[200, type, [response]]
|
101
|
+
end
|
102
|
+
|
103
|
+
def ensure_reactor_running!
|
104
|
+
Thread.new { EM.run } unless EM.reactor_running?
|
105
|
+
while not EM.reactor_running?; end
|
106
|
+
end
|
107
|
+
|
108
|
+
class DeferredBody
|
109
|
+
include EventMachine::Deferrable
|
110
|
+
alias :each :callback
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|