koala 0.9.0 → 1.0.0.beta
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/CHANGELOG +33 -7
- data/Manifest +6 -14
- data/Rakefile +4 -3
- data/koala.gemspec +13 -8
- data/lib/koala/graph_api.rb +16 -3
- data/lib/koala/http_services.rb +100 -16
- data/lib/koala/rest_api.rb +73 -6
- data/lib/koala/test_users.rb +72 -0
- data/lib/koala.rb +101 -71
- data/readme.md +14 -13
- data/spec/facebook_data.yml +14 -8
- data/spec/koala/api_base_tests.rb +7 -0
- data/spec/koala/assets/beach.jpg +0 -0
- data/spec/koala/graph_and_rest_api/graph_and_rest_api_no_token_tests.rb +5 -1
- data/spec/koala/graph_and_rest_api/graph_and_rest_api_with_token_tests.rb +9 -4
- data/spec/koala/graph_api/graph_api_no_access_token_tests.rb +10 -61
- data/spec/koala/graph_api/graph_api_tests.rb +86 -0
- data/spec/koala/graph_api/graph_api_with_access_token_tests.rb +130 -126
- data/spec/koala/graph_api/graph_collection_tests.rb +2 -2
- data/spec/koala/live_testing_data_helper.rb +40 -12
- data/spec/koala/net_http_service_tests.rb +401 -152
- data/spec/koala/oauth/oauth_tests.rb +41 -72
- data/spec/koala/rest_api/rest_api_no_access_token_tests.rb +7 -76
- data/spec/koala/rest_api/rest_api_tests.rb +118 -0
- data/spec/koala/rest_api/rest_api_with_access_token_tests.rb +6 -4
- data/spec/koala/test_users/test_users_tests.rb +208 -0
- data/spec/koala/typhoeus_service_tests.rb +156 -0
- data/spec/koala_spec_helper.rb +43 -4
- data/spec/koala_spec_without_mocks.rb +4 -4
- data/spec/mock_facebook_responses.yml +69 -3
- data/spec/mock_http_service.rb +16 -1
- metadata +49 -23
- data/examples/oauth_playground/Capfile +0 -2
- data/examples/oauth_playground/LICENSE +0 -22
- data/examples/oauth_playground/Rakefile +0 -4
- data/examples/oauth_playground/config/deploy.rb +0 -39
- data/examples/oauth_playground/config/facebook.yml +0 -13
- data/examples/oauth_playground/config.ru +0 -27
- data/examples/oauth_playground/lib/load_facebook.rb +0 -3
- data/examples/oauth_playground/lib/oauth_playground.rb +0 -187
- data/examples/oauth_playground/readme.md +0 -8
- data/examples/oauth_playground/spec/oauth_playground_spec.rb +0 -35
- data/examples/oauth_playground/spec/spec_helper.rb +0 -36
- data/examples/oauth_playground/tmp/restart.txt +0 -0
- data/examples/oauth_playground/views/index.erb +0 -206
- data/examples/oauth_playground/views/layout.erb +0 -39
data/lib/koala.rb
CHANGED
|
@@ -5,8 +5,9 @@ require 'digest/md5'
|
|
|
5
5
|
require 'rubygems'
|
|
6
6
|
require 'json'
|
|
7
7
|
|
|
8
|
-
#
|
|
8
|
+
# OpenSSL and Base64 are required to support signed_request
|
|
9
9
|
require 'openssl'
|
|
10
|
+
require 'base64'
|
|
10
11
|
|
|
11
12
|
# include default http services
|
|
12
13
|
require 'koala/http_services'
|
|
@@ -17,10 +18,14 @@ require 'koala/graph_api'
|
|
|
17
18
|
# add REST API methods
|
|
18
19
|
require 'koala/rest_api'
|
|
19
20
|
|
|
21
|
+
# add realtime update methods
|
|
20
22
|
require 'koala/realtime_updates'
|
|
21
23
|
|
|
24
|
+
# add test user methods
|
|
25
|
+
require 'koala/test_users'
|
|
26
|
+
|
|
22
27
|
module Koala
|
|
23
|
-
|
|
28
|
+
|
|
24
29
|
module Facebook
|
|
25
30
|
# Ruby client library for the Facebook Platform.
|
|
26
31
|
# Copyright 2010 Facebook
|
|
@@ -36,7 +41,7 @@ module Koala
|
|
|
36
41
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
37
42
|
# License for the specific language governing permissions and limitations
|
|
38
43
|
# under the License.
|
|
39
|
-
#
|
|
44
|
+
#
|
|
40
45
|
# This client library is designed to support the Graph API and the official
|
|
41
46
|
# Facebook JavaScript SDK, which is the canonical way to implement
|
|
42
47
|
# Facebook authentication. Read more about the Graph API at
|
|
@@ -48,7 +53,8 @@ module Koala
|
|
|
48
53
|
def initialize(access_token = nil)
|
|
49
54
|
@access_token = access_token
|
|
50
55
|
end
|
|
51
|
-
|
|
56
|
+
attr_reader :access_token
|
|
57
|
+
|
|
52
58
|
def api(path, args = {}, verb = "get", options = {}, &error_checking_block)
|
|
53
59
|
# Fetches the given path in the Graph API.
|
|
54
60
|
args["access_token"] = @access_token || @app_access_token if @access_token || @app_access_token
|
|
@@ -58,20 +64,20 @@ module Koala
|
|
|
58
64
|
|
|
59
65
|
# make the request via the provided service
|
|
60
66
|
result = Koala.make_request(path, args, verb, options)
|
|
61
|
-
|
|
67
|
+
|
|
62
68
|
# Check for any 500 errors before parsing the body
|
|
63
69
|
# since we're not guaranteed that the body is valid JSON
|
|
64
70
|
# in the case of a server error
|
|
65
71
|
raise APIError.new({"type" => "HTTP #{result.status.to_s}", "message" => "Response body: #{result.body}"}) if result.status >= 500
|
|
66
|
-
|
|
67
|
-
# Parse the body as JSON and check for errors if provided a mechanism to do so
|
|
72
|
+
|
|
73
|
+
# Parse the body as JSON and check for errors if provided a mechanism to do so
|
|
68
74
|
# Note: Facebook sometimes sends results like "true" and "false", which aren't strictly objects
|
|
69
75
|
# and cause JSON.parse to fail -- so we account for that by wrapping the result in []
|
|
70
76
|
body = response = JSON.parse("[#{result.body.to_s}]")[0]
|
|
71
77
|
if error_checking_block
|
|
72
78
|
yield(body)
|
|
73
79
|
end
|
|
74
|
-
|
|
80
|
+
|
|
75
81
|
# now return the desired information
|
|
76
82
|
if options[:http_component]
|
|
77
83
|
result.send(options[:http_component])
|
|
@@ -80,51 +86,57 @@ module Koala
|
|
|
80
86
|
end
|
|
81
87
|
end
|
|
82
88
|
end
|
|
83
|
-
|
|
89
|
+
|
|
84
90
|
class GraphAPI < API
|
|
85
91
|
include GraphAPIMethods
|
|
86
92
|
end
|
|
87
|
-
|
|
93
|
+
|
|
88
94
|
class RestAPI < API
|
|
89
95
|
include RestAPIMethods
|
|
90
96
|
end
|
|
91
|
-
|
|
97
|
+
|
|
92
98
|
class GraphAndRestAPI < API
|
|
93
99
|
include GraphAPIMethods
|
|
94
100
|
include RestAPIMethods
|
|
95
101
|
end
|
|
96
|
-
|
|
102
|
+
|
|
97
103
|
class RealtimeUpdates < API
|
|
98
104
|
include RealtimeUpdateMethods
|
|
99
105
|
end
|
|
100
|
-
|
|
106
|
+
|
|
107
|
+
class TestUsers < API
|
|
108
|
+
include TestUserMethods
|
|
109
|
+
# make the Graph API accessible in case someone wants to make other calls to interact with their users
|
|
110
|
+
attr_reader :graph_api
|
|
111
|
+
end
|
|
112
|
+
|
|
101
113
|
class APIError < Exception
|
|
102
114
|
attr_accessor :fb_error_type
|
|
103
115
|
def initialize(details = {})
|
|
104
|
-
self.fb_error_type = details["type"]
|
|
116
|
+
self.fb_error_type = details["type"]
|
|
105
117
|
super("#{fb_error_type}: #{details["message"]}")
|
|
106
118
|
end
|
|
107
119
|
end
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
|
|
121
|
+
|
|
110
122
|
class OAuth
|
|
111
123
|
attr_reader :app_id, :app_secret, :oauth_callback_url
|
|
112
124
|
def initialize(app_id, app_secret, oauth_callback_url = nil)
|
|
113
125
|
@app_id = app_id
|
|
114
126
|
@app_secret = app_secret
|
|
115
|
-
@oauth_callback_url = oauth_callback_url
|
|
127
|
+
@oauth_callback_url = oauth_callback_url
|
|
116
128
|
end
|
|
117
129
|
|
|
118
130
|
def get_user_info_from_cookie(cookie_hash)
|
|
119
131
|
# Parses the cookie set by the official Facebook JavaScript SDK.
|
|
120
|
-
#
|
|
132
|
+
#
|
|
121
133
|
# cookies should be a Hash, like the one Rails provides
|
|
122
|
-
#
|
|
134
|
+
#
|
|
123
135
|
# If the user is logged in via Facebook, we return a dictionary with the
|
|
124
136
|
# keys "uid" and "access_token". The former is the user's Facebook ID,
|
|
125
137
|
# and the latter can be used to make authenticated requests to the Graph API.
|
|
126
138
|
# If the user is not logged in, we return None.
|
|
127
|
-
#
|
|
139
|
+
#
|
|
128
140
|
# Download the official Facebook JavaScript SDK at
|
|
129
141
|
# http://github.com/facebook/connect-js/. Read more about Facebook
|
|
130
142
|
# authentication at http://developers.facebook.com/docs/authentication/.
|
|
@@ -139,21 +151,21 @@ module Koala
|
|
|
139
151
|
|
|
140
152
|
# generate the signature and make sure it matches what we expect
|
|
141
153
|
auth_string = components.keys.sort.collect {|a| a == "sig" ? nil : "#{a}=#{components[a]}"}.reject {|a| a.nil?}.join("")
|
|
142
|
-
sig = Digest::MD5.hexdigest(auth_string + @app_secret)
|
|
154
|
+
sig = Digest::MD5.hexdigest(auth_string + @app_secret)
|
|
143
155
|
sig == components["sig"] && (components["expires"] == "0" || Time.now.to_i < components["expires"].to_i) ? components : nil
|
|
144
156
|
end
|
|
145
157
|
end
|
|
146
158
|
alias_method :get_user_info_from_cookies, :get_user_info_from_cookie
|
|
147
|
-
|
|
159
|
+
|
|
148
160
|
def get_user_from_cookie(cookies)
|
|
149
161
|
if info = get_user_info_from_cookies(cookies)
|
|
150
162
|
string = info["uid"]
|
|
151
163
|
end
|
|
152
164
|
end
|
|
153
165
|
alias_method :get_user_from_cookies, :get_user_from_cookie
|
|
154
|
-
|
|
166
|
+
|
|
155
167
|
# URLs
|
|
156
|
-
|
|
168
|
+
|
|
157
169
|
def url_for_oauth_code(options = {})
|
|
158
170
|
# for permissions, see http://developers.facebook.com/docs/authentication/permissions
|
|
159
171
|
permissions = options[:permissions]
|
|
@@ -163,54 +175,71 @@ module Koala
|
|
|
163
175
|
raise ArgumentError, "url_for_oauth_code must get a callback either from the OAuth object or in the options!" unless callback
|
|
164
176
|
|
|
165
177
|
# Creates the URL for oauth authorization for a given callback and optional set of permissions
|
|
166
|
-
"https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}"
|
|
178
|
+
"https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}"
|
|
167
179
|
end
|
|
168
|
-
|
|
180
|
+
|
|
169
181
|
def url_for_access_token(code, options = {})
|
|
170
182
|
# Creates the URL for the token corresponding to a given code generated by Facebook
|
|
171
183
|
callback = options[:callback] || @oauth_callback_url
|
|
172
184
|
raise ArgumentError, "url_for_access_token must get a callback either from the OAuth object or in the parameters!" unless callback
|
|
173
185
|
"https://#{GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@app_secret}&code=#{code}"
|
|
174
186
|
end
|
|
175
|
-
|
|
187
|
+
|
|
176
188
|
def get_access_token_info(code)
|
|
177
189
|
# convenience method to get a parsed token from Facebook for a given code
|
|
178
190
|
# should this require an OAuth callback URL?
|
|
179
191
|
get_token_from_server(:code => code, :redirect_uri => @oauth_callback_url)
|
|
180
192
|
end
|
|
181
|
-
|
|
193
|
+
|
|
182
194
|
def get_access_token(code)
|
|
183
195
|
# upstream methods will throw errors if needed
|
|
184
|
-
if info = get_access_token_info(code)
|
|
185
|
-
string = info["access_token"]
|
|
196
|
+
if info = get_access_token_info(code)
|
|
197
|
+
string = info["access_token"]
|
|
186
198
|
end
|
|
187
199
|
end
|
|
188
|
-
|
|
200
|
+
|
|
189
201
|
def get_app_access_token_info
|
|
190
|
-
# convenience method to get a the application's sessionless access token
|
|
202
|
+
# convenience method to get a the application's sessionless access token
|
|
191
203
|
get_token_from_server({:type => 'client_cred'}, true)
|
|
192
204
|
end
|
|
193
|
-
|
|
205
|
+
|
|
194
206
|
def get_app_access_token
|
|
195
207
|
if info = get_app_access_token_info
|
|
196
|
-
string = info["access_token"]
|
|
208
|
+
string = info["access_token"]
|
|
197
209
|
end
|
|
198
210
|
end
|
|
199
|
-
|
|
200
|
-
#
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
encoded_sig,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
|
|
212
|
+
# provided directly by Facebook
|
|
213
|
+
# see https://github.com/facebook/crypto-request-examples/blob/master/sample.rb
|
|
214
|
+
# and http://developers.facebook.com/docs/authentication/canvas/encryption_proposal
|
|
215
|
+
def parse_signed_request(input, max_age = 3600)
|
|
216
|
+
encoded_sig, encoded_envelope = input.split('.', 2)
|
|
217
|
+
envelope = JSON.parse(base64_url_decode(encoded_envelope))
|
|
218
|
+
algorithm = envelope['algorithm']
|
|
219
|
+
|
|
220
|
+
raise 'Invalid request. (Unsupported algorithm.)' \
|
|
221
|
+
if algorithm != 'AES-256-CBC HMAC-SHA256' && algorithm != 'HMAC-SHA256'
|
|
222
|
+
|
|
223
|
+
raise 'Invalid request. (Too old.)' \
|
|
224
|
+
if algorithm == "AES-256-CBC HMAC-SHA256" && envelope['issued_at'].to_i < Time.now.to_i - max_age
|
|
225
|
+
|
|
226
|
+
raise 'Invalid request. (Invalid signature.)' \
|
|
227
|
+
if base64_url_decode(encoded_sig) !=
|
|
228
|
+
OpenSSL::HMAC.hexdigest(
|
|
229
|
+
'sha256', @app_secret, encoded_envelope).split.pack('H*')
|
|
230
|
+
|
|
231
|
+
# for requests that are signed, but not encrypted, we're done
|
|
232
|
+
return envelope if algorithm == 'HMAC-SHA256'
|
|
233
|
+
|
|
234
|
+
# otherwise, decrypt the payload
|
|
235
|
+
cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
|
|
236
|
+
cipher.decrypt
|
|
237
|
+
cipher.key = @app_secret
|
|
238
|
+
cipher.iv = base64_url_decode(envelope['iv'])
|
|
239
|
+
cipher.padding = 0
|
|
240
|
+
decrypted_data = cipher.update(base64_url_decode(envelope['payload']))
|
|
241
|
+
decrypted_data << cipher.final
|
|
242
|
+
return JSON.parse(decrypted_data.strip)
|
|
214
243
|
end
|
|
215
244
|
|
|
216
245
|
# from session keys
|
|
@@ -220,66 +249,68 @@ module Koala
|
|
|
220
249
|
:type => 'client_cred',
|
|
221
250
|
:sessions => sessions.join(",")
|
|
222
251
|
}, true, "exchange_sessions")
|
|
223
|
-
|
|
252
|
+
|
|
224
253
|
# Facebook returns an empty body in certain error conditions
|
|
225
|
-
if response == ""
|
|
226
|
-
raise APIError.new(
|
|
254
|
+
if response == ""
|
|
255
|
+
raise APIError.new({
|
|
256
|
+
"type" => "ArgumentError",
|
|
257
|
+
"message" => "get_token_from_session_key received an error (empty response body) for sessions #{sessions.inspect}!"
|
|
258
|
+
})
|
|
227
259
|
end
|
|
228
|
-
|
|
260
|
+
|
|
229
261
|
JSON.parse(response)
|
|
230
262
|
end
|
|
231
|
-
|
|
263
|
+
|
|
232
264
|
def get_tokens_from_session_keys(sessions)
|
|
233
265
|
# get the original hash results
|
|
234
266
|
results = get_token_info_from_session_keys(sessions)
|
|
235
267
|
# now recollect them as just the access tokens
|
|
236
268
|
results.collect { |r| r ? r["access_token"] : nil }
|
|
237
269
|
end
|
|
238
|
-
|
|
270
|
+
|
|
239
271
|
def get_token_from_session_key(session)
|
|
240
272
|
# convenience method for a single key
|
|
241
273
|
# gets the overlaoded strings automatically
|
|
242
274
|
get_tokens_from_session_keys([session])[0]
|
|
243
275
|
end
|
|
244
|
-
|
|
276
|
+
|
|
245
277
|
protected
|
|
246
|
-
|
|
278
|
+
|
|
247
279
|
def get_token_from_server(args, post = false)
|
|
248
280
|
# fetch the result from Facebook's servers
|
|
249
281
|
result = fetch_token_string(args, post)
|
|
250
|
-
|
|
282
|
+
|
|
251
283
|
# if we have an error, parse the error JSON and raise an error
|
|
252
284
|
raise APIError.new((JSON.parse(result)["error"] rescue nil) || {}) if result =~ /error/
|
|
253
285
|
|
|
254
286
|
# otherwise, parse the access token
|
|
255
|
-
parse_access_token(result)
|
|
287
|
+
parse_access_token(result)
|
|
256
288
|
end
|
|
257
|
-
|
|
289
|
+
|
|
258
290
|
def parse_access_token(response_text)
|
|
259
291
|
components = response_text.split("&").inject({}) do |hash, bit|
|
|
260
292
|
key, value = bit.split("=")
|
|
261
293
|
hash.merge!(key => value)
|
|
262
294
|
end
|
|
263
|
-
components
|
|
295
|
+
components
|
|
264
296
|
end
|
|
265
297
|
|
|
266
298
|
def fetch_token_string(args, post = false, endpoint = "access_token")
|
|
267
299
|
Koala.make_request("/oauth/#{endpoint}", {
|
|
268
|
-
:client_id => @app_id,
|
|
300
|
+
:client_id => @app_id,
|
|
269
301
|
:client_secret => @app_secret
|
|
270
|
-
}.merge!(args), post ? "post" : "get").body
|
|
302
|
+
}.merge!(args), post ? "post" : "get", :use_ssl => true).body
|
|
271
303
|
end
|
|
272
|
-
|
|
304
|
+
|
|
273
305
|
# base 64
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
"#{string}==".tr("-_", "+/").unpack("m")[0]
|
|
306
|
+
# directly from https://github.com/facebook/crypto-request-examples/raw/master/sample.rb
|
|
307
|
+
def base64_url_decode(str)
|
|
308
|
+
str += '=' * (4 - str.length.modulo(4))
|
|
309
|
+
Base64.decode64(str.gsub('-', '+').gsub('_', '/'))
|
|
279
310
|
end
|
|
280
311
|
end
|
|
281
312
|
end
|
|
282
|
-
|
|
313
|
+
|
|
283
314
|
# finally, set up the http service Koala methods used to make requests
|
|
284
315
|
# you can use your own (for HTTParty, etc.) by calling Koala.http_service = YourModule
|
|
285
316
|
def self.http_service=(service)
|
|
@@ -288,7 +319,6 @@ module Koala
|
|
|
288
319
|
|
|
289
320
|
# by default, try requiring Typhoeus -- if that works, use it
|
|
290
321
|
begin
|
|
291
|
-
require 'typhoeus'
|
|
292
322
|
Koala.http_service = TyphoeusService
|
|
293
323
|
rescue LoadError
|
|
294
324
|
Koala.http_service = NetHTTPService
|
data/readme.md
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
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, 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
10
|
Graph API
|
|
11
11
|
----
|
|
12
|
-
The Graph API is the simple, slick new interface to Facebook's data. Using it with Koala is quite straightforward:
|
|
12
|
+
The Graph API is the simple, slick new interface to Facebook's data. Using it with Koala is quite straightforward:
|
|
13
13
|
|
|
14
14
|
graph = Koala::Facebook::GraphAPI.new(oauth_access_token)
|
|
15
15
|
profile = graph.get_object("me")
|
|
16
16
|
friends = graph.get_connections("me", "friends")
|
|
17
17
|
graph.put_object("me", "feed", :message => "I am writing on my wall!")
|
|
18
18
|
|
|
19
|
-
The response of most requests is the JSON data returned from the Facebook servers as a Hash.
|
|
19
|
+
The response of most requests is the JSON data returned from the Facebook servers as a Hash.
|
|
20
20
|
|
|
21
21
|
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
|
-
|
|
22
|
+
|
|
23
23
|
# Returns the feed items for the currently logged-in user as a GraphCollection
|
|
24
24
|
feed = graph.get_connections("me", "feed")
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
# GraphCollection is a sub-class of Array, so you can use it as a usual Array
|
|
27
27
|
first_entry = feed[0]
|
|
28
28
|
last_entry = feed.last
|
|
@@ -33,7 +33,7 @@ When retrieving data that returns an array of results (for example, when calling
|
|
|
33
33
|
# Returns an array describing the URL for the next page: [path, arguments]
|
|
34
34
|
# This is useful for paging across multiple requests
|
|
35
35
|
next_path, next_args = feed.next_page_params
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
# You can use those params to easily get the next (or prevous) page
|
|
38
38
|
page = graph.get_page(feed.next_page_params)
|
|
39
39
|
|
|
@@ -41,25 +41,26 @@ Check out the wiki for more examples.
|
|
|
41
41
|
|
|
42
42
|
The old-school REST API
|
|
43
43
|
-----
|
|
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.
|
|
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.
|
|
45
45
|
|
|
46
46
|
Koala now supports the old-school REST API using OAuth access tokens; to use this, instantiate your class using the RestAPI class:
|
|
47
47
|
|
|
48
48
|
@rest = Koala::Facebook::RestAPI.new(oauth_access_token)
|
|
49
49
|
@rest.fql_query(my_fql_query) # convenience method
|
|
50
50
|
@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.
|
|
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.
|
|
53
53
|
|
|
54
54
|
(If you want the power of both APIs in the palm of your hand, try out the GraphAndRestAPI class.)
|
|
55
55
|
|
|
56
56
|
OAuth
|
|
57
57
|
-----
|
|
58
58
|
You can use the Graph and REST APIs without an OAuth access token, but the real magic happens when you provide Facebook an OAuth token to prove you're authenticated. Koala provides an OAuth class to make that process easy:
|
|
59
|
-
@oauth = Koala::Facebook::OAuth.new(app_id,
|
|
59
|
+
@oauth = Koala::Facebook::OAuth.new(app_id, app_secret, callback_url)
|
|
60
60
|
|
|
61
61
|
If your application uses Koala and the Facebook [JavaScript SDK](http://github.com/facebook/connect-js) (formerly Facebook Connect), you can use the OAuth class to parse the cookies:
|
|
62
|
-
@oauth.
|
|
62
|
+
@oauth.get_user_from_cookies(cookies) # gets the user's ID
|
|
63
|
+
@oauth.get_user_info_from_cookies(cookies) # parses and returns the entire hash
|
|
63
64
|
|
|
64
65
|
And if you have to use the more complicated [redirect-based OAuth process](http://developers.facebook.com/docs/authentication/), Koala helps out there, too:
|
|
65
66
|
# generate authenticating URL
|
|
@@ -118,7 +119,7 @@ Some resources to help you as you play with Koala and the Graph API:
|
|
|
118
119
|
Testing
|
|
119
120
|
-----
|
|
120
121
|
|
|
121
|
-
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:
|
|
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:
|
|
122
123
|
# From the spec directory
|
|
123
124
|
spec koala_spec.rb
|
|
124
125
|
|
data/spec/facebook_data.yml
CHANGED
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
# Just remember to update all fields!
|
|
5
5
|
|
|
6
6
|
# You must supply this value yourself to test the GraphAPI class.
|
|
7
|
-
# Your OAuth token should have publish_stream and
|
|
8
|
-
oauth_token:
|
|
7
|
+
# Your OAuth token should have publish_stream, read_stream, and user_photos permissions.
|
|
8
|
+
oauth_token:
|
|
9
9
|
|
|
10
10
|
# for testing the OAuth class
|
|
11
11
|
# baseline app
|
|
12
12
|
oauth_test_data:
|
|
13
13
|
# You must supply this value yourself, since they will expire.
|
|
14
|
-
code:
|
|
14
|
+
code:
|
|
15
15
|
# easiest way to get session keys: use multiple test accounts with the Javascript login at http://oauth.twoalex.com
|
|
16
|
-
session_key:
|
|
16
|
+
session_key:
|
|
17
17
|
multiple_session_keys:
|
|
18
|
-
-
|
|
19
|
-
-
|
|
18
|
+
-
|
|
19
|
+
-
|
|
20
20
|
|
|
21
21
|
# These values will work out of the box
|
|
22
22
|
app_id: 119908831367602
|
|
@@ -44,8 +44,14 @@ oauth_test_data:
|
|
|
44
44
|
signed_request_result:
|
|
45
45
|
"0": payload
|
|
46
46
|
algorithm: HMAC-SHA256
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
# signed params
|
|
48
|
+
# examples from http://developers.facebook.com/docs/authentication/canvas/encryption_proposal
|
|
49
|
+
signed_params: t63pZQ4Q3ZTHJt0hOsKrY2pb28xRlduW0pg4lL_Zhl4.eyJhbGdvcml0aG0iOiJBRVMtMjU2LUNCQyBITUFDLVNIQTI1NiIsImlzc3VlZF9hdCI6MTI4NzYwMTk4OCwiaXYiOiJmRExKQ1cteWlYbXVOYTI0ZVNhckpnIiwicGF5bG9hZCI6IllHeW00cG9Rbk1UckVnaUFPa0ZUVkk4NWxsNVJ1VWlFbC1JZ3FmeFRPVEhRTkl2VlZJOFk4a1Z1T29lS2FXT2Vhc3NXRlRFdjBRZ183d0NDQkVlbjdsVUJCemxGSjFWNjNISjNBZjBTSW5nY3hXVEo3TDZZTGF0TW13WGdEQXZXbjVQc2ZxeldrNG1sOWg5RExuWXB0V0htREdMNmlCaU9oTjdXeUk3cDZvRXBWcmlGdUp3X2NoTG9QYjhhM3ZHRG5vVzhlMlN4eDA2QTJ4MnhraWFwdmcifQ
|
|
50
|
+
signed_params_secret: 13750c9911fec5865d01f3bd00bdf4db
|
|
51
|
+
signed_params_result:
|
|
52
|
+
access_token: "101244219942650|2.wdrSr7KyE_VwQ0fjwOfW9A__.3600.1287608400-499091902|XzxMQd-_4tjlC2VEgide4rmg6LI"
|
|
53
|
+
expires_in: 6412
|
|
54
|
+
user_id: "499091902"
|
|
49
55
|
|
|
50
56
|
subscription_test_data:
|
|
51
57
|
subscription_path: http://oauth.twoalex.com/subscriptions
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
class ApiBaseTests < Test::Unit::TestCase
|
|
2
|
+
|
|
2
3
|
describe "Koala API base class" do
|
|
3
4
|
before(:each) do
|
|
4
5
|
@service = Koala::Facebook::API.new
|
|
@@ -29,6 +30,12 @@ class ApiBaseTests < Test::Unit::TestCase
|
|
|
29
30
|
service.api('anything')
|
|
30
31
|
end
|
|
31
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
|
+
|
|
32
39
|
it "should get the attribute of a Koala::Response given by the http_component parameter" do
|
|
33
40
|
http_component = :method_name
|
|
34
41
|
|
|
Binary file
|
|
@@ -3,8 +3,12 @@ class GraphAndRestAPINoTokenTests < Test::Unit::TestCase
|
|
|
3
3
|
before(:each) do
|
|
4
4
|
@api = Koala::Facebook::GraphAndRestAPI.new
|
|
5
5
|
end
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
it_should_behave_like "Koala RestAPI"
|
|
7
8
|
it_should_behave_like "Koala RestAPI without an access token"
|
|
9
|
+
|
|
10
|
+
it_should_behave_like "Koala GraphAPI"
|
|
8
11
|
it_should_behave_like "Koala GraphAPI without an access token"
|
|
12
|
+
it_should_behave_like "Koala GraphAPI with GraphCollection"
|
|
9
13
|
end
|
|
10
14
|
end
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
class GraphAndRestAPIWithTokenTests < Test::Unit::TestCase
|
|
2
2
|
describe "Koala GraphAndRestAPI without an access token" do
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
it_should_behave_like "Koala GraphAPI with an access token"
|
|
6
|
-
|
|
3
|
+
include LiveTestingDataHelper
|
|
4
|
+
|
|
7
5
|
before(:each) do
|
|
8
6
|
@api = Koala::Facebook::GraphAndRestAPI.new(@token)
|
|
9
7
|
end
|
|
8
|
+
|
|
9
|
+
it_should_behave_like "Koala RestAPI"
|
|
10
|
+
it_should_behave_like "Koala RestAPI with an access token"
|
|
11
|
+
|
|
12
|
+
it_should_behave_like "Koala GraphAPI"
|
|
13
|
+
it_should_behave_like "Koala GraphAPI with an access token"
|
|
14
|
+
it_should_behave_like "Koala GraphAPI with GraphCollection"
|
|
10
15
|
end
|
|
11
16
|
end
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
shared_examples_for "Koala GraphAPI without an access token" do
|
|
2
|
-
it "should get public data about a user" do
|
|
3
|
-
result = @api.get_object("koppel")
|
|
4
|
-
# the results should have an ID and a name, among other things
|
|
5
|
-
(result["id"] && result["name"]).should
|
|
6
|
-
end
|
|
7
2
|
|
|
8
3
|
it "should not get private data about a user" do
|
|
9
4
|
result = @api.get_object("koppel")
|
|
@@ -11,46 +6,14 @@ shared_examples_for "Koala GraphAPI without an access token" do
|
|
|
11
6
|
result["updated_time"].should be_nil
|
|
12
7
|
end
|
|
13
8
|
|
|
14
|
-
it "should get public data about a Page" do
|
|
15
|
-
result = @api.get_object("contextoptional")
|
|
16
|
-
# the results should have an ID and a name, among other things
|
|
17
|
-
(result["id"] && result["name"]).should
|
|
18
|
-
end
|
|
19
|
-
|
|
20
9
|
it "should not be able to get data about 'me'" do
|
|
21
10
|
lambda { @api.get_object("me") }.should raise_error(Koala::Facebook::APIError)
|
|
22
11
|
end
|
|
23
12
|
|
|
24
|
-
it "should be able to get multiple objects" do
|
|
25
|
-
results = @api.get_objects(["contextoptional", "naitik"])
|
|
26
|
-
results.length.should == 2
|
|
27
|
-
end
|
|
28
|
-
|
|
29
13
|
it "shouldn't be able to access connections from users" do
|
|
30
14
|
lambda { @api.get_connections("lukeshepard", "likes") }.should raise_error(Koala::Facebook::APIError)
|
|
31
15
|
end
|
|
32
16
|
|
|
33
|
-
it "should be able to access a user's picture" do
|
|
34
|
-
@api.get_picture("chris.baclig").should =~ /http\:\/\//
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
it "should be able to access a user's picture, given a picture type" do
|
|
38
|
-
@api.get_picture("chris.baclig", {:type => 'large'}).should =~ /^http\:\/\//
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
it "should be able to access connections from public Pages" do
|
|
42
|
-
result = @api.get_connections("contextoptional", "photos")
|
|
43
|
-
result.should be_a(Array)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# paging
|
|
47
|
-
# see also graph_collection_tests
|
|
48
|
-
it "should make a request for a page when provided a specific set of page params" do
|
|
49
|
-
query = [1, 2]
|
|
50
|
-
@api.should_receive(:graph_call).with(*query)
|
|
51
|
-
@api.get_page(query)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
17
|
it "should not be able to put an object" do
|
|
55
18
|
lambda { @result = @api.put_object("lukeshepard", "feed", :message => "Hello, world") }.should raise_error(Koala::Facebook::APIError)
|
|
56
19
|
puts "Error! Object #{@result.inspect} somehow put onto Luke Shepard's wall!" if @result
|
|
@@ -60,7 +23,7 @@ shared_examples_for "Koala GraphAPI without an access token" do
|
|
|
60
23
|
it "should not be able to post to a feed" do
|
|
61
24
|
(lambda do
|
|
62
25
|
attachment = {:name => "Context Optional", :link => "http://www.contextoptional.com/"}
|
|
63
|
-
@result = @api.put_wall_post("Hello, world", attachment, "contextoptional")
|
|
26
|
+
@result = @api.put_wall_post("Hello, world", attachment, "contextoptional")
|
|
64
27
|
end).should raise_error(Koala::Facebook::APIError)
|
|
65
28
|
puts "Error! Object #{@result.inspect} somehow put onto Context Optional's wall!" if @result
|
|
66
29
|
end
|
|
@@ -68,7 +31,7 @@ shared_examples_for "Koala GraphAPI without an access token" do
|
|
|
68
31
|
it "should not be able to comment on an object" do
|
|
69
32
|
# random public post on the ContextOptional wall
|
|
70
33
|
lambda { @result = @api.put_comment("7204941866_119776748033392", "The hackathon was great!") }.should raise_error(Koala::Facebook::APIError)
|
|
71
|
-
puts "Error! Object #{@result.inspect} somehow commented on post 7204941866_119776748033392!" if @result
|
|
34
|
+
puts "Error! Object #{@result.inspect} somehow commented on post 7204941866_119776748033392!" if @result
|
|
72
35
|
end
|
|
73
36
|
|
|
74
37
|
it "should not be able to like an object" do
|
|
@@ -76,29 +39,13 @@ shared_examples_for "Koala GraphAPI without an access token" do
|
|
|
76
39
|
end
|
|
77
40
|
|
|
78
41
|
# DELETE
|
|
79
|
-
it "should not be able to delete posts" do
|
|
42
|
+
it "should not be able to delete posts" do
|
|
80
43
|
# test post on the Ruby SDK Test application
|
|
81
44
|
lambda { @result = @api.delete_object("115349521819193_113815981982767") }.should raise_error(Koala::Facebook::APIError)
|
|
82
45
|
end
|
|
83
|
-
|
|
84
|
-
# SEARCH
|
|
85
|
-
it "should be able to search" do
|
|
86
|
-
result = @api.search("facebook")
|
|
87
|
-
result.length.should be_an(Integer)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
it_should_behave_like "Koala GraphAPI with GraphCollection"
|
|
91
46
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
Koala.should_receive(:make_request).with(
|
|
95
|
-
anything,
|
|
96
|
-
anything,
|
|
97
|
-
anything,
|
|
98
|
-
hash_not_including(:rest_api => true)
|
|
99
|
-
).and_return(Koala::Response.new(200, "", {}))
|
|
100
|
-
|
|
101
|
-
@api.api("anything")
|
|
47
|
+
it "should not be able to delete a like" do
|
|
48
|
+
lambda { @api.delete_like("7204941866_119776748033392") }.should raise_error(Koala::Facebook::APIError)
|
|
102
49
|
end
|
|
103
50
|
end
|
|
104
51
|
|
|
@@ -106,9 +53,11 @@ class FacebookNoAccessTokenTests < Test::Unit::TestCase
|
|
|
106
53
|
describe "Koala GraphAPI without an access token" do
|
|
107
54
|
before :each do
|
|
108
55
|
@api = Koala::Facebook::GraphAPI.new
|
|
109
|
-
end
|
|
110
|
-
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it_should_behave_like "Koala GraphAPI"
|
|
111
59
|
it_should_behave_like "Koala GraphAPI without an access token"
|
|
60
|
+
it_should_behave_like "Koala GraphAPI with GraphCollection"
|
|
61
|
+
|
|
112
62
|
end
|
|
113
63
|
end
|
|
114
|
-
|