koala 0.9.0 → 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 (82) hide show
  1. data/.autotest +12 -0
  2. data/.gitignore +5 -0
  3. data/.travis.yml +8 -0
  4. data/CHANGELOG +71 -7
  5. data/Gemfile +7 -0
  6. data/LICENSE +1 -1
  7. data/Manifest +10 -15
  8. data/Rakefile +13 -13
  9. data/autotest/discover.rb +1 -0
  10. data/koala.gemspec +36 -16
  11. data/lib/koala/batch_operation.rb +74 -0
  12. data/lib/koala/graph_api.rb +200 -134
  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 +22 -47
  18. data/lib/koala/oauth.rb +181 -0
  19. data/lib/koala/realtime_updates.rb +5 -14
  20. data/lib/koala/rest_api.rb +82 -10
  21. data/lib/koala/test_users.rb +85 -0
  22. data/lib/koala/uploadable_io.rb +175 -0
  23. data/lib/koala.rb +67 -229
  24. data/readme.md +71 -38
  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/cases/test_users_spec.rb +221 -0
  37. data/spec/cases/uploadable_io_spec.rb +193 -0
  38. data/spec/fixtures/beach.jpg +0 -0
  39. data/spec/fixtures/cat.m4v +0 -0
  40. data/spec/{facebook_data.yml → fixtures/facebook_data.yml} +18 -14
  41. data/spec/fixtures/mock_facebook_responses.yml +409 -0
  42. data/spec/spec_helper.rb +19 -0
  43. data/spec/support/graph_api_shared_examples.rb +495 -0
  44. data/spec/support/json_testing_fix.rb +18 -0
  45. data/spec/support/live_testing_data_helper.rb +40 -0
  46. data/spec/support/mock_http_service.rb +96 -0
  47. data/spec/support/rest_api_shared_examples.rb +285 -0
  48. data/spec/support/setup_mocks_or_live.rb +51 -0
  49. data/spec/support/uploadable_io_shared_examples.rb +76 -0
  50. metadata +131 -59
  51. data/examples/oauth_playground/Capfile +0 -2
  52. data/examples/oauth_playground/LICENSE +0 -22
  53. data/examples/oauth_playground/Rakefile +0 -4
  54. data/examples/oauth_playground/config/deploy.rb +0 -39
  55. data/examples/oauth_playground/config/facebook.yml +0 -13
  56. data/examples/oauth_playground/config.ru +0 -27
  57. data/examples/oauth_playground/lib/load_facebook.rb +0 -3
  58. data/examples/oauth_playground/lib/oauth_playground.rb +0 -187
  59. data/examples/oauth_playground/readme.md +0 -8
  60. data/examples/oauth_playground/spec/oauth_playground_spec.rb +0 -35
  61. data/examples/oauth_playground/spec/spec_helper.rb +0 -36
  62. data/examples/oauth_playground/tmp/restart.txt +0 -0
  63. data/examples/oauth_playground/views/index.erb +0 -206
  64. data/examples/oauth_playground/views/layout.erb +0 -39
  65. data/init.rb +0 -2
  66. data/spec/koala/api_base_tests.rb +0 -95
  67. data/spec/koala/graph_and_rest_api/graph_and_rest_api_no_token_tests.rb +0 -10
  68. data/spec/koala/graph_and_rest_api/graph_and_rest_api_with_token_tests.rb +0 -11
  69. data/spec/koala/graph_api/graph_api_no_access_token_tests.rb +0 -114
  70. data/spec/koala/graph_api/graph_api_with_access_token_tests.rb +0 -150
  71. data/spec/koala/graph_api/graph_collection_tests.rb +0 -104
  72. data/spec/koala/live_testing_data_helper.rb +0 -15
  73. data/spec/koala/net_http_service_tests.rb +0 -181
  74. data/spec/koala/oauth/oauth_tests.rb +0 -440
  75. data/spec/koala/realtime_updates/realtime_updates_tests.rb +0 -187
  76. data/spec/koala/rest_api/rest_api_no_access_token_tests.rb +0 -94
  77. data/spec/koala/rest_api/rest_api_with_access_token_tests.rb +0 -36
  78. data/spec/koala_spec.rb +0 -18
  79. data/spec/koala_spec_helper.rb +0 -31
  80. data/spec/koala_spec_without_mocks.rb +0 -19
  81. data/spec/mock_facebook_responses.yml +0 -241
  82. data/spec/mock_http_service.rb +0 -81
@@ -8,64 +8,39 @@ module Koala
8
8
  end
9
9
  end
10
10
 
11
- module NetHTTPService
12
- # this service uses Net::HTTP to send requests to the graph
11
+ module HTTPService
12
+ # common functionality for all HTTP services
13
13
  def self.included(base)
14
14
  base.class_eval do
