turntabler 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/.rspec +2 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +383 -0
- data/Rakefile +11 -0
- data/examples/Gemfile +3 -0
- data/examples/Gemfile.lock +29 -0
- data/examples/autobop.rb +13 -0
- data/examples/autofan.rb +13 -0
- data/examples/blacklist.rb +16 -0
- data/examples/bop.rb +15 -0
- data/examples/bopcount.rb +20 -0
- data/examples/chat_bot.rb +16 -0
- data/examples/modlist.rb +19 -0
- data/examples/switch.rb +40 -0
- data/examples/time_afk_list.rb +46 -0
- data/lib/turntabler/assertions.rb +36 -0
- data/lib/turntabler/authorized_user.rb +217 -0
- data/lib/turntabler/avatar.rb +34 -0
- data/lib/turntabler/boot.rb +22 -0
- data/lib/turntabler/client.rb +457 -0
- data/lib/turntabler/connection.rb +176 -0
- data/lib/turntabler/digest_helpers.rb +13 -0
- data/lib/turntabler/error.rb +5 -0
- data/lib/turntabler/event.rb +239 -0
- data/lib/turntabler/handler.rb +67 -0
- data/lib/turntabler/loggable.rb +11 -0
- data/lib/turntabler/message.rb +24 -0
- data/lib/turntabler/playlist.rb +50 -0
- data/lib/turntabler/preferences.rb +70 -0
- data/lib/turntabler/resource.rb +194 -0
- data/lib/turntabler/room.rb +377 -0
- data/lib/turntabler/room_directory.rb +133 -0
- data/lib/turntabler/snag.rb +16 -0
- data/lib/turntabler/song.rb +247 -0
- data/lib/turntabler/sticker.rb +48 -0
- data/lib/turntabler/sticker_placement.rb +25 -0
- data/lib/turntabler/user.rb +274 -0
- data/lib/turntabler/version.rb +9 -0
- data/lib/turntabler/vote.rb +19 -0
- data/lib/turntabler.rb +102 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/turntabler_spec.rb +4 -0
- data/turntable.gemspec +24 -0
- metadata +173 -0
@@ -0,0 +1,457 @@
|
|
1
|
+
require 'fiber'
|
2
|
+
|
3
|
+
require 'turntabler/authorized_user'
|
4
|
+
require 'turntabler/avatar'
|
5
|
+
require 'turntabler/connection'
|
6
|
+
require 'turntabler/error'
|
7
|
+
require 'turntabler/event'
|
8
|
+
require 'turntabler/handler'
|
9
|
+
require 'turntabler/loggable'
|
10
|
+
require 'turntabler/room_directory'
|
11
|
+
require 'turntabler/song'
|
12
|
+
require 'turntabler/sticker'
|
13
|
+
require 'turntabler/user'
|
14
|
+
|
15
|
+
module Turntabler
|
16
|
+
# Provides access to the Turntable API
|
17
|
+
class Client
|
18
|
+
include Assertions
|
19
|
+
include DigestHelpers
|
20
|
+
include Loggable
|
21
|
+
|
22
|
+
# The unique id representing this client
|
23
|
+
# @return [String]
|
24
|
+
attr_reader :id
|
25
|
+
|
26
|
+
# Sets the current room the user is in
|
27
|
+
# @param [Turntabler::Room] value The new room
|
28
|
+
# @api private
|
29
|
+
attr_writer :room
|
30
|
+
|
31
|
+
# The directory for looking up / creating rooms
|
32
|
+
# @return [Turntabler::RoomDirectory]
|
33
|
+
attr_reader :rooms
|
34
|
+
|
35
|
+
# The response timeout configured for the connection
|
36
|
+
# @return [Fixnum]
|
37
|
+
attr_reader :timeout
|
38
|
+
|
39
|
+
# Creates a new client for communicating with Turntable.fm with the given
|
40
|
+
# user id / auth token.
|
41
|
+
#
|
42
|
+
# @param [String] user_id The user to authenticate with
|
43
|
+
# @param [String] auth The authentication token for the user
|
44
|
+
# @param [Hash] options The configuration options for the client
|
45
|
+
# @option options [String] :id The unique identifier representing this client
|
46
|
+
# @option options [String] :room The id of the room to initially enter
|
47
|
+
# @option options [Fixnum] :timeout (10) The amount of seconds to allow to elapse for requests before timing out
|
48
|
+
# @option options [Boolean] :reconnect (false) Whether to allow the client to automatically reconnect when disconnected either by Turntable or by the network
|
49
|
+
# @option options [Fixnum] :reconnect_wait (5) The amount of seconds to wait before reconnecting
|
50
|
+
# @raise [Turntabler::Error] if an invalid option is specified
|
51
|
+
# @yield Runs the given block within the context if the client (for DSL-type usage)
|
52
|
+
def initialize(user_id, auth, options = {}, &block)
|
53
|
+
options = {
|
54
|
+
:id => "#{Time.now.to_i}-#{rand}",
|
55
|
+
:timeout => 10,
|
56
|
+
:reconnect => false,
|
57
|
+
:reconnect_wait => 5
|
58
|
+
}.merge(options)
|
59
|
+
assert_valid_keys(options, :id, :room, :url, :timeout, :reconnect, :reconnect_wait)
|
60
|
+
|
61
|
+
@id = options[:id]
|
62
|
+
@user = AuthorizedUser.new(self, :_id => user_id, :auth => auth)
|
63
|
+
@rooms = RoomDirectory.new(self)
|
64
|
+
@event_handlers = {}
|
65
|
+
@timeout = options[:timeout]
|
66
|
+
@reconnect = options[:reconnect]
|
67
|
+
@reconnect_wait = options[:reconnect_wait]
|
68
|
+
|
69
|
+
# Setup default event handlers
|
70
|
+
on(:heartbeat) { on_heartbeat }
|
71
|
+
on(:session_missing) { on_session_missing }
|
72
|
+
|
73
|
+
# Connect to an initial room / server
|
74
|
+
if room_name = options[:room]
|
75
|
+
room(room_name).enter
|
76
|
+
elsif url = options[:url]
|
77
|
+
connect(url)
|
78
|
+
else
|
79
|
+
connect
|
80
|
+
end
|
81
|
+
|
82
|
+
instance_eval(&block) if block_given?
|
83
|
+
end
|
84
|
+
|
85
|
+
# Initiates a connection with the given url. Once a connection is started,
|
86
|
+
# this will also attempt to authenticate the user.
|
87
|
+
#
|
88
|
+
# @api private
|
89
|
+
# @note This wil only open a new connection if the client isn't already connected to the given url
|
90
|
+
# @param [String] url The url to open a connection to
|
91
|
+
# @return [true]
|
92
|
+
# @raise [Turntabler::Error] if the connection cannot be opened
|
93
|
+
def connect(url = room(digest(rand)).url)
|
94
|
+
if !@connection || !@connection.connected? || @connection.url != url
|
95
|
+
# Close any existing connection
|
96
|
+
close
|
97
|
+
|
98
|
+
# Create a new connection to the given url
|
99
|
+
@connection = Connection.new(url, :timeout => timeout, :params => {:clientid => id, :userid => user.id, :userauth => user.auth})
|
100
|
+
@connection.handler = lambda {|data| on_message(data)}
|
101
|
+
@connection.start
|
102
|
+
|
103
|
+
# Wait until the connection is authenticated
|
104
|
+
wait do |fiber|
|
105
|
+
on(:session_missing, :once => true) { fiber.resume }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
true
|
110
|
+
end
|
111
|
+
|
112
|
+
# Closes the current connection to Turntable if one was previously opened.
|
113
|
+
#
|
114
|
+
# @return [true]
|
115
|
+
def close(allow_reconnect = false)
|
116
|
+
if @connection
|
117
|
+
@update_timer.cancel if @update_timer
|
118
|
+
@update_timer = nil
|
119
|
+
@connection.close
|
120
|
+
|
121
|
+
wait do |fiber|
|
122
|
+
on(:session_ended, :once => true) { fiber.resume }
|
123
|
+
end
|
124
|
+
|
125
|
+
on_session_ended(allow_reconnect)
|
126
|
+
end
|
127
|
+
|
128
|
+
true
|
129
|
+
end
|
130
|
+
|
131
|
+
# Gets the chat server url currently connected to
|
132
|
+
#
|
133
|
+
# @api private
|
134
|
+
# @return [String]
|
135
|
+
def url
|
136
|
+
@connection && @connection.url
|
137
|
+
end
|
138
|
+
|
139
|
+
# Runs the given API command.
|
140
|
+
#
|
141
|
+
# @api private
|
142
|
+
# @param [String] command The name of the command to execute
|
143
|
+
# @param [Hash] params The parameters to pass into the command
|
144
|
+
# @return [Hash] The data returned from the Turntable service
|
145
|
+
# @raise [Turntabler::Error] if the connection is not open or the command fails to execute
|
146
|
+
def api(command, params = {})
|
147
|
+
raise(Turntabler::Error, 'Connection is not open') unless @connection && @connection.connected?
|
148
|
+
|
149
|
+
message_id = @connection.publish(params.merge(:api => command))
|
150
|
+
|
151
|
+
# Wait until we get a response for the given message
|
152
|
+
data = wait do |fiber|
|
153
|
+
on(:response_received, :once => true, :if => {'msgid' => message_id}) {|data| fiber.resume(data)}
|
154
|
+
end
|
155
|
+
|
156
|
+
if data['success']
|
157
|
+
data
|
158
|
+
else
|
159
|
+
error = data['error'] || data['err']
|
160
|
+
raise Error, "Command \"#{command}\" failed with message: \"#{error}\""
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Registers a handler to invoke when an event occurs in Turntable.
|
165
|
+
#
|
166
|
+
# @param [Symbol] event The event to register a handler for
|
167
|
+
# @param [Hash] options The configuration options for the handler
|
168
|
+
# @option options [Hash] :if Specifies a set of key-value pairs that must be matched in the event data in order to run the handler
|
169
|
+
# @option options [Boolean] :once (false) Whether to only run the handler once
|
170
|
+
# @return [true]
|
171
|
+
#
|
172
|
+
# == Room Events
|
173
|
+
#
|
174
|
+
# * +:room_updated+ - Information about the room was updated
|
175
|
+
#
|
176
|
+
# @example
|
177
|
+
# client.on :room_updated do |room| # Room
|
178
|
+
# puts room.description
|
179
|
+
# # ...
|
180
|
+
# end
|
181
|
+
#
|
182
|
+
# == User Events
|
183
|
+
#
|
184
|
+
# * +:user_entered+ - A user entered the room
|
185
|
+
# * +:user_left+ - A user left the room
|
186
|
+
# * +:user_booted+ - A user has been booted from the room
|
187
|
+
# * +:user_updated+ - A user's name / profile was updated
|
188
|
+
# * +:user_spoke+ - A user spoke in the chat room
|
189
|
+
#
|
190
|
+
# @example
|
191
|
+
# client.on :user_entered do |user| # User
|
192
|
+
# puts user.id
|
193
|
+
# # ...
|
194
|
+
# end
|
195
|
+
#
|
196
|
+
# client.on :user_left do |user| # User
|
197
|
+
# puts user.id
|
198
|
+
# # ...
|
199
|
+
# end
|
200
|
+
#
|
201
|
+
# client.on :user_booted do |boot| # Boot
|
202
|
+
# puts boot.user.id
|
203
|
+
# puts boot.reason
|
204
|
+
# # ...
|
205
|
+
# end
|
206
|
+
#
|
207
|
+
# client.on :user_updated do |user| # User
|
208
|
+
# puts user.laptop_name
|
209
|
+
# # ...
|
210
|
+
# end
|
211
|
+
#
|
212
|
+
# client.on :user_spoke do |message| # Message
|
213
|
+
# puts message.content
|
214
|
+
# # ...
|
215
|
+
# end
|
216
|
+
#
|
217
|
+
# == DJ Events
|
218
|
+
#
|
219
|
+
# * +:dj_added+ - A new DJ was added to the booth
|
220
|
+
# * +:dj_removed+ - A DJ was removed from the booth
|
221
|
+
#
|
222
|
+
# @example
|
223
|
+
# client.on :dj_added do |user| # User
|
224
|
+
# puts user.id
|
225
|
+
# # ...
|
226
|
+
# end
|
227
|
+
#
|
228
|
+
# client.on :dj_removed do |user| # User
|
229
|
+
# puts user.id
|
230
|
+
# # ...
|
231
|
+
# end
|
232
|
+
#
|
233
|
+
# == Moderator Events
|
234
|
+
#
|
235
|
+
# * +:moderator_added+ - A new moderator was added to the room
|
236
|
+
# * +:moderator_removed+ - A moderator was removed from the room
|
237
|
+
#
|
238
|
+
# @example
|
239
|
+
# client.on :moderator_added do |user| # User
|
240
|
+
# puts user.id
|
241
|
+
# # ...
|
242
|
+
# end
|
243
|
+
#
|
244
|
+
# client.on :moderator_removed do |user| # User
|
245
|
+
# puts user.id
|
246
|
+
# # ...
|
247
|
+
# end
|
248
|
+
#
|
249
|
+
# == Song Events
|
250
|
+
#
|
251
|
+
# * +:song_unavailable+ - Indicates that there are no more songs to play in the room
|
252
|
+
# * +:song_started+ - A new song has started playing
|
253
|
+
# * +:song_ended+ - The current song has ended. This is typically followed by a +:song_started+ or +:song_unavailable+ event.
|
254
|
+
# * +:song_voted+ - One or more votes were cast for the song
|
255
|
+
# * +:song_snagged+ - A user in the room has queued the current song onto their playlist
|
256
|
+
# * +:song_blocked+ - A song was skipped due to a copyright claim
|
257
|
+
# * +:song_limited+ - A song was skipped due to a limit on # of plays per hour
|
258
|
+
#
|
259
|
+
# @example
|
260
|
+
# client.on :song_unavailable do
|
261
|
+
# # ...
|
262
|
+
# end
|
263
|
+
#
|
264
|
+
# client.on :song_started do |song| # Song
|
265
|
+
# puts song.title
|
266
|
+
# # ...
|
267
|
+
# end
|
268
|
+
#
|
269
|
+
# client.on :song_ended do |song| # Song
|
270
|
+
# puts song.title
|
271
|
+
# # ...
|
272
|
+
# end
|
273
|
+
#
|
274
|
+
# client.on :song_voted do |song| # Song
|
275
|
+
# puts song.up_votes_count
|
276
|
+
# puts song.down_votes_count
|
277
|
+
# puts song.votes
|
278
|
+
# # ...
|
279
|
+
# end
|
280
|
+
#
|
281
|
+
# client.on :song_snagged do |snag| # Snag
|
282
|
+
# puts snag.user.id
|
283
|
+
# puts snag.song.id
|
284
|
+
# # ...
|
285
|
+
# end
|
286
|
+
#
|
287
|
+
# client.on :song_blocked do |song| # Song
|
288
|
+
# puts song.id
|
289
|
+
# # ...
|
290
|
+
# end
|
291
|
+
#
|
292
|
+
# client.on :song_limited do |song| # Song
|
293
|
+
# puts song.id
|
294
|
+
# # ...
|
295
|
+
# end
|
296
|
+
#
|
297
|
+
# == Messaging Events
|
298
|
+
#
|
299
|
+
# * +:message_received+ - A private message was received from another user in the room
|
300
|
+
#
|
301
|
+
# @example
|
302
|
+
# client.on :message_received do |message| # Message
|
303
|
+
# puts message.content
|
304
|
+
# # ...
|
305
|
+
# end
|
306
|
+
def on(event, options = {}, &block)
|
307
|
+
event = event.to_sym
|
308
|
+
@event_handlers[event] ||= []
|
309
|
+
@event_handlers[event] << Handler.new(event, options, &block)
|
310
|
+
true
|
311
|
+
end
|
312
|
+
|
313
|
+
# Gets the current room the authorized user is in or builds a new room
|
314
|
+
# bound to the given room id.
|
315
|
+
#
|
316
|
+
# @param [String] room_id The id of the room to build
|
317
|
+
# @return [Turntabler::Room]
|
318
|
+
# @example
|
319
|
+
# client.room # => #<Turntabler::Room id="ab28f..." ...>
|
320
|
+
# client.room('50985...') # => #<Turntabler::Room id="50985..." ...>
|
321
|
+
def room(room_id = nil)
|
322
|
+
room_id ? Room.new(self, :_id => room_id) : @room
|
323
|
+
end
|
324
|
+
|
325
|
+
# Gets the current authorized user or builds a new user bound to the given
|
326
|
+
# user id.
|
327
|
+
#
|
328
|
+
# @param [String] user_id The id of the user to build
|
329
|
+
# @return [Turntabler::User]
|
330
|
+
# @example
|
331
|
+
# client.user # => #<Turntabler::User id="fb129..." ...>
|
332
|
+
# client.user('a34bd...') # => #<Turntabler::User id="a34bd..." ...>
|
333
|
+
def user(user_id = nil)
|
334
|
+
user_id ? User.new(self, :_id => user_id) : @user
|
335
|
+
end
|
336
|
+
|
337
|
+
# Get all avatars availble on Turntable.
|
338
|
+
#
|
339
|
+
# @return [Array<Turntabler::Avatar>]
|
340
|
+
# @raise [Turntabler::Error] if the command fails
|
341
|
+
# @example
|
342
|
+
# client.avatars # => [#<Turntabler::Avatar ...>, ...]
|
343
|
+
def avatars
|
344
|
+
data = api('user.available_avatars')
|
345
|
+
avatars = []
|
346
|
+
data['avatars'].each do |avatar_group|
|
347
|
+
avatar_group['avatarids'].each do |avatar_id|
|
348
|
+
avatars << Avatar.new(self, :_id => avatar_id, :min => avatar_group['min'], :acl => avatar_group['acl'])
|
349
|
+
end
|
350
|
+
end
|
351
|
+
avatars
|
352
|
+
end
|
353
|
+
|
354
|
+
# Get all stickers available on Turntable.
|
355
|
+
#
|
356
|
+
# @return [Array<Turntabler::Sticker>]
|
357
|
+
# @raise [Turntabler::Error] if the command fails
|
358
|
+
# @example
|
359
|
+
# client.stickers # => [#<Turntabler::Sticker id="...">, ...]
|
360
|
+
def stickers
|
361
|
+
data = api('sticker.get')
|
362
|
+
data['stickers'].map {|attrs| Sticker.new(self, attrs)}
|
363
|
+
end
|
364
|
+
|
365
|
+
# Builds a new song bound to the given song id.
|
366
|
+
#
|
367
|
+
# @param [String] song_id The id of the song to build
|
368
|
+
# @return [Turntabler::Song]
|
369
|
+
# @example
|
370
|
+
# client.song('a34bd...') # => #<Turntabler::Song id="a34bd..." ...>
|
371
|
+
def song(song_id)
|
372
|
+
Song.new(self, :_id => song_id)
|
373
|
+
end
|
374
|
+
|
375
|
+
# Finds songs that match the given query.
|
376
|
+
#
|
377
|
+
# @param [String] query The query string to search for
|
378
|
+
# @param [Hash] options The configuration options for the search
|
379
|
+
# @option options [Fixnum] :page The page number to get from the results
|
380
|
+
# @return [Array<Turntabler::Song>]
|
381
|
+
# @raise [ArgumentError] if an invalid option is specified
|
382
|
+
# @raise [Turntabler::Error] if the command fails
|
383
|
+
# @example
|
384
|
+
# client.search_song('Like a Rolling Stone') # => [#<Turntabler::Sticker ...>, ...]
|
385
|
+
def search_song(query, options = {})
|
386
|
+
assert_valid_keys(options, :page)
|
387
|
+
options = {:page => 1}.merge(options)
|
388
|
+
|
389
|
+
api('file.search', :query => query, :page => options[:page])
|
390
|
+
|
391
|
+
# Wait for the async callback
|
392
|
+
songs = wait do |fiber|
|
393
|
+
on(:search_completed, :once => true, :if => {'query' => query}) {|songs| fiber.resume(songs)}
|
394
|
+
on(:search_failed, :once => true, :if => {'query' => query}) { fiber.resume }
|
395
|
+
end
|
396
|
+
|
397
|
+
songs || raise(Error, 'Search failed to complete')
|
398
|
+
end
|
399
|
+
|
400
|
+
private
|
401
|
+
# Callback when a message has been received from Turntable. This will run
|
402
|
+
# any handlers registered for the event associated with the message.
|
403
|
+
def on_message(data)
|
404
|
+
if Event.command?(data['command'])
|
405
|
+
event = Event.new(self, data)
|
406
|
+
handlers = @event_handlers[event.name] || []
|
407
|
+
handlers.each do |handler|
|
408
|
+
success = handler.run(event)
|
409
|
+
handlers.delete(handler) if success && handler.once
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
# Callback when a heartbeat message has been received from Turntable determining
|
415
|
+
# whether this client is still alive.
|
416
|
+
def on_heartbeat
|
417
|
+
user.update(:status => user.status)
|
418
|
+
end
|
419
|
+
|
420
|
+
# Callback when session authentication is missing from the connection. This
|
421
|
+
# will automatically authenticate with configured user as well as set up a
|
422
|
+
# heartbeat.
|
423
|
+
def on_session_missing
|
424
|
+
user.authenticate
|
425
|
+
user.fan_of
|
426
|
+
user.update(:status => user.status)
|
427
|
+
|
428
|
+
# Periodically update the user's status to remain available
|
429
|
+
@update_timer.cancel if @update_timer
|
430
|
+
@update_timer = EM::Synchrony.add_periodic_timer(10) { user.update(:status => user.status) }
|
431
|
+
end
|
432
|
+
|
433
|
+
# Callback when the session has ended. This will automatically reconnect if
|
434
|
+
# allowed to do so.
|
435
|
+
def on_session_ended(allow_reconnect)
|
436
|
+
url = @connection.url
|
437
|
+
room = @room
|
438
|
+
@connection = nil
|
439
|
+
@room = nil
|
440
|
+
|
441
|
+
# Automatically reconnect to the room / server if allowed
|
442
|
+
if @reconnect && allow_reconnect
|
443
|
+
EM::Synchrony.add_timer(@reconnect_wait) do
|
444
|
+
room ? room.enter : connect(url)
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# Pauses the current fiber until it is resumed with response data. This
|
450
|
+
# can only get resumed explicitly by the provided block.
|
451
|
+
def wait
|
452
|
+
fiber = Fiber.current
|
453
|
+
yield(fiber)
|
454
|
+
Fiber.yield
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'faye/websocket'
|
2
|
+
require 'em-http'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require 'turntabler/assertions'
|
6
|
+
require 'turntabler/loggable'
|
7
|
+
|
8
|
+
module Turntabler
|
9
|
+
# Represents the interface for sending and receiving data in Turntable
|
10
|
+
# @api private
|
11
|
+
class Connection
|
12
|
+
include Assertions
|
13
|
+
include Loggable
|
14
|
+
|
15
|
+
# Tracks the list of APIs that don't work through the web socket -- these
|
16
|
+
# must be requested through the HTTP channel
|
17
|
+
# @return [Array<String>]
|
18
|
+
HTTP_APIS = %w(room.directory_rooms user.get_prefs)
|
19
|
+
|
20
|
+
# The URL that this connection is bound to
|
21
|
+
# @return [String]
|
22
|
+
attr_reader :url
|
23
|
+
|
24
|
+
# The callback to run when a message is received from the underlying socket.
|
25
|
+
# The data passed to the callback will always be a hash.
|
26
|
+
# @return [Proc]
|
27
|
+
attr_accessor :handler
|
28
|
+
|
29
|
+
# Builds a new connection for sending / receiving data via the given url.
|
30
|
+
#
|
31
|
+
# @note This will *not* open the connection -- #start must be explicitly called in order to do so.
|
32
|
+
# @param [String] url The URL to open a conection to
|
33
|
+
# @param [Hash] options The connection options
|
34
|
+
# @option options [Fixnum] :timeout The amount of time to allow to elapse for requests before timing out
|
35
|
+
# @option options [Hash] :params A default set of params that will get included on every message sent
|
36
|
+
# @raise [ArgumentError] if an invalid option is specified
|
37
|
+
def initialize(url, options = {})
|
38
|
+
assert_valid_keys(options, :timeout, :params)
|
39
|
+
|
40
|
+
@url = url
|
41
|
+
@message_id = 0
|
42
|
+
@timeout = options[:timeout]
|
43
|
+
@default_params = options[:params] || {}
|
44
|
+
end
|
45
|
+
|
46
|
+
# Initiates the connection with turntable
|
47
|
+
#
|
48
|
+
# @return [true]
|
49
|
+
def start
|
50
|
+
@socket = Faye::WebSocket::Client.new(url)
|
51
|
+
@socket.onopen = lambda {|event| on_open(event)}
|
52
|
+
@socket.onclose = lambda {|event| on_close(event)}
|
53
|
+
@socket.onmessage = lambda {|event| on_message(event)}
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
# Closes the connection (if one was previously opened)
|
58
|
+
#
|
59
|
+
# @return [true]
|
60
|
+
def close
|
61
|
+
@socket.close if @socket
|
62
|
+
true
|
63
|
+
end
|
64
|
+
|
65
|
+
# Whether this connection's socket is currently open
|
66
|
+
#
|
67
|
+
# @return [Boolean] +true+ if the connection is open, otherwise +false+
|
68
|
+
def connected?
|
69
|
+
@connected
|
70
|
+
end
|
71
|
+
|
72
|
+
# Publishes the given params to the underlying web socket. The defaults
|
73
|
+
# initially configured as part of the connection will also be included in
|
74
|
+
# the message.
|
75
|
+
#
|
76
|
+
# @param [Hash] params The parameters to include in the message sent
|
77
|
+
# @return [Fixnum] The id of the message delivered
|
78
|
+
def publish(params)
|
79
|
+
params[:msgid] = message_id = next_message_id
|
80
|
+
params = @default_params.merge(params)
|
81
|
+
|
82
|
+
logger.debug "Message sent: #{params.inspect}"
|
83
|
+
|
84
|
+
if HTTP_APIS.include?(params[:api])
|
85
|
+
publish_to_http(params)
|
86
|
+
else
|
87
|
+
publish_to_socket(params)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Add timeout handler
|
91
|
+
EventMachine.add_timer(@timeout) do
|
92
|
+
dispatch('msgid' => message_id, 'command' => 'response_received', 'error' => 'timed out')
|
93
|
+
end if @timeout
|
94
|
+
|
95
|
+
message_id
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
# Publishes the given params to the web socket
|
100
|
+
def publish_to_socket(params)
|
101
|
+
message = params.to_json
|
102
|
+
data = "~m~#{message.length}~m~#{message}"
|
103
|
+
@socket.send(data)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Publishes the given params to the HTTP API
|
107
|
+
def publish_to_http(params)
|
108
|
+
api = params.delete(:api)
|
109
|
+
message_id = params[:msgid]
|
110
|
+
|
111
|
+
http = EventMachine::HttpRequest.new("http://turntable.fm/api/#{api}").get(:query => params)
|
112
|
+
if http.response_header.status == 200
|
113
|
+
# Command executed properly: parse the results
|
114
|
+
success, data = JSON.parse(http.response)
|
115
|
+
data = {'result' => data} unless data.is_a?(Hash)
|
116
|
+
message = data.merge('success' => success)
|
117
|
+
else
|
118
|
+
# Command failed to run
|
119
|
+
message = {'success' => false, 'error' => http.error}
|
120
|
+
end
|
121
|
+
message.merge!('msgid' => message_id)
|
122
|
+
|
123
|
+
# Run the message handler
|
124
|
+
event = Faye::WebSocket::API::Event.new('message', :data => "~m~#{Time.now.to_i}~m~#{JSON.generate(message)}")
|
125
|
+
on_message(event)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Runs the configured handler with the given message
|
129
|
+
def dispatch(message)
|
130
|
+
Turntabler.run { @handler.call(message) } if @handler
|
131
|
+
end
|
132
|
+
|
133
|
+
# Callback when the socket is opened.
|
134
|
+
def on_open(event)
|
135
|
+
logger.debug 'Socket opened'
|
136
|
+
@connected = true
|
137
|
+
end
|
138
|
+
|
139
|
+
# Callback when the socket is closed. This will mark the connection as no
|
140
|
+
# longer connected.
|
141
|
+
def on_close(event)
|
142
|
+
logger.debug 'Socket closed'
|
143
|
+
@connected = false
|
144
|
+
@socket = nil
|
145
|
+
dispatch('command' => 'session_ended')
|
146
|
+
end
|
147
|
+
|
148
|
+
# Callback when a message has been received from the remote server on the
|
149
|
+
# open socket.
|
150
|
+
def on_message(event)
|
151
|
+
data = event.data
|
152
|
+
|
153
|
+
response = data.match(/~m~\d*~m~(.*)/)[1]
|
154
|
+
message =
|
155
|
+
case response
|
156
|
+
when /no_session/
|
157
|
+
{'command' => 'no_session'}
|
158
|
+
when /~h~([0-9]+)/
|
159
|
+
# Send the heartbeat command back to the server
|
160
|
+
@socket.send($1)
|
161
|
+
{'command' => 'heartbeat'}
|
162
|
+
else
|
163
|
+
JSON.parse(response)
|
164
|
+
end
|
165
|
+
message['command'] = 'response_received' if message['msgid']
|
166
|
+
|
167
|
+
logger.debug "Message received: #{message.inspect}"
|
168
|
+
dispatch(message)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Calculates what the next message id should be sent to turntable
|
172
|
+
def next_message_id
|
173
|
+
@message_id += 1
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Turntabler
|
2
|
+
# Provides a set of helper functions for dealing with message digests
|
3
|
+
# @api private
|
4
|
+
module DigestHelpers
|
5
|
+
# Generates a SHA1 hash from the given data
|
6
|
+
#
|
7
|
+
# @param [String] data The data to create a hash from
|
8
|
+
# @return [String]
|
9
|
+
def digest(data)
|
10
|
+
Digest::SHA1.hexdigest(data.to_s)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|