koala 0.10.0 → 1.0.0.beta2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. data/CHANGELOG +34 -7
  2. data/Manifest +8 -1
  3. data/Rakefile +4 -4
  4. data/koala.gemspec +10 -8
  5. data/lib/koala/graph_api.rb +188 -123
  6. data/lib/koala/http_services.rb +92 -22
  7. data/lib/koala/rest_api.rb +73 -6
  8. data/lib/koala/test_users.rb +18 -5
  9. data/lib/koala/uploadable_io.rb +115 -0
  10. data/lib/koala.rb +81 -72
  11. data/readme.md +18 -12
  12. data/spec/facebook_data.yml +18 -14
  13. data/spec/koala/assets/beach.jpg +0 -0
  14. data/spec/koala/graph_and_rest_api/graph_and_rest_api_no_token_tests.rb +5 -1
  15. data/spec/koala/graph_and_rest_api/graph_and_rest_api_with_token_tests.rb +8 -3
  16. data/spec/koala/graph_api/graph_api_no_access_token_tests.rb +13 -62
  17. data/spec/koala/graph_api/graph_api_tests.rb +85 -0
  18. data/spec/koala/graph_api/graph_api_with_access_token_tests.rb +167 -123
  19. data/spec/koala/http_services/http_service_tests.rb +51 -0
  20. data/spec/koala/http_services/net_http_service_tests.rb +339 -0
  21. data/spec/koala/http_services/typhoeus_service_tests.rb +162 -0
  22. data/spec/koala/live_testing_data_helper.rb +1 -1
  23. data/spec/koala/oauth/oauth_tests.rb +19 -85
  24. data/spec/koala/rest_api/rest_api_no_access_token_tests.rb +5 -74
  25. data/spec/koala/rest_api/rest_api_tests.rb +118 -0
  26. data/spec/koala/rest_api/rest_api_with_access_token_tests.rb +5 -3
  27. data/spec/koala/test_users/test_users_tests.rb +49 -48
  28. data/spec/koala/uploadable_io/uploadable_io_tests.rb +246 -0
  29. data/spec/koala_spec_helper.rb +32 -6
  30. data/spec/koala_spec_without_mocks.rb +3 -3
  31. data/spec/mock_facebook_responses.yml +43 -20
  32. data/spec/mock_http_service.rb +16 -3
  33. metadata +39 -8
  34. data/spec/koala/net_http_service_tests.rb +0 -186
@@ -8,63 +8,133 @@ module Koala
8
8
  end
9
9
  end
10
10
 
11
+ module HTTPService
12
+ # common functionality for all HTTP services
13
+ def self.included(base)
14
+ base.class_eval do
15
+ class << self
16
+ attr_accessor :always_use_ssl
17
+ end
18
+
19
+ def self.server(options = {})
20
+ "#{options[:beta] ? "beta." : ""}#{options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER}"
21
+ end
22
+
23
+ protected
24
+
25
+ def self.params_require_multipart?(param_hash)
26
+ param_hash.any? { |key, value| value.kind_of?(Koala::UploadableIO) }
27
+ end
28
+
29
+ def self.multipart_requires_content_type?
30
+ true
31
+ end
32
+ end
33
+ end
34
+ end
35
+
11
36
  module NetHTTPService
12
37
  # this service uses Net::HTTP to send requests to the graph
13
38
  def self.included(base)
14
39
  base.class_eval do
15
- require 'net/http' unless defined?(Net::HTTP)
16
- require 'net/https'
40
+ require "net/http" unless defined?(Net::HTTP)
41
+ require "net/https"
42
+ require "net/http/post/multipart"
43
+
44
+ include Koala::HTTPService
17
45
 
18
46
  def self.make_request(path, args, verb, options = {})
19
47
  # We translate args to a valid query string. If post is specified,
20
48
  # we send a POST request to the given path with the given arguments.
21
49
 
50
+ # by default, we use SSL only for private requests
51
+ # this makes public requests faster
52
+ private_request = args["access_token"] || @always_use_ssl || options[:use_ssl]
53
+
22
54
  # if the verb isn't get or post, send it as a post argument
23
55
  args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
24
56
 
