koala 2.5.0 → 3.0.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 +4 -4
- data/.travis.yml +5 -3
- data/Gemfile +1 -1
- data/ISSUE_TEMPLATE +25 -0
- data/PULL_REQUEST_TEMPLATE +11 -0
- data/changelog.md +66 -4
- data/code_of_conduct.md +64 -12
- data/koala.gemspec +3 -0
- data/lib/koala/api/batch_operation.rb +3 -6
- data/lib/koala/api/{graph_api.rb → graph_api_methods.rb} +13 -102
- data/lib/koala/api/graph_batch_api.rb +112 -65
- data/lib/koala/api/graph_collection.rb +19 -12
- data/lib/koala/api/graph_error_checker.rb +1 -1
- data/lib/koala/api.rb +49 -25
- data/lib/koala/configuration.rb +49 -0
- data/lib/koala/errors.rb +1 -1
- data/lib/koala/http_service/multipart_request.rb +6 -10
- data/lib/koala/http_service/request.rb +135 -0
- data/lib/koala/http_service/response.rb +6 -4
- data/lib/koala/http_service/uploadable_io.rb +0 -4
- data/lib/koala/http_service.rb +18 -76
- data/lib/koala/oauth.rb +7 -7
- data/lib/koala/realtime_updates.rb +26 -21
- data/lib/koala/test_users.rb +9 -8
- data/lib/koala/version.rb +1 -1
- data/lib/koala.rb +6 -8
- data/readme.md +50 -109
- data/spec/cases/api_spec.rb +99 -69
- data/spec/cases/configuration_spec.rb +11 -0
- data/spec/cases/graph_api_batch_spec.rb +73 -42
- data/spec/cases/graph_api_spec.rb +15 -29
- data/spec/cases/graph_collection_spec.rb +47 -34
- data/spec/cases/graph_error_checker_spec.rb +6 -1
- data/spec/cases/http_service/request_spec.rb +242 -0
- data/spec/cases/http_service/response_spec.rb +24 -0
- data/spec/cases/http_service_spec.rb +102 -296
- data/spec/cases/koala_spec.rb +7 -5
- data/spec/cases/oauth_spec.rb +40 -1
- data/spec/cases/realtime_updates_spec.rb +51 -13
- data/spec/cases/test_users_spec.rb +56 -2
- data/spec/cases/uploadable_io_spec.rb +31 -31
- data/spec/fixtures/cat.m4v +0 -0
- data/spec/fixtures/facebook_data.yml +4 -6
- data/spec/fixtures/mock_facebook_responses.yml +29 -69
- data/spec/fixtures/vcr_cassettes/app_test_accounts.yml +97 -0
- data/spec/integration/graph_collection_spec.rb +8 -5
- data/spec/spec_helper.rb +2 -2
- data/spec/support/graph_api_shared_examples.rb +143 -336
- data/spec/support/koala_test.rb +8 -10
- data/spec/support/mock_http_service.rb +9 -9
- data/spec/support/uploadable_io_shared_examples.rb +4 -4
- metadata +31 -11
- data/.autotest +0 -12
- data/Guardfile +0 -6
- data/autotest/discover.rb +0 -1
- data/lib/koala/api/rest_api.rb +0 -135
- data/spec/support/rest_api_shared_examples.rb +0 -168
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
1
|
+
require "koala/api"
|
|
2
|
+
require "koala/api/batch_operation"
|
|
3
3
|
|
|
4
4
|
module Koala
|
|
5
5
|
module Facebook
|
|
6
6
|
# @private
|
|
7
|
-
class GraphBatchAPI
|
|
7
|
+
class GraphBatchAPI
|
|
8
8
|
# inside a batch call we can do anything a regular Graph API can do
|
|
9
9
|
include GraphAPIMethods
|
|
10
10
|
|
|
11
|
+
# Limits from @see https://developers.facebook.com/docs/marketing-api/batch-requests/v2.8
|
|
12
|
+
MAX_CALLS = 50
|
|
13
|
+
|
|
11
14
|
attr_reader :original_api
|
|
12
15
|
def initialize(api)
|
|
13
|
-
super(api.access_token, api.app_secret)
|
|
14
16
|
@original_api = api
|
|
15
17
|
end
|
|
16
18
|
|
|
@@ -18,7 +20,9 @@ module Koala
|
|
|
18
20
|
@batch_calls ||= []
|
|
19
21
|
end
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
# Enqueue a call into the batch for later processing.
|
|
24
|
+
# See API#graph_call
|
|
25
|
+
def graph_call(path, args = {}, verb = "get", options = {}, &post_processing)
|
|
22
26
|
# normalize options for consistency
|
|
23
27
|
options = Koala::Utils.symbolize_hash(options)
|
|
24
28
|
|
|
@@ -34,74 +38,117 @@ module Koala
|
|
|
34
38
|
nil # batch operations return nothing immediately
|
|
35
39
|
end
|
|
36
40
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
alias_method :graph_call, :graph_call_in_batch
|
|
41
|
-
|
|
42
|
-
# execute the queued batch calls
|
|
41
|
+
# execute the queued batch calls. limits it to 50 requests per call.
|
|
42
|
+
# NOTE: if you use `name` and JsonPath references, you should ensure to call `execute` for each
|
|
43
|
+
# co-reference group and that the group size is not greater than the above limits.
|
|
43
44
|
def execute(http_options = {})
|
|
44
|
-
return []
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
args
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
return [] if batch_calls.empty?
|
|
46
|
+
|
|
47
|
+
batch_results = []
|
|
48
|
+
batch_calls.each_slice(MAX_CALLS) do |batch|
|
|
49
|
+
# Turn the call args collected into what facebook expects
|
|
50
|
+
args = {"batch" => batch_args(batch)}
|
|
51
|
+
batch.each do |call|
|
|
52
|
+
args.merge!(call.files || {})
|
|
53
|
+
end
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
raise BadFacebookResponse.new(200, '', "Facebook returned an empty body")
|
|
55
|
+
original_api.graph_call("/", args, "post", http_options) do |response|
|
|
56
|
+
raise bad_response if response.nil?
|
|
57
|
+
|
|
58
|
+
batch_results += generate_results(response, batch)
|
|
57
59
|
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
batch_results
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def generate_results(response, batch)
|
|
66
|
+
index = 0
|
|
67
|
+
response.map do |call_result|
|
|
68
|
+
batch_op = batch[index]
|
|
69
|
+
index += 1
|
|
70
|
+
post_process = batch_op.post_processing
|
|
58
71
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if call_result
|
|
68
|
-
parsed_headers = if call_result.has_key?('headers')
|
|
69
|
-
call_result['headers'].inject({}) { |headers, h| headers[h['name']] = h['value']; headers}
|
|
70
|
-
else
|
|
71
|
-
{}
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
if (error = check_response(call_result['code'], call_result['body'].to_s, parsed_headers))
|
|
75
|
-
raw_result = error
|
|
76
|
-
else
|
|
77
|
-
# (see note in regular api method about JSON parsing)
|
|
78
|
-
body = JSON.load("[#{call_result['body'].to_s}]")[0]
|
|
79
|
-
|
|
80
|
-
# Get the HTTP component they want
|
|
81
|
-
raw_result = case batch_op.http_options[:http_component]
|
|
82
|
-
when :status
|
|
83
|
-
call_result["code"].to_i
|
|
84
|
-
when :headers
|
|
85
|
-
# facebook returns the headers as an array of k/v pairs, but we want a regular hash
|
|
86
|
-
parsed_headers
|
|
87
|
-
else
|
|
88
|
-
body
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# turn any results that are pageable into GraphCollections
|
|
94
|
-
# and pass to post-processing callback if given
|
|
95
|
-
result = GraphCollection.evaluate(raw_result, @original_api)
|
|
96
|
-
if batch_op.post_processing
|
|
97
|
-
batch_op.post_processing.call(result)
|
|
98
|
-
else
|
|
99
|
-
result
|
|
100
|
-
end
|
|
72
|
+
# turn any results that are pageable into GraphCollections
|
|
73
|
+
result = result_from_response(call_result, batch_op)
|
|
74
|
+
|
|
75
|
+
# and pass to post-processing callback if given
|
|
76
|
+
if post_process
|
|
77
|
+
post_process.call(result)
|
|
78
|
+
else
|
|
79
|
+
result
|
|
101
80
|
end
|
|
102
81
|
end
|
|
103
82
|
end
|
|
104
83
|
|
|
84
|
+
def bad_response
|
|
85
|
+
# Facebook sometimes reportedly returns an empty body at times
|
|
86
|
+
BadFacebookResponse.new(200, "", "Facebook returned an empty body")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def result_from_response(response, options)
|
|
90
|
+
return nil if response.nil?
|
|
91
|
+
|
|
92
|
+
headers = headers_from_response(response)
|
|
93
|
+
error = error_from_response(response, headers)
|
|
94
|
+
component = options.http_options[:http_component]
|
|
95
|
+
|
|
96
|
+
error || desired_component(
|
|
97
|
+
component: component,
|
|
98
|
+
response: response,
|
|
99
|
+
headers: headers
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def headers_from_response(response)
|
|
104
|
+
headers = response.fetch("headers", [])
|
|
105
|
+
|
|
106
|
+
headers.inject({}) do |compiled_headers, header|
|
|
107
|
+
compiled_headers.merge(header.fetch("name") => header.fetch("value"))
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def error_from_response(response, headers)
|
|
112
|
+
code = response["code"]
|
|
113
|
+
body = response["body"].to_s
|
|
114
|
+
|
|
115
|
+
GraphErrorChecker.new(code, body, headers).error_if_appropriate
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def batch_args(calls_for_batch)
|
|
119
|
+
calls = calls_for_batch.map do |batch_op|
|
|
120
|
+
batch_op.to_batch_params(access_token, app_secret)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
JSON.dump calls
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def json_body(response)
|
|
127
|
+
# quirks_mode is needed because Facebook sometimes returns a raw true or false value --
|
|
128
|
+
# in Ruby 2.4 we can drop that.
|
|
129
|
+
JSON.parse(response.fetch("body"), quirks_mode: true)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def desired_component(component:, response:, headers:)
|
|
133
|
+
result = Koala::HTTPService::Response.new(response['status'], response['body'], headers)
|
|
134
|
+
|
|
135
|
+
# Get the HTTP component they want
|
|
136
|
+
case component
|
|
137
|
+
when :status then response["code"].to_i
|
|
138
|
+
# facebook returns the headers as an array of k/v pairs, but we want a regular hash
|
|
139
|
+
when :headers then headers
|
|
140
|
+
# (see note in regular api method about JSON parsing)
|
|
141
|
+
else GraphCollection.evaluate(result, original_api)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def access_token
|
|
146
|
+
original_api.access_token
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def app_secret
|
|
150
|
+
original_api.app_secret
|
|
151
|
+
end
|
|
105
152
|
end
|
|
106
153
|
end
|
|
107
154
|
end
|
|
@@ -16,30 +16,41 @@ module Koala
|
|
|
16
16
|
attr_reader :api
|
|
17
17
|
# The entire raw response from Facebook.
|
|
18
18
|
attr_reader :raw_response
|
|
19
|
+
# The headers from the Facebook response
|
|
20
|
+
attr_reader :headers
|
|
19
21
|
|
|
20
22
|
# Initialize the array of results and store various additional paging-related information.
|
|
21
23
|
#
|
|
22
|
-
# @param
|
|
24
|
+
# @param [Koala::HTTPService::Response] response object wrapping the raw Facebook response
|
|
23
25
|
# @param api the Graph {Koala::Facebook::API API} instance to use to make calls
|
|
24
26
|
# (usually the API that made the original call).
|
|
25
27
|
#
|
|
26
|
-
# @return [Koala::Facebook::GraphCollection] an initialized GraphCollection
|
|
28
|
+
# @return [Koala::Facebook::API::GraphCollection] an initialized GraphCollection
|
|
27
29
|
# whose paging, summary, raw_response, and api attributes are populated.
|
|
28
30
|
def initialize(response, api)
|
|
29
|
-
super response["data"]
|
|
30
|
-
@paging = response["paging"]
|
|
31
|
-
@summary = response["summary"]
|
|
32
|
-
@raw_response = response
|
|
31
|
+
super response.data["data"]
|
|
32
|
+
@paging = response.data["paging"]
|
|
33
|
+
@summary = response.data["summary"]
|
|
34
|
+
@raw_response = response.data
|
|
33
35
|
@api = api
|
|
36
|
+
@headers = response.headers
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
# @private
|
|
37
40
|
# Turn the response into a GraphCollection if they're pageable;
|
|
38
|
-
# if not, return the original response.
|
|
41
|
+
# if not, return the data of the original response.
|
|
39
42
|
# The Ads API (uniquely so far) returns a hash rather than an array when queried
|
|
40
43
|
# with get_connections.
|
|
41
44
|
def self.evaluate(response, api)
|
|
42
|
-
|
|
45
|
+
return nil if response.nil?
|
|
46
|
+
|
|
47
|
+
is_pageable?(response) ? self.new(response, api) : response.data
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# response will always be an instance of Koala::HTTPService::Response
|
|
51
|
+
# since that is what we get from Koala::Facebook::API#api
|
|
52
|
+
def self.is_pageable?(response)
|
|
53
|
+
response.data.is_a?(Hash) && response.data["data"].is_a?(Array)
|
|
43
54
|
end
|
|
44
55
|
|
|
45
56
|
# Retrieve the next page of results.
|
|
@@ -113,9 +124,5 @@ module Koala
|
|
|
113
124
|
end
|
|
114
125
|
end
|
|
115
126
|
end
|
|
116
|
-
|
|
117
|
-
# @private
|
|
118
|
-
# legacy support for when GraphCollection lived directly under Koala::Facebook
|
|
119
|
-
GraphCollection = API::GraphCollection
|
|
120
127
|
end
|
|
121
128
|
end
|
data/lib/koala/api.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# graph_batch_api and legacy are required at the bottom, since they depend on API being defined
|
|
2
|
-
require 'koala/api/
|
|
3
|
-
require 'koala/api/
|
|
2
|
+
require 'koala/api/graph_api_methods'
|
|
3
|
+
require 'koala/api/graph_collection'
|
|
4
4
|
require 'openssl'
|
|
5
5
|
|
|
6
6
|
module Koala
|
|
@@ -15,7 +15,7 @@ module Koala
|
|
|
15
15
|
# https://developers.facebook.com/docs/graph-api/securing-requests/)
|
|
16
16
|
# @note If no access token is provided, you can only access some public information.
|
|
17
17
|
# @return [Koala::Facebook::API] the API client
|
|
18
|
-
def initialize(access_token =
|
|
18
|
+
def initialize(access_token = Koala.config.access_token, app_secret = Koala.config.app_secret)
|
|
19
19
|
@access_token = access_token
|
|
20
20
|
@app_secret = app_secret
|
|
21
21
|
end
|
|
@@ -23,13 +23,50 @@ module Koala
|
|
|
23
23
|
attr_reader :access_token, :app_secret
|
|
24
24
|
|
|
25
25
|
include GraphAPIMethods
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
# Make a call directly to the Graph API.
|
|
28
|
+
# (See any of the other methods for example invocations.)
|
|
29
|
+
#
|
|
30
|
+
# @param path the Graph API path to query (no leading / needed)
|
|
31
|
+
# @param args (see #get_object)
|
|
32
|
+
# @param verb the type of HTTP request to make (get, post, delete, etc.)
|
|
33
|
+
# @options (see #get_object)
|
|
34
|
+
#
|
|
35
|
+
# @yield response when making a batch API call, you can pass in a block
|
|
36
|
+
# that parses the results, allowing for cleaner code.
|
|
37
|
+
# The block's return value is returned in the batch results.
|
|
38
|
+
# See the code for {#get_picture} for examples.
|
|
39
|
+
# (Not needed in regular calls; you'll probably rarely use this.)
|
|
40
|
+
#
|
|
41
|
+
# @raise [Koala::Facebook::APIError] if Facebook returns an error
|
|
42
|
+
#
|
|
43
|
+
# @return the result from Facebook
|
|
44
|
+
def graph_call(path, args = {}, verb = "get", options = {}, &post_processing)
|
|
45
|
+
# enable appsecret_proof by default
|
|
46
|
+
options = {:appsecret_proof => true}.merge(options) if @app_secret
|
|
47
|
+
response = api(path, args, verb, options)
|
|
48
|
+
|
|
49
|
+
error = GraphErrorChecker.new(response.status, response.body, response.headers).error_if_appropriate
|
|
50
|
+
raise error if error
|
|
51
|
+
|
|
52
|
+
# if we want a component other than the body (e.g. redirect header for images), provide that
|
|
53
|
+
http_component = options[:http_component]
|
|
54
|
+
desired_data = if options[:http_component]
|
|
55
|
+
http_component == :response ? response : response.send(http_component)
|
|
56
|
+
else
|
|
57
|
+
# turn this into a GraphCollection if it's pageable
|
|
58
|
+
API::GraphCollection.evaluate(response, self)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# now process as appropriate for the given call (get picture header, etc.)
|
|
62
|
+
post_processing ? post_processing.call(desired_data) : desired_data
|
|
63
|
+
end
|
|
64
|
+
|
|
27
65
|
|
|
28
66
|
# Makes a request to the appropriate Facebook API.
|
|
29
67
|
# @note You'll rarely need to call this method directly.
|
|
30
68
|
#
|
|
31
69
|
# @see GraphAPIMethods#graph_call
|
|
32
|
-
# @see RestAPIMethods#rest_call
|
|
33
70
|
#
|
|
34
71
|
# @param path the server path for this request (leading / is prepended if not present)
|
|
35
72
|
# @param args arguments to be sent to Facebook
|
|
@@ -44,14 +81,10 @@ module Koala
|
|
|
44
81
|
# @option options [Boolean] :beta use Facebook's beta tier
|
|
45
82
|
# @option options [Boolean] :use_ssl force SSL for this request, even if it's tokenless.
|
|
46
83
|
# (All API requests with access tokens use SSL.)
|
|
47
|
-
# @param error_checking_block a block to evaluate the response status for additional JSON-encoded errors
|
|
48
|
-
#
|
|
49
|
-
# @yield The response for evaluation
|
|
50
|
-
#
|
|
51
84
|
# @raise [Koala::Facebook::ServerError] if Facebook returns an error (response status >= 500)
|
|
52
85
|
#
|
|
53
|
-
# @return
|
|
54
|
-
def api(path, args = {}, verb = "get", options = {}
|
|
86
|
+
# @return a Koala::HTTPService::Response object representing the returned Facebook data
|
|
87
|
+
def api(path, args = {}, verb = "get", options = {})
|
|
55
88
|
# we make a copy of args so the modifications (added access_token & appsecret_proof)
|
|
56
89
|
# do not affect the received argument
|
|
57
90
|
args = args.dup
|
|
@@ -60,6 +93,7 @@ module Koala
|
|
|
60
93
|
# This is explicitly needed in batch requests so GraphCollection
|
|
61
94
|
# results preserve any specific access tokens provided
|
|
62
95
|
args["access_token"] ||= @access_token || @app_access_token if @access_token || @app_access_token
|
|
96
|
+
|
|
63
97
|
if options.delete(:appsecret_proof) && args["access_token"] && @app_secret
|
|
64
98
|
args["appsecret_proof"] = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), @app_secret, args["access_token"])
|
|
65
99
|
end
|
|
@@ -77,18 +111,7 @@ module Koala
|
|
|
77
111
|
raise Koala::Facebook::ServerError.new(result.status.to_i, result.body)
|
|
78
112
|
end
|
|
79
113
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# if we want a component other than the body (e.g. redirect header for images), return that
|
|
83
|
-
if component = options[:http_component]
|
|
84
|
-
component == :response ? result : result.send(options[:http_component])
|
|
85
|
-
else
|
|
86
|
-
# parse the body as JSON and run it through the error checker (if provided)
|
|
87
|
-
# Note: Facebook sometimes sends results like "true" and "false", which are valid[RFC7159]
|
|
88
|
-
# but unsupported by Ruby's stdlib[RFC4627] and cause JSON.load to fail -- so we account for
|
|
89
|
-
# that by wrapping the result in []
|
|
90
|
-
JSON.load("[#{result.body.to_s}]")[0]
|
|
91
|
-
end
|
|
114
|
+
result
|
|
92
115
|
end
|
|
93
116
|
|
|
94
117
|
private
|
|
@@ -114,8 +137,9 @@ module Koala
|
|
|
114
137
|
def preserve_form_arguments?(options)
|
|
115
138
|
options[:format] == :json || options[:preserve_form_arguments] || Koala.config.preserve_form_arguments
|
|
116
139
|
end
|
|
140
|
+
|
|
141
|
+
def check_response(http_status, body, headers)
|
|
142
|
+
end
|
|
117
143
|
end
|
|
118
144
|
end
|
|
119
145
|
end
|
|
120
|
-
|
|
121
|
-
require 'koala/api/graph_batch_api'
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
# Certain Facebook services (beta, video) require you to access different
|
|
32
|
+
# servers. If you're using your own servers, for instance, for a proxy,
|
|
33
|
+
# you can change both the matcher (what value to change when updating the URL) and the
|
|
34
|
+
# replacement values (what to add).
|
|
35
|
+
#
|
|
36
|
+
# So, for instance, to use the beta stack, we match on .facebook and change it to .beta.facebook.
|
|
37
|
+
# If you're talking to fbproxy.mycompany.com, you could set up beta.fbproxy.mycompany.com for
|
|
38
|
+
# FB's beta tier, and set the matcher to /\.fbproxy/ and the beta_replace to '.beta.fbproxy'.
|
|
39
|
+
attr_accessor :host_path_matcher
|
|
40
|
+
attr_accessor :video_replace
|
|
41
|
+
attr_accessor :beta_replace
|
|
42
|
+
|
|
43
|
+
def initialize
|
|
44
|
+
# Default to our default values.
|
|
45
|
+
Koala::HTTPService::DEFAULT_SERVERS.each_pair do |key, value|
|
|
46
|
+
self.public_send("#{key}=", value)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/koala/errors.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require 'faraday'
|
|
2
2
|
|
|
3
|
-
module Koala
|
|
3
|
+
module Koala
|
|
4
4
|
module HTTPService
|
|
5
5
|
class MultipartRequest < Faraday::Request::Multipart
|
|
6
6
|
# Facebook expects nested parameters to be passed in a certain way
|
|
@@ -8,15 +8,15 @@ module Koala
|
|
|
8
8
|
# Faraday needs two changes to make that work:
|
|
9
9
|
# 1) [] need to be escaped (e.g. params[foo]=bar ==> params%5Bfoo%5D=bar)
|
|
10
10
|
# 2) such messages need to be multipart-encoded
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
self.mime_type = 'multipart/form-data'.freeze
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
def process_request?(env)
|
|
15
15
|
# if the request values contain any hashes or arrays, multipart it
|
|
16
16
|
super || !!(env[:body].respond_to?(:values) && env[:body].values.find {|v| v.is_a?(Hash) || v.is_a?(Array)})
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
|
|
20
20
|
def process_params(params, prefix = nil, pieces = nil, &block)
|
|
21
21
|
params.inject(pieces || []) do |all, (key, value)|
|
|
22
22
|
key = "#{prefix}%5B#{key}%5D" if prefix
|
|
@@ -34,8 +34,4 @@ module Koala
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
|
-
|
|
38
|
-
# @private
|
|
39
|
-
# legacy support for when MultipartRequest lived directly under Koala
|
|
40
|
-
MultipartRequest = HTTPService::MultipartRequest
|
|
41
37
|
end
|
|
@@ -0,0 +1,135 @@
|
|
|
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 if there's a token
|
|
110
|
+
return opts unless raw_args["access_token"]
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
use_ssl: true,
|
|
114
|
+
ssl: {verify: true}.merge(opts[:ssl] || {})
|
|
115
|
+
}.merge(opts)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Determines whether a given path already contains an API version.
|
|
119
|
+
#
|
|
120
|
+
# @param path the URL path.
|
|
121
|
+
#
|
|
122
|
+
# @return true or false accordingly.
|
|
123
|
+
def path_contains_api_version?
|
|
124
|
+
# looks for "/$MAJOR[.$MINOR]/" in the path
|
|
125
|
+
match = /^\/?(v\d+(?:\.\d+)?)\//.match(raw_path)
|
|
126
|
+
!!(match && match[1])
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def replace_server_component(host, condition_met, replacement)
|
|
130
|
+
return host unless condition_met
|
|
131
|
+
host.gsub(Koala.config.host_path_matcher, replacement)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -9,10 +9,12 @@ module Koala
|
|
|
9
9
|
@body = body
|
|
10
10
|
@headers = headers
|
|
11
11
|
end
|
|
12
|
+
|
|
13
|
+
def data
|
|
14
|
+
# quirks_mode is needed because Facebook sometimes returns a raw true or false value --
|
|
15
|
+
# in Ruby 2.4 we can drop that.
|
|
16
|
+
@data ||= JSON.parse(body, quirks_mode: true) unless body.empty?
|
|
17
|
+
end
|
|
12
18
|
end
|
|
13
19
|
end
|
|
14
|
-
|
|
15
|
-
# @private
|
|
16
|
-
# legacy support for when Response lived directly under Koala
|
|
17
|
-
Response = HTTPService::Response
|
|
18
20
|
end
|