koala 1.0.0 → 1.1.0rc3
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 +3 -1
- data/.travis.yml +8 -0
- data/CHANGELOG +25 -2
- data/autotest/discover.rb +1 -0
- data/koala.gemspec +8 -8
- data/lib/koala/batch_operation.rb +74 -0
- data/lib/koala/graph_api.rb +103 -102
- 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 +13 -113
- 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/uploadable_io.rb +137 -77
- data/lib/koala.rb +36 -196
- data/readme.md +24 -12
- data/spec/cases/api_base_spec.rb +4 -4
- data/spec/cases/graph_api_batch_spec.rb +609 -0
- data/spec/cases/http_services/http_service_spec.rb +87 -12
- data/spec/cases/http_services/net_http_service_spec.rb +259 -77
- data/spec/cases/http_services/typhoeus_service_spec.rb +29 -21
- data/spec/cases/koala_spec.rb +55 -0
- data/spec/cases/oauth_spec.rb +1 -1
- data/spec/cases/realtime_updates_spec.rb +3 -3
- data/spec/cases/test_users_spec.rb +1 -1
- data/spec/cases/uploadable_io_spec.rb +56 -14
- data/spec/fixtures/cat.m4v +0 -0
- data/spec/fixtures/mock_facebook_responses.yml +100 -5
- data/spec/spec_helper.rb +1 -1
- data/spec/support/graph_api_shared_examples.rb +106 -35
- data/spec/support/mock_http_service.rb +57 -56
- data/spec/support/rest_api_shared_examples.rb +131 -7
- data/spec/support/setup_mocks_or_live.rb +3 -4
- metadata +33 -48
data/lib/koala/uploadable_io.rb
CHANGED
|
@@ -2,9 +2,9 @@ require 'koala'
|
|
|
2
2
|
|
|
3
3
|
module Koala
|
|
4
4
|
class UploadableIO
|
|
5
|
-
attr_reader :io_or_path, :content_type
|
|
5
|
+
attr_reader :io_or_path, :content_type, :requires_base_http_service
|
|
6
6
|
|
|
7
|
-
def initialize(io_or_path_or_mixed, content_type = nil)
|
|
7
|
+
def initialize(io_or_path_or_mixed, content_type = nil, filename = nil)
|
|
8
8
|
# see if we got the right inputs
|
|
9
9
|
if content_type.nil?
|
|
10
10
|
parse_init_mixed_param io_or_path_or_mixed
|
|
@@ -13,103 +13,163 @@ module Koala
|
|
|
13
13
|
@content_type = content_type
|
|
14
14
|
end
|
|
15
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
|
+
|
|
16
22
|
raise KoalaError.new("Invalid arguments to initialize an UploadableIO") unless @io_or_path
|
|
17
23
|
raise KoalaError.new("Unable to determine MIME type for UploadableIO") if !@content_type && Koala.multipart_requires_content_type?
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
def to_upload_io
|
|
21
|
-
UploadIO.new(@io_or_path, @content_type,
|
|
27
|
+
UploadIO.new(@io_or_path, @content_type, @filename)
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
def to_file
|
|
25
31
|
@io_or_path.is_a?(String) ? File.open(@io_or_path) : @io_or_path
|
|
26
32
|
end
|
|
33
|
+
|
|
34
|
+
def self.binary_content?(content)
|
|
35
|
+
content.is_a?(UploadableIO) || DETECTION_STRATEGIES.detect {|method| send(method, content)}
|
|
36
|
+
end
|
|
27
37
|
|
|
28
38
|
private
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
41
56
|
end
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
49
68
|
end
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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)
|
|
57
80
|
end
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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)
|
|
65
92
|
end
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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)
|
|
72
99
|
end
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
85
112
|
end
|
|
86
|
-
nil # if we can't find anything
|
|
87
113
|
end
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
98
125
|
end
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 ""
|
|
105
134
|
nil
|
|
106
|
-
|
|
135
|
+
# images
|
|
136
|
+
when "jpg", "jpeg"
|
|
107
137
|
"image/jpeg"
|
|
108
|
-
|
|
138
|
+
when "png"
|
|
109
139
|
"image/png"
|
|
110
|
-
|
|
140
|
+
when "gif"
|
|
111
141
|
"image/gif"
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
114
174
|
end
|
|
115
|
-
end
|
|
175
|
+
end
|
data/lib/koala.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require 'cgi'
|
|
2
2
|
require 'digest/md5'
|
|
3
3
|
|
|
4
|
-
require '
|
|
4
|
+
require 'multi_json'
|
|
5
5
|
|
|
6
6
|
# OpenSSL and Base64 are required to support signed_request
|
|
7
7
|
require 'openssl'
|
|
@@ -9,10 +9,16 @@ require 'base64'
|
|
|
9
9
|
|
|
10
10
|
# include koala modules
|
|
11
11
|
require 'koala/http_services'
|
|
12
|
+
require 'koala/http_services/net_http_service'
|
|
13
|
+
require 'koala/oauth'
|
|
12
14
|
require 'koala/graph_api'
|
|
15
|
+
require 'koala/graph_batch_api'
|
|
16
|
+
require 'koala/batch_operation'
|
|
17
|
+
require 'koala/graph_collection'
|
|
13
18
|
require 'koala/rest_api'
|
|
14
19
|
require 'koala/realtime_updates'
|
|
15
20
|
require 'koala/test_users'
|
|
21
|
+
require 'koala/http_services'
|
|
16
22
|
|
|
17
23
|
# add KoalaIO class
|
|
18
24
|
require 'koala/uploadable_io'
|
|
@@ -47,27 +53,27 @@ module Koala
|
|
|
47
53
|
# in the case of a server error
|
|
48
54
|
raise APIError.new({"type" => "HTTP #{result.status.to_s}", "message" => "Response body: #{result.body}"}) if result.status >= 500
|
|
49
55
|
|
|
50
|
-
#
|
|
56
|
+
# parse the body as JSON and run it through the error checker (if provided)
|
|
51
57
|
# Note: Facebook sometimes sends results like "true" and "false", which aren't strictly objects
|
|
52
|
-
# and cause
|
|
53
|
-
body =
|
|
54
|
-
if error_checking_block
|
|
55
|
-
yield(body)
|
|
56
|
-
end
|
|
58
|
+
# and cause MultiJson.decode to fail -- so we account for that by wrapping the result in []
|
|
59
|
+
body = MultiJson.decode("[#{result.body.to_s}]")[0]
|
|
60
|
+
yield body if error_checking_block
|
|
57
61
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
result.send(options[:http_component])
|
|
61
|
-
else
|
|
62
|
-
body
|
|
63
|
-
end
|
|
62
|
+
# if we want a component other than the body (e.g. redirect header for images), return that
|
|
63
|
+
options[:http_component] ? result.send(options[:http_component]) : body
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# APIs
|
|
68
|
+
|
|
67
69
|
class GraphAPI < API
|
|
68
70
|
include GraphAPIMethods
|
|
69
71
|
end
|
|
70
|
-
|
|
72
|
+
|
|
73
|
+
class GraphBatchAPI < GraphAPI
|
|
74
|
+
include GraphBatchAPIMethods
|
|
75
|
+
end
|
|
76
|
+
|
|
71
77
|
class RestAPI < API
|
|
72
78
|
include RestAPIMethods
|
|
73
79
|
end
|
|
@@ -87,6 +93,8 @@ module Koala
|
|
|
87
93
|
attr_reader :graph_api
|
|
88
94
|
end
|
|
89
95
|
|
|
96
|
+
# Errors
|
|
97
|
+
|
|
90
98
|
class APIError < StandardError
|
|
91
99
|
attr_accessor :fb_error_type
|
|
92
100
|
def initialize(details = {})
|
|
@@ -94,201 +102,33 @@ module Koala
|
|
|
94
102
|
super("#{fb_error_type}: #{details["message"]}")
|
|
95
103
|
end
|
|
96
104
|
end
|
|
105
|
+
end
|
|
97
106
|
|
|
107
|
+
class KoalaError < StandardError; end
|
|
98
108
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
@oauth_callback_url = oauth_callback_url
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def get_user_info_from_cookie(cookie_hash)
|
|
108
|
-
# Parses the cookie set by the official Facebook JavaScript SDK.
|
|
109
|
-
#
|
|
110
|
-
# cookies should be a Hash, like the one Rails provides
|
|
111
|
-
#
|
|
112
|
-
# If the user is logged in via Facebook, we return a dictionary with the
|
|
113
|
-
# keys "uid" and "access_token". The former is the user's Facebook ID,
|
|
114
|
-
# and the latter can be used to make authenticated requests to the Graph API.
|
|
115
|
-
# If the user is not logged in, we return None.
|
|
116
|
-
#
|
|
117
|
-
# Download the official Facebook JavaScript SDK at
|
|
118
|
-
# http://github.com/facebook/connect-js/. Read more about Facebook
|
|
119
|
-
# authentication at http://developers.facebook.com/docs/authentication/.
|
|
120
|
-
|
|
121
|
-
if fb_cookie = cookie_hash["fbs_" + @app_id.to_s]
|
|
122
|
-
# remove the opening/closing quote
|
|
123
|
-
fb_cookie = fb_cookie.gsub(/\"/, "")
|
|
124
|
-
|
|
125
|
-
# since we no longer get individual cookies, we have to separate out the components ourselves
|
|
126
|
-
components = {}
|
|
127
|
-
fb_cookie.split("&").map {|param| param = param.split("="); components[param[0]] = param[1]}
|
|
128
|
-
|
|
129
|
-
# generate the signature and make sure it matches what we expect
|
|
130
|
-
auth_string = components.keys.sort.collect {|a| a == "sig" ? nil : "#{a}=#{components[a]}"}.reject {|a| a.nil?}.join("")
|
|
131
|
-
sig = Digest::MD5.hexdigest(auth_string + @app_secret)
|
|
132
|
-
sig == components["sig"] && (components["expires"] == "0" || Time.now.to_i < components["expires"].to_i) ? components : nil
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
alias_method :get_user_info_from_cookies, :get_user_info_from_cookie
|
|
136
|
-
|
|
137
|
-
def get_user_from_cookie(cookies)
|
|
138
|
-
if info = get_user_info_from_cookies(cookies)
|
|
139
|
-
string = info["uid"]
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
alias_method :get_user_from_cookies, :get_user_from_cookie
|
|
143
|
-
|
|
144
|
-
# URLs
|
|
145
|
-
|
|
146
|
-
def url_for_oauth_code(options = {})
|
|
147
|
-
# for permissions, see http://developers.facebook.com/docs/authentication/permissions
|
|
148
|
-
permissions = options[:permissions]
|
|
149
|
-
scope = permissions ? "&scope=#{permissions.is_a?(Array) ? permissions.join(",") : permissions}" : ""
|
|
150
|
-
display = options.has_key?(:display) ? "&display=#{options[:display]}" : ""
|
|
151
|
-
|
|
152
|
-
callback = options[:callback] || @oauth_callback_url
|
|
153
|
-
raise ArgumentError, "url_for_oauth_code must get a callback either from the OAuth object or in the options!" unless callback
|
|
154
|
-
|
|
155
|
-
# Creates the URL for oauth authorization for a given callback and optional set of permissions
|
|
156
|
-
"https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}#{display}"
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def url_for_access_token(code, options = {})
|
|
160
|
-
# Creates the URL for the token corresponding to a given code generated by Facebook
|
|
161
|
-
callback = options[:callback] || @oauth_callback_url
|
|
162
|
-
raise ArgumentError, "url_for_access_token must get a callback either from the OAuth object or in the parameters!" unless callback
|
|
163
|
-
"https://#{GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@app_secret}&code=#{code}"
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def get_access_token_info(code, options = {})
|
|
167
|
-
# convenience method to get a parsed token from Facebook for a given code
|
|
168
|
-
# should this require an OAuth callback URL?
|
|
169
|
-
get_token_from_server({:code => code, :redirect_uri => @oauth_callback_url}, false, options)
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def get_access_token(code, options = {})
|
|
173
|
-
# upstream methods will throw errors if needed
|
|
174
|
-
if info = get_access_token_info(code, options)
|
|
175
|
-
string = info["access_token"]
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def get_app_access_token_info(options = {})
|
|
180
|
-
# convenience method to get a the application's sessionless access token
|
|
181
|
-
get_token_from_server({:type => 'client_cred'}, true, options)
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def get_app_access_token(options = {})
|
|
185
|
-
if info = get_app_access_token_info(options)
|
|
186
|
-
string = info["access_token"]
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Originally provided directly by Facebook, however this has changed
|
|
191
|
-
# as their concept of crypto changed. For historic purposes, this is their proposal:
|
|
192
|
-
# https://developers.facebook.com/docs/authentication/canvas/encryption_proposal/
|
|
193
|
-
# Currently see https://github.com/facebook/php-sdk/blob/master/src/facebook.php#L758
|
|
194
|
-
# for a more accurate reference implementation strategy.
|
|
195
|
-
def parse_signed_request(input)
|
|
196
|
-
encoded_sig, encoded_envelope = input.split('.', 2)
|
|
197
|
-
signature = base64_url_decode(encoded_sig).unpack("H*").first
|
|
198
|
-
envelope = JSON.parse(base64_url_decode(encoded_envelope))
|
|
199
|
-
|
|
200
|
-
raise "SignedRequest: Unsupported algorithm #{envelope['algorithm']}" if envelope['algorithm'] != 'HMAC-SHA256'
|
|
201
|
-
|
|
202
|
-
# now see if the signature is valid (digest, key, data)
|
|
203
|
-
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @app_secret, encoded_envelope.tr("-_", "+/"))
|
|
204
|
-
raise 'SignedRequest: Invalid signature' if (signature != hmac)
|
|
205
|
-
|
|
206
|
-
return envelope
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
# from session keys
|
|
210
|
-
def get_token_info_from_session_keys(sessions, options = {})
|
|
211
|
-
# fetch the OAuth tokens from Facebook
|
|
212
|
-
response = fetch_token_string({
|
|
213
|
-
:type => 'client_cred',
|
|
214
|
-
:sessions => sessions.join(",")
|
|
215
|
-
}, true, "exchange_sessions", options)
|
|
216
|
-
|
|
217
|
-
# Facebook returns an empty body in certain error conditions
|
|
218
|
-
if response == ""
|
|
219
|
-
raise APIError.new({
|
|
220
|
-
"type" => "ArgumentError",
|
|
221
|
-
"message" => "get_token_from_session_key received an error (empty response body) for sessions #{sessions.inspect}!"
|
|
222
|
-
})
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
JSON.parse(response)
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def get_tokens_from_session_keys(sessions, options = {})
|
|
229
|
-
# get the original hash results
|
|
230
|
-
results = get_token_info_from_session_keys(sessions, options)
|
|
231
|
-
# now recollect them as just the access tokens
|
|
232
|
-
results.collect { |r| r ? r["access_token"] : nil }
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def get_token_from_session_key(session, options = {})
|
|
236
|
-
# convenience method for a single key
|
|
237
|
-
# gets the overlaoded strings automatically
|
|
238
|
-
get_tokens_from_session_keys([session], options)[0]
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
protected
|
|
242
|
-
|
|
243
|
-
def get_token_from_server(args, post = false, options = {})
|
|
244
|
-
# fetch the result from Facebook's servers
|
|
245
|
-
result = fetch_token_string(args, post, "access_token", options)
|
|
246
|
-
|
|
247
|
-
# if we have an error, parse the error JSON and raise an error
|
|
248
|
-
raise APIError.new((JSON.parse(result)["error"] rescue nil) || {}) if result =~ /error/
|
|
249
|
-
|
|
250
|
-
# otherwise, parse the access token
|
|
251
|
-
parse_access_token(result)
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def parse_access_token(response_text)
|
|
255
|
-
components = response_text.split("&").inject({}) do |hash, bit|
|
|
256
|
-
key, value = bit.split("=")
|
|
257
|
-
hash.merge!(key => value)
|
|
258
|
-
end
|
|
259
|
-
components
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def fetch_token_string(args, post = false, endpoint = "access_token", options = {})
|
|
263
|
-
Koala.make_request("/oauth/#{endpoint}", {
|
|
264
|
-
:client_id => @app_id,
|
|
265
|
-
:client_secret => @app_secret
|
|
266
|
-
}.merge!(args), post ? "post" : "get", {:use_ssl => true}.merge!(options)).body
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
# base 64
|
|
270
|
-
# directly from https://github.com/facebook/crypto-request-examples/raw/master/sample.rb
|
|
271
|
-
def base64_url_decode(str)
|
|
272
|
-
str += '=' * (4 - str.length.modulo(4))
|
|
273
|
-
Base64.decode64(str.tr('-_', '+/'))
|
|
274
|
-
end
|
|
275
|
-
end
|
|
109
|
+
# Make an api request using the provided api service or one passed by the caller
|
|
110
|
+
def self.make_request(path, args, verb, options = {})
|
|
111
|
+
http_service = options.delete(:http_service) || Koala.http_service
|
|
112
|
+
options = options.merge(:use_ssl => true) if @always_use_ssl
|
|
113
|
+
http_service.make_request(path, args, verb, options)
|
|
276
114
|
end
|
|
277
115
|
|
|
278
|
-
class KoalaError< StandardError; end
|
|
279
|
-
|
|
280
116
|
# finally, set up the http service Koala methods used to make requests
|
|
281
117
|
# you can use your own (for HTTParty, etc.) by calling Koala.http_service = YourModule
|
|
282
|
-
|
|
283
|
-
|
|
118
|
+
class << self
|
|
119
|
+
attr_accessor :http_service
|
|
120
|
+
attr_accessor :always_use_ssl
|
|
121
|
+
attr_accessor :base_http_service
|
|
284
122
|
end
|
|
123
|
+
Koala.base_http_service = NetHTTPService
|
|
285
124
|
|
|
286
125
|
# by default, try requiring Typhoeus -- if that works, use it
|
|
287
126
|
# if you have Typheous and don't want to use it (or want another service),
|
|
288
127
|
# you can run Koala.http_service = NetHTTPService (or MyHTTPService)
|
|
289
128
|
begin
|
|
129
|
+
require 'koala/http_services/typhoeus_service'
|
|
290
130
|
Koala.http_service = TyphoeusService
|
|
291
131
|
rescue LoadError
|
|
292
|
-
Koala.http_service =
|
|
132
|
+
Koala.http_service = Koala.base_http_service
|
|
293
133
|
end
|
|
294
134
|
end
|
data/readme.md
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
Koala
|
|
2
2
|
====
|
|
3
|
-
Koala
|
|
3
|
+
[Koala](http://github.com/arsduo/koala) is a new Facebook library for Ruby, supporting the Graph API (including the batch requests and photo uploads), the REST API, realtime updates, test users, and OAuth validation. We wrote Koala with four goals:
|
|
4
4
|
|
|
5
|
-
* Lightweight: Koala should be as light and simple as Facebook’s own new libraries, providing API accessors and returning simple JSON. (We clock in, with comments, just over 750 lines of code.)
|
|
6
|
-
* Fast: Koala should, out of the box, be quick. In addition to supporting the vanilla Ruby networking libraries, it natively supports Typhoeus, our preferred gem for making fast HTTP requests. Of course,
|
|
5
|
+
* Lightweight: Koala should be as light and simple as Facebook’s own new libraries, providing API accessors and returning simple JSON. (We clock in, with comments, at just over 750 lines of code.)
|
|
6
|
+
* Fast: Koala should, out of the box, be quick. In addition to supporting the vanilla Ruby networking libraries, it natively supports Typhoeus, our preferred gem for making fast HTTP requests. Of course, that brings us to our next topic:
|
|
7
7
|
* Flexible: Koala should be useful to everyone, regardless of their current configuration. (We have no dependencies beyond the JSON gem. Koala also has a built-in mechanism for using whichever HTTP library you prefer to make requests against the graph.)
|
|
8
8
|
* Tested: Koala should have complete test coverage, so you can rely on it. (Our complete test coverage can be run against either mocked responses or the live Facebook servers.)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Installation
|
|
11
11
|
---
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
Easy:
|
|
14
|
+
|
|
15
|
+
[sudo|rvm] gem install koala
|
|
16
|
+
# for 1.1rc add --pre
|
|
13
17
|
|
|
14
|
-
sudo gem install koala
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
Or in Bundler:
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
gem "koala" # add ', "~> 1.1rc"' for the release candidate
|
|
19
22
|
|
|
20
23
|
Graph API
|
|
21
24
|
----
|
|
@@ -44,9 +47,18 @@ When retrieving data that returns an array of results (for example, when calling
|
|
|
44
47
|
# This is useful for paging across multiple requests
|
|
45
48
|
next_path, next_args = feed.next_page_params
|
|
46
49
|
|
|
47
|
-
# You can use those params to easily get the next (or
|
|
50
|
+
# You can use those params to easily get the next (or previous) page
|
|
48
51
|
page = graph.get_page(feed.next_page_params)
|
|
49
52
|
|
|
53
|
+
You can make multiple calls at once using Facebook's batch API:
|
|
54
|
+
|
|
55
|
+
# Returns an array of results as if they were called non-batch
|
|
56
|
+
graph.batch do
|
|
57
|
+
graph.get_connections('me', 'friends')
|
|
58
|
+
graph.get_object('me')
|
|
59
|
+
graph.get_picture('me')
|
|
60
|
+
end
|
|
61
|
+
|
|
50
62
|
Check out the wiki for more examples.
|
|
51
63
|
|
|
52
64
|
The old-school REST API
|
|
@@ -132,12 +144,12 @@ Testing
|
|
|
132
144
|
Unit tests are provided for all of Koala's methods. By default, these tests run against mock responses and hence are ready out of the box:
|
|
133
145
|
|
|
134
146
|
# From anywhere in the project directory:
|
|
135
|
-
rake spec
|
|
147
|
+
bundle exec rake spec
|
|
136
148
|
|
|
137
149
|
|
|
138
150
|
You can also run live tests against Facebook's servers:
|
|
139
151
|
|
|
140
152
|
# Again from anywhere in the project directory:
|
|
141
|
-
LIVE=true rake spec
|
|
153
|
+
LIVE=true bundle exec rake spec
|
|
142
154
|
|
|
143
|
-
Important Note: to run the live tests, you have to provide some of your own data in spec/fixtures/facebook_data.yml: a valid OAuth access token with publish\_stream, read\_stream, and user\_photos permissions and an OAuth code that can be used to generate an access token. You can get
|
|
155
|
+
Important Note: to run the live tests, you have to provide some of your own data in spec/fixtures/facebook_data.yml: a valid OAuth access token with publish\_stream, read\_stream, and user\_photos permissions and an OAuth code that can be used to generate an access token. You can get this data at the OAuth Playground; if you want to use your own app, remember to swap out the app ID, secret, and other values. (The file also provides valid values for other tests, which you're welcome to swap out for data specific to your own application.)
|
data/spec/cases/api_base_spec.rb
CHANGED
|
@@ -52,21 +52,21 @@ describe "Koala::Facebook::API" do
|
|
|
52
52
|
Koala.stub(:make_request).and_return(response)
|
|
53
53
|
|
|
54
54
|
json_body = mock('JSON body')
|
|
55
|
-
|
|
55
|
+
MultiJson.stub(:decode).and_return([json_body])
|
|
56
56
|
|
|
57
57
|
@service.api('anything').should == json_body
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
it "should execute
|
|
60
|
+
it "should execute an error checking block if provided" do
|
|
61
61
|
body = '{}'
|
|
62
62
|
Koala.stub(:make_request).and_return(Koala::Response.new(200, body, {}))
|
|
63
63
|
|
|
64
64
|
yield_test = mock('Yield Tester')
|
|
65
65
|
yield_test.should_receive(:pass)
|
|
66
66
|
|
|
67
|
-
@service.api('anything') do |arg|
|
|
67
|
+
@service.api('anything', {}, "get") do |arg|
|
|
68
68
|
yield_test.pass
|
|
69
|
-
arg.should ==
|
|
69
|
+
arg.should == MultiJson.decode(body)
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
|