koala 1.1.0rc2 → 1.1.0rc3

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.
@@ -0,0 +1,87 @@
1
+ module Koala
2
+ module Facebook
3
+ module GraphBatchAPIMethods
4
+
5
+ def self.included(base)
6
+ base.class_eval do
7
+ alias_method :graph_call_outside_batch, :graph_call
8
+ alias_method :graph_call, :graph_call_in_batch
9
+
10
+ alias_method :check_graph_api_response, :check_response
11
+ alias_method :check_response, :check_graph_batch_api_response
12
+ end
13
+ end
14
+
15
+ def batch_calls
16
+ @batch_calls ||= []
17
+ end
18
+
19
+ def graph_call_in_batch(path, args = {}, verb = "get", options = {}, &post_processing)
20
+ # for batch APIs, we queue up the call details (incl. post-processing)
21
+ batch_calls << BatchOperation.new(
22
+ :url => path,
23
+ :args => args,
24
+ :method => verb,
25
+ :access_token => options['access_token'] || access_token,
26
+ :http_options => options,
27
+ :post_processing => post_processing
28
+ )
29
+ nil # batch operations return nothing immediately
30
+ end
31
+
32
+ def check_graph_batch_api_response(response)
33
+ if response.is_a?(Hash) && response["error"] && !response["error"].is_a?(Hash)
34
+ APIError.new("type" => "Error #{response["error"]}", "message" => response["error_description"])
35
+ else
36
+ check_graph_api_response(response)
37
+ end
38
+ end
39
+
40
+ def execute(http_options = {})
41
+ return [] unless batch_calls.length > 0
42
+ # Turn the call args collected into what facebook expects
43
+ args = {}
44
+ args["batch"] = MultiJson.encode(batch_calls.map { |batch_op|
45
+ args.merge!(batch_op.files) if batch_op.files
46
+ batch_op.to_batch_params(access_token)
47
+ })
48
+
49
+ graph_call_outside_batch('/', args, 'post', http_options) do |response|
50
+ # map the results with post-processing included
51
+ index = 0 # keep compat with ruby 1.8 - no with_index for map
52
+ response.map do |call_result|
53
+ # Get the options hash
54
+ batch_op = batch_calls[index]
55
+ index += 1
56
+
57
+ if call_result
58
+ # (see note in regular api method about JSON parsing)
59
+ body = MultiJson.decode("[#{call_result['body'].to_s}]")[0]
60
+
61
+ unless call_result["code"].to_i >= 500 || error = check_response(body)
62
+ # Get the HTTP component they want
63
+ data = case batch_op.http_options[:http_component]
64
+ when :status
65
+ call_result["code"].to_i
66
+ when :headers
67
+ # facebook returns the headers as an array of k/v pairs, but we want a regular hash
68
+ call_result['headers'].inject({}) { |headers, h| headers[h['name']] = h['value']; headers}
69
+ else
70
+ body
71
+ end
72
+
73
+ # process it if we are given a block to process with
74
+ batch_op.post_processing ? batch_op.post_processing.call(data) : data
75
+ else
76
+ error || APIError.new({"type" => "HTTP #{call_result["code"].to_s}", "message" => "Response body: #{body}"})
77
+ end
78
+ else
79
+ nil
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,54 @@
1
+ module Koala
2
+ module Facebook
3
+ class GraphCollection < Array
4
+ # This class is a light wrapper for collections returned
5
+ # from the Graph API.
6
+ #
7
+ # It extends Array to allow direct access to the data colleciton
8
+ # which should allow it to drop in seamlessly.
9
+ #
10
+ # It also allows access to paging information and the
11
+ # ability to get the next/previous page in the collection
12
+ # by calling next_page or previous_page.
13
+ attr_reader :paging
14
+ attr_reader :api
15
+
16
+ def initialize(response, api)
17
+ super response["data"]
18
+ @paging = response["paging"]
19
+ @api = api
20
+ end
21
+
22
+ # defines methods for NEXT and PREVIOUS pages
23
+ %w{next previous}.each do |this|
24
+
25
+ # def next_page
26
+ # def previous_page
27
+ define_method "#{this.to_sym}_page" do
28
+ base, args = send("#{this}_page_params")
29
+ base ? @api.get_page([base, args]) : nil
30
+ end
31
+
32
+ # def next_page_params
33
+ # def previous_page_params
34
+ define_method "#{this.to_sym}_page_params" do
35
+ return nil unless @paging and @paging[this]
36
+ parse_page_url(@paging[this])
37
+ end
38
+ end
39
+
40
+ def parse_page_url(url)
41
+ match = url.match(/.com\/(.*)\?(.*)/)
42
+ base = match[1]
43
+ args = match[2]
44
+ params = CGI.parse(args)
45
+ new_params = {}
46
+ params.each_pair do |key,value|
47
+ new_params[key] = value.join ","
48
+ end
49
+ [base,new_params]
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -13,18 +13,20 @@ module Koala
13
13
  def self.included(base)
