Thursday, March 31, 2011

Proxied - Web Service for the static page web app

I mentioned in a previous post how I had been building a couple of static page apps that used only Javascript with JSONP to get and push data. These apps I have been working on mostly involved using Facebook and Twitter, and therefore I didn't run into too many problems with the fact that I had no way to persist data (leaving out HTML5 local storage, which doesn't allow aggregation across users anyway). However I have gotten to the point with the projects where I'd really like to be able to do some things that involve aggegation of data across users, and frankly I have not found a webservice to do this (I went through all this in my last post on this subject).

So having thought this over more I think such a service, which I have named Proxied, would have the following:

  • Login/Registration/Session as a service capability - Proxied would have the ability to establish a user session per Proxied application, so that other Proxied API calls could be limited to logged in users
  • True to its name Proxied would proxy calls to any api, ensuring they return JSONP
  • For many API's some sort of authentication is needed, in those cases Proxied would store your API credentials and do whatever signing is needed for that API.
I think a specific example is probably most helpful.

Say you want to build a new HackerNews like web app, you'll need
  1. Login system
  2. Database to store submitted links
  3. Database to store up-votes, down-votes, karma
To do this with a static page web application and Proxied we'd use the Proxied login system to create user sessions and then a cloud database server (say SimpleDB, or MongoHQ) to store the data. We'd create whatever tables (or no tables for NOSQL) we need to in the cloud DB, and then figure out what the cloud DB API endpoint url(s) would be for updating the tables, we'd set those endpoint URLs in Proxied along with our cloud DB credentials, and signing methodology (OAUTH or whatever). Proxied would then produce a JSONP accessible endpoint (or better yet, with clever CNAME's, JSONP wouldn't be necessary) that would proxy the call to the cloud DB API to update our application's data. In Proxied we could set the cloud DB proxy endpoint(s) to be accessible only by logged in users, therefore restraining willy nilly use of our cloud DB by nefarious folks.

Basically one would use Proxied to setup API endpoints for what, in a traditional webapp, would be the controller (in Rails parlance) endpoints on your own server. For a RESTFUL architecture you'd have Proxied endpoints to GET/PUT/DELETE/POST your data to a cloud service. As you add more data tables (or non-tables for NOSQL), you'd add more endpoints in Proxied to service that data.

At this point you can just write the app as a static web page that uses Javascript to update the application data as necessary. Need to crunch data? Well that should be done offline anyway, using Map-Reduce or the like, just update the cloud DB with your crunching results and make that available through Proxied.

Nothing restrains you from using more than one web page, but all the pages would be static. I'd then host the web pages on Amazon S3 thereby obviating the need for ANY server at all on our part, and ensuring the application is scalable (to the degree the underlying services are scaleable).

What I want to know is whether this is a service that developers would use? If so, how much would you be willing to pay for it?


Tuesday, November 30, 2010

Phonegap, JQuery Mobile, Twitter and Facebook

Recently I have been working on a mobile website that I wanted to turn into a iOS app. Not knowing (or wanting to know) Objective C, I decided to use PhoneGap to build a HTML/Javascript/CSS based application. My mobile website uses JQuery Mobile, so I wanted to continue to use that framework if at all possible (it offers a lot of easy mobile styling and touch based events). My application also makes substantial use of the Facebook and Twitter APIs, so I also needed to find a way to authenticate with those services (both use a form of OAuth).

The first thing I tackled was the OAuth integration portions. I naively thought that the two services respective Javascript login procedures would work in the WebKit window that PhoneGap creates, however I found neither operated. That meant that I had to fall back on doing actual OAuth. I found a great blog post describing how to do this for Facebook, however it didn't do everything I needed it to do, so I thought I describe what I did in more detail.

