fs-facebooker 1.0.37

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 (135) hide show
  1. data/.autotest +15 -0
  2. data/CHANGELOG.rdoc +24 -0
  3. data/COPYING.rdoc +19 -0
  4. data/Manifest.txt +133 -0
  5. data/README.rdoc +104 -0
  6. data/Rakefile +85 -0
  7. data/TODO.rdoc +4 -0
  8. data/examples/desktop_login.rb +14 -0
  9. data/facebooker.gemspec +38 -0
  10. data/generators/facebook/facebook_generator.rb +14 -0
  11. data/generators/facebook/templates/config/facebooker.yml +49 -0
  12. data/generators/facebook/templates/public/javascripts/facebooker.js +83 -0
  13. data/generators/facebook_controller/USAGE +33 -0
  14. data/generators/facebook_controller/facebook_controller_generator.rb +40 -0
  15. data/generators/facebook_controller/templates/controller.rb +7 -0
  16. data/generators/facebook_controller/templates/functional_test.rb +11 -0
  17. data/generators/facebook_controller/templates/helper.rb +2 -0
  18. data/generators/facebook_controller/templates/view.fbml.erb +2 -0
  19. data/generators/facebook_controller/templates/view.html.erb +2 -0
  20. data/generators/facebook_publisher/facebook_publisher_generator.rb +14 -0
  21. data/generators/facebook_publisher/templates/create_facebook_templates.rb +15 -0
  22. data/generators/facebook_publisher/templates/publisher.rb +3 -0
  23. data/generators/facebook_scaffold/USAGE +27 -0
  24. data/generators/facebook_scaffold/facebook_scaffold_generator.rb +118 -0
  25. data/generators/facebook_scaffold/templates/controller.rb +93 -0
  26. data/generators/facebook_scaffold/templates/facebook_style.css +2579 -0
  27. data/generators/facebook_scaffold/templates/functional_test.rb +89 -0
  28. data/generators/facebook_scaffold/templates/helper.rb +2 -0
  29. data/generators/facebook_scaffold/templates/layout.fbml.erb +6 -0
  30. data/generators/facebook_scaffold/templates/layout.html.erb +17 -0
  31. data/generators/facebook_scaffold/templates/style.css +74 -0
  32. data/generators/facebook_scaffold/templates/view_edit.fbml.erb +13 -0
  33. data/generators/facebook_scaffold/templates/view_edit.html.erb +18 -0
  34. data/generators/facebook_scaffold/templates/view_index.fbml.erb +24 -0
  35. data/generators/facebook_scaffold/templates/view_index.html.erb +24 -0
  36. data/generators/facebook_scaffold/templates/view_new.fbml.erb +12 -0
  37. data/generators/facebook_scaffold/templates/view_new.html.erb +17 -0
  38. data/generators/facebook_scaffold/templates/view_show.fbml.erb +10 -0
  39. data/generators/facebook_scaffold/templates/view_show.html.erb +10 -0
  40. data/generators/publisher/publisher_generator.rb +14 -0
  41. data/generators/xd_receiver/templates/xd_receiver.html +10 -0
  42. data/generators/xd_receiver/xd_receiver_generator.rb +10 -0
  43. data/init.rb +25 -0
  44. data/install.rb +12 -0
  45. data/lib/facebooker.rb +179 -0
  46. data/lib/facebooker/adapters/adapter_base.rb +91 -0
  47. data/lib/facebooker/adapters/bebo_adapter.rb +77 -0
  48. data/lib/facebooker/adapters/facebook_adapter.rb +52 -0
  49. data/lib/facebooker/admin.rb +42 -0
  50. data/lib/facebooker/batch_request.rb +45 -0
  51. data/lib/facebooker/data.rb +57 -0
  52. data/lib/facebooker/feed.rb +78 -0
  53. data/lib/facebooker/logging.rb +44 -0
  54. data/lib/facebooker/mobile.rb +20 -0
  55. data/lib/facebooker/mock/service.rb +50 -0
  56. data/lib/facebooker/mock/session.rb +18 -0
  57. data/lib/facebooker/model.rb +139 -0
  58. data/lib/facebooker/models/affiliation.rb +10 -0
  59. data/lib/facebooker/models/album.rb +11 -0
  60. data/lib/facebooker/models/applicationproperties.rb +39 -0
  61. data/lib/facebooker/models/applicationrestrictions.rb +10 -0
  62. data/lib/facebooker/models/cookie.rb +10 -0
  63. data/lib/facebooker/models/education_info.rb +11 -0
  64. data/lib/facebooker/models/event.rb +28 -0
  65. data/lib/facebooker/models/friend_list.rb +16 -0
  66. data/lib/facebooker/models/group.rb +36 -0
  67. data/lib/facebooker/models/info_item.rb +10 -0
  68. data/lib/facebooker/models/info_section.rb +10 -0
  69. data/lib/facebooker/models/location.rb +8 -0
  70. data/lib/facebooker/models/notifications.rb +17 -0
  71. data/lib/facebooker/models/page.rb +28 -0
  72. data/lib/facebooker/models/photo.rb +19 -0
  73. data/lib/facebooker/models/tag.rb +12 -0
  74. data/lib/facebooker/models/user.rb +497 -0
  75. data/lib/facebooker/models/video.rb +9 -0
  76. data/lib/facebooker/models/work_info.rb +10 -0
  77. data/lib/facebooker/parser.rb +642 -0
  78. data/lib/facebooker/rails/backwards_compatible_param_checks.rb +31 -0
  79. data/lib/facebooker/rails/controller.rb +344 -0
  80. data/lib/facebooker/rails/cucumber.rb +28 -0
  81. data/lib/facebooker/rails/cucumber/world.rb +46 -0
  82. data/lib/facebooker/rails/extensions/action_controller.rb +48 -0
  83. data/lib/facebooker/rails/extensions/rack_setup.rb +6 -0
  84. data/lib/facebooker/rails/extensions/routing.rb +15 -0
  85. data/lib/facebooker/rails/facebook_form_builder.rb +112 -0
  86. data/lib/facebooker/rails/facebook_pretty_errors.rb +22 -0
  87. data/lib/facebooker/rails/facebook_request_fix.rb +30 -0
  88. data/lib/facebooker/rails/facebook_request_fix_2-3.rb +31 -0
  89. data/lib/facebooker/rails/facebook_session_handling.rb +68 -0
  90. data/lib/facebooker/rails/facebook_url_helper.rb +192 -0
  91. data/lib/facebooker/rails/facebook_url_rewriting.rb +60 -0
  92. data/lib/facebooker/rails/helpers.rb +794 -0
  93. data/lib/facebooker/rails/helpers/fb_connect.rb +118 -0
  94. data/lib/facebooker/rails/integration_session.rb +38 -0
  95. data/lib/facebooker/rails/profile_publisher_extensions.rb +42 -0
  96. data/lib/facebooker/rails/publisher.rb +550 -0
  97. data/lib/facebooker/rails/routing.rb +49 -0
  98. data/lib/facebooker/rails/test_helpers.rb +68 -0
  99. data/lib/facebooker/rails/utilities.rb +22 -0
  100. data/lib/facebooker/server_cache.rb +24 -0
  101. data/lib/facebooker/service.rb +102 -0
  102. data/lib/facebooker/session.rb +606 -0
  103. data/lib/facebooker/version.rb +9 -0
  104. data/lib/net/http_multipart_post.rb +123 -0
  105. data/lib/rack/facebook.rb +77 -0
  106. data/lib/tasks/facebooker.rake +18 -0
  107. data/lib/tasks/tunnel.rake +46 -0
  108. data/rails/init.rb +1 -0
  109. data/setup.rb +1585 -0
  110. data/templates/layout.erb +24 -0
  111. data/test/facebooker/adapters_test.rb +96 -0
  112. data/test/facebooker/admin_test.rb +102 -0
  113. data/test/facebooker/batch_request_test.rb +83 -0
  114. data/test/facebooker/data_test.rb +86 -0
  115. data/test/facebooker/logging_test.rb +43 -0
  116. data/test/facebooker/mobile_test.rb +45 -0
  117. data/test/facebooker/model_test.rb +133 -0
  118. data/test/facebooker/models/event_test.rb +15 -0
  119. data/test/facebooker/models/photo_test.rb +16 -0
  120. data/test/facebooker/models/user_test.rb +343 -0
  121. data/test/facebooker/rails/facebook_request_fix_2-3_test.rb +24 -0
  122. data/test/facebooker/rails/facebook_url_rewriting_test.rb +39 -0
  123. data/test/facebooker/rails/publisher_test.rb +481 -0
  124. data/test/facebooker/rails_integration_test.rb +1398 -0
  125. data/test/facebooker/server_cache_test.rb +44 -0
  126. data/test/facebooker/session_test.rb +614 -0
  127. data/test/facebooker_test.rb +951 -0
  128. data/test/fixtures/multipart_post_body_with_only_parameters.txt +33 -0
  129. data/test/fixtures/multipart_post_body_with_single_file.txt +38 -0
  130. data/test/fixtures/multipart_post_body_with_single_file_that_has_nil_key.txt +38 -0
  131. data/test/net/http_multipart_post_test.rb +52 -0
  132. data/test/rack/facebook_test.rb +61 -0
  133. data/test/rails_test_helper.rb +27 -0
  134. data/test/test_helper.rb +74 -0
  135. metadata +232 -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={})