14
14
  base.class_eval do
15
15
  class << self
16
- attr_accessor :always_use_ssl, :proxy, :timeout, :ca_path, :ca_file
16
+ attr_accessor :always_use_ssl, :proxy, :timeout
17
17
  end
18
18
 
19
19
  def self.server(options = {})
20
- "#{options[:beta] ? "beta." : ""}#{options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER}"
20
+ server = "#{options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER}"
21
+ server.gsub!(/\.facebook/, "-video.facebook") if options[:video]
22
+ "#{options[:beta] ? "beta." : ""}#{server}"
21
23
  end
22
24
 
23
25
  def self.encode_params(param_hash)
24
26
  # unfortunately, we can't use to_query because that's Rails, not Ruby
25
27
  # if no hash (e.g. no auth token) return empty string
26
28
  ((param_hash || {}).collect do |key_and_value|
27
- key_and_value[1] = key_and_value[1].to_json unless key_and_value[1].is_a? String
29
+ key_and_value[1] = MultiJson.encode(key_and_value[1]) unless key_and_value[1].is_a? String
28
30
  "#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
29
31
  end).join("&")
30
32
  end
@@ -7,6 +7,11 @@ module Koala
7
7
  # this service uses Net::HTTP to send requests to the graph
8
8
  include Koala::HTTPService
9
9
 
10
+ # Net::HTTP-specific values
11
+ class << self
12
+ attr_accessor :ca_file, :ca_path, :verify_mode
13
+ end
14
+
10
15
  def self.make_request(path, args, verb, options = {})
11
16
  # We translate args to a valid query string. If post is specified,
12
17
  # we send a POST request to the given path with the given arguments.
@@ -15,17 +20,13 @@ module Koala
15
20
  # this makes public requests faster
16
21
  private_request = args["access_token"] || @always_use_ssl || options[:use_ssl]
17
22
 
18
- # if proxy/timeout options aren't passed, check if defaults are set
19
- options[:proxy] ||= proxy
20
- options[:timeout] ||= timeout
21
-
22
23
  # if the verb isn't get or post, send it as a post argument
23
24
  args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
24
25
 
25
26
  http = create_http(server(options), private_request, options)
26
27
 
27
- result = http.start do |http|
28
- response, body = if verb == "post"
28
+ response = http.start do |http|
29
+ if verb == "post"
29
30
  if params_require_multipart? args
30
31
  http.request Net::HTTP::Post::Multipart.new path, encode_multipart_params(args)
31
32
  else
@@ -34,9 +35,9 @@ module Koala
34
35
  else
35
36
  http.get("#{path}?#{encode_params(args)}")
36
37
  end
37
-
38
- Koala::Response.new(response.code.to_i, body, response)
39
38
  end
39
+
40
+ Koala::Response.new(response.code.to_i, response.body, response)
40
41
  end
41
42
 
42
43
  protected
@@ -44,44 +45,48 @@ module Koala
44
45
  # unfortunately, we can't use to_query because that's Rails, not Ruby
45
46
  # if no hash (e.g. no auth token) return empty string
46
47
  ((param_hash || {}).collect do |key_and_value|
47
- key_and_value[1] = key_and_value[1].to_json if key_and_value[1].class != String
48
+ key_and_value[1] = MultiJson.encode(key_and_value[1]) if key_and_value[1].class != String
48
49
  "#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
49
50
  end).join("&")
50
51
  end
51
-
52
+
52
53
  def self.encode_multipart_params(param_hash)
