Showing posts with label Invite. Show all posts
Showing posts with label Invite. Show all posts

Tuesday, January 19, 2010

Extending Contacts Gem to fetch LinkedIn contacts to invite

Deprecated owing to captcha fixation!

Prepare:

Getting contacts gem to play as plugin make it easier to manipulate accordingly.
Do whatever way or find a simple cut & paste like solution here...
Extending Ruby on Rails Contacts Gem to invite Facebook users
Put linkedin.rb into "#{RAILS_ROOT}/vendor/plugins/contacts/lib/contacts"[Plugin] OR "#{GEM_PATH}/contacts-x.x.x/lib/contacts" containing...
class Contacts
  class Linkedin < Base
    URL = "http://linkedin.com"
    LOGIN_URL = "https://www.linkedin.com/secure/login?trk=hb_signin"
    LOGIN_POST_URL = "https://www.linkedin.com/secure/login"
    HOME_URL = "http://www.linkedin.com/home"
    CONTACT_URL = "http://www.linkedin.com/dwr/exec/ConnectionsBrowserService.getMyConnections.dwr"
    REFERER_URL = "http://www.linkedin.com/connections?trk=hb_side_cnts"
    PROTOCOL_ERROR = "LinkedIn has changed its protocols, please upgrade this library."
    
    def real_connect
      data, resp, cookies, forward = get(LOGIN_URL)
      
      if resp.code_type != Net::HTTPOK
        raise ConnectionError, PROTOCOL_ERROR
      end
      
      postdata = "csrfToken=guest_token&session_key=#{@login}&session_password=#{@password}&session_login=Sign In&session_login=&session_rikey="
      
      data, resp, cookies, forward = post(LOGIN_POST_URL, postdata, cookies)
      
      if data.index("The email address or password you provided does not match our records.")
        raise AuthenticationError, "Username and password do not match!"
      elsif data.index('Please enter your password') || data.index('Please enter your email address.')
       raise AuthenticationError, "Email or Password can not be blank!"
      elsif cookies == ""
        raise ConnectionError, PROTOCOL_ERROR
      end
      
      data, resp, cookies, forward = get(HOME_URL, cookies, URL)
      
      if resp.code_type != Net::HTTPOK
        raise ConnectionError, PROTOCOL_ERROR
      end
      
      @ajax_session = get_element_string(data,'name="csrfToken" value="','"')
      @logout_url = "https://www.linkedin.com/secure/login?session_full_logout=&trk=hb_signout"
      @cookies = cookies
    end
    
    def contacts      
   raise ConnectionError, PROTOCOL_ERROR unless @ajax_session
   
   postdata = "callCount=1"
   postdata+= "&JSESSIONID=#{@ajax_session}"
   postdata+= "&c0-scriptName=ConnectionsBrowserService"
   postdata+= "&c0-methodName=getMyConnections"
   postdata+= "&c0-param0=string:0"
   postdata+= "&c0-param1=number:-1"
   postdata+= "&c0-param2=string:DONT_CARE"
   postdata+= "&c0-param3=number:500"
   postdata+= "&c0-param4=boolean:false"
   postdata+= "&c0-param5=boolean:true"
   postdata+= "&xml=true"
   
   data, resp, cookies, forward = ajaxpost(CONTACT_URL, postdata, @cookies, REFERER_URL)
   raise ConnectionError, PROTOCOL_ERROR if resp.code_type != Net::HTTPOK
   cr = /detailsLink=s\d+;(.*?)\.firstName=/
   fr = /emailAddress=s\d+;var s\d+=\"(.*?)\";s\d+/
   er =  /var s\d+=\"(.*?)\";s\d+.emailAddress/
   
   contacts = []
   results = data.scan(cr)
   results.each do |result|
    result = result.to_s
    first_name = result.scan(fr).to_s
    email = result.scan(er).to_s
    contacts << [first_name, email]
   end
   
   logout
   return contacts
    end
    
    def logout
     return false unless @ajax_session
     if @logout_url && @cookies
      url = URI.parse(@logout_url)
       http = open_http(url)
       http.get("#{url.path}?#{url.query}",
        "Cookie" => @cookies
        )
        return true
     end
    end
    
    private
    
    def ajaxpost(url, postdata, cookies="", referer="")
   url = URI.parse(url)
   http = open_http(url)
   resp, data = http.post(url.path, postdata,
     "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
     "Accept-Encoding" => "gzip",
     "Cookie" => cookies,
     "Referer" => referer,
     "Content-Type" => 'text/plain;charset=UTF-8'
   )
   data = uncompress(resp, data)
   cookies = parse_cookies(resp.response['set-cookie'], cookies)
   forward = resp.response['Location']
   forward ||= (data =~ /<meta.*?url='([^']+)'/ ? CGI.unescapeHTML($1) : nil)
   if(not forward.nil?) && URI.parse(forward).host.nil?
    forward = url.scheme.to_s + "://" + url.host.to_s + forward
   end
   return data, resp, cookies, forward
    end
    
  end
  TYPES[:linkedin] = Linkedin
