- Calendar -

March 2010
Mo Tu We Th Fr Sa Su
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31

- Archive -

- Browse By Random Tag -

- Most Commented -

- Random Favourites -

- Blogs I Like -

- Email Viruses Received -

- My Geek Code -

-BEGIN GEEK CODE BLOCK-
Version: 3.12
GIT d-- s: a- C++ UL++ P+++ L+++ E--- W+++ N+ o-- K- w--- O- M-- V- PS+++ PE-- Y++ PGP t++ 5+++ X R tv b+ DI+ D++ G e h r+ y+
--END GEEK CODE BLOCK--
Get The Encoder
Get The Decoder

- My Blog Code -

-BEGIN BLOG CODE BLOCK-
B6 d+ t++ k+ s++ u-- f i++ o+ x+ e l c-- --END BLOG CODE BLOCK--
Blog Code Encoder
Blog Code Decoder

- The Internet is Cool -

- Nifty Blog Toys -

RSS Feed

- Content License -

Blog

Facebook Page Syncronisation

This is going to be a rather technical post, coupled with a smattering of rants about Facebook so those of you uninterested in such things might just wanna skip this one.

As part of my work on my new company, I'm building a syncroniser for status updates between Twitter, Facebook, and our site. Eventually, it'll probably include additional services like Flickr, but for now, I'm just focusing on these two external systems.

A Special Case

Reading this far, you might think that this isn't really all that difficult for either Twitter or Facebook. After all, both have rather well-documented and heavily used APIs for pushing and pulling data to and from a user's stream, so why bother writing about it? Well for those with my special requirements, I found that Facebook has constructed a tiny, private hell, one in which I was trapped for four days over the Christmas break. In an effort to save others from this pain, I'm posting my experiences here. If you have questions regarding this setup, or feel that I've missed something, feel free to comment here and I'll see what I can do for you.

So, lets start with my special requirements. The first stumbler was the fact that my project is using Python, something not officially supported by Facebook. Instead, they've left the job to the community which has produced two separate libraries with different interfaces and feature sets.

Second, I wasn't trying to syncronise the user streams. Instead, I needed push/pull rights for the stream on a Facebook Page, like those created for companies, politicians, famous people, or products. Facebook claims full support for this, but in reality it's quite obvious that these features have been crowbared into the overall design, leaving gaping holes in the integration path.

What Not to Do

  • Don't expect Facebook to do the right/smart thing. Everything in Facebookland can be done in one of 3 or 4 ways and none of them do exactly what you want. You must accept this.
  • Don't try to hack Facebook into submission. It doesn't work. Facebook isn't doing that thing that makes sense because they forgot or didn't care to do it in the first place. Accept it and deal. If you try to compose elaborate tricks to force Facebook's hand, you'll only burn 8 hours, forget to eat or sleep in the process and it still won't work.

What to Do

Step 1: Your basic Facebook App

If you don't know how to create and setup a basic canvas page in Django, this post is not for you. Go read up on that and come back when you're ready.

You need a simple app so for starters get yourself a standard "Hello World" canvas page that requires a login. You can probably do this in minifb, but PyFacebook makes this easy since it comes with handy Django method decorators:

# views.py
from django.http import HttpResponse, HttpResponseRedirect
import facebook

@facebook.djangofb.require_login()
def fbCanvas(request):
    return HttpResponse("Hello World")
Step 2: Ask the User to Grant Permissions

This will force the user to add your application before proceeding, which is all fine and good but that doesn't give you access to much of anything you want, so we'll change the view to use a template that asks the user to click on a link to continue:

# views.py
from django.shortcuts import render_to_response
from django.template import RequestContext
import facebook

@facebook.djangofb.require_login()
def fbCanvas(request):
    return render_to_response(
        "social/canvas.fbml",
        {},
        context_instance=RequestContext(request)
    )

Note what I mentioned above, that we're asking the user to click on a link rather than issuing a redirect. I fought with Facebook for a good few hours to get this to happen all without user-input and it worked... sometimes. My advice is to just go with the user-clickable link. That way seems fool-proof (so far).

Here's our template:

<!-- canvas.fbml -->
<fb:header>
    <p>To enable the syncronisation, you'll need to grant us permission to read/write to your Facebook stream.  To do that, just <a href="http://www.facebook.com/connect/prompt_permissions.php?api_key=de33669a10a4219daecf0436ce829a2e&v=1.0&next=http://apps.facebook.com/myappname/granted/%3fxxRESULTTOKENxx&display=popup&ext_perm=read_stream,publish_stream,offline_access&enable_profile_selector=1">click here</a>.
</fb:header>