25
- server = options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER
26
- http = Net::HTTP.new(server, 443)
27
- http.use_ssl = true
28
- # we turn off certificate validation to avoid the
29
- # "warning: peer certificate won't be verified in this SSL session" warning
30
- # not sure if this is the right way to handle it
31
- # see http://redcorundum.blogspot.com/2008/03/ssl-certificates-and-nethttps.html
32
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
33
-
34
- result = http.start { |http|
35
- response, body = (verb == "post" ? http.post(path, encode_params(args)) : http.get("#{path}?#{encode_params(args)}"))
57
+ http = Net::HTTP.new(server(options), private_request ? 443 : nil)
58
+ http.use_ssl = true if private_request
59
+
60
+ result = http.start do |http|
61
+ response, body = if verb == "post"
62
+ if params_require_multipart? args
63
+ http.request Net::HTTP::Post::Multipart.new path, encode_multipart_params(args)
64
+ else
65
+ http.post(path, encode_params(args))
66
+ end
67
+ else
68
+ http.get("#{path}?#{encode_params(args)}")
69
+ end
70
+
36
71
  Koala::Response.new(response.code.to_i, body, response)
37
- }
72
+ end
38
73
  end
39
74
 
40
75
  protected
41
76
  def self.encode_params(param_hash)
42
77
  # unfortunately, we can't use to_query because that's Rails, not Ruby
43
78
  # if no hash (e.g. no auth token) return empty string
