Google Apps Script

Tutorial: Twitter Approval Manager

Vic Fryzel, Google Apps Script Team

September 21, 2010
Updated August 15, 2013

Goal

This tutorial will show you how to create a Twitter Approval Manager application with Google Apps Script. The application will make use of MailApp , OAuthConfig , ScriptProperties, UiApp, UrlFetch, and of course Twitter.

Time to Complete

Approximately 2 hours

Prerequisites

Before beginning this tutorial, you should have a good understanding of a few other Google Apps Script modules. The code samples in this tutorial also assume a basic level of engineering knowledge. The code is not necessarily explained line-by-line, but rather in larger blocks.

Using This Tutorial

This tutorial is laid out in different sections, but primarily is a usage walk-through and then a code walk-through of an application. There are two ways to go about reading this tutorial. You can either write each line of code yourself as you progress through the tutorial, running the script after you’ve finished, or you can copy the public spreadsheet to get the script, run it, and then go through the code on your own.

Using the Twitter Demo Application

Setting Up Twitter

To use the script in this tutorial, you must have a Twitter account, and a Twitter application consumer key and consumer secret pair.

First, sign up for a Twitter account. Once you have signed up and logged in to your Twitter account, it's then time to register a new application with Twitter. You can register your new application with Twitter using their application manager at http://dev.twitter.com/apps/new.

Once, there, you will see a form similar to the older version below.

Important: For this tutorial to work, you must:

  1. enter a Callback URL of https://script.google.com/macros
  2. select the Read and Write access level (on the Settings page, after submitting the initial form)

Create Twitter App

Once you agree to the Twitter Rules of the Road and click Create your Twitter application, your application will be registered. You should land at a page giving details about your application. Among these details, you will see two fields labeled Consumer Key and Consumer Secret. Their values will be something like XXvX293jjLpgPJdeCw and uBH1PE7zw2309jdfhwWWmY68HjtwY8LaY3ynjw respectively. Save these values for future use in the script.

Setting Up the Script, Publishing as a Service

To get started, copy this spreadsheet containing the script for this tutorial. Once you've got your own copy of the spreadsheet, open it and you will notice the spreadsheet will be empty; that is intentional, as we won't be using any of the spreadsheet's cells in this tutorial.

The first thing you need to do is publish your script as a service. To do this, open the script editor by clicking Tools > Script Editor... In the new window, click Publish > Deploy as web app...

Publish Script as a Service Dialog

Allow all users (in your domain, if you are on a Google Apps domain) to run the script as you. Enable the service. Copy and save the URL given, as we will use it later.

Configuring the Script

When you open the spreadsheet, you will see a Twitter menu in the menu bar (there might be a slight delay before the menu shows up.)

Twitter Menu

Click Twitter > Configure to configure the script.

Script Authorization Dialog

You will now see an authorization dialog, asking for you to allow the script to access some parts of your Google account. Click Authorize, then Accept. Once the script is authorized, select Twitter > Configure once more. You will be presented with a dialog to configure the script.

Script Configuration Dialog

  1. enter the email addresses of your Approvers separated by commas
  2. enter your Twitter OAuth Consumer Key and Twitter OAuth Consumer Secret that you saved from the section Setting Up Twitter
  3. enter your web app URL that you saved from the section Setting Up the Script, Publishing as a Service
  4. click Save Configuration

Important: Once the script is configured, open the script editor at Tools > Scripts > Script Editor, and in the function drop down, select the authorize function. Then, click the Play button. This will pop up a box asking you to authorize; click Authorize.

OAuth Authorization Dialog

You will then be forwarded to Twitter for authentication. Important: authenticate with the Twitter account for which you will be approving tweets. Revoking this access later must be done on the service's end, so to revoke this access, see this Twitter page.

After you authenticate, Twitter will ask you to allow Google Apps Script access to the Twitter account. Click Allow.

Twitter Authorization Dialog

Tweeting

To send a tweet out to yourself for approval, click Twitter > Tweet.

Tweet Dialog

Enter your tweet, and click Send Tweet for Approval.

An email will be sent to all approvers asking for approval. Each approver must click the link given in their email in order to approve each tweet. Once all approvers approve a tweet, the tweet will automatically be posted to the Twitter account.

You're Done!

You have now set up the script to work, and are able to use it. Next, let's go through specifically how this was implemented with Google Apps Script.

The Code

Introduction

The Twitter Demo Application is designed as a Model-View-Controller. Our data model here is backed entirely by ScriptProperties, a feature that is demonstrated fully in this tutorial. Our view is composed entirely by UiApp interfaces. Lastly, our controllers are methods that will take care of things like talking to Twitter, sending emails, storing data in the data model, or displaying user interfaces.

Any data model function will begin with the word "get," "set," or "add" as they will get, set, or add data. Any view function will begin with the word "render." All other functions, for the most part, are controller functions, and do the interesting stuff of the application.

Comments

This is a complete application. It is commented thoroughly throughout. The comments have been included here for your reference, so that you don't have to compare and contrast code snippets with the full application source code. If you're unsure about a concept, it is worth reading the comments.

