koala 1.0.0 → 1.1.0

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 (39) hide show
  1. data/.autotest +12 -0
  2. data/.gitignore +3 -1
  3. data/.travis.yml +8 -0
  4. data/CHANGELOG +26 -2
  5. data/Gemfile +4 -0
  6. data/autotest/discover.rb +1 -0
  7. data/koala.gemspec +8 -8
  8. data/lib/koala/batch_operation.rb +74 -0
  9. data/lib/koala/graph_api.rb +103 -102
  10. data/lib/koala/graph_batch_api.rb +87 -0
  11. data/lib/koala/graph_collection.rb +54 -0
  12. data/lib/koala/http_services/net_http_service.rb +92 -0
  13. data/lib/koala/http_services/typhoeus_service.rb +37 -0
  14. data/lib/koala/http_services.rb +13 -113
  15. data/lib/koala/oauth.rb +181 -0
  16. data/lib/koala/realtime_updates.rb +5 -14
  17. data/lib/koala/rest_api.rb +13 -8
  18. data/lib/koala/uploadable_io.rb +137 -77
  19. data/lib/koala.rb +36 -196
  20. data/readme.md +51 -32
  21. data/spec/cases/api_base_spec.rb +4 -4
  22. data/spec/cases/graph_api_batch_spec.rb +609 -0
  23. data/spec/cases/http_services/http_service_spec.rb +87 -12
  24. data/spec/cases/http_services/net_http_service_spec.rb +259 -77
  25. data/spec/cases/http_services/typhoeus_service_spec.rb +29 -21
  26. data/spec/cases/koala_spec.rb +55 -0
  27. data/spec/cases/oauth_spec.rb +1 -1
  28. data/spec/cases/realtime_updates_spec.rb +3 -3
  29. data/spec/cases/test_users_spec.rb +1 -1
  30. data/spec/cases/uploadable_io_spec.rb +56 -14
  31. data/spec/fixtures/cat.m4v +0 -0
  32. data/spec/fixtures/mock_facebook_responses.yml +100 -5
  33. data/spec/spec_helper.rb +2 -1
  34. data/spec/support/graph_api_shared_examples.rb +106 -35
  35. data/spec/support/json_testing_fix.rb +18 -0
  36. data/spec/support/mock_http_service.rb +57 -56
  37. data/spec/support/rest_api_shared_examples.rb +131 -7
  38. data/spec/support/setup_mocks_or_live.rb +3 -4
  39. metadata +34 -47
