Module: MiniFB

Defined in:
lib/mini_fb.rb,
lib/version.rb

Defined Under Namespace

Classes: FaceBookError, FaceBookSecret, GraphObject, OAuthSession, Photos, Session, User

Constant Summary collapse

FB_URL =

Global constants

"http://api.facebook.com/restserver.php"
FB_API_VERSION =
"1.0"
BAD_JSON_METHODS =
["users.getloggedinuser", "auth.promotesession", "users.hasapppermission",
                    "Auth.revokeExtendedPermission", "auth.revokeAuthorization",
                    "pages.isAdmin", "pages.isFan",
                    "stream.publish",
                    "dashboard.addNews", "dashboard.addGlobalNews", "dashboard.publishActivity",
                    "dashboard.incrementcount", "dashboard.setcount"
].collect { |x| x.downcase }
VERSION =
"2.4.0"
@@logging =
false
@@log =
Logger.new(STDOUT)
@@http =
HTTPClient.new

Class Method Summary collapse

Class Method Details

.authenticate_as_app(app_id, secret) ⇒ Object

Return a JSON object of working Oauth tokens from working session keys, returned in order given



555
556
557
558
559
560
561
562
563
564
565
566
567
568
# File 'lib/mini_fb.rb', line 555

def self.authenticate_as_app(app_id, secret)
    url = "#{graph_base}oauth/access_token"
    params = {}
    params["type"] = "client_cred"
    params["client_id"] = "#{app_id}"
    params["client_secret"] = "#{secret}"
    options = {}
    options[:params] = params
    options[:method] = :get
    options[:response_type] = :json
    resp = fetch(url, options)
    puts 'resp=' + resp.to_s if @@logging
    resp
end

.base64_url_decode(str) ⇒ Object

Ruby’s implementation of base64 decoding seems to be reading the string in multiples of 4 and ignoring any extra characters if there are no white-space characters at the end. Since facebook does not take this into account, this function fills any string with white spaces up to the point where it becomes divisible by 4, then it replaces ‘-’ with ‘+’ and ‘_’ with ‘/’ (URL-safe decoding), and decodes the result.



340
341
342
343
# File 'lib/mini_fb.rb', line 340

def self.base64_url_decode(str)
    str = str + "=" * (4 - str.size % 4) unless str.size % 4 == 0
    return Base64.decode64(str.tr("-_", "+/"))
end

.call(api_key, secret, method, kwargs) ⇒ Object

The secret argument should be an instance of FacebookSecret to hide value from simple introspection.



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/mini_fb.rb', line 185

def MiniFB.call(api_key, secret, method, kwargs)

    puts 'kwargs=' + kwargs.inspect if @@logging

    if secret.is_a? String
        secret = FaceBookSecret.new(secret)
    end

    # Prepare arguments for call
    call_id = kwargs.fetch("call_id", true)
    if call_id == true
        kwargs["call_id"] = Time.now.tv_sec.to_s
    else
        kwargs.delete("call_id")
    end

    custom_format = kwargs.include?("format") || kwargs.include?("callback")
    kwargs["format"] ||= "JSON"
    kwargs["v"] ||= FB_API_VERSION
    kwargs["api_key"]||= api_key
    kwargs["method"] ||= method

    file_name = kwargs.delete("filename")

    kwargs["sig"] = signature_for(kwargs, secret.value.call)

    fb_method = kwargs["method"].downcase
    if fb_method == "photos.upload"
        # Then we need a multipart post
        response = MiniFB.post_upload(file_name, kwargs)
    else

        begin
            response = Net::HTTP.post_form(URI.parse(FB_URL), post_params(kwargs))
        rescue SocketError => err
            # why are we catching this and throwing as different error?  hmmm..
            # raise IOError.new( "Cannot connect to the facebook server: " + err )
            raise err
        end
    end

    # Handle response
    return response.body if custom_format

    body = response.body

    puts 'response=' + body.inspect if @@logging
    begin
        data = JSON.parse(body)
        if data.include?("error_msg")
            raise FaceBookError.new(data["error_code"] || 1, data["error_msg"])
        end

    rescue JSON::ParserError => ex
        if BAD_JSON_METHODS.include?(fb_method) # Little hack because this response isn't valid JSON
            if body == "0" || body == "false"
                return false
            end
            return body
        else
            raise ex
        end
    end
    return data
end

.delete(access_token, ids, options = {}) ⇒ Object

Sends a DELETE request to the Facebook Graph API options:

- type: eg: feed, home, etc
- metadata: to include  in response. true/false
- params: Any additional parameters you would like to submit