Data Model

The data model is made up of a series of getters and setters. These methods are in the following code block, along with a few constants defined to hold ScriptProperties names.

The important thing to realize here is that, for the most part, we'll be using the methods ScriptProperties.getProperty(name) and ScriptProperties.setProperty(name, value). Since value can only be a string, we must use some JSON encoding and decoding in some cases to serialize and unserialize our data into something that can be stored as a ScriptProperties. Essentially, ScriptProperties represent our database. As long as you can represent your data as a string, and don't want to use a spreadsheet for storing your data, ScriptProperties are for you.

ScriptProperties are stored on a per-script basis. That is, if you copy your script to another document, the ScriptProperties will not be maintained. If you ever need to store properties on a per-user basis, you should use UserProperties, which are used in more or less an identical manner to ScriptProperties.

Note that ScriptProperties.getProperty(name) will return null if the value is unset, and thus you will see a lot of if(myVar == null) in the getters below, to ensure that the return values are given as strings.

// Copyright 2010 Google Inc. All Rights Reserved.
 
/**
 * @fileoverview Google Apps Script demo application to illustrate usage of:
 *     MailApp
 *     OAuthConfig
 *     ScriptProperties
 *     Twitter Integration
 *     UiApp
 *     UrlFetchApp
 *     
 * @author vicfryzel@google.com (Vic Fryzel)
 */
 
/**
 * Key of ScriptProperties for Twitter approvers.
 * @type {String}
 * @const
 */
var APPROVERS_PROPERTY_NAME = "twitterApprovers";
 
/**
 * Key of ScriptProperties for Twitter consumer key.
 * @type {String}
 * @const
 */
var CONSUMER_KEY_PROPERTY_NAME = "twitterConsumerKey";
 
/**
 * Key of ScriptProperties for Twitter consumer secret.
 * @type {String}
 * @const
 */
var CONSUMER_SECRET_PROPERTY_NAME = "twitterConsumerSecret";
 
/**
 * Key of ScriptProperties for Twitter consumer secret.
 * @type {String}
 * @const
 */
var SERVICE_URL_PROPERTY_NAME = "serviceUrl";
 
/**
 * Key of ScriptProperties for tweets and all approvers.
 * @type {String}
 * @const
 */
var TWEETS_APPROVERS_PROPERTY_NAME = "twitterTweetsWithApprovers";
 
/**
 * @return String All approver email addresses that are required
 *                for a tweet to be posted.  Comma-delimited.
 */
function getApprovers() {
  var approvers = ScriptProperties.getProperty(APPROVERS_PROPERTY_NAME);
  if (approvers == null) {
    approvers = "";
  }
  return approvers;
}
 
/**
 * @param String Approver email address required to give approval
 *               prior to a tweet going live.  Comma-delimited.
 */
function setApprovers(approvers) {
  ScriptProperties.setProperty(APPROVERS_PROPERTY_NAME, approvers);
}
 
/**
 * @return String OAuth consumer key to use when tweeting.
 */
function getConsumerKey() {
  var key = ScriptProperties.getProperty(CONSUMER_KEY_PROPERTY_NAME);
  if (key == null) {
    key = "";
  }
  return key;
}
 
/**
 * @param String OAuth consumer key to use when tweeting.
 */
function setConsumerKey(key) {
  ScriptProperties.setProperty(CONSUMER_KEY_PROPERTY_NAME, key);
}
 
/**
 * @return String OAuth consumer secret to use when tweeting.
 */
function getConsumerSecret() {
  var secret = ScriptProperties.getProperty(CONSUMER_SECRET_PROPERTY_NAME);
  if (secret == null) {
    secret = "";
  }
  return secret;
}
 
/**
 * @param String OAuth consumer secret to use when tweeting.
 */
function setConsumerSecret(secret) {
  ScriptProperties.setProperty(CONSUMER_SECRET_PROPERTY_NAME, secret);
}
 
/**
 * @return String URL where this script is published as a service.
 */
function getServiceUrl() {
  var url = ScriptProperties.getProperty(SERVICE_URL_PROPERTY_NAME);
  if (url == null) {
    url = "";
  }
  return url;
}
 
/**
 * @param String URL where this script is published as a service.
 */
function setServiceUrl(url) {
  ScriptProperties.setProperty(SERVICE_URL_PROPERTY_NAME, url);
}
 
/**
 * @return bool True if all of the configuration properties are set,
 *              false if otherwise.
 */
function isConfigured() {
  return getApprovers() != "" && getConsumerKey() != "" &&
      getConsumerSecret != "" && getServiceUrl() != "";
}
 
/**
 * @return {String: String} Approvers for each tweet, keyed on tweet.
 */
function getApproversWithTweets() {
  var property = ScriptProperties.getProperty(
          TWEETS_APPROVERS_PROPERTY_NAME);
  if (property == null) {
    return {};
  }
  var approversWithTweets = Utilities.jsonParse(property);
  if (approversWithTweets == null) {
    approversWithTweets = {};
  }
  return approversWithTweets;
}
 
