koala 1.0.0 → 1.2.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 (54) hide show
  1. data/.autotest +12 -0
  2. data/.gitignore +3 -1
  3. data/.travis.yml +9 -0
  4. data/CHANGELOG +62 -2
  5. data/Gemfile +8 -0
  6. data/Rakefile +0 -1
  7. data/autotest/discover.rb +1 -0
  8. data/koala.gemspec +13 -14
  9. data/lib/koala/batch_operation.rb +74 -0
  10. data/lib/koala/graph_api.rb +145 -132
  11. data/lib/koala/graph_batch_api.rb +97 -0
  12. data/lib/koala/graph_collection.rb +59 -0
  13. data/lib/koala/http_service.rb +176 -0
  14. data/lib/koala/oauth.rb +191 -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 -17
  18. data/lib/koala/uploadable_io.rb +153 -87
  19. data/lib/koala/utils.rb +11 -0
  20. data/lib/koala/version.rb +3 -0
  21. data/lib/koala.rb +59 -217
  22. data/readme.md +92 -53
  23. data/spec/cases/{api_base_spec.rb → api_spec.rb} +31 -6
  24. data/spec/cases/error_spec.rb +32 -0
  25. data/spec/cases/graph_and_rest_api_spec.rb +12 -21
  26. data/spec/cases/graph_api_batch_spec.rb +582 -0
  27. data/spec/cases/graph_api_spec.rb +11 -14
  28. data/spec/cases/graph_collection_spec.rb +116 -0
  29. data/spec/cases/http_service_spec.rb +446 -0
  30. data/spec/cases/koala_spec.rb +54 -0
  31. data/spec/cases/oauth_spec.rb +319 -213
  32. data/spec/cases/realtime_updates_spec.rb +45 -31
  33. data/spec/cases/rest_api_spec.rb +23 -7
  34. data/spec/cases/test_users_spec.rb +123 -75
  35. data/spec/cases/uploadable_io_spec.rb +120 -37
  36. data/spec/cases/utils_spec.rb +10 -0
  37. data/spec/fixtures/cat.m4v +0 -0
  38. data/spec/fixtures/facebook_data.yml +26 -24
  39. data/spec/fixtures/mock_facebook_responses.yml +203 -78
  40. data/spec/spec_helper.rb +30 -5
  41. data/spec/support/graph_api_shared_examples.rb +149 -118
  42. data/spec/support/json_testing_fix.rb +42 -0
  43. data/spec/support/koala_test.rb +187 -0
  44. data/spec/support/mock_http_service.rb +62 -58
  45. data/spec/support/ordered_hash.rb +205 -0
  46. data/spec/support/rest_api_shared_examples.rb +139 -15
  47. data/spec/support/uploadable_io_shared_examples.rb +2 -8
  48. metadata +90 -114
  49. data/lib/koala/http_services.rb +0 -146
  50. data/spec/cases/http_services/http_service_spec.rb +0 -54
  51. data/spec/cases/http_services/net_http_service_spec.rb +0 -350
  52. data/spec/cases/http_services/typhoeus_service_spec.rb +0 -144
  53. data/spec/support/live_testing_data_helper.rb +0 -40
  54. data/spec/support/setup_mocks_or_live.rb +0 -52