15
+ facebook_verb(:get,path,params)
16
+ end
17
+
18
+ def facebook_post(path,params={})
19
+ facebook_verb(:post,path,params)
20
+ end
21
+
22
+ def facebook_put(path,params={})
23
+ facebook_verb(:put,path,params)
24
+ end
25
+
26
+ def facebook_delete(path,params={})
27
+ facebook_verb(:delete,path,params)
28
+ end
29
+
30
+ def facebook_verb(verb,path, params={})
31
+ send verb, path, facebook_params(params).reverse_merge(:canvas => true)
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,102 @@
1
+ begin
2
+ require 'curb'
3
+ Facebooker.use_curl = true
4
+ rescue LoadError
5
+ require 'net/http'
6
+ end
7
+ require 'facebooker/parser'
8
+ module Facebooker
9
+ class Service
10
+ def initialize(api_base, api_path, api_key)
11
+ @api_base = api_base
12
+ @api_path = api_path
13
+ @api_key = api_key
14
+ end
15
+
16
+ # TODO: support ssl
17
+ def post(params)
18
+ attempt = 0
19
+ Parser.parse(params[:method], post_form(url,params) )
20
+ rescue Errno::ECONNRESET, EOFError
21
+ if attempt == 0
22
+ attempt += 1
23
+ retry
24
+ end
25
+ end
26
+
27
+ def post_form(url,params)
28
+ if Facebooker.use_curl?
29
+ post_form_with_curl(url,params)
30
+ else
31
+ post_form_with_net_http(url,params)
32
+ end
33
+ end
34
+
35
+ def post_form_with_net_http(url,params)
36
+ post_params = {}
37
+ params.each do |k,v|
38
+ post_params[k] = v
39
+ post_params[k] = Facebooker.json_encode(v) if Array === v
40
+ end
41
+ Net::HTTP.post_form(url, post_params)
42
+ end
43
+
44
+ def post_form_with_curl(url,params,multipart=false)
45
+ response = Curl::Easy.http_post(url.to_s, *to_curb_params(params)) do |c|
46
+ c.multipart_form_post = multipart
47
+ c.timeout = Facebooker.timeout
48
+ end
49
+ response.body_str
50
+ end
51
+
52
+ def post_multipart_form(url,params)
53
+ if Facebooker.use_curl?
54
+ post_form_with_curl(url,params,true)
55
+ else
56
+ post_multipart_form_with_net_http(url,params)
57
+ end
58
+ end
59
+
60
+ def post_multipart_form_with_net_http(url,params)
61
+ Net::HTTP.post_multipart_form(url, params)
62
+ end
63
+
64
+ def post_file(params)
65
+ service_url = url(params.delete(:base))
66
+ result = post_multipart_form(service_url, params)
67
+ Parser.parse(params[:method], result)
68
+ end
69
+
70
+ private
71
+ def url(base = nil)
72
+ base ||= @api_base
73
+ URI.parse('http://'+ base + @api_path)
74
+ end
75
+
76
+ # Net::HTTP::MultipartPostFile
77
+ def multipart_post_file?(object)
78
+ object.respond_to?(:content_type) &&
79
+ object.respond_to?(:data) &&
80
+ object.respond_to?(:filename)
81
+ end
82
+
83
+ def to_curb_params(params)
84
+ parray = []
85
+ params.each_pair do |k,v|
86
+ if multipart_post_file?(v)
87
+ # Curl doesn't like blank field names
88
+ field = Curl::PostField.file((k.blank? ? 'xxx' : k.to_s), nil, File.basename(v.filename))
89
+ field.content_type = v.content_type
90
+ field.content = v.data
91
+ parray << field
92
+ else
93
+ parray << Curl::PostField.content(
94
+ k.to_s,
95
+ Array === v ? Facebooker.json_encode(v) : v.to_s
96
+ )
97
+ end
98
+ end
99
+ parray
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,606 @@
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 InvalidAlbumId < StandardError; end
54
+ class AlbumIsFull < StandardError; end
55
+ class MissingOrInvalidImageFile < StandardError; end
56
+ class TooManyUnapprovedPhotosPending < StandardError; end
57
+ class ExtendedPermissionRequired < StandardError; end
58
+ class InvalidFriendList < StandardError; end
59
+ class UserUnRegistrationFailed < StandardError
60
+ attr_accessor :failed_users
61
+ end
62
+ class UserRegistrationFailed < StandardError
63
+ attr_accessor :failed_users
64
+ end
65
+
66
+ API_SERVER_BASE_URL = ENV["FACEBOOKER_API"] == "new" ? "api.new.facebook.com" : "api.facebook.com"
67
+ API_PATH_REST = "/restserver.php"
68
+ WWW_SERVER_BASE_URL = ENV["FACEBOOKER_API"] == "new" ? "www.new.facebook.com" : "www.facebook.com"
69
+ WWW_PATH_LOGIN = "/login.php"
70
+ WWW_PATH_ADD = "/add.php"
71
+ WWW_PATH_INSTALL = "/install.php"
72
+
73
+ attr_writer :auth_token
74
+ attr_reader :session_key
75
+
76
+ def self.create(api_key=nil, secret_key=nil)
77
+ api_key ||= self.api_key
78
+ secret_key ||= self.secret_key
79
+ raise ArgumentError unless !api_key.nil? && !secret_key.nil?
80
+ new(api_key, secret_key)
81
+ end
82
+
83
+ def self.api_key
84
+ extract_key_from_environment(:api) || extract_key_from_configuration_file(:api) rescue report_inability_to_find_key(:api)
85
+ end
86
+
87
+ def self.secret_key
88
+ extract_key_from_environment(:secret) || extract_key_from_configuration_file(:secret) rescue report_inability_to_find_key(:secret)
89
+ end
90
+
91
+ def self.current
92
+ Thread.current['facebook_session']
93
+ end
94
+
95
+ def self.current=(session)
96
+ Thread.current['facebook_session'] = session
97
+ end
98
+
99
+ def login_url(options={})
100
+ options = default_login_url_options.merge(options)
101
+ "#{Facebooker.login_url_base(@api_key)}#{login_url_optional_parameters(options)}"
102
+ end
103
+
104
+ def install_url(options={})
105
+ "#{Facebooker.install_url_base(@api_key)}#{install_url_optional_parameters(options)}"
106
+ end
107
+
108
+ # The url to get user to approve extended permissions
109
+ # http://wiki.developers.facebook.com/index.php/Extended_permission
110
+ #
111
+ # permissions:
112
+ # * email
113
+ # * offline_access
114
+ # * status_update
115
+ # * photo_upload
116
+ # * video_upload
117
+ # * create_listing
118
+ # * create_event
119
+ # * rsvp_event
120
+ # * sms
121
+ def permission_url(permission,options={})
122
+ options = default_login_url_options.merge(options)
123
+ "http://#{Facebooker.www_server_base_url}/authorize.php?api_key=#{@api_key}&v=1.0&ext_perm=#{permission}#{install_url_optional_parameters(options)}"
124
+ end
125
+
126
+ def install_url_optional_parameters(options)
127
+ optional_parameters = []
128
+ optional_parameters += add_next_parameters(options)
129
+ optional_parameters.join
130
+ end
131
+
132
+ def add_next_parameters(options)
133
+ opts = []
134
+ opts << "&next=#{CGI.escape(options[:next])}" if options[:next]
135
+ opts << "&next_cancel=#{CGI.escape(options[:next_cancel])}" if options[:next_cancel]
136
+ opts
137
+ end
138
+
139
+ def login_url_optional_parameters(options)
140
+ # It is important that unused options are omitted as stuff like &canvas=false will still display the canvas.
141
+ optional_parameters = []
142
+ optional_parameters += add_next_parameters(options)
143
+ optional_parameters << "&skipcookie=true" if options[:skip_cookie]
144
+ optional_parameters << "&hide_checkbox=true" if options[:hide_checkbox]
145
+ optional_parameters << "&canvas=true" if options[:canvas]
146
+ optional_parameters.join
147
+ end
148
+
149
+ def default_login_url_options
150
+ {}
151
+ end
152
+
153
+ def initialize(api_key, secret_key)
154
+ @api_key = api_key
155
+ @secret_key = secret_key
156
+ @batch_request = nil
157
+ @session_key = nil
158
+ @uid = nil
159
+ @auth_token = nil
160
+ @secret_from_session = nil
161
+ @expires = nil
162
+ end
163
+
164
+ def secret_for_method(method_name)
165
+ @secret_key
166
+ end
167
+
168
+ def auth_token
169
+ @auth_token ||= post 'facebook.auth.createToken'
170
+ end
171
+
172
+ def infinite?
173
+ @expires == 0
174
+ end
175
+
176
+ def expired?
177
+ @expires.nil? || (!infinite? && Time.at(@expires) <= Time.now)
178
+ end
179
+
180
+ def secured?
181
+ !@session_key.nil? && !expired?
182
+ end
183
+
184
+ def secure!
185
+ response = post 'facebook.auth.getSession', :auth_token => auth_token
186
+ secure_with!(response['session_key'], response['uid'], response['expires'], response['secret'])
187
+ end
188
+
189
+ def secure_with!(session_key, uid = nil, expires = nil, secret_from_session = nil)
190
+ @session_key = session_key
191
+ @uid = uid ? Integer(uid) : post('facebook.users.getLoggedInUser', :session_key => session_key)
192
+ @expires = Integer(expires)
193
+ @secret_from_session = secret_from_session
194
+ end
195
+
196
+ def fql_query(query, format = 'XML')
197
+ post('facebook.fql.query', :query => query, :format => format) do |response|
198
+ type = response.shift
199
+ return [] if type.nil?
200
+ response.shift.map do |hash|
201
+ case type
202
+ when 'user'
203
+ user = User.new
204
+ user.session = self
205
+ user.populate_from_hash!(hash)
206
+ user
207
+ when 'photo'
208
+ Photo.from_hash(hash)
209
+ when 'page'
210
+ Page.from_hash(hash)
211
+ when 'page_admin'
212
+ Page.from_hash(hash)
213
+ when 'event_member'
214
+ Event::Attendance.from_hash(hash)
215
+ else
216
+ hash
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ def user
223
+ @user ||= User.new(uid, self)
224
+ end
225
+
226
+ #
227
+ # This one has so many parameters, a Hash seemed cleaner than a long param list. Options can be:
228
+ # :uid => Filter by events associated with a user with this uid
229
+ # :eids => Filter by this list of event ids. This is a comma-separated list of eids.
230
+ # :start_time => Filter with this UTC as lower bound. A missing or zero parameter indicates no lower bound. (Time or Integer)
231
+ # :end_time => Filter with this UTC as upper bound. A missing or zero parameter indicates no upper bound. (Time or Integer)
232
+ # :rsvp_status => Filter by this RSVP status.
233
+ def events(options = {})
234
+ @events ||= post('facebook.events.get', options) do |response|
235
+ response.map do |hash|
236
+ Event.from_hash(hash)
237
+ end
238
+ end
239
+ end
240
+
241
+ def event_members(eid)
242
+ @members ||= post('facebook.events.getMembers', :eid => eid) do |response|
243
+ response.map do |attendee_hash|
244
+ Event::Attendance.from_hash(attendee_hash)
245
+ end
246
+ end
247
+ end
248
+
249
+ def users_standard(user_ids, fields=[])
250
+ post("facebook.users.getStandardInfo",:uids=>user_ids.join(","),:fields=>User.standard_fields(fields)) do |users|
251
+ users.map { |u| User.new(u)}
252
+ end
253
+ end
254
+
255
+ def users(user_ids, fields=[])
256
+ post("facebook.users.getInfo",:uids=>user_ids.join(","),:fields=>User.user_fields(fields)) do |users|
257
+ users.map { |u| User.new(u)}
258
+ end
259
+ end
260
+
261
+ def pages(options = {})
262
+ raise ArgumentError, 'fields option is mandatory' unless options.has_key?(:fields)
263
+ @pages ||= {}
264
+ @pages[options] ||= post('facebook.pages.getInfo', options) do |response|
265
+ response.map do |hash|
266
+ Page.from_hash(hash)
267
+ end
268
+ end
269
+ end
270
+
271
+ # Takes page_id and uid, returns true if uid is a fan of the page_id
272
+ def is_fan(page_id, uid)
273
+ post('facebook.pages.isFan', :page_id=>page_id, :uid=>uid)
274
+ end
275
+
276
+ #
277
+ # Returns a proxy object for handling calls to Facebook cached items
278
+ # such as images and FBML ref handles
279
+ def server_cache
280
+ Facebooker::ServerCache.new(self)
281
+ end
282
+
283
+ #
284
+ # Returns a proxy object for handling calls to the Facebook Data API
285
+ def data
286
+ Facebooker::Data.new(self)
287
+ end
288
+
289
+ def admin
290
+ Facebooker::Admin.new(self)
291
+ end
292
+
293
+ def mobile
294
+ Facebooker::Mobile.new(self)
295
+ end
296
+
297
+ #
298
+ # Given an array like:
299
+ # [[userid, otheruserid], [yetanotherid, andanotherid]]
300
+ # returns a Hash indicating friendship of those pairs:
301
+ # {[userid, otheruserid] => true, [yetanotherid, andanotherid] => false}
302
+ # if one of the Hash values is nil, it means the facebook platform's answer is "I don't know"
303
+ def check_friendship(array_of_pairs_of_users)
304
+ uids1 = []
305
+ uids2 = []
306
+ array_of_pairs_of_users.each do |pair|
307
+ uids1 << pair.first
308
+ uids2 << pair.last
309
+ end
310
+ post('facebook.friends.areFriends', :uids1 => uids1.join(','), :uids2 => uids2.join(','))
311
+ end
312
+
313
+ def get_photos(pids = nil, subj_id = nil, aid = nil)
314
+ if [subj_id, pids, aid].all? {|arg| arg.nil?}
315
+ raise ArgumentError, "Can't get a photo without a picture, album or subject ID"
316
+ end
317
+ @photos = post('facebook.photos.get', :subj_id => subj_id, :pids => pids, :aid => aid ) do |response|
318
+ response.map do |hash|
319
+ Photo.from_hash(hash)
320
+ end
321
+ end
322
+ end
323
+
324
+ def get_albums(aids)
325
+ @albums = post('facebook.photos.getAlbums', :aids => aids) do |response|
326
+ response.map do |hash|
327
+ Album.from_hash(hash)
328
+ end
329
+ end
330
+ end
331
+
332
+ def get_tags(pids)
333
+ @tags = post('facebook.photos.getTags', :pids => pids) do |response|
334
+ response.map do |hash|
335
+ Tag.from_hash(hash)
336
+ end
337
+ end
338
+ end
339
+
340
+ def add_tags(pid, x, y, tag_uid = nil, tag_text = nil )
341
+ if [tag_uid, tag_text].all? {|arg| arg.nil?}
342
+ raise ArgumentError, "Must enter a name or string for this tag"
343
+ end
344
+ @tags = post('facebook.photos.addTag', :pid => pid, :tag_uid => tag_uid, :tag_text => tag_text, :x => x, :y => y )
345
+ end
346
+
347
+ def send_notification(user_ids, fbml, email_fbml = nil)
348
+ params = {:notification => fbml, :to_ids => user_ids.map{ |id| User.cast_to_facebook_id(id)}.join(',')}
349
+ if email_fbml
350
+ params[:email] = email_fbml
351
+ end
352
+ params[:type]="user_to_user"
353
+ # if there is no uid, this is an announcement
354
+ unless uid?
355
+ params[:type]="app_to_user"
356
+ end
357
+
358
+ post 'facebook.notifications.send', params,uid?
359
+ end
360
+
361
+ ##
362
+ # Register a template bundle with Facebook.
363
+ # returns the template id to use to send using this template
364
+ def register_template_bundle(one_line_story_templates,short_story_templates=nil,full_story_template=nil, action_links=nil)
365
+ parameters = {:one_line_story_templates => Array(one_line_story_templates).to_json}
366
+
367
+ parameters[:action_links] = action_links.to_json unless action_links.blank?
368
+
369
+ parameters[:short_story_templates] = Array(short_story_templates).to_json unless short_story_templates.blank?
370
+
371
+ parameters[:full_story_template] = full_story_template.to_json unless full_story_template.blank?
372
+
373
+ post("facebook.feed.registerTemplateBundle", parameters, false)
374
+ end
375
+
376
+ ##
377
+ # publish a previously rendered template bundle
378
+ # see http://wiki.developers.facebook.com/index.php/Feed.publishUserAction
379
+ #
380
+ def publish_user_action(bundle_id,data={},target_ids=nil,body_general=nil,story_size=nil)
381
+ parameters={:template_bundle_id=>bundle_id,:template_data=>data.to_json}
382
+ parameters[:target_ids] = target_ids unless target_ids.blank?
383
+ parameters[:body_general] = body_general unless body_general.blank?
384
+ parameters[:story_size] = story_size unless story_size.nil?
385
+ post("facebook.feed.publishUserAction", parameters)
386
+ end
387
+
388
+
389
+ ##
390
+ # Send email to as many as 100 users at a time
391
+ def send_email(user_ids, subject, text, fbml = nil)
392
+ user_ids = Array(user_ids)
393
+ params = {:fbml => fbml, :recipients => user_ids.map{ |id| User.cast_to_facebook_id(id)}.join(','), :text => text, :subject => subject}
394
+ post 'facebook.notifications.sendEmail', params
395
+ end
396
+
397
+ # Only serialize the bare minimum to recreate the session.
398
+ def marshal_load(variables)#:nodoc:
399
+ fields_to_serialize.each_with_index{|field, index| instance_variable_set_value(field, variables[index])}
400
+ end
401
+
402
+ # Only serialize the bare minimum to recreate the session.
403
+ def marshal_dump#:nodoc:
404
+ fields_to_serialize.map{|field| instance_variable_value(field)}
405
+ end
406
+
407
+ # Only serialize the bare minimum to recreate the session.
408
+ def to_yaml( opts = {} )
409
+ YAML::quick_emit(self.object_id, opts) do |out|
410
+ out.map(taguri) do |map|
411
+ fields_to_serialize.each do |field|
412
+ map.add(field, instance_variable_value(field))
413
+ end
414
+ end
415
+ end
416
+ end
417
+
418
+ def instance_variable_set_value(field, value)
419
+ self.instance_variable_set("@#{field}", value)
420
+ end
421
+
422
+ def instance_variable_value(field)
423
+ self.instance_variable_get("@#{field}")
424
+ end
425
+
426
+ def fields_to_serialize
427
+ %w(session_key uid expires secret_from_session auth_token api_key secret_key)
428
+ end
429
+
430
+ class Desktop < Session
431
+ def login_url
432
+ super + "&auth_token=#{auth_token}"
433
+ end
434
+
435
+ def secret_for_method(method_name)
436
+ secret = auth_request_methods.include?(method_name) ? super : @secret_from_session
437
+ secret
438
+ end
439
+
440
+ def post(method, params = {},use_session=false)
441
+ if method == 'facebook.profile.getFBML' || method == 'facebook.profile.setFBML'
442
+ raise NonSessionUser.new("User #{@uid} is not the logged in user.") unless @uid == params[:uid]
443
+ end
444
+ super
445
+ end
446
+ private
447
+ def auth_request_methods
448
+ ['facebook.auth.getSession', 'facebook.auth.createToken']
449
+ end
450
+ end
451
+
452
+ def batch_request?
453
+ @batch_request
454
+ end
455
+
456
+ def add_to_batch(req,&proc)
457
+ batch_request = BatchRequest.new(req,proc)
458
+ Thread.current[:facebooker_current_batch_queue]<<batch_request
459
+ batch_request
460
+ end
461
+
462
+ # Submit the enclosed requests for this session inside a batch
463
+ #
464
+ # All requests will be sent to Facebook at the end of the block
465
+ # each method inside the block will return a proxy object
466
+ # attempting to access the proxy before the end of the block will yield an exception
467
+ #
468
+ # For Example:
469
+ #
470
+ # facebook_session.batch do
471
+ # @send_result = facebook_session.send_notification([12451752],"Woohoo")
472
+ # @albums = facebook_session.user.albums
473
+ # end
474
+ # puts @albums.first.inspect
475
+ #
476
+ # is valid, however
477
+ #
478
+ # facebook_session.batch do
479
+ # @send_result = facebook_session.send_notification([12451752],"Woohoo")
480
+ # @albums = facebook_session.user.albums
481
+ # puts @albums.first.inspect
482
+ # end
483
+ #
484
+ # will raise Facebooker::BatchRequest::UnexecutedRequest
485
+ #
486
+ # If an exception is raised while processing the result, that exception will be
487
+ # re-raised on the next access to that object or when exception_raised? is called
488
+ #
489
+ # for example, if the send_notification resulted in TooManyUserCalls being raised,
490
+ # calling
491
+ # @send_result.exception_raised?
492
+ # would re-raise that exception
493
+ # if there was an error retrieving the albums, it would be re-raised when
494
+ # @albums.first
495
+ # is called
496
+ #
497
+ def batch(serial_only=false)
498
+ @batch_request=true
499
+ Thread.current[:facebooker_current_batch_queue]=[]
500
+ yield
501
+ # Set the batch request to false so that post will execute the batch job
502
+ @batch_request=false
503
+ BatchRun.current_batch=Thread.current[:facebooker_current_batch_queue]
504
+ post("facebook.batch.run",:method_feed=>BatchRun.current_batch.map{|q| q.uri}.to_json,:serial_only=>serial_only.to_s)
505
+ ensure
506
+ @batch_request=false
507
+ BatchRun.current_batch=nil
508
+ end
509
+
510
+ def post_without_logging(method, params = {}, use_session_key = true, &proc)
511
+ add_facebook_params(params, method)
512
+ use_session_key && @session_key && params[:session_key] ||= @session_key
513
+ final_params=params.merge(:sig => signature_for(params))
514
+ if batch_request?
515
+ add_to_batch(final_params,&proc)
516
+ else
517
+ result = service.post(final_params)
518
+ result = yield result if block_given?
519
+ result
520
+ end
521
+ end
522
+
523
+ def post(method, params = {}, use_session_key = true, &proc)
524
+ if batch_request?
525
+ post_without_logging(method, params, use_session_key, &proc)
526
+ else
527
+ Logging.log_fb_api(method, params) do
528
+ post_without_logging(method, params, use_session_key, &proc)
529
+ end
530
+ end
531
+ end
532
+
533
+ def post_file(method, params = {})
534
+ base = params.delete(:base)
535
+ Logging.log_fb_api(method, params) do
536
+ add_facebook_params(params, method)
537
+ @session_key && params[:session_key] ||= @session_key unless params[:uid]
538
+ service.post_file(params.merge(:base => base, :sig => signature_for(params.reject{|key, value| key.nil?})))
539
+ end
540
+ end
541
+
542
+
543
+ @configuration_file_path = nil
544
+
545
+ def self.configuration_file_path
546
+ @configuration_file_path || File.expand_path("~/.facebookerrc")
547
+ end
548
+
549
+ def self.configuration_file_path=(path)
550
+ @configuration_file_path = path
551
+ end
552
+
553
+ private
554
+ def add_facebook_params(hash, method)
555
+ hash[:method] = method
556
+ hash[:api_key] = @api_key
557
+ hash[:call_id] = Time.now.to_f.to_s unless method == 'facebook.auth.getSession'
558
+ hash[:v] = "1.0"
559
+ end
560
+
561
+ # This ultimately delgates to the adapter
562
+ def self.extract_key_from_environment(key_name)
563
+ Facebooker.send(key_name.to_s + "_key") rescue nil
564
+ end
565
+
566
+ def self.extract_key_from_configuration_file(key_name)
567
+ read_configuration_file[key_name]
568
+ end
569
+
570
+ def self.report_inability_to_find_key(key_name)
571
+ raise ConfigurationMissing, "Could not find configuration information for #{key_name}"
572
+ end
573
+
574
+ def self.read_configuration_file
575
+ eval(File.read(configuration_file_path))
576
+ end
577
+
578
+ def service
579
+ @service ||= Service.new(Facebooker.api_server_base, Facebooker.api_rest_path, @api_key)
580
+ end
581
+
582
+ def uid
583
+ @uid || (secure!; @uid)
584
+ end
585
+
586
+ def uid?
587
+ !! @uid
588
+ end
589
+
590
+ def signature_for(params)
591
+ raw_string = params.inject([]) do |collection, pair|
592
+ collection << pair.map { |x|
593
+ Array === x ? Facebooker.json_encode(x) : x
594
+ }.join("=")
595
+ collection
596
+ end.sort.join
597
+ Digest::MD5.hexdigest([raw_string, secret_for_method(params[:method])].join)
598
+ end
599
+ end
600
+
601
+ class CanvasSession < Session
602
+ def default_login_url_options
603
+ {:canvas => true}
604
+ end
605
+ end
606
+ end