module Facebooker module Rails # ActionMailer like module for publishing Facbook messages # # To use, create a subclass and define methods # Each method should start by calling send_as to specify the type of message # Valid options are :email and :notification, :user_action, :profile, :ref, :publish_stream # # # Below is an example of each type # # class TestPublisher < Facebooker::Rails::Publisher # # The new message templates are supported as well # # First, create a method that contains your templates: # # You may include multiple one line story templates and short story templates # # but only one full story template # # Your most specific template should be first # # # # Before using, you must register your template by calling register. For this example # # You would call TestPublisher.register_publish_action # # Registering the template will store the template id returned from Facebook in the # # facebook_templates table that is created when you create your first publisher # def publish_action_template # one_line_story_template "{*actor*} did stuff with {*friend*}" # one_line_story_template "{*actor*} did stuff" # short_story_template "{*actor*} has a title {*friend*}", render(:partial=>"short_body") # short_story_template "{*actor*} has a title", render(:partial=>"short_body") # full_story_template "{*actor*} has a title {*friend*}", render(:partial=>"full_body") # action_links action_link("My text {*template_var*}","{*link_url*}") # end # # # To send a registered template, you need to create a method to set the data # # The publisher will look up the template id from the facebook_templates table # def publish_action(f) # send_as :user_action # from f # story_size SHORT # or ONE_LINE or FULL # data :friend=>"Mike" # end # # # # Provide a from user to send a general notification # # if from is nil, this will send an announcement # def notification(to,f) # send_as :notification # recipients to # from f # fbml "Not" # end # # def email(to,f) # send_as :email # recipients to # from f # title "Email" # fbml 'text' # text fbml # end # # This will render the profile in /users/profile.fbml.erb # # it will set @user to user_to_update in the template # # The mobile profile will be rendered from the app/views/test_publisher/_mobile.erb # # template # def profile_update(user_to_update,user_with_session_to_use) # send_as :profile # from user_with_session_to_use # recipients user_to_update # profile render(:file=>"users/profile.fbml.erb",:assigns=>{:user=>user_to_update}) # profile_action "A string" # mobile_profile render(:partial=>"mobile",:assigns=>{:user=>user_to_update}) # end # # # Update the given handle ref with the content from a # # template # def ref_update(user) # send_as :ref # from user # fbml render(:file=>"users/profile",:assigns=>{:user=>user_to_update}) # handle "a_ref_handle" # end # # # Publish a post into the stream on the user's Wall and News Feed. # def publish_stream(user_with_session_to_use, user_to_update, params) # send_as :publish_stream # from user_with_session_to_use # target user_to_update # attachment params[:attachment] # message params[:message] # action_links params[:action_links] # end # # # To send a message, use ActionMailer like semantics # TestPublisher.deliver_action(@user) # # For testing, you may want to create an instance of the underlying message without sending it # TestPublisher.create_action(@user) # will create and return an instance of Facebooker::Feeds::Action # # Publisher makes many helpers available, including the linking and asset helpers class Publisher #story sizes from the Facebooker API ONE_LINE=1 SHORT=2 FULL=4 def initialize @from = nil @full_story_template = nil @recipients = nil @controller = PublisherController.new(self) end def self.default_url_options {:host => Facebooker.canvas_server_base + Facebooker.facebook_path_prefix} end def default_url_options self.class.default_url_options end # use facebook options everywhere def request_comes_from_facebook? true end class FacebookTemplate < ::ActiveRecord::Base cattr_accessor :template_cache self.template_cache = {} def self.inspect(*args) "FacebookTemplate" end def template_changed?(hash) if respond_to?(:content_hash) content_hash != hash else false end end def deactivate Facebooker::Session.create.deactivate_template_bundle_by_id(self.bundle_id) return true rescue Facebooker::Session::TemplateBundleInvalid => e return false end class << self def register(klass,method) publisher = setup_publisher(klass,method) template_id = Facebooker::Session.create.register_template_bundle(publisher.one_line_story_templates,publisher.short_story_templates,publisher.full_story_template,publisher.action_links) template = find_or_initialize_by_template_name(template_name(klass,method)) template.deactivate if template.bundle_id # deactivate old templates to avoid exceeding templates/app limit template.bundle_id = template_id template.content_hash = hashed_content(klass,method) if template.respond_to?(:content_hash) template.save! cache(klass,method,template) template end def for_class_and_method(klass,method) find_cached(klass,method) end def bundle_id_for_class_and_method(klass,method) for_class_and_method(klass,method).bundle_id end def cache(klass,method,template) template_cache[template_name(klass,method)] = template end def clear_cache! self.template_cache = {} end def find_cached(klass,method) template_cache[template_name(klass,method)] || find_in_db(klass,method) end def find_in_db(klass,method) template = find_by_template_name(template_name(klass,method)) if template.nil? || template.template_changed?(hashed_content(klass, method)) template = register(klass,method) end template end def setup_publisher(klass,method) publisher = klass.new publisher.send method + '_template' publisher end def hashed_content(klass, method) publisher = setup_publisher(klass,method) # sort the Hash elements (in the short_story and full_story) before generating MD5 Digest::MD5.hexdigest [publisher.one_line_story_templates, (publisher.short_story_templates and publisher.short_story_templates.collect{|ss| ss.to_a.sort_by{|e| e[0].to_s}}), (publisher.full_story_template and publisher.full_story_template.to_a.sort_by{|e| e[0].to_s}) ].to_json end def template_name(klass,method) "#{Facebooker.api_key}: #{klass.name}::#{method}" end end end class_inheritable_accessor :master_helper_module attr_accessor :one_line_story_templates, :short_story_templates attr_writer :action_links cattr_accessor :skip_registry self.skip_registry = false class InvalidSender < StandardError; end class UnknownBodyType < StandardError; end class UnspecifiedBodyType < StandardError; end class Email attr_accessor :title attr_accessor :text attr_accessor :fbml end class Notification attr_accessor :fbml end class Profile attr_accessor :profile attr_accessor :profile_action attr_accessor :mobile_profile attr_accessor :profile_main end class Ref attr_accessor :handle attr_accessor :fbml end class UserAction attr_accessor :data attr_reader :target_ids attr_accessor :body_general attr_accessor :template_id attr_accessor :template_name attr_accessor :story_size def target_ids=(val) @target_ids = val.is_a?(Array) ? val.join(",") : val end def data_hash data||{} end end class PublishStream attr_accessor :target attr_accessor :attachment attr_accessor :action_links attr_accessor :message end cattr_accessor :ignore_errors attr_accessor :_body def recipients(*args) if args.size==0 @recipients else @recipients=args.first end end def from(*args) if args.size==0 @from else @from=args.first end end def send_as(option) self._body=case option when :action Facebooker::Feed::Action.new when :story Facebooker::Feed::Story.new when :templatized_action Facebooker::Feed::TemplatizedAction.new when :notification Notification.new when :email Email.new when :profile Profile.new when :ref Ref.new when :user_action UserAction.new when :publish_stream PublishStream.new else raise UnknownBodyType.new("Unknown type to publish") end end def full_story_template(title=nil,body=nil,params={}) if title.nil? @full_story_template else @full_story_template=params.merge(:template_title=>title, :template_body=>body) end end def one_line_story_template(str) @one_line_story_templates ||= [] @one_line_story_templates << str end def short_story_template(title,body,params={}) @short_story_templates ||= [] @short_story_templates << params.merge(:template_title=>title, :template_body=>body) end def action_links(*links) if links.blank? @action_links else @action_links = links end end def method_missing(name,*args) if args.size==1 and self._body.respond_to?("#{name}=") self._body.send("#{name}=",*args) elsif self._body.respond_to?(name) self._body.send(name,*args) else super end end # work around the fact that facebook cares about the order of the keys in the hash class ImageHolder attr_accessor :src,:href def initialize(src,href) self.src=src self.href=href end def ==(other) self.src == other.src && self.href == other.href end def to_json(*args) "{\"src\":#{src.to_json}, \"href\":#{href.to_json}}" end end def image(src,target) ImageHolder.new(image_path(src),target.respond_to?(:to_str) ? target : url_for(target)) end def action_link(text,target) {:text=>text, :href=>target} end def requires_from_user?(from,body) ! (announcement_notification?(from,body) or ref_update?(body) or profile_update?(body)) end def profile_update?(body) body.is_a?(Profile) end def ref_update?(body) body.is_a?(Ref) end def announcement_notification?(from,body) from.nil? and body.is_a?(Notification) end def send_message(method) @recipients = @recipients.is_a?(Array) ? @recipients : [@recipients] if from.nil? and @recipients.size==1 and requires_from_user?(from,_body) @from = @recipients.first end # notifications can # omit the from address raise InvalidSender.new("Sender must be a Facebooker::User") unless from.is_a?(Facebooker::User) || !requires_from_user?(from,_body) case _body when Facebooker::Feed::TemplatizedAction,Facebooker::Feed::Action from.publish_action(_body) when Facebooker::Feed::Story @recipients.each {|r| r.publish_story(_body)} when Notification (from.nil? ? Facebooker::Session.create : from.session).send_notification(@recipients,_body.fbml) when Email from.session.send_email(@recipients, _body.title, _body.text, _body.fbml) when Profile # If recipient and from aren't the same person, create a new user object using the # userid from recipient and the session from from @from = Facebooker::User.new(Facebooker::User.cast_to_facebook_id(@recipients.first),Facebooker::Session.create) @from.set_profile_fbml(_body.profile, _body.mobile_profile, _body.profile_action, _body.profile_main) when Ref Facebooker::Session.create.server_cache.set_ref_handle(_body.handle,_body.fbml) when UserAction @from.session.publish_user_action(_body.template_id,_body.data_hash,_body.target_ids,_body.body_general,_body.story_size) when PublishStream @from.publish_to(_body.target, {:attachment => _body.attachment, :action_links => @action_links, :message => _body.message }) else raise UnspecifiedBodyType.new("You must specify a valid send_as") end end # nodoc # needed for actionview def logger RAILS_DEFAULT_LOGGER end # nodoc # delegate to action view. Set up assigns and render def render(opts) opts = opts.dup body = opts.delete(:assigns) || {} initialize_template_class(body.dup.merge(:controller=>self)).render(opts) end def initialize_template_class(assigns) template_root = "#{RAILS_ROOT}/app/views" controller_root = File.join(template_root,self.class.controller_path) #only do this on Rails 2.1 if ActionController::Base.respond_to?(:append_view_path) # only add the view path once unless ActionController::Base.view_paths.include?(controller_root) ActionController::Base.append_view_path(controller_root) ActionController::Base.append_view_path(controller_root+"/..") end view_paths = ActionController::Base.view_paths else view_paths = [template_root, controller_root] end returning ActionView::Base.new(view_paths, assigns, self) do |template| template.controller=self template.extend(self.class.master_helper_module) def template.request_comes_from_facebook? true end end end self.master_helper_module = Module.new self.master_helper_module.module_eval do # url_helper delegates to @controller, # so we need to define that in the template # we make it point to the publisher include ActionView::Helpers::UrlHelper include ActionView::Helpers::TextHelper include ActionView::Helpers::TagHelper include ActionView::Helpers::FormHelper include ActionView::Helpers::FormTagHelper include ActionView::Helpers::AssetTagHelper include ActionView::Helpers::NumberHelper include Facebooker::Rails::Helpers #define this for the publisher views def protect_against_forgery? @paf ||= ActionController::Base.new.send(:protect_against_forgery?) end # url_for calls in publishers tend to want full paths def url_for(options = {}) super(options.kind_of?(Hash) ? {:only_path => false}.update(options) : options) end end ActionController::Routing::Routes.named_routes.install(self.master_helper_module) include self.master_helper_module class <