koala 1.0.0 → 1.2.0beta1

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 (52) hide show
  1. data/.autotest +12 -0
  2. data/.gitignore +3 -1
  3. data/.travis.yml +9 -0
  4. data/CHANGELOG +52 -2
  5. data/Gemfile +8 -0
  6. data/Rakefile +0 -1
  7. data/autotest/discover.rb +1 -0
  8. data/koala.gemspec +14 -14
  9. data/lib/koala/batch_operation.rb +74 -0
  10. data/lib/koala/graph_api.rb +157 -133
  11. data/lib/koala/graph_batch_api.rb +87 -0
  12. data/lib/koala/graph_collection.rb +54 -0
  13. data/lib/koala/http_service.rb +177 -0
  14. data/lib/koala/oauth.rb +181 -0
  15. data/lib/koala/realtime_updates.rb +23 -29
  16. data/lib/koala/rest_api.rb +13 -8
  17. data/lib/koala/test_users.rb +33 -16
  18. data/lib/koala/uploadable_io.rb +152 -87
  19. data/lib/koala/utils.rb +11 -0
  20. data/lib/koala.rb +54 -217
  21. data/readme.md +71 -52
  22. data/spec/cases/api_base_spec.rb +6 -6
  23. data/spec/cases/error_spec.rb +32 -0
  24. data/spec/cases/graph_and_rest_api_spec.rb +20 -3
  25. data/spec/cases/graph_api_batch_spec.rb +600 -0
  26. data/spec/cases/graph_api_spec.rb +21 -4
  27. data/spec/cases/http_service_spec.rb +446 -0
  28. data/spec/cases/koala_spec.rb +50 -0
  29. data/spec/cases/oauth_spec.rb +220 -201
  30. data/spec/cases/realtime_updates_spec.rb +45 -31
  31. data/spec/cases/rest_api_spec.rb +23 -7
  32. data/spec/cases/test_users_spec.rb +112 -52
  33. data/spec/cases/uploadable_io_spec.rb +92 -37
  34. data/spec/cases/utils_spec.rb +10 -0
  35. data/spec/fixtures/cat.m4v +0 -0
  36. data/spec/fixtures/facebook_data.yml +23 -22
  37. data/spec/fixtures/mock_facebook_responses.yml +201 -76
  38. data/spec/spec_helper.rb +30 -5
  39. data/spec/support/graph_api_shared_examples.rb +134 -56
  40. data/spec/support/json_testing_fix.rb +42 -0
  41. data/spec/support/koala_test.rb +163 -0
  42. data/spec/support/mock_http_service.rb +60 -57
  43. data/spec/support/ordered_hash.rb +205 -0
  44. data/spec/support/rest_api_shared_examples.rb +139 -15
  45. data/spec/support/uploadable_io_shared_examples.rb +2 -8
  46. metadata +98 -112
  47. data/lib/koala/http_services.rb +0 -146
  48. data/spec/cases/http_services/http_service_spec.rb +0 -54
  49. data/spec/cases/http_services/net_http_service_spec.rb +0 -350
  50. data/spec/cases/http_services/typhoeus_service_spec.rb +0 -144
  51. data/spec/support/live_testing_data_helper.rb +0 -40
  52. data/spec/support/setup_mocks_or_live.rb +0 -52
