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
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
18 class UnknownError < StandardError; end
19 class SpecialUserAuthorizationRequired < StandardError; end
20 class ServiceUnavailable < StandardError; end
21 class MaxRequestsDepleted < StandardError; end
22 class HostNotAllowed < StandardError; end
23 class MissingOrInvalidParameter < StandardError; end
24 class InvalidAPIKey < StandardError; end
25 class SessionExpired < StandardError; end
26 class CallOutOfOrder < StandardError; end
27 class IncorrectSignature < StandardError; end
28 class SignatureTooOld < StandardError; end
29 class TooManyUserCalls < StandardError; end
30 class TooManyUserActionCalls < StandardError; end
31 class InvalidFeedTitleLink < StandardError; end
32 class InvalidFeedTitleLength < StandardError; end
33 class InvalidFeedTitleName < StandardError; end
34 class BlankFeedTitle < StandardError; end
35 class FeedBodyLengthTooLong < StandardError; end
36 class InvalidFeedPhotoSource < StandardError; end
37 class InvalidFeedPhotoLink < StandardError; end
38 class TemplateDataMissingRequiredTokens < StandardError; end
39 class FeedMarkupInvalid < StandardError; end
40 class FeedTitleDataInvalid < StandardError; end
41 class FeedTitleTemplateInvalid < StandardError; end
42 class FeedBodyDataInvalid < StandardError; end
43 class FeedBodyTemplateInvalid < StandardError; end
44 class FeedPhotosNotRetrieved < StandardError; end
45 class FeedTargetIdsInvalid < StandardError; end
46 class TemplateBundleInvalid < StandardError; end
47 class ConfigurationMissing < StandardError; end
48 class FQLParseError < StandardError; end
49 class FQLFieldDoesNotExist < StandardError; end
50 class FQLTableDoesNotExist < StandardError; end
51 class FQLStatementNotIndexable < StandardError; end
52 class FQLFunctionDoesNotExist < StandardError; end
53 class FQLWrongNumberArgumentsPassedToFunction < 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 InvalidFriendList < StandardError; end
60 class UserUnRegistrationFailed < StandardError
61 attr_accessor :failed_users
63 class UserRegistrationFailed < StandardError
64 attr_accessor :failed_users
67 API_SERVER_BASE_URL = ENV["FACEBOOKER_API"] == "new" ? "api.new.facebook.com" : "api.facebook.com"
68 API_PATH_REST = "/restserver.php"
69 WWW_SERVER_BASE_URL = ENV["FACEBOOKER_API"] == "new" ? "www.new.facebook.com" : "www.facebook.com"
70 WWW_PATH_LOGIN = "/login.php"
71 WWW_PATH_ADD = "/add.php"
72 WWW_PATH_INSTALL = "/install.php"
74 attr_writer :auth_token
75 attr_accessor :session_key
76 attr_reader :secret_from_session
78 def self.create(api_key=nil, secret_key=nil)
79 api_key ||= self.api_key
80 secret_key ||= self.secret_key
81 raise ArgumentError unless !api_key.nil? && !secret_key.nil?
82 new(api_key, secret_key)
86 extract_key_from_environment(:api) || extract_key_from_configuration_file(:api) rescue report_inability_to_find_key(:api)
90 extract_key_from_environment(:secret) || extract_key_from_configuration_file(:secret) rescue report_inability_to_find_key(:secret)
94 Thread.current['facebook_session']
97 def self.current=(session)
98 Thread.current['facebook_session'] = session
101 def login_url(options={})
102 options = default_login_url_options.merge(options)
103 "#{Facebooker.login_url_base(@api_key)}#{login_url_optional_parameters(options)}"
106 def install_url(options={})
107 "#{Facebooker.install_url_base(@api_key)}#{install_url_optional_parameters(options)}"
110 # The url to get user to approve extended permissions
111 # http://wiki.developers.facebook.com/index.php/Extended_permission
123 def permission_url(permission,options={})
124 options = default_login_url_options.merge(options)
125 "http://#{Facebooker.www_server_base_url}/authorize.php?api_key=#{@api_key}&v=1.0&ext_perm=#{permission}#{install_url_optional_parameters(options)}"
128 def install_url_optional_parameters(options)
129 optional_parameters = []
130 optional_parameters += add_next_parameters(options)
131 optional_parameters.join
134 def add_next_parameters(options)
136 opts << "&next=#{CGI.escape(options[:next])}" if options[:next]
137 opts << "&next_cancel=#{CGI.escape(options[:next_cancel])}" if options[:next_cancel]
141 def login_url_optional_parameters(options)
142 # It is important that unused options are omitted as stuff like &canvas=false will still display the canvas.
143 optional_parameters = []
144 optional_parameters += add_next_parameters(options)
145 optional_parameters << "&skipcookie=true" if options[:skip_cookie]
146 optional_parameters << "&hide_checkbox=true" if options[:hide_checkbox]
147 optional_parameters << "&canvas=true" if options[:canvas]
148 optional_parameters.join
151 def default_login_url_options
155 def initialize(api_key, secret_key)
157 @secret_key = secret_key
162 @secret_from_session = nil
166 def secret_for_method(method_name)
171 @auth_token ||= post 'facebook.auth.createToken'
179 @expires.nil? || (!infinite? && Time.at(@expires) <= Time.now)
183 !@session_key.nil? && !expired?
186 def secure!(args = {})
187 response = post 'facebook.auth.getSession', :auth_token => auth_token, :generate_session_secret => args[:generate_session_secret] ? "1" : "0"
188 secure_with!(response['session_key'], response['uid'], response['expires'], response['secret'])
191 def secure_with_session_secret!
192 self.secure!(:generate_session_secret => true)
195 def secure_with!(session_key, uid = nil, expires = nil, secret_from_session = nil)
196 @session_key = session_key
197 @uid = uid ? Integer(uid) : post('facebook.users.getLoggedInUser', :session_key => session_key)
198 @expires = Integer(expires)
199 @secret_from_session = secret_from_session
202 def fql_build_object(type, hash)
207 user.populate_from_hash!(hash)
210 Photo.from_hash(hash)
216 Group.from_hash(hash)
218 Event::Attendance.from_hash(hash)
224 def fql_query(query, format = 'XML')
225 post('facebook.fql.query', :query => query, :format => format) do |response|
226 type = response.shift
227 return [] if type.nil?
228 response.shift.map do |hash|
229 fql_build_object(type, hash)
234 def fql_multiquery(queries, format = 'XML')
236 post('facebook.fql.multiquery', :queries => queries.to_json, :format => format) do |responses|
237 responses.each do |response|
238 name = response.shift
239 response = response.shift
240 type = response.shift
243 value = response.shift.map do |hash|
244 fql_build_object(type, hash)
247 results[name] = value
254 @user ||= User.new(uid, self)
258 # This one has so many parameters, a Hash seemed cleaner than a long param list. Options can be:
259 # :uid => Filter by events associated with a user with this uid
260 # :eids => Filter by this list of event ids. This is a comma-separated list of eids.
261 # :start_time => Filter with this UTC as lower bound. A missing or zero parameter indicates no lower bound. (Time or Integer)
262 # :end_time => Filter with this UTC as upper bound. A missing or zero parameter indicates no upper bound. (Time or Integer)
263 # :rsvp_status => Filter by this RSVP status.
264 def events(options = {})
265 @events ||= post('facebook.events.get', options) do |response|
266 response.map do |hash|
267 Event.from_hash(hash)
272 def event_members(eid)
273 @members ||= post('facebook.events.getMembers', :eid => eid) do |response|
274 response.map do |attendee_hash|
275 Event::Attendance.from_hash(attendee_hash)
280 def users_standard(user_ids, fields=[])
281 post("facebook.users.getStandardInfo",:uids=>user_ids.join(","),:fields=>User.standard_fields(fields)) do |users|
282 users.map { |u| User.new(u)}
286 def users(user_ids, fields=[])
287 post("facebook.users.getInfo",:uids=>user_ids.join(","),:fields=>User.user_fields(fields)) do |users|
288 users.map { |u| User.new(u)}
292 def pages(options = {})
293 raise ArgumentError, 'fields option is mandatory' unless options.has_key?(:fields)
295 @pages[options] ||= post('facebook.pages.getInfo', options) do |response|
296 response.map do |hash|
302 # Takes page_id and uid, returns true if uid is a fan of the page_id
303 def is_fan(page_id, uid)
304 post('facebook.pages.isFan', :page_id=>page_id, :uid=>uid)
308 # Returns a proxy object for handling calls to Facebook cached items
309 # such as images and FBML ref handles
311 Facebooker::ServerCache.new(self)
315 # Returns a proxy object for handling calls to the Facebook Data API
317 Facebooker::Data.new(self)
321 Facebooker::Admin.new(self)
325 Facebooker::Mobile.new(self)
329 # Given an array like:
330 # [[userid, otheruserid], [yetanotherid, andanotherid]]
331 # returns a Hash indicating friendship of those pairs:
332 # {[userid, otheruserid] => true, [yetanotherid, andanotherid] => false}
333 # if one of the Hash values is nil, it means the facebook platform's answer is "I don't know"
334 def check_friendship(array_of_pairs_of_users)
337 array_of_pairs_of_users.each do |pair|
341 post('facebook.friends.areFriends', :uids1 => uids1.join(','), :uids2 => uids2.join(','))
344 def get_photos(pids = nil, subj_id = nil, aid = nil)
345 if [subj_id, pids, aid].all? {|arg| arg.nil?}
346 raise ArgumentError, "Can't get a photo without a picture, album or subject ID"
348 @photos = post('facebook.photos.get', :subj_id => subj_id, :pids => pids, :aid => aid ) do |response|
349 response.map do |hash|
350 Photo.from_hash(hash)
356 @albums = post('facebook.photos.getAlbums', :aids => aids) do |response|
357 response.map do |hash|
358 Album.from_hash(hash)
364 @tags = post('facebook.photos.getTags', :pids => pids) do |response|
365 response.map do |hash|
371 def add_tags(pid, x, y, tag_uid = nil, tag_text = nil )
372 if [tag_uid, tag_text].all? {|arg| arg.nil?}
373 raise ArgumentError, "Must enter a name or string for this tag"
375 @tags = post('facebook.photos.addTag', :pid => pid, :tag_uid => tag_uid, :tag_text => tag_text, :x => x, :y => y )
378 def send_notification(user_ids, fbml, email_fbml = nil)
379 params = {:notification => fbml, :to_ids => user_ids.map{ |id| User.cast_to_facebook_id(id)}.join(',')}
381 params[:email] = email_fbml
383 params[:type]="user_to_user"
384 # if there is no uid, this is an announcement
386 params[:type]="app_to_user"
389 post 'facebook.notifications.send', params,uid?
393 # Register a template bundle with Facebook.
394 # returns the template id to use to send using this template
395 def register_template_bundle(one_line_story_templates,short_story_templates=nil,full_story_template=nil, action_links=nil)
396 parameters = {:one_line_story_templates => Array(one_line_story_templates).to_json}
398 parameters[:action_links] = action_links.to_json unless action_links.blank?
400 parameters[:short_story_templates] = Array(short_story_templates).to_json unless short_story_templates.blank?
402 parameters[:full_story_template] = full_story_template.to_json unless full_story_template.blank?
404 post("facebook.feed.registerTemplateBundle", parameters, false)
408 # publish a previously rendered template bundle
409 # see http://wiki.developers.facebook.com/index.php/Feed.publishUserAction
411 def publish_user_action(bundle_id,data={},target_ids=nil,body_general=nil,story_size=nil)
412 parameters={:template_bundle_id=>bundle_id,:template_data=>data.to_json}
413 parameters[:target_ids] = target_ids unless target_ids.blank?
414 parameters[:body_general] = body_general unless body_general.blank?
415 parameters[:story_size] = story_size unless story_size.nil?
416 post("facebook.feed.publishUserAction", parameters)
421 # Send email to as many as 100 users at a time
422 def send_email(user_ids, subject, text, fbml = nil)
423 user_ids = Array(user_ids)
424 params = {:fbml => fbml, :recipients => user_ids.map{ |id| User.cast_to_facebook_id(id)}.join(','), :text => text, :subject => subject}
425 post 'facebook.notifications.sendEmail', params
428 # Only serialize the bare minimum to recreate the session.
429 def marshal_load(variables)#:nodoc:
430 fields_to_serialize.each_with_index{|field, index| instance_variable_set_value(field, variables[index])}
433 # Only serialize the bare minimum to recreate the session.
434 def marshal_dump#:nodoc:
435 fields_to_serialize.map{|field| instance_variable_value(field)}
438 # Only serialize the bare minimum to recreate the session.
439 def to_yaml( opts = {} )
440 YAML::quick_emit(self.object_id, opts) do |out|
441 out.map(taguri) do |map|
442 fields_to_serialize.each do |field|
443 map.add(field, instance_variable_value(field))
449 def instance_variable_set_value(field, value)
450 self.instance_variable_set("@#{field}", value)
453 def instance_variable_value(field)
454 self.instance_variable_get("@#{field}")
457 def fields_to_serialize
458 %w(session_key uid expires secret_from_session auth_token api_key secret_key)
461 class Desktop < Session
463 super + "&auth_token=#{auth_token}"
466 def secret_for_method(method_name)
467 secret = auth_request_methods.include?(method_name) ? super : @secret_from_session
471 def post(method, params = {},use_session=false)
472 if method == 'facebook.profile.getFBML' || method == 'facebook.profile.setFBML'
473 raise NonSessionUser.new("User #{@uid} is not the logged in user.") unless @uid == params[:uid]
478 def auth_request_methods
479 ['facebook.auth.getSession', 'facebook.auth.createToken']
487 def add_to_batch(req,&proc)
488 batch_request = BatchRequest.new(req,proc)
489 Thread.current[:facebooker_current_batch_queue]<<batch_request
493 # Submit the enclosed requests for this session inside a batch
495 # All requests will be sent to Facebook at the end of the block
496 # each method inside the block will return a proxy object
497 # attempting to access the proxy before the end of the block will yield an exception
501 # facebook_session.batch do
502 # @send_result = facebook_session.send_notification([12451752],"Woohoo")
503 # @albums = facebook_session.user.albums
505 # puts @albums.first.inspect
509 # facebook_session.batch do
510 # @send_result = facebook_session.send_notification([12451752],"Woohoo")
511 # @albums = facebook_session.user.albums
512 # puts @albums.first.inspect
515 # will raise Facebooker::BatchRequest::UnexecutedRequest
517 # If an exception is raised while processing the result, that exception will be
518 # re-raised on the next access to that object or when exception_raised? is called
520 # for example, if the send_notification resulted in TooManyUserCalls being raised,
522 # @send_result.exception_raised?
523 # would re-raise that exception
524 # if there was an error retrieving the albums, it would be re-raised when
528 def batch(serial_only=false)
530 Thread.current[:facebooker_current_batch_queue]=[]
532 # Set the batch request to false so that post will execute the batch job
534 BatchRun.current_batch=Thread.current[:facebooker_current_batch_queue]
535 post("facebook.batch.run",:method_feed=>BatchRun.current_batch.map{|q| q.uri}.to_json,:serial_only=>serial_only.to_s)
538 BatchRun.current_batch=nil
541 def post_without_logging(method, params = {}, use_session_key = true, &proc)
542 add_facebook_params(params, method)
543 use_session_key && @session_key && params[:session_key] ||= @session_key
544 final_params=params.merge(:sig => signature_for(params))
546 add_to_batch(final_params,&proc)
548 result = service.post(final_params)
549 result = yield result if block_given?
554 def post(method, params = {}, use_session_key = true, &proc)
556 post_without_logging(method, params, use_session_key, &proc)
558 Logging.log_fb_api(method, params) do
559 post_without_logging(method, params, use_session_key, &proc)
564 def post_file(method, params = {})
565 base = params.delete(:base)
566 Logging.log_fb_api(method, params) do
567 add_facebook_params(params, method)
568 @session_key && params[:session_key] ||= @session_key unless params[:uid]
569 service.post_file(params.merge(:base => base, :sig => signature_for(params.reject{|key, value| key.nil?})))
574 @configuration_file_path = nil
576 def self.configuration_file_path
577 @configuration_file_path || File.expand_path("~/.facebookerrc")
580 def self.configuration_file_path=(path)
581 @configuration_file_path = path
585 def add_facebook_params(hash, method)
586 hash[:method] = method
587 hash[:api_key] = @api_key
588 hash[:call_id] = Time.now.to_f.to_s unless method == 'facebook.auth.getSession'
592 # This ultimately delgates to the adapter
593 def self.extract_key_from_environment(key_name)
594 Facebooker.send(key_name.to_s + "_key") rescue nil
597 def self.extract_key_from_configuration_file(key_name)
598 read_configuration_file[key_name]
601 def self.report_inability_to_find_key(key_name)
602 raise ConfigurationMissing, "Could not find configuration information for #{key_name}"
605 def self.read_configuration_file
606 eval(File.read(configuration_file_path))
610 @service ||= Service.new(Facebooker.api_server_base, Facebooker.api_rest_path, @api_key)
614 @uid || (secure!; @uid)
621 def signature_for(params)
622 raw_string = params.inject([]) do |collection, pair|
623 collection << pair.map { |x|
624 Array === x ? Facebooker.json_encode(x) : x
628 Digest::MD5.hexdigest([raw_string, secret_for_method(params[:method])].join)
632 class CanvasSession < Session
633 def default_login_url_options