Friday, October 4, 2013

Facebook Graph API & OAuth 2.0 & Flash

Facebook Graph API & OAuth 2.0 & Flash (update)



As previously mentioned, facebook released a new Graph API. It is based on OAuth 2.0 protocol (old authorization token also works). While it is fresh thing, there is no much ActionScript stuff around, so I came with FacebookOAuthGraph class. This class is meant to be used as an abstract class, while it contains just the basic authentication algorithm and call method to request data. It stores access token in SharedObject, so next time you came into app, you get connected on background without noticing (no popup etc.). Your token should expire in 24 hours.

Here is the code for the following flex app, to make it work, get latest FacebookOAuthGraph and FacebookOAuthGraphEvent classes.
?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
    applicationComplete="init()">
<mx:Script>
<![CDATA[
     
    import sk.yoz.events.FacebookOAuthGraphEvent;
    import sk.yoz.net.FacebookOAuthGraph;
     
    // facebook Application ID
    private var clientId:String = "268718683475";
     
    // path to our callback
    private var redirectURI:String =
        "http://blog.yoz.sk/examples/FacebookOAuthGraph/callback.html";
     
    // required extended permissions
    private var scope:String = "publish_stream,user_photos,user_photo_video_tags";
     
    private var facebook:FacebookOAuthGraph = new FacebookOAuthGraph();
     
    [Bindable] private var connected:Boolean;
     
    private function init():void
    {
        facebook.clientId = clientId;
        facebook.redirectURI = redirectURI;
        facebook.scope = scope;
        facebook.useSecuredPath = true;
        facebook.addEventListener(FacebookOAuthGraphEvent.AUTHORIZED, authorized);
 
        // stage.root.loaderInfo.parameters
        facebook.autoConnect(parameters);
         
        log.text += "checkSavedToken()\n";
    }
     
    private function connect():void
    {
        facebook.connect();
         
        log.text += "connect()\n";
    }
     
    private function authorized(event:FacebookOAuthGraphEvent):void
    {
        connected = true;
         
        log.text += "authorized\n";
    }
     
    private function call(path:String, binary:Boolean):void
    {
        var loader:URLLoader = facebook.call(path);
        loader.dataFormat = binary
            ? URLLoaderDataFormat.BINARY
            : URLLoaderDataFormat.TEXT;
        loader.addEventListener(FacebookOAuthGraphEvent.DATA, callComplete);
        log.text += "call(" + path + ")\n";
    }
     
    private function changeStatus(message:String):void
    {
        var data:URLVariables = new URLVariables();
        data.message = message;
        var method:String = URLRequestMethod.POST;
        var loader:URLLoader = facebook.call("me/feed", data, method);
        loader.addEventListener(FacebookOAuthGraphEvent.DATA, callComplete);
        log.text += "changeStatus(" + message + ")\n";
    }
     
    private function callComplete(event:FacebookOAuthGraphEvent):void
    {
        log.text += "call completed -> see result\n";
         
        if(event.rawData is ByteArray)
        {
            var loader:Loader = new Loader();
            loader.loadBytes(event.rawData as ByteArray);
            loader.contentLoaderInfo.addEventListener(Event.COMPLETE,
                function():void
                {
                    image.source = loader;
                });
        }
        else
        {
            result.text = event.rawData.toString();
        }
    }
]]>
</mx:Script>
<mx:HBox>
    <mx:Button click="connect()" label="connect" />
    <mx:Text text="{connected ? 'connected' : 'not connected'}" />
</mx:HBox>
<mx:HBox visible="{!connected}" includeInLayout="{!connected}">
    <mx:Text text="#access_token=121161974560905%7C2..." />
    <mx:TextInput id="hash" />
    <mx:Button label="add hash" click="facebook.confirmConnection(hash.text)"/>
</mx:HBox>
<mx:HBox>
    <mx:TextInput id="path" text="me" />
    <mx:Button label="call" click="call(path.text, false)" enabled="{connected}"/>
    <mx:Spacer width="20" />
    <mx:TextInput id="path2" text="me/picture" />
    <mx:Button label="call binary" click="call(path2.text, true)" enabled="{connected}"/>
</mx:HBox>
<mx:HBox>
    <mx:TextInput id="status" text="testing FacebookOAuthGraph" />
    <mx:Button label="change status" click="changeStatus(status.text)" enabled="{connected}"/>
</mx:HBox>
<mx:HDividedBox width="100%" height="100%">
    <mx:TextArea width="30%" height="100%" id="log"/>
    <mx:TextArea width="70%" height="100%" id="result"/>
    <mx:Image id="image" />
</mx:HDividedBox>
</mx:Application>
Make sure your html wrapper defines correct allowScriptAccess and both id and name for <object> tag. This enables ExternalInterface.objectID. With swfobject use:
?
01
02
03
04
05
06
07
08
09
10
var params = {
    allowScriptAccess: "sameDomain"
};
 