/**
 * @return [String] Approvers who have approved the given tweet.
 */
function getApproversForTweet(tweet) {
  var approversWithTweets = getApproversWithTweets();
  var approvers = [];
  if (approversWithTweets) {
    if (approversWithTweets[tweet]) {
      approvers = approversWithTweets[tweet];
    }
  }
  return approvers;
}
 
/**
 * @return boolean True if the given tweet has enough approvers.
 */
function tweetHasEnoughApprovers(tweet) {
  var currentApprovers = getApproversForTweet(tweet);
  var allApprovers = getApprovers().split();
  var currentApproversMap = {};
  for (var approver in currentApprovers) {
    currentApproversMap[approver] = 1;
  }
  for (var approver in allApprovers) {
    if (typeof currentApproversMap[approver] == "undefined") {
      return false;
    }
  }
  return true;
}
 
/**
 * @param String Tweet to approve.
 * @param String Approver to mark as approved for the given tweet.
 */
function addApproverForTweet(tweet, approver) {
  var approvers = getApproversForTweet(tweet);
  if (-1 == approvers.indexOf(approver)) {
    approvers.push(approver);
  }
  
  var approversWithTweets = getApproversWithTweets();
  approversWithTweets[tweet] = approvers;
  ScriptProperties.setProperty(TWEETS_APPROVERS_PROPERTY_NAME,
                               Utilities.jsonStringify(approversWithTweets));
}

Views

Our views will primarily display UiApp widgets in the browser. This will include things like showing dialog boxes, text boxes, buttons, etc.

The views in the code snippets below are also coupled with a few controllers. These controllers need to be shown next to their relevant views, because they are things like click handlers. That is, if a user clicks a button, the relevant controller or click handler should handle that click. So if a user clicks the Save Configuration button, the saveConfiguration(e) method will be called, where e is the click event.

Click handlers often must close the dialog from which they were called. For example, a Save Configuration button should also close the Configuration dialog. To do this, you'll see the click handler get the active UI application (var app = UiApp.getActiveApplication(),) and then close it (app.close().) A very important line that people tend to forget here is to return app;. If you don't return the application you closed, your click handler will not succeed in closing the active application dialog.

UI widgets, and for the most part the entire UiApp module, are very closely related to their corresponding Google Web Toolkit methods. Most widgets are created by getting the active application, and calling something like app.createX(), where X is something like Label, or Button. Once these widgets are created, they are added to a panel. A panel is essentially a layout manager; when you add widgets to a panel, the panel dictates where they show up in the dialog. If you want two columns of widgets (labels and textboxes,) you'll probably use a Grid panel. If you'd rather have your widgets show up next to each other based on how wide or tall they are, you may use a Flow panel.

Below, we see the code used to render both dialogs available for this application in the Twitter menu of the document: Configure and Tweet.

/** Retrieve config params from the UI and store them. */
function saveConfiguration(e) {
  setApprovers(e.parameter.approvers);
  setConsumerKey(e.parameter.consumerKey);
  setConsumerSecret(e.parameter.consumerSecret);
  setServiceUrl(e.parameter.serviceUrl);
  var app = UiApp.getActiveApplication();
  app.close();
  return app;
}
 
/**
 * Configure all UI components and display a dialog to allow the user to 
 * configure approvers.
 */
function renderConfigurationDialog() {
  var doc = SpreadsheetApp.getActiveSpreadsheet();
  var app = UiApp.createApplication().setTitle(
      "Add and Remove Twitter Approvers");
  app.setStyleAttribute("padding", "10px");
  
  var helpLabel = app.createLabel(
      "From here, you can configure the approvers for tweets and your OAuth "
      + "credentials.  Approvers will be emailed every time someone tries to "
      + "tweet, and will have to give their approval before the tweet goes "
      + "to Twitter.");
  helpLabel.setStyleAttribute("text-align", "justify");
  var approverListLabel = app.createLabel(
      "Approvers (comma separated):");
  var approverList = app.createTextArea();
  approverList.setName("approvers");
  approverList.setWidth("100%");
  approverList.setText(getApprovers());
  
  var consumerKeyLabel = app.createLabel(
      "Twitter OAuth Consumer Key:");
  var consumerKey = app.createTextBox();
  consumerKey.setName("consumerKey");
  consumerKey.setWidth("100%");
  consumerKey.setText(getConsumerKey());
  var consumerSecretLabel = app.createLabel(
      "Twitter OAuth Consumer Secret:");
  var consumerSecret = app.createTextBox();
  consumerSecret.setName("consumerSecret");
  consumerSecret.setWidth("100%");
  consumerSecret.setText(getConsumerSecret());
  
  var serviceUrlLabel = app.createLabel(
      "Publish as a Service URL:");
  var serviceUrl = app.createTextBox();
  serviceUrl.setName("serviceUrl");
  serviceUrl.setWidth("100%");
  serviceUrl.setText(getServiceUrl());
  
  var saveHandler = app.createServerClickHandler("saveConfiguration");
  var saveButton = app.createButton("Save Configuration", saveHandler);
  
  var listPanel = app.createGrid(4, 2);
  listPanel.setStyleAttribute("margin-top", "10px")
  listPanel.setWidth("100%");
  listPanel.setWidget(0, 0, approverListLabel);
  listPanel.setWidget(0, 1, approverList);
  listPanel.setWidget(1, 0, consumerKeyLabel);
  listPanel.setWidget(1, 1, consumerKey);
  listPanel.setWidget(2, 0, consumerSecretLabel);
  listPanel.setWidget(2, 1, consumerSecret);
  listPanel.setWidget(3, 0, serviceUrlLabel);
  listPanel.setWidget(3, 1, serviceUrl);
  
  // Ensure that all form fields get sent along to the handler
  saveHandler.addCallbackElement(listPanel);
  
  var dialogPanel = app.createFlowPanel();
  dialogPanel.add(helpLabel);
  dialogPanel.add(listPanel);
  dialogPanel.add(saveButton);
  app.add(dialogPanel);
  doc.show(app);
}
 