15
- require 'net/http' unless defined?(Net::HTTP)
16
- require 'net/https'
17
-
18
- def self.make_request(path, args, verb, options = {})
19
- # We translate args to a valid query string. If post is specified,
20
- # we send a POST request to the given path with the given arguments.
21
-
22
- # if the verb isn't get or post, send it as a post argument
23
- args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
24
-
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)}"))
36
- Koala::Response.new(response.code.to_i, body, response)
37
- }
15
+ class << self
16
+ attr_accessor :always_use_ssl, :proxy, :timeout
38
17
  end
39
18
 
40
- protected
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}"
23
+ end
24
+
41
25
  def self.encode_params(param_hash)
42
26
  # unfortunately, we can't use to_query because that's Rails, not Ruby
43
27
  # if no hash (e.g. no auth token) return empty string
44
- ((param_hash || {}).collect do |key_and_value|
45
- key_and_value[1] = key_and_value[1].to_json if key_and_value[1].class != String
28
+ ((param_hash || {}).collect do |key_and_value|
29
+ key_and_value[1] = MultiJson.encode(key_and_value[1]) unless key_and_value[1].is_a? String
46
30
  "#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
47
31
  end).join("&")
48
32
  end
49
- end
50
- end
51
- end
52
-
53
- module TyphoeusService
54
- # this service uses Typhoeus to send requests to the graph
55
- def self.included(base)
56
- base.class_eval do
57
- require 'typhoeus' unless defined?(Typhoeus)
58
- include Typhoeus
59
-
60
- 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)
33
+
34
+ protected
35
+
36
+ def self.params_require_multipart?(param_hash)
37
+ param_hash.any? { |key, value| value.kind_of?(Koala::UploadableIO) }
67
38
  end
68
- end # class_eval
39
+
40
+ def self.multipart_requires_content_type?
41
+ true
42
+ end
43
+ end
69
44
  end
70
45
  end
71
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
@@ -1,23 +1,95 @@
1
1
  module Koala
2
2
  module Facebook
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
-
10
- def rest_call(method, args = {})
11
- response = api("method/#{method}", args.merge('format' => 'json'), 'get', :rest_api => true) do |response|
9
+
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
16
+
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|
12
21
  # check for REST API-specific errors
13
22
  if response.is_a?(Hash) && response["error_code"]
14
23
  raise APIError.new("type" => response["error_code"], "message" => response["error_msg"])
15
24
  end
16
25
  end
17
-
18
- response
19
26
  end
27
+
28
+ # read-only methods for which we can use API-read
29
+ # taken directly from the FB PHP library (https://github.com/facebook/php-sdk/blob/master/src/facebook.php)
30
+ READ_ONLY_METHODS = [
31
+ 'admin.getallocation',
32
+ 'admin.getappproperties',
33
+ 'admin.getbannedusers',
34
+ 'admin.getlivestreamvialink',
35
+ 'admin.getmetrics',
36
+ 'admin.getrestrictioninfo',
37
+ 'application.getpublicinfo',
38
+ 'auth.getapppublickey',
39
+ 'auth.getsession',
40
+ 'auth.getsignedpublicsessiondata',
41
+ 'comments.get',
42
+ 'connect.getunconnectedfriendscount',
43
+ 'dashboard.getactivity',
44
+ 'dashboard.getcount',
45
+ 'dashboard.getglobalnews',
46
+ 'dashboard.getnews',
47
+ 'dashboard.multigetcount',
48
+ 'dashboard.multigetnews',
49
+ 'data.getcookies',
50
+ 'events.get',
51
+ 'events.getmembers',
52
+ 'fbml.getcustomtags',
53
+ 'feed.getappfriendstories',
54
+ 'feed.getregisteredtemplatebundlebyid',
55
+ 'feed.getregisteredtemplatebundles',
56
+ 'fql.multiquery',
57
+ 'fql.query',
58
+ 'friends.arefriends',
59
+ 'friends.get',
60
+ 'friends.getappusers',
61
+ 'friends.getlists',
62
+ 'friends.getmutualfriends',
63
+ 'gifts.get',
64
+ 'groups.get',
65
+ 'groups.getmembers',
66
+ 'intl.gettranslations',
67
+ 'links.get',
68
+ 'notes.get',
69
+ 'notifications.get',
70
+ 'pages.getinfo',
71
+ 'pages.isadmin',
72
+ 'pages.isappadded',
73
+ 'pages.isfan',
74
+ 'permissions.checkavailableapiaccess',
75
+ 'permissions.checkgrantedapiaccess',
76
+ 'photos.get',
77
+ 'photos.getalbums',
78
+ 'photos.gettags',
79
+ 'profile.getinfo',
80
+ 'profile.getinfooptions',
81
+ 'stream.get',
82
+ 'stream.getcomments',
83
+ 'stream.getfilters',
84
+ 'users.getinfo',
85
+ 'users.getloggedinuser',
86
+ 'users.getstandardinfo',
87
+ 'users.hasapppermission',
88
+ 'users.isappuser',
89
+ 'users.isverified',
90
+ 'video.getuploadlimits'
91
+ ]
20
92
  end
21
-
93
+
22
94
  end # module Facebook
23
- end # module Koala
95
+ 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,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