643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
# File 'lib/mini_fb.rb', line 643

def self.delete(access_token, ids, options={})
    url = graph_base
    url << "v#{options[:version]}/" if options[:version]
    params = options[:params] || {}
    if ids.is_a?(Array)
      params["ids"] = ids.join(',')
    else
      url << "#{ids}"
    end
    url << "/#{options[:type]}" if options[:type]
    options.delete(:type)
    options.each do |key, value|
        params[key] = "#{value}"
    end
    params["access_token"] = "#{(access_token)}"
    options[:params] = params
    options[:method] = :delete
    return fetch(url, options)

end

.disable_loggingObject



58
59
60
61
# File 'lib/mini_fb.rb', line 58

def self.disable_logging
    @@logging = false
    @@log.level = Logger::ERROR
end

.enable_loggingObject



53
54
55
56
# File 'lib/mini_fb.rb', line 53

def self.enable_logging
    @@logging = true
    @@log.level = Logger::DEBUG
end

.fb_exchange_token(app_id, secret, access_token) ⇒ Object

Gets long-lived token from the Facebook Graph API options:

- app_id: your app ID (string)
- secret: your app secret (string)
- access_token: short-lived user token (string)

returns a hash with one value being ‘access_token’, the other being ‘expires_in’

Throws MiniFB::FaceBookError if response from Facebook Graph API is not successful



525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/mini_fb.rb', line 525

def self.fb_exchange_token(app_id, secret, access_token)
    oauth_url = "#{graph_base}oauth/access_token"
    oauth_url << "?client_id=#{app_id}"
    oauth_url << "&client_secret=#{secret}"
    oauth_url << "&grant_type=fb_exchange_token"
    oauth_url << "&fb_exchange_token=#{CGI.escape(access_token)}"
    response = @@http.get oauth_url
    body = response.body.to_s
    puts 'resp=' + body if @@logging
    res_hash = JSON.parse(body)
    unless response.ok?
      raise MiniFB::FaceBookError.new(response.status, "#{res_hash["error"]["type"]}: #{res_hash["error"]["message"]}")
    end
    return res_hash
end

.fetch(url, options = {}) ⇒ Object



704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
# File 'lib/mini_fb.rb', line 704

def self.fetch(url, options={})
    case options[:method]
    when :post
        @@log.debug 'url_post=' + url if @@logging
        response = @@http.post url, options[:params]
    when :delete
        if options[:params] && options[:params].size > 0
            url += '?' + options[:params].map { |k, v|  CGI.escape(k.to_s) + '=' + CGI.escape(v.to_s) }.join('&')
        end
        @@log.debug 'url_delete=' + url if @@logging
        response = @@http.delete url
    else
        if options[:params] && options[:params].size > 0
            url += '?' + options[:params].map { |k, v|  CGI.escape(k.to_s) + '=' + CGI.escape(v.to_s) }.join('&')
        end
        @@log.debug 'url_get=' + url if @@logging
        response = @@http.get url
    end

    resp = response.body
    @@log.debug "Response = #{resp}. Status = #{response.status}" if @@logging
    @@log.debug 'API Version =' + response.headers["Facebook-API-Version"].to_s if @@logging

    if options[:response_type] == :params
        # Some methods return a param like string, for example: access_token=11935261234123|rW9JMxbN65v_pFWQl5LmHHABC
        params = {}
        params_array = resp.to_s.split("&")
        params_array.each do |p|
            ps = p.split("=")
            params[ps[0]] = ps[1]
        end
        return params
    else
        res_hash = JSON.parse(resp.to_s.size > 2 ? resp.to_s : {response: resp.to_s}.to_json)
        unless response.ok?
            raise MiniFB::FaceBookError.new(response.status, "#{res_hash["error"]["type"]}: #{res_hash["error"]["message"]}")
        end
    end

    if res_hash.is_a? Array # fql  return this
        res_hash.collect! { |x| x.is_a?(Hash) ? Hashie::Mash.new(x) : x }
    else
        res_hash = { response: res_hash } unless res_hash.is_a? Hash
        res_hash = Hashie::Mash.new(res_hash)
    end

    if res_hash.include?("error_msg")
        raise FaceBookError.new(res_hash["error_code"] || 1, res_hash["error_msg"])
    end

    res_hash
end

.fql(access_token, fql_query, options = {}) ⇒ Object

Executes an FQL query



665
666
667
668
669
670
671
672
673
674
# File 'lib/mini_fb.rb', line 665