53
- Hash[*param_hash.collect do |key, value|
54
+ Hash[*param_hash.collect do |key, value|
54
55
  [key, value.kind_of?(Koala::UploadableIO) ? value.to_upload_io : value]
55
56
  end.flatten]
56
57
  end
57
58
 
58
59
  def self.create_http(server, private_request, options)
59
- if options[:proxy]
60
- proxy = URI.parse(options[:proxy])
60
+ if proxy_server = options[:proxy] || proxy
61
+ proxy = URI.parse(proxy_server)
61
62
  http = Net::HTTP.new(server, private_request ? 443 : nil,
62
63
  proxy.host, proxy.port, proxy.user, proxy.password)
63
64
  else
64
- http = Net::HTTP.new(server, private_request ? 443 : nil)
65
+ http = Net::HTTP.new(server, private_request ? 443 : nil)
65
66
  end
66
-
67
- if options[:timeout]
68
- http.open_timeout = options[:timeout]
69
- http.read_timeout = options[:timeout]
67
+
68
+ if timeout_value = options[:timeout] || timeout
69
+ http.open_timeout = timeout_value
70
+ http.read_timeout = timeout_value
70
71
  end
71
-
72
+
72
73
  # For HTTPS requests, set the proper CA certificates
73
74
  if private_request
74
75
  http.use_ssl = true
75
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
76
+ http.verify_mode = options[:verify_mode] || verify_mode || OpenSSL::SSL::VERIFY_PEER
76
77
 
77
- options[:ca_file] ||= ca_file
78
- http.ca_file = options[:ca_file] if options[:ca_file] && File.exists?(options[:ca_file])
78
+ if cert_file = options[:ca_file] || ca_file
79
+ raise Errno::ENOENT, "Certificate file #{cert_file.inspect} does not exist!" unless File.exists?(cert_file)
80
+ http.ca_file = cert_file
81
+ end
79
82
 
80
- options[:ca_path] ||= ca_path
81
- http.ca_path = options[:ca_path] if options[:ca_path] && Dir.exists?(options[:ca_path])
83
+ if cert_path = options[:ca_path] || ca_path
84
+ raise Errno::ENOENT, "Certificate path #{cert_path.inspect} does not exist!" unless File.directory?(cert_path)
85
+ http.ca_path = cert_path
86
+ end
82
87
  end
83
-
88
+
84
89
  http
85
90
  end
86
91
  end
87
- end
92
+ end
@@ -3,14 +3,14 @@ require "typhoeus" unless defined?(Typhoeus)
3
3
  module Koala
4
4
  module TyphoeusService
5
5
  # this service uses Typhoeus to send requests to the graph
6
- include Typhoeus
6
+ include Typhoeus
7
7
  include Koala::HTTPService
8
8
 
9
9
  def self.make_request(path, args, verb, options = {})
10
10
  # if the verb isn't get or post, send it as a post argument
11
11
  args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
12
12
 
13
- # switch any UploadableIOs to the files Typhoeus expects
13
+ # switch any UploadableIOs to the files Typhoeus expects
14
14
  args.each_pair {|key, value| args[key] = value.to_file if value.is_a?(UploadableIO)}
15
15
 
16
16
  # you can pass arguments directly to Typhoeus using the :typhoeus_options key
@@ -27,9 +27,9 @@ module Koala
27
27
  response = Typhoeus::Request.send(verb, "#{prefix}://#{server(options)}#{path}", typhoeus_options)
28
28
  Koala::Response.new(response.code, response.body, response.headers_hash)
29
29
  end
30
-
30
+
31
31
  protected
32
-
32
+
33
33
  def self.multipart_requires_content_type?
34
34
  false # Typhoeus handles multipart file types, we don't have to require it
35
35
  end
data/lib/koala/oauth.rb CHANGED
@@ -99,7 +99,7 @@ module Koala
99
99
  def parse_signed_request(input)
100
100
  encoded_sig, encoded_envelope = input.split('.', 2)
101
101
  signature = base64_url_decode(encoded_sig).unpack("H*").first
102
- envelope = JSON.parse(base64_url_decode(encoded_envelope))
102
+ envelope = MultiJson.decode(base64_url_decode(encoded_envelope))
103
103
 
104
104
  raise "SignedRequest: Unsupported algorithm #{envelope['algorithm']}" if envelope['algorithm'] != 'HMAC-SHA256'
105
105
 
@@ -126,7 +126,7 @@ module Koala
126
126
  })
