turntabler 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +7 -0
  2. data/.rspec +2 -0
  3. data/.yardopts +7 -0
  4. data/CHANGELOG.md +5 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE +20 -0
  7. data/README.md +383 -0
  8. data/Rakefile +11 -0
  9. data/examples/Gemfile +3 -0
  10. data/examples/Gemfile.lock +29 -0
  11. data/examples/autobop.rb +13 -0
  12. data/examples/autofan.rb +13 -0
  13. data/examples/blacklist.rb +16 -0
  14. data/examples/bop.rb +15 -0
  15. data/examples/bopcount.rb +20 -0
  16. data/examples/chat_bot.rb +16 -0
  17. data/examples/modlist.rb +19 -0
  18. data/examples/switch.rb +40 -0
  19. data/examples/time_afk_list.rb +46 -0
  20. data/lib/turntabler/assertions.rb +36 -0
  21. data/lib/turntabler/authorized_user.rb +217 -0
  22. data/lib/turntabler/avatar.rb +34 -0
  23. data/lib/turntabler/boot.rb +22 -0
  24. data/lib/turntabler/client.rb +457 -0
  25. data/lib/turntabler/connection.rb +176 -0
  26. data/lib/turntabler/digest_helpers.rb +13 -0
  27. data/lib/turntabler/error.rb +5 -0
  28. data/lib/turntabler/event.rb +239 -0
  29. data/lib/turntabler/handler.rb +67 -0
  30. data/lib/turntabler/loggable.rb +11 -0
  31. data/lib/turntabler/message.rb +24 -0
  32. data/lib/turntabler/playlist.rb +50 -0
  33. data/lib/turntabler/preferences.rb +70 -0
  34. data/lib/turntabler/resource.rb +194 -0
  35. data/lib/turntabler/room.rb +377 -0
  36. data/lib/turntabler/room_directory.rb +133 -0
  37. data/lib/turntabler/snag.rb +16 -0
  38. data/lib/turntabler/song.rb +247 -0
  39. data/lib/turntabler/sticker.rb +48 -0
  40. data/lib/turntabler/sticker_placement.rb +25 -0
  41. data/lib/turntabler/user.rb +274 -0
  42. data/lib/turntabler/version.rb +9 -0
  43. data/lib/turntabler/vote.rb +19 -0
  44. data/lib/turntabler.rb +102 -0
  45. data/spec/spec_helper.rb +7 -0
  46. data/spec/turntabler_spec.rb +4 -0
  47. data/turntable.gemspec +24 -0
  48. 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
@@ -0,0 +1,5 @@
1
+ module Turntabler
2
+ # Represents an error that occurred while interacting with the Turntable API
3
+ class Error < StandardError
4
+ end
5
+ end