@@ -0,0 +1,92 @@
1
+ require "net/http" unless defined?(Net::HTTP)
2
+ require "net/https"
3
+ require "net/http/post/multipart"
4
+
5
+ module Koala
6
+ module NetHTTPService
7
+ # this service uses Net::HTTP to send requests to the graph
8
+ include Koala::HTTPService
9
+
10
+ # Net::HTTP-specific values
11
+ class << self
12
+ attr_accessor :ca_file, :ca_path, :verify_mode
13
+ end
14
+
15
+ def self.make_request(path, args, verb, options = {})
16
+ # We translate args to a valid query string. If post is specified,
17
+ # we send a POST request to the given path with the given arguments.
18
+
19
+ # by default, we use SSL only for private requests
20
+ # this makes public requests faster
21
+ private_request = args["access_token"] || @always_use_ssl || options[:use_ssl]
22
+
23
+ # if the verb isn't get or post, send it as a post argument
24
+ args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
25
+
26
+ http = create_http(server(options), private_request, options)
27
+
28
+ response = http.start do |http|
29
+ if verb == "post"
30
+ if params_require_multipart? args
31
+ http.request Net::HTTP::Post::Multipart.new path, encode_multipart_params(args)
32
+ else
33
+ http.post(path, encode_params(args))
34
+ end
35
+ else
36
+ http.get("#{path}?#{encode_params(args)}")
37
+ end
38
+ end
39
+
40
+ Koala::Response.new(response.code.to_i, response.body, response)
41
+ end
42
+
43
+ protected
44
+ def self.encode_params(param_hash)
45
+ # unfortunately, we can't use to_query because that's Rails, not Ruby
46
+ # if no hash (e.g. no auth token) return empty string
47
+ ((param_hash || {}).collect do |key_and_value|
48
+ key_and_value[1] = MultiJson.encode(key_and_value[1]) if key_and_value[1].class != String
49
+ "#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
50
+ end).join("&")
51
+ end
52
+
53
+ def self.encode_multipart_params(param_hash)
54
+ Hash[*param_hash.collect do |key, value|
55
+ [key, value.kind_of?(Koala::UploadableIO) ? value.to_upload_io : value]
56
+ end.flatten]
57
+ end
58
+
59
+ def self.create_http(server, private_request, options)
60
+ if proxy_server = options[:proxy] || proxy
61
+ proxy = URI.parse(proxy_server)
62
+ http = Net::HTTP.new(server, private_request ? 443 : nil,
63
+ proxy.host, proxy.port, proxy.user, proxy.password)
64
+ else
65
+ http = Net::HTTP.new(server, private_request ? 443 : nil)
66
+ end
67
+
68
+ if timeout_value = options[:timeout] || timeout
69
+ http.open_timeout = timeout_value
70
+ http.read_timeout = timeout_value
71
+ end
72
+
73
+ # For HTTPS requests, set the proper CA certificates
74
+ if private_request
75
+ http.use_ssl = true
76
+ http.verify_mode = options[:verify_mode] || verify_mode || OpenSSL::SSL::VERIFY_PEER
77
+
78
+ if cert_file = options[:ca_file] || ca_file
79
+ raise Errno::ENOENT, "Certificate file #{cert_file.inspect} does not exist!" unless File.exists?(cert_file)
80
+ http.ca_file = cert_file
81
+ end
82
+
83
+ if cert_path = options[:ca_path] || ca_path
84
+ raise Errno::ENOENT, "Certificate path #{cert_path.inspect} does not exist!" unless File.directory?(cert_path)
85
+ http.ca_path = cert_path
86
+ end
87
+ end
88
+
89
+ http
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,37 @@
1
+ require "typhoeus" unless defined?(Typhoeus)
2
+
3
+ module Koala
4
+ module TyphoeusService
5
+ # this service uses Typhoeus to send requests to the graph
6
+ include Typhoeus
7
+ include Koala::HTTPService
8
+
9
+ def self.make_request(path, args, verb, options = {})
10
+ # if the verb isn't get or post, send it as a post argument
11
+ args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
12
+
13
+ # switch any UploadableIOs to the files Typhoeus expects
14
+ args.each_pair {|key, value| args[key] = value.to_file if value.is_a?(UploadableIO)}
15
+
16
+ # you can pass arguments directly to Typhoeus using the :typhoeus_options key
17
+ typhoeus_options = {:params => args}.merge(options[:typhoeus_options] || {})
18
+
19
+ # if proxy/timeout options aren't passed, check if defaults are set
20
+ typhoeus_options[:proxy] ||= proxy
21
+ typhoeus_options[:timeout] ||= timeout
22
+
23
+ # by default, we use SSL only for private requests (e.g. with access token)
24
+ # this makes public requests faster
25
+ prefix = (args["access_token"] || @always_use_ssl || options[:use_ssl]) ? "https" : "http"
26
+
27
+ response = Typhoeus::Request.send(verb, "#{prefix}://#{server(options)}#{path}", typhoeus_options)
28
+ Koala::Response.new(response.code, response.body, response.headers_hash)
29
+ end
30
+
31
+ protected
32
+
33
+ def self.multipart_requires_content_type?
34
+ false # Typhoeus handles multipart file types, we don't have to require it
35
+ end
36
+ end
37
+ end
@@ -13,134 +13,34 @@ module Koala
13
13
  def self.included(base)
14
14
  base.class_eval do
15
15
  class << self
16
- attr_accessor :always_use_ssl
16
+ attr_accessor :always_use_ssl, :proxy, :timeout
17
17
  end
18
-
18
+
19
19
  def self.server(options = {})
20
- "#{options[:beta] ? "beta." : ""}#{options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER}"
20
+ server = "#{options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER}"
21
+ server.gsub!(/\.facebook/, "-video.facebook") if options[:video]
22
+ "#{options[:beta] ? "beta." : ""}#{server}"
21
23
  end