/**
 * Retrieve a tweet from the UI, save it, and send it for approval.
 */
function sendTweetForApproval(e) {
  var tweet = e.parameter.tweet;
  // Note that mailApprovers() is defined later in this script.
  mailApprovers(getApprovers(), tweet);
  var app = UiApp.getActiveApplication();
  app.close();
  return app;
}
 
/**
 * Configure all UI components and display a dialog to receive a tweet
 * from the current user.
 */
function renderTweetDialog() {
  var doc = SpreadsheetApp.getActiveSpreadsheet();
  var app = UiApp.createApplication().setTitle(
      "Send Tweet for Approval");
  app.setStyleAttribute("padding", "10px");
  app.setHeight(100);
  
  var helpLabel = app.createLabel(
    "Enter your tweet below:");
  helpLabel.setStyleAttribute("text-align", "justify");
  var tweet = app.createTextBox().setName("tweet").setWidth("100%");
  var sendHandler = app.createServerClickHandler("sendTweetForApproval");
  sendHandler.addCallbackElement(tweet);
  var sendButton = app.createButton("Send Tweet for Approval", sendHandler);
  
  var dialogPanel = app.createFlowPanel();
  dialogPanel.add(helpLabel);
  dialogPanel.add(tweet);
  dialogPanel.add(sendButton);
  app.add(dialogPanel);
  doc.show(app);
}

Controllers

Now that we've got the data model and views under control, we can finally get into the core of the code. We'll break the code up into sections, method by method.

Google Apps Script allows scripts to send emails via MailApp. MailApp's implementation is very complete, in that it allows us to do some advanced things like send multi-part emails, specify a reply-to address, and BCC recipients.

You'll notice that the code below starts by using the Session class to get the active user’s username and email address. This allows us to identify who is requesting that a tweet be posted, so that reviewers will know who is making the request in the email.

Next, the code calculates the URL that approvers can click to actually approve a tweet. This loops back to something we setup previously, the Publish Script as a Service URL. Most simply, this URL allows users to access methods in a script by browsing to a URL in their browser. In this case, we want approvers to be able to approve a tweet by clicking the link we give them in the email.

The bulk of this method is nothing but string concatenation. There are better ways to do this, but for simplicity we’re just manually creating a plain-text and HTML body for the email.

Lastly, this method calls MailApp.sendEmail(), which you may have guessed, sends this email to the specified recipients (in this case the approvers.)

/**
 * Email the given approvers a message about the given tweet, giving them a
 * link to use to approve the tweet.
 *
 * @param [String] Approver email addresses.
 * @param String Tweet for review.
 */
function mailApprovers(approvers, tweet) {
  if (!approvers) {
    return;
  }
  var currentUsername = Session.getActiveUser().getUserLoginId();
  var currentUserEmail = Session.getActiveUser().getEmail();
  var url = getServiceUrl() + "?tweet=" + encodeURIComponent(tweet);
  var message = "Hi,\n\n" + currentUsername + " requests that you approve "
      + "their tweet, which reads:\n\n" + tweet + "\n\nTo approve this tweet, "
      + "please browse to:\n\n" + url + "\n\nOtherwise, please email "
      + currentUserEmail + " and let " + currentUsername + " know that you "
      + "don't approve.";
  var htmlMessage = "<p>Hi,</p><p>" + currentUsername + " requests that you approve "
      + "their tweet, which reads:</p><p><i>" + tweet + "</i></p><p>To approve this tweet, "
      + "please <a href=\"" + url + "\">click here</a>.  Otherwise, please email "
      + currentUserEmail + " and let " + currentUsername + " know that you "
      + "don't approve.</p>";
  var additionalArgs = {
    "bcc": getApprovers(),
    "htmlBody": htmlMessage,
    "replyTo": currentUserEmail
  };
  MailApp.sendEmail(currentUserEmail, "Tweet Review", message, additionalArgs);
}

Events and Menus

