Getting Your Web 2.0 on, iPhone Style

Long has it been since a post was made here; my apologies. Last week I totally snapped and bought an iPhone despite the modest litany of reasons I thought I shouldn’t (locked to one carrier, not standard GSM, battery life qualms, questionable extensibility) and my overall reaction is do it. If you are a smart and resourceful person, as I am sure you are since you are reading this blog, you will have no trouble bending the iPhone to your whims. This is one such report.

Lately, I’ve been Twittering (yes, it’s a verb now, too). For those of you who aren’t in the know, Twitter is a service that allows you to send a text message reporting, simply, what you are doing. That text message is then displayed on Twitter’s site (if you so desire), but is also relayed via SMS to any other Twitter users who are “following” you. Likewise, you receive text messages from Twitter when people you are “following” send in their updates.

The whole thing is a little bit silly, but the reported effect is to have a better peripheral idea of your friends’ daily lives and circumstances without having to hassle them synchronously (a little Web 2.0 lingo for you there). Thus, when you run into so-and-so, you may remember that they recently crashed their car or that they saw some movie you wanted to see, and so on. It’s not important information, but it’s neat.

So there I was, Twittering my heart out, when I realized that more of my friends read Facebook on a daily basis than participate in Twitter, and that Facebook provides an interface for updating your “status,” which is printed next to your picture on your profile page and disseminated through the “news feed.” Obviously, I could not stand idly by and let Twitter be updated but not Facebook! Ruby to the rescue.

The first step was to build an interface to Facebook’s status updating system, which is not supported in their public API (for shame). Because there is no programming interface for it, I had to make my script access the page through normal HTTP methods, forge the login security of the browser and cookies, and post the update that way. Here is the code:

require 'net/http'
require 'net/https'
require 'cgi'

def StatusFacebook(status,opt)
   if not opt.is_a? Hash or (not opt.has_key? 'user' or not opt.has_key? 'pass')
      return false
   end

   ua = opt.has_key?('ua') ? opt['ua'] : 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.4) Gecko/20060612 Firefox/1.5.0.4 Flock/0.7.0.17.1'
   post_headers = {
      'Content-Type' => 'application/x-www-form-urlencoded',
      'User-Agent' => ua
   }
   get_headers = {
      'User-Agent' => ua
   }

   http = Net::HTTP.new('www.facebook.com')
   response = http.request_get('/login.php', get_headers)

   challenge = response.body.match(/<input[^<]*?name="challenge".*?>/)[0].match(/value="(.*?)"/)[1]

   loginform = "email="+CGI.escape(opt['user'])+"&pass="+CGI.escape(opt['pass'])+"&challenge=#{challenge}&md5pass=&next="

   cookies = []
   response.get_fields('Set-Cookie').each do |c|
      co,k,v = c.match(/^(.*?)=(.*?);/).to_a
      if ['test_cookie', 'login', 'reg_fb_ref', 'reg_fb_gate'].include? k
         cookies << co
      end
   end

   post_headers['Cookie']  = cookies.join(' ')
   get_headers['Cookie']      = cookies.join(' ')

   http = Net::HTTP.new('login.facebook.com', 443)
   http.use_ssl = true
   http.verify_mode = OpenSSL::SSL::VERIFY_NONE

   response = http.request_post('/login.php', loginform, post_headers)

   if response.code == '302'
      cookies = []
      response.get_fields('Set-Cookie').each do |c|
         co,k,v = c.match(/^(.*?)=(.*?);/).to_a
         if ['test_cookie', 'login', 'login_x', 'xs', 'c_user', 'h_user'].include? k
            cookies << co
         end
      end

      post_headers['Cookie']  = cookies.join(' ')
      get_headers['Cookie']      = cookies.join(' ')

      get_headers['Referer'] = 'https://login.facebook.com/login.php'
      get_headers.delete('Referer')
      http = Net::HTTP.new('www.facebook.com')

      response = http.request_get('/home.php?', get_headers)

      if response.code == '200'
         post_form_id = response.body.match(/<input[^<]*?id="post_form_id".*?>/)[0].match(/value="(.*?)"/)[1]

         if not post_form_id.empty?
            statusform = 'status=' + CGI.escape(status) + '&post_form_id=' + post_form_id

            post_headers['Referer'] = 'http://www.facebook.com/home.php'
            response = http.request_post('/updatestatus.php', statusform, post_headers)

            if response.body.match(/#{status}/)
               return true
            else
               return false
            end
         end
      end
   end

   false
end  

Feel free to pick through it. The important part to notice is the handling of the authentication cookies. I used the “Live HTTP Headers” Firefox Add-In to trace the headers during a Facebook login, took note of the cookies required, and selected only those cookies to be transmitted. Facebook has some pretty slick security measures as well, including a “challenge” code that is generated on the login form and checked along with your authentication information, and a “post_form_id” value that appears within every link and form post on the front page of the site, presumably to further validate the session integrity. There are a couple of neat .match() lines to capture those.

Step two was to access Twitter. Twitter does have an API, which is accessed through REST (for Twitter’s interface, that’s just a fancy shmancy term for “post a form”). Here is the Ruby function for updating Twitter:

require 'net/http'
require 'cgi'

def StatusTwitter(status,opt)
   if not opt.is_a? Hash or (not opt.has_key? 'user' or not opt.has_key? 'pass')
      return false
   end

   ua = opt.has_key?('ua') ? opt['ua'] : 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.4) Gecko/20060612 Firefox/1.5.0.4 Flock/0.7.0.17.1'
   post_headers = {
      'Content-Type' => 'application/x-www-form-urlencoded',
      'User-Agent' => ua
   }
   get_headers = {
      'User-Agent' => ua
   }

   response = Net::HTTP.post_form(URI.parse('http://'+CGI.escape(opt['user'])+':'+CGI.escape(opt['pass'])+'@twitter.com/statuses/update.xml'),
                                          { 'status' => status } )

   return true if response.code == '200'
   return false
