facebooker 1.0.13 → 1.0.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/Manifest.txt +29 -16
  2. data/Rakefile +9 -2
  3. data/generators/facebook/templates/public/javascripts/facebooker.js +1 -8
  4. data/init.rb +3 -1
  5. data/lib/facebooker.rb +4 -2
  6. data/lib/facebooker/adapters/facebook_adapter.rb +4 -0
  7. data/lib/facebooker/admin.rb +2 -2
  8. data/lib/facebooker/mobile.rb +20 -0
  9. data/lib/facebooker/mock/service.rb +50 -0
  10. data/lib/facebooker/mock/session.rb +18 -0
  11. data/lib/facebooker/model.rb +3 -3
  12. data/lib/facebooker/models/applicationrestrictions.rb +10 -0
  13. data/lib/facebooker/models/user.rb +35 -3
  14. data/lib/facebooker/models/user.rb.orig +396 -0
  15. data/lib/facebooker/models/user.rb.rej +17 -0
  16. data/lib/facebooker/models/video.rb +9 -0
  17. data/lib/facebooker/parser.rb +22 -1
  18. data/lib/facebooker/rails/controller.rb +34 -12
  19. data/lib/facebooker/rails/cucumber.rb +28 -0
  20. data/lib/facebooker/rails/cucumber/world.rb +46 -0
  21. data/lib/facebooker/rails/facebook_pretty_errors.rb +19 -11
  22. data/lib/facebooker/rails/helpers.rb +203 -122
  23. data/lib/facebooker/rails/helpers/fb_connect.rb +56 -5
  24. data/lib/facebooker/rails/integration_session.rb +38 -0
  25. data/lib/facebooker/rails/publisher.rb +15 -2
  26. data/lib/facebooker/rails/test_helpers.rb +11 -26
  27. data/lib/facebooker/service.rb +6 -3
  28. data/lib/facebooker/session.rb +24 -2
  29. data/lib/facebooker/session.rb.orig +564 -0
  30. data/lib/facebooker/session.rb.rej +29 -0
  31. data/lib/facebooker/version.rb +1 -1
  32. data/lib/tasks/tunnel.rake +2 -2
  33. data/test/{adapters_test.rb → facebooker/adapters_test.rb} +2 -4
  34. data/test/{facebook_admin_test.rb → facebooker/admin_test.rb} +2 -2
  35. data/test/{batch_request_test.rb → facebooker/batch_request_test.rb} +3 -2
  36. data/test/{facebook_data_test.rb → facebooker/data_test.rb} +2 -2
  37. data/test/{logging_test.rb → facebooker/logging_test.rb} +3 -3
  38. data/test/facebooker/mobile_test.rb +45 -0
  39. data/test/{model_test.rb → facebooker/model_test.rb} +36 -4
  40. data/test/{event_test.rb → facebooker/models/event_test.rb} +2 -2
  41. data/test/{user_test.rb → facebooker/models/user_test.rb} +10 -5
  42. data/test/{publisher_test.rb → facebooker/rails/publisher_test.rb} +12 -18
  43. data/test/{rails_integration_test.rb → facebooker/rails_integration_test.rb} +347 -272
  44. data/test/{facebook_cache_test.rb → facebooker/server_cache_test.rb} +1 -1
  45. data/test/{session_test.rb → facebooker/session_test.rb} +3 -2
  46. data/test/facebooker_test.rb +19 -1
  47. data/test/{http_multipart_post_test.rb → net/http_multipart_post_test.rb} +2 -4
  48. data/test/rails_test_helper.rb +11 -0
  49. data/test/test_helper.rb +8 -2
  50. metadata +45 -32
  51. data/lib/facebooker/rails/facebook_asset_path.rb +0 -18
@@ -11,27 +11,78 @@ module Facebooker
11
11
  end
12
12
  end
13
13
 
14
- def init_fb_connect(*required_features)
14
+ def init_fb_connect(*required_features,&proc)
15
+ additions = ""
16
+ if block_given?
17
+ additions = capture(&proc)
18
+ end
15
19
  init_string = "FB.Facebook.init('#{Facebooker.api_key}','/xd_receiver.html');"
