turntabler 0.0.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/.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
|