Module: MiniFB

Defined in:
lib/mini_fb.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_VIDEO_URL =
"https://api-video.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 }
@@logging =
false
@@log =
Logger.new(STDOUT)

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



562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# File 'lib/mini_fb.rb', line 562

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}"
#      resp = RestClient.get url
    options = {}
    options[:params] = params
    options[:method] = :get
    options[:response_type] = :params
    resp = fetch(url, options)
    puts 'resp=' + resp.body.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.



356
357
358
359
# File 'lib/mini_fb.rb', line 356

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
250
# 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")
    mime_type = kwargs.delete("mime_type") || 'image/jpeg'

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

    fb_method = kwargs["method"].downcase
    if (fb_method == "photos.upload" || fb_method == 'video.upload')
        # Then we need a multipart post
        response = MiniFB.post_upload(file_name, kwargs, mime_type)
    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

.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

.ensure_correct_extension(filename, mime_type) ⇒ Object



406
407
408
409
410
411
412
# File 'lib/mini_fb.rb', line 406

def self.ensure_correct_extension(filename, mime_type)
    allowed_extensions = MIME::Types[mime_type].first.extensions
    extension = File.extname(filename)[1 .. -1]
    if !allowed_extensions.include? extension
      filename += '.' + allowed_extensions.first
    end
end

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



659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
# File 'lib/mini_fb.rb', line 659

def self.fetch(url, options={})

    begin
        if options[:method] == :post
            @@log.debug 'url_post=' + url if @@logging
            resp = RestClient.post url, options[:params]
        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
            resp = RestClient.get url
        end

        @@log.debug 'resp=' + resp.to_s if @@log.debug?

        if options[:response_type] == :params
            # Some methods return a param like string, for example: access_token=11935261234123|rW9JMxbN65v_pFWQl5LmHHABC
            params = {}
            params_array = resp.split("&")
            params_array.each do |p|
                ps = p.split("=")
                params[ps[0]] = ps[1]
            end
            return params
        else
            begin
                res_hash = JSON.parse(resp.to_s)
            rescue
                # quick fix for things like stream.publish that don't return json
                res_hash = JSON.parse("{\"response\": #{resp.to_s}}")
            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 = 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

        return res_hash
    rescue RestClient::Exception => ex
        puts "ex.http_code=" + ex.http_code.to_s
        puts 'ex.http_body=' + ex.http_body if @@logging
        res_hash = JSON.parse(ex.http_body) # probably should ensure it has a good response
        raise MiniFB::FaceBookError.new(ex.http_code, "#{res_hash["error"]["type"]}: #{res_hash["error"]["message"]}")
    end

end

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

Executes an FQL query



620
621
622
623
624
625
626
627
628
629
# File 'lib/mini_fb.rb', line 620

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 metadata in response. true/false
- params: Any additional parameters you would like to submit


583
584
585
586
587
588
589
590
591
592
# File 'lib/mini_fb.rb', line 583

def self.get(access_token, id, options={})
    url = "#{graph_base}#{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



515
516
517
# File 'lib/mini_fb.rb', line 515

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


398
399
400
401
402
403
# File 'lib/mini_fb.rb', line 398

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)" })


636
637
638
639
640
641
642
643
644
645
# File 'lib/mini_fb.rb', line 636

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

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

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



531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
# File 'lib/mini_fb.rb', line 531

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 = RestClient.get oauth_url
    puts 'resp=' + resp.body.to_s if @@logging
    params = {}
    params_array = resp.split("&")
    params_array.each do |p|
        ps = p.split("=")
        params[ps[0]] = ps[1]
    end
    return params
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



549
550
551
552
553
554
555
556
557
558
559
# File 'lib/mini_fb.rb', line 549

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


521
522
523
524
525
526
527
528
# File 'lib/mini_fb.rb', line 521

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.



367
368
369
370
# File 'lib/mini_fb.rb', line 367

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 metadata in response. true/false
- params: Any additional parameters you would like to submit


599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
# File 'lib/mini_fb.rb', line 599

def self.post(access_token, id, options={})
    url = "#{graph_base}#{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, mime_type = 'image/jpeg') ⇒ Object



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
277
278
279
280
281
# File 'lib/mini_fb.rb', line 252

def MiniFB.post_upload(filename, kwargs, mime_type = 'image/jpeg')
    content = File.open(filename, 'rb') { |f| f.read }
    boundary = "END_OF_PART_#{rand(1 << 64).to_s(16)}"
    header = {'Content-type' => "multipart/form-data, boundary=#{boundary}"}

    # Make sure the filename has the correct extension.
    # Facebook is really picky about this.
    remote_filename = ensure_correct_extension(File.basename(filename), mime_type)

    # 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=\"#{remote_filename}\"\r\n" <<
            "Content-Transfer-Encoding: binary\r\n" <<
            "Content-Type: #{mime_type}\r\n\r\n" <<
            content <<
            "\r\n" <<
            "--#{boundary}--"

    # Call Facebook with POST multipart/form-data request
    url = (mime_type.split('/').first == 'video') ? FB_VIDEO_URL : FB_URL
    raw_post(url, query, header)
end

.raw_post(url, body, headers) ⇒ Object



283
284
285
286
287
288
289
290
291
292
# File 'lib/mini_fb.rb', line 283

def MiniFB.raw_post(url, body, headers)
    uri = URI.parse(url)
    uri.port = (uri.scheme == 'https') ? 443 : 80
    http = Net::HTTP.new(uri.host, uri.port)
    if uri.scheme == 'https'
        http.use_ssl = true
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    end
    http.start { |h| h.post(uri.path, body, headers) }
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


650
651
652
653
654
655
656
657
# File 'lib/mini_fb.rb', line 650

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.



714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
# File 'lib/mini_fb.rb', line 714

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



344
345
346
347
348
349
350
# File 'lib/mini_fb.rb', line 344

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



751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
# File 'lib/mini_fb.rb', line 751

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.



388
389
390
391
# File 'lib/mini_fb.rb', line 388

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.



379
380
381
382
383
384
385
# File 'lib/mini_fb.rb', line 379

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.



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/mini_fb.rb', line 295

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.



335
336
337
338
339
340
# File 'lib/mini_fb.rb', line 335

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