See that big URL? It's option #5 (of 6) for granting extended permissions to a Facebook App for a user. It's the easiest to use and hasn't broken for me yet (Numbers 1, 2, 3 and 4 all regularly complained about silly things like not having the app instaled when this was not the case, but your milage may vary). Basically, the user will be directed to a page asking her to grant read_stream, publish_stream, and offline_access to your app on whichever pages or users she selects from the list of pages she administers. Details for modifying this URL can be found in the Facebook Developer Wiki.

Step 3: Understanding Facebook's Hackery

So you see how in the previous section, adding enable_profile_selector=1 to the URL will tell Facebook to ask the user to specify which pages to which she'd like to grant these shiny new permissions? Well that's nifty and all, but they don't tell you which pages the user selected.

When the permission questions are finished, Facebook does a POST to the URL specified in next=. The post will include a bunch of cool stuff, including the all important infinite session key and the user id doing all of this, but it doesn't tell you anything about the choices made. You don't even know what page ids were in the list, let alone which ones were selected to have what permissions. Nice job there Facebook.

Step 4: The Workaround

My workaround for this isn't pretty, and worse, depends on a reasonably intelligent end-user (not always a healthy assumption), but after four days cursing Facebook for their API crowbarring, I could come up with nothing better. Basically, when the user returns to us from the permissioning steps, we capture that infinite session id, do a lookup for a complete list of pages our user maintains and then bounce them out of Facebook back to our site to complete the process by asking them to tell us what they just told Facebook. I'll start with the page defined in next=:

# views.py
@facebook.djangofb.require_login()
def fbGranted(request):

    from cPickle import dumps as pickle
    from urllib  import quote as encode

    from myproject.myapp.models import FbGetPageLookup

    return render_to_response(
        "social/granted.fbml",
        {
            "redirect": "http://mysite.com/social/facebook/link/?session=%s&pages=%s" % (
                request.POST.get("fb_sig_session_key"),
                encode(pickle(FbGetPageLookup(request.facebook, request.POST["fb_sig_user"])))
            )
        },
        context_instance=RequestContext(request)
    )
# models.py
def FbGetPageLookup(fb, uid):
    return fb.fql.query("""
        SELECT
            page_id,
            name
        FROM
            page
        WHERE
            page_id IN (
                SELECT
                    page_id
                FROM
                    page_admin
                WHERE
                    uid = %s
            )
    """ % uid)

The above code will fetch a list of page ids from Facebok using FQL, and coupling it with the shiny new infinite session key, bounce the user out of Facebook and back to your site where you'll use that info to re-ask the user about which page(s) you want them to link to Facebook.

Step 5: Capture That page_id

How you capture and store the page id is up to you. For me, I had to create a list of organisations we're storing locally and let the user compare that list of organisations to the list of Facebook Pages and make the links appropriately. Your process will probably be different. Regardless of how you do it, just make sure that for every page you wish to syncronise with Facebook, you have a session_key and page_id.

Step 6: Push & Pull

Because connectivity with Facebook (and Twitter) is notonoriously flakey, I don't recommend doing your syncronisation in real-time unless your use-case demands it. Instead, run the code via cron, or better yet as a daemon operating on a queue depending on the amount of data you're playing with. However you do it, the calls are the same:

import facebook

# Setup your connection
fb = facebook.Facebook(settings.FACEBOOK_API_KEY, settings.FACEBOOK_SECRET_KEY)
infinitesessionkey = "your infinite session key from facebook"
pageid             = "the page id the user picked"

# To push to Facebook:
fb(
    method="stream_publish",
    args={
        "session_key": infinitesessionkey,
        "message":     message,
        "target_id":   "NULL",
        "uid":         pageid
    }
)

# To pull from Facebook:
fb(
    method="stream_get",
    args={
        "session_key": infinitesessionkey,
        "source_ids": pageid
    }
)["posts"]

Conclusion

And that's it. It looks pretty complicated, and... well it is. For the most part, Facebook's documentation is pretty thorough, it's just that certain features like this page_id thing appear to have fallen off their radar. I'm sure that they'll change it in a few months though, which will make my brain hurt again :-(

Catching up Before the End

It's funny, I've had mountains of "free" time lately and somehow, none at all available to do the simplest of cumulative tasks. I've not replied to the nineteen emails sitting in my inbox, and keeping this site up to date has clearly not been a priority. However, in an effort to "clean house" so to speak before the New Year, I'll try to cover everything here. If you like to read everything, I suggest taking a moment to procure a beverage.

Carolling: A Reunion

Grandma Nana at Christmas dinner

