Use the Facebook API in a Google Apps Script web app?

Protector one picture Protector one · Jan 30, 2014 · Viewed 14.9k times · Source

I'm trying to create a Facebook app using a Google Apps Script as web app, as the back-end. The only Facebook API that seems applicable is the Javascript SDK, but I can't even get that one to work.
The current problem I'm having is that the Facebook JS SDK uses Javascript identifiers that end with: "__". Google Apps Script restricts names that end in double underscores.

If I use a modified copy of Facebook's JS file without double underscores in names, I get this error:
Refused to display [URL] in a frame because it set 'X-Frame-Options' to 'SAMEORIGIN'

Any idea how to get GAS playing nicely with FB?

Answer

Alan Wells picture Alan Wells · Apr 25, 2014

I've figured out how to use Apps Script UrlFetchApp.fetch and the HTML Service to log a user in to my Apps Script App with Facebook. I can also post to Facebook with Apps Script.

  • You don't need the Facebook Javascript SDK

General Overview

There are 8 different Facebook Platforms:

Facebook Platforms

The Facebook Platform that I'm using with Apps Script is the Website platform. The Website does not run embedded in Facebook.

What you can't do: (As far as I know)

  • There is a difference between a Website that interacts with Facebook, and a Facebook Page Tab. A Facebook Page Tab runs embedded in Facebook. My experience is, that the Apps Script URL causes problems with a Facebook Page Tab. The URL for Apps Script has lots of stuff appended to the end that Facebook seems to strip out. (Or something like that) I've tried many different variations of Google products with Facebook. You can use a Google Site as a Facebook Page Tab, but if you embed a Google Apps Script inside a Site, that is then inside a Facebook Page Tab, it causes a Cross Domain error. So, the only option I can get to work is the Website option.

A sort of work around that I use for the Page Tab problem, is to use Thunderpenny Static HTML as a Facebook Page Tab, that then links to either my Apps Script App, or a GAE App. (Depending upon whether I need HTTP S or not)

You could use a Google Site as the Page Tab, but Thunderpenny can design an app with HTML, Javascript and CSS as you normally would. OH! And I've tried using the Facebook Javascript SDK inside of Thunderpenny, and got nowhere with that. Besides, the Thunderpenny app doesn't have a backend, Apps Script does have a backend (.gs code) where you can hide your Facebook App Token.

The basics steps are:

  • Trigger a link to Facebook oauth from your App.
  • href="https://www.facebook.com/dialog/oauth?client_id=yourIDhereNoQuotes&redirect_uri=https://script.google.com/macros/s
  • Capture the returned Facebook token from the URL with an OnLoad function
  • You can NOT process the Facebook redirect URL back to your application with doGet(e). Here's why. Apps Script needs to see a question mark in the URL in order to parse the URL. Facebook returns a URL with a different configuration.
  • Run a script when your app loads to process the Facebook Token
  • window.onload=function(){};
  • The onload function runs a .gs function to verify the token
  • Use UrlFetchApp.fetch to debug the token
  • Cache the login status in private cache
  • var cache = CacheService.getPrivateCache();
  • Check log in status when needed.

Note that Facebook keeps track of your app login status independently. But your app could have a logged in status of still active when the Facebook token has expired. So you need a way to check that.

Use Official Facebook Graphics

Brand Resources - Facebook

Download Facebook graphics. Read Do's and Dont's

Create a Facebook Login Button

<div id="FbLog">
    <a href="https://www.facebook.com/dialog/oauth?client_id=YourClientID&redirect_uri=https://script.google.com/macros/s/YourAppsScript/exec?&response_type=token&scope=publish_stream"><img src="https://FacebookGraphic.png" height="95%" width="95%"></a>
</div>

Style the Facebook Login Button

#FbLog {
    padding: 10px;
    background-color: white;
    margin-left:auto;
    margin-right:auto;
    cursor: pointer;
}

Window OnLoad Code

Here is the window.onload client side code that I use to capture the Facebook token. This is only used to capture the Facebook token, not to validate the token. My app allows both Facebook login, and a regular login. Validation of the Facebook token is done in another code.