16
20
  unless required_features.blank?
17
21
  init_string = <<-FBML
18
22
  Element.observe(window,'load', function() {
19
23
  FB_RequireFeatures(#{required_features.to_json}, function() {
20
24
  #{init_string}
25
+ #{additions}
21
26
  });
22
27
  });
23
28
  FBML
24
29
  end
25
- javascript_tag init_string
30
+ if block_given?
31
+ concat javascript_tag(init_string)
32
+ else
33
+ javascript_tag init_string
34
+ end
26
35
  end
27
-
28
- def fb_login_button(callback=nil)
29
- content_tag("fb:login-button",nil,(callback.nil? ? {} : {:onlogin=>callback}))
36
+
37
+ # Render an <fb:login-button> element
38
+ #
39
+ # ==== Examples
40
+ #
41
+ # <%= fb_login_button%>
42
+ # => <fb:login-button></fb:login-button>
43
+ #
44
+ # Specifying a javascript callback
45
+ #
46
+ # <%= fb_login_button 'update_something();'%>
47
+ # => <fb:login-button onlogin='update_something();'></fb:login-button>
48
+ #
49
+ # Adding options <em>See:</em> http://wiki.developers.facebook.com/index.php/Fb:login-button
50
+ #
51
+ # <%= fb_login_button 'update_something();', :size => :small, :background => :dark%>
52
+ # => <fb:login-button background='dark' onlogin='update_something();' size='small'></fb:login-button>
53
+ #
54
+ def fb_login_button(*args)
55
+
56
+ callback = args.first
57
+ options = args.second || {}
58
+ options.merge!(:onlogin=>callback)if callback
59
+
60
+ content_tag("fb:login-button",nil, options)
61
+ end
62
+ def fb_login_and_redirect(url)
63
+ js = update_page do |page|
64
+ page.redirect_to url
65
+ end
66
+ content_tag("fb:login-button",nil,:onlogin=>js)
30
67
  end
31
68
 
32
69
  def fb_unconnected_friends_count
33
70
  content_tag "fb:unconnected-friends-count",nil
34
71
  end
72
+
73
+ def fb_logout_link(text,url)
74
+ js = update_page do |page|
75
+ page.call "FB.Connect.logoutAndRedirect",url
76
+ end
77
+ link_to_function text, js
78
+ end
79
+
80
+ def fb_user_action(action)
81
+ update_page do |page|
82
+ page.call "FB.Connect.showFeedDialog",action.template_id,action.data,action.target_ids,action.body_general,nil,"FB.RequireConnect.promptConnect"
83
+ end
84
+ end
85
+
35
86
  end
36
87
  end
37
88
  end
@@ -0,0 +1,38 @@
1
+ require 'action_controller/integration'
2
+
3
+ class Facebooker::Rails::IntegrationSession < ActionController::Integration::Session
4
+ include Facebooker::Rails::TestHelpers
5
+ attr_accessor :default_request_params, :canvas
6
+
7
+ def process(method, path, parameters = nil, headers = nil)
8
+ if canvas
9
+ parameters = facebook_params(@default_request_params.merge(parameters || {}))
10
+ end
11
+ super method, path, parameters, headers
12
+ end
13
+
14
+ def reset!
15
+ self.default_request_params = {:fb_sig_in_canvas => '1'}.with_indifferent_access
16
+ self.canvas = true
17
+ super
18
+ end
19
+
20
+ def get(path, parameters = nil, headers = nil)
21
+ if canvas
22
+ post path, (parameters || {}).merge('fb_sig_request_method' => 'GET'), headers
23
+ else
24
+ super path, parameters, headers
25
+ end
26
+ end
27
+
28
+ %w(put delete).each do |method|
29
+ define_method method do |*args|
30
+ if canvas
31
+ path, parameters, headers = *args
32
+ post path, (parameters || {}).merge('_method' => method.upcase), headers
33
+ else
34
+ super *args
35
+ end
36
+ end
37
+ end
38
+ end
@@ -312,10 +312,23 @@ module Facebooker
312
312
  end
