koala 1.0.0.beta → 1.1.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 (69) hide show
  1. data/.autotest +12 -0
  2. data/.gitignore +5 -0
  3. data/.travis.yml +8 -0
  4. data/CHANGELOG +42 -4
  5. data/Gemfile +7 -0
  6. data/LICENSE +1 -1
  7. data/Manifest +5 -2
  8. data/Rakefile +13 -14
  9. data/autotest/discover.rb +1 -0
  10. data/koala.gemspec +35 -20
  11. data/lib/koala/batch_operation.rb +74 -0
  12. data/lib/koala/graph_api.rb +196 -143
  13. data/lib/koala/graph_batch_api.rb +87 -0
  14. data/lib/koala/graph_collection.rb +54 -0
  15. data/lib/koala/http_services/net_http_service.rb +92 -0
  16. data/lib/koala/http_services/typhoeus_service.rb +37 -0
  17. data/lib/koala/http_services.rb +15 -124
  18. data/lib/koala/oauth.rb +181 -0
  19. data/lib/koala/realtime_updates.rb +5 -14
  20. data/lib/koala/rest_api.rb +13 -8
  21. data/lib/koala/test_users.rb +21 -8
  22. data/lib/koala/uploadable_io.rb +175 -0
  23. data/lib/koala.rb +48 -240
  24. data/readme.md +60 -28
  25. data/spec/cases/api_base_spec.rb +101 -0
  26. data/spec/cases/graph_and_rest_api_spec.rb +31 -0
  27. data/spec/cases/graph_api_batch_spec.rb +609 -0
  28. data/spec/cases/graph_api_spec.rb +25 -0
  29. data/spec/cases/http_services/http_service_spec.rb +129 -0
  30. data/spec/cases/http_services/net_http_service_spec.rb +532 -0
  31. data/spec/cases/http_services/typhoeus_service_spec.rb +152 -0
  32. data/spec/cases/koala_spec.rb +55 -0
  33. data/spec/cases/oauth_spec.rb +409 -0
  34. data/spec/cases/realtime_updates_spec.rb +184 -0
  35. data/spec/cases/rest_api_spec.rb +25 -0
  36. data/spec/{koala/test_users/test_users_tests.rb → cases/test_users_spec.rb} +47 -34
  37. data/spec/cases/uploadable_io_spec.rb +193 -0
  38. data/spec/fixtures/cat.m4v +0 -0
  39. data/spec/{facebook_data.yml → fixtures/facebook_data.yml} +12 -14
  40. data/spec/{mock_facebook_responses.yml → fixtures/mock_facebook_responses.yml} +408 -306
  41. data/spec/spec_helper.rb +19 -0
  42. data/spec/support/graph_api_shared_examples.rb +495 -0
  43. data/spec/support/json_testing_fix.rb +18 -0
  44. data/spec/{koala → support}/live_testing_data_helper.rb +39 -42
  45. data/spec/support/mock_http_service.rb +96 -0
  46. data/spec/support/rest_api_shared_examples.rb +285 -0
  47. data/spec/support/setup_mocks_or_live.rb +51 -0
  48. data/spec/support/uploadable_io_shared_examples.rb +76 -0
  49. metadata +110 -64
  50. data/init.rb +0 -2
  51. data/spec/koala/api_base_tests.rb +0 -102
  52. data/spec/koala/graph_and_rest_api/graph_and_rest_api_no_token_tests.rb +0 -14
  53. data/spec/koala/graph_and_rest_api/graph_and_rest_api_with_token_tests.rb +0 -16
  54. data/spec/koala/graph_api/graph_api_no_access_token_tests.rb +0 -63
  55. data/spec/koala/graph_api/graph_api_tests.rb +0 -86
  56. data/spec/koala/graph_api/graph_api_with_access_token_tests.rb +0 -154
  57. data/spec/koala/graph_api/graph_collection_tests.rb +0 -104
  58. data/spec/koala/net_http_service_tests.rb +0 -430
  59. data/spec/koala/oauth/oauth_tests.rb +0 -409
  60. data/spec/koala/realtime_updates/realtime_updates_tests.rb +0 -187
  61. data/spec/koala/rest_api/rest_api_no_access_token_tests.rb +0 -25
  62. data/spec/koala/rest_api/rest_api_tests.rb +0 -118
  63. data/spec/koala/rest_api/rest_api_with_access_token_tests.rb +0 -38
  64. data/spec/koala/typhoeus_service_tests.rb +0 -156
  65. data/spec/koala_spec.rb +0 -18
  66. data/spec/koala_spec_helper.rb +0 -70
  67. data/spec/koala_spec_without_mocks.rb +0 -19
  68. data/spec/mock_http_service.rb +0 -96
  69. /data/spec/{koala/assets → fixtures}/beach.jpg +0 -0
