koala 1.0.0 → 1.1.0rc

Sign up to get free protection for your applications and to get access to all the features.
data/.autotest ADDED
@@ -0,0 +1,12 @@
1
+ # Override autotest default magic to rerun all tests every time a
2
+ # change is detected on the file system.
3
+ class Autotest
4
+
5
+ def get_to_green
6
+ begin
7
+ rerun_all_tests
8
+ wait_for_changes unless all_good
9
+ end until all_good
10
+ end
11
+
12
+ end
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  pkg
2
2
  .project
3
- Gemfile.lock
3
+ Gemfile.lock
4
+ .rvmrc
data/CHANGELOG CHANGED
@@ -1,3 +1,21 @@
1
+ v1.1
2
+ New/updated methods:
3
+ -- Batch API support through Koala::Facebook::GraphAPI.batch (thanks, seejohnrun!)
4
+ -- includes file uploads, error handling, and FQL
5
+ -- Added GraphAPI#get_comments_for_urls (thanks, amrnt!)
6
+ -- Added RestAPI#fql_multiquery, which simplifies the results (thanks, amrnt!)
7
+ Updated methods:
8
+ -- RealtimeUpdates now uses a GraphAPI object instead of its own API
9
+ -- RestAPI#rest_call now has an optional last argument for method, for calls requiring POST, DELETE, etc. (thanks, sshilo!)
10
+ -- Filename can now be specified when uploading (e.g. for Ads API) (thanks, sshilo!)
11
+ -- get_objects([]) returns [] instead of a Facebook error in non-batch mode (thanks, aselder!)
12
+ Internal improvements:
13
+ -- HTTP services are more modular and can be changed on the fly (thanks, chadk!)
14
+ -- Includes support for uploading StringIOs and other non-files via Net::HTTP even when using TyphoeusService
15
+ -- Support for global proxy and timeout settings (thanks, itchy!)
16
+ -- Support for setting certificate path and file to address Net::HTTP errors under Ruby 1.9.2
17
+ -- Koala now uses the modern Typhoeus API (thanks, aselder!)
18
+
1
19
  v1.0
2
20
  New methods:
3
21
  -- Photo and file upload now supported through #put_picture
@@ -0,0 +1 @@
1
+ Autotest.add_discovery { "rspec2" }
data/koala.gemspec CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{koala}
5
- s.version = "1.0.0"
6
- s.date = %q{2011-05-01}
5
+ s.version = "1.1.0rc"
6
+ s.date = %q{2011-06-06}
7
7
 
8
8
  s.summary = %q{A lightweight, flexible library for Facebook with support for the Graph API, the REST API, realtime updates, and OAuth authentication.}
9
9
  s.description = %q{Koala is a lightweight, flexible Ruby SDK for Facebook. It allows read/write access to the social graph via the Graph and REST APIs, as well as support for realtime updates and OAuth and Facebook Connect authentication. Koala is fully tested and supports Net::HTTP and Typhoeus connections out of the box and can accept custom modules for other services.}
@@ -30,20 +30,20 @@ Gem::Specification.new do |s|
30
30
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
31
31
  s.add_runtime_dependency(%q<json>, ["~> 1.0"])
32
32
  s.add_runtime_dependency(%q<multipart-post>, ["~> 1.0"])
33
- s.add_development_dependency(%q<rspec>, ["~> 2.5.0"])
33
+ s.add_development_dependency(%q<rspec>, ["~> 2.5"])
34
34
  s.add_development_dependency(%q<rake>, ["~> 0.8.7"])
35
35
  s.add_development_dependency(%q<typhoeus>, ["~> 0.2.4"])
36
36
  else
37
37
  s.add_dependency(%q<json>, ["~> 1.0"])
38
38
  s.add_dependency(%q<multipart-post>, ["~> 1.0"])
39
- s.add_dependency(%q<rspec>, ["~> 2.5.0"])
39
+ s.add_dependency(%q<rspec>, ["~> 2.5"])
40
40
  s.add_dependency(%q<rake>, ["~> 0.8.7"])
41
41
  s.add_dependency(%q<typhoeus>, ["~> 0.2.4"])
42
42
  end
43
43
  else
44
44
  s.add_dependency(%q<json>, ["~> 1.0"])
45
45
  s.add_dependency(%q<multipart-post>, ["~> 1.0"])
46
- s.add_dependency(%q<rspec>, ["~> 2.5.0"])
46
+ s.add_dependency(%q<rspec>, ["~> 2.5"])
47
47
  s.add_dependency(%q<rake>, ["~> 0.8.7"])