The first thing you need to do is download the ChildBrowser Plugin and drag and drop the files under the ChildBrowser/iPhone to the plugins folder of your XCode project. Then put the ChildBrowser.js file in your www folder and make sure to include the file as a script tag in your html files AFTER the phonegap.js file.
<script src="phonegap.js" type="application/x-javascript" charset="utf-8"></script>
<script src="ChildBrowser.js" type="application/x-javascript" charset="utf-8"></script>
The next step is to fire off the following when you want to login to Facebook (like a click event or the like).
function(){
var my_client_id = "YOUR_CLIENT_ID", my_redirect_uri = "http://www.facebook.com/connect/login_success.html", my_type = "user_agent", my_display = "touch";

var authorize_url = "https://graph.facebook.com/oauth/authorize?";
authorize_url += "client_id=" + my_client_id;
authorize_url += "&redirect_uri=" + my_redirect_uri;
authorize_url += "&display=" + my_display;
authorize_url += "&scope=read_stream,publish_stream,offline_access,publish_checkins"

client_browser = ChildBrowser.install();
client_browser.onLocationChange = function(loc){
facebookLocChanged(loc);
};
if (client_browser != null) {
window.plugins.childBrowser.showWebPage(authorize_url);
}
});
This basically does the first step of the OAuth flow and shows the Facebook Authorization page int a ChildBrowser window.

The next thing we do is wait for a change in the location of the ChildBrowser window to the redirect_uri.
function facebookLocChanged(loc){
/* Here we check if the url is the login success */

if (loc.indexOf("http://www.facebook.com/connect/login_success.html") > -1) {
client_browser.close();
var fbCode = loc.match(/code=(.*)$/)[1]
$.ajax(
{
url:'https://graph.facebook.com/oauth/access_token?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&code='+fbCode+'&redirect_uri=http://www.facebook.com/connect/login_success.html',
data: {},
success: function(data, status){
localStorage.facebook_token = data.split("=")[1];
client_browser.close();
initialize_facebook();
},
error: function(error) {
client_browser.close();
},
dataType: 'text',
type: 'POST'
}
)
}
}
This completes the OAuth process closes the ChildBrowser (I close the ChildBrowser first because I like the user experience) and stores the facebook_token. Note here we have taken advantage of the fact that Cross-Domain AJAX calls are allowed when using PhoneGap. We can now make Facebook calls. I wrote a little helper method for appending on our facebook_token to urls when making calls.
function create_facebook_url(base) {
if(device) {
if(base.indexOf("?") == -1) {
return base + "?access_token=" + localStorage.facebook_token
} else {
return base + "&access_token=" + localStorage.facebook_token
}
} else {
return base;
}
}