Way back in October, I received a text message from my old friend Michelle containing a request to re-capture some of our better memories by going carolling this year, an annual tradition we once supported by hadn't attempted for nearly a decade. Excited at the thought of it, I agreed to play my role and she recruited Gary (another old friend) and a Soprano friend of theirs for the task. I did some digging of my own and managed to coax Merry out as well and with a group of five very out-of-practise choir folk, we set out on December 19th to bring some Christmas cheer to the suburbs.

The whole thing didn't go off nearly as well as we'd hoped at the start. The first neighbourhood we landed in seemed to be filled with people who didn't like carollers at all. No matter how hard we sung, no one came to the door. We quickly decided that Surrey sucked and that the uber-Christians in Langley were more likely to be receptive. We were right, and then tilted the odds even greater in our favour by selectively hitting neighbourhoods filled with Christmas lights and people we knew personally :-) This made the bitter cold somewhat more bearable since we were repeatedly asked in for free drinks and cookies. Had the night been kinder and our start been earlier, we might have hit more houses, but as it worked out, we collected $30 for the food bank and had a really nice time singing with old friends.

My parents at Christmas dinner

I'd also like to take a moment to thank Michelle personally for single-handedly organising the whole thing. Despite my best intentions, I contributed very little to the planning. Michelle is a rock star.

Christmas: Another Reunion

Fighting the odds, I managed to catch my flight out of Vancouver to Kelowna on time, bailing out of the Lower Mainland just before the Storm from Hell ravaged the area. My condolences to those who were booked on flights set to leave only hours after mine -- as I understand it, a whole lot of people spent Christmas in YVR this year.

I arrived here in Kelowna in preparation of two big events: Christmas and my cousin Ashley's wedding. Thanks to the latter, the former was filled with distant relatives whom I see to rarely as it is. Ashley's brother Fraser was here, all the way from London and he brought is girlfriend and their common friend, both from Spain. My (2nd) cousin Roy was here, as was his mother June and a big chunk of my uncle's family as well. All good people, all with interesting stories I've not heard before.

The happy couple: Ashley and Jared Nelson

In terms of a Christmas "haul", the biggest most impressive gift was a hand-made cookbook from my parents containing family recipes from all the big chefs in the family. My father's pastas, my grandmother's famous soup... it's all in there. A really great gift.

Oh, and Lara, you'll be pleased to know that I got six pairs of socks as well :-)

The Wedding

If you've been following my Twitter feed, you probably already know that Ashley's wedding was outside, in the dark, on a mountain, under the trees, in the snow... with bagpipes. It sounds insane, and it was, but it was also beautiful. Ashley wore a gorgeous gown, and covered it with a pretty white hood to keep her warm during the (mercifully short) service. The bride cried, the groom cried, and I think even the Man of Honour cried. Young love is so cute. The Groom wore a black tux with red pinstripes and a white tie and, along with his groomsmen, bright red skate shoes. They were awesome.

The reception was about as fun and exciting as most receptions usually are. Lots of old people, lots of 80s and 90s music (courtesy of my brother the DJ) and lots of dancing. The bride and groom had a few really great performances on the dance floor and much fun was had by all. Only one blight on the whole thing really: one of the guests, a bridesmaid's date no less showed up in jeans, a hoodie, a cowboy hat, and plumber's crack. I tried to convince my mother to lecture him on his lack of respect but she didn't go for it. But yes, this is normal out here.

Catching up

My brother the DJ

I decided before I came up here that I'd spend a great deal of time teaching myself a new web framework called Django. It's a real framework (as opposed to Drupal, which is in fact a content-management system) based on a relatively new language called Python. So far the experience has been two-sided for me. On the one hand Django appears to do a lot for you so code is smaller and easier to maintain, but on the other hand I feel like a lot of the simplicity and art in coding has disappeared. Where you once saw a long, easy to read set of files filled with a series of very short declarative statements, you now have something that reads more like a novel. More compact yes, but is it art anymore?

I've also promised myself that I'd get through my emails this week -- all nineteen of them. This task, along with fixing up Stephen's site (I haven't forgotten about you!) has proven ridiculously difficult though, since Internet connectivity here is terrible at best. I have to syphon access from a neighbour's flaky router that routinely drops connectivity for hours at a time. At this very moment in fact, I'm writing this post into a file in the hopes that I'll be able to acquire some bandwidth later tomorrow at my father's store.

So that's everything for now. It's 2:30am now, but before I go to bed I think that I'll put together some good images for this post. I'll try to find some good shots of Christmas and the wedding. Next up is my New Year's recap post -- not sure when I'll have time to write it though.

pit-faulty