Building an OpenSocial App with Google App Engine

While you can write OpenSocial apps that run solely in JavaScript and use the Persistence API to store data on the container, many OpenSocial apps communicate with a third-party server for data storage or application logic. Integrating with your own third-party server allows you to add new dimensions to your app, like providing a data API, hosting static content, or allowing configuration through an admin console.

In this article, we'll build an app that is similar to the gift-giving application built in the OpenSocial tutorial. When a user views the app, they see a drop-down menu of gifts (such as a peanut, or a red pistachio nut) and another drop-down menu containing a list of their friends. The user can give any of these gifts to a friend and the gift transaction will be displayed. The app will also display any gifts that the user has received. You can find all the source code used to run this application in the opensocial-gifts project on Google Code Project Hosting. You can also install this app on the orkut sandbox.

The original gift-giving app is built using 100% client-side OpenSocial code and is therefore subject to a number of limitations imposed by the container rendering the app, such as the amount of data the container will let you store, and the access controls related to when you can read and write data. With Google App Engine, you can manage all this data on an external server, freeing your app from any constraints imposed by the container. Viva la revolución!

Audience

The ideal audience for this article knows a little about Google App Engine, and a little about OpenSocial. We'll build this app from ground up, but it will help if you've worked through the Google App Engine Getting Started Guide and the Opensocial Tutorial.

OpenSocial apps are written in JavaScript and HTML, so you'll need to be able to read these languages to understand the examples. You'll also need to be familiar with Python because that's what all the server side Google App Engine code is written in. That said, we're not doing anything too complicated here, so if you've got some basic web development experience, you should be fine.

Note: You'll need a Google account and the App Engine SDK to start coding.

Architecture

By the end of this article, you'll have constructed an OpenSocial app where user's can give each other gifts. Along the way, we'll implement components to store, access, manipulate, and display application data (gifts and gift transactions). As you implement more functional apps, the implementations for each of these components will likely be more complex, but the general interactions and components themselves can remain mostly the same. This gift-giving app has five such components (in order of appearance):

  1. Google App Engine app
  2. Database model
  3. Admin interface
  4. JSON data API
  5. OpenSocial application spec

Total servers you need to maintain: 0.

Google App Engine app (app.yaml and gifts.py)

Google App Engine projects are defined and configured using an app.yaml file that defines how to handle incoming requests. In this file, we'll configure our app to send requests to gifts.py where we'll create a WSGIApplication that will handle these requests and send appropriate responses.

Database model (db_model.py)

The data for this app will be stored using the Google App Engine datastore. We'll store the gifts that are available for giving as a character string for now. When a user gives a gift we'll store the sender, receiver, and gift given as a gift transaction. We can leverage the IDs provided by the OpenSocial API to identify the users involved in a gift transaction.

Admin interface (admin.py)

We'll use Google App Engine to host a small web application that will allow us to perform some simple operations on the datastore. We'll use Google App Engine's authentication and identity management tools to ensure that only certain users can access this console and manipulate data directly. Our admin interface will allow us to initialize or view the data in the datastore, but you could extend this concept to enable more complex management tasks.

Note: Google App Engine provides an Admin Console where you can perform many useful tasks, like manipulating data in the datastore, viewing application logs, and accessing metrics about your app's usage.

JSON data API (api.py)

We'll expose the data in the datastore through a data API (also hosted by Google App Engine). The API will accept HTTP GET requests to access gifts or gift transactions and return the data as a JSON string. The API will also accept HTTP POST requests to add a gift transaction to the datastore.

OpenSocial application spec (gifts.xml)

Our users will be interacting with the OpenSocial app, defined in an application spec XML file. The application spec contains HTML and JavaScript that will be rendered by an OpenSocial container, and the JavaScript will run client-side in the user's browser. The OpenSocial app will make calls to the data API hosted on Google App Engine using gadgets.io.makeRequest.

Setting up a Google App Engine app