48
48
  s.add_dependency(%q<typhoeus>, ["~> 0.2.4"])
49
49
  end
data/lib/koala.rb CHANGED
@@ -9,10 +9,14 @@ require 'base64'
9
9
 
10
10
  # include koala modules
11
11
  require 'koala/http_services'
12
+ require 'koala/http_services/net_http_service'
13
+ require 'koala/oauth'
12
14
  require 'koala/graph_api'
15
+ require 'koala/graph_api_batch'
13
16
  require 'koala/rest_api'
14
17
  require 'koala/realtime_updates'
15
18
  require 'koala/test_users'
19
+ require 'koala/http_services'
16
20
 
17
21
  # add KoalaIO class
18
22
  require 'koala/uploadable_io'
@@ -47,27 +51,24 @@ module Koala
47
51
  # in the case of a server error
48
52
  raise APIError.new({"type" => "HTTP #{result.status.to_s}", "message" => "Response body: #{result.body}"}) if result.status >= 500
49
53
 
50
- # Parse the body as JSON and check for errors if provided a mechanism to do so
54
+ # parse the body as JSON and run it through the error checker (if provided)
51
55
  # Note: Facebook sometimes sends results like "true" and "false", which aren't strictly objects
52
56
  # and cause JSON.parse to fail -- so we account for that by wrapping the result in []
53
- body = response = JSON.parse("[#{result.body.to_s}]")[0]
54
- if error_checking_block
55
- yield(body)
56
- end
57
+ body = JSON.parse("[#{result.body.to_s}]")[0]
58
+ yield body if error_checking_block
57
59
 
58
- # now return the desired information
59
- if options[:http_component]
60
- result.send(options[:http_component])
61
- else
62
- body
63
- end
60
+ # if we want a component other than the body (e.g. redirect header for images), return that
61
+ options[:http_component] ? result.send(options[:http_component]) : body
64
62
  end
65
63
  end
66
64
 
65
+ # APIs
66
+
67
67
  class GraphAPI < API
68
68
  include GraphAPIMethods
69
+ include GraphAPIBatchMethods
69
70
  end
70
-
71
+
71
72
  class RestAPI < API
72
73
  include RestAPIMethods
73
74
  end
@@ -87,6 +88,8 @@ module Koala
87
88
  attr_reader :graph_api
88
89
  end
89
90
 
91
+ # Errors
92
+
90
93
  class APIError < StandardError
91
94
  attr_accessor :fb_error_type
92
95
  def initialize(details = {})
@@ -94,201 +97,33 @@ module Koala
94
97
  super("#{fb_error_type}: #{details["message"]}")
95
98
  end
96
99
  end
100
+ end
97
101
 
102
+ class KoalaError < StandardError; end
98
103
 
