koala 0.4 → 3.7.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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +32 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +1 -0
  5. data/.yardopts +3 -0
  6. data/Gemfile +25 -0
  7. data/ISSUE_TEMPLATE +25 -0
  8. data/LICENSE +22 -0
  9. data/Manifest +32 -5
  10. data/PULL_REQUEST_TEMPLATE +11 -0
  11. data/Rakefile +12 -12
  12. data/changelog.md +781 -0
  13. data/code_of_conduct.md +74 -0
  14. data/koala.gemspec +28 -24
  15. data/lib/koala/api/batch_operation.rb +86 -0
  16. data/lib/koala/api/graph_api_methods.rb +504 -0
  17. data/lib/koala/api/graph_batch_api.rb +167 -0
  18. data/lib/koala/api/graph_collection.rb +129 -0
  19. data/lib/koala/api/graph_error_checker.rb +72 -0
  20. data/lib/koala/api.rb +159 -0
  21. data/lib/koala/configuration.rb +56 -0
  22. data/lib/koala/errors.rb +126 -0
  23. data/lib/koala/http_service/request.rb +133 -0
  24. data/lib/koala/http_service/response.rb +20 -0
  25. data/lib/koala/http_service/uploadable_io.rb +183 -0
  26. data/lib/koala/http_service.rb +108 -0
  27. data/lib/koala/oauth.rb +342 -0
  28. data/lib/koala/realtime_updates.rb +151 -0
  29. data/lib/koala/test_users.rb +189 -0
  30. data/lib/koala/utils.rb +41 -0
  31. data/lib/koala/version.rb +3 -0
  32. data/lib/koala.rb +51 -291
  33. data/readme.md +269 -21
  34. data/spec/cases/api_spec.rb +362 -0
  35. data/spec/cases/configuration_spec.rb +11 -0
  36. data/spec/cases/error_spec.rb +143 -0
  37. data/spec/cases/graph_api_batch_spec.rb +788 -0
  38. data/spec/cases/graph_api_spec.rb +76 -0
  39. data/spec/cases/graph_collection_spec.rb +192 -0
  40. data/spec/cases/graph_error_checker_spec.rb +147 -0
  41. data/spec/cases/http_service/request_spec.rb +250 -0
  42. data/spec/cases/http_service/response_spec.rb +24 -0
  43. data/spec/cases/http_service_spec.rb +280 -0
  44. data/spec/cases/koala_spec.rb +57 -0
  45. data/spec/cases/koala_test_spec.rb +5 -0
  46. data/spec/cases/oauth_spec.rb +647 -0
  47. data/spec/cases/realtime_updates_spec.rb +327 -0
  48. data/spec/cases/test_users_spec.rb +383 -0
  49. data/spec/cases/uploadable_io_spec.rb +266 -0
  50. data/spec/cases/utils_spec.rb +55 -0
  51. data/spec/fixtures/beach.jpg +0 -0
  52. data/spec/fixtures/cat.m4v +0 -0
  53. data/spec/fixtures/facebook_data.yml +63 -0
  54. data/spec/fixtures/mock_facebook_responses.yml +483 -0
  55. data/spec/fixtures/vcr_cassettes/app_test_accounts.yml +97 -0
  56. data/spec/fixtures/vcr_cassettes/friend_list_next_page.yml +121 -0
  57. data/spec/integration/graph_collection_spec.rb +24 -0
  58. data/spec/spec_helper.rb +25 -0
  59. data/spec/support/custom_matchers.rb +28 -0
  60. data/spec/support/graph_api_shared_examples.rb +534 -0
  61. data/spec/support/koala_test.rb +251 -0
  62. data/spec/support/mock_http_service.rb +140 -0
  63. data/spec/support/uploadable_io_shared_examples.rb +70 -0
  64. metadata +206 -62
  65. data/CHANGELOG +0 -24
  66. data/init.rb +0 -2
  67. data/lib/http_services.rb +0 -60
  68. data/test/facebook_data.yml +0 -5
  69. data/test/koala/facebook_no_access_token_tests.rb +0 -119
  70. data/test/koala/facebook_with_access_token_tests.rb +0 -106
  71. data/test/koala_tests.rb +0 -30