window.onload=function(){
  //console.log("This onload did run");

  //get the URL out of the browsers address bar
  //the URL can have multiple different strings attached depending upon the situation.
  //1)Sign in with Facebook. 2) Someone wanting to buy an item 3) Someone wanting to input an item for sale.
  //Any situation other than the FB log in is initiated in the back end.
  window.daURL = window.location;
  window.urlStrng = daURL.toString();

  //console.log("url: " + urlStrng);
  //If there was a FaceBook login, there will be a hashtag in the url string
  window.hashPos = urlStrng.indexOf("#");
  if (window.hashPos > 0) {
    mainStart('InputBlock', 'InputForm');
    window.urlEnd = urlStrng.split("#", 2);
    window.urlTkn = urlEnd[1].split("&", 2);
    window.fbAcsTkn = urlTkn[0].split("=", 2);
    window.finalTkn = fbAcsTkn[1];

    window.scndExpire = urlStrng.substring(urlStrng.indexOf("_in=")+4, urlStrng.length);
    console.log("scndExpire: " + scndExpire);

    google.script.run.withFailureHandler(onFailure)
    .withSuccessHandler(showFBsuccess)
    .processFB_LogIn(window.finalTkn, scndExpire)
  }
  else {
    //If it's not a Facebook log in, go to next two choices
    //If the URL string has &L in it, then item listing info is being passed because someone clicked 'Buy'
    window.whatToLoad = urlStrng.indexOf("&L");
    console.log("Second option of onload ran");
    if (window.whatToLoad > 0) {
      google.script.run.withFailureHandler(onFailure)
        .withSuccessHandler(injectBuyForm)
        .include('MutualCmit');
     } else {
     google.script.run.withFailureHandler(onFailure)
       .withSuccessHandler(injectSignInForm)
       .include('SignIn');
     };
  };
};

Note that even though the Facebook login is triggered in the front end, the validation happens in .gs code. Someone could inject a false Facebook token, but it's not going to pass inspection in the server side code.

This is the .gs code to process the Facebook Login:

Validation of Facebook Token GS Code

//I put this cache line at the very top of the `.gs` file. The other code
// can be put somewhere lower.

var cache = CacheService.getPrivateCache();

function processFB_LogIn(argFB_Tkn, expTime) {
    cache.put('fbTkn', argFB_Tkn, 4000);
    cache.put('fbExpr', expTime, 4000);

    var meFBtkn = cache.get('fbTkn');

    Logger.log("FaceBook Token: " + meFBtkn);

     //This section is for verifying (debug) the user actually signed in through Facebook
    //The first FB token is passed in from the URL right after the user signs in, and when this apps Script loads.
    //IMPORTANT!!!    IMPORTANT!!!   You MUST escape the | character with code %7C

    var AppAccssTkn = 'YourAppID%7YourAppToken'; //This never changes unless app secret changes
    var optnGetTkn = {"method" : "get", "muteHttpExceptions" : true};
      //This 'Debugs' the token returned in the URL after the user signed in with Facebook.  You "should" verify that the token is real.
      var rsltDebug = UrlFetchApp.fetch("https://graph.facebook.com/debug_token?input_token="  + meFBtkn  + "&access_token=" + AppAccssTkn, optnGetTkn);
      var debugTxt = rsltDebug.getContentText();
      Logger.log("debugTxt: " + debugTxt);

      var jsonObj = JSON.parse(debugTxt);
      Logger.log("jsonObj: " + jsonObj);
      //This is the FB user ID
      var useIdTxt = jsonObj.data.user_id;
      cache.put('pubIDcache', useIdTxt, 4000);

      var tknValid = jsonObj.data.is_valid;

      Logger.log("reslt of the debug: " + useIdTxt);
      Logger.log("tknValid: " + tknValid);

      var getFbUseName = UrlFetchApp.fetch("https://graph.facebook.com/" + useIdTxt + "/?fields=first_name&access_token=" + AppAccssTkn, optnGetTkn);

      var objUseName = JSON.parse(getFbUseName);
      var arryFirstName = objUseName.first_name;
      Logger.log("user name: " + arryFirstName);

      cache.put('fbFrstName', arryFirstName, 4000);

   if (tknValid === false) {
     return 'notValid';
   }
   else if (arryFirstName != null) {
     //This is how it's determined if someone is logged in or not.
     cache.put('imin', '9847594ujglfugfjogj', 4000);
     return arryFirstName;
  };
};

You need a one time App token that won't change unless you App Secret changes. You must generate that with a one time run piece of code.

Get Your App Access Token with this code:

//A Facebook App Token never changes unless you go to the Facebook Developers Console, and you
//change the App Secret.  So, do NOT keep requesting a new App Token.  Just get it once, then
//hard code it into a backend secret function.
// The App Token can be used to modify your App, but you can just do that 'Manually'
function getOneTimeFB_AppToken() {
  Logger.log("getOneTimeFB_AppToken ran");
  //keep this info secret
  //Generate an App Access Token
  var ysshAppID = 'Your App ID';
  var ysshAppSecret = 'Your App Secret';
  var optnAppTkn = {"method" : "get"};
  var getAppTknURL = "https://graph.facebook.com/oauth/access_token?client_id=" + ysshAppID + "&client_secret=" + ysshAppSecret + "&grant_type=client_credentials"
  var getAppTkn = UrlFetchApp.fetch(getAppTknURL, optnAppTkn);
  Logger.log("Object returned from GET: " + getAppTkn)
  var myAppTkn = getAppTkn.getContentText();
  Logger.log("myAppTkn: " + myAppTkn);
};