def self.fql(access_token, fql_query, options={})
    url = "https://api.facebook.com/method/fql.query"
    params = options[:params] || {}
    params["access_token"] = "#{(access_token)}"
    params["metadata"] = "1" if options[:metadata]
    params["query"] = fql_query
    params["format"] = "JSON"
    options[:params] = params
    return fetch(url, options)
end

.get(access_token, id, options = {}) ⇒ Object

Gets data from the Facebook Graph API options:

- type: eg: feed, home, etc
- metadata: to include  in response. true/false
- params: Any additional parameters you would like to submit


575
576
577
578
579
580
581
582
583
584
585
586
# File 'lib/mini_fb.rb', line 575

def self.get(access_token, id, options={})
    url = graph_base
    url << "v#{options[:version]}/" if options[:version]
    url << id
    url << "/#{options[:type]}" if options[:type]
    params = options[:params] || {}
    params["access_token"] = "#{(access_token)}"
    params["metadata"] = "1" if options[:metadata]
    params["fields"] = options[:fields].join(",") if options[:fields]
    options[:params] = params
    return fetch(url, options)
end

.graph_baseObject



490
491
492
# File 'lib/mini_fb.rb', line 490

def self.graph_base
    "https://graph.facebook.com/"
end

.log_level=(level) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/mini_fb.rb', line 34

def self.log_level=(level)
    if level.is_a? Numeric
        @@log.level = level
    else
        @@log.level = case level
            when :fatal
                @@log.level = Logger::FATAL
            when :error
                @@log.level = Logger::ERROR
            when :warn
                @@log.level = Logger::WARN
            when :info
                @@log.level = Logger::INFO
            when :debug
                @@log.level = Logger::DEBUG
                      end
    end
end

.login_url(api_key, options = {}) ⇒ Object

Returns the login/add app url for your application.

options:

- :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
- :canvas => true/false - to say whether this is a canvas app or not


382
383
384
385
386
387
# File 'lib/mini_fb.rb', line 382

def self.(api_key, options={})
     = "http://api.facebook.com/login.php?api_key=#{api_key}"
     << "&next=#{options[:next]}" if options[:next]
     << "&canvas" if options[:canvas]
    
end

.multifql(access_token, fql_queries, options = {}) ⇒ Object

Executes multiple FQL queries Example:

MiniFB.multifql(access_token, { :statuses => “SELECT status_id, message FROM status WHERE uid = 12345”,

:privacy => "SELECT object_id, description FROM privacy WHERE object_id IN (SELECT status_id FROM #statuses)" })


681
682
683
684
685
686
687
688
689
690
# File 'lib/mini_fb.rb', line 681

def self.multifql(access_token, fql_queries, options={})
    url = "https://api.facebook.com/method/fql.multiquery"
    params = options[:params] || {}
    params["access_token"] = "#{(access_token)}"
    params["metadata"] = "1" if options[:metadata]
    params["queries"] = JSON[fql_queries]
    params[:format] = "JSON"
    options[:params] = params
    return fetch(url, options)
end

.multiget(access_token, ids, options = {}) ⇒ Object

Gets multiple data from the Facebook Graph API options:

- type: eg: feed, home, etc
- metadata: to include  in response. true/false
- params: Any additional parameters you would like to submit

Example:

MiniFB.multiget(access_token, [123, 234])

Can throw a connection Timeout if there is too many items



598
599
600
601
602
603
604
605
606
607
608
609
# File 'lib/mini_fb.rb', line 598

def self.multiget(access_token, ids, options={})
    url = graph_base
    url << "v#{options[:version]}/" if options[:version]
    url << "#{options[:type]}" if options[:type]
    params = options[:params] || {}
    params["ids"] = ids.join(',')
    params["access_token"] = "#{(access_token)}"
    params["metadata"] = "1" if options[:metadata]
    params["fields"] = options[:fields].join(",") if options[:fields]
    options[:params] = params
    return fetch(url, options)
end

.oauth_access_token(app_id, redirect_uri, secret, code) ⇒ Object

returns a hash with one value being ‘access_token’, the other being ‘expires’



506
507
508
509
510
511
512
513
514
515
# File 'lib/mini_fb.rb', line 506

def self.oauth_access_token(app_id, redirect_uri, secret, code)
    oauth_url = "#{graph_base}oauth/access_token"
    oauth_url << "?client_id=#{app_id}"
    oauth_url << "&redirect_uri=#{CGI.escape(redirect_uri)}"
    oauth_url << "&client_secret=#{secret}"
    oauth_url << "&code=#{CGI.escape(code)}"
    resp = @@http.get oauth_url
    puts 'resp=' + resp.body.to_s if @@logging
    JSON.parse(resp.body.to_s)
