koala 0.10.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -0
- data/CHANGELOG +37 -7
- data/Gemfile +3 -0
- data/LICENSE +1 -1
- data/Manifest +8 -1
- data/Rakefile +13 -14
- data/koala.gemspec +36 -19
- data/lib/koala/graph_api.rb +188 -123
- data/lib/koala/http_services.rb +93 -18
- data/lib/koala/rest_api.rb +73 -6
- data/lib/koala/test_users.rb +21 -8
- data/lib/koala/uploadable_io.rb +115 -0
- data/lib/koala.rb +104 -120
- data/readme.md +29 -16
- 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_spec.rb +25 -0
- data/spec/cases/http_services/http_service_spec.rb +54 -0
- data/spec/cases/http_services/net_http_service_spec.rb +350 -0
- data/spec/cases/http_services/typhoeus_service_spec.rb +144 -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} +78 -72
- data/spec/cases/uploadable_io_spec.rb +151 -0
- data/spec/fixtures/beach.jpg +0 -0
- data/spec/{facebook_data.yml → fixtures/facebook_data.yml} +13 -9
- data/spec/{mock_facebook_responses.yml → fixtures/mock_facebook_responses.yml} +314 -289
- data/spec/spec_helper.rb +18 -0
- data/spec/support/graph_api_shared_examples.rb +424 -0
- data/spec/{koala → support}/live_testing_data_helper.rb +39 -42
- data/spec/{mock_http_service.rb → support/mock_http_service.rb} +94 -81
- data/spec/support/rest_api_shared_examples.rb +161 -0
- data/spec/support/setup_mocks_or_live.rb +52 -0
- data/spec/support/uploadable_io_shared_examples.rb +76 -0
- metadata +127 -43
- 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 -10
- data/spec/koala/graph_and_rest_api/graph_and_rest_api_with_token_tests.rb +0 -11
- data/spec/koala/graph_api/graph_api_no_access_token_tests.rb +0 -114
- data/spec/koala/graph_api/graph_api_with_access_token_tests.rb +0 -150
- data/spec/koala/graph_api/graph_collection_tests.rb +0 -104
- data/spec/koala/net_http_service_tests.rb +0 -186
- data/spec/koala/oauth/oauth_tests.rb +0 -438
- data/spec/koala/realtime_updates/realtime_updates_tests.rb +0 -187
- data/spec/koala/rest_api/rest_api_no_access_token_tests.rb +0 -94
- data/spec/koala/rest_api/rest_api_with_access_token_tests.rb +0 -36
- data/spec/koala_spec.rb +0 -18
- data/spec/koala_spec_helper.rb +0 -48
- data/spec/koala_spec_without_mocks.rb +0 -19
data/lib/koala.rb
CHANGED
|
@@ -1,82 +1,60 @@
|
|
|
1
1
|
require 'cgi'
|
|
2
2
|
require 'digest/md5'
|
|
3
3
|
|
|
4
|
-
# rubygems is required to support json, how facebook returns data
|
|
5
|
-
require 'rubygems'
|
|
6
4
|
require 'json'
|
|
7
5
|
|
|
8
|
-
#
|
|
6
|
+
# OpenSSL and Base64 are required to support signed_request
|
|
9
7
|
require 'openssl'
|
|
8
|
+
require 'base64'
|
|
10
9
|
|
|
11
|
-
# include
|
|
10
|
+
# include koala modules
|
|
12
11
|
require 'koala/http_services'
|
|
13
|
-
|
|
14
|
-
# add Graph API methods
|
|
15
12
|
require 'koala/graph_api'
|
|
16
|
-
|
|
17
|
-
# add REST API methods
|
|
18
13
|
require 'koala/rest_api'
|
|
19
|
-
|
|
20
|
-
# add realtime update methods
|
|
21
14
|
require 'koala/realtime_updates'
|
|
22
|
-
|
|
23
|
-
# add test user methods
|
|
24
15
|
require 'koala/test_users'
|
|
25
16
|
|
|
17
|
+
# add KoalaIO class
|
|
18
|
+
require 'koala/uploadable_io'
|
|
19
|
+
|
|
26
20
|
module Koala
|
|
27
|
-
|
|
21
|
+
|
|
28
22
|
module Facebook
|
|
29
23
|
# Ruby client library for the Facebook Platform.
|
|
30
|
-
# Copyright 2010
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
34
|
-
# not use this file except in compliance with the License. You may obtain
|
|
35
|
-
# a copy of the License at
|
|
36
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
37
|
-
#
|
|
38
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
39
|
-
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
40
|
-
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
41
|
-
# License for the specific language governing permissions and limitations
|
|
42
|
-
# under the License.
|
|
43
|
-
#
|
|
44
|
-
# This client library is designed to support the Graph API and the official
|
|
45
|
-
# Facebook JavaScript SDK, which is the canonical way to implement
|
|
46
|
-
# Facebook authentication. Read more about the Graph API at
|
|
47
|
-
# http://developers.facebook.com/docs/api. You can download the Facebook
|
|
48
|
-
# JavaScript SDK at http://github.com/facebook/connect-js/.
|
|
24
|
+
# Copyright 2010-2011 Alex Koppel
|
|
25
|
+
# Contributors: Alex Koppel, Chris Baclig, Rafi Jacoby, and the team at Context Optional
|
|
26
|
+
# http://github.com/arsduo/koala
|
|
49
27
|
|
|
50
28
|
class API
|
|
51
|
-
# initialize with an access token
|
|
29
|
+
# initialize with an access token
|
|
52
30
|
def initialize(access_token = nil)
|
|
53
31
|
@access_token = access_token
|
|
54
32
|
end
|
|
55
33
|
attr_reader :access_token
|
|
56
|
-
|
|
34
|
+
|
|
57
35
|
def api(path, args = {}, verb = "get", options = {}, &error_checking_block)
|
|
58
36
|
# Fetches the given path in the Graph API.
|
|
59
37
|
args["access_token"] = @access_token || @app_access_token if @access_token || @app_access_token
|
|
60
|
-
|
|
38
|
+
|
|
61
39
|
# add a leading /
|
|
62
40
|
path = "/#{path}" unless path =~ /^\//
|
|
63
41
|
|
|
64
42
|
# make the request via the provided service
|
|
65
43
|
result = Koala.make_request(path, args, verb, options)
|
|
66
|
-
|
|
44
|
+
|
|
67
45
|
# Check for any 500 errors before parsing the body
|
|
68
46
|
# since we're not guaranteed that the body is valid JSON
|
|
69
47
|
# in the case of a server error
|
|
70
48
|
raise APIError.new({"type" => "HTTP #{result.status.to_s}", "message" => "Response body: #{result.body}"}) if result.status >= 500
|
|
71
|
-
|
|
72
|
-
# Parse the body as JSON and check for errors if provided a mechanism to do so
|
|
49
|
+
|
|
50
|
+
# Parse the body as JSON and check for errors if provided a mechanism to do so
|
|
73
51
|
# Note: Facebook sometimes sends results like "true" and "false", which aren't strictly objects
|
|
74
52
|
# and cause JSON.parse to fail -- so we account for that by wrapping the result in []
|
|
75
53
|
body = response = JSON.parse("[#{result.body.to_s}]")[0]
|
|
76
54
|
if error_checking_block
|
|
77
55
|
yield(body)
|
|
78
56
|
end
|
|
79
|
-
|
|
57
|
+
|
|
80
58
|
# now return the desired information
|
|
81
59
|
if options[:http_component]
|
|
82
60
|
result.send(options[:http_component])
|
|
@@ -85,57 +63,57 @@ module Koala
|
|
|
85
63
|
end
|
|
86
64
|
end
|
|
87
65
|
end
|
|
88
|
-
|
|
66
|
+
|
|
89
67
|
class GraphAPI < API
|
|
90
68
|
include GraphAPIMethods
|
|
91
69
|
end
|
|
92
|
-
|
|
70
|
+
|
|
93
71
|
class RestAPI < API
|
|
94
72
|
include RestAPIMethods
|
|
95
73
|
end
|
|
96
|
-
|
|
74
|
+
|
|
97
75
|
class GraphAndRestAPI < API
|
|
98
76
|
include GraphAPIMethods
|
|
99
77
|
include RestAPIMethods
|
|
100
78
|
end
|
|
101
|
-
|
|
79
|
+
|
|
102
80
|
class RealtimeUpdates < API
|
|
103
81
|
include RealtimeUpdateMethods
|
|
104
82
|
end
|
|
105
|
-
|
|
83
|
+
|
|
106
84
|
class TestUsers < API
|
|
107
85
|
include TestUserMethods
|
|
108
86
|
# make the Graph API accessible in case someone wants to make other calls to interact with their users
|
|
109
87
|
attr_reader :graph_api
|
|
110
88
|
end
|
|
111
|
-
|
|
112
|
-
class APIError <
|
|
89
|
+
|
|
90
|
+
class APIError < StandardError
|
|
113
91
|
attr_accessor :fb_error_type
|
|
114
92
|
def initialize(details = {})
|
|
115
|
-
self.fb_error_type = details["type"]
|
|
93
|
+
self.fb_error_type = details["type"]
|
|
116
94
|
super("#{fb_error_type}: #{details["message"]}")
|
|
117
95
|
end
|
|
118
96
|
end
|
|
119
|
-
|
|
120
|
-
|
|
97
|
+
|
|
98
|
+
|
|
121
99
|
class OAuth
|
|
122
100
|
attr_reader :app_id, :app_secret, :oauth_callback_url
|
|
123
101
|
def initialize(app_id, app_secret, oauth_callback_url = nil)
|
|
124
102
|
@app_id = app_id
|
|
125
103
|
@app_secret = app_secret
|
|
126
|
-
@oauth_callback_url = oauth_callback_url
|
|
104
|
+
@oauth_callback_url = oauth_callback_url
|
|
127
105
|
end
|
|
128
106
|
|
|
129
107
|
def get_user_info_from_cookie(cookie_hash)
|
|
130
108
|
# Parses the cookie set by the official Facebook JavaScript SDK.
|
|
131
|
-
#
|
|
109
|
+
#
|
|
132
110
|
# cookies should be a Hash, like the one Rails provides
|
|
133
|
-
#
|
|
111
|
+
#
|
|
134
112
|
# If the user is logged in via Facebook, we return a dictionary with the
|
|
135
113
|
# keys "uid" and "access_token". The former is the user's Facebook ID,
|
|
136
114
|
# and the latter can be used to make authenticated requests to the Graph API.
|
|
137
115
|
# If the user is not logged in, we return None.
|
|
138
|
-
#
|
|
116
|
+
#
|
|
139
117
|
# Download the official Facebook JavaScript SDK at
|
|
140
118
|
# http://github.com/facebook/connect-js/. Read more about Facebook
|
|
141
119
|
# authentication at http://developers.facebook.com/docs/authentication/.
|
|
@@ -150,150 +128,155 @@ module Koala
|
|
|
150
128
|
|
|
151
129
|
# generate the signature and make sure it matches what we expect
|
|
152
130
|
auth_string = components.keys.sort.collect {|a| a == "sig" ? nil : "#{a}=#{components[a]}"}.reject {|a| a.nil?}.join("")
|
|
153
|
-
sig = Digest::MD5.hexdigest(auth_string + @app_secret)
|
|
131
|
+
sig = Digest::MD5.hexdigest(auth_string + @app_secret)
|
|
154
132
|
sig == components["sig"] && (components["expires"] == "0" || Time.now.to_i < components["expires"].to_i) ? components : nil
|
|
155
133
|
end
|
|
156
134
|
end
|
|
157
135
|
alias_method :get_user_info_from_cookies, :get_user_info_from_cookie
|
|
158
|
-
|
|
136
|
+
|
|
159
137
|
def get_user_from_cookie(cookies)
|
|
160
138
|
if info = get_user_info_from_cookies(cookies)
|
|
161
139
|
string = info["uid"]
|
|
162
140
|
end
|
|
163
141
|
end
|
|
164
142
|
alias_method :get_user_from_cookies, :get_user_from_cookie
|
|
165
|
-
|
|
143
|
+
|
|
166
144
|
# URLs
|
|
167
|
-
|
|
145
|
+
|
|
168
146
|
def url_for_oauth_code(options = {})
|
|
169
147
|
# for permissions, see http://developers.facebook.com/docs/authentication/permissions
|
|
170
148
|
permissions = options[:permissions]
|
|
171
149
|
scope = permissions ? "&scope=#{permissions.is_a?(Array) ? permissions.join(",") : permissions}" : ""
|
|
172
|
-
|
|
150
|
+
display = options.has_key?(:display) ? "&display=#{options[:display]}" : ""
|
|
151
|
+
|
|
173
152
|
callback = options[:callback] || @oauth_callback_url
|
|
174
153
|
raise ArgumentError, "url_for_oauth_code must get a callback either from the OAuth object or in the options!" unless callback
|
|
175
154
|
|
|
176
155
|
# Creates the URL for oauth authorization for a given callback and optional set of permissions
|
|
177
|
-
"https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}"
|
|
156
|
+
"https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}#{display}"
|
|
178
157
|
end
|
|
179
|
-
|
|
158
|
+
|
|
180
159
|
def url_for_access_token(code, options = {})
|
|
181
160
|
# Creates the URL for the token corresponding to a given code generated by Facebook
|
|
182
161
|
callback = options[:callback] || @oauth_callback_url
|
|
183
162
|
raise ArgumentError, "url_for_access_token must get a callback either from the OAuth object or in the parameters!" unless callback
|
|
184
163
|
"https://#{GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@app_secret}&code=#{code}"
|
|
185
164
|
end
|
|
186
|
-
|
|
187
|
-
def get_access_token_info(code)
|
|
165
|
+
|
|
166
|
+
def get_access_token_info(code, options = {})
|
|
188
167
|
# convenience method to get a parsed token from Facebook for a given code
|
|
189
168
|
# should this require an OAuth callback URL?
|
|
190
|
-
get_token_from_server(:code => code, :redirect_uri => @oauth_callback_url)
|
|
169
|
+
get_token_from_server({:code => code, :redirect_uri => @oauth_callback_url}, false, options)
|
|
191
170
|
end
|
|
192
|
-
|
|
193
|
-
def get_access_token(code)
|
|
171
|
+
|
|
172
|
+
def get_access_token(code, options = {})
|
|
194
173
|
# upstream methods will throw errors if needed
|
|
195
|
-
if info = get_access_token_info(code)
|
|
196
|
-
string = info["access_token"]
|
|
174
|
+
if info = get_access_token_info(code, options)
|
|
175
|
+
string = info["access_token"]
|
|
197
176
|
end
|
|
198
177
|
end
|
|
199
|
-
|
|
200
|
-
def get_app_access_token_info
|
|
201
|
-
# convenience method to get a the application's sessionless access token
|
|
202
|
-
get_token_from_server({:type => 'client_cred'}, true)
|
|
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)
|
|
203
182
|
end
|
|
204
|
-
|
|
205
|
-
def get_app_access_token
|
|
206
|
-
if info = get_app_access_token_info
|
|
207
|
-
string = info["access_token"]
|
|
183
|
+
|
|
184
|
+
def get_app_access_token(options = {})
|
|
185
|
+
if info = get_app_access_token_info(options)
|
|
186
|
+
string = info["access_token"]
|
|
208
187
|
end
|
|
209
188
|
end
|
|
210
|
-
|
|
211
|
-
# signed_request
|
|
212
|
-
def parse_signed_request(request)
|
|
213
|
-
# Facebook's signed requests come in two parts -- the signature and the data payload
|
|
214
|
-
# see http://developers.facebook.com/docs/authentication/canvas
|
|
215
|
-
encoded_sig, payload = request.split(".")
|
|
216
|
-
|
|
217
|
-
sig = base64_url_decode(encoded_sig)
|
|
218
189
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
225
207
|
end
|
|
226
208
|
|
|
227
209
|
# from session keys
|
|
228
|
-
def get_token_info_from_session_keys(sessions)
|
|
210
|
+
def get_token_info_from_session_keys(sessions, options = {})
|
|
229
211
|
# fetch the OAuth tokens from Facebook
|
|
230
212
|
response = fetch_token_string({
|
|
231
213
|
:type => 'client_cred',
|
|
232
214
|
:sessions => sessions.join(",")
|
|
233
|
-
}, true, "exchange_sessions")
|
|
234
|
-
|
|
215
|
+
}, true, "exchange_sessions", options)
|
|
216
|
+
|
|
235
217
|
# Facebook returns an empty body in certain error conditions
|
|
236
|
-
if response == ""
|
|
218
|
+
if response == ""
|
|
237
219
|
raise APIError.new({
|
|
238
|
-
"type" => "ArgumentError",
|
|
220
|
+
"type" => "ArgumentError",
|
|
239
221
|
"message" => "get_token_from_session_key received an error (empty response body) for sessions #{sessions.inspect}!"
|
|
240
222
|
})
|
|
241
223
|
end
|
|
242
|
-
|
|
224
|
+
|
|
243
225
|
JSON.parse(response)
|
|
244
226
|
end
|
|
245
|
-
|
|
246
|
-
def get_tokens_from_session_keys(sessions)
|
|
227
|
+
|
|
228
|
+
def get_tokens_from_session_keys(sessions, options = {})
|
|
247
229
|
# get the original hash results
|
|
248
|
-
results = get_token_info_from_session_keys(sessions)
|
|
230
|
+
results = get_token_info_from_session_keys(sessions, options)
|
|
249
231
|
# now recollect them as just the access tokens
|
|
250
232
|
results.collect { |r| r ? r["access_token"] : nil }
|
|
251
233
|
end
|
|
252
|
-
|
|
253
|
-
def get_token_from_session_key(session)
|
|
234
|
+
|
|
235
|
+
def get_token_from_session_key(session, options = {})
|
|
254
236
|
# convenience method for a single key
|
|
255
237
|
# gets the overlaoded strings automatically
|
|
256
|
-
get_tokens_from_session_keys([session])[0]
|
|
238
|
+
get_tokens_from_session_keys([session], options)[0]
|
|
257
239
|
end
|
|
258
|
-
|
|
240
|
+
|
|
259
241
|
protected
|
|
260
|
-
|
|
261
|
-
def get_token_from_server(args, post = false)
|
|
242
|
+
|
|
243
|
+
def get_token_from_server(args, post = false, options = {})
|
|
262
244
|
# fetch the result from Facebook's servers
|
|
263
|
-
result = fetch_token_string(args, post)
|
|
264
|
-
|
|
245
|
+
result = fetch_token_string(args, post, "access_token", options)
|
|
246
|
+
|
|
265
247
|
# if we have an error, parse the error JSON and raise an error
|
|
266
248
|
raise APIError.new((JSON.parse(result)["error"] rescue nil) || {}) if result =~ /error/
|
|
267
249
|
|
|
268
250
|
# otherwise, parse the access token
|
|
269
|
-
parse_access_token(result)
|
|
251
|
+
parse_access_token(result)
|
|
270
252
|
end
|
|
271
|
-
|
|
253
|
+
|
|
272
254
|
def parse_access_token(response_text)
|
|
273
255
|
components = response_text.split("&").inject({}) do |hash, bit|
|
|
274
256
|
key, value = bit.split("=")
|
|
275
257
|
hash.merge!(key => value)
|
|
276
258
|
end
|
|
277
|
-
components
|
|
259
|
+
components
|
|
278
260
|
end
|
|
279
261
|
|
|
280
|
-
def fetch_token_string(args, post = false, endpoint = "access_token")
|
|
262
|
+
def fetch_token_string(args, post = false, endpoint = "access_token", options = {})
|
|
281
263
|
Koala.make_request("/oauth/#{endpoint}", {
|
|
282
|
-
:client_id => @app_id,
|
|
264
|
+
:client_id => @app_id,
|
|
283
265
|
:client_secret => @app_secret
|
|
284
|
-
}.merge!(args), post ? "post" : "get").body
|
|
266
|
+
}.merge!(args), post ? "post" : "get", {:use_ssl => true}.merge!(options)).body
|
|
285
267
|
end
|
|
286
|
-
|
|
268
|
+
|
|
287
269
|
# base 64
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
"#{string}==".tr("-_", "+/").unpack("m")[0]
|
|
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('-_', '+/'))
|
|
293
274
|
end
|
|
294
275
|
end
|
|
295
276
|
end
|
|
296
|
-
|
|
277
|
+
|
|
278
|
+
class KoalaError< StandardError; end
|
|
279
|
+
|
|
297
280
|
# finally, set up the http service Koala methods used to make requests
|
|
298
281
|
# you can use your own (for HTTParty, etc.) by calling Koala.http_service = YourModule
|
|
299
282
|
def self.http_service=(service)
|
|
@@ -301,8 +284,9 @@ module Koala
|
|
|
301
284
|
end
|
|
302
285
|
|
|
303
286
|
# by default, try requiring Typhoeus -- if that works, use it
|
|
287
|
+
# if you have Typheous and don't want to use it (or want another service),
|
|
288
|
+
# you can run Koala.http_service = NetHTTPService (or MyHTTPService)
|
|
304
289
|
begin
|
|
305
|
-
require 'typhoeus'
|
|
306
290
|
Koala.http_service = TyphoeusService
|
|
307
291
|
rescue LoadError
|
|
308
292
|
Koala.http_service = NetHTTPService
|
data/readme.md
CHANGED
|
@@ -1,28 +1,38 @@
|
|
|
1
1
|
Koala
|
|
2
2
|
====
|
|
3
|
-
Koala (<a href="http://github.com/arsduo/koala" target="_blank">http://github.com/arsduo/koala</a>) is a new Facebook library for Ruby, supporting the Graph API, the old REST API, realtime updates, and OAuth validation. We wrote Koala with four goals:
|
|
3
|
+
Koala (<a href="http://github.com/arsduo/koala" target="_blank">http://github.com/arsduo/koala</a>) is a new Facebook library for Ruby, supporting the Graph API (including photo uploads), the old REST API, realtime updates, 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
|
|
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
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
|
+
1.0
|
|
11
|
+
---
|
|
12
|
+
Version 1.0 is due out on May 1st, 2011 with a ton of great features.
|
|
13
|
+
|
|
14
|
+
sudo gem install koala
|
|
15
|
+
|
|
16
|
+
Until then, you can install the release candidate like so:
|
|
17
|
+
|
|
18
|
+
sudo gem install koala --pre
|
|
19
|
+
|
|
10
20
|
Graph API
|
|
11
21
|
----
|
|
12
|
-
The Graph API is the simple, slick new interface to Facebook's data. Using it with Koala is quite straightforward:
|
|
22
|
+
The Graph API is the simple, slick new interface to Facebook's data. Using it with Koala is quite straightforward:
|
|
13
23
|
|
|
14
24
|
graph = Koala::Facebook::GraphAPI.new(oauth_access_token)
|
|
15
25
|
profile = graph.get_object("me")
|
|
16
26
|
friends = graph.get_connections("me", "friends")
|
|
17
27
|
graph.put_object("me", "feed", :message => "I am writing on my wall!")
|
|
18
28
|
|
|
19
|
-
The response of most requests is the JSON data returned from the Facebook servers as a Hash.
|
|
29
|
+
The response of most requests is the JSON data returned from the Facebook servers as a Hash.
|
|
20
30
|
|
|
21
31
|
When retrieving data that returns an array of results (for example, when calling GraphAPI#get_connections or GraphAPI#search) a GraphCollection object (a sub-class of Array) will be returned, which contains added methods for getting the next and previous page of results:
|
|
22
|
-
|
|
32
|
+
|
|
23
33
|
# Returns the feed items for the currently logged-in user as a GraphCollection
|
|
24
34
|
feed = graph.get_connections("me", "feed")
|
|
25
|
-
|
|
35
|
+
|
|
26
36
|
# GraphCollection is a sub-class of Array, so you can use it as a usual Array
|
|
27
37
|
first_entry = feed[0]
|
|
28
38
|
last_entry = feed.last
|
|
@@ -33,7 +43,7 @@ When retrieving data that returns an array of results (for example, when calling
|
|
|
33
43
|
# Returns an array describing the URL for the next page: [path, arguments]
|
|
34
44
|
# This is useful for paging across multiple requests
|
|
35
45
|
next_path, next_args = feed.next_page_params
|
|
36
|
-
|
|
46
|
+
|
|
37
47
|
# You can use those params to easily get the next (or prevous) page
|
|
38
48
|
page = graph.get_page(feed.next_page_params)
|
|
39
49
|
|
|
@@ -41,15 +51,15 @@ Check out the wiki for more examples.
|
|
|
41
51
|
|
|
42
52
|
The old-school REST API
|
|
43
53
|
-----
|
|
44
|
-
Where the Graph API and the old REST API overlap, you should choose the Graph API. Unfortunately, that overlap is far from complete, and there are many important API calls that can't yet be done via the Graph.
|
|
54
|
+
Where the Graph API and the old REST API overlap, you should choose the Graph API. Unfortunately, that overlap is far from complete, and there are many important API calls that can't yet be done via the Graph.
|
|
45
55
|
|
|
46
56
|
Koala now supports the old-school REST API using OAuth access tokens; to use this, instantiate your class using the RestAPI class:
|
|
47
57
|
|
|
48
58
|
@rest = Koala::Facebook::RestAPI.new(oauth_access_token)
|
|
49
59
|
@rest.fql_query(my_fql_query) # convenience method
|
|
50
60
|
@rest.rest_call("stream.publish", arguments_hash) # generic version
|
|
51
|
-
|
|
52
|
-
We reserve the right to expand the built-in REST API coverage to additional convenience methods in the future, depending on how fast Facebook moves to fill in the gaps.
|
|
61
|
+
|
|
62
|
+
We reserve the right to expand the built-in REST API coverage to additional convenience methods in the future, depending on how fast Facebook moves to fill in the gaps.
|
|
53
63
|
|
|
54
64
|
(If you want the power of both APIs in the palm of your hand, try out the GraphAndRestAPI class.)
|
|
55
65
|
|
|
@@ -119,12 +129,15 @@ Some resources to help you as you play with Koala and the Graph API:
|
|
|
119
129
|
Testing
|
|
120
130
|
-----
|
|
121
131
|
|
|
122
|
-
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:
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
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
|
+
|
|
134
|
+
# From anywhere in the project directory:
|
|
135
|
+
rake spec
|
|
136
|
+
|
|
125
137
|
|
|
126
138
|
You can also run live tests against Facebook's servers:
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
|
|
140
|
+
# Again from anywhere in the project directory:
|
|
141
|
+
LIVE=true rake spec
|
|
129
142
|
|
|
130
|
-
Important Note: to run the live tests, you have to provide some of your own data: a valid OAuth access token with publish\_stream
|
|
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 thisdata 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.)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe "Koala::Facebook::API" do
|
|
4
|
+
before(:each) do
|
|
5
|
+
@service = Koala::Facebook::API.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it "should not include an access token if none was given" do
|
|
9
|
+
Koala.should_receive(:make_request).with(
|
|
10
|
+
anything,
|
|
11
|
+
hash_not_including('access_token' => 1),
|
|
12
|
+
anything,
|
|
13
|
+
anything
|
|
14
|
+
).and_return(Koala::Response.new(200, "", ""))
|
|
15
|
+
|
|
16
|
+
@service.api('anything')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "should include an access token if given" do
|
|
20
|
+
token = 'adfadf'
|
|
21
|
+
service = Koala::Facebook::API.new token
|
|
22
|
+
|
|
23
|
+
Koala.should_receive(:make_request).with(
|
|
24
|
+
anything,
|
|
25
|
+
hash_including('access_token' => token),
|
|
26
|
+
anything,
|
|
27
|
+
anything
|
|
28
|
+
).and_return(Koala::Response.new(200, "", ""))
|
|
29
|
+
|
|
30
|
+
service.api('anything')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "should have an attr_reader for access token" do
|
|
34
|
+
token = 'adfadf'
|
|
35
|
+
service = Koala::Facebook::API.new token
|
|
36
|
+
service.access_token.should == token
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "should get the attribute of a Koala::Response given by the http_component parameter" do
|
|
40
|
+
http_component = :method_name
|
|
41
|
+
|
|
42
|
+
response = mock('Mock KoalaResponse', :body => '', :status => 200)
|
|
43
|
+
response.should_receive(http_component).and_return('')
|
|
44
|
+
|
|
45
|
+
Koala.stub(:make_request).and_return(response)
|
|
46
|
+
|
|
47
|
+
@service.api('anything', 'get', {}, :http_component => http_component)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "should return the body of the request as JSON if no http_component is given" do
|
|
51
|
+
response = stub('response', :body => 'body', :status => 200)
|
|
52
|
+
Koala.stub(:make_request).and_return(response)
|
|
53
|
+
|
|
54
|
+
json_body = mock('JSON body')
|
|
55
|
+
JSON.stub(:parse).and_return([json_body])
|
|
56
|
+
|
|
57
|
+
@service.api('anything').should == json_body
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "should execute a block with the response body if passed one" do
|
|
61
|
+
body = '{}'
|
|
62
|
+
Koala.stub(:make_request).and_return(Koala::Response.new(200, body, {}))
|
|
63
|
+
|
|
64
|
+
yield_test = mock('Yield Tester')
|
|
65
|
+
yield_test.should_receive(:pass)
|
|
66
|
+
|
|
67
|
+
@service.api('anything') do |arg|
|
|
68
|
+
yield_test.pass
|
|
69
|
+
arg.should == JSON.parse(body)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "should raise an API error if the HTTP response code is greater than or equal to 500" do
|
|
74
|
+
Koala.stub(:make_request).and_return(Koala::Response.new(500, 'response body', {}))
|
|
75
|
+
|
|
76
|
+
lambda { @service.api('anything') }.should raise_exception(Koala::Facebook::APIError)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "should handle rogue true/false as responses" do
|
|
80
|
+
Koala.should_receive(:make_request).and_return(Koala::Response.new(200, 'true', {}))
|
|
81
|
+
@service.api('anything').should be_true
|
|
82
|
+
|
|
83
|
+
Koala.should_receive(:make_request).and_return(Koala::Response.new(200, 'false', {}))
|
|
84
|
+
@service.api('anything').should be_false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe "with regard to leading slashes" do
|
|
88
|
+
it "should add a leading / to the path if not present" do
|
|
89
|
+
path = "anything"
|
|
90
|
+
Koala.should_receive(:make_request).with("/#{path}", anything, anything, anything).and_return(Koala::Response.new(200, 'true', {}))
|
|
91
|
+
@service.api(path)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "shouldn't change the path if a leading / is present" do
|
|
95
|
+
path = "/anything"
|
|
96
|
+
Koala.should_receive(:make_request).with(path, anything, anything, anything).and_return(Koala::Response.new(200, 'true', {}))
|
|
97
|
+
@service.api(path)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe "Koala::Facebook::GraphAndRestAPI" do
|
|
4
|
+
include LiveTestingDataHelper
|
|
5
|
+
|
|
6
|
+
describe "with an access token" do
|
|
7
|
+
before(:each) do
|
|
8
|
+
@api = Koala::Facebook::GraphAndRestAPI.new(@token)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it_should_behave_like "Koala RestAPI"
|
|
12
|
+
it_should_behave_like "Koala RestAPI with an access token"
|
|
13
|
+
|
|
14
|
+
it_should_behave_like "Koala GraphAPI"
|
|
15
|
+
it_should_behave_like "Koala GraphAPI with an access token"
|
|
16
|
+
it_should_behave_like "Koala GraphAPI with GraphCollection"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe "without an access token" do
|
|
20
|
+
before(:each) do
|
|
21
|
+
@api = Koala::Facebook::GraphAndRestAPI.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it_should_behave_like "Koala RestAPI"
|
|
25
|
+
it_should_behave_like "Koala RestAPI without an access token"
|
|
26
|
+
|
|
27
|
+
it_should_behave_like "Koala GraphAPI"
|
|
28
|
+
it_should_behave_like "Koala GraphAPI without an access token"
|
|
29
|
+
it_should_behave_like "Koala GraphAPI with GraphCollection"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe "Koala::Facebook::GraphAPI" do
|
|
4
|
+
include LiveTestingDataHelper
|
|
5
|
+
|
|
6
|
+
context "with an access token" do
|
|
7
|
+
before :each do
|
|
8
|
+
@api = Koala::Facebook::GraphAPI.new(@token)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it_should_behave_like "Koala GraphAPI"
|
|
12
|
+
it_should_behave_like "Koala GraphAPI with an access token"
|
|
13
|
+
it_should_behave_like "Koala GraphAPI with GraphCollection"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context "without an access token" do
|
|
17
|
+
before :each do
|
|
18
|
+
@api = Koala::Facebook::GraphAPI.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it_should_behave_like "Koala GraphAPI"
|
|
22
|
+
it_should_behave_like "Koala GraphAPI without an access token"
|
|
23
|
+
it_should_behave_like "Koala GraphAPI with GraphCollection"
|
|
24
|
+
end
|
|
25
|
+
end
|