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.
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,239 @@
1
+ require 'turntabler/boot'
2
+ require 'turntabler/message'
3
+ require 'turntabler/snag'
4
+ require 'turntabler/song'
5
+
6
+ module Turntabler
7
+ # Provides access to all of the events that get triggered by incoming messages
8
+ # from the Turntable API
9
+ # @api private
10
+ class Event
11
+ class << self
12
+ # Maps Turntable command => event name
13
+ # @return [Hash<String, String>]
14
+ attr_reader :commands
15
+
16
+ # Defines a new event that maps to the given Turntable command. The
17
+ # block defines how to typecast the data that is received from Turntable.
18
+ #
19
+ # @param [String] name The name of the event exposed to the rest of the library
20
+ # @param [String] command The Turntable command that this event name maps to
21
+ # @yield [data] Gives the data to typecast to the block
22
+ # @yieldparam [Hash] data The data received from Turntable
23
+ # @yieldreturn The typecasted data that should be passed into any handlers bound to the event
24
+ # @return [nil]
25
+ def handle(name, command = name, &block)
26
+ block ||= lambda {}
27
+ commands[command] = name
28
+
29
+ define_method("typecast_#{command}_event", &block)
30
+ protected :"typecast_#{command}_event"
31
+ end
32
+
33
+ # Determines whether the given command is handled
34
+ #
35
+ # @param [String] command The command to check for the existence of
36
+ # @return [Boolean] +true+ if the command exists, otherwise +false+
37
+ def command?(command)
38
+ command && commands.include?(command.to_sym)
39
+ end
40
+ end
41
+
42
+ @commands = {}
43
+
44
+ # An authenticated session is missing
45
+ handle :session_missing, :no_session
46
+
47
+ # The client is being asked to disconnect
48
+ handle :session_end_requested, :killdashnine do
49
+ room_id = data['roomid']
50
+ if !room_id || room && room.id == room_id
51
+ client.close(true)
52
+ data['msg'] || 'Unknown reason'
53
+ end
54
+ end
55
+
56
+ # The client's connection has closed
57
+ handle :session_ended
58
+
59
+ # A heartbeat was received from Turntable to ensure the client's connection
60
+ # is still valid
61
+ handle :heartbeat
62
+
63
+ # A response was receivied from a prior command sent to Turntable
64
+ handle :response_received do
65
+ data
66
+ end
67
+
68
+ # Information about the room was updated
69
+ handle :room_updated, :update_room do
70
+ room.attributes = data
71
+ room
72
+ end
73
+
74
+ # One or more users have entered the room
75
+ handle :user_entered, :registered do
76
+ data['user'].map do |attrs|
77
+ user = room.build_user(attrs)
78
+ room.listeners << user
79
+ user
80
+ end
81
+ end
82
+
83
+ # One or more users have left the room
84
+ handle :user_left, :deregistered do
85
+ data['user'].map do |attrs|
86
+ user = room.build_user(attrs)
87
+ room.listeners.delete(user)
88
+ user
89
+ end
90
+ end
91
+
92
+ # A user has been booted from the room
93
+ handle :user_booted, :booted_user do
94
+ boot = Boot.new(client, data)
95
+ client.room = nil if boot.user == client.user
96
+ boot
97
+ end
98
+
99
+ # A user's name / profile has been updated
100
+ handle :user_updated, :update_user do
101
+ fans_change = data.delete('fans')
102
+ user = room.build_user(data)
103
+ user.attributes = {'fans' => user.fans_count + fans_change} if fans_change
104
+ user
105
+ end
106
+
107
+ # A user's stickers have been updated
108
+ handle :user_updated, :update_sticker_placements do
109
+ room.build_user(data)
110
+ end
111
+
112
+ # A user spoke in the chat room
113
+ handle :user_spoke, :speak do
114
+ Message.new(client, data)
115
+ end
116
+
117
+ # A new dj was added to the room
118
+ handle :dj_added, :add_dj do
119
+ new_djs = []
120
+ data['user'].each_with_index do |attrs, index|
121
+ new_djs << user = room.build_user(attrs.merge('placements' => data['placements'][index]))
122
+ room.listeners << user
123
+ room.djs << user
124
+ end
125
+ new_djs
126
+ end
127
+
128
+ # A dj was removed from the room
129
+ handle :dj_removed, :rem_dj do
130
+ data['user'].map do |attrs|
131
+ user = room.build_user(attrs)
132
+ room.listeners << user
133
+ room.djs.delete(user)
134
+ user
135
+ end
136
+ end
137
+
138
+ # A new moderator was added to the room
139
+ handle :moderator_added, :new_moderator do
140
+ user = room.build_user(data)
141
+ room.listeners << user
142
+ room.moderators << user
143
+ user
144
+ end
145
+
146
+ # A moderator was removed from the room
147
+ handle :moderator_removed, :rem_moderator do
148
+ user = room.build_user(data)
149
+ room.moderators.delete(user)
150
+ user
151
+ end
152
+
153
+ # There are no more songs to play in the room
154
+ handle :song_unavailable, :nosong do
155
+ room.attributes = data['room'].merge('current_song' => nil)
156
+ nil
157
+ end
158
+
159
+ # A new song has started playing
160
+ handle :song_started, :newsong do
161
+ room.attributes = data['room']
162
+ room.current_song
163
+ end
164
+
165
+ # The current song has ended
166
+ handle :song_ended, :endsong do
167
+ room.attributes = data['room']
168
+ room.current_song
169
+ end
170
+
171
+ # A vote was cast for the song
172
+ handle :song_voted, :update_votes do
173
+ room.attributes = data['room']
174
+ room.current_song
175
+ end
176
+
177
+ # A user in the room has queued the current song onto their playlist
178
+ handle :song_snagged, :snagged do
179
+ Snag.new(client, data.merge(:song => room.current_song))
180
+ end
181
+
182
+ # A song was skipped due to a copyright claim
183
+ handle :song_blocked do
184
+ Song.new(client, data)
185
+ end
186
+
187
+ # A song was skipped due to a limit on # of plays per hour
188
+ handle :song_limited, :dmca_error do
189
+ Song.new(client, data)
190
+ end
191
+
192
+ # A private message was received from another user in the room
193
+ handle :message_received, :pmmed do
194
+ Message.new(client, data)
195
+ end
196
+
197
+ # A song search has completed and the results are available
198
+ handle :search_completed, :search_complete do
199
+ [data['docs'].map {|attrs| Song.new(client, attrs)}]
200
+ end
201
+
202
+ # A song search failed to complete
203
+ handle :search_failed
204
+
205
+ # The name of the event that was triggered
206
+ # @return [String]
207
+ attr_reader :name
208
+
209
+ # The raw hash of data parsed from the event
210
+ # @return [Hash]
211
+ attr_reader :data
212
+
213
+ # The typecasted results parsed from the data
214
+ # @return [Array]
215
+ attr_reader :results
216
+
217
+ # Creates a new event triggered with the given data
218
+ #
219
+ # @param [Turntabler::Client] client The client that this event is bound to
220
+ # @param [Hash] data The response data from Turntable
221
+ def initialize(client, data)
222
+ @client = client
223
+ @data = data
224
+ command = data['command'].to_sym
225
+ @name = self.class.commands[command]
226
+ @results = __send__("typecast_#{command}_event")
227
+ @results = [@results] unless @results.is_a?(Array)
228
+ end
229
+
230
+ private
231
+ # The client that all APIs filter through
232
+ attr_reader :client
233
+
234
+ # Gets the current room the user is in
235
+ def room
236
+ client.room
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,67 @@
1
+ require 'turntabler/event'
2
+
3
+ module Turntabler
4
+ # Represents a callback that's been bound to a particular event
5
+ # @api private
6
+ class Handler
7
+ include Assertions
8
+
9
+ # The event this handler is bound to
10
+ # @return [String]
11
+ attr_reader :event
12
+
13
+ # Whether to only call the handler once and then never again
14
+ # @return [Boolean] +true+ if only called once, otherwise +false+
15
+ attr_reader :once
16
+
17
+ # The data that must be matched in order for the handler to run
18
+ # @return [Hash<String, Object>]
19
+ attr_reader :conditions
20
+
21
+ # Builds a new handler bound to the given event.
22
+ #
23
+ # @param [String] event The name of the event to bind to
24
+ # @param [Hash] options The configuration options
25
+ # @option options [Boolean] :once (false) Whether to only call the handler once
26
+ # @option options [Hash] :if (nil) Data that must be matched to run
27
+ # @raise [ArgumentError] if an invalid option is specified
28
+ def initialize(event, options = {}, &block)
29
+ assert_valid_values(event, *Event.commands.values)
30
+ assert_valid_keys(options, :once, :if)
31
+ options = {:once => false, :if => nil}.merge(options)
32
+
33
+ @event = event
34
+ @once = options[:once]
35
+ @conditions = options[:if]
36
+ @block = block
37
+ end
38
+
39
+ # Runs this handler with results from the given event.
40
+ #
41
+ # @param [Array] event The event being triggered
42
+ # @return [Boolean] +true+ if conditions were matcher to run the handler, otherwise +false+
43
+ def run(event)
44
+ if conditions_match?(event.data)
45
+ # Run the block for each individual result
46
+ event.results.each do |result|
47
+ @block.call(*[result].compact)
48
+ end
49
+
50
+ true
51
+ else
52
+ false
53
+ end
54
+ end
55
+
56
+ private
57
+ # Determines whether the conditions configured for this handler match the
58
+ # event data
59
+ def conditions_match?(data)
60
+ if conditions
61
+ conditions.all? {|(key, value)| data[key] == value}
62
+ else
63
+ true
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,11 @@
1
+ module Turntabler
2
+ # Provides a set of helper methods for logging
3
+ # @api private
4
+ module Loggable
5
+ private
6
+ # Delegates access to the logger to Turntabler.logger
7
+ def logger
8
+ Turntabler.logger
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ require 'turntabler/resource'
2
+ require 'turntabler/user'
3
+
4
+ module Turntabler
5
+ # Represents a message that was sent to or from the current user. This can
6
+ # either be within the context of a room or a private conversation.
7
+ class Message < Resource
8
+ # The user who sent the message
9
+ # @return [Turntabler::User]
10
+ attribute :sender, :senderid do |id|
11
+ room? ? room.build_user(:_id => id) : User.new(client, :_id => id)
12
+ end
13
+
14
+ # The text of the message
15
+ # @return [String]
16
+ attribute :content, :text
17
+
18
+ # The time at which the message was created
19
+ # @return [Time]
20
+ attribute :created_at, :time do |value|
21
+ Time.at(value)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
1
+ require 'turntabler/resource'
2
+ require 'turntabler/song'
3
+
4
+ module Turntabler
5
+ # Represents a collection of songs managed by the user and that can be played
6
+ # within a room
7
+ class Playlist < Resource
8
+ # The songs that have been added to this playlist
9
+ # @return [Array<Turntabler::Song>]
10
+ attribute :songs, :list do |songs|
11
+ songs.map {|attrs| Song.new(client, attrs)}
12
+ end
13
+
14
+ # Loads the attributes for this playlist. Attributes will automatically load
15
+ # when accessed, but this allows data to be forcefully loaded upfront.
16
+ #
17
+ # @param [Hash] options The configuration options
18
+ # @option options [Boolean] minimal (false) Whether to only include the identifiers for songs and not the entire metadata
19
+ # @return [true]
20
+ # @raise [Turntabler::Error] if the command fails
21
+ # @example
22
+ # playlist.load # => true
23
+ # playlist.songs # => [#<Turntabler::Song ...>, ...]
24
+ def load(options = {})
25
+ assert_valid_keys(options, :minimal)
26
+ options = {:minimal => false}.merge(options)
27
+
28
+ data = api('playlist.all', options)
29
+ self.attributes = data
30
+ super()
31
+ end
32
+
33
+ # Gets the song with the given id.
34
+ #
35
+ # @param [String] song_id The id for the song
36
+ # @return [Turntabler::Song]
37
+ # @raise [Turntabler::Error] if the list of songs fail to load
38
+ # @example
39
+ # playlist.song('4fd8...') # => #<Turntabler::Song ...>
40
+ def song(song_id)
41
+ songs.detect {|song| song.id == song_id}
42
+ end
43
+
44
+ private
45
+ def api(command, options = {})
46
+ options[:playlist_name] = id
47
+ super
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,70 @@
1
+ require 'turntabler/resource'
2
+
3
+ module Turntabler
4
+ # Represents the site preferences for the authorized user
5
+ class Preferences < Resource
6
+ # Send e-mails if a fan starts DJing
7
+ # @return [Boolean]
8
+ attribute :notify_dj
9
+
10
+ # Send e-mails when someone becomes a fan
11
+ # @return [Boolean]
12
+ attribute :notify_fan
13
+
14
+ # Sends infrequent e-mails about news
15
+ # @return [Boolean]
16
+ attribute :notify_news
17
+
18
+ # Sends e-mails at random times with a different subsection of the digits of pi
19
+ # @return [Boolean]
20
+ attribute :notify_random
21
+
22
+ # Publishes to facebook songs awesomed in public rooms
23
+ # @return [Boolean]
24
+ attribute :facebook_awesome
25
+
26
+ # Publishes to facebook when a public room is joined
27
+ # @return [Boolean]
28
+ attribute :facebook_join
29
+
30
+ # Publishes to facebook when DJing in a public room
31
+ # @return [Boolean]
32
+ attribute :facebook_dj
33
+
34
+ # Loads the user's current Turntable preferences.
35
+ #
36
+ # @return [true]
37
+ # @raise [Turntabler::Error] if the command fails
38
+ # @example
39
+ # preferences.load # => true
40
+ def load
41
+ data = api('user.get_prefs')
42
+ self.attributes = data['result'].inject({}) do |result, (preference, value, id, description)|
43
+ result[preference] = value
44
+ result
45
+ end
46
+ super
47
+ end
48
+
49
+ # Updates the preferences.
50
+ #
51
+ # @param [Hash] attributes The attributes to update
52
+ # @option attributes [Boolean] :notify_dj
53
+ # @option attributes [Boolean] :notify_fan
54
+ # @option attributes [Boolean] :notify_news
55
+ # @option attributes [Boolean] :notify_random
56
+ # @option attributes [Boolean] :facbeook_awesome
57
+ # @option attributes [Boolean] :facebook_join
58
+ # @option attributes [Boolean] :facebook_dj
59
+ # @return [true]
60
+ # @raise [Turntabler::Error] if the command fails
61
+ # @example
62
+ # preferences.update(:notify_dj => false) # => true
63
+ def update(attributes = {})
64
+ assert_valid_values(attributes, :notify_dj, :notify_fan, :notify_news, :notify_random, :facebook_awesome, :facebook_join, :facebook_dj)
65
+ api('user.edit_prefs', attributes)
66
+ self.attributes = attributes
67
+ true
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,194 @@
1
+ require 'pp'
2
+ require 'digest/sha1'
3
+ require 'turntabler/assertions'
4
+ require 'turntabler/digest_helpers'
5
+
6
+ module Turntabler
7
+ # Represents an object that's been created using content from Turntable. This
8
+ # encapsulates responsibilities such as reading and writing attributes.
9
+ #
10
+ # By default all Turntable resources have a +:id+ attribute defined.
11
+ class Resource
12
+ include Assertions
13
+ include DigestHelpers
14
+
15
+ class << self
16
+ include Assertions
17
+
18
+ # Defines a new Turntable attribute on this class. By default, the name
19
+ # of the attribute is assumed to be the same name that Turntable specifies
20
+ # in its API. If the names are different, this can be overridden on a
21
+ # per-attribute basis.
22
+ #
23
+ # Configuration options:
24
+ # * +:load+ - Whether the resource should be loaded remotely from
25
+ # Turntable in order to access the attribute. Default is true.
26
+ #
27
+ # == Examples
28
+ #
29
+ # # Define a "name" attribute that maps to a Turntable "name" attribute
30
+ # attribute :name
31
+ #
32
+ # # Define an "id" attribute that maps to a Turntable "_id" attribute
33
+ # attribute :id, :_id
34
+ #
35
+ # # Define an "user_id" attribute that maps to both a Turntable "user_id" and "userid" attribute
36
+ # attribute :user_id, :user_id, :userid
37
+ #
38
+ # # Define a "time" attribute that maps to a Turntable "time" attribute
39
+ # # and converts the value to a Time object
40
+ # attribute :time do |value|
41
+ # Time.at(value)
42
+ # end
43
+ #
44
+ # # Define a "created_at" attribute that maps to a Turntable "time" attribute
45
+ # # and converts the value to a Time object
46
+ # attribute :created_at, :time do |value|
47
+ # Time.at(value)
48
+ # end
49
+ #
50
+ # # Define a "friends" attribute that does *not* get loaded from Turntable
51
+ # # when accessed
52
+ # attribute :friends, :load => false
53
+ #
54
+ # @api private
55
+ # @!macro [attach] attribute
56
+ # @!attribute [r] $1
57
+ def attribute(name, *turntable_names, &block)
58
+ options = turntable_names.last.is_a?(Hash) ? turntable_names.pop : {}
59
+ assert_valid_keys(options, :load)
60
+ options = {:load => true}.merge(options)
61
+
62
+ # Reader
63
+ define_method(name) do
64
+ load if instance_variable_get("@#{name}").nil? && !loaded? && options[:load]
65
+ instance_variable_get("@#{name}")
66
+ end
67
+
68
+ # Query
69
+ define_method("#{name}?") do
70
+ !!__send__(name)
71
+ end
72
+
73
+ # Typecasting
74
+ block ||= lambda {|value| value}
75
+ define_method("typecast_#{name}", &block)
76
+ protected :"typecast_#{name}"
77
+
78
+ # Attribute name conversion
79
+ turntable_names = [name] if turntable_names.empty?
80
+ turntable_names.each do |turntable_name|
81
+ define_method("#{turntable_name}=") do |value|
82
+ instance_variable_set("@#{name}", value.nil? ? nil : __send__("typecast_#{name}", value))
83
+ end
84
+ protected :"#{turntable_name}="
85
+ end
86
+ end
87
+ end
88
+
89
+ # The unique identifier for this resource
90
+ # @return [String, Fixnum]
91
+ attribute :id, :_id, :load => false
92
+
93
+ # Initializes this resources with the given attributes. This will continue
94
+ # to call the superclass's constructor with any additional arguments that
95
+ # get specified.
96
+ #
97
+ # @api private
98
+ def initialize(client, attributes = {}, *args)
99
+ @loaded = false
100
+ @client = client
101
+ self.attributes = attributes
102
+ super(*args)
103
+ end
104
+
105
+ # Loads the attributes for this resource from Turntable. By default this is
106
+ # a no-op and just marks the resource as loaded.
107
+ #
108
+ # @return [true]
109
+ def load
110
+ @loaded = true
111
+ end
112
+ alias :reload :load
113
+
114
+ # Determines whether the current resource has been loaded from Turntable.
115
+ #
116
+ # @return [Boolean] +true+ if the resource has been loaded, otherwise +false+
117
+ def loaded?
118
+ @loaded
119
+ end
120
+
121
+ # Attempts to set attributes on the object only if they've been explicitly
122
+ # defined by the class. Note that this will also attempt to interpret any
123
+ # "metadata" properties as additional attributes.
124
+ #
125
+ # @api private
126
+ def attributes=(attributes)
127
+ if attributes
128
+ attributes.each do |attribute, value|
129
+ attribute = attribute.to_s
130
+ if attribute == 'metadata'
131
+ self.attributes = value
132
+ else
133
+ __send__("#{attribute}=", value) if respond_to?("#{attribute}=")
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ # Forces this object to use PP's implementation of inspection.
140
+ #
141
+ # @api private
142
+ def pretty_print(q)
143
+ q.pp_object(self)
144
+ end
145
+ alias inspect pretty_print_inspect
146
+
147
+ # Defines the instance variables that should be printed when inspecting this
148
+ # object. This ignores the +client+ and +loaded+ attributes.
149
+ #
150
+ # @api private
151
+ def pretty_print_instance_variables
152
+ (instance_variables - [:'@client', :'@loaded']).sort
153
+ end
154
+
155
+ # Determines whether this resource is equal to another based on their
156
+ # unique identifiers.
157
+ #
158
+ # @return [Boolean] +true+ if the resource ids are equal, otherwise +false+
159
+ def ==(other)
160
+ if other && other.respond_to?(:id) && other.id
161
+ other.id == id
162
+ else
163
+ false
164
+ end
165
+ end
166
+ alias :eql? :==
167
+
168
+ # Generates a hash for this resource based on the unique identifier
169
+ #
170
+ # @return [Fixnum]
171
+ def hash
172
+ id.hash
173
+ end
174
+
175
+ private
176
+ # The client that all APIs filter through
177
+ attr_reader :client
178
+
179
+ # Runs the given API command on the client.
180
+ def api(command, options = {})
181
+ client.api(command, options)
182
+ end
183
+
184
+ # Gets the current room the user is in
185
+ def room
186
+ client.room || raise(Turntabler::Error, 'User is not currently in a room')
187
+ end
188
+
189
+ # Determines whether the user is currently in a room
190
+ def room?
191
+ !client.room.nil?
192
+ end
193
+ end
194
+ end