@@ -13,143 +13,34 @@ module Koala
13
13
  def self.included(base)
14
14
  base.class_eval do
15
15
  class << self
16
- attr_accessor :always_use_ssl
16
+ attr_accessor :always_use_ssl, :proxy, :timeout
17
17
  end
18
-
19
- protected
20
- def self.params_require_multipart?(param_hash)
21
- param_hash.any? { |key, value| is_valid_file_hash?(value) }
22
- end
23
-
24
- # A file hash can take two forms:
25
- # - A hash with "content_type" and "path" keys where "path" is the local path
26
- # to the file to be uploaded.
27
- # - A hash with the "file" key containing an already-opened IO that responds to "read"
28
- # as well as "content_type" and the "path" key to the original file
29
- # ("path"" is required by multipart-post even for opened files)
30
- #
31
- # Valid inputs for a file to be posted via multipart/form-data
32
- # are based on the criteria for an UploadIO to be created
33
- # See : https://github.com/nicksieger/multipart-post/blob/master/lib/composite_io.rb
34
- def self.is_valid_file_hash?(value)
35
- value.kind_of?(Hash) and value.key?("content_type") and value.key?("path") and (
36
- !value.key?("file") or value["file"].respond_to?(:read)
37
- )
38
- end
39
- end
40
- end
41
- end
42
18
 
43
- module NetHTTPService
44
- # this service uses Net::HTTP to send requests to the graph
45
- def self.included(base)
46
- base.class_eval do
47
- require "net/http" unless defined?(Net::HTTP)
48
- require "net/https"
49
- require "net/http/post/multipart"
50
-
51
- include Koala::HTTPService
52
-
53
- def self.make_request(path, args, verb, options = {})
54
- # We translate args to a valid query string. If post is specified,
55
- # we send a POST request to the given path with the given arguments.
56
-
57
- # by default, we use SSL only for private requests
58
- # this makes public requests faster
59
- private_request = args["access_token"] || @always_use_ssl || options[:use_ssl]
60
-
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
-
64
- server = options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER
65
- http = Net::HTTP.new(server, private_request ? 443 : nil)
66
- http.use_ssl = true if private_request
67
-
68
- # we turn off certificate validation to avoid the
69
- # "warning: peer certificate won't be verified in this SSL session" warning
70
- # not sure if this is the right way to handle it
71
- # see http://redcorundum.blogspot.com/2008/03/ssl-certificates-and-nethttps.html
72
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
73
-
74
- result = http.start do |http|
75
- response, body = if verb == "post"
76
- if params_require_multipart? args
77
- http.request Net::HTTP::Post::Multipart.new path, encode_multipart_params(args)
78
- else
79
- http.post(path, encode_params(args))
80
- end
81
- else
82
- http.get("#{path}?#{encode_params(args)}")
83
- end
84
-
85
- Koala::Response.new(response.code.to_i, body, response)
86
- end
19
+ def self.server(options = {})
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}"
87
23
  end
88
-
89
- protected
24
+
90
25
  def self.encode_params(param_hash)
91
26
  # unfortunately, we can't use to_query because that's Rails, not Ruby
92
27
  # if no hash (e.g. no auth token) return empty string
93
28
  ((param_hash || {}).collect do |key_and_value|
94
- key_and_value[1] = key_and_value[1].to_json if key_and_value[1].class != String
29
+ key_and_value[1] = MultiJson.encode(key_and_value[1]) unless key_and_value[1].is_a? String
95
30
  "#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
96
31
  end).join("&")