end
Require linkedin.rb into lib/contacts.rb
require 'linkedin'

Is it working?

Console>> Contacts::Linkedin.new('linkedin-id','password').contacts
=> [["User1", 'Email1'], ["User2", 'Email2'], ["User3", 'Email3']]
Yes! :)

Twitter API And Extending Rails Contacts Gem To Invite Friends

Introduction:

Yet to install contacts gem?
OR, how to get it to play as plugin?
Follow this link...
Extending Ruby on Rails Contacts Gem to invite Facebook users
Got through?
Put twitter.rb into "#{RAILS_ROOT}/vendor/plugins/contacts/lib/contacts" containing...
class Contacts
 class Twitter < Base
  
  URL = "http://twitter.com"
  LOGIN_URL = "http://twitter.com/account/verify_credentials."
  ONTACT_URL = "http://twitter.com/statuses/friends."
  DMESSAGE_URL = "http://twitter.com/direct_messages/new."
  LOGOUT_URL = "http://twitter.com/account/end_session."
  PROTOCOL_ERROR = "Twitter has changed its protocols, please upgrade this library."
    
  def real_connect
   format = 'xml'
   api_url = LOGIN_URL + format
   url = URI.parse(api_url)
   req = Net::HTTP::Get.new(url.path)
   req.basic_auth(@login, @password)
   resp = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }
     
   if resp.code_type == Net::HTTPUnauthorized
    raise AuthenticationError, "Username and password do not match!"
   elsif resp.code_type != Net::HTTPOK
    raise ConnectionError, PROTOCOL_ERROR
   end
     
   return true
  end
   
  def contacts
   format = 'json'
   api_url = CONTACT_URL + format
   url = URI.parse(api_url)
   req = Net::HTTP::Get.new(url.path)
   req.basic_auth(@login, @password)
   resp = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }
     
   if resp.code_type != Net::HTTPOK
    raise ConnectionError, PROTOCOL_ERROR
   end
      
   data = resp.body
   data = Contacts.parse_json(data) if data
   return [] if data.empty?
   contacts = []
   data.each do |d|
    contacts << [d['name'],d['id']]
   end
   return contacts
      
  end
   
  def send_message(contact, msg, format = 'json')
   return "Direct Message must been less than 140 characters." if msg && msg.length > 160
   return "Direct Message must have something in it..." if msg.nil? || msg.length < 1
     
   api_url = DMESSAGE_URL + format
   url = URI.parse(api_url)
   req = Net::HTTP::Post.new(url.path)
   req.basic_auth(@login, @password)
   req.set_form_data({'user' => contact.last, 'text'=> msg }, '&')
   resp = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }
   return true if resp.code_type == Net::HTTPOK
   return false
  end
   
  def logout
   #...
  end
   
 end
 TYPES[:twitter] = Twitter
end
We also need to pick appropriate JSON, if we are getting contacts in json format.
# Use ActiveSupport's version of JSON if available
if !Object.const_defined?('ActiveSupport')
  require 'json/add/rails'
end

class Contacts
  def self.parse_json(string)
    if Object.const_defined?('ActiveSupport') && ActiveSupport.const_defined?('JSON')
      ActiveSupport::JSON.decode(string)
    elsif Object.const_defined?('JSON')
      JSON.parse(string)
    else
      raise 'Contacts requires JSON or Rails (with ActiveSupport::JSON)'
    end
  end
end
Don't forget to add this into lib/contacts.rb
require 'twitter'
Done!

Usage

C:\Me\Workspace\contacts>ruby script/console
Loading development environment (Rails 2.3.4)
>> twitter=Contacts::Twitter.new('twitter-id','password')
...
...
>> contacts = twitter.contacts
=> [["Jeremy Piven", 20221159], ["The Next Web", 10876852], ["bob saget", 38536306], ["Joel Stein", 24608680], ["Moin Haidar", 38841828]]
>> contacts.each{|c|twitter.send_message(c, 'Your Message')}
All should be well :)

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...:)