@@ -0,0 +1,97 @@
1
+ module Koala
2
+ module Facebook
3
+ module GraphBatchAPIMethods
4
+
5
+ def self.included(base)
6
+ base.class_eval do
7
+ attr_reader :original_api
8
+
9
+ def initialize(access_token, api)
10
+ super(access_token)
11
+ @original_api = api
12
+ end
13
+
14
+ alias_method :graph_call_outside_batch, :graph_call
15
+ alias_method :graph_call, :graph_call_in_batch
16
+
17
+ alias_method :check_graph_api_response, :check_response
18
+ alias_method :check_response, :check_graph_batch_api_response
19
+ end
20
+ end
21
+
22
+ def batch_calls
23
+ @batch_calls ||= []
24
+ end
25
+
26
+ def graph_call_in_batch(path, args = {}, verb = "get", options = {}, &post_processing)
27
+ # for batch APIs, we queue up the call details (incl. post-processing)
28
+ batch_calls << BatchOperation.new(
29
+ :url => path,
30
+ :args => args,
31
+ :method => verb,
32
+ :access_token => options['access_token'] || access_token,
33
+ :http_options => options,
34
+ :post_processing => post_processing
35
+ )
36
+ nil # batch operations return nothing immediately
37
+ end
38
+
39
+ def check_graph_batch_api_response(response)
40
+ if response.is_a?(Hash) && response["error"] && !response["error"].is_a?(Hash)
41
+ APIError.new("type" => "Error #{response["error"]}", "message" => response["error_description"])
42
+ else
43
+ check_graph_api_response(response)
44
+ end
45
+ end
46
+
47
+ def execute(http_options = {})
48
+ return [] unless batch_calls.length > 0
49
+ # Turn the call args collected into what facebook expects
50
+ args = {}
51
+ args["batch"] = MultiJson.encode(batch_calls.map { |batch_op|
52
+ args.merge!(batch_op.files) if batch_op.files
53
+ batch_op.to_batch_params(access_token)
54
+ })
55
+
56
+ batch_result = graph_call_outside_batch('/', args, 'post', http_options) do |response|
57
+ # map the results with post-processing included
58
+ index = 0 # keep compat with ruby 1.8 - no with_index for map
59
+ response.map do |call_result|
60
+ # Get the options hash
61
+ batch_op = batch_calls[index]
62
+ index += 1
63
+
64
+ if call_result
65
+ # (see note in regular api method about JSON parsing)
66
+ body = MultiJson.decode("[#{call_result['body'].to_s}]")[0]
67
+
68
+ unless call_result["code"].to_i >= 500 || error = check_response(body)
69
+ # Get the HTTP component they want
70
+ data = case batch_op.http_options[:http_component]
71
+ when :status
72
+ call_result["code"].to_i
73
+ when :headers
74
+ # facebook returns the headers as an array of k/v pairs, but we want a regular hash
75
+ call_result['headers'].inject({}) { |headers, h| headers[h['name']] = h['value']; headers}
76
+ else
77
+ body
78
+ end
79
+
80
+ # process it if we are given a block to process with
81
+ batch_op.post_processing ? batch_op.post_processing.call(data) : data
82
+ else
83
+ error || APIError.new({"type" => "HTTP #{call_result["code"].to_s}", "message" => "Response body: #{body}"})
84
+ end
85
+ else
86
+ nil
87
+ end
88
+ end
89
+ end
90
+
91
+ # turn any results that are pageable into GraphCollections
92
+ batch_result.inject([]) {|processed_results, raw| processed_results << GraphCollection.evaluate(raw, @original_api)}
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,59 @@
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, :api, :raw_response
14
+
15
+ def self.evaluate(response, api)
16
+ # turn the response into a GraphCollection if it's pageable; if not, return the original response
17
+ response.is_a?(Hash) && response["data"].is_a?(Array) ? self.new(response, api) : response
18
+ end
19
+
20
+ def initialize(response, api)
21
+ super response["data"]
22
+ @paging = response["paging"]
23
+ @raw_response = response
24
+ @api = api
25
+ end
26
+
27
+ # defines methods for NEXT and PREVIOUS pages
28
+ %w{next previous}.each do |this|
29
+
30
+ # def next_page
31
+ # def previous_page
32
+ define_method "#{this.to_sym}_page" do
33
+ base, args = send("#{this}_page_params")
34
+ base ? @api.get_page([base, args]) : nil
35
+ end
36
+
37
+ # def next_page_params
38
+ # def previous_page_params
39
+ define_method "#{this.to_sym}_page_params" do
40
+ return nil unless @paging and @paging[this]
41
+ parse_page_url(@paging[this])
42
+ end
43
+ end
44
+
45
+ def parse_page_url(url)
46
+ match = url.match(/.com\/(.*)\?(.*)/)
47
+ base = match[1]
48
+ args = match[2]
49
+ params = CGI.parse(args)
50
+ new_params = {}
51
+ params.each_pair do |key,value|
52
+ new_params[key] = value.join ","
53
+ end
54
+ [base,new_params]
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,176 @@
1
+ require 'faraday'
2
+
3
+ module Koala
4
+ class Response
5
+ attr_reader :status, :body, :headers
6
+ def initialize(status, body, headers)
7
+ @status = status
8
+ @body = body
9
+ @headers = headers
10
+ end
11
+ end
12
+
13
+ module HTTPService
14
+ # common functionality for all HTTP services
15
+
16
+ class << self
17
+ attr_accessor :faraday_middleware, :http_options
18
+ end
19
+
20
+ @http_options ||= {}
21
+
22
+ DEFAULT_MIDDLEWARE = Proc.new do |builder|
23
+ builder.request :multipart
24
+ builder.request :url_encoded
25
+ builder.adapter Faraday.default_adapter
26
+ end
27
+
28
+ def self.server(options = {})
29
+ server = "#{options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER}"
30
+ server.gsub!(/\.facebook/, "-video.facebook") if options[:video]
31
+ "#{options[:use_ssl] ? "https" : "http"}://#{options[:beta] ? "beta." : ""}#{server}"
32
+ end
33
+
34
+ def self.make_request(path, args, verb, options = {})
35
+ # if the verb isn't get or post, send it as a post argument
36
+ args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
37
+
38
+ # turn all the keys to strings (Faraday has issues with symbols under 1.8.7) and resolve UploadableIOs
39
+ params = args.inject({}) {|hash, kv| hash[kv.first.to_s] = kv.last.is_a?(UploadableIO) ? kv.last.to_upload_io : kv.last; hash}
40
+
41
+ # figure out our options for this request
42
+ request_options = {:params => (verb == "get" ? params : {})}.merge(http_options || {}).merge(process_options(options))
43
+ request_options[:use_ssl] = true if args["access_token"] # require http if there's a token
44
+
45
+ # set up our Faraday connection
46
+ # we have to manually assign params to the URL or the
47
+ conn = Faraday.new(server(request_options), request_options, &(faraday_middleware || DEFAULT_MIDDLEWARE))
48
+
49
+ response = conn.send(verb, path, (verb == "post" ? params : {}))
50
+ Koala::Response.new(response.status.to_i, response.body, response.headers)
51
+ end
52
+
53
+ def self.encode_params(param_hash)
54
+ # unfortunately, we can't use to_query because that's Rails, not Ruby
55
+ # if no hash (e.g. no auth token) return empty string
56
+ # this is used mainly by the Batch API nowadays
57
+ ((param_hash || {}).collect do |key_and_value|
58
+ key_and_value[1] = MultiJson.encode(key_and_value[1]) unless key_and_value[1].is_a? String
59
+ "#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
60
+ end).join("&")
61
+ end
62
+
63
+ # deprecations
64
+ # not elegant or compact code, but temporary
65
+
66
+ def self.always_use_ssl
67
+ Koala::Utils.deprecate("HTTPService.always_use_ssl is now HTTPService.http_options[:use_ssl]; always_use_ssl will be removed in a future version.")
68
+ http_options[:use_ssl]
69
+ end
70
+
71
+ def self.always_use_ssl=(value)
72
+ Koala::Utils.deprecate("HTTPService.always_use_ssl is now HTTPService.http_options[:use_ssl]; always_use_ssl will be removed in a future version.")
73
+ http_options[:use_ssl] = value
74
+ end
75
+
76
+ def self.timeout
77
+ Koala::Utils.deprecate("HTTPService.timeout is now HTTPService.http_options[:timeout]; .timeout will be removed in a future version.")
78
+ http_options[:timeout]
79
+ end
80
+
81
+ def self.timeout=(value)
82
+ Koala::Utils.deprecate("HTTPService.timeout is now HTTPService.http_options[:timeout]; .timeout will be removed in a future version.")
83
+ http_options[:timeout] = value
84
+ end
85
+
86
+ def self.timeout
87
+ Koala::Utils.deprecate("HTTPService.timeout is now HTTPService.http_options[:timeout]; .timeout will be removed in a future version.")
88
+ http_options[:timeout]
89
+ end
90
+
91
+ def self.timeout=(value)
92
+ Koala::Utils.deprecate("HTTPService.timeout is now HTTPService.http_options[:timeout]; .timeout will be removed in a future version.")
93
+ http_options[:timeout] = value
94
+ end
95
+
96
+ def self.proxy
97
+ Koala::Utils.deprecate("HTTPService.proxy is now HTTPService.http_options[:proxy]; .proxy will be removed in a future version.")
98
+ http_options[:proxy]
99
+ end
100
+
101
+ def self.proxy=(value)
102
+ Koala::Utils.deprecate("HTTPService.proxy is now HTTPService.http_options[:proxy]; .proxy will be removed in a future version.")
103
+ http_options[:proxy] = value
104
+ end
105
+
106
+ def self.ca_path
107
+ Koala::Utils.deprecate("HTTPService.ca_path is now (HTTPService.http_options[:ssl] ||= {})[:ca_path]; .ca_path will be removed in a future version.")
108
+ (http_options[:ssl] || {})[:ca_path]
109
+ end
110
+
111
+ def self.ca_path=(value)
112
+ Koala::Utils.deprecate("HTTPService.ca_path is now (HTTPService.http_options[:ssl] ||= {})[:ca_path]; .ca_path will be removed in a future version.")
113
+ (http_options[:ssl] ||= {})[:ca_path] = value
114
+ end
115
+
116
+ def self.ca_file
117
+ Koala::Utils.deprecate("HTTPService.ca_file is now (HTTPService.http_options[:ssl] ||= {})[:ca_file]; .ca_file will be removed in a future version.")
118
+ (http_options[:ssl] || {})[:ca_file]
119
+ end
120
+
121
+ def self.ca_file=(value)
122
+ Koala::Utils.deprecate("HTTPService.ca_file is now (HTTPService.http_options[:ssl] ||= {})[:ca_file]; .ca_file will be removed in a future version.")
123
+ (http_options[:ssl] ||= {})[:ca_file] = value
124
+ end
125
+
126
+ def self.verify_mode
127
+ Koala::Utils.deprecate("HTTPService.verify_mode is now (HTTPService.http_options[:ssl] ||= {})[:verify_mode]; .verify_mode will be removed in a future version.")
128
+ (http_options[:ssl] || {})[:verify_mode]
129
+ end
130
+
131
+ def self.verify_mode=(value)
132
+ Koala::Utils.deprecate("HTTPService.verify_mode is now (HTTPService.http_options[:ssl] ||= {})[:verify_mode]; .verify_mode will be removed in a future version.")
133
+ (http_options[:ssl] ||= {})[:verify_mode] = value
134
+ end
135
+
136
+ def self.process_options(options)
137
+ if typhoeus_options = options.delete(:typhoeus_options)
138
+ 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.")
139
+ options = options.merge(typhoeus_options)
140
+ end
141
+
142
+ if ca_file = options.delete(:ca_file)
143
+ 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.")
144
+ (options[:ssl] ||= {})[:ca_file] = ca_file
145
+ end
146
+
147
+ if ca_path = options.delete(:ca_path)
148
+ 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.")
149
+ (options[:ssl] ||= {})[:ca_path] = ca_path
150
+ end
151
+
152
+ if verify_mode = options.delete(:verify_mode)
153
+ 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.")
154
+ (options[:ssl] ||= {})[:verify_mode] = verify_mode
155
+ end
156
+
157
+ options
158
+ end
159
+ end
160
+
161
+ module TyphoeusService
162
+ def self.deprecated_interface
163
+ # support old-style interface with a warning
164
+ Koala::Utils.deprecate("the TyphoeusService module is deprecated; to use Typhoeus, set Faraday.default_adapter = :typhoeus. Enabling Typhoeus for all Faraday connections.")
165
+ Faraday.default_adapter = :typhoeus
166
+ end
167
+ end
168
+
169
+ module NetHTTPService
170
+ def self.deprecated_interface
171
+ # support old-style interface with a warning
172
+ 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.")
173
+ Faraday.default_adapter = :net_http
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,191 @@
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 Facebook's JavaScript SDK.
13
+ # You can pass Rack/Rails/Sinatra's cookie hash directly to this method.
14
+ #
15
+ # If the user is logged in via Facebook, we return a dictionary with the
16
+ # keys "uid" and "access_token". The former is the user's Facebook ID,
17
+ # and the latter can be used to make authenticated requests to the Graph API.
18
+ # If the user is not logged in, we return None.
19
+
20
+ if signed_cookie = cookie_hash["fbsr_#{@app_id}"]
21
+ parse_signed_cookie(signed_cookie)
22
+ elsif unsigned_cookie = cookie_hash["fbs_#{@app_id}"]
23
+ parse_unsigned_cookie(unsigned_cookie)
24
+ end
25
+ end
26
+ alias_method :get_user_info_from_cookies, :get_user_info_from_cookie
27
+
28
+ def get_user_from_cookie(cookies)
29
+ if info = get_user_info_from_cookies(cookies)
30
+ string = info["uid"]
31
+ end
32
+ end
33
+ alias_method :get_user_from_cookies, :get_user_from_cookie
34
+
35
+ # URLs
36
+
37
+ def url_for_oauth_code(options = {})
38
+ # for permissions, see http://developers.facebook.com/docs/authentication/permissions
39
+ permissions = options[:permissions]
40
+ scope = permissions ? "&scope=#{permissions.is_a?(Array) ? permissions.join(",") : permissions}" : ""
41
+ display = options.has_key?(:display) ? "&display=#{options[:display]}" : ""
42
+
43
+ callback = options[:callback] || @oauth_callback_url
44
+ raise ArgumentError, "url_for_oauth_code must get a callback either from the OAuth object or in the options!" unless callback
45
+
46
+ # Creates the URL for oauth authorization for a given callback and optional set of permissions
47
+ "https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}#{display}"
48
+ end
49
+
50
+ def url_for_access_token(code, options = {})
51
+ # Creates the URL for the token corresponding to a given code generated by Facebook
52
+ callback = options[:callback] || @oauth_callback_url
53
+ raise ArgumentError, "url_for_access_token must get a callback either from the OAuth object or in the parameters!" unless callback
54
+ "https://#{GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@app_secret}&code=#{code}"
55
+ end
56
+
57
+ def get_access_token_info(code, options = {})
58
+ # convenience method to get a parsed token from Facebook for a given code
59
+ # should this require an OAuth callback URL?
60
+ get_token_from_server({:code => code, :redirect_uri => options[:redirect_uri] || @oauth_callback_url}, false, options)
61
+ end
62
+
63
+ def get_access_token(code, options = {})
64
+ # upstream methods will throw errors if needed
65
+ if info = get_access_token_info(code, options)
66
+ string = info["access_token"]
67
+ end
68
+ end
69
+
70
+ def get_app_access_token_info(options = {})
71
+ # convenience method to get a the application's sessionless access token
72
+ get_token_from_server({:type => 'client_cred'}, true, options)
73
+ end
74
+
75
+ def get_app_access_token(options = {})
76
+ if info = get_app_access_token_info(options)
77
+ string = info["access_token"]
78
+ end
79
+ end
80
+
81
+ # Originally provided directly by Facebook, however this has changed
82
+ # as their concept of crypto changed. For historic purposes, this is their proposal:
83
+ # https://developers.facebook.com/docs/authentication/canvas/encryption_proposal/
84
+ # Currently see https://github.com/facebook/php-sdk/blob/master/src/facebook.php#L758
85
+ # for a more accurate reference implementation strategy.
86
+ def parse_signed_request(input)
87
+ encoded_sig, encoded_envelope = input.split('.', 2)
88
+ signature = base64_url_decode(encoded_sig).unpack("H*").first
89
+ envelope = MultiJson.decode(base64_url_decode(encoded_envelope))
90
+
91
+ raise "SignedRequest: Unsupported algorithm #{envelope['algorithm']}" if envelope['algorithm'] != 'HMAC-SHA256'
92
+
93
+ # now see if the signature is valid (digest, key, data)
94
+ hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @app_secret, encoded_envelope)
95
+ raise 'SignedRequest: Invalid signature' if (signature != hmac)
96
+
97
+ return envelope
98
+ end
99
+
100
+ # from session keys
101
+ def get_token_info_from_session_keys(sessions, options = {})
102
+ # fetch the OAuth tokens from Facebook
103
+ response = fetch_token_string({
104
+ :type => 'client_cred',
105
+ :sessions => sessions.join(",")
106
+ }, true, "exchange_sessions", options)
107
+
108
+ # Facebook returns an empty body in certain error conditions
109
+ if response == ""
110
+ raise APIError.new({
111
+ "type" => "ArgumentError",
112
+ "message" => "get_token_from_session_key received an error (empty response body) for sessions #{sessions.inspect}!"
113
+ })
114
+ end
115
+
116
+ MultiJson.decode(response)
117
+ end
118
+
119
+ def get_tokens_from_session_keys(sessions, options = {})
120
+ # get the original hash results
121
+ results = get_token_info_from_session_keys(sessions, options)
122
+ # now recollect them as just the access tokens
123
+ results.collect { |r| r ? r["access_token"] : nil }
124
+ end
125
+
126
+ def get_token_from_session_key(session, options = {})
127
+ # convenience method for a single key
128
+ # gets the overlaoded strings automatically
129
+ get_tokens_from_session_keys([session], options)[0]
130
+ end
131
+
132
+ protected
133
+
134
+ def get_token_from_server(args, post = false, options = {})
135
+ # fetch the result from Facebook's servers
136
+ result = fetch_token_string(args, post, "access_token", options)
137
+
138
+ # if we have an error, parse the error JSON and raise an error
139
+ raise APIError.new((MultiJson.decode(result)["error"] rescue nil) || {}) if result =~ /error/
140
+
141
+ # otherwise, parse the access token
142
+ parse_access_token(result)
143
+ end
144
+
145
+ def parse_access_token(response_text)
146
+ components = response_text.split("&").inject({}) do |hash, bit|
147
+ key, value = bit.split("=")
148
+ hash.merge!(key => value)
149
+ end
150
+ components
151
+ end
152
+
153
+ def parse_unsigned_cookie(fb_cookie)
154
+ # remove the opening/closing quote
155
+ fb_cookie = fb_cookie.gsub(/\"/, "")
156
+
157
+ # since we no longer get individual cookies, we have to separate out the components ourselves
158
+ components = {}
159
+ fb_cookie.split("&").map {|param| param = param.split("="); components[param[0]] = param[1]}
160
+
161
+ # generate the signature and make sure it matches what we expect
162
+ auth_string = components.keys.sort.collect {|a| a == "sig" ? nil : "#{a}=#{components[a]}"}.reject {|a| a.nil?}.join("")
163
+ sig = Digest::MD5.hexdigest(auth_string + @app_secret)
164
+ sig == components["sig"] && (components["expires"] == "0" || Time.now.to_i < components["expires"].to_i) ? components : nil
165
+ end
166
+
167
+ def parse_signed_cookie(fb_cookie)
168
+ components = parse_signed_request(fb_cookie)
169
+ if (code = components["code"]) && token_info = get_access_token_info(code, :redirect_uri => '')
170
+ components.merge(token_info)
171
+ else
172
+ nil
173
+ end
174
+ end
175
+
176
+ def fetch_token_string(args, post = false, endpoint = "access_token", options = {})
177
+ Koala.make_request("/oauth/#{endpoint}", {
178
+ :client_id => @app_id,
179
+ :client_secret => @app_secret
180
+ }.merge!(args), post ? "post" : "get", {:use_ssl => true}.merge!(options)).body
181
+ end
182
+
183
+ # base 64
184
+ # directly from https://github.com/facebook/crypto-request-examples/raw/master/sample.rb
185
+ def base64_url_decode(str)
186
+ str += '=' * (4 - str.length.modulo(4))
187
+ Base64.decode64(str.tr('-_', '+/'))
188
+ end
189
+ end
190
+ end
191
+ 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)
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