├── .rvmrc ├── .gitignore ├── lib ├── version.rb └── mini_fb.rb ├── Gemfile ├── spec ├── test_helper.rb └── mini_fb_spec.rb ├── Rakefile ├── mini_fb.gemspec ├── Gemfile.lock ├── LICENSE.txt └── README.markdown /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm ree-1.8.7@mini-fb 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | .idea 3 | mini_fb_tests.yml 4 | *.gem 5 | -------------------------------------------------------------------------------- /lib/version.rb: -------------------------------------------------------------------------------- 1 | module MiniFB 2 | VERSION = "2.4.0" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'jeweler2' 6 | gem 'rspec', '~> 2.11.0' 7 | gem 'rake' 8 | gem 'activesupport' -------------------------------------------------------------------------------- /spec/test_helper.rb: -------------------------------------------------------------------------------- 1 | module TestHelper 2 | class << self 3 | 4 | def config 5 | @config ||= File.open(config_path) { |yf| YAML::load(yf) } 6 | end 7 | 8 | private 9 | 10 | def config_path 11 | File.expand_path("../mini_fb_tests.yml", File.dirname(__FILE__)) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | 2 | begin 3 | require 'jeweler2' 4 | Jeweler::Tasks.new do |gemspec| 5 | gemspec.name = "mini_fb" 6 | gemspec.summary = "Tiny facebook library" 7 | gemspec.description = "Tiny facebook library" 8 | gemspec.email = "travis@appoxy.com" 9 | gemspec.homepage = "http://github.com/appoxy/mini_fb" 10 | gemspec.authors = ["Travis Reeder"] 11 | gemspec.files = FileList['lib/**/*.rb'] 12 | gemspec.add_dependency 'rest-client' 13 | gemspec.add_dependency 'hashie' 14 | end 15 | Jeweler::GemcutterTasks.new 16 | rescue LoadError 17 | puts "Jeweler not available. Install it with: sudo gem install jeweler2" 18 | end 19 | 20 | require 'rspec/core/rake_task' 21 | RSpec::Core::RakeTask.new do |t| 22 | t.rspec_opts = ["--color", '--format doc'] 23 | end -------------------------------------------------------------------------------- /mini_fb.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path('../lib/version', __FILE__) 2 | 3 | Gem::Specification.new do |gem| 4 | gem.authors = ["Travis Reeder"] 5 | gem.email = ["travis@appoxy.com"] 6 | gem.description = "Tiny facebook library. By http://www.appoxy.com" 7 | gem.summary = "Tiny facebook library. By http://www.appoxy.com" 8 | gem.homepage = "http://github.com/appoxy/mini_fb/" 9 | 10 | gem.files = `git ls-files`.split($\) 11 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 12 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 13 | gem.name = "mini_fb" 14 | gem.require_paths = ["lib"] 15 | gem.version = MiniFB::VERSION 16 | 17 | gem.required_rubygems_version = ">= 1.3.6" 18 | gem.required_ruby_version = Gem::Requirement.new(">= 1.8") 19 | gem.add_runtime_dependency "httpclient", ">= 0" 20 | gem.add_runtime_dependency "hashie", ">= 0" 21 | end 22 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | mini_fb (2.0.0) 5 | hashie 6 | hashie 7 | rest-client 8 | rest-client 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | activesupport (3.2.12) 14 | i18n (~> 0.6) 15 | multi_json (~> 1.0) 16 | diff-lcs (1.1.3) 17 | git (1.2.5) 18 | hashie (2.0.2) 19 | i18n (0.6.4) 20 | jeweler2 (2.0.9) 21 | git (>= 1.2.5) 22 | mime-types (1.21) 23 | multi_json (1.6.1) 24 | rake (10.0.3) 25 | rest-client (1.6.7) 26 | mime-types (>= 1.16) 27 | rspec (2.11.0) 28 | rspec-core (~> 2.11.0) 29 | rspec-expectations (~> 2.11.0) 30 | rspec-mocks (~> 2.11.0) 31 | rspec-core (2.11.1) 32 | rspec-expectations (2.11.3) 33 | diff-lcs (~> 1.1.3) 34 | rspec-mocks (2.11.2) 35 | 36 | PLATFORMS 37 | ruby 38 | 39 | DEPENDENCIES 40 | activesupport 41 | jeweler2 42 | mini_fb! 43 | rake 44 | rspec (~> 2.11.0) 45 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Appoxy LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /spec/mini_fb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'uri' 3 | require 'yaml' 4 | require 'active_support/core_ext' 5 | require_relative '../lib/mini_fb' 6 | require_relative 'test_helper' 7 | 8 | describe MiniFB do 9 | before do 10 | MiniFB.log_level = :warn 11 | end 12 | 13 | let(:app_id){TestHelper.config['app_id']} 14 | let(:app_secret){TestHelper.config['app_secret']} 15 | let(:access_token){TestHelper.config['access_token']} 16 | 17 | describe '#authenticate_as_app' do 18 | 19 | it 'authenticates with valid params' do 20 | res = MiniFB.authenticate_as_app(app_id, app_secret) 21 | expect(res).to include('access_token') 22 | expect(res['access_token']).to match(/^#{app_id}/) 23 | end 24 | end 25 | 26 | describe '#signed_request_params' do 27 | let (:req) { 'vlXgu64BQGFSQrY0ZcJBZASMvYvTHu9GQ0YM9rjPSso.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsIjAiOiJwYXlsb2FkIn0' } 28 | let (:secret) { 'secret' } 29 | 30 | it 'decodes params' do 31 | expect(MiniFB.signed_request_params(secret, req)).to eq({"0" => "payload"}) 32 | end 33 | end 34 | 35 | describe '#exchange_token' do 36 | let(:invalid_app_id) { '12345' } 37 | let(:invalid_secret) { 'secret' } 38 | let(:invalid_access_token) { 'token' } 39 | 40 | it 'returns valid long-lived token' do 41 | res = MiniFB.fb_exchange_token(app_id, app_secret, access_token) 42 | 43 | expect(res).to include('access_token') 44 | expect(res).to include('expires_in') 45 | end 46 | 47 | it 'raises error on request with invalid params' do 48 | error_message = 'Facebook error 400: OAuthException: '\ 49 | 'Error validating application. '\ 50 | 'Cannot get application info due to a system error.' 51 | 52 | expect do 53 | MiniFB.fb_exchange_token(invalid_app_id, invalid_secret, invalid_access_token) 54 | end.to raise_error(MiniFB::FaceBookError, error_message) 55 | end 56 | 57 | it 'raise error on request with invalid token' do 58 | error_message = 'Facebook error 400: OAuthException: '\ 59 | 'Invalid OAuth access token.' 60 | 61 | expect do 62 | MiniFB.fb_exchange_token(app_id, app_secret, invalid_access_token) 63 | end.to raise_error(MiniFB::FaceBookError, error_message) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | MiniFB - the simple miniature facebook library 2 | ============================================== 3 | 4 | MiniFB is a small, lightweight Ruby library for interacting with the [Facebook API](http://wiki.developers.facebook.com/index.php/API). 5 | 6 | Brought to you by: [![Appoxy](https://lh5.googleusercontent.com/_-J9DSaseOX8/TX2Bq564w-I/AAAAAAAAxYU/xjeReyoxa8o/s800/appoxy-small%20%282%29.png)](http://www.appoxy.com) 7 | 8 | Support 9 | -------- 10 | 11 | Join our Discussion Group at: 12 | 13 | Demo Rails Application 14 | ------------------- 15 | 16 | There is a demo Rails app that uses mini_fb graph api at: [http://github.com/appoxy/mini_fb_demo](http://github.com/appoxy/mini_fb_demo) 17 | 18 | Installation 19 | ------------- 20 | 21 | gem install mini_fb 22 | 23 | 24 | Facebook Graph API 25 | ================== 26 | 27 | For an overview of what this is all about, see . 28 | 29 | Authentication 30 | -------------- 31 | 32 | Facebook now uses Oauth 2 for authentication, but don't worry, this part is easy. 33 | 34 | # Get your oauth url 35 | @oauth_url = MiniFB.oauth_url(FB_APP_ID, # your Facebook App ID (NOT API_KEY) 36 | "http://www.yoursite.com/sessions/create", # redirect url 37 | :scope=>MiniFB.scopes.join(",")) # This asks for all permissions 38 | # Have your users click on a link to @oauth_url 39 | ..... 40 | # Then in your /sessions/create 41 | access_token_hash = MiniFB.oauth_access_token(FB_APP_ID, "http://www.yoursite.com/sessions/create", FB_SECRET, params[:code]) 42 | @access_token = access_token_hash["access_token"] 43 | # TODO: This is where you'd want to store the token in your database 44 | # but for now, we'll just keep it in the cookie so we don't need a database 45 | cookies[:access_token] = @access_token 46 | 47 | That's it. You now need to hold onto this access_token. We've put it in a cookie for now, but you probably 48 | want to store it in your database or something. 49 | 50 | General Use 51 | -------------------------- 52 | 53 | @response_hash = MiniFB.get(@access_token, @id, @options = {}) 54 | 55 | 56 | It's very simple: 57 | 58 | @id = {some ID of something in facebook} || "me" 59 | @options: 60 | type = {some facebook type like feed, friends, or photos} 61 | version = {version of the graph api}. Example: "2.5" 62 | fields = {array of fields to explicit fetch} 63 | params = {optional params} 64 | 65 | # @response_hash is a hash, but also allows object like syntax for instance, the following is true: 66 | @response_hash["user"] == @response_hash.user 67 | 68 | See for the available types. 69 | 70 | Posting Data to Facebook 71 | ------------------------ 72 | 73 | Also pretty simple: 74 | 75 | @id = {some ID of something in facebook} 76 | @type = {some type of post like comments, likes, feed} # required here 77 | @response_hash = MiniFB.post(@access_token, @id, :type=>@type) 78 | 79 | FQL 80 | --- 81 | 82 | my_query = "select uid,a,b,c from users where ...." 83 | @res = MiniFB.fql(@access_token, my_query) 84 | 85 | Logging 86 | ------- 87 | 88 | To enabled logging: 89 | 90 | MiniFB.enable_logging 91 | 92 | 93 | Original Facebook API 94 | ===================== 95 | 96 | This API will probably go away at some point, so you should use the Graph API above in most cases. 97 | 98 | 99 | General Usage 100 | ------------- 101 | 102 | The most general case is to use MiniFB.call method: 103 | 104 | user_hash = MiniFB.call(FB_API_KEY, FB_SECRET, "Users.getInfo", "session_key"=>@session_key, "uids"=>@uid, "fields"=>User.all_fields) 105 | 106 | Which simply returns the parsed json response from Facebook. 107 | 108 | 109 | Oauth 2.0 Authentication and Original Rest Api 110 | ------------- 111 | 112 | You can use the Graph api Oauth 2.0 token with original api methods. BEWARE: This has only been tested against stream.publish at present. 113 | 114 | MiniFB.rest(@access_token, "rest.api.method", options) 115 | 116 | eg: 117 | 118 | response = MiniFB.rest(@access_token, "stream.publish", :params => { 119 | :uid => @user_id, :target_id => @target_user_id, 120 | :message => "Hello other user!" 121 | }) 122 | 123 | all responses will be json. In the instance of 'bad json' methods, the response will formatted {'response': '#{bad_response_string}'} 124 | 125 | 126 | Some Higher Level Objects for Common Uses 127 | ---------------------- 128 | 129 | Get a MiniFB::Session: 130 | 131 | @fb = MiniFB::Session.new(FB_API_KEY, FB_SECRET, @fb_session, @fb_uid) 132 | 133 | Then it makes it a bit easier to use call for a particular user/session. 134 | 135 | response = @fb.call("stream.get") 136 | 137 | With the session, you can then get the user information for the session/uid. 138 | 139 | user = @fb.user 140 | 141 | Then get info from the user: 142 | 143 | first_name = user["first_name"] 144 | 145 | Or profile photos: 146 | 147 | photos = user.profile_photos 148 | 149 | Or if you want other photos, try: 150 | 151 | photos = @fb.photos("pids"=>[12343243,920382343,9208348]) 152 | 153 | 154 | Higher Level Objects with OAuth2 155 | -------------------------------- 156 | 157 | Get a MiniFB::OAuthSession with a Spanish locale: 158 | 159 | @fb = MiniFB::OAuthSession.new(access_token, 'es_ES') 160 | 161 | Using the session object to make requests: 162 | 163 | @fb.get('117199051648010') 164 | @fb.post('me', :type => :feed, :params => { 165 | :message => "This is me from MiniFB" 166 | }) 167 | @fb.fql('SELECT id FROM object_url WHERE url="http://www.imdb.com/title/tt1250777/"') 168 | @fb.rest('notes.create', :params => { 169 | :title => "ToDo", :content => "Try MiniFB" 170 | }) 171 | 172 | Getting graph objects through the session: 173 | 174 | @fb.me 175 | @fb.me.name 176 | @fb.me.connections 177 | @fb.me.feed 178 | 179 | @ssp = @fb.graph_object('117199051648010') 180 | @ssp.mission 181 | @ssp.photos 182 | 183 | 184 | Facebook Connect 185 | ---------------- 186 | 187 | This is actually very easy, first follow these instructions: http://wiki.developers.facebook.com/index.php/Connect/Setting_Up_Your_Site 188 | 189 | Then add the following script to the page where you put the login button so it looks like this: 190 | 191 | 196 | 197 | 198 | Define an fb_connect method in your login/sessions controller like so: 199 | 200 | def fb_connect 201 | @fb_info = MiniFB.parse_cookie_information(FB_APP_ID, cookies) # some users may have to use their API rather than the app. ID. 202 | puts "uid=#{@fb_info['uid']}" 203 | puts "session=#{@fb_info['session_key']}" 204 | 205 | if MiniFB.verify_cookie_signature(FB_APP_ID, FB_SECRET, cookies) 206 | # And here you would create the user if it doesn't already exist, then redirect them to wherever you want. 207 | else 208 | # The cookies may have been modified as the signature does not match 209 | end 210 | 211 | end 212 | 213 | 214 | Photo Uploads 215 | ------------- 216 | 217 | This is as simple as calling: 218 | 219 | @fb.call("photos.upload", "filename"=>"") 220 | 221 | The file_name parameter will be used as the file data. 222 | -------------------------------------------------------------------------------- /lib/mini_fb.rb: -------------------------------------------------------------------------------- 1 | #MiniFB - the simple miniature facebook library 2 | #MiniFB is a small, lightweight Ruby library for interacting with the Facebook API. 3 | # 4 | #Brought to you by: www.appoxy.com 5 | # 6 | #Support 7 | # 8 | #Join our Discussion Group at: http://groups.google.com/group/mini_fb 9 | # 10 | #Demo Rails Application 11 | # 12 | #There is a demo Rails app that uses mini_fb graph api at: http://github.com/appoxy/mini_fb_demo 13 | 14 | require 'digest/md5' 15 | require 'erb' 16 | require 'json' unless defined? JSON 17 | require 'httpclient' 18 | require 'hashie' 19 | require 'base64' 20 | require 'openssl' 21 | require 'logger' 22 | 23 | module MiniFB 24 | 25 | # Global constants 26 | FB_URL = "http://api.facebook.com/restserver.php" 27 | FB_API_VERSION = "1.0" 28 | 29 | @@logging = false 30 | @@log = Logger.new(STDOUT) 31 | @@http = HTTPClient.new 32 | 33 | 34 | def self.log_level=(level) 35 | if level.is_a? Numeric 36 | @@log.level = level 37 | else 38 | @@log.level = case level 39 | when :fatal 40 | @@log.level = Logger::FATAL 41 | when :error 42 | @@log.level = Logger::ERROR 43 | when :warn 44 | @@log.level = Logger::WARN 45 | when :info 46 | @@log.level = Logger::INFO 47 | when :debug 48 | @@log.level = Logger::DEBUG 49 | end 50 | end 51 | end 52 | 53 | def self.enable_logging 54 | @@logging = true 55 | @@log.level = Logger::DEBUG 56 | end 57 | 58 | def self.disable_logging 59 | @@logging = false 60 | @@log.level = Logger::ERROR 61 | end 62 | 63 | class FaceBookError < StandardError 64 | attr_accessor :code 65 | # Error that happens during a facebook call. 66 | def initialize(error_code, error_msg) 67 | @code = error_code 68 | super("Facebook error #{error_code}: #{error_msg}") 69 | end 70 | end 71 | 72 | class Session 73 | attr_accessor :api_key, :secret_key, :session_key, :uid 74 | 75 | 76 | def initialize(api_key, secret_key, session_key, uid) 77 | @api_key = api_key 78 | @secret_key = FaceBookSecret.new secret_key 79 | @session_key = session_key 80 | @uid = uid 81 | end 82 | 83 | # returns current user 84 | def user 85 | return @user unless @user.nil? 86 | @user = User.new(MiniFB.call(@api_key, @secret_key, "Users.getInfo", "session_key"=>@session_key, "uids"=>@uid, "fields"=>User.all_fields)[0], self) 87 | @user 88 | end 89 | 90 | def photos 91 | Photos.new(self) 92 | end 93 | 94 | 95 | def call(method, params={}) 96 | return MiniFB.call(api_key, secret_key, method, params.update("session_key"=>session_key)) 97 | end 98 | 99 | end 100 | 101 | class User 102 | FIELDS = [:uid, :status, :political, :pic_small, :name, :quotes, :is_app_user, :tv, :profile_update_time, :meeting_sex, :hs_info, :timezone, :relationship_status, :hometown_location, :about_me, :wall_count, :significant_other_id, :pic_big, :music, :work_history, :sex, :religion, :notes_count, :activities, :pic_square, :movies, :has_added_app, :education_history, :birthday, :birthday_date, :first_name, :meeting_for, :last_name, :interests, :current_location, :pic, :books, :affiliations, :locale, :profile_url, :proxied_email, :email, :email_hashes, :allowed_restrictions, :pic_with_logo, :pic_big_with_logo, :pic_small_with_logo, :pic_square_with_logo] 103 | STANDARD_FIELDS = [:uid, :first_name, :last_name, :name, :timezone, :birthday, :sex, :affiliations, :locale, :profile_url, :proxied_email, :email] 104 | 105 | def self.all_fields 106 | FIELDS.join(",") 107 | end 108 | 109 | def self.standard_fields 110 | STANDARD_FIELDS.join(",") 111 | end 112 | 113 | def initialize(fb_hash, session) 114 | @fb_hash = fb_hash 115 | @session = session 116 | end 117 | 118 | def [](key) 119 | @fb_hash[key] 120 | end 121 | 122 | def uid 123 | return self["uid"] 124 | end 125 | 126 | def profile_photos 127 | @session.photos.get("uid"=>uid, "aid"=>profile_pic_album_id) 128 | end 129 | 130 | def profile_pic_album_id 131 | merge_aid(-3, uid) 132 | end 133 | 134 | def merge_aid(aid, uid) 135 | uid = uid.to_i 136 | ret = (uid << 32) + (aid & 0xFFFFFFFF) 137 | # puts 'merge_aid=' + ret.inspect 138 | return ret 139 | end 140 | end 141 | 142 | class Photos 143 | 144 | def initialize(session) 145 | @session = session 146 | end 147 | 148 | def get(params) 149 | pids = params["pids"] 150 | if !pids.nil? && pids.is_a?(Array) 151 | pids = pids.join(",") 152 | params["pids"] = pids 153 | end 154 | @session.call("photos.get", params) 155 | end 156 | end 157 | 158 | BAD_JSON_METHODS = ["users.getloggedinuser", "auth.promotesession", "users.hasapppermission", 159 | "Auth.revokeExtendedPermission", "auth.revokeAuthorization", 160 | "pages.isAdmin", "pages.isFan", 161 | "stream.publish", 162 | "dashboard.addNews", "dashboard.addGlobalNews", "dashboard.publishActivity", 163 | "dashboard.incrementcount", "dashboard.setcount" 164 | ].collect { |x| x.downcase } 165 | 166 | # THIS IS FOR THE OLD FACEBOOK API, NOT THE GRAPH ONE. See MiniFB.get and MiniFB.post for Graph API 167 | # 168 | # Call facebook server with a method request. Most keyword arguments 169 | # are passed directly to the server with a few exceptions. 170 | # The 'sig' value will always be computed automatically. 171 | # The 'v' version will be supplied automatically if needed. 172 | # The 'call_id' defaults to True, which will generate a valid 173 | # number. Otherwise it should be a valid number or False to disable. 174 | 175 | # The default return is a parsed json object. 176 | # Unless the 'format' and/or 'callback' arguments are given, 177 | # in which case the raw text of the reply is returned. The string 178 | # will always be returned, even during errors. 179 | 180 | # If an error occurs, a FacebookError exception will be raised 181 | # with the proper code and message. 182 | 183 | # The secret argument should be an instance of FacebookSecret 184 | # to hide value from simple introspection. 185 | def MiniFB.call(api_key, secret, method, kwargs) 186 | 187 | puts 'kwargs=' + kwargs.inspect if @@logging 188 | 189 | if secret.is_a? String 190 | secret = FaceBookSecret.new(secret) 191 | end 192 | 193 | # Prepare arguments for call 194 | call_id = kwargs.fetch("call_id", true) 195 | if call_id == true 196 | kwargs["call_id"] = Time.now.tv_sec.to_s 197 | else 198 | kwargs.delete("call_id") 199 | end 200 | 201 | custom_format = kwargs.include?("format") || kwargs.include?("callback") 202 | kwargs["format"] ||= "JSON" 203 | kwargs["v"] ||= FB_API_VERSION 204 | kwargs["api_key"]||= api_key 205 | kwargs["method"] ||= method 206 | 207 | file_name = kwargs.delete("filename") 208 | 209 | kwargs["sig"] = signature_for(kwargs, secret.value.call) 210 | 211 | fb_method = kwargs["method"].downcase 212 | if fb_method == "photos.upload" 213 | # Then we need a multipart post 214 | response = MiniFB.post_upload(file_name, kwargs) 215 | else 216 | 217 | begin 218 | response = Net::HTTP.post_form(URI.parse(FB_URL), post_params(kwargs)) 219 | rescue SocketError => err 220 | # why are we catching this and throwing as different error? hmmm.. 221 | # raise IOError.new( "Cannot connect to the facebook server: " + err ) 222 | raise err 223 | end 224 | end 225 | 226 | # Handle response 227 | return response.body if custom_format 228 | 229 | body = response.body 230 | 231 | puts 'response=' + body.inspect if @@logging 232 | begin 233 | data = JSON.parse(body) 234 | if data.include?("error_msg") 235 | raise FaceBookError.new(data["error_code"] || 1, data["error_msg"]) 236 | end 237 | 238 | rescue JSON::ParserError => ex 239 | if BAD_JSON_METHODS.include?(fb_method) # Little hack because this response isn't valid JSON 240 | if body == "0" || body == "false" 241 | return false 242 | end 243 | return body 244 | else 245 | raise ex 246 | end 247 | end 248 | return data 249 | end 250 | 251 | def MiniFB.post_upload(filename, kwargs) 252 | content = File.open(filename, 'rb') { |f| f.read } 253 | boundary = Digest::MD5.hexdigest(content) 254 | header = {'Content-type' => "multipart/form-data, boundary=#{boundary}"} 255 | 256 | # Build query 257 | query = '' 258 | kwargs.each { |a, v| 259 | query << 260 | "--#{boundary}\r\n" << 261 | "Content-Disposition: form-data; name=\"#{a}\"\r\n\r\n" << 262 | "#{v}\r\n" 263 | } 264 | query << 265 | "--#{boundary}\r\n" << 266 | "Content-Disposition: form-data; filename=\"#{File.basename(filename)}\"\r\n" << 267 | "Content-Transfer-Encoding: binary\r\n" << 268 | "Content-Type: image/jpeg\r\n\r\n" << 269 | content << 270 | "\r\n" << 271 | "--#{boundary}--" 272 | 273 | # Call Facebook with POST multipart/form-data request 274 | uri = URI.parse(FB_URL) 275 | Net::HTTP.start(uri.host) { |http| http.post uri.path, query, header } 276 | end 277 | 278 | # Returns true is signature is valid, false otherwise. 279 | def MiniFB.verify_signature(secret, arguments) 280 | if arguments.is_a? String 281 | #new way: params[:session] 282 | session = JSON.parse(arguments) 283 | 284 | signature = session.delete('sig') 285 | return false if signature.nil? 286 | 287 | arg_string = String.new 288 | session.sort.each { |k, v| arg_string << "#{k}=#{v}" } 289 | if Digest::MD5.hexdigest(arg_string + secret) == signature 290 | return true 291 | end 292 | else 293 | #old way 294 | 295 | signature = arguments.delete("fb_sig") 296 | return false if signature.nil? 297 | 298 | unsigned = Hash.new 299 | signed = Hash.new 300 | 301 | arguments.each do |k, v| 302 | if k =~ /^fb_sig_(.*)/ then 303 | signed[$1] = v 304 | else 305 | unsigned[k] = v 306 | end 307 | end 308 | 309 | arg_string = String.new 310 | signed.sort.each { |kv| arg_string << kv[0] << "=" << kv[1] } 311 | if Digest::MD5.hexdigest(arg_string + secret) == signature 312 | return true 313 | end 314 | end 315 | return false 316 | end 317 | 318 | # This function takes the app secret and the signed request, and verifies if the request is valid. 319 | def self.verify_signed_request(secret, req) 320 | s, p = req.split(".") 321 | sig = base64_url_decode(s) 322 | expected_sig = OpenSSL::HMAC.digest('SHA256', secret, p) 323 | return sig == expected_sig 324 | end 325 | 326 | # This function decodes the data sent by Facebook and returns a Hash. 327 | # See: http://developers.facebook.com/docs/authentication/canvas 328 | def self.signed_request_params(secret, req) 329 | s, p = req.split(".") 330 | p = base64_url_decode(p) 331 | h = JSON.parse(p) 332 | h.delete('algorithm') if h['algorithm'] == 'HMAC-SHA256' 333 | h 334 | end 335 | 336 | # Ruby's implementation of base64 decoding seems to be reading the string in multiples of 4 and ignoring 337 | # any extra characters if there are no white-space characters at the end. Since facebook does not take this 338 | # into account, this function fills any string with white spaces up to the point where it becomes divisible 339 | # by 4, then it replaces '-' with '+' and '_' with '/' (URL-safe decoding), and decodes the result. 340 | def self.base64_url_decode(str) 341 | str = str + "=" * (4 - str.size % 4) unless str.size % 4 == 0 342 | return Base64.decode64(str.tr("-_", "+/")) 343 | end 344 | 345 | # Parses cookies in order to extract the facebook cookie and parse it into a useable hash 346 | # 347 | # options: 348 | # * app_id - the connect applications app_id (some users may find they have to use their facebook API key) 349 | # * secret - the connect application secret 350 | # * cookies - the cookies given by facebook - it is ok to just pass all of the cookies, the method will do the filtering for you. 351 | def MiniFB.parse_cookie_information(app_id, cookies) 352 | return nil if cookies["fbs_#{app_id}"].nil? 353 | Hash[*cookies["fbs_#{app_id}"].split('&').map { |v| v.gsub('"', '').split('=', 2) }.flatten] 354 | end 355 | 356 | # Validates that the cookies sent by the user are those that were set by facebook. Since your 357 | # secret is only known by you and facebook it is used to sign all of the cookies set. 358 | # 359 | # options: 360 | # * app_id - the connect applications app_id (some users may find they have to use their facebook API key) 361 | # * secret - the connect application secret 362 | # * cookies - the cookies given by facebook - it is ok to just pass all of the cookies, the method will do the filtering for you. 363 | def MiniFB.verify_cookie_signature(app_id, secret, cookies) 364 | fb_keys = MiniFB.parse_cookie_information(app_id, cookies) 365 | return false if fb_keys.nil? 366 | 367 | signature = fb_keys.delete('sig') 368 | return signature == Digest::MD5.hexdigest(fb_keys.map { |k, v| "#{k}=#{v}" }.sort.join + secret) 369 | end 370 | 371 | # DEPRECATED: Please use verify_cookie_signature instead. 372 | def MiniFB.verify_connect_signature(api_key, secret, cookies) 373 | warn "DEPRECATION WARNING: 'verify_connect_signature' has been renamed to 'verify_cookie_signature' as Facebook no longer calls this 'connect'" 374 | MiniFB.verify_cookie_signature(api_key, secret, cookies) 375 | end 376 | 377 | # Returns the login/add app url for your application. 378 | # 379 | # options: 380 | # - :next => a relative next page to go to. relative to your facebook connect url or if :canvas is true, then relative to facebook app url 381 | # - :canvas => true/false - to say whether this is a canvas app or not 382 | def self.login_url(api_key, options={}) 383 | login_url = "http://api.facebook.com/login.php?api_key=#{api_key}" 384 | login_url << "&next=#{options[:next]}" if options[:next] 385 | login_url << "&canvas" if options[:canvas] 386 | login_url 387 | end 388 | 389 | # Manages access_token and locale params for an OAuth connection 390 | class OAuthSession 391 | 392 | def initialize(access_token, locale="en_US") 393 | @access_token = access_token 394 | @locale = locale 395 | end 396 | 397 | def get(id, options={}) 398 | MiniFB.get(@access_token, id, session_options(options)) 399 | end 400 | 401 | def post(id, options={}) 402 | MiniFB.post(@access_token, id, session_options(options)) 403 | end 404 | 405 | def fql(fql_query, options={}) 406 | MiniFB.fql(@access_token, fql_query, session_options(options)) 407 | end 408 | 409 | def multifql(fql_queries, options={}) 410 | MiniFB.multifql(@access_token, fql_queries, session_options(options)) 411 | end 412 | 413 | def rest(api_method, options={}) 414 | MiniFB.rest(@access_token, api_method, session_options(options)) 415 | end 416 | 417 | # Returns a GraphObject for the given id 418 | def graph_object(id) 419 | MiniFB::GraphObject.new(self, id) 420 | end 421 | 422 | # Returns and caches a GraphObject for the user 423 | def me 424 | @me ||= graph_object('me') 425 | end 426 | 427 | private 428 | def session_options(options) 429 | (options[:params] ||= {})[:locale] ||= @locale 430 | options 431 | end 432 | end 433 | 434 | # Wraps a graph object for easily accessing its connections 435 | class GraphObject 436 | # Creates a GraphObject using an OAuthSession or access_token 437 | def initialize(session_or_token, id) 438 | @oauth_session = if session_or_token.is_a?(MiniFB::OAuthSession) 439 | session_or_token 440 | else 441 | MiniFB::OAuthSession.new(session_or_token) 442 | end 443 | @id = id 444 | @object = @oauth_session.get(id, :metadata => true) 445 | @connections_cache = {} 446 | end 447 | 448 | def inspect 449 | "<##{self.class.name} #{@object.inspect}>" 450 | end 451 | 452 | def connections 453 | @object.metadata.connections.keys 454 | end 455 | 456 | unless RUBY_VERSION >= '1.9' 457 | undef :id, :type 458 | end 459 | 460 | def methods 461 | super + @object.keys.include?(key) + connections.include?(key) 462 | end 463 | 464 | def respond_to?(method) 465 | @object.keys.include?(key) || connections.include?(key) || super 466 | end 467 | 468 | def keys 469 | @object.keys 470 | end 471 | 472 | def [](key) 473 | @object[key] 474 | end 475 | 476 | def method_missing(method, *args, &block) 477 | key = method.to_s 478 | if @object.keys.include?(key) 479 | @object[key] 480 | elsif @connections_cache.has_key?(key) 481 | @connections_cache[key] 482 | elsif connections.include?(key) 483 | @connections_cache[key] = @oauth_session.get(@id, :type => key) 484 | else 485 | super 486 | end 487 | end 488 | end 489 | 490 | def self.graph_base 491 | "https://graph.facebook.com/" 492 | end 493 | 494 | # options: 495 | # - scope: comma separated list of extends permissions. see http://developers.facebook.com/docs/authentication/permissions 496 | def self.oauth_url(app_id, redirect_uri, options={}) 497 | oauth_url = "#{graph_base}oauth/authorize" 498 | oauth_url << "?client_id=#{app_id}" 499 | oauth_url << "&redirect_uri=#{CGI.escape(redirect_uri)}" 500 | # oauth_url << "&scope=#{options[:scope]}" if options[:scope] 501 | oauth_url << ("&" + options.map { |k, v| "%s=%s" % [k, v] }.join('&')) unless options.empty? 502 | oauth_url 503 | end 504 | 505 | # returns a hash with one value being 'access_token', the other being 'expires' 506 | def self.oauth_access_token(app_id, redirect_uri, secret, code) 507 | oauth_url = "#{graph_base}oauth/access_token" 508 | oauth_url << "?client_id=#{app_id}" 509 | oauth_url << "&redirect_uri=#{CGI.escape(redirect_uri)}" 510 | oauth_url << "&client_secret=#{secret}" 511 | oauth_url << "&code=#{CGI.escape(code)}" 512 | resp = @@http.get oauth_url 513 | puts 'resp=' + resp.body.to_s if @@logging 514 | JSON.parse(resp.body.to_s) 515 | end 516 | 517 | # Gets long-lived token from the Facebook Graph API 518 | # options: 519 | # - app_id: your app ID (string) 520 | # - secret: your app secret (string) 521 | # - access_token: short-lived user token (string) 522 | # returns a hash with one value being 'access_token', the other being 'expires_in' 523 | # 524 | # Throws MiniFB::FaceBookError if response from Facebook Graph API is not successful 525 | def self.fb_exchange_token(app_id, secret, access_token) 526 | oauth_url = "#{graph_base}oauth/access_token" 527 | oauth_url << "?client_id=#{app_id}" 528 | oauth_url << "&client_secret=#{secret}" 529 | oauth_url << "&grant_type=fb_exchange_token" 530 | oauth_url << "&fb_exchange_token=#{CGI.escape(access_token)}" 531 | response = @@http.get oauth_url 532 | body = response.body.to_s 533 | puts 'resp=' + body if @@logging 534 | res_hash = JSON.parse(body) 535 | unless response.ok? 536 | raise MiniFB::FaceBookError.new(response.status, "#{res_hash["error"]["type"]}: #{res_hash["error"]["message"]}") 537 | end 538 | return res_hash 539 | end 540 | 541 | # Return a JSON object of working Oauth tokens from working session keys, returned in order given 542 | def self.oauth_exchange_session(app_id, secret, session_keys) 543 | url = "#{graph_base}oauth/exchange_sessions" 544 | params = {} 545 | params["client_id"] = "#{app_id}" 546 | params["client_secret"] = "#{secret}" 547 | params["sessions"] = "#{session_keys}" 548 | options = {} 549 | options[:params] = params 550 | options[:method] = :post 551 | return fetch(url, options) 552 | end 553 | 554 | # Return a JSON object of working Oauth tokens from working session keys, returned in order given 555 | def self.authenticate_as_app(app_id, secret) 556 | url = "#{graph_base}oauth/access_token" 557 | params = {} 558 | params["type"] = "client_cred" 559 | params["client_id"] = "#{app_id}" 560 | params["client_secret"] = "#{secret}" 561 | options = {} 562 | options[:params] = params 563 | options[:method] = :get 564 | options[:response_type] = :json 565 | resp = fetch(url, options) 566 | puts 'resp=' + resp.to_s if @@logging 567 | resp 568 | end 569 | 570 | # Gets data from the Facebook Graph API 571 | # options: 572 | # - type: eg: feed, home, etc 573 | # - metadata: to include metadata in response. true/false 574 | # - params: Any additional parameters you would like to submit 575 | def self.get(access_token, id, options={}) 576 | url = graph_base 577 | url << "v#{options[:version]}/" if options[:version] 578 | url << id 579 | url << "/#{options[:type]}" if options[:type] 580 | params = options[:params] || {} 581 | params["access_token"] = "#{(access_token)}" 582 | params["metadata"] = "1" if options[:metadata] 583 | params["fields"] = options[:fields].join(",") if options[:fields] 584 | options[:params] = params 585 | return fetch(url, options) 586 | end 587 | 588 | # Gets multiple data from the Facebook Graph API 589 | # options: 590 | # - type: eg: feed, home, etc 591 | # - metadata: to include metadata in response. true/false 592 | # - params: Any additional parameters you would like to submit 593 | # Example: 594 | # 595 | # MiniFB.multiget(access_token, [123, 234]) 596 | # 597 | # Can throw a connection Timeout if there is too many items 598 | def self.multiget(access_token, ids, options={}) 599 | url = graph_base 600 | url << "v#{options[:version]}/" if options[:version] 601 | url << "#{options[:type]}" if options[:type] 602 | params = options[:params] || {} 603 | params["ids"] = ids.join(',') 604 | params["access_token"] = "#{(access_token)}" 605 | params["metadata"] = "1" if options[:metadata] 606 | params["fields"] = options[:fields].join(",") if options[:fields] 607 | options[:params] = params 608 | return fetch(url, options) 609 | end 610 | 611 | # Posts data to the Facebook Graph API 612 | # options: 613 | # - type: eg: feed, home, etc 614 | # - metadata: to include metadata in response. true/false 615 | # - params: Any additional parameters you would like to submit 616 | def self.post(access_token, id, options={}) 617 | url = graph_base 618 | url << "v#{options[:version]}/" if options[:version] 619 | url << id 620 | url << "/#{options[:type]}" if options[:type] 621 | options.delete(:type) 622 | params = options[:params] || {} 623 | options.each do |key, value| 624 | if value.kind_of?(File) 625 | params[key] = value 626 | else 627 | params[key] = "#{value}" 628 | end 629 | end 630 | params["access_token"] = "#{(access_token)}" 631 | params["metadata"] = "1" if options[:metadata] 632 | options[:params] = params 633 | options[:method] = :post 634 | return fetch(url, options) 635 | 636 | end 637 | 638 | # Sends a DELETE request to the Facebook Graph API 639 | # options: 640 | # - type: eg: feed, home, etc 641 | # - metadata: to include metadata in response. true/false 642 | # - params: Any additional parameters you would like to submit 643 | def self.delete(access_token, ids, options={}) 644 | url = graph_base 645 | url << "v#{options[:version]}/" if options[:version] 646 | params = options[:params] || {} 647 | if ids.is_a?(Array) 648 | params["ids"] = ids.join(',') 649 | else 650 | url << "#{ids}" 651 | end 652 | url << "/#{options[:type]}" if options[:type] 653 | options.delete(:type) 654 | options.each do |key, value| 655 | params[key] = "#{value}" 656 | end 657 | params["access_token"] = "#{(access_token)}" 658 | options[:params] = params 659 | options[:method] = :delete 660 | return fetch(url, options) 661 | 662 | end 663 | 664 | # Executes an FQL query 665 | def self.fql(access_token, fql_query, options={}) 666 | url = "https://api.facebook.com/method/fql.query" 667 | params = options[:params] || {} 668 | params["access_token"] = "#{(access_token)}" 669 | params["metadata"] = "1" if options[:metadata] 670 | params["query"] = fql_query 671 | params["format"] = "JSON" 672 | options[:params] = params 673 | return fetch(url, options) 674 | end 675 | 676 | # Executes multiple FQL queries 677 | # Example: 678 | # 679 | # MiniFB.multifql(access_token, { :statuses => "SELECT status_id, message FROM status WHERE uid = 12345", 680 | # :privacy => "SELECT object_id, description FROM privacy WHERE object_id IN (SELECT status_id FROM #statuses)" }) 681 | def self.multifql(access_token, fql_queries, options={}) 682 | url = "https://api.facebook.com/method/fql.multiquery" 683 | params = options[:params] || {} 684 | params["access_token"] = "#{(access_token)}" 685 | params["metadata"] = "1" if options[:metadata] 686 | params["queries"] = JSON[fql_queries] 687 | params[:format] = "JSON" 688 | options[:params] = params 689 | return fetch(url, options) 690 | end 691 | 692 | # Uses new Oauth 2 authentication against old Facebook REST API 693 | # options: 694 | # - params: Any additional parameters you would like to submit 695 | def self.rest(access_token, api_method, options={}) 696 | url = "https://api.facebook.com/method/#{api_method}" 697 | params = options[:params] || {} 698 | params[:access_token] = access_token 699 | params[:format] = "JSON" 700 | options[:params] = params 701 | return fetch(url, options) 702 | end 703 | 704 | def self.fetch(url, options={}) 705 | case options[:method] 706 | when :post 707 | @@log.debug 'url_post=' + url if @@logging 708 | response = @@http.post url, options[:params] 709 | when :delete 710 | if options[:params] && options[:params].size > 0 711 | url += '?' + options[:params].map { |k, v| CGI.escape(k.to_s) + '=' + CGI.escape(v.to_s) }.join('&') 712 | end 713 | @@log.debug 'url_delete=' + url if @@logging 714 | response = @@http.delete url 715 | else 716 | if options[:params] && options[:params].size > 0 717 | url += '?' + options[:params].map { |k, v| CGI.escape(k.to_s) + '=' + CGI.escape(v.to_s) }.join('&') 718 | end 719 | @@log.debug 'url_get=' + url if @@logging 720 | response = @@http.get url 721 | end 722 | 723 | resp = response.body 724 | @@log.debug "Response = #{resp}. Status = #{response.status}" if @@logging 725 | @@log.debug 'API Version =' + response.headers["Facebook-API-Version"].to_s if @@logging 726 | 727 | if options[:response_type] == :params 728 | # Some methods return a param like string, for example: access_token=11935261234123|rW9JMxbN65v_pFWQl5LmHHABC 729 | params = {} 730 | params_array = resp.to_s.split("&") 731 | params_array.each do |p| 732 | ps = p.split("=") 733 | params[ps[0]] = ps[1] 734 | end 735 | return params 736 | else 737 | res_hash = JSON.parse(resp.to_s.size > 2 ? resp.to_s : {response: resp.to_s}.to_json) 738 | unless response.ok? 739 | raise MiniFB::FaceBookError.new(response.status, "#{res_hash["error"]["type"]}: #{res_hash["error"]["message"]}") 740 | end 741 | end 742 | 743 | if res_hash.is_a? Array # fql return this 744 | res_hash.collect! { |x| x.is_a?(Hash) ? Hashie::Mash.new(x) : x } 745 | else 746 | res_hash = { response: res_hash } unless res_hash.is_a? Hash 747 | res_hash = Hashie::Mash.new(res_hash) 748 | end 749 | 750 | if res_hash.include?("error_msg") 751 | raise FaceBookError.new(res_hash["error_code"] || 1, res_hash["error_msg"]) 752 | end 753 | 754 | res_hash 755 | end 756 | 757 | # Returns all available scopes. 758 | def self.scopes 759 | scopes = %w{ 760 | about_me activities birthday checkins education_history 761 | events groups hometown interests likes location notes 762 | online_presence photo_video_tags photos relationships 763 | religion_politics status videos website work_history 764 | } 765 | scopes.map! do |scope| 766 | ["user_#{scope}", "friends_#{scope}"] 767 | end.flatten! 768 | 769 | scopes += %w{ 770 | read_insights read_stream read_mailbox read_friendlists read_requests 771 | email ads_management xmpp_login 772 | publish_stream create_event rsvp_event sms offline_access 773 | } 774 | end 775 | 776 | 777 | # This function expects arguments as a hash, so 778 | # it is agnostic to different POST handling variants in ruby. 779 | # 780 | # Validate the arguments received from facebook. This is usually 781 | # sent for the iframe in Facebook's canvas. It is not necessary 782 | # to use this on the auth_token and uid passed to callbacks like 783 | # post-add and post-remove. 784 | # 785 | # The arguments must be a mapping of to string keys and values 786 | # or a string of http request data. 787 | # 788 | # If the data is invalid or not signed properly, an empty 789 | # dictionary is returned. 790 | # 791 | # The secret argument should be an instance of FacebookSecret 792 | # to hide value from simple introspection. 793 | # 794 | # DEPRECATED, use verify_signature instead 795 | def MiniFB.validate(secret, arguments) 796 | 797 | signature = arguments.delete("fb_sig") 798 | return arguments if signature.nil? 799 | 800 | unsigned = Hash.new 801 | signed = Hash.new 802 | 803 | arguments.each do |k, v| 804 | if k =~ /^fb_sig_(.*)/ then 805 | signed[$1] = v 806 | else 807 | unsigned[k] = v 808 | end 809 | end 810 | 811 | arg_string = String.new 812 | signed.sort.each { |kv| arg_string << kv[0] << "=" << kv[1] } 813 | if Digest::MD5.hexdigest(arg_string + secret) != signature 814 | unsigned # Hash is incorrect, return only unsigned fields. 815 | else 816 | unsigned.merge signed 817 | end 818 | end 819 | 820 | class FaceBookSecret 821 | # Simple container that stores a secret value. 822 | # Proc cannot be dumped or introspected by normal tools. 823 | attr_reader :value 824 | 825 | def initialize(value) 826 | @value = Proc.new { value } 827 | end 828 | end 829 | 830 | private 831 | def self.post_params(params) 832 | post_params = {} 833 | params.each do |k, v| 834 | k = k.to_s unless k.is_a?(String) 835 | if Array === v || Hash === v 836 | post_params[k] = JSON.dump(v) 837 | else 838 | post_params[k] = v 839 | end 840 | end 841 | post_params 842 | end 843 | 844 | def self.signature_for(params, secret) 845 | params.delete_if { |k, v| v.nil? } 846 | raw_string = params.inject([]) do |collection, pair| 847 | collection << pair.map { |x| 848 | Array === x ? JSON.dump(x) : x 849 | }.join("=") 850 | collection 851 | end.sort.join 852 | Digest::MD5.hexdigest([raw_string, secret].join) 853 | end 854 | end 855 | --------------------------------------------------------------------------------