313
313
  end
314
314
 
315
- def image(src,target)
316
- {:src=>image_path(src),:href=> target.respond_to?(:to_str) ? target : url_for(target)}
315
+ # work around the fact that facebook cares about the order of the keys in the hash
316
+ class ImageHolder
317
+ attr_accessor :src,:href
318
+ def initialize(src,href)
319
+ self.src=src
320
+ self.href=href
321
+ end
322
+
323
+ def to_json(*args)
324
+ "{\"src\":#{src.to_json}, \"href\":#{href.to_json}}"
325
+ end
317
326
  end
318
327
 
328
+ def image(src,target)
329
+ ImageHolder.new(image_path(src),target.respond_to?(:to_str) ? target : url_for(target))
330
+ end
331
+
319
332
  def action_link(text,target)
320
333
  {:text=>text, :href=>target}
321
334
  end
@@ -22,25 +22,22 @@ module Facebooker
22
22
  def facebook_put(path,params={})
23
23
  facebook_verb(:put,path,params)
24
24
  end
25
+
25
26
  def facebook_delete(path,params={})
26
27
  facebook_verb(:delete,path,params)
27
28
  end
28
29
 
29
30
  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
31
+ send verb, path, facebook_params(params).reverse_merge(:canvas => true)
35
32
  end
36
33
 
37
- def facebook_parameters(overrides=nil)
38
- overrides ||= {}
39
- params = default_facebook_parameters.merge(overrides)
40
- params.merge(:fb_sig => generate_signature(params.stringify_keys))
34
+ def facebook_params(params = {})
35
+ params = default_facebook_parameters.with_indifferent_access.merge(params || {})
36
+ sig = generate_signature params
37
+ params.merge(:fb_sig => sig)
41
38
  end
42
39
 
43
- private
40
+ private
44
41
 
45
42
  def default_facebook_parameters
46
43
  {
@@ -53,28 +50,16 @@ module Facebooker
53
50
  }
54
51
  end
55
52
 
56
- def facebook_params(params)
57
- params.inject({}) do |fb_params, pair|
58
- unless pair.first.to_s.match(/^fb_sig_/).nil?
59
- fb_params[pair.first] = pair.last
60
- end
61
- fb_params
62
- end
63
- end
64
-
65
53
  def facebook_redirect_url
66
54
  match = @response.body.match(/<fb:redirect url="([^"]+)"/)
67
55
  match.nil? ? nil : match.captures[0]
68
56
  end
69
57
 
70
- def generate_signature(facebook_params)
71
- facebook_sig_params = facebook_params.inject({}) do |collection, pair|
72
- collection[pair.first.sub(/^fb_sig_/, '')] = pair.last
73
- collection
58
+ def generate_signature(params)
59
+ facebook_params = params.select { |param,_| param =~ /^fb_sig_/ }.map do |param, value|
60
+ [param.sub(/^fb_sig_/, ''), value].join('=')
74
61
  end
75
-
76
- raw_string = facebook_sig_params.map{ |*args| args.join('=') }.sort.join
77
- Digest::MD5.hexdigest([raw_string, Facebooker::Session.secret_key].join)
62
+ Digest::MD5.hexdigest([facebook_params.sort.join, Facebooker::Session.secret_key].join)
78
63
  end
79
64
 
80
65
  end
@@ -58,12 +58,15 @@ module Facebooker
58
58
  end
59
59
 
60
60
  def post_file(params)
61
- Parser.parse(params[:method], post_multipart_form(url, params))
61
+ service_url = url(params.delete(:base))
62
+ result = post_multipart_form(service_url, params)
63
+ Parser.parse(params[:method], result)
62
64
  end
63
65
 
64
66
  private
65
- def url
66
- URI.parse('http://'+ @api_base + @api_path)
67
+ def url(base = nil)
68
+ base ||= @api_base
69
+ URI.parse('http://'+ base + @api_path)
67
70
  end
68
71
 
69
72
  # Net::HTTP::MultipartPostFile