97
32
  end
33
+
34
+ protected
98
35
 
99
- def self.encode_multipart_params(param_hash)
100
- Hash[*param_hash.collect do |key, value|
101
- if is_valid_file_hash?(value)
102
- if value.key?("file")
103
- value = UploadIO.new(value["file"], value["content_type"], value["path"])
104
- else
105
- value = UploadIO.new(value["path"], value['content_type'])
106
- end
107
- end
108
-
109
- [key, value]
110
- end.flatten]
36
+ def self.params_require_multipart?(param_hash)
37
+ param_hash.any? { |key, value| value.kind_of?(Koala::UploadableIO) }
111
38
  end
112
- end
113
- end
114
- end
115
-
116
- module TyphoeusService
117
- # this service uses Typhoeus to send requests to the graph
118
-
119
- # used for multipart file uploads (see below)
120
- class NetHTTPInterface
121
- include NetHTTPService
122
- end
123
-
124
- def self.included(base)
125
- base.class_eval do
126
- require "typhoeus" unless defined?(Typhoeus)
127
- include Typhoeus
128
-
129
- include Koala::HTTPService
130
-
131
- def self.make_request(path, args, verb, options = {})
132
- unless params_require_multipart?(args)
133
- # if the verb isn't get or post, send it as a post argument
134
- args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
135
- server = options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER
136
-
137
- # you can pass arguments directly to Typhoeus using the :typhoeus_options key
138
- typhoeus_options = {:params => args}.merge(options[:typhoeus_options] || {})
139
-
140
- # by default, we use SSL only for private requests (e.g. with access token)
141
- # this makes public requests faster
142
- prefix = (args["access_token"] || @always_use_ssl || options[:use_ssl]) ? "https" : "http"
143
-
144
- response = self.send(verb, "#{prefix}://#{server}#{path}", typhoeus_options)
145
- Koala::Response.new(response.code, response.body, response.headers_hash)
146
- else
147
- # we have to use NetHTTPService for multipart for file uploads
148
- # until Typhoeus integrates support
149
- Koala::TyphoeusService::NetHTTPInterface.make_request(path, args, verb, options)
150
- end
39
+
40
+ def self.multipart_requires_content_type?
41
+ true
151
42
  end
152
- end # class_eval
43
+ end
153
44
  end
154
45
  end
155
46
  end