// Example of using create_facebook_url
FB.api(create_facebook_url("/me"), function(result){
current_facebook_user = result;
}

For Twitter we follow a similar procedure, though the Twitter OAuth is markedly more complicated. To handle some of the more complex OAuth operations I opted to use an oauth javascript library, so this also needs to be included in your index.html files.
<script src="/javascripts/sha1.js" type="text/javascript"></script>
<script src="/javascripts/oauth.js" type="text/javascript"></script>
Note the sha1.js file is included with the oauth library.

Once we have the libraries included we can now start the OAuth process.
accessor =
{ consumerKey : "YOUR_CONSUMER_KEY"
, consumerSecret: "YOUR_CONSUMER_SECRET"
, serviceProvider:
{ signatureMethod : "HMAC-SHA1"
, requestTokenURL : "http://api.twitter.com/oauth/request_token"
, userAuthorizationURL: "https://api.twitter.com/oauth/authorize"
, accessTokenURL : "https://api.twitter.com/oauth/access_token"
, echoURL : "http://localhost/oauth-provider/echo"
}
};

var message = {
method: "post", action: accessor.serviceProvider.requestTokenURL
, parameters: [["scope", "http://www.google.com/m8/feeds/"]]
};
var requestBody = OAuth.formEncode(message.parameters);
OAuth.completeRequest(message, accessor);
var authorizationHeader = OAuth.getAuthorizationHeader("", message.parameters);
var requestToken = new XMLHttpRequest();
requestToken.onreadystatechange = function receiveRequestToken() {
if (requestToken.readyState == 4) {
var results = OAuth.decodeForm(requestToken.responseText);
var oauth_token = OAuth.getParameter(results, "oauth_token");
var authorize_url = "http://api.twitter.com/oauth/authorize?oauth_token="+oauth_token;
client_browser = ChildBrowser.install();
client_browser.onLocationChange = function(loc){
twitterLocChanged(loc, requestToken, accessor);
};
if (client_browser != null) {
window.plugins.childBrowser.showWebPage(authorize_url);
}
}};
requestToken.open(message.method, message.action, true);
requestToken.setRequestHeader("Authorization", authorizationHeader);
requestToken.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
requestToken.send(requestBody);
Basically we are forming the proper OAuth request to get a request_token from Twitter and once we receive back our token we use it to open a ChildBrowser pointing at the proper Authorization page. Next we are (same as Facebook) look for a location change on our ChildBrowser. This time we look for the location to change to the callback that we set in our Twitter Application definition.
function twitterLocChanged(loc, requestToken, accessor){
/* Here we check if the url is the login success */
if (loc.indexOf("http://www.headingtoo.com/") > -1) {
client_browser.close();
var results = OAuth.decodeForm(requestToken.responseText);
message = {method: "post", action: accessor.serviceProvider.accessTokenURL};
OAuth.completeRequest(message,
{ consumerKey : accessor.consumerKey
, consumerSecret: accessor.consumerSecret
, token : OAuth.getParameter(results, "oauth_token")
, tokenSecret : OAuth.getParameter(results, "oauth_token_secret")
});
var requestAccess = new XMLHttpRequest();
requestAccess.onreadystatechange = function receiveAccessToken() {
if (requestAccess.readyState == 4) {
var params = get_url_vars_from_string(requestAccess.responseText);
localStorage.twitter_token = params["oauth_token"];
localStorage.twitter_secret_token = params["oauth_token_secret"];
localStorage.twitter_user_name = params["screen_name"];
localStorage.twitter_user_id = params["user_id"];
intialize_twitter();
}
};
requestAccess.open(message.method, message.action, true);
requestAccess.setRequestHeader("Authorization", OAuth.getAuthorizationHeader("", message.parameters));
requestAccess.send();
}
}
This code is fired when we notice our ChildBrowser has gone to the defined callback url, we then complete the OAuth process by asking for an access_token, which we put in local storage. We can then use that access token to make request on behalf of our user.
// helper
function get_url_vars_from_string(url) {
var vars = [], hash;
var hashes = url.slice(url.indexOf('?') + 1).split('&');
for(var i = 0; i < hashes.length; i++)
{
hash = hashes[i].split('=');
vars.push(hash[0]);
vars[hash[0]] = hash[1];
}
return vars;

}

// helper
function create_twitter_oauth_ajax_data(url, method, params, success, error) {
var ajax_data = {
url: url,
data: params,
dataType: 'json',
type: method,
timeout: 60*1000,
beforeSend: function(req){
var message = {
method: method,
action: url
};
var message_params = params;
message.parameters = message_params;
debug_log(message.parameters);
OAuth.completeRequest(message, {
consumerKey: accessor.consumerKey,
consumerSecret: accessor.consumerSecret,
token: localStorage.twitter_token,
tokenSecret: localStorage.twitter_secret_token
});
debug_log(message);
req.setRequestHeader("oauth_consumer_key", accessor.consumerKey);
req.setRequestHeader("oauth_nonce", message.parameters['oauth_nonce']);
req.setRequestHeader("oauth_signature_method", 'HMAC-SHA1');
req.setRequestHeader("oauth_token", localStorage.twitter_token);
req.setRequestHeader("oauth_timestamp", message.parameters['oauth_timestamp']);
req.setRequestHeader("oauth_version", '1.0');
req.setRequestHeader("Authorization", OAuth.getAuthorizationHeader("", message.parameters));
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
},
success: success,
error: error
}
return ajax_data;
}
var success = function(data, responseText) {
current_twitter_user = data;
};
var error = function(error){
debug_log(error['responseText']);
current_twitter_user = null;
debug_log("problem getting twitter user");
};
var ajax_data = create_twitter_oauth_ajax_data('http://api.twitter.com/1/users/show.json', 'get', {screen_name:localStorage.twitter_screen_name, user_id:localStorage.twitter_user_id}, success, error);
$.ajax(
ajax_data
);
Above I show some helper methods I created and a call to get a user's data (which doesn't necessarily need authentication, but demonstrates how it is done anyway).

The final thing that I learned has to do with the current version of JQuery Mobile. I was having trouble getting the app to switch pages when the pages were all in different files. JQuery Mobile is supposed to do an AJAX request for the separate files and then include them in the "one-page-app" that is creates. This didn't seem to be working, so what I did was include all my pages in the index.html page, and stopped trying to use separate files. To do this you just have to build separate divs with each of the pages.
<div data-role="page">

