facebooker 0.9.5

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 (60) hide show
  1. data/CHANGELOG.txt +0 -0
  2. data/COPYING +19 -0
  3. data/History.txt +3 -0
  4. data/Manifest.txt +60 -0
  5. data/README +44 -0
  6. data/README.txt +79 -0
  7. data/Rakefile +38 -0
  8. data/TODO.txt +10 -0
  9. data/facebooker.yml.tpl +36 -0
  10. data/init.rb +52 -0
  11. data/install.rb +5 -0
  12. data/lib/facebooker.rb +63 -0
  13. data/lib/facebooker/affiliation.rb +10 -0
  14. data/lib/facebooker/album.rb +11 -0
  15. data/lib/facebooker/cookie.rb +10 -0
  16. data/lib/facebooker/data.rb +38 -0
  17. data/lib/facebooker/education_info.rb +11 -0
  18. data/lib/facebooker/event.rb +26 -0
  19. data/lib/facebooker/feed.rb +65 -0
  20. data/lib/facebooker/group.rb +35 -0
  21. data/lib/facebooker/location.rb +8 -0
  22. data/lib/facebooker/model.rb +118 -0
  23. data/lib/facebooker/notifications.rb +17 -0
  24. data/lib/facebooker/parser.rb +386 -0
  25. data/lib/facebooker/photo.rb +9 -0
  26. data/lib/facebooker/rails/controller.rb +174 -0
  27. data/lib/facebooker/rails/facebook_asset_path.rb +18 -0
  28. data/lib/facebooker/rails/facebook_form_builder.rb +112 -0
  29. data/lib/facebooker/rails/facebook_request_fix.rb +14 -0
  30. data/lib/facebooker/rails/facebook_session_handling.rb +58 -0
  31. data/lib/facebooker/rails/facebook_url_rewriting.rb +31 -0
  32. data/lib/facebooker/rails/helpers.rb +535 -0
  33. data/lib/facebooker/rails/routing.rb +49 -0
  34. data/lib/facebooker/rails/test_helpers.rb +11 -0
  35. data/lib/facebooker/rails/utilities.rb +22 -0
  36. data/lib/facebooker/server_cache.rb +24 -0
  37. data/lib/facebooker/service.rb +25 -0
  38. data/lib/facebooker/session.rb +385 -0
  39. data/lib/facebooker/tag.rb +12 -0
  40. data/lib/facebooker/user.rb +200 -0
  41. data/lib/facebooker/version.rb +9 -0
  42. data/lib/facebooker/work_info.rb +9 -0
  43. data/lib/net/http_multipart_post.rb +123 -0
  44. data/lib/tasks/facebooker.rake +16 -0
  45. data/lib/tasks/tunnel.rake +39 -0
  46. data/setup.rb +1585 -0
  47. data/test/event_test.rb +15 -0
  48. data/test/facebook_cache_test.rb +43 -0
  49. data/test/facebook_data_test.rb +50 -0
  50. data/test/facebooker_test.rb +766 -0
  51. data/test/fixtures/multipart_post_body_with_only_parameters.txt +33 -0
  52. data/test/fixtures/multipart_post_body_with_single_file.txt +38 -0
  53. data/test/fixtures/multipart_post_body_with_single_file_that_has_nil_key.txt +38 -0
  54. data/test/http_multipart_post_test.rb +54 -0
  55. data/test/model_test.rb +79 -0
  56. data/test/rails_integration_test.rb +732 -0
  57. data/test/session_test.rb +396 -0
  58. data/test/test_helper.rb +54 -0
  59. data/test/user_test.rb +101 -0
  60. metadata +130 -0