99
- class OAuth
100
- attr_reader :app_id, :app_secret, :oauth_callback_url
101
- def initialize(app_id, app_secret, oauth_callback_url = nil)
102
- @app_id = app_id
103
- @app_secret = app_secret
104
- @oauth_callback_url = oauth_callback_url
105
- end
106
-
107
- def get_user_info_from_cookie(cookie_hash)
108
- # Parses the cookie set by the official Facebook JavaScript SDK.
109
- #
110
- # cookies should be a Hash, like the one Rails provides
111
- #
112
- # If the user is logged in via Facebook, we return a dictionary with the
113
- # keys "uid" and "access_token". The former is the user's Facebook ID,
114
- # and the latter can be used to make authenticated requests to the Graph API.
115
- # If the user is not logged in, we return None.
116
- #
117
- # Download the official Facebook JavaScript SDK at
118
- # http://github.com/facebook/connect-js/. Read more about Facebook
119
- # authentication at http://developers.facebook.com/docs/authentication/.
120
-
121
- if fb_cookie = cookie_hash["fbs_" + @app_id.to_s]
122
- # remove the opening/closing quote
123
- fb_cookie = fb_cookie.gsub(/\"/, "")
124
-
125
- # since we no longer get individual cookies, we have to separate out the components ourselves
126
- components = {}
127
- fb_cookie.split("&").map {|param| param = param.split("="); components[param[0]] = param[1]}
128
-
129
- # generate the signature and make sure it matches what we expect
130
- auth_string = components.keys.sort.collect {|a| a == "sig" ? nil : "#{a}=#{components[a]}"}.reject {|a| a.nil?}.join("")
131
- sig = Digest::MD5.hexdigest(auth_string + @app_secret)
132
- sig == components["sig"] && (components["expires"] == "0" || Time.now.to_i < components["expires"].to_i) ? components : nil
133
- end
134
- end
135
- alias_method :get_user_info_from_cookies, :get_user_info_from_cookie
136
-
137
- def get_user_from_cookie(cookies)
138
- if info = get_user_info_from_cookies(cookies)
139
- string = info["uid"]
140
- end
141
- end
142
- alias_method :get_user_from_cookies, :get_user_from_cookie
143
-
144
- # URLs
145
-
146
- def url_for_oauth_code(options = {})
147
- # for permissions, see http://developers.facebook.com/docs/authentication/permissions
148
- permissions = options[:permissions]
149
- scope = permissions ? "&scope=#{permissions.is_a?(Array) ? permissions.join(",") : permissions}" : ""
150
- display = options.has_key?(:display) ? "&display=#{options[:display]}" : ""
151
-
152
- callback = options[:callback] || @oauth_callback_url
153
- raise ArgumentError, "url_for_oauth_code must get a callback either from the OAuth object or in the options!" unless callback
154
-
155
- # Creates the URL for oauth authorization for a given callback and optional set of permissions
156
- "https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}#{display}"
157
- end
158
-
159
- def url_for_access_token(code, options = {})
160
- # Creates the URL for the token corresponding to a given code generated by Facebook
161
- callback = options[:callback] || @oauth_callback_url
162
- raise ArgumentError, "url_for_access_token must get a callback either from the OAuth object or in the parameters!" unless callback
163
- "https://#{GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@app_secret}&code=#{code}"
164
- end
165
-
166
- def get_access_token_info(code, options = {})
167
- # convenience method to get a parsed token from Facebook for a given code
168
- # should this require an OAuth callback URL?
169
- get_token_from_server({:code => code, :redirect_uri => @oauth_callback_url}, false, options)
170
- end
171
-
172
- def get_access_token(code, options = {})
173
- # upstream methods will throw errors if needed
174
- if info = get_access_token_info(code, options)
175
- string = info["access_token"]
176
- end
177
- end
178
-
179
- def get_app_access_token_info(options = {})
180
- # convenience method to get a the application's sessionless access token
181
- get_token_from_server({:type => 'client_cred'}, true, options)
182
- end
183
-
184
- def get_app_access_token(options = {})
185
- if info = get_app_access_token_info(options)
186
- string = info["access_token"]
187
- end
188
- end
189
-
190
- # Originally provided directly by Facebook, however this has changed
191
- # as their concept of crypto changed. For historic purposes, this is their proposal:
192
- # https://developers.facebook.com/docs/authentication/canvas/encryption_proposal/
193
- # Currently see https://github.com/facebook/php-sdk/blob/master/src/facebook.php#L758
194
- # for a more accurate reference implementation strategy.
195
- def parse_signed_request(input)
196
- encoded_sig, encoded_envelope = input.split('.', 2)
197
- signature = base64_url_decode(encoded_sig).unpack("H*").first
198
- envelope = JSON.parse(base64_url_decode(encoded_envelope))
199
-
200
- raise "SignedRequest: Unsupported algorithm #{envelope['algorithm']}" if envelope['algorithm'] != 'HMAC-SHA256'
201
-
202
- # now see if the signature is valid (digest, key, data)
203
- hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @app_secret, encoded_envelope.tr("-_", "+/"))
204
- raise 'SignedRequest: Invalid signature' if (signature != hmac)
205
-
206
- return envelope
207
- end
208
-
209
- # from session keys
210
- def get_token_info_from_session_keys(sessions, options = {})
211
- # fetch the OAuth tokens from Facebook
212
- response = fetch_token_string({
213
- :type => 'client_cred',
214
- :sessions => sessions.join(",")
215
- }, true, "exchange_sessions", options)
216
-
217
- # Facebook returns an empty body in certain error conditions
218
- if response == ""
219
- raise APIError.new({
220
- "type" => "ArgumentError",
221
- "message" => "get_token_from_session_key received an error (empty response body) for sessions #{sessions.inspect}!"
222
- })
223
- end
224
-
225
- JSON.parse(response)
226
- end
227
-
228
- def get_tokens_from_session_keys(sessions, options = {})
229
- # get the original hash results
230
- results = get_token_info_from_session_keys(sessions, options)
231
- # now recollect them as just the access tokens
232
- results.collect { |r| r ? r["access_token"] : nil }
233
- end
234
-
235
- def get_token_from_session_key(session, options = {})
236
- # convenience method for a single key
237
- # gets the overlaoded strings automatically
238
- get_tokens_from_session_keys([session], options)[0]
239
- end
240
-
241
- protected
242
-
243
- def get_token_from_server(args, post = false, options = {})
244
- # fetch the result from Facebook's servers
245
- result = fetch_token_string(args, post, "access_token", options)
246
-
247
- # if we have an error, parse the error JSON and raise an error
248
- raise APIError.new((JSON.parse(result)["error"] rescue nil) || {}) if result =~ /error/
249
-
250
- # otherwise, parse the access token
251
- parse_access_token(result)
252
- end
253
-
254
- def parse_access_token(response_text)
255
- components = response_text.split("&").inject({}) do |hash, bit|
256
- key, value = bit.split("=")
257
- hash.merge!(key => value)
258
- end
259
- components
260
- end
261
-
262
- def fetch_token_string(args, post = false, endpoint = "access_token", options = {})
263
- Koala.make_request("/oauth/#{endpoint}", {
264
- :client_id => @app_id,
265
- :client_secret => @app_secret
266
- }.merge!(args), post ? "post" : "get", {:use_ssl => true}.merge!(options)).body
267
- end
268
-
269
- # base 64
270
- # directly from https://github.com/facebook/crypto-request-examples/raw/master/sample.rb
271
- def base64_url_decode(str)
272
- str += '=' * (4 - str.length.modulo(4))
273
- Base64.decode64(str.tr('-_', '+/'))
274
- end
275
- end
104
+ # Make an api request using the provided api service or one passed by the caller
105
+ def self.make_request(path, args, verb, options = {})
106
+ http_service = options.delete(:http_service) || Koala.http_service
107
+ options = options.merge(:use_ssl => true) if @always_use_ssl
108
+ http_service.make_request(path, args, verb, options)
276
109
  end