Next up, we have a few simple controllers that do nothing but render the appropriate dialog. The configure() method renders the configuration dialog, and the tweet() method renders either the configuration dialog or tweet dialog, based on whether or not the script is already configured.

The important part of the following code block is the onOpen() method. The name "onOpen" for a method is special, because when the document owning this script is opened, the onOpen() method will be called as an event. In this case, onOpen() adds a menu to the base Spreadsheets UI called Twitter. It gives the menu two menu entries, named Tweet and Configure. These menu entries point to the tweet() and configure() controllers we just defined. So when a use clicks one of these menu entries, the relevant method will be called.

/** Controller to render approvers UI and apply configuration. */
function configure() {
  renderConfigurationDialog();
}
 
/** Controller to render tweet UI. */
function tweet() {
  if (!isConfigured()) {
    configure();
  } else {
    renderTweetDialog();
  }
}
 
/** When the spreadsheet is opened, add a Twitter menu. */
function onOpen() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var menuEntries = [ {name: "Tweet", functionName: "tweet"},
                      {name: "Configure", functionName: "configure"} ];
  ss.addMenu("Twitter", menuEntries);
}

OAuth

The last part of our code is the most complex, primarily because it deals with OAuth. OAuth is a complicated beast, but what it essentially allows us to do is take actions on behalf of users of other services. In our case, we're going to post a tweet to Twitter on behalf of the Twitter account we authorized previously. We're only going to post this tweet if we have enough approvers for the tweet, but the power to post the tweet is controlled by our program.

If you're developing for Twitter, you can safely copy the following authorize() function and begin using UrlFetchApp to make requests against the Twitter API. However, if you're developing for another service, you will need to change the OAuth config URLs to the ones that that service provides for OAuth authorization.

Besides that, you will need a consumer key and consumer secret for your application with the service. In this case, our consumer key and consumer secret are for our Twitter application. That means, like before, we've gone to Twitter's website and asked them "we'd like to create an application for your API, can we please have a consumer key and consumer secret?" The consumer key and consumer secret they gave us is what is used below. In cases that are not Twitter, you may even use a consumer key and secret of "anonymous" and "anonymous". An example of using anonymous/anonymous consumer key/secret would often be while using the Google GData APIs.

The doGet() method is another one of those special event methods, like onOpen(). doGet() is called whenever a user browses to the Publish Script as a Service URL that we've referred to previously. That means that doGet() is the method that will be called whenever an approver clicks the approval link we gave them in the email.

Since doGet() is the method that will essentially be called when an approver wants to approve a tweet, we will make doGet() be the function that actually sends the tweet to Twitter if an approver was the final approver needed for the tweet.

Finally, you will notice that our doGet() method actually returns a UiApp application instance. This is a set of labels that will be displayed to the approver after they've successfully approved a tweet, so that they have some feedback about what's happened. After that, if the tweet has gone live based on the approval, the instance will contain a label saying so.

/**
 * Authorize against Twitter.  This method must be run prior to 
 * clicking any link in a script email.  If you click a link in an
 * email, you will get a message stating:
 * "Authorization is required to perform that action."
 */
function authorize() {
  var oauthConfig = UrlFetchApp.addOAuthService("twitter");
  oauthConfig.setAccessTokenUrl(
      "https://api.twitter.com/oauth/access_token");
  oauthConfig.setRequestTokenUrl(
      "https://api.twitter.com/oauth/request_token");
  oauthConfig.setAuthorizationUrl(
      "https://api.twitter.com/oauth/authorize");
  oauthConfig.setConsumerKey(getConsumerKey());
  oauthConfig.setConsumerSecret(getConsumerSecret());
  var requestData = {
    "method": "GET",
    "oAuthServiceName": "twitter",
    "oAuthUseToken": "always"
  };
  // We make this request to ensure that we are authorized prior to
  // actually using the script.  The following three lines serve no
  // functional purpose otherwise.
  var result = UrlFetchApp.fetch(
      "https://api.twitter.com/1.1/statuses/mentions_timeline.json",
      requestData);
}
 
/**
 * Approve the given tweet and post it if this is the last required
 * approval.
 *
 * @param String "tweet" GET parameter (e.g. e.parameter.tweet)
 */
function doGet(e) {
  var tweet = e.parameter.tweet;
  var currentUserEmail = Session.getActiveUser().getEmail();
  addApproverForTweet(tweet, currentUserEmail);
  var app = UiApp.createApplication().setTitle("Approved");
  var panel = app.createFlowPanel();
  
  panel.add(app.createLabel().setText(
      "You have approved the tweet: \"" + tweet + "\""));
  if (tweetHasEnoughApprovers(tweet)) {
    // Authorize to Twitter
    authorize();
    // Tweet must be URI encoded in order to make it to Twitter safely
    var encodedTweet = encodeURIComponent(tweet);
    
    var requestData = {
      // Twitter API requires us to send an HTTP POST request
      "method": "POST",
      // This should be the name of the service we configure in authorize()
      "oAuthServiceName": "twitter",
      // Always send an OAuth token along with this API request
      "oAuthUseToken": "always"
    };
    try {
      // Actually make the request that posts a tweet!
      var result = UrlFetchApp.fetch(
          "https://api.twitter.com/1.1/statuses/update.json?status=" + encodedTweet,
          requestData);
    } catch (exception) {
      // An error occurred!  Log it, so that it’s visible in the script editor’s log
      Logger.log(exception);
    }
    panel.add(app.createLabel().setText(
        "Enough approvers were found, tweet posted!"));
  }
  
  app.add(panel);
  return app;
}