@@ -0,0 +1,87 @@
1
+ module Koala
2
+ module Facebook
3
+ module GraphBatchAPIMethods
4
+
5
+ def self.included(base)
6
+ base.class_eval do
7
+ alias_method :graph_call_outside_batch, :graph_call
8
+ alias_method :graph_call, :graph_call_in_batch
9
+
10
+ alias_method :check_graph_api_response, :check_response
11
+ alias_method :check_response, :check_graph_batch_api_response
12
+ end
13
+ end
14
+
15
+ def batch_calls
16
+ @batch_calls ||= []
17
+ end
18
+
19
+ def graph_call_in_batch(path, args = {}, verb = "get", options = {}, &post_processing)
20
+ # for batch APIs, we queue up the call details (incl. post-processing)
21
+ batch_calls << BatchOperation.new(
22
+ :url => path,
23
+ :args => args,
24
+ :method => verb,
25
+ :access_token => options['access_token'] || access_token,
26
+ :http_options => options,
27
+ :post_processing => post_processing
28
+ )
29
+ nil # batch operations return nothing immediately
30
+ end
31
+
32
+ def check_graph_batch_api_response(response)
33
+ if response.is_a?(Hash) && response["error"] && !response["error"].is_a?(Hash)
34
+ APIError.new("type" => "Error #{response["error"]}", "message" => response["error_description"])
35
+ else
36
+ check_graph_api_response(response)
37
+ end
38
+ end
39
+
40
+ def execute(http_options = {})
41
+ return [] unless batch_calls.length > 0
42
+ # Turn the call args collected into what facebook expects
43
+ args = {}
44
+ args["batch"] = MultiJson.encode(batch_calls.map { |batch_op|
45
+ args.merge!(batch_op.files) if batch_op.files
46
+ batch_op.to_batch_params(access_token)
47
+ })
48
+
49
+ graph_call_outside_batch('/', args, 'post', http_options) do |response|
50
+ # map the results with post-processing included
51
+ index = 0 # keep compat with ruby 1.8 - no with_index for map
52
+ response.map do |call_result|
53
+ # Get the options hash
54
+ batch_op = batch_calls[index]
55
+ index += 1
56
+
57
+ if call_result
58
+ # (see note in regular api method about JSON parsing)
59
+ body = MultiJson.decode("[#{call_result['body'].to_s}]")[0]
60
+
61
+ unless call_result["code"].to_i >= 500 || error = check_response(body)
62
+ # Get the HTTP component they want
63
+ data = case batch_op.http_options[:http_component]
64
+ when :status
65
+ call_result["code"].to_i
66
+ when :headers
67
+ # facebook returns the headers as an array of k/v pairs, but we want a regular hash
68
+ call_result['headers'].inject({}) { |headers, h| headers[h['name']] = h['value']; headers}
69
+ else
70
+ body
71
+ end
72
+
73
+ # process it if we are given a block to process with
74
+ batch_op.post_processing ? batch_op.post_processing.call(data) : data
75
+ else
76
+ error || APIError.new({"type" => "HTTP #{call_result["code"].to_s}", "message" => "Response body: #{body}"})
77
+ end
78
+ else
79
+ nil
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,54 @@
1
+ module Koala
2
+ module Facebook
3
+ class GraphCollection < Array
4
+ # This class is a light wrapper for collections returned
5
+ # from the Graph API.
6
+ #
7
+ # It extends Array to allow direct access to the data colleciton
8
+ # which should allow it to drop in seamlessly.
9
+ #
10
+ # It also allows access to paging information and the
11
+ # ability to get the next/previous page in the collection
12
+ # by calling next_page or previous_page.
13
+ attr_reader :paging
14
+ attr_reader :api
15
+
16
+ def initialize(response, api)
17
+ super response["data"]
18
+ @paging = response["paging"]
19
+ @api = api
20
+ end
21
+
22
+ # defines methods for NEXT and PREVIOUS pages
23
+ %w{next previous}.each do |this|
24
+
25
+ # def next_page
26
+ # def previous_page
27
+ define_method "#{this.to_sym}_page" do
28
+ base, args = send("#{this}_page_params")
29
+ base ? @api.get_page([base, args]) : nil
30
+ end
31
+
32
+ # def next_page_params
33
+ # def previous_page_params
34
+ define_method "#{this.to_sym}_page_params" do
35
+ return nil unless @paging and @paging[this]
36
+ parse_page_url(@paging[this])
37
+ end
38
+ end
39
+
40
+ def parse_page_url(url)
41
+ match = url.match(/.com\/(.*)\?(.*)/)
42
+ base = match[1]
43
+ args = match[2]
44
+ params = CGI.parse(args)
45
+ new_params = {}
46
+ params.each_pair do |key,value|
47
+ new_params[key] = value.join ","
48
+ end
49
+ [base,new_params]
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,177 @@
1
+ require 'faraday'
2
+ require 'faraday_stack'
3
+
4
+ module Koala
5
+ class Response
6
+ attr_reader :status, :body, :headers
7
+ def initialize(status, body, headers)
8
+ @status = status
9
+ @body = body
10
+ @headers = headers
11
+ end
12
+ end
13
+
14
+ module HTTPService
15
+ # common functionality for all HTTP services
16
+
17
+ class << self
18
+ attr_accessor :faraday_middleware, :http_options
19
+ end
20
+
21
+ @http_options ||= {}
22
+
23
+ DEFAULT_MIDDLEWARE = Proc.new do |builder|
24
+ builder.request :multipart
25
+ builder.request :url_encoded
26
+ builder.adapter Faraday.default_adapter
27
+ end
28
+
29
+ def self.server(options = {})
30
+ server = "#{options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER}"
31
+ server.gsub!(/\.facebook/, "-video.facebook") if options[:video]
32
+ "#{options[:use_ssl] ? "https" : "http"}://#{options[:beta] ? "beta." : ""}#{server}"
33
+ end
34
+
35
+ def self.make_request(path, args, verb, options = {})
36
+ # if the verb isn't get or post, send it as a post argument
37
+ args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
38
+
39
+ # turn all the keys to strings (Faraday has issues with symbols under 1.8.7) and resolve UploadableIOs
40
+ params = args.inject({}) {|hash, kv| hash[kv.first.to_s] = kv.last.is_a?(UploadableIO) ? kv.last.to_upload_io : kv.last; hash}
41
+
42
+ # figure out our options for this request
43
+ request_options = {:params => (verb == "get" ? params : {})}.merge(http_options || {}).merge(process_options(options))
44
+ request_options[:use_ssl] = true if args["access_token"] # require http if there's a token
45
+
46
+ # set up our Faraday connection
47
+ # we have to manually assign params to the URL or the
48
+ conn = Faraday.new(server(request_options), request_options, &(faraday_middleware || DEFAULT_MIDDLEWARE))
49
+
50
+ response = conn.send(verb, path, (verb == "post" ? params : {}))
51
+ Koala::Response.new(response.status.to_i, response.body, response.headers)
52
+ end
53
+
54
+ def self.encode_params(param_hash)
55
+ # unfortunately, we can't use to_query because that's Rails, not Ruby
56
+ # if no hash (e.g. no auth token) return empty string
57
+ # this is used mainly by the Batch API nowadays
58
+ ((param_hash || {}).collect do |key_and_value|
59
+ key_and_value[1] = MultiJson.encode(key_and_value[1]) unless key_and_value[1].is_a? String
60
+ "#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
61
+ end).join("&")
62
+ end
63
+
64
+ # deprecations
65
+ # not elegant or compact code, but temporary
66
+
67
+ def self.always_use_ssl
68
+ Koala::Utils.deprecate("HTTPService.always_use_ssl is now HTTPService.http_options[:use_ssl]; always_use_ssl will be removed in a future version.")
69
+ http_options[:use_ssl]
70
+ end
71
+
72
+ def self.always_use_ssl=(value)
73
+ Koala::Utils.deprecate("HTTPService.always_use_ssl is now HTTPService.http_options[:use_ssl]; always_use_ssl will be removed in a future version.")
74
+ http_options[:use_ssl] = value
75
+ end
76
+
77
+ def self.timeout
78
+ Koala::Utils.deprecate("HTTPService.timeout is now HTTPService.http_options[:timeout]; .timeout will be removed in a future version.")
79
+ http_options[:timeout]
80
+ end
81
+
82
+ def self.timeout=(value)
83
+ Koala::Utils.deprecate("HTTPService.timeout is now HTTPService.http_options[:timeout]; .timeout will be removed in a future version.")
84
+ http_options[:timeout] = value
85
+ end
86
+
87
+ def self.timeout
88
+ Koala::Utils.deprecate("HTTPService.timeout is now HTTPService.http_options[:timeout]; .timeout will be removed in a future version.")
89
+ http_options[:timeout]
90
+ end
91
+
92
+ def self.timeout=(value)
93
+ Koala::Utils.deprecate("HTTPService.timeout is now HTTPService.http_options[:timeout]; .timeout will be removed in a future version.")
94
+ http_options[:timeout] = value
95
+ end
96
+
97
+ def self.proxy
98
+ Koala::Utils.deprecate("HTTPService.proxy is now HTTPService.http_options[:proxy]; .proxy will be removed in a future version.")
99
+ http_options[:proxy]
100
+ end
101
+
102
+ def self.proxy=(value)
103
+ Koala::Utils.deprecate("HTTPService.proxy is now HTTPService.http_options[:proxy]; .proxy will be removed in a future version.")
104
+ http_options[:proxy] = value
105
+ end
106
+
107
+ def self.ca_path
108
+ Koala::Utils.deprecate("HTTPService.ca_path is now (HTTPService.http_options[:ssl] ||= {})[:ca_path]; .ca_path will be removed in a future version.")
109
+ (http_options[:ssl] || {})[:ca_path]
110
+ end
111
+
112
+ def self.ca_path=(value)
113
+ Koala::Utils.deprecate("HTTPService.ca_path is now (HTTPService.http_options[:ssl] ||= {})[:ca_path]; .ca_path will be removed in a future version.")
114
+ (http_options[:ssl] ||= {})[:ca_path] = value
115
+ end
116
+
117
+ def self.ca_file
118
+ Koala::Utils.deprecate("HTTPService.ca_file is now (HTTPService.http_options[:ssl] ||= {})[:ca_file]; .ca_file will be removed in a future version.")
119
+ (http_options[:ssl] || {})[:ca_file]
120
+ end
121
+
122
+ def self.ca_file=(value)
123
+ Koala::Utils.deprecate("HTTPService.ca_file is now (HTTPService.http_options[:ssl] ||= {})[:ca_file]; .ca_file will be removed in a future version.")
124
+ (http_options[:ssl] ||= {})[:ca_file] = value
125
+ end
126
+
127
+ def self.verify_mode
128
+ Koala::Utils.deprecate("HTTPService.verify_mode is now (HTTPService.http_options[:ssl] ||= {})[:verify_mode]; .verify_mode will be removed in a future version.")
129
+ (http_options[:ssl] || {})[:verify_mode]
130
+ end
131
+
132
+ def self.verify_mode=(value)
133
+ Koala::Utils.deprecate("HTTPService.verify_mode is now (HTTPService.http_options[:ssl] ||= {})[:verify_mode]; .verify_mode will be removed in a future version.")
134
+ (http_options[:ssl] ||= {})[:verify_mode] = value
135
+ end
136
+
137
+ def self.process_options(options)
138
+ if typhoeus_options = options.delete(:typhoeus_options)
139
+ Koala::Utils.deprecate("typhoeus_options should now be included directly in the http_options hash. Support for this key will be removed in a future version.")
140
+ options = options.merge(typhoeus_options)
141
+ end
142
+
143
+ if ca_file = options.delete(:ca_file)
144
+ Koala::Utils.deprecate("http_options[:ca_file] should now be passed inside (http_options[:ssl] = {}) -- that is, http_options[:ssl][:ca_file]. Support for this key will be removed in a future version.")
145
+ (options[:ssl] ||= {})[:ca_file] = ca_file
146
+ end
147
+
148
+ if ca_path = options.delete(:ca_path)
149
+ Koala::Utils.deprecate("http_options[:ca_path] should now be passed inside (http_options[:ssl] = {}) -- that is, http_options[:ssl][:ca_path]. Support for this key will be removed in a future version.")
150
+ (options[:ssl] ||= {})[:ca_path] = ca_path
151
+ end
152
+
153
+ if verify_mode = options.delete(:verify_mode)
154
+ Koala::Utils.deprecate("http_options[:verify_mode] should now be passed inside (http_options[:ssl] = {}) -- that is, http_options[:ssl][:verify_mode]. Support for this key will be removed in a future version.")
155
+ (options[:ssl] ||= {})[:verify_mode] = verify_mode
156
+ end
157
+
158
+ options
159
+ end
160
+ end
161
+
162
+ module TyphoeusService
163
+ def self.deprecated_interface
164
+ # support old-style interface with a warning
165
+ Koala::Utils.deprecate("the TyphoeusService module is deprecated; to use Typhoeus, set Faraday.default_adapter = :typhoeus. Enabling Typhoeus for all Faraday connections.")
166
+ Faraday.default_adapter = :typhoeus
167
+ end
168
+ end
169
+
170
+ module NetHTTPService
171
+ def self.deprecated_interface
172
+ # support old-style interface with a warning
173
+ Koala::Utils.deprecate("the NetHTTPService module is deprecated; to use Net::HTTP, set Faraday.default_adapter = :net_http. Enabling Net::HTTP for all Faraday connections.")
174
+ Faraday.default_adapter = :net_http
175
+ end
176
+ end
177
+ 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
@@ -1,15 +1,13 @@
1
- require 'koala'
2
-
3
1
  module Koala
