koala 1.2.1 → 1.3.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.
- data/.gitignore +3 -1
- data/.rspec +1 -0
- data/.travis.yml +4 -0
- data/.yardopts +3 -0
- data/CHANGELOG +28 -0
- data/Gemfile +14 -0
- data/Guardfile +6 -0
- data/koala.gemspec +3 -3
- data/lib/koala/api/batch_operation.rb +83 -0
- data/lib/koala/api/graph_api.rb +476 -0
- data/lib/koala/{graph_batch_api.rb → api/graph_batch_api.rb} +22 -17
- data/lib/koala/api/graph_collection.rb +107 -0
- data/lib/koala/api/legacy.rb +26 -0
- data/lib/koala/{rest_api.rb → api/rest_api.rb} +34 -13
- data/lib/koala/api.rb +93 -0
- data/lib/koala/http_service/multipart_request.rb +41 -0
- data/lib/koala/http_service/response.rb +18 -0
- data/lib/koala/http_service/uploadable_io.rb +187 -0
- data/lib/koala/http_service.rb +69 -20
- data/lib/koala/oauth.rb +170 -36
- data/lib/koala/realtime_updates.rb +89 -51
- data/lib/koala/test_users.rb +122 -32
- data/lib/koala/utils.rb +11 -4
- data/lib/koala/version.rb +1 -1
- data/lib/koala.rb +16 -96
- data/readme.md +9 -9
- data/spec/cases/api_spec.rb +19 -12
- data/spec/cases/error_spec.rb +10 -0
- data/spec/cases/graph_api_batch_spec.rb +100 -58
- data/spec/cases/graph_collection_spec.rb +23 -7
- data/spec/cases/http_service_spec.rb +5 -26
- data/spec/cases/koala_spec.rb +22 -4
- data/spec/cases/legacy_spec.rb +115 -0
- data/spec/cases/multipart_request_spec.rb +7 -7
- data/spec/cases/oauth_spec.rb +134 -48
- data/spec/cases/realtime_updates_spec.rb +154 -47
- data/spec/cases/test_users_spec.rb +276 -219
- data/spec/cases/uploadable_io_spec.rb +1 -1
- data/spec/cases/utils_spec.rb +29 -5
- data/spec/fixtures/mock_facebook_responses.yml +41 -30
- data/spec/spec_helper.rb +3 -0
- data/spec/support/custom_matchers.rb +28 -0
- data/spec/support/graph_api_shared_examples.rb +192 -14
- data/spec/support/koala_test.rb +10 -1
- data/spec/support/mock_http_service.rb +2 -2
- data/spec/support/rest_api_shared_examples.rb +5 -165
- metadata +75 -99
- data/lib/koala/batch_operation.rb +0 -74
- data/lib/koala/graph_api.rb +0 -270
- data/lib/koala/graph_collection.rb +0 -59
- data/lib/koala/multipart_request.rb +0 -35
- data/lib/koala/uploadable_io.rb +0 -181
- data/spec/cases/graph_and_rest_api_spec.rb +0 -22
- data/spec/cases/graph_api_spec.rb +0 -22
- data/spec/cases/rest_api_spec.rb +0 -22
|
@@ -1,22 +1,17 @@
|
|
|
1
|
+
require 'koala/api'
|
|
2
|
+
require 'koala/api/batch_operation'
|
|
3
|
+
|
|
1
4
|
module Koala
|
|
2
5
|
module Facebook
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
6
|
+
# @private
|
|
7
|
+
class GraphBatchAPI < API
|
|
8
|
+
# inside a batch call we can do anything a regular Graph API can do
|
|
9
|
+
include GraphAPIMethods
|
|
16
10
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
attr_reader :original_api
|
|
12
|
+
def initialize(access_token, api)
|
|
13
|
+
super(access_token)
|
|
14
|
+
@original_api = api
|
|
20
15
|
end
|
|
21
16
|
|
|
22
17
|
def batch_calls
|
|
@@ -38,12 +33,22 @@ module Koala
|
|
|
38
33
|
|
|
39
34
|
def check_graph_batch_api_response(response)
|
|
40
35
|
if response.is_a?(Hash) && response["error"] && !response["error"].is_a?(Hash)
|
|
41
|
-
|
|
36
|
+
# old error format -- see http://developers.facebook.com/blog/post/596/
|
|
37
|
+
APIError.new({"type" => "Error #{response["error"]}", "message" => response["error_description"]}.merge(response))
|
|
42
38
|
else
|
|
43
39
|
check_graph_api_response(response)
|
|
44
40
|
end
|
|
45
41
|
end
|
|
46
42
|
|
|
43
|
+
# redefine the graph_call and check_response methods
|
|
44
|
+
# so we can use this API inside the batch block just like any regular Graph API
|
|
45
|
+
alias_method :graph_call_outside_batch, :graph_call
|
|
46
|
+
alias_method :graph_call, :graph_call_in_batch
|
|
47
|
+
|
|
48
|
+
alias_method :check_graph_api_response, :check_response
|
|
49
|
+
alias_method :check_response, :check_graph_batch_api_response
|
|
50
|
+
|
|
51
|
+
# execute the queued batch calls
|
|
47
52
|
def execute(http_options = {})
|
|
48
53
|
return [] unless batch_calls.length > 0
|
|
49
54
|
# Turn the call args collected into what facebook expects
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module Koala
|
|
2
|
+
module Facebook
|
|
3
|
+
class API
|
|
4
|
+
# A light wrapper for collections returned from the Graph API.
|
|
5
|
+
# It extends Array to allow you to page backward and forward through
|
|
6
|
+
# result sets, and providing easy access to paging information.
|
|
7
|
+
class GraphCollection < Array
|
|
8
|
+
|
|
9
|
+
# The raw paging information from Facebook (next/previous URLs).
|
|
10
|
+
attr_reader :paging
|
|
11
|
+
# @return [Koala::Facebook::GraphAPI] the api used to make requests.
|
|
12
|
+
attr_reader :api
|
|
13
|
+
# The entire raw response from Facebook.
|
|
14
|
+
attr_reader :raw_response
|
|
15
|
+
|
|
16
|
+
# Initialize the array of results and store various additional paging-related information.
|
|
17
|
+
#
|
|
18
|
+
# @param response the response from Facebook (a hash whose "data" key is an array)
|
|
19
|
+
# @param api the Graph {Koala::Facebook::API API} instance to use to make calls
|
|
20
|
+
# (usually the API that made the original call).
|
|
21
|
+
#
|
|
22
|
+
# @return [Koala::Facebook::GraphCollection] an initialized GraphCollection
|
|
23
|
+
# whose paging, raw_response, and api attributes are populated.
|
|
24
|
+
def initialize(response, api)
|
|
25
|
+
super response["data"]
|
|
26
|
+
@paging = response["paging"]
|
|
27
|
+
@raw_response = response
|
|
28
|
+
@api = api
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @private
|
|
32
|
+
# Turn the response into a GraphCollection if they're pageable;
|
|
33
|
+
# if not, return the original response.
|
|
34
|
+
# The Ads API (uniquely so far) returns a hash rather than an array when queried
|
|
35
|
+
# with get_connections.
|
|
36
|
+
def self.evaluate(response, api)
|
|
37
|
+
response.is_a?(Hash) && response["data"].is_a?(Array) ? self.new(response, api) : response
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Retrieve the next page of results.
|
|
41
|
+
#
|
|
42
|
+
# @return a GraphCollection array of additional results (an empty array if there are no more results)
|
|
43
|
+
def next_page
|
|
44
|
+
base, args = next_page_params
|
|
45
|
+
base ? @api.get_page([base, args]) : nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Retrieve the previous page of results.
|
|
49
|
+
#
|
|
50
|
+
# @return a GraphCollection array of additional results (an empty array if there are no earlier results)
|
|
51
|
+
def previous_page
|
|
52
|
+
base, args = previous_page_params
|
|
53
|
+
base ? @api.get_page([base, args]) : nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Arguments that can be sent to {Koala::Facebook::API#graph_call} to retrieve the next page of results.
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# @api.graph_call(*collection.next_page_params)
|
|
60
|
+
#
|
|
61
|
+
# @return an array of arguments, or nil if there are no more pages
|
|
62
|
+
def next_page_params
|
|
63
|
+
@paging && @paging["next"] ? parse_page_url(@paging["next"]) : nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Arguments that can be sent to {Koala::Facebook::API#graph_call} to retrieve the previous page of results.
|
|
67
|
+
#
|
|
68
|
+
# @example
|
|
69
|
+
# @api.graph_call(*collection.previous_page_params)
|
|
70
|
+
#
|
|
71
|
+
# @return an array of arguments, or nil if there are no previous pages
|
|
72
|
+
def previous_page_params
|
|
73
|
+
@paging && @paging["previous"] ? parse_page_url(@paging["previous"]) : nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @private
|
|
77
|
+
def parse_page_url(url)
|
|
78
|
+
GraphCollection.parse_page_url(url)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Parse the previous and next page URLs Facebook provides in pageable results.
|
|
82
|
+
# You'll mainly need to use this when using a non-Rails framework (one without url_for);
|
|
83
|
+
# to store paging information between page loads, pass the URL (from GraphCollection#paging)
|
|
84
|
+
# and use parse_page_url to turn it into parameters useful for {Koala::Facebook::API#get_page}.
|
|
85
|
+
#
|
|
86
|
+
# @param url the paging URL to turn into graph_call parameters
|
|
87
|
+
#
|
|
88
|
+
# @return an array of parameters that can be provided via graph_call(*parsed_params)
|
|
89
|
+
def self.parse_page_url(url)
|
|
90
|
+
match = url.match(/.com\/(.*)\?(.*)/)
|
|
91
|
+
base = match[1]
|
|
92
|
+
args = match[2]
|
|
93
|
+
params = CGI.parse(args)
|
|
94
|
+
new_params = {}
|
|
95
|
+
params.each_pair do |key,value|
|
|
96
|
+
new_params[key] = value.join ","
|
|
97
|
+
end
|
|
98
|
+
[base,new_params]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @private
|
|
104
|
+
# legacy support for when GraphCollection lived directly under Koala::Facebook
|
|
105
|
+
GraphCollection = API::GraphCollection
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'koala/api'
|
|
2
|
+
module Koala
|
|
3
|
+
module Facebook
|
|
4
|
+
# Legacy support for old pre-1.2 APIs
|
|
5
|
+
|
|
6
|
+
# A wrapper for the old APIs deprecated in 1.2.0, which triggers a deprecation warning when used.
|
|
7
|
+
# Otherwise, this class functions identically to API.
|
|
8
|
+
# @see API
|
|
9
|
+
# @private
|
|
10
|
+
class OldAPI < API
|
|
11
|
+
def initialize(*args)
|
|
12
|
+
Koala::Utils.deprecate("#{self.class.name} is deprecated and will be removed in a future version; please use the API class instead.")
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @private
|
|
18
|
+
class GraphAPI < OldAPI; end
|
|
19
|
+
|
|
20
|
+
# @private
|
|
21
|
+
class RestAPI < OldAPI; end
|
|
22
|
+
|
|
23
|
+
# @private
|
|
24
|
+
class GraphAndRestAPI < OldAPI; end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -2,27 +2,47 @@ module Koala
|
|
|
2
2
|
module Facebook
|
|
3
3
|
REST_SERVER = "api.facebook.com"
|
|
4
4
|
|
|
5
|
+
# Methods used to interact with Facebook's legacy REST API.
|
|
6
|
+
# Where possible, you should use the newer, faster Graph API to interact with Facebook;
|
|
7
|
+
# in the future, the REST API will be deprecated.
|
|
8
|
+
# For now, though, there are a few methods that can't be done through the Graph API.
|
|
9
|
+
#
|
|
10
|
+
# When using the REST API, Koala will use Facebook's faster read-only servers
|
|
11
|
+
# whenever the call allows.
|
|
12
|
+
#
|
|
13
|
+
# See https://github.com/arsduo/koala/wiki/REST-API for a general introduction to Koala
|
|
14
|
+
# and the Rest API.
|
|
5
15
|
module RestAPIMethods
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
results.inject({}) {|outcome, data| outcome[data["name"]] = data["fql_result_set"]; outcome}
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
16
|
+
# Set a Facebook application's properties.
|
|
17
|
+
#
|
|
18
|
+
# @param properties a hash of properties you want to update with their new values.
|
|
19
|
+
# @param (see #rest_call)
|
|
20
|
+
# @param options (see #rest_call)
|
|
21
|
+
#
|
|
22
|
+
# @return true if successful, false if not. (This call currently doesn't give useful feedback on failure.)
|
|
17
23
|
def set_app_properties(properties, args = {}, options = {})
|
|
18
24
|
raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "setAppProperties requires an access token"}) unless @access_token
|
|
19
25
|
rest_call("admin.setAppProperties", args.merge(:properties => MultiJson.encode(properties)), options, "post")
|
|
20
26
|
end
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
# Make a call to the REST API.
|
|
29
|
+
#
|
|
30
|
+
# @note The order of the last two arguments is non-standard (for historical reasons). Sorry.
|
|
31
|
+
#
|
|
32
|
+
# @param fb_method the API call you want to make
|
|
33
|
+
# @param args (see Koala::Facebook::GraphAPIMethods#graph_call)
|
|
34
|
+
# @param options (see Koala::Facebook::GraphAPIMethods#graph_call)
|
|
35
|
+
# @param verb (see Koala::Facebook::GraphAPIMethods#graph_call)
|
|
36
|
+
#
|
|
37
|
+
# @raise [Koala::Facebook::APIError] if Facebook returns an error
|
|
38
|
+
#
|
|
39
|
+
# @return the result from Facebook
|
|
40
|
+
def rest_call(fb_method, args = {}, options = {}, verb = "get")
|
|
41
|
+
Koala::Utils.deprecate("The REST API is now deprecated; please use the equivalent Graph API methods instead. See http://developers.facebook.com/blog/post/616/.")
|
|
42
|
+
|
|
23
43
|
options = options.merge!(:rest_api => true, :read_only => READ_ONLY_METHODS.include?(fb_method.to_s))
|
|
24
44
|
|
|
25
|
-
api("method/#{fb_method}", args.merge('format' => 'json'),
|
|
45
|
+
api("method/#{fb_method}", args.merge('format' => 'json'), verb, options) do |response|
|
|
26
46
|
# check for REST API-specific errors
|
|
27
47
|
if response.is_a?(Hash) && response["error_code"]
|
|
28
48
|
raise APIError.new("type" => response["error_code"], "message" => response["error_msg"])
|
|
@@ -30,6 +50,7 @@ module Koala
|
|
|
30
50
|
end
|
|
31
51
|
end
|
|
32
52
|
|
|
53
|
+
# @private
|
|
33
54
|
# read-only methods for which we can use API-read
|
|
34
55
|
# taken directly from the FB PHP library (https://github.com/facebook/php-sdk/blob/master/src/facebook.php)
|
|
35
56
|
READ_ONLY_METHODS = [
|
data/lib/koala/api.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# graph_batch_api and legacy are required at the bottom, since they depend on API being defined
|
|
2
|
+
require 'koala/api/graph_api'
|
|
3
|
+
require 'koala/api/rest_api'
|
|
4
|
+
|
|
5
|
+
module Koala
|
|
6
|
+
module Facebook
|
|
7
|
+
class API
|
|
8
|
+
# Creates a new API client.
|
|
9
|
+
# @param [String] access_token access token
|
|
10
|
+
# @note If no access token is provided, you can only access some public information.
|
|
11
|
+
# @return [Koala::Facebook::API] the API client
|
|
12
|
+
def initialize(access_token = nil)
|
|
13
|
+
@access_token = access_token
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :access_token
|
|
17
|
+
|
|
18
|
+
include GraphAPIMethods
|
|
19
|
+
include RestAPIMethods
|
|
20
|
+
|
|
21
|
+
# Makes a request to the appropriate Facebook API.
|
|
22
|
+
# @note You'll rarely need to call this method directly.
|
|
23
|
+
#
|
|
24
|
+
# @see GraphAPIMethods#graph_call
|
|
25
|
+
# @see RestAPIMethods#rest_call
|
|
26
|
+
#
|
|
27
|
+
# @param path the server path for this request (leading / is prepended if not present)
|
|
28
|
+
# @param args arguments to be sent to Facebook
|
|
29
|
+
# @param verb the HTTP method to use
|
|
30
|
+
# @param options request-related options for Koala and Faraday.
|
|
31
|
+
# See https://github.com/arsduo/koala/wiki/HTTP-Services for additional options.
|
|
32
|
+
# @option options [Symbol] :http_component which part of the response (headers, body, or status) to return
|
|
33
|
+
# @option options [Boolean] :beta use Facebook's beta tier
|
|
34
|
+
# @option options [Boolean] :use_ssl force SSL for this request, even if it's tokenless.
|
|
35
|
+
# (All API requests with access tokens use SSL.)
|
|
36
|
+
# @param error_checking_block a block to evaluate the response status for additional JSON-encoded errors
|
|
37
|
+
#
|
|
38
|
+
# @yield The response body for evaluation
|
|
39
|
+
#
|
|
40
|
+
# @raise [Koala::Facebook::APIError] if Facebook returns an error (response status >= 500)
|
|
41
|
+
#
|
|
42
|
+
# @return the body of the response from Facebook (unless another http_component is requested)
|
|
43
|
+
def api(path, args = {}, verb = "get", options = {}, &error_checking_block)
|
|
44
|
+
# Fetches the given path in the Graph API.
|
|
45
|
+
args["access_token"] = @access_token || @app_access_token if @access_token || @app_access_token
|
|
46
|
+
|
|
47
|
+
# add a leading /
|
|
48
|
+
path = "/#{path}" unless path =~ /^\//
|
|
49
|
+
|
|
50
|
+
# make the request via the provided service
|
|
51
|
+
result = Koala.make_request(path, args, verb, options)
|
|
52
|
+
|
|
53
|
+
# Check for any 500 errors before parsing the body
|
|
54
|
+
# since we're not guaranteed that the body is valid JSON
|
|
55
|
+
# in the case of a server error
|
|
56
|
+
raise APIError.new({"type" => "HTTP #{result.status.to_s}", "message" => "Response body: #{result.body}"}) if result.status >= 500
|
|
57
|
+
|
|
58
|
+
# parse the body as JSON and run it through the error checker (if provided)
|
|
59
|
+
# Note: Facebook sometimes sends results like "true" and "false", which aren't strictly objects
|
|
60
|
+
# and cause MultiJson.decode to fail -- so we account for that by wrapping the result in []
|
|
61
|
+
body = MultiJson.decode("[#{result.body.to_s}]")[0]
|
|
62
|
+
yield body if error_checking_block
|
|
63
|
+
|
|
64
|
+
# if we want a component other than the body (e.g. redirect header for images), return that
|
|
65
|
+
if component = options[:http_component]
|
|
66
|
+
component == :response ? result : result.send(options[:http_component])
|
|
67
|
+
else
|
|
68
|
+
body
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class APIError < StandardError
|
|
74
|
+
attr_accessor :fb_error_type, :raw_response
|
|
75
|
+
|
|
76
|
+
# Creates a new APIError.
|
|
77
|
+
#
|
|
78
|
+
# Assigns the error type (as reported by Facebook) to #fb_error_type
|
|
79
|
+
# and the raw error response available to #raw_response.
|
|
80
|
+
#
|
|
81
|
+
# @param details error details containing "type" and "message" keys.
|
|
82
|
+
def initialize(details = {})
|
|
83
|
+
self.raw_response = details
|
|
84
|
+
self.fb_error_type = details["type"]
|
|
85
|
+
super("#{fb_error_type}: #{details["message"]}")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
require 'koala/api/graph_batch_api'
|
|
92
|
+
# legacy support for old pre-1.2 API interfaces
|
|
93
|
+
require 'koala/api/legacy'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
|
|
3
|
+
module Koala
|
|
4
|
+
module HTTPService
|
|
5
|
+
class MultipartRequest < Faraday::Request::Multipart
|
|
6
|
+
# Facebook expects nested parameters to be passed in a certain way
|
|
7
|
+
# Based on our testing (https://github.com/arsduo/koala/issues/125),
|
|
8
|
+
# Faraday needs two changes to make that work:
|
|
9
|
+
# 1) [] need to be escaped (e.g. params[foo]=bar ==> params%5Bfoo%5D=bar)
|
|
10
|
+
# 2) such messages need to be multipart-encoded
|
|
11
|
+
|
|
12
|
+
self.mime_type = 'multipart/form-data'.freeze
|
|
13
|
+
|
|
14
|
+
def process_request?(env)
|
|
15
|
+
# if the request values contain any hashes or arrays, multipart it
|
|
16
|
+
super || !!(env[:body].respond_to?(:values) && env[:body].values.find {|v| v.is_a?(Hash) || v.is_a?(Array)})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def process_params(params, prefix = nil, pieces = nil, &block)
|
|
21
|
+
params.inject(pieces || []) do |all, (key, value)|
|
|
22
|
+
key = "#{prefix}%5B#{key}%5D" if prefix
|
|
23
|
+
|
|
24
|
+
case value
|
|
25
|
+
when Array
|
|
26
|
+
values = value.inject([]) { |a,v| a << [nil, v] }
|
|
27
|
+
process_params(values, key, all, &block)
|
|
28
|
+
when Hash
|
|
29
|
+
process_params(value, key, all, &block)
|
|
30
|
+
else
|
|
31
|
+
all << block.call(key, value)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @private
|
|
39
|
+
# legacy support for when MultipartRequest lived directly under Koala
|
|
40
|
+
MultipartRequest = HTTPService::MultipartRequest
|
|
41
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Koala
|
|
2
|
+
module HTTPService
|
|
3
|
+
class Response
|
|
4
|
+
attr_reader :status, :body, :headers
|
|
5
|
+
|
|
6
|
+
# Creates a new Response object, which standardizes the response received by Facebook for use within Koala.
|
|
7
|
+
def initialize(status, body, headers)
|
|
8
|
+
@status = status
|
|
9
|
+
@body = body
|
|
10
|
+
@headers = headers
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @private
|
|
16
|
+
# legacy support for when Response lived directly under Koala
|
|
17
|
+
Response = HTTPService::Response
|
|
18
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
require "net/http/post/multipart"
|
|
2
|
+
|
|
3
|
+
module Koala
|
|
4
|
+
module HTTPService
|
|
5
|
+
class UploadableIO
|
|
6
|
+
attr_reader :io_or_path, :content_type, :filename
|
|
7
|
+
|
|
8
|
+
def initialize(io_or_path_or_mixed, content_type = nil, filename = nil)
|
|
9
|
+
# see if we got the right inputs
|
|
10
|
+
parse_init_mixed_param io_or_path_or_mixed, content_type
|
|
11
|
+
|
|
12
|
+
# filename is used in the Ads API
|
|
13
|
+
# if it's provided, take precedence over the detected filename
|
|
14
|
+
# otherwise, fall back to a dummy name
|
|
15
|
+
@filename = filename || @filename || "koala-io-file.dum"
|
|
16
|
+
|
|
17
|
+
raise KoalaError.new("Invalid arguments to initialize an UploadableIO") unless @io_or_path
|
|
18
|
+
raise KoalaError.new("Unable to determine MIME type for UploadableIO") if !@content_type
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_upload_io
|
|
22
|
+
UploadIO.new(@io_or_path, @content_type, @filename)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_file
|
|
26
|
+
@io_or_path.is_a?(String) ? File.open(@io_or_path) : @io_or_path
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.binary_content?(content)
|
|
30
|
+
content.is_a?(UploadableIO) || DETECTION_STRATEGIES.detect {|method| send(method, content)}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
DETECTION_STRATEGIES = [
|
|
35
|
+
:sinatra_param?,
|
|
36
|
+
:rails_3_param?,
|
|
37
|
+
:file_param?
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
PARSE_STRATEGIES = [
|
|
41
|
+
:parse_rails_3_param,
|
|
42
|
+
:parse_sinatra_param,
|
|
43
|
+
:parse_file_object,
|
|
44
|
+
:parse_string_path,
|
|
45
|
+
:parse_io
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
def parse_init_mixed_param(mixed, content_type = nil)
|
|
49
|
+
PARSE_STRATEGIES.each do |method|
|
|
50
|
+
send(method, mixed, content_type)
|
|
51
|
+
return if @io_or_path && @content_type
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Expects a parameter of type ActionDispatch::Http::UploadedFile
|
|
56
|
+
def self.rails_3_param?(uploaded_file)
|
|
57
|
+
uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_rails_3_param(uploaded_file, content_type = nil)
|
|
61
|
+
if UploadableIO.rails_3_param?(uploaded_file)
|
|
62
|
+
@io_or_path = uploaded_file.tempfile.path
|
|
63
|
+
@content_type = content_type || uploaded_file.content_type
|
|
64
|
+
@filename = uploaded_file.original_filename
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Expects a Sinatra hash of file info
|
|
69
|
+
def self.sinatra_param?(file_hash)
|
|
70
|
+
file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_sinatra_param(file_hash, content_type = nil)
|
|
74
|
+
if UploadableIO.sinatra_param?(file_hash)
|
|
75
|
+
@io_or_path = file_hash[:tempfile]
|
|
76
|
+
@content_type = content_type || file_hash[:type] || detect_mime_type(tempfile)
|
|
77
|
+
@filename = file_hash[:filename]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# takes a file object
|
|
82
|
+
def self.file_param?(file)
|
|
83
|
+
file.kind_of?(File)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def parse_file_object(file, content_type = nil)
|
|
87
|
+
if UploadableIO.file_param?(file)
|
|
88
|
+
@io_or_path = file
|
|
89
|
+
@content_type = content_type || detect_mime_type(file.path)
|
|
90
|
+
@filename = File.basename(file.path)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def parse_string_path(path, content_type = nil)
|
|
95
|
+
if path.kind_of?(String)
|
|
96
|
+
@io_or_path = path
|
|
97
|
+
@content_type = content_type || detect_mime_type(path)
|
|
98
|
+
@filename = File.basename(path)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def parse_io(io, content_type = nil)
|
|
103
|
+
if io.respond_to?(:read)
|
|
104
|
+
@io_or_path = io
|
|
105
|
+
@content_type = content_type
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
MIME_TYPE_STRATEGIES = [
|
|
110
|
+
:use_mime_module,
|
|
111
|
+
:use_simple_detection
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
def detect_mime_type(filename)
|
|
115
|
+
if filename
|
|
116
|
+
MIME_TYPE_STRATEGIES.each do |method|
|
|
117
|
+
result = send(method, filename)
|
|
118
|
+
return result if result
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
nil # if we can't find anything
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def use_mime_module(filename)
|
|
125
|
+
# if the user has installed mime/types, we can use that
|
|
126
|
+
# if not, rescue and return nil
|
|
127
|
+
begin
|
|
128
|
+
type = MIME::Types.type_for(filename).first
|
|
129
|
+
type ? type.to_s : nil
|
|
130
|
+
rescue
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def use_simple_detection(filename)
|
|
136
|
+
# very rudimentary extension analysis for images
|
|
137
|
+
# first, get the downcased extension, or an empty string if it doesn't exist
|
|
138
|
+
extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
|
|
139
|
+
case extension
|
|
140
|
+
when ""
|
|
141
|
+
nil
|
|
142
|
+
# images
|
|
143
|
+
when "jpg", "jpeg"
|
|
144
|
+
"image/jpeg"
|
|
145
|
+
when "png"
|
|
146
|
+
"image/png"
|
|
147
|
+
when "gif"
|
|
148
|
+
"image/gif"
|
|
149
|
+
|
|
150
|
+
# video
|
|
151
|
+
when "3g2"
|
|
152
|
+
"video/3gpp2"
|
|
153
|
+
when "3gp", "3gpp"
|
|
154
|
+
"video/3gpp"
|
|
155
|
+
when "asf"
|
|
156
|
+
"video/x-ms-asf"
|
|
157
|
+
when "avi"
|
|
158
|
+
"video/x-msvideo"
|
|
159
|
+
when "flv"
|
|
160
|
+
"video/x-flv"
|
|
161
|
+
when "m4v"
|
|
162
|
+
"video/x-m4v"
|
|
163
|
+
when "mkv"
|
|
164
|
+
"video/x-matroska"
|
|
165
|
+
when "mod"
|
|
166
|
+
"video/mod"
|
|
167
|
+
when "mov", "qt"
|
|
168
|
+
"video/quicktime"
|
|
169
|
+
when "mp4", "mpeg4"
|
|
170
|
+
"video/mp4"
|
|
171
|
+
when "mpe", "mpeg", "mpg", "tod", "vob"
|
|
172
|
+
"video/mpeg"
|
|
173
|
+
when "nsv"
|
|
174
|
+
"application/x-winamp"
|
|
175
|
+
when "ogm", "ogv"
|
|
176
|
+
"video/ogg"
|
|
177
|
+
when "wmv"
|
|
178
|
+
"video/x-ms-wmv"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# @private
|
|
185
|
+
# legacy support for when UploadableIO lived directly under Koala
|
|
186
|
+
UploadableIO = HTTPService::UploadableIO
|
|
187
|
+
end
|