yury-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 (48) hide show
  1. data/CHANGELOG.txt +0 -0
  2. data/COPYING +19 -0
  3. data/History.txt +16 -0
  4. data/Manifest.txt +77 -0
  5. data/README +46 -0
  6. data/README.txt +81 -0
  7. data/Rakefile +62 -0
  8. data/TODO.txt +10 -0
  9. data/facebooker.yml.tpl +41 -0
  10. data/init.rb +53 -0
  11. data/install.rb +7 -0
  12. data/lib/facebooker.rb +143 -0
  13. data/lib/facebooker/data.rb +40 -0
  14. data/lib/facebooker/feed.rb +78 -0
  15. data/lib/facebooker/model.rb +123 -0
  16. data/lib/facebooker/parser.rb +518 -0
  17. data/lib/facebooker/rails/controller.rb +241 -0
  18. data/lib/facebooker/rails/facebook_asset_path.rb +18 -0
  19. data/lib/facebooker/rails/facebook_form_builder.rb +112 -0
  20. data/lib/facebooker/rails/facebook_request_fix.rb +24 -0
  21. data/lib/facebooker/rails/facebook_session_handling.rb +58 -0
  22. data/lib/facebooker/rails/facebook_url_rewriting.rb +39 -0
  23. data/lib/facebooker/rails/helpers.rb +615 -0
  24. data/lib/facebooker/rails/routing.rb +49 -0
  25. data/lib/facebooker/rails/test_helpers.rb +88 -0
  26. data/lib/facebooker/rails/utilities.rb +22 -0
  27. data/lib/facebooker/server_cache.rb +24 -0
  28. data/lib/facebooker/service.rb +31 -0
  29. data/lib/facebooker/session.rb +550 -0
  30. data/lib/facebooker/version.rb +9 -0
  31. data/lib/net/http_multipart_post.rb +123 -0
  32. data/lib/tasks/facebooker.rake +17 -0
  33. data/lib/tasks/tunnel.rake +43 -0
  34. data/setup.rb +1585 -0
  35. data/test/event_test.rb +15 -0
  36. data/test/facebook_cache_test.rb +43 -0
  37. data/test/facebook_data_test.rb +50 -0
  38. data/test/facebooker_test.rb +855 -0
  39. data/test/fixtures/multipart_post_body_with_only_parameters.txt +33 -0
  40. data/test/fixtures/multipart_post_body_with_single_file.txt +38 -0
  41. data/test/fixtures/multipart_post_body_with_single_file_that_has_nil_key.txt +38 -0
  42. data/test/http_multipart_post_test.rb +54 -0
  43. data/test/model_test.rb +91 -0
  44. data/test/rails_integration_test.rb +993 -0
  45. data/test/session_test.rb +559 -0
  46. data/test/test_helper.rb +60 -0
  47. data/test/user_test.rb +219 -0
  48. 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,88 @@