var attributes = {
    id: "FacebookOAuthGraphTest",
    name: "FacebookOAuthGraphTest"
};
swfobject.embedSWF("FacebookOAuthGraphTest.swf", "alternative", "100%", "100%", "10.0.0",
    "expressInstall.swf", flashvars, params, attributes);
callback.html pushes url hash into flash app. When running this application from desktop (creating/debugging), your callback.html located on public domain has no access to its opener (different domain – XSS), so you need to pass access_token manualy into <TextInput id=”hash”>, but once your flash application is on the same domain with callback, it works automaticaly.
callback.html:
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="sk" lang="sk" dir="ltr">
<head>
    <script type="text/javascript">
    <!--
        if(window.opener && window.opener.confirmFacebookConnection)
        {
            window.opener.confirmFacebookConnection(window.location.hash);
            self.close();
        }
    //-->
    </script>
</head>
<body>
<p>You may now close this window.</p>
</body>
</html>
Click connect and allow facebook application. Facebook redirects to callback.html that pastes hash into flash and closes popup. Now you are authenticated. Next time you visit this flash application (refresh this page) you will get authenticated in background (if your access token is still valid). Notice, some graph api calls returns JSON objects (me), other may return binary data (me/picture). For now it may take some time to finish calls (5 second or more), but I hope facebook will soon make it fast.

You get your JSON decoded data via event.data. Just make sure you do not try to decode ByteArray (eg. me/picture)
?
1
2
3
4
5
// call("me")
private function callComplete(event:FacebookOAuthGraphEvent):void
{
    trace(event.data.name);
}
Some calls to test:
?
01
02
03
04
05
06
07
08
09
10
11
Friends:             me/friends
News feed:           me/home
Profile feed (Wall): me/feed
Likes:               me/likes
Movies:              me/movies
Books:               me/books
Notes:               me/notes
Photos:              me/photos
Videos:              me/videos
Events:              me/events
Groups:              me/groups
Additional information about my facebook app (see all settings):
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
Application ID:         268718683475
App Domain:              yoz.sk
Website / Site URL:      http://blog.yoz.sk/examples/FacebookOAuthGraph/
App on FB / Canvas URL:  http://blog.yoz.sk/examples/FacebookOAuthGraph/facebook.php?a=b
Page Tab / Page Tab URL: http://blog.yoz.sk/examples/FacebookOAuthGraph/facebook.html?
Canvas type:             iframe
Developer Mode:          Off
App Type:                Native/Desktop
Sandbox Mode:            Disabled
Remove Deprecated APIs:  Enabled
signed_request for Canvas: Enabled
Timezone-less events:    Enabled
Encrypted Access Token:  Enabled
...other Migrations:     Disabled
Based on facebook access token, it should be valid approximately for 24 hours. Notice the bold part, works as expiration time (unix time format). Use FacebookOAuthGraph.tokenToExpiration() to parse Date:
access_token=268718683475%7C2.w3fjqz80Xi1CdBXt7Ygh6A__.86400.1273158000-1215645368%7CDGv2l2HtTymd6cM6Fy_8k6P_8CQ.
Important update: May 19, 2010: Application is working again. Due to massive support in bug tracker (thanks all for your votes), facebook devs changed secured crossdomain.xml, so it allows unsecured requests. changeStatus() method added into example and some minor changes in FacebookOAuthGraph class (please update).
Here is what happend and why this app was not working for a while:
  • May 11, 2010: facebook changed rules, so requests on unsecured graph service (via http://graph.facebook.com…) were limited to just those without access_token parameter. Requets to secured service (https://graph.facebook.com) resulted in security violation due to missing secure=”false” parameter in crossdomain.xml
  • May 12, 2010: bug submitted
  • May 12, 2010 – May 14, 2010: massive bug voting
  • May 14, 2010: Bug confirmed by facebook dev team
  • May 19, 2010: Bug fixed, crossdomain file changed
I am glad that facebook devs listens and care. :-)

FAQ

Is there a .fla version available?

Yes, there is a simple working Flash CS4 archive available for download (.fla + all sources). The demo connects the facebook application and once connected, it uploads generated picture into photo album.

How can I use this class in my iframe canvas page?

Please read article Authorizing Iframe Facebook Applications For Graph API

How can I make this class do more advanced things and what are the best practices?

Please read article Extending FacebookOAuthGraph Class, where all the most comon extendings are described.

When I connect with this app in one browser (firefox), and I run another browser (chrome) I get automatically connected. Why?

Your authorization token is stored in SharedObject, that is OS-user persistent (eg. windows user). No matter what browser you run, all of those reads data from one SharedObject. If you need cookie or session persistent authorization, please extend my class and override methods that use SharedObject.