Wrapping Up, Next Steps

You should now have a working Twitter approval application that makes wide use of different services in Google Apps Script.

Full Code

The full code from this tutorial follows.

// Copyright 2010 Google Inc. All Rights Reserved.
 
/**
 * @fileoverview Google Apps Script demo application to illustrate usage of:
 *     MailApp
 *     OAuthConfig
 *     ScriptProperties
 *     Twitter Integration
 *     UiApp
 *     UrlFetchApp
 *     
 * @author vicfryzel@google.com (Vic Fryzel)
 */
 
/**
 * Key of ScriptProperties for Twitter approvers.
 * @type {String}
 * @const
 */
var APPROVERS_PROPERTY_NAME = "twitterApprovers";
 
/**
 * Key of ScriptProperties for Twitter consumer key.
 * @type {String}
 * @const
 */
var CONSUMER_KEY_PROPERTY_NAME = "twitterConsumerKey";
 
/**
 * Key of ScriptProperties for Twitter consumer secret.
 * @type {String}
 * @const
 */
var CONSUMER_SECRET_PROPERTY_NAME = "twitterConsumerSecret";
 
/**
 * Key of ScriptProperties for Twitter consumer secret.
 * @type {String}
 * @const
 */
var SERVICE_URL_PROPERTY_NAME = "serviceUrl";
 
/**
 * Key of ScriptProperties for tweets and all approvers.
 * @type {String}
 * @const
 */
var TWEETS_APPROVERS_PROPERTY_NAME = "twitterTweetsWithApprovers";
 
/**
 * @return String All approver email addresses that are required
 *                for a tweet to be posted.  Comma-delimited.
 */
function getApprovers() {
  var approvers = ScriptProperties.getProperty(APPROVERS_PROPERTY_NAME);
  if (approvers == null) {
    approvers = "";
  }
  return approvers;
}
 
/**
 * @param String Approver email address required to give approval
 *               prior to a tweet going live.  Comma-delimited.
 */
function setApprovers(approvers) {
  ScriptProperties.setProperty(APPROVERS_PROPERTY_NAME, approvers);
}
 
/**
 * @return String OAuth consumer key to use when tweeting.
 */
function getConsumerKey() {
  var key = ScriptProperties.getProperty(CONSUMER_KEY_PROPERTY_NAME);
  if (key == null) {
    key = "";
  }
  return key;
}
 
/**
 * @param String OAuth consumer key to use when tweeting.
 */
function setConsumerKey(key) {
  ScriptProperties.setProperty(CONSUMER_KEY_PROPERTY_NAME, key);
}
 
/**
 * @return String OAuth consumer secret to use when tweeting.
 */
function getConsumerSecret() {
  var secret = ScriptProperties.getProperty(CONSUMER_SECRET_PROPERTY_NAME);
  if (secret == null) {
    secret = "";
  }
  return secret;
}
 
/**
 * @param String OAuth consumer secret to use when tweeting.
 */
function setConsumerSecret(secret) {
  ScriptProperties.setProperty(CONSUMER_SECRET_PROPERTY_NAME, secret);
}
 
/**
 * @return String URL where this script is published as a service.
 */
function getServiceUrl() {
  var url = ScriptProperties.getProperty(SERVICE_URL_PROPERTY_NAME);
  if (url == null) {
    url = "";
  }
  return url;
}
 
/**
 * @param String URL where this script is published as a service.
 */
function setServiceUrl(url) {
 ScriptProperties.setProperty(SERVICE_URL_PROPERTY_NAME, url);
}
 
/**
 * @return bool True if all of the configuration properties are set,
 *              false if otherwise.
 */
function isConfigured() {
  return getApprovers() != "" && getConsumerKey() != "" &&
      getConsumerSecret != "" && getServiceUrl() != "";
}
 
/**
 * @return {String: String} Approvers for each tweet, keyed on tweet.
 */
function getApproversWithTweets() {
  var property = ScriptProperties.getProperty(
      TWEETS_APPROVERS_PROPERTY_NAME);
  if (property == null) {
    return {};
  }
  var approversWithTweets = Utilities.jsonParse(property);
  if (approversWithTweets == null) {
    approversWithTweets = {};
  }
  return approversWithTweets;
}
 
/**
 * @return [String] Approvers who have approved the given tweet.
 */
function getApproversForTweet(tweet) {
  var approversWithTweets = getApproversWithTweets();
  var approvers = [];
  if (approversWithTweets) {
    if (approversWithTweets[tweet]) {
      approvers = approversWithTweets[tweet];
    }
  }
  return approvers;
}
 