4
2
  module Facebook
5
3
  module RealtimeUpdateMethods
6
4
  # note: to subscribe to real-time updates, you must have an application access token
7
-
5
+
8
6
  def self.included(base)
9
7
  # make the attributes readable
10
8
  base.class_eval do
11
- attr_reader :app_id, :app_access_token, :secret
12
-
9
+ attr_reader :api, :app_id, :app_access_token, :secret
10
+
13
11
  # parses the challenge params and makes sure the call is legitimate
14
12
  # returns the challenge string to be sent back to facebook if true
15
13
  # returns false otherwise
@@ -20,7 +18,7 @@ module Koala
20
18
  # you can make sure this is legitimate through two ways
21
19
  # if your store the token across the calls, you can pass in the token value
22
20
  # and we'll make sure it matches
23
- (verify_token && params["hub.verify_token"] == verify_token) ||
21
+ (verify_token && params["hub.verify_token"] == verify_token) ||
24
22
  # alternately, if you sent a specially-constructed value (such as a hash of various secret values)
25
23
  # you can pass in a block, which we'll call with the verify_token sent by Facebook
26
24
  # if it's legit, return anything that evaluates to true; otherwise, return nil or false
@@ -32,7 +30,7 @@ module Koala
32
30
  end
33
31
  end
