buddy 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # Buddy
2
+
3
+ Buddy is a lightweight Facebook library for Ruby.
4
+
5
+ ## Overview
6
+
7
+ * Support for iFrame Apps
8
+ * Uses signed_request parameters for authentication
9
+ * Supports the Graph API and old REST API
10
+ * Fully compatible with **Rails 3** and **Ruby 1.9.2**
11
+
12
+
13
+ ## License
14
+
15
+ released under the **MIT license**
16
+
17
+ Copyright (c) 2010:
18
+
19
+ * Ole Riesenberg
20
+ * Klaus Breyer
21
+
22
+ Some concepts and code adapted from [Facebooker](http://github.com/mmangino/facebooker)
23
+
24
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
25
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
26
+ THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,33 @@
1
+ module Buddy
2
+ module Rails
3
+ module BackwardsCompatibleParamChecks
4
+ def one_or_true( value )
5
+ case value
6
+ when String then
7
+ value == "1"
8
+ when Numeric then
9
+ value.to_f == 1.0
10
+ when TrueClass then
11
+ true
12
+ else
13
+ false
14
+ end
15
+ end
16
+
17
+ def zero_or_false( value )
18
+ case value
19
+ when String then
20
+ value.empty? || value == "0"
21
+ when Numeric then
22
+ value.to_f == 0.0
23
+ when FalseClass then
24
+ true
25
+ when NilClass then
26
+ true
27
+ else
28
+ false
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,88 @@
1
+ module Buddy
2
+ module Rails
3
+ module Controller
4
+ include Buddy::Rails::BackwardsCompatibleParamChecks
5
+ include Buddy::Rails::UrlHelper
6
+
7
+ def self.included(controller)
8
+ controller.helper_method :request_comes_from_facebook?
9
+ end
10
+
11
+ def js_redirect_to(uri, target = nil)
12
+ if request_is_facebook_canvas? and !request_is_facebook_tab? and !target.nil?
13
+ render(:text => "<script>#{target.to_s}.location.href='#{uri}'</script>", :layout => false)
14
+ elsif request_is_facebook_canvas? and !request_is_facebook_tab?
15
+ render(:text => "<script>self.location.href='#{uri}'</script>", :layout => false)
16
+ end
17
+ end
18
+
19
+ def top_redirect_to(uri)
20
+ js_redirect_to(uri, :top)
21
+ end
22
+
23
+ private
24
+ def facebook_session
25
+ @facebook_session
26
+ end
27
+
28
+ def application_is_installed?
29
+ !params[:oauth_token].blank?
30
+ end
31
+
32
+ def request_is_facebook_canvas?
33
+ params[:profile_id].blank?
34
+ end
35
+
36
+ def request_is_facebook_tab?
37
+ !params[:profile_id].blank?
38
+ end
39
+
40
+ def clear_facebook_session_information
41
+ session[:facebook_session] = nil
42
+ @facebook_session = nil
43
+ end
44
+
45
+ def new_facebook_session
46
+ Buddy::Session.create(Buddy.current_config['app_id'], Buddy.current_config['secret'])
47
+ end
48
+
49
+ def create_facebook_session
50
+ @facebook_session = new_facebook_session
51
+ @facebook_session.secure!(params[:user_id], params[:oauth_token], params[:expires])
52
+ @facebook_session.secured?
53
+ end
54
+
55
+ def set_facebook_session
56
+ session_set = !@facebook_session.blank? && @facebook_session.secured?
57
+ unless session_set
58
+ session_set = create_facebook_session
59
+ session[:facebook_session] = @facebook_session if session_set
60
+ end
61
+ if session_set
62
+ Session.current = @facebook_session
63
+ end
64
+ return session_set
65
+ end
66
+
67
+ def ensure_facebook_session
68
+ return create_new_facebook_session_and_redirect! unless application_is_installed?
69
+ set_facebook_session || create_new_facebook_session_and_redirect!
70
+ end
71
+
72
+ def default_after_install_url
73
+ url_for('/')
74
+ end
75
+
76
+ def create_new_facebook_session_and_redirect!
77
+ session[:facebook_session] = new_facebook_session
78
+ next_url = defined?(:after_install_url) ? default_after_install_url : after_install_url
79
+ top_redirect_to session[:facebook_session].install_url({:next => next_url}) if @installation_required
80
+ end
81
+
82
+ def ensure_application_is_installed
83
+ @installation_required = true
84
+ ensure_facebook_session
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,31 @@
1
+ module ::ActionController
2
+ class Base
3
+ def self.inherited_with_buddy(subclass)
4
+ inherited_without_buddy(subclass)
5
+ #if subclass.to_s == "ApplicationController"
6
+ subclass.send(:include, Buddy::Rails::Controller)
7
+ #subclass.helper Buddy::Rails::Helpers
8
+ #end
9
+ end
10
+ class << self
11
+ alias_method_chain :inherited, :buddy
12
+ end
13
+ end
14
+ end
15
+
16
+
17
+ # When making get requests, Facebook sends fb_sig parameters both in the query string
18
+ # and also in the post body. We want to ignore the query string ones because they are one
19
+ # request out of date
20
+ # We only do thise when there are POST parameters so that IFrame linkage still works
21
+ #class ActionController::Request
22
+ # def query_parameters_with_buddy
23
+ # if request_parameters.blank?
24
+ # query_parameters_without_buddy
25
+ # else
26
+ # (query_parameters_without_buddy||{}).reject {|key,value| key.to_s =~ /^fb_sig/}
27
+ # end
28
+ # end
29
+
30
+ # alias_method_chain :query_parameters, :buddy
31
+ #end
@@ -0,0 +1,24 @@
1
+ module Buddy
2
+ module Rails
3
+ module UrlHelper
4
+ def url_for(options = {})
5
+ options ||= {}
6
+ opts = case options
7
+ when Hash
8
+ options.merge!({ :only_path => true }) #unless options[:canvas] == false
9
+ else
10
+ options
11
+ end
12
+ url = super(opts)
13
+
14
+ if url.include?("http://") #todo: make it better
15
+ url
16
+ elsif options.is_a?(Hash) && options[:canvas] == false
17
+ "#{Buddy.current_config["callback_url"]}#{url}"
18
+ else
19
+ "http://apps.facebook.com/#{Buddy.current_config["canvas_page_name"]}#{url}"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ require 'buddy'
2
+ require 'buddy/rails/url_helper'
3
+ require 'rails'
4
+
5
+ module Buddy
6
+ class Railtie < ::Rails::Railtie
7
+ initializer "buddy.configure_rails_initialization" do |app|
8
+ app.config.middleware.insert_before(ActionDispatch::RemoteIp, ::Rack::Facebook::RemoteIp)
9
+ app.config.middleware.insert_before(ActionDispatch::ParamsParser, ::Rack::Facebook::ParamsParser)
10
+ app.config.action_controller.asset_host = Buddy.buddy_config['default']['callback_url']
11
+
12
+ ActionView::Helpers::UrlHelper.send(:include, Buddy::Rails::UrlHelper)
13
+
14
+ Mime::Type.register "text/html", :fbml
15
+ Mime::Type.register "text/javascript", :fbjs
16
+ Buddy.logger = ::Rails.logger
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,54 @@
1
+ require 'observer'
2
+ module Buddy
3
+ class OAuthException < ArgumentError
4
+ end
5
+
6
+ module Service
7
+ class << self
8
+ def call(api_method, params = {}, options = {})
9
+ Buddy.caller.call(api_method, params, options)
10
+ end
11
+
12
+ def get(resource, params = {})
13
+ result = GraphApiClient.get(resource, :query => params).parsed_response
14
+ raise OAuthException.new(result["error"]["message"]) if result["error"]
15
+ result
16
+ end
17
+
18
+ def post(resource, params = {})
19
+ result = GraphApiClient.post(resource, :query => params).parsed_response
20
+ raise OAuthException.new(result["error"]["message"]) if result["error"]
21
+ result
22
+ end
23
+ end
24
+
25
+ class GraphApiClient
26
+ include HTTParty
27
+ base_uri 'https://graph.facebook.com'
28
+ end
29
+
30
+ class Caller
31
+ include Observable
32
+ def call(api_method, params = {}, options = {})
33
+ begin
34
+ application = options[:app] || 'default'
35
+ rescue NoMethodError => e
36
+ raise ArgumentError.new("app not specified in buddy.yml")
37
+ end
38
+
39
+ changed
40
+ notify_observers(api_method, params, options)
41
+
42
+ result = nil
43
+ time = Benchmark.realtime do
44
+ result = MiniFB.call(Buddy.buddy_config[application]["api_key"],
45
+ Buddy.buddy_config[application]["secret"],
46
+ api_method,
47
+ params.stringify_keys)
48
+ end
49
+ Buddy.logger.info("Calling #{api_method} (#{params.inspect}) - #{time}")
50
+ result
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,91 @@
1
+ module Buddy
2
+ class Session
3
+ def self.create(app_id = nil, secret_key = nil)
4
+ app_id ||= self.app_id
5
+ secret_key ||= self.secret_key
6
+ raise ArgumentError unless !app_id.nil? && !secret_key.nil?
7
+ new(app_id, secret_key)
8
+ end
9
+
10
+ def self.app_id
11
+ Buddy.current_config["app_id"]
12
+ end
13
+
14
+ def self.api_key
15
+ Buddy.current_config["api_key"]
16
+ end
17
+
18
+ def self.secret_key
19
+ Buddy.current_config["secret"]
20
+ end
21
+
22
+ def self.current
23
+ Thread.current['facebook_session']
24
+ end
25
+
26
+ def self.current=(session)
27
+ Thread.current['facebook_session'] = session
28
+ end
29
+
30
+ def call(api_method, params = {}, options = {})
31
+ params.merge!(:uids => uid, :access_token => access_token)
32
+ Buddy::Service.call(api_method, params, options)
33
+ end
34
+
35
+ def get(resource, params = {})
36
+ params.merge!(:access_token => access_token) unless params[:access_token]
37
+ Buddy::Service.get(resource, params)
38
+ end
39
+
40
+ def post(resource, params = {})
41
+ params.merge!(:access_token => access_token) unless params[:access_token]
42
+ Buddy::Service.post(resource, params)
43
+ end
44
+
45
+ def install_url(options = {})
46
+ "http://www.facebook.com/connect/uiserver.php?app_id=#{Buddy.current_config['app_id']}&next=#{options[:next]}&display=page&locale=de_DE&return_session=0&fbconnect=0&canvas=1&legacy_return=1&method=permissions.request#{"&perms="+Buddy.current_config['perms'] if Buddy.current_config['perms']}"
47
+ end
48
+
49
+ def initialize(app_id, secret_key)
50
+ @app_id = app_id
51
+ @secret_key = secret_key
52
+ @uid = nil
53
+ @access_token = nil
54
+ @expires = nil
55
+ end
56
+
57
+ def secure!(uid, access_token, expires)
58
+ @uid = uid
59
+ @access_token = access_token
60
+ @expires = expires
61
+ end
62
+
63
+ def infinite?
64
+ @expires == 0
65
+ end
66
+
67
+ def expired?
68
+ @expires.nil? || (!infinite? && Time.at(@expires) <= Time.now)
69
+ end
70
+
71
+ def secured?
72
+ !@uid.nil? && !@access_token.nil? && !expired?
73
+ end
74
+
75
+ def user
76
+ @user ||= Buddy::User.new(@uid, self)
77
+ end
78
+
79
+ def uid
80
+ @uid
81
+ end
82
+
83
+ def access_token
84
+ @access_token
85
+ end
86
+
87
+ def expires
88
+ @expires
89
+ end
90
+ end
91
+ end
data/lib/buddy/user.rb ADDED
@@ -0,0 +1,22 @@
1
+ module Buddy
2
+ class User
3
+ FIELDS = [: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_hashes, :allowed_restrictions, :pic_with_logo, :pic_big_with_logo, :pic_small_with_logo, :pic_square_with_logo, :online_presence, :verified, :profile_blurb, :username, :website, :is_blocked, :family, :email]
4
+ STANDARD_FIELDS = [:uid, :first_name, :last_name, :name, :timezone, :birthday, :sex, :affiliations, :locale, :profile_url, :proxied_email, :email]
5
+
6
+ def initialize(uid, session)
7
+ @uid = uid
8
+ @session = session
9
+ end
10
+
11
+ def uid
12
+ @uid
13
+ end
14
+
15
+ alias :facebook_id :uid
16
+ alias :id :uid
17
+
18
+ def to_i
19
+ @uid
20
+ end
21
+ end
22
+ end
data/lib/buddy.rb ADDED
@@ -0,0 +1,70 @@
1
+ require 'bundler'
2
+
3
+ require 'base64'
4
+ require 'openssl'
5
+ require 'yajl'
6
+ require 'mini_fb'
7
+ require 'httparty'
8
+
9
+ require 'rack/facebook'
10
+
11
+ require 'buddy/user'
12
+ require 'buddy/session'
13
+ require 'buddy/service'
14
+
15
+ module Buddy
16
+ @buddy_config = {}
17
+
18
+ class << self
19
+ attr_accessor :logger, :caller
20
+
21
+ def load_configuration(yaml)
22
+ return false unless File.exist?(yaml)
23
+ @buddy_config = YAML.load(ERB.new(File.read(yaml)).result)[::Rails.env]
24
+ self.current_config = buddy_config['default']
25
+
26
+ buddy_config
27
+ end
28
+
29
+ def buddy_config
30
+ @buddy_config
31
+ end
32
+
33
+ def set_asset_host_to_callback_url
34
+ buddy_config[app]['set_asset_host_to_callback_url']
35
+ end
36
+
37
+ def timeout(app = 'default')
38
+ buddy_config[app]['timeout']
39
+ end
40
+
41
+ def use_application(api_key)
42
+ buddy_config.each do |c|
43
+ if c[1]["api_key"] == api_key
44
+ return self.current_config = c[1]
45
+ end
46
+ end
47
+ end
48
+
49
+ def current_config
50
+ Thread.current['current_buddy_config']
51
+ end
52
+
53
+ def current_config=(config)
54
+ Thread.current['current_buddy_config'] = config
55
+ end
56
+ end
57
+ end
58
+
59
+ buddy_config = File.join(Bundler.root, "config", "buddy.yml")
60
+
61
+ BUDDY = Buddy.load_configuration(buddy_config)
62
+ Buddy.logger = Rails.logger
63
+ Buddy.caller = Buddy::Service::Caller.new
64
+
65
+ require 'buddy/rails/backwards_compatible_param_checks'
66
+ require 'buddy/rails/url_helper'
67
+ require 'buddy/rails/controller'
68
+ require 'buddy/rails/controller_extensions'
69
+
70
+ require 'buddy/railtie'
@@ -0,0 +1,86 @@
1
+ module Rack
2
+ # This Rack middleware checks the signature of Facebook params, and
3
+ # converts them to Ruby objects when appropiate. Also, it converts
4
+ # the request method from the Facebook POST to the original HTTP
5
+ # method used by the client.
6
+ #
7
+ # If the signature is wrong, it returns a "400 Invalid Facebook Signature".
8
+ #
9
+ # Optionally, it can take a block that receives the Rack environment
10
+ # and returns a value that evaluates to true when we want the middleware to
11
+ # be executed for the specific request.
12
+ #
13
+ # == Usage
14
+ #
15
+ # In your config.ru:
16
+ #
17
+ # require 'rack/facebook'
18
+ # use Rack::Facebook, "my_facebook_secret_key"
19
+ #
20
+ # Using a block condition:
21
+ #
22
+ # use Rack::Facebook, "my_facebook_secret_key" do |env|
23
+ # env['REQUEST_URI'] =~ /^\/facebook_only/
24
+ # end
25
+ #
26
+
27
+ module Facebook
28
+ class RemoteIp
29
+ def initialize(app)
30
+ @app = app
31
+ end
32
+
33
+ def call(env)
34
+ env['REMOTE_ADDR'] = env['X-FB-USER-REMOTE-ADDR'] if env['X-FB-USER-REMOTE-ADDR']
35
+ @app.call(env)
36
+ end
37
+ end
38
+
39
+ class ParamsParser
40
+ def initialize(app, &condition)
41
+ @app = app
42
+ @condition = condition
43
+ end
44
+
45
+ def call(env)
46
+ return @app.call(env) unless @condition.nil? || @condition.call(env)
47
+
48
+ request = Rack::Request.new(env)
49
+
50
+ signed_request = request.params["signed_request"]
51
+ if signed_request
52
+ signature, signed_params = signed_request.split('.')
53
+
54
+ unless signed_request_is_valid?(Buddy.current_config['secret'], signature, signed_params)
55
+ return Rack::Response.new(["Invalid Facebook signature"], 400).finish
56
+ end
57
+
58
+ signed_params = Yajl::Parser.new.parse(base64_url_decode(signed_params))
59
+ signed_params.each do |k,v|
60
+ request.params[k] = v
61
+ end
62
+ end
63
+
64
+ @app.call(env)
65
+ end
66
+
67
+ private
68
+
69
+ # This function takes the app secret and the signed request, and verifies if the request is valid.
70
+ def signed_request_is_valid?(secret, signature, params)
71
+ sig = base64_url_decode(signature)
72
+ expected_sig = OpenSSL::HMAC.digest('SHA256', secret, params.tr("-_", "+/"))
73
+ return sig == expected_sig
74
+ end
75
+
76
+ # Ruby's implementation of base64 decoding reads the string in multiples of 6 and ignores any extra bytes.
77
+ # Since facebook does not take this into account, this function fills any string with white spaces up to
78
+ # the point where it becomes divisible by 6, then it replaces '-' with '+' and '_' with '/' (URL-safe decoding),
79
+ # and decodes the result.
80
+ def base64_url_decode(str)
81
+ str = str + "=" * (6 - str.size % 6) unless str.size % 6 == 0
82
+ return Base64.decode64(str.tr("-_", "+/"))
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,10 @@
1
+ require 'test/unit'
2
+ require 'mocha'
3
+
4
+ class BuddyTests < Test::Unit::TestCase
5
+ def test_user_getinfo
6
+ result = [{"first_name"=>"Ole", "name"=>"Foo Bar", "uid"=>123456789}]
7
+
8
+ Buddy::Service.expects(:call).with('Users.getInfo', {:uids => '686373959', :fields => 'uid, name, first_name'}).returns(result)
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: buddy
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 4
8
+ - 0
9
+ version: 0.4.0
10
+ platform: ruby
11
+ authors:
12
+ - Ole Riesenberg
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-09-08 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: mini_fb
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ - 2
31
+ - 2
32
+ version: 0.2.2
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: yajl-ruby
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: httparty
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ type: :runtime
60
+ version_requirements: *id003
61
+ description: buddybrand's facebook library
62
+ email: labs@buddybrand.de
63
+ executables: []
64
+
65
+ extensions: []
66
+
67
+ extra_rdoc_files:
68
+ - README.md
69
+ files:
70
+ - lib/buddy.rb
71
+ - lib/buddy/rails/backwards_compatible_param_checks.rb
72
+ - lib/buddy/rails/controller.rb
73
+ - lib/buddy/rails/controller_extensions.rb
74
+ - lib/buddy/rails/url_helper.rb
75
+ - lib/buddy/railtie.rb
76
+ - lib/buddy/service.rb
77
+ - lib/buddy/session.rb
78
+ - lib/buddy/user.rb
79
+ - lib/rack/facebook.rb
80
+ - test/test_buddy.rb
81
+ - README.md
82
+ has_rdoc: true
83
+ homepage: http://buddybrand.de
84
+ licenses: []
85
+
86
+ post_install_message:
87
+ rdoc_options:
88
+ - --charset=UTF-8
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ segments:
97
+ - 0
98
+ version: "0"
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ segments:
105
+ - 0
106
+ version: "0"
107
+ requirements: []
108
+
109
+ rubyforge_project:
110
+ rubygems_version: 1.3.7
111
+ signing_key:
112
+ specification_version: 3
113
+ summary: buddybrand's facebook library
114
+ test_files:
115
+ - test/test_buddy.rb