/**
 * @return boolean True if the given tweet has enough approvers.
 */
function tweetHasEnoughApprovers(tweet) {
  var currentApprovers = getApproversForTweet(tweet);
  var allApprovers = getApprovers().split();
  var currentApproversMap = {};
  for (var approver in currentApprovers) {
    currentApproversMap[approver] = 1;
  }
  for (var approver in allApprovers) {
    if (typeof currentApproversMap[approver] == "undefined") {
      return false;
    }
  }
  return true;
}
 
/**
 * @param String Tweet to approve.
 * @param String Approver to mark as approved for the given tweet.
 */
function addApproverForTweet(tweet, approver) {
  var approvers = getApproversForTweet(tweet);
  if (-1 == approvers.indexOf(approver)) {
    approvers.push(approver);
  }
  
  var approversWithTweets = getApproversWithTweets();
  approversWithTweets[tweet] = approvers;
  ScriptProperties.setProperty(TWEETS_APPROVERS_PROPERTY_NAME,
                               Utilities.jsonStringify(approversWithTweets));
}
 
/** Retrieve config params from the UI and store them. */
function saveConfiguration(e) {
  setApprovers(e.parameter.approvers);
  setConsumerKey(e.parameter.consumerKey);
  setConsumerSecret(e.parameter.consumerSecret);
  setServiceUrl(e.parameter.serviceUrl);
  var app = UiApp.getActiveApplication();
  app.close();
  return app;
}
 
/**
 * Configure all UI components and display a dialog to allow the user to 
 * configure approvers.
 */
function renderConfigurationDialog() {
  var doc = SpreadsheetApp.getActiveSpreadsheet();
  var app = UiApp.createApplication().setTitle(
      "Add and Remove Twitter Approvers");
  app.setStyleAttribute("padding", "10px");
  
  var helpLabel = app.createLabel(
      "From here, you can configure the approvers for tweets and your OAuth "
      + "credentials.  Approvers will be emailed every time someone tries to "
      + "tweet, and will have to give their approval before the tweet goes "
      + "to Twitter.");
  helpLabel.setStyleAttribute("text-align", "justify");
  var approverListLabel = app.createLabel(
      "Approvers (comma separated):");
  var approverList = app.createTextArea();
  approverList.setName("approvers");
  approverList.setWidth("100%");
  approverList.setText(getApprovers());
 
  var consumerKeyLabel = app.createLabel(
      "Twitter OAuth Consumer Key:");
  var consumerKey = app.createTextBox();
  consumerKey.setName("consumerKey");
  consumerKey.setWidth("100%");
  consumerKey.setText(getConsumerKey());
  var consumerSecretLabel = app.createLabel(
      "Twitter OAuth Consumer Secret:");
  var consumerSecret = app.createTextBox();
  consumerSecret.setName("consumerSecret");
  consumerSecret.setWidth("100%");
  consumerSecret.setText(getConsumerSecret());
  
  var serviceUrlLabel = app.createLabel(
      "Publish as a Service URL:");
  var serviceUrl = app.createTextBox();
  serviceUrl.setName("serviceUrl");
  serviceUrl.setWidth("100%");
  serviceUrl.setText(getServiceUrl());
  
  var saveHandler = app.createServerClickHandler("saveConfiguration");
  var saveButton = app.createButton("Save Configuration", saveHandler);
  
  var listPanel = app.createGrid(4, 2);
  listPanel.setStyleAttribute("margin-top", "10px")
  listPanel.setWidth("100%");
  listPanel.setWidget(0, 0, approverListLabel);
  listPanel.setWidget(0, 1, approverList);
  listPanel.setWidget(1, 0, consumerKeyLabel);
  listPanel.setWidget(1, 1, consumerKey);
  listPanel.setWidget(2, 0, consumerSecretLabel);
  listPanel.setWidget(2, 1, consumerSecret);
  listPanel.setWidget(3, 0, serviceUrlLabel);
  listPanel.setWidget(3, 1, serviceUrl);
  
  // Ensure that all form fields get sent along to the handler
  saveHandler.addCallbackElement(listPanel);
  
  var dialogPanel = app.createFlowPanel();
  dialogPanel.add(helpLabel);
  dialogPanel.add(listPanel);
  dialogPanel.add(saveButton);
  app.add(dialogPanel);
  doc.show(app);
}
 
/**
 * Email the given approvers a message about the given tweet, giving them a
 * link to use to approve the tweet.
 *
 * @param [String] Approver email addresses.
 * @param String Tweet for review.
 */
