facebooker-lite 1.0.67

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 (118) hide show
  1. data/.autotest +15 -0
  2. data/CHANGELOG.rdoc +24 -0
  3. data/COPYING.rdoc +19 -0
  4. data/Manifest.txt +114 -0
  5. data/README.rdoc +120 -0
  6. data/Rakefile +94 -0
  7. data/TODO.rdoc +4 -0
  8. data/facebooker.gemspec +42 -0
  9. data/generators/xd_receiver/templates/xd_receiver.html +10 -0
  10. data/generators/xd_receiver/templates/xd_receiver_ssl.html +10 -0
  11. data/generators/xd_receiver/xd_receiver_generator.rb +10 -0
  12. data/init.rb +25 -0
  13. data/install.rb +12 -0
  14. data/lib/facebooker.rb +261 -0
  15. data/lib/facebooker/adapters/adapter_base.rb +91 -0
  16. data/lib/facebooker/adapters/bebo_adapter.rb +77 -0
  17. data/lib/facebooker/adapters/facebook_adapter.rb +60 -0
  18. data/lib/facebooker/admin.rb +42 -0
  19. data/lib/facebooker/application.rb +37 -0
  20. data/lib/facebooker/attachment.rb +51 -0
  21. data/lib/facebooker/batch_request.rb +45 -0
  22. data/lib/facebooker/data.rb +57 -0
  23. data/lib/facebooker/feed.rb +78 -0
  24. data/lib/facebooker/logging.rb +44 -0
  25. data/lib/facebooker/mobile.rb +20 -0
  26. data/lib/facebooker/mock/service.rb +50 -0
  27. data/lib/facebooker/mock/session.rb +18 -0
  28. data/lib/facebooker/model.rb +139 -0
  29. data/lib/facebooker/models/affiliation.rb +10 -0
  30. data/lib/facebooker/models/album.rb +11 -0
  31. data/lib/facebooker/models/applicationproperties.rb +39 -0
  32. data/lib/facebooker/models/applicationrestrictions.rb +10 -0
  33. data/lib/facebooker/models/comment.rb +9 -0
  34. data/lib/facebooker/models/cookie.rb +10 -0
  35. data/lib/facebooker/models/education_info.rb +11 -0
  36. data/lib/facebooker/models/event.rb +28 -0
  37. data/lib/facebooker/models/family_relative_info.rb +7 -0
  38. data/lib/facebooker/models/friend_list.rb +16 -0
  39. data/lib/facebooker/models/group.rb +36 -0
  40. data/lib/facebooker/models/info_item.rb +10 -0
  41. data/lib/facebooker/models/info_section.rb +10 -0
  42. data/lib/facebooker/models/location.rb +8 -0
  43. data/lib/facebooker/models/message_thread.rb +89 -0
  44. data/lib/facebooker/models/notifications.rb +17 -0
  45. data/lib/facebooker/models/page.rb +46 -0
  46. data/lib/facebooker/models/photo.rb +19 -0
  47. data/lib/facebooker/models/tag.rb +12 -0
  48. data/lib/facebooker/models/user.rb +751 -0
  49. data/lib/facebooker/models/video.rb +9 -0
  50. data/lib/facebooker/models/work_info.rb +10 -0
  51. data/lib/facebooker/parser.rb +970 -0
  52. data/lib/facebooker/rails/backwards_compatible_param_checks.rb +31 -0
  53. data/lib/facebooker/rails/controller.rb +353 -0
  54. data/lib/facebooker/rails/extensions/action_controller.rb +47 -0
  55. data/lib/facebooker/rails/extensions/rack_setup.rb +16 -0
  56. data/lib/facebooker/rails/extensions/routing.rb +15 -0
  57. data/lib/facebooker/rails/facebook_pretty_errors.rb +22 -0
  58. data/lib/facebooker/rails/facebook_request_fix.rb +28 -0
  59. data/lib/facebooker/rails/facebook_request_fix_2-3.rb +31 -0
  60. data/lib/facebooker/rails/facebook_session_handling.rb +68 -0
  61. data/lib/facebooker/rails/facebook_url_rewriting.rb +60 -0
  62. data/lib/facebooker/rails/helpers/fb_connect.rb +75 -0
  63. data/lib/facebooker/rails/integration_session.rb +38 -0
  64. data/lib/facebooker/rails/profile_publisher_extensions.rb +42 -0
  65. data/lib/facebooker/rails/publisher.rb +608 -0
  66. data/lib/facebooker/rails/routing.rb +49 -0
  67. data/lib/facebooker/rails/test_helpers.rb +68 -0
  68. data/lib/facebooker/rails/utilities.rb +22 -0
  69. data/lib/facebooker/server_cache.rb +24 -0
  70. data/lib/facebooker/service.rb +103 -0
  71. data/lib/facebooker/service/base_service.rb +19 -0
  72. data/lib/facebooker/service/curl_service.rb +44 -0
  73. data/lib/facebooker/service/net_http_service.rb +12 -0
  74. data/lib/facebooker/service/typhoeus_multi_service.rb +27 -0
  75. data/lib/facebooker/service/typhoeus_service.rb +17 -0
  76. data/lib/facebooker/session.rb +786 -0
  77. data/lib/facebooker/stream_post.rb +19 -0
  78. data/lib/facebooker/version.rb +9 -0
  79. data/lib/net/http_multipart_post.rb +123 -0
  80. data/lib/rack/facebook.rb +89 -0
  81. data/lib/rack/facebook_session.rb +21 -0
  82. data/lib/tasks/facebooker.rake +19 -0
  83. data/lib/tasks/facebooker.rb +2 -0
  84. data/lib/tasks/tunnel.rake +46 -0
  85. data/rails/init.rb +1 -0
  86. data/setup.rb +1585 -0
  87. data/templates/layout.erb +24 -0
  88. data/test/facebooker/adapters_test.rb +191 -0
  89. data/test/facebooker/admin_test.rb +102 -0
  90. data/test/facebooker/application_test.rb +110 -0
  91. data/test/facebooker/attachment_test.rb +72 -0
  92. data/test/facebooker/batch_request_test.rb +83 -0
  93. data/test/facebooker/data_test.rb +86 -0
  94. data/test/facebooker/logging_test.rb +43 -0
  95. data/test/facebooker/mobile_test.rb +45 -0
  96. data/test/facebooker/model_test.rb +133 -0
  97. data/test/facebooker/models/event_test.rb +15 -0
  98. data/test/facebooker/models/page_test.rb +56 -0
  99. data/test/facebooker/models/photo_test.rb +16 -0
  100. data/test/facebooker/models/user_test.rb +1074 -0
  101. data/test/facebooker/rails/facebook_request_fix_2-3_test.rb +25 -0
  102. data/test/facebooker/rails/facebook_url_rewriting_test.rb +76 -0
  103. data/test/facebooker/rails/integration_session_test.rb +13 -0
  104. data/test/facebooker/rails/publisher_test.rb +538 -0
  105. data/test/facebooker/rails_integration_test.rb +1543 -0
  106. data/test/facebooker/server_cache_test.rb +44 -0
  107. data/test/facebooker/service_test.rb +58 -0
  108. data/test/facebooker/session_test.rb +883 -0
  109. data/test/facebooker_test.rb +1263 -0
  110. data/test/fixtures/multipart_post_body_with_only_parameters.txt +33 -0
  111. data/test/fixtures/multipart_post_body_with_single_file.txt +38 -0
  112. data/test/fixtures/multipart_post_body_with_single_file_that_has_nil_key.txt +38 -0
  113. data/test/net/http_multipart_post_test.rb +52 -0
  114. data/test/rack/facebook_session_test.rb +34 -0
  115. data/test/rack/facebook_test.rb +73 -0
  116. data/test/rails_test_helper.rb +36 -0
  117. data/test/test_helper.rb +74 -0
  118. metadata +278 -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,68 @@
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={}, session=nil, flash=nil)
15
+ facebook_verb(:get, path, params, session, flash)
16
+ end
17
+
18
+ def facebook_post(path,params={}, session=nil, flash=nil)
19
+ facebook_verb(:post, path, params, session, flash)
20
+ end
21
+
22
+ def facebook_put(path,params={}, session=nil, flash=nil)
23
+ facebook_verb(:put, path, params, session, flash)
24
+ end
25
+
26
+ def facebook_delete(path,params={}, session=nil, flash=nil)
27
+ facebook_verb(:delete, path, params, session, flash)
28
+ end
29
+
30
+ def facebook_verb(verb, path, params={}, session=nil, flash=nil)
31
+ send verb, path, facebook_params(params).reverse_merge(:canvas => true), session, flash
32
+ end
33
+
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)
38
+ end
39
+
40
+ private
41
+
42
+ def default_facebook_parameters
43
+ {
44
+ :fb_sig_added => "1",
45
+ :fb_sig_session_key => "facebook_session_key",
46
+ :fb_sig_user => "1234",
47
+ :fb_sig_expires => "0",
48
+ :fb_sig_in_canvas => "1",
49
+ :fb_sig_time => Time.now.to_f
50
+ }
51
+ end
52
+
53
+ def facebook_redirect_url
54
+ match = @response.body.match(/<fb:redirect url="([^"]+)"/)
55
+ match.nil? ? nil : match.captures[0]
56
+ end
57
+
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('=')
61
+ end
62
+ Digest::MD5.hexdigest([facebook_params.sort.join, Facebooker::Session.secret_key].join)
63
+ end
64
+
65
+ end
66
+ end
67
+ end
68
+
@@ -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,103 @@
1
+ require 'facebooker/parser'
2
+ module Facebooker
3
+ class Service
4
+ def initialize(api_base, api_path, api_key)
5
+ @api_base = api_base
6
+ @api_path = api_path
7
+ @api_key = api_key
8
+ end
9
+
10
+ @active_service = nil
11
+ def self.active_service
12
+ unless @active_service
13
+ if Facebooker.use_curl?
14
+ @active_service = Facebooker::Service::CurlService.new
15
+ else
16
+ @active_service = Facebooker::Service::NetHttpService.new
17
+ end
18
+ end
19
+ @active_service
20
+ end
21
+
22
+ def self.active_service=(new_service)
23
+ @active_service = new_service
24
+ end
25
+
26
+ def self.with_service(service)
27
+ old_service = active_service
28
+ self.active_service = service
29
+ begin
30
+ yield
31
+ ensure
32
+ self.active_service = old_service
33
+ end
34
+ end
35
+
36
+
37
+ # Process all calls to Facebook in th block asynchronously
38
+ # nil will be returned from all calls and no results will be parsed. This is mostly useful
39
+ # for sending large numbers of notifications or sending a lot of profile updates
40
+ #
41
+ # for example:
42
+ # User.find_in_batches(:batch_size => 200) do |users|
43
+ # Faceboooker::Service.with_async do
44
+ # users.each {|u| u.facebook_session.send_notification(...)}
45
+ # end
46
+ # end
47
+ #
48
+ # Note: You shouldn't make more than about 200 api calls in a with_async block
49
+ # or you might exhaust all filehandles.
50
+ #
51
+ # This functionality require the typhoeus gem
52
+ #
53
+ def self.with_async(&proc)
54
+ block_with_process = Proc.new { proc.call ; process_async}
55
+ with_service(Facebooker::Service::TyphoeusMultiService.new,&block_with_process)
56
+ end
57
+
58
+ def self.process_async
59
+ active_service.process
60
+ end
61
+
62
+
63
+ # TODO: support ssl
64
+ def post(params)
65
+ attempt = 0
66
+ if active_service.parse_results?
67
+ Parser.parse(params[:method], post_form(url,params) )
68
+ else
69
+ post_form(url,params)
70
+ end
71
+ rescue Errno::ECONNRESET, EOFError
72
+ if attempt == 0
73
+ attempt += 1
74
+ retry
75
+ end
76
+ end
77
+
78
+ def post_form(url,params)
79
+ active_service.post_form(url,params)
80
+ end
81
+
82
+ def post_multipart_form(url,params)
83
+ active_service.post_multipart_form(url,params)
84
+ end
85
+
86
+ def active_service
87
+ self.class.active_service
88
+ end
89
+
90
+ def post_file(params)
91
+ service_url = url(params.delete(:base))
92
+ result = post_multipart_form(service_url, params)
93
+ Parser.parse(params[:method], result)
94
+ end
95
+
96
+ private
97
+ def url(base = nil)
98
+ base ||= @api_base
99
+ URI.parse('http://'+ base + @api_path)
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,19 @@
1
+ class Facebooker::Service::BaseService
2
+ def parse_results?
3
+ true
4
+ end
5
+
6
+ def post_params(params)
7
+ post_params = {}
8
+ params.each do |k,v|
9
+ k = k.to_s unless k.is_a?(String)
10
+ if Array === v || Hash === v
11
+ post_params[k] = Facebooker.json_encode(v)
12
+ else
13
+ post_params[k] = v
14
+ end
15
+ end
16
+ post_params
17
+ end
18
+
19
+ end
@@ -0,0 +1,44 @@
1
+ require 'curb'
2
+ Facebooker.use_curl = true
3
+ class Facebooker::Service::CurlService < Facebooker::Service::BaseService
4
+ def post_form(url,params,multipart=false)
5
+ curl = Curl::Easy.new(url.to_s) do |c|
6
+ c.multipart_form_post = multipart
7
+ c.timeout = Facebooker.timeout
8
+ end
9
+ curl.http_post(*to_curb_params(params))
10
+ curl.body_str
11
+ end
12
+
13
+ def post_multipart_form(url,params)
14
+ post_form(url,params,true)
15
+ end
16
+
17
+ # Net::HTTP::MultipartPostFile
18
+ def multipart_post_file?(object)
19
+ object.respond_to?(:content_type) &&
20
+ object.respond_to?(:data) &&
21
+ object.respond_to?(:filename)
22
+ end
23
+
24
+ def to_curb_params(params)
25
+ parray = []
26
+ params.each_pair do |k,v|
27
+ if multipart_post_file?(v)
28
+ # Curl doesn't like blank field names
29
+ field = Curl::PostField.file((k.blank? ? 'xxx' : k.to_s), nil, File.basename(v.filename))
30
+ field.content_type = v.content_type
31
+ field.content = v.data
32
+ parray << field
33
+ else
34
+ parray << Curl::PostField.content(
35
+ k.to_s,
36
+ (Array === v || Hash===v) ? Facebooker.json_encode(v) : v.to_s
37
+ )
38
+ end
39
+ end
40
+ parray
41
+ end
42
+
43
+ end
44
+
@@ -0,0 +1,12 @@
1
+ require 'net/http'
2
+ class Facebooker::Service::NetHttpService <Facebooker::Service::BaseService
3
+ def post_form(url,params)
4
+ Net::HTTP.post_form(url, post_params(params))
5
+ end
6
+
7
+ def post_multipart_form(url,params)
8
+ Net::HTTP.post_multipart_form(url, params)
9
+ end
10
+
11
+
12
+ end
@@ -0,0 +1,27 @@
1
+ class Facebooker::Service::TyphoeusMultiService < Facebooker::Service::TyphoeusService
2
+
3
+ def initialize
4
+ @result_objects = []
5
+ end
6
+
7
+ def parse_results?
8
+ false
9
+ end
10
+
11
+ #define a method that returns nothing
12
+ define_remote_method :async_post, :on_success => lambda {|r| puts "."}
13
+
14
+ def perform_post(url,params)
15
+ add_result(self.class.async_post(:base_uri=>url,:params=>params))
16
+ end
17
+
18
+ def add_result(obj)
19
+ @result_objects << obj
20
+ end
21
+
22
+ def process
23
+ # we need to access all objects to make sure the proxy has made the request
24
+ @result_objects.each(&:nil?)
25
+ @result_objects = []
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ require 'typhoeus'
2
+ class Facebooker::Service::TyphoeusService < Facebooker::Service::BaseService
3
+ include Typhoeus
4
+ def post_form(url,params)
5
+ perform_post(url.to_s,post_params(params))
6
+ end
7
+
8
+ def perform_post(url,params)
9
+ self.class.post(url,:params=>post_params)
10
+ end
11
+
12
+ def post_multipart_form(url,params)
13
+ raise "Multipart not supported on Typhoeus"
14
+ end
15
+
16
+
17
+ end
@@ -0,0 +1,786 @@
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
+
10
+ #
11
+ # Raised when a facebook session has expired. This
12
+ # happens when the timeout is reached, or when the
13
+ # user logs out of facebook
14
+ # can be handled with:
15
+ # rescue_from Facebooker::Session::SessionExpired, :with => :some_method_name
16
+ class SessionExpired < StandardError; end
17
+
18
+ class UnknownError < StandardError; end
19
+ class ServiceUnavailable < StandardError; end
20
+ class MaxRequestsDepleted < StandardError; end
21
+ class HostNotAllowed < StandardError; end
22
+ class MissingOrInvalidParameter < StandardError; end
23
+ class InvalidAPIKey < StandardError; end
24
+ class SessionExpired < StandardError; end
25
+ class CallOutOfOrder < StandardError; end
26
+ class IncorrectSignature < StandardError; end
27
+ class SignatureTooOld < StandardError; end
28
+ class TooManyUserCalls < StandardError; end
29
+ class TooManyUserActionCalls < StandardError; end
30
+ class InvalidFeedTitleLink < StandardError; end
31
+ class InvalidFeedTitleLength < StandardError; end
32
+ class InvalidFeedTitleName < StandardError; end
33
+ class BlankFeedTitle < StandardError; end
34
+ class FeedBodyLengthTooLong < StandardError; end
35
+ class InvalidFeedPhotoSource < StandardError; end
36
+ class InvalidFeedPhotoLink < StandardError; end
37
+ class TemplateDataMissingRequiredTokens < StandardError; end
38
+ class FeedMarkupInvalid < StandardError; end
39
+ class FeedTitleDataInvalid < StandardError; end
40
+ class FeedTitleTemplateInvalid < StandardError; end
41
+ class FeedBodyDataInvalid < StandardError; end
42
+ class FeedBodyTemplateInvalid < StandardError; end
43
+ class FeedPhotosNotRetrieved < StandardError; end
44
+ class FeedTargetIdsInvalid < StandardError; end
45
+ class TemplateBundleInvalid < StandardError; end
46
+ class ConfigurationMissing < StandardError; end
47
+ class FQLParseError < StandardError; end
48
+ class FQLFieldDoesNotExist < StandardError; end
49
+ class FQLTableDoesNotExist < StandardError; end
50
+ class FQLStatementNotIndexable < StandardError; end
51
+ class FQLFunctionDoesNotExist < StandardError; end
52
+ class FQLWrongNumberArgumentsPassedToFunction < StandardError; end
53
+ class PermissionError < StandardError; end
54
+ class InvalidAlbumId < StandardError; end
55
+ class AlbumIsFull < StandardError; end
56
+ class MissingOrInvalidImageFile < StandardError; end
57
+ class TooManyUnapprovedPhotosPending < StandardError; end
58
+ class ExtendedPermissionRequired < StandardError; end
59
+ class ReadMailboxExtendedPermissionRequired < StandardError; end
60
+ class InvalidFriendList < StandardError; end
61
+ class UserUnRegistrationFailed < StandardError
62
+ attr_accessor :failed_users
63
+ end
64
+ class UserRegistrationFailed < StandardError
65
+ attr_accessor :failed_users
66
+ end
67
+
68
+ API_SERVER_BASE_URL = ENV["FACEBOOKER_API"] == "new" ? "api.new.facebook.com" : "api.facebook.com"
69
+ API_PATH_REST = "/restserver.php"
70
+ WWW_SERVER_BASE_URL = ENV["FACEBOOKER_API"] == "new" ? "www.new.facebook.com" : "www.facebook.com"
71
+ WWW_PATH_LOGIN = "/login.php"
72
+ WWW_PATH_ADD = "/add.php"
73
+ WWW_PATH_INSTALL = "/install.php"
74
+
75
+ attr_writer :auth_token
76
+ attr_reader :session_key
77
+ attr_reader :secret_from_session
78
+
79
+ def self.create(api_key=nil, secret_key=nil)
80
+ api_key ||= self.api_key
81
+ secret_key ||= self.secret_key
82
+ raise ArgumentError unless !api_key.nil? && !secret_key.nil?
83
+ new(api_key, secret_key)
84
+ end
85
+
86
+ def self.api_key
87
+ extract_key_from_environment(:api) || extract_key_from_configuration_file(:api) rescue report_inability_to_find_key(:api)
88
+ end
89
+
90
+ def self.secret_key
91
+ extract_key_from_environment(:secret) || extract_key_from_configuration_file(:secret) rescue report_inability_to_find_key(:secret)
92
+ end
93
+
94
+ def self.current
95
+ Thread.current['facebook_session']
96
+ end
97
+
98
+ def self.current=(session)
99
+ Thread.current['facebook_session'] = session
100
+ end
101
+
102
+ def login_url(options={})
103
+ options = default_login_url_options.merge(options)
104
+ "#{Facebooker.login_url_base}#{login_url_optional_parameters(options)}"
105
+ end
106
+
107
+ def install_url(options={})
108
+ "#{Facebooker.install_url_base}#{install_url_optional_parameters(options)}"
109
+ end
110
+
111
+ # The url to get user to approve extended permissions
112
+ # http://wiki.developers.facebook.com/index.php/Extended_permission
113
+ #
114
+ # permissions:
115
+ # * email
116
+ # * offline_access
117
+ # * status_update
118
+ # * photo_upload
119
+ # * video_upload
120
+ # * create_listing
121
+ # * create_event
122
+ # * rsvp_event
123
+ # * sms
124
+ # * read_mailbox
125
+ def permission_url(permission,options={})
126
+ options = default_login_url_options.merge(options)
127
+ options = add_next_parameters(options)
128
+ options << "&ext_perm=#{permission}"
129
+ "#{Facebooker.permission_url_base}#{options.join}"
130
+ end
131
+
132
+ def connect_permission_url(permission,options={})
133
+ options = default_login_url_options.merge(options)
134
+ options = add_next_parameters(options)
135
+ options << "&ext_perm=#{permission}"
136
+ "#{Facebooker.connect_permission_url_base}#{options.join}"
137
+ end
138
+
139
+ def install_url_optional_parameters(options)
140
+ optional_parameters = []
141
+ optional_parameters += add_next_parameters(options)
142
+ optional_parameters.join
143
+ end
144
+
145
+ def add_next_parameters(options)
146
+ opts = []
147
+ opts << "&next=#{CGI.escape(options[:next])}" if options[:next]
148
+ opts << "&next_cancel=#{CGI.escape(options[:next_cancel])}" if options[:next_cancel]
149
+ opts
150
+ end
151
+
152
+ def login_url_optional_parameters(options)
153
+ # It is important that unused options are omitted as stuff like &canvas=false will still display the canvas.
154
+ optional_parameters = []
155
+ optional_parameters += add_next_parameters(options)
156
+ optional_parameters << "&skipcookie=true" if options[:skip_cookie]
157
+ optional_parameters << "&hide_checkbox=true" if options[:hide_checkbox]
158
+ optional_parameters << "&canvas=true" if options[:canvas]
159
+ optional_parameters << "&fbconnect=true" if options[:fbconnect]
160
+ optional_parameters << "&return_session=true" if options[:return_session]
161
+ optional_parameters << "&session_key_only=true" if options[:session_key_only]
162
+ optional_parameters << "&req_perms=#{options[:req_perms]}" if options[:req_perms]
163
+ optional_parameters.join
164
+ end
165
+
166
+ def default_login_url_options
167
+ {}
168
+ end
169
+
170
+ def initialize(api_key, secret_key)
171
+ @api_key = api_key
172
+ @secret_key = secret_key
173
+ @batch_request = nil
174
+ @session_key = nil
175
+ @uid = nil
176
+ @auth_token = nil
177
+ @secret_from_session = nil
178
+ @expires = nil
179
+ end
180
+
181
+ def secret_for_method(method_name)
182
+ @secret_key
183
+ end
184
+
185
+ def auth_token
186
+ @auth_token ||= post 'facebook.auth.createToken'
187
+ end
188
+
189
+ def infinite?
190
+ @expires == 0
191
+ end
192
+
193
+ def expired?
194
+ @expires.nil? || (!infinite? && Time.at(@expires) <= Time.now)
195
+ end
196
+
197
+ def secured?
198
+ !@session_key.nil? && !expired?
199
+ end
200
+
201
+ def secure!(args = {})
202
+ response = post 'facebook.auth.getSession', :auth_token => auth_token, :generate_session_secret => args[:generate_session_secret] ? "1" : "0"
203
+ secure_with!(response['session_key'], response['uid'], response['expires'], response['secret'])
204
+ end
205
+
206
+ def secure_with_session_secret!
207
+ self.secure!(:generate_session_secret => true)
208
+ end
209
+
210
+ def secure_with!(session_key, uid = nil, expires = nil, secret_from_session = nil)
211
+ @session_key = session_key
212
+ @uid = uid ? Integer(uid) : post('facebook.users.getLoggedInUser', :session_key => session_key)
213
+ @expires = expires ? Integer(expires) : 0
214
+ @secret_from_session = secret_from_session
215
+ end
216
+
217
+ def fql_build_object(type, hash)
218
+ case type
219
+ when 'user'
220
+ user = User.new
221
+ user.session = self
222
+ user.populate_from_hash!(hash)
223
+ user
224
+ when 'photo'
225
+ Photo.from_hash(hash)
226
+ when 'album'
227
+ Album.from_hash(hash)
228
+ when 'page'
229
+ Page.from_hash(hash)
230
+ when 'page_admin'
231
+ Page.from_hash(hash)
232
+ when 'group'
233
+ Group.from_hash(hash)
234
+ when 'event'
235
+ Event.from_hash(hash)
236
+ when 'event_member'
237
+ Event::Attendance.from_hash(hash)
238
+ else
239
+ hash
240
+ end
241
+ end
242
+
243
+ def fql_query(query, format = 'XML')
244
+ post('facebook.fql.query', :query => query, :format => format) do |response|
245
+ type = response.shift
246
+ if type.nil?
247
+ []
248
+ else
249
+ response.shift.map do |hash|
250
+ fql_build_object(type, hash)
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+ def fql_multiquery(queries, format = 'XML')
257
+ results = {}
258
+ post('facebook.fql.multiquery', :queries => queries.to_json, :format => format) do |responses|
259
+ responses.each do |response|
260
+ name = response.shift
261
+ response = response.shift
262
+ type = response.shift
263
+ value = []
264
+ unless type.nil?
265
+ value = response.shift.map do |hash|
266
+ fql_build_object(type, hash)
267
+ end
268
+ end
269
+ results[name] = value
270
+ end
271
+ end
272
+ results
273
+ end
274
+
275
+ def user
276
+ @user ||= User.new(uid, self)
277
+ end
278
+
279
+ #
280
+ # This one has so many parameters, a Hash seemed cleaner than a long param list. Options can be:
281
+ # :uid => Filter by events associated with a user with this uid
282
+ # :eids => Filter by this list of event ids. This is a comma-separated list of eids.
283
+ # :start_time => Filter with this UTC as lower bound. A missing or zero parameter indicates no lower bound. (Time or Integer)
284
+ # :end_time => Filter with this UTC as upper bound. A missing or zero parameter indicates no upper bound. (Time or Integer)
285
+ # :rsvp_status => Filter by this RSVP status.
286
+ def events(options = {})
287
+ @events ||= {}
288
+ @events[options.to_s] ||= post('facebook.events.get', options) do |response|
289
+ response.map do |hash|
290
+ Event.from_hash(hash)
291
+ end
292
+ end
293
+ end
294
+
295
+ # Creates an event with the event_info hash and an optional Net::HTTP::MultipartPostFile for the event picture.
296
+ # If ActiveSupport::TimeWithZone is installed (it's in Rails > 2.1), and start_time or end_time are given as
297
+ # ActiveSupport::TimeWithZone, then they will be assumed to represent local time for the event. They will automatically be
298
+ # converted to the expected timezone for Facebook, which is PST or PDT depending on when the event occurs.
299
+ # Returns the eid of the newly created event
300
+ # http://wiki.developers.facebook.com/index.php/Events.create
301
+ def create_event(event_info, multipart_post_file = nil)
302
+ if defined?(ActiveSupport::TimeWithZone) && defined?(ActiveSupport::TimeZone)
303
+ # Facebook expects all event local times to be in Pacific Time, so we need to take the actual local time and
304
+ # send it to Facebook as if it were Pacific Time converted to Unix epoch timestamp. Very confusing...
305
+ facebook_time = ActiveSupport::TimeZone["Pacific Time (US & Canada)"]
306
+
307
+ start_time = event_info.delete(:start_time) || event_info.delete('start_time')
308
+ if start_time && start_time.is_a?(ActiveSupport::TimeWithZone)
309
+ event_info['start_time'] = facebook_time.parse(start_time.strftime("%Y-%m-%d %H:%M:%S")).to_i
310
+ else
311
+ event_info['start_time'] = start_time
312
+ end
313
+
314
+ end_time = event_info.delete(:end_time) || event_info.delete('end_time')
315
+ if end_time && end_time.is_a?(ActiveSupport::TimeWithZone)
316
+ event_info['end_time'] = facebook_time.parse(end_time.strftime("%Y-%m-%d %H:%M:%S")).to_i
317
+ else
318
+ event_info['end_time'] = end_time
319
+ end
320
+ end
321
+
322
+ post_file('facebook.events.create', :event_info => event_info.to_json, nil => multipart_post_file)
323
+ end
324
+
325
+ # Cancel an event
326
+ # http://wiki.developers.facebook.com/index.php/Events.cancel
327
+ # E.g:
328
+ # @session.cancel_event('100321123', :cancel_message => "It's raining...")
329
+ # # => Returns true if all went well
330
+ def cancel_event(eid, options = {})
331
+ result = post('facebook.events.cancel', options.merge(:eid => eid))
332
+ result == '1' ? true : false
333
+ end
334
+
335
+ def event_members(eid)
336
+ @members ||= {}
337
+ @members[eid] ||= post('facebook.events.getMembers', :eid => eid) do |response|
338
+ response.map do |attendee_hash|
339
+ Event::Attendance.from_hash(attendee_hash)
340
+ end
341
+ end
342
+ end
343
+
344
+ def users_standard(user_ids, fields=[])
345
+ post("facebook.users.getStandardInfo",:uids=>user_ids.join(","),:fields=>User.standard_fields(fields)) do |users|
346
+ users.map { |u| User.new(u)}
347
+ end
348
+ end
349
+
350
+ def users(user_ids, fields=[])
351
+ post("facebook.users.getInfo",:uids=>user_ids.join(","),:fields=>User.user_fields(fields)) do |users|
352
+ users.map { |u| User.new(u)}
353
+ end
354
+ end
355
+
356
+ def pages(options = {})
357
+ raise ArgumentError, 'fields option is mandatory' unless options.has_key?(:fields)
358
+ @pages ||= {}
359
+ @pages[options] ||= post('facebook.pages.getInfo', options) do |response|
360
+ response.map do |hash|
361
+ Page.from_hash(hash)
362
+ end
363
+ end
364
+ end
365
+
366
+ # Takes page_id and uid, returns true if uid is a fan of the page_id
367
+ def is_fan(page_id, uid)
368
+ puts "Deprecated. Use Page#user_is_fan? instead"
369
+ Page.new(page_id).user_is_fan?(uid)
370
+ end
371
+
372
+
373
+ #
374
+ # Returns a proxy object for handling calls to Facebook cached items
375
+ # such as images and FBML ref handles
376
+ def server_cache
377
+ Facebooker::ServerCache.new(self)
378
+ end
379
+
380
+ #
381
+ # Returns a proxy object for handling calls to the Facebook Data API
382
+ def data
383
+ Facebooker::Data.new(self)
384
+ end
385
+
386
+ def admin
387
+ Facebooker::Admin.new(self)
388
+ end
389
+
390
+ def application
391
+ Facebooker::Application.new(self)
392
+ end
393
+
394
+ def mobile
395
+ Facebooker::Mobile.new(self)
396
+ end
397
+
398
+ #
399
+ # Given an array like:
400
+ # [[userid, otheruserid], [yetanotherid, andanotherid]]
401
+ # returns a Hash indicating friendship of those pairs:
402
+ # {[userid, otheruserid] => true, [yetanotherid, andanotherid] => false}
403
+ # if one of the Hash values is nil, it means the facebook platform's answer is "I don't know"
404
+ def check_friendship(array_of_pairs_of_users)
405
+ uids1 = []
406
+ uids2 = []
407
+ array_of_pairs_of_users.each do |pair|
408
+ uids1 << pair.first
409
+ uids2 << pair.last
410
+ end
411
+ post('facebook.friends.areFriends', :uids1 => uids1.join(','), :uids2 => uids2.join(','))
412
+ end
413
+
414
+ def get_photos(pids = nil, subj_id = nil, aid = nil)
415
+ if [subj_id, pids, aid].all? {|arg| arg.nil?}
416
+ raise ArgumentError, "Can't get a photo without a picture, album or subject ID"
417
+ end
418
+ # We have to normalize params orherwise FB complain about signature
419
+ params = {:pids => pids, :subj_id => subj_id, :aid => aid}.delete_if { |k,v| v.nil? }
420
+ @photos = post('facebook.photos.get', params ) do |response|
421
+ response.map do |hash|
422
+ Photo.from_hash(hash)
423
+ end
424
+ end
425
+ end
426
+
427
+ #remove a comment from a given xid stream with comment_id
428
+ def remove_comment(xid,comment_id)
429
+ post('facebook.comments.remove', :xid=>xid, :comment_id =>comment_id)
430
+ end
431
+
432
+ #pulls comment list for a given XID
433
+ def get_comments(xid)
434
+ @comments = post('facebook.comments.get', :xid => xid) do |response|
435
+ response.map do |hash|
436
+ Comment.from_hash(hash)
437
+ end
438
+ end
439
+ end
440
+
441
+ def get_albums(aids)
442
+ @albums = post('facebook.photos.getAlbums', :aids => aids) do |response|
443
+ response.map do |hash|
444
+ Album.from_hash(hash)
445
+ end
446
+ end
447
+ end
448
+
449
+ ###
450
+ # Retrieve a viewer's facebook stream
451
+ # See http://wiki.developers.facebook.com/index.php/Stream.get for options
452
+ #
453
+ def get_stream(viewer_id, options = {})
454
+
455
+ @stream = post('facebook.stream.get', prepare_get_stream_options(viewer_id, options), true) do |response|
456
+ response
457
+ end
458
+ end
459
+
460
+ def get_tags(pids)
461
+ @tags = post('facebook.photos.getTags', :pids => pids) do |response|
462
+ response.map do |hash|
463
+ Tag.from_hash(hash)
464
+ end
465
+ end
466
+ end
467
+
468
+ def add_tags(pid, x, y, tag_uid = nil, tag_text = nil )
469
+ if [tag_uid, tag_text].all? {|arg| arg.nil?}
470
+ raise ArgumentError, "Must enter a name or string for this tag"
471
+ end
472
+ @tags = post('facebook.photos.addTag', :pid => pid, :tag_uid => tag_uid, :tag_text => tag_text, :x => x, :y => y )
473
+ end
474
+
475
+ def send_notification(user_ids, fbml, email_fbml = nil)
476
+ params = {:notification => fbml, :to_ids => user_ids.map{ |id| User.cast_to_facebook_id(id)}.join(',')}
477
+ if email_fbml
478
+ params[:email] = email_fbml
479
+ end
480
+ params[:type]="user_to_user"
481
+ # if there is no uid, this is an announcement
482
+ unless uid?
483
+ params[:type]="app_to_user"
484
+ end
485
+
486
+ post 'facebook.notifications.send', params,uid?
487
+ end
488
+
489
+ ##
490
+ # Register a template bundle with Facebook.
491
+ # returns the template id to use to send using this template
492
+ def register_template_bundle(one_line_story_templates,short_story_templates=nil,full_story_template=nil, action_links=nil)
493
+ templates = ensure_array(one_line_story_templates)
494
+ parameters = {:one_line_story_templates => templates.to_json}
495
+
496
+ unless action_links.blank?
497
+ parameters[:action_links] = action_links.to_json
498
+ end
499
+
500
+ unless short_story_templates.blank?
501
+ templates = ensure_array(short_story_templates)
502
+ parameters[:short_story_templates] = templates.to_json
503
+ end
504
+
505
+ unless full_story_template.blank?
506
+ parameters[:full_story_template] = full_story_template.to_json
507
+ end
508
+
509
+ post("facebook.feed.registerTemplateBundle", parameters, false)
510
+ end
511
+
512
+ ##
513
+ # Deactivate a template bundle with Facebook.
514
+ # Returns true if a bundle with the specified id is active and owned by this app.
515
+ # Useful to avoid exceeding the 100 templates/app limit.
516
+ def deactivate_template_bundle_by_id(template_bundle_id)
517
+ post("facebook.feed.deactivateTemplateBundleByID", {:template_bundle_id => template_bundle_id.to_s}, false)
518
+ end
519
+
520
+ def active_template_bundles
521
+ post("facebook.feed.getRegisteredTemplateBundles",{},false)
522
+ end
523
+
524
+ ##
525
+ # publish a previously rendered template bundle
526
+ # see http://wiki.developers.facebook.com/index.php/Feed.publishUserAction
527
+ #
528
+ def publish_user_action(bundle_id,data={},target_ids=nil,body_general=nil,story_size=nil)
529
+ parameters={:template_bundle_id=>bundle_id,:template_data=>data.to_json}
530
+ parameters[:target_ids] = target_ids unless target_ids.blank?
531
+ parameters[:body_general] = body_general unless body_general.blank?
532
+ parameters[:story_size] = story_size unless story_size.nil?
533
+ post("facebook.feed.publishUserAction", parameters)
534
+ end
535
+
536
+ ##
537
+ # Upload strings in native locale to facebook for translations.
538
+ #
539
+ # e.g. facebook_session.upload_native_strings([:text => "Welcome {user}", :description => "Welcome message to currently logged in user."])
540
+ # returns the number of strings uploaded
541
+ #
542
+ # See http://wiki.developers.facebook.com/index.php/Intl.uploadNativeStrings for method options
543
+ #
544
+ def upload_native_strings(native_strings)
545
+ raise ArgumentError, "You must provide strings to upload" if native_strings.nil?
546
+
547
+ post('facebook.intl.uploadNativeStrings', :native_strings => Facebooker.json_encode(native_strings)) do |response|
548
+ response
549
+ end
550
+ end
551
+
552
+ ##
553
+ # Send email to as many as 100 users at a time
554
+ def send_email(user_ids, subject, text, fbml = nil)
555
+ user_ids = Array(user_ids)
556
+ params = {:fbml => fbml, :recipients => user_ids.map{ |id| User.cast_to_facebook_id(id)}.join(','), :text => text, :subject => subject}
557
+ post 'facebook.notifications.sendEmail', params, false
558
+ end
559
+
560
+ # Only serialize the bare minimum to recreate the session.
561
+ def marshal_load(variables)#:nodoc:
562
+ fields_to_serialize.each_with_index{|field, index| instance_variable_set_value(field, variables[index])}
563
+ end
564
+
565
+ # Only serialize the bare minimum to recreate the session.
566
+ def marshal_dump#:nodoc:
567
+ fields_to_serialize.map{|field| instance_variable_value(field)}
568
+ end
569
+
570
+ # Only serialize the bare minimum to recreate the session.
571
+ def to_yaml( opts = {} )
572
+ YAML::quick_emit(self.object_id, opts) do |out|
573
+ out.map(taguri) do |map|
574
+ fields_to_serialize.each do |field|
575
+ map.add(field, instance_variable_value(field))
576
+ end
577
+ end
578
+ end
579
+ end
580
+
581
+ def instance_variable_set_value(field, value)
582
+ self.instance_variable_set("@#{field}", value)
583
+ end
584
+
585
+ def instance_variable_value(field)
586
+ self.instance_variable_get("@#{field}")
587
+ end
588
+
589
+ def fields_to_serialize
590
+ %w(session_key uid expires secret_from_session auth_token api_key secret_key)
591
+ end
592
+
593
+ class Desktop < Session
594
+ def login_url
595
+ super + "&auth_token=#{auth_token}"
596
+ end
597
+
598
+ def secret_for_method(method_name)
599
+ secret = auth_request_methods.include?(method_name) ? super : @secret_from_session
600
+ secret
601
+ end
602
+
603
+ def post(method, params = {},use_session=false)
604
+ if method == 'facebook.profile.getFBML' || method == 'facebook.profile.setFBML'
605
+ raise NonSessionUser.new("User #{@uid} is not the logged in user.") unless @uid == params[:uid]
606
+ end
607
+ super
608
+ end
609
+ private
610
+ def auth_request_methods
611
+ ['facebook.auth.getSession', 'facebook.auth.createToken']
612
+ end
613
+ end
614
+
615
+ def batch_request?
616
+ @batch_request
617
+ end
618
+
619
+ def add_to_batch(req,&proc)
620
+ batch_request = BatchRequest.new(req,proc)
621
+ Thread.current[:facebooker_current_batch_queue]<<batch_request
622
+ batch_request
623
+ end
624
+
625
+ # Submit the enclosed requests for this session inside a batch
626
+ #
627
+ # All requests will be sent to Facebook at the end of the block
628
+ # each method inside the block will return a proxy object
629
+ # attempting to access the proxy before the end of the block will yield an exception
630
+ #
631
+ # For Example:
632
+ #
633
+ # facebook_session.batch do
634
+ # @send_result = facebook_session.send_notification([12451752],"Woohoo")
635
+ # @albums = facebook_session.user.albums
636
+ # end
637
+ # puts @albums.first.inspect
638
+ #
639
+ # is valid, however
640
+ #
641
+ # facebook_session.batch do
642
+ # @send_result = facebook_session.send_notification([12451752],"Woohoo")
643
+ # @albums = facebook_session.user.albums
644
+ # puts @albums.first.inspect
645
+ # end
646
+ #
647
+ # will raise Facebooker::BatchRequest::UnexecutedRequest
648
+ #
649
+ # If an exception is raised while processing the result, that exception will be
650
+ # re-raised on the next access to that object or when exception_raised? is called
651
+ #
652
+ # for example, if the send_notification resulted in TooManyUserCalls being raised,
653
+ # calling
654
+ # @send_result.exception_raised?
655
+ # would re-raise that exception
656
+ # if there was an error retrieving the albums, it would be re-raised when
657
+ # @albums.first
658
+ # is called
659
+ #
660
+ def batch(serial_only=false)
661
+ @batch_request=true
662
+ Thread.current[:facebooker_current_batch_queue]=[]
663
+ yield
664
+ # Set the batch request to false so that post will execute the batch job
665
+ @batch_request=false
666
+ BatchRun.current_batch=Thread.current[:facebooker_current_batch_queue]
667
+ post("facebook.batch.run",:method_feed=>BatchRun.current_batch.map{|q| q.uri}.to_json,:serial_only=>serial_only.to_s)
668
+ ensure
669
+ @batch_request=false
670
+ BatchRun.current_batch=nil
671
+ end
672
+
673
+ def post_without_logging(method, params = {}, use_session_key = true, &proc)
674
+ add_facebook_params(params, method)
675
+ use_session_key && @session_key && params[:session_key] ||= @session_key
676
+ final_params=params.merge(:sig => signature_for(params))
677
+ if batch_request?
678
+ add_to_batch(final_params,&proc)
679
+ else
680
+ result = service.post(final_params)
681
+ result = yield result if block_given?
682
+ result
683
+ end
684
+ end
685
+
686
+ def post(method, params = {}, use_session_key = true, &proc)
687
+ if batch_request? or Facebooker::Logging.skip_api_logging
688
+ post_without_logging(method, params, use_session_key, &proc)
689
+ else
690
+ Logging.log_fb_api(method, params) do
691
+ post_without_logging(method, params, use_session_key, &proc)
692
+ end
693
+ end
694
+ end
695
+
696
+ def post_file(method, params = {})
697
+ base = params.delete(:base)
698
+ Logging.log_fb_api(method, params) do
699
+ add_facebook_params(params, method)
700
+ @session_key && params[:session_key] ||= @session_key unless params[:uid]
701
+ service.post_file(params.merge(:base => base, :sig => signature_for(params.reject{|key, value| key.nil?})))
702
+ end
703
+ end
704
+
705
+
706
+ @configuration_file_path = nil
707
+
708
+ def self.configuration_file_path
709
+ @configuration_file_path || File.expand_path("~/.facebookerrc")
710
+ end
711
+
712
+ def self.configuration_file_path=(path)
713
+ @configuration_file_path = path
714
+ end
715
+
716
+ private
717
+ def add_facebook_params(hash, method)
718
+ hash[:method] = method
719
+ hash[:api_key] = @api_key
720
+ hash[:call_id] = Time.now.to_f.to_s unless method == 'facebook.auth.getSession'
721
+ hash[:v] = "1.0"
722
+ end
723
+
724
+ # This ultimately delgates to the adapter
725
+ def self.extract_key_from_environment(key_name)
726
+ Facebooker.send(key_name.to_s + "_key") rescue nil
727
+ end
728
+
729
+ def self.extract_key_from_configuration_file(key_name)
730
+ read_configuration_file[key_name]
731
+ end
732
+
733
+ def self.report_inability_to_find_key(key_name)
734
+ raise ConfigurationMissing, "Could not find configuration information for #{key_name}"
735
+ end
736
+
737
+ def self.read_configuration_file
738
+ eval(File.read(configuration_file_path))
739
+ end
740
+
741
+ def service
742
+ @service ||= Service.new(Facebooker.api_server_base, Facebooker.api_rest_path, @api_key)
743
+ end
744
+
745
+ def uid
746
+ @uid || (secure!; @uid)
747
+ end
748
+
749
+ def uid?
750
+ !! @uid
751
+ end
752
+
753
+ def signature_for(params)
754
+ params.delete_if { |k,v| v.nil? }
755
+ raw_string = params.inject([]) do |collection, pair|
756
+ collection << pair.map { |x|
757
+ Array === x ? Facebooker.json_encode(x) : x
758
+ }.join("=")
759
+ collection
760
+ end.sort.join
761
+ Digest::MD5.hexdigest([raw_string, secret_for_method(params[:method])].join)
762
+ end
763
+
764
+ def ensure_array(value)
765
+ value.is_a?(Array) ? value : [value]
766
+ end
767
+
768
+ def prepare_get_stream_options(viewer_id, options)
769
+ opts = {}
770
+
771
+ opts[:viewer_id] = viewer_id if viewer_id.is_a?(Integer)
772
+ opts[:source_ids] = options[:source_ids] if options[:source_ids]
773
+ opts[:start_time] = options[:start_time].to_i if options[:start_time]
774
+ opts[:end_time] = options[:end_time].to_i if options[:end_time]
775
+ opts[:limit] = options[:limit] if options[:limit].is_a?(Integer)
776
+ opts[:metadata] = Facebooker.json_encode(options[:metadata]) if options[:metadata]
777
+ opts
778
+ end
779
+ end
780
+
781
+ class CanvasSession < Session
782
+ def default_login_url_options
783
+ {:canvas => true}
784
+ end
785
+ end
786
+ end