<div id="home_page" data-role="header">
</div><!-- /header -->

<div class="home_page" data-role="content">
<div id="main">
<div id="home">
<div id="logins">
<span id="twitter_login" class="button twitter"><img id="twitter_login_button" alt="twitter login" src="/images/twitter_16.png"/>&nbsp;Login</span><span class="logout no_show" id="twitter_logout_container" style="display:none"><img id="twitter_avatar" alt="" height="16" src="" width="16"><img src="/images/twitter_16.png" alt='Twitter' title="Twitter" /><a id="twitter_logged_in_link" href="#" target="blank"></a><a href="#" id="twitter_logout" class="logout">logout</a></span>
<span id="facebook_login" class="button facebook"><img src="/images/facebook_16.png" />&nbsp;Login</span><span class="logout no_show" id="facebook_logout_container" style="display:none;"><img id="facebook_avatar" alt="" height="16" src="" width="16"><img src="/images/facebook_16.png" alt='Facebook' title="Facebook" /><a id="facebook_logged_in_link" href="" target="blank"></a><a id="facebook_logout" href="#" class="logout">logout</a></span>
</div>

<div class="logo">
<img src="/images/logo.png"/>
</div>
<input type="text" id="where" value="Where are you headed?"/>
<div id="where_message_container">
<textarea id="where_message"></textarea>
<a href="#" id="where_message_submit">Submit</a>
</div>
</div>
</div>
<div id="fb-root"></div>
<script src="http://connect.facebook.net/en_US/all.js"></script>
<script>
FB.init({appId: '161124300591022', status: true, cookie: true,
xfbml: true, scope:'publish_checkins'});
</script>
</div>
<!-- /content -->

<div data-role="footer">
<div class="ui-grid-b">
<div class="ui-block-a">
<a href="#ui-page-start" data-role="button">
Heading Too
</a>
</div>
<div class="ui-block-b">
<a href="#/mobile/friends_headingtoos.html" data-role="button">
Friends
</a>
</div>
<div class="ui-block-c">
<a href="#/mobile/nearby_headingtoos.html" data-role="button">
Everyone
</a>
</div>
</div><!-- /grid-a -->
</div><!-- /page -->
</div>
<div id="/mobile/friends_headingtoos.html" data-role="page">

<div data-role="header">
<h1>Friends</h1>
</div><!-- /header -->

<div data-role="content">
<div id="friends_headingtoos">
<ul></ul>
</div>
</div><!-- /content -->
<div data-role="footer">
<div class="ui-grid-b">
<div class="ui-block-a">
<a href="#ui-page-start" data-role="button">
Heading Too
</a>
</div>
<div class="ui-block-b">
<a href="#/mobile/friends_headingtoos.html" data-role="button">
Friends
</a>
</div>
<div class="ui-block-c">
<a href="#/mobile/nearby_headingtoos.html" data-role="button">
Everyone
</a>
</div>
</div><!-- /grid-a -->
</div><!-- /header -->
</div>
<div id="/mobile/nearby_headingtoos.html" data-role="page">
<div data-role="header">
<h1>Nearby</h1>
</div><!-- /header -->

<div data-role="content">
<div id="nearby_headingtoos">
<div class="map"></div>
<ul></ul>
</div>
</div>
<div data-role="footer">
<div class="ui-grid-b">
<div class="ui-block-a">
<a href="#ui-page-start" data-role="button">
Heading Too
</a>
</div>
<div class="ui-block-b">
<a href="#/mobile/friends_headingtoos.html" data-role="button">
Friends
</a>
</div>
<div class="ui-block-c">
<a href="#/mobile/nearby_headingtoos.html" data-role="button">
Everyone
</a>
</div>

</div><!-- /grid-a -->
</div><!-- /header -->

Basically just adding in DIV's with the proper id's guarantees they will be loaded when a link with a href="#the_id" is clicked. This works with much more regularity than using external files. Since JQuery Mobile is so young I'd guess that this will be a non-issue at some point.

As always ask questions and leave feedback in the comments.

Wednesday, November 17, 2010

Static page apps -- where is the database?