function mailApprovers(approvers, tweet) {
  if (!approvers) {
    return;
  }
  var currentUsername = Session.getActiveUser().getUserLoginId();
  var currentUserEmail = Session.getActiveUser().getEmail();
  var url = getServiceUrl() + "?tweet=" + encodeURIComponent(tweet);
  var message = "Hi,\n\n" + currentUsername + " requests that you approve "
      + "their tweet, which reads:\n\n" + tweet + "\n\nTo approve this tweet, "
      + "please browse to:\n\n" + url + "\n\nOtherwise, please email "
      + currentUserEmail + " and let " + currentUsername + " know that you "
      + "don't approve.";
  var htmlMessage = "<p>Hi,</p><p>" + currentUsername + " requests that you approve "
      + "their tweet, which reads:</p><p><i>" + tweet + "</i></p><p>To approve this tweet, "
      + "please <a href=\"" + url + "\">click here</a>.  Otherwise, please email "
      + currentUserEmail + " and let " + currentUsername + " know that you "
      + "don't approve.</p>";
  var additionalArgs = {
    "bcc": getApprovers(),
    "htmlBody": htmlMessage,
    "replyTo": currentUserEmail
  };
  MailApp.sendEmail(currentUserEmail, "Tweet Review", message, additionalArgs);
}
 
/**
 * Retrieve a tweet from the UI, save it, and send it for approval.
 */
function sendTweetForApproval(e) {
  var tweet = e.parameter.tweet;
  mailApprovers(getApprovers(), tweet);
  var app = UiApp.getActiveApplication();
  app.close();
  return app;
}
 
/**
 * Configure all UI components and display a dialog to receive a tweet
 * from the current user.
 */
function renderTweetDialog() {
  var doc = SpreadsheetApp.getActiveSpreadsheet();
  var app = UiApp.createApplication().setTitle(
      "Send Tweet for Approval");
  app.setStyleAttribute("padding", "10px");
  app.setHeight(100);
  
  var helpLabel = app.createLabel(
    "Enter your tweet below:");
  helpLabel.setStyleAttribute("text-align", "justify");
  var tweet = app.createTextBox().setName("tweet").setWidth("100%");
  var sendHandler = app.createServerClickHandler("sendTweetForApproval");
  sendHandler.addCallbackElement(tweet);
  var sendButton = app.createButton("Send Tweet for Approval", sendHandler);
  
  var dialogPanel = app.createFlowPanel();
  dialogPanel.add(helpLabel);
  dialogPanel.add(tweet);
  dialogPanel.add(sendButton);
  app.add(dialogPanel);
  doc.show(app);
}
 
/** Controller to render approvers UI and apply configuration. */
function configure() {
  renderConfigurationDialog();
}
 
/** Controller to render tweet UI. */
function tweet() {
  if (!isConfigured()) {
    configure();
  } else {
    renderTweetDialog();
  }
}
 
/** When the spreadsheet is opened, add a Twitter menu. */
function onOpen() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var menuEntries = [ {name: "Tweet", functionName: "tweet"},
                      {name: "Configure", functionName: "configure"} ];
  ss.addMenu("Twitter", menuEntries);
}
 
/**
 * Authorize against Twitter.  This method must be run prior to 
 * clicking any link in a script email.  If you click a link in an
 * email, you will get a message stating:
 * "Authorization is required to perform that action."
 */
function authorize() {
  var oauthConfig = UrlFetchApp.addOAuthService("twitter");
  oauthConfig.setAccessTokenUrl(
      "https://api.twitter.com/oauth/access_token");
  oauthConfig.setRequestTokenUrl(
      "https://api.twitter.com/oauth/request_token");
  oauthConfig.setAuthorizationUrl(
      "https://api.twitter.com/oauth/authorize");
  oauthConfig.setConsumerKey(getConsumerKey());
  oauthConfig.setConsumerSecret(getConsumerSecret());
  var requestData = {
    "method": "GET",
    "oAuthServiceName": "twitter",
    "oAuthUseToken": "always"
  };
  var result = UrlFetchApp.fetch(
      "https://api.twitter.com/1.1/statuses/mentions_timeline.json",
      requestData);
}
 
/**
 * Approve the given tweet and post it if this is the last required
 * approval.
 *
 * @param String "tweet" GET parameter (e.g. e.parameter.tweet)
 */
function doGet(e) {
  var tweet = e.parameter.tweet;
  var currentUserEmail = Session.getActiveUser().getEmail();
  addApproverForTweet(tweet, currentUserEmail);
  var app = UiApp.createApplication().setTitle("Approved");
  var panel = app.createFlowPanel();
  
  panel.add(app.createLabel().setText(
      "You have approved the tweet: \"" + tweet + "\""));
  if (tweetHasEnoughApprovers(tweet)) {
    // Authorize to Twitter
    authorize();
    // Tweet must be URI encoded in order to make it to Twitter safely
    var encodedTweet = encodeURIComponent(tweet);
    var requestData = {
      "method": "POST",
      "oAuthServiceName": "twitter",
      "oAuthUseToken": "always"
    };
    try {
      var result = UrlFetchApp.fetch(
          "https://api.twitter.com/1.1/statuses/update.json?status=" + encodedTweet,
          requestData);
    } catch (e) {
      Logger.log(e);
    }
    panel.add(app.createLabel().setText(
        "Enough approvers were found, tweet posted!"));
  }
 
  app.add(panel);
  return app;
}

Authentication required

You need to be signed in with Google+ to do that.

Signing you in...

Google Developers needs your permission to do that.