22
-
23
- protected
24
24
 
25
- def self.params_require_multipart?(param_hash)
26
- param_hash.any? { |key, value| value.kind_of?(Koala::UploadableIO) }
27
- end
28
-
29
- def self.multipart_requires_content_type?
30
- true
31
- end
32
- end
33
- end
34
- end
35
-
36
- module NetHTTPService
37
- # this service uses Net::HTTP to send requests to the graph
38
- def self.included(base)
39
- base.class_eval do
40
- require "net/http" unless defined?(Net::HTTP)
41
- require "net/https"
42
- require "net/http/post/multipart"
43
-
44
- include Koala::HTTPService
45
-
46
- def self.make_request(path, args, verb, options = {})
47
- # We translate args to a valid query string. If post is specified,
48
- # we send a POST request to the given path with the given arguments.
49
-
50
- # by default, we use SSL only for private requests
51
- # this makes public requests faster
52
- private_request = args["access_token"] || @always_use_ssl || options[:use_ssl]
53
-
54
- # if the verb isn't get or post, send it as a post argument
55
- args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
56
-
57
- http = create_http(server(options), private_request, options)
58
- http.use_ssl = true if private_request
59
-
60
- result = http.start do |http|
61
- response, body = if verb == "post"
62
- if params_require_multipart? args
63
- http.request Net::HTTP::Post::Multipart.new path, encode_multipart_params(args)
64
- else
65
- http.post(path, encode_params(args))
66
- end
67
- else
68
- http.get("#{path}?#{encode_params(args)}")
69
- end
70
-
71
- Koala::Response.new(response.code.to_i, body, response)
72
- end
73
- end
74
-
75
- protected
76
25
  def self.encode_params(param_hash)
77
26
  # unfortunately, we can't use to_query because that's Rails, not Ruby
78
27
  # if no hash (e.g. no auth token) return empty string
79
28
  ((param_hash || {}).collect do |key_and_value|
80
- key_and_value[1] = key_and_value[1].to_json if key_and_value[1].class != String
29
+ key_and_value[1] = MultiJson.encode(key_and_value[1]) unless key_and_value[1].is_a? String
81
30
  "#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
82
31
  end).join("&")
83
32
  end
33
+
34
+ protected
84
35
 
85
- def self.encode_multipart_params(param_hash)
86
- Hash[*param_hash.collect do |key, value|
87
- [key, value.kind_of?(Koala::UploadableIO) ? value.to_upload_io : value]
88
- end.flatten]
89
- end
90
-
91
- def self.create_http(server, private_request, options)
92
- if options[:proxy]
93
- proxy = URI.parse(options[:proxy])
94
- http = Net::HTTP.new(server, private_request ? 443 : nil,
95
- proxy.host, proxy.port, proxy.user, proxy.password)
96
- else
97
- http = Net::HTTP.new(server, private_request ? 443 : nil)
98
- end
99
- if options[:timeout]
100
- http.open_timeout = options[:timeout]
101
- http.read_timeout = options[:timeout]
102
- end
103
- http
104
- end
105
-
106
- end
107
- end
108
- end
109
-
110
- module TyphoeusService
111
- # this service uses Typhoeus to send requests to the graph
112
-
113
- def self.included(base)
114
- base.class_eval do
115
- require "typhoeus" unless defined?(Typhoeus)
116
- include Typhoeus
117
-
118
- include Koala::HTTPService
119
-
120
- def self.make_request(path, args, verb, options = {})
121
- # if the verb isn't get or post, send it as a post argument
122
- args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
123
-
124
- # switch any UploadableIOs to the files Typhoeus expects
125
- args.each_pair {|key, value| args[key] = value.to_file if value.is_a?(UploadableIO)}
126
-
127
- # you can pass arguments directly to Typhoeus using the :typhoeus_options key
128
- typhoeus_options = {:params => args}.merge(options[:typhoeus_options] || {})
129
-
130
- # by default, we use SSL only for private requests (e.g. with access token)
131
- # this makes public requests faster
132
- prefix = (args["access_token"] || @always_use_ssl || options[:use_ssl]) ? "https" : "http"
133
-
134
- response = self.send(verb, "#{prefix}://#{server(options)}#{path}", typhoeus_options)
135
- Koala::Response.new(response.code, response.body, response.headers_hash)
36
+ def self.params_require_multipart?(param_hash)
37
+ param_hash.any? { |key, value| value.kind_of?(Koala::UploadableIO) }
136
38
  end
