162 | HTML
163 | end
164 |
165 | module_function
166 |
167 | # ==================== begin options utility =======================
168 | def rest_graph_oget key
169 | if rest_graph_options_ctl.has_key?(key)
170 | rest_graph_options_ctl[key]
171 | else
172 | RestGraph.send("default_#{key}")
173 | end
174 | end
175 |
176 | def rest_graph_options_ctl
177 | @rest_graph_options_ctl ||= {}
178 | end
179 |
180 | def rest_graph_options_new
181 | @rest_graph_options_new ||= {}
182 | end
183 | # ==================== end options utility =======================
184 |
185 |
186 |
187 | # ==================== begin facebook check ======================
188 | def rest_graph_check_params_signed_request
189 | return if rest_graph.authorized? || !params[:signed_request]
190 |
191 | rest_graph.parse_signed_request!(params[:signed_request])
192 | logger.debug("DEBUG: RestGraph: detected signed_request, parsed:" \
193 | " #{rest_graph.data.inspect}")
194 |
195 | if rest_graph.authorized?
196 | rest_graph_write_rg_fbs
197 | else
198 | logger.warn(
199 | "WARN: RestGraph: bad signed_request: #{params[:signed_request]}")
200 | end
201 | end
202 |
203 | # if the code is bad or not existed,
204 | # check if there's one in session,
205 | # meanwhile, there the sig and access_token is correct,
206 | # that means we're in the context of canvas
207 | def rest_graph_check_params_session
208 | return if rest_graph.authorized? || !params[:session]
209 |
210 | rest_graph.parse_json!(params[:session])
211 | logger.debug("DEBUG: RestGraph: detected session, parsed:" \
212 | " #{rest_graph.data.inspect}")
213 |
214 | if rest_graph.authorized?
215 | rest_graph_write_rg_fbs
216 | else
217 | logger.warn("WARN: RestGraph: bad session: #{params[:session]}")
218 | end
219 | end
220 |
221 | # if we're not in canvas nor code passed,
222 | # we could check out cookies as well.
223 | def rest_graph_check_cookie
224 | return if rest_graph.authorized? ||
225 | (!cookies["fbsr_#{rest_graph.app_id}"] &&
226 | !cookies["fbs_#{rest_graph.app_id}"])
227 |
228 | rest_graph.parse_cookies!(cookies)
229 | logger.debug("DEBUG: RestGraph: detected cookies, parsed:" \
230 | " #{rest_graph.data.inspect}")
231 |
232 | rest_graph_write_rg_fbs if rest_graph.authorized?
233 | end
234 |
235 | # exchange the code with access_token
236 | def rest_graph_check_code
237 | return if rest_graph.authorized? || !params[:code]
238 |
239 | rest_graph.authorize!(:code => params[:code],
240 | :redirect_uri => rest_graph_normalized_request_uri)
241 | logger.debug(
242 | "DEBUG: RestGraph: detected code with " \
243 | "#{rest_graph_normalized_request_uri}, " \
244 | "parsed: #{rest_graph.data.inspect}")
245 |
246 | rest_graph_write_rg_fbs if rest_graph.authorized?
247 | end
248 | # ==================== end facebook check ======================
249 |
250 |
251 |
252 | # ==================== begin check ================================
253 | def rest_graph_storage_key
254 | "rest_graph_fbs_#{rest_graph_oget(:app_id)}"
255 | end
256 |
257 | def rest_graph_check_rg_fbs
258 | rest_graph_check_rg_handler # custom method to store fbs
259 | rest_graph_check_rg_session # prefered way to store fbs
260 | rest_graph_check_rg_cookies # in canvas, session might not work..
261 | end
262 |
263 | def rest_graph_check_rg_handler handler=rest_graph_oget(:check_handler)
264 | return if rest_graph.authorized? || !handler
265 | rest_graph.parse_fbs!(handler.call)
266 | logger.debug("DEBUG: RestGraph: called check_handler, parsed:" \
267 | " #{rest_graph.data.inspect}")
268 | end
269 |
270 | def rest_graph_check_rg_session
271 | return if rest_graph.authorized? || !rest_graph_oget(:write_session) ||
272 | !(fbs = session[rest_graph_storage_key])
273 | rest_graph.parse_fbs!(fbs)
274 | logger.debug("DEBUG: RestGraph: detected rest-graph session, parsed:" \
275 | " #{rest_graph.data.inspect}")
276 | end
277 |
278 | def rest_graph_check_rg_cookies
279 | return if rest_graph.authorized? || !rest_graph_oget(:write_cookies) ||
280 | !(fbs = cookies[rest_graph_storage_key])
281 | rest_graph.parse_fbs!(fbs)
282 | logger.debug("DEBUG: RestGraph: detected rest-graph cookies, parsed:" \
283 | " #{rest_graph.data.inspect}")
284 | end
285 | # ==================== end check ================================
286 | # ==================== begin write ================================
287 | def rest_graph_write_rg_fbs
288 | rest_graph_write_rg_handler
289 | rest_graph_write_rg_session
290 | rest_graph_write_rg_cookies
291 | end
292 |
293 | def rest_graph_write_rg_handler handler=rest_graph_oget(:write_handler)
294 | return if !handler
295 | handler.call(fbs = rest_graph.fbs)
296 | logger.debug("DEBUG: RestGraph: called write_handler: fbs => #{fbs}")
297 | end
298 |
299 | def rest_graph_write_rg_session
300 | return if !rest_graph_oget(:write_session)
301 | session[rest_graph_storage_key] = fbs = rest_graph.fbs
302 | logger.debug("DEBUG: RestGraph: wrote session: fbs => #{fbs}")
303 | end
304 |
305 | def rest_graph_write_rg_cookies
306 | return if !rest_graph_oget(:write_cookies)
307 | cookies[rest_graph_storage_key] = fbs = rest_graph.fbs
308 | logger.debug("DEBUG: RestGraph: wrote cookies: fbs => #{fbs}")
309 | end
310 | # ==================== end write ================================
311 |
312 |
313 |
314 | # ==================== begin misc ================================
315 | def rest_graph_cleanup
316 | cookies.delete("fbs_#{rest_graph.app_id}")
317 | cookies.delete("fbsr_#{rest_graph.app_id}")
318 | cookies.delete(rest_graph_storage_key)
319 | session.delete(rest_graph_storage_key)
320 | end
321 |
322 | def rest_graph_normalized_request_uri
323 | uri = if rest_graph_in_canvas?
324 | # rails 3 uses newer rack which has fullpath
325 | "http://apps.facebook.com/#{rest_graph_oget(:canvas)}" +
326 | (request.respond_to?(:fullpath) ?
327 | request.fullpath : request.request_uri)
328 | else
329 | request.url
330 | end
331 |
332 | rest_graph_filter_uri(uri)
333 | end
334 |
335 | def rest_graph_filter_uri uri
336 | URI.parse(uri).tap{ |uri|
337 | uri.query = uri.query.split('&').reject{ |q|
338 | q =~ /^(code|session|signed_request)\=/
339 | }.join('&') if uri.query
340 | uri.query = nil if uri.query.blank?
341 | }.to_s
342 | rescue URI::InvalidURIError => e
343 | if @rest_graph_facebook_filter_uri_retry
344 | raise e
345 | else
346 | @rest_graph_facebook_filter_uri_retry = uri = URI.encode(uri)
347 | retry
348 | end
349 | end
350 |
351 | def rest_graph_in_canvas?
352 | !rest_graph_oget(:canvas).blank?
353 | end
354 |
355 | def rest_graph_auto_authorize?
356 | !rest_graph_oget(:auto_authorize_scope) .blank? ||
357 | !rest_graph_oget(:auto_authorize_options).blank? ||
358 | rest_graph_oget(:auto_authorize)
359 | end
360 |
361 | def rest_graph_extract_options options, method
362 | # Hash[] is for ruby 1.8.7
363 | Hash[options.send(method){ |(k, v)| RestGraph::Attributes.member?(k) }]
364 | end
365 | # ==================== end misc ================================
366 | end
367 |
368 | if Rails::VERSION::MAJOR == 2
369 | RestGraph::RailsUtil.init(Rails)
370 | end
371 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rest-graph [](http://travis-ci.org/godfat/rest-graph)
2 |
3 | by Cardinal Blue
4 |
5 | Tutorial on setting up a sample Facebook application with Rails 3
6 | and RestGraph could be found on [samplergthree][]. Instead, if you're
7 | an experienced Ruby programmer, you might also want to look at
8 | [detailed documents][].
9 |
10 | [samplergthree]: https://github.com/cardinalblue/samplergthree
11 | [detailed documents]: https://github.com/godfat/rest-graph/blob/master/doc/ToC.md
12 |
13 | ## LINKS:
14 |
15 | * [github](https://github.com/godfat/rest-graph)
16 | * [rubygems](https://rubygems.org/gems/rest-graph)
17 | * [rdoc](http://rdoc.info/projects/godfat/rest-graph)
18 | * [mailing list](http://groups.google.com/group/rest-graph/topics)
19 |
20 | ## DESCRIPTION:
21 |
22 | A lightweight Facebook Graph API client
23 |
24 | We have moved the development from rest-graph to [rest-more][].
25 | From now on, we would only fix bugs in rest-graph rather than adding
26 | features, and we would only backport important changes from rest-more
27 | once in a period. If you want the latest goodies, please see [rest-more][]
28 | Otherwise, you can stay with rest-graph with bugs fixes.
29 |
30 | [rest-more]: https://github.com/godfat/rest-more
31 |
32 | ## FEATURES:
33 |
34 | * Simple Graph API call
35 | * Simple FQL call
36 | * Utility to extract access_token and check sig in cookies/signed_request
37 |
38 | ## REQUIREMENTS:
39 |
40 | * Tested with MRI 1.8.7 and 1.9.2 and Rubinius 1.2.2.
41 | Because of development gems can't work well on JRuby,
42 | let me know if rest-graph is working on JRuby, thanks!
43 |
44 | * (must) pick one HTTP client:
45 | - gem install rest-client
46 | - gem install em-http-request
47 |
48 | * (optional) pick one JSON parser/generator:
49 | - gem install yajl-ruby
50 | - gem install json
51 | - gem install json_pure
52 |
53 | * (optional) parse access_token in HTTP_COOKIE
54 | - gem install rack
55 |
56 | * (optional) to use rest-graph/test_util
57 | - gem install rr
58 |
59 | ## INSTALLATION:
60 |
61 | gem install rest-graph
62 |
63 | Or if you want development version, put this in Gemfile:
64 |
65 | gem 'rest-graph', :git => 'git://github.com/godfat/rest-graph.git',
66 | :submodules => true
67 |
68 | Or as a Rails2 plugin:
69 |
70 | ./script/plugin install git://github.com/godfat/rest-graph.git
71 |
72 | ## QUICK START:
73 |
74 | require 'rest-graph'
75 | rg = RestGraph.new(:access_token => 'myaccesstokenfromfb')
76 | rg.get('me')
77 | rg.get('me/likes')
78 | rg.get('search', :q => 'taiwan')
79 |
80 | ### Obtaining an access token
81 |
82 | If you are using Rails, we recommend that you include a module called
83 | RestGraph::RailsUtil into your controllers. (Your code contributions
84 | for other Ruby frameworks would be appreciated!). RestGraph::RailsUtil
85 | adds the following two methods to your controllers:
86 |
87 | rest_graph_setup: Attempts to find an access_token from the environment
88 | and initializes a RestGraph object with it.
89 | Most commonly used inside a filter.
90 |
91 | rest_graph: Accesses the RestGraph object by rest_graph_setup.
92 |
93 | ### Example usage:
94 |
95 | class MyController < ActionController::Base
96 | include RestGraph::RailsUtil
97 | before_filter :setup
98 |
99 | def myaction
100 | @medata = rest_graph.get('me')
101 | end
102 |
103 | private
104 | def setup
105 | rest_graph_setup(:app_id => '123',
106 | :canvas => 'mycanvas',
107 | :auto_authorize_scope => 'email')
108 | # See below for more options
109 | end
110 | end
111 |
112 | ### Default setup
113 |
114 | New RestGraph objects can read their default setup configuration from a
115 | YAML configuration file. Which is the same as passing to rest_graph_setup.
116 |
117 | * [Example](test/config/rest-graph.yaml)
118 |
119 | To enable, just require anywhere:
120 |
121 | require 'rest-graph'
122 |
123 | Or if you're using bundler, add this line into Gemfile:
124 |
125 | gem 'rest-graph'
126 |
127 | ## SETUP OPTIONS:
128 |
129 | Here are ALL the available options for new instance of RestGraph.
130 |
131 | rg = RestGraph.new(
132 | :access_token => TOKEN , # default nil
133 | :graph_server => 'https://graph.facebook.com/', # this is default
134 | :old_server => 'https://api.facebook.com/' , # this is default
135 | :accept => 'text/javascript' , # this is default
136 | :lang => 'en-us' , # affect search
137 | :auto_decode => true , # decode by json
138 | # default true
139 | :app_id => '123' , # default nil
140 | :secret => '1829' , # default nil
141 |
142 | :cache => {} ,
143 | # A cache for the same API call. Any object quacks like a hash
144 | # should work, and Rails.cache works, too. (because of a patch in
145 | # RailsUtil)
146 |
147 | :error_handler => lambda{ |error, url| raise ::RestGraph::Error.parse(error, url) }
148 | # This handler callback is only called if auto_decode is
149 | # set to true, otherwise, it's ignored. And raising exception
150 | # is the default unless you're using RailsUtil and enabled
151 | # auto_authorize. That way, RailsUtil would do redirect
152 | # instead of raising an exception.
153 |
154 | :log_method => method(:puts),
155 | # This way, any log message would be output by puts. If you want to
156 | # change the log format, use log_handler instead. See below:
157 |
158 | :log_handler => lambda{ |event|
159 | Rails.logger.
160 | debug("Spent #{event.duration} requesting #{event.url}")})
161 | # You might not want to touch this if you're using RailsUtil.
162 | # Otherwise, the default behavior is do nothing. (i.e. no logging)
163 |
164 | And here are ALL the available options for rest_graph_setup. Note that all
165 | options for RestGraph instance are also valid options for rest_graph_setup.
166 |
167 | rest_graph_setup(#
168 | # == All the above RestGraph options, plus
169 | #
170 | :canvas => 'mycanvas', # default ''
171 | :auto_authorize => true , # default false
172 | :auto_authorize_scope => 'email' , # default ''
173 | :auto_authorize_options => {} , # default {}
174 | # auto_authorize means it will do redirect to oauth
175 | # API automatically if the access_token is invalid or
176 | # missing. So you would like to setup scope if you're
177 | # using it. Note that: setting scope implies setting
178 | # auto_authorize to true, even it's false.
179 |
180 | :ensure_authorized => false , # default false
181 | # This means if the access_token is not there,
182 | # then do auto_authorize.
183 |
184 | :write_session => true , # default false
185 | :write_cookies => false , # default false
186 | :write_handler =>
187 | lambda{ |fbs| @cache[uid] = fbs } , # default nil
188 | :check_handler =>
189 | lambda{ @cache[uid] }) # default nil
190 | # If we're not using Facebook JavaScript SDK,
191 | # then we'll need to find a way to store the fbs,
192 | # which contains access_token and/or user id. In a
193 | # standalone site or iframe canvas application, you might
194 | # want to just use the Rails (or other framework) session
195 |
196 | ### Alternate ways to setup RestGraph:
197 |
198 | 1. Set upon RestGraph object creation:
199 |
200 | rg = RestGraph.new :app_id => 1234
201 |
202 | 2. Set via the rest_graph_setup call in a Controller:
203 |
204 | rest_graph_setup :app_id => 1234
205 |
206 | 3. Load from a YAML file
207 |
208 | require 'rest-graph/config_util'
209 | RestGraph.load_config('path/to/rest-graph.yaml', 'production')
210 | rg = RestGraph.new
211 |
212 | 4. Load config automatically
213 |
214 | require 'rest-graph' # under Rails, would load config/rest-graph.yaml
215 | rg = RestGraph.new
216 |
217 | 5. Override directly
218 |
219 | module MyDefaults
220 | def default_app_id
221 | '456'
222 | end
223 |
224 | def default_secret
225 | 'category theory'
226 | end
227 | end
228 | RestGraph.send(:extend, MyDefaults)
229 | rg = RestGraph.new
230 |
231 | ## API REFERENCE:
232 |
233 | ### Facebook Graph API:
234 |
235 | #### get
236 | # GET https://graph.facebook.com/me?access_token=TOKEN
237 | rg.get('me')
238 |
239 | # GET https://graph.facebook.com/me?metadata=1&access_token=TOKEN
240 | rg.get('me', :metadata => '1')
241 |
242 | # extra options:
243 | # auto_decode: Bool # decode with json or not in this API request
244 | # # default: auto_decode in rest-graph instance
245 | # timeout: Int # the timeout for this API request
246 | # # default: timeout in rest-graph instance
247 | # secret: Bool # use secret_acccess_token or not
248 | # # default: false
249 | # cache: Bool # use cache or not; if it's false, update cache, too
250 | # # default: true
251 | # expires_in: Int # control when would the cache be expired
252 | # # default: nil
253 | # async: Bool # use eventmachine for http client or not
254 | # # default: false, but true in aget family
255 | # headers: Hash # additional hash you want to pass
256 | # # default: {}
257 | rg.get('me', {:metadata => '1'}, :secret => true, expires_in => 600)
258 |
259 | # When using eventmachine
260 | rg.get('me', {:metadata => '1'}, :async => true) do |result|
261 | # This block is called even on failure
262 | end
263 |
264 | #### post
265 |
266 | rg.post('me/feed', :message => 'bread!')
267 |
268 | #### fql
269 |
270 | Make an arbitrary [FQL][] query
271 |
272 | [FQL]: http://developers.facebook.com/docs/reference/fql/
273 |
274 | rg.fql('SELECT name FROM page WHERE page_id="123"')
275 |
276 | #### fql_multi
277 |
278 | rg.fql_multi(:q1 => 'SELECT name FROM page WHERE page_id="123"',
279 | :q2 => 'SELECT name FROM page WHERE page_id="456"')
280 |
281 | #### old_rest
282 |
283 | Call functionality from Facebook's old REST API:
284 |
285 | rg.old_rest(
286 | 'stream.publish',
287 | { :message => 'Greetings',
288 | :attachment => {:name => 'Wikipedia',
289 | :href => 'http://wikipedia.org/',
290 | :caption => 'Wikipedia says hi.',
291 | :media => [{:type => 'image',
292 | :src => 'http://wikipedia.org/logo.png',
293 | :href => 'http://wikipedia.org/'}]
294 | }.to_json,
295 | :action_links => [{:text => 'Go to Wikipedia',
296 | :href => 'http://wikipedia.org/'}
297 | ].to_json
298 | },
299 | :auto_decode => false) # You'll need to set auto_decode to false for
300 | # this API request if Facebook is not returning
301 | # a proper formatted JSON response. Otherwise,
302 | # this could be omitted.
303 |
304 | # Some Old Rest API requires a special access token with app secret
305 | # inside of it. For those methods, use secret_old_rest instead of the
306 | # usual old_rest with common access token.
307 | rg.secret_old_rest('admin.getAppProperties', :properties => 'app_id')
308 |
309 | ### Utility Methods:
310 |
311 | #### parse_???
312 |
313 | All the methods that obtain an access_token will automatically save it.
314 |
315 | If you have the session in the cookies,
316 | then RestGraph can parse the cookies:
317 |
318 | rg.parse_cookies!(cookies)
319 |
320 | If you're writing a Rack application, you might want to parse
321 | the session directly from Rack env:
322 |
323 | rg.parse_rack_env!(env)
324 |
325 | #### access_token
326 |
327 | rg.access_token
328 |
329 | Data associated with the access_token (which might or might not
330 | available, depending on how the access_token was obtained).
331 |
332 | rg.data
333 | rg.data['uid']
334 | rg.data['expires']
335 |
336 | #### Default values
337 |
338 | Read from the rest-graph.yaml file.
339 |
340 | RestGraph.default_???
341 |
342 | ### Other ways of getting an access token
343 |
344 | #### authorize_url
345 |
346 | Returns the redirect URL for authorizing
347 |
348 | # https://graph.facebook.com/oauth/authorize?
349 | # client_id=123&redirect_uri=http%3A%2F%2Fw3.org%2F
350 | rg.authorize_url(:redirect_uri => 'http://w3.org/', :scope => 'email')
351 |
352 | #### authorize!
353 |
354 | Makes a call to Facebook to convert
355 | the authorization "code" into an access token:
356 |
357 | # https://graph.facebook.com/oauth/access_token?
358 | # code=CODE&client_id=123&client_secret=1829&
359 | # redirect_uri=http%3A%2F%2Fw3.org%2F
360 | rg.authorize!(:redirect_uri => 'http://w3.org/', :code => 'CODE')
361 |
362 | #### exchange_sessions
363 |
364 | Takes a session key from the old REST API
365 | (non-Graph API) and converts to an access token:
366 |
367 | # https://graph.facebook.com/oauth/exchange_sessions?sessions=SESSION
368 | rg.exchange_sessions(:sessions => params[:fb_sig_session_key])
369 |
370 | ## CONTRIBUTORS:
371 |
372 | * Andrew Liu (@eggegg)
373 | * andy (@coopsite)
374 | * Barnabas Debreczeni (@keo)
375 | * Bruce Chu (@bruchu)
376 | * Ethan Czahor (@ethanz5)
377 | * Florent Vaucelle (@florent)
378 | * Jaime Cham (@jcham)
379 | * John Fan (@johnfan)
380 | * Lin Jen-Shin (@godfat)
381 | * Mariusz Pruszynski (@snicky)
382 | * Nicolas Fouché (@nfo)
383 | * topac (@topac)
384 | * Yutaro Sugai (@hokkai7go)
385 |
386 | ## LICENSE:
387 |
388 | Apache License 2.0
389 |
390 | Copyright (c) 2010-2014, Cardinal Blue
391 |
392 | Licensed under the Apache License, Version 2.0 (the "License");
393 | you may not use this file except in compliance with the License.
394 | You may obtain a copy of the License at
395 |
396 |
397 |
398 | Unless required by applicable law or agreed to in writing, software
399 | distributed under the License is distributed on an "AS IS" BASIS,
400 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
401 | See the License for the specific language governing permissions and
402 | limitations under the License.
403 |
--------------------------------------------------------------------------------
/lib/rest-graph/core.rb:
--------------------------------------------------------------------------------
1 |
2 | # optional http client
3 | begin; require 'restclient' ; rescue LoadError; end
4 | begin; gem 'em-http-request'
5 | require 'em-http-request'; rescue LoadError; end
6 |
7 | # optional gem
8 | begin; require 'rack' ; rescue LoadError; end
9 |
10 | # stdlib
11 | require 'digest/md5'
12 | require 'openssl'
13 |
14 | require 'cgi'
15 | require 'timeout'
16 |
17 | # the data structure used in RestGraph
18 | RestGraphStruct = Struct.new(:access_token,
19 | :auto_decode, :timeout,
20 | :graph_server, :old_server,
21 | :accept, :lang,
22 | :app_id, :secret,
23 | :data, :cache,
24 | :expires_in,
25 | :log_method,
26 | :log_handler,
27 | :error_handler) unless defined?(RestGraphStruct)
28 |
29 | class RestGraph < RestGraphStruct
30 | EventStruct = Struct.new(:duration, :url) unless
31 | defined?(::RestGraph::EventStruct)
32 |
33 | Attributes = RestGraphStruct.members.map(&:to_sym) unless
34 | defined?(::RestGraph::Attributes)
35 |
36 | class Event < EventStruct
37 | # self.class.name[/(?<=::)\w+$/] if RUBY_VERSION >= '1.9.2'
38 | def name; self.class.name[/::\w+$/].tr(':', ''); end
39 | def to_s; "RestGraph: spent #{sprintf('%f', duration)} #{name} #{url}";end
40 | end
41 | class Event::MultiDone < Event; end
42 | class Event::Requested < Event; end
43 | class Event::CacheHit < Event; end
44 | class Event::Failed < Event; end
45 |
46 | class Error < RuntimeError
47 | class AccessToken < Error; end
48 | class InvalidAccessToken < AccessToken; end
49 | class MissingAccessToken < AccessToken; end
50 |
51 | attr_reader :error, :url
52 | def initialize error, url=''
53 | @error, @url = error, url
54 | super("#{error.inspect} from #{url}")
55 | end
56 |
57 | module Util
58 | extend self
59 | def parse error, url=''
60 | return Error.new(error, url) unless error.kind_of?(Hash)
61 | if invalid_token?(error)
62 | InvalidAccessToken.new(error, url)
63 | elsif missing_token?(error)
64 | MissingAccessToken.new(error, url)
65 | else
66 | Error.new(error, url)
67 | end
68 | end
69 |
70 | def invalid_token? error
71 | (%w[OAuthInvalidTokenException
72 | OAuthException].include?((error['error'] || {})['type'])) ||
73 | (error['error_code'] == 190) # Invalid OAuth 2.0 Access Token
74 | end
75 |
76 | def missing_token? error
77 | (error['error'] || {})['message'] =~ /^An active access token/ ||
78 | (error['error_code'] == 104) # Requires valid signature
79 | end
80 | end
81 | extend Util
82 | end
83 |
84 | # honor default attributes
85 | Attributes.each{ |name|
86 | module_eval <<-RUBY
87 | def #{name}
88 | if (r = super).nil? then self.#{name} = self.class.default_#{name}
89 | else r end
90 | end
91 | RUBY
92 | }
93 |
94 | # setup defaults
95 | module DefaultAttributes
96 | extend self
97 | def default_access_token; nil ; end
98 | def default_auto_decode ; true ; end
99 | def default_strict ; false ; end
100 | def default_timeout ; 10 ; end
101 | def default_graph_server; 'https://graph.facebook.com/'; end
102 | def default_old_server ; 'https://api.facebook.com/' ; end
103 | def default_accept ; 'text/javascript' ; end
104 | def default_lang ; 'en-us' ; end
105 | def default_app_id ; nil ; end
106 | def default_secret ; nil ; end
107 | def default_data ; {} ; end
108 | def default_cache ; nil ; end
109 | def default_expires_in ; 600 ; end
110 | def default_log_method ; nil ; end
111 | def default_log_handler ; nil ; end
112 | def default_error_handler
113 | lambda{ |error, url| raise ::RestGraph::Error.parse(error, url) }
114 | end
115 | end
116 | extend DefaultAttributes
117 |
118 | # Fallback to ruby-hmac gem in case system openssl
119 | # lib doesn't support SHA256 (OSX 10.5)
120 | def self.hmac_sha256 key, data
121 | OpenSSL::HMAC.digest('sha256', key, data)
122 | rescue RuntimeError
123 | require 'hmac-sha2'
124 | HMAC::SHA256.digest(key, data)
125 | end
126 |
127 | # begin json backend adapter
128 | module YajlRuby
129 | def self.extended mod
130 | mod.const_set(:ParseError, Yajl::ParseError)
131 | end
132 | def json_encode hash
133 | Yajl::Encoder.encode(hash)
134 | end
135 | def json_decode json
136 | Yajl::Parser.parse(json)
137 | end
138 | end
139 |
140 | module Json
141 | def self.extended mod
142 | mod.const_set(:ParseError, JSON::ParserError)
143 | end
144 | def json_encode hash
145 | JSON.dump(hash)
146 | end
147 | def json_decode json
148 | JSON.parse(json)
149 | end
150 | end
151 |
152 | module Gsub
153 | class ParseError < RuntimeError; end
154 | def self.extended mod
155 | mod.const_set(:ParseError, Gsub::ParseError)
156 | end
157 | # only works for flat hash
158 | def json_encode hash
159 | middle = hash.inject([]){ |r, (k, v)|
160 | r << "\"#{k}\":\"#{v.gsub('"','\\"')}\""
161 | }.join(',')
162 | "{#{middle}}"
163 | end
164 | def json_decode json
165 | raise NotImplementedError.new(
166 | 'You need to install either yajl-ruby, json, or json_pure gem')
167 | end
168 | end
169 |
170 | def self.select_json! picked=false
171 | if defined?(::Yajl)
172 | extend YajlRuby
173 | elsif defined?(::JSON)
174 | extend Json
175 | elsif picked
176 | extend Gsub
177 | else
178 | # pick a json gem if available
179 | %w[yajl json].each{ |json|
180 | begin
181 | require json
182 | break
183 | rescue LoadError
184 | end
185 | }
186 | select_json!(true)
187 | end
188 | end
189 | select_json! unless respond_to?(:json_decode)
190 | # end json backend adapter
191 |
192 |
193 |
194 |
195 |
196 | # common methods
197 |
198 | def initialize o={}
199 | o.each{ |key, value| send("#{key}=", value) if respond_to?("#{key}=") }
200 | end
201 |
202 | def access_token
203 | data['access_token'] || data['oauth_token']
204 | end
205 |
206 | def access_token= token
207 | data['access_token'] = token
208 | end
209 |
210 | def authorized?
211 | !!access_token
212 | end
213 |
214 | def secret_access_token
215 | "#{app_id}|#{secret}"
216 | end
217 |
218 | def lighten! o={}
219 | [:cache, :log_method, :log_handler, :error_handler].each{ |obj|
220 | send("#{obj}=", false) }
221 | send(:initialize, o)
222 | self
223 | end
224 |
225 | def lighten o={}
226 | dup.lighten!(o)
227 | end
228 |
229 | def inspect
230 | "#"
232 | end
233 |
234 | def attributes
235 | Hash[each_pair.map{ |k, v| [k, send(k)] }]
236 | end
237 |
238 |
239 |
240 |
241 | # graph api related methods
242 |
243 | def url path, query={}, server=graph_server, opts={}
244 | "#{server}#{path}#{build_query_string(query, opts)}"
245 | end
246 |
247 | # extra options:
248 | # auto_decode: Bool # decode with json or not in this API request
249 | # # default: auto_decode in rest-graph instance
250 | # timeout: Int # the timeout for this API request
251 | # # default: timeout in rest-graph instance
252 | # secret: Bool # use secret_acccess_token or not
253 | # # default: false
254 | # cache: Bool # use cache or not; if it's false, update cache, too
255 | # # default: true
256 | # expires_in: Int # control when would the cache be expired
257 | # # default: nil
258 | # async: Bool # use eventmachine for http client or not
259 | # # default: false, but true in aget family
260 | # headers: Hash # additional hash you want to pass
261 | # # default: {}
262 | def get path, query={}, opts={}, &cb
263 | request(opts, [:get , url(path, query, graph_server, opts)], &cb)
264 | end
265 |
266 | def delete path, query={}, opts={}, &cb
267 | request(opts, [:delete, url(path, query, graph_server, opts)], &cb)
268 | end
269 |
270 | def post path, payload={}, query={}, opts={}, &cb
271 | request(opts, [:post , url(path, query, graph_server, opts), payload],
272 | &cb)
273 | end
274 |
275 | def put path, payload={}, query={}, opts={}, &cb
276 | request(opts, [:put , url(path, query, graph_server, opts), payload],
277 | &cb)
278 | end
279 |
280 | # request by eventmachine (em-http)
281 |
282 | def aget path, query={}, opts={}, &cb
283 | get(path, query, {:async => true}.merge(opts), &cb)
284 | end
285 |
286 | def adelete path, query={}, opts={}, &cb
287 | delete(path, query, {:async => true}.merge(opts), &cb)
288 | end
289 |
290 | def apost path, payload={}, query={}, opts={}, &cb
291 | post(path, payload, query, {:async => true}.merge(opts), &cb)
292 | end
293 |
294 | def aput path, payload={}, query={}, opts={}, &cb
295 | put(path, payload, query, {:async => true}.merge(opts), &cb)
296 | end
297 |
298 | def multi reqs, opts={}, &cb
299 | request({:async => true}.merge(opts),
300 | *reqs.map{ |(meth, path, query, payload)|
301 | [meth, url(path, query || {}, graph_server, opts), payload]
302 | }, &cb)
303 | end
304 |
305 |
306 |
307 |
308 |
309 | def next_page hash, opts={}, &cb
310 | if hash['paging'].kind_of?(Hash) && hash['paging']['next']
311 | request(opts, [:get, URI.encode(hash['paging']['next'])], &cb)
312 | else
313 | yield(nil) if block_given?
314 | end
315 | end
316 |
317 | def prev_page hash, opts={}, &cb
318 | if hash['paging'].kind_of?(Hash) && hash['paging']['previous']
319 | request(opts, [:get, URI.encode(hash['paging']['previous'])], &cb)
320 | else
321 | yield(nil) if block_given?
322 | end
323 | end
324 | alias_method :previous_page, :prev_page
325 |
326 | def for_pages hash, pages=1, opts={}, kind=:next_page, &cb
327 | if pages > 1
328 | merge_data(send(kind, hash, opts){ |result|
329 | yield(result.freeze) if block_given?
330 | for_pages(result, pages - 1, opts, kind, &cb) if result
331 | }, hash)
332 | else
333 | yield(nil) if block_given?
334 | hash
335 | end
336 | end
337 |
338 |
339 |
340 |
341 |
342 | # cookies, app_id, secrect related below
343 |
344 | def parse_rack_env! env
345 | env['HTTP_COOKIE'].to_s =~ /fbs_#{app_id}=([^\;]+)/
346 | self.data = parse_fbs!($1)
347 | end
348 |
349 | def parse_cookies! cookies
350 | self.data = if fbsr = cookies["fbsr_#{app_id}"]
351 | parse_fbsr!(fbsr)
352 | else fbs = cookies["fbs_#{app_id}"]
353 | parse_fbs!(fbs)
354 | end
355 | end
356 |
357 | def parse_fbs! fbs
358 | self.data = check_sig_and_return_data(
359 | # take out facebook sometimes there but sometimes not quotes in cookies
360 | Rack::Utils.parse_query(fbs.to_s.sub(/^"/, '').sub(/"$/, '')))
361 | end
362 |
363 | def parse_fbsr! fbsr
364 | old_data = parse_signed_request!(fbsr)
365 | # beware! maybe facebook would take out the code someday
366 | return self.data = old_data unless old_data && old_data['code']
367 | # passing empty redirect_uri is needed!
368 | authorize!(:code => old_data['code'], :redirect_uri => '')
369 | self.data = old_data.merge(data)
370 | end
371 |
372 | def parse_json! json
373 | self.data = json &&
374 | check_sig_and_return_data(self.class.json_decode(json))
375 | rescue ParseError
376 | self.data = nil
377 | end
378 |
379 | def fbs
380 | "#{fbs_without_sig(data).join('&')}&sig=#{calculate_sig(data)}"
381 | end
382 |
383 | # facebook's new signed_request...
384 |
385 | def parse_signed_request! request
386 | sig_encoded, json_encoded = request.split('.')
387 | return self.data = nil unless sig_encoded && json_encoded
388 | sig, json = [sig_encoded, json_encoded].map{ |str|
389 | "#{str.tr('-_', '+/')}==".unpack('m').first
390 | }
391 | self.data = check_sig_and_return_data(
392 | self.class.json_decode(json).merge('sig' => sig)){
393 | self.class.hmac_sha256(secret, json_encoded)
394 | }
395 | rescue ParseError
396 | self.data = nil
397 | end
398 |
399 |
400 |
401 |
402 |
403 | # oauth related
404 |
405 | def authorize_url opts={}
406 | query = {:client_id => app_id, :access_token => nil}.merge(opts)
407 | "#{graph_server}oauth/authorize#{build_query_string(query)}"
408 | end
409 |
410 | def authorize! opts={}
411 | payload = {:client_id => app_id, :client_secret => secret}.merge(opts)
412 | self.data = Rack::Utils.parse_query(
413 | request({:auto_decode => false}.merge(opts),
414 | [:post, url('oauth/access_token'), payload]))
415 | end
416 |
417 |
418 |
419 |
420 |
421 | # old rest facebook api, i will definitely love to remove them someday
422 |
423 | def old_rest path, query={}, opts={}, &cb
424 | uri = url("method/#{path}", {:format => 'json'}.merge(query),
425 | old_server, opts)
426 | if opts[:post]
427 | request(
428 | opts.merge(:uri => uri),
429 | [:post,
430 | url("method/#{path}", {:format => 'json'}, old_server, opts),
431 | query],
432 | &cb)
433 | else
434 | request(opts, [:get, uri], &cb)
435 | end
436 | end
437 |
438 | def secret_old_rest path, query={}, opts={}, &cb
439 | old_rest(path, query, {:secret => true}.merge(opts), &cb)
440 | end
441 |
442 | def fql code, query={}, opts={}, &cb
443 | old_rest('fql.query', {:query => code}.merge(query), opts, &cb)
444 | end
445 |
446 | def fql_multi codes, query={}, opts={}, &cb
447 | old_rest('fql.multiquery',
448 | {:queries => self.class.json_encode(codes)}.merge(query), opts, &cb)
449 | end
450 |
451 | def exchange_sessions query={}, opts={}, &cb
452 | q = {:client_id => app_id, :client_secret => secret,
453 | :type => 'client_cred'}.merge(query)
454 | request(opts, [:post, url('oauth/exchange_sessions', q)], &cb)
455 | end
456 |
457 |
458 |
459 |
460 |
461 | def request opts, *reqs, &cb
462 | Timeout.timeout(opts[:timeout] || timeout){
463 | reqs.each{ |(meth, uri, payload)|
464 | next if meth != :get # only get result would get cached
465 | cache_assign(opts, uri, nil)
466 | } if opts[:cache] == false # remove cache if we don't want it
467 |
468 | if opts[:async]
469 | request_em(opts, reqs, &cb)
470 | else
471 | request_rc(opts, *reqs.first, &cb)
472 | end
473 | }
474 | end
475 |
476 | protected
477 | def request_em opts, reqs
478 | start_time = Time.now
479 | rs = reqs.map{ |(meth, uri, payload)|
480 | r = EM::HttpRequest.new(uri).send(meth, :body => payload,
481 | :head => build_headers(opts))
482 | if cached = cache_get(opts, uri)
483 | # TODO: this is hack!!
484 | r.instance_variable_set('@response', cached)
485 | r.instance_variable_set('@state' , :finish)
486 | r.on_request_complete
487 | r.succeed(r)
488 | else
489 | r.callback{
490 | cache_for(opts, uri, meth, r.response)
491 | log(Event::Requested.new(Time.now - start_time, uri))
492 | }
493 | r.error{
494 | log(Event::Failed.new(Time.now - start_time, uri))
495 | }
496 | end
497 | r
498 | }
499 |
500 | m = EM::MultiRequest.new
501 | rs.each.with_index{ |r, i| m.add(i, r) }
502 | m.callback{
503 | # TODO: how to deal with the failed?
504 | clients = m.responses[:callback].sort.map(&:last)
505 | results = clients.map{ |client|
506 | post_request(opts, client.last_effective_url, client.response)
507 | }
508 |
509 | if reqs.size == 1
510 | yield(results.first)
511 | else
512 | log(Event::MultiDone.new(Time.now - start_time,
513 | clients.map(&:last_effective_url).join(', ')))
514 | yield(results)
515 | end
516 | }
517 | end
518 |
519 | def request_rc opts, meth, uri, payload=nil, &cb
520 | start_time = Time.now
521 | post_request(opts, uri,
522 | cache_get(opts, uri) || fetch(opts, uri, meth, payload),
523 | &cb)
524 | rescue RestClient::Exception => e
525 | post_request(opts, uri, e.http_body, &cb)
526 | ensure
527 | log(Event::Requested.new(Time.now - start_time, uri))
528 | end
529 |
530 | def build_query_string query={}, opts={}
531 | token = opts[:secret] ? secret_access_token : access_token
532 | qq = token ? {:access_token => token}.merge(query) : query
533 | q = qq.select{ |k, v| v } # compacting the hash
534 | return '' if q.empty?
535 | return '?' + q.map{ |(k, v)| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
536 | end
537 |
538 | def build_headers opts={}
539 | headers = {}
540 | headers['Accept'] = accept if accept
541 | headers['Accept-Language'] = lang if lang
542 | headers.merge(opts[:headers] || {})
543 | end
544 |
545 | def post_request opts, uri, result, &cb
546 | if decode?(opts)
547 | # [this].first is not needed for yajl-ruby
548 | decoded = self.class.json_decode("[#{result}]").first
549 | check_error(opts, uri, decoded, &cb)
550 | else
551 | block_given? ? yield(result) : result
552 | end
553 | rescue ParseError => error
554 | error_handler.call(error, uri) if error_handler
555 | end
556 |
557 | def decode? opts
558 | if opts.has_key?(:auto_decode)
559 | opts[:auto_decode]
560 | else
561 | auto_decode
562 | end
563 | end
564 |
565 | def check_sig_and_return_data cookies
566 | cookies if secret && if block_given?
567 | yield
568 | else
569 | calculate_sig(cookies)
570 | end == cookies['sig']
571 | end
572 |
573 | def check_error opts, uri, hash
574 | if error_handler && hash.kind_of?(Hash) &&
575 | (hash['error'] || # from graph api
576 | hash['error_code']) # from fql
577 | cache_assign(opts, uri, nil)
578 | error_handler.call(hash, uri)
579 | else
580 | block_given? ? yield(hash) : hash
581 | end
582 | end
583 |
584 | def calculate_sig cookies
585 | Digest::MD5.hexdigest(fbs_without_sig(cookies).join + secret)
586 | end
587 |
588 | def fbs_without_sig cookies
589 | cookies.reject{ |(k, v)| k == 'sig' }.sort.map{ |a| a.join('=') }
590 | end
591 |
592 | def cache_key opts, uri
593 | Digest::MD5.hexdigest(opts[:uri] || uri)
594 | end
595 |
596 | def cache_assign opts, uri, value
597 | return unless cache
598 | cache[cache_key(opts, uri)] = value
599 | end
600 |
601 | def cache_get opts, uri
602 | return unless cache
603 | start_time = Time.now
604 | cache[cache_key(opts, uri)].tap{ |result|
605 | log(Event::CacheHit.new(Time.now - start_time, uri)) if result
606 | }
607 | end
608 |
609 | def cache_for opts, uri, meth, value
610 | return unless cache
611 | # fake post (opts[:post] => true) is considered get and need cache
612 | return if meth != :get unless opts[:post]
613 |
614 | expires = opts[:expires_in] || expires_in
615 | if expires.kind_of?(Fixnum) && cache.method(:store).arity == -3
616 | cache.store(cache_key(opts, uri), value,
617 | :expires_in => expires)
618 | else
619 | cache_assign(opts, uri, value)
620 | end
621 | end
622 |
623 | def fetch opts, uri, meth, payload
624 | RestClient::Request.execute(:method => meth, :url => uri,
625 | :headers => build_headers(opts),
626 | :payload => payload).body.
627 | tap{ |result| cache_for(opts, uri, meth, result) }
628 | end
629 |
630 | def merge_data lhs, rhs
631 | [lhs, rhs].each{ |hash|
632 | return rhs.reject{ |k, v| k == 'paging' } if
633 | !hash.kind_of?(Hash) || !hash['data'].kind_of?(Array)
634 | }
635 | lhs['data'].unshift(*rhs['data'])
636 | lhs
637 | end
638 |
639 | def log event
640 | log_handler.call(event) if log_handler
641 | log_method .call("DEBUG: #{event}") if log_method
642 | end
643 | end
644 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | # CHANGES
2 |
3 | ## rest-graph 2.0.3 -- 2013-04-26
4 |
5 | * [RestGraph::RailsUtil] Adopt latest changes from rest-more. See:
6 | https://github.com/godfat/rest-more/commit/101e4b25f11e4cf3713fa2f1d6b5a46982266e44
7 | https://github.com/godfat/rest-more/commit/0e7d4db588ecc287fd8b2840c880dfb3fe3c3096
8 |
9 | ## rest-graph 2.0.2 -- 2012-07-13
10 |
11 | * [RestGraph] Adopt latest em-http-request.
12 |
13 | ### Bugs fixes back ported from [rest-more][]
14 |
15 | * [RestGraph::RailsUtil] Change the redirect log level from WARN to INFO.
16 | * [RestGraph::RailsUtil] Since Facebook would return correct URL now,
17 | we don't have to try to use `URI.encode` anymore. Actually, that
18 | causes bugs.
19 |
20 | Please upgrade to [rest-more][].
21 |
22 | ## rest-graph 2.0.1 -- 2011-11-25
23 |
24 | ### Bugs fixes back ported from [rest-more][]
25 |
26 | * [RestGraph] Now we're using POST in `authorize!` to exchange the
27 | access_token with the code instead of GET. If we're using GET,
28 | we would run into a risk where a user might use the code to
29 | get other people's access_token via the cache. Using POST would
30 | prevent this because POSTs are not cached.
31 |
32 | * [RestGraph::RailsUtil] Fixed a serious bug. The bug would jump up if
33 | you're using :write_session or :write_cookies or :write_handler along
34 | with :auto_authorize, for example:
35 | `rest_graph_setup(:auto_authorize => true, :write_session => true)`
36 | The problem is that RestGraph::RailsUtil is not removing the invalid
37 | access_token stored in session or cookie, and yet it is considered
38 | authorized, making redirecting to Facebook and redirecting back doesn't
39 | update the access_token. `rest_graph_cleanup` is introduced to remove
40 | all invalid access_tokens, which would get called once the user is
41 | redirected to Facebook, fixing this bug.
42 |
43 | [rest-more]: https://github.com/godfat/rest-more
44 |
45 | ## rest-graph 2.0.0 -- 2011-10-08
46 |
47 | We have moved the development from rest-graph to [rest-core][].
48 | By now on, we would only fix bugs in rest-graph rather than adding
49 | features, and we would only backport important changes from rest-core
50 | once in a period. If you want the latest goodies, please see [rest-core][]
51 | Otherwise, you can stay with rest-graph with bugs fixes.
52 |
53 | [rest-core]: https://github.com/godfat/rest-core
54 |
55 | * [RestGraph] Added `RestGraph#parse_fbsr!` which can parse Facebook's new
56 | cookie. Also, `RestGraph#parse_cookies!` would look for that too.
57 | * [RestGraph] Added `expires_in` attribute which would be passed to cache.
58 | The default value is 600 seconds. No effect if there's no cache, or if
59 | the cache didn't support `:expires_in` option.
60 | * [RestGraph] Now `RestGraph#initialize` accepts string keys.
61 | * [RestGraph] We don't support em-http-request >= 1 at the moment.
62 | * [RestGraph] Fixed that parsing an invalid signed_request would raise an
63 | error. From now on it would simply ignore it and wipe out the data.
64 |
65 | * [RailsUtil] Now by default, RestGraph would cache all GET requests in
66 | `Rails.cache` for 600 seconds. You can change this by running:
67 |
68 | rest_graph_setup(:cache => nil, :expires_in => 3600)
69 |
70 | To disable cache or lengthen/shorten the lifetime of the cache result.
71 |
72 | ## rest-graph 1.9.1 -- 2011-06-07
73 |
74 | * [RestGraph] Fixed parse_fbs! where fbs contains some json. (thanks Bruce)
75 |
76 | ## rest-graph 1.9.0 -- 2011-05-26
77 |
78 | * [RestGraph] Removed deprecated rest-graph/auto_load.rb, and
79 | rest-graph/autoload.rb. Please simply require 'rest-graph'
80 |
81 | * [RestGraph] Removed deprecated strict option. Facebook had fixed their bug.
82 |
83 | * [RestGraph] Removed deprecated broken_old_rest. Please use old_rest instead.
84 |
85 | * [RestGraph] Removed deprecated suppress_decode. Plz use auto_decode instead.
86 |
87 | * [RestGraph] Introduced RestGraph#attributes.
88 |
89 | * [RestGraph] RestGraph#lighten and RestGraph#lighten! now takes an extra
90 | argument which let you set attributes.
91 |
92 | * [RestGraph] Now RestGraph#get/post has an option for timeout which
93 | make this request use this timeout instead of the default one.
94 |
95 | * [RestGraph] URI.encode on Facebook's broken paging URL. (thanks andy)
96 |
97 | * [RestGraph] Now all old_rest based methods accept a :post option, which
98 | means use POST for this API request, but still treating it
99 | a GET request for rest-graph, so that caching and other
100 | similar features would still work.
101 |
102 | The reason for this option is that some FQL could be very long,
103 | FQL multiquery could be even much more longer! Long URLs might
104 | cause problems, thus we have provided this option to use POST
105 | instead.
106 |
107 | ## rest-graph 1.8.0 -- 2011-03-08
108 |
109 | * [RestGraph] Now require 'rest-graph/autoload' is deprecated, simply use
110 | require 'rest-graph' would require anything you "might" or
111 | might not want. Use require 'rest-graph/core' for core
112 | functionality instead.
113 |
114 | * [RestGraph] Now RestGraph#get/post has two extra cache options, one is
115 | `expires_in', to indicate how long does this result should be
116 | cached. Another one is `cache', if passing false, it means
117 | the cache should be updated with this request, no matter it's
118 | cached or not before.
119 |
120 | * [ConfigUtil] LoadConfig is renamed to ConfigUtil, and autoload is removed.
121 |
122 | * [ConfigUtil] ConfigUtil is extended into RestGraph, so RestGraph.load_config
123 | is equivalent to ConfigUtil.load_config.
124 |
125 | * [RailsUtil] Now FBML canvas is no longer supported. Setting iframe in
126 | rest-graph.yaml or pass to rest_graph_setup(:iframe => true)
127 | would be a no-op. Setting :canvas => 'name' should imply we're
128 | using iframe. This make it less trouble to find how to do the
129 | redirect correctly.
130 |
131 | * [RailsUtil] Now it should work better with Rails3. Previously, the load
132 | order problem would make rest-graph.yaml is not auto-picked.
133 | For now we're using Railtie initializer to make it load better.
134 | Rails2 should not be affected with this change.
135 |
136 | * [RailsUtil] Now rest-graph related methods are all private or protected,
137 | this would avoid them being treated as Rails actions.
138 |
139 | * [RailsUtil] Fixed a bug that calling rest_graph_setup didn't update options
140 | for rest_graph. This is fixed by reinitialize rest_graph in
141 | rest_graph_setup.
142 |
143 | * [RailsUtil] You can use rest_graph_js_redirect to do full page redirect
144 | in canvas iframe.
145 |
146 | * [TestUtil] Fixed stub format for method/fql.multiquery. Facebook has weird
147 | and inconsistent format for different API.
148 |
149 | * [FacebookUtil] Added some very Facebook specific utilities.
150 |
151 | ## rest-graph 1.7.0 -- 2010-12-30
152 |
153 | * [RestGraph] Renamed rest-graph/auto_load to rest-graph/autoload; auto_load
154 | would be still working for a while.
155 |
156 | * [RestGraph] Renamed suppress_decode to auto_decode, and reverse semantics.
157 | suppress_decode would still be working for a while.
158 |
159 | * [RestGraph] Now for every API call, for example, get/post/fql/url etc,
160 | you could pass a :secret => true option to indicate that
161 | for this API call, use secret_access_token instead of the
162 | normal one.
163 |
164 | * [RestGraph] Removed RestGraph#strict option. That is, removed the hack on
165 | Facebook's response. It's causing problems...
166 |
167 | * [RailsUtil] Don't check cookies/session if write_cookies/write_session is
168 | false. That is avoid accidentally checking unwanted fbs.
169 |
170 | * [RailsUtil] Extract rest_graph_filter_uri, which would filter session,
171 | code, singed_request, etc. on the redirecting URI.
172 |
173 | ## rest-graph 1.6.0 -- 2010-11-19
174 |
175 | * [RestGraph] Added em-http as an alternate for HTTP client. So rest-client
176 | is no longer a must-have dependency. Pass :async => true to
177 | RestGraph#get/delete/post/put will pick em-http to do the
178 | request instead of rest-client. Callback would be the block
179 | passed to that particular method. There are short hands for
180 | this, too: use RestGraph#aget/adelete/apost/aput.
181 |
182 | Additionally, callback style also works for rest-client. e.g.
183 |
184 | rg. get('me') # rest-client
185 | rg. get('me', {}, :async => true){ |result| } # em-http
186 | rg.aget('me') { |result| } # em-http
187 | rg. get('me') { |result| } # rest-client
188 |
189 | So you can always use callback style to ease porting from
190 | blocking mode (synchronized) to non-blocking mode (evented).
191 |
192 | * [RestGraph] Introduced RestGraph#multi to do multiple API call concurrently.
193 | To use this feature, you'll need to run RestGraph inside an
194 | EventMachine event loop and install em-http. (And you may want
195 | async-rack if you're doing Rack application too.)
196 |
197 | * [RestGraph] Added a timeout option, which makes rest-graph raise a
198 | Timeout::Error when any of the API call exceeds the timeout
199 | limit. This might have some issues in Rubinius 1.1, I'm not
200 | sure why.
201 |
202 | * [RestGraph] Added a log_method option, which would be used for default
203 | logging template, which is extracted from RailsUtil. So the
204 | log_method in RailsUtil would be `logger.method(:debug)',
205 | while in a terminal, you might want `method(:puts)'.
206 |
207 | Previously, you can only use log_handler for logging, which
208 | you need to write your custom logging template. Now for common
209 | logging, using log_method is enough. On the other hand,
210 | log_handler could be used for event handling.
211 |
212 | * [RestGraph] Introduced Event::MultiDone and Event::Failed. Those event
213 | objects would be passed to log_handler and/or log_method.
214 | The former means a multi-request is done, the latter means
215 | a request is failed.
216 |
217 | * [TestUtil] A collection of tools integrated with RR to ease the pain of
218 | testing. There are 3 levels of tools to stub the result of
219 | calling APIs. The highest level is TestUtil.login(1234) which
220 | would stub a number of results to pretend the user 1234 is
221 | logged-in.
222 |
223 | The second level are the get/post/put/delete methods for
224 | TestUtil. For example, to make rg.get('1234') return a
225 | particular value (such as a hash {'a' => 1}), use
226 | TestUtil.get('1234'){ {'a' => 1} } to set it up to return
227 | the specified value (typically a hash).
228 |
229 | The third level is for setting default_data and default_response
230 | for TestUtil. The default_data is the default value for rg.data,
231 | which includes the access_token and the user_id (uid). The
232 | default_response is the response given by any RestGraph API call
233 | (e.g. get, post) when no explicit response has been defined in
234 | the second level.
235 |
236 | To use TestUtil, remember to install RR (gem install rr) and
237 | require 'rest-graph/test_util'. Then put
238 | RestGraph::TestUtil.setup before any test case starts, and put
239 | RestGraph::TestUtil.teardown after any test case ends. Setup
240 | would stub default_data and default_response for you, and
241 | teardown would remove any stubs on RestGraph. For Rails, you
242 | might want to put these in test_helper.rb under "setup" and
243 | "teardown" block, just as the name suggested. For bacon or
244 | rspec style testing, these can be placed in the "before" and
245 | "after" blocks.
246 |
247 | In addition, you can get the API calls history via
248 | RestGraph::TestUtil.history. This would get cleaned up in
249 | RestGraph::TestUtil.teardown as well.
250 |
251 | ## rest-graph 1.5.0 -- 2010-10-11
252 |
253 | * [RestGraph] Make sure RestGraph::Error#message is string, that way,
254 | irb could print out error message correctly. Introduced
255 | RestGraph::Error#error for original error hash. Thanks Bluce.
256 |
257 | * [RestGraph] Make RestGraph#inspect honor default attributes, see:
258 | http://groups.google.com/group/rest-graph/browse_thread/thread/7ad5c81fbb0334e8
259 |
260 | * [RestGraph] Introduced RestGraph::Error::AccessToken,
261 | RestGraph::Error::InvalidAccessToken,
262 | RestGraph::Error::MissingAccessToken.
263 | RestGraph::Error::AccessToken is the parent of the others,
264 | and RestGraph::Error is the parent of all above.
265 |
266 | * [RestGraph] Add RestGraph#next_page and RestGraph#prev_page.
267 | To get next page for a result from Facebook, example:
268 |
269 | rg.next_page(rg.get('me/feed'))
270 |
271 | * [RestGraph] Add RestGraph#for_pages that would crawl from page 1 to
272 | a number of pages you specified. For example, this might
273 | crawl down all the feeds:
274 |
275 | rg.for_pages(rg.get('me/feed'), 1000)
276 |
277 | * [RestGraph] Added RestGraph#secret_old_rest, see:
278 | http://www.nivas.hr/blog/2010/09/03/facebook-php-sdk-access-token-signing-bug/
279 |
280 | If you're getting this error from calling old_rest:
281 |
282 | The method you are calling or the FQL table you are querying cannot be
283 | called using a session secret or by a desktop application.
284 |
285 | Then try secret_old_rest instead. The problem is that the access_token
286 | should be formatted by "#{app_id}|#{secret}" instead of the usual one.
287 |
288 | * [RestGraph] Added RestGraph#strict.
289 | In some API, e.g. admin.getAppProperties, Facebook returns
290 | broken JSON, which is double encoded, and is not a well-formed
291 | JSON. That case, we'll need to double parse the JSON to get
292 | the correct result. For example, Facebook might return this:
293 |
294 | "{\"app_id\":\"123\"}"
295 |
296 | instead of the correct one:
297 |
298 | {"app_id":"123"}
299 |
300 | P.S. This doesn't matter for people who don't use :auto_decode.
301 | So under non-strict mode, which is the default, rest-graph
302 | would handle this for you.
303 |
304 | * [RestGraph] Fallback to ruby-hmac gem if we have an old openssl lib when
305 | parsing signed_request which requires HMAC SHA256.
306 | (e.g. Mac OS 10.5) Thanks Barnabas Debreczeni!
307 |
308 | * [RailsUtil] Only rescue RestGraph::Error::AccessToken in controller,
309 | make other errors raise through.
310 |
311 | * [RailsUtil] Remove bad fbs in cookies when doing new authorization.
312 | * [RailsUtil] Make signed_request and session higher priority than
313 | fbs in cookies. Since Facebook is not deleting bad fbs,
314 | I guess? Thanks Barnabas Debreczeni!
315 |
316 | * [RailsUtil] URI.encode before URI.parse for broken URL Facebook passed.
317 |
318 | ## rest-graph 1.4.6 -- 2010-09-01
319 |
320 | * [RestGraph] Now it will try to pick yajl-ruby or json gem from memory first,
321 | if it's not there, then try to load one and try to pick one
322 | again. This way, it won't force you to load two gems at the
323 | same time if you've installed them both. In addition, there's
324 | a bug in yajl/json_gem pointed out at:
325 | http://github.com/brianmario/yajl-ruby/issues/31
326 | So we're using Nicolas' patch to use yajl directly to workaround
327 | this issue when we've chosen yajl-ruby json backend.
328 |
329 | * [RestGraph] Only cache GET request, don't cache POST/PUT/DELETE
330 |
331 | * [RestGrahp] Add RestGraph#lighten and RestGraph#lighten! to remove any
332 | handler and cache object to make it serializable.
333 |
334 | * [RailsUtil] Add ensure_authorized option which enforces the user has
335 | authorized to the application.
336 |
337 | * [RailsUtil] Unified rest_graph_storage_key, which used in cookies/session
338 | storage, and the key would depend on app_id, just like Facebook
339 | JavaScript SDK which use fbs_[app_id] as the name of cookie.
340 | This way, you are able to run different applications with
341 | different permissions in one Rails application.
342 |
343 | * [RailsUtil] Now rest_graph_authorize defaults to do redirect.
344 | Previously, you'll need to use:
345 | `rest_graph_authorize(message, true)`
346 | Now it's:
347 | `rest_graph_authorize(message)`
348 |
349 | ## rest-graph 1.4.5 -- 2010-08-07
350 |
351 | * [RestGraph] Treat oauth_token as access_token as well. This came from
352 | Facebook's new signed_request. Why didn't they choose
353 | consistent name? Why different signature algorithm?
354 |
355 | * [RailsUtil] Fixed a bug that didn't reject signed_request in redirect_uri.
356 | Now code, session, and signed_request are rejected.
357 |
358 | * [RailsUtil] Added write_handler and check_handler option to write/check
359 | fbs with user code, instead of using sessions/cookies.
360 | That way, you can save fbs into memcache or somewhere.
361 |
362 | ## rest-graph 1.4.4 -- 2010-08-06
363 |
364 | * [RailsUtil] Fixed a bug that empty query appends a question mark,
365 | that confuses Facebook, so that redirect_uri didn't match.
366 |
367 | ## rest-graph 1.4.3 -- 2010-08-06
368 |
369 | * [RestGraph] Fixed a bug in RestGraph#fbs, which didn't join '&'.
370 | Thanks, Andrew.
371 |
372 | * [RailsUtil] Fixed a bug that wrongly rewrites request URI.
373 | Previously it is processed by regular expressions,
374 | now we're using URI.parse to handle this. Closed #4.
375 | Thanks, Justin.
376 |
377 | * [RailsUtil] Favor Request#fullpath over Request#request_uri,
378 | which came from newer Rack and thus for Rails 3.
379 |
380 | ## rest-graph 1.4.2 -- 2010-08-05
381 |
382 | * [RestGraph] Added RestGraph#fbs to generate fbs with correct sig,
383 | to be used for future parse_fbs! See the bug in RailsUtil.
384 |
385 | * [RailsUtil] Added iframe and write_cookies option.
386 | * [RailsUtil] Fixed a bug that write_session didn't parse because parse_fbs!
387 | reject the fbs due to missing sig.
388 | * [RailsUtil] Fixed a bug that in Rails 3, must call safe_html to prevent
389 | unintended HTML escaping. Thanks, Justin.
390 |
391 | * Thanks a lot, Andrew.
392 |
393 | ## rest-graph 1.4.1 -- 2010-08-04
394 |
395 | * [RestGraph] Call error_handler when response contains error_code as well,
396 | which came from FQL response. Thanks Florent.
397 |
398 | * [RestGraph] Added RestGraph#parse_signed_request!
399 |
400 | * [RestGraph] Added RestGraph#url to generate desired API request URL,
401 | in case you'll want to use different HTTP client, such as em-http-request,
402 | or pass the API request to different process of data fetcher.
403 |
404 | * [RestGraph] Added an :cache option that allow you to pass a cache
405 | object, which should respond to [] and []= for reading and writing.
406 | The cache key would be MD5 hexdigest from the URL being called.
407 | pass :cache => Rails.cache to rest_graph_setup when using RailsUtil.
408 |
409 | * [RailsUtil] Pass :cache => Rails.cache to rest_graph_setup to enable caching.
410 | * [RailsUtil] Favor signed_request over session in rest_graph_setup
411 | * [RailsUtil] Now it's possible to setup all options in rest-graph.yaml.
412 |
413 | ## rest-graph 1.4.0 -- 2010-07-15
414 |
415 | Changes only for RailsUtil, the core (rest-graph.rb) is pretty stable for now.
416 |
417 | * Internal code rearrangement.
418 | * Removed url_for helper, it's too hard to do it right.
419 | * Removed @fb_sig_in_canvas hack.
420 | * Added rest_graph method in helper.
421 | * Fixed a bug that logging redirect but not really do direct.
422 | * Now passing :auto_authorize_scope implies :auto_authorize => true.
423 | * Now :canvas option takes the name of canvas, instead of a boolean.
424 | * Now :auto_authorize default to false.
425 | * Now :auto_authorize_scope default to nothing.
426 | * Now there's :write_session option to save fbs in session, default to false.
427 |
428 | ## rest-graph 1.3.0 -- 2010-06-11
429 |
430 | * Now rest-graph is rescuing all exceptions from rest-client.
431 | * Added RestGraph#exchange_sessions to exchange old sessions to access tokens.
432 |
433 | * Added RestGraph#old_rest, see:
434 | http://developers.facebook.com/docs/reference/rest/
435 |
436 | * Now all API request accept an additional options argument,
437 | you may pass :suppress_decode => true to turn off auto-decode this time.
438 | e.g. rg.get('bad/json', {:query => 'string'}, :suppress_decode => true)
439 | This is for Facebook who didn't always return JSON in response.
440 |
441 | * Renamed fql_server to old_server.
442 | * Favor yaji/json_gem first, then falls back to json, and json_pure.
443 | * Fixed a bug that cookie format from Facebook varies. No idea why.
444 |
445 | for RailsUtil:
446 |
447 | * Big and fat refactoring in RailsUtil, see example for detail:
448 | http://github.com/godfat/rest-graph/tree/rest-graph-1.3.0/example
449 | * url_for and link_to would auto pass :host option if it's inside canvas.
450 |
451 | ## rest-graph 1.2.1 -- 2010-06-02
452 |
453 | * Deprecated RailsController, use RailsUtil instead.
454 | * Fixed a bug that passing access_token in query string
455 | in RestGraph#authorize_url
456 | * Fixed a bug that Facebook changed the format (I think) of fbs_ in cookies.
457 | Thanks betelgeuse, closes #1
458 | http://github.com/cardinalblue/rest-graph/issues/issue/1
459 |
460 | ## rest-graph 1.2.0 -- 2010-05-27
461 |
462 | * Add RestGraph#parse_json!
463 | * Add RailsController to help you integrate into Rails.
464 | * Simplify arguments checking and require dependency.
465 | * Now if there's no secret in RestGraph, sig check would always fail.
466 | * Now there's a Rails example.
467 | http://github.com/godfat/rest-graph/tree/master/example
468 |
469 | * Add error_handler option. Default behavior is raising ::RestGraph::Error.
470 | You may want to pass your private controller method to do redirection.
471 | Extracted from README:
472 | # You may want to do redirect instead of raising exception, for example,
473 | # in a Rails application, you might have this private controller method:
474 | def redirect_to_authorize error = nil
475 | redirect_to @rg.authorize_url(:redirect_uri => request.url)
476 | end
477 |
478 | # and you'll use that private method to do error handling:
479 | def setup_rest_graph
480 | @rg = RestGraph.new(:error_handler => method(:redirect_to_authorize))
481 | end
482 |
483 | * Add log_handler option. Default behavior is do nothing.
484 | You may want to do this in Rails:
485 | RestGraph.new(:log_hanlder => lambda{ |duration, url|
486 | Rails.logger.debug("RestGraph " \
487 | "spent #{duration} " \
488 | "requesting #{url}")
489 | })
490 |
491 | * Add RestGraph#fql_multi to do FQL multiquery. Thanks Ethan Czahor
492 | Usage: rg.fql_multi(:query1 => 'SELECT ...', :query2 => 'SELECT ...')
493 |
494 | ## rest-graph 1.1.1 -- 2010-05-21
495 |
496 | * Add oauth realted utilites -- authorize_url and authorize!
497 | * Fixed a bug that in Ruby 1.8.7-, nil =~ /regexp/ equals to false.
498 | It is nil as expected in Ruby 1.9.1+
499 |
500 | ## rest-graph 1.1.0 -- 2010-05-13
501 |
502 | * Main repository was moved to http://github.com/godfat/rest-graph
503 | Sorry for the inconvenience. I'll keep pushing to both repositories until
504 | I am too lazy to do that.
505 |
506 | * Better way to deal with default attributes, use class methods.
507 |
508 | * If you want to auto load config, do require 'rest-graph/auto_load'
509 | if it's rails, it would load the config from config/rest-graph.y(a)ml.
510 | if you're using rails plugin, we do require 'rest-graph/auto_load'
511 | for you.
512 |
513 | * Config could be loaded manually as well. require 'rest-graph/load_config'
514 | and RestGraph::LoadConfig.load_config!('path/to/rest-graph.yaml', 'env')
515 |
516 | ## rest-graph 1.0.0 -- 2010-05-06
517 |
518 | * now access_token is saved in data attributes.
519 | * cookies related methods got renamed, and saved all data in RestGraph
520 | * parse failed would return nil, while data is always a hash
521 |
522 | ## rest-graph 0.9.0 -- 2010-05-04
523 |
524 | * renamed :server option to :graph_server
525 | * added :fql_server option and fql support.
526 | * cookies related parsing utility is now instance methods.
527 | you'll need to pass app_id and secret when initializing
528 | * if sig in cookies is bad, then it won't extract the access_token
529 |
530 | ## rest-graph 0.8.1 -- 2010-05-03
531 |
532 | * added access_token parsing utility
533 |
534 | ## rest-graph 0.8.0 -- 2010-05-03
535 |
536 | * release early, release often
537 |
--------------------------------------------------------------------------------