Search code examples
google-apps-scripttwitteroauth-2.0twitter-oauthbearer-token

How can I tweet from Google App Script with OAuth2 library?


I've successfully made my GAS for tweet with OAuth1. It's known that OAuth1 GAS library is deprecated, so I'm trying to migrate to OAuth2 library.

I saw a few changes, but I don't get the correct way to authorize my request with this.

The main questions I have right now are:

  • Bearer Token replace in OAuth2 to key&access tokens in OAuth1?
  • I don't need key&access to authorize rquest? I'm base on example of the Google developers's site itself

For more clarity, I put the code, extracted from Google developers's site, adapted for my propouses:

// Call this function just once, to initialize the OAuth client.
function initializeOAuthClient() {
  if (typeof OAuth2 === 'undefined') {
    var libUrl = 'https://developers.google.com/google-ads/scripts/docs/examples/oauth20-library';
    throw Error('OAuth2 library not found. Please take a copy of the OAuth2 ' +
        'library from ' + libUrl + ' and append to the bottom of this script.');
  }
  var tokenUrl = 'https://api.twitter.com/oauth2/token';
  authUrlFetch = OAuth2.withClientCredentials(
      tokenUrl, CONSUMER_KEY, CONSUMER_SECRET);
}

function sendTweet(status) {
  var service = accessProtectedResource(SERVICE_UPDATE_URL, "post");
  
  if (service.hasAccess()) {
    var url = 'https://api.twitter.com/1.1/statuses/update.json?include_entities=true&status=' + percentEncode(status);
    var response = service.fetch(url);
    //var result = JSON.parse(response.getContentText());
    
    return response;
  }
}


/**
 * Attempts to access a non-Google API using a constructed service
 * object.
 *
 * If your add-on needs access to non-Google APIs that require OAuth,
 * you need to implement this method. You can use the OAuth1 and
 * OAuth2 Apps Script libraries to help implement it.
 *
 * @param {String} url         The URL to access.
 * @param {String} method_opt  The HTTP method. Defaults to GET.
 * @param {Object} headers_opt The HTTP headers. Defaults to an empty
 *                             object. The Authorization field is added
 *                             to the headers in this method.
 * @return {HttpResponse} the result from the UrlFetchApp.fetch() call.
 */
function accessProtectedResource(url, method_opt, headers_opt) {
  var service = getOAuthService();
  var maybeAuthorized = service.hasAccess();
  if (maybeAuthorized) {
    // A token is present, but it may be expired or invalid. Make a
    // request and check the response code to be sure.

    // Make the UrlFetch request and return the result.
    var accessToken = service.getAccessToken();
    var method = method_opt || 'post';
    var headers = headers_opt || {};
    headers['Authorization'] =
        Utilities.formatString('Bearer %s', accessToken);
    var resp = UrlFetchApp.fetch(url, {
      'headers': headers,
      'method' : method,
      'muteHttpExceptions': true, // Prevents thrown HTTP exceptions.
    });

    var code = resp.getResponseCode();
    if (code >= 200 && code < 300) {
      return resp.getContentText("utf-8"); // Success
    } else if (code == 401 || code == 403) {
       // Not fully authorized for this action.
       maybeAuthorized = false;
    } else {
       // Handle other response codes by logging them and throwing an
       // exception.
       console.error("Backend server error (%s): %s", code.toString(),
                     resp.getContentText("utf-8"));
       throw ("Backend server error: " + code);
    }
  }

  if (!maybeAuthorized) {
    // Invoke the authorization flow using the default authorization
    // prompt card.
    CardService.newAuthorizationException()
        .setAuthorizationUrl(service.getAuthorizationUrl())
        .setResourceDisplayName("Display name to show to the user")
        .throwException();
  }
}

/**
 * Create a new OAuth service to facilitate accessing an API.
 * This example assumes there is a single service that the add-on needs to
 * access. Its name is used when persisting the authorized token, so ensure
 * it is unique within the scope of the property store. You must set the
 * client secret and client ID, which are obtained when registering your
 * add-on with the API.
 *
 * See the Apps Script OAuth2 Library documentation for more
 * information:
 *   https://github.com/googlesamples/apps-script-oauth2#1-create-the-oauth2-service
 *
 *  @return A configured OAuth2 service object.
 */
function getOAuthService() {
  
  return OAuth2.createService('SERVICE_NAME')
      .setAuthorizationBaseUrl('SERVICE_AUTH_URL')
      .setTokenUrl('SERVICE_AUTH_TOKEN_URL')
      .setClientId('CLIENT_ID')
      .setClientSecret('CLIENT_SECRET')
      .setScope('SERVICE_SCOPE_REQUESTS')
      .setCallbackFunction('authCallback')
      .setCache(CacheService.getUserCache())
      .setPropertyStore(PropertiesService.getScriptProperties())
}