137
-
138
- private
139
-
39
+
140
40
  def self.multipart_requires_content_type?
141
- false # Typhoeus handles multipart file types, we don't have to require it
41
+ true
142
42
  end
143
- end # class_eval
43
+ end
144
44
  end
145
45
  end
146
46
  end
@@ -0,0 +1,181 @@
1
+ module Koala
2
+ module Facebook
3
+ class OAuth
4
+ attr_reader :app_id, :app_secret, :oauth_callback_url
5
+ def initialize(app_id, app_secret, oauth_callback_url = nil)
6
+ @app_id = app_id
7
+ @app_secret = app_secret
8
+ @oauth_callback_url = oauth_callback_url
9
+ end
10
+
11
+ def get_user_info_from_cookie(cookie_hash)
12
+ # Parses the cookie set by the official Facebook JavaScript SDK.
13
+ #
14
+ # cookies should be a Hash, like the one Rails provides
15
+ #
16
+ # If the user is logged in via Facebook, we return a dictionary with the
17
+ # keys "uid" and "access_token". The former is the user's Facebook ID,
18
+ # and the latter can be used to make authenticated requests to the Graph API.
19
+ # If the user is not logged in, we return None.
20
+ #
21
+ # Download the official Facebook JavaScript SDK at
22
+ # http://github.com/facebook/connect-js/. Read more about Facebook
23
+ # authentication at http://developers.facebook.com/docs/authentication/.
24
+
25
+ if fb_cookie = cookie_hash["fbs_" + @app_id.to_s]
26
+ # remove the opening/closing quote
27
+ fb_cookie = fb_cookie.gsub(/\"/, "")
28
+
29
+ # since we no longer get individual cookies, we have to separate out the components ourselves
30
+ components = {}
31
+ fb_cookie.split("&").map {|param| param = param.split("="); components[param[0]] = param[1]}
32
+
33
+ # generate the signature and make sure it matches what we expect
34
+ auth_string = components.keys.sort.collect {|a| a == "sig" ? nil : "#{a}=#{components[a]}"}.reject {|a| a.nil?}.join("")
35
+ sig = Digest::MD5.hexdigest(auth_string + @app_secret)
36
+ sig == components["sig"] && (components["expires"] == "0" || Time.now.to_i < components["expires"].to_i) ? components : nil
37
+ end
38
+ end
39
+ alias_method :get_user_info_from_cookies, :get_user_info_from_cookie
40
+
41
+ def get_user_from_cookie(cookies)
42
+ if info = get_user_info_from_cookies(cookies)
43
+ string = info["uid"]
44
+ end
45
+ end
46
+ alias_method :get_user_from_cookies, :get_user_from_cookie
47
+
48
+ # URLs
49
+
50
+ def url_for_oauth_code(options = {})
51
+ # for permissions, see http://developers.facebook.com/docs/authentication/permissions
52
+ permissions = options[:permissions]
53
+ scope = permissions ? "&scope=#{permissions.is_a?(Array) ? permissions.join(",") : permissions}" : ""
54
+ display = options.has_key?(:display) ? "&display=#{options[:display]}" : ""
55
+
56
+ callback = options[:callback] || @oauth_callback_url
57
+ raise ArgumentError, "url_for_oauth_code must get a callback either from the OAuth object or in the options!" unless callback
58
+
59
+ # Creates the URL for oauth authorization for a given callback and optional set of permissions
60
+ "https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}#{display}"
61
+ end
62
+
63
+ def url_for_access_token(code, options = {})
64
+ # Creates the URL for the token corresponding to a given code generated by Facebook
65
+ callback = options[:callback] || @oauth_callback_url
66
+ raise ArgumentError, "url_for_access_token must get a callback either from the OAuth object or in the parameters!" unless callback
67
+ "https://#{GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@app_secret}&code=#{code}"
68
+ end
69
+
70
+ def get_access_token_info(code, options = {})
71
+ # convenience method to get a parsed token from Facebook for a given code
72
+ # should this require an OAuth callback URL?
73
+ get_token_from_server({:code => code, :redirect_uri => @oauth_callback_url}, false, options)
74
+ end
75
+
76
+ def get_access_token(code, options = {})
77
+ # upstream methods will throw errors if needed
78
+ if info = get_access_token_info(code, options)
79
+ string = info["access_token"]
80
+ end
81
+ end
82
+
83
+ def get_app_access_token_info(options = {})
84
+ # convenience method to get a the application's sessionless access token
85
+ get_token_from_server({:type => 'client_cred'}, true, options)
86
+ end
87
+
88
+ def get_app_access_token(options = {})
89
+ if info = get_app_access_token_info(options)
90
+ string = info["access_token"]
91
+ end
92
+ end
93
+
94
+ # Originally provided directly by Facebook, however this has changed
95
+ # as their concept of crypto changed. For historic purposes, this is their proposal:
96
+ # https://developers.facebook.com/docs/authentication/canvas/encryption_proposal/
97
+ # Currently see https://github.com/facebook/php-sdk/blob/master/src/facebook.php#L758
98
+ # for a more accurate reference implementation strategy.
99
+ def parse_signed_request(input)
100
+ encoded_sig, encoded_envelope = input.split('.', 2)
101
+ signature = base64_url_decode(encoded_sig).unpack("H*").first
102
+ envelope = MultiJson.decode(base64_url_decode(encoded_envelope))
103
+
104
+ raise "SignedRequest: Unsupported algorithm #{envelope['algorithm']}" if envelope['algorithm'] != 'HMAC-SHA256'
105
+
106
+ # now see if the signature is valid (digest, key, data)
107
+ hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @app_secret, encoded_envelope.tr("-_", "+/"))
108
+ raise 'SignedRequest: Invalid signature' if (signature != hmac)
109
+
110
+ return envelope
111
+ end
112
+
113
+ # from session keys
114
+ def get_token_info_from_session_keys(sessions, options = {})
115
+ # fetch the OAuth tokens from Facebook
116
+ response = fetch_token_string({
117
+ :type => 'client_cred',
118
+ :sessions => sessions.join(",")
119
+ }, true, "exchange_sessions", options)
120
+
121
+ # Facebook returns an empty body in certain error conditions
122
+ if response == ""
123
+ raise APIError.new({
124
+ "type" => "ArgumentError",
125
+ "message" => "get_token_from_session_key received an error (empty response body) for sessions #{sessions.inspect}!"
126
+ })
127
+ end
128
+
129
+ MultiJson.decode(response)
130
+ end
131
+
132
+ def get_tokens_from_session_keys(sessions, options = {})
133
+ # get the original hash results
134
+ results = get_token_info_from_session_keys(sessions, options)
135
+ # now recollect them as just the access tokens
136
+ results.collect { |r| r ? r["access_token"] : nil }
137
+ end
138
+
139
+ def get_token_from_session_key(session, options = {})
140
+ # convenience method for a single key
141
+ # gets the overlaoded strings automatically
142
+ get_tokens_from_session_keys([session], options)[0]
143
+ end
144
+
145
+ protected
146
+
147
+ def get_token_from_server(args, post = false, options = {})
148
+ # fetch the result from Facebook's servers
149
+ result = fetch_token_string(args, post, "access_token", options)
150
+
151
+ # if we have an error, parse the error JSON and raise an error
152
+ raise APIError.new((MultiJson.decode(result)["error"] rescue nil) || {}) if result =~ /error/
153
+
154
+ # otherwise, parse the access token
155
+ parse_access_token(result)
156
+ end
157
+
158
+ def parse_access_token(response_text)
159
+ components = response_text.split("&").inject({}) do |hash, bit|
160
+ key, value = bit.split("=")
161
+ hash.merge!(key => value)
162
+ end
163
+ components
164
+ end
165
+
166
+ def fetch_token_string(args, post = false, endpoint = "access_token", options = {})
167
+ Koala.make_request("/oauth/#{endpoint}", {
168
+ :client_id => @app_id,
169
+ :client_secret => @app_secret
170
+ }.merge!(args), post ? "post" : "get", {:use_ssl => true}.merge!(options)).body
171
+ end
172
+
173
+ # base 64
174
+ # directly from https://github.com/facebook/crypto-request-examples/raw/master/sample.rb
175
+ def base64_url_decode(str)
176
+ str += '=' * (4 - str.length.modulo(4))
177
+ Base64.decode64(str.tr('-_', '+/'))
178
+ end
179
+ end
180
+ end
181
+ end
@@ -46,6 +46,8 @@ module Koala
46
46
  oauth = Koala::Facebook::OAuth.new(@app_id, @secret)
