├── .gitignore ├── .gitmodules ├── .travis.yml ├── CHANGES.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── TODO.md ├── doc ├── ToC.md ├── dependency.md ├── design.md ├── heroku-facebook.md ├── rails.md ├── test.md └── tutorial.md ├── example ├── multi │ ├── config.ru │ └── rainbows.rb ├── rails3 │ ├── Gemfile │ ├── Rakefile │ ├── app │ │ ├── controllers │ │ │ └── application_controller.rb │ │ └── views │ │ │ └── application │ │ │ └── helper.html.erb │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── secret_token.rb │ │ │ └── session_store.rb │ │ ├── rest-graph.yaml │ │ └── routes.rb │ └── test │ │ ├── functional │ │ └── application_controller_test.rb │ │ ├── test_helper.rb │ │ └── unit │ │ └── rails_util_test.rb └── sinatra │ └── config.ru ├── init.rb ├── lib ├── rest-graph.rb └── rest-graph │ ├── config_util.rb │ ├── core.rb │ ├── facebook_util.rb │ ├── rails_util.rb │ ├── test_util.rb │ └── version.rb ├── rest-graph.gemspec └── test ├── common.rb ├── config └── rest-graph.yaml ├── test_api.rb ├── test_cache.rb ├── test_default.rb ├── test_error.rb ├── test_facebook.rb ├── test_handler.rb ├── test_load_config.rb ├── test_misc.rb ├── test_multi.rb ├── test_oauth.rb ├── test_old.rb ├── test_page.rb ├── test_parse.rb ├── test_rest-graph.rb ├── test_serialize.rb ├── test_test_util.rb └── test_timeout.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /pkg/ 2 | Gemfile.lock 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "task"] 2 | path = task 3 | url = git://github.com/godfat/gemgem.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 'git submodule update --init' 2 | script: 'ruby -r bundler/setup -S rake test:travis' 3 | 4 | env: 5 | - 'RESTGRAPH=rest-graph' 6 | - 'RESTGRAPH=rails3' 7 | 8 | rvm: 9 | - 1.9.3 10 | - 2.0.0 11 | - ruby 12 | - rbx 13 | - jruby 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | 2 | source 'http://rubygems.org' 3 | 4 | gemspec 5 | 6 | gem 'rest-client' 7 | gem 'em-http-request' 8 | 9 | gem 'rake' 10 | gem 'bacon' 11 | gem 'muack' 12 | gem 'webmock' 13 | 14 | gem 'json' 15 | gem 'json_pure' 16 | 17 | gem 'rack' 18 | gem 'ruby-hmac' 19 | 20 | platforms :ruby do 21 | gem 'yajl-ruby' 22 | end 23 | 24 | platforms :rbx do 25 | gem 'rubysl-fiber' # used in rest-core 26 | gem 'rubysl-singleton' # used in rake 27 | gem 'rubysl-rexml' # used in crack used in webmock 28 | gem 'rubysl-bigdecimal' # used in crack used in webmock 29 | gem 'rubysl-base64' # used in em-socksify used in em-http-request 30 | gem 'rubysl-test-unit' # used in activesupport 31 | gem 'rubysl-enumerator' # used in activesupport 32 | gem 'rubysl-benchmark' # used in activesupport 33 | gem 'racc' # used in journey used in actionpack 34 | end 35 | 36 | platforms :jruby do 37 | gem 'jruby-openssl' 38 | end 39 | 40 | gem 'rails', '3.2.16' if ENV['RESTGRAPH'] == 'rails3' 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rest-graph [![Build Status](http://travis-ci.org/godfat/rest-graph.png)](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 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | 2 | begin 3 | require "#{dir = File.dirname(__FILE__)}/task/gemgem" 4 | rescue LoadError 5 | sh 'git submodule update --init' 6 | exec Gem.ruby, '-S', $PROGRAM_NAME, *ARGV 7 | end 8 | 9 | Gemgem.init(dir) do |s| 10 | require 'rest-graph/version' 11 | s.name = 'rest-graph' 12 | s.version = RestGraph::VERSION 13 | s.homepage = 'https://github.com/godfat/rest-graph' 14 | 15 | %w[].each{ |g| s.add_runtime_dependency(g) } 16 | %w[].each{ |g| s.add_development_dependency(g) } 17 | 18 | s.authors = ['Cardinal Blue', 'Lin Jen-Shin (godfat)'] 19 | end 20 | 21 | module Gemgem 22 | module_function 23 | def test_rails *rails 24 | rails.each{ |framework| 25 | opts = Rake.application.options 26 | args = (opts.singleton_methods - [:rakelib, :trace_output]).map{ |arg| 27 | if arg.to_s !~ /=$/ && opts.send(arg) 28 | "--#{arg}" 29 | else 30 | '' 31 | end 32 | }.join(' ') 33 | Rake.sh "cd example/#{framework}; #{Gem.ruby} -S rake test #{args}" 34 | } 35 | end 36 | end 37 | 38 | desc 'Run example tests' 39 | task 'test:example' do 40 | Gemgem.test_rails('rails3') 41 | end 42 | 43 | desc 'Run all tests' 44 | task 'test:all' => ['test', 'test:example'] 45 | 46 | desc 'Run different json test' 47 | task 'test:json' do 48 | %w[yajl json].each{ |json| 49 | Rake.sh "#{Gem.ruby} -S rake -r #{json} test" 50 | } 51 | end 52 | 53 | task 'test:travis' do 54 | case ENV['RESTGRAPH'] 55 | when 'rails3'; Gemgem.test_rails('rails3') 56 | else ; Rake::Task['test'].invoke 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## RailsUtil 4 | 5 | * provide a :validate_access_token or so option 6 | * test for rails util for writing cookie 7 | 8 | ## RestGraph 9 | 10 | * error_handler can't be turned off 11 | * more docs? 12 | * more examples? 13 | -------------------------------------------------------------------------------- /doc/ToC.md: -------------------------------------------------------------------------------- 1 | 2 | # rest-graph documentation 3 | 4 | ## Table of Content 5 | 6 | * [Tutorial for the beginners, using Rails 3](tutorial.md) 7 | * [Overview and Design Concept](design.md) 8 | * [Picking Dependency](dependency.md) 9 | * [Testing](test.md) 10 | * [Working in Rails](rails.md) 11 | -------------------------------------------------------------------------------- /doc/dependency.md: -------------------------------------------------------------------------------- 1 | 2 | # Dependency 3 | 4 | ## Introduction 5 | 6 | Because rest-graph is designed to be lightweight and modular, it should 7 | depend on as little things as possible, and give people the power to choose 8 | their preference. rest-graph is depending on at least a HTTP client, and 9 | optionally depending on a JSON parser if you want to auto-decode the JSON 10 | from servers. (and `auto_decode` is actually the default.) It might also be 11 | depending on [Rack][] for some operation, for example, 12 | `RestGraph#parse_rack_env!` and `RestGraph#parse_cookies!` is using 13 | `Rack::Utils.parse_query`. For those operations, Rack is needed. 14 | 15 | [Rack]: https://github.com/rack/rack 16 | 17 | ## HTTP client (must pick one) 18 | 19 | At the beginning, rest-graph uses [rest-client][], and is a must install 20 | runtime dependency. Later, the support of [em-http-request][] is added, 21 | so now you can pick either of them or both of them. 22 | 23 | Usually, rest-client is used for synchronized (blocking) operations; 24 | Contrarily, em-http-request is used for asynchronized (evented) operations. 25 | 26 | If you don't know what's the difference between them, just use rest-client. 27 | It's a lot easier to use, and have been tested more. If you don't know how 28 | to pick, then you might be already using rest-client. 29 | 30 | This is an example of using rest-client: 31 | 32 | data = RestGraph.new.get('me') 33 | 34 | This is an example of using em-http-request: 35 | 36 | RestGraph.new.aget('me'){ |data| } 37 | 38 | This is using em-http-request, too: 39 | 40 | RestGraph.new.get('me', {}, {:async => true}){ |data| } 41 | 42 | [rest-client]: https://github.com/archiloque/rest-client 43 | [em-http-request]: https://github.com/igrigorik/em-http-request 44 | 45 | ## JSON parser (optional, but needed by default) 46 | 47 | When `auto_decode` is set to true, rest-graph would use a JSON parser to 48 | parse the JSON and return a Ruby object corresponding to that JSON. The most 49 | widely used JSON parser is [json][], it has two distributions, one is `json`, 50 | another one is `json_pure`. The former is written in Ruby and C, the latter 51 | is purely written in Ruby. They are too widely used so you might want to 52 | use it on your application, too. But [yajl-ruby][] is a lot more recommended, 53 | it's... generally better, you can take a look on [yajl-ruby's README][] 54 | 55 | rest-graph would first try to use Yajl, if it's not defined, then try JSON. 56 | If it's not defined either, then it would try to `require 'yajl'`, rescue 57 | `LoadError`, and `request 'json'`. The latter would either load json or 58 | json_pure depending on the system. 59 | 60 | So to force using yajl-ruby, you could require 'yajl-ruby' before rest-graph. 61 | There's no way to force using json when yajl-ruby is already used though. 62 | Anyone needs this? File a ticket on our [issue tracker][] 63 | 64 | [json]: https://github.com/flori/json 65 | [yajl-ruby]: https://github.com/brianmario/yajl-ruby 66 | [yajl-ruby's README]: https://github.com/brianmario/yajl-ruby/blob/master/README.rdoc 67 | [issue tracker]: https://github.com/godfat/rest-graph/issues 68 | 69 | ## Rack (optional, needed when parsing cookie) 70 | 71 | Actually I wonder if anyone would not use [Rack][]. But since it's really 72 | optional, so I'll just leave it as optional. 73 | 74 | ## RR (optional, needed when using rest-graph/test_util) 75 | 76 | test_util is built on top of [RR][], so to use test_util, RR is required. 77 | 78 | [RR]: https://github.com/btakita/rr 79 | -------------------------------------------------------------------------------- /doc/design.md: -------------------------------------------------------------------------------- 1 | 2 | # Design 3 | 4 | ## Introduction 5 | 6 | rest-graph is a lightweight Facebook Graph API client. By lightweight, it 7 | means it's modular and compact, and only provides essential functionality. 8 | It's designed to be transparent to Facebook Graph API, so it doesn't try to 9 | fix Facebook's bugs nor inconsistency problems. People should be able to 10 | read Facebook's documentation (though sometimes it's not quite helpful) in 11 | order to use rest-graph, instead of learning how rest-graph would work. 12 | (of course, Ruby experience is required.) 13 | 14 | In other words, before starts, you might need to know how Facebook's Graph 15 | API works, for example, how to fetch data, how to do authentication, etc. 16 | Here's the links: 17 | 18 | * Graph API: 19 | * Authentication: 20 | 21 | For advanced usage, you might want to read the followings, too: 22 | 23 | * FQL: 24 | * Old REST API: 25 | 26 | Since rest-graph is trying to be transparent to Facebook Graph API, some 27 | might find it's not so useful because of Facebook's bugs or inconsistency 28 | problems. This might be a disadvantage comparing to others client libraries 29 | which mimic the issues for you, but the advantage would be for people who 30 | have already known how Facebook Graph API works, say, people who used to 31 | develop Facebook Apps with PHP or any other tools, could easily know how to 32 | use rest-graph with their old knowledges. On the other hand, if Facebook 33 | fixed their bugs or inconsistency problems, you don't need to wait for 34 | rest-graph fixing the problems. You will directly depend on Facebook, 35 | but not depend on rest-graph which depends on Facebook. More layers, 36 | more problems. rest-graph is a client library, but not Facebook framework. 37 | 38 | Still, if the inconsistency problems are very obvious and would not change 39 | in the near future, for example, `oauth/access_token` API would not return 40 | a JSON as typical Graph APIs do, instead, it returns a query string as in 41 | URL. In this case, rest-graph uses `Rack::Utils.parse_query` to parse the 42 | query for you. If you feel there are more cases rest-graph should handle 43 | like this, please feel free to file a ticket on our [issue tracker][] on 44 | Github. 45 | 46 | * 47 | 48 | Or you could start a topic on our mailing list: 49 | 50 | * 51 | 52 | Either one is welcomed. 53 | 54 | ## Name 55 | 56 | [rest-graph][] is named after its first dependency [rest-client][]. 57 | 58 | [rest-graph]: https://github.com/godfat/rest-graph 59 | [rest-client]: https://github.com/archiloque/rest-client 60 | 61 | ## License 62 | 63 | rest-graph is licensed under Apache License 2.0. 64 | 65 | ## Target Usage 66 | 67 | rest-graph is split into many parts to better target different users coming 68 | from different areas. Essentially, the core functionality is all in one file 69 | which is `lib/rest-graph/core.rb`. You can copy that file and put into your 70 | projects' load path, or use gem to install rest-graph, and then just require 71 | the file with `require 'rest-graph/core'`. If you don't care about memory 72 | footprint or code bloats, want something that "Just Work™" it's ok to 73 | just `require 'rest-graph'`. It would try to load up anything you *might* 74 | or *might not* need. It should Just Work™ out of the box, otherwise, 75 | please file a ticket on our [issue tracker][]. 76 | 77 | [issue tracker]: https://github.com/godfat/rest-graph/issues 78 | 79 | Target usages are as following: 80 | 81 | * No matter where rest-graph whould be used, for convenience and laziness, 82 | just `require 'rest-graph'`. Then you're good to go. 83 | 84 | * Used in backend engine or non-web application. You might only want the 85 | core functionality that rest-graph provides. In this case, simply use 86 | `require 'rest-graph/core'` 87 | 88 | * Used in web applications, and then mostly you'll want more than the core. 89 | For example, how to get the access token, how to authenticate to Facebook, 90 | and how to use different settings (e.g. app_id, secret, etc) in different 91 | environments. In this case, you'll want `require 'rest-graph/rails_util'` 92 | (if your web framework is Rails) and `require 'rest-graph/config_util'`. 93 | See below for usages of those two extra utilities. 94 | 95 | ## File Structure 96 | 97 | rest-graph is modular, so utilities are all separated. Different `require` 98 | will get you different things. Here we list all different requires. 99 | 100 | * `require 'rest-graph'` 101 | 102 | This is for convenience and lazies which just loads everything. 103 | You can see below for usages. 104 | 105 | * `require 'rest-graph/core'` 106 | 107 | This is for the core functionality. Other than API calls (which is 108 | documented in [rdoc][]), you might want to have some default values 109 | for `RestGraph.new`, then you don't have to do this all the time: 110 | `RestGraph.new(:app_id => '1829')`. For example, you might want: 111 | `RestGraph.new.app_id` to return value `'1829'` instead of `nil`, 112 | and still be able to overwrite it when passing `:app_id` like this: 113 | `RestGraph.new(:app_id => 'abcd')`. If so, you'll do: 114 | 115 | require 'rest-graph/core' 116 | 117 | module MyRestGraphSettings 118 | def default_app_id 119 | '1829' 120 | end 121 | end 122 | 123 | RestGraph.send(:extend, MyRestGraphSettings) 124 | 125 | RestGraph.new.app_id # => '1829' 126 | 127 | Or you can simply define it in `RestGraph`. 128 | 129 | class RestGraph 130 | def self.default_app_id 131 | '1829' 132 | end 133 | end 134 | 135 | RestGraph.new.app_id # => '1829' 136 | 137 | If you want to set those defaults in a config file with different 138 | environments, then `require 'rest-graph/config_util'` is for you. 139 | See below. 140 | 141 | [rdoc]: http://rdoc.info/projects/godfat/rest-graph 142 | 143 | * `require 'rest-graph/config_util` 144 | 145 | This is for automatically reading settings from a certain config file. 146 | To use it, use: `RestGraph.load_config(path_to_yaml_file, environment)` 147 | A config file would look like this: [rest-graph.yaml][] You can embed 148 | ERB template in it. After the config has been loaded, every call to 149 | `RestGraph.new` would respect those settings. e.g. `RestGraph.new.app_id` 150 | would return the app_id you set in the config file, instead of `nil`. 151 | 152 | [rest-graph.yaml]: ../test/config/rest-graph.yaml 153 | 154 | * `require 'rest-graph/rails_util'` 155 | 156 | This is for people using Rails. (compatible and tested with both Rails 2 157 | and Rails 3) `include RestGraph::RailsUtil` in your controller would 158 | give you `rest_graph_setup` and `rest_graph` methods in both controller 159 | and helper. The former is used to configure the behaviour for each action, 160 | the latter is used to access the instance of `RestGraph` which is setup in 161 | `rest_graph_setup`. 162 | 163 | See [rails.md][] to learn more about this utility. 164 | 165 | [rails.md]: rails.md 166 | 167 | * `require 'rest-graph/test_util'` 168 | 169 | Quoted from Wikipedia's description about [Unit testing][]: 170 | 171 | > Ideally, each test case is independent from the others: substitutes 172 | > like method stubs, mock objects, fakes and test harnesses can be 173 | > used to assist testing a module in isolation. 174 | 175 | We won't even want to be depending on the Internet. It's slow, and 176 | unstable. You might have already tried [webmock][] or [fakeweb][], 177 | they are good tools, but a bit tedious to use if we're faking graph 178 | API calls. That's why `RestGraph::TestUtil` comes into play. It uses 179 | [RR][] to make stubs for API calls, and you can change the data that 180 | the stubs provide. This way, it's a lot easier to test your application. 181 | 182 | You can emulate a user login with `RestGraph::TestUtil.login(1234)`, 183 | which will give you a fake user data upon calling `/me`. It will 184 | give you a fake access token, too. 185 | 186 | See [test.md][] to learn more about this utility. 187 | 188 | See Martin Fowler's great article to learn more about mocks: 189 | [Mocks Aren't Stubs][] 190 | 191 | [Unit testing]: http://en.wikipedia.org/wiki/Unit_testing 192 | [webmock]: https://github.com/bblimke/webmock 193 | [fakeweb]: https://github.com/chrisk/fakeweb 194 | [RR]: https://github.com/btakita/rr 195 | [test.md]: test.md 196 | [Mocks Aren't Stubs]: http://martinfowler.com/articles/mocksArentStubs.html 197 | 198 | * `require 'rest-graph/facebook_util'` 199 | 200 | Facebook has some very inconsistent behaviour. This utility is here to 201 | fix those inconsistencies, providing you a more comprehensive operation 202 | on data. Also, it has permission list build in, without the trouble 203 | looking through Facebook's documentation. 204 | 205 | This utility is not fully and carefully written, please file a ticket 206 | on our [issue tracker][] if you want something not presented currently. 207 | -------------------------------------------------------------------------------- /doc/heroku-facebook.md: -------------------------------------------------------------------------------- 1 | # Setting a Facebook app on Heroku 2 | 3 | ## Software Installation and Configuration (1) 4 | 5 | ### Mac OS 6 | 7 | * Install Xcode from the install DVD or Apple website or Mac App Store. 8 | 9 | * Install Homebrew (package manager). 10 | 11 | curl https://gist.github.com/raw/323731/install_homebrew.rb > /tmp/install_homebrew.rb 12 | ruby /tmp/install_homebrew.rb 13 | 14 | * Install Git (source code manager). 15 | 16 | brew install git 17 | 18 | * Install database. You may pick anything that Rails supports, but since 19 | Heroku uses PostgreSQL, we recommend to use the same database. 20 | 21 | brew install postgresql 22 | initdb /usr/local/var/postgres 23 | pg_ctl -D /usr/local/var/postgres start 24 | createuser --createdb YourProject 25 | 26 | * Setup postgres to auto-start after boot. 27 | 28 | cp `brew --prefix postgresql`/org.postgresql.postgres.plist ~/Library/LaunchAgents/ 29 | launchctl load -w ~/Library/LaunchAgents/org.postgresql.postgres.plist 30 | 31 | * ...or start up postgres manually: 32 | 33 | pg_ctl -D /usr/local/var/postgres start 34 | 35 | * Install Ruby 1.9.2 (via Homebrew, but if you prefer RVM, it's fine). 36 | 37 | brew install ruby 38 | 39 | ### Ubuntu (Linux) 40 | 41 | * Install various tools 42 | 43 | sudo apt-get update 44 | sudo apt-get install gcc g++ make libssl-dev zlib1g-dev libreadline5-dev libyaml-dev libxml2-dev 45 | 46 | * Install Git (source code manger). 47 | 48 | sudo apt-get install git 49 | 50 | * Install database. You may pick anything Rails supports, but since 51 | Heroku uses PostgreSQL, we recommend to use the same database. 52 | 53 | sudo apt-get install postgresql libpq-dev 54 | sudo /etc/init.d/postgresql restart 55 | sudo -u postgres createuser --createdb YourProject 56 | 57 | You might need to edit `pg_hba.conf` (the path would be something like this: 58 | /etc/postgresql/8.4/main/pg_hba.conf) to make sure _YourProject_ has the 59 | access to your local database. For example, has the following line: 60 | 61 | local all YourProject trust 62 | 63 | And make sure there's **NO** this line: (it conflicts with the above) 64 | 65 | local all all ident 66 | 67 | You should restart postgresql after updating `pg_hba.conf` 68 | 69 | sudo /etc/init.d/postgresql restart 70 | 71 | * Install Ruby 1.9.2 72 | 73 | bash < <( curl http://rvm.beginrescueend.com/releases/rvm-install-head ) 74 | echo '[[ -s "$HOME/.rvm/scripts/rvm" ]] && . "$HOME/.rvm/scripts/rvm"' >> ~/.bash_profile 75 | source $HOME/.rvm/scripts/rvm 76 | rvm install 1.9.2 77 | rvm use 1.9.2 78 | 79 | ### Windows (not tested) 80 | 81 | * Install Git 82 | * Install Ruby 1.9.2 83 | * Install PostgreSQL 84 | 85 | ## Software Installation and Configuration (2) 86 | 87 | ### General (OS-independent) 88 | 89 | * Depending on the OS and your configuration, you may need to prefix with 90 | "sudo" to install the gems. You don't have to if you're following the 91 | instructions above to install Ruby (RVM or Homebrew), or running on Windows. 92 | 93 | * Configure Git (~/.gitconfig) 94 | 95 | git config --global user.name 'Your Name' 96 | git config --global user.email 'your@email.com' 97 | 98 | * Install gems 99 | 100 | echo 'gem: --no-ri --no-rdoc' >> ~/.gemrc 101 | gem install rails pg heroku 102 | 103 | Note: on newer Macs, if pg fails to install, try this: 104 | 105 | env ARCHFLAGS='-arch x86_64' gem install pg 106 | 107 | * Generate RSA keys and upload to Heroku. (you'll need a [Heroku][] account) 108 | 109 | ssh-keygen -t rsa -C 'your@email.com' 110 | heroku keys:add ~/.ssh/id_rsa.pub 111 | 112 | [Heroku]: http://heroku.com 113 | 114 | ## Create a Rails application and push to Heroku 115 | 116 | * Rails 3 project 117 | 118 | rails new 'YourProject' 119 | cd 'YourProject' 120 | 121 | * Git initialization 122 | 123 | git init 124 | git add . 125 | git commit -m 'first commit' 126 | 127 | * Switch to PostgreSQL. Edit the Gemfile and change `gem 'sqlite3'` to `gem 'pg'`: 128 | 129 | bundle check 130 | git add Gemfile Gemfile.lock 131 | git commit -m 'switch to postgresql' 132 | 133 | * Set up the Heroku application 134 | 135 | heroku create 'YourProject' 136 | 137 | * Push to Heroku. (from local master branch to remote master branch) 138 | 139 | git push heroku master:master 140 | 141 | * Take a look at yourproject.heroku.com. If you have terminal browser lynx 142 | installed, you can run this: 143 | 144 | lynx yourproject.heroku.com 145 | 146 | Otherwise, just use your favorite browser to view it. 147 | 148 | ## For development, set up the application to run locally on your computer 149 | 150 | * Edit `config/database.yml` with following: 151 | 152 | development: 153 | adapter: postgresql 154 | username: YourProject 155 | database: YourProject_development 156 | 157 | test: 158 | adapter: postgresql 159 | username: YourProject 160 | database: YourProject_test 161 | 162 | * Setup local database 163 | 164 | rake db:create 165 | rake db:migrate 166 | rake db:schema:dump # update schema.rb for reference 167 | rake db:test:prepare # sometimes this is needed to run tests 168 | 169 | * Run Ruby server (WEBrick) 170 | 171 | rails server 172 | 173 | * or run Thin server (need to update Gemfile with `gem 'thin'`) 174 | 175 | gem install thin 176 | rails server thin 177 | 178 | ## Create a Facebook Application 179 | 180 | * 181 | 182 | ## Using rest-graph 183 | 184 | ### Tutorial 185 | 186 | * 187 | -------------------------------------------------------------------------------- /doc/rails.md: -------------------------------------------------------------------------------- 1 | 2 | # Rails 3 | 4 | ## Introduction 5 | 6 | ## Standalone Website 7 | 8 | ## Facebook iframe Canvas 9 | 10 | ## Config 11 | 12 | ## Options 13 | -------------------------------------------------------------------------------- /doc/test.md: -------------------------------------------------------------------------------- 1 | 2 | # Test 3 | 4 | ## Introduction 5 | 6 | A collection of tools integrated with [RR][] to ease the pain of 7 | testing. There are 3 levels of tools to stub the result of 8 | calling APIs. The highest level is `TestUtil.login(1234)` which 9 | would stub a number of results to pretend the user 1234 is 10 | logged-in. 11 | 12 | The second level are the get/post/put/delete methods for 13 | TestUtil. For example, to make rg.get('1234') return a 14 | particular value (such as a hash {'a' => 1}), use 15 | TestUtil.get('1234'){ {'a' => 1} } to set it up to return 16 | the specified value (typically a hash). 17 | 18 | The third level is for setting default_data and default_response 19 | for TestUtil. The default_data is the default value for rg.data, 20 | which includes the access_token and the user_id (uid). The 21 | default_response is the response given by any RestGraph API call 22 | (e.g. get, post) when no explicit response has been defined in 23 | the second level. 24 | 25 | To use TestUtil, remember to install RR (gem install rr) and 26 | require 'rest-graph/test_util'. Then put 27 | RestGraph::TestUtil.setup before any test case starts, and put 28 | RestGraph::TestUtil.teardown after any test case ends. Setup 29 | would stub default_data and default_response for you, and 30 | teardown would remove any stubs on RestGraph. For Rails, you 31 | might want to put these in test_helper.rb under "setup" and 32 | "teardown" block, just as the name suggested. For bacon or 33 | rspec style testing, these can be placed in the "before" and 34 | "after" blocks. 35 | 36 | In addition, you can get the API calls history via 37 | RestGraph::TestUtil.history. This would get cleaned up in 38 | RestGraph::TestUtil.teardown as well. 39 | 40 | [RR]: https://github.com/btakita/rr 41 | 42 | ## Login emulation 43 | 44 | ## default_response 45 | 46 | ## 47 | -------------------------------------------------------------------------------- /doc/tutorial.md: -------------------------------------------------------------------------------- 1 | The code in this tutorial could be found on [samplergthree][] 2 | 3 | [samplergthree]: https://github.com/cardinalblue/samplergthree 4 | 5 | # How to build a Facebook application within Rails 3 using the RestGraph gem 6 | 7 | 1. Before you start, I strongly recommend reading these: 8 | 9 | * Apps on Facebook.com -> 10 | * Graph API -> 11 | * Authentication -> 12 | * Heroku: Building a Facebook Application -> 13 | 14 | 15 | 2. Go to [FB Developers website](http://facebook.com/developers) and create a new FB app. Set its canvas name, canvas url and your site URL. Make sure the canvas type is set to "iframe" (which should be the case by default). 16 | 17 | 18 | 3. Build a new Rails application. 19 | 20 | rails new 21 | 22 | 23 | 4. Declare RestGraph and its dependencies in the Gemfile. Add these lines: 24 | 25 | gem 'rest-graph' 26 | 27 | # these gems are used in rest-graph 28 | gem 'rest-client', '>=1.6' 29 | gem 'json' # you may also use other JSON parsers/generators, i.e. 'yajl-ruby' or 'json_pure' 30 | 31 | And run: 32 | 33 | bundle install 34 | 35 | 36 | 5. In order to configure your Rails application for the Facebook application you created, you must create a rest-graph.yaml file in your /config directory and fill it with your Facebook configuration. If you plan to run your application in the Facebook canvas, also provide a canvas name. 37 | 38 | Example: 39 | 40 | development: 41 | app_id: 'XXXXXXXXXXXXXX' 42 | secret: 'YYYYYYYYYYYYYYYYYYYYYYYYYYY' 43 | callback_host: 'my.dev.host.com' 44 | 45 | production: 46 | app_id: 'XXXXXXXXXXXXXX' 47 | secret: 'YYYYYYYYYYYYYYYYYYYYYYYYYYY' 48 | canvas: 'yourcanvasname' 49 | callback_host: 'my.production.host.com' 50 | 51 | 52 | If you push to Heroku, your production callback_host should be `yourappname.heroku.com`. You can also access your app directly running `rails server` (or just `rails s`) in your console, but if you do not have an external IP address (e.g. you are behind a NAT), you will need to use a service called a tunnel in order to make your application accessible to the outer world (and Facebook callbacks). You'll find more information on setting up a tunnel here: . 53 | 54 | 6. Let's create a first controller for your app - ScratchController. 55 | 56 | rails generate controller Scratch 57 | 58 | 7. The next step will be to include rest-graph in your controller. You should put this line in: 59 | 60 | include RestGraph::RailsUtil 61 | 62 | Now you can make use of the RestGraph commands :) 63 | 64 | 8. To actually use rest-graph in a controller action, you will need to first call "rest_graph_setup", which reads the configuration from the rest-graph.yaml and creates a rest_graph object. Let's set this up in a before_filter. 65 | 66 | Add this line after the `include RestGraph::RailsUtil`: 67 | 68 | before_filter :filter_setup_rest_graph 69 | 70 | And declare filter_setup_rest_graph as a private function: 71 | 72 | private 73 | 74 | def filter_setup_rest_graph 75 | rest_graph_setup(:auto_authorize => true) 76 | end 77 | 78 | The `:auto_authorize` argument of rest_graph_setup tells RestGraph to redirect users to the app authorization page if the app is not authorized yet. 79 | 80 | Hooray! You can now perform all kinds of Graph API operations using the rest_graph object. 81 | 82 | 9. Let's start with following sample action in the Scratch controller: 83 | 84 | def me 85 | render :text => rest_graph.get('me').inspect 86 | end 87 | 88 | 10. To run this, go to the /config/routes.rb file to set up the default routing. For now you will just need this line: 89 | 90 | match ':controller/:action' 91 | 92 | 11. Commit your change in git, and then push to Heroku: 93 | 94 | git add . 95 | git commit -m "first test of rest-graph using scratch/me" 96 | git push heroku master 97 | 98 | After you push your app to Heroku, you can open in your browser. If you are not yet logged into Facebook, it will ask you to log in. If you are logged in your Facebook account, this address should redirect you to the authorization page and ask if you want to let your application access your public information. After you confirm, you should be redirected back to 'scratch/me' action which will show your basic information. 99 | 100 | 12. To see other information, such as your home feed, is very easy. You can add another sample action to your controller: 101 | 102 | def feed 103 | render :text => rest_graph.get('me/home').inspect 104 | end 105 | 106 | If you will push the changes to Heroku and go to , the page should display a JSON hash with all the data from your Facebook home feed. 107 | 108 | 109 | 13. Now let's try to access your Facebook wall. You need to add a new action to your controller: 110 | 111 | def wall 112 | render :text => rest_graph.get('me/feed').inspect 113 | end 114 | 115 | Note that Facebook's naming is such that your home news feeds is accessed via `me/home` and your profile walls is accessed via `me/feed` ... 116 | 117 | Actually, I need to warn you that this time the action won't work properly. Why? Because users didn't grant you the permission to access their walls! You need to ask them for this special permission and that means you need to add something more to your controller. 118 | 119 | So, we will organize all the permissions we need as a scope and pass them to the rest_graph_setup call. I find it handy to make the scope array and declare what kind of permissions I need just inside this array. If you feel it's a good idea, you can add this line to your private setup function, just before you call rest_graph_setup: 120 | 121 | scope = [] 122 | scope << 'read_stream' 123 | 124 | The only permission you need right now is the 'read_stream' permission. You can find out more about different kinds of user permissions here: 125 | 126 | You also need to add the auto_authorize_scope argument to the rest_graph_setup. It will look this way now: 127 | 128 | rest_graph_setup(:auto_authorize => true, :auto_authorize_scope => scope.join(',')) 129 | 130 | As you see, you might as well pass the argument like this `:auto_authorize_scope => 'read_stream'`, but once you have to get a lot of different permissions, it's very useful to put them all in an array, because it's more readable and you can easily delete or comment out any one of them. 131 | 132 | Now save your work and push it to Heroku or just try in your tunneled development environment. /scratch/wall URL should give you the hash with user's wall data now! 133 | 134 | Remember. Anytime you need to get data of a new kind, you need to ask user for a certain permission first and that means you need to declare this permission in your scope array! 135 | 136 | 14. What else? If you know how to deal with hashes then you will definitely know how to get any kind of data you want using the rest_graph object. Let's say you want to get a last object from a user's wall (last in terms of time, last posted, so the first on the wall and therefore first to Ruby). Let's take a look at the /scratch/feed page. The hash which is printed on this page has 2 keys - data and paging. Let's leave the paging key aside. What's more interesting here comes as a value of 'data'. So the last object in any user's wall will be simply: 137 | 138 | rest_graph.get('me/feed')['data'].first 139 | 140 | Now let's say you want only to keep the name of the author of this particular object. You can get it by using: 141 | 142 | rest_graph.get('me/feed')['data'].first['from']['name'] 143 | 144 | That's it! 145 | 146 | 15. More information on customizing RestGraph and its functions are to be found here: 147 | -------------------------------------------------------------------------------- /example/multi/config.ru: -------------------------------------------------------------------------------- 1 | 2 | id = '4' 3 | times = 10 4 | 5 | require 'async-rack' 6 | require 'rest-graph' 7 | 8 | use Rack::ContentType 9 | use Rack::Reloader 10 | 11 | module RG 12 | module_function 13 | def create env 14 | RestGraph.new(:log_method => env['rack.logger'].method(:debug)) 15 | end 16 | end 17 | 18 | run Rack::Builder.new{ 19 | map('/async'){ 20 | run lambda{ |env| 21 | RG.create(env).multi([[:get, id]] * times){ |r| 22 | env['async.callback'].call [200, {}, r.map(&:inspect)] 23 | } 24 | throw :async 25 | } 26 | } 27 | map('/sync'){ 28 | run lambda{ |env| 29 | [200, {}, (0...times).map{ RG.create(env).get(id) }.map(&:inspect)] 30 | } 31 | } 32 | map('/'){ 33 | run lambda{ |env| 34 | [200, {'Content-Type' => 'text/html'}, 35 | [<<-HTML 36 | 37 | go to /async for em-http-request (multi) result,
38 | go to /sync for rest-client result.
39 | 40 | HTML 41 | ]] 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /example/multi/rainbows.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | Rainbows! do 4 | use :EventMachine 5 | end 6 | -------------------------------------------------------------------------------- /example/rails3/Gemfile: -------------------------------------------------------------------------------- 1 | 2 | source 'https://rubygems.org' 3 | 4 | gem 'rails', '3.2.16' 5 | 6 | gem 'rest-client' # for rest-graph 7 | gem 'rest-graph', :path => '../../' 8 | 9 | group :test do 10 | gem 'muack' 11 | gem 'webmock' 12 | end 13 | 14 | platforms :ruby do 15 | gem 'yajl-ruby' 16 | end 17 | 18 | platforms :jruby do 19 | gem 'jruby-openssl' 20 | end 21 | 22 | platforms :rbx do 23 | gem 'rubysl-fiber' # used in rest-core 24 | gem 'rubysl-singleton' # used in rake 25 | gem 'rubysl-rexml' # used in crack used in webmock 26 | gem 'rubysl-bigdecimal' # used in crack used in webmock 27 | gem 'rubysl-test-unit' # used in activesupport 28 | gem 'rubysl-enumerator' # used in activesupport 29 | gem 'rubysl-benchmark' # used in activesupport 30 | gem 'racc' # used in journey used in actionpack 31 | end 32 | -------------------------------------------------------------------------------- /example/rails3/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | require 'rake' 6 | 7 | Rails3::Application.load_tasks 8 | -------------------------------------------------------------------------------- /example/rails3/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | 2 | class ApplicationController < ActionController::Base 3 | protect_from_forgery 4 | 5 | include RestGraph::RailsUtil 6 | 7 | before_filter :filter_common , :only => [:index] 8 | before_filter :filter_canvas , :only => [:canvas] 9 | before_filter :filter_options , :only => [:options] 10 | before_filter :filter_no_auto , :only => [:no_auto] 11 | before_filter :filter_diff_app_id , :only => [:diff_app_id] 12 | before_filter :filter_diff_canvas , :only => [:diff_canvas] 13 | before_filter :filter_iframe_canvas, :only => [:iframe_canvas] 14 | before_filter :filter_cache , :only => [:cache] 15 | before_filter :filter_hanlder , :only => [:handler_] 16 | before_filter :filter_session , :only => [:session_] 17 | before_filter :filter_cookies , :only => [:cookies_] 18 | 19 | def index 20 | render :text => rest_graph.get('me').to_json 21 | end 22 | alias_method :canvas , :index 23 | alias_method :options , :index 24 | alias_method :diff_canvas , :index 25 | alias_method :iframe_canvas, :index 26 | alias_method :handler_ , :index 27 | alias_method :session_ , :index 28 | alias_method :cookies_ , :index 29 | 30 | def no_auto 31 | rest_graph.get('me') 32 | rescue RestGraph::Error 33 | render :text => 'XD' 34 | end 35 | 36 | def diff_app_id 37 | render :text => rest_graph.app_id 38 | end 39 | 40 | def cache 41 | url = rest_graph.url('cache') 42 | rest_graph.get('cache') 43 | rest_graph.get('cache') 44 | render :text => Rails.cache.read(Digest::MD5.hexdigest(url)) 45 | end 46 | 47 | def error 48 | raise RestGraph::Error.new("don't rescue me") 49 | end 50 | 51 | def reinitialize 52 | rest_graph_setup(:cache => {'a' => 'b'}) 53 | render :text => YAML.dump(rest_graph.cache) 54 | end 55 | 56 | def helper; end 57 | 58 | def defaults 59 | rest_graph_setup 60 | render :text => (rest_graph.cache == Rails.cache && 61 | rest_graph.log_method.receiver == Rails.logger) 62 | end 63 | 64 | def parse_cookies 65 | rest_graph_setup 66 | render :text => 'dummy' 67 | end 68 | 69 | private 70 | def filter_common 71 | rest_graph_setup(:auto_authorize => true, :canvas => '') 72 | end 73 | 74 | def filter_canvas 75 | rest_graph_setup(:canvas => RestGraph.default_canvas, 76 | :auto_authorize_scope => 'publish_stream') 77 | end 78 | 79 | def filter_diff_canvas 80 | rest_graph_setup(:canvas => 'ToT', 81 | :auto_authorize_scope => 'email') 82 | end 83 | 84 | def filter_iframe_canvas 85 | rest_graph_setup(:canvas => 'zzz', 86 | :auto_authorize => true) 87 | end 88 | 89 | def filter_no_auto 90 | rest_graph_setup(:auto_authorize => false) 91 | end 92 | 93 | def filter_diff_app_id 94 | rest_graph_setup(:app_id => 'zzz', 95 | :auto_authorize => true) 96 | end 97 | 98 | def filter_options 99 | rest_graph_setup(:auto_authorize_options => {:scope => 'bogus'}, 100 | :canvas => nil) 101 | end 102 | 103 | def filter_cache 104 | rest_graph_setup(:cache => Rails.cache) 105 | end 106 | 107 | def filter_hanlder 108 | rest_graph_setup(:write_handler => method(:write_handler), 109 | :check_handler => method(:check_handler)) 110 | end 111 | 112 | def write_handler fbs 113 | Rails.cache[:fbs] = fbs 114 | end 115 | 116 | def check_handler 117 | Rails.cache[:fbs] 118 | end 119 | 120 | def filter_session 121 | rest_graph_setup(:write_session => true, :auto_authorize => true) 122 | end 123 | 124 | def filter_cookies 125 | rest_graph_setup(:write_cookies => true, :auto_authorize => true) 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /example/rails3/app/views/application/helper.html.erb: -------------------------------------------------------------------------------- 1 | <%= rest_graph.app_id %> 2 | -------------------------------------------------------------------------------- /example/rails3/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails3::Application 5 | -------------------------------------------------------------------------------- /example/rails3/config/application.rb: -------------------------------------------------------------------------------- 1 | 2 | # Set up gems listed in the Gemfile. 3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 4 | 5 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 6 | 7 | require 'action_controller/railtie' 8 | require 'rails/test_unit/railtie' 9 | 10 | Bundler.require(:default, Rails.env) 11 | 12 | module Rails3 13 | class Application < Rails::Application 14 | config.encoding = 'utf-8' 15 | 16 | logger = Logger.new($stdout) 17 | logger.level = Logger::INFO 18 | config.logger = logger 19 | 20 | # Configure sensitive parameters which will be filtered from the log file. 21 | config.filter_parameters += [:password] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /example/rails3/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 5 | 6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 7 | -------------------------------------------------------------------------------- /example/rails3/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Rails3::Application.initialize! 6 | -------------------------------------------------------------------------------- /example/rails3/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails3::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the webserver when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_view.debug_rjs = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Print deprecation notices to the Rails logger 18 | config.active_support.deprecation = :log 19 | 20 | # Only use best-standards-support built into browsers 21 | config.action_dispatch.best_standards_support = :builtin 22 | end 23 | 24 | -------------------------------------------------------------------------------- /example/rails3/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails3::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The production environment is meant for finished, "live" apps. 5 | # Code is not reloaded between requests 6 | config.cache_classes = true 7 | 8 | # Full error reports are disabled and caching is turned on 9 | config.consider_all_requests_local = false 10 | config.action_controller.perform_caching = true 11 | 12 | # Specifies the header that your server uses for sending files 13 | config.action_dispatch.x_sendfile_header = "X-Sendfile" 14 | 15 | # For nginx: 16 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' 17 | 18 | # If you have no front-end server that supports something like X-Sendfile, 19 | # just comment this out and Rails will serve the files 20 | 21 | # See everything in the log (default is :info) 22 | # config.log_level = :debug 23 | 24 | # Use a different logger for distributed setups 25 | # config.logger = SyslogLogger.new 26 | 27 | # Use a different cache store in production 28 | # config.cache_store = :mem_cache_store 29 | 30 | # Disable Rails's static asset server 31 | # In production, Apache or nginx will already do this 32 | config.serve_static_assets = false 33 | 34 | # Enable serving of images, stylesheets, and javascripts from an asset server 35 | # config.action_controller.asset_host = "http://assets.example.com" 36 | 37 | # Disable delivery errors, bad email addresses will be ignored 38 | # config.action_mailer.raise_delivery_errors = false 39 | 40 | # Enable threaded mode 41 | # config.threadsafe! 42 | 43 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 44 | # the I18n.default_locale when a translation can not be found) 45 | config.i18n.fallbacks = true 46 | 47 | # Send deprecation notices to registered listeners 48 | config.active_support.deprecation = :notify 49 | end 50 | -------------------------------------------------------------------------------- /example/rails3/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails3::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Log error messages when you accidentally call methods on nil. 11 | config.whiny_nils = true 12 | 13 | # Show full error reports and disable caching 14 | config.consider_all_requests_local = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Raise exceptions instead of rendering exception templates 18 | config.action_dispatch.show_exceptions = false 19 | 20 | # Disable request forgery protection in test environment 21 | config.action_controller.allow_forgery_protection = false 22 | 23 | # Use SQL instead of Active Record's schema dumper when creating the test database. 24 | # This is necessary if your schema can't be completely dumped by the schema dumper, 25 | # like if you have constraints or database-specific column types 26 | # config.active_record.schema_format = :sql 27 | 28 | # Print deprecation notices to the stderr 29 | config.active_support.deprecation = :stderr 30 | end 31 | -------------------------------------------------------------------------------- /example/rails3/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Rails3::Application.config.secret_token = '74c293b4c5df1981d2f92785aa5538a21b0901293d693e6bb828669cbb1f1d1f5ddd1b3e21325304c90e952a4866a9eec7620379b6f6c27aae0670cdefda97ae' 8 | -------------------------------------------------------------------------------- /example/rails3/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails3::Application.config.session_store :cookie_store, :key => '_rails3_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Rails3::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /example/rails3/config/rest-graph.yaml: -------------------------------------------------------------------------------- 1 | 2 | development: &default 3 | app_id: '123' 4 | secret: '456' 5 | canvas: 'can' 6 | 7 | production: 8 | *default 9 | 10 | test: 11 | *default 12 | -------------------------------------------------------------------------------- /example/rails3/config/routes.rb: -------------------------------------------------------------------------------- 1 | 2 | Rails3::Application.routes.draw do 3 | root :controller => 'application', :action => 'index' 4 | match ':action', :controller => 'application' 5 | end 6 | -------------------------------------------------------------------------------- /example/rails3/test/functional/application_controller_test.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'test_helper' 3 | require 'webmock' 4 | require 'muack' 5 | 6 | WebMock.disable_net_connect! 7 | 8 | class ApplicationControllerTest < ActionController::TestCase 9 | include WebMock::API 10 | include Muack::API 11 | 12 | def setup 13 | body = rand(2) == 0 ? '{"error":{"type":"OAuthException"}}' : 14 | '{"error_code":104}' 15 | 16 | stub_request(:get, 'https://graph.facebook.com/me'). 17 | to_return(:body => body) 18 | end 19 | 20 | def teardown 21 | Muack.verify 22 | WebMock.reset! 23 | end 24 | 25 | def assert_url expected 26 | assert_equal(expected, normalize_url(assigns(:rest_graph_authorize_url))) 27 | if @response.status == 200 # js redirect 28 | assert_equal( 29 | expected, 30 | normalize_url( 31 | @response.body.match(/window\.top\.location\.href = '(.+?)'/)[1])) 32 | 33 | assert_equal( 34 | CGI.escapeHTML(expected), 35 | normalize_url( 36 | @response.body.match(/content="0;url=(.+?)"/)[1], '&')) 37 | 38 | assert_equal( 39 | CGI.escapeHTML(expected), 40 | normalize_url( 41 | @response.body.match(//)[1], '&')) 42 | end 43 | end 44 | 45 | def test_index 46 | get(:index) 47 | assert_response :redirect 48 | 49 | url = normalize_url( 50 | 'https://graph.facebook.com/oauth/authorize?client_id=123&' \ 51 | 'scope=&redirect_uri=http%3A%2F%2Ftest.host%2F') 52 | 53 | assert_url(url) 54 | end 55 | 56 | def test_canvas 57 | get(:canvas) 58 | assert_response :success 59 | 60 | url = normalize_url( 61 | 'https://graph.facebook.com/oauth/authorize?client_id=123&' \ 62 | 'scope=publish_stream&' \ 63 | 'redirect_uri=http%3A%2F%2Fapps.facebook.com%2Fcan%2Fcanvas') 64 | 65 | assert_url(url) 66 | end 67 | 68 | def test_diff_canvas 69 | get(:diff_canvas) 70 | assert_response :success 71 | 72 | url = normalize_url( 73 | 'https://graph.facebook.com/oauth/authorize?client_id=123&' \ 74 | 'scope=email&' \ 75 | 'redirect_uri=http%3A%2F%2Fapps.facebook.com%2FToT%2Fdiff_canvas') 76 | 77 | assert_url(url) 78 | end 79 | 80 | def test_iframe_canvas 81 | get(:iframe_canvas) 82 | assert_response :success 83 | 84 | url = normalize_url( 85 | 'https://graph.facebook.com/oauth/authorize?client_id=123&' \ 86 | 'scope=&' \ 87 | 'redirect_uri=http%3A%2F%2Fapps.facebook.com%2Fzzz%2Fiframe_canvas') 88 | 89 | assert_url(url) 90 | end 91 | 92 | def test_options 93 | get(:options) 94 | assert_response :redirect 95 | 96 | url = normalize_url( 97 | 'https://graph.facebook.com/oauth/authorize?client_id=123&' \ 98 | 'scope=bogus&' \ 99 | 'redirect_uri=http%3A%2F%2Ftest.host%2Foptions') 100 | 101 | assert_url(url) 102 | end 103 | 104 | def test_protected 105 | assert_nil @controller.public_methods.find{ |m| m.to_s =~ /^rest_graph/ } 106 | end 107 | 108 | def test_no_auto 109 | get(:no_auto) 110 | assert_response :success 111 | assert_equal 'XD', @response.body 112 | end 113 | 114 | def test_app_id 115 | get(:diff_app_id) 116 | assert_response :success 117 | assert_equal 'zzz', @response.body 118 | end 119 | 120 | def test_cache 121 | WebMock.reset! 122 | stub_request(:get, 'https://graph.facebook.com/cache'). 123 | to_return(:body => '{"message":"ok"}') 124 | 125 | get(:cache) 126 | assert_response :success 127 | assert_equal '{"message":"ok"}', @response.body 128 | end 129 | 130 | def test_handler 131 | WebMock.reset! 132 | stub_request(:get, 'https://graph.facebook.com/me?access_token=aloha'). 133 | to_return(:body => '["snowman"]') 134 | 135 | Rails.cache[:fbs] = RestGraph.new(:access_token => 'aloha').fbs 136 | get(:handler_) 137 | assert_response :success 138 | assert_equal '["snowman"]', @response.body 139 | ensure 140 | Rails.cache.clear 141 | end 142 | 143 | def test_session 144 | WebMock.reset! 145 | stub_request(:get, 'https://graph.facebook.com/me?access_token=wozilla'). 146 | to_return(:body => '["fireball"]') 147 | 148 | @request.session[RestGraph::RailsUtil.rest_graph_storage_key] = 149 | RestGraph.new(:access_token => 'wozilla').fbs 150 | 151 | get(:session_) 152 | assert_response :success 153 | assert_equal '["fireball"]', @response.body 154 | end 155 | 156 | def test_cookies 157 | WebMock.reset! 158 | stub_request(:get, 'https://graph.facebook.com/me?access_token=blizzard'). 159 | to_return(:body => '["yeti"]') 160 | 161 | @request.cookies[RestGraph::RailsUtil.rest_graph_storage_key] = 162 | RestGraph.new(:access_token => 'blizzard').fbs 163 | 164 | get(:cookies_) 165 | assert_response :success 166 | assert_equal '["yeti"]', @response.body 167 | end 168 | 169 | def test_wrong_session 170 | WebMock.reset! 171 | stub_request(:get, 'https://graph.facebook.com/me'). 172 | to_return(:body => '{"error":{"type":"OAuthException"}}') 173 | 174 | session = @request.session 175 | key = RestGraph::RailsUtil.rest_graph_storage_key 176 | session[key] = 'bad' 177 | 178 | get(:session_) 179 | assert_equal nil, session[key] 180 | end 181 | 182 | def test_wrong_cookies 183 | WebMock.reset! 184 | stub_request(:get, 'https://graph.facebook.com/me'). 185 | to_return(:body => '{"error":{"type":"OAuthException"}}') 186 | 187 | cookies = @request.cookies 188 | key = RestGraph::RailsUtil.rest_graph_storage_key 189 | session[key] = 'bad' 190 | 191 | get(:cookies_) 192 | assert_equal nil, cookies[key] 193 | end 194 | 195 | def test_error 196 | get(:error) 197 | rescue => e 198 | assert_equal RestGraph::Error, e.class 199 | end 200 | 201 | def test_reinitailize 202 | get(:reinitialize) 203 | assert_response :success 204 | assert_equal({'a' => 'b'}, YAML.load(@response.body)) 205 | end 206 | 207 | def test_helper 208 | get(:helper) 209 | assert_response :success 210 | assert_equal RestGraph.default_app_id, @response.body.strip 211 | end 212 | 213 | def test_defaults 214 | get(:defaults) 215 | assert_response :success 216 | assert_equal 'true', @response.body.strip 217 | end 218 | 219 | def setup_cookies key 220 | cookies = {"#{key}_#{RestGraph.default_app_id}" => 'dummy'} 221 | stub(@controller).cookies{cookies} 222 | f = RestGraph.new 223 | stub(@controller).rest_graph{f} 224 | mock(f).parse_cookies!(cookies){} 225 | end 226 | 227 | def test_parse_cookies_fbs 228 | setup_cookies('fbs') 229 | get(:parse_cookies) 230 | end 231 | 232 | def test_parse_cookies_fbsr 233 | setup_cookies('fbsr') 234 | get(:parse_cookies) 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /example/rails3/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | 2 | ENV["RAILS_ENV"] = "test" 3 | require File.expand_path('../../config/environment', __FILE__) 4 | require 'rails/test_help' 5 | 6 | class ActiveSupport::TestCase 7 | def normalize_query query, amp='&' 8 | '?' + query[1..-1].split(amp).sort.join(amp) 9 | end 10 | 11 | def normalize_url url, amp='&' 12 | url.sub(/\?.+/){ |query| normalize_query(query, amp) } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /example/rails3/test/unit/rails_util_test.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'test_helper' 3 | require 'muack' 4 | 5 | class RailsUtilTest < ActiveSupport::TestCase 6 | include Muack::API 7 | 8 | def setup_mock url 9 | mock(RestGraph::RailsUtil).rest_graph_in_canvas?{ false } 10 | mock(RestGraph::RailsUtil).request{ 11 | mock(Object.new).url{ url }.object 12 | } 13 | end 14 | 15 | def test_rest_graph_normalized_request_uri_0 16 | setup_mock( 'http://test.com/?code=123&lang=en') 17 | assert_equal('http://test.com/?lang=en', 18 | RestGraph::RailsUtil.rest_graph_normalized_request_uri) 19 | end 20 | 21 | def test_rest_graph_normalized_request_uri_1 22 | setup_mock( 'http://test.com/?lang=en&code=123') 23 | assert_equal('http://test.com/?lang=en', 24 | RestGraph::RailsUtil.rest_graph_normalized_request_uri) 25 | end 26 | 27 | def test_rest_graph_normalized_request_uri_2 28 | setup_mock( 'http://test.com/?session=abc&lang=en&code=123') 29 | assert_equal('http://test.com/?lang=en', 30 | RestGraph::RailsUtil.rest_graph_normalized_request_uri) 31 | end 32 | 33 | def test_rest_graph_normalized_request_uri_3 34 | setup_mock( 'http://test.com/?code=123') 35 | assert_equal('http://test.com/', 36 | RestGraph::RailsUtil.rest_graph_normalized_request_uri) 37 | end 38 | 39 | def test_rest_graph_normalized_request_uri_4 40 | setup_mock( 'http://test.com/?signed_request=abc&code=123') 41 | assert_equal('http://test.com/', 42 | RestGraph::RailsUtil.rest_graph_normalized_request_uri) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /example/sinatra/config.ru: -------------------------------------------------------------------------------- 1 | 2 | require 'sinatra' 3 | require 'rest-graph' 4 | 5 | app_id = '123' 6 | secret = 'abc' 7 | config = {:app_id => app_id, 8 | :secret => secret} 9 | 10 | post '/' do 11 | rg = RestGraph.new(config) 12 | rg.parse_signed_request!(params['signed_request']) 13 | "#{rg.get('me').inspect.gsub('<', '<')}\n" 14 | end 15 | 16 | run Sinatra::Application 17 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-graph' 3 | -------------------------------------------------------------------------------- /lib/rest-graph.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-graph/core' 3 | require 'rest-graph/config_util' 4 | require 'rest-graph/facebook_util' 5 | require 'rest-graph/version' 6 | 7 | require 'rest-graph/rails_util' if Object.const_defined?(:Rails) 8 | -------------------------------------------------------------------------------- /lib/rest-graph/config_util.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'erb' 3 | require 'yaml' 4 | 5 | require 'rest-graph/core' 6 | 7 | module RestGraph::ConfigUtil 8 | extend self 9 | 10 | def load_config_for_all 11 | RestGraph::ConfigUtil.load_config_for_rails if 12 | Object.const_defined?(:Rails) 13 | end 14 | 15 | def load_config_for_rails app=Rails 16 | root = app.root 17 | file = ["#{root}/config/rest-graph.yaml", # YAML should use .yaml 18 | "#{root}/config/rest-graph.yml"].find{|path| File.exist?(path)} 19 | return unless file 20 | 21 | RestGraph::ConfigUtil.load_config(file, Rails.env) 22 | end 23 | 24 | def load_config file, env 25 | config = YAML.load(ERB.new(File.read(file)).result(binding)) 26 | defaults = config[env] 27 | return unless defaults 28 | 29 | mod = Module.new 30 | mod.module_eval(defaults.inject([]){ |r, (k, v)| 31 | # quote strings, leave others free (e.g. false, numbers, etc) 32 | r << <<-RUBY 33 | def default_#{k} 34 | #{v.kind_of?(String) ? "'#{v}'" : v} 35 | end 36 | RUBY 37 | }.join, __FILE__, __LINE__) 38 | 39 | RestGraph.send(:extend, mod) 40 | end 41 | end 42 | 43 | RestGraph.send(:extend, RestGraph::ConfigUtil) 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/rest-graph/facebook_util.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-graph/core' 3 | 4 | module RestGraph::FacebookUtil 5 | PERMISSIONS = %w[ 6 | publish_stream 7 | create_event 8 | rsvp_event 9 | sms 10 | offline_access 11 | publish_checkins 12 | 13 | user_about_me friends_about_me 14 | user_activities friends_activities 15 | user_birthday friends_birthday 16 | user_education_history friends_education_history 17 | user_events friends_events 18 | user_groups friends_groups 19 | user_hometown friends_hometown 20 | user_interests friends_interests 21 | user_likes friends_likes 22 | user_location friends_location 23 | user_notes friends_notes 24 | user_online_presence friends_online_presence 25 | user_photo_video_tags friends_photo_video_tags 26 | user_photos friends_photos 27 | user_relationships friends_relationships 28 | user_relationship_details friends_relationship_details 29 | user_religion_politics friends_religion_politics 30 | user_status friends_status 31 | user_videos friends_videos 32 | user_website friends_website 33 | user_work_history friends_work_history 34 | email 35 | read_friendlists manage_friendlists 36 | read_insights 37 | read_mailbox 38 | read_requests 39 | read_stream 40 | xmpp_login 41 | ads_management 42 | user_checkins friends_checkins 43 | 44 | manage_pages 45 | ] 46 | 47 | USER_PERMISSIONS = PERMISSIONS.reject{|perm| perm.start_with?('friends_')} 48 | 49 | def fix_fql_multi result 50 | result.inject({}){ |r, i| r[i['name']] = i['fql_result_set']; r } 51 | end 52 | 53 | def fix_permissions result 54 | # Hash[] is for ruby 1.8.7 55 | result.first && Hash[result.first.select{ |k, v| v == 1 }].keys 56 | end 57 | 58 | def permissions uid, selected_permissions=PERMISSIONS 59 | fix_permissions( 60 | fql(permissions_fql(uid, selected_permissions), {}, :secret => true)) 61 | end 62 | 63 | def user_permissions uid 64 | permissions(uid, USER_PERMISSIONS) 65 | end 66 | 67 | def permissions_fql uid, selected_permissions=PERMISSIONS 68 | sanitized_uid = uid.to_s.tr("'", '') 69 | selected = selected_permissions.join(',') 70 | "SELECT #{selected} FROM permissions where uid = '#{sanitized_uid}'" 71 | end 72 | end 73 | 74 | RestGraph.send(:include, RestGraph::FacebookUtil) 75 | -------------------------------------------------------------------------------- /lib/rest-graph/rails_util.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'cgi' 3 | require 'uri' 4 | 5 | require 'rest-graph/core' 6 | 7 | if Rails::VERSION::MAJOR >= 3 8 | class RestGraph 9 | class Railtie < Rails::Railtie 10 | initializer 'rest-graph' do |app| 11 | RestGraph::RailsUtil.init(app) 12 | end 13 | end 14 | end 15 | end 16 | 17 | # this cannot be put here because of load order, 18 | # so put in the bottom of this file to load up for rails2. 19 | # if Rails::VERSION::MAJOR == 2 20 | # ::RestGraph::RailsUtil.init(Rails) 21 | # end 22 | 23 | class RestGraph 24 | module DefaultAttributes 25 | def default_log_method ; Rails.logger.method(:debug); end 26 | def default_cache ; Rails.cache ; end 27 | def default_canvas ; '' ; end 28 | def default_iframe ; false; end 29 | def default_auto_authorize ; false; end 30 | def default_auto_authorize_options; {} ; end 31 | def default_auto_authorize_scope ; '' ; end 32 | def default_ensure_authorized ; false; end 33 | def default_write_session ; false; end 34 | def default_write_cookies ; false; end 35 | def default_write_handler ; nil; end 36 | def default_check_handler ; nil; end 37 | end 38 | 39 | module RailsCache 40 | def [] key ; read(key) ; end 41 | def []= key, value; write(key, value) ; end 42 | def store key, value, 43 | options={}; write(key, value, options); end 44 | end 45 | end 46 | 47 | module RestGraph::RailsUtil 48 | def self.init app=Rails 49 | ActiveSupport::Cache::Store.send(:include, RestGraph::RailsCache) 50 | RestGraph::ConfigUtil.load_config_for_rails(app) 51 | end 52 | 53 | module Helper 54 | def rest_graph 55 | controller.send(:rest_graph) 56 | end 57 | end 58 | 59 | def self.included controller 60 | # skip if included already, any better way to detect this? 61 | return if controller.respond_to?(:rest_graph, true) 62 | 63 | controller.rescue_from(RestGraph::Error::AccessToken, 64 | :with => :rest_graph_on_access_token_error) 65 | controller.helper(RestGraph::RailsUtil::Helper) 66 | controller.instance_methods.select{ |method| 67 | method.to_s =~ /^rest_graph/ 68 | }.each{ |method| controller.send(:protected, method) } 69 | end 70 | 71 | def rest_graph_setup options={} 72 | rest_graph_options_ctl.merge!(rest_graph_extract_options(options, :reject)) 73 | rest_graph_options_new.merge!(rest_graph_extract_options(options, :select)) 74 | 75 | # we'll need to reinitialize rest_graph with the new options, 76 | # otherwise if you're calling rest_graph before rest_graph_setup, 77 | # you'll end up with default options without the ones you've passed 78 | # into rest_graph_setup. 79 | rest_graph.send(:initialize, rest_graph_options_new) 80 | 81 | rest_graph_check_params_signed_request # canvas 82 | rest_graph_check_params_session # i think it would be deprecated 83 | rest_graph_check_code # oauth api 84 | rest_graph_check_rg_fbs # check rest-graph storage 85 | rest_graph_check_cookie # for js sdk (canvas or not) 86 | 87 | if rest_graph_oget(:ensure_authorized) && !rest_graph.authorized? 88 | rest_graph_authorize('ensure authorized') 89 | false # action halt, redirect to do authorize, 90 | # eagerly, as opposed to auto_authorize 91 | else 92 | true # keep going 93 | end 94 | end 95 | 96 | # override this if you need different app_id and secret 97 | def rest_graph 98 | @rest_graph ||= RestGraph.new(rest_graph_options_new) 99 | end 100 | 101 | def rest_graph_on_access_token_error error=nil 102 | rest_graph_authorize(error, false) 103 | end 104 | alias_method :rest_graph_on_error, # backward compatibility 105 | :rest_graph_on_access_token_error 106 | 107 | def rest_graph_authorize error=nil, force_redirect=true 108 | logger.info("INFO: RestGraph: #{error.inspect}") 109 | 110 | if force_redirect || rest_graph_auto_authorize? 111 | @rest_graph_authorize_url = rest_graph.authorize_url( 112 | {:redirect_uri => rest_graph_normalized_request_uri, 113 | :scope => rest_graph_oget(:auto_authorize_scope)}. 114 | merge(rest_graph_oget(:auto_authorize_options))) 115 | 116 | logger.debug("DEBUG: RestGraph: redirect to #{@rest_graph_authorize_url}") 117 | 118 | rest_graph_cleanup 119 | rest_graph_authorize_redirect 120 | end 121 | end 122 | 123 | # override this if you want the simple redirect_to 124 | def rest_graph_authorize_redirect 125 | unless rest_graph_in_canvas? 126 | redirect_to @rest_graph_authorize_url 127 | else 128 | rest_graph_js_redirect(@rest_graph_authorize_url, 129 | rest_graph_authorize_body) 130 | end 131 | end 132 | 133 | def rest_graph_js_redirect redirect_url, body='' 134 | render :inline => <<-HTML 135 | 137 | 138 | 139 | 142 | 147 | 148 | 149 | #{body} 150 | 151 | 152 | HTML 153 | end 154 | 155 | def rest_graph_authorize_body redirect_url=@rest_graph_authorize_url 156 | <<-HTML 157 |
158 | Please 159 | authorize 160 | if this page is not automatically redirected. 161 |
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 | -------------------------------------------------------------------------------- /lib/rest-graph/test_util.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-graph/core' 3 | require 'muack' 4 | 5 | require 'uri' 6 | 7 | module RestGraph::TestUtil 8 | extend Muack::API 9 | 10 | Methods = [:get, :delete, :post, :put] 11 | 12 | module_function 13 | def setup 14 | any_instance_of(RestGraph){ |rg| 15 | stub(rg).data{default_data} 16 | 17 | stub(rg).fetch(is_a(Hash) , is_a(String), 18 | is_a(Symbol), anything ){ |opts, uri, meth, payload| 19 | history << [meth, uri, payload] 20 | http = 'https?://[\w\d]+(\.[\w\d]+)+/' 21 | response = case uri 22 | when %r{#{http}method/fql.multiquery} 23 | RestGraph.json_decode( 24 | Rack::Utils.parse_query( 25 | URI.parse(opts[:uri] || uri).query)['queries']). 26 | keys.map{ |q| 27 | {'name' => q, 28 | 'fql_result_set' => [default_response]} 29 | } 30 | when %r{#{http}method/(\w+\.\w+)} 31 | case $2 32 | when 'friends.getAppUsers' 33 | [5678] 34 | else 35 | [default_response] 36 | end 37 | else 38 | default_response 39 | end 40 | RestGraph.json_encode(response) 41 | } 42 | } 43 | self 44 | end 45 | alias_method :before, :setup 46 | 47 | def teardown 48 | history.clear 49 | Muack.verify 50 | self 51 | end 52 | alias_method :after, :teardown 53 | 54 | def default_response 55 | @default_response ||= {'data' => []} 56 | end 57 | 58 | def default_data 59 | @default_data ||= {'uid' => '1234'} 60 | end 61 | 62 | self.class.module_eval{ 63 | attr_writer :default_response, :default_data 64 | } 65 | 66 | def history 67 | @history ||= [] 68 | end 69 | 70 | def login id=default_data['uid'] 71 | teardown 72 | setup 73 | 74 | uid = id.to_s 75 | expires = '123456789' 76 | app_id = RestGraph.default_app_id || '5678' 77 | session_key = "2.random_string.3600.#{expires}-#{uid}" 78 | salt = 'random-salt' 79 | access_token = "#{app_id}|#{session_key}|#{salt}" 80 | 81 | self.default_data = { 'uid' => uid, 82 | 'access_token' => access_token, 83 | 'session_key' => session_key} 84 | 85 | get('me'){ user(uid) } 86 | self 87 | end 88 | 89 | def user id 90 | { 'id' => id, 91 | 'name' => 'rest-graph stubbed-user', 92 | 'first_name' => 'rest-graph', 93 | 'last_name' => 'stubbed-user', 94 | 'link' => 'http://www.facebook.com/rest-graph', 95 | 'about' => 'this is a stubbed user in rest-graph', 96 | 'hometown' => {'id' => id*2, 'name' => 'Taiwan'}, 97 | 'bio' => 'A super simple Facebook Open Graph API client', 98 | 'quotes' => 'Write programs that do one thing and do it well.', 99 | 'timezone' => 8, 100 | 'locale' => 'en_US', 101 | 'verified' => true, 102 | 'updated_time' => '2010-05-07T15:04:08+0000'} 103 | end 104 | 105 | singleton_class.module_eval(Methods.map{ |meth| 106 | <<-RUBY 107 | def #{meth} *args, &block 108 | any_instance_of(RestGraph){ |rg| 109 | stub(rg).#{meth}(*args, &block) 110 | stub(rg).#{meth}(anything) 111 | } 112 | end 113 | RUBY 114 | }.join("\n"), __FILE__, __LINE__) 115 | end 116 | -------------------------------------------------------------------------------- /lib/rest-graph/version.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-graph/core' 3 | 4 | RestGraph::VERSION = '2.0.3' 5 | -------------------------------------------------------------------------------- /rest-graph.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # stub: rest-graph 2.0.3 ruby lib 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "rest-graph" 6 | s.version = "2.0.3" 7 | 8 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 9 | s.require_paths = ["lib"] 10 | s.authors = [ 11 | "Cardinal Blue", 12 | "Lin Jen-Shin (godfat)"] 13 | s.date = "2014-01-07" 14 | s.description = "A lightweight Facebook Graph API client\n\nWe have moved the development from rest-graph to [rest-more][].\nFrom now on, we would only fix bugs in rest-graph rather than adding\nfeatures, and we would only backport important changes from rest-more\nonce in a period. If you want the latest goodies, please see [rest-more][]\nOtherwise, you can stay with rest-graph with bugs fixes.\n\n[rest-more]: https://github.com/godfat/rest-more" 15 | s.email = ["godfat (XD) godfat.org"] 16 | s.files = [ 17 | ".gitignore", 18 | ".gitmodules", 19 | ".travis.yml", 20 | "CHANGES.md", 21 | "Gemfile", 22 | "LICENSE", 23 | "README.md", 24 | "Rakefile", 25 | "TODO.md", 26 | "doc/ToC.md", 27 | "doc/dependency.md", 28 | "doc/design.md", 29 | "doc/heroku-facebook.md", 30 | "doc/rails.md", 31 | "doc/test.md", 32 | "doc/tutorial.md", 33 | "example/multi/config.ru", 34 | "example/multi/rainbows.rb", 35 | "example/rails3/Gemfile", 36 | "example/rails3/Rakefile", 37 | "example/rails3/app/controllers/application_controller.rb", 38 | "example/rails3/app/views/application/helper.html.erb", 39 | "example/rails3/config.ru", 40 | "example/rails3/config/application.rb", 41 | "example/rails3/config/boot.rb", 42 | "example/rails3/config/environment.rb", 43 | "example/rails3/config/environments/development.rb", 44 | "example/rails3/config/environments/production.rb", 45 | "example/rails3/config/environments/test.rb", 46 | "example/rails3/config/initializers/secret_token.rb", 47 | "example/rails3/config/initializers/session_store.rb", 48 | "example/rails3/config/rest-graph.yaml", 49 | "example/rails3/config/routes.rb", 50 | "example/rails3/test/functional/application_controller_test.rb", 51 | "example/rails3/test/test_helper.rb", 52 | "example/rails3/test/unit/rails_util_test.rb", 53 | "example/sinatra/config.ru", 54 | "init.rb", 55 | "lib/rest-graph.rb", 56 | "lib/rest-graph/config_util.rb", 57 | "lib/rest-graph/core.rb", 58 | "lib/rest-graph/facebook_util.rb", 59 | "lib/rest-graph/rails_util.rb", 60 | "lib/rest-graph/test_util.rb", 61 | "lib/rest-graph/version.rb", 62 | "rest-graph.gemspec", 63 | "task/README.md", 64 | "task/gemgem.rb", 65 | "test/common.rb", 66 | "test/config/rest-graph.yaml", 67 | "test/test_api.rb", 68 | "test/test_cache.rb", 69 | "test/test_default.rb", 70 | "test/test_error.rb", 71 | "test/test_facebook.rb", 72 | "test/test_handler.rb", 73 | "test/test_load_config.rb", 74 | "test/test_misc.rb", 75 | "test/test_multi.rb", 76 | "test/test_oauth.rb", 77 | "test/test_old.rb", 78 | "test/test_page.rb", 79 | "test/test_parse.rb", 80 | "test/test_rest-graph.rb", 81 | "test/test_serialize.rb", 82 | "test/test_test_util.rb", 83 | "test/test_timeout.rb"] 84 | s.homepage = "https://github.com/godfat/rest-graph" 85 | s.licenses = ["Apache License 2.0"] 86 | s.rubygems_version = "2.2.0" 87 | s.summary = "A lightweight Facebook Graph API client" 88 | s.test_files = [ 89 | "test/test_api.rb", 90 | "test/test_cache.rb", 91 | "test/test_default.rb", 92 | "test/test_error.rb", 93 | "test/test_facebook.rb", 94 | "test/test_handler.rb", 95 | "test/test_load_config.rb", 96 | "test/test_misc.rb", 97 | "test/test_multi.rb", 98 | "test/test_oauth.rb", 99 | "test/test_old.rb", 100 | "test/test_page.rb", 101 | "test/test_parse.rb", 102 | "test/test_rest-graph.rb", 103 | "test/test_serialize.rb", 104 | "test/test_test_util.rb", 105 | "test/test_timeout.rb"] 106 | end 107 | -------------------------------------------------------------------------------- /test/common.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-graph' 3 | 4 | # need to require this before webmock in order to enable mocking in em-http 5 | require 'em-http-request' 6 | 7 | require 'webmock' 8 | require 'muack' 9 | require 'bacon' 10 | 11 | # for testing lighten (serialization) 12 | require 'yaml' 13 | 14 | WebMock.disable_net_connect! 15 | Bacon.summary_on_exit 16 | Bacon::Context.__send__(:include, Muack::API, WebMock::API) 17 | 18 | module TestHelper 19 | module_function 20 | def ensure_rollback 21 | yield 22 | 23 | ensure # the defaults should remain the same! 24 | RestGraph.send(:extend, RestGraph::DefaultAttributes.dup) 25 | 26 | TestHelper.attrs_no_callback.each{ |name| 27 | RestGraph.new.send(name).should == 28 | RestGraph::DefaultAttributes.send("default_#{name}") 29 | } 30 | end 31 | 32 | def normalize_query query 33 | '?' + query[1..-1].split('&').sort.join('&') 34 | end 35 | 36 | def normalize_url url 37 | url.sub(/\?.+/){ |query| TestHelper.normalize_query(query) } 38 | end 39 | 40 | def attrs_no_callback 41 | RestGraph::Attributes.reject{ |attr| 42 | attr.to_s =~ /_handler/ 43 | } 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/config/rest-graph.yaml: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | app_id: 41829 4 | secret: <%= 'r41829'.reverse %> 5 | auto_decode: false 6 | lang: zh-tw 7 | auto_authorize_scope: 'publish_stream' 8 | -------------------------------------------------------------------------------- /test/test_api.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | describe RestGraph do 9 | after do 10 | WebMock.reset! 11 | Muack.verify 12 | end 13 | 14 | should 'generate correct url' do 15 | TestHelper.normalize_url( 16 | RestGraph.new(:access_token => 'awesome').url('path', :query => 'str')). 17 | should == 18 | 'https://graph.facebook.com/path?access_token=awesome&query=str' 19 | end 20 | 21 | should 'request to correct server' do 22 | stub_request(:get, 'http://nothing.godfat.org/me').with( 23 | :headers => {'Accept' => 'text/plain', 24 | 'Accept-Language' => 'zh-tw', 25 | 'Accept-Encoding' => 'gzip, deflate', # this is by ruby 26 | }.merge(RUBY_VERSION < '1.9.2' ? 27 | {} : 28 | {'User-Agent' => 'Ruby'})). # this is by ruby 29 | to_return(:body => '{"data": []}') 30 | 31 | RestGraph.new(:graph_server => 'http://nothing.godfat.org/', 32 | :lang => 'zh-tw', 33 | :accept => 'text/plain').get('me').should == {'data' => []} 34 | end 35 | 36 | should 'pass custom headers' do 37 | stub_request(:get, 'http://example.com/').with( 38 | :headers => {'Accept' => 'text/javascript', 39 | 'Accept-Language' => 'en-us', 40 | 'Accept-Encoding' => 'gzip, deflate', # this is by ruby 41 | 'X-Forwarded-For' => '127.0.0.1', 42 | }.merge(RUBY_VERSION < '1.9.2' ? 43 | {} : 44 | {'User-Agent' => 'Ruby'})). # this is by ruby 45 | to_return(:body => '{"data": []}') 46 | 47 | RestGraph.new.request({:headers => {'X-Forwarded-For' => '127.0.0.1'}}, 48 | [:get, 'http://example.com']). 49 | should == {'data' => []} 50 | 51 | EM.run{ 52 | RestGraph.new.request({:headers => {'X-Forwarded-For' => '127.0.0.1', 53 | 'User-Agent' => 'Ruby', 54 | 'Accept-Encoding' => 55 | 'gzip, deflate'}, 56 | :async => true}, 57 | [:get, 'http://example.com']){ |result| 58 | result.should == {'data' => []} 59 | EM.stop 60 | } 61 | } if RUBY_ENGINE != 'jruby' # eventmachine for jruby is broken 62 | end 63 | 64 | should 'post right' do 65 | stub_request(:post, 'https://graph.facebook.com/feed/me'). 66 | with(:body => 'message=hi%20there').to_return(:body => 'ok') 67 | 68 | RestGraph.new(:auto_decode => false). 69 | post('feed/me', :message => 'hi there').should == 'ok' 70 | end 71 | 72 | should 'use secret_access_token' do 73 | stub_request(:get, 74 | 'https://graph.facebook.com/me?access_token=1|2'). 75 | to_return(:body => 'ok') 76 | 77 | rg = RestGraph.new(:auto_decode => false, :access_token => 'wrong', 78 | :app_id => '1', :secret => '2') 79 | rg.get('me', {}, :secret => true).should == 'ok' 80 | rg.url('me', {}, rg.graph_server, :secret => true).should == 81 | 'https://graph.facebook.com/me?access_token=1%7C2' 82 | end 83 | 84 | should 'suppress auto-decode in an api call' do 85 | stub_request(:get, 'https://graph.facebook.com/woot'). 86 | to_return(:body => 'bad json') 87 | 88 | rg = RestGraph.new(:auto_decode => true) 89 | rg.get('woot', {}, :auto_decode => false).should == 'bad json' 90 | rg.auto_decode.should == true 91 | end 92 | 93 | should 'call post_request after request' do 94 | url = 'https://graph.facebook.com/feed/me' 95 | stub_request(:put, url). 96 | with(:body => 'message=hi%20there').to_return(:body => '[]') 97 | 98 | mock(rg = RestGraph.new).post_request({}, url, '[]') 99 | rg.put('feed/me', :message => 'hi there'). 100 | should == [] 101 | end 102 | 103 | should 'not raise exception when encountering error' do 104 | [500, 401, 402, 403].each{ |status| 105 | stub_request(:delete, 'https://graph.facebook.com/123').to_return( 106 | :body => '[]', :status => status) 107 | 108 | RestGraph.new.delete('123').should == [] 109 | } 110 | end 111 | 112 | should 'convert query to string' do 113 | mock(o = Object.new).to_s{ 'i am mock' } 114 | stub_request(:get, "https://graph.facebook.com/search?q=i%20am%20mock"). 115 | to_return(:body => 'ok') 116 | RestGraph.new(:auto_decode => false).get('search', :q => o).should == 'ok' 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /test/test_cache.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | describe RestGraph do 9 | after do 10 | WebMock.reset! 11 | Muack.verify 12 | end 13 | 14 | describe 'cache' do 15 | before do 16 | @url, @body = "https://graph.facebook.com/cache", '{"message":"ok"}' 17 | @cache = {} 18 | @rg = RestGraph.new(:cache => @cache, :auto_decode => false) 19 | stub_request(:get, @url).to_return(:body => @body).times(1) 20 | end 21 | 22 | should 'enable cache if passing cache' do 23 | 3.times{ @rg.get('cache').should == @body } 24 | @cache.should == {@rg.send(:cache_key, {}, @url) => @body} 25 | end 26 | 27 | should 'respect expires_in' do 28 | mock(@cache).method(:store){ mock(Object.new).arity{ -3 }.object } 29 | mock(@cache).store(@rg.send(:cache_key, {}, @url), @body, 30 | :expires_in => 3){} 31 | @rg.get('cache', {}, :expires_in => 3).should == @body 32 | end 33 | 34 | should 'update cache if there is cache option set to false' do 35 | @rg.get('cache') .should == @body 36 | stub_request(:get, @url).to_return(:body => @body.reverse).times(2) 37 | @rg.get('cache') .should == @body 38 | @rg.get('cache', {}, :cache => false).should == @body.reverse 39 | @rg.get('cache') .should == @body.reverse 40 | @rg.cache = nil 41 | @rg.get('cache', {}, :cache => false).should == @body.reverse 42 | end 43 | end 44 | 45 | should 'not cache post/put/delete' do 46 | [:put, :post, :delete].each{ |meth| 47 | url, body = "https://graph.facebook.com/cache", '{"message":"ok"}' 48 | stub_request(meth, url).to_return(:body => body).times(3) 49 | 50 | cache = {} 51 | rg = RestGraph.new(:cache => cache) 52 | 3.times{ 53 | if meth == :delete 54 | rg.send(meth, 'cache').should == {'message' => 'ok'} 55 | else 56 | rg.send(meth, 'cache', 'payload').should == {'message' => 'ok'} 57 | end 58 | } 59 | cache.should == {} 60 | } 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/test_default.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | describe RestGraph do 9 | should 'honor default attributes' do 10 | RestGraph.members.reject{ |name| 11 | name.to_s =~ /method$|handler$|detector$/ }.each{ |name| 12 | RestGraph.new.send(name).should == 13 | RestGraph .send("default_#{name}") 14 | } 15 | end 16 | 17 | should 'use module to override default attributes' do 18 | klass = RestGraph.dup 19 | klass.send(:extend, Module.new do 20 | def default_app_id 21 | '1829' 22 | end 23 | end) 24 | 25 | klass.new.app_id.should == '1829' 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/test_error.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | describe RestGraph::Error do 9 | after do 10 | WebMock.reset! 11 | Muack.verify 12 | end 13 | 14 | should 'have the right ancestors' do 15 | RestGraph::Error::AccessToken .should < RestGraph::Error 16 | 17 | RestGraph::Error::InvalidAccessToken.should < 18 | RestGraph::Error::AccessToken 19 | 20 | RestGraph::Error::MissingAccessToken.should < 21 | RestGraph::Error::AccessToken 22 | end 23 | 24 | should 'parse right' do 25 | %w[OAuthInvalidTokenException OAuthException].each{ |type| 26 | RestGraph::Error.parse('error' => {'type' => type}). 27 | should.kind_of?(RestGraph::Error::InvalidAccessToken) 28 | } 29 | 30 | RestGraph::Error.parse('error' => {'type' => 'QueryParseException', 31 | 'message' => 'An active access token..'}). 32 | should.kind_of?(RestGraph::Error::MissingAccessToken) 33 | 34 | RestGraph::Error.parse('error' => {'type' => 'QueryParseException', 35 | 'message' => 'Oh active access token..'}). 36 | should.not.kind_of?(RestGraph::Error::MissingAccessToken) 37 | 38 | RestGraph::Error.parse('error_code' => 190). 39 | should.kind_of?(RestGraph::Error::InvalidAccessToken) 40 | 41 | RestGraph::Error.parse('error_code' => 104). 42 | should.kind_of?(RestGraph::Error::MissingAccessToken) 43 | 44 | RestGraph::Error.parse('error_code' => 999). 45 | should.not.kind_of?(RestGraph::Error::AccessToken) 46 | 47 | error = RestGraph::Error.parse(['not a hash']) 48 | error.should.not.kind_of?(RestGraph::Error::AccessToken) 49 | error.should .kind_of?(RestGraph::Error) 50 | end 51 | 52 | should 'nuke cache upon errors' do 53 | stub_request(:get, 'https://graph.facebook.com/me'). 54 | to_return(:body => '{"error":"wrong"}').times(2) 55 | 56 | rg = RestGraph.new(:cache => {}, :error_handler => lambda{|e,u|}) 57 | rg.get('me'); rg.get('me') 58 | rg.cache.values.should == [nil] 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/test_facebook.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | require 'rest-graph/facebook_util' 9 | 10 | describe RestGraph::FacebookUtil do 11 | after do 12 | Muack.verify 13 | end 14 | 15 | before do 16 | @res = [{'publish_stream' => 1, 'email' => 0}] 17 | end 18 | 19 | should 'fix_permission' do 20 | RestGraph.new.fix_permissions(@res).should == %w[publish_stream] 21 | end 22 | 23 | should 'fix_fql_multi' do 24 | RestGraph.new.fix_fql_multi([{'name'=>'a', 'fql_result_set'=> @res}]). 25 | should == {'a' => @res} 26 | end 27 | 28 | should 'permissions' do 29 | mock(rg = RestGraph.new).fql( 30 | rg.permissions_fql(1234, 31 | RestGraph::FacebookUtil::PERMISSIONS), {}, :secret => true 32 | ){ @res } 33 | 34 | rg.permissions(1234).should == %w[publish_stream] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/test_handler.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | describe RestGraph do 9 | after do 10 | WebMock.reset! 11 | Muack.verify 12 | end 13 | 14 | describe 'log handler' do 15 | should 'log whenever doing network request' do 16 | stub_request(:get, 'https://graph.facebook.com/me'). 17 | to_return(:body => '{}') 18 | 19 | begin # workaround polluting Time object, which would break rbx 20 | time = Object.new 21 | mock(time).now{ 666 } 22 | mock(time).now{ 999 } 23 | RestGraph.const_set(:Time, time) 24 | 25 | logger = [] 26 | rg = RestGraph.new(:log_handler => lambda{ |e| 27 | logger << [e.duration, e.url] }) 28 | rg.get('me') 29 | 30 | logger.last.should == [333, 'https://graph.facebook.com/me'] 31 | ensure 32 | RestGraph.send(:remove_const, :Time) 33 | end 34 | end 35 | end 36 | 37 | describe 'with Graph API' do 38 | before do 39 | @id = lambda{ |obj, url| obj } 40 | @error = '{"error":{"type":"Exception","message":"(#2500)"}}' 41 | @error_hash = RestGraph.json_decode(@error) 42 | 43 | stub_request(:get, 'https://graph.facebook.com/me'). 44 | to_return(:body => @error) 45 | end 46 | 47 | should 'call error_handler if error occurred' do 48 | RestGraph.new(:error_handler => @id).get('me').should == @error_hash 49 | end 50 | 51 | should 'raise ::RestGraph::Error in default error_handler' do 52 | begin 53 | RestGraph.new.get('me') 54 | rescue ::RestGraph::Error => e 55 | e.error .should == @error_hash 56 | e.message.should == 57 | "#{@error_hash.inspect} from https://graph.facebook.com/me" 58 | end 59 | end 60 | end 61 | 62 | describe 'with FQL API' do 63 | # Example of an actual response (without newline) 64 | # {"error_code":603,"error_msg":"Unknown table: bad_table", 65 | # "request_args":[{"key":"method","value":"fql.query"}, 66 | # {"key":"format","value":"json"}, 67 | # {"key":"query","value": 68 | # "SELECT name FROM bad_table WHERE uid=12345"}]} 69 | before do 70 | @id = lambda{ |obj, url| obj } 71 | @fql_error = '{"error_code":603,"error_msg":"Unknown table: bad"}' 72 | @fql_error_hash = RestGraph.json_decode(@fql_error) 73 | 74 | @bad_fql_query = 'SELECT name FROM bad_table WHERE uid="12345"' 75 | bad_fql_request = "https://api.facebook.com/method/fql.query?" \ 76 | "format=json&query=#{CGI.escape(@bad_fql_query)}" 77 | 78 | stub_request(:get, bad_fql_request).to_return(:body => @fql_error) 79 | end 80 | 81 | should 'call error_handler if error occurred' do 82 | RestGraph.new(:error_handler => @id).fql(@bad_fql_query). 83 | should == @fql_error_hash 84 | end 85 | 86 | should 'raise ::RestGraph::Error in default error_handler' do 87 | begin 88 | RestGraph.new.fql(@bad_fql_query) 89 | rescue ::RestGraph::Error => e 90 | e.error .should == @fql_error_hash 91 | e.message.should.start_with?( 92 | "#{@fql_error_hash.inspect} from " \ 93 | "https://api.facebook.com/method/fql.query?") 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/test_load_config.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | require 'rest-graph/config_util' 9 | 10 | describe RestGraph::ConfigUtil do 11 | 12 | after do 13 | Muack.verify 14 | end 15 | 16 | should 'honor rails config' do 17 | ::Rails = Object.new 18 | mock(Rails).env { 'test' }.times(2) 19 | mock(Rails).root{ File.dirname(__FILE__) }.times(2) 20 | 21 | check = lambda{ 22 | RestGraph.default_app_id.should == 41829 23 | RestGraph.default_secret.should == 'r41829'.reverse 24 | RestGraph.default_auto_decode.should == false 25 | RestGraph.default_lang.should == 'zh-tw' 26 | } 27 | 28 | [RestGraph::ConfigUtil, RestGraph].each{ |const| 29 | TestHelper.ensure_rollback{ 30 | const.load_config_for_rails 31 | check.call 32 | } 33 | 34 | TestHelper.ensure_rollback{ 35 | const.load_config( 36 | "#{File.dirname(__FILE__)}/config/rest-graph.yaml", 37 | 'test') 38 | check.call 39 | } 40 | } 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/test_misc.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | describe RestGraph do 9 | after do 10 | WebMock.reset! 11 | Muack.verify 12 | end 13 | 14 | should 'return true in authorized? if there is an access_token' do 15 | RestGraph.new(:access_token => '1').authorized?.should == true 16 | RestGraph.new(:access_token => nil).authorized?.should == false 17 | end 18 | 19 | should 'treat oauth_token as access_token as well' do 20 | rg = RestGraph.new 21 | hate_facebook = 'why the hell two different name?' 22 | rg.data['oauth_token'] = hate_facebook 23 | rg.authorized?.should == true 24 | rg.access_token == hate_facebook 25 | end 26 | 27 | should 'build correct headers' do 28 | rg = RestGraph.new(:accept => 'text/html', 29 | :lang => 'zh-tw') 30 | rg.send(:build_headers).should == {'Accept' => 'text/html', 31 | 'Accept-Language' => 'zh-tw'} 32 | end 33 | 34 | should 'build empty query string' do 35 | RestGraph.new.send(:build_query_string).should == '' 36 | end 37 | 38 | should 'create access_token in query string' do 39 | RestGraph.new(:access_token => 'token').send(:build_query_string). 40 | should == '?access_token=token' 41 | end 42 | 43 | should 'build correct query string' do 44 | TestHelper.normalize_query( 45 | RestGraph.new(:access_token => 'token').send(:build_query_string, 46 | :message => 'hi!!')). 47 | should == '?access_token=token&message=hi%21%21' 48 | 49 | TestHelper.normalize_query( 50 | RestGraph.new.send(:build_query_string, :message => 'hi!!', 51 | :subject => '(&oh&)')). 52 | should == '?message=hi%21%21&subject=%28%26oh%26%29' 53 | end 54 | 55 | should 'auto decode json' do 56 | RestGraph.new(:auto_decode => true). 57 | send(:post_request, {}, '', '[]').should == [] 58 | end 59 | 60 | should 'not auto decode json' do 61 | RestGraph.new(:auto_decode => false). 62 | send(:post_request, {}, '', '[]').should == '[]' 63 | end 64 | 65 | should 'give attributes' do 66 | RestGraph.new(:auto_decode => false).attributes.keys.map(&:to_s).sort. 67 | should == RestGraphStruct.members.map(&:to_s).sort 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/test_multi.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | describe 'RestGraph#multi' do 9 | after do 10 | WebMock.reset! 11 | Muack.verify 12 | end 13 | 14 | should 'do multi query with em-http-request' do 15 | url = 'https://graph.facebook.com/me' 16 | stub_request(:get, url).to_return(:body => '{"data":"get"}') 17 | stub_request(:put, url).to_return(:body => '{"data":"put"}') 18 | rg = RestGraph.new 19 | mock(rg).request_em(anything, anything) 20 | EM.run{ 21 | rg.multi([[:get, 'me'], [:put, 'me']]){ |results| 22 | results.should == [{'data' => 'get'}, {'data' => 'put'}] 23 | EM.stop 24 | } 25 | } 26 | end 27 | 28 | should 'call aget, aput family with multi' do 29 | url = 'https://graph.facebook.com/me' 30 | %w[aget adelete apost aput].each{ |meth| 31 | stub_request("#{meth[1..-1]}".to_sym, url). 32 | to_return(:body => "{\"data\":\"#{meth}\"}") 33 | rg = RestGraph.new 34 | mock(rg).request_em(anything, anything) 35 | EM.run{ 36 | rg.send(meth, 'me', {}){ |result| 37 | result.should == {'data' => meth.to_s} 38 | EM.stop 39 | } 40 | } 41 | } 42 | end 43 | 44 | should 'for_pages' do 45 | rg = RestGraph.new 46 | 47 | args = [is_a(Hash), is_a(Array)] 48 | flag = false 49 | stub(rg).request_em(*args).peek_return{ |r| flag = true; r } 50 | 51 | %w[next previous].each{ |type| 52 | kind = "#{type}_page" 53 | data = {'paging' => {type => 'http://z'}, 'data' => ['z']} 54 | 55 | # invalid pages or just the page itself 56 | # not really need network 57 | nils = 0 58 | ranges = -1..1 59 | ranges.each{ |page| 60 | rg.for_pages(data, page, {:async => true}, kind){ |r| 61 | if r 62 | r.should == data 63 | else 64 | nils += 1 65 | end 66 | }.should == data 67 | } 68 | nils.should == ranges.to_a.size 69 | 70 | (2..4).each{ |pages| 71 | # merge data 72 | stub_request(:get, 'z').to_return(:body => '{"data":["y"]}') 73 | expects = [{'data' => %w[y]}, nil] 74 | 75 | EM.run{ 76 | rg.for_pages(data, pages, {:async => true}, kind){ |r| 77 | r.should == expects.shift 78 | EM.stop if expects.empty? 79 | } 80 | } 81 | 82 | # this data cannot be merged 83 | stub_request(:get, 'z').to_return(:body => '{"data":"y"}') 84 | expects = [{'data' => 'y'}, nil] 85 | 86 | EM.run{ 87 | rg.for_pages(data, pages, {:async => true}, kind){ |r| 88 | r.should == expects.shift 89 | EM.stop if expects.empty? 90 | } 91 | } 92 | } 93 | 94 | stub_request(:get, 'z').to_return(:body => 95 | '{"paging":{"'+type+'":"http://yyy"},"data":["y"]}') 96 | stub_request(:get, 'yyy').to_return(:body => '{"data":["x"]}') 97 | expects = [{'data' => %w[y]}, {'data' => %w[x]}, nil] 98 | 99 | EM.run{ 100 | rg.for_pages(data, 3, {:async => true}, kind){ |rr| 101 | rr.frozen?.should == true unless rr.nil? 102 | if rr 103 | r = rr.dup 104 | r.delete('paging') 105 | else 106 | r = rr 107 | end 108 | r.should == expects.shift 109 | EM.stop if expects.empty? 110 | } 111 | } 112 | } 113 | 114 | flag.should == true 115 | end 116 | 117 | # should 'cache in multi' do 118 | # end 119 | # 120 | # should 'logging' do 121 | # end 122 | # 123 | # should 'error handler?' do 124 | # end 125 | end if RUBY_ENGINE != 'jruby' # eventmachine for jruby is broken 126 | -------------------------------------------------------------------------------- /test/test_oauth.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | describe RestGraph do 9 | before do 10 | @rg = RestGraph.new(:app_id => '29', :secret => '18') 11 | @uri = 'http://zzz.tw' 12 | end 13 | 14 | after do 15 | WebMock.reset! 16 | end 17 | 18 | should 'return correct oauth url' do 19 | TestHelper.normalize_url(@rg.authorize_url(:redirect_uri => @uri)). 20 | should == 'https://graph.facebook.com/oauth/authorize?' \ 21 | 'client_id=29&redirect_uri=http%3A%2F%2Fzzz.tw' 22 | end 23 | 24 | should 'do authorizing and parse result and save it in data' do 25 | stub_request(:post, 'https://graph.facebook.com/oauth/access_token'). \ 26 | with(:body => {'client_id' => '29' , 27 | 'client_secret' => '18' , 28 | 'redirect_uri' => 'http://zzz.tw', 29 | 'code' => 'zzz'}). 30 | to_return(:body => 'access_token=baken&expires=2918') 31 | 32 | result = {'access_token' => 'baken', 'expires' => '2918'} 33 | 34 | @rg.authorize!(:redirect_uri => @uri, :code => 'zzz'). 35 | should == result 36 | @rg.data.should == result 37 | end 38 | 39 | should 'not append access_token in authorize_url even presented' do 40 | RestGraph.new(:access_token => 'do not use me').authorize_url. 41 | should == 'https://graph.facebook.com/oauth/authorize' 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /test/test_old.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | describe RestGraph do 9 | after do 10 | WebMock.reset! 11 | Muack.verify 12 | end 13 | 14 | should 'do fql query with/without access_token' do 15 | fql = 'SELECT name FROM likes where id="123"' 16 | query = "format=json&query=#{CGI.escape(fql)}" 17 | stub_request(:get, "https://api.facebook.com/method/fql.query?#{query}"). 18 | to_return(:body => '[]') 19 | 20 | RestGraph.new.fql(fql).should == [] 21 | 22 | token = 'token'.reverse 23 | stub_request(:get, "https://api.facebook.com/method/fql.query?#{query}" \ 24 | "&access_token=#{token}"). 25 | to_return(:body => '[]') 26 | 27 | RestGraph.new(:access_token => token).fql(fql).should == [] 28 | end 29 | 30 | should 'do fql.mutilquery correctly' do 31 | f0 = 'SELECT display_name FROM application WHERE app_id="233082465238"' 32 | f1 = 'SELECT display_name FROM application WHERE app_id="110225210740"' 33 | f0q, f1q = "\"#{f0.gsub('"', '\\"')}\"", "\"#{f1.gsub('"', '\\"')}\"" 34 | q = "format=json&queries=#{CGI.escape("{\"f0\":#{f0q},\"f1\":#{f1q}}")}" 35 | p = "format=json&queries=#{CGI.escape("{\"f1\":#{f1q},\"f0\":#{f0q}}")}" 36 | 37 | stub_multi = lambda{ 38 | stub_request(:get, 39 | "https://api.facebook.com/method/fql.multiquery?#{q}"). 40 | to_return(:body => '[]') 41 | 42 | stub_request(:get, 43 | "https://api.facebook.com/method/fql.multiquery?#{p}"). 44 | to_return(:body => '[]') 45 | } 46 | 47 | stub_multi.call 48 | RestGraph.new.fql_multi(:f0 => f0, :f1 => f1).should == [] 49 | end 50 | 51 | should 'cache fake post in fql' do 52 | query = 'select name from user where uid = 4' 53 | body = '[{"name":"Mark Zuckerberg"}]' 54 | stub_request(:post, 55 | 'https://api.facebook.com/method/fql.query?format=json'). 56 | with(:body => {:query => query}). 57 | to_return(:body => body) 58 | 59 | RestGraph.new(:cache => (cache = {})).fql(query, {}, :post => true). 60 | first['name'] .should == 'Mark Zuckerberg' 61 | cache.size .should == 1 62 | cache.values.first.should == body 63 | 64 | WebMock.reset! # should hit the cache 65 | 66 | RestGraph.new(:cache => cache).fql(query, {}, :post => true). 67 | first['name'] .should == 'Mark Zuckerberg' 68 | cache.size .should == 1 69 | cache.values.first.should == body 70 | 71 | # query changed 72 | should.raise(WebMock::NetConnectNotAllowedError) do 73 | RestGraph.new(:cache => cache).fql(query.upcase, {}, :post => true) 74 | end 75 | 76 | # cache should work for normal get 77 | RestGraph.new(:cache => cache).fql(query). 78 | first['name'] .should == 'Mark Zuckerberg' 79 | cache.size .should == 1 80 | cache.values.first.should == body 81 | end 82 | 83 | should 'do facebook old rest api' do 84 | body = 'hate facebook inconsistent' 85 | stub_request(:get, 86 | 'https://api.facebook.com/method/notes.create?format=json'). 87 | to_return(:body => body) 88 | 89 | RestGraph.new.old_rest('notes.create', {}, :auto_decode => false). 90 | should == body 91 | end 92 | 93 | should 'exchange sessions for access token' do 94 | stub_request(:post, 95 | 'https://graph.facebook.com/oauth/exchange_sessions?' \ 96 | 'type=client_cred&client_id=id&client_secret=di&' \ 97 | 'sessions=bad%20bed'). 98 | to_return(:body => '[{"access_token":"bogus"}]') 99 | 100 | RestGraph.new(:app_id => 'id', 101 | :secret => 'di'). 102 | exchange_sessions(:sessions => 'bad bed'). 103 | first['access_token'].should == 'bogus' 104 | end 105 | 106 | should 'use an secret access_token' do 107 | stub_request(:get, 108 | 'https://api.facebook.com/method/admin.getAppProperties?' \ 109 | 'access_token=123%7Cs&format=json&properties=app_id' 110 | ).to_return(:body => '{"app_id":"123"}') 111 | 112 | RestGraph.new(:app_id => '123', :secret => 's'). 113 | secret_old_rest('admin.getAppProperties', :properties => 'app_id'). 114 | should == {'app_id' => '123'} 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/test_page.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | describe RestGraph do 9 | after do 10 | WebMock.reset! 11 | Muack.verify 12 | end 13 | 14 | should 'get the next/prev page' do 15 | rg = RestGraph.new 16 | %w[next previous].each{ |type| 17 | kind = "#{type}_page" 18 | rg.send(kind, {}) .should == nil 19 | rg.send(kind, {'paging' => []}).should == nil 20 | rg.send(kind, {'paging' => {}}).should == nil 21 | 22 | stub_request(:get, 'zzz').to_return(:body => '["ok"]') 23 | rg.send(kind, {'paging' => {type => 'zzz'}}).should == ['ok'] 24 | } 25 | end 26 | 27 | should 'merge all pages into one' do 28 | rg = RestGraph.new 29 | %w[next previous].each{ |type| 30 | kind = "#{type}_page" 31 | data = {'paging' => {type => 'zzz'}, 'data' => ['z']} 32 | 33 | # invalid pages or just the page itself 34 | (-1..1).each{ |page| 35 | rg.for_pages(data, page, {}, kind).should == data 36 | } 37 | 38 | (2..4).each{ |pages| 39 | # merge data 40 | stub_request(:get, 'zzz').to_return(:body => '{"data":["y"]}') 41 | rg.for_pages(data, pages, {}, kind).should == {'data' => %w[z y]} 42 | 43 | # this data cannot be merged 44 | stub_request(:get, 'zzz').to_return(:body => '{"data":"y"}') 45 | rg.for_pages(data, pages, {}, kind).should == {'data' => %w[z]} 46 | } 47 | 48 | stub_request(:get, 'zzz').to_return(:body => 49 | '{"paging":{"'+type+'":"yyy"},"data":["y"]}') 50 | stub_request(:get, 'yyy').to_return(:body => '{"data":["x"]}') 51 | 52 | rg.for_pages(data, 3, {}, kind).should == {'data' => %w[z y x]} 53 | } 54 | end 55 | 56 | should 'for_pages with callback' do 57 | rg = RestGraph.new 58 | %w[next previous].each{ |type| 59 | kind = "#{type}_page" 60 | data = {'paging' => {type => 'zzz'}, 'data' => ['z']} 61 | 62 | # invalid pages or just the page itself 63 | nils = 0 64 | ranges = -1..1 65 | ranges.each{ |page| 66 | rg.for_pages(data, page, {}, kind){ |r| 67 | if r 68 | r.should == data 69 | else 70 | nils += 1 71 | end 72 | }.should == data 73 | } 74 | nils.should == ranges.to_a.size 75 | 76 | (2..4).each{ |pages| 77 | # merge data 78 | stub_request(:get, 'zzz').to_return(:body => '{"data":["y"]}') 79 | expects = [{'data' => %w[y]}, nil] 80 | rg.for_pages(data, pages, {}, kind){ |r| 81 | r.should == expects.shift 82 | }.should == {'data' => %w[z y]} 83 | expects.empty?.should == true 84 | 85 | # this data cannot be merged 86 | stub_request(:get, 'zzz').to_return(:body => '{"data":"y"}') 87 | expects = [{'data' => 'y'}, nil] 88 | rg.for_pages(data, pages, {}, kind){ |r| 89 | r.should == expects.shift 90 | }.should == {'data' => %w[z]} 91 | expects.empty?.should == true 92 | } 93 | 94 | stub_request(:get, 'zzz').to_return(:body => 95 | '{"paging":{"'+type+'":"yyy"},"data":["y"]}') 96 | stub_request(:get, 'yyy').to_return(:body => '{"data":["x"]}') 97 | 98 | expects = [{'data' => %w[y]}, {'data' => %w[x]}, nil] 99 | rg.for_pages(data, 3, {}, kind){ |rr| 100 | if rr 101 | r = rr.dup 102 | r.delete('paging') 103 | else 104 | r = rr 105 | end 106 | r.should == expects.shift 107 | }.should == {'data' => %w[z y x]} 108 | } 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /test/test_parse.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | describe RestGraph do 9 | 10 | should 'return nil if parse error, but not when call data directly' do 11 | rg = RestGraph.new 12 | rg.parse_cookies!({}).should == nil 13 | rg.data .should == {} 14 | end 15 | 16 | should 'parse if fbs contains json as well' do 17 | algorithm = 'HMAC-SHA256' 18 | user = '{"country"=>"us", "age"=>{"min"=>21}}' 19 | data = {'algorithm' => algorithm, 'user' => user} 20 | rg = RestGraph.new(:data => data, :secret => 'secret') 21 | sig = rg.send(:calculate_sig, data) 22 | rg.parse_fbs!("\"#{rg.fbs}\"").should == data.merge('sig' => sig) 23 | end 24 | 25 | should 'extract correct access_token or fail checking sig' do 26 | access_token = '1|2-5|f.' 27 | app_id = '1829' 28 | secret = app_id.reverse 29 | sig = '398262caea8442bd8801e8fba7c55c8a' 30 | fbs = "access_token=#{CGI.escape(access_token)}&expires=0&" \ 31 | "secret=abc&session_key=def-456&sig=#{sig}&uid=3" 32 | 33 | check = lambda{ |token, fbs| 34 | http_cookie = 35 | "__utma=123; __utmz=456.utmcsr=(d)|utmccn=(d)|utmcmd=(n); " \ 36 | "fbs_#{app_id}=#{fbs}" 37 | 38 | rg = RestGraph.new(:app_id => app_id, :secret => secret) 39 | rg.parse_rack_env!('HTTP_COOKIE' => http_cookie). 40 | should.kind_of?(token ? Hash : NilClass) 41 | rg.access_token.should == token 42 | 43 | rg.parse_rack_env!('HTTP_COOKIE' => nil).should == nil 44 | rg.data.should == {} 45 | 46 | rg.parse_cookies!({"fbs_#{app_id}" => fbs}). 47 | should.kind_of?(token ? Hash : NilClass) 48 | rg.access_token.should == token 49 | 50 | rg.parse_fbs!(fbs). 51 | should.kind_of?(token ? Hash : NilClass) 52 | rg.access_token.should == token 53 | } 54 | check.call(access_token, fbs) 55 | check.call(access_token, "\"#{fbs}\"") 56 | fbs << '&inject=evil"' 57 | check.call(nil, fbs) 58 | check.call(nil, "\"#{fbs}\"") 59 | end 60 | 61 | should 'not pass if there is no secret, prevent from forgery' do 62 | rg = RestGraph.new 63 | rg.parse_fbs!('"feed=me&sig=bddd192cf27f22c05f61c8bea24fa4b7"'). 64 | should == nil 65 | end 66 | 67 | should 'parse json correctly' do 68 | rg = RestGraph.new 69 | 70 | rg.parse_json!('bad json') .should == nil 71 | rg.parse_json!('{"no":"sig"}').should == nil 72 | rg.parse_json!('{"feed":"me","sig":"bddd192cf27f22c05f61c8bea24fa4b7"}'). 73 | should == nil 74 | 75 | rg = RestGraph.new(:secret => 'bread') 76 | rg.parse_json!('{"feed":"me","sig":"20393e7823730308938a86ecf1c88b14"}'). 77 | should == {'feed' => 'me', 'sig' => "20393e7823730308938a86ecf1c88b14"} 78 | rg.data.empty?.should == false 79 | rg.parse_json!('bad json') 80 | rg.data.empty?.should == true 81 | end 82 | 83 | should 'parse signed_request' do 84 | secret = 'aloha' 85 | json = RestGraph.json_encode('ooh' => 'dir', 'moo' => 'bar') 86 | encode = lambda{ |str| 87 | [str].pack('m').tr("\n=", '').tr('+/', '-_') 88 | } 89 | json_encoded = encode[json] 90 | sig = OpenSSL::HMAC.digest('sha256', secret, json_encoded) 91 | signed_request = "#{encode[sig]}.#{json_encoded}" 92 | 93 | rg = RestGraph.new(:secret => secret) 94 | rg.parse_signed_request!(signed_request) 95 | rg.data['ooh'].should == 'dir' 96 | rg.data['moo'].should == 'bar' 97 | 98 | signed_request = "#{encode[sig[0..-4]+'bad']}.#{json_encoded}" 99 | rg.parse_signed_request!(signed_request).should == nil 100 | rg.data .should == {} 101 | end 102 | 103 | should 'parse invalid signed_request' do 104 | RestGraph.new.parse_signed_request!('bad').should == nil 105 | end 106 | 107 | should 'fallback to ruby-hmac if Digest.new raise an runtime error' do 108 | key, data = 'top', 'secret' 109 | digest = OpenSSL::HMAC.digest('sha256', key, data) 110 | mock(OpenSSL::HMAC).digest('sha256', key, data){ raise 'boom' } 111 | RestGraph.hmac_sha256(key, data).should == digest 112 | end 113 | 114 | should 'generate correct fbs with correct sig' do 115 | RestGraph.new(:access_token => 'fake', :secret => 's').fbs.should == 116 | "access_token=fake&sig=#{Digest::MD5.hexdigest('access_token=fakes')}" 117 | end 118 | 119 | should 'parse fbs from facebook response which lacks sig...' do 120 | rg = RestGraph.new(:access_token => 'a', :secret => 'z') 121 | rg.parse_fbs!(rg.fbs) .should.kind_of?(Hash) 122 | rg.data.empty?.should == false 123 | rg.parse_fbs!(rg.fbs.sub(/sig\=\w+/, 'sig=abc')).should == nil 124 | rg.data.empty?.should == true 125 | end 126 | 127 | should 'generate correct fbs with additional parameters' do 128 | rg = RestGraph.new(:access_token => 'a', :secret => 'z') 129 | rg.data['expires'] = '1234' 130 | rg.parse_fbs!(rg.fbs) .should.kind_of?(Hash) 131 | rg.data['access_token'] .should == 'a' 132 | rg.data['expires'] .should == '1234' 133 | end 134 | 135 | end 136 | -------------------------------------------------------------------------------- /test/test_rest-graph.rb: -------------------------------------------------------------------------------- 1 | 2 | $LOAD_PATH << File.dirname(__FILE__) 3 | $LOAD_PATH.uniq! 4 | 5 | Dir["#{File.dirname(__FILE__)}/*.rb"].each{ |file| 6 | next if file == __FILE__ 7 | next if ARGV.map{ |path| File.expand_path(path) 8 | }.include?(File.expand_path(file)) 9 | require File.basename(file).sub(/\..+$/, '') 10 | } 11 | -------------------------------------------------------------------------------- /test/test_serialize.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | describe RestGraph do 9 | after do 10 | WebMock.reset! 11 | Muack.verify 12 | end 13 | 14 | should 'be serialized with lighten' do 15 | [Marshal, YAML].each{ |engine| 16 | test = lambda{ |obj| engine.load(engine.dump(obj)) } 17 | rg = RestGraph.new(:log_handler => lambda{}) 18 | lambda{ test[rg] }.should.raise(TypeError) 19 | test[rg.lighten].should == rg.lighten 20 | lambda{ test[rg] }.should.raise(TypeError) 21 | rg.lighten! 22 | test[rg.lighten].should == rg 23 | } 24 | end 25 | 26 | should 'lighten takes options to change attributes' do 27 | RestGraph.new.lighten(:timeout => 100 ).timeout.should == 100 28 | RestGraph.new.lighten(:lang => 'zh-TW').lang .should == 'zh-TW' 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/test_test_util.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | require 'rest-graph/test_util' 9 | 10 | describe RestGraph::TestUtil do 11 | before do 12 | RestGraph::TestUtil.setup 13 | end 14 | 15 | after do 16 | RestGraph::TestUtil.teardown 17 | end 18 | 19 | should 'stub requests and store result and teardown do cleanup' do 20 | RestGraph.new.get('me') .should == {'data' => []} 21 | RestGraph::TestUtil.history .should == 22 | [[:get, "https://graph.facebook.com/me", nil]] 23 | 24 | RestGraph::TestUtil.teardown 25 | 26 | RestGraph::TestUtil.history.should == [] 27 | begin 28 | RestGraph.new.get('me') 29 | rescue WebMock::NetConnectNotAllowedError 30 | else 31 | 'never'.should == 'reach' 32 | end 33 | end 34 | 35 | should 'have default response' do 36 | default = {'meta' => []} 37 | RestGraph::TestUtil.default_response = default 38 | RestGraph.new.get('me') .should == default 39 | end 40 | 41 | should 'have default data' do 42 | rg = RestGraph.new 43 | rg.data['uid'] .should == '1234' 44 | RestGraph::TestUtil.default_data = {'uid' => '4321'} 45 | rg.data['uid'] .should == '4321' 46 | RestGraph.new.data['uid'].should == '4321' 47 | end 48 | 49 | should 'be easy to stub data' do 50 | response = {'data' => 'me'} 51 | RestGraph::TestUtil.get('me'){ response } 52 | RestGraph.new.get('me').should == response 53 | RestGraph.new.get('he').should == RestGraph::TestUtil.default_response 54 | end 55 | 56 | should 'emulate login' do 57 | RestGraph::TestUtil.login(1829) 58 | rg = RestGraph.new 59 | rg.data['uid'].should == '1829' 60 | rg.authorized?.should == true 61 | rg.get('me').should == RestGraph::TestUtil.user('1829') 62 | end 63 | 64 | should 'reset before login' do 65 | RestGraph::TestUtil.login(1234).login(1829) 66 | rg = RestGraph.new 67 | rg.data['uid'].should == '1829' 68 | rg.authorized?.should == true 69 | rg.get('me').should == RestGraph::TestUtil.user('1829') 70 | RestGraph::TestUtil.login(1234) 71 | rg.data['uid'].should == '1234' 72 | rg.authorized?.should == true 73 | rg.get('me').should == RestGraph::TestUtil.user('1234') 74 | end 75 | 76 | should 'return correct response in fake get' do 77 | RestGraph.new.fql('', {}, :post => true). 78 | should == [RestGraph::TestUtil.default_response] 79 | 80 | RestGraph.new.fql_multi({:a => '', :b => ''}, {}, :post => true). 81 | sort_by{ |h| h['name'] }. 82 | should == [{'name' => 'a', 83 | 'fql_result_set' => [RestGraph::TestUtil.default_response]}, 84 | {'name' => 'b', 85 | 'fql_result_set' => [RestGraph::TestUtil.default_response]}] 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/test_timeout.rb: -------------------------------------------------------------------------------- 1 | 2 | if respond_to?(:require_relative, true) 3 | require_relative 'common' 4 | else 5 | require File.dirname(__FILE__) + '/common' 6 | end 7 | 8 | describe RestGraph do 9 | after do 10 | WebMock.reset! 11 | Muack.verify 12 | end 13 | 14 | should 'respect timeout' do 15 | stub_request(:get, 'https://graph.facebook.com/me'). 16 | to_return(:body => '{}') 17 | mock(Timeout).timeout(is_a(Numeric)) 18 | RestGraph.new.get('me').should == {} 19 | end 20 | 21 | should 'override timeout' do 22 | mock(Timeout).timeout(99){ true } 23 | RestGraph.new(:timeout => 1).get('me', {}, :timeout => 99).should == true 24 | end 25 | end 26 | --------------------------------------------------------------------------------