@@ -0,0 +1,181 @@
1
+ module Koala
2
+ module Facebook
3
+ class OAuth
4
+ attr_reader :app_id, :app_secret, :oauth_callback_url
5
+ def initialize(app_id, app_secret, oauth_callback_url = nil)
6
+ @app_id = app_id
7
+ @app_secret = app_secret
8
+ @oauth_callback_url = oauth_callback_url
9
+ end
10
+
11
+ def get_user_info_from_cookie(cookie_hash)
12
+ # Parses the cookie set by the official Facebook JavaScript SDK.
13
+ #
14
+ # cookies should be a Hash, like the one Rails provides
15
+ #
16
+ # If the user is logged in via Facebook, we return a dictionary with the
17
+ # keys "uid" and "access_token". The former is the user's Facebook ID,
18
+ # and the latter can be used to make authenticated requests to the Graph API.
19
+ # If the user is not logged in, we return None.
20
+ #
21
+ # Download the official Facebook JavaScript SDK at
22
+ # http://github.com/facebook/connect-js/. Read more about Facebook
23
+ # authentication at http://developers.facebook.com/docs/authentication/.
24
+
25
+ if fb_cookie = cookie_hash["fbs_" + @app_id.to_s]
26
+ # remove the opening/closing quote
27
+ fb_cookie = fb_cookie.gsub(/\"/, "")
28
+
29
+ # since we no longer get individual cookies, we have to separate out the components ourselves
30
+ components = {}
31
+ fb_cookie.split("&").map {|param| param = param.split("="); components[param[0]] = param[1]}
32
+
33
+ # generate the signature and make sure it matches what we expect
34
+ auth_string = components.keys.sort.collect {|a| a == "sig" ? nil : "#{a}=#{components[a]}"}.reject {|a| a.nil?}.join("")
35
+ sig = Digest::MD5.hexdigest(auth_string + @app_secret)
36
+ sig == components["sig"] && (components["expires"] == "0" || Time.now.to_i < components["expires"].to_i) ? components : nil
37
+ end
38
+ end
39
+ alias_method :get_user_info_from_cookies, :get_user_info_from_cookie
40
+
41
+ def get_user_from_cookie(cookies)
42
+ if info = get_user_info_from_cookies(cookies)
43
+ string = info["uid"]
44
+ end
45
+ end
46
+ alias_method :get_user_from_cookies, :get_user_from_cookie
47
+
48
+ # URLs
49
+
50
+ def url_for_oauth_code(options = {})
51
+ # for permissions, see http://developers.facebook.com/docs/authentication/permissions
52
+ permissions = options[:permissions]
53
+ scope = permissions ? "&scope=#{permissions.is_a?(Array) ? permissions.join(",") : permissions}" : ""
54
+ display = options.has_key?(:display) ? "&display=#{options[:display]}" : ""
55
+
56
+ callback = options[:callback] || @oauth_callback_url
57
+ raise ArgumentError, "url_for_oauth_code must get a callback either from the OAuth object or in the options!" unless callback
58
+
59
+ # Creates the URL for oauth authorization for a given callback and optional set of permissions
60
+ "https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}#{display}"
61
+ end
62
+
63
+ def url_for_access_token(code, options = {})
64
+ # Creates the URL for the token corresponding to a given code generated by Facebook
65
+ callback = options[:callback] || @oauth_callback_url
66
+ raise ArgumentError, "url_for_access_token must get a callback either from the OAuth object or in the parameters!" unless callback
67
+ "https://#{GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@app_secret}&code=#{code}"
68
+ end
69
+
70
+ def get_access_token_info(code, options = {})
71
+ # convenience method to get a parsed token from Facebook for a given code
72
+ # should this require an OAuth callback URL?
73
+ get_token_from_server({:code => code, :redirect_uri => @oauth_callback_url}, false, options)
74
+ end
75
+
76
+ def get_access_token(code, options = {})
77
+ # upstream methods will throw errors if needed
78
+ if info = get_access_token_info(code, options)
79
+ string = info["access_token"]
80
+ end
81
+ end
82
+
83
+ def get_app_access_token_info(options = {})
84
+ # convenience method to get a the application's sessionless access token
85
+ get_token_from_server({:type => 'client_cred'}, true, options)
86
+ end
87
+
88
+ def get_app_access_token(options = {})
89
+ if info = get_app_access_token_info(options)
90
+ string = info["access_token"]
91
+ end
92
+ end
93
+
94
+ # Originally provided directly by Facebook, however this has changed
95
+ # as their concept of crypto changed. For historic purposes, this is their proposal:
96
+ # https://developers.facebook.com/docs/authentication/canvas/encryption_proposal/
97
+ # Currently see https://github.com/facebook/php-sdk/blob/master/src/facebook.php#L758
98
+ # for a more accurate reference implementation strategy.
99
+ def parse_signed_request(input)
100
+ encoded_sig, encoded_envelope = input.split('.', 2)
101
+ signature = base64_url_decode(encoded_sig).unpack("H*").first
102
+ envelope = MultiJson.decode(base64_url_decode(encoded_envelope))
103
+
104
+ raise "SignedRequest: Unsupported algorithm #{envelope['algorithm']}" if envelope['algorithm'] != 'HMAC-SHA256'
105
+
106
+ # now see if the signature is valid (digest, key, data)
107
+ hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @app_secret, encoded_envelope.tr("-_", "+/"))
108
+ raise 'SignedRequest: Invalid signature' if (signature != hmac)
109
+
110
+ return envelope
111
+ end
112
+
113
+ # from session keys
114
+ def get_token_info_from_session_keys(sessions, options = {})
115
+ # fetch the OAuth tokens from Facebook
116
+ response = fetch_token_string({
117
+ :type => 'client_cred',
118
+ :sessions => sessions.join(",")
119
+ }, true, "exchange_sessions", options)
120
+
121
+ # Facebook returns an empty body in certain error conditions
122
+ if response == ""
123
+ raise APIError.new({
124
+ "type" => "ArgumentError",
125
+ "message" => "get_token_from_session_key received an error (empty response body) for sessions #{sessions.inspect}!"
126
+ })
127
+ end
128
+
129
+ MultiJson.decode(response)
130
+ end
131
+
132
+ def get_tokens_from_session_keys(sessions, options = {})
133
+ # get the original hash results
134
+ results = get_token_info_from_session_keys(sessions, options)
135
+ # now recollect them as just the access tokens
136
+ results.collect { |r| r ? r["access_token"] : nil }
137
+ end
138
+
139
+ def get_token_from_session_key(session, options = {})
140
+ # convenience method for a single key
141
+ # gets the overlaoded strings automatically
142
+ get_tokens_from_session_keys([session], options)[0]
143
+ end
144
+
145
+ protected
146
+
147
+ def get_token_from_server(args, post = false, options = {})
148
+ # fetch the result from Facebook's servers
149
+ result = fetch_token_string(args, post, "access_token", options)
150
+
151
+ # if we have an error, parse the error JSON and raise an error
152
+ raise APIError.new((MultiJson.decode(result)["error"] rescue nil) || {}) if result =~ /error/
153
+
154
+ # otherwise, parse the access token
155
+ parse_access_token(result)
156
+ end
157
+
158
+ def parse_access_token(response_text)
159
+ components = response_text.split("&").inject({}) do |hash, bit|
160
+ key, value = bit.split("=")
161
+ hash.merge!(key => value)
162
+ end
163
+ components
164
+ end
165
+
166
+ def fetch_token_string(args, post = false, endpoint = "access_token", options = {})
167
+ Koala.make_request("/oauth/#{endpoint}", {
168
+ :client_id => @app_id,
169
+ :client_secret => @app_secret
170
+ }.merge!(args), post ? "post" : "get", {:use_ssl => true}.merge!(options)).body
171
+ end
172
+
173
+ # base 64
174
+ # directly from https://github.com/facebook/crypto-request-examples/raw/master/sample.rb
175
+ def base64_url_decode(str)
176
+ str += '=' * (4 - str.length.modulo(4))
177
+ Base64.decode64(str.tr('-_', '+/'))
178
+ end
179
+ end
180
+ end
181
+ end
@@ -46,6 +46,8 @@ module Koala
46
46
  oauth = Koala::Facebook::OAuth.new(@app_id, @secret)
