jkarlsson-mini_fb 1.1.7

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 (5) hide show
  1. data/LICENSE.txt +20 -0
  2. data/README.markdown +222 -0
  3. data/lib/mini_fb.rb +811 -0
  4. data/test/test_mini_fb.rb +79 -0
  5. metadata +100 -0
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Appoxy LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,222 @@
1
+ MiniFB - the simple miniature facebook library
2
+ ==============================================
3
+
4
+ MiniFB is a small, lightweight Ruby library for interacting with the [Facebook API](http://wiki.developers.facebook.com/index.php/API).
5
+
6
+ Brought to you by: [![Appoxy](http://www.simpledeployr.com/images/global/appoxy-small.png)](http://www.appoxy.com)
7
+
8
+ Support
9
+ --------
10
+
11
+ Join our Discussion Group at: <http://groups.google.com/group/mini_fb>
12
+
13
+ Demo Rails Application
14
+ -------------------
15
+
16
+ There is a demo Rails app that uses mini_fb graph api at: [http://github.com/appoxy/mini_fb_demo](http://github.com/appoxy/mini_fb_demo)
17
+
18
+ Installation
19
+ -------------
20
+
21
+ gem install mini_fb
22
+
23
+
24
+ Facebook Graph API
25
+ ==================
26
+
27
+ For an overview of what this is all about, see <http://developers.facebook.com/docs/api>.
28
+
29
+ Authentication
30
+ --------------
31
+
32
+ Facebook now uses Oauth 2 for authentication, but don't worry, this part is easy.
33
+
34
+ # Get your oauth url
35
+ @oauth_url = MiniFB.oauth_url(FB_APP_ID, # your Facebook App ID (NOT API_KEY)
36
+ "http://www.yoursite.com/sessions/create", # redirect url
37
+ :scope=>MiniFB.scopes.join(",")) # This asks for all permissions
38
+ # Have your users click on a link to @oauth_url
39
+ .....
40
+ # Then in your /sessions/create
41
+ access_token_hash = MiniFB.oauth_access_token(FB_APP_ID, "http://www.yoursite.com/sessions/create", FB_SECRET, params[:code])
42
+ @access_token = access_token_hash["access_token"]
43
+ # TODO: This is where you'd want to store the token in your database
44
+ # but for now, we'll just keep it in the cookie so we don't need a database
45
+ cookies[:access_token] = @access_token
46
+
47
+ That's it. You now need to hold onto this access_token. We've put it in a cookie for now, but you probably
48
+ want to store it in your database or something.
49
+
50
+ Getting Data from Facebook
51
+ --------------------------
52
+
53
+ It's very simple:
54
+
55
+ @id = {some ID of something in facebook} || "me"
56
+ @type = {some facebook type like feed, friends, or photos} # (optional) nil will just return the object data directly
57
+ @response_hash = MiniFB.get(@access_token, @id, :type=>@type)
58
+ # @response_hash is a hash, but also allows object like syntax for instance, the following is true:
59
+ @response_hash["user"] == @response_hash.user
60
+
61
+ See <http://developers.facebook.com/docs/api> for the available types.
62
+
63
+ Posting Data to Facebook
64
+ ------------------------
65
+
66
+ Also pretty simple:
67
+
68
+ @id = {some ID of something in facebook}
69
+ @type = {some type of post like comments, likes, feed} # required here
70
+ @response_hash = MiniFB.post(@access_token, @id, :type=>@type)
71
+
72
+ FQL
73
+ ---
74
+
75
+ my_query = "select uid,a,b,c from users where ...."
76
+ @res = MiniFB.fql(@access_token, my_query)
77
+
78
+ Logging
79
+ -------
80
+
81
+ To enabled logging:
82
+
83
+ MiniFB.enable_logging
84
+
85
+
86
+ Original Facebook API
87
+ =====================
88
+
89
+ This API will probably go away at some point, so you should use the Graph API above in most cases.
90
+
91
+
92
+ General Usage
93
+ -------------
94
+
95
+ The most general case is to use MiniFB.call method:
96
+
97
+ user_hash = MiniFB.call(FB_API_KEY, FB_SECRET, "Users.getInfo", "session_key"=>@session_key, "uids"=>@uid, "fields"=>User.all_fields)
98
+
99
+ Which simply returns the parsed json response from Facebook.
100
+
101
+
102
+ Oauth 2.0 Authentication and Original Rest Api
103
+ -------------
104
+
105
+ You can use the Graph api Oauth 2.0 token with original api methods. BEWARE: This has only been tested against stream.publish at present.
106
+
107
+ MiniFB.rest(@access_token, "rest.api.method", options)
108
+
109
+ eg:
110
+
111
+ response = MiniFB.rest(@access_token, "stream.publish", :params => {
112
+ :uid => @user_id, :target_id => @target_user_id,
113
+ :message => "Hello other user!"
114
+ })
115
+
116
+ all responses will be json. In the instance of 'bad json' methods, the response will formatted {'response': '#{bad_response_string}'}
117
+
118
+
119
+ Some Higher Level Objects for Common Uses
120
+ ----------------------
121
+
122
+ Get a MiniFB::Session:
123
+
124
+ @fb = MiniFB::Session.new(FB_API_KEY, FB_SECRET, @fb_session, @fb_uid)
125
+
126
+ Then it makes it a bit easier to use call for a particular user/session.
127
+
128
+ response = @fb.call("stream.get")
129
+
130
+ With the session, you can then get the user information for the session/uid.
131
+
132
+ user = @fb.user
133
+
134
+ Then get info from the user:
135
+
136
+ first_name = user["first_name"]
137
+
138
+ Or profile photos:
139
+
140
+ photos = user.profile_photos
141
+
142
+ Or if you want other photos, try:
143
+
144
+ photos = @fb.photos("pids"=>[12343243,920382343,9208348])
145
+
146
+
147
+ Higher Level Objects with OAuth2
148
+ --------------------------------
149
+
150
+ Get a MiniFB::OAuthSession with a Spanish locale:
151
+
152
+ @fb = MiniFB::OAuthSession.new(access_token, 'es_ES')
153
+
154
+ Using the session object to make requests:
155
+
156
+ @fb.get('117199051648010')
157
+ @fb.post('me', :type => :feed, :params => {
158
+ :message => "This is me from MiniFB"
159
+ })
160
+ @fb.fql('SELECT id FROM object_url WHERE url="http://www.imdb.com/title/tt1250777/"')
161
+ @fb.rest('notes.create', :params => {
162
+ :title => "ToDo", :content => "Try MiniFB"
163
+ })
164
+
165
+ Getting graph objects through the session:
166
+
167
+ @fb.me
168
+ @fb.me.name
169
+ @fb.me.connections
170
+ @fb.me.feed
171
+
172
+ @ssp = @fb.graph_object('117199051648010')
173
+ @ssp.mission
174
+ @ssp.photos
175
+
176
+
177
+ Facebook Connect
178
+ ----------------
179
+
180
+ This is actually very easy, first follow these instructions: http://wiki.developers.facebook.com/index.php/Connect/Setting_Up_Your_Site
181
+
182
+ Then add the following script to the page where you put the login button so it looks like this:
183
+
184
+ <script>
185
+ function facebook_onlogin(){
186
+ document.location.href = "<%= url_for :action=>"fb_connect" %>";
187
+ }
188
+ </script>
189
+ <fb:login-button onlogin="facebook_onlogin();"></fb:login-button>
190
+
191
+ Define an fb_connect method in your login/sessions controller like so:
192
+
193
+ def fb_connect
194
+ @fb_info = MiniFB.parse_cookie_information(FB_APP_ID, cookies) # some users may have to use their API rather than the app. ID.
195
+ puts "uid=#{@fb_info['uid']}"
196
+ puts "session=#{@fb_info['session_key']}"
197
+
198
+ if MiniFB.verify_cookie_signature(FB_APP_ID, FB_SECRET, cookies)
199
+ # And here you would create the user if it doesn't already exist, then redirect them to wherever you want.
200
+ else
201
+ # The cookies may have been modified as the signature does not match
202
+ end
203
+
204
+ end
205
+
206
+
207
+ Photo Uploads
208
+ -------------
209
+
210
+ This is as simple as calling:
211
+
212
+ @fb.call("photos.upload", "filename"=>"<full path to file>")
213
+
214
+ The file_name parameter will be used as the file data.
215
+
216
+ Video Uploads
217
+ -------------
218
+
219
+ Similar to photos, but the correct [mime-type](http://en.wikipedia.org/wiki/Internet_media_type) is required:
220
+
221
+ @fb.call("video.upload", "filename"=>"<full path to file>", "mime_type" => "video/mp4")
222
+
@@ -0,0 +1,811 @@
1
+ #MiniFB - the simple miniature facebook library
2
+ #MiniFB is a small, lightweight Ruby library for interacting with the Facebook API.
3
+ #
4
+ #Brought to you by: www.appoxy.com
5
+ #
6
+ #Support
7
+ #
8
+ #Join our Discussion Group at: http://groups.google.com/group/mini_fb
9
+ #
10
+ #Demo Rails Application
11
+ #
12
+ #There is a demo Rails app that uses mini_fb graph api at: http://github.com/appoxy/mini_fb_demo
13
+
14
+ require 'digest/md5'
15
+ require 'erb'
16
+ require 'json' unless defined? JSON
17
+ require 'rest_client'
18
+ require 'hashie'
19
+ require 'base64'
20
+ require 'openssl'
21
+ require 'logger'
22
+ require 'mime/types'
23
+
24
+ module MiniFB
25
+
26
+ # Global constants
27
+ FB_URL = "http://api.facebook.com/restserver.php"
28
+ FB_VIDEO_URL = "https://api-video.facebook.com/restserver.php"
29
+ FB_API_VERSION = "1.0"
30
+
31
+ @@logging = false
32
+ @@log = Logger.new(STDOUT)
33
+
34
+ def self.log_level=(level)
35
+ if level.is_a? Numeric
36
+ @@log.level = level
37
+ else
38
+ @@log.level = case level
39
+ when :fatal
40
+ @@log.level = Logger::FATAL
41
+ when :error
42
+ @@log.level = Logger::ERROR
43
+ when :warn
44
+ @@log.level = Logger::WARN
45
+ when :info
46
+ @@log.level = Logger::INFO
47
+ when :debug
48
+ @@log.level = Logger::DEBUG
49
+ end
50
+ end
51
+ end
52
+
53
+ def self.enable_logging
54
+ @@logging = true
55
+ @@log.level = Logger::DEBUG
56
+ end
57
+
58
+ def self.disable_logging
59
+ @@logging = false
60
+ @@log.level = Logger::ERROR
61
+ end
62
+
63
+ class FaceBookError < StandardError
64
+ attr_accessor :code
65
+ # Error that happens during a facebook call.
66
+ def initialize(error_code, error_msg)
67
+ @code = error_code
68
+ super("Facebook error #{error_code}: #{error_msg}")
69
+ end
70
+ end
71
+
72
+ class Session
73
+ attr_accessor :api_key, :secret_key, :session_key, :uid
74
+
75
+
76
+ def initialize(api_key, secret_key, session_key, uid)
77
+ @api_key = api_key
78
+ @secret_key = FaceBookSecret.new secret_key
79
+ @session_key = session_key
80
+ @uid = uid
81
+ end
82
+
83
+ # returns current user
84
+ def user
85
+ return @user unless @user.nil?
86
+ @user = User.new(MiniFB.call(@api_key, @secret_key, "Users.getInfo", "session_key"=>@session_key, "uids"=>@uid, "fields"=>User.all_fields)[0], self)
87
+ @user
88
+ end
89
+
90
+ def photos
91
+ Photos.new(self)
92
+ end
93
+
94
+
95
+ def call(method, params={})
96
+ return MiniFB.call(api_key, secret_key, method, params.update("session_key"=>session_key))
97
+ end
98
+
99
+ end
100
+
101
+ class User
102
+ 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, :email_hashes, :allowed_restrictions, :pic_with_logo, :pic_big_with_logo, :pic_small_with_logo, :pic_square_with_logo]
103
+ STANDARD_FIELDS = [:uid, :first_name, :last_name, :name, :timezone, :birthday, :sex, :affiliations, :locale, :profile_url, :proxied_email, :email]
104
+
105
+ def self.all_fields
106
+ FIELDS.join(",")
107
+ end
108
+
109
+ def self.standard_fields
110
+ STANDARD_FIELDS.join(",")
111
+ end
112
+
113
+ def initialize(fb_hash, session)
114
+ @fb_hash = fb_hash
115
+ @session = session
116
+ end
117
+
118
+ def [](key)
119
+ @fb_hash[key]
120
+ end
121
+
122
+ def uid
123
+ return self["uid"]
124
+ end
125
+
126
+ def profile_photos
127
+ @session.photos.get("uid"=>uid, "aid"=>profile_pic_album_id)
128
+ end
129
+
130
+ def profile_pic_album_id
131
+ merge_aid(-3, uid)
132
+ end
133
+
134
+ def merge_aid(aid, uid)
135
+ uid = uid.to_i
136
+ ret = (uid << 32) + (aid & 0xFFFFFFFF)
137
+ # puts 'merge_aid=' + ret.inspect
138
+ return ret
139
+ end
140
+ end
141
+
142
+ class Photos
143
+
144
+ def initialize(session)
145
+ @session = session
146
+ end
147
+
148
+ def get(params)
149
+ pids = params["pids"]
150
+ if !pids.nil? && pids.is_a?(Array)
151
+ pids = pids.join(",")
152
+ params["pids"] = pids
153
+ end
154
+ @session.call("photos.get", params)
155
+ end
156
+ end
157
+
158
+ BAD_JSON_METHODS = ["users.getloggedinuser", "auth.promotesession", "users.hasapppermission",
159
+ "Auth.revokeExtendedPermission", "auth.revokeAuthorization",
160
+ "pages.isAdmin", "pages.isFan",
161
+ "stream.publish",
162
+ "dashboard.addNews", "dashboard.addGlobalNews", "dashboard.publishActivity",
163
+ "dashboard.incrementcount", "dashboard.setcount"
164
+ ].collect { |x| x.downcase }
165
+
166
+ # THIS IS FOR THE OLD FACEBOOK API, NOT THE GRAPH ONE. See MiniFB.get and MiniFB.post for Graph API
167
+ #
168
+ # Call facebook server with a method request. Most keyword arguments
169
+ # are passed directly to the server with a few exceptions.
170
+ # The 'sig' value will always be computed automatically.
171
+ # The 'v' version will be supplied automatically if needed.
172
+ # The 'call_id' defaults to True, which will generate a valid
173
+ # number. Otherwise it should be a valid number or False to disable.
174
+
175
+ # The default return is a parsed json object.
176
+ # Unless the 'format' and/or 'callback' arguments are given,
177
+ # in which case the raw text of the reply is returned. The string
178
+ # will always be returned, even during errors.
179
+
180
+ # If an error occurs, a FacebookError exception will be raised
181
+ # with the proper code and message.
182
+
183
+ # The secret argument should be an instance of FacebookSecret
184
+ # to hide value from simple introspection.
185
+ def MiniFB.call(api_key, secret, method, kwargs)
186
+
187
+ puts 'kwargs=' + kwargs.inspect if @@logging
188
+
189
+ if secret.is_a? String
190
+ secret = FaceBookSecret.new(secret)
191
+ end
192
+
193
+ # Prepare arguments for call
194
+ call_id = kwargs.fetch("call_id", true)
195
+ if call_id == true
196
+ kwargs["call_id"] = Time.now.tv_sec.to_s
197
+ else
198
+ kwargs.delete("call_id")
199
+ end
200
+
201
+ custom_format = kwargs.include?("format") || kwargs.include?("callback")
202
+ kwargs["format"] ||= "JSON"
203
+ kwargs["v"] ||= FB_API_VERSION
204
+ kwargs["api_key"]||= api_key
205
+ kwargs["method"] ||= method
206
+
207
+ file_name = kwargs.delete("filename")
208
+ mime_type = kwargs.delete("mime_type") || 'image/jpeg'
209
+
210
+ kwargs["sig"] = signature_for(kwargs, secret.value.call)
211
+
212
+ fb_method = kwargs["method"].downcase
213
+ if (fb_method == "photos.upload" || fb_method == 'video.upload')
214
+ # Then we need a multipart post
215
+ response = MiniFB.post_upload(file_name, kwargs, mime_type)
216
+ else
217
+
218
+ begin
219
+ response = Net::HTTP.post_form(URI.parse(FB_URL), post_params(kwargs))
220
+ rescue SocketError => err
221
+ # why are we catching this and throwing as different error? hmmm..
222
+ # raise IOError.new( "Cannot connect to the facebook server: " + err )
223
+ raise err
224
+ end
225
+ end
226
+
227
+ # Handle response
228
+ return response.body if custom_format
229
+
230
+ body = response.body
231
+
232
+ puts 'response=' + body.inspect if @@logging
233
+ begin
234
+ data = JSON.parse(body)
235
+ if data.include?("error_msg")
236
+ raise FaceBookError.new(data["error_code"] || 1, data["error_msg"])
237
+ end
238
+
239
+ rescue JSON::ParserError => ex
240
+ if BAD_JSON_METHODS.include?(fb_method) # Little hack because this response isn't valid JSON
241
+ if body == "0" || body == "false"
242
+ return false
243
+ end
244
+ return body
245
+ else
246
+ raise ex
247
+ end
248
+ end
249
+ return data
250
+ end
251
+
252
+ def MiniFB.post_upload(filename, kwargs, mime_type = 'image/jpeg')
253
+ content = File.open(filename, 'rb') { |f| f.read }
254
+ boundary = "END_OF_PART_#{rand(1 << 64).to_s(16)}"
255
+ header = {'Content-type' => "multipart/form-data, boundary=#{boundary}"}
256
+
257
+ # Make sure the filename has the correct extension.
258
+ # Facebook is really picky about this.
259
+ remote_filename = ensure_correct_extension(File.basename(filename), mime_type)
260
+
261
+ # Build query
262
+ query = ''
263
+ kwargs.each { |a, v|
264
+ query <<
265
+ "--#{boundary}\r\n" <<
266
+ "Content-Disposition: form-data; name=\"#{a}\"\r\n\r\n" <<
267
+ "#{v}\r\n"
268
+ }
269
+ query <<
270
+ "--#{boundary}\r\n" <<
271
+ "Content-Disposition: form-data; filename=\"#{remote_filename}\"\r\n" <<
272
+ "Content-Transfer-Encoding: binary\r\n" <<
273
+ "Content-Type: #{mime_type}\r\n\r\n" <<
274
+ content <<
275
+ "\r\n" <<
276
+ "--#{boundary}--"
277
+
278
+ # Call Facebook with POST multipart/form-data request
279
+ url = (mime_type.split('/').first == 'video') ? FB_VIDEO_URL : FB_URL
280
+ raw_post(url, query, header)
281
+ end
282
+
283
+ def MiniFB.raw_post(url, body, headers)
284
+ uri = URI.parse(url)
285
+ uri.port = (uri.scheme == 'https') ? 443 : 80
286
+ http = Net::HTTP.new(uri.host, uri.port)
287
+ if uri.scheme == 'https'
288
+ http.use_ssl = true
289
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
290
+ end
291
+ http.start { |h| h.post(uri.path, body, headers) }
292
+ end
293
+
294
+ # Returns true is signature is valid, false otherwise.
295
+ def MiniFB.verify_signature(secret, arguments)
296
+ if arguments.is_a? String
297
+ #new way: params[:session]
298
+ session = JSON.parse(arguments)
299
+
300
+ signature = session.delete('sig')
301
+ return false if signature.nil?
302
+
303
+ arg_string = String.new
304
+ session.sort.each { |k, v| arg_string << "#{k}=#{v}" }
305
+ if Digest::MD5.hexdigest(arg_string + secret) == signature
306
+ return true
307
+ end
308
+ else
309
+ #old way
310
+
311
+ signature = arguments.delete("fb_sig")
312
+ return false if signature.nil?
313
+
314
+ unsigned = Hash.new
315
+ signed = Hash.new
316
+
317
+ arguments.each do |k, v|
318
+ if k =~ /^fb_sig_(.*)/ then
319
+ signed[$1] = v
320
+ else
321
+ unsigned[k] = v
322
+ end
323
+ end
324
+
325
+ arg_string = String.new
326
+ signed.sort.each { |kv| arg_string << kv[0] << "=" << kv[1] }
327
+ if Digest::MD5.hexdigest(arg_string + secret) == signature
328
+ return true
329
+ end
330
+ end
331
+ return false
332
+ end
333
+
334
+ # This function takes the app secret and the signed request, and verifies if the request is valid.
335
+ def self.verify_signed_request(secret, req)
336
+ s, p = req.split(".")
337
+ sig = base64_url_decode(s)
338
+ expected_sig = OpenSSL::HMAC.digest('SHA256', secret, p.tr("-_", "+/"))
339
+ return sig == expected_sig
340
+ end
341
+
342
+ # This function decodes the data sent by Facebook and returns a Hash.
343
+ # See: http://developers.facebook.com/docs/authentication/canvas
344
+ def self.signed_request_params(secret, req)
345
+ s, p = req.split(".")
346
+ p = base64_url_decode(p)
347
+ h = JSON.parse(p)
348
+ h.delete('algorithm') if h['algorithm'] == 'HMAC-SHA256'
349
+ h
350
+ end
351
+
352
+ # Ruby's implementation of base64 decoding seems to be reading the string in multiples of 4 and ignoring
353
+ # any extra characters if there are no white-space characters at the end. Since facebook does not take this
354
+ # into account, this function fills any string with white spaces up to the point where it becomes divisible
355
+ # by 4, then it replaces '-' with '+' and '_' with '/' (URL-safe decoding), and decodes the result.
356
+ def self.base64_url_decode(str)
357
+ str = str + "=" * (4 - str.size % 4) unless str.size % 4 == 0
358
+ return Base64.decode64(str.tr("-_", "+/"))
359
+ end
360
+
361
+ # Parses cookies in order to extract the facebook cookie and parse it into a useable hash
362
+ #
363
+ # options:
364
+ # * app_id - the connect applications app_id (some users may find they have to use their facebook API key)
365
+ # * secret - the connect application secret
366
+ # * 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
+ def MiniFB.parse_cookie_information(app_id, cookies)
368
+ return nil if cookies["fbs_#{app_id}"].nil?
369
+ Hash[*cookies["fbs_#{app_id}"].split('&').map { |v| v.gsub('"', '').split('=', 2) }.flatten]
370
+ end
371
+
372
+ # Validates that the cookies sent by the user are those that were set by facebook. Since your
373
+ # secret is only known by you and facebook it is used to sign all of the cookies set.
374
+ #
375
+ # options:
376
+ # * app_id - the connect applications app_id (some users may find they have to use their facebook API key)
377
+ # * secret - the connect application secret
378
+ # * 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
+ def MiniFB.verify_cookie_signature(app_id, secret, cookies)
380
+ fb_keys = MiniFB.parse_cookie_information(app_id, cookies)
381
+ return false if fb_keys.nil?
382
+
383
+ signature = fb_keys.delete('sig')
384
+ return signature == Digest::MD5.hexdigest(fb_keys.map { |k, v| "#{k}=#{v}" }.sort.join + secret)
385
+ end
386
+
387
+ # <b>DEPRECATED:</b> Please use <tt>verify_cookie_signature</tt> instead.
388
+ def MiniFB.verify_connect_signature(api_key, secret, cookies)
389
+ warn "DEPRECATION WARNING: 'verify_connect_signature' has been renamed to 'verify_cookie_signature' as Facebook no longer calls this 'connect'"
390
+ MiniFB.verify_cookie_signature(api_key, secret, cookies)
391
+ end
392
+
393
+ # Returns the login/add app url for your application.
394
+ #
395
+ # options:
396
+ # - :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
397
+ # - :canvas => true/false - to say whether this is a canvas app or not
398
+ def self.login_url(api_key, options={})
399
+ login_url = "http://api.facebook.com/login.php?api_key=#{api_key}"
400
+ login_url << "&next=#{options[:next]}" if options[:next]
401
+ login_url << "&canvas" if options[:canvas]
402
+ login_url
403
+ end
404
+
405
+
406
+ def self.ensure_correct_extension(filename, mime_type)
407
+ allowed_extensions = MIME::Types[mime_type].first.extensions
408
+ extension = File.extname(filename)[1 .. -1]
409
+ if !allowed_extensions.include? extension
410
+ filename += '.' + allowed_extensions.first
411
+ end
412
+ end
413
+
414
+ # Manages access_token and locale params for an OAuth connection
415
+ class OAuthSession
416
+
417
+ def initialize(access_token, locale="en_US")
418
+ @access_token = access_token
419
+ @locale = locale
420
+ end
421
+
422
+ def get(id, options={})
423
+ MiniFB.get(@access_token, id, session_options(options))
424
+ end
425
+
426
+ def post(id, options={})
427
+ MiniFB.post(@access_token, id, session_options(options))
428
+ end
429
+
430
+ def fql(fql_query, options={})
431
+ MiniFB.fql(@access_token, fql_query, session_options(options))
432
+ end
433
+
434
+ def multifql(fql_queries, options={})
435
+ MiniFB.multifql(@access_token, fql_queries, session_options(options))
436
+ end
437
+
438
+ def rest(api_method, options={})
439
+ MiniFB.rest(@access_token, api_method, session_options(options))
440
+ end
441
+
442
+ # Returns a GraphObject for the given id
443
+ def graph_object(id)
444
+ MiniFB::GraphObject.new(self, id)
445
+ end
446
+
447
+ # Returns and caches a GraphObject for the user
448
+ def me
449
+ @me ||= graph_object('me')
450
+ end
451
+
452
+ private
453
+ def session_options(options)
454
+ (options[:params] ||= {})[:locale] ||= @locale
455
+ options
456
+ end
457
+ end
458
+
459
+ # Wraps a graph object for easily accessing its connections
460
+ class GraphObject
461
+ # Creates a GraphObject using an OAuthSession or access_token
462
+ def initialize(session_or_token, id)
463
+ @oauth_session = if session_or_token.is_a?(MiniFB::OAuthSession)
464
+ session_or_token
465
+ else
466
+ MiniFB::OAuthSession.new(session_or_token)
467
+ end
468
+ @id = id
469
+ @object = @oauth_session.get(id, :metadata => true)
470
+ @connections_cache = {}
471
+ end
472
+
473
+ def inspect
474
+ "<##{self.class.name} #{@object.inspect}>"
475
+ end
476
+
477
+ def connections
478
+ @object.metadata.connections.keys
479
+ end
480
+
481
+ unless RUBY_VERSION =~ /1\.9/
482
+ undef :id, :type
483
+ end
484
+
485
+ def methods
486
+ super + @object.keys.include?(key) + connections.include?(key)
487
+ end
488
+
489
+ def respond_to?(method)
490
+ @object.keys.include?(key) || connections.include?(key) || super
491
+ end
492
+
493
+ def keys
494
+ @object.keys
495
+ end
496
+
497
+ def [](key)
498
+ @object[key]
499
+ end
500
+
501
+ def method_missing(method, *args, &block)
502
+ key = method.to_s
503
+ if @object.keys.include?(key)
504
+ @object[key]
505
+ elsif @connections_cache.has_key?(key)
506
+ @connections_cache[key]
507
+ elsif connections.include?(key)
508
+ @connections_cache[key] = @oauth_session.get(@id, :type => key)
509
+ else
510
+ super
511
+ end
512
+ end
513
+ end
514
+
515
+ def self.graph_base
516
+ "https://graph.facebook.com/"
517
+ end
518
+
519
+ # options:
520
+ # - scope: comma separated list of extends permissions. see http://developers.facebook.com/docs/authentication/permissions
521
+ def self.oauth_url(app_id, redirect_uri, options={})
522
+ oauth_url = "#{graph_base}oauth/authorize"
523
+ oauth_url << "?client_id=#{app_id}"
524
+ oauth_url << "&redirect_uri=#{CGI.escape(redirect_uri)}"
525
+ # oauth_url << "&scope=#{options[:scope]}" if options[:scope]
526
+ oauth_url << ("&" + options.map { |k, v| "%s=%s" % [k, v] }.join('&')) unless options.empty?
527
+ oauth_url
528
+ end
529
+
530
+ # returns a hash with one value being 'access_token', the other being 'expires'
531
+ def self.oauth_access_token(app_id, redirect_uri, secret, code)
532
+ oauth_url = "#{graph_base}oauth/access_token"
533
+ oauth_url << "?client_id=#{app_id}"
534
+ oauth_url << "&redirect_uri=#{CGI.escape(redirect_uri)}"
535
+ oauth_url << "&client_secret=#{secret}"
536
+ oauth_url << "&code=#{CGI.escape(code)}"
537
+ resp = RestClient.get oauth_url
538
+ puts 'resp=' + resp.body.to_s if @@logging
539
+ params = {}
540
+ params_array = resp.split("&")
541
+ params_array.each do |p|
542
+ ps = p.split("=")
543
+ params[ps[0]] = ps[1]
544
+ end
545
+ return params
546
+ end
547
+
548
+ # Return a JSON object of working Oauth tokens from working session keys, returned in order given
549
+ def self.oauth_exchange_session(app_id, secret, session_keys)
550
+ url = "#{graph_base}oauth/exchange_sessions"
551
+ params = {}
552
+ params["client_id"] = "#{app_id}"
553
+ params["client_secret"] = "#{secret}"
554
+ params["sessions"] = "#{session_keys}"
555
+ options = {}
556
+ options[:params] = params
557
+ options[:method] = :post
558
+ return fetch(url, options)
559
+ end
560
+
561
+ # Return a JSON object of working Oauth tokens from working session keys, returned in order given
562
+ def self.authenticate_as_app(app_id, secret)
563
+ url = "#{graph_base}oauth/access_token"
564
+ params = {}
565
+ params["type"] = "client_cred"
566
+ params["client_id"] = "#{app_id}"
567
+ params["client_secret"] = "#{secret}"
568
+ # resp = RestClient.get url
569
+ options = {}
570
+ options[:params] = params
571
+ options[:method] = :get
572
+ options[:response_type] = :params
573
+ resp = fetch(url, options)
574
+ puts 'resp=' + resp.body.to_s if @@logging
575
+ resp
576
+ end
577
+
578
+ # Gets data from the Facebook Graph API
579
+ # options:
580
+ # - type: eg: feed, home, etc
581
+ # - metadata: to include metadata in response. true/false
582
+ # - params: Any additional parameters you would like to submit
583
+ def self.get(access_token, id, options={})
584
+ url = "#{graph_base}#{id}"
585
+ url << "/#{options[:type]}" if options[:type]
586
+ params = options[:params] || {}
587
+ params["access_token"] = "#{(access_token)}"
588
+ params["metadata"] = "1" if options[:metadata]
589
+ params["fields"] = options[:fields].join(",") if options[:fields]
590
+ options[:params] = params
591
+ return fetch(url, options)
592
+ end
593
+
594
+ # Posts data to the Facebook Graph API
595
+ # options:
596
+ # - type: eg: feed, home, etc
597
+ # - metadata: to include metadata in response. true/false
598
+ # - params: Any additional parameters you would like to submit
599
+ def self.post(access_token, id, options={})
600
+ url = "#{graph_base}#{id}"
601
+ url << "/#{options[:type]}" if options[:type]
602
+ options.delete(:type)
603
+ params = options[:params] || {}
604
+ options.each do |key, value|
605
+ if value.kind_of?(File)
606
+ params[key] = value
607
+ else
608
+ params[key] = "#{value}"
609
+ end
610
+ end
611
+ params["access_token"] = "#{(access_token)}"
612
+ params["metadata"] = "1" if options[:metadata]
613
+ options[:params] = params
614
+ options[:method] = :post
615
+ return fetch(url, options)
616
+
617
+ end
618
+
619
+ # Executes an FQL query
620
+ def self.fql(access_token, fql_query, options={})
621
+ url = "https://api.facebook.com/method/fql.query"
622
+ params = options[:params] || {}
623
+ params["access_token"] = "#{(access_token)}"
624
+ params["metadata"] = "1" if options[:metadata]
625
+ params["query"] = fql_query
626
+ params["format"] = "JSON"
627
+ options[:params] = params
628
+ return fetch(url, options)
629
+ end
630
+
631
+ # Executes multiple FQL queries
632
+ # Example:
633
+ #
634
+ # MiniFB.multifql(access_token, { :statuses => "SELECT status_id, message FROM status WHERE uid = 12345",
635
+ # :privacy => "SELECT object_id, description FROM privacy WHERE object_id IN (SELECT status_id FROM #statuses)" })
636
+ def self.multifql(access_token, fql_queries, options={})
637
+ url = "https://api.facebook.com/method/fql.multiquery"
638
+ params = options[:params] || {}
639
+ params["access_token"] = "#{(access_token)}"
640
+ params["metadata"] = "1" if options[:metadata]
641
+ params["queries"] = JSON[fql_queries]
642
+ params[:format] = "JSON"
643
+ options[:params] = params
644
+ return fetch(url, options)
645
+ end
646
+
647
+ # Uses new Oauth 2 authentication against old Facebook REST API
648
+ # options:
649
+ # - params: Any additional parameters you would like to submit
650
+ def self.rest(access_token, api_method, options={})
651
+ url = "https://api.facebook.com/method/#{api_method}"
652
+ params = options[:params] || {}
653
+ params[:access_token] = access_token
654
+ params[:format] = "JSON"
655
+ options[:params] = params
656
+ return fetch(url, options)
657
+ end
658
+
659
+ def self.fetch(url, options={})
660
+
661
+ begin
662
+ if options[:method] == :post
663
+ @@log.debug 'url_post=' + url if @@logging
664
+ resp = RestClient.post url, options[:params]
665
+ else
666
+ if options[:params] && options[:params].size > 0
667
+ url += '?' + options[:params].map { |k, v| CGI.escape(k.to_s) + '=' + CGI.escape(v.to_s) }.join('&')
668
+ end
669
+ @@log.debug 'url_get=' + url if @@logging
670
+ resp = RestClient.get url
671
+ end
672
+
673
+ @@log.debug 'resp=' + resp.to_s if @@log.debug?
674
+
675
+ if options[:response_type] == :params
676
+ # Some methods return a param like string, for example: access_token=11935261234123|rW9JMxbN65v_pFWQl5LmHHABC
677
+ params = {}
678
+ params_array = resp.split("&")
679
+ params_array.each do |p|
680
+ ps = p.split("=")
681
+ params[ps[0]] = ps[1]
682
+ end
683
+ return params
684
+ else
685
+ begin
686
+ res_hash = JSON.parse(resp.to_s)
687
+ rescue
688
+ # quick fix for things like stream.publish that don't return json
689
+ res_hash = JSON.parse("{\"response\": #{resp.to_s}}")
690
+ end
691
+ end
692
+
693
+ if res_hash.is_a? Array # fql return this
694
+ res_hash.collect! { |x| x.is_a?(Hash) ? Hashie::Mash.new(x) : x }
695
+ else
696
+ res_hash = Hashie::Mash.new(res_hash)
697
+ end
698
+
699
+ if res_hash.include?("error_msg")
700
+ raise FaceBookError.new(res_hash["error_code"] || 1, res_hash["error_msg"])
701
+ end
702
+
703
+ return res_hash
704
+ rescue RestClient::Exception => ex
705
+ puts "ex.http_code=" + ex.http_code.to_s
706
+ puts 'ex.http_body=' + ex.http_body if @@logging
707
+ res_hash = JSON.parse(ex.http_body) # probably should ensure it has a good response
708
+ raise MiniFB::FaceBookError.new(ex.http_code, "#{res_hash["error"]["type"]}: #{res_hash["error"]["message"]}")
709
+ end
710
+
711
+ end
712
+
713
+ # Returns all available scopes.
714
+ def self.scopes
715
+ scopes = %w{
716
+ about_me activities birthday checkins education_history
717
+ events groups hometown interests likes location notes
718
+ online_presence photo_video_tags photos relationships
719
+ religion_politics status videos website work_history
720
+ }
721
+ scopes.map! do |scope|
722
+ ["user_#{scope}", "friends_#{scope}"]
723
+ end.flatten!
724
+
725
+ scopes += %w{
726
+ read_insights read_stream read_mailbox read_friendlists read_requests
727
+ email ads_management xmpp_login
728
+ publish_stream create_event rsvp_event sms offline_access
729
+ }
730
+ end
731
+
732
+
733
+ # This function expects arguments as a hash, so
734
+ # it is agnostic to different POST handling variants in ruby.
735
+ #
736
+ # Validate the arguments received from facebook. This is usually
737
+ # sent for the iframe in Facebook's canvas. It is not necessary
738
+ # to use this on the auth_token and uid passed to callbacks like
739
+ # post-add and post-remove.
740
+ #
741
+ # The arguments must be a mapping of to string keys and values
742
+ # or a string of http request data.
743
+ #
744
+ # If the data is invalid or not signed properly, an empty
745
+ # dictionary is returned.
746
+ #
747
+ # The secret argument should be an instance of FacebookSecret
748
+ # to hide value from simple introspection.
749
+ #
750
+ # DEPRECATED, use verify_signature instead
751
+ def MiniFB.validate(secret, arguments)
752
+
753
+ signature = arguments.delete("fb_sig")
754
+ return arguments if signature.nil?
755
+
756
+ unsigned = Hash.new
757
+ signed = Hash.new
758
+
759
+ arguments.each do |k, v|
760
+ if k =~ /^fb_sig_(.*)/ then
761
+ signed[$1] = v
762
+ else
763
+ unsigned[k] = v
764
+ end
765
+ end
766
+
767
+ arg_string = String.new
768
+ signed.sort.each { |kv| arg_string << kv[0] << "=" << kv[1] }
769
+ if Digest::MD5.hexdigest(arg_string + secret) != signature
770
+ unsigned # Hash is incorrect, return only unsigned fields.
771
+ else
772
+ unsigned.merge signed
773
+ end
774
+ end
775
+
776
+ class FaceBookSecret
777
+ # Simple container that stores a secret value.
778
+ # Proc cannot be dumped or introspected by normal tools.
779
+ attr_reader :value
780
+
781
+ def initialize(value)
782
+ @value = Proc.new { value }
783
+ end
784
+ end
785
+
786
+ private
787
+ def self.post_params(params)
788
+ post_params = {}
789
+ params.each do |k, v|
790
+ k = k.to_s unless k.is_a?(String)
791
+ if Array === v || Hash === v
792
+ post_params[k] = JSON.dump(v)
793
+ else
794
+ post_params[k] = v
795
+ end
796
+ end
797
+ post_params
798
+ end
799
+
800
+ def self.signature_for(params, secret)
801
+ params.delete_if { |k, v| v.nil? }
802
+ raw_string = params.inject([]) do |collection, pair|
803
+ collection << pair.map { |x|
804
+ Array === x ? JSON.dump(x) : x
805
+ }.join("=")
806
+ collection
807
+ end.sort.join
808
+ Digest::MD5.hexdigest([raw_string, secret].join)
809
+ end
810
+
811
+ end
@@ -0,0 +1,79 @@
1
+ require 'test/unit'
2
+ require 'rspec'
3
+ require 'uri'
4
+ require 'yaml'
5
+ require 'active_support/core_ext'
6
+ require_relative '../lib/mini_fb'
7
+
8
+ describe "Some Feature" do
9
+
10
+ before :all do
11
+ @is_setup = true
12
+ @config = File.open(File.expand_path("~/.test_configs/mini_fb_tests.yml")) { |yf| YAML::load(yf) }
13
+ puts "@config=" + @config.inspect
14
+ MiniFB.log_level = :debug
15
+
16
+ @oauth_url = MiniFB.oauth_url(@config['fb_app_id'], # your Facebook App ID (NOT API_KEY)
17
+ "http://localhost:3000", # redirect url
18
+ :scope=>MiniFB.scopes.join(","))
19
+ puts "If you need an access token, go here in your browser:"
20
+ puts "#{@oauth_url}"
21
+ puts "Then grab the 'code' parameter in the redirect url and add it to mini_fb_tests.yml."
22
+ end
23
+
24
+
25
+
26
+ before :each do
27
+ # this code runs once per-test
28
+ end
29
+
30
+ it "should do something useful, rather than just being called test1" do
31
+ # el code here
32
+ puts 'whatup'
33
+ true.should be_true
34
+ end
35
+
36
+ it 'test_uri_escape' do
37
+ URI.escape("x=y").should eq("x=y")
38
+ end
39
+
40
+ it 'test_authenticate_as_app' do
41
+ res = MiniFB.authenticate_as_app(@config["fb_api_key"], @config["fb_secret"])
42
+ puts 'res=' + res.inspect
43
+ res.should include("access_token")
44
+ res["access_token"].should match(/^#{@config['fb_app_id']}/)#starts_with?(@config["fb_app_id"].to_s)
45
+ end
46
+
47
+
48
+ it 'test_signed_request_params' do
49
+ # Example request and secret taken from http://developers.facebook.com/docs/authentication/canvas
50
+ secret = 'secret'
51
+ req = 'vlXgu64BQGFSQrY0ZcJBZASMvYvTHu9GQ0YM9rjPSso.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsIjAiOiJwYXlsb2FkIn0'
52
+ assert_equal MiniFB.signed_request_params(secret, req), {"0" => "payload"}
53
+ end
54
+
55
+ end
56
+
57
+
58
+ def access_token
59
+ @config['access_token']
60
+ end
61
+
62
+
63
+ def test_me_with_fields
64
+ fields = {
65
+ 'interests' => [:name],
66
+ 'activities'=> [:name],
67
+ 'music' => [:name],
68
+ 'videos' => [:name],
69
+ 'television'=> [:name],
70
+ 'movies' => [:name],
71
+ 'likes' => [:name],
72
+ 'work' => [:name],
73
+ 'education' => [:name],
74
+ 'books' => [:name]
75
+ }
76
+
77
+ snap = MiniFB.get(access_token, 'me', :fields =>fields.keys)
78
+
79
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jkarlsson-mini_fb
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 1
8
+ - 7
9
+ version: 1.1.7
10
+ platform: ruby
11
+ authors:
12
+ - Travis Reeder
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-02-17 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rest-client
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :runtime
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: hashie
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 0
41
+ version: "0"
42
+ type: :runtime
43
+ version_requirements: *id002
44
+ - !ruby/object:Gem::Dependency
45
+ name: mime-types
46
+ prerelease: false
47
+ requirement: &id003 !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ segments:
52
+ - 0
53
+ version: "0"
54
+ type: :runtime
55
+ version_requirements: *id003
56
+ description: Tiny facebook library. This fork adds video upload support
57
+ email: travis@appoxy.com
58
+ executables: []
59
+
60
+ extensions: []
61
+
62
+ extra_rdoc_files:
63
+ - LICENSE.txt
64
+ - README.markdown
65
+ files:
66
+ - lib/mini_fb.rb
67
+ - LICENSE.txt
68
+ - README.markdown
69
+ has_rdoc: true
70
+ homepage: http://github.com/jkarlsson/mini_fb
71
+ licenses: []
72
+
73
+ post_install_message:
74
+ rdoc_options: []
75
+
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ segments:
83
+ - 0
84
+ version: "0"
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ segments:
90
+ - 0
91
+ version: "0"
92
+ requirements: []
93
+
94
+ rubyforge_project:
95
+ rubygems_version: 1.3.6
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: Tiny facebook library
99
+ test_files:
100
+ - test/test_mini_fb.rb