@@ -94,6 +94,19 @@ module Facebooker
94
94
  "#{Facebooker.install_url_base(@api_key)}#{install_url_optional_parameters(options)}"
95
95
  end
96
96
 
97
+ # The url to get user to approve extended permissions
98
+ # http://wiki.developers.facebook.com/index.php/Extended_permission
99
+ #
100
+ # permissions:
101
+ # * email
102
+ # * offline_access
103
+ # * status_update
104
+ # * photo_upload
105
+ # * video_upload
106
+ # * create_listing
107
+ # * create_event
108
+ # * rsvp_event
109
+ # * sms
97
110
  def permission_url(permission,options={})
98
111
  options = default_login_url_options.merge(options)
99
112
  "http://#{Facebooker.www_server_base_url}/authorize.php?api_key=#{@api_key}&v=1.0&ext_perm=#{permission}#{install_url_optional_parameters(options)}"
@@ -176,6 +189,10 @@ module Facebooker
176
189
  user
177
190
  when 'photo'
178
191
  Photo.from_hash(hash)
192
+ when 'page'
193
+ Page.from_hash(hash)
194
+ when 'page_admin'
195
+ Page.from_hash(hash)
179
196
  when 'event_member'
180
197
  Event::Attendance.from_hash(hash)
181
198
  else
@@ -256,6 +273,10 @@ module Facebooker
256
273
  Facebooker::Admin.new(self)
257
274
  end
258
275
 
276
+ def mobile
277
+ Facebooker::Mobile.new(self)
278
+ end
279
+
259
280
  #
260
281
  # Given an array like:
261
282
  # [[userid, otheruserid], [yetanotherid, andanotherid]]
@@ -492,10 +513,11 @@ module Facebooker
492
513
  end
493
514
 
494
515
  def post_file(method, params = {})
516
+ base = params.delete(:base)
495
517
  Logging.log_fb_api(method, params) do
496
518
  add_facebook_params(params, method)
497
- @session_key && params[:session_key] ||= @session_key
498
- service.post_file(params.merge(:sig => signature_for(params.reject{|key, value| key.nil?})))
519
+ @session_key && params[:session_key] ||= @session_key unless params[:uid]
520
+ service.post_file(params.merge(:base => base, :sig => signature_for(params.reject{|key, value| key.nil?})))
499
521
  end
500
522
  end
501
523
 