47
47
  @app_access_token = oauth.get_app_access_token
48
48
  end
49
+
50
+ @graph_api = GraphAPI.new(@app_access_token)
49
51
  end
50
52
 
51
53
  # subscribes for realtime updates
@@ -59,7 +61,7 @@ module Koala
59
61
  :verify_token => verify_token
60
62
  }
61
63
  # a subscription is a success if Facebook returns a 200 (after hitting your server for verification)
62
- api(subscription_path, args, 'post', :http_component => :status) == 200
64
+ @graph_api.graph_call(subscription_path, args, 'post', :http_component => :status) == 200
63
65
  end
64
66
 
65
67
  # removes subscription for object
@@ -67,24 +69,13 @@ module Koala
67
69
  def unsubscribe(object = nil)
68
70
  args = {}
69
71
  args[:object] = object if object
70
- api(subscription_path, args, 'delete', :http_component => :status) == 200
72
+ @graph_api.graph_call(subscription_path, args, 'delete', :http_component => :status) == 200
71
73
  end
72
74
 
73
75
  def list_subscriptions
74
- api(subscription_path)["data"]
76
+ @graph_api.graph_call(subscription_path)["data"]
75
77
  end
76
78
 
77
- def api(*args) # same as GraphAPI
78
- response = super(*args) do |response|
79
- # check for subscription errors
80
- if response.is_a?(Hash) && error_details = response["error"]
81
- raise APIError.new(error_details)
82
- end
83
- end
84
-
85
- response
86
- end
87
-
88
79
  protected
