Tuesday, December 15, 2009

Extending Ruby on Rails Contacts Gem to invite Facebook users

Introduction:

Contacts Gem is an interface to import email contacts from various providers like Yahoo, Gmail, Hotmail, and Plaxo including AOL to avail invite functionality.

Download:

sudo gem install contacts
http://github.com/cardmagic/contacts
git clone git://github.com/cardmagic/contacts.git
Do not forget to install Json Gem as well.
gem isntall json

Facebook Inviter

First thing first. You need to have Mechanize Gem including its dependency(Nokogiri). Very simple...
sudo gem install mechanize
this will install Nokogiri as well.

Using Contacts Gem as Plugin

One more thing, I am using Contacts Gem as plugin to get things easier. Making this Gem as plugin is not a big deal! Move the Gem, either getting it from Local Gem repository C:\ruby\lib\ruby\gems\1.8\gems\ or whatever you have downloaded, to '#{RAILS_ROOT}/vendor/plugins'.
Then create initializer init.rb into #{RAILS_ROOT}/vendor/plugins/contacts/.


init.rb
require 'contacts'
Get to the console(restart to load the plugin) to check it out.
C:\Me\Workspace\contacts>ruby script/console
Loading development environment (Rails 2.3.4)
>> Contacts
=> Contacts
So far so good.

Now back to facebook fetcher Fix a base class(say page_scraper.rb) for all the fetchers(facebook, orkut etc...) that uses mechanize into #{RAILS_ROOT}/vendor/plugins/contacts/lib/contacts extending Contacts's base class as follow.


page_scraper.rb
require 'mechanize'
class Contacts
 class PageScraper < Base
 
   attr_accessor :agent
   
   # creates the Mechanize agent used to do the scraping 
   def create_agent
     self.agent = WWW::Mechanize.new
     agent.keep_alive = false
     agent
   end
   
   # Logging in
   def prepare; end # stub

   def validate(page)
      return false if page.nil?
      return false unless page.respond_to?('forms')
      return true
   end
 
   def strip_html( html )
     html.gsub(/<\/?[^>]*>/, '')
   end
   
 end
end
Again in the same location(#{RAILS_ROOT}/vendor/plugins/contacts/lib/contacts) put facebook.rb containing...
require 'page_scraper'
class Contacts
  class Facebook < PageScraper
    URL                 = "http://www.facebook.com/"
    LOGIN_URL      = "http://m.facebook.com/"
    PROTOCOL_ERROR      = "Facebook has changed its protocols, please upgrade this library."
    
    def real_connect
     create_agent
     prepare
    end
    
    def prepare
     page = agent.get(LOGIN_URL)
     raise ConnectionError, PROTOCOL_ERROR unless validate(page)
      login_form = page.forms.first
     login_form.email = @login
     login_form.pass = @password
     login_form.charset_test = ''
     page = login_form.submit
     raise AuthenticationError, "Email and Password do not match!" if page.body =~ /Incorrect email\/password combination/
     #Friends Page
     friends_links = page.links_with(:href => /\/friends.php/)
     friends_link = friends_links.collect{|flink| flink.href if flink.text == 'Friends'}.compact.first
     raise ConnectionError, PROTOCOL_ERROR unless friends_link
     friends_url = 'http://m.facebook.com' + friends_link
     page = agent.get(friends_url)
     #My Friends Page
     my_friends_link = page.links_with(:href => /\/friends.php/)
     @everyone = my_friends_link.collect{|l| l.href if l.text == 'Everyone'}.compact.first
     @everyone = my_friends_link.collect{|l| l.href if l.text == 'Friends'}.compact.first unless @everyone
     @logout_url = page.link_with(:href => /logout/)
     return true
    end
    
    def contacts
     contacts = []
     next_page = true
     current_page = 2
     everyone = 'http://m.facebook.com' + @everyone
     page = agent.get(everyone)
     raise AuthenticationError, "You must login first!" if page.body =~ /Enter your login and password/
     
     while(next_page)
      next_page = false
      data = page.search('//tr[@valign="top"]')
      if data
       data.children.each do |node|
        pf_nodes = node.children.children
        if pf_nodes
         fr_name = msg_link = ''
         pf_nodes.each do |nd|
          fr_name = nd.text if nd.name == 'a' && nd.has_attribute?('href')
          if nd.name == 'span' || nd.text.include?('Message')
           nd.children.each do |n|
            msg_link = n.attributes['href'] if n.name == 'a' && n.text == 'Message' && n.has_attribute?('href') && n.attributes['href'].to_s.include?('compose')
           end
          end
         end
         contacts << [fr_name, msg_link.to_s] unless msg_link.blank?
        end
       end
      end # End Outer if
      data = page.search('//div[@class="pad"]')
    data.each do |node|
     childs = node.children
     next if childs.empty?
     childs.each do |c|
      if c.name == 'a' && c.text.to_i == current_page
       next_page = 'http://m.facebook.com' + c.attributes['href'].to_s
       break
      end
     end
    end
      current_page += 1
    page = agent.get(next_page) unless next_page == false
     end #End While
     return contacts
    end
    
    def send_message(contact, message, subj=nil)
     success = false
     if contact
      url_inbox = "http://m.facebook.com" + contact.first
      page = agent.get(url_inbox)
      raise AuthenticationError, "You must login first!" if page.body =~ /Enter your login and password/
      return false unless validate(page)
      page.forms.each do |f|
       if f.has_field?('body')
        f.subject = subj if subj && !subj.empty?
        f.body = message
        f.charset_test = ''
        page = agent.submit(f, f.buttons.first)
        return true if page.code == '200'
       end
      end
      
     end
     return success
    end
    
     def logout
      agent.click @logout_url if @logout_url
      return true
    end
    
  end

  TYPES[:facebook] = Facebook
end
Ultimately, Open lib/contacts.rb and add this at the end of the line as
require 'facebook'
You are done

Usage

C:\Me\Workspace\contacts>ruby script/console
Loading development environment (Rails 2.3.4)
>> fb=Contacts::Facebook.new('facebookid','password')
...
...
>> contacts = fb.contacts
=> [["User1", "/inbox/?r3a2c6b2e&r39ade298&compose&ids=100000485673107&refid=5"], ["User2", "/inbox/?r46861c8d&rd5ef5fb6&compose&ids=691998819&refid=5"]]
>> contacts.each{|c|fb.send_message(c, 'Your Message', 'Subject')}
Done! but scopes are always there for improvement...:)

3 comments:

  1. Hello Modin,

    This script in ruby not work on the Facebook new protocol, you have a new code???

    Thanks

    Felipe V Fraga - felipe.fraga@circlo.com.br

    ReplyDelete
  2. I got error when I use this code to get connect.

    raise ConnectionError, PROTOCOL_ERROR unless validate(page)

    error message - undefined validate method

    ReplyDelete
  3. undefined method validate on invoking

    fb=Contacts::Facebook.new('facebookid','password')

    NoMethodError: undefined method `validate' for #

    ReplyDelete