@@ -0,0 +1,564 @@
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 users_standard(user_ids, fields=[])
214
+ post("facebook.users.getStandardInfo",:uids=>user_ids.join(","),:fields=>User.user_fields(fields)) do |users|
215
+ users.map { |u| User.new(u)}
216
+ end
217
+ end
218
+
219
+ def users(user_ids, fields=[])
220
+ post("facebook.users.getInfo",:uids=>user_ids.join(","),:fields=>User.standard_fields(fields)) do |users|
221
+ users.map { |u| User.new(u)}
222
+ end
223
+ end
224
+
225
+ def pages(options = {})
226
+ raise ArgumentError, 'fields option is mandatory' unless options.has_key?(:fields)
227
+ @pages ||= {}
228
+ @pages[options] ||= post('facebook.pages.getInfo', options) do |response|
229
+ response.map do |hash|
230
+ Page.from_hash(hash)
231
+ end
232
+ end
233
+ end
234
+
235
+ #
236
+ # Returns a proxy object for handling calls to Facebook cached items
237
+ # such as images and FBML ref handles
238
+ def server_cache
239
+ Facebooker::ServerCache.new(self)
240
+ end
241
+
242
+ #
243
+ # Returns a proxy object for handling calls to the Facebook Data API
244
+ def data
245
+ Facebooker::Data.new(self)
246
+ end
247
+
248
+ def admin
249
+ Facebooker::Admin.new(self)
250
+ end
251
+
252
+ #
253
+ # Given an array like:
254
+ # [[userid, otheruserid], [yetanotherid, andanotherid]]
255
+ # returns a Hash indicating friendship of those pairs:
256
+ # {[userid, otheruserid] => true, [yetanotherid, andanotherid] => false}
257
+ # if one of the Hash values is nil, it means the facebook platform's answer is "I don't know"
258
+ def check_friendship(array_of_pairs_of_users)
259
+ uids1 = []
260
+ uids2 = []
261
+ array_of_pairs_of_users.each do |pair|
262
+ uids1 << pair.first
263
+ uids2 << pair.last
264
+ end
265
+ post('facebook.friends.areFriends', :uids1 => uids1.join(','), :uids2 => uids2.join(','))
266
+ end
267
+
268
+ def get_photos(pids = nil, subj_id = nil, aid = nil)
269
+ if [subj_id, pids, aid].all? {|arg| arg.nil?}
270
+ raise ArgumentError, "Can't get a photo without a picture, album or subject ID"
271
+ end
272
+ @photos = post('facebook.photos.get', :subj_id => subj_id, :pids => pids, :aid => aid ) do |response|
273
+ response.map do |hash|
274
+ Photo.from_hash(hash)
275
+ end
276
+ end
277
+ end
278
+
279
+ def get_albums(aids)
280
+ @albums = post('facebook.photos.getAlbums', :aids => aids) do |response|
281
+ response.map do |hash|
282
+ Album.from_hash(hash)
283
+ end
284
+ end
285
+ end
286
+
287
+ def get_tags(pids)
288
+ @tags = post('facebook.photos.getTags', :pids => pids) do |response|
289
+ response.map do |hash|
290
+ Tag.from_hash(hash)
291
+ end
292
+ end
293
+ end
294
+
295
+ def add_tags(pid, x, y, tag_uid = nil, tag_text = nil )
296
+ if [tag_uid, tag_text].all? {|arg| arg.nil?}
297
+ raise ArgumentError, "Must enter a name or string for this tag"
298
+ end
299
+ @tags = post('facebook.photos.addTag', :pid => pid, :tag_uid => tag_uid, :tag_text => tag_text, :x => x, :y => y )
300
+ end
301
+
302
+ def send_notification(user_ids, fbml, email_fbml = nil)
303
+ params = {:notification => fbml, :to_ids => user_ids.map{ |id| User.cast_to_facebook_id(id)}.join(',')}
304
+ if email_fbml
305
+ params[:email] = email_fbml
306
+ end
307
+ params[:type]="user_to_user"
308
+ # if there is no uid, this is an announcement
309
+ unless uid?
310
+ params[:type]="app_to_user"
311
+ end
312
+
313
+ post 'facebook.notifications.send', params,uid?
314
+ end
315
+
316
+ ##
317
+ # Register a template bundle with Facebook.
318
+ # returns the template id to use to send using this template
319
+ def register_template_bundle(one_line_story_templates,short_story_templates=nil,full_story_template=nil, action_links=nil)
320
+ if !one_line_story_templates.is_a?(Array)
321
+ one_line_story_templates = [one_line_story_templates]
322
+ end
323
+ parameters = {:one_line_story_templates=>one_line_story_templates.to_json}
324
+
325
+ if !action_links.blank?
326
+ parameters[:action_links] = action_links.to_json
327
+ end
328
+
329
+ if !short_story_templates.blank?
330
+ short_story_templates = [short_story_templates] unless short_story_templates.is_a?(Array)
331
+ parameters[:short_story_templates]= short_story_templates.to_json
332
+ end
333
+
334
+ if !full_story_template.blank?
335
+ parameters[:full_story_template]= full_story_template.to_json
336
+ end
337
+ post("facebook.feed.registerTemplateBundle", parameters,false)
338
+ end
339
+
340
+ ##
341
+ # publish a previously rendered template bundle
342
+ # see http://wiki.developers.facebook.com/index.php/Feed.publishUserAction
343
+ #
344
+ def publish_user_action(bundle_id,data={},target_ids=nil,body_general=nil)
345
+ parameters={:template_bundle_id=>bundle_id,:template_data=>data.to_json}
346
+ parameters[:target_ids] = target_ids unless target_ids.blank?
347
+ parameters[:body_general] = body_general unless body_general.blank?
348
+ post("facebook.feed.publishUserAction", parameters)
349
+ end
350
+
351
+
352
+ ##
353
+ # Send email to as many as 100 users at a time
354
+ def send_email(user_ids, subject, text, fbml = nil)
355
+ user_ids = Array(user_ids)
356
+ params = {:fbml => fbml, :recipients => user_ids.map{ |id| User.cast_to_facebook_id(id)}.join(','), :text => text, :subject => subject}
357
+ post 'facebook.notifications.sendEmail', params
358
+ end
359
+
360
+ # Only serialize the bare minimum to recreate the session.
361
+ def marshal_load(variables)#:nodoc:
362
+ fields_to_serialize.each_with_index{|field, index| instance_variable_set_value(field, variables[index])}
363
+ end
364
+
365
+ # Only serialize the bare minimum to recreate the session.
366
+ def marshal_dump#:nodoc:
367
+ fields_to_serialize.map{|field| instance_variable_value(field)}
368
+ end
369
+
370
+ # Only serialize the bare minimum to recreate the session.
371
+ def to_yaml( opts = {} )
372
+ YAML::quick_emit(self.object_id, opts) do |out|
373
+ out.map(taguri) do |map|
374
+ fields_to_serialize.each do |field|
375
+ map.add(field, instance_variable_value(field))
376
+ end
377
+ end
378
+ end
379
+ end
380
+
381
+ def instance_variable_set_value(field, value)
382
+ self.instance_variable_set("@#{field}", value)
383
+ end
384
+
385
+ def instance_variable_value(field)
386
+ self.instance_variable_get("@#{field}")
387
+ end
388
+
389
+ def fields_to_serialize
390
+ %w(session_key uid expires secret_from_session auth_token api_key secret_key)
391
+ end
392
+
393
+ class Desktop < Session
394
+ def login_url
395
+ super + "&auth_token=#{auth_token}"
396
+ end
397
+
398
+ def secret_for_method(method_name)
399
+ secret = auth_request_methods.include?(method_name) ? super : @secret_from_session
400
+ secret
401
+ end
402
+
403
+ def post(method, params = {},use_session=false)
404
+ if method == 'facebook.profile.getFBML' || method == 'facebook.profile.setFBML'
405
+ raise NonSessionUser.new("User #{@uid} is not the logged in user.") unless @uid == params[:uid]
406
+ end
407
+ super
408
+ end
409
+ private
410
+ def auth_request_methods
411
+ ['facebook.auth.getSession', 'facebook.auth.createToken']
412
+ end
413
+ end
414
+
415
+ def batch_request?
416
+ @batch_request
417
+ end
418
+
419
+ def add_to_batch(req,&proc)
420
+ batch_request = BatchRequest.new(req,proc)
421
+ Thread.current[:facebooker_current_batch_queue]<<batch_request
422
+ batch_request
423
+ end
424
+
425
+ # Submit the enclosed requests for this session inside a batch
426
+ #
427
+ # All requests will be sent to Facebook at the end of the block
428
+ # each method inside the block will return a proxy object
429
+ # attempting to access the proxy before the end of the block will yield an exception
430
+ #
431
+ # For Example:
432
+ #
433
+ # facebook_session.batch do
434
+ # @send_result = facebook_session.send_notification([12451752],"Woohoo")
435
+ # @albums = facebook_session.user.albums
436
+ # end
437
+ # puts @albums.first.inspect
438
+ #
439
+ # is valid, however
440
+ #
441
+ # facebook_session.batch do
442
+ # @send_result = facebook_session.send_notification([12451752],"Woohoo")
443
+ # @albums = facebook_session.user.albums
444
+ # puts @albums.first.inspect
445
+ # end
446
+ #
447
+ # will raise Facebooker::BatchRequest::UnexecutedRequest
448
+ #
449
+ # If an exception is raised while processing the result, that exception will be
450
+ # re-raised on the next access to that object or when exception_raised? is called
451
+ #
452
+ # for example, if the send_notification resulted in TooManyUserCalls being raised,
453
+ # calling
454
+ # @send_result.exception_raised?
455
+ # would re-raise that exception
456
+ # if there was an error retrieving the albums, it would be re-raised when
457
+ # @albums.first
458
+ # is called
459
+ #
460
+ def batch(serial_only=false)
461
+ @batch_request=true
462
+ Thread.current[:facebooker_current_batch_queue]=[]
463
+ yield
464
+ # Set the batch request to false so that post will execute the batch job
465
+ @batch_request=false
466
+ BatchRun.current_batch=Thread.current[:facebooker_current_batch_queue]
467
+ post("facebook.batch.run",:method_feed=>BatchRun.current_batch.map{|q| q.uri}.to_json,:serial_only=>serial_only.to_s)
468
+ ensure
469
+ @batch_request=false
470
+ BatchRun.current_batch=nil
471
+ end
472
+
473
+ def post_without_logging(method, params = {}, use_session_key = true, &proc)
474
+ add_facebook_params(params, method)
475
+ use_session_key && @session_key && params[:session_key] ||= @session_key
476
+ final_params=params.merge(:sig => signature_for(params))
477
+ if batch_request?
478
+ add_to_batch(final_params,&proc)
479
+ else
480
+ result = service.post(final_params)
481
+ result = yield result if block_given?
482
+ result
483
+ end
484
+ end
485
+
486
+ def post(method, params = {}, use_session_key = true, &proc)
487
+ if batch_request?
488
+ post_without_logging(method, params, use_session_key, &proc)
489
+ else
490
+ Logging.log_fb_api(method, params) do
491
+ post_without_logging(method, params, use_session_key, &proc)
492
+ end
493
+ end
494
+ end
495
+
496
+ def post_file(method, params = {})
497
+ Logging.log_fb_api(method, params) do
498
+ add_facebook_params(params, method)
499
+ @session_key && params[:session_key] ||= @session_key
500
+ service.post_file(params.merge(:sig => signature_for(params.reject{|key, value| key.nil?})))
501
+ end
502
+ end
503
+
504
+
505
+ def self.configuration_file_path
506
+ @configuration_file_path || File.expand_path("~/.facebookerrc")
507
+ end
508
+
509
+ def self.configuration_file_path=(path)
510
+ @configuration_file_path = path
511
+ end
512
+
513
+ private
514
+ def add_facebook_params(hash, method)
515
+ hash[:method] = method
516
+ hash[:api_key] = @api_key
517
+ hash[:call_id] = Time.now.to_f.to_s unless method == 'facebook.auth.getSession'
518
+ hash[:v] = "1.0"
519
+ end
520
+
521
+ # This ultimately delgates to the adapter
522
+ def self.extract_key_from_environment(key_name)
523
+ Facebooker.send(key_name.to_s + "_key") rescue nil
524
+ end
525
+
526
+ def self.extract_key_from_configuration_file(key_name)
527
+ read_configuration_file[key_name]
528
+ end
529
+
530
+ def self.report_inability_to_find_key(key_name)
531
+ raise ConfigurationMissing, "Could not find configuration information for #{key_name}"
532
+ end
533
+
534
+ def self.read_configuration_file
535
+ eval(File.read(configuration_file_path))
536
+ end
537
+
538
+ def service
539
+ @service ||= Service.new(Facebooker.api_server_base, Facebooker.api_rest_path, @api_key)
540
+ end
541
+
542
+ def uid
543
+ @uid || (secure!; @uid)
544
+ end
545
+
546
+ def uid?
547
+ ! @uid.nil?
548
+ end
549
+
550
+ def signature_for(params)
551
+ raw_string = params.inject([]) do |collection, pair|
552
+ collection << pair.join("=")
553
+ collection
554
+ end.sort.join
555
+ Digest::MD5.hexdigest([raw_string, secret_for_method(params[:method])].join)
556
+ end
557
+ end
558
+
559
+ class CanvasSession < Session
560
+ def default_login_url_options
561
+ {:canvas => true}
562
+ end
563
+ end
564
+ end