end

.oauth_exchange_session(app_id, secret, session_keys) ⇒ Object

Return a JSON object of working Oauth tokens from working session keys, returned in order given



542
543
544
545
546
547
548
549
550
551
552
# File 'lib/mini_fb.rb', line 542

def self.oauth_exchange_session(app_id, secret, session_keys)
    url = "#{graph_base}oauth/exchange_sessions"
    params = {}
    params["client_id"] = "#{app_id}"
    params["client_secret"] = "#{secret}"
    params["sessions"] = "#{session_keys}"
    options = {}
    options[:params] = params
    options[:method] = :post
    return fetch(url, options)
end

.oauth_url(app_id, redirect_uri, options = {}) ⇒ Object

options:

- scope: comma separated list of extends permissions. see http://developers.facebook.com/docs/authentication/permissions


496
497
498
499
500
501
502
503
# File 'lib/mini_fb.rb', line 496

def self.oauth_url(app_id, redirect_uri, options={})
    oauth_url = "#{graph_base}oauth/authorize"
    oauth_url << "?client_id=#{app_id}"
    oauth_url << "&redirect_uri=#{CGI.escape(redirect_uri)}"
#        oauth_url << "&scope=#{options[:scope]}" if options[:scope]
    oauth_url << ("&" + options.map { |k, v| "%s=%s" % [k, v] }.join('&')) unless options.empty?
    oauth_url
end

Parses cookies in order to extract the facebook cookie and parse it into a useable hash

options:

  • app_id - the connect applications app_id (some users may find they have to use their facebook API key)

  • secret - the connect application secret

  • 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
352
353
354
# File 'lib/mini_fb.rb', line 351

def MiniFB.parse_cookie_information(app_id, cookies)
    return nil if cookies["fbs_#{app_id}"].nil?
    Hash[*cookies["fbs_#{app_id}"].split('&').map { |v| v.gsub('"', '').split('=', 2) }.flatten]
end

.post(access_token, id, options = {}) ⇒ Object

Posts data to the Facebook Graph API options:

- type: eg: feed, home, etc
- metadata: to include  in response. true/false
- params: Any additional parameters you would like to submit


616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
# File 'lib/mini_fb.rb', line 616

def self.post(access_token, id, options={})
    url = graph_base
    url << "v#{options[:version]}/" if options[:version]
    url << id
    url << "/#{options[:type]}" if options[:type]
    options.delete(:type)
    params = options[:params] || {}
    options.each do |key, value|
        if value.kind_of?(File)
            params[key] = value
        else
            params[key] = "#{value}"
        end
    end
    params["access_token"] = "#{(access_token)}"
    params["metadata"] = "1" if options[:metadata]
    options[:params] = params
    options[:method] = :post
    return fetch(url, options)

end

.post_upload(filename, kwargs) ⇒ Object



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/mini_fb.rb', line 251

def MiniFB.post_upload(filename, kwargs)
    content = File.open(filename, 'rb') { |f| f.read }
    boundary = Digest::MD5.hexdigest(content)
    header = {'Content-type' => "multipart/form-data, boundary=#{boundary}"}

    # Build query
    query = ''
    kwargs.each { |a, v|
        query <<
                "--#{boundary}\r\n" <<
                "Content-Disposition: form-data; name=\"#{a}\"\r\n\r\n" <<
                "#{v}\r\n"
    }
    query <<
            "--#{boundary}\r\n" <<
            "Content-Disposition: form-data; filename=\"#{File.basename(filename)}\"\r\n" <<
            "Content-Transfer-Encoding: binary\r\n" <<
            "Content-Type: image/jpeg\r\n\r\n" <<
            content <<
            "\r\n" <<
            "--#{boundary}--"

    # Call Facebook with POST multipart/form-data request
    uri = URI.parse(FB_URL)
    Net::HTTP.start(uri.host) { |http| http.post uri.path, query, header }
end

.rest(access_token, api_method, options = {}) ⇒ Object

Uses new Oauth 2 authentication against old Facebook REST API options:

- params: Any additional parameters you would like to submit


695
696
697
698
699
700
701
702
# File 'lib/mini_fb.rb', line 695

def self.rest(access_token, api_method, options={})
    url = "https://api.facebook.com/method/#{api_method}"
    params = options[:params] || {}
    params[:access_token] = access_token
    params[:format] = "JSON"
    options[:params] = params
    return fetch(url, options)
end

.scopesObject

Returns all available scopes.



