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.
- data/.autotest +12 -0
- data/.gitignore +5 -0
- data/.travis.yml +8 -0
- data/CHANGELOG +42 -4
- data/Gemfile +7 -0
- data/LICENSE +1 -1
- data/Manifest +5 -2
- data/Rakefile +13 -14
- data/autotest/discover.rb +1 -0
- data/koala.gemspec +35 -20
- data/lib/koala/batch_operation.rb +74 -0
- data/lib/koala/graph_api.rb +196 -143
- data/lib/koala/graph_batch_api.rb +87 -0
- data/lib/koala/graph_collection.rb +54 -0
- data/lib/koala/http_services/net_http_service.rb +92 -0
- data/lib/koala/http_services/typhoeus_service.rb +37 -0
- data/lib/koala/http_services.rb +15 -124
- data/lib/koala/oauth.rb +181 -0
- data/lib/koala/realtime_updates.rb +5 -14
- data/lib/koala/rest_api.rb +13 -8
- data/lib/koala/test_users.rb +21 -8
- data/lib/koala/uploadable_io.rb +175 -0
- data/lib/koala.rb +48 -240
- data/readme.md +60 -28
- data/spec/cases/api_base_spec.rb +101 -0
- data/spec/cases/graph_and_rest_api_spec.rb +31 -0
- data/spec/cases/graph_api_batch_spec.rb +609 -0
- data/spec/cases/graph_api_spec.rb +25 -0
- data/spec/cases/http_services/http_service_spec.rb +129 -0
- data/spec/cases/http_services/net_http_service_spec.rb +532 -0
- data/spec/cases/http_services/typhoeus_service_spec.rb +152 -0
- data/spec/cases/koala_spec.rb +55 -0
- data/spec/cases/oauth_spec.rb +409 -0
- data/spec/cases/realtime_updates_spec.rb +184 -0
- data/spec/cases/rest_api_spec.rb +25 -0
- data/spec/{koala/test_users/test_users_tests.rb → cases/test_users_spec.rb} +47 -34
- data/spec/cases/uploadable_io_spec.rb +193 -0
- data/spec/fixtures/cat.m4v +0 -0
- data/spec/{facebook_data.yml → fixtures/facebook_data.yml} +12 -14
- data/spec/{mock_facebook_responses.yml → fixtures/mock_facebook_responses.yml} +408 -306
- data/spec/spec_helper.rb +19 -0
- data/spec/support/graph_api_shared_examples.rb +495 -0
- data/spec/support/json_testing_fix.rb +18 -0
- data/spec/{koala → support}/live_testing_data_helper.rb +39 -42
- data/spec/support/mock_http_service.rb +96 -0
- data/spec/support/rest_api_shared_examples.rb +285 -0
- data/spec/support/setup_mocks_or_live.rb +51 -0
- data/spec/support/uploadable_io_shared_examples.rb +76 -0
- metadata +110 -64
- data/init.rb +0 -2
- data/spec/koala/api_base_tests.rb +0 -102
- data/spec/koala/graph_and_rest_api/graph_and_rest_api_no_token_tests.rb +0 -14
- data/spec/koala/graph_and_rest_api/graph_and_rest_api_with_token_tests.rb +0 -16
- data/spec/koala/graph_api/graph_api_no_access_token_tests.rb +0 -63
- data/spec/koala/graph_api/graph_api_tests.rb +0 -86
- data/spec/koala/graph_api/graph_api_with_access_token_tests.rb +0 -154
- data/spec/koala/graph_api/graph_collection_tests.rb +0 -104
- data/spec/koala/net_http_service_tests.rb +0 -430
- data/spec/koala/oauth/oauth_tests.rb +0 -409
- data/spec/koala/realtime_updates/realtime_updates_tests.rb +0 -187
- data/spec/koala/rest_api/rest_api_no_access_token_tests.rb +0 -25
- data/spec/koala/rest_api/rest_api_tests.rb +0 -118
- data/spec/koala/rest_api/rest_api_with_access_token_tests.rb +0 -38
- data/spec/koala/typhoeus_service_tests.rb +0 -156
- data/spec/koala_spec.rb +0 -18
- data/spec/koala_spec_helper.rb +0 -70
- data/spec/koala_spec_without_mocks.rb +0 -19
- data/spec/mock_http_service.rb +0 -96
- /data/spec/{koala/assets → fixtures}/beach.jpg +0 -0
data/lib/koala/http_services.rb
CHANGED
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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]
|
|
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.
|
|
100
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
43
|
+
end
|
|
153
44
|
end
|
|
154
45
|
end
|
|
155
46
|
end
|
data/lib/koala/oauth.rb
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
72
|
+
@graph_api.graph_call(subscription_path, args, 'delete', :http_component => :status) == 200
|
|
71
73
|
end
|
|
72
74
|
|
|
73
75
|
def list_subscriptions
|
|
74
|
-
|
|
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
|
data/lib/koala/rest_api.rb
CHANGED
|
@@ -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',
|
|
6
|
+
def fql_query(fql, args = {}, options = {})
|
|
7
|
+
rest_call('fql.query', args.merge(:query => fql), options)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def
|
|
11
|
-
|
|
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
|
-
|
|
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
|
data/lib/koala/test_users.rb
CHANGED
|
@@ -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
|
|
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(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|