277
110
 
278
- class KoalaError< StandardError; end
279
-
280
111
  # finally, set up the http service Koala methods used to make requests
281
112
  # you can use your own (for HTTParty, etc.) by calling Koala.http_service = YourModule
282
- def self.http_service=(service)
283
- self.send(:include, service)
113
+ class << self
114
+ attr_accessor :http_service
115
+ attr_accessor :always_use_ssl
116
+ attr_accessor :base_http_service
284
117
  end
118
+ Koala.base_http_service = NetHTTPService
285
119
 
286
120
  # by default, try requiring Typhoeus -- if that works, use it
287
121
  # if you have Typheous and don't want to use it (or want another service),
288
122
  # you can run Koala.http_service = NetHTTPService (or MyHTTPService)
289
123
  begin
124
+ require 'koala/http_services/typhoeus_service'
290
125
  Koala.http_service = TyphoeusService
291
126
  rescue LoadError
292
- Koala.http_service = NetHTTPService
127
+ Koala.http_service = Koala.base_http_service
293
128
  end
294
129
  end
@@ -28,7 +28,20 @@ module Koala
28
28
  # If you are using the JavaScript SDK, you can use the
29
29
  # Koala::Facebook::OAuth.get_user_from_cookie() method below to get the OAuth access token
30
30
  # for the active user from the cookie saved by the SDK.
31
-
31
+
32
+ def self.included(base)
33
+ base.class_eval do
34
+ def self.check_response(response)
35
+ # check for Graph API-specific errors
36
+ # this returns an error, which is immediately raised (non-batch)
37
+ # or added to the list of batch results (batch)
38
+ if response.is_a?(Hash) && error_details = response["error"]
39
+ APIError.new(error_details)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
32
45
  # Objects
33
46
 
34
47
  def get_object(id, args = {}, options = {})
@@ -37,10 +50,10 @@ module Koala
37
50
  end
38
51
 
39
52
  def get_objects(ids, args = {}, options = {})
40
- # Fetchs all of the given object from the graph.
41
- # We return a map from ID to object. If any of the IDs are invalid,
42
- # we raise an exception.
43
- graph_call("", args.merge("ids" => ids.join(",")), "get", options)
53
+ # Fetchs all of the given objects from the graph.
54
+ # If any of the IDs are invalid, they'll raise an exception.
55
+ return [] if ids.empty?
56
+ graph_call("", args.merge("ids" => ids.respond_to?(:join) ? ids.join(",") : ids), "get", options)
44
57
  end
45
58
 
46
59
  def put_object(parent_object, connection_name, args = {}, options = {})
@@ -71,10 +84,19 @@ module Koala
71
84
 
72
85
  def get_connections(id, connection_name, args = {}, options = {})
73
86
  # Fetchs the connections for given object.