127
127
  end
128
128
 
129
- JSON.parse(response)
129
+ MultiJson.decode(response)
130
130
  end
131
131
 
132
132
  def get_tokens_from_session_keys(sessions, options = {})
@@ -149,7 +149,7 @@ module Koala
149
149
  result = fetch_token_string(args, post, "access_token", options)
150
150
 
151
151
  # if we have an error, parse the error JSON and raise an error
152
- raise APIError.new((JSON.parse(result)["error"] rescue nil) || {}) if result =~ /error/
152
+ raise APIError.new((MultiJson.decode(result)["error"] rescue nil) || {}) if result =~ /error/
153
153
 
154
154
  # otherwise, parse the access token
155
155
  parse_access_token(result)
@@ -8,7 +8,7 @@ module Koala
8
8
  end
9
9
 
10
10
  def fql_multiquery(queries = {}, args = {}, options = {})
11
- if results = rest_call('fql.multiquery', args.merge(:queries => queries.to_json), options)
11
+ if results = rest_call('fql.multiquery', args.merge(:queries => MultiJson.encode(queries)), options)
12
12
  # simplify the multiquery result format
13
13
  results.inject({}) {|outcome, data| outcome[data["name"]] = data["fql_result_set"]; outcome}
14
14
  end
@@ -36,108 +36,140 @@ module Koala
36
36
  end
37
37
 
38
38
  private
39
- DETECTION_STRATEGIES = [
40
- :sinatra_param?,
41
- :rails_3_param?,
42
- :file_param?
43
- ]
39
+ DETECTION_STRATEGIES = [
40
+ :sinatra_param?,
41
+ :rails_3_param?,
42
+ :file_param?
43
+ ]
44
+
45
+ PARSE_STRATEGIES = [
46
+ :parse_rails_3_param,
47
+ :parse_sinatra_param,
48
+ :parse_file_object,
49
+ :parse_string_path
50
+ ]
44
51
 
45
- PARSE_STRATEGIES = [
46
- :parse_rails_3_param,
47
- :parse_sinatra_param,
48
- :parse_file_object,
49
- :parse_string_path
50
- ]
51
-
52
- def parse_init_mixed_param(mixed)
53
- PARSE_STRATEGIES.each do |method|
54
- send(method, mixed)
55
- return if @io_or_path && @content_type
56
- end
57
- end
58
-
59
- # Expects a parameter of type ActionDispatch::Http::UploadedFile
60
- def self.rails_3_param?(uploaded_file)
61
- uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
62
- end
63
-
64
- def parse_rails_3_param(uploaded_file)
65
- if UploadableIO.rails_3_param?(uploaded_file)
66
- @io_or_path = uploaded_file.tempfile.path
67
- @content_type = uploaded_file.content_type
68
- end
69
- end
70
-
71
- # Expects a Sinatra hash of file info
72
- def self.sinatra_param?(file_hash)
73
- file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
52
+ def parse_init_mixed_param(mixed)
53
+ PARSE_STRATEGIES.each do |method|
54
+ send(method, mixed)
55
+ return if @io_or_path && @content_type
74
56
  end
75
-
76
- def parse_sinatra_param(file_hash)
77
- if UploadableIO.sinatra_param?(file_hash)
78
- @io_or_path = file_hash[:tempfile]
79
- @content_type = file_hash[:type] || detect_mime_type(tempfile)
80
- end
57
+ end
58
+
59
+ # Expects a parameter of type ActionDispatch::Http::UploadedFile
60
+ def self.rails_3_param?(uploaded_file)
61
+ uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
62
+ end
63
+
64
+ def parse_rails_3_param(uploaded_file)
65
+ if UploadableIO.rails_3_param?(uploaded_file)
66
+ @io_or_path = uploaded_file.tempfile.path
67
+ @content_type = uploaded_file.content_type
81
68
  end
82
-
83
- # takes a file object
84
- def self.file_param?(file)
85
- file.kind_of?(File)
69
+ end
70
+
71
+ # Expects a Sinatra hash of file info
72
+ def self.sinatra_param?(file_hash)
73
+ file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
74
+ end
75
+
76
+ def parse_sinatra_param(file_hash)
77
+ if UploadableIO.sinatra_param?(file_hash)
78
+ @io_or_path = file_hash[:tempfile]
79
+ @content_type = file_hash[:type] || detect_mime_type(tempfile)
86
80
  end
