fbdoorman 0.0.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.
- data/CHANGELOG.md +3 -0
- data/LICENSE +21 -0
- data/README.md +142 -0
- data/Rakefile +27 -0
- data/VERSION +1 -0
- data/app/controllers/clearance/confirmations_controller.rb +76 -0
- data/app/controllers/clearance/facebook_controller.rb +66 -0
- data/app/controllers/clearance/passwords_controller.rb +85 -0
- data/app/controllers/clearance/sessions_controller.rb +67 -0
- data/app/controllers/clearance/users_controller.rb +36 -0
- data/app/models/clearance_mailer.rb +21 -0
- data/app/views/clearance_mailer/change_password.html.erb +9 -0
- data/app/views/clearance_mailer/confirmation.html.erb +5 -0
- data/app/views/facebook/_fbjs.html.erb +14 -0
- data/app/views/facebook/closed.html.erb +1 -0
- data/app/views/passwords/edit.html.erb +23 -0
- data/app/views/passwords/new.html.erb +15 -0
- data/app/views/sessions/new.html.erb +25 -0
- data/app/views/users/_form.html.erb +13 -0
- data/app/views/users/new.html.erb +6 -0
- data/generators/fbdoorman/USAGE +1 -0
- data/generators/fbdoorman/fbdoorman_generator.rb +68 -0
- data/generators/fbdoorman/lib/insert_commands.rb +33 -0
- data/generators/fbdoorman/lib/rake_commands.rb +22 -0
- data/generators/fbdoorman/templates/README +43 -0
- data/generators/fbdoorman/templates/clearance.rb +3 -0
- data/generators/fbdoorman/templates/facebook.yml +7 -0
- data/generators/fbdoorman/templates/factories.rb +13 -0
- data/generators/fbdoorman/templates/migrations/create_users.rb +24 -0
- data/generators/fbdoorman/templates/migrations/update_users.rb +44 -0
- data/generators/fbdoorman/templates/user.rb +3 -0
- data/lib/clearance/authentication.rb +143 -0
- data/lib/clearance/configuration.rb +25 -0
- data/lib/clearance/extensions/errors.rb +6 -0
- data/lib/clearance/extensions/rescue.rb +5 -0
- data/lib/clearance/routes.rb +55 -0
- data/lib/clearance/user.rb +207 -0
- data/lib/facebook_helpers.rb +48 -0
- data/lib/fbdoorman.rb +27 -0
- data/lib/mini_fb.rb +673 -0
- data/rails/init.rb +1 -0
- metadata +110 -0
data/lib/mini_fb.rb
ADDED
@@ -0,0 +1,673 @@
|
|
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
|
+
|
20
|
+
module MiniFB
|
21
|
+
|
22
|
+
# Global constants
|
23
|
+
FB_URL = "http://api.facebook.com/restserver.php"
|
24
|
+
FB_API_VERSION = "1.0"
|
25
|
+
#Define here the values for your application
|
26
|
+
#FB_URL = "http://api.facebook.com/restserver.php"
|
27
|
+
#FB_API_VERSION = "1.0"
|
28
|
+
|
29
|
+
@@logging = false
|
30
|
+
|
31
|
+
def self.enable_logging
|
32
|
+
@@logging = true
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.disable_logging
|
36
|
+
@@logging = false
|
37
|
+
end
|
38
|
+
|
39
|
+
class FaceBookError < StandardError
|
40
|
+
attr_accessor :code
|
41
|
+
# Error that happens during a facebook call.
|
42
|
+
def initialize(error_code, error_msg)
|
43
|
+
@code = error_code
|
44
|
+
super("Facebook error #{error_code}: #{error_msg}")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Session
|
49
|
+
attr_accessor :api_key, :secret_key, :session_key, :uid
|
50
|
+
|
51
|
+
|
52
|
+
def initialize(api_key, secret_key, session_key, uid)
|
53
|
+
@api_key = api_key
|
54
|
+
@secret_key = FaceBookSecret.new secret_key
|
55
|
+
@session_key = session_key
|
56
|
+
@uid = uid
|
57
|
+
end
|
58
|
+
|
59
|
+
# returns current user
|
60
|
+
def user
|
61
|
+
return @user unless @user.nil?
|
62
|
+
@user = User.new(MiniFB.call(@api_key, @secret_key, "Users.getInfo", "session_key"=>@session_key, "uids"=>@uid, "fields"=>User.all_fields)[0], self)
|
63
|
+
@user
|
64
|
+
end
|
65
|
+
|
66
|
+
def photos
|
67
|
+
Photos.new(self)
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
def call(method, params={})
|
72
|
+
return MiniFB.call(api_key, secret_key, method, params.update("session_key"=>session_key))
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
class User
|
78
|
+
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]
|
79
|
+
STANDARD_FIELDS = [:uid, :first_name, :last_name, :name, :timezone, :birthday, :sex, :affiliations, :locale, :profile_url, :proxied_email, :email]
|
80
|
+
|
81
|
+
def self.all_fields
|
82
|
+
FIELDS.join(",")
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.standard_fields
|
86
|
+
STANDARD_FIELDS.join(",")
|
87
|
+
end
|
88
|
+
|
89
|
+
def initialize(fb_hash, session)
|
90
|
+
@fb_hash = fb_hash
|
91
|
+
@session = session
|
92
|
+
end
|
93
|
+
|
94
|
+
def [](key)
|
95
|
+
@fb_hash[key]
|
96
|
+
end
|
97
|
+
|
98
|
+
def uid
|
99
|
+
return self["uid"]
|
100
|
+
end
|
101
|
+
|
102
|
+
def profile_photos
|
103
|
+
@session.photos.get("uid"=>uid, "aid"=>profile_pic_album_id)
|
104
|
+
end
|
105
|
+
|
106
|
+
def profile_pic_album_id
|
107
|
+
merge_aid(-3, uid)
|
108
|
+
end
|
109
|
+
|
110
|
+
def merge_aid(aid, uid)
|
111
|
+
uid = uid.to_i
|
112
|
+
ret = (uid << 32) + (aid & 0xFFFFFFFF)
|
113
|
+
# puts 'merge_aid=' + ret.inspect
|
114
|
+
return ret
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class Photos
|
119
|
+
|
120
|
+
def initialize(session)
|
121
|
+
@session = session
|
122
|
+
end
|
123
|
+
|
124
|
+
def get(params)
|
125
|
+
pids = params["pids"]
|
126
|
+
if !pids.nil? && pids.is_a?(Array)
|
127
|
+
pids = pids.join(",")
|
128
|
+
params["pids"] = pids
|
129
|
+
end
|
130
|
+
@session.call("photos.get", params)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
BAD_JSON_METHODS = ["users.getloggedinuser", "auth.promotesession", "users.hasapppermission",
|
135
|
+
"Auth.revokeExtendedPermission", "pages.isAdmin", "pages.isFan",
|
136
|
+
"stream.publish",
|
137
|
+
"dashboard.addNews", "dashboard.addGlobalNews", "dashboard.publishActivity",
|
138
|
+
"dashboard.incrementcount", "dashboard.setcount"
|
139
|
+
].collect { |x| x.downcase }
|
140
|
+
|
141
|
+
# Call facebook server with a method request. Most keyword arguments
|
142
|
+
# are passed directly to the server with a few exceptions.
|
143
|
+
# The 'sig' value will always be computed automatically.
|
144
|
+
# The 'v' version will be supplied automatically if needed.
|
145
|
+
# The 'call_id' defaults to True, which will generate a valid
|
146
|
+
# number. Otherwise it should be a valid number or False to disable.
|
147
|
+
|
148
|
+
# The default return is a parsed json object.
|
149
|
+
# Unless the 'format' and/or 'callback' arguments are given,
|
150
|
+
# in which case the raw text of the reply is returned. The string
|
151
|
+
# will always be returned, even during errors.
|
152
|
+
|
153
|
+
# If an error occurs, a FacebookError exception will be raised
|
154
|
+
# with the proper code and message.
|
155
|
+
|
156
|
+
# The secret argument should be an instance of FacebookSecret
|
157
|
+
# to hide value from simple introspection.
|
158
|
+
def MiniFB.call(api_key, secret, method, kwargs)
|
159
|
+
|
160
|
+
puts 'kwargs=' + kwargs.inspect if @@logging
|
161
|
+
|
162
|
+
if secret.is_a? String
|
163
|
+
secret = FaceBookSecret.new(secret)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Prepare arguments for call
|
167
|
+
call_id = kwargs.fetch("call_id", true)
|
168
|
+
if call_id == true
|
169
|
+
kwargs["call_id"] = Time.now.tv_sec.to_s
|
170
|
+
else
|
171
|
+
kwargs.delete("call_id")
|
172
|
+
end
|
173
|
+
|
174
|
+
custom_format = kwargs.include?("format") || kwargs.include?("callback")
|
175
|
+
kwargs["format"] ||= "JSON"
|
176
|
+
kwargs["v"] ||= FB_API_VERSION
|
177
|
+
kwargs["api_key"]||= api_key
|
178
|
+
kwargs["method"] ||= method
|
179
|
+
|
180
|
+
file_name = kwargs.delete("filename")
|
181
|
+
|
182
|
+
kwargs["sig"] = signature_for(kwargs, secret.value.call)
|
183
|
+
|
184
|
+
fb_method = kwargs["method"].downcase
|
185
|
+
if fb_method == "photos.upload"
|
186
|
+
# Then we need a multipart post
|
187
|
+
response = MiniFB.post_upload(file_name, kwargs)
|
188
|
+
else
|
189
|
+
|
190
|
+
begin
|
191
|
+
response = Net::HTTP.post_form(URI.parse(FB_URL), post_params(kwargs))
|
192
|
+
rescue SocketError => err
|
193
|
+
# why are we catching this and throwing as different error? hmmm..
|
194
|
+
# raise IOError.new( "Cannot connect to the facebook server: " + err )
|
195
|
+
raise err
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Handle response
|
200
|
+
return response.body if custom_format
|
201
|
+
|
202
|
+
body = response.body
|
203
|
+
|
204
|
+
puts 'response=' + body.inspect if @@logging
|
205
|
+
begin
|
206
|
+
data = JSON.parse(body)
|
207
|
+
if data.include?("error_msg")
|
208
|
+
raise FaceBookError.new(data["error_code"] || 1, data["error_msg"])
|
209
|
+
end
|
210
|
+
|
211
|
+
rescue JSON::ParserError => ex
|
212
|
+
if BAD_JSON_METHODS.include?(fb_method) # Little hack because this response isn't valid JSON
|
213
|
+
if body == "0" || body == "false"
|
214
|
+
return false
|
215
|
+
end
|
216
|
+
return body
|
217
|
+
else
|
218
|
+
raise ex
|
219
|
+
end
|
220
|
+
end
|
221
|
+
return data
|
222
|
+
end
|
223
|
+
|
224
|
+
def MiniFB.post_upload(filename, kwargs)
|
225
|
+
content = File.open(filename, 'rb') { |f| f.read }
|
226
|
+
boundary = Digest::MD5.hexdigest(content)
|
227
|
+
header = {'Content-type' => "multipart/form-data, boundary=#{boundary}"}
|
228
|
+
|
229
|
+
# Build query
|
230
|
+
query = ''
|
231
|
+
kwargs.each { |a, v|
|
232
|
+
query <<
|
233
|
+
"--#{boundary}\r\n" <<
|
234
|
+
"Content-Disposition: form-data; name=\"#{a}\"\r\n\r\n" <<
|
235
|
+
"#{v}\r\n"
|
236
|
+
}
|
237
|
+
query <<
|
238
|
+
"--#{boundary}\r\n" <<
|
239
|
+
"Content-Disposition: form-data; filename=\"#{File.basename(filename)}\"\r\n" <<
|
240
|
+
"Content-Transfer-Encoding: binary\r\n" <<
|
241
|
+
"Content-Type: image/jpeg\r\n\r\n" <<
|
242
|
+
content <<
|
243
|
+
"\r\n" <<
|
244
|
+
"--#{boundary}--"
|
245
|
+
|
246
|
+
# Call Facebook with POST multipart/form-data request
|
247
|
+
uri = URI.parse(FB_URL)
|
248
|
+
Net::HTTP.start(uri.host) { |http| http.post uri.path, query, header }
|
249
|
+
end
|
250
|
+
|
251
|
+
# Returns true is signature is valid, false otherwise.
|
252
|
+
def MiniFB.verify_signature(secret, arguments)
|
253
|
+
signature = arguments.delete("fb_sig")
|
254
|
+
return false if signature.nil?
|
255
|
+
|
256
|
+
unsigned = Hash.new
|
257
|
+
signed = Hash.new
|
258
|
+
|
259
|
+
arguments.each do |k, v|
|
260
|
+
if k =~ /^fb_sig_(.*)/ then
|
261
|
+
signed[$1] = v
|
262
|
+
else
|
263
|
+
unsigned[k] = v
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
arg_string = String.new
|
268
|
+
signed.sort.each { |kv| arg_string << kv[0] << "=" << kv[1] }
|
269
|
+
if Digest::MD5.hexdigest(arg_string + secret) == signature
|
270
|
+
return true
|
271
|
+
end
|
272
|
+
return false
|
273
|
+
end
|
274
|
+
|
275
|
+
# Parses cookies in order to extract the facebook cookie and parse it into a useable hash
|
276
|
+
#
|
277
|
+
# options:
|
278
|
+
# * app_id - the connect applications app_id (some users may find they have to use their facebook API key)
|
279
|
+
# * secret - the connect application secret
|
280
|
+
# * cookies - the cookies given by facebook - it is ok to just pass all of the cookies, the method will do the filtering for you.
|
281
|
+
def MiniFB.parse_cookie_information(app_id, cookies)
|
282
|
+
return nil if cookies["fbs_#{app_id}"].nil?
|
283
|
+
Hash[*cookies["fbs_#{app_id}"].split('&').map { |v| v.gsub('"', '').split('=', 2) }.flatten]
|
284
|
+
end
|
285
|
+
|
286
|
+
# Validates that the cookies sent by the user are those that were set by facebook. Since your
|
287
|
+
# secret is only known by you and facebook it is used to sign all of the cookies set.
|
288
|
+
#
|
289
|
+
# options:
|
290
|
+
# * app_id - the connect applications app_id (some users may find they have to use their facebook API key)
|
291
|
+
# * secret - the connect application secret
|
292
|
+
# * cookies - the cookies given by facebook - it is ok to just pass all of the cookies, the method will do the filtering for you.
|
293
|
+
def MiniFB.verify_cookie_signature(app_id, secret, cookies)
|
294
|
+
fb_keys = MiniFB.parse_cookie_information(app_id, cookies)
|
295
|
+
return false if fb_keys.nil?
|
296
|
+
|
297
|
+
signature = fb_keys.delete('sig')
|
298
|
+
return signature == Digest::MD5.hexdigest(fb_keys.map { |k, v| "#{k}=#{v}" }.sort.join + secret)
|
299
|
+
end
|
300
|
+
|
301
|
+
# <b>DEPRECATED:</b> Please use <tt>verify_cookie_signature</tt> instead.
|
302
|
+
def MiniFB.verify_connect_signature(api_key, secret, cookies)
|
303
|
+
warn "DEPRECATION WARNING: 'verify_connect_signature' has been renamed to 'verify_cookie_signature' as Facebook no longer calls this 'connect'"
|
304
|
+
MiniFB.verify_cookie_signature(api_key, secret, cookies)
|
305
|
+
end
|
306
|
+
|
307
|
+
# Returns the login/add app url for your application.
|
308
|
+
#
|
309
|
+
# options:
|
310
|
+
# - :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
|
311
|
+
# - :canvas => true/false - to say whether this is a canvas app or not
|
312
|
+
def self.login_url(api_key, options={})
|
313
|
+
login_url = "http://api.facebook.com/login.php?api_key=#{api_key}"
|
314
|
+
login_url << "&next=#{options[:next]}" if options[:next]
|
315
|
+
login_url << "&canvas" if options[:canvas]
|
316
|
+
login_url
|
317
|
+
end
|
318
|
+
|
319
|
+
# Manages access_token and locale params for an OAuth connection
|
320
|
+
class OAuthSession
|
321
|
+
|
322
|
+
def initialize(access_token, locale="en_US")
|
323
|
+
@access_token = access_token
|
324
|
+
@locale = locale
|
325
|
+
end
|
326
|
+
|
327
|
+
def get(id, options={})
|
328
|
+
MiniFB.get(@access_token, id, session_options(options))
|
329
|
+
end
|
330
|
+
|
331
|
+
def post(id, options={})
|
332
|
+
MiniFB.post(@access_token, id, session_options(options))
|
333
|
+
end
|
334
|
+
|
335
|
+
def fql(fql_query, options={})
|
336
|
+
MiniFB.fql(@access_token, fql_query, session_options(options))
|
337
|
+
end
|
338
|
+
|
339
|
+
def multifql(fql_queries, options={})
|
340
|
+
MiniFB.multifql(@access_token, fql_queries, session_options(options))
|
341
|
+
end
|
342
|
+
|
343
|
+
def rest(api_method, options={})
|
344
|
+
MiniFB.rest(@access_token, api_method, session_options(options))
|
345
|
+
end
|
346
|
+
|
347
|
+
# Returns a GraphObject for the given id
|
348
|
+
def graph_object(id)
|
349
|
+
MiniFB::GraphObject.new(self, id)
|
350
|
+
end
|
351
|
+
|
352
|
+
# Returns and caches a GraphObject for the user
|
353
|
+
def me
|
354
|
+
@me ||= graph_object('me')
|
355
|
+
end
|
356
|
+
|
357
|
+
private
|
358
|
+
def session_options(options)
|
359
|
+
(options[:params] ||= {})[:locale] ||= @locale
|
360
|
+
options
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
# Wraps a graph object for easily accessing its connections
|
365
|
+
class GraphObject
|
366
|
+
# Creates a GraphObject using an OAuthSession or access_token
|
367
|
+
def initialize(session_or_token, id)
|
368
|
+
@oauth_session = if session_or_token.is_a?(MiniFB::OAuthSession)
|
369
|
+
session_or_token
|
370
|
+
else
|
371
|
+
MiniFB::OAuthSession.new(session_or_token)
|
372
|
+
end
|
373
|
+
@id = id
|
374
|
+
@object = @oauth_session.get(id, :metadata => true)
|
375
|
+
@connections_cache = {}
|
376
|
+
end
|
377
|
+
|
378
|
+
def inspect
|
379
|
+
"<##{self.class.name} #{@object.inspect}>"
|
380
|
+
end
|
381
|
+
|
382
|
+
def connections
|
383
|
+
@object.metadata.connections.keys
|
384
|
+
end
|
385
|
+
|
386
|
+
unless RUBY_VERSION =~ /1\.9/
|
387
|
+
undef :id, :type
|
388
|
+
end
|
389
|
+
|
390
|
+
def methods
|
391
|
+
super + @object.keys.include?(key) + connections.include?(key)
|
392
|
+
end
|
393
|
+
|
394
|
+
def respond_to?(method)
|
395
|
+
@object.keys.include?(key) || connections.include?(key) || super
|
396
|
+
end
|
397
|
+
|
398
|
+
def keys
|
399
|
+
@object.keys
|
400
|
+
end
|
401
|
+
|
402
|
+
def [](key)
|
403
|
+
@object[key]
|
404
|
+
end
|
405
|
+
|
406
|
+
def method_missing(method, *args, &block)
|
407
|
+
key = method.to_s
|
408
|
+
if @object.keys.include?(key)
|
409
|
+
@object[key]
|
410
|
+
elsif @connections_cache.has_key?(key)
|
411
|
+
@connections_cache[key]
|
412
|
+
elsif connections.include?(key)
|
413
|
+
@connections_cache[key] = @oauth_session.get(@id, :type => key)
|
414
|
+
else
|
415
|
+
super
|
416
|
+
end
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
def self.graph_base
|
421
|
+
"https://graph.facebook.com/"
|
422
|
+
end
|
423
|
+
|
424
|
+
# options:
|
425
|
+
# - scope: comma separated list of extends permissions. see http://developers.facebook.com/docs/authentication/permissions
|
426
|
+
def self.oauth_url(app_id, redirect_uri, options={})
|
427
|
+
oauth_url = "#{graph_base}oauth/authorize"
|
428
|
+
oauth_url << "?client_id=#{app_id}"
|
429
|
+
oauth_url << "&redirect_uri=#{URI.escape(redirect_uri)}"
|
430
|
+
# oauth_url << "&scope=#{options[:scope]}" if options[:scope]
|
431
|
+
oauth_url << ("&" + options.map { |k, v| "%s=%s" % [k, v] }.join('&')) unless options.empty?
|
432
|
+
oauth_url
|
433
|
+
end
|
434
|
+
|
435
|
+
# returns a hash with one value being 'access_token', the other being 'expires'
|
436
|
+
def self.oauth_access_token(app_id, redirect_uri, secret, code)
|
437
|
+
oauth_url = "#{graph_base}oauth/access_token"
|
438
|
+
oauth_url << "?client_id=#{app_id}"
|
439
|
+
oauth_url << "&redirect_uri=#{URI.escape(redirect_uri)}"
|
440
|
+
oauth_url << "&client_secret=#{secret}"
|
441
|
+
oauth_url << "&code=#{URI.escape(code)}"
|
442
|
+
resp = RestClient.get oauth_url
|
443
|
+
puts 'resp=' + resp.body.to_s if @@logging
|
444
|
+
params = {}
|
445
|
+
params_array = resp.split("&")
|
446
|
+
params_array.each do |p|
|
447
|
+
ps = p.split("=")
|
448
|
+
params[ps[0]] = ps[1]
|
449
|
+
end
|
450
|
+
return params
|
451
|
+
end
|
452
|
+
|
453
|
+
# Gets data from the Facebook Graph API
|
454
|
+
# options:
|
455
|
+
# - type: eg: feed, home, etc
|
456
|
+
# - metadata: to include metadata in response. true/false
|
457
|
+
# - params: Any additional parameters you would like to submit
|
458
|
+
def self.get(access_token, id, options={})
|
459
|
+
url = "#{graph_base}#{id}"
|
460
|
+
url << "/#{options[:type]}" if options[:type]
|
461
|
+
params = options[:params] || {}
|
462
|
+
params["access_token"] = "#{(access_token)}"
|
463
|
+
params["metadata"] = "1" if options[:metadata]
|
464
|
+
options[:params] = params
|
465
|
+
return fetch(url, options)
|
466
|
+
end
|
467
|
+
|
468
|
+
# Posts data to the Facebook Graph API
|
469
|
+
# options:
|
470
|
+
# - type: eg: feed, home, etc
|
471
|
+
# - metadata: to include metadata in response. true/false
|
472
|
+
# - params: Any additional parameters you would like to submit
|
473
|
+
def self.post(access_token, id, options={})
|
474
|
+
url = "#{graph_base}#{id}"
|
475
|
+
url << "/#{options[:type]}" if options[:type]
|
476
|
+
options.delete(:type)
|
477
|
+
params = options[:params] || {}
|
478
|
+
options.each do |key, value|
|
479
|
+
if value.kind_of?(File)
|
480
|
+
params[key] = value
|
481
|
+
else
|
482
|
+
params[key] = "#{value}"
|
483
|
+
end
|
484
|
+
end
|
485
|
+
params["access_token"] = "#{(access_token)}"
|
486
|
+
params["metadata"] = "1" if options[:metadata]
|
487
|
+
options[:params] = params
|
488
|
+
options[:method] = :post
|
489
|
+
return fetch(url, options)
|
490
|
+
|
491
|
+
end
|
492
|
+
|
493
|
+
# Executes an FQL query
|
494
|
+
def self.fql(access_token, fql_query, options={})
|
495
|
+
url = "https://api.facebook.com/method/fql.query"
|
496
|
+
params = options[:params] || {}
|
497
|
+
params["access_token"] = "#{(access_token)}"
|
498
|
+
params["metadata"] = "1" if options[:metadata]
|
499
|
+
params["query"] = fql_query
|
500
|
+
params["format"] = "JSON"
|
501
|
+
options[:params] = params
|
502
|
+
return fetch(url, options)
|
503
|
+
end
|
504
|
+
|
505
|
+
# Executes multiple FQL queries
|
506
|
+
# Example:
|
507
|
+
#
|
508
|
+
# MiniFB.multifql(access_token, { :statuses => "SELECT status_id, message FROM status WHERE uid = 12345",
|
509
|
+
# :privacy => "SELECT object_id, description FROM privacy WHERE object_id IN (SELECT status_id FROM #statuses)" })
|
510
|
+
def self.multifql(access_token, fql_queries, options={})
|
511
|
+
url = "https://api.facebook.com/method/fql.multiquery"
|
512
|
+
params = options[:params] || {}
|
513
|
+
params["access_token"] = "#{(access_token)}"
|
514
|
+
params["metadata"] = "1" if options[:metadata]
|
515
|
+
params["queries"] = JSON[fql_queries]
|
516
|
+
params[:format] = "JSON"
|
517
|
+
options[:params] = params
|
518
|
+
return fetch(url, options)
|
519
|
+
end
|
520
|
+
|
521
|
+
# Uses new Oauth 2 authentication against old Facebook REST API
|
522
|
+
# options:
|
523
|
+
# - params: Any additional parameters you would like to submit
|
524
|
+
def self.rest(access_token, api_method, options={})
|
525
|
+
url = "https://api.facebook.com/method/#{api_method}"
|
526
|
+
params = options[:params] || {}
|
527
|
+
params[:access_token] = access_token
|
528
|
+
params[:format] = "JSON"
|
529
|
+
options[:params] = params
|
530
|
+
return fetch(url, options)
|
531
|
+
end
|
532
|
+
|
533
|
+
|
534
|
+
def self.fetch(url, options={})
|
535
|
+
|
536
|
+
begin
|
537
|
+
if options[:method] == :post
|
538
|
+
puts 'url_post=' + url if @@logging
|
539
|
+
resp = RestClient.post url, options[:params]
|
540
|
+
else
|
541
|
+
if options[:params] && options[:params].size > 0
|
542
|
+
url += '?' + options[:params].map { |k, v| URI.escape("%s=%s" % [k, v]) }.join('&')
|
543
|
+
end
|
544
|
+
puts 'url_get=' + url if @@logging
|
545
|
+
resp = RestClient.get url
|
546
|
+
end
|
547
|
+
|
548
|
+
puts 'resp=' + resp.to_s if @@logging
|
549
|
+
|
550
|
+
begin
|
551
|
+
res_hash = JSON.parse(resp.to_s)
|
552
|
+
rescue
|
553
|
+
# quick fix for things like stream.publish that don't return json
|
554
|
+
res_hash = JSON.parse("{\"response\": #{resp.to_s}}")
|
555
|
+
end
|
556
|
+
|
557
|
+
if res_hash.is_a? Array # fql return this
|
558
|
+
res_hash.collect! { |x| Hashie::Mash.new(x) }
|
559
|
+
else
|
560
|
+
res_hash = Hashie::Mash.new(res_hash)
|
561
|
+
end
|
562
|
+
|
563
|
+
if res_hash.include?("error_msg")
|
564
|
+
raise FaceBookError.new(res_hash["error_code"] || 1, res_hash["error_msg"])
|
565
|
+
end
|
566
|
+
|
567
|
+
return res_hash
|
568
|
+
rescue RestClient::Exception => ex
|
569
|
+
puts ex.http_code.to_s
|
570
|
+
puts 'ex.http_body=' + ex.http_body if @@logging
|
571
|
+
res_hash = JSON.parse(ex.http_body) # probably should ensure it has a good response
|
572
|
+
raise MiniFB::FaceBookError.new(ex.http_code, "#{res_hash["error"]["type"]}: #{res_hash["error"]["message"]}")
|
573
|
+
end
|
574
|
+
|
575
|
+
end
|
576
|
+
|
577
|
+
# Returns all available scopes.
|
578
|
+
def self.scopes
|
579
|
+
scopes = %w{
|
580
|
+
about_me activities birthday education_history events groups
|
581
|
+
hometown interests likes location notes online_presence
|
582
|
+
photo_video_tags photos relationships religion_politics
|
583
|
+
status videos website work_history
|
584
|
+
}
|
585
|
+
scopes.map! do |scope|
|
586
|
+
["user_#{scope}", "friends_#{scope}"]
|
587
|
+
end.flatten!
|
588
|
+
|
589
|
+
scopes += %w{
|
590
|
+
read_insights read_stream read_mailbox read_friendlists read_requests
|
591
|
+
email ads_management xmpp_login
|
592
|
+
publish_stream create_event rsvp_event sms offline_access
|
593
|
+
}
|
594
|
+
end
|
595
|
+
|
596
|
+
# This function expects arguments as a hash, so
|
597
|
+
# it is agnostic to different POST handling variants in ruby.
|
598
|
+
#
|
599
|
+
# Validate the arguments received from facebook. This is usually
|
600
|
+
# sent for the iframe in Facebook's canvas. It is not necessary
|
601
|
+
# to use this on the auth_token and uid passed to callbacks like
|
602
|
+
# post-add and post-remove.
|
603
|
+
#
|
604
|
+
# The arguments must be a mapping of to string keys and values
|
605
|
+
# or a string of http request data.
|
606
|
+
#
|
607
|
+
# If the data is invalid or not signed properly, an empty
|
608
|
+
# dictionary is returned.
|
609
|
+
#
|
610
|
+
# The secret argument should be an instance of FacebookSecret
|
611
|
+
# to hide value from simple introspection.
|
612
|
+
#
|
613
|
+
# DEPRECATED, use verify_signature instead
|
614
|
+
def MiniFB.validate(secret, arguments)
|
615
|
+
|
616
|
+
signature = arguments.delete("fb_sig")
|
617
|
+
return arguments if signature.nil?
|
618
|
+
|
619
|
+
unsigned = Hash.new
|
620
|
+
signed = Hash.new
|
621
|
+
|
622
|
+
arguments.each do |k, v|
|
623
|
+
if k =~ /^fb_sig_(.*)/ then
|
624
|
+
signed[$1] = v
|
625
|
+
else
|
626
|
+
unsigned[k] = v
|
627
|
+
end
|
628
|
+
end
|
629
|
+
|
630
|
+
arg_string = String.new
|
631
|
+
signed.sort.each { |kv| arg_string << kv[0] << "=" << kv[1] }
|
632
|
+
if Digest::MD5.hexdigest(arg_string + secret) != signature
|
633
|
+
unsigned # Hash is incorrect, return only unsigned fields.
|
634
|
+
else
|
635
|
+
unsigned.merge signed
|
636
|
+
end
|
637
|
+
end
|
638
|
+
|
639
|
+
class FaceBookSecret
|
640
|
+
# Simple container that stores a secret value.
|
641
|
+
# Proc cannot be dumped or introspected by normal tools.
|
642
|
+
attr_reader :value
|
643
|
+
|
644
|
+
def initialize(value)
|
645
|
+
@value = Proc.new { value }
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
649
|
+
private
|
650
|
+
def self.post_params(params)
|
651
|
+
post_params = {}
|
652
|
+
params.each do |k, v|
|
653
|
+
k = k.to_s unless k.is_a?(String)
|
654
|
+
if Array === v || Hash === v
|
655
|
+
post_params[k] = JSON.dump(v)
|
656
|
+
else
|
657
|
+
post_params[k] = v
|
658
|
+
end
|
659
|
+
end
|
660
|
+
post_params
|
661
|
+
end
|
662
|
+
|
663
|
+
def self.signature_for(params, secret)
|
664
|
+
params.delete_if { |k, v| v.nil? }
|
665
|
+
raw_string = params.inject([]) do |collection, pair|
|
666
|
+
collection << pair.map { |x|
|
667
|
+
Array === x ? JSON.dump(x) : x
|
668
|
+
}.join("=")
|
669
|
+
collection
|
670
|
+
end.sort.join
|
671
|
+
Digest::MD5.hexdigest([raw_string, secret].join)
|
672
|
+
end
|
673
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'clearance'
|