koala 0.9.0 → 1.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.
Files changed (66) hide show
  1. data/.gitignore +3 -0
  2. data/CHANGELOG +47 -7
  3. data/Gemfile +3 -0
  4. data/LICENSE +1 -1
  5. data/Manifest +10 -15
  6. data/Rakefile +13 -13
  7. data/koala.gemspec +36 -16
  8. data/lib/koala/graph_api.rb +188 -123
  9. data/lib/koala/http_services.rb +93 -18
  10. data/lib/koala/rest_api.rb +73 -6
  11. data/lib/koala/test_users.rb +85 -0
  12. data/lib/koala/uploadable_io.rb +115 -0
  13. data/lib/koala.rb +114 -116
  14. data/readme.md +32 -18
  15. data/spec/cases/api_base_spec.rb +101 -0
  16. data/spec/cases/graph_and_rest_api_spec.rb +31 -0
  17. data/spec/cases/graph_api_spec.rb +25 -0
  18. data/spec/cases/http_services/http_service_spec.rb +54 -0
  19. data/spec/cases/http_services/net_http_service_spec.rb +350 -0
  20. data/spec/cases/http_services/typhoeus_service_spec.rb +144 -0
  21. data/spec/cases/oauth_spec.rb +409 -0
  22. data/spec/cases/realtime_updates_spec.rb +184 -0
  23. data/spec/cases/rest_api_spec.rb +25 -0
  24. data/spec/cases/test_users_spec.rb +221 -0
  25. data/spec/cases/uploadable_io_spec.rb +151 -0
  26. data/spec/fixtures/beach.jpg +0 -0
  27. data/spec/{facebook_data.yml → fixtures/facebook_data.yml} +18 -14
  28. data/spec/{mock_facebook_responses.yml → fixtures/mock_facebook_responses.yml} +314 -241
  29. data/spec/spec_helper.rb +18 -0
  30. data/spec/support/graph_api_shared_examples.rb +424 -0
  31. data/spec/support/live_testing_data_helper.rb +40 -0
  32. data/spec/{mock_http_service.rb → support/mock_http_service.rb} +94 -80
  33. data/spec/support/rest_api_shared_examples.rb +161 -0
  34. data/spec/support/setup_mocks_or_live.rb +52 -0
  35. data/spec/support/uploadable_io_shared_examples.rb +76 -0
  36. metadata +140 -55
  37. data/examples/oauth_playground/Capfile +0 -2
  38. data/examples/oauth_playground/LICENSE +0 -22
  39. data/examples/oauth_playground/Rakefile +0 -4
  40. data/examples/oauth_playground/config/deploy.rb +0 -39
  41. data/examples/oauth_playground/config/facebook.yml +0 -13
  42. data/examples/oauth_playground/config.ru +0 -27
  43. data/examples/oauth_playground/lib/load_facebook.rb +0 -3
  44. data/examples/oauth_playground/lib/oauth_playground.rb +0 -187
  45. data/examples/oauth_playground/readme.md +0 -8
  46. data/examples/oauth_playground/spec/oauth_playground_spec.rb +0 -35
  47. data/examples/oauth_playground/spec/spec_helper.rb +0 -36
  48. data/examples/oauth_playground/tmp/restart.txt +0 -0
  49. data/examples/oauth_playground/views/index.erb +0 -206
  50. data/examples/oauth_playground/views/layout.erb +0 -39
  51. data/init.rb +0 -2
  52. data/spec/koala/api_base_tests.rb +0 -95
  53. data/spec/koala/graph_and_rest_api/graph_and_rest_api_no_token_tests.rb +0 -10
  54. data/spec/koala/graph_and_rest_api/graph_and_rest_api_with_token_tests.rb +0 -11
  55. data/spec/koala/graph_api/graph_api_no_access_token_tests.rb +0 -114
  56. data/spec/koala/graph_api/graph_api_with_access_token_tests.rb +0 -150
  57. data/spec/koala/graph_api/graph_collection_tests.rb +0 -104
  58. data/spec/koala/live_testing_data_helper.rb +0 -15
  59. data/spec/koala/net_http_service_tests.rb +0 -181
  60. data/spec/koala/oauth/oauth_tests.rb +0 -440
  61. data/spec/koala/realtime_updates/realtime_updates_tests.rb +0 -187
  62. data/spec/koala/rest_api/rest_api_no_access_token_tests.rb +0 -94
  63. data/spec/koala/rest_api/rest_api_with_access_token_tests.rb +0 -36
  64. data/spec/koala_spec.rb +0 -18
  65. data/spec/koala_spec_helper.rb +0 -31
  66. data/spec/koala_spec_without_mocks.rb +0 -19
