koala 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.autotest +12 -0
- data/.gitignore +3 -1
- data/.travis.yml +9 -0
- data/CHANGELOG +62 -2
- data/Gemfile +8 -0
- data/Rakefile +0 -1
- data/autotest/discover.rb +1 -0
- data/koala.gemspec +13 -14
- data/lib/koala/batch_operation.rb +74 -0
- data/lib/koala/graph_api.rb +145 -132
- data/lib/koala/graph_batch_api.rb +97 -0
- data/lib/koala/graph_collection.rb +59 -0
- data/lib/koala/http_service.rb +176 -0
- data/lib/koala/oauth.rb +191 -0
- data/lib/koala/realtime_updates.rb +23 -29
- data/lib/koala/rest_api.rb +13 -8
- data/lib/koala/test_users.rb +33 -17
- data/lib/koala/uploadable_io.rb +153 -87
- data/lib/koala/utils.rb +11 -0
- data/lib/koala/version.rb +3 -0
- data/lib/koala.rb +59 -217
- data/readme.md +92 -53
- data/spec/cases/{api_base_spec.rb → api_spec.rb} +31 -6
- data/spec/cases/error_spec.rb +32 -0
- data/spec/cases/graph_and_rest_api_spec.rb +12 -21
- data/spec/cases/graph_api_batch_spec.rb +582 -0
- data/spec/cases/graph_api_spec.rb +11 -14
- data/spec/cases/graph_collection_spec.rb +116 -0
- data/spec/cases/http_service_spec.rb +446 -0
- data/spec/cases/koala_spec.rb +54 -0
- data/spec/cases/oauth_spec.rb +319 -213
- data/spec/cases/realtime_updates_spec.rb +45 -31
- data/spec/cases/rest_api_spec.rb +23 -7
- data/spec/cases/test_users_spec.rb +123 -75
- data/spec/cases/uploadable_io_spec.rb +120 -37
- data/spec/cases/utils_spec.rb +10 -0
- data/spec/fixtures/cat.m4v +0 -0
- data/spec/fixtures/facebook_data.yml +26 -24
- data/spec/fixtures/mock_facebook_responses.yml +203 -78
- data/spec/spec_helper.rb +30 -5
- data/spec/support/graph_api_shared_examples.rb +149 -118
- data/spec/support/json_testing_fix.rb +42 -0
- data/spec/support/koala_test.rb +187 -0
- data/spec/support/mock_http_service.rb +62 -58
- data/spec/support/ordered_hash.rb +205 -0
- data/spec/support/rest_api_shared_examples.rb +139 -15
- data/spec/support/uploadable_io_shared_examples.rb +2 -8
- metadata +90 -114
- data/lib/koala/http_services.rb +0 -146
- data/spec/cases/http_services/http_service_spec.rb +0 -54
- data/spec/cases/http_services/net_http_service_spec.rb +0 -350
- data/spec/cases/http_services/typhoeus_service_spec.rb +0 -144
- data/spec/support/live_testing_data_helper.rb +0 -40
- data/spec/support/setup_mocks_or_live.rb +0 -52
data/lib/koala.rb
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
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'
|
|
8
8
|
require 'base64'
|
|
9
9
|
|
|
10
10
|
# include koala modules
|
|
11
|
-
require 'koala/
|
|
11
|
+
require 'koala/oauth'
|
|
12
12
|
require 'koala/graph_api'
|
|
13
|
+
require 'koala/graph_batch_api'
|
|
14
|
+
require 'koala/batch_operation'
|
|
15
|
+
require 'koala/graph_collection'
|
|
13
16
|
require 'koala/rest_api'
|
|
14
17
|
require 'koala/realtime_updates'
|
|
15
18
|
require 'koala/test_users'
|
|
16
19
|
|
|
20
|
+
# HTTP module so we can communicate with Facebook
|
|
21
|
+
require 'koala/http_service'
|
|
22
|
+
|
|
17
23
|
# add KoalaIO class
|
|
18
24
|
require 'koala/uploadable_io'
|
|
19
25
|
|
|
26
|
+
# miscellaneous
|
|
27
|
+
require 'koala/utils'
|
|
28
|
+
require 'koala/version'
|
|
29
|
+
|
|
20
30
|
module Koala
|
|
21
31
|
|
|
22
32
|
module Facebook
|
|
@@ -25,6 +35,7 @@ module Koala
|
|
|
25
35
|
# Contributors: Alex Koppel, Chris Baclig, Rafi Jacoby, and the team at Context Optional
|
|
26
36
|
# http://github.com/arsduo/koala
|
|
27
37
|
|
|
38
|
+
# APIs
|
|
28
39
|
class API
|
|
29
40
|
# initialize with an access token
|
|
30
41
|
def initialize(access_token = nil)
|
|
@@ -32,10 +43,13 @@ module Koala
|
|
|
32
43
|
end
|
|
33
44
|
attr_reader :access_token
|
|
34
45
|
|
|
46
|
+
include GraphAPIMethods
|
|
47
|
+
include RestAPIMethods
|
|
48
|
+
|
|
35
49
|
def api(path, args = {}, verb = "get", options = {}, &error_checking_block)
|
|
36
50
|
# Fetches the given path in the Graph API.
|
|
37
51
|
args["access_token"] = @access_token || @app_access_token if @access_token || @app_access_token
|
|
38
|
-
|
|
52
|
+
|
|
39
53
|
# add a leading /
|
|
40
54
|
path = "/#{path}" unless path =~ /^\//
|
|
41
55
|
|
|
@@ -47,45 +61,42 @@ module Koala
|
|
|
47
61
|
# in the case of a server error
|
|
48
62
|
raise APIError.new({"type" => "HTTP #{result.status.to_s}", "message" => "Response body: #{result.body}"}) if result.status >= 500
|
|
49
63
|
|
|
50
|
-
#
|
|
64
|
+
# parse the body as JSON and run it through the error checker (if provided)
|
|
51
65
|
# 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
|
|
66
|
+
# and cause MultiJson.decode to fail -- so we account for that by wrapping the result in []
|
|
67
|
+
body = MultiJson.decode("[#{result.body.to_s}]")[0]
|
|
68
|
+
yield body if error_checking_block
|
|
57
69
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
result.send(options[:http_component])
|
|
61
|
-
else
|
|
62
|
-
body
|
|
63
|
-
end
|
|
70
|
+
# if we want a component other than the body (e.g. redirect header for images), return that
|
|
71
|
+
options[:http_component] ? result.send(options[:http_component]) : body
|
|
64
72
|
end
|
|
65
73
|
end
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
# special enhanced APIs
|
|
76
|
+
class GraphBatchAPI < API
|
|
77
|
+
include GraphBatchAPIMethods
|
|
69
78
|
end
|
|
70
79
|
|
|
71
|
-
class
|
|
72
|
-
include
|
|
80
|
+
class RealtimeUpdates
|
|
81
|
+
include RealtimeUpdateMethods
|
|
73
82
|
end
|
|
74
83
|
|
|
75
|
-
class
|
|
76
|
-
include
|
|
77
|
-
include RestAPIMethods
|
|
84
|
+
class TestUsers
|
|
85
|
+
include TestUserMethods
|
|
78
86
|
end
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
# legacy support for old APIs
|
|
89
|
+
class OldAPI < API;
|
|
90
|
+
def initialize(*args)
|
|
91
|
+
Koala::Utils.deprecate("#{self.class.name} is deprecated and will be removed in a future version; please use the API class instead.")
|
|
92
|
+
super
|
|
93
|
+
end
|
|
82
94
|
end
|
|
95
|
+
class GraphAPI < OldAPI; end
|
|
96
|
+
class RestAPI < OldAPI; end
|
|
97
|
+
class GraphAndRestAPI < OldAPI; end
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
include TestUserMethods
|
|
86
|
-
# make the Graph API accessible in case someone wants to make other calls to interact with their users
|
|
87
|
-
attr_reader :graph_api
|
|
88
|
-
end
|
|
99
|
+
# Errors
|
|
89
100
|
|
|
90
101
|
class APIError < StandardError
|
|
91
102
|
attr_accessor :fb_error_type
|
|
@@ -94,201 +105,32 @@ module Koala
|
|
|
94
105
|
super("#{fb_error_type}: #{details["message"]}")
|
|
95
106
|
end
|
|
96
107
|
end
|
|
108
|
+
end
|
|
97
109
|
|
|
110
|
+
class KoalaError < StandardError; end
|
|
98
111
|
|
|
99
|
-
class OAuth
|
|
100
|
-
attr_reader :app_id, :app_secret, :oauth_callback_url
|
|
101
|
-
def initialize(app_id, app_secret, oauth_callback_url = nil)
|
|
102
|
-
@app_id = app_id
|
|
103
|
-
@app_secret = app_secret
|
|
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
112
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
str += '=' * (4 - str.length.modulo(4))
|
|
273
|
-
Base64.decode64(str.tr('-_', '+/'))
|
|
274
|
-
end
|
|
275
|
-
end
|
|
113
|
+
# finally, the few things defined on the Koala module itself
|
|
114
|
+
class << self
|
|
115
|
+
attr_accessor :http_service
|
|
276
116
|
end
|
|
277
117
|
|
|
278
|
-
class KoalaError< StandardError; end
|
|
279
|
-
|
|
280
|
-
# finally, set up the http service Koala methods used to make requests
|
|
281
|
-
# you can use your own (for HTTParty, etc.) by calling Koala.http_service = YourModule
|
|
282
118
|
def self.http_service=(service)
|
|
283
|
-
|
|
119
|
+
if service.respond_to?(:deprecated_interface)
|
|
120
|
+
# if this is a deprecated module, support the old interface
|
|
121
|
+
# by changing the default adapter so the right library is used
|
|
122
|
+
# we continue to use the single HTTPService module for everything
|
|
123
|
+
service.deprecated_interface
|
|
124
|
+
else
|
|
125
|
+
# if it's a real http_service, use it
|
|
126
|
+
@http_service = service
|
|
127
|
+
end
|
|
284
128
|
end
|
|
285
129
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
# you can run Koala.http_service = NetHTTPService (or MyHTTPService)
|
|
289
|
-
begin
|
|
290
|
-
Koala.http_service = TyphoeusService
|
|
291
|
-
rescue LoadError
|
|
292
|
-
Koala.http_service = NetHTTPService
|
|
130
|
+
def self.make_request(path, args, verb, options = {})
|
|
131
|
+
http_service.make_request(path, args, verb, options)
|
|
293
132
|
end
|
|
133
|
+
|
|
134
|
+
# we use Faraday as our main service, with mock as the other main one
|
|
135
|
+
self.http_service = HTTPService
|
|
294
136
|
end
|
data/readme.md
CHANGED
|
@@ -1,67 +1,95 @@
|
|
|
1
|
+
[](http://travis-ci.org/arsduo/koala)
|
|
2
|
+
|
|
1
3
|
Koala
|
|
2
4
|
====
|
|
3
|
-
Koala
|
|
5
|
+
[Koala](http://github.com/arsduo/koala) is a 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
6
|
|
|
5
|
-
* Lightweight: Koala should be as light and simple as Facebook’s own
|
|
6
|
-
* Fast: Koala should, out of the box, be quick.
|
|
7
|
-
* Flexible: Koala should be useful to everyone, regardless of their current configuration. (We
|
|
8
|
-
* Tested: Koala should have complete test coverage, so you can rely on it. (Our
|
|
7
|
+
* Lightweight: Koala should be as light and simple as Facebook’s own libraries, providing API accessors and returning simple JSON.
|
|
8
|
+
* Fast: Koala should, out of the box, be quick. Out of the box, we use Facebook's faster read-only servers when possible and if available, the Typhoeus gem to make snappy Facebook requests. Of course, that brings us to our next topic:
|
|
9
|
+
* Flexible: Koala should be useful to everyone, regardless of their current configuration. (We support JRuby, Rubinius, and REE as well as vanilla Ruby, and use the Faraday library to provide complete flexibility over how HTTP requests are made.)
|
|
10
|
+
* Tested: Koala should have complete test coverage, so you can rely on it. (Our test coverage is complete and can be run against either mocked responses or the live Facebook servers.)
|
|
9
11
|
|
|
10
|
-
1
|
|
12
|
+
Facebook Changes on October 1, 2011
|
|
11
13
|
---
|
|
12
|
-
Version 1.0 is due out on May 1st, 2011 with a ton of great features.
|
|
13
|
-
|
|
14
|
-
sudo gem install koala
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
**Koala 1.2 supports all of Facebook's new authentication schemes**, which will be introduced on October 1, 2011; the old Javascript library and older authentication schemes will be deprecated at the same time.
|
|
16
|
+
|
|
17
|
+
To test your application, upgrade to the latest version of Koala (see below) and configure your application according to Facebook's [OAuth 2.0 and HTTPS Migration](https://developers.facebook.com/docs/oauth2-https-migration/) guide. If you have the appropriate calls to get_user_info_from_cookies (apps using the Javascript SDK) and/or parse_signed_params (for Canvas and tab apps), your application should work without a hitch.
|
|
18
|
+
|
|
19
|
+
_Note_: in their new secure cookie format, Facebook provides an OAuth code, which Koala automatically exchanges for an access token. Because this involves a call to Facebook's servers, you should consider storing the user's access token in their session and only calling get_user_info_from_cookies when necessary (access_token not present, you discover it's expired, etc.). Otherwise, you'll be calling out to Facebook each time the user loads a page, slowing down your site. (As we figure out best practices for this, we'll update the wiki.)
|
|
20
|
+
|
|
21
|
+
Installation
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
Easy:
|
|
25
|
+
|
|
26
|
+
[sudo|rvm] gem install koala
|
|
27
|
+
|
|
28
|
+
Or in Bundler:
|
|
29
|
+
|
|
30
|
+
gem "koala"
|
|
31
|
+
|
|
20
32
|
Graph API
|
|
21
33
|
----
|
|
22
34
|
The Graph API is the simple, slick new interface to Facebook's data. Using it with Koala is quite straightforward:
|
|
35
|
+
|
|
36
|
+
@graph = Koala::Facebook::API.new(oauth_access_token)
|
|
37
|
+
# in 1.1 or earlier, use GraphAPI instead of API
|
|
38
|
+
|
|
39
|
+
profile = @graph.get_object("me")
|
|
40
|
+
friends = @graph.get_connections("me", "friends")
|
|
41
|
+
@graph.put_object("me", "feed", :message => "I am writing on my wall!")
|
|
23
42
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
graph.put_object("me", "feed", :message => "I am writing on my wall!")
|
|
43
|
+
# you can even use the new Timeline API
|
|
44
|
+
# see https://developers.facebook.com/docs/beta/opengraph/tutorial/
|
|
45
|
+
@graph.put_connections("me", "namespace:action", :object => object_url)
|
|
28
46
|
|
|
29
47
|
The response of most requests is the JSON data returned from the Facebook servers as a Hash.
|
|
30
48
|
|
|
31
|
-
When retrieving data that returns an array of results (for example, when calling
|
|
49
|
+
When retrieving data that returns an array of results (for example, when calling API#get_connections or API#search) a GraphCollection object will be returned, which makes it easy to page through the results:
|
|
32
50
|
|
|
33
51
|
# Returns the feed items for the currently logged-in user as a GraphCollection
|
|
34
|
-
feed = graph.get_connections("me", "feed")
|
|
35
|
-
|
|
36
|
-
# GraphCollection is a sub-class of Array, so you can use it as a usual Array
|
|
37
|
-
first_entry = feed[0]
|
|
38
|
-
last_entry = feed.last
|
|
39
|
-
|
|
40
|
-
# Returns the next page of results (also as a GraphCollection)
|
|
52
|
+
feed = @graph.get_connections("me", "feed")
|
|
53
|
+
feed.each {|f| do_something_with_item(f) } # it's a subclass of Array
|
|
41
54
|
next_feed = feed.next_page
|
|
42
55
|
|
|
43
|
-
#
|
|
44
|
-
# This is useful for
|
|
45
|
-
|
|
56
|
+
# You can also get an array describing the URL for the next page: [path, arguments]
|
|
57
|
+
# This is useful for storing page state across multiple browser requests
|
|
58
|
+
next_page_params = feed.next_page_params
|
|
59
|
+
page = @graph.get_page(next_page_params)
|
|
60
|
+
|
|
61
|
+
You can also make multiple calls at once using Facebook's batch API:
|
|
46
62
|
|
|
47
|
-
#
|
|
48
|
-
|
|
63
|
+
# Returns an array of results as if they were called non-batch
|
|
64
|
+
@graph.batch do |batch_api|
|
|
65
|
+
batch_api.get_object('me')
|
|
66
|
+
batch_api.put_wall_post('Making a post in a batch.')
|
|
67
|
+
end
|
|
49
68
|
|
|
50
|
-
Check out the wiki for more examples.
|
|
69
|
+
Check out the wiki for more details and examples.
|
|
51
70
|
|
|
52
|
-
The
|
|
71
|
+
The REST API
|
|
53
72
|
-----
|
|
54
73
|
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.
|
|
55
74
|
|
|
56
|
-
Koala
|
|
75
|
+
Fortunately, Koala supports the REST API using the very same interface; to use this, instantiate an API:
|
|
57
76
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
@rest = Koala::Facebook::API.new(oauth_access_token)
|
|
78
|
+
# in 1.1 or earlier, use RestAPI instead of API
|
|
79
|
+
|
|
80
|
+
@rest.fql_query(my_fql_query) # convenience method
|
|
81
|
+
@rest.fql_multiquery(fql_query_hash) # convenience method
|
|
82
|
+
@rest.rest_call("stream.publish", arguments_hash) # generic version
|
|
61
83
|
|
|
62
|
-
|
|
84
|
+
Of course, you can use the Graph API methods on the same object -- the power of two APIs right in the palm of your hand.
|
|
63
85
|
|
|
64
|
-
|
|
86
|
+
@api = Koala::Facebook::API.new(oauth_access_token)
|
|
87
|
+
# in 1.1 or earlier, use GraphAndRestAPI instead of API
|
|
88
|
+
|
|
89
|
+
@api = Koala::Facebook::API.new(oauth_access_token)
|
|
90
|
+
fql = @api.fql_query(my_fql_query)
|
|
91
|
+
@api.put_wall_post(process_result(fql))
|
|
92
|
+
|
|
65
93
|
|
|
66
94
|
OAuth
|
|
67
95
|
-----
|
|
@@ -70,7 +98,7 @@ You can use the Graph and REST APIs without an OAuth access token, but the real
|
|
|
70
98
|
|
|
71
99
|
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:
|
|
72
100
|
@oauth.get_user_from_cookies(cookies) # gets the user's ID
|
|
73
|
-
|
|
101
|
+
@oauth.get_user_info_from_cookies(cookies) # parses and returns the entire hash
|
|
74
102
|
|
|
75
103
|
And if you have to use the more complicated [redirect-based OAuth process](http://developers.facebook.com/docs/authentication/), Koala helps out there, too:
|
|
76
104
|
# generate authenticating URL
|
|
@@ -81,20 +109,18 @@ And if you have to use the more complicated [redirect-based OAuth process](http:
|
|
|
81
109
|
You can also get your application's own access token, which can be used without a user session for subscriptions and certain other requests:
|
|
82
110
|
@oauth.get_app_access_token
|
|
83
111
|
|
|
84
|
-
|
|
112
|
+
For those building apps on Facebook, parsing signed requests is simple:
|
|
113
|
+
@oauth.parse_signed_request(signed_request_string)
|
|
85
114
|
|
|
86
|
-
|
|
87
|
-
@oauth.parse_signed_request(request)
|
|
88
|
-
|
|
89
|
-
*Exchanging session keys:* Stuck building tab applications on Facebook? Wishing you had an OAuth token so you could use the Graph API? You're in luck! Koala now allows you to exchange session keys for OAuth access tokens:
|
|
115
|
+
Or, if for some horrible reason, you're still using session keys, despair not! It's easy to turn them into shiny, modern OAuth tokens:
|
|
90
116
|
@oauth.get_token_from_session_key(session_key)
|
|
91
117
|
@oauth.get_tokens_from_session_keys(array_of_session_keys)
|
|
92
118
|
|
|
119
|
+
That's it! It's pretty simple once you get the hang of it. If you're new to OAuth, though, check out the wiki and the OAuth Playground example site (see below).
|
|
120
|
+
|
|
93
121
|
Real-time Updates
|
|
94
122
|
-----
|
|
95
|
-
The Graph API
|
|
96
|
-
|
|
97
|
-
Currently, Facebook only supports subscribing to users, permissions and errors. On top of that, there are limitations on what attributes and connections for each of these objects you can subscribe to updates for. Check the [official Facebook documentation](http://developers.facebook.com/docs/api/realtime) for more details.
|
|
123
|
+
Sometimes, reaching out to Facebook is a pain -- let it reach out to you instead. The Graph API allows your application to subscribe to real-time updates for certain objects in the graph; check the [official Facebook documentation](http://developers.facebook.com/docs/api/realtime) for more details on what objects you can subscribe to and what limitations may apply.
|
|
98
124
|
|
|
99
125
|
Koala makes it easy to interact with your applications using the RealtimeUpdates class:
|
|
100
126
|
|
|
@@ -118,6 +144,17 @@ And to top it all off, RealtimeUpdates provides a static method to respond to Fa
|
|
|
118
144
|
|
|
119
145
|
For more information about meet_challenge and the RealtimeUpdates class, check out the Real-Time Updates page on the wiki.
|
|
120
146
|
|
|
147
|
+
Test Users
|
|
148
|
+
-----
|
|
149
|
+
|
|
150
|
+
We also support the test users API, allowing you to conjure up fake users and command them to do your bidding using the Graph or REST API:
|
|
151
|
+
|
|
152
|
+
@test_users = Koala::Facebook::TestUsers.new(:app_id => id, :secret => secret)
|
|
153
|
+
user = @test_users.create(is_app_installed, desired_permissions)
|
|
154
|
+
user_graph_api = Koala::Facebook::API.new(user["access_token"])
|
|
155
|
+
# or, if you want to make a whole community:
|
|
156
|
+
@test_users.create_network(network_size, is_app_installed, common_permissions)
|
|
157
|
+
|
|
121
158
|
See examples, ask questions
|
|
122
159
|
-----
|
|
123
160
|
Some resources to help you as you play with Koala and the Graph API:
|
|
@@ -130,14 +167,16 @@ Testing
|
|
|
130
167
|
-----
|
|
131
168
|
|
|
132
169
|
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
|
-
|
|
170
|
+
|
|
134
171
|
# From anywhere in the project directory:
|
|
135
|
-
rake spec
|
|
136
|
-
|
|
172
|
+
bundle exec rake spec
|
|
173
|
+
|
|
137
174
|
|
|
138
175
|
You can also run live tests against Facebook's servers:
|
|
139
|
-
|
|
176
|
+
|
|
140
177
|
# Again from anywhere in the project directory:
|
|
141
|
-
LIVE=true rake spec
|
|
178
|
+
LIVE=true bundle exec rake spec
|
|
179
|
+
# you can also test against Facebook's beta tier
|
|
180
|
+
LIVE=true BETA=true bundle exec rake spec
|
|
142
181
|
|
|
143
|
-
|
|
182
|
+
By default, the live tests are run against test users, so you can run them as frequently as you want. If you want to run them against a real user, however, you can fill in the OAuth token, code, and access\_token values in spec/fixtures/facebook_data.yml. See the wiki for more details.
|
|
@@ -44,7 +44,7 @@ describe "Koala::Facebook::API" do
|
|
|
44
44
|
|
|
45
45
|
Koala.stub(:make_request).and_return(response)
|
|
46
46
|
|
|
47
|
-
@service.api('anything', 'get',
|
|
47
|
+
@service.api('anything', {}, 'get', :http_component => http_component)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
it "should return the body of the request as JSON if no http_component is given" do
|
|
@@ -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
|
|
|
@@ -98,4 +98,29 @@ describe "Koala::Facebook::API" do
|
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
describe "with an access token" do
|
|
102
|
+
before(:each) do
|
|
103
|
+
@api = Koala::Facebook::API.new(@token)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it_should_behave_like "Koala RestAPI"
|
|
107
|
+
it_should_behave_like "Koala RestAPI with an access token"
|
|
108
|
+
|
|
109
|
+
it_should_behave_like "Koala GraphAPI"
|
|
110
|
+
it_should_behave_like "Koala GraphAPI with an access token"
|
|
111
|
+
it_should_behave_like "Koala GraphAPI with GraphCollection"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe "without an access token" do
|
|
115
|
+
before(:each) do
|
|
116
|
+
@api = Koala::Facebook::API.new
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it_should_behave_like "Koala RestAPI"
|
|
120
|
+
it_should_behave_like "Koala RestAPI without an access token"
|
|
121
|
+
|
|
122
|
+
it_should_behave_like "Koala GraphAPI"
|
|
123
|
+
it_should_behave_like "Koala GraphAPI without an access token"
|
|
124
|
+
it_should_behave_like "Koala GraphAPI with GraphCollection"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Koala::Facebook::APIError do
|
|
4
|
+
it "is a StandardError" do
|
|
5
|
+
Koala::Facebook::APIError.new.should be_a(StandardError)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it "has an accessor for fb_error_type" do
|
|
9
|
+
Koala::Facebook::APIError.instance_methods.map(&:to_sym).should include(:fb_error_type)
|
|
10
|
+
Koala::Facebook::APIError.instance_methods.map(&:to_sym).should include(:fb_error_type=)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "sets fb_error_type to details['type']" do
|
|
14
|
+
type = "foo"
|
|
15
|
+
Koala::Facebook::APIError.new("type" => type).fb_error_type.should == type
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "sets the error message details['type']: details['message']" do
|
|
19
|
+
type = "foo"
|
|
20
|
+
message = "bar"
|
|
21
|
+
error = Koala::Facebook::APIError.new("type" => type, "message" => message)
|
|
22
|
+
error.message.should =~ /#{type}/
|
|
23
|
+
error.message.should =~ /#{message}/
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe Koala::KoalaError do
|
|
28
|
+
it "is a StandardError" do
|
|
29
|
+
Koala::KoalaError.new.should be_a(StandardError)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|