47
47
  @app_access_token = oauth.get_app_access_token
48
48
  end
49
+
50
+ @graph_api = GraphAPI.new(@app_access_token)
49
51
  end
50
52
 
51
53
  # subscribes for realtime updates
@@ -59,7 +61,7 @@ module Koala
59
61
  :verify_token => verify_token
60
62
  }
61
63
  # a subscription is a success if Facebook returns a 200 (after hitting your server for verification)
62
- api(subscription_path, args, 'post', :http_component => :status) == 200
64
+ @graph_api.graph_call(subscription_path, args, 'post', :http_component => :status) == 200
63
65
  end
64
66
 
65
67
  # removes subscription for object
@@ -67,24 +69,13 @@ module Koala
67
69
  def unsubscribe(object = nil)
68
70
  args = {}
69
71
  args[:object] = object if object
70
- api(subscription_path, args, 'delete', :http_component => :status) == 200
72
+ @graph_api.graph_call(subscription_path, args, 'delete', :http_component => :status) == 200
71
73
  end
72
74
 
73
75
  def list_subscriptions
74
- api(subscription_path)["data"]
76
+ @graph_api.graph_call(subscription_path)["data"]
75
77
  end
76
78
 
77
- def api(*args) # same as GraphAPI
78
- response = super(*args) do |response|
79
- # check for subscription errors
80
- if response.is_a?(Hash) && error_details = response["error"]
81
- raise APIError.new(error_details)
82
- end
83
- end
84
-
85
- response
86
- end
87
-
88
79
  protected
89
80
 
90
81
  def subscription_path
@@ -3,21 +3,26 @@ module Koala
3
3
  REST_SERVER = "api.facebook.com"
4
4
 
5
5
  module RestAPIMethods