758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
# File 'lib/mini_fb.rb', line 758

def self.scopes
    scopes = %w{
        about_me activities birthday checkins education_history
        events groups hometown interests likes location notes
        online_presence photo_video_tags photos relationships
        religion_politics status videos website work_history
    }
    scopes.map! do |scope|
        ["user_#{scope}", "friends_#{scope}"]
    end.flatten!

    scopes += %w{
      read_insights read_stream read_mailbox read_friendlists read_requests
      email ads_management xmpp_login
      publish_stream create_event rsvp_event sms offline_access
    }
end

.signed_request_params(secret, req) ⇒ Object

This function decodes the data sent by Facebook and returns a Hash. See: developers.facebook.com/docs/authentication/canvas



328
329
330
331
332
333
334
# File 'lib/mini_fb.rb', line 328

def self.signed_request_params(secret, req)
    s, p = req.split(".")
    p = base64_url_decode(p)
    h = JSON.parse(p)
    h.delete('algorithm') if h['algorithm'] == 'HMAC-SHA256'
    h
end

.validate(secret, arguments) ⇒ Object

DEPRECATED, use verify_signature instead



795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
# File 'lib/mini_fb.rb', line 795

def MiniFB.validate(secret, arguments)

    signature = arguments.delete("fb_sig")
    return arguments if signature.nil?

    unsigned = Hash.new
    signed = Hash.new

    arguments.each do |k, v|
        if k =~ /^fb_sig_(.*)/ then
            signed[$1] = v
        else
            unsigned[k] = v
        end
    end

    arg_string = String.new
    signed.sort.each { |kv| arg_string << kv[0] << "=" << kv[1] }
    if Digest::MD5.hexdigest(arg_string + secret) != signature
        unsigned # Hash is incorrect, return only unsigned fields.
    else
        unsigned.merge signed
    end
end

.verify_connect_signature(api_key, secret, cookies) ⇒ Object

DEPRECATED: Please use verify_cookie_signature instead.



372
373
374
375
# File 'lib/mini_fb.rb', line 372

def MiniFB.verify_connect_signature(api_key, secret, cookies)
    warn "DEPRECATION WARNING: 'verify_connect_signature' has been renamed to 'verify_cookie_signature' as Facebook no longer calls this 'connect'"
    MiniFB.verify_cookie_signature(api_key, secret, cookies)
end

Validates that the cookies sent by the user are those that were set by facebook. Since your secret is only known by you and facebook it is used to sign all of the cookies set.

options:

  • app_id - the connect applications app_id (some users may find they have to use their facebook API key)

  • secret - the connect application secret

  • 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
364
365
366
367
368
369
# File 'lib/mini_fb.rb', line 363

def MiniFB.verify_cookie_signature(app_id, secret, cookies)
    fb_keys = MiniFB.parse_cookie_information(app_id, cookies)
    return false if fb_keys.nil?

    signature = fb_keys.delete('sig')
    return signature == Digest::MD5.hexdigest(fb_keys.map { |k, v| "#{k}=#{v}" }.sort.join + secret)
end

.verify_signature(secret, arguments) ⇒ Object

Returns true is signature is valid, false otherwise.



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/mini_fb.rb', line 279

def MiniFB.verify_signature(secret, arguments)
    if arguments.is_a? String
        #new way: params[:session]
        session = JSON.parse(arguments)

        signature = session.delete('sig')
        return false if signature.nil?

        arg_string = String.new
        session.sort.each { |k, v| arg_string << "#{k}=#{v}" }
        if Digest::MD5.hexdigest(arg_string + secret) == signature
            return true
        end
    else
        #old way

        signature = arguments.delete("fb_sig")
        return false if signature.nil?

        unsigned = Hash.new
        signed = Hash.new

        arguments.each do |k, v|
            if k =~ /^fb_sig_(.*)/ then
                signed[$1] = v
            else
                unsigned[k] = v
            end
        end

        arg_string = String.new
        signed.sort.each { |kv| arg_string << kv[0] << "=" << kv[1] }
        if Digest::MD5.hexdigest(arg_string + secret) == signature
            return true
        end
    end
    return false
end

.verify_signed_request(secret, req) ⇒ Object

This function takes the app secret and the signed request, and verifies if the request is valid.



319
320
321
322
323
324
# File 'lib/mini_fb.rb', line 319

def self.verify_signed_request(secret, req)
    s, p = req.split(".")
    sig = base64_url_decode(s)
    expected_sig = OpenSSL::HMAC.digest('SHA256', secret, p)
    return sig == expected_sig
end