@@ -0,0 +1,49 @@
1
+ module Facebooker
2
+ module Rails
3
+ module Routing
4
+ module RouteSetExtensions
5
+ def self.included(base)
6
+ base.alias_method_chain :extract_request_environment, :facebooker
7
+ end
8
+
9
+ def extract_request_environment_with_facebooker(request)
10
+ env = extract_request_environment_without_facebooker(request)
11
+ env.merge :canvas => (request.parameters[:fb_sig_in_canvas]=="1")
12
+ end
13
+ end
14
+ module MapperExtensions
15
+
16
+ # Generates pseudo-resource routes. Since everything is a POST, routes can't be identified
17
+ # using HTTP verbs. Therefore, the action is appended to the beginning of each named route,
18
+ # except for index.
19
+ #
20
+ # Example:
21
+ # map.facebook_resources :profiles
22
+ #
23
+ # Generates the following routes:
24
+ #
25
+ # new_profile POST /profiles/new {:controller=>"profiles", :action=>"new"}
26
+ # profiles POST /profiles/index {:controller=>"profiles", :action=>"index"}
27
+ # show_profile POST /profiles/:id/show {:controller=>"profiles", :action=>"show"}
28
+ # create_profile POST /profiles/create {:controller=>"profiles", :action=>"create"}
29
+ # edit_profile POST /profiles/:id/edit {:controller=>"profiles", :action=>"edit"}
30
+ # update_profile POST /profiles/:id/update {:controller=>"profiles", :action=>"update"}
31
+ # destroy_profile POST /profiles/:id/destroy {:controller=>"profiles", :action=>"destroy"}
32
+ #
33
+ def facebook_resources(name_sym)
34
+ name = name_sym.to_s
35
+
36
+ with_options :controller => name, :conditions => { :method => :post } do |map|
37
+ map.named_route("new_#{name.singularize}", "#{name}/new", :action => 'new')
38
+ map.named_route(name, "#{name}/index", :action => 'index')
39
+ map.named_route("show_#{name.singularize}", "#{name}/:id/show", :action => 'show', :id => /\d+/)
40
+ map.named_route("create_#{name.singularize}", "#{name}/create", :action => 'create')
41
+ map.named_route("edit_#{name.singularize}", "#{name}/:id/edit", :action => 'edit', :id => /\d+/)
42
+ map.named_route("update_#{name.singularize}", "#{name}/:id/update", :action => 'update', :id => /\d+/)
43
+ map.named_route("destroy_#{name.singularize}", "#{name}/:id/destroy", :action => 'destroy', :id => /\d+/)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,11 @@
1
+ module Facebooker
2
+ module Rails
3
+ module TestHelpers
4
+ def assert_fb_redirect_to(url)
5
+ assert_equal "fb:redirect", response_from_page_or_rjs.children.first.name
6
+ assert_equal url, response_from_page_or_rjs.children.first.attributes['url']
7
+ end
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,22 @@
1
+ module Facebooker
2
+ module Rails
3
+ class Utilities
4
+ class << self
5
+ def refresh_all_images(session)
6
+ Dir.glob(File.join(RAILS_ROOT,"public","images","*.{png,jpg,gif}")).each do |img|
7
+ refresh_image(session,img)
8
+ end
9
+ end
10
+
11
+ def refresh_image(session,full_path)
12
+ basename=File.basename(full_path)
13
+ base_path=ActionController::Base.asset_host
14
+ base_path += "/" unless base_path.ends_with?("/")
15
+ image_path=base_path+"images/#{basename}"
16
+ puts "refreshing: #{image_path}"
17
+ session.server_cache.refresh_img_src(image_path)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module Facebooker
2
+ class ServerCache
3
+ def initialize(session)
4
+ @session = session
5
+ end
6
+
7
+ #
8
+ # Stores an FBML reference on the server for use
9
+ # across multiple users in FBML
10
+ def set_ref_handle(handle_name, fbml_source)
11
+ (@session.post 'facebook.fbml.setRefHandle', :handle => handle_name, :fbml => fbml_source) == '1'
12
+ end
13
+
14
+ ##
15
+ # Fetches and re-caches the content stored at the given URL, for use in a fb:ref FBML tag.
16
+ def refresh_ref_url(url)
17
+ (@session.post 'facebook.fbml.refreshRefUrl', :url => url) == '1'
18
+ end
19
+
20
+ def refresh_img_src(url)
21
+ (@session.post 'facebook.fbml.refreshImgSrc', :url => url) == '1'
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ require 'net/http'
2
+ require 'facebooker/parser'
3
+ module Facebooker
4
+ class Service
5
+ def initialize(api_base, api_path, api_key)
6
+ @api_base = api_base
7
+ @api_path = api_path
8
+ @api_key = api_key
9
+ end
10
+
11
+ # TODO: support ssl
12
+ def post(params)
13
+ Parser.parse(params[:method], Net::HTTP.post_form(url, params))
14
+ end
15
+
16
+ def post_file(params)
17
+ Parser.parse(params[:method], Net::HTTP.post_multipart_form(url, params))
18
+ end
19
+
20
+ private
21
+ def url
22
+ URI.parse('http://'+ @api_base + @api_path)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,385 @@
1
+ require 'digest/md5'
2
+ require 'cgi'
3
+
4
+ module Facebooker
5
+ #
6
+ # Raised when trying to perform an operation on a user
7
+ # other than the logged in user (if that's unallowed)
8
+ class NonSessionUser < StandardError; end
9
+ class Session
10
+ class SessionExpired < StandardError; end
11
+ class UnknownError < StandardError; end
12
+ class ServiceUnavailable < StandardError; end
13
+ class MaxRequestsDepleted < StandardError; end
14
+ class HostNotAllowed < StandardError; end
15
+ class MissingOrInvalidParameter < StandardError; end
16
+ class InvalidAPIKey < StandardError; end
17
+ class SessionExpired < StandardError; end
18
+ class CallOutOfOrder < StandardError; end
19
+ class IncorrectSignature < StandardError; end
20
+ class SignatureTooOld < StandardError; end
21
+ class TooManyUserCalls < StandardError; end
22
+ class TooManyUserActionCalls < StandardError; end
23
+ class InvalidFeedTitleLink < StandardError; end
24
+ class InvalidFeedTitleLength < StandardError; end
25
+ class InvalidFeedTitleName < StandardError; end
26
+ class BlankFeedTitle < StandardError; end
27
+ class FeedBodyLengthTooLong < StandardError; end
28
+ class InvalidFeedPhotoSource < StandardError; end
29
+ class InvalidFeedPhotoLink < StandardError; end
30
+ class FeedMarkupInvalid < StandardError; end
31
+ class FeedTitleDataInvalid < StandardError; end
32
+ class FeedTitleTemplateInvalid < StandardError; end
33
+ class FeedBodyDataInvalid < StandardError; end
34
+ class FeedBodyTemplateInvalid < StandardError; end
35
+ class FeedPhotosNotRetrieved < StandardError; end
36
+ class FeedTargetIdsInvalid < StandardError; end
37
+ class ConfigurationMissing < StandardError; end
38
+ class FQLParseError < StandardError; end
39
+ class FQLFieldDoesNotExist < StandardError; end
40
+ class FQLTableDoesNotExist < StandardError; end
41
+ class FQLStatementNotIndexable < StandardError; end
42
+ class FQLFunctionDoesNotExist < StandardError; end
43
+ class FQLWrongNumberArgumentsPassedToFunction < StandardError; end
44
+ class InvalidAlbumId < StandardError; end
45
+ class AlbumIsFull < StandardError; end
46
+ class MissingOrInvalidImageFile < StandardError; end
47
+ class TooManyUnapprovedPhotosPending < StandardError; end
48
+
49
+ API_SERVER_BASE_URL = "api.facebook.com"
50
+ API_PATH_REST = "/restserver.php"
51
+ WWW_SERVER_BASE_URL = "www.facebook.com"
52
+ WWW_PATH_LOGIN = "/login.php"
53
+ WWW_PATH_ADD = "/add.php"
54
+ WWW_PATH_INSTALL = "/install.php"
55
+
56
+ attr_writer :auth_token
57
+ attr_reader :session_key
58
+
59
+ def self.create(api_key=nil, secret_key=nil)
60
+ api_key ||= self.api_key
61
+ secret_key ||= self.secret_key
62
+ raise ArgumentError unless !api_key.nil? && !secret_key.nil?
63
+ new(api_key, secret_key)
64
+ end
65
+
66
+ def self.api_key
67
+ extract_key_from_environment(:api) || extract_key_from_configuration_file(:api) rescue report_inability_to_find_key(:api)
68
+ end
69
+
70
+ def self.secret_key
71
+ extract_key_from_environment(:secret) || extract_key_from_configuration_file(:secret) rescue report_inability_to_find_key(:secret)
72
+ end
73
+
74
+ def self.current
75
+ @current_session
76
+ end
77
+
78
+ def self.current=(session)
79
+ @current_session=session
80
+ end
81
+
82
+ def login_url(options={})
83
+ options = default_login_url_options.merge(options)
84
+ "http://www.facebook.com/login.php?api_key=#{@api_key}&v=1.0#{login_url_optional_parameters(options)}"
85
+ end
86
+
87
+ def install_url(options={})
88
+ "http://www.facebook.com/install.php?api_key=#{@api_key}&v=1.0#{install_url_optional_parameters(options)}"
89
+ end
90
+
91
+ def install_url_optional_parameters(options)
92
+ optional_parameters = []
93
+ optional_parameters << "&next=#{CGI.escape(options[:next])}" if options[:next]
94
+ optional_parameters.join
95
+ end
96
+
97
+ def login_url_optional_parameters(options)
98
+ # It is important that unused options are omitted as stuff like &canvas=false will still display the canvas.
99
+ optional_parameters = []
100
+ optional_parameters << "&next=#{CGI.escape(options[:next])}" if options[:next]
101
+ optional_parameters << "&skipcookie=true" if options[:skip_cookie]
102
+ optional_parameters << "&hide_checkbox=true" if options[:hide_checkbox]
103
+ optional_parameters << "&canvas=true" if options[:canvas]
104
+ optional_parameters.join
105
+ end
106
+
107
+ def default_login_url_options
108
+ {}
109
+ end
110
+
111
+ def initialize(api_key, secret_key)
112
+ @api_key = api_key
113
+ @secret_key = secret_key
114
+ end
115
+
116
+ def secret_for_method(method_name)
117
+ @secret_key
118
+ end
119
+
120
+ def auth_token
121
+ @auth_token ||= post 'facebook.auth.createToken'
122
+ end
123
+
124
+ def infinite?
125
+ @expires == 0
126
+ end
127
+
128
+ def expired?
129
+ @expires.nil? || (!infinite? && Time.at(@expires) <= Time.now)
130
+ end
131
+
132
+ def secured?
133
+ !@session_key.nil? && !expired?
134
+ end
135
+
136
+ def secure!
137
+ response = post 'facebook.auth.getSession', :auth_token => auth_token
138
+ secure_with!(response['session_key'], response['uid'], response['expires'], response['secret'])
139
+ end
140
+
141
+ def secure_with!(session_key, uid, expires, secret_from_session = nil)
142
+ @session_key = session_key
143
+ @uid = Integer(uid)
144
+ @expires = Integer(expires)
145
+ @secret_from_session = secret_from_session
146
+ end
147
+
148
+ def fql_query(query, format = 'XML')
149
+ response = post('facebook.fql.query', :query => query, :format => format)
150
+ type = response.shift
151
+ response.shift.map do |hash|
152
+ case type
153
+ when 'user'
154
+ user = User.new
155
+ user.session = self
156
+ user.populate_from_hash!(hash)
157
+ user
158
+ when 'photo'
159
+ Photo.from_hash(hash)
160
+ when 'event_member'
161
+ Event::Attendance.from_hash(hash)
162
+ end
163
+ end
164
+ end
165
+
166
+ def user
167
+ @user ||= User.new(uid, self)
168
+ end
169
+
170
+ #
171
+ # This one has so many parameters, a Hash seemed cleaner than a long param list. Options can be:
172
+ # :uid => Filter by events associated with a user with this uid
173
+ # :eids => Filter by this list of event ids. This is a comma-separated list of eids.
174
+ # :start_time => Filter with this UTC as lower bound. A missing or zero parameter indicates no lower bound. (Time or Integer)
175
+ # :end_time => Filter with this UTC as upper bound. A missing or zero parameter indicates no upper bound. (Time or Integer)
176
+ # :rsvp_status => Filter by this RSVP status.
177
+ def events(options = {})
178
+ @events ||= post('facebook.events.get', options).map do |hash|
179
+ Event.from_hash(hash)
180
+ end
181
+ end
182
+
183
+ def event_members(eid)
184
+ @members ||= post('facebook.events.getMembers', :eid => eid).map do |attendee_hash|
185
+ Event::Attendance.from_hash(attendee_hash)
186
+ end
187
+ end
188
+
189
+
190
+ #
191
+ # Returns a proxy object for handling calls to Facebook cached items
192
+ # such as images and FBML ref handles
193
+ def server_cache
194
+ Facebooker::ServerCache.new(self)
195
+ end
196
+
197
+ #
198
+ # Returns a proxy object for handling calls to the Facebook Data API
199
+ def data
200
+ Facebooker::Data.new(self)
201
+ end
202
+
203
+ #
204
+ # Given an array like:
205
+ # [[userid, otheruserid], [yetanotherid, andanotherid]]
206
+ # returns a Hash indicating friendship of those pairs:
207
+ # {[userid, otheruserid] => true, [yetanotherid, andanotherid] => false}
208
+ # if one of the Hash values is nil, it means the facebook platform's answer is "I don't know"
209
+ def check_friendship(array_of_pairs_of_users)
210
+ uids1 = []
211
+ uids2 = []
212
+ array_of_pairs_of_users.each do |pair|
213
+ uids1 = pair.first
214
+ uids2 = pair.last
215
+ end
216
+ post('facebook.friends.areFriends', :uids1 => uids1, :uids2 => uids2)
217
+ end
218
+
219
+ def get_photos(pids = nil, subj_id = nil, aid = nil)
220
+ if [subj_id, pids, aid].all? {|arg| arg.nil?}
221
+ raise ArgumentError, "Can't get a photo without a picture, album or subject ID"
222
+ end
223
+ @photos = post('facebook.photos.get', :subj_id => subj_id, :pids => pids, :aid => aid ).map do |hash|
224
+ Photo.from_hash(hash)
225
+ end
226
+ end
227
+
228
+ def get_albums(aids)
229
+ @albums = post('facebook.photos.getAlbums', :aids => aids).map do |hash|
230
+ Album.from_hash(hash)
231
+ end
232
+ end
233
+
234
+ def get_tags(pids)
235
+ @tags = post('facebook.photos.getTags', :pids => pids).map do |hash|
236
+ Tag.from_hash(hash)
237
+ end
238
+ end
239
+
240
+ def add_tags(pid, x, y, tag_uid = nil, tag_text = nil )
241
+ if [tag_uid, tag_text].all? {|arg| arg.nil?}
242
+ raise ArgumentError, "Must enter a name or string for this tag"
243
+ end
244
+ @tags = post('facebook.photos.addTag', :pid => pid, :tag_uid => tag_uid, :tag_text => tag_text, :x => x, :y => y )
245
+ end
246
+
247
+ def send_notification(user_ids, fbml, email_fbml = nil)
248
+ params = {:notification => fbml, :to_ids => user_ids.map{ |id| User.cast_to_facebook_id(id)}.join(',')}
249
+ if email_fbml
250
+ params[:email] = email_fbml
251
+ end
252
+ post 'facebook.notifications.send', params
253
+ end
254
+
255
+ ##
256
+ # Send email to as many as 100 users at a time
257
+ def send_email(user_ids, subject, text, fbml = nil)
258
+ user_ids = Array(user_ids)
259
+ params = {:fbml => fbml, :recipients => user_ids.map{ |id| User.cast_to_facebook_id(id)}.join(','), :text => text, :subject => subject}
260
+ post 'facebook.notifications.sendEmail', params
261
+ end
262
+
263
+ # Only serialize the bare minimum to recreate the session.
264
+ def marshal_load(variables)#:nodoc:
265
+ fields_to_serialize.each_with_index{|field, index| instance_variable_set_value(field, variables[index])}
266
+ end
267
+
268
+ # Only serialize the bare minimum to recreate the session.
269
+ def marshal_dump#:nodoc:
270
+ fields_to_serialize.map{|field| instance_variable_value(field)}
271
+ end
272
+
273
+ # Only serialize the bare minimum to recreate the session.
274
+ def to_yaml( opts = {} )
275
+ YAML::quick_emit(self.object_id, opts) do |out|
276
+ out.map(taguri) do |map|
277
+ fields_to_serialize.each do |field|
278
+ map.add(field, instance_variable_value(field))
279
+ end
280
+ end
281
+ end
282
+ end
283
+
284
+ def instance_variable_set_value(field, value)
285
+ self.instance_variable_set("@#{field}", value)
286
+ end
287
+
288
+ def instance_variable_value(field)
289
+ self.instance_variable_get("@#{field}")
290
+ end
291
+
292
+ def fields_to_serialize
293
+ %w(session_key uid expires secret_from_session auth_token api_key secret_key)
294
+ end
295
+
296
+ class Desktop < Session
297
+ def login_url
298
+ super + "&auth_token=#{auth_token}"
299
+ end
300
+
301
+ def secret_for_method(method_name)
302
+ secret = auth_request_methods.include?(method_name) ? super : @secret_from_session
303
+ secret
304
+ end
305
+
306
+ def post(method, params = {})
307
+ if method == 'facebook.profile.getFBML' || method == 'facebook.profile.setFBML'
308
+ raise NonSessionUser.new("User #{@uid} is not the logged in user.") unless @uid == params[:uid]
309
+ end
310
+ super
311
+ end
312
+ private
313
+ def auth_request_methods
314
+ ['facebook.auth.getSession', 'facebook.auth.createToken']
315
+ end
316
+ end
317
+
318
+ def post(method, params = {})
319
+ add_facebook_params(params, method)
320
+ @session_key && params[:session_key] ||= @session_key
321
+ service.post(params.merge(:sig => signature_for(params)))
322
+ end
323
+
324
+ def post_file(method, params = {})
325
+ add_facebook_params(params, method)
326
+ @session_key && params[:session_key] ||= @session_key
327
+ service.post_file(params.merge(:sig => signature_for(params.reject{|key, value| key.nil?})))
328
+ end
329
+
330
+
331
+ def self.configuration_file_path
332
+ @configuration_file_path || File.expand_path("~/.facebookerrc")
333
+ end
334
+
335
+ def self.configuration_file_path=(path)
336
+ @configuration_file_path = path
337
+ end
338
+
339
+ private
340
+ def add_facebook_params(hash, method)
341
+ hash[:method] = method
342
+ hash[:api_key] = @api_key
343
+ hash[:call_id] = Time.now.to_f.to_s unless method == 'facebook.auth.getSession'
344
+ hash[:v] = "1.0"
345
+ end
346
+
347
+ def self.extract_key_from_environment(key_name)
348
+ val = ENV["FACEBOOK_" + key_name.to_s.upcase + "_KEY"]
349
+ end
350
+
351
+ def self.extract_key_from_configuration_file(key_name)
352
+ read_configuration_file[key_name]
353
+ end
354
+
355
+ def self.report_inability_to_find_key(key_name)
356
+ raise ConfigurationMissing, "Could not find configuration information for #{key_name}"
357
+ end
358
+
359
+ def self.read_configuration_file
360
+ eval(File.read(configuration_file_path))
361
+ end
362
+
363
+ def service
364
+ @service ||= Service.new(API_SERVER_BASE_URL, API_PATH_REST, @api_key)
365
+ end
366
+
367
+ def uid
368
+ @uid || (secure!; @uid)
369
+ end
370
+
371
+ def signature_for(params)
372
+ raw_string = params.inject([]) do |collection, pair|
373
+ collection << pair.join("=")
374
+ collection
375
+ end.sort.join
376
+ Digest::MD5.hexdigest([raw_string, secret_for_method(params[:method])].join)
377
+ end
378
+ end
379
+
380
+ class CanvasSession < Session
381
+ def default_login_url_options
382
+ {:canvas => true}
383
+ end
384
+ end
385
+ end