facebooker 1.0.13 → 1.0.18

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 (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