/**
 * Boilerplate code to determine if a request is authorized and returns
 * a corresponding HTML message. When the user completes the OAuth2 flow
 * on the service provider's website, this function is invoked from the
 * service. In order for authorization to succeed you must make sure that
 * the service knows how to call this function by setting the correct
 * redirect URL.
 *
 * The redirect URL to enter is:
 * https://script.google.com/macros/d/<Apps Script ID>/usercallback
 *
 * See the Apps Script OAuth2 Library documentation for more
 * information:
 *   https://github.com/googlesamples/apps-script-oauth2#1-create-the-oauth2-service
 *
 *  @param {Object} callbackRequest The request data received from the
 *                  callback function. Pass it to the service's
 *                  handleCallback() method to complete the
 *                  authorization process.
 *  @return {HtmlOutput} a success or denied HTML message to display to
 *          the user. Also sets a timer to close the window
 *          automatically.
 */
function authCallback(callbackRequest) {
  var authorized = getOAuthService().handleCallback(callbackRequest);
  if (authorized) {
    return HtmlService.createHtmlOutput(
      'Success! <script>setTimeout(function() { top.window.close() }, 1);</script>');
  } else {
    return HtmlService.createHtmlOutput('Denied');
  }
}

/**
 * Unauthorizes the non-Google service. This is useful for OAuth
 * development/testing.  Run this method (Run > resetOAuth in the script
 * editor) to reset OAuth to re-prompt the user for OAuth.
 */
function resetOAuth() {
  getOAuthService().reset();
}

function main() {
  try {
      let result = sendTweet("Este va a ser un gran día!\n https://www.instagram.com/amos_oficialba/");
      Logger.log("Resultado: " + result);
  }
  catch(err) {
    console.log(err["stack"]);
  }
}

Solution

  • Native support was removed from OAuthConfig, but that does not prevent your app to make an OAuth 1 request to external APIs. The open source library OAuth1 for Apps Script was created as a replacement in case you were using OAuthConfig before.

    To Tweet from Google Apps Script with the OAuth1 for Apps Script library:

    1. You need to setup the callback URL in your Twitter Developer portal. When using this library, the callback URL will always be in the format https://script.google.com/macros/s/YOUR_SCRIPT_ID/usercallback. You will need to replace YOUR_SCRIPT_ID with, well, your script's ID.
    2. In Google Apps Script, go to the File menu and select Project properties. Take a note of your script ID. enter image description here
    3. In the Twitter Developer portal, select your app, then click Edit under Authentication settings.
    4. Add the callback URL, then click Save when done: enter image description here
    5. Back in Google Apps Script, select the Resources menu, then click Libraries.
    6. In the Libraries window, import the OAuth1 library by typing its ID 1CXDCY5sqT9ph64fFwSzVtXnbjpSfWdRymafDrtIZ7Z_hwysTY7IIhi7s, then click Add.
    7. Select the latest version (18 at the time of writing) The Libraries window displaying the OAuth library being selected

    Once done, use this script to setup a valid Tweet request. Replace CONSUMER_KEY and CONSUMER_SECRET with the API key and secret for your app; replace TOKEN, and TOKEN_SECRET with your user's access token and access token secret.

    var CONSUMER_KEY = 'your consumer key';
    var CONSUMER_SECRET = 'your consumer secret';
    var TOKEN = 'your access token';
    var TOKEN_SECRET = 'your access token secret';
    /**
     * Authorizes and makes a request to the Twitter API.
     */
    function run() {
      var service = getService();
      Logger.log(service.getCallbackUrl())
      if (service.hasAccess()) {
        var url = 'https://api.twitter.com/1.1/statuses/update.json';
        var payload = {
          status: 'just setting up my google apps script'
        };
        var response = service.fetch(url, {
          method: 'post',
          payload: payload
        });
        var result = JSON.parse(response.getContentText());
        Logger.log(JSON.stringify(result, null, 2));
      } else {
        var authorizationUrl = service.authorize();
        Logger.log('Open the following URL and re-run the script: %s',
            authorizationUrl);
      }
    } 
    
    function doGet() {
      return HtmlService.createHtmlOutput(ScriptApp.getService().getUrl());
    }
    
    /**
     * Reset the authorization state, so that it can be re-tested.
     */
    function reset() {
      var service = getService();
      service.reset();
    }
    
    /**
     * Configures the service.
     */
    function getService() {
      return OAuth1.createService('Twitter')
          // Set the endpoint URLs.
          .setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
          .setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
          .setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
    
          // Set the consumer key and secret.
          .setConsumerKey(CONSUMER_KEY)
          .setConsumerSecret(CONSUMER_SECRET)
    
          // Set your user's access token key and secret
          .setAccessToken(TOKEN, TOKEN_SECRET)
    
          .setCallbackFunction('authCallback')  
    }
    
    /**
     * Handles the OAuth callback.
     */
    function authCallback(request) {
      var service = getService();
      var authorized = service.handleCallback(request);
      if (authorized) {
        return HtmlService.createHtmlOutput('Success!');
      } else {
        return HtmlService.createHtmlOutput('Denied');
      }
    }
    

    Alternatively, you can use Google's own OAuth 1 replacement script to sign OAuth 1 requests. You can find an example of usage in the Google Ads script page.