Post to Facebook GS Code

function fncPostItemFB(arg1ToPost, arg2ToPost, arg3ToPost, argEtcToPost) {
  var fbCacheTkn = cache.get('fbTkn');
  Logger.log("fbCacheTkn: " + fbCacheTkn);

  if (fbCacheTkn === null) {
    return false;
  };
  Logger.log("fncPostItemFB ran: " + fbCacheTkn);
  return fncPostSecurly_(arg1ToPost, arg2ToPost, arg3ToPost, argEtcToPost);
};

function fncPostSecurly_(arg1ToPost, arg2ToPost, arg3ToPost, argEtcToPost) {
  Logger.log("fncPostSecurly ran");

  var sttsUpdate = argToPost + "your text to post here" + argToPost;
  var fromLogInTkn = cache.get('fbTkn');

  Logger.log("cache FB token: " + fromLogInTkn);

  //This is added security https://developers.facebook.com/docs/graph-api/securing-requests/
  var appsecret_sig = Utilities.computeHmacSha256Signature(fromLogInTkn, "YourAppSecret");
  var optnPostFB = {"method" : "post"};  //
  var PostToFB_URL = "https://graph.facebook.com/FacebookPageOrGroupID/feed?message=" + sttsUpdate + "&access_token=" 
    + fromLogInTkn; // + "&appsecret_proof=" + appsecret_sig;


    //Make a post to the Page
    var whatHappened = UrlFetchApp.fetch(PostToFB_URL, optnPostFB );
    //The return from facebook is an object.  Has to be converted to a string.
    var strFrmFbObj = whatHappened.getContentText();
    Logger.log("Return value of Post: " + strFrmFbObj);

    //When a post is successfully made to Facebook, a return object is passed back with an id value.

    var rtrnVerify = strFrmFbObj.indexOf('{\"id\":\"');
    Logger.log("rtrnVerify: " + rtrnVerify);

    if (rtrnVerify != -1) {
      return true;
    } else {
      return false;
    };
 };

Post to Facebook Front End Javascript Code

<script>
window.WriteInput = function(whereToPost) {

    window.strngCtgry = document.getElementById('id_Category').value;
    window.strngMaker = document.getElementById('id_Maker').value;
    window.strngAskingPrice = document.getElementById('id_AskingPrice').value;
    window.strngType = document.getElementById('id_ShrtDesc').value;
    window.strngFunction = document.getElementById('id_Function').value;
    window.strngCosmetic = document.getElementById('id_Cosmetic').value;  
    window.strngDescription = document.getElementById('id_Description').value;
    window.strngUserID = document.getElementById('pubID_Holder').textContent;
    window.addrIP = document.getElementById('IP_Holder').textContent;

  if (whereToPost === 'fb') {
    console.log("fncPostToFB ran" + strngDescription);
    if (strngDescription === "" || strngAskingPrice === "") {alert("Missing Input"); return false;};
    google.script.run.withFailureHandler(postFbFail)
    .withSuccessHandler(savedToFB)
    .fncPostItemFB(strngCtgry, strngMaker, strngAskingPrice, strngType, strngDescription, strngFunction, strngCosmetic, addrIP);
  } else {
    google.script.run.withFailureHandler(onFailure)
      .withSuccessHandler(savedLst)
      .fncSaveItem(strngCtgry, strngMaker, strngAskingPrice, strngType, strngDescription, strngFunction, strngCosmetic, addrIP);
      };
    };

window.savedLst = function(rtrnInput) {
   if (rtrnInput === false) {
     alert("Failed to Save Data");
   }
   else if (rtrnInput === "NotLogged") {
     alert("You are not logged in!");
     mainStart('SignInBody', 'SignIn');
   }
   else if (rtrnInput === "noItemForPic") {
     alert("You Need to Save an Item to attach the Picture to");
   }
   else {
  alert("Your Data Was Saved!");
  //Show the listing that was just saved next to the upload Pics button
  document.getElementById('listToPic').innerHTML = document.getElementById('id_ShrtDesc').value +
    ", " + document.getElementById('id_Description').value +
    ", - Made By: " + document.getElementById('id_Maker').value +
    ", Price: $" + document.getElementById('id_AskingPrice').value;

    };
};

window.postFbFail = function() {
  alert("Failed to Post to Facebook!  Try Signing In Again.");
  unsignFB();
};

window.savedToFB = function(pstFbStat) {
  if (pstFbStat === false) {
    alert("You are Not Signed in to Facebook!");
    unsignFB();
    google.script.run.withFailureHandler(onFailure)
      .signOutFB();
  } else {
    alert("Your Item was Posted to Facebook!");
  };
};

</script>