some
[appoyo.git] / vendor / plugins / facebooker / lib / facebooker / session.rb
blob4b7ec49ab56675c270ca66bcec690d42c512db80
1 require 'cgi'
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
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
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
62     end
63     class UserRegistrationFailed < StandardError
64       attr_accessor :failed_users
65     end
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)
83     end
85     def self.api_key
86       extract_key_from_environment(:api) || extract_key_from_configuration_file(:api) rescue report_inability_to_find_key(:api)
87     end
89     def self.secret_key
90       extract_key_from_environment(:secret) || extract_key_from_configuration_file(:secret) rescue report_inability_to_find_key(:secret)
91     end
93     def self.current
94       Thread.current['facebook_session']
95     end
97     def self.current=(session)
98       Thread.current['facebook_session'] = session
99     end
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)}"
104     end
106     def install_url(options={})
107       "#{Facebooker.install_url_base(@api_key)}#{install_url_optional_parameters(options)}"
108     end
110     # The url to get user to approve extended permissions
111     # http://wiki.developers.facebook.com/index.php/Extended_permission
112     #
113     # permissions:
114     # * email
115     # * offline_access
116     # * status_update
117     # * photo_upload
118     # * video_upload
119     # * create_listing
120     # * create_event
121     # * rsvp_event
122     # * sms
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)}"
126     end
128     def install_url_optional_parameters(options)
129       optional_parameters = []      
130       optional_parameters += add_next_parameters(options)
131       optional_parameters.join
132     end
134     def add_next_parameters(options)
135       opts = []
136       opts << "&next=#{CGI.escape(options[:next])}" if options[:next]
137       opts << "&next_cancel=#{CGI.escape(options[:next_cancel])}" if options[:next_cancel]
138       opts
139     end
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
149     end
151     def default_login_url_options
152       {}
153     end
155     def initialize(api_key, secret_key)
156       @api_key        = api_key
157       @secret_key     = secret_key
158       @batch_request  = nil
159       @session_key    = nil
160       @uid            = nil
161       @auth_token     = nil
162       @secret_from_session = nil
163       @expires        = nil
164     end
166     def secret_for_method(method_name)
167       @secret_key
168     end
170     def auth_token
171       @auth_token ||= post 'facebook.auth.createToken'
172     end
174     def infinite?
175       @expires == 0
176     end
178     def expired?
179       @expires.nil? || (!infinite? && Time.at(@expires) <= Time.now)
180     end
182     def secured?
183       !@session_key.nil? && !expired?
184     end
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'])
189     end    
190     
191     def secure_with_session_secret!
192       self.secure!(:generate_session_secret => true)
193     end
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
200     end
202     def fql_build_object(type, hash)
203       case type
204       when 'user'
205         user = User.new
206         user.session = self
207         user.populate_from_hash!(hash)
208         user
209       when 'photo'
210         Photo.from_hash(hash)
211       when 'page'
212         Page.from_hash(hash)
213       when 'page_admin'
214         Page.from_hash(hash)
215       when 'group'
216         Group.from_hash(hash)
217       when 'event_member'
218         Event::Attendance.from_hash(hash)
219       else
220         hash
221       end
222     end
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)
230         end
231       end
232     end
234     def fql_multiquery(queries, format = 'XML')
235       results = {}
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
241           value = [] 
242           unless type.nil?
243             value = response.shift.map do |hash|
244               fql_build_object(type, hash)
245             end
246           end
247           results[name] = value
248         end
249       end
250       results
251     end
253     def user
254       @user ||= User.new(uid, self)
255     end
257     #
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)
268         end
269       end
270     end
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)
276         end
277       end
278     end
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)}
283       end
284     end
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)}
289       end
290     end
292     def pages(options = {})
293       raise ArgumentError, 'fields option is mandatory' unless options.has_key?(:fields)
294       @pages ||= {}
295       @pages[options] ||= post('facebook.pages.getInfo', options) do |response|
296         response.map do |hash|
297           Page.from_hash(hash)
298         end
299       end
300     end
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)
305     end    
307     #
308     # Returns a proxy object for handling calls to Facebook cached items
309     # such as images and FBML ref handles
310     def server_cache
311       Facebooker::ServerCache.new(self)
312     end
314     #
315     # Returns a proxy object for handling calls to the Facebook Data API
316     def data
317       Facebooker::Data.new(self)
318     end
320     def admin
321       Facebooker::Admin.new(self)
322     end
324     def mobile
325       Facebooker::Mobile.new(self)
326     end
328     #
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)
335       uids1 = []
336       uids2 = []
337       array_of_pairs_of_users.each do |pair|
338         uids1 << pair.first
339         uids2 << pair.last
340       end
341       post('facebook.friends.areFriends', :uids1 => uids1.join(','), :uids2 => uids2.join(','))
342     end
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" 
347       end
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)
351         end
352       end
353     end
355     def get_albums(aids)
356       @albums = post('facebook.photos.getAlbums', :aids => aids) do |response|
357         response.map do |hash|        
358           Album.from_hash(hash)
359         end
360       end
361     end
363     def get_tags(pids)
364       @tags = post('facebook.photos.getTags', :pids => pids)  do |response|
365         response.map do |hash|
366           Tag.from_hash(hash)
367         end
368       end
369     end
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"        
374       end
375       @tags = post('facebook.photos.addTag', :pid => pid, :tag_uid => tag_uid, :tag_text => tag_text, :x => x, :y => y )
376     end
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(',')}
380       if email_fbml
381         params[:email] = email_fbml
382       end
383       params[:type]="user_to_user"
384       # if there is no uid, this is an announcement
385       unless uid?
386         params[:type]="app_to_user"
387       end
389       post 'facebook.notifications.send', params,uid?
390     end 
392     ##
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)
405     end
407     ##
408     # publish a previously rendered template bundle
409     # see http://wiki.developers.facebook.com/index.php/Feed.publishUserAction
410     #
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)
417     end
420     ##
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
426     end
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])}
431     end
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)}
436     end
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))
444           end
445         end
446       end
447     end
449     def instance_variable_set_value(field, value)
450       self.instance_variable_set("@#{field}", value)
451     end
453     def instance_variable_value(field)
454       self.instance_variable_get("@#{field}")
455     end
457     def fields_to_serialize
458       %w(session_key uid expires secret_from_session auth_token api_key secret_key)
459     end
461     class Desktop < Session
462       def login_url
463         super + "&auth_token=#{auth_token}"
464       end
466       def secret_for_method(method_name)
467         secret = auth_request_methods.include?(method_name) ? super : @secret_from_session
468         secret
469       end
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]
474         end
475         super
476       end
477       private
478         def auth_request_methods
479           ['facebook.auth.getSession', 'facebook.auth.createToken']
480         end
481     end
483     def batch_request?
484       @batch_request
485     end
487     def add_to_batch(req,&proc)
488       batch_request = BatchRequest.new(req,proc)
489       Thread.current[:facebooker_current_batch_queue]<<batch_request
490       batch_request
491     end
493     # Submit the enclosed requests for this session inside a batch
494     # 
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
498     #
499     # For Example:
500     #
501     #   facebook_session.batch do
502     #     @send_result = facebook_session.send_notification([12451752],"Woohoo")
503     #     @albums = facebook_session.user.albums
504     #   end
505     #   puts @albums.first.inspect
506     #
507     # is valid, however
508     #
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
513     #   end
514     #
515     # will raise Facebooker::BatchRequest::UnexecutedRequest
516     #
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
519     #
520     # for example, if the send_notification resulted in TooManyUserCalls being raised,
521     # calling 
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 
525     #  @albums.first 
526     # is called
527     #
528     def batch(serial_only=false)
529       @batch_request=true
530       Thread.current[:facebooker_current_batch_queue]=[]
531       yield
532       # Set the batch request to false so that post will execute the batch job
533       @batch_request=false
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)
536     ensure
537       @batch_request=false
538       BatchRun.current_batch=nil
539     end
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))
545       if batch_request?
546         add_to_batch(final_params,&proc)
547       else
548         result = service.post(final_params)
549         result = yield result if block_given?
550         result
551       end
552     end
554     def post(method, params = {}, use_session_key = true, &proc)
555       if batch_request?
556         post_without_logging(method, params, use_session_key, &proc)
557       else
558         Logging.log_fb_api(method, params) do
559           post_without_logging(method, params, use_session_key, &proc)
560         end
561       end
562     end
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?})))
570       end
571     end
574     @configuration_file_path = nil
576     def self.configuration_file_path
577       @configuration_file_path || File.expand_path("~/.facebookerrc")
578     end
580     def self.configuration_file_path=(path)
581       @configuration_file_path = path
582     end
584     private
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'
589         hash[:v] = "1.0"
590       end
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
595       end
597       def self.extract_key_from_configuration_file(key_name)
598         read_configuration_file[key_name]
599       end
601       def self.report_inability_to_find_key(key_name)
602         raise ConfigurationMissing, "Could not find configuration information for #{key_name}"
603       end
605       def self.read_configuration_file
606         eval(File.read(configuration_file_path))
607       end
609       def service
610         @service ||= Service.new(Facebooker.api_server_base, Facebooker.api_rest_path, @api_key)      
611       end
613       def uid
614         @uid || (secure!; @uid)
615       end
617       def uid?
618         !! @uid
619       end
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
625           }.join("=")
626           collection
627         end.sort.join
628         Digest::MD5.hexdigest([raw_string, secret_for_method(params[:method])].join)
629       end
630   end
632   class CanvasSession < Session
633     def default_login_url_options
634       {:canvas => true}
635     end
636   end