@@ -0,0 +1,129 @@
1
+ require 'addressable/uri'
2
+ require 'cgi'
3
+
4
+ module Koala
5
+ module Facebook
6
+ class API
7
+ # A light wrapper for collections returned from the Graph API.
8
+ # It extends Array to allow you to page backward and forward through
9
+ # result sets, and providing easy access to paging information.
10
+ class GraphCollection < Array
11
+
12
+ # The raw paging information from Facebook (next/previous URLs).
13
+ attr_reader :paging
14
+ # The raw summary information from Facebook (total counts).
15
+ attr_reader :summary
16
+ # @return [Koala::Facebook::GraphAPI] the api used to make requests.
17
+ attr_reader :api
18
+ # The entire raw response from Facebook.
19
+ attr_reader :raw_response
20
+ # The headers from the Facebook response
21
+ attr_reader :headers
22
+
23
+ # Initialize the array of results and store various additional paging-related information.
24
+ #
25
+ # @param [Koala::HTTPService::Response] response object wrapping the raw Facebook response
26
+ # @param api the Graph {Koala::Facebook::API API} instance to use to make calls
27
+ # (usually the API that made the original call).
28
+ #
29
+ # @return [Koala::Facebook::API::GraphCollection] an initialized GraphCollection
30
+ # whose paging, summary, raw_response, and api attributes are populated.
31
+ def initialize(response, api)
32
+ super response.data["data"]
33
+ @paging = response.data["paging"]
34
+ @summary = response.data["summary"]
35
+ @raw_response = response.data
36
+ @api = api
37
+ @headers = response.headers
38
+ end
39
+
40
+ # @private
41
+ # Turn the response into a GraphCollection if they're pageable;
42
+ # if not, return the data of the original response.
43
+ # The Ads API (uniquely so far) returns a hash rather than an array when queried
44
+ # with get_connections.
45
+ def self.evaluate(response, api)
46
+ return nil if response.nil?
47
+
48
+ is_pageable?(response) ? self.new(response, api) : response.data
49
+ end
50
+
51
+ # response will always be an instance of Koala::HTTPService::Response
52
+ # since that is what we get from Koala::Facebook::API#api
53
+ def self.is_pageable?(response)
54
+ response.data.is_a?(Hash) && response.data["data"].is_a?(Array)
55
+ end
56
+
57
+ # Retrieve the next page of results.
58
+ #
59
+ # @param [Hash] extra_params Some optional extra parameters for paging. For supported parameters see https://developers.facebook.com/docs/reference/api/pagination/
60
+ #
61
+ # @example With optional extra params
62
+ # wall = api.get_connections("me", "feed", since: 1379593891)
63
+ # wall.next_page(since: 1379593891)
64
+ #
65
+ # @return a GraphCollection array of additional results (an empty array if there are no more results)
66
+ def next_page(extra_params = {})
67
+ base, args = next_page_params
68
+ base ? @api.get_page([base, args.merge(extra_params)]) : nil
69
+ end
70
+
71
+ # Retrieve the previous page of results.
72
+ #
73
+ # @param [Hash] extra_params Some optional extra parameters for paging. For supported parameters see https://developers.facebook.com/docs/reference/api/pagination/
74
+ #
75
+ # @return a GraphCollection array of additional results (an empty array if there are no earlier results)
76
+ def previous_page(extra_params = {})
77
+ base, args = previous_page_params
78
+ base ? @api.get_page([base, args.merge(extra_params)]) : nil
79
+ end
80
+
81
+ # Arguments that can be sent to {Koala::Facebook::API#graph_call} to retrieve the next page of results.
82
+ #
83
+ # @example
84
+ # @api.graph_call(*collection.next_page_params)
85
+ #
86
+ # @return an array of arguments, or nil if there are no more pages
87
+ def next_page_params
88
+ @paging && @paging["next"] ? parse_page_url(@paging["next"]) : nil
89
+ end
90
+
91
+ # Arguments that can be sent to {Koala::Facebook::API#graph_call} to retrieve the previous page of results.
92
+ #
93
+ # @example
94
+ # @api.graph_call(*collection.previous_page_params)
95
+ #
96
+ # @return an array of arguments, or nil if there are no previous pages
97
+ def previous_page_params
98
+ @paging && @paging["previous"] ? parse_page_url(@paging["previous"]) : nil
99
+ end
100
+
101
+ # @private
102
+ def parse_page_url(url)
103
+ GraphCollection.parse_page_url(url)
104
+ end
105
+
106
+ # Parse the previous and next page URLs Facebook provides in pageable results.
107
+ # You'll mainly need to use this when using a non-Rails framework (one without url_for);
108
+ # to store paging information between page loads, pass the URL (from GraphCollection#paging)
109
+ # and use parse_page_url to turn it into parameters useful for {Koala::Facebook::API#get_page}.
110
+ #
111
+ # @param url the paging URL to turn into graph_call parameters
112
+ #
113
+ # @return an array of parameters that can be provided via graph_call(*parsed_params)
114
+ def self.parse_page_url(url)
115
+ uri = Addressable::URI.parse(url)
116
+
117
+ base = uri.path.sub(/^\//, '')
118
+ params = CGI.parse(uri.query)
119
+
120
+ new_params = {}
121
+ params.each_pair do |key,value|
122
+ new_params[key] = value.join ","
123
+ end
124
+ [base,new_params]
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,72 @@
1
+ module Koala
2
+ module Facebook
3
+ # This class, given a Koala::HTTPService::Response object, will check for Graph API-specific
4
+ # errors. This returns an error of the appropriate type which can be immediately raised
5
+ # (non-batch) or added to the list of batch results (batch)
6
+ class GraphErrorChecker
7
+ attr_reader :http_status, :body, :headers
8
+ def initialize(http_status, body, headers)
9
+ @http_status = http_status.to_i
10
+ @body = body
11
+ @headers = headers
12
+ end
13
+
14
+ # Facebook has a set of standardized error codes, some of which represent problems with the
15
+ # token.
16
+ AUTHENTICATION_ERROR_CODES = [102, 190, 450, 452, 2500]
17
+
18
+ # Facebook can return debug information in the response headers -- see
19
+ # https://developers.facebook.com/docs/graph-api/using-graph-api#bugdebug
20
+ DEBUG_HEADERS = %w[x-fb-debug x-fb-rev x-fb-trace-id x-business-use-case-usage x-ad-account-usage x-app-usage]
21
+
22
+ def error_if_appropriate
23
+ if http_status >= 400
24
+ error_class.new(http_status, body, error_info)
25
+ end
26
+ end
27
+
28
+ protected
29
+
30
+ def error_class
31
+ if auth_error?
32
+ # See: https://developers.facebook.com/docs/authentication/access-token-expiration/
33
+ # https://developers.facebook.com/bugs/319643234746794?browse=search_4fa075c0bd9117b20604672
34
+ AuthenticationError
35
+ else
36
+ ClientError
37
+ end
38
+ end
39
+
40
+ def auth_error?
41
+ # tbh, I'm not sure why we restrict Facebook-reported OAuthExceptions to only those without
42
+ # codes or whose codes match the list above -- let's investigate changing this later.
43
+ error_info['type'] == 'OAuthException' &&
44
+ (!error_info['code'] || AUTHENTICATION_ERROR_CODES.include?(error_info['code'].to_i))
45
+ end
46
+
47
+ def error_info
48
+ # Build up the complete error info from whatever Facebook gives us plus the header
49
+ # information
50
+ @error_info ||= DEBUG_HEADERS.inject(base_error_info) do |hash, error_key|
51
+ hash[error_key] = headers[error_key] if headers[error_key]
52
+ hash
53
+ end
54
+ end
55
+
56
+ def base_error_info
57
+ response_hash['error'] || {}
58
+ end
59
+
60
+ def response_hash
61
+ # Normally, we start with the response body. If it isn't valid JSON, we start with an empty
62
+ # hash and fill it with error data.
63
+ @response_hash ||= begin
64
+ parsed_body = JSON.parse(body)
65
+ parsed_body.is_a?(Hash) ? parsed_body : {}
66
+ rescue JSON::ParserError
67
+ {}
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
data/lib/koala/api.rb ADDED
@@ -0,0 +1,159 @@
1
+ # graph_batch_api and legacy are required at the bottom, since they depend on API being defined
2
+ require 'koala/api/graph_api_methods'
3
+ require 'koala/api/graph_collection'
4
+ require 'openssl'
5
+
6
+ module Koala
7
+ module Facebook
8
+ class API
9
+ # Creates a new API client.
10
+ # @param [String] access_token access token
11
+ # @param [String] app_secret app secret, for tying your access tokens to your app secret
12
+ # If you provide an app secret, your requests will be
13
+ # signed by default, unless you pass appsecret_proof:
14
+ # false as an option to the API call. (See
15
+ # https://developers.facebook.com/docs/graph-api/securing-requests/)
16
+ # @param [Block] rate_limit_hook block called with limits received in facebook response headers
17
+ # @note If no access token is provided, you can only access some public information.
18
+ # @return [Koala::Facebook::API] the API client
19
+ def initialize(access_token = Koala.config.access_token, app_secret = Koala.config.app_secret, rate_limit_hook = Koala.config.rate_limit_hook)
20
+ @access_token = access_token
21
+ @app_secret = app_secret
22
+ @rate_limit_hook = rate_limit_hook
23
+ end
24
+
25
+ attr_reader :access_token, :app_secret, :rate_limit_hook
26
+
27
+ include GraphAPIMethods
28
+
29
+ # Make a call directly to the Graph API.
30
+ # (See any of the other methods for example invocations.)
31
+ #
32
+ # @param path the Graph API path to query (no leading / needed)
33
+ # @param args (see #get_object)
34
+ # @param verb the type of HTTP request to make (get, post, delete, etc.)
35
+ # @options (see #get_object)
36
+ #
37
+ # @yield response when making a batch API call, you can pass in a block
38
+ # that parses the results, allowing for cleaner code.
39
+ # The block's return value is returned in the batch results.
40
+ # See the code for {#get_picture} for examples.
41
+ # (Not needed in regular calls; you'll probably rarely use this.)
42
+ #
43
+ # @raise [Koala::Facebook::APIError] if Facebook returns an error
44
+ #
45
+ # @return the result from Facebook
46
+ def graph_call(path, args = {}, verb = "get", options = {}, &post_processing)
47
+ # enable appsecret_proof by default
48
+ options = {:appsecret_proof => true}.merge(options) if @app_secret
49
+ response = api(path, args, verb, options)
50
+
51
+ error = GraphErrorChecker.new(response.status, response.body, response.headers).error_if_appropriate
52
+ raise error if error
53
+
54
+ # if we want a component other than the body (e.g. redirect header for images), provide that
55
+ http_component = options[:http_component]
56
+ desired_data = if options[:http_component]
57
+ http_component == :response ? response : response.send(http_component)
58
+ else
59
+ # turn this into a GraphCollection if it's pageable
60
+ API::GraphCollection.evaluate(response, self)
61
+ end
62
+
63
+ if rate_limit_hook
64
+ limits = %w(x-business-use-case-usage x-ad-account-usage x-app-usage).each_with_object({}) do |key, hash|
65
+ value = response.headers.fetch(key, nil)
66
+ next unless value
67
+ hash[key] = JSON.parse(response.headers[key])
68
+ rescue JSON::ParserError => e
69
+ Koala::Utils.logger.error("#{e.class}: #{e.message} while parsing #{key} = #{value}")
70
+ end
71
+
72
+ rate_limit_hook.call(limits) if limits.keys.any?
73
+ end
74
+
75
+ # now process as appropriate for the given call (get picture header, etc.)
76
+ post_processing ? post_processing.call(desired_data) : desired_data
77
+ end
78
+
79
+
80
+ # Makes a request to the appropriate Facebook API.
81
+ # @note You'll rarely need to call this method directly.
82
+ #
83
+ # @see GraphAPIMethods#graph_call
84
+ #
85
+ # @param path the server path for this request (leading / is prepended if not present)
86
+ # @param args arguments to be sent to Facebook
87
+ # @param verb the HTTP method to use
88
+ # @param options request-related options for Koala and Faraday.
89
+ # See https://github.com/arsduo/koala/wiki/HTTP-Services for additional options.
90
+ # @option options [Symbol] :http_component which part of the response (headers, body, or status) to return
91
+ # @option options [Symbol] :format which request format to use. Currently, :json is supported
92
+ # @option options [Symbol] :preserve_form_arguments preserve arrays in arguments, which are
93
+ # expected by certain FB APIs (see the ads API in particular,
94
+ # https://developers.facebook.com/docs/marketing-api/adgroup/v2.4)
95
+ # @option options [Boolean] :beta use Facebook's beta tier
96
+ # @option options [Boolean] :use_ssl force SSL for this request, even if it's tokenless.
97
+ # (All API requests with access tokens use SSL.)
98
+ # @raise [Koala::Facebook::ServerError] if Facebook returns an error (response status >= 500)
99
+ #
100
+ # @return a Koala::HTTPService::Response object representing the returned Facebook data
101
+ def api(path, args = {}, verb = "get", options = {})
102
+ # we make a copy of args so the modifications (added access_token & appsecret_proof)
103
+ # do not affect the received argument
104
+ args = args.dup
105
+
106
+ # If a access token is explicitly provided, use that
107
+ # This is explicitly needed in batch requests so GraphCollection
108
+ # results preserve any specific access tokens provided
109
+ args["access_token"] ||= @access_token || @app_access_token if @access_token || @app_access_token
110
+
111
+ if options.delete(:appsecret_proof) && args["access_token"] && @app_secret
112
+ args["appsecret_proof"] = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), @app_secret, args["access_token"])
113
+ end
114
+
115
+ # Translate any arrays in the params into comma-separated strings
116
+ args = sanitize_request_parameters(args) unless preserve_form_arguments?(options)
117
+
118
+ # add a leading / if needed...
119
+ path = "/#{path}" unless path.to_s =~ /^\//
120
+
121
+ # make the request via the provided service
122
+ result = Koala.make_request(path, args, verb, options)
123
+
124
+ if result.status.to_i >= 500
125
+ raise Koala::Facebook::ServerError.new(result.status.to_i, result.body)
126
+ end
127
+
128
+ result
129
+ end
130
+
131
+ private
132
+
133
+ # Sanitizes Ruby objects into Facebook-compatible string values.
134
+ #
135
+ # @param parameters a hash of parameters.
136
+ #
137
+ # Returns a hash in which values that are arrays of non-enumerable values
138
+ # (Strings, Symbols, Numbers, etc.) are turned into comma-separated strings.
139
+ def sanitize_request_parameters(parameters)
140
+ parameters.reduce({}) do |result, (key, value)|
141
+ # if the parameter is an array that contains non-enumerable values,
142
+ # turn it into a comma-separated list
143
+ # in Ruby 1.8.7, strings are enumerable, but we don't care
144
+ if value.is_a?(Array) && value.none? {|entry| entry.is_a?(Enumerable) && !entry.is_a?(String)}
145
+ value = value.join(",")
146
+ end
147
+ result.merge(key => value)
148
+ end
149
+ end
150
+
151
+ def preserve_form_arguments?(options)
152
+ options[:format] == :json || options[:preserve_form_arguments] || Koala.config.preserve_form_arguments
153
+ end
154
+
155
+ def check_response(http_status, body, headers)
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,56 @@
1
+ # Global configuration for Koala.
2
+ class Koala::Configuration
3
+ # The default access token to be used if none is otherwise supplied.
4
+ attr_accessor :access_token
5
+
6
+ # The default app secret value to be used if none is otherwise supplied.
7
+ attr_accessor :app_secret
8
+
9
+ # The default application ID to use if none is otherwise supplied.
10
+ attr_accessor :app_id
11
+
12
+ # The default app access token to be used if none is otherwise supplied.
13
+ attr_accessor :app_access_token
14
+
15
+ # The default API version to use if none is otherwise specified.
16
+ attr_accessor :api_version
17
+
18
+ # The default value to use for the oauth_callback_url if no other is provided.
19
+ attr_accessor :oauth_callback_url
20
+
21
+ # Whether to preserve arrays in arguments, which are expected by certain FB APIs (see the ads API
22
+ # in particular, https://developers.facebook.com/docs/marketing-api/adgroup/v2.4)
23
+ attr_accessor :preserve_form_arguments
24
+
25
+ # The server to use for Graph API requests
26
+ attr_accessor :graph_server
27
+
28
+ # The server to use when constructing dialog URLs.
29
+ attr_accessor :dialog_host
30
+
31
+ # Whether or not to mask tokens
32
+ attr_accessor :mask_tokens
33
+
34
+ # Called with the info for the rate limits in the response header
35
+ attr_accessor :rate_limit_hook
36
+
37
+ # Certain Facebook services (beta, video) require you to access different
38
+ # servers. If you're using your own servers, for instance, for a proxy,
39
+ # you can change both the matcher (what value to change when updating the URL) and the
40
+ # replacement values (what to add).
41
+ #
42
+ # So, for instance, to use the beta stack, we match on .facebook and change it to .beta.facebook.
43
+ # If you're talking to fbproxy.mycompany.com, you could set up beta.fbproxy.mycompany.com for
44
+ # FB's beta tier, and set the matcher to /\.fbproxy/ and the beta_replace to '.beta.fbproxy'.
45
+ attr_accessor :host_path_matcher
46
+ attr_accessor :video_replace
47
+ attr_accessor :beta_replace
48
+
49
+ def initialize
50
+ # Default to our default values.
51
+ Koala::HTTPService::DEFAULT_SERVERS.each_pair do |key, value|
52
+ self.public_send("#{key}=", value)
53
+ end
54
+ self.mask_tokens = true
55
+ end
56
+ end
@@ -0,0 +1,126 @@
1
+ module Koala
2
+
3
+ class KoalaError < StandardError; end
4
+
5
+ module Facebook
6
+
7
+ # The OAuth signature is incomplete, invalid, or using an unsupported algorithm
8
+ class OAuthSignatureError < ::Koala::KoalaError; end
9
+
10
+ # Required for realtime updates validation
11
+ class AppSecretNotDefinedError < ::Koala::KoalaError; end
12
+
13
+ # Facebook responded with an error to an API request. If the exception contains a nil
14
+ # http_status, then the error was detected before making a call to Facebook. (e.g. missing access token)
15
+ class APIError < ::Koala::KoalaError
16
+ attr_accessor :http_status,
17
+ :response_body,
18
+ :fb_error_type,
19
+ :fb_error_code,
20
+ :fb_error_subcode,
21
+ :fb_error_message,
22
+ :fb_error_user_msg,
23
+ :fb_error_user_title,
24
+ :fb_error_trace_id,
25
+ :fb_error_debug_trace_id,
26
+ :fb_error_debug,
27
+ :fb_error_rev,
28
+ :fb_buc_usage,
29
+ :fb_ada_usage,
30
+ :fb_app_usage
31
+
32
+ # Create a new API Error
33
+ #
34
+ # @param http_status [Integer] The HTTP status code of the response
35
+ # @param response_body [String] The response body
36
+ # @param error_info One of the following:
37
+ # [Hash] The error information extracted from the request
38
+ # ("type", "code", "error_subcode", "message")
39
+ # [String] The error description
40
+ # If error_info is nil or not provided, the method will attempt to extract
41
+ # the error info from the response_body
42
+ #
43
+ # @return the newly created APIError
44
+ def initialize(http_status, response_body, error_info = nil)
45
+ if response_body
46
+ self.response_body = response_body.strip
47
+ else
48
+ self.response_body = ''
49
+ end
50
+ self.http_status = http_status
51
+
52
+ if error_info && error_info.is_a?(String)
53
+ message = error_info
54
+ else
55
+ unless error_info
56
+ begin
57
+ error_info = JSON.parse(response_body)['error'] if response_body
58
+ rescue
59
+ end
60
+ error_info ||= {}
61
+ end
62
+
63
+ self.fb_error_type = error_info["type"]
64
+ self.fb_error_code = error_info["code"]
65
+ self.fb_error_subcode = error_info["error_subcode"]
66
+ self.fb_error_message = error_info["message"]
67
+ self.fb_error_user_msg = error_info["error_user_msg"]
68
+ self.fb_error_user_title = error_info["error_user_title"]
69
+ self.fb_error_trace_id = error_info["fbtrace_id"]
70
+
71
+ self.fb_error_debug_trace_id = error_info["x-fb-trace-id"]
72
+ self.fb_error_debug = error_info["x-fb-debug"]
73
+ self.fb_error_rev = error_info["x-fb-rev"]
74
+ self.fb_buc_usage = json_parse_for(error_info, "x-business-use-case-usage")
75
+ self.fb_ada_usage = json_parse_for(error_info, "x-ad-account-usage")
76
+ self.fb_app_usage = json_parse_for(error_info, "x-app-usage")
77
+
78
+ error_array = []
79
+ %w(type code error_subcode message error_user_title error_user_msg fbtrace_id x-fb-trace-id x-fb-debug x-fb-rev).each do |key|
80
+ error_array << "#{key}: #{error_info[key]}" if error_info[key]
81
+ end
82
+
83
+ if error_array.empty?
84
+ message = self.response_body
85
+ else
86
+ message = error_array.join(', ')
87
+ end
88
+ end
89
+ message += " [HTTP #{http_status}]" if http_status
90
+
91
+ super(message)
92
+ end
93
+
94
+ private
95
+
96
+ # refs: https://developers.facebook.com/docs/graph-api/overview/rate-limiting/#headers
97
+ # NOTE: The header will contain a JSON-formatted string that describes current application rate limit usage.
98
+ def json_parse_for(error_info, key)
99
+ string = error_info[key]
100
+ return if string.nil?
101
+
102
+ JSON.parse(string)
103
+ rescue JSON::ParserError => e
104
+ Koala::Utils.logger.error("#{e.class}: #{e.message} while parsing #{key} = #{string}")
105
+ nil
106
+ end
107
+ end
108
+
109
+ # Facebook returned an invalid response body
110
+ class BadFacebookResponse < APIError; end
111
+
112
+ # Facebook responded with an error while attempting to request an access token
113
+ class OAuthTokenRequestError < APIError; end
114
+
115
+ # Any error with a 5xx HTTP status code
116
+ class ServerError < APIError; end
117
+
118
+ # Any error with a 4xx HTTP status code
119
+ class ClientError < APIError; end
120
+
121
+ # All graph API authentication failures.
122
+ class AuthenticationError < ClientError; end
123
+
124
+ end
125
+
126
+ end
@@ -0,0 +1,133 @@
1
+ module Koala
2
+ module HTTPService
3
+ class Request
4
+ attr_reader :raw_path, :raw_args, :raw_verb, :raw_options
5
+
6
+ # @param path the server path for this request
7
+ # @param args (see Koala::Facebook::API#api)
8
+ # @param verb the HTTP method to use.
9
+ # If not get or post, this will be turned into a POST request with the appropriate :method
10
+ # specified in the arguments.
11
+ # @param options various flags to indicate which server to use. (see Koala::Facebook::API#api)
12
+ # @param options
13
+ # @option options :video use the server designated for video uploads
14
+ # @option options :beta use the beta tier
15
+ # @option options :use_ssl force https, even if not needed
16
+ # @option options :json whether or not to send JSON to Facebook
17
+ def initialize(path:, verb:, args: {}, options: {})
18
+ @raw_path = path
19
+ @raw_args = args
20
+ @raw_verb = verb
21
+ @raw_options = options
22
+ end
23
+
24
+ # Determines which type of request to send to Facebook. Facebook natively accepts GETs and POSTs, for others we have to include the method in the post body.
25
+ #
26
+ # @return one of get or post
27
+ def verb
28
+ ["get", "post"].include?(raw_verb) ? raw_verb : "post"
29
+ end
30
+
31
+ # Determines the path to be requested on Facebook, incorporating an API version if specified.
32
+ #
33
+ # @return the original path, with API version if appropriate.
34
+ def path
35
+ # if an api_version is specified and the path does not already contain
36
+ # one, prepend it to the path
37
+ api_version = raw_options[:api_version] || Koala.config.api_version
38
+ if api_version && !path_contains_api_version?
39
+ begins_with_slash = raw_path[0] == "/"
40
+ divider = begins_with_slash ? "" : "/"
41
+ "/#{api_version}#{divider}#{raw_path}"
42
+ else
43
+ raw_path
44
+ end
45
+ end
46
+
47
+ # Determines any arguments to be sent in a POST body.
48
+ #
49
+ # @return {} for GET; the provided args for POST; those args with the method parameter for
50
+ # other values
51
+ def post_args
52
+ if raw_verb == "get"
53
+ {}
54
+ elsif raw_verb == "post"
55
+ args
56
+ else
57
+ args.merge(method: raw_verb)
58
+ end
59
+ end
60
+
61
+ def get_args
62
+ raw_verb == "get" ? args : {}
63
+ end
64
+
65
+ # Calculates a set of request options to pass to Faraday.
66
+ #
67
+ # @return a hash combining GET parameters (if appropriate), default options, and
68
+ # any specified for the request.
69
+ def options
70
+ # figure out our options for this request
71
+ add_ssl_options(
72
+ # for GETs, we pass the params to Faraday to encode
73
+ {params: get_args}.merge(HTTPService.http_options).merge(raw_options)
74
+ )
75
+ end
76
+
77
+ # Whether or not this request should use JSON.
78
+ #
79
+ # @return true or false
80
+ def json?
81
+ raw_options[:format] == :json
82
+ end
83
+
84
+ # The address of the appropriate Facebook server.
85
+ #
86
+ # @return a complete server address with protocol
87
+ def server
88
+ uri = "#{options[:use_ssl] ? "https" : "http"}://#{Koala.config.graph_server}"
89
+ # if we want to use the beta tier or the video server, make those substitutions as
90
+ # appropriate
91
+ replace_server_component(
92
+ replace_server_component(uri, options[:video], Koala.config.video_replace),
93
+ options[:beta],
94
+ Koala.config.beta_replace
95
+ )
96
+ end
97
+
98
+ protected
99
+
100
+ # The arguments to include in the request.
101
+ def args
102
+ raw_args.inject({}) do |hash, (key, value)|
103
+ # Resolve UploadableIOs into data Facebook can work with
104
+ hash.merge(key => value.is_a?(UploadableIO) ? value.to_upload_io : value)
105
+ end
106
+ end
107
+
108
+ def add_ssl_options(opts)
109
+ # require https by default (can be overriden by explicitly setting other SSL options)
110
+ {
111
+ use_ssl: true,
112
+ ssl: {verify: true}.merge(opts[:ssl] || {})
113
+ }.merge(opts)
114
+ end
115
+
116
+ # Determines whether a given path already contains an API version.
117
+ #
118
+ # @param path the URL path.
119
+ #
120
+ # @return true or false accordingly.
121
+ def path_contains_api_version?
122
+ # looks for "/$MAJOR[.$MINOR]/" in the path
123
+ match = /^\/?(v\d+(?:\.\d+)?)\//.match(raw_path)
124
+ !!(match && match[1])
125
+ end
126
+
127
+ def replace_server_component(host, condition_met, replacement)
128
+ return host unless condition_met
129
+ host.gsub(Koala.config.host_path_matcher, replacement)
130
+ end
131
+ end
132
+ end
133
+ end