rack-fb 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/README.markdown +75 -0
  2. data/Rakefile +74 -0
  3. data/lib/mini_fb.rb +279 -0
  4. data/lib/rack/facebook.rb +154 -0
  5. metadata +78 -0
@@ -0,0 +1,75 @@
1
+ = rack-fb
2
+
3
+ Rack-fb is currently a work in progress. It aims to be a lightweight client for the [Facebook API](http://wiki.developers.facebook.com/index.php/API).
4
+
5
+ Installation
6
+ -------------
7
+
8
+ gem install rack-fb
9
+
10
+ General Usage
11
+ -------------
12
+
13
+ The most general case is to use MiniFB.call method:
14
+
15
+ user_hash = MiniFB.call(FB_API_KEY, FB_SECRET, "Users.getInfo", "session_key"=>@session_key, "uids"=>@uid, "fields"=>User.all_fields)
16
+
17
+ Which simply returns the parsed json response from Facebook.
18
+
19
+ Some Higher Level Objects for Common Uses
20
+ ----------------------
21
+
22
+ Get a MiniFB::Session:
23
+
24
+ @fb = MiniFB::Session.new(FB_API_KEY, FB_SECRET, @fb_session, @fb_uid)
25
+
26
+ With the session, you can then get the user information for the session/uid.
27
+
28
+ user = @fb.user
29
+
30
+ Then get info from the user:
31
+
32
+ first_name = user["first_name"]
33
+
34
+ Or profile photos:
35
+
36
+ photos = user.profile_photos
37
+
38
+ Or if you want other photos, try:
39
+
40
+ photos = @fb.photos("pids"=>[12343243,920382343,9208348])
41
+
42
+ Support
43
+ --------
44
+
45
+ Join our Discussion Group at: http://groups.google.com/group/mini_fb
46
+
47
+ =======
48
+ This Rack middleware checks the signature of Facebook params, and
49
+ converts them to Ruby objects when appropiate. Also, it converts
50
+ the request method from the Facebook POST to the original HTTP
51
+ method used by the client.
52
+
53
+ If the signature is wrong, it returns a "400 Invalid Facebook Signature".
54
+
55
+ Optionally, it can take a block that receives the Rack environment
56
+ and returns a value that evaluates to true when we want the middleware to
57
+ be executed for the specific request.
58
+
59
+ # Usage
60
+
61
+ In your config.ru:
62
+
63
+ require 'rack/facebook'
64
+ use Rack::Facebook, "my_facebook_secret_key"
65
+
66
+ Using a block condition:
67
+
68
+ use Rack::Facebook, "my_facebook_secret_key" do |env|
69
+ env['REQUEST_URI'] =~ /^\/facebook_only/
70
+ end
71
+
72
+ # Credits
73
+
74
+ Carlos Paramio
75
+
@@ -0,0 +1,74 @@
1
+
2
+
3
+ require "rubygems"
4
+ require "rake/gempackagetask"
5
+ require "rake/rdoctask"
6
+
7
+ require "spec"
8
+ require "spec/rake/spectask"
9
+ Spec::Rake::SpecTask.new do |t|
10
+ t.spec_opts = %w(--format specdoc --colour)
11
+ t.libs = ["spec"]
12
+ end
13
+
14
+
15
+ task :default => ["spec"]
16
+
17
+ # This builds the actual gem. For details of what all these options
18
+ # mean, and other ones you can add, check the documentation here:
19
+ #
20
+ # http://rubygems.org/read/chapter/20
21
+ #
22
+ spec = Gem::Specification.new do |s|
23
+
24
+ # Change these as appropriate
25
+ s.name = "rack-fb"
26
+ s.version = "0.0.1"
27
+ s.summary = "Facebook middleware and API client"
28
+ s.author = "John Mendonca"
29
+ s.email = "joaosinho@gmail.com"
30
+ s.homepage = "http://github.com/johnmendonca/rack-fb"
31
+
32
+ s.has_rdoc = true
33
+ s.extra_rdoc_files = %w(README.markdown)
34
+ s.rdoc_options = %w(--main README.markdown)
35
+
36
+ # Add any extra files to include in the gem
37
+ s.files = %w(README.markdown Rakefile) + Dir.glob("{spec,lib/**/*}")
38
+ s.require_paths = ["lib"]
39
+
40
+ # If you want to depend on other gems, add them here, along with any
41
+ # relevant versions
42
+ s.add_dependency("rack", "~> 1.0.1")
43
+
44
+ # If your tests use any gems, include them here
45
+ s.add_development_dependency("rspec")
46
+
47
+ # If you want to publish automatically to rubyforge, you'll may need
48
+ # to tweak this, and the publishing task below too.
49
+ s.rubyforge_project = "rack-fb"
50
+ end
51
+
52
+ # This task actually builds the gem. We also regenerate a static
53
+ # .gemspec file, which is useful if something (i.e. GitHub) will
54
+ # be automatically building a gem for this project. If you're not
55
+ # using GitHub, edit as appropriate.
56
+ Rake::GemPackageTask.new(spec) do |pkg|
57
+ pkg.gem_spec = spec
58
+
59
+ # Generate the gemspec file for github.
60
+ file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
61
+ File.open(file, "w") {|f| f << spec.to_ruby }
62
+ end
63
+
64
+ # Generate documentation
65
+ Rake::RDocTask.new do |rd|
66
+ rd.main = "README.markdown"
67
+ rd.rdoc_files.include("README.markdown", "lib/**/*.rb")
68
+ rd.rdoc_dir = "rdoc"
69
+ end
70
+
71
+ desc 'Clear out RDoc and generated packages'
72
+ task :clean => [:clobber_rdoc, :clobber_package] do
73
+ rm "#{spec.name}.gemspec"
74
+ end
@@ -0,0 +1,279 @@
1
+ require 'digest/md5'
2
+ require 'erb'
3
+ require 'json' unless defined? JSON
4
+
5
+ module MiniFB
6
+
7
+ # Global constants
8
+ FB_URL = "http://api.facebook.com/restserver.php"
9
+ FB_API_VERSION = "1.0"
10
+
11
+ @@logging = false
12
+
13
+ def enable_logging
14
+ @@logging = true
15
+ end
16
+
17
+ def disable_logging
18
+ @@logging = false
19
+ end
20
+
21
+ class FaceBookError < StandardError
22
+ # Error that happens during a facebook call.
23
+ def initialize( error_code, error_msg )
24
+ super("Facebook error #{error_code}: #{error_msg}" )
25
+ end
26
+ end
27
+
28
+ class Session
29
+ attr_accessor :api_key, :secret_key, :session_key, :uid
30
+
31
+
32
+ def initialize(api_key, secret_key, session_key, uid)
33
+ @api_key = api_key
34
+ @secret_key = FaceBookSecret.new secret_key
35
+ @session_key = session_key
36
+ @uid = uid
37
+ end
38
+
39
+ # returns current user
40
+ def user
41
+ return @user unless @user.nil?
42
+ @user = User.new(MiniFB.call(@api_key, @secret_key, "Users.getInfo", "session_key"=>@session_key, "uids"=>@uid, "fields"=>User.all_fields)[0], self)
43
+ @user
44
+ end
45
+
46
+ def photos
47
+ Photos.new(self)
48
+ end
49
+
50
+
51
+ def call(method, params={})
52
+ return MiniFB.call(api_key, secret_key, method, params.update("session_key"=>session_key))
53
+ end
54
+
55
+
56
+ end
57
+ class User
58
+ 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_hashes, :allowed_restrictions, :pic_with_logo, :pic_big_with_logo, :pic_small_with_logo, :pic_square_with_logo]
59
+ STANDARD_FIELDS = [:uid, :first_name, :last_name, :name, :timezone, :birthday, :sex, :affiliations, :locale, :profile_url, :proxied_email]
60
+
61
+ def self.all_fields
62
+ FIELDS.join(",")
63
+ end
64
+
65
+ def self.standard_fields
66
+ STANDARD_FIELDS.join(",")
67
+ end
68
+
69
+ def initialize(fb_hash, session)
70
+ @fb_hash = fb_hash
71
+ @session = session
72
+ end
73
+
74
+ def [](key)
75
+ @fb_hash[key]
76
+ end
77
+
78
+ def uid
79
+ return self["uid"]
80
+ end
81
+
82
+ def profile_photos
83
+ @session.photos.get("uid"=>uid, "aid"=>profile_pic_album_id)
84
+ end
85
+
86
+ def profile_pic_album_id
87
+ merge_aid(-3, uid)
88
+ end
89
+
90
+ def merge_aid(aid, uid)
91
+ uid = uid.to_i
92
+ ret = (uid << 32) + (aid & 0xFFFFFFFF)
93
+ # puts 'merge_aid=' + ret.inspect
94
+ return ret
95
+ end
96
+ end
97
+
98
+ class Photos
99
+
100
+ def initialize(session)
101
+ @session = session
102
+ end
103
+
104
+ def get(params)
105
+ pids = params["pids"]
106
+ if !pids.nil? && pids.is_a?(Array)
107
+ pids = pids.join(",")
108
+ params["pids"] = pids
109
+ end
110
+ @session.call("photos.get", params)
111
+ end
112
+ end
113
+
114
+ BAD_JSON_METHODS = ["users.getLoggedInUser","auth.promoteSession"]
115
+
116
+ # Call facebook server with a method request. Most keyword arguments
117
+ # are passed directly to the server with a few exceptions.
118
+ # The 'sig' value will always be computed automatically.
119
+ # The 'v' version will be supplied automatically if needed.
120
+ # The 'call_id' defaults to True, which will generate a valid
121
+ # number. Otherwise it should be a valid number or False to disable.
122
+
123
+ # The default return is a parsed json object.
124
+ # Unless the 'format' and/or 'callback' arguments are given,
125
+ # in which case the raw text of the reply is returned. The string
126
+ # will always be returned, even during errors.
127
+
128
+ # If an error occurs, a FacebookError exception will be raised
129
+ # with the proper code and message.
130
+
131
+ # The secret argument should be an instance of FacebookSecret
132
+ # to hide value from simple introspection.
133
+ def MiniFB.call( api_key, secret, method, kwargs )
134
+
135
+ puts 'kwargs=' + kwargs.inspect
136
+
137
+ if secret.is_a? String
138
+ secret = FaceBookSecret.new(secret)
139
+ end
140
+
141
+ # Prepare arguments for call
142
+ call_id = kwargs.fetch("call_id", true)
143
+ if call_id == true then
144
+ kwargs["call_id"] = Time.now.tv_sec.to_s
145
+ else
146
+ kwargs.delete("call_id")
147
+ end
148
+
149
+ custom_format = kwargs.include?("format") or kwargs.include?("callback")
150
+ kwargs["format"] ||= "JSON"
151
+ kwargs["v"] ||= FB_API_VERSION
152
+ kwargs["api_key"]||= api_key
153
+ kwargs["method"] ||= method
154
+
155
+ # Hash with secret
156
+ arg_string = String.new
157
+ # todo: convert symbols to strings, symbols break the next line
158
+ kwargs.sort.each { |kv| arg_string << kv[0] << "=" << kv[1].to_s }
159
+ kwargs["sig"] = Digest::MD5.hexdigest( arg_string + secret.value.call )
160
+
161
+ # Call website with POST request
162
+ begin
163
+ response = Net::HTTP.post_form( URI.parse(FB_URL), kwargs )
164
+ rescue SocketError => err
165
+ raise IOError.new( "Cannot connect to the facebook server: " + err )
166
+ end
167
+
168
+ # Handle response
169
+ return response.body if custom_format
170
+
171
+ fb_method = kwargs["method"]
172
+ body = response.body
173
+
174
+ begin
175
+ data = JSON.parse( body )
176
+ puts 'response=' + data.inspect if @@logging
177
+ if data.include?( "error_msg" ) then
178
+ raise FaceBookError.new( data["error_code"] || 1, data["error_msg"] )
179
+ end
180
+
181
+ rescue JSON::ParserError => ex
182
+ if BAD_JSON_METHODS.include?(fb_method) # Little hack because this response isn't valid JSON
183
+ return body
184
+ else
185
+ raise ex
186
+ end
187
+ end
188
+ return data
189
+ end
190
+
191
+ # Returns true is signature is valid, false otherwise.
192
+ def MiniFB.verify_signature( secret, arguments )
193
+ signature = arguments.delete( "fb_sig" )
194
+ return false if signature.nil?
195
+
196
+ unsigned = Hash.new
197
+ signed = Hash.new
198
+
199
+ arguments.each do |k, v|
200
+ if k =~ /^fb_sig_(.*)/ then
201
+ signed[$1] = v
202
+ else
203
+ unsigned[k] = v
204
+ end
205
+ end
206
+
207
+ arg_string = String.new
208
+ signed.sort.each { |kv| arg_string << kv[0] << "=" << kv[1] }
209
+ if Digest::MD5.hexdigest( arg_string + secret ) == signature
210
+ return true
211
+ end
212
+ return false
213
+ end
214
+
215
+ # Returns the login/add app url for your application.
216
+ #
217
+ # options:
218
+ # - :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
219
+ # - :canvas => true/false - to say whether this is a canvas app or not
220
+ def self.login_url(api_key, options={})
221
+ login_url = "http://api.facebook.com/login.php?api_key=#{api_key}"
222
+ login_url << "&next=#{options[:next]}" if options[:next]
223
+ login_url << "&canvas" if options[:canvas]
224
+ login_url
225
+ end
226
+
227
+ # This function expects arguments as a hash, so
228
+ # it is agnostic to different POST handling variants in ruby.
229
+ #
230
+ # Validate the arguments received from facebook. This is usually
231
+ # sent for the iframe in Facebook's canvas. It is not necessary
232
+ # to use this on the auth_token and uid passed to callbacks like
233
+ # post-add and post-remove.
234
+ #
235
+ # The arguments must be a mapping of to string keys and values
236
+ # or a string of http request data.
237
+ #
238
+ # If the data is invalid or not signed properly, an empty
239
+ # dictionary is returned.
240
+ #
241
+ # The secret argument should be an instance of FacebookSecret
242
+ # to hide value from simple introspection.
243
+ #
244
+ # DEPRECATED, use verify_signature instead
245
+ def MiniFB.validate( secret, arguments )
246
+
247
+ signature = arguments.delete( "fb_sig" )
248
+ return arguments if signature.nil?
249
+
250
+ unsigned = Hash.new
251
+ signed = Hash.new
252
+
253
+ arguments.each do |k, v|
254
+ if k =~ /^fb_sig_(.*)/ then
255
+ signed[$1] = v
256
+ else
257
+ unsigned[k] = v
258
+ end
259
+ end
260
+
261
+ arg_string = String.new
262
+ signed.sort.each { |kv| arg_string << kv[0] << "=" << kv[1] }
263
+ if Digest::MD5.hexdigest( arg_string + secret ) != signature
264
+ unsigned # Hash is incorrect, return only unsigned fields.
265
+ else
266
+ unsigned.merge signed
267
+ end
268
+ end
269
+
270
+ class FaceBookSecret
271
+ # Simple container that stores a secret value.
272
+ # Proc cannot be dumped or introspected by normal tools.
273
+ attr_reader :value
274
+
275
+ def initialize( value )
276
+ @value = Proc.new { value }
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,154 @@
1
+ require 'digest'
2
+ require 'rack/request'
3
+
4
+ module Rack
5
+ # This Rack middleware checks the signature of Facebook params and
6
+ # converts them to Ruby objects when appropiate. Also, it converts
7
+ # the request method from the Facebook POST to the original HTTP
8
+ # method used by the client.
9
+ #
10
+ # If the signature is wrong, it returns a "400 Invalid Facebook Signature".
11
+ #
12
+ # Optionally, it can take a block that receives the Rack environment
13
+ # and returns a value that evaluates to true when we want the middleware to
14
+ # be executed for the specific request.
15
+ #
16
+ # == Usage
17
+ #
18
+ # In your rack builder:
19
+ #
20
+ # use Rack::Facebook, :application_secret => "SECRET", :api_key => "APIKEY"
21
+ #
22
+ # Using a block condition:
23
+ #
24
+ # use Rack::Facebook, options do |env|
25
+ # env['REQUEST_URI'] =~ /^\/facebook_only/
26
+ # end
27
+ #
28
+ # == References
29
+ # * http://wiki.developers.facebook.com/index.php/Authorizing_Applications
30
+ # * http://wiki.developers.facebook.com/index.php/Verifying_The_Signature
31
+ #
32
+ class Facebook
33
+ def initialize(app, options, &condition)
34
+ @app = app
35
+ @options = options
36
+ @condition = condition
37
+ end
38
+
39
+ def app_name
40
+ @options[:application_name]
41
+ end
42
+
43
+ def secret
44
+ @options[:application_secret]
45
+ end
46
+
47
+ def api_key
48
+ @options[:api_key]
49
+ end
50
+
51
+ def call(env)
52
+ request = Request.new(env)
53
+ request.api_key = api_key
54
+
55
+ if passes_condition?(request) and request.facebook?
56
+ valid = true
57
+
58
+ if request.params_signature
59
+ fb_params = request.extract_facebook_params(:post)
60
+
61
+ if valid = valid_signature?(fb_params, request.params_signature)
62
+ env["facebook.original_method"] = env["REQUEST_METHOD"]
63
+ env["REQUEST_METHOD"] = fb_params.delete("request_method")
64
+ save_facebook_params(fb_params, env)
65
+ end
66
+ elsif request.cookies_signature
67
+ cookie_params = request.extract_facebook_params(:cookies)
68
+ valid = valid_signature?(cookie_params, request.cookies_signature)
69
+ end
70
+
71
+ unless valid
72
+ return [400, {"Content-Type" => "text/html"}, ["Invalid Facebook signature"]]
73
+ end
74
+ end
75
+ return @app.call(env)
76
+ end
77
+
78
+ private
79
+
80
+ def passes_condition?(request)
81
+ @condition.nil? or @condition.call(request.env)
82
+ end
83
+
84
+ def valid_signature?(fb_params, actual_sig)
85
+ actual_sig == calculate_signature(fb_params)
86
+ end
87
+
88
+ def calculate_signature(hash)
89
+ raw_string = hash.map{ |*pair| pair.join('=') }.sort.join
90
+ Digest::MD5.hexdigest([raw_string, secret].join)
91
+ end
92
+
93
+ def save_facebook_params(params, env)
94
+ params.each do |key, value|
95
+ ruby_value = case key
96
+ when 'added', 'page_added', 'in_canvas', 'in_profile_tab', 'in_new_facebook', 'position_fix', 'logged_out_facebook'
97
+ value == '1'
98
+ when 'expires', 'profile_update_time', 'time'
99
+ Time.at(value.to_f) rescue TypeError
100
+ when 'friends'
101
+ value.split(',')
102
+ else
103
+ value
104
+ end
105
+
106
+ env["facebook.#{key}"] = ruby_value
107
+ end
108
+
109
+ env["facebook.app_name"] = app_name
110
+ env["facebook.api_key"] = api_key
111
+ env["facebook.secret"] = secret
112
+ end
113
+
114
+ class Request < ::Rack::Request
115
+ FB_PREFIX = "fb_sig".freeze
116
+ attr_accessor :api_key
117
+
118
+ def facebook?
119
+ params_signature or cookies_signature
120
+ end
121
+
122
+ def params_signature
123
+ return @params_signature if @params_signature or @params_signature == false
124
+ @params_signature = self.POST.delete(FB_PREFIX) || false
125
+ end
126
+
127
+ def cookies_signature
128
+ cookies[@api_key]
129
+ end
130
+
131
+ def extract_facebook_params(where)
132
+
133
+ case where
134
+ when :post
135
+ source = self.POST
136
+ prefix = FB_PREFIX
137
+ when :cookies
138
+ source = cookies
139
+ prefix = @api_key
140
+ end
141
+
142
+ prefix = "#{prefix}_"
143
+
144
+ source.inject({}) do |extracted, (key, value)|
145
+ if key.index(prefix) == 0
146
+ extracted[key.sub(prefix, '')] = value
147
+ source.delete(key) if :post == where
148
+ end
149
+ extracted
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-fb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - John Mendonca
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-16 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 1.0.1
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description:
36
+ email: joaosinho@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README.markdown
43
+ files:
44
+ - README.markdown
45
+ - Rakefile
46
+ - lib/rack/facebook.rb
47
+ - lib/mini_fb.rb
48
+ has_rdoc: true
49
+ homepage: http://github.com/johnmendonca/rack-fb
50
+ licenses: []
51
+
52
+ post_install_message:
53
+ rdoc_options:
54
+ - --main
55
+ - README.markdown
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ version:
70
+ requirements: []
71
+
72
+ rubyforge_project: rack-fb
73
+ rubygems_version: 1.3.5
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Facebook middleware and API client
77
+ test_files: []
78
+