89
80
 
90
81
  def subscription_path
@@ -3,21 +3,26 @@ module Koala
3
3
  REST_SERVER = "api.facebook.com"
4
4
 
5
5
  module RestAPIMethods
6
- def fql_query(fql)
7
- rest_call('fql.query', 'query' => fql)
6
+ def fql_query(fql, args = {}, options = {})
7
+ rest_call('fql.query', args.merge(:query => fql), options)
8
8
  end
9
9
 
10
- def rest_call(method, args = {}, options = {})
11
- options = options.merge!(:rest_api => true, :read_only => READ_ONLY_METHODS.include?(method))
10
+ def fql_multiquery(queries = {}, args = {}, options = {})
11
+ if results = rest_call('fql.multiquery', args.merge(:queries => MultiJson.encode(queries)), options)
12
+ # simplify the multiquery result format
13
+ results.inject({}) {|outcome, data| outcome[data["name"]] = data["fql_result_set"]; outcome}
14
+ end
15
+ end
12
16
 
13
- response = api("method/#{method}", args.merge('format' => 'json'), 'get', options) do |response|
17
+ def rest_call(fb_method, args = {}, options = {}, method = "get")
18
+ options = options.merge!(:rest_api => true, :read_only => READ_ONLY_METHODS.include?(fb_method.to_s))
19
+
20
+ api("method/#{fb_method}", args.merge('format' => 'json'), method, options) do |response|
14
21
  # check for REST API-specific errors
15
22
  if response.is_a?(Hash) && response["error_code"]
16
23
  raise APIError.new("type" => response["error_code"], "message" => response["error_msg"])
17
24
  end
18
25
  end
19
-
20
- response
21
26
  end
22
27
 
23
28
  # read-only methods for which we can use API-read
@@ -87,4 +92,4 @@ module Koala
87
92
  end
88
93
 
89
94
  end # module Facebook
90
- end # module Koala
95
+ end # module Koala