koala 1.0.0 → 1.1.0rc2

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.
@@ -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 = JSON.parse(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
+ JSON.parse(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((JSON.parse(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 => queries.to_json), 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
@@ -2,9 +2,9 @@ require 'koala'
2
2
 
3
3
  module Koala
4
4
  class UploadableIO
5
- attr_reader :io_or_path, :content_type
5
+ attr_reader :io_or_path, :content_type, :requires_base_http_service
6
6
 
7
- def initialize(io_or_path_or_mixed, content_type = nil)
7
+ def initialize(io_or_path_or_mixed, content_type = nil, filename = nil)
8
8
  # see if we got the right inputs
9
9
  if content_type.nil?
10
10
  parse_init_mixed_param io_or_path_or_mixed
@@ -13,19 +13,35 @@ module Koala
13
13
  @content_type = content_type
14
14
  end
15
15
 
16
+ # Probably a StringIO or similar object, which won't work with Typhoeus
17
+ @requires_base_http_service = @io_or_path.respond_to?(:read) && !@io_or_path.kind_of?(File)
18
+
19
+ # filename is used in the Ads API
20
+ @filename = filename || "koala-io-file.dum"
21
+
16
22
  raise KoalaError.new("Invalid arguments to initialize an UploadableIO") unless @io_or_path
17
23
  raise KoalaError.new("Unable to determine MIME type for UploadableIO") if !@content_type && Koala.multipart_requires_content_type?
18
24
  end
19
25
 
20
26
  def to_upload_io
21
- UploadIO.new(@io_or_path, @content_type, "koala-io-file.dum")
27
+ UploadIO.new(@io_or_path, @content_type, @filename)
22
28
  end
23
29
 
24
30
  def to_file
25
31
  @io_or_path.is_a?(String) ? File.open(@io_or_path) : @io_or_path
26
32
  end
33
+
34
+ def self.binary_content?(content)
35
+ content.is_a?(UploadableIO) || DETECTION_STRATEGIES.detect {|method| send(method, content)}
36
+ end
27
37
 
28
38
  private
39
+ DETECTION_STRATEGIES = [
40
+ :sinatra_param?,
41
+ :rails_3_param?,
42
+ :file_param?
43
+ ]
44
+
29
45
  PARSE_STRATEGIES = [
30
46
  :parse_rails_3_param,
31
47
  :parse_sinatra_param,
@@ -41,24 +57,36 @@ module Koala
41
57
  end
42
58
 
43
59
  # Expects a parameter of type ActionDispatch::Http::UploadedFile
60
+ def self.rails_3_param?(uploaded_file)
61
+ uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
62
+ end
63
+
44
64
  def parse_rails_3_param(uploaded_file)
45
- if uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
65
+ if UploadableIO.rails_3_param?(uploaded_file)
46
66
  @io_or_path = uploaded_file.tempfile.path
47
67
  @content_type = uploaded_file.content_type
48
68
  end
49
69
  end
50
70
 
51
71
  # Expects a Sinatra hash of file info
72
+ def self.sinatra_param?(file_hash)
73
+ file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
74
+ end
75
+
52
76
  def parse_sinatra_param(file_hash)
53
- if file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
77
+ if UploadableIO.sinatra_param?(file_hash)
54
78
  @io_or_path = file_hash[:tempfile]
55
79
  @content_type = file_hash[:type] || detect_mime_type(tempfile)
56
80
  end
57
81
  end
58
82
 
59
83
  # takes a file object
84
+ def self.file_param?(file)
85
+ file.kind_of?(File)
86
+ end
87
+
60
88
  def parse_file_object(file)
61
- if file.kind_of?(File)
89
+ if UploadableIO.file_param?(file)
62
90
  @io_or_path = file
63
91
  @content_type = detect_mime_type(file.path)
64
92
  end
@@ -112,4 +140,4 @@ module Koala
112
140
  end
113
141
  end
114
142
  end
115
- end
143
+ 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