How to add request parameters into my call?

Pass URLVariables object as a second parameter to call() method:
?
1
2
3
4
// eg. in order to make this call "me/picture?type=large", do the following:
var data:URLVariables = new URLVariables();
data.type = "large"
call("me/picture", data);

Why I can not access me/photos?

Facebook has changed rules again. You also need “user_photo_video_tags” permission within your app. I have already added it into my app, so please remove your browser cache, refresh and click connect button again (even if you are connected already). Now it should work.

How to upload photo with graph api?

This is a piece of working code from Sean, thnx Sean:
?
01
02
03
04
05
06
07
08
09
10
// MultipartURLLoader by Eugene Zatepyakin can be found here: http://bit.ly/9wx4q7
public function uploadImageCall(path:String, ba:ByteArray, message:String, token:String=null):MultipartURLLoader
{
    var mpLoader:MultipartURLLoader = new MultipartURLLoader();
    mpLoader.addVariable("message", message);
    mpLoader.addFile(ba, "image.jpg", "image");
    loaderAddListeners(mpLoader.loader);
    mpLoader.load(apiSecuredPath + "/me/photos?access_token="+ token);
    return mpLoader;
}

Can I make fql calls with this class?

However graph api does not implement fql calls, you can make fql calls using this class. The resulted data are not JSON but XML:
?
1
2
3
4
5
6
7
8
var data:URLVariables = new URLVariables();
data.query = "SELECT uid, name FROM user WHERE uid = XXX"; // insert your uid
var loader:URLLoader = facebook.call("method/fql.query", data,
    URLRequestMethod.POST, null, "https://api.facebook.com");
loader.addEventListener(FacebookOAuthGraphEvent.DATA, fqlComplete);
 
private function fqlComplete(event:FacebookOAuthGraphEvent):void{
        var xml:XML = new XML(event.rawData);
For fqlComplete method, please read Parsing FQL result article.

Can I post feeds with attachments?

Graph API has not documented attachment functionality for feeds, anyway, you can publish streams with attachments with this class as simple as:
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
var media:Object = {};
media.type = "flash";
media.swfsrc = "http://zombo.com/inrozxa.swf";
media.imgsrc = "http://blog.yoz.sk/wp-content/uploads/3d-150x150.jpg";
media.width = "80";
media.height = "80";
media.expanded_width = "120";
media.expanded_height = "120";
 
var attachment:Object = {};
attachment.name = "test name";
attachment.href = "http://blog.yoz.sk"
attachment.description = "test description";
attachment.caption = "test caption";
attachment.media = [media];
 
var data:URLVariables = new URLVariables();
data.message = "test message";
data.attachment = JSON.encode(attachment);
 
facebook.call("method/stream.publish", data, URLRequestMethod.POST, null, "https://api.facebook.com");

How can I develop my app locally when callback only works on the domain?

While callback is not able to push access_token into your app on runtime, I just copy access_token value into my code and publish again:
?
1
2
3
4
5
6
if(!parameters.session)
parameters.session = JSON.encode({
    access_token:String("123864964303507%7C2._pSS7WlemeGB9M0RV8Vnuw__.3600.1276246800-1215645368%7C-kYjWPx4MR6DXc5Clcnv5kXX3t4.")
        .replace(/\%7C/g, "|")
});
facebook.autoConnect(parameters);

Hi Is there a way to implement a “i Like” button with your class?

Sorry, you can not use graph API to like stuff, there is no method for that. Even by testing like request:
?
01
02
03
04
05
06
07
08
09
10
href: http://www.facebook.com/pages/***
node_type: page
edge_type: like
page_id: 12345
action_text
now_connected: true
nctr[_mod]: connect
post_form_id: 123ABF***********
fb_dtsg: TAfAJ
post_form_id_source: AsyncRequest
… post_form_id is some static required parameter that you can not guess.

This example app works with Internet Explorer, but my one results in Error #2032

Please complie your app to Flash Player 10 (or later). It should fix this issue. Credits goes to Garcimore :-) , thnx. The issue has been identified as header Content-Type: application/json, that is returned from facebook. Suggested workaround (by facebook dev team) is to use POST variables with your requests, however it is reported as not working solution.

I can not make it run in my Mac-Safari sonfiguration

It seems there is a bug in Safari on Mac that does not let you open popup via ExternalInterface. You should use navigateToURL() in that case. Credits goes to Beans, thank you. More instructions here.
?
1
2
3
var js:String = "return window.navigator.userAgent.indexOf('Safari') > -1);"
if(ExternalInterface.call("function(){" + js + "}")){
   // safari specific code

Source:
http://blog.yoz.sk/2010/05/facebook-graph-api-and-oauth-2-and-flash/

0 comments:

Post a Comment