First you need to decide on an application identifier for your application. This identifier must be unique across all Google App Engine applications, so for this article, use opensocial-gifts-username where username is your username (e.g. opensocial-gifts-johndoe). If you have a Google App Engine account, log into the Google App Engine admin console and create an app. Since App Engine is currently in a preview release, you can only publish up to three applications for now (although you can create as many local applications as you'd like). Bearing this in mind, you may want to use a generic application identifier, like username-dev.

The first iteration of our application will have just two files: app.yaml and gifts.py. The app.yaml file contains configuration details about your app, including which Python scripts to execute when your app receives requests. Here's a simple app.yaml file to get us started:

application: opensocial-gifts-username
version: 1
runtime: python
api_version: 1

handlers:
- url: /.*
  script: gifts.py

Note: The value used for application needs to uniquely identify your Google app Engine app so be sure to replace username with your username (e.g. opensocial-gifts-jdoe).

In the app.yaml file, we specified that all requests should be handled by gifts.py using the regex /.*. Let's define a WSGIApplication in gift.py and create a temporary RequestHandler so we can do a quick sanity check. Here's the content in gifts.py:

# Standard libraries
import wsgiref.handlers

# AppEngine imports
from google.appengine.ext import webapp

class SanityCheck(webapp.RequestHandler):
  def get(self):
    self.response.out.write("You're not crazy!")

# Map URLs to request handler class
application = webapp.WSGIApplication([('-', SanityCheck)],
                                     debug=True)

# Fire it up!
wsgiref.handlers.CGIHandler().run(application)

Now that we've got a simple app, we'll test it with the development web server. If you haven't already, download the SDK and uncompress it. From the google_appengine directory, run './dev_appserver.py <your_app_directory>;'. Verify that you can access your app from a browser (the URL will be http://localhost:8080/).

Using Google App Engine to store data

The original gift-giving app uses the OpenSocial Persistence API to record the gifts that each user gives. Since data can only be written for the VIEWER, the original app has to do some extra work to perform simple operations. For example, to show all the gifts a person has received, the app iterates through all of their friends to see which have given them a gift. Another potential issue with the Persistence API is that the storage limits are set by the container. We'll circumvent both of these issues by using Google App Engine to store our data.

Defining the data model

We basically just need to store a history of who gave what gift to whom. For each gift, we can just store the name of the gift. For each gift transaction, we need to store the sender, recipient, and a reference to the gift that was given. We can use the OpenSocial user IDs to identify the sender and recipient, and we can use the db.Key() object for the reference to the gift object.

To define this model in the Google App Engine datastore, create a file called db_model.py and insert the following Python code:

from google.appengine.ext import db

class Gift(db.Model):
  name = db.StringProperty()

class GiftTransaction(db.Model):
  sender_id = db.StringProperty()
  receiver_id = db.StringProperty()
  gift = db.ReferenceProperty(Gift)

This defines two Model classes, one to represent a gift and one to represent a gift transaction where a sender gives a recipient a gift.

Populating the datastore

Now that we have a data model, we need some way to populate it. Create a file called admin.py and define an Admin class to handle administrative operations like this. Let's start with two methods for initializing the gifts and gift transactions in the datastore:

from db_model import Gift, GiftTransaction

class Admin:
  """Initializes the list of gifts in the datastore."""

  def initGifts(self):
    """Deletes any existing gifts and add the default gifts."""
    for gift in Gift.all():
      gift.delete()
    GIFT_NAMES = ['a cashew nut',
                  'a peanut',
                  'a hazelnut',
                  'a red pistachio nut']
    for name in GIFT_NAMES:
      gift = Gift()
      gift.name = name
      gift.put()

  def initGiftTransactions(self):
    """Deletes any existing gift transactions."""
    for t in GiftTransaction.all():
      t.delete()

Accessing the datastore

Now let's add a couple methods to the Admin class for accessing the gifts and gift transactions in the datastore.

  def getGiftNames(self):
    names = []
    for gift in Gift.all():
      names.append(gift.name)
    return names

  def getGiftTransactions(self):
    giftTransactions = []
    for t in GiftTransaction.all():
      giftTransactions.append("sender: %s, reciever: %s, gift: %s" %
          (t.sender_id, t.receiver_id, t.gift.key()))
    return giftTransactions

Great, now we can read and write data in the datastore - but how do we invoke this Python code? That's where the admin webapp comes into play.

A simple Google App Engine web interface

Now we'll extend our Google App Engine application to include an admin web application so we can initialize or view the data in the data store from a browser. We'll create a request handler so that we can invoke the Admin class by sending a GET request to a certain URL, like http://opensocial-gifts-*_username_*.appspot.com/admin?action=init.

Creating a request handler

The AdminServer class will be a subclass of the RequestHandler class provided by the google.appengine.ext.webapp package. We can implement a get method that will be invoked any time the application forwards a request to this class. Add the following import statement and class definition to admin.py:

from google.appengine.ext import webapp

class AdminServer(webapp.RequestHandler):
  """Handles requests to /admin URLs and delegates to the Admin class."""

  def get(self):
    """Handle GET requests."""
    self.response.out.write('Welcome to the admin webapp')

Forwarding requests

First, let's get rid of the SanityCheck class we started out with in gifts.py. Instead, import the admin module so we can access the AdminServer class. We'll edit the WSGIApplication constructor to forward requests for "/admin" URLs to the AdminServer class. Here's an updated version of the gifts.py file with changes bolded:

# Standard libraries
import wsgiref.handlers

# AppEngine imports
from google.appengine.ext import webapp

'''# OpenSocial Gifts imports
import admin'''

# Map URLs to request handler classes
application = webapp.WSGIApplication('''[('-admin', admin.AdminServer)]''',
                                     debug=True)

# Fire it up!
wsgiref.handlers.CGIHandler().run(application)

Now hit the URL http://localhost:8080/admin with your browser. You should see the admin web application welcome message.

Identifying the user

One of the coolest features of App Engine is that it handles authentication and identity management for you. We only need a few lines of code to allow for user logins and we can differentiate between regular users and administrators with the users.IsCurrentUserAdmin method. We'll leverage this in the get method to make sure normal users can't access our admin console:

from google.appengine.api import users

class AdminServer(webapp.RequestHandler):
  """Handles requests to /admin URLs and delegates to the Admin class."""

  def get(self):
    """Ensure that the user is an admin."""
    if not users.GetCurrentUser():
      loginUrl = users.CreateLoginURL(self.request.uri)
      self.response.out.write('<a href="%s">Login</a>' % loginUrl)
      return
 
    if not users.IsCurrentUserAdmin():
      self.response.out.write('You must be an admin to view this page.')
      return
 
    self._handleRequest()
 
def _handleRequest(self):
  self.response.out.write('Welcome to the admin web interface')

Browse to the admin console again and you'll see a "Login" link. If you login as a user that's not an administrator of the Google App Engine app, you'll see a message stating that "You must be an admin to view this page."

Invoking the Admin class

Now we can add some code to invoke the Admin class by implementing the _handleRequest method from the previous code snippet. The following method searches the request URL for a parameter called 'action' and, based on this value, either initializes the datastore or lists the gifts and gift transactions.

  def _handleRequest(self):
    """Invokes methods from the Admin class based on the 'action' parameter"""
    admin = Admin()
    action = self.request.get('action')
    if action h1. 'init':
      admin.initGifts()
      admin.initGiftTransactions()
      msg = "Gifts have been initialized, gift transactions have been cleared."
      self.response.out.write(msg)
    elif action 'list':
      self.response.out.write("Gifts = %s" % admin.getGiftNames())
      self.response.out.write("<br>")
      self.response.out.write("Gift Transactions = %s" % admin.getGiftTransactions())
    else:
      html = []
      html.append('<a href="/admin?action=init">Initialize datastore</a><br>')
      html.append('<a href="/admin?action=list">List all data in datastore</a>')
      self.response.out.write(''.join(html))

Note that if no 'action' parameter is given (or if the value is not 'init' or 'list') the handler will display links to initialize the datastore or list the gift data.

Creating a simple data API with Google App Engine

Requesting Gifts

Let's start by creating a request handler that will return the list of gifts in a JSON format. If a request comes in to http://opensocial-gifts-*_username_*.appspot.com/gifts, we should return:

["a cashew nut", "a peanut", "a hazelnut", "a red pistachio nut"]

Create a file called api.py to contain the API request handler. Implement the get method to return the list of gifts as a JSON string.

# App Engine imports
from google.appengine.ext import webapp

# Third party imports
import json

# OpenSocial Gifts imports
from db_model import Gift, GiftTransaction

class ApiServer(webapp.RequestHandler):
  """Handles requests to /gifts URLs and reponds with JSON strings."""
  
  def get(self):
    """Respond with a JSON string representation of the lists of gifts."""
    gifts = []
    for gift in Gift.all():
      item = {'key' : str(gift.key()),
              'name' : gift.name }
      gifts.append(item)
    self.response.out.write(json.write(gifts))

Note: The ApiServer class uses the python-json package. The json.py file is included in the application directory with the rest of the source code. Thanks, Patrick Dlogan!

Now update the application in gifts.py to forward requests to the '/gifts' path to this new API request handler.

import admin
import api

# Map URLs to request handler class
application = webapp.WSGIApplication([('/admin', admin.AdminServer),
                                      '''('/gifts', api.ApiServer)'''],
                                     debug=True)

Requesting GiftTransactions

We also want to expose GiftTransactions through this api, so let's add another value to the WGSIApplication constructor in gifts.py:

# Map URLs to request handler class
application = webapp.WSGIApplication([('/admin', admin.AdminServer),
                                      ('/gifts', api.ApiServer),
                                      '''('/giftTransactions', api.ApiServer)'''],
                                     debug=True)

Now we have two types of GET requests coming into the ApiServer and we can differentiate them based on the URL path:

  def get(self):
    """Call the appropriate handler based on the path of the HTTP request."""
    if self.request.path.beginsWith('gifts'):
      self._handleGifts()
    elif self.request.path.beginsWith('giftTransactions'):
      self._handleGiftTransactions()

  def _handleGifts(self):
    gifts = []
    for gift in Gift.all():
      item = {'key' : str(gift.key()),
              'name' : gift.name }
      gifts.append(item)
    self.response.out.write(json.write(gifts))

  def _handleGiftTransactions(self):
    #TODO(you) return a list of GiftTransactions as JSON

In the last snippet, the code for returning the list of gifts was moved into the _handleGifts section. Now we need to implement the _handleGiftTransactions method.

We should expect requests for two lists of GiftTransactions: a list of gifts a user has sent and a list of gifts a user has received. Let's design the API to accept the sender or receiver ID as a URL parameter and determine the list of GiftTransactions to return based on the values of these parameters.

  def _returnGiftTransactions(self):
    """Return the list of transactions specified by the URL query parameters."""
    sender_id = self.request.get("sender_id")
    receiver_id = self.request.get("receiver_id")
    giftTransactions = self._getGiftTransactions(sender_id, receiver_id)

    results = []
    for giftTransaction in giftTransactions:
      item = { 'sender_id' : giftTransaction.sender_id,
               'receiver_id' : giftTransaction.receiver_id,
               'gift_name' : giftTransaction.gift.name }
      results.append(item)
    self.response.out.write(json.write(results))

  def _getGiftTransactions(self, sender_id, receiver_id):
    results = []
    if sender_id:
      results = GiftTransaction.gql('WHERE sender_id=:sender_id', 
                                sender_id=sender_id)
    elif receiver_id:
      results = GiftTransaction.gql('WHERE receiver_id=:receiver_id',
                                receiver_id=receiver_id)
    else:
      results = GiftTransaction.all()

    return results;

Recording GiftTransactions

The last feature we need to add to the data API is the ability to record a new GiftTransaction. These requests will come in as HTTP POST requests to the /giftTransactions path with the sender ID, receiver ID, and gift key included as POST data. To handle this request, we simply need to implement a post method in the ApiServer class.

  def post(self):
    """Store a new gift transaction in the datastore based on the POST data."""
    giftTransaction = GiftTransaction()
    giftTransaction.sender_id = self.request.get('sender_id')
    giftTransaction.receiver_id = self.request.get('receiver_id')
    giftTransaction.gift = Gift.get(self.request.get('gift_key')).key()
    giftTransaction.put()

API Reference

Here's a summary of the API we just built:

HTTP Method

URL

Description

Example Response

GET

/gifts

Returns the names and keys of all gifts in the datastore as a JSON array.

[{"name" : "a peanut", "key" : "ABC"}, {"name" : "a cashew"}, {"key" : "XYZ"}]

GET

/giftTransactions?receiver_id=xxxx

Returns an array of gift transactions where the receiver is specified by the URL parameter receiver_id.

[{"sender_id":"yyyy", "receiver_id":"xxxx", "gift_key":"XYZ"},{"sender_id":"zzzz", "receiver_id":"xxxx", "gift_key":"ABC"}]

GET

/giftTransactions?sender_id=xxxx

Returns an array of gift transactions where the sender is specified by the URL parameter sender_id.

[{"sender_id":"xxxx", receiver_id:"yyyy", gift_key:"XYZ"}, {"sender_id":"xxxx", receiver_id:"zzzz", gift_key:"XYZ"}]

HTTP Method

URL

Description

Example POST data

POST

/giftTransactions

Creates a new gift transaction in the datastore based on the sender, receiver, and gift key specified in the POST data.

sender_id=xxxx&receiver_id=yyyy&gift_key=XYZ

Serving static files with Google App Engine

You can configure your app to serve up static content by editing the app.yaml file. Just specify the URL of the static directory and the name of the directory that will contain static content. Here's a copy of the app.yaml file with these changes bolded:

application: opensocial-gifts-'''''_username_'''''
version: 1
runtime: python
api_version: 1

handlers:

'''- url: /static
  static_dir: static'''

- url: /.*
  script: gifts.py

Note: Google App Engine caches static files for 10 minutes by default, but you can control this caching period in the app.yaml file. You'll find the details in the Configuring an App documentation.

In your application directory, create a directory called static. This directory is where we'll keep our OpenSocial app spec. Create a file called gifts.xml in the static directory and add the following content:

<?xml version="1.0" encoding="UTF-8"?>;
<Module>;
  <ModulePrefs title="Gifts" />;
  <Content type="html">;
    <![CDATA[
      Hello, Google App Engine!
    ]]>;
  </Content>;
</Module>;

Browse to http://localhost:8080/static/gifts.xml and verify that you can see your OpenSocial application spec.

Communication between OpenSocial and Google App Engine

Now that we have some data in the datastore and an API to access it, we can enhance our OpenSocial app by enabling it to communicate with the Google App Engine project. We'll start by requesting the list of gifts and friends and displaying them in drop-down menus. Then we'll request the gift transactions from the data API we built. Finally, we'll let the user actually give a gift by POSTing a new gift transaction to the data API.

Publishing your app

Up to this point, we've been using the (local) development app server, but in order for an OpenSocial container like orkut or MySpace to access your application spec, the file needs to be hosted publicly. From the google_appengine directory, run ./appcfg.py update <your_app_directory>; from the application directory to publish your app. After the upload is complete, make sure you can access the OpenSocial application spec XML file at http://opensocial-gifts-*_username_*/static/gifts.xml from your browser.

Requesting friends

The following application will request the list of friends from the OpenSocial container when the page loads and display the friends in a drop-down menu.

<?xml version="1.0" encoding="UTF-8"?>;
<Module>;
  <ModulePrefs title="Gifts" >;
    '''<Require feature="opensocial-0.8"/>'''
  </ModulePrefs>;
  <Content type="html">;
    <![CDATA[
    <script>;
'''gadgets.util.registerOnLoadHandler(init);

function init() {
  loadFriends();
}

function loadFriends() {
  var req = opensocial.newDataRequest();

  var viewerFriendsIdSpec = opensocial.newIdSpec({ "userId" : "VIEWER", "groupId" : "FRIENDS" });
  var opt_params = {};
  opt_params[opensocial.DataRequest.PeopleRequestFields.MAX] = 100;
  req.add(req.newFetchPeopleRequest(viewerFriendsIdSpec, opt_params), 'viewerFriends');
  req.send(onLoadFriends);
}

function onLoadFriends(data) {
  var viewerFriends = data.get('viewerFriends').getData();
 
  html = new Array();
  html.push('<select id="person">;');
  viewerFriends.each(function(person) {
    html.push('<option value="' + person.getId() + '">;' + person.getDisplayName() + "</option>;");
  });
  html.push('</select>;');
  document.getElementById('friends').innerHTML = html.join(''); 
}
      </script>;
      <span id="friends"></span>;'''
    ]]>;
  </Content>;
</Module>;

The loadFriends method sends a DataRequest to the OpenSocial container and specifies the onLoadFriends method as a callback function. The onLoadFriends method receives a DataResponse object that contains an opensocial.Collection of opensocial.Person objects which represent the user's friends. The name of each of these friends is then added as an option in the drop-down menu. The drop-down menu, a <select>; element, is then inserted into the 'friends' span.

Requesting gifts

Next, let's add a drop-down menu for the list of gifts. We'll request the gift data, format it into a drop-down menu, and add it to the HTML in a 'gifts' span. First, add the 'gifts' span at the end of the application spec:

<?xml version="1.0" encoding="UTF-8"?>;
<Module>;
  <ModulePrefs title="Gifts" >;
    <Require feature="opensocial-0.8"/>
  </ModulePrefs>;
  <Content type="html">;
    <![CDATA[
      <script>;
        <-- all the JavaScript -->;
      </script>;
      '''Give <span id="gifts">;</span>; to <span id="friends">;</span>;.'''
    ]]>;
  </Content>;
</Module>;

Next, create a loadGifts method and invoke it when the page loads.

function init() {
  loadFriends();
  '''loadGifts();'''
}

'''function loadGifts() {
  var params = {};
  params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;
  var url = 'http://opensocial-gifts-*_username_*.appspot.com/gifts';

  gadgets.io.makeRequest(url, onLoadGifts, params);
}'''

The loadGifts method uses the gadgets.io.MakeRequest method to send a request to the JSON data API for the array of available gifts. Once we get the response, the callback method, onLoadGifts, will display the gifts in a drop-down menu.

function onLoadGifts(response) {
  var gifts = response.data;
  var html = new Array();
  html.push('<select id="nut">;');
  for (var i = 0; i < gifts.length; i++) {
    html.push('<option value="' + gifts[i].key + '">;' + gifts[i].name + '</option>;');
  }
  html.push('</select>;');
  document.getElementById('gifts').innerHTML = html.join('');
}

Requesting gift transactions

Next we want to display the gifts the user has given and received. Since the data API only returns user IDs, we'll need to build an object to map the IDs to the names of our friends (so the app can display names instead of user IDs). We can build this object at the same time that we build the drop-down menu of the user's friends, then pass it into a new method called loadGiftTransactions where we'll actually make the requests to the data API running on the Google App Engine project. To do this, modify the onLoadFriends method so it looks like this:

function onLoadFriends(data) {
  var viewer = data.get('viewer').getData();
  var viewerFriends = data.get('viewerFriends').getData();
  '''var friends = new Array();'''
 
  html = new Array();
  html.push('<select id="person">;');
  viewerFriends.each(function(person) {
    html.push('<option value="' + person.getId() + '">;' + person.getDisplayName() + "</option>;");
    '''friends[person.getId()] = person.getDisplayName();'''
  });
  html.push('</select>;');
  document.getElementById('friends').innerHTML = html.join(''); 

  '''loadGiftTransactions(viewer, friends);'''
}

In the loadGiftTransactions method, we'll construct the appropriate URLs to fetch this data from the data API and use makeRequest to send the requests for gift transaction data. Each call to makeRequest specifies a callback method, which is actually a closure so that we can use the friends object built in the onLoadFriends method when processing the results of the data request. Here is the implementation for the loadGiftTransactions method and the two callbacks:

function loadGiftTransactions(viewer, friends) {
  // Get the gift transactions where the VIEWER is the sender
  var url = 'http://opensocial-gifts-'''''_username_'''''/giftTransactions?sender_id=' + viewer.getId();
  gadgets.io.makeRequest(url, onLoadGiftsGivenClosure(friends));

  // Get the gift transactions where the VIEWER is the receiver
  var url = 'http://opensocial-gifts-'''''_username_'''''/giftTransactions?receiver_id=' + viewer.getId();
  gadgets.io.makeRequest(url, onLoadGiftsReceivedClosure(friends));
}

function onLoadGiftsGivenClosure(friends) {
  return function(response) {
    var giftTransactions = gadgets.json.parse(response.data);   
    var html = new Array();
    html.push('You have given:');
    html.push('<ul>');
    for (var i=0; i<giftTransactions.length; i++) {
      html.push('<li>' + friends[giftTransactions(i].receiver_id] + ' received ');
      html.push(giftTransactions[i].gift_name + '</li>');
    }
    html.push('</ul>');
    document.getElementById('given').innerHTML = html.join('');

    gadgets.window.adjustHeight();
  }
}

function onLoadGiftsReceivedClosure(friends) {
  return function(response) {
    var giftTransactions = gadgets.json.parse(response.data);   
    var html = new Array();
    html.push('You have received:<ul>');
    for (var i=0; i<giftTransactions.length; i++) {
      html.push('<li>' + giftTransactions[i].gift_name + ' from ');
      html.push(friends[giftTransactions(i].sender_id] + '</li>');
    }
    html.push('</ul>');
    document.getElementById('received').innerHTML = html.join('');

    gadgets.window.adjustHeight();
  }
}

Since displaying these gift transactions may take up more height than the container provides by default, these callbacks use gadgets.window.adjustHeight to resize the app after inserting the data into the page. In order to use this new method, you need to include <Require feature="dynamic-height"/> in the <ModulePrefs> element of the application spec.

Finally, add the 'given' and 'received' <div> elements to the HTML section of the application spec:

<?xml version="1.0" encoding="UTF-8"?>
<Module>
  <ModulePrefs title="Gifts" >
    <Require feature="opensocial-0.8"/>
    *<Require feature="dynamic-height"/>*
  </ModulePrefs>
  <Content type="html">
    <![CDATA[
      <script>
        <-- all the JavaScript -->
      </script>
      Give <span id="gifts"></span> to <span id="friends"></span>.
      '''<div id="given"></div>
      <div id="received"</div>'''
    ]]>
  </Content>
</Module>

Recording new gift transactions

The last piece for functionality to add is the ability for a user to actually give a gift to their friend. To do this, we'll add a "Give!" link to the HTML that will invoke a giveGift JavaScript function when clicked.

<?xml version="1.0" encoding="UTF-8"?>
<Module>
  <ModulePrefs title="Gifts" >
    <Require feature="opensocial-0.8"/>
    <Require feature="dynamic-height"/>
  </ModulePrefs>
  <Content type="html">
    <![CDATA[
      <script>
        <-- all the JavaScript -->
      </script>
      Give <span id="gifts"></span> to <span id="friends"></span>.
      '''<a href="javascript:void(0);" onclick='giveGift();'>Give!</a>'''    
      <div id="given"></div>
      <div id="received"</div>
    ]]>
  </Content>
</Module>

Note: When using links to invoke JavaScript functions, always use href="javascript:void(0);". Using href="#" causes unexpected results in the container.

The giftGive function just requests the VIEWER from the container and sends the receiver and gift key data to the callback function for that request. In the callback, we make a POST request (using makeRequest) to the data API that contains the sender and receiver IDs and the key of the gift given as post data.

function giveGift() {
  var gift_key = document.getElementById('nut').value;
  var receiver_id = document.getElementById('person').value;
 
  var req = opensocial.newDataRequest();
  req.add(req.newFetchPersonRequest(opensocial.IdSpec.PersonId.VIEWER), 'viewer');
  req.send(postGiftTransactionClosure(receiver_id, gift_key));
}

function postGiftTransactionClosure(receiver_id, gift_key) {
  return function(response) {
    var sender_id = response.get('viewer').getData().getId();
    var params = {};
    params[gadgets.io.RequestParameters.METHOD] = gadgets.io.MethodType.POST;
    post_data = gadgets.io.encodeValues({
        'sender_id' : sender_id,
        'receiver_id' : receiver_id,
        'gift_key' : gift_key });
    params[gadgets.io.RequestParameters.POST_DATA] = post_data;
    var url = http://opensocial-gifts-''''''_username_'''''/giftTransactions';

    gadgets.io.makeRequest(url, loadFriends, params);
  }
}

Notice that the callback function for this makeRequest call is loadFriends. This will basically redraw the app after the gift transaction is processed.

Sending and verifying signed requests

You don't want just anybody to be able to access your application data through the data API. OpenSocial containers can add digital signatures to the requests that go out to your servers, or in this case, Google App Engine.

To send a signed request from your OpenSocial app, just change the parameters sent to makeRequest as follows:

function postGiftTransactionClosure(receiver_id, gift_key) {
  return function(response) {
    var sender_id = response.get('viewer').getData().getId();
    var params = {};
    '''''params[gadgets.io.RequestParameters.AUTHORIZATION = gadgets.io.AuthorizationType.SIGNED;'''''
    params[gadgets.io.RequestParameters.METHOD] = gadgets.io.MethodType.POST;
    post_data = gadgets.io.encodeValues({
        'sender_id' : sender_id,
        'receiver_id' : receiver_id,
        'gift_key' : gift_key });
    params[gadgets.io.RequestParameters.POST_DATA] = post_data;
    var url = 'http://opensocial-gifts-'''''_username_'''''/giftTransactions';

    gadgets.io.makeRequest(url, loadFriends, params);
  }
}

Using signed requests is most important when you're executing actions on the user's behalf since you don't want a malicious user performing actions for a legitimate user. For example, a malicious user could send POST requests to the /giftTransactions URL of our data API and include any sender ID, receiver ID, or gift key. By signing your requests, you can protect your data from unauthorized access - if a request is forged, you can reply with an error message or nothing at all.

You will need to add code to the api.py class to verify the signature received from the container. We can implement an _isValidSignature() method and call it before processing GET or POST requests:

'''def _isValidSignature(self):
  return False'''

def get(self):
  """Respond with a JSON string representation of the lists of gifts."""
  '''if not self._isValidSignature():
    self.response.out.write(json.write({}))
    return'''

  if self.request.path.startswith('/gifts'):
    self._returnGifts()
  elif self.request.path.startswith('/giftTransactions'):
    self._returnGiftTransactions()

def post(self):
  """Store a new gift transaction in the datastore based on the POST data."""
  '''if not self._isValidSignature():
    return'''

  giftTransaction = GiftTransaction()
  giftTransaction.sender_id = self.request.get('sender_id')
  giftTransaction.receiver_id = self.request.get('receiver_id')
  giftTransaction.gift = Gift.get(self.request.get('gift_key')).key()
  giftTransaction.put()

OpenSocial uses OAuth's method for signing requests and containers may use the HMAC-SHA1 or RSA-SHA1 algorithms. The following sample code demonstrates the RSA-SHA1 algorithm and assumes the container is orkut. Orkut's public key is available in an x509 certificate, which has been parsed, converted to hex value, and hard-coded in the public_key_str variable.

import hashlib
import urllib

import oauth    
from Crypto.PublicKey import RSA
from Crypto.Util import number

def _isValidSignature(self):

  # Construct a RSA.pubkey object
  exponent = 65537
  public_key_str = """0x
00b1e057678343866db89d7dec2518
99261bf2f5e0d95f5d868f81d600c9
a101c9e6da20606290228308551ed3
acf9921421dcd01ef1de35dd3275cd
4983c7be0be325ce8dfc3af6860f7a
b0bf32742cd9fb2fcd1cd1756bbc40
0b743f73acefb45d26694caf4f26b9
765b9f65665245524de957e8c547c3
58781fdfb68ec056d1"""
  public_key_long = long(public_key_str, 16)
  public_key = RSA.construct((public_key_long, exponent))
      
  # Rebuild the message hash locally
  oauth_request = oauth.OAuthRequest(http_method=self.request.method, 
                                     http_url=self.request.url, 
                                     parameters=self.request.params.mixed())
  message = '&'.join((oauth.escape(oauth_request.get_normalized_http_method()),
                      oauth.escape(oauth_request.get_normalized_http_url()),
                      oauth.escape(oauth_request.get_normalized_parameters()),))
  local_hash = hashlib.sha1(message).digest()

  # Apply the public key to the signature from the remote host
  sig = urllib.unquote(self.request.params.mixed()["oauth_signature"]).decode('base64')
  remote_hash = public_key.encrypt(sig, '')[0][-20:]
  
  # Verify that the locally-built value matches the value from the remote server.
  return local_hashh1. remote_hash

The _isValidSignature method uses two third party modules. OAuth's Python client library was written by Leah Culver and the RSA code is just a small piece of the pycrypto toolkit. Score one for open source!

In case you didn't cut and paste everything just right, this application is hosted in the opensocial-gifts Google Code project where you can find the OpenSocial application spec, the full implementation of the JSON data API, and all the other files used in this application. Next Steps h1. This tutorial has only scratched the surface of what you can do with Google App Engine and OpenSocial. Try adding some of these features to your app:

  • Display pictures for each gift instead of text (Hint: store the images in your static directory).
  • Expand the admin interface to allow more granular access to the data (e.g. delete or update a single entity rather than resetting everything at once).
  • Use templates to render the admin interface.
  • Include timestamps in gift transactions and only show recent gifts in the app.
  • Allow new gifts to be added through the admin interface.
  • Let users send a custom message with their gift.
  • Create a profile view for the OpenSocial app.

Happy coding! Resources ==

Developer Forums

If you have questions while working through this tutorial, you can ask other developers in one of the following forums:

Reference

Google App Engine

OpenSocial