34
32
  end
35
-
33
+
36
34
  def initialize(options = {})
37
35
  @app_id = options[:app_id]
38
36
  @app_access_token = options[:app_access_token]
@@ -40,56 +38,52 @@ module Koala
40
38
  unless @app_id && (@app_access_token || @secret) # make sure we have what we need
41
39
  raise ArgumentError, "Initialize must receive a hash with :app_id and either :app_access_token or :secret! (received #{options.inspect})"
42
40
  end
43
-
41
+
44
42
  # fetch the access token if we're provided a secret
45
43
  if @secret && !@app_access_token
46
44
  oauth = Koala::Facebook::OAuth.new(@app_id, @secret)
47
45
  @app_access_token = oauth.get_app_access_token
48
46
  end
47
+
48
+ @graph_api = API.new(@app_access_token)
49
49
  end
50
-
50
+
51
51
  # subscribes for realtime updates
52
52
  # your callback_url must be set up to handle the verification request or the subscription will not be set up
53
53
  # http://developers.facebook.com/docs/api/realtime
54
54
  def subscribe(object, fields, callback_url, verify_token)
55
55
  args = {
56
- :object => object,
56
+ :object => object,
57
57
  :fields => fields,
58
58
  :callback_url => callback_url,
59
59
  :verify_token => verify_token
60
60
  }
61
61
  # 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
62
+ @graph_api.graph_call(subscription_path, args, 'post', :http_component => :status) == 200
63
63
  end
64
-
64
+
65
65
  # removes subscription for object
66
66
  # if object is nil, it will remove all subscriptions
67
67
  def unsubscribe(object = nil)
68
68
  args = {}
69
69
  args[:object] = object if object
70
- api(subscription_path, args, 'delete', :http_component => :status) == 200
70
+ @graph_api.graph_call(subscription_path, args, 'delete', :http_component => :status) == 200
71
71
  end
72
-
72
+
73
73
  def list_subscriptions
74
- api(subscription_path)["data"]
74
+ @graph_api.graph_call(subscription_path)["data"]
75
75
  end
76
-
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
-
76
+
77
+ def graph_api
78
+ Koala::Utils.deprecate("the TestUsers.graph_api accessor is deprecated and will be removed in a future version; please use .api instead.")
79
+ @api
80
+ end
81
+
88
82
  protected
89
-
83
+
90
84
  def subscription_path
91
85
  @subscription_path ||= "#{@app_id}/subscriptions"
92
86
  end
93
87
  end
94
88
  end
95
- end
89
+ end
@@ -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