87
-
88
- def parse_file_object(file)
89
- if UploadableIO.file_param?(file)
90
- @io_or_path = file
91
- @content_type = detect_mime_type(file.path)
92
- end
81
+ end
82
+
83
+ # takes a file object
84
+ def self.file_param?(file)
85
+ file.kind_of?(File)
86
+ end
87
+
88
+ def parse_file_object(file)
89
+ if UploadableIO.file_param?(file)
90
+ @io_or_path = file
91
+ @content_type = detect_mime_type(file.path)
93
92
  end
94
-
95
- def parse_string_path(path)
96
- if path.kind_of?(String)
97
- @io_or_path = path
98
- @content_type = detect_mime_type(path)
99
- end
93
+ end
94
+
95
+ def parse_string_path(path)
96
+ if path.kind_of?(String)
97
+ @io_or_path = path
98
+ @content_type = detect_mime_type(path)
100
99
  end
101
-
102
- MIME_TYPE_STRATEGIES = [
103
- :use_mime_module,
104
- :use_simple_detection
105
- ]
106
-
107
- def detect_mime_type(filename)
108
- if filename
109
- MIME_TYPE_STRATEGIES.each do |method|
110
- result = send(method, filename)
111
- return result if result
112
- end
100
+ end
101
+
102
+ MIME_TYPE_STRATEGIES = [
103
+ :use_mime_module,
104
+ :use_simple_detection
105
+ ]
106
+
107
+ def detect_mime_type(filename)
108
+ if filename
109
+ MIME_TYPE_STRATEGIES.each do |method|
110
+ result = send(method, filename)
111
+ return result if result
113
112
  end
114
- nil # if we can't find anything
115
113
  end
116
-
117
- def use_mime_module(filename)
118
- # if the user has installed mime/types, we can use that
119
- # if not, rescue and return nil
120
- begin
121
- type = MIME::Types.type_for(filename).first
122
- type ? type.to_s : nil
123
- rescue
124
- nil
125
- end
114
+ nil # if we can't find anything
115
+ end
116
+
117
+ def use_mime_module(filename)
118
+ # if the user has installed mime/types, we can use that
119
+ # if not, rescue and return nil
120
+ begin
121
+ type = MIME::Types.type_for(filename).first
122
+ type ? type.to_s : nil
123
+ rescue
124
+ nil
126
125
  end
127
-
128
- def use_simple_detection(filename)
129
- # very rudimentary extension analysis for images
130
- # first, get the downcased extension, or an empty string if it doesn't exist
131
- extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
132
- if extension == ""
126
+ end
127
+
128
+ def use_simple_detection(filename)
129
+ # very rudimentary extension analysis for images
130
+ # first, get the downcased extension, or an empty string if it doesn't exist
131
+ extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
132
+ case extension
133
+ when ""
133
134
  nil
134
- elsif extension == "jpg" || extension == "jpeg"
135
+ # images
136
+ when "jpg", "jpeg"
135
137
  "image/jpeg"
136
- elsif extension == "png"
138
+ when "png"
137
139
  "image/png"
138
- elsif extension == "gif"
140
+ when "gif"
139
141
  "image/gif"
140
- end
141
- end
142
+
143
+ # video
144
+ when "3g2"
145
+ "video/3gpp2"
146
+ when "3gp", "3gpp"
147
+ "video/3gpp"
148
+ when "asf"
149
+ "video/x-ms-asf"
150
+ when "avi"
151
+ "video/x-msvideo"
152
+ when "flv"
153
+ "video/x-flv"
154
+ when "m4v"
155
+ "video/x-m4v"
156
+ when "mkv"
157
+ "video/x-matroska"
158
+ when "mod"
159
+ "video/mod"
160
+ when "mov", "qt"
161
+ "video/quicktime"
162
+ when "mp4", "mpeg4"
163
+ "video/mp4"
164
+ when "mpe", "mpeg", "mpg", "tod", "vob"
165
+ "video/mpeg"
166
+ when "nsv"
167
+ "application/x-winamp"
168
+ when "ogm", "ogv"
169
+ "video/ogg"
170
+ when "wmv"
171
+ "video/x-ms-wmv"
172
+ end
173
+ end
142
174
  end
143
175
  end