1
+ module Facebooker
2
+ module Rails
3
+ module TestHelpers
4
+ def assert_facebook_redirect_to(url)
5
+ assert_response :success
6
+ assert_not_nil facebook_redirect_url
7
+ assert_equal url, facebook_redirect_url
8
+ end
9
+
10
+ def follow_facebook_redirect!
11
+ facebook_post facebook_redirect_url
12
+ end
13
+
14
+ def facebook_get(path,params={})
15
+ facebook_verb(:get,path,params)
16
+ end
17
+
18
+ def facebook_post(path,params={})
19
+ facebook_verb(:post,path,params)
20
+ end
21
+
22
+ def facebook_put(path,params={})
23
+ facebook_verb(:put,path,params)
24
+ end
25
+ def facebook_delete(path,params={})
26
+ facebook_verb(:delete,path,params)
27
+ end
28
+
29
+ def facebook_verb(verb,path, params={})
30
+ params = default_facebook_parameters.update(params)
31
+ params.merge!(:fb_sig => generate_signature(facebook_params(params).stringify_keys))
32
+
33
+ params = params.update(:canvas => true).update(params)
34
+ send verb, path, params
35
+ end
36
+
37
+ def facebook_post(path, params={}, fb_params=facebook_parameters)
38
+ params = fb_params.merge(:canvas => true).merge(params)
39
+ post path, params
40
+ end
41
+
42
+ def facebook_parameters(overrides=nil)
43
+ overrides ||= {}
44
+ params = default_facebook_parameters.merge(overrides)
45
+ params.merge(:fb_sig => generate_signature(params.stringify_keys))
46
+ end
47
+
48
+ private
49
+
50
+ def default_facebook_parameters
51
+ {
52
+ :fb_sig_added => "1",
53
+ :fb_sig_session_key => "facebook_session_key",
54
+ :fb_sig_user => "1234",
55
+ :fb_sig_expires => "0",
56
+ :fb_sig_in_canvas => "1",
57
+ :fb_sig_time => Time.now.to_f
58
+ }
59
+ end
60
+
61
+ def facebook_params(params)
62
+ params.inject({}) do |fb_params, pair|
63
+ unless pair.first.to_s.match(/^fb_sig_/).nil?
64
+ fb_params[pair.first] = pair.last
65
+ end
66
+ fb_params
67
+ end
68
+ end
69
+
70
+ def facebook_redirect_url
71
+ match = @response.body.match(/<fb:redirect url="([^"]+)"/)
72
+ match.nil? ? nil : match.captures[0]
73
+ end
74
+
75
+ def generate_signature(facebook_params)
76
+ facebook_sig_params = facebook_params.inject({}) do |collection, pair|
77
+ collection[pair.first.sub(/^fb_sig_/, '')] = pair.last
78
+ collection
79
+ end
80
+
81
+ raw_string = facebook_sig_params.map{ |*args| args.join('=') }.sort.join
82
+ Digest::MD5.hexdigest([raw_string, Facebooker::Session.secret_key].join)
83
+ end
84
+
85
+ end
86
+ end
87
+ end
88
+
@@ -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},false) == '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},false) == '1'
18
+ end
19
+
20
+ def refresh_img_src(url)
21
+ (@session.post 'facebook.fbml.refreshImgSrc', {:url => url},false) == '1'
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
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
+ attempt = 0
14
+ Parser.parse(params[:method], Net::HTTP.post_form(url, params))
15
+ rescue Errno::ECONNRESET, EOFError
16
+ if attempt == 0
17
+ attempt += 1
18
+ retry
19
+ end
20
+ end
21
+
22
+ def post_file(params)
23
+ Parser.parse(params[:method], Net::HTTP.post_multipart_form(url, params))
24
+ end
25
+
26
+ private
27
+ def url
28
+ URI.parse('http://'+ @api_base + @api_path)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,550 @@
1
+ require 'cgi'
2
+
3
+ module Facebooker
4
+ #
5
+ # Raised when trying to perform an operation on a user
6
+ # other than the logged in user (if that's unallowed)
7
+ class NonSessionUser < StandardError; end
8
+ class Session
9
+ class SessionExpired < StandardError; end
10
+ class UnknownError < StandardError; end
11
+ class ServiceUnavailable < StandardError; end
12
+ class MaxRequestsDepleted < StandardError; end
13
+ class HostNotAllowed < StandardError; end
14
+ class MissingOrInvalidParameter < StandardError; end
15
+ class InvalidAPIKey < StandardError; end
16
+ class SessionExpired < StandardError; end
17
+ class CallOutOfOrder < StandardError; end
18
+ class IncorrectSignature < StandardError; end
19
+ class SignatureTooOld < StandardError; end
20
+ class TooManyUserCalls < StandardError; end
21
+ class TooManyUserActionCalls < StandardError; end
22
+ class InvalidFeedTitleLink < StandardError; end
23
+ class InvalidFeedTitleLength < StandardError; end
24
+ class InvalidFeedTitleName < StandardError; end
25
+ class BlankFeedTitle < StandardError; end
26
+ class FeedBodyLengthTooLong < StandardError; end
27
+ class InvalidFeedPhotoSource < StandardError; end
28
+ class InvalidFeedPhotoLink < StandardError; end
29
+ class TemplateDataMissingRequiredTokens < 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 TemplateBundleInvalid < StandardError; end
38
+ class ConfigurationMissing < StandardError; end
39
+ class FQLParseError < StandardError; end
40
+ class FQLFieldDoesNotExist < StandardError; end
41
+ class FQLTableDoesNotExist < StandardError; end
42
+ class FQLStatementNotIndexable < StandardError; end
43
+ class FQLFunctionDoesNotExist < StandardError; end
44
+ class FQLWrongNumberArgumentsPassedToFunction < StandardError; end
45
+ class InvalidAlbumId < StandardError; end
46
+ class AlbumIsFull < StandardError; end
47
+ class MissingOrInvalidImageFile < StandardError; end
48
+ class TooManyUnapprovedPhotosPending < StandardError; end
49
+ class ExtendedPermissionRequired < StandardError; end
50
+ class InvalidFriendList < StandardError; end
51
+ class UserRegistrationFailed < StandardError
52
+ attr_accessor :failed_users
53
+ end
54
+
55
+ API_SERVER_BASE_URL = ENV["FACEBOOKER_API"] == "new" ? "api.new.facebook.com" : "api.facebook.com"
56
+ API_PATH_REST = "/restserver.php"
57
+ WWW_SERVER_BASE_URL = ENV["FACEBOOKER_API"] == "new" ? "www.new.facebook.com" : "www.facebook.com"
58
+ WWW_PATH_LOGIN = "/login.php"
59
+ WWW_PATH_ADD = "/add.php"
60
+ WWW_PATH_INSTALL = "/install.php"
61
+
62
+ attr_writer :auth_token
63
+ attr_reader :session_key
64
+
65
+ def self.create(api_key=nil, secret_key=nil)
66
+ api_key ||= self.api_key
67
+ secret_key ||= self.secret_key
68
+ raise ArgumentError unless !api_key.nil? && !secret_key.nil?
69
+ new(api_key, secret_key)
70
+ end
71
+
72
+ def self.api_key
73
+ extract_key_from_environment(:api) || extract_key_from_configuration_file(:api) rescue report_inability_to_find_key(:api)
74
+ end
75
+
76
+ def self.secret_key
77
+ extract_key_from_environment(:secret) || extract_key_from_configuration_file(:secret) rescue report_inability_to_find_key(:secret)
78
+ end
79
+
80
+ def self.current
81
+ @current_session
82
+ end
83
+
84
+ def self.current=(session)
85
+ @current_session=session
86
+ end
87
+
88
+ def login_url(options={})
89
+ options = default_login_url_options.merge(options)
90
+ "#{Facebooker.login_url_base(@api_key)}#{login_url_optional_parameters(options)}"
91
+ end
92
+
93
+ def install_url(options={})
94
+ "#{Facebooker.install_url_base(@api_key)}#{install_url_optional_parameters(options)}"
95
+ end
96
+
97
+ def permission_url(permission,options={})
98
+ options = default_login_url_options.merge(options)
99
+ "http://#{Facebooker.www_server_base_url}/authorize.php?api_key=#{@api_key}&v=1.0&ext_perm=#{permission}#{install_url_optional_parameters(options)}"
100
+ end
101
+
102
+ def install_url_optional_parameters(options)
103
+ optional_parameters = []
104
+ optional_parameters += add_next_parameters(options)
105
+ optional_parameters.join
106
+ end
107
+
108
+ def add_next_parameters(options)
109
+ opts = []
110
+ opts << "&next=#{CGI.escape(options[:next])}" if options[:next]
111
+ opts << "&next_cancel=#{CGI.escape(options[:next_cancel])}" if options[:next_cancel]
112
+ opts
113
+ end
114
+
115
+ def login_url_optional_parameters(options)
116
+ # It is important that unused options are omitted as stuff like &canvas=false will still display the canvas.
117
+ optional_parameters = []
118
+ optional_parameters += add_next_parameters(options)
119
+ optional_parameters << "&skipcookie=true" if options[:skip_cookie]
120
+ optional_parameters << "&hide_checkbox=true" if options[:hide_checkbox]
121
+ optional_parameters << "&canvas=true" if options[:canvas]
122
+ optional_parameters.join
123
+ end
124
+
125
+ def default_login_url_options
126
+ {}
127
+ end
128
+
129
+ def initialize(api_key, secret_key)
130
+ @api_key = api_key
131
+ @secret_key = secret_key
132
+ end
133
+
134
+ def secret_for_method(method_name)
135
+ @secret_key
136
+ end
137
+
138
+ def auth_token
139
+ @auth_token ||= post 'facebook.auth.createToken'
140
+ end
141
+
142
+ def infinite?
143
+ @expires == 0
144
+ end
145
+
146
+ def expired?
147
+ @expires.nil? || (!infinite? && Time.at(@expires) <= Time.now)
148
+ end
149
+
150
+ def secured?
151
+ !@session_key.nil? && !expired?
152
+ end
153
+
154
+ def secure!
155
+ response = post 'facebook.auth.getSession', :auth_token => auth_token
156
+ secure_with!(response['session_key'], response['uid'], response['expires'], response['secret'])
157
+ end
158
+
159
+ def secure_with!(session_key, uid = nil, expires = nil, secret_from_session = nil)
160
+ @session_key = session_key
161
+ @uid = uid ? Integer(uid) : post('facebook.users.getLoggedInUser', :session_key => session_key)
162
+ @expires = Integer(expires)
163
+ @secret_from_session = secret_from_session
164
+ end
165
+
166
+ def fql_query(query, format = 'XML')
167
+ post('facebook.fql.query', :query => query, :format => format) do |response|
168
+ type = response.shift
169
+ return [] if type.nil?
170
+ response.shift.map do |hash|
171
+ case type
172
+ when 'user'
173
+ user = User.new
174
+ user.session = self
175
+ user.populate_from_hash!(hash)
176
+ user
177
+ when 'photo'
178
+ Photo.from_hash(hash)
179
+ when 'event_member'
180
+ Event::Attendance.from_hash(hash)
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ def user
187
+ @user ||= User.new(uid, self)
188
+ end
189
+
190
+ #
191
+ # This one has so many parameters, a Hash seemed cleaner than a long param list. Options can be:
192
+ # :uid => Filter by events associated with a user with this uid
193
+ # :eids => Filter by this list of event ids. This is a comma-separated list of eids.
194
+ # :start_time => Filter with this UTC as lower bound. A missing or zero parameter indicates no lower bound. (Time or Integer)
195
+ # :end_time => Filter with this UTC as upper bound. A missing or zero parameter indicates no upper bound. (Time or Integer)
196
+ # :rsvp_status => Filter by this RSVP status.
197
+ def events(options = {})
198
+ @events ||= post('facebook.events.get', options) do |response|
199
+ response.map do |hash|
200
+ Event.from_hash(hash)
201
+ end
202
+ end
203
+ end
204
+
205
+ def event_members(eid)
206
+ @members ||= post('facebook.events.getMembers', :eid => eid) do |response|
207
+ response.map do |attendee_hash|
208
+ Event::Attendance.from_hash(attendee_hash)
209
+ end
210
+ end
211
+ end
212
+
213
+ def pages(options = {})
214
+ raise ArgumentError, 'fields option is mandatory' unless options.has_key?(:fields)
215
+ @pages ||= {}
216
+ @pages[options] ||= post('facebook.pages.getInfo', options) do |response|
217
+ response.map do |hash|
218
+ Page.from_hash(hash)
219
+ end
220
+ end
221
+ end
222
+
223
+ #
224
+ # Returns a proxy object for handling calls to Facebook cached items
225
+ # such as images and FBML ref handles
226
+ def server_cache
227
+ Facebooker::ServerCache.new(self)
228
+ end
229
+
230
+ #
231
+ # Returns a proxy object for handling calls to the Facebook Data API
232
+ def data
233
+ Facebooker::Data.new(self)
234
+ end
235
+
236
+ def admin
237
+ Facebooker::Admin.new(self)
238
+ end
239
+
240
+ #
241
+ # Given an array like:
242
+ # [[userid, otheruserid], [yetanotherid, andanotherid]]
243
+ # returns a Hash indicating friendship of those pairs:
244
+ # {[userid, otheruserid] => true, [yetanotherid, andanotherid] => false}
245
+ # if one of the Hash values is nil, it means the facebook platform's answer is "I don't know"
246
+ def check_friendship(array_of_pairs_of_users)
247
+ uids1 = []
248
+ uids2 = []
249
+ array_of_pairs_of_users.each do |pair|
250
+ uids1 = pair.first
251
+ uids2 = pair.last
252
+ end
253
+ post('facebook.friends.areFriends', :uids1 => uids1, :uids2 => uids2)
254
+ end
255
+
256
+ def get_photos(pids = nil, subj_id = nil, aid = nil)
257
+ if [subj_id, pids, aid].all? {|arg| arg.nil?}
258
+ raise ArgumentError, "Can't get a photo without a picture, album or subject ID"
259
+ end
260
+ @photos = post('facebook.photos.get', :subj_id => subj_id, :pids => pids, :aid => aid ) do |response|
261
+ response.map do |hash|
262
+ Photo.from_hash(hash)
263
+ end
264
+ end
265
+ end
266
+
267
+ def get_albums(aids)
268
+ @albums = post('facebook.photos.getAlbums', :aids => aids) do |response|
269
+ response.map do |hash|
270
+ Album.from_hash(hash)
271
+ end
272
+ end
273
+ end
274
+
275
+ def get_tags(pids)
276
+ @tags = post('facebook.photos.getTags', :pids => pids) do |response|
277
+ response.map do |hash|
278
+ Tag.from_hash(hash)
279
+ end
280
+ end
281
+ end
282
+
283
+ def add_tags(pid, x, y, tag_uid = nil, tag_text = nil )
284
+ if [tag_uid, tag_text].all? {|arg| arg.nil?}
285
+ raise ArgumentError, "Must enter a name or string for this tag"
286
+ end
287
+ @tags = post('facebook.photos.addTag', :pid => pid, :tag_uid => tag_uid, :tag_text => tag_text, :x => x, :y => y )
288
+ end
289
+
290
+ def send_notification(user_ids, fbml, email_fbml = nil)
291
+ params = {:notification => fbml, :to_ids => user_ids.map{ |id| User.cast_to_facebook_id(id)}.join(',')}
292
+ if email_fbml
293
+ params[:email] = email_fbml
294
+ end
295
+ params[:type]="user_to_user"
296
+ # if there is no uid, this is an announcement
297
+ unless uid?
298
+ params[:type]="app_to_user"
299
+ end
300
+
301
+ post 'facebook.notifications.send', params,uid?
302
+ end
303
+
304
+ ##
305
+ # Register a template bundle with Facebook.
306
+ # returns the template id to use to send using this template
307
+ def register_template_bundle(one_line_story_templates,short_story_templates=nil,full_story_template=nil)
308
+
309
+ if !one_line_story_templates.is_a?(Array)
310
+ one_line_story_templates = [one_line_story_templates]
311
+ end
312
+ parameters = {:one_line_story_templates=>one_line_story_templates.to_json}
313
+
314
+
315
+ if !short_story_templates.blank?
316
+ short_story_templates = [short_story_templates] unless short_story_templates.is_a?(Array)
317
+ parameters[:short_story_templates]= short_story_templates.to_json
318
+ end
319
+
320
+ if !full_story_template.blank?
321
+ parameters[:full_story_template]= full_story_template.to_json
322
+ end
323
+ post("facebook.feed.registerTemplateBundle", parameters,false)
324
+ end
325
+
326
+ ##
327
+ # publish a previously rendered template bundle
328
+ # see http://wiki.developers.facebook.com/index.php/Feed.publishUserAction
329
+ #
330
+ def publish_user_action(bundle_id,data={},target_ids=nil,body_general=nil)
331
+ parameters={:template_bundle_id=>bundle_id,:template_data=>data.to_json}
332
+ parameters[:target_ids] = target_ids unless target_ids.blank?
333
+ parameters[:body_general] = body_general unless body_general.blank?
334
+ post("facebook.feed.publishUserAction", parameters)
335
+ end
336
+
337
+
338
+ ##
339
+ # Send email to as many as 100 users at a time
340
+ def send_email(user_ids, subject, text, fbml = nil)
341
+ user_ids = Array(user_ids)
342
+ params = {:fbml => fbml, :recipients => user_ids.map{ |id| User.cast_to_facebook_id(id)}.join(','), :text => text, :subject => subject}
343
+ post 'facebook.notifications.sendEmail', params
344
+ end
345
+
346
+ # Only serialize the bare minimum to recreate the session.
347
+ def marshal_load(variables)#:nodoc:
348
+ fields_to_serialize.each_with_index{|field, index| instance_variable_set_value(field, variables[index])}
349
+ end
350
+
351
+ # Only serialize the bare minimum to recreate the session.
352
+ def marshal_dump#:nodoc:
353
+ fields_to_serialize.map{|field| instance_variable_value(field)}
354
+ end
355
+
356
+ # Only serialize the bare minimum to recreate the session.
357
+ def to_yaml( opts = {} )
358
+ YAML::quick_emit(self.object_id, opts) do |out|
359
+ out.map(taguri) do |map|
360
+ fields_to_serialize.each do |field|
361
+ map.add(field, instance_variable_value(field))
362
+ end
363
+ end
364
+ end
365
+ end
366
+
367
+ def instance_variable_set_value(field, value)
368
+ self.instance_variable_set("@#{field}", value)
369
+ end
370
+
371
+ def instance_variable_value(field)
372
+ self.instance_variable_get("@#{field}")
373
+ end
374
+
375
+ def fields_to_serialize
376
+ %w(session_key uid expires secret_from_session auth_token api_key secret_key)
377
+ end
378
+
379
+ class Desktop < Session
380
+ def login_url
381
+ super + "&auth_token=#{auth_token}"
382
+ end
383
+
384
+ def secret_for_method(method_name)
385
+ secret = auth_request_methods.include?(method_name) ? super : @secret_from_session
386
+ secret
387
+ end
388
+
389
+ def post(method, params = {},use_session=false)
390
+ if method == 'facebook.profile.getFBML' || method == 'facebook.profile.setFBML'
391
+ raise NonSessionUser.new("User #{@uid} is not the logged in user.") unless @uid == params[:uid]
392
+ end
393
+ super
394
+ end
395
+ private
396
+ def auth_request_methods
397
+ ['facebook.auth.getSession', 'facebook.auth.createToken']
398
+ end
399
+ end
400
+
401
+ def batch_request?
402
+ @batch_request
403
+ end
404
+
405
+ def add_to_batch(req,&proc)
406
+ batch_request = BatchRequest.new(req,proc)
407
+ Thread.current[:facebooker_current_batch_queue]<<batch_request
408
+ batch_request
409
+ end
410
+
411
+ # Submit the enclosed requests for this session inside a batch
412
+ #
413
+ # All requests will be sent to Facebook at the end of the block
414
+ # each method inside the block will return a proxy object
415
+ # attempting to access the proxy before the end of the block will yield an exception
416
+ #
417
+ # For Example:
418
+ #
419
+ # facebook_session.batch do
420
+ # @send_result = facebook_session.send_notification([12451752],"Woohoo")
421
+ # @albums = facebook_session.user.albums
422
+ # end
423
+ # puts @albums.first.inspect
424
+ #
425
+ # is valid, however
426
+ #
427
+ # facebook_session.batch do
428
+ # @send_result = facebook_session.send_notification([12451752],"Woohoo")
429
+ # @albums = facebook_session.user.albums
430
+ # puts @albums.first.inspect
431
+ # end
432
+ #
433
+ # will raise Facebooker::BatchRequest::UnexecutedRequest
434
+ #
435
+ # If an exception is raised while processing the result, that exception will be
436
+ # re-raised on the next access to that object or when exception_raised? is called
437
+ #
438
+ # for example, if the send_notification resulted in TooManyUserCalls being raised,
439
+ # calling
440
+ # @send_result.exception_raised?
441
+ # would re-raise that exception
442
+ # if there was an error retrieving the albums, it would be re-raised when
443
+ # @albums.first
444
+ # is called
445
+ #
446
+ def batch(serial_only=false)
447
+ @batch_request=true
448
+ Thread.current[:facebooker_current_batch_queue]=[]
449
+ yield
450
+ # Set the batch request to false so that post will execute the batch job
451
+ @batch_request=false
452
+ BatchRun.current_batch=Thread.current[:facebooker_current_batch_queue]
453
+ post("facebook.batch.run",:method_feed=>BatchRun.current_batch.map{|q| q.uri}.to_json,:serial_only=>serial_only.to_s)
454
+ ensure
455
+ @batch_request=false
456
+ BatchRun.current_batch=nil
457
+ end
458
+
459
+ def post_without_logging(method, params = {}, use_session_key = true, &proc)
460
+ add_facebook_params(params, method)
461
+ use_session_key && @session_key && params[:session_key] ||= @session_key
462
+ final_params=params.merge(:sig => signature_for(params))
463
+ if batch_request?
464
+ add_to_batch(final_params,&proc)
465
+ else
466
+ result = service.post(final_params)
467
+ result = yield result if block_given?
468
+ result
469
+ end
470
+ end
471
+
472
+ def post(method, params = {}, use_session_key = true, &proc)
473
+ if batch_request?
474
+ post_without_logging(method, params, use_session_key, &proc)
475
+ else
476
+ Logging.log_fb_api(method, params) do
477
+ post_without_logging(method, params, use_session_key, &proc)
478
+ end
479
+ end
480
+ end
481
+
482
+ def post_file(method, params = {})
483
+ Logging.log_fb_api(method, params) do
484
+ add_facebook_params(params, method)
485
+ @session_key && params[:session_key] ||= @session_key
486
+ service.post_file(params.merge(:sig => signature_for(params.reject{|key, value| key.nil?})))
487
+ end
488
+ end
489
+
490
+
491
+ def self.configuration_file_path
492
+ @configuration_file_path || File.expand_path("~/.facebookerrc")
493
+ end
494
+
495
+ def self.configuration_file_path=(path)
496
+ @configuration_file_path = path
497
+ end
498
+
499
+ private
500
+ def add_facebook_params(hash, method)
501
+ hash[:method] = method
502
+ hash[:api_key] = @api_key
503
+ hash[:call_id] = Time.now.to_f.to_s unless method == 'facebook.auth.getSession'
504
+ hash[:v] = "1.0"
505
+ end
506
+
507
+ # This ultimately delgates to the adapter
508
+ def self.extract_key_from_environment(key_name)
509
+ Facebooker.send(key_name.to_s + "_key") rescue nil
510
+ end
511
+
512
+ def self.extract_key_from_configuration_file(key_name)
513
+ read_configuration_file[key_name]
514
+ end
515
+
516
+ def self.report_inability_to_find_key(key_name)
517
+ raise ConfigurationMissing, "Could not find configuration information for #{key_name}"
518
+ end
519
+
520
+ def self.read_configuration_file
521
+ eval(File.read(configuration_file_path))
522
+ end
523
+
524
+ def service
525
+ @service ||= Service.new(Facebooker.api_server_base, Facebooker.api_rest_path, @api_key)
526
+ end
527
+
528
+ def uid
529
+ @uid || (secure!; @uid)
530
+ end
531
+
532
+ def uid?
533
+ ! @uid.nil?
534
+ end
535
+
536
+ def signature_for(params)
537
+ raw_string = params.inject([]) do |collection, pair|
538
+ collection << pair.join("=")
539
+ collection
540
+ end.sort.join
541
+ Digest::MD5.hexdigest([raw_string, secret_for_method(params[:method])].join)
542
+ end
543
+ end
544
+
545
+ class CanvasSession < Session
546
+ def default_login_url_options
547
+ {:canvas => true}
548
+ end
549
+ end
550
+ end