facebookrb 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []