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,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