end  

Then there is bootstrap code that calls those two functions. Hooray. So what about the iPhone, then?

Well, thanks to the iPhone’s nearly complete and rather robust support for e-mail, I decided to create a way for my home mail server to receive a message and use it to update the status on those sites. From there, the possibilities would be endless. All I needed was a little procmail recipe to check that the message was addressed to my status-update address and a little script to process the full e-mail message and run the update. Here is the procmail destination file that bootstraps the updates:

#!/usr/bin/env ruby

BASEPATH = __FILE__.match(/^.*\//)[0]

require "#{BASEPATH}status_twitter"
require "#{BASEPATH}status_facebook"
require 'net/smtp'

$stdin.each do |line|
   if subject = line.match(/^Subject:(.*?)$/)

      # Format the subject.
      status_fb = subject[1].strip
      status_tw = subject[1].strip
      # No initial cap for Facebook.
      status_fb[0] = status_fb[0].chr.downcase
      # Initial cap for Twitter.
      status_tw[0] = status_tw[0].chr.upcase

      ret_fb = StatusFacebook(status_fb, {
         'user' => 'your_user_name',
         'pass' => 'your_password'
      })
      ret_tw = StatusTwitter(status_tw, {
         'user' => 'your_user_name',
         'pass' => 'your_password'
      })

      ret_fb = (ret_fb) ? 'Success' : 'Failure'
      ret_tw = (ret_tw) ? 'Success' : 'Failure'

msg = <<EOF
From: Status <my.status.email@my.domain>
To: iPhone <my.iphone.acct@my.domain>
Subject: Update report

I have submitted your update, "#{status_tw}"

Facebook: #{ret_fb}
Twitter:  #{ret_fb}
EOF
      
      Net::SMTP.start('my.mail.server') do |smtp|
         smtp.send_message msg,
                        'my.status.email@my.domain',
                        'my.iphone.acct@my.domain'
      end
      exit 0
   end
end

exit 0  

I just set procmail to deliver the message to that script if it matched my updater address (by using :0, no flags). That means I never receive e-mail to that address in my actual mail client, which is good. The script also returns an e-mail to my phone to let me know it succeeded because it would take me too long to figure out if it did or not through mobile Safari.

One of the cool things that this script also does is to capitalize the first letter of the update for the Twitter version and lowercase the first letter of the Facebook version. This is done because Facebook builds a sentence like “Aaron is “, while Twitter simply posts the sentence it is entirety. Being a slut for grammar, I require that capitalization and punctuation flow like a river. One also must keep in mind the point-of-view and temporal sense in which the update is written.

I have a plan to make the system handle Facebook updates even more cleverly by replacing “my” with “his” and things like that, though I might be biting off more than I can chew with that one.

Cool, huh?

1 Response to “Getting Your Web 2.0 on, iPhone Style”


  1. 1 Brad

    Hey, I’m just curious if this is still working for you… I’m trying to do something similar, but regardless of what I try, I get a “Incompatible Browser” message back from Facebook. I’m wondering if they’ve taken measures to avoid this type of thing, or if I’m just missing something…

    Brad

Leave a Reply