74
- result = graph_call("#{id}/#{connection_name}", args, "get", options)
75
- result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
87
+ graph_call("#{id}/#{connection_name}", args, "get", options) do |result|
88
+ result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
89
+ end
76
90
  end
77
91
 
92
+ def get_comments_for_urls(urls = [], args = {}, options = {})
93
+ # Fetchs the comments for given URLs (array or comma-separated string)
94
+ # see https://developers.facebook.com/blog/post/490
95
+ return [] if urls.empty?
96
+ args.merge!(:ids => urls.respond_to?(:join) ? urls.join(",") : urls)
97
+ get_object("comments", args, options)
98
+ end
99
+
78
100
  def put_connections(id, connection_name, args = {}, options = {})
79
101
  # Posts a certain connection
80
102
  raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Write operations require an access token"}) unless @access_token
@@ -93,8 +115,9 @@ module Koala
93
115
 
94
116
  def get_picture(object, args = {}, options = {})
95
117
  # Gets a picture object, returning the URL (which Facebook sends as a header)
96
- result = graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers))
97
- result["Location"]
118
+ graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers)) do |result|
119
+ result["Location"]
120
+ end
98
121
  end
99
122
 
100
123
  def put_picture(*picture_args)
@@ -123,7 +146,9 @@ module Koala
123
146
  options = picture_args[3 + args_offset] || {}
124
147
 
125
148
  args["source"] = Koala::UploadableIO.new(*picture_args.slice(0, 1 + args_offset))
126
-
149
+
150
+ options[:http_service] = Koala.base_http_service if args["source"].requires_base_http_service
151
+
127
152
  self.put_object(target_id, "photos", args, options)
128
153
  end
129
154
 
@@ -172,47 +197,62 @@ module Koala
172
197
 
173
198
  def search(search_terms, args = {}, options = {})
174
199
  args.merge!({:q => search_terms}) unless search_terms.nil?
175
- result = graph_call("search", args, "get", options)
176
- result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
200
+ graph_call("search", args, "get", options) do |result|
201
+ result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
202
+ end
177
203
  end
178
204
 
179
205
  # API access
180
-
181
- def graph_call(*args)
206
+
207
+ # Make a call which may or may not be batched
208
+ def graph_call(path, args = {}, verb = "get", options = {}, &post_processing)
182
209
  # Direct access to the Facebook API
183
210
  # see any of the above methods for example invocations
184
- response = api(*args) do |response|
185
- # check for Graph API-specific errors
186
- if response.is_a?(Hash) && error_details = response["error"]
187
- raise APIError.new(error_details)
211
+ unless GraphAPI.batch_mode?
212
+ result = api(path, args, verb, options) do |response|
213
+ if error = GraphAPI.check_response(response)
214
+ raise error
215
+ end
188
216
  end
217
+
218
+ # now process as appropriate (get picture header, make GraphCollection, etc.)
219
+ post_processing ? post_processing.call(result) : result
220
+ else
221
+ # for batch APIs, we queue up the call details (incl. post-processing)
222
+ GraphAPI.batch_calls << BatchOperation.new(
223
+ :url => path,
224
+ :args => args,
225
+ :method => verb,
226
+ :access_token => @access_token,
227
+ :http_options => options,
228
+ :post_processing => post_processing
229
+ )
230
+ nil # batch operations return nothing immediately
189
231
  end
190
-
191
- response
192
- end
232
+ end
193
233
 
194
234
  # GraphCollection support
195
-
196
235
  def get_page(params)
197
236
  # Pages through a set of results stored in a GraphCollection
198
237
  # Used for connections and search results
199
- result = graph_call(*params)
200
- result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
238
+ graph_call(*params) do |result|
239
+ result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
240
+ end
201
241
  end
202
242
 
203
243
  end
204
244
 
205
245
 
206
246
  class GraphCollection < Array
207
- #This class is a light wrapper for collections returned
208
- #from the Graph API.
247
+ # This class is a light wrapper for collections returned
248
+ # from the Graph API.
209
249
  #
210
- #It extends Array to allow direct access to the data colleciton
211
- #which should allow it to drop in seamlessly.
250
+ # It extends Array to allow direct access to the data colleciton
251
+ # which should allow it to drop in seamlessly.
212
252
  #
213
- #It also allows access to paging information and the
214
- #ability to get the next/previous page in the collection
215
- #by calling next_page or previous_page.
253
+ # It also allows access to paging information and the
254
+ # ability to get the next/previous page in the collection
255
+ # by calling next_page or previous_page.
216
256
  attr_reader :paging
217
257
  attr_reader :api
218
258