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.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +32 -0
- data/.gitignore +9 -0
- data/.rspec +1 -0
- data/.yardopts +3 -0
- data/Gemfile +25 -0
- data/ISSUE_TEMPLATE +25 -0
- data/LICENSE +22 -0
- data/Manifest +32 -5
- data/PULL_REQUEST_TEMPLATE +11 -0
- data/Rakefile +12 -12
- data/changelog.md +781 -0
- data/code_of_conduct.md +74 -0
- data/koala.gemspec +28 -24
- data/lib/koala/api/batch_operation.rb +86 -0
- data/lib/koala/api/graph_api_methods.rb +504 -0
- data/lib/koala/api/graph_batch_api.rb +167 -0
- data/lib/koala/api/graph_collection.rb +129 -0
- data/lib/koala/api/graph_error_checker.rb +72 -0
- data/lib/koala/api.rb +159 -0
- data/lib/koala/configuration.rb +56 -0
- data/lib/koala/errors.rb +126 -0
- data/lib/koala/http_service/request.rb +133 -0
- data/lib/koala/http_service/response.rb +20 -0
- data/lib/koala/http_service/uploadable_io.rb +183 -0
- data/lib/koala/http_service.rb +108 -0
- data/lib/koala/oauth.rb +342 -0
- data/lib/koala/realtime_updates.rb +151 -0
- data/lib/koala/test_users.rb +189 -0
- data/lib/koala/utils.rb +41 -0
- data/lib/koala/version.rb +3 -0
- data/lib/koala.rb +51 -291
- data/readme.md +269 -21
- data/spec/cases/api_spec.rb +362 -0
- data/spec/cases/configuration_spec.rb +11 -0
- data/spec/cases/error_spec.rb +143 -0
- data/spec/cases/graph_api_batch_spec.rb +788 -0
- data/spec/cases/graph_api_spec.rb +76 -0
- data/spec/cases/graph_collection_spec.rb +192 -0
- data/spec/cases/graph_error_checker_spec.rb +147 -0
- data/spec/cases/http_service/request_spec.rb +250 -0
- data/spec/cases/http_service/response_spec.rb +24 -0
- data/spec/cases/http_service_spec.rb +280 -0
- data/spec/cases/koala_spec.rb +57 -0
- data/spec/cases/koala_test_spec.rb +5 -0
- data/spec/cases/oauth_spec.rb +647 -0
- data/spec/cases/realtime_updates_spec.rb +327 -0
- data/spec/cases/test_users_spec.rb +383 -0
- data/spec/cases/uploadable_io_spec.rb +266 -0
- data/spec/cases/utils_spec.rb +55 -0
- data/spec/fixtures/beach.jpg +0 -0
- data/spec/fixtures/cat.m4v +0 -0
- data/spec/fixtures/facebook_data.yml +63 -0
- data/spec/fixtures/mock_facebook_responses.yml +483 -0
- data/spec/fixtures/vcr_cassettes/app_test_accounts.yml +97 -0
- data/spec/fixtures/vcr_cassettes/friend_list_next_page.yml +121 -0
- data/spec/integration/graph_collection_spec.rb +24 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/custom_matchers.rb +28 -0
- data/spec/support/graph_api_shared_examples.rb +534 -0
- data/spec/support/koala_test.rb +251 -0
- data/spec/support/mock_http_service.rb +140 -0
- data/spec/support/uploadable_io_shared_examples.rb +70 -0
- metadata +206 -62
- data/CHANGELOG +0 -24
- data/init.rb +0 -2
- data/lib/http_services.rb +0 -60
- data/test/facebook_data.yml +0 -5
- data/test/koala/facebook_no_access_token_tests.rb +0 -119
- data/test/koala/facebook_with_access_token_tests.rb +0 -106
- 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
|
data/lib/koala/errors.rb
ADDED
|
@@ -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
|