44
- ((param_hash || {}).collect do |key_and_value|
79
+ ((param_hash || {}).collect do |key_and_value|
45
80
  key_and_value[1] = key_and_value[1].to_json if key_and_value[1].class != String
46
81
  "#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
47
82
  end).join("&")
48
83
  end
84
+
85
+ def self.encode_multipart_params(param_hash)
86
+ Hash[*param_hash.collect do |key, value|
87
+ [key, value.kind_of?(Koala::UploadableIO) ? value.to_upload_io : value]
88
+ end.flatten]
89
+ end
49
90
  end
50
91
  end
51
92
  end
52
93
 
53
94
  module TyphoeusService
54
95
  # this service uses Typhoeus to send requests to the graph
96
+
97
+ # used for multipart file uploads (see below)
98
+ class NetHTTPInterface
99
+ include NetHTTPService
100
+ end
101
+
55
102
  def self.included(base)
56
103
  base.class_eval do
57
- require 'typhoeus' unless defined?(Typhoeus)
104
+ require "typhoeus" unless defined?(Typhoeus)
58
105
  include Typhoeus
106
+
107
+ include Koala::HTTPService
59
108
 
60
109
  def self.make_request(path, args, verb, options = {})
61
- # if the verb isn't get or post, send it as a post argument
62
- args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
63
- server = options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER
64
- typhoeus_options = {:params => args}.merge(options[:typhoeus_options] || {})
65
- response = self.send(verb, "https://#{server}#{path}", typhoeus_options)
66
- Koala::Response.new(response.code, response.body, response.headers_hash)
110
+ unless params_require_multipart?(args)
111
+ # if the verb isn't get or post, send it as a post argument
112
+ args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
113
+
114
+ # switch any UploadableIOs to the files Typhoeus expects
115
+ # args.each_pair {|key, value| args[key] = value.to_file if value.is_a?(UploadableIO)}
116
+
117
+ # you can pass arguments directly to Typhoeus using the :typhoeus_options key
118
+ typhoeus_options = {:params => args}.merge(options[:typhoeus_options] || {})
119
+
120
+ # by default, we use SSL only for private requests (e.g. with access token)
121
+ # this makes public requests faster
122
+ prefix = (args["access_token"] || @always_use_ssl || options[:use_ssl]) ? "https" : "http"
123
+
124
+ response = self.send(verb, "#{prefix}://#{server(options)}#{path}", typhoeus_options)
125
+ Koala::Response.new(response.code, response.body, response.headers_hash)
126
+ else
127
+ # we have to use NetHTTPService for multipart for file uploads
128
+ # until Typhoeus integrates support
129
+ Koala::TyphoeusService::NetHTTPInterface.make_request(path, args, verb, options)
130
+ end
67
131
  end
132
+
133
+ private
134
+ # Typhoeus file uploads are not currently supported; this will be added when we can get them working
135
+ #def self.multipart_requires_content_type?
136
+ # false # Typhoeus handles multipart file types, we don't have to require it
137
+ #end
68
138
  end # class_eval
69
139
  end
70
140
  end
@@ -1,23 +1,90 @@
1
1
  module Koala
2
2
  module Facebook
3
3
  REST_SERVER = "api.facebook.com"
4
-
4
+
5
5
  module RestAPIMethods
6
6
  def fql_query(fql)
7
7
  rest_call('fql.query', 'query' => fql)
8
8
  end
9
-
10
- def rest_call(method, args = {})
11
- response = api("method/#{method}", args.merge('format' => 'json'), 'get', :rest_api => true) do |response|
9
+
10
+ def rest_call(method, args = {}, options = {})
11
+ options = options.merge!(:rest_api => true, :read_only => READ_ONLY_METHODS.include?(method))
12
+
13
+ response = api("method/#{method}", args.merge('format' => 'json'), 'get', options) do |response|
12
14
  # check for REST API-specific errors
13
15
  if response.is_a?(Hash) && response["error_code"]
14
16
  raise APIError.new("type" => response["error_code"], "message" => response["error_msg"])
15
17
  end
16
18
  end
17
-
19
+
18
20
  response
19
21
  end
22
+
23
+ # read-only methods for which we can use API-read
24
+ # taken directly from the FB PHP library (https://github.com/facebook/php-sdk/blob/master/src/facebook.php)
25
+ READ_ONLY_METHODS = [
26
+ 'admin.getallocation',
27
+ 'admin.getappproperties',
28
+ 'admin.getbannedusers',
29
+ 'admin.getlivestreamvialink',
30
+ 'admin.getmetrics',
31
+ 'admin.getrestrictioninfo',
32
+ 'application.getpublicinfo',
33
+ 'auth.getapppublickey',
34
+ 'auth.getsession',
35
+ 'auth.getsignedpublicsessiondata',
36
+ 'comments.get',
37
+ 'connect.getunconnectedfriendscount',
38
+ 'dashboard.getactivity',
39
+ 'dashboard.getcount',
40
+ 'dashboard.getglobalnews',
41
+ 'dashboard.getnews',
42
+ 'dashboard.multigetcount',
43
+ 'dashboard.multigetnews',
44
+ 'data.getcookies',
45
+ 'events.get',
46
+ 'events.getmembers',
47
+ 'fbml.getcustomtags',
48
+ 'feed.getappfriendstories',
49
+ 'feed.getregisteredtemplatebundlebyid',
50
+ 'feed.getregisteredtemplatebundles',
51
+ 'fql.multiquery',
52
+ 'fql.query',
53
+ 'friends.arefriends',
54
+ 'friends.get',
55
+ 'friends.getappusers',
56
+ 'friends.getlists',
57
+ 'friends.getmutualfriends',
58
+ 'gifts.get',
59
+ 'groups.get',
60
+ 'groups.getmembers',
61
+ 'intl.gettranslations',
62
+ 'links.get',
63
+ 'notes.get',
64
+ 'notifications.get',
65
+ 'pages.getinfo',
66
+ 'pages.isadmin',
67
+ 'pages.isappadded',
68
+ 'pages.isfan',
69
+ 'permissions.checkavailableapiaccess',
70
+ 'permissions.checkgrantedapiaccess',
71
+ 'photos.get',
72
+ 'photos.getalbums',
73
+ 'photos.gettags',
74
+ 'profile.getinfo',
75
+ 'profile.getinfooptions',
76
+ 'stream.get',
77
+ 'stream.getcomments',
78
+ 'stream.getfilters',
79
+ 'users.getinfo',
80
+ 'users.getloggedinuser',
81
+ 'users.getstandardinfo',
82
+ 'users.hasapppermission',
83
+ 'users.isappuser',
84
+ 'users.isverified',
85
+ 'video.getuploadlimits'
86
+ ]
20
87
  end
21
-
88
+
22
89
  end # module Facebook
23
90
  end # module Koala
@@ -40,12 +40,25 @@ module Koala
40
40
  list.each {|u| delete u }
41
41
  end
42
42
 
43
- def befriend(user1, user2)
44
- user1 = user1["id"] if user1.is_a?(Hash)
45
- user2 = user2["id"] if user2.is_a?(Hash)
46
- @graph_api.graph_call("/#{user1}/friends/#{user2}") && @graph_api.graph_call("/#{user2}/friends/#{user1}")
43
+ def befriend(user1_hash, user2_hash)
44
+ user1_id = user1_hash["id"] || user1_hash[:id]
45
+ user2_id = user2_hash["id"] || user2_hash[:id]
46
+ user1_token = user1_hash["access_token"] || user1_hash[:access_token]
47
+ user2_token = user2_hash["access_token"] || user2_hash[:access_token]
48
+ unless user1_id && user2_id && user1_token && user2_token
49
+ # we explicitly raise an error here to minimize the risk of confusing output
50
+ # if you pass in a string (as was previously supported) no local exception would be raised
51
+ # but the Facebook call would fail
52
+ raise ArgumentError, "TestUsers#befriend requires hash arguments with id and access_token"
53
+ end
54
+
55
+ u1_graph_api = GraphAPI.new(user1_token)
56
+ u2_graph_api = GraphAPI.new(user2_token)
57
+
58
+ u1_graph_api.graph_call("#{user1_id}/friends/#{user2_id}", {}, "post") &&
59
+ u2_graph_api.graph_call("#{user2_id}/friends/#{user1_id}", {}, "post")
47
60
  end
48
-
61
+
49
62
  def create_network(network_size, installed = true, permissions = '')
50
63
  network_size = 50 if network_size > 50 # FB's max is 50
51
64
  users = (0...network_size).collect { create(installed, permissions) }
@@ -0,0 +1,115 @@
1
+ require 'koala'
2
+
3
+ module Koala
4
+ class UploadableIO
5
+ attr_reader :io_or_path, :content_type
6
+
7
+ def initialize(io_or_path_or_mixed, content_type = nil)
8
+ # see if we got the right inputs
9
+ if content_type.nil?
10
+ parse_init_mixed_param io_or_path_or_mixed
11
+ elsif !content_type.nil? && (io_or_path_or_mixed.respond_to?(:read) or io_or_path_or_mixed.kind_of?(String))
12
+ @io_or_path = io_or_path_or_mixed
13
+ @content_type = content_type
14
+ end
15
+
16
+ raise KoalaError.new("Invalid arguments to initialize an UploadableIO") unless @io_or_path
17
+ raise KoalaError.new("Unable to determine MIME type for UploadableIO") if !@content_type && Koala.multipart_requires_content_type?
18
+ end
19
+
20
+ def to_upload_io
21
+ UploadIO.new(@io_or_path, @content_type, "koala-io-file.dum")
22
+ end
23
+
24
+ def to_file
25
+ @io_or_path.is_a?(String) ? File.open(@io_or_path) : @io_or_path
26
+ end
27
+
28
+ private
29
+ PARSE_STRATEGIES = [
30
+ :parse_rails_3_param,
31
+ :parse_sinatra_param,
32
+ :parse_file_object,
33
+ :parse_string_path
34
+ ]
35
+
36
+ def parse_init_mixed_param(mixed)
37
+ PARSE_STRATEGIES.each do |method|
38
+ send(method, mixed)
39
+ return if @io_or_path && @content_type
40
+ end
41
+ end
42
+
43
+ # Expects a parameter of type ActionDispatch::Http::UploadedFile
44
+ def parse_rails_3_param(uploaded_file)
45
+ if uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.respond_to?(:path)
46
+ @io_or_path = uploaded_file.tempfile.path
47
+ @content_type = uploaded_file.content_type
48
+ end
49
+ end
50
+
51
+ # Expects a Sinatra hash of file info
52
+ def parse_sinatra_param(file_hash)
53
+ if file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
54
+ @io_or_path = file_hash[:tempfile]
55
+ @content_type = file_hash[:type] || detect_mime_type(tempfile)
56
+ end
57
+ end
58
+
59
+ # takes a file object
60
+ def parse_file_object(file)
61
+ if file.kind_of?(File)
62
+ @io_or_path = file
63
+ @content_type = detect_mime_type(file.path)
64
+ end
65
+ end
66
+
67
+ def parse_string_path(path)
68
+ if path.kind_of?(String)
69
+ @io_or_path = path
70
+ @content_type = detect_mime_type(path)
71
+ end
72
+ end
73
+
74
+ MIME_TYPE_STRATEGIES = [
75
+ :use_mime_module,
76
+ :use_simple_detection
77
+ ]
78
+
79
+ def detect_mime_type(filename)
80
+ if filename
81
+ MIME_TYPE_STRATEGIES.each do |method|
82
+ result = send(method, filename)
83
+ return result if result
84
+ end
85
+ end
86
+ nil # if we can't find anything
87
+ end
88
+
89
+ def use_mime_module(filename)
90
+ # if the user has installed mime/types, we can use that
91
+ # if not, rescue and return nil
92
+ begin
93
+ type = MIME::Types.type_for(filename).first
94
+ type ? type.to_s : nil
95
+ rescue
96
+ nil
97
+ end
98
+ end
99
+
100
+ def use_simple_detection(filename)
101
+ # very rudimentary extension analysis for images
102
+ # first, get the downcased extension, or an empty string if it doesn't exist
103
+ extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
104
+ if extension == ""
105
+ nil
106
+ elsif extension == "jpg" || extension == "jpeg"
107
+ "image/jpeg"
108
+ elsif extension == "png"
109
+ "image/png"
110
+ elsif extension == "gif"
111
+ "image/gif"
112
+ end
113
+ end
114
+ end
115
+ end