facebookrb 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.rdoc +62 -27
  2. data/Rakefile +2 -2
  3. data/lib/facebookrb.rb +214 -61
  4. metadata +2 -12
@@ -1,47 +1,82 @@
1
- = facebookrb
1
+ = facebookrb
2
2
 
3
- Facebookrb is currently a work in progress.
4
- It aims to be a lightweight yet fully featured client for the [Facebook API](http://wiki.developers.facebook.com/index.php/API).
5
- It is mostly based on MiniFB (http://github.com/appoxy/mini_fb) and includes some features inspired by rack-facebook (http://github.com/carlosparamio/rack-facebook).
6
- In other instances, it emulates the official Facebook PHP library.
3
+ Facebookrb aims to be a lightweight yet fully featured client for the
4
+ {Facebook API}[http://wiki.developers.facebook.com/index.php/API], drawing
5
+ on the best features from existing Ruby Facebook libraries.
6
+ The functions and features are intentionally kept simple, so you can spend
7
+ your time reading the official Facebook documentation rather than learning
8
+ how the code works.
7
9
 
8
10
  == Installation
9
11
 
10
12
  gem install facebookrb
11
13
 
12
14
  == General Usage
13
-
14
- require 'facebookrb'
15
15
 
16
- You will want to create a client to make calls to Facebook
16
+ The easiest way is to use the middleware. Put this in your config.ru, Sinatra application, or wherever you declare middleware:
17
17
 
18
- fb_client = FacebookRb::Client.new("API_KEY", "SECRET")
18
+ require 'facebookrb'
19
19
 
20
- If you are using the middleware, there will be one created for you already,
21
- available in `env['facebook.client']`.
20
+ use FacebookRb::Middleware, :api_key => "APIKEY", :secret => "SECRET"
22
21
 
23
- user_hash = fb_client.call("Users.getInfo", "session_key"=>@session_key, "uids"=>@uid, "fields"=>User.all_fields)
22
+ This will create a Facebook API client, populate it with any parameters from Facebook, and store it in the Rack env for you.
24
23
 
25
- Which simply returns the parsed json response from Facebook.
26
- The client will ask for results in JSON by default.
24
+ fb = env['facebook.client']
27
25
 
28
- == Middleware
26
+ Make a call using short or long format (thanks tmm1/sinbook for the short version)
29
27
 
30
- The middleware checks the signature of Facebook params, and stores them in env.
28
+ user = fb.users.getInfo('uids' => '123235345', 'fields' => ['name', 'sex', 'religion'])
29
+ user = fb.call('users.getInfo', 'uids' => '123235345', 'fields' => ['name', 'sex', 'religion'])
31
30
 
32
- require 'facebookrb'
31
+ This call parses the JSON from Facebook and returns the resulting objects (a Hash, Array, String, or Integer depending on how complex the JSON is).
32
+ The raw text of the response is also available:
33
+
34
+ fb.call(...)
35
+ fb.last_response
36
+
37
+ If you received params from Facebook, and they are {valid}[http://wiki.developers.facebook.com/index.php/Verifying_The_Signature], then you can access them:
38
+
39
+ fb.params['user']
40
+ fb['user'] # Also works
41
+
42
+ The 'session_key' param will automatically be passed forward to any API calls if available.
43
+
44
+ == Options
45
+
46
+ The options for the middleware and the client are identical, and are values you get from Facebook:
47
+
48
+ :api_key, :secret, :canvas_url
49
+
50
+ == Features
51
+
52
+ === Facebook Connect
53
+
54
+ The library supports reading parameters from cookies, so Connect support should be there (not thoroughly tested ATM).
55
+
56
+ === {Batching}[http://wiki.developers.facebook.com/index.php/Using_Batching_API]
57
+
58
+ results = fb.batch do
59
+ fb.application.getPublicInfo(...)
60
+ fb.users.getInfo(...)
61
+ end
62
+
63
+ results[0] # => result for first call
64
+ results[1] # => result for second call
33
65
 
34
- use Rack::Facebook, :api_key => "APIKEY", :secret => "SECRET"
66
+ == Potential Features
35
67
 
36
- The Facebook parameters in the request are stored in `env['facebook.params']` and `env['facebook.client'].fb_params` if the request is valid (http://wiki.developers.facebook.com/index.php/Verifying_The_Signature)
68
+ These would all be easy to add if anyone needs support for them.
37
69
 
38
- == Planned Features
70
+ * {Base domain}[http://wiki.developers.facebook.com/index.php/Base_Domain]
71
+ * {Preload FQL}[http://wiki.developers.facebook.com/index.php/Preload_FQL]
72
+ * {Permissions API}[http://wiki.developers.facebook.com/index.php/Permissions_API]
73
+ * {Session Secret}[http://wiki.developers.facebook.com/index.php/Session_Secret]
74
+ * Converting the request method from the Facebook POST to the original HTTP method used by the client. (http://wiki.developers.facebook.com/index.php/Fb_sig_request_method)
39
75
 
40
- * Facebook Connect
41
- * Batching (http://wiki.developers.facebook.com/index.php/Using_Batching_API)
42
- * Base domain (http://wiki.developers.facebook.com/index.php/Base_Domain)
43
- * Preload FQL (http://wiki.developers.facebook.com/index.php/Preload_FQL)
44
- * Permissions API (http://wiki.developers.facebook.com/index.php/Permissions_API)
45
- * Session Secret (http://wiki.developers.facebook.com/index.php/Session_Secret)
46
- * Converting the request method from the Facebook POST to the original HTTP method used by the client. (http://wiki.developers.facebook.com/index.php/Fb_sig_request_method)
76
+ == Acknowledgements
47
77
 
78
+ The code for this project was initially derived from:
79
+ * {sinbook}[http://github.com/tmm1/sinbook]
80
+ * {MiniFB}[http://github.com/appoxy/mini_fb]
81
+ * {rack-facebook}[http://github.com/carlosparamio/rack-facebook]
82
+ * The official Facebook PHP library
data/Rakefile CHANGED
@@ -21,7 +21,7 @@ spec = Gem::Specification.new do |s|
21
21
 
22
22
  # Change these as appropriate
23
23
  s.name = "facebookrb"
24
- s.version = "0.1.0"
24
+ s.version = "0.1.1"
25
25
  s.summary = "Simple Facebook API client and middleware"
26
26
  s.author = "John Mendonca"
27
27
  s.email = "joaosinho@gmail.com"
@@ -40,7 +40,7 @@ spec = Gem::Specification.new do |s|
40
40
  s.add_dependency("yajl-ruby")
41
41
 
42
42
  # If your tests use any gems, include them here
43
- s.add_development_dependency("rspec")
43
+ #s.add_development_dependency("rspec")
44
44
 
45
45
  # If you want to publish automatically to rubyforge, you'll may need
46
46
  # to tweak this, and the publishing task below too.
@@ -1,3 +1,4 @@
1
+ require 'rack/request' unless defined?(Rack::Request)
1
2
  require 'uri'
2
3
  require 'net/http'
3
4
  require 'digest/md5'
@@ -29,28 +30,41 @@ module FacebookRb
29
30
  USER_FIELDS_STANDARD = [:uid, :first_name, :last_name, :name, :timezone,
30
31
  :birthday, :sex, :affiliations, :locale, :profile_url, :proxied_email]
31
32
 
32
- attr_accessor :api_key, :secret, :session_key, :fb_params
33
- attr_accessor :last_response
33
+ attr_accessor :api_key, :secret, :canvas_url, :format
34
+ attr_accessor :pending_batch, :batch_queue, :last_response
34
35
 
35
- def initialize(api_key, secret, session_key=nil)
36
- @api_key = api_key
37
- @secret = secret
38
- @session_key = session_key
36
+ def initialize(options = {})
37
+ self.api_key = options[:api_key] || options['api_key']
38
+ self.secret = options[:secret] || options['secret']
39
+ self.canvas_url = options[:canvas_url] || options['canvas_url']
40
+ self.format = options[:format] || options['format'] || 'JSON'
39
41
  end
40
42
 
41
- def fb_params=(params)
42
- @fb_params = params
43
- @session_key = params['session_key'] if params
43
+ def params
44
+ @params ||= {}
45
+ end
46
+
47
+ def [](key)
48
+ params[key]
49
+ end
50
+
51
+ def valid?
52
+ !params.empty?
53
+ end
54
+
55
+ def url(path = nil)
56
+ path ? "#{canvas_url}#{path}" : canvas_url
57
+ end
58
+
59
+ def addurl
60
+ "http://apps.facebook.com/add.php?api_key=#{self.api_key}"
44
61
  end
45
62
 
46
63
  #
47
64
  # Call facebook server with a method request. Most keyword arguments
48
65
  # are passed directly to the server with a few exceptions.
49
66
  #
50
- # The default return is a parsed json object.
51
- # Unless the 'format' and/or 'callback' arguments are given,
52
- # in which case the raw text of the reply is returned. The string
53
- # will always be returned, even during errors.
67
+ # Returns a parsed json object.
54
68
  #
55
69
  # If an error occurs, a FacebookError exception will be raised
56
70
  # with the proper code and message.
@@ -60,42 +74,177 @@ module FacebookRb
60
74
 
61
75
  # Prepare standard arguments for call
62
76
  api_params['method'] ||= method
63
- api_params['api_key'] ||= api_key
64
- api_params['session_key'] ||= session_key
65
- api_params['call_id'] ||= Time.now.to_s
77
+ api_params['api_key'] ||= self.api_key
78
+ api_params['format'] ||= self.format
79
+ api_params['session_key'] ||= self.params['session_key']
80
+ api_params['call_id'] ||= Time.now.to_f.to_s
66
81
  api_params['v'] ||= FB_API_VERSION
67
- api_params['format'] ||= 'JSON'
68
82
 
69
- # Encode any Array or Hash arguments into JSON
70
- json_encoder = Yajl::Encoder.new
71
- api_params.each do |key, value|
72
- if value.is_a?(Array) || value.is_a?(Hash)
73
- api_params[key] = json_encoder.encode(value)
74
- end
75
- end
83
+ convert_outgoing_params(api_params)
76
84
 
77
85
  api_params['sig'] = generate_signature(api_params, self.secret)
78
86
 
79
- # Call website with POST request
87
+ #If in a batch, stash the params in the queue and bail
88
+ if pending_batch
89
+ batch_queue << api_params.map { |k,v| "#{k}=#{v}" }.join('&')
90
+ return
91
+ end
92
+
93
+ # Call Facebook with POST request
80
94
  response = Net::HTTP.post_form( URI.parse(FB_URL), api_params )
81
95
 
82
96
  # Handle response
83
97
  self.last_response = response.body
84
98
  data = Yajl::Parser.parse(response.body)
85
99
 
86
- if data.include?('error_msg')
100
+ if data.is_a?(Hash) && data['error_msg']
87
101
  raise FacebookError.new(data['error_code'], data['error_msg'])
88
102
  end
89
103
 
90
104
  return data
91
105
  end
92
106
 
107
+ #
108
+ # Performs a batch API operation (batch.run)
109
+ #
110
+ # Example:
111
+ # results = fb.batch do
112
+ # fb.application.getPublicInfo(...)
113
+ # fb.users.getInfo(...)
114
+ # end
115
+ #
116
+ # Options:
117
+ # * :raise_errors (default true) - since a batch returns results for
118
+ # multiple calls, some may return errors while others do not, by default
119
+ # an exception will be raised if any errors are found. Set to
120
+ # false to disable this and handle errors yourself
121
+ def batch(options = {})
122
+ return unless block_given?
123
+ #TODO: real error code/message
124
+ raise FacebookError.new('error_code', 'error_msg') if pending_batch
125
+
126
+ self.batch_queue = []
127
+ self.pending_batch = true
128
+ options[:raise_errors] = true if options[:raise_errors].nil?
129
+
130
+ yield self
131
+
132
+ self.pending_batch = false
133
+ results = call('batch.run',
134
+ :method_feed => self.batch_queue,
135
+ :serial_only => true)
136
+ self.batch_queue = nil
137
+
138
+ #Batch results are an array of JSON strings, parse each
139
+ results.map! { |json| Yajl::Parser.parse(json) }
140
+
141
+ results.each do |data|
142
+ if data.is_a?(Hash) && data['error_msg']
143
+ raise FacebookError.new(data['error_code'], data['error_msg'])
144
+ end
145
+ end if options[:raise_errors]
146
+
147
+ results
148
+ end
149
+
93
150
  def add_special_params(method, params)
94
151
  #call_as_apikey for Permissions API
95
152
  #ss Session secret
96
153
  #use_ssl_resources
97
154
  end
98
155
 
156
+ #
157
+ # Extracts and validates any Facebook parameters from the request, and
158
+ # stores them in the client.
159
+ # We look for parameters from POST, GET, then cookies, in that order.
160
+ # POST and GET are always more up-to-date than cookies, so we prefer
161
+ # those if they are available.
162
+ #
163
+ # Parameters:
164
+ # env the Rack environment, or a Rack request
165
+ #
166
+ def extract_params(env)
167
+ #Accept a Request object or the env
168
+ if env.is_a?(Rack::Request)
169
+ request = env
170
+ else
171
+ request = Rack::Request.new(env)
172
+ end
173
+
174
+ #Fetch from POST
175
+ fb_params = get_params_from(request.POST, 'fb_sig')
176
+
177
+ #Fetch from GET
178
+ unless fb_params
179
+ # note that with preload FQL, it's possible to receive POST params in
180
+ # addition to GET, and a different prefix is used for each
181
+ fb_get_params = get_params_from(request.GET, 'fb_sig') || {}
182
+ fb_post_params = get_params_from(request.POST, 'fb_post_sig') || {}
183
+ fb_get_params.merge!(fb_post_params)
184
+ fb_params = fb_get_params unless fb_get_params.empty?
185
+ end
186
+
187
+ #Fetch from cookies
188
+ unless fb_params
189
+ fb_params = get_params_from(request.cookies, self.api_key)
190
+ end
191
+
192
+ @params = convert_incoming_params(fb_params)
193
+ end
194
+
195
+ #
196
+ # Converts some parameter values into more useful forms:
197
+ # * 0 or 1 into true/false
198
+ # * Time values into Time objects
199
+ # * Comma separated lists into arrays
200
+ #
201
+ def convert_incoming_params(params)
202
+ return nil unless params
203
+
204
+ params.each do |key, value|
205
+ case key
206
+ when 'friends', 'linked_account_ids'
207
+ params[key] = value.split(',')
208
+ when /(time|expires)$/
209
+ if value == '0'
210
+ params[key] = nil
211
+ else
212
+ params[key] = Time.at(value.to_f)
213
+ end
214
+ when /^(logged_out|position_|in_|is_)/, /added$/
215
+ params[key] = (value == '1')
216
+ else
217
+ params[key] = value
218
+ end
219
+ end
220
+
221
+ params
222
+ end
223
+
224
+ #
225
+ # Converts parameters being sent to Facebook from ruby objects to the
226
+ # appropriate text representation
227
+ #
228
+ def convert_outgoing_params(params)
229
+ json_encoder = Yajl::Encoder.new
230
+ params.each do |key, value|
231
+ params.delete(key) if value.nil?
232
+
233
+ case value
234
+ when Array, Hash
235
+ params[key] = json_encoder.encode(value)
236
+ when Time
237
+ params[key] = value.to_i.to_s
238
+ when TrueClass
239
+ params[key] = '1'
240
+ when FalseClass
241
+ params[key] = '0'
242
+ end
243
+ end
244
+
245
+ params
246
+ end
247
+
99
248
  # Get the signed parameters that were sent from Facebook. Validates the set
100
249
  # of parameters against the included signature.
101
250
  #
@@ -115,7 +264,7 @@ module FacebookRb
115
264
  # the subset of parameters containing the given prefix, and also matching
116
265
  # the signature associated with them, or nil if the params do not validate
117
266
  #
118
- def get_valid_fb_params(params, namespace='fb_sig')
267
+ def get_params_from(params, namespace='fb_sig')
119
268
  prefix = "#{namespace}_"
120
269
  fb_params = Hash.new
121
270
  params.each do |key, value|
@@ -124,23 +273,12 @@ module FacebookRb
124
273
  end
125
274
  end
126
275
 
127
- signature = params[namespace]
128
- return fb_params if signature && valid_signature?(fb_params, signature)
276
+ param_sig = params[namespace]
277
+ gen_sig = generate_signature(fb_params, self.secret)
278
+ return fb_params if param_sig == gen_sig
129
279
  nil
130
280
  end
131
281
 
132
- # Validates that a given set of parameters match their signature.
133
- # Parameters all match a given input prefix, such as "fb_sig".
134
- #
135
- # Parameters:
136
- # fb_params an array of all Facebook-sent parameters, not
137
- # including the signature itself
138
- # expected_sig the expected result to check against
139
- #
140
- def valid_signature?(fb_params, expected_sig)
141
- expected_sig == generate_signature(fb_params, self.secret)
142
- end
143
-
144
282
  # Generate a signature using the application secret key.
145
283
  #
146
284
  # The only two entities that know your secret key are you and Facebook,
@@ -157,17 +295,38 @@ module FacebookRb
157
295
  # a md5 hash to be checked against the signature provided by Facebook
158
296
  #
159
297
  def generate_signature(fb_params, secret)
160
- #Convert any symbol keys to strings
161
- #otherwise sort() bombs
162
- fb_params.each_key do |key|
163
- fb_params[key.to_s] = fb_params.delete(key) if key.is_a?(Symbol)
298
+ str = fb_params.map { |k,v| "#{k}=#{v}" }.sort.join
299
+ Digest::MD5.hexdigest("#{str}#{secret}")
300
+ end
301
+
302
+ #
303
+ # Allows making calls like `client.users.getInfo`
304
+ #
305
+ class APIProxy
306
+ TYPES = %w[ admin application auth comments connect data events
307
+ fbml feed fql friends groups links liveMessage notes notifications
308
+ pages photos profile sms status stream users video ]
309
+
310
+ alias :__class__ :class
311
+ alias :__inspect__ :inspect
312
+ instance_methods.each { |m| undef_method m unless m =~ /^(__|object_id)/ }
313
+ alias :inspect :__inspect__
314
+
315
+ def initialize name, obj
316
+ @name, @obj = name, obj
164
317
  end
165
318
 
166
- str = String.new
167
- fb_params.sort.each do |key, value|
168
- str << "#{key.to_s}=#{value}"
319
+ def method_missing method, opts = {}
320
+ @obj.call "#{@name}.#{method}", opts
169
321
  end
170
- Digest::MD5.hexdigest("#{str}#{secret}")
322
+ end
323
+
324
+ APIProxy::TYPES.each do |n|
325
+ class_eval %[
326
+ def #{n}
327
+ (@proxies||={})[:#{n}] ||= APIProxy.new(:#{n}, self)
328
+ end
329
+ ]
171
330
  end
172
331
  end
173
332
 
@@ -189,19 +348,13 @@ module FacebookRb
189
348
  end
190
349
 
191
350
  def call(env)
192
- request = Rack::Request.new(env)
193
-
194
- client = Client.new(@options[:api_key], @options[:secret])
351
+ fb = Client.new(@options)
352
+ fb.extract_params(env)
195
353
 
196
- if fb_params = client.get_valid_fb_params(request.params, 'fb_sig')
197
- #env["facebook.original_method"] = env["REQUEST_METHOD"]
198
- #env["REQUEST_METHOD"] = fb_params['request_method']
199
- else
200
- fb_params = client.get_valid_fb_params(request.cookies, @options[:api_key])
201
- end
202
- client.fb_params = fb_params
203
- env['facebook.params'] = fb_params
204
- env['facebook.client'] = client
354
+ env['facebook.client'] = fb
355
+
356
+ #env["facebook.original_method"] = env["REQUEST_METHOD"]
357
+ #env["REQUEST_METHOD"] = fb_params['request_method']
205
358
 
206
359
  return @app.call(env)
207
360
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: facebookrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Mendonca
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-18 00:00:00 -08:00
12
+ date: 2010-02-05 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -22,16 +22,6 @@ dependencies:
22
22
  - !ruby/object:Gem::Version
23
23
  version: "0"
24
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
25
  description:
36
26
  email: joaosinho@gmail.com
37
27
  executables: []