Recently I've been doing a lot of work on applications integrating with social networks and came to the conclusion that with the jsonp apis available from Twitter and Facebook there is no need to actually have a database server, a web application server for dynamic html and all the other accoutrements that most web applications need.

This works rather well in a lot of cases (especially because of JQuery and newer frameworks for organizing single page apps, like sammy.js), I've only found 2 main things that frustrate going this way with all apps.
  1. Some very interesting API's do not have JSONP interfaces, this limits what we can work with and therefore what we can accomplish.
  2. There is no way to store data in a centralized database (HTML5 offers local storage options, but that doesn't allow for aggregation of data).
The first point above can be addressed using Yahoo!Pipes to essentially proxy calls and convert other kinds of output (XML, HTML, etc) to JSONP (see this). Yahoo!Pipes is a nice solution but doesn't have a strict SLA, published rate limits and an easy way to upgrade to a pay per request service.

The second point I haven't found any solution for at all. The problem seems to stem from the fact that private keys cannot be hidden client-side, therefore if one want to use something like Amazon SimpleDB one has to proxy the call themselves to keep their private keys stored securely server-side.

I think there is a spot for some cloud service businesses to fill the voids mentioned above (thinking Amazon style pay-per-use pricing model). I quickly mocked up a node.js app that takes a request with a API URL that is supposed to be proxied, gets the API URL, parses it and returns it as a JSONP response. Node.js is perfect for this kind of work because it is so IO intensive. The database issue is a slightly more difficult problem but is solvable as well.

Soooo.... Anyone interested in working on this? Anyone have some solutions I missed? Lemme know in the comments.

Monday, June 21, 2010

Code tests - What do they test?

So recently I had a recruiter set me up with an interview in a completely different industry (the financial industry) than my native industry (Internet). My understanding was I was coming as a senior, experienced PhD, with a background in making real systems work and scale. I wasn't particularly interested in a new job in the financial industry, but it sounded intriguing and they sounded like they really wanted me to check it out.

Things went swimmingly till I was asked to do a 2 hour timed code test. I balked, saying that I wasn't sure what they were going to learn from it, catching the HR person quite off guard. Long story short, they decided I wasn't "gung-ho" enough for the job, and we both moved on.

Well this whole thing got me thinking, what are code tests good for? I basically see 3 categories of job candidates, senior, intermediate and junior and the usefulness of code tests vary wildly from senior to junior. I think that HR folks need to be trained to handle each type differently. First off identifying the types:
  1. Senior - Years of experience (3 or more, but not hard and fast), example sites (a portfolio), production code to look at. Blog or tweets related to the field.
  2. Intermediate - Years of experience (less than 3, but not hard and fast), some example sites they worked on as a team but not many (a thin portfolio).
  3. Junior - Years of experience (less than 1, right out of school), very few or no examples.
If the HR person can't identify the person then don't give a code test right away, do a screening interview (< 30 minutes)

Once you have the person in/around a category (nothing is black and white) I think you handle all 3 differently.
  1. Senior - no code tests, respect their experience. Get another senior developer to interview (on the phone) to determine if the candidate is experienced, and most importantly has the right experience for the job. Make sure to ask about things that are key to your product/business, example: If you run a high traffic website, make sure to ask about cache-semantics, and/or eventual consistency.
  2. Intermediate - maybe give a code test (I wouldn't). Understand that a code test will be no more useful than a good interview at figuring out the candidate's skill set.
  3. Junior - by all means give a code test. The candidate has no reason to expect not to get a test, they have no proof of their ability.
So I think code tests are useful in some instances, but no matter what I think a good interview (not even a long one) will be able to determine a lot more than a code test. To be honest an interview is easier on the candidate, and the engineer who would have to grade the code test (boring, tedious work). The plus of an interview is that you get to feel out the candidate on culture as well, find out if they are funny, moody, easily frustrated...

One other thing is to consider the ordering of the process. I think a code test (as you can probably guess from above), should not be an initial step. With this finance company I had a 15 minute interview with an HR person and then was supposed to take a test. I had yet to know what the job would be (what tech is used, what Artificial Intelligence is used, if any, what the big problems are, in short, how fun the job would be for me), and I was being asked to devote 2 hours out of my (busy) life to do a timed test (which stressed me out).

So I think the best way to handle the process is to do some sort of screening interview with an engineer first. This is for the hiring company and the candidate! Both will have a much better idea of each other's interest level after that. Remember the candidate is interviewing you as much as you are interviewing the candidate. This especially goes for the highly experienced, highly skilled candidates.

So that is it, I guess that could be classified as a rant, but it is not a very aggressive one...

Thursday, June 10, 2010

Twitter @Anywhere Javascript API exposed


So I have been working on some serious Twitter and Facebook integration on the client-side for a recent project. To do some of the things I wanted to do I needed to use the Twitter @Anywhere Javascript API. The only problem with using the Twitter @Anywhere API is that it is very thinly documented and not likely to be updated soon.

Being the enterprising fellow I am, I pulled down there main API Javascript code from Firebug and put it into Eclipse (with Aptana) highlighted the one line of code (there were no line breaks), did an Apple-Shift-F on the code (fix the syntax), and boom! I now had a nicely formatted Javascript file with the Twitter @Anywhere API. I won't publish it here as that is Twitter's property and I don't want to get into any trouble.

What I will talk about is some of the things I learned about how to use the @Anywhere API; some things that your really need to make it feasible to use at all, even just to test.

First major find was how to pass parameters through the @Anywhere API to the regular Twitter API. The current @Anywhere cheatsheet describes doing calls like:


User.current.homeTimeline()

That by default will only give you back the first 20 items in the current user's feed, it doesn't allow using a "since_id" or a "max_id" parameter to restrict what items are returned, and it doesn't let you do pagination of the returned results.

From looking over the code I noticed you could input options to the call by doing the following:


User.current.homeTimeline({count:200, rpp:3, since_id:171981, max_id:181981});
This would fire off a request to the regular Twitter API that looks like:
http://api.twitter.com/1/statuses/home_timeline.xml?count=200&rpp=3&since_id=171981&max_id=181981
Which is exactly what we want, now we can control what we get back with the same precision as the regular Twitter API.

The next thing I figured out was how to avoid using the type of statements that the Twitter @Anywhere API cheatsheet examples use. For example the cheatsheet uses stuff like this:


User.current.homeTimeLine().first(20).each(
function(status) {
alert(status.text);
}
);

This is great when you want to do something atomically with each status you get back, but absolutely sucks when you want to know when you have completed going through all the statuses. The above example immediately returns, it is not synchronous, which makes it nearly impossible to know when it is done (maybe some closure could do it, but I couldn't figure it out).

To combat this I dug into the Twitter @Anywhere API further and found that I can also pass in options to put my own callback for success and error:

User.current.homeTimeline({count:200, rpp:3, since_id:171981, max_id:181981, success:my_success_method, error:my_error_method});

Now your callbacks will be called and you will see the "data" element passed in to your callback has an "array" element, which contains all the returned results. Now you can go through your results at your leisure, and know when you are done to fire off other methods.

That is pretty much all I have found out thus far, but it is enough to do quite a bit of work with the Twitter @Anywhere Javascript API. As always cheers and jeers in the comments...

Friday, May 7, 2010

Facebook OAuth on Rails..

So last night I took on trying to integrate Facebook's new OAuth authorization in Ruby On Rails, and in this post I am going to try and give everyone some idea of how to do this.

In the past I have use facebooker to integrate with facebook, so I already had a facebook.yml setup with all the configuration information for my facebook app:



development:
application_id: XXXXXXXXXXXX
api_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
secret_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
canvas_page_name: blah
callback_url: http://localhost:3000/
pretty_errors: true
set_asset_host_to_callback_url: false
tunnel:
public_host_username:
public_host:
public_port: 4007
local_port: 3000
server_alive_interval: 0

The only real difference between this and the normal facebooker file is the addition of the application_id, which is now used in the OAuth authentication.

The first step is to add an endpoint to start the first leg of the OAuth.

def new_facebook
facebook_settings = YAML::load(File.open("#{RAILS_ROOT}/config/facebooker.yml"))
redirect_to "https://graph.facebook.com/oauth/authorize?client_id=#{facebook_settings[RAILS_ENV]['application_id']}&redirect_uri=#{APP_URL}/facebook_credentials/facebook_oauth_callback&scope=offline_access,publish_stream"
end

This will redirect you to Facebook, it sets the "client_id" to the "application_id" mentioned above. This particular redirect also asks Facebook to ask your user to grant some special permissions, namely "offline_access", and "publish_stream" (allow me to access my users accounts anytime, and let me publish to their streams), there are many other permission detailed here. Also note that we provided a "redirect_url" to Facebook which Facebook will redirect to once the user grants us access to their Facebook account. So now we need to create an endpoint for the "redirect_url"



def facebook_oauth_callback
if not params[:code].nil?
facebook_settings = YAML::load(File.open("#{RAILS_ROOT}/config/facebooker.yml"))
callback = "#{APP_URL}/facebook_credentials/facebook_oauth_callback"
url = URI.parse("https://graph.facebook.com/oauth/access_token?client_id=#{facebook_settings[RAILS_ENV]['application_id']}&redirect_uri=#{callback}&client_secret=#{facebook_settings[RAILS_ENV]['secret_key']}&code=#{CGI::escape(params[:code])}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = (url.scheme == 'https')
tmp_url = url.path+"?"+url.query
request = Net::HTTP::Get.new(tmp_url)
response = http.request(request)
data = response.body
access_token = data.split("=")[1]
url = URI.parse("https://graph.facebook.com/me?access_token=#{CGI::escape(access_token)}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = (url.scheme == 'https')
tmp_url = url.path+"?"+url.query
request = Net::HTTP::Get.new(tmp_url)
response = http.request(request)
user_data = response.body
user_data_obj = JSON.parse(user_data)
flash[:notice] = 'Facebook successfully connected.'
@social_credential = SocialCredential.create_or_find_new_facebook_cred(access_token, session['rsecret'])
end
end
Here we are doing a lot of things. First we make sure that Facebook returns us the "code" parameter, we need this to complete the authentication. Once we have the code, we then need to request an "access_token" from Facebook. This was tricky for me, because the url is an SSL/HTTPS url and normal httparty or net/http tricks didn't work. Eventually I got it worked out as can be seen in the code above. Once we get the "access_token" we are pretty much done, in this case I go ahead and get the current user's Facebook data by hitting "https://graph.facebook.com/me?access_token=XXXXXXXXXXXXX" and then populate an ActiveRecord object with the data I receive (including the "access_token") for later use.

Keeping the "access_token" is important, because in my case it lets me update the users stream whenever I need to (hence the "offline_access" in the initial request).

I know from my Twitter stream that there is work on wrapping the Facebook OpenGraph API in ruby but I don't know where it stands. It is a pretty simple api, and won't require much, but it would be nice to have it abstracted some.

Okay, cheers and jeers in the comments please.

Sunday, April 11, 2010

My (new) Apple Manifesto

So the iPad came out and it is a pretty cool looking, interesting device. Following right on the heels of that was the iphone 4.0 OS announcement with all kinds of goodies and one glaring baddie. The baddie was Apple becoming even more closed by cutting anything that is not developed in Objective C out of the App Store. This change coupled with the random App Store rejections prompted me to rethink my philosophy on buying and using Apple products.

So I have an iPhone, a Mac Book Pro (MBP) and I also have a hackintoshed desktop running Snow Leopard. I like all of them (although for computers, I really just like OS X). I plan on keeping them all. I am also a developer and my philosophy on development is to keep things as open as possible. Therefore the Apple policies really bug me.

My decision (I know you have been holding your proverbial breathe) is NOT to boycott Apple products but to devote no time to building software that runs on their proprietary closed platform. If I want an iPad, or the new iPhone I'll get one, no need for self-denial. I just won't build anything for their systems, everything I build will be a web application of some sort using HTML 5 or whatever is available.

Maybe it is a cop-out, as I might still be feeding the beast, but I am not just going to ignore the best products on the market and by building more rich web applications instead of apps, I'll be opening up the Apple products despite what Apple is trying to do. I certainly will look at all the other options much more thoroughly, as I'd rather be on a more open platform. Lastly, I will definitely hackintosh more machines (I don't expect to buy ANY more Apple laptops or desktops, as I can hackintosh much cheaper and more powerful machines with no loss of OS X goodness).

So that is it, my new Apple manifesto, leave the cheers and jeers in the comments.