@@ -8,63 +8,138 @@ 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 = create_http(server(options), private_request, options)
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
90
+
91
+ def self.create_http(server, private_request, options)
92
+ if options[:proxy]
93
+ proxy = URI.parse(options[:proxy])
94
+ http = Net::HTTP.new(server, private_request ? 443 : nil,
95
+ proxy.host, proxy.port, proxy.user, proxy.password)
96
+ else
97
+ http = Net::HTTP.new(server, private_request ? 443 : nil)
98
+ end
99
+ if options[:timeout]
100
+ http.open_timeout = options[:timeout]
101
+ http.read_timeout = options[:timeout]
102
+ end
103
+ http
104
+ end
105
+
49
106
  end
50
107
  end
51
108
  end
52
109
 
53
110
  module TyphoeusService
54
111
  # this service uses Typhoeus to send requests to the graph
112
+
55
113
  def self.included(base)
56
114
  base.class_eval do
57
- require 'typhoeus' unless defined?(Typhoeus)
115
+ require "typhoeus" unless defined?(Typhoeus)
58
116
  include Typhoeus
117
+
118
+ include Koala::HTTPService
59
119
 
60
120
  def self.make_request(path, args, verb, options = {})
61
121
  # if the verb isn't get or post, send it as a post argument
62
122
  args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
63
- server = options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER
123
+
124
+ # switch any UploadableIOs to the files Typhoeus expects
125
+ args.each_pair {|key, value| args[key] = value.to_file if value.is_a?(UploadableIO)}
126
+
127
+ # you can pass arguments directly to Typhoeus using the :typhoeus_options key
64
128
  typhoeus_options = {:params => args}.merge(options[:typhoeus_options] || {})
65
- response = self.send(verb, "https://#{server}#{path}", typhoeus_options)
129
+
130
+ # by default, we use SSL only for private requests (e.g. with access token)
131
+ # this makes public requests faster
132
+ prefix = (args["access_token"] || @always_use_ssl || options[:use_ssl]) ? "https" : "http"
133
+
134
+ response = self.send(verb, "#{prefix}://#{server(options)}#{path}", typhoeus_options)
66
135
  Koala::Response.new(response.code, response.body, response.headers_hash)
67
136
  end
137
+
138
+ private
139
+
140
+ def self.multipart_requires_content_type?
141
+ false # Typhoeus handles multipart file types, we don't have to require it
142
+ end
68
143
  end # class_eval
69
144
  end
70
145
  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
@@ -0,0 +1,85 @@
1
+ require 'koala'
2
+
3
+ module Koala
4
+ module Facebook
5
+ module TestUserMethods
6
+
7
+ def initialize(options = {})
8
+ @app_id = options[:app_id]
9
+ @app_access_token = options[:app_access_token]
10
+ @secret = options[:secret]
11
+ unless @app_id && (@app_access_token || @secret) # make sure we have what we need
12
+ raise ArgumentError, "Initialize must receive a hash with :app_id and either :app_access_token or :secret! (received #{options.inspect})"
13
+ end
14
+
15
+ # fetch the access token if we're provided a secret
16
+ if @secret && !@app_access_token
17
+ oauth = Koala::Facebook::OAuth.new(@app_id, @secret)
18
+ @app_access_token = oauth.get_app_access_token
19
+ end
20
+ @graph_api = GraphAPI.new(@app_access_token)
21
+ end
22
+
23
+ def create(installed, permissions = nil, args = {}, options = {})
24
+ # Creates and returns a test user
25
+ args['installed'] = installed
26
+ args['permissions'] = (permissions.is_a?(Array) ? permissions.join(",") : permissions) if installed
27
+ result = @graph_api.graph_call(accounts_path, args, "post", options)
28
+ end
29
+
30
+ def list
31
+ @graph_api.graph_call(accounts_path)["data"]
32
+ end
33
+
34
+ def delete(test_user)
35
+ test_user = test_user["id"] if test_user.is_a?(Hash)
36
+ @graph_api.delete_object(test_user)
37
+ end
38
+
39
+ def delete_all
40
+ list.each {|u| delete u }
41
+ end
42
+
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 for both users 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")
60
+ end
61
+
62
+ def create_network(network_size, installed = true, permissions = '')
63
+ network_size = 50 if network_size > 50 # FB's max is 50
64
+ users = (0...network_size).collect { create(installed, permissions) }
65
+ friends = users.clone
66
+ users.each do |user|
67
+ # Remove this user from list of friends
68
+ friends.delete_at(0)
69
+ # befriend all the others
70
+ friends.each do |friend|
71
+ befriend(user, friend)
72
+ end
73
+ end
74
+ return users
75
+ end
76
+
77
+ protected
78
+
79
+ def accounts_path
80
+ @accounts_path ||= "/#{@app_id}/accounts/test-users"
81
+ end
82
+
83
+ end # TestUserMethods
84
+ end # Facebook
85
+ end # Koala
@@ -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.tempfile.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