6
- def fql_query(fql)
7
- rest_call('fql.query', 'query' => fql)
6
+ def fql_query(fql, args = {}, options = {})
7
+ rest_call('fql.query', args.merge(:query => fql), options)
8
8
  end
9
9
 
10
- def rest_call(method, args = {}, options = {})
11
- options = options.merge!(:rest_api => true, :read_only => READ_ONLY_METHODS.include?(method))
10
+ def fql_multiquery(queries = {}, args = {}, options = {})
11
+ if results = rest_call('fql.multiquery', args.merge(:queries => MultiJson.encode(queries)), options)
12
+ # simplify the multiquery result format
13
+ results.inject({}) {|outcome, data| outcome[data["name"]] = data["fql_result_set"]; outcome}
14
+ end
15
+ end
12
16
 
13
- response = api("method/#{method}", args.merge('format' => 'json'), 'get', options) do |response|
17
+ def rest_call(fb_method, args = {}, options = {}, method = "get")
18
+ options = options.merge!(:rest_api => true, :read_only => READ_ONLY_METHODS.include?(fb_method.to_s))
19
+
20
+ api("method/#{fb_method}", args.merge('format' => 'json'), method, options) do |response|
14
21
  # check for REST API-specific errors
15
22
  if response.is_a?(Hash) && response["error_code"]
16
23
  raise APIError.new("type" => response["error_code"], "message" => response["error_msg"])
17
24
  end
18
25
  end
19
-
20
- response
21
26
  end
22
27
 
23
28
  # read-only methods for which we can use API-read
@@ -87,4 +92,4 @@ module Koala
87
92
  end
88
93
 
89
94
  end # module Facebook
90
- end # module Koala
95
+ end # module Koala
@@ -20,11 +20,11 @@ module Koala
20
20
  @graph_api = GraphAPI.new(@app_access_token)
21
21
  end
22
22
 
23
- def create(installed, permissions = nil)
23
+ def create(installed, permissions = nil, args = {}, options = {})
24
24
  # Creates and returns a test user
25
- args = {'installed' => installed}
25
+ args['installed'] = installed
26
26
  args['permissions'] = (permissions.is_a?(Array) ? permissions.join(",") : permissions) if installed
27
- result = @graph_api.graph_call(accounts_path, args, "post")
27
+ result = @graph_api.graph_call(accounts_path, args, "post", options)
28
28
  end
29
29
 
30
30
  def list
@@ -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 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")
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,175 @@
1
+ require 'koala'
2
+
3
+ module Koala
4
+ class UploadableIO
5
+ attr_reader :io_or_path, :content_type, :requires_base_http_service
6
+
7
+ def initialize(io_or_path_or_mixed, content_type = nil, filename = 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
+ # Probably a StringIO or similar object, which won't work with Typhoeus
17
+ @requires_base_http_service = @io_or_path.respond_to?(:read) && !@io_or_path.kind_of?(File)
18
+
19
+ # filename is used in the Ads API
20
+ @filename = filename || "koala-io-file.dum"
21
+
22
+ raise KoalaError.new("Invalid arguments to initialize an UploadableIO") unless @io_or_path
23
+ raise KoalaError.new("Unable to determine MIME type for UploadableIO") if !@content_type && Koala.multipart_requires_content_type?
24
+ end
25
+
26
+ def to_upload_io
27
+ UploadIO.new(@io_or_path, @content_type, @filename)
28
+ end
29
+
30
+ def to_file
31
+ @io_or_path.is_a?(String) ? File.open(@io_or_path) : @io_or_path
32
+ end
33
+
34
+ def self.binary_content?(content)
35
+ content.is_a?(UploadableIO) || DETECTION_STRATEGIES.detect {|method| send(method, content)}
36
+ end
37
+
38
+ private
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
+ ]
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)
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)
80
+ 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)
92
+ 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)
99
+ 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
112
+ end
113
+ 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
125
+ end
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 ""
134
+ nil
135
+ # images
136
+ when "jpg", "jpeg"
137
+ "image/jpeg"
138
+ when "png"
139
+ "image/png"
140
+ when "gif"
141
+ "image/gif"
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
174
+ end
175
+ end