This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Building OpenSocial Apps A Field Guide to Working with the MySpace Platform Chris Cole Chad Russell Jessica Whyte
Upper Saddle River, NJ • Boston • Indianapolis • San Francisco New York • Toronto • Montreal • London • Munich • Paris • Madrid Capetown • Sydney • Tokyo • Singapore • Mexico City
Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and the publisher was aware of a trademark claim, the designations have been printed with initial capital letters or in all capitals.
Editor-in-Chief Mark L. Taub
The screenshots and other depictions of myspace.com contained in this book may not accurately represent myspace.com as it exists today or in the future, including without limitation with respect to any policies, technical specs or product design.
Development Editor Songlin Qiu
The authors and publisher have taken care in the preparation of this book, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein.
Managing Editor John Fuller
The publisher offers excellent discounts on this book when ordered in quantity for bulk purchases or special sales, which may include electronic versions and/or custom covers and content particular to your business, training goals, marketing focus, and branding interests. For more information, please contact: U.S. Corporate and Government Sales (800) 382-3419 [email protected]
Full-Service Production Manager Julie B. Nahil Project Management diacriTech LLC Copy Editor Barbara Wood Indexer Jack Lewis
For sales outside the United States, please contact: International Sales [email protected]
Technical Reviewers Cassie Doll Bess Ho Benjamin Schupak Book Designer Gary Adair Compositor diacriTech LLC
❖
This book is dedicated to my ever-suffering wife, Kristen, and our two crazy and wonderful children, Darien and Reece.Thanks for working overtime with the kids, baby. —Chris Cole To the reader, we hope this book serves you well. —Chad Russell and Jessica Whyte ❖
Contents at a Glance Contents
viii
Foreword
xvi
Acknowledgments
xviii
About the Authors
xix
Introduction
xxi
I: Building Your First MySpace Application 1 Your First MySpace App
3
2 Getting Basic MySpace Data
9
3 Getting Additional MySpace Data 4 Persisting Information
29
47
5 Communication and Viral Features
67
6 Mashups and External Server Communications 91 7 Flushing and Fleshing: Expanding Your
App and Person-to-Person Game Play
117
II: Other Ways to Build Apps 8 OAuth and Phoning Home 9 External Iframe Apps
153
177
10 OSML, Gadgets, and the Data Pipeline
213
11 Advanced OSML: Templates, Internationalization, and View Navigation 239
III: Growth and How to Deal with It 12 App Life Cycle
265
13 Performance, Scaling, and Security
283
Contents at a Glance
14 Marketing and Monetizing
305
15 Porting Your App to OpenSocial 0.9
References Index
355
351
329
vii
Contents Foreword xvi Acknowledgments About the Authors Introduction xxi
xviii xix
I: Building Your First MySpace Application 1 Your First MySpace App
3
Creating the App—“Hello World”
3
Step 1: Sign Up for a Developer Account Step 2: Create an App
Step 3: Enter Your Source Code Installing and Running Your App Summary
3
4 4
7
7
2 Getting Basic MySpace Data
9
The Two Concepts That Every Developer Should Know 9 Basic Concepts: Owner and Viewer
9
Basic Concepts: Permissions for Accessing MySpace Data 10 Starting Our Tic-Tac-Toe App
10
Accessing MySpace User Data
11
Accessing Profile Information Using the opensocial.Person Object 15 Getting More than Just the Default Profile Data
18
opensocial.DataResponse and opensocial. ResponseItem (aka, Using MySpace User Data)
19
Error Handling Summary
24
27
3 Getting Additional MySpace Data
29
How to Fetch a Friend List and Make Use of the Data 29 Getting the Friend List Filters and Sorts
31
30
Contents
Paging
32
Using the Data Fetching Media Photos
37
39
39
Albums and Videos
41
Using opensocial.requestPermission and opensocial.hasPermission to Check a User’s Permission Settings 43 Summary
45
4 Persisting Information App Data Store
47
47
Saving and Retrieving Data
48
Refactoring to Build a Local App Data Store Cookies
56
Why You Shouldn’t Use Cookies
57
Building the Cookie Jacker App Third-Party Database Storage Summary
51
59
64
65
5 Communication and Viral Features
67
Using opensocial.requestShareApp to Spread Your App to Other Users 67 Defining requestShareApp
70
Writing the requestShareApp Code Calling requestShareApp
71
72
The requestShareApp Callback
72
Using opensocial.requestSendMessage to Send Messages and Communications 74 Defining requestSendMessage
75
Writing the requestSendMessage Code Callback in requestSendMessage
76
78
Getting Your App Listed on the Friend Updates with opensocial.requestCreateActivity Basics 79 Defining opensocial.requestCreateActivity
79
Using the Template System to Create Activities Data Types
80
Reserved Variable Names Aggregation
82
81
80
ix
x
Contents
Body and Media Items
82
Using the Template Editor to Create Templates Using opensocial.requestCreateActivity Sending Notifications Summary
88
90
6 Mashups and External Server Communications Communicating with External Servers Mashups
91
92
Adding a Feed Reader to Our App
93
Overview of gadgets.io.makeRequest Response Structure
94
96
Handling JSON Content
97
Handling Partial HTML Content Handling RSS Feed Content Handling XML Content
97
97
98
“User’s Pick” Feed Reader
98
Setup and Design of the Feed Reader FEED Content Type
98
104
XML Content Type with Parsing TEXT Content Type
105
107
Adding a Feed Refresh Option Feed Automation Candy
109
110
Secure Communication with Signed makeRequest 111 Adding an Image Search Overview of JSONP
112 112
Implementing the Image Search Posting Data with a Form Summary
83
85
113
114
114
7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play 117 Turn-Based Games Design Overview
117 118
Adding FriendPicker App Data Game Store
119 125
Supporting Person-to-Person Game Play
133
91
Contents
Adding P2P Game Play Support in the Game Engine 133 Adding User Feedback
135
Fleshing Out P2P Game Logic
138
Finishing and Clearing a Game “Real-Time” Play
144
146
Advantages and Disadvantages of App Data P2P Play 148 Summary
148
II: Other Ways to Build Apps 8 OAuth and Phoning Home What Is OAuth?
153
153
OAuth Libraries
154
Setting Up Your Environment When Is OAuth Not OAuth? Secure Phone Home
154 157
157
Unsigned GET Request Signed POST Request
158 162
Testing Your OAuth Implementation Locally Making Real MySpace Requests
166
169
Spicing Up the Home and Profile Surfaces Using makeRequest 173 Summary
174
9 External Iframe Apps REST APIs
177
178
How a REST Web Service Is Addressed Setting Up an External Iframe App The Server Code REST API List The Client Code
181
183 197
Friends Web Service and Paging The Profile Endpoint
199
203
Sending Messages Using IFPC
208
Using the 0.7 Container for postTo
210
The Friends Response from the REST API Summary
212
178
179
211
xi
xii
Contents
10 OSML, Gadgets, and the Data Pipeline The Big Picture
213
Gadget XML
214
Data Pipeline OSML
213
214
214
Writing a Gadget
214
“Hello World” Gadget
214
Adding a Second Surface to the Gadget Declaring and Using Basic Data Data Pipelining
219
DataContext
220
Data Tags
217
218
220
In-Network versus Out-of-Network Data
221
Data Tags os:ViewerRequest and os:OwnerRequest 222 Data Tag os:PeopleRequest
222
Data Tag os:ActivitiesRequest Data Tag os:DataRequest
223
223
JavaScript Blocks in OSML Apps
225
OpenSocial Markup Language (OSML) Basic Display Tags
Remote Content Display Tags Control Flow Tags
226
226
Putting It Together: OSML Tic-Tac-Toe Setting Up the Gadget
230
235
Displaying Data Lists Summary
226
227
Reusing Common Content Working with Data
225
226
237
238
11 Advanced OSML: Templates, Internationalization, and View Navigation 239 Inline Tag Templates
239
Defining and Using a Tag Template Using Client-Side Templates Working with Subviews
242
245
Converting Tabs to Subviews
245
240
Contents
HTML Fragment Rendering
248
Adding Content with os:Get
248
Adding Targeted Content with myspace:RenderRequest 249 Data Listeners
250
Displaying JSON Results with a Data Listener Internationalization and Message Bundles Creating Our First Message Bundle
256
Creating Translations of the Message Bundle Including Translations in an App and Testing Future Directions Summary
260
261
III: Growth and How to Deal with It 12 App Life Cycle
265
Publishing Your App
265
What’s Allowed, or Why So Many Apps Get Rejected 266 Dealing with Rejection
267
Contesting a Rejection
267
Managing Your App
274
Hiding and Deleting an App
274
Making Changes to a Live App (Multiple Versions) 274 Republishing a Live App
275
Changing the App Profile/Landing Page Managing Developers Managing Testers
275
279 279
Event Handling—Installs and Uninstalls Suspension and Deletion of Your App Summary
279
280
281
13 Performance, Scaling, and Security Performance and Responsiveness
283
283
What Is Responsive Performance and What Is Scale Performance? 283 Design for Responsiveness
284
251
255 257 258
xiii
xiv
Contents
Responsive OpenSocial App Performance Guidelines 285 Design for Scale
292
App Guidelines for Internet-Scale Performance Stability and Fault Tolerance Rule 1: Validate Inputs
299 299
Rule 2: Test OpenSocial DataResponse Objects for Errors 300 Rule 3: Provide Time-Outs and Error Flow
300
Rule 4: Don’t Assume That Weird Error Was an Anomaly 300 User and Application Security User Data Security
301
Application Security
301
Hacking and Cracking Summary
300
302
303
14 Marketing and Monetizing
305
Using MySpace to Promote Your App The App Gallery
306
306
App Profile, or Bringing Out the Bling MySpace’s Own MyAds
308
User Base and Viral Spreading Listen to Your Customers Ads
308
309 311
311
Google AdSense Cubics
RockYou! Ads Micropayments PayPal Boku Others
311
313 314 316
316 317 318
Interviews with Successful App Developers Dave Westwood: BuddyPoke (www.myspace.com/buddypoke) Eugene Park: Flixster (www.myspace.com/flixstermovies)
318 321
318
293
Contents
Tom Kincaid: TK’s Apps (www.myspace.com/tomsapps) Dan Yue: Playdom (www.myspace.com/playdom) Summary
322 324
326
15 Porting Your App to OpenSocial 0.9 Media Item Support opensocial.Album Fetching Albums
330 333
Fetching Media Items
335
Updating Albums and Media Items Uploading Media Items
340
Simplification of App Data
341
REST APIs Summary
343 348
References Index
355
351
329
330
338
xv
Foreword The Internet is constantly evolving, with vast arrays of information on every topic growing at a remarkable pace. Google’s 1998 search index had only 26 million Web pages; a decade later it recognizes more than 1 trillion URLs.With so much information, we need a new set of tools to make sense of the Internet.The transition is from a strict focus on informational content, to being able to take advantage of our context, our relationships, and our activities. Enter the social Web, a relatively new twist to the Internet, which is being built up by social networks, portals, and even more traditional businesses.The “Open Stack” is a set of specifications being developed by grass-roots communities all over the world, enabling developers to create new products and services enhanced by user-specific data.The Open Stack includes specifications such as OAuth, which provides secure access to data; OpenID, a global identity standard; and OpenSocial, a common API for building applications.These specifications are becoming the underlying infrastructure for the social Web, weaving a social fabric throughout the Web. OpenSocial enables developers to learn a single core programming model that can be applied to all “OpenSocial containers,” those sites that support the OpenSocial specification.With standards-based tools, including a JavaScript-based gadget API, a REST-based data access API, lightweight storage capabilities, and access to common viral channels, developers can build inside those containers as well as create applications for mobile phones or other sites. In late 2009, less than two years after its introduction, more than 50 sites have implemented support for the OpenSocial specification. In aggregate, these sites provide developers with access to more than 750 million users all over the world. By taking advantage of the OpenSocial API and the information available in this book, you will have a great opportunity to reach a lot of users. For example, you’ll find the OpenSocial API supported by many major sites that span the globe:Yahoo!, iGoogle, Xiaonei and 51.com (China), Mixi (Japan), orkut (Brazil), Netlog (Europe), and, of course, MySpace. Beyond that, OpenSocial is also supported by more productivity-oriented sites like LinkedIn and by Fortune 100 companies as diverse as IBM and Lockheed Martin. With this book, you can quickly get up and running with OpenSocial on MySpace, and you’ll be poised to leverage that experience to reach users on other sites as well.The in-depth programming examples provide a good introduction to the design options available when building with the OpenSocial API, and the code is open source for ease of use and future reference. Additionally, the MySpace Platform tips sprinkled throughout will help you avoid common mistakes and understand the intricacies of their platform policies. Getting a feel for how social platforms operate will be valuable as you continue to explore the wide world of OpenSocial. OpenSocial is constantly evolving, just like the rest of the Internet.The OpenSocial specification is managed through an open process where anyone can contribute their ideas to influence the next version of the specification and help move the social Web forward.
Since its creation, there have been several significant revisions to the specification, introducing some new programming methodologies and improvements that make it easier for new developers to start using OpenSocial. As you’re getting into the OpenSocial API, be sure to contribute back to the OpenSocial.org community your ideas on how to improve the specification. It’s open. It’s social. It’s up to you. —Dan Peterson, president, OpenSocial Foundation San Francisco, California August 2009
Acknowledgments Chris Cole: I’d like to acknowledge the great team at MySpace that helped build the developer platform and all the people who’ve contributed to refining the OpenSocial specification. Compromise is hard, but a bad spec would have been even harder. Chad Russell: Thank you to Dan Peterson, for being a resource and agreeing to write our Foreword. And, of course, a big thanks to our technical editors, Bess Ho, Cassie Doll, and Ben Schupak, who all found a number of issues that would otherwise slipped through the cracks. Jessica Whyte: Thank you to Trina MacDonald and Olivia Basegio at Addison-Wesley for your support. I’d also like to acknowledge Tom Kincaid, Eugene Park, Dave Westwood, Dan Yue, Jon Nguyen, and Katie Simpkins for taking the time to answer all of my (many) questions about the platform.
About the Authors Chris Cole is a software architect at MySpace and is a major contributor to building the MySpace Open Platform and keeping it committed to OpenSocial. He’s been a core contributor to the OpenSocial 0.9 specification and is the primary architect and implementer of OSML (OpenSocial Markup Language) on the MySpace platform. Chris has been in software for fifteen years and has spent much of that time riding the various waves of the Internet and pushing the boundaries of the Web. Chad Russell is the lead developer on the MySpace OpenSocial team. He knows the OpenSocial spec front to back, in addition to every tip, trick, and nuance of the MySpace platform itself. Chad holds an engineering degree from the University of Toronto and currently resides in Seattle,Washington. Jessica Whyte has worked for several years as a journalist, most recently with Journalists for Human Rights, and is currently a graduate student at the University of Washington, studying Human-Centered Design and Engineering. She lives in Seattle with her husband and coauthor, Chad Russell.
This page intentionally left blank
Introduction Welcome to the wonderful world of social apps.You are about to enter—or have already entered—a fast-paced and treacherous landscape that can be exciting, frustrating, intellectually challenging—and yes, it can even be pretty profitable. We hope to be your guide through both the hidden pitfalls and the soaring peaks. There is much to gain from this exciting new market; social apps have been around for only a few years, after all. Many successful apps have yet to be created and then discovered by the millions upon millions of passionate MySpace users. In this book we’ll start from scratch and walk you through the entire process of building apps, from signing up as a developer all the way to building highly complex social apps that can scale out to thousands of users.We have extensive experience on the MySpace Development Platform (MDP), from building apps to helping build the platform itself.We’ll point out the many idiosyncrasies and quirks of the platform, as well as the many ways to squeeze out a little better performance by making a few tweaks to your existing apps. Throughout this book we’ll demonstrate best practices and show lots of sample code as we take a step-by-step approach to app development. Starting with a simple “Hello World”–style app, we’ll add functionality until we have a fully built, feature-rich app. We’ll fetch data on the current user, get the user’s friend list and photos, and parse all the data that comes back to display it on the screen. We’ll send app invitations and notifications to help spark viral growth and send requests back to Web services running on a third-party server in order to process and store data. Finally, and possibly most important, we’ll show you how to make money developing apps using ads and micropayments. When it comes to developing social apps on the MySpace platform, the sky is the limit. As of this writing, the most popular app on MySpace has more than 13 million installs. Maybe your app will be the next big thing. All it takes is a good idea and a little bit of knowledge. If you provide the former, we can provide the latter. Our hope is that this book is accessible both to experienced social app developers and to those developers who have only heard about it on the news.To that end, if you’re standing in the technology section of your local bookstore reading this, and you have a bit of computer programming knowledge mixed in with some free time and curiosity, this book is for you. We’ll start from scratch in this introduction to ramp you up on MySpace and OpenSocial. For those who already have apps up and running out in the wild, this book is also for you, as we’ll dig pretty deeply into the platform a little later on.You may be familiar with the ideas and terms we introduce here, so feel free to skip ahead a bit. (Check out the section on tools, though!)
xxii
Introduction
How to Read This Book There are a couple of ways to read this book. If you’re an experienced app developer, you can use this as a reference book.The index will direct you to specific topics, where you can consult the various tables, sample code, and tips. Or, if you need to learn a new aspect of the platform, you can skip directly to that chapter and dive right in. If you’re not an experienced app developer, we suggest you start from the beginning. We’ll start off with a “Hello World”–style app in Chapter 1 and add to it in each subsequent chapter. By the time the app is completed, you’ll have a broad knowledge base of the entire platform.We then branch out into some advanced topics to take you a step further.
Assumptions Made by the Authors This is not a book about how to code in JavaScript or Python or any other language.We will try to follow good practice in our sample code throughout the book, and we’ll point out a few interesting tidbits along the way, but that’s all.This is a book on how to write OpenSocial apps on MySpace. To that end, we assume the following: n
n
You have basic knowledge of computer programming in general. Maybe you work at a software company and write PHP every day, or you’ve taken a few programming classes, or you’ve taught yourself by constructing your own Web page. You have basic knowledge of JavaScript or are willing to pick up the basics.We’re not talking advanced stuff here, just the basics like calling functions, giving values to variables, and that kind of thing.
With all that said, let’s get started!
What Is MySpace? Ah, MySpace. Land of lost friends now found, hours wasted, and every band on the planet, large and small. Oh, and spam. Lots of spam. You can find the site here: www.myspace.com. MySpace is a social network. You sign up for free and you’re given a “profile”; the Profile page is a Web page that others can view and you can edit. On your Profile you can add information about yourself, pick a display name, and upload a picture. Others who view your Profile can leave comments for all to see.The cascading style sheets (CSS) of Profile pages can also be edited, so individuals can alter the styles of the page to “bling” out their Profiles in ways both beautiful and stunningly, shockingly bad. In addition to a Profile page, you are given a Home page (shown in Figure I.1), which only you can see. It contains your notifications, “You have new mail!” and the like, along with various links to other parts of the site and updates from your friends. It is the friend aspect that truly makes a social network.The idea is simple; you sign up and are assigned a single solitary friend:Tom, one of the cofounders of MySpace.
Introduction
Figure I.1
A MySpace Home page.
Through various means, such as searching, browsing, or having a robot guess “People You May Know,” you can find new friends and add them to your list. Having other users as friends grants you certain privileges to their information. You may have access to their Profiles even if they’re set to “Private”; you see their updates on your feed; and, most important to us, friends can interact inside apps. That’s MySpace in a nutshell. You sign up, bling out your Profile, add some friends, upload some pictures, install some apps, and send some messages.
What Is OpenSocial? OpenSocial is a specification that defines how Web sites allow third-party apps to run on their sites.That means many things. For example, OpenSocial defines what information a Web site can and must expose for each of its users. It defines a client-side JavaScript runtime environment with a set of APIs that describe how to fetch and update that user data.This environment is commonly called an “OpenSocial container.” Likewise, it defines a set of server-side APIs that manipulate the same data but can be called from a server running your language of choice, such as PHP. It also defines a markup language called OSML that can be used to draw your app and fetch data. This specification was started by the folks at Google, along with the help of MySpace, Hi5, and other initial early adopters. Maintenance of the spec has since been passed to the OpenSocial Foundation and the community.That means any implementers, such as MySpace, Orkut, and LinkedIn, app developers, or really anyone at all can suggest modifications to the spec and participate in setting the direction.Those modifications are then voted on in a public forum and either brought into the spec (found at http:// opensocial.org) or rejected.
xxiii
xxiv
Introduction
Any site on the Web is free to implement the OpenSocial spec.The idea is that when a site becomes OpenSocial-compliant, it is able to run almost any of the thousands of apps already created using OpenSocial. If a site has correctly implemented all the various APIs, an app running on MySpace should run just as well anywhere else.That’s the theory, anyway. In practice there are differences, some small, some large, between the various OpenSocial implementers, which can make it tricky to maintain an app on multiple sites.
What Is the MySpace Open Platform? The MySpace Open Platform (sometimes called MySpace Developer Platform or MDP) is simply MySpace’s implementation of the OpenSocial spec. MDP is a blanket term that covers how to sign up as a developer, how to update your app, and how to actually run your app. MySpace tries its best (we really do) to fully implement the OpenSocial spec, but there are bound to be some inconsistencies. Most of these inconsistencies are small, but we’ll cover them as they come up throughout this book. MySpace has also implemented some features that aren’t in the spec; we’ll cover these as well. More than on any other OpenSocial-enabled site, developing apps on the MySpace platform gives you access to a huge number of users and potential customers. As of this writing, MySpace has more than 130 million monthly active users and is growing every day. Forty-five million of those users have installed apps.To give you an idea of the demographics, 53% of app users are female, with an average age of 18 to 24.That is a very marketable demographic to have at your fingertips.
What Tools Will You Need? Here’s where we finally start to get a little technical. After many long nights spent debugging why that callback wasn’t being fired or where that stupid 401 unauthorized error was coming from, we’ve found some really useful tools that will make your life easier. Let’s repeat that for effect:These tools will make your life easier.
Browsers First, a quick note on browsers. Install Firefox right now. Go to www.firefox.com. In our opinion it’s best to develop on Firefox, get your app running the way you like it, then worry about running it in Internet Explorer (IE) as well. Developing on the Web is still an “IE and everyone else” process. You get an app working in Firefox, and that means it will probably work just fine in Safari, Chrome, and Opera.Then you can bang your head for a few hours (days?) fighting floating divs and onclick handlers in IE.
Firebug Firebug is the main reason Firefox is the Web developer’s browser of choice. Firebug is an incredible tool for all Web developers, and every single one of us should install this
Introduction
Firefox extension. It’s even more useful because all JavaScript “alert” and “confirm” functions are blocked by the OpenSocial container, so it really is the best way to debug. Get it at http://getfirebug.com. Firebug has many useful functions.We won’t go into every last one here, but there are a few that we use every single day. To follow along at home, install the Firebug extension, restart Firefox, navigate to your Web page of choice, then click Tools → Firebug → Open Firebug.The Firebug window will open at the bottom of the browser. Inspect The Inspect feature allows you to view and edit CSS and inline style on any HTML element currently on the page. Click Inspect at the top left of the Firebug window and hover your cursor over a Web page. You’ll see that the various HTML elements become highlighted. Clicking on one will show you the HTML markup of that element in the left pane and its current style in the right pane. You can then add a style or modify the existing style on the fly. This is very useful for building up user interface (UI) elements quickly. Instead of making a change, saving a file, uploading it, and reloading the page in a browser, you can just edit the style very quickly and try different values.When you find what works in Firebug, you can make one edit to your page and it should be good to go. Console The Console tab shows you a couple of useful things. The first useful bit of information is that any and all JavaScript errors are displayed here, with a description of the error along with its file name and line number. The second is that any outgoing Web service requests are shown here. You’ll be able to view the exact outgoing URL, any parameters and headers sent along, and the response from the server. As most MySpace requests are sent as outgoing Web service requests, this is an invaluable debugging tool. You’ll be able to see if the correct parameters were appended to the request, and you won’t have to output the response onto a div on the UI to see if you got the correct response or some sort of error. Script The Script tab is your debugger. You can load any HTML or JavaScript file into the left pane by selecting the file in the drop-down above that pane. You can then set breakpoints at any line with a green line number. In the right pane you can set watches on variables, view the current stack, and manipulate the breakpoints you’ve set. With the Watch window you can interrogate any object by simply typing the variable name into the New Watch Expression bar. Doing so displays the object’s current state and value. But you can also directly manipulate objects. Say you wanted to see what would happen if a Boolean variable were true instead of false; you can set that directly by entering something like varName = true into the Watch Expression bar. Need to figure out what’s causing that JavaScript error? Set a breakpoint on that line and inspect the objects and variables that are causing the issue. Need to figure out exactly what the response looks like from a request to the MySpace servers? Set a
xxv
xxvi
Introduction
Figure I.2
Interrogating the response from MySpace’s servers using Firebug.
breakpoint in your callback function and add the response object to your Watch list (see this in action in Figure I.2). There are countless other things you can do with the Script tab, and Firebug in general, but we can’t discuss them all here.We hope we’ve sold you on its usefulness and that you’ll discover the many great features it has to offer.
Fiddler Fiddler is a free tool that watches all the traffic coming into and out of your computer. With respect to building apps, it is useful mainly if you decide to use the server APIs as opposed to the JavaScript APIs.We’ll discuss what that means later on in the book, but keep this tool in mind when you get there.To give you a little bit of a preview, you’ll be sending requests for data to MySpace, and those requests need to be sent using an authentication scheme. Using the authentication scheme is tricky, but Fiddler will help you see exactly what you’re sending to MySpace and exactly what MySpace is sending back to you. Get it here: www.fiddler2.com. One other thing to note is that Fiddler works natively in Internet Explorer but not Firefox. If you do a little bit of Googling, you can get it up and running in both browsers.
I Building Your First MySpace Application
1
Your First MySpace App
2
Getting Basic MySpace Data
3
Getting Additional MySpace Data
4
Persisting Information
5
Communication and Viral Features
6
Mashups and External Server Communications
7
Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
This page intentionally left blank
1 Your First MySpace App Tarehiscommonly book is primarily a guide to writing OpenSocial applications (or “apps” as they known and as we’ll refer to them throughout this book), but with a focus on the MySpace platform. This first chapter is designed to give you an overview of what to expect from developing with OpenSocial for MySpace.To do that, we’ll start with a “Hello World” app, something simple to introduce you to MySpace and OpenSocial. As we make our way through Chapter 1, we’ll refer to any other chapters that may delve deeper into a particular topic. Feel free to skip to the more detailed chapters if something is not clear. Now, let’s get started.
Creating the App—“Hello World” No programming book is complete without the requisite “Hello World” program. And who are we to flout tradition? We’ll create our version of “Hello World” with the small addition of personalizing it for the current MySpace user—also known as the “Viewer” in OpenSocial parlance. In a few easy steps we’ll sign up for a developer account and build our first app.
Step 1: Sign Up for a Developer Account Before you can do anything, you’ll need a basic MySpace user account. Once you have that, the next step is to sign yourself up as a developer. MySpace recently changed this procedure from a manual approval process that could take a few days to a self-serve one.You simply need to have a valid e-mail account associated with your MySpace account. 1. Log on and go to the MySpace developer site at http://developer.myspace.com. 2. Click the MyApps link.This sends you to the Signup page. 3. Fill out the form and accept the terms of service. Click Next.
4
Chapter 1 Your First MySpace App
4. Solve the “CAPTCHA” puzzle and click Finish.This sends a confirmation message to your e-mail account. 5. When you get the verification e-mail, click the embedded link.You must be currently signed in to MySpace under the user account requesting developer status for the verification to be successful.
Step 2: Create an App Before you can create a “Hello World” app, you must first create your app record.This defines the app Profile, a contact e-mail address, and some meta-information about your app. Here’s how to do it: 1. Log on and go to the MySpace developer site at http://developer.myspace.com. 2. Click the MyApps link. 3. Click the Create New App button. 4. Click the Create On-Site App button. A general information form will appear. 5. Fill in the form with some values for your sample app: Application Title: Hello World E-mail: [email protected] Password: <something_you_will_remember> Terms: The e-mail address used for each app must be unique—a sometimes frustrating requirement that’s covered in more detail in Chapter 12, App Life Cycle. 6. Solve the nigh-impossible word puzzle by entering the correct “CAPTCHA” letters. 7. On the Upload Application XML screen, click Skip This Step since we’ll be doing a simple, direct edit of the app’s details. 8. Complete the form on the next screen.You must set values for all the required fields, but it’s not critical what the values are. Application Description: “Hello World” app Primary Category: 9. Click Save. 10. Click the Edit App Source tab. You are now ready to write code. Go to the next section and collect $200.
Step 3: Enter Your Source Code The first thing we need to do is pick a “surface” on which to build our app. Canvas is typically the easiest surface to work with, so we’ll use that. There are two basic formats for entering app source code: as gadget XML or in one of the surface source editors. Since this is just an introduction, we’re sticking with the
Creating the App—“Hello World”
What are Surfaces? Surfaces—sometimes also referred to as “views”—are a notion OpenSocial inherits from the Google Gadgets spec. A surface denotes a drawing surface on which an app can live. Currently there are three surfaces for MySpace OpenSocial apps: Home: The user’s private Home portal page. This is what is displayed to the user when he or she first logs in to MySpace. In the future, you may want to use this space to show vital app stats, news, or specific user details. Profile: The user’s public Profile page. This is what is displayed on the user’s public page, meaning it’s visible to the world. This surface is a great way to promote and advertise your app. See Chapter 14, Marketing and Monetizing, for more information on this topic. Canvas: A full page usable by the app. This is typically where the user plays and interacts with the app.
surface source editor. Authoring your app as gadget XML brings you a little more power but a lot more complexity. Now, let’s enter the following “Hello World” code and run it:
OpenSocial Hello World
<script type = "text/javascript"> var viewer = null; /** * Initial data request to load Viewer */ function getViewerData(){ var req = opensocial.newDataRequest(); req.add(req.newFetchPersonRequest( opensocial.IdSpec.PersonId.VIEWER), "viewer"); req.send(getDataCallback); } /** * Callback handler to process the response * @param {Object} data */ function getDataCallback(data){ if (data.hadError()) { var err = "Processing Error: " + data.get("viewer").getErrorMessage(); writeMessage(err + " You should install the app"); return; }
5
6
Chapter 1 Your First MySpace App
viewer = data.get("viewer").getData(); if(viewer){ writeMessage("Hello, " + viewer.getDisplayName()); } else{ writeMessage("Wow, you don't exist"); } } /** * Convenience method to write our message to the page * @param {Object} msg */ function writeMessage(msg){ var elem = document.getElementById("message"); elem.innerHTML = msg; } //Register to look for initial data gadgets.util.registerOnLoadHandler(getViewerData);
Once you’ve entered the code, click Save Application Source and you’ll notice a View Development Version link. Click this now. It will open a new window running the app. Any predictions on what you’ll see? Processing Error: Permission denied to all viewer resources. You should install the app. Chances are your prediction was wrong. Privacy is of the utmost concern at MySpace, so only apps that you have installed may see your data.We’ll get more into privacy in Chapter 3, Getting Additional MySpace Data.To make matters even more confusing for you as a developer, it may actually work and show your display name.This happens when a feature called Open Canvas is enabled.This feature has been on and off about ten different times over a year-long period. Just be ready to open up Firebug, a browser debugging tool identified in the book’s Introduction. To solve this permission problem, let’s install the app for your current developer account.
Summary
Installing and Running Your App You must first install your app before you can truly interact with it. From the developer site this is fairly easy: 1. On the MySpace developer site (http://developer.myspace.com) click the MyApps link. 2. Find your target app in the list. Click the Edit Details link for your app. 3. In the upper right corner you will see a Visit Profile link. Click this link to go to the app’s Profile page. 4. Click the Add This App button. Select the Add button in the lightbox dialog. You will then continue to the app’s Canvas page. Now, see the magic. Hello, John Doe The app is running and should now display your publicly visible name (not your full name). Welcome to the brave new world of social applications.You are now one of the elite who will be defining what’s shaping up to be a billion-dollar industry over the next decade.
Summary In this chapter we hit the ground running and covered a lot of it.You may not realize it yet, but we touched on asynchronous network processing, user privacy, app error handling, app life-cycle management, and some of the Byzantine ins and outs of the MySpace Developer Platform.That’s a lot of work to get to “Hello World.” If you’re sweating a little bit, don’t worry. We’ll guide you through this thing in a way that makes sense and helps you start making more apps, and sooner than you thought possible. If you find yourself saying, “What’s this simplistic stuff?” don’t worry either. While this book is designed to build a single app in increasing steps of complexity, it’s also laid out in such a way that you can skip over sections that are not of interest and still get useful, focused information from the chapters that are of interest. Note Code listings and/or code examples for this chapter can be found on our Google Code page under http://opensocialtictactoe.googlecode.com.
7
This page intentionally left blank
2 Getting Basic MySpace Data Ithenn thisexpand chapter we’ll start with a very basic version of the Tic-Tac-Toe application and on it to include examples of how you might fetch a user’s name, address, and gender.We’ll then show you how to use that information in your application. On the way, we’ll cover how MySpace and OpenSocial define an opensocial.Person object and how to use that information to enrich your application. We’ll also get to really play with our Tic-Tac-Toe game.This will be our example app, and over the course of the book we’ll take you from a basic game to a fully functional and feature-rich application. At each step along the way we’ll teach you new methods and programming tricks for accessing and using the OpenSocial API. Code examples will be shown in full and then broken down into segments for further explanation, allowing you to zero in on a specific feature or trouble spot.
The Two Concepts That Every Developer Should Know Before we begin, there are two basic concepts that every MySpace and OpenSocial app developer should know about: n n
In the world of MySpace apps, everyone is either an Owner or a Viewer. The MySpace permission model is a complex and confusing beast that will likely leave you frustrated and angry.
On that note, let’s get started!
Basic Concepts: Owner and Viewer The concept of Owner and Viewer is central to OpenSocial, and it boils down to Owner ⫽ the MySpace user on whose surface the application is running Viewer ⫽ the MySpace user who is currently interacting with the application
10
Chapter 2 Getting Basic MySpace Data
Once you start accessing MySpace data, all the requests you make are made in the context of the Owner and the Viewer. For example, let’s say John has the app installed and Jane is viewing John’s MySpace Profile. John is the Owner and Jane is the Viewer. Alternatively, if John is looking at the app on his own MySpace Profile, he is both the Owner and the Viewer. When you request data, you’ll be asking for information about either the Owner or the Viewer.
Basic Concepts: Permissions for Accessing MySpace Data Not all user data is accessible to apps, and the data that is accessible is subject to a sometimes convoluted permission model. The key idea behind the MySpace permission model (dubbed Open Canvas) is that under certain circumstances an app can access a user’s data even if that user hasn’t installed the app. However, this access can happen only on the app’s Canvas surface and not on the Home or Profile surfaces. Under this model there are essentially three core questions used to determine whether an app can access a piece of data: 1. Does the Viewer have the app installed? 2. Is the Viewer logged in? 3. Has the Viewer blocked the app? Blocking an app prevents any and all access to the Viewer’s data. Figure 2.1 is a quick reference chart for Open Canvas; remember, though, that these permissions can change. So stay on your toes and watch out for permission traps when accessing user data.
Starting Our Tic-Tac-Toe App The first thing we need to do is to get the basic Tic-Tac-Toe app started. Since this app is just scaffolding on which to hang OpenSocial concepts, we’re going to gloss over a lot of the details. At this initial level our app could just as easily be a JavaScript game embedded in a Web page. It is the OpenSocial code that we will add later that will make the game come alive. Note Before we start, you should have the basic version of Tic-Tac-Toe installed as an app on your MySpace developer account. You will find the code on our Google Code page at http://code.google.com/p/opensocialtictactoe/source/browse/trunk/chapter2/chapter2ttt. html. For instructions on how to create, publish, and edit an app, please refer to Chapter 1, Your First MySpace App.
The game is a simple table grid of nine squares. Each square has a click handler to record player moves. Underneath the surface is a state-of-the-art computer AI (i.e., random-move engine) to play against.
Accessing MySpace User Data
Viewer has added the app
Viewer hasn’t added the app
Viewer is logged out
Viewer has blocked the app
Basic Owner data* (Home)
Yes
Yes
Yes
Yes
Basic Owner data* (Profile)
Yes
Yes
Yes
Yes
Basic Owner data* (Canvas)
Yes
Yes
Yes
Yes
Basic Viewer data* (Home)
Yes
No
No
No
Basic Viewer data* (Profile)
Yes
No
No
No
Basic Viewer data* (Canvas)
Yes
Yes
No
No
Owner’s friend list
Yes
Yes
Yes
Yes
Viewer’s friend list
Yes
No
No
No
Owner’s public media**
Yes
Yes
Yes
Yes
Viewer’s public media**
Yes
No
No
No
*Basic person data is defined as the user’s display name, URL, Profile picture, and user ID. ** “Media” includes photos, albums, and videos.
Figure 2.1
MySpace Open Canvas permission model for accessing user data.
The remainder of this chapter will introduce some basic concepts that are central to OpenSocial and apply them to our Tic-Tac-Toe app. At the end of this chapter, our app will display the current player’s name, gender, current location, and Profile image.
Accessing MySpace User Data MySpace Profile data is represented in OpenSocial by the opensocial.Person object. Essentially, opensocial.Person ⫽
a MySpace Profile
The opensocial.Person object contains the methods shown in Table 2.1. In addition, the opensocial.Person object supports a wide range of data fields, such as a user’s ID, Profile picture, or favorite movies.Table 2.2 presents all of the supported person fields along with their return types.
11
12
Chapter 2 Getting Basic MySpace Data
Table 2.1
opensocial.Person Methods*
Method
Purpose
getDisplayName()
Gets a text display name for this person; guaranteed to return a useful string
getField(key, opt_params)
Gets data for this person that is associated with the specified key
getId()
Gets an ID that can be permanently associated with this person
isOwner()
Returns true if this person object represents the owner of the current page
isViewer()
Returns true if this person object represents the currently logged-in user
*Portions of this chart are modifications based on work created and shared by Google (http://code.google.com/ apis/opensocial/docs/0.8/reference/#opensocial.Person) and used according to terms described in the Creative Commons 2.5 Attribution License (http://creativecommons.org/licenses/by/2.5/).
Table 2.2
MySpace opensocial.Person Fields*
MySpace-Supported Person Field
Description
Return Type
opensocial.Person. Field.ABOUT_ME
A general statement about the person
String
opensocial.Person. Field.AGE
Person’s age
Number
opensocial.Person.Field. BODY_TYPE
An object containing the person’s body type; MySpace returns only opensocial.
opensocial. BodyType
BodyType.Field.BUILD
and opensocial. BodyType.Field.HEIGHT opensocial.Person.Field. BOOKS**
Person’s favorite books
Array of strings
opensocial.Person.Field. CHILDREN
Description of the person’s children
String
opensocial.Person.Field. CURRENT_LOCATION
Person’s current location; MySpace returns only
opensocial. Address
opensocial.Address. Field.REGION, opensocial. Address.Field.COUNTRY, and opensocial.Address. Field.POSTAL_CODE opensocial.Person.Field. DATE_OF_BIRTH
Person’s date of birth
Date
opensocial.Person.Field. DRINKER
Person’s drinking status (with the enum’s key referencing
opensocial. Enum
opensocial.Enum.Drinker)
Accessing MySpace User Data
Table 2.2
Continued
MySpace-Supported Person Field
Description
Return Type
opensocial.Person.Field. ETHNICITY
Person’s ethnicity
String
opensocial.Person.Field. GENDER
Person’s gender (with the enum’s key referencing opensocial. Enum.Gender)
opensocial. Enum
opensocial.Person.Field. HAS_APP
Whether or not the person has added the app
Boolean
opensocial.Person.Field. HEROES**
Person’s favorite heroes
Array of strings
opensocial.Person.Field.ID
MySpace user ID
String
opensocial.Person.Field. INTERESTS**
Person’s interests, hobbies, or passions
Array of strings
opensocial.Person.Field. JOBS
Jobs the person has held; MySpace returns only
Array of
opensocial.Organization. Field.NAME and opensocial. Organization.Field.TITLE
opensocial. Organization
opensocial.Person.Field. LOOKING_FOR
Person’s statement about whom or what he or she is looking for or what he or she is interested in meeting people for
opensocial. Enum
opensocial.Person.Field. MOVIES**
Person’s favorite movies
Array of strings
opensocial.Person.Field. MUSIC**
Person’s favorite music
Array of strings
opensocial.Person.Field. NAME
An object containing the person’s name; MySpace returns only
Medium-sized version of the image returned by THUMBNAIL_URL; MySpace returns opensocial.Url. Field.ADDRESS and
opensocial. Url
opensocial.Url.Field. TYPE. This field is not part of
the OpenSocial spec and is a MySpace-specific extension. MyOpenSpace.Person.Field. LARGE_IMAGE
Large version of the image returned by THUMBNAIL_URL; MySpace returns opensocial. Url.Field.ADDRESS and
opensocial. Url
opensocial.Url.Field. TYPE. This field is not part of
the OpenSocial spec and is a MySpace-specific extension. *Portions of this chart are modifications based on work created and shared by Google (http://code.google.com/ apis/opensocial/docs/0.8/reference/#opensocial.Person.Field) and used according to terms described in the Creative Commons 2.5 Attribution License (http://creativecommons.org/licenses/by/2.5/). **The data response for these fields is unstructured and is always an array of one element. For example, a user’s heroes aren’t separated out into array elements; instead, the response is a text blob. This is because these fields on a MySpace Profile are free text.
Accessing MySpace User Data
Accessing Profile Information Using the opensocial.Person Object The first thing we’re going to add to our basic Tic-Tac-Toe app is the ability to get the default fields for our Viewer by fetching the corresponding opensocial.Person object using an opensocial.DataRequest. MySpace defines these default opensocial. Person.Field values for the Person object: n n n n
Profile URL, or opensocial.Person.Field.PROFILE_URL Image URL, or opensocial.Person.Field.THUMBNAIL_URL Display name, or opensocial.Person.Field.NAME User ID, or opensocial.Person.Field.ID
These are the bare-minimum default fields that, as long as you have access to the Person object, will always be returned.
One thing to note is that all requests to the MySpace API are asynchronous, meaning you launch them as “fire and forget” requests.Typically you make a request and specify a callback function; when the server is done processing your request, it passes the results as a parameter into your callback function. One result of this is that you’re probably going to want to put your data requests at the start of the code.That way the MySpace API can start processing your request as soon as possible, and your app won’t be stuck waiting for data to come back.
How Asynchronous JavaScript Requests Work JavaScript is capable of making synchronous or asynchronous data requests back to servers on the current page’s originating domain. Under the hood this is accomplished via the XMLHttpRequest object. This may be alternately referred to as XHR or Ajax. OpenSocial wraps this functionality up in the opensocial.DataRequest object, so you don’t need to know all the gory details. Because this is always done asynchronously with OpenSocial, your code needs to specify a callback function to handle the server response. JavaScript in the browser is inherently single-threaded, which basically means that it can do only one thing at a time. Asynchronous processing allows your app to not “freeze” while it’s waiting for API requests to the MySpace servers to respond. A callback function allows your app to resume processing when it gets the data response.
Let’s take a look at a function that does the following: 1. Instantiates an opensocial.DataRequest object 2. Calls newFetchPersonRequest to create a generic request object, passing in the VIEWER as a parameter 3. Adds the request to the queue by calling add(), passing in a key to name the request
15
16
Chapter 2 Getting Basic MySpace Data
The following code shows this function: /** * Makes the initial call for the Viewer's opensocial.Person * object and requests all fields supported by MySpace */ function getInitialData(){ // Returns an opensocial.DataRequest object var req = opensocial.newDataRequest(); // Create a request object, passing in the Viewer; this will // specify we want the opensocial.Person for the Viewer var viewer_req = req.newFetchPersonRequest(opensocial.IdSpec.PersonId.VIEWER); // Add the request to the queue and give it a key req.add(viewer_req, "v"); // Start processing the requests and specify the callback function req.send(getDataCallback); }
The first line of the function simply returns an opensocial.DataRequest object. The DataRequest object is designed to take care of all your requesting needs.While developing our app, we’ll make heavy use of it. opensocial.IdSpec.PersonId.VIEWER is one of many enums you’ll encounter in OpenSocial. In JavaScript this actually resolves into a string, and on MySpace specifically it is equal to the string VIEWER.You can use the string and enum interchangeably, but we recommend using the enum for two reasons: n
n
If you mistype the enum, your mistake triggers a JavaScript error, but if you mistype the string, it isn’t immediately obvious that something went wrong, so it is harder to debug. The values to which the enums resolve aren’t yet clearly outlined in the OpenSocial spec, so different containers might resolve enums to different strings.
We use opensocial.IdSpec.PersonId.VIEWER here to state that we want to fetch the Viewer’s data. Had we wanted to fetch the Owner’s data, we would have used opensocial.IdSpec.PersonId.OWNER. newFetchPersonRequest takes the ID of the user you want to request as a parameter (in this case the Viewer) and returns an object.That object is actually unnamed by OpenSocial, and the documentation simply refers to it as a “request.”This object is never used directly beyond how it’s used here—passed into DataRequest’s add() function. Essentially, the DataRequest object creates “request” objects that are passed into its own add function. It may seem a little confusing, but remember—you’ll never have to use that mysterious, unnamed “request” object directly, and all OpenSocial API calls follow this general pattern.
Accessing MySpace User Data
The add function takes that request and adds it to a queue that is stored in the DataRequest object. We also give the request a key and, in this case, the string "v". This key is used to identify the response of a specific request; we’ll use this key later when we talk about how to parse responses. The last line tells the container to start processing the request queue by calling send().We pass the name of our callback function into send() as a parameter; when the server is done processing our request, it calls this function while passing in the response. You can see the flow of the OpenSocial data request and response pattern in Figure 2.2.
1. Create an opensocial.DataRequest object
CODE EXAMPLE: var req 5 opensocial.newDataRequest();
Instantiate an opensocial.DataRequest object. The DataRequest object can hold several requests at once which can be sent as a batch to the server.
2. Add the request to the DataRequest object CODE EXAMPLE: req.add(req.newFetchPersonRequest("OWNER"), "your_owner_key"); req.add(req.newFetchPersonRequest("VIEWER");
Here you can also pass in an optional key for each request (the first request) or no key at all (the second request). When you parse the response you’ll use the key to identify each request. If you don’t pass in a key, the MySpace container will add a default key.
3. Send the request to the server and request a callback
CODE EXAMPLE: req.send (callback_func);
All requests to the MySpace servers are asynchronous. That means requests are sent in a “fire and forget” manner. The request is sent to the server and the server creates the response. Once the response is ready, the callback function is executed and the response is passed in.
4. Receive and parse the response from the server CODE EXAMPLE: function callback_func(data){ data.get("your_owner_key").getData(); }
Figure 2.2
The server will return an opensocial.DataResponse object which will, in turn, contain a number of opensocial.ResponseItem objects. Each of these will correspond to a request made in step 2.
Basic request and response pattern for OpenSocial.
17
18
Chapter 2 Getting Basic MySpace Data
Getting More than Just the Default Profile Data Fields beyond the default person fields typically follow a slightly different request pattern and may also require higher permission levels for access. As an example of this, we’ll be requesting our Viewer’s status message (like a headline or shout-out), address, and gender.We’ve modified the function shown previously to now specify that we want this additional data: /** * Makes the initial call for the Viewer's opensocial.Person * object and requests all fields supported by MySpace */ function getInitialData(){ // Returns an opensocial.DataRequest object var req = opensocial.newDataRequest(); // Used to specify what data is returned in the response var fields = [opensocial.Person.Field.CURRENT_LOCATION, opensocial.Person.Field.GENDER, opensocial.Person.Field.STATUS]; // Create an empty object to use for passing in the parameters; // the parameters here are additional person fields from above var opt_params = {}; // Add the list of fields to the parameters opt_params[opensocial.DataRequest.PeopleRequestFields.PROFILE_DETAILS] = fields; // Create a request object, passing in the Viewer; this will // specify we want the opensocial.Person for the Viewer var viewer_req = req.newFetchPersonRequest(opensocial.IdSpec.PersonId.VIEWER,opt_params); // Add the request to the queue and give it a key req.add(viewer_req, "v"); // Start processing the request and specify the callback function req.send(getDataCallback); }
Let’s take a look at what’s changed.The first line is the same; we instantiate an opensocial.DataRequest object. In the next line we create an array of opensocial.Person.Field enums; this list corresponds to the specific Person fields
we want from the API. Next, an empty object is created called opt_params.This is another OpenSocial pattern that you’ll see repeated in many places. Requests typically have some default action, and if you want to modify the default action, you need to supply some parameters to
Accessing MySpace User Data
define what you want to do.You do this by adding certain properties to an object and passing that object into the request. When the MySpace container is processing a request for Viewer or Owner data, it checks the parameters sent in for a property named profileDetail. If it finds such a property, it knows that additional fields were requested. So let’s take a look at that line again: // Add the list of fields to the parameters opt_params[opensocial.DataRequest.PeopleRequestFields.PROFILE_DETAILS] = fields;
On MySpace, opensocial.DataRequest.PeopleRequestFields.PROFILE_DETAILS resolves to the string profileDetail. We’re creating that property and setting it equal to the array of opensocial.Person.Field values we created earlier.The container now knows that additional fields were requested. The rest of the function is the same as before—we create the request, add it to the queue, and send it off.
opensocial.DataResponse and opensocial.ResponseItem (aka, Using MySpace User Data) The various person fields are returned in different ways, but there are essentially three types of responses: n
n
n
A literal, such as a string or a number. For example, opensocial.Person.Field.STATUS returns a string. An object. For example, opensocial.Person.Field.CURRENT_LOCATION returns an opensocial.Address object. An enum. For example, opensocial.Person.Field.GENDER returns an opensocial.Enum enum object.
All responses from the API come in the form of an opensocial.DataResponse object.The DataResponse object contains some number of opensocial.ResponseItem objects; each ResponseItem corresponds to the requests we added to the DataRequest queue. For now, we just have the one request for the Viewer; we’ll look at dealing with multiple requests and ResponseItem objects in the next chapter. To get a particular ResponseItem object from an opensocial.DataResponse, you need to use a key. If you already provided an optional key, use that. Otherwise, you can use the default key created by the MySpace container. Tip: Use an Optional Key It’s recommended that you always use an optional key, as the default keys can be tricky to use. But, if you’re curious, the container-generated keys are found in the MyOpenSpace.RequestType namespace.
19
20
Chapter 2 Getting Basic MySpace Data
/** * The callback method for the initial request * @param {opensocial.DataResponse} data */ function getDataCallback(data){ // "v" was the key created in the request, // data is an object of type opensocial.DataResponse // data.get("v") returns an opensocial.ResponseItem // data.get("v").getData() will return the actual data, // in this case an opensocial.Person object viewer = data.get("v").getData(); // Now let's do something with all that data printPerson(); }
Let’s take a look at this function more closely.The first thing to notice is that it is the function we specified when we called send() in our previous request function. // Start processing the request and specify the callback function req.send(getDataCallback);
The function getDataCallback is executed when the server has processed our request and prepared a response.The first line in getDataCallback takes that DataResponse object and calls the get() function, passing in our key.Above we named our Viewer request "v"; here we use that key to get the ResponseItem that corresponds to that request. The function call data.get("v") returns the ResponseItem object that contains the Viewer’s opensocial.Person object; we get the Person by calling getData(). Now the variable viewer contains an object of type opensocial.Person, which in turn contains the details of the Viewer, specifically the four default fields plus the current location, gender, and status.Table 2.3 shows the methods available for the ResponseItem object. Table 2.3
opensocial.ResponseItem Methods*
Method
Purpose
getData()
Gets the response data
getErrorCode()
If an error was generated by the request, returns the error code
getErrorMessage()
If an error was generated by the request, returns the error message
getOriginalDataRequest()
Returns the original data request
hadError()
Returns true if there was an error in fetching this data from the server
*Portions of this chart are modifications based on work created and shared by Google (http://code.google.com/ apis/opensocial/docs/0.8/reference/#opensocial.ResponseItem) and used according to terms described in the Creative Commons 2.5 Attribution License (http://creativecommons.org/licenses/by/2.5/).
Accessing MySpace User Data
The next section of our code calls the function printPerson(), which is a helper function that we use to parse out the details of our Viewer and output them to the screen. Here we will parse out all three types of responses—enums, strings, and objects: /** * Output the Viewer data onto the surface */ function printPerson(){ if(null !== viewer){ // You can set the src attribute of an // tag directly with THUMBNAIL_URL document.getElementById("profile_image").src = ➥viewer.getField(opensocial.Person.Field.THUMBNAIL_URL); // getDisplayName is a shortcut for // getField(opensocial.Person.Field.NICKNAME) document.getElementById("name").innerHTML = viewer.getDisplayName(); // Get the Viewer's status var status = viewer.getField(opensocial.Person.Field.STATUS); if(status && status.length > 0){ // If the status has been set, append it after the name document.getElementById("name").innerHTML += " \"" + status + "\""; } // Get the opensocial.Address object var location = viewer.getField(opensocial.Person.Field.CURRENT_LOCATION); // The Address object is used similarly to the Person object; both pass a // field into a getField function document.getElementById("location").innerHTML = ➥location.getField(opensocial.Address.Field.REGION); // gender is an opensocial.Enum object var gender = viewer.getField(opensocial.Person.Field.GENDER); // getDisplayValue is defined by the specific container and can and // will differ between containers and is designed for displaying only document.getElementById("gender").innerHTML = gender.getDisplayValue(); // The response of getKey is defined by OpenSocial and therefore // you can compare the result to some known value if(gender.getKey() === opensocial.Enum.Gender.FEMALE){ document.getElementById("myinfo").style.backgroundColor = "#fcf"; // pink }
21
22
Chapter 2 Getting Basic MySpace Data
else{ document.getElementById("myinfo").style.backgroundColor = "#09f"; // blue } } }
Let’s break down this code.To access the data from the viewer variable, which is of type opensocial.Person, we’ll typically need to use the getField() function. After we confirm that viewer isn’t null, we call getField(), pass in the field we want as a parameter, and set that value to the src attribute of an image tag.The field we asked for, opensocial.Person.Field.THUMBNAIL_URL, is returned as a string, so we can just access it directly.That’s really all there is to parsing out a Person field that is returned as a string. For details on the return type for every supported Person field, refer back to Table 2.2 in this chapter. The next line uses a shortcut to retrieve the Viewer’s display name. Another shortcut, getID(), exists to retrieve the Viewer’s ID. All other fields are accessed using getField(), as you’ll see in the next few lines.We could have used getField (opensocial.Person.Field.NICKNAME) to retrieve the display name, but the shortcut is cleaner. Next, we parse out the Viewer’s current location; this field is returned as an opensocial.Address object. You’ll notice that the opensocial.Address object behaves similarly to the opensocial.Person object. Both have fields that describe the types of data you can retrieve, which in turn are fetched using the getField() function. Many of the objects in OpenSocial follow this pattern. Finally, we parse out the gender, which is an opensocial.Enum object.There are two useful types of data in each opensocial.Enum: the display value and the key.The display value is retrieved using getDisplayValue() and is a string that is defined by each container as it sees fit. For example, the display value for the female gender, depending on the container, could be “female” or “It’s a girl!” or even “http://example.com/girl.jpg.” Because these display values can be different from container to container, the key is typically used when making comparisons as the key values are defined in the OpenSocial spec. In our code, we output the display value to the surface but use the key to make a logical comparison; we set the background color to pink if the gender is female (i.e., it’s equal to opensocial.Enum.Gender.FEMALE), or blue otherwise. In the full code listing for the chapter, which you can find at http://code.google.com/ p/opensocialtictactoe/source/browse/#svn/trunk/chapter2, we added a function that goes through each Person field and parses it. If you’re wondering how to parse a particular field that wasn’t covered here, take a look at the code and you should be able to find what you need.
Accessing MySpace User Data
Talking to Your Parent The MySpace site itself runs on the domain myspace.com. All MySpace apps live on a domain different from that, usually msappspace.com, unless it’s an external iframe app (see Chapter 9, External Iframe Aps, for details on those). Because of this difference in domains, the iframe in which your app runs can’t access the outer page. The app is said to be in a “jail domain.” Cross-domain access is a security measure built into all modern browsers, and MySpace makes use of it to prevent any malicious attacks on the myspace.com domain. However, there are a few functions that are exposed to apps that do talk to the parent page. A cross-domain scripting trick called inter-frame procedure call (IFPC) opens a few holes in the jail domain for apps to take advantage of. Let’s take a look at two of the more useful functions. The first is gadgets.window.adjustHeight(new_height). This function asks the parent page to resize the iframe vertically. new_height can take on several types of values. It can be an integer greater than 1; in this case the iframe’s height is set to the specified number in pixels. It can be a fraction greater than 0 and less than or equal to 1. In this case the iframe is resized to try to fill the specified fraction of the page. A new_height of 0.5 would cause the iframe to fill half the available space, for example. Last, new_height can be left out completely; in this case the iframe is resized to fill its contents. You’ll see in the app that we use adjustHeight like this: gadgets.window.adjustHeight();
This causes the iframe to be resized to fill its contents. This is usually the best way to go, as it can be hard to know exactly how big your content is at all times. The other option is to resize your app to a very large number when it loads, such as 5000 pixels. This works well on the Canvas page but not on Home or Profile. The other useful function is gadgets.views.requestNavigateTo(view). This function causes the browser to navigate away from the current page to the specified view. It’s how you take the user from the Home page to your Canvas page, for example. The supported view names are gadgets.views.ViewType.HOME, gadgets.views.ViewType.PROFILE, and gadgets.views.ViewType.CANVAS. Let’s take a look at a handy wrapper function: // This function wraps gadgets.views.requestNavigateTo, // view comes from the enum gadgets.views.ViewType function rNT(view){ // Get the list of views that the container currently supports; // This returns a hash table keyed by view name var supported = gadgets.views.getSupportedViews();
23
24
Chapter 2 Getting Basic MySpace Data
// If the view name passed in is in the supported hash table if(supported[view]){ // Request to navigate to that view gadgets.views.requestNavigateTo(supported[view]); } }
This function takes in the name of the view to navigate to and then gets the list of currently supported views by calling gadgets.views.getSupportedViews(). This function returns a hash table of gadgets.views.View objects, keyed by the names of the views. If the list of supported views contains the specified view, requestNavigateTo is invoked and the View object is passed in. To make use of the rNT function, you might do something like this in the onclick handler of a button: rNT(gadgets.views.ViewType.CANVAS);
That will cause the browser to navigate to the app’s Canvas page.
Error Handling JavaScript errors from apps seem to be an all-too-common occurrence, but they don’t have to be. Most developers don’t expect errors to occur, and so they don’t test or prepare for them as a result, but errors do happen.Your best defense against them is to admit that yes, they do occur, and yes, you need to be ready for them. OpenSocial offers a few functions you can use to deal with errors. In the following example we check for errors before we parse the response for data. If an error is found, the response data isn’t parsed. /** * Check if the response had an error; if it did, log it and if * it was an INTERNAL_ERROR attempt to retry the request * @param {opensocial.DataResponse} data */ function requestHadError(data){ // Return true if data is null or undefined if(!data) return true; // Check the opensocial.DataResponse for the global error flag if(data.hadError()){ // Find the specific opensocial.ResponseItem that had the error var ri = data.get(Tic-Tac-Toe.RequestKeys.VIEWER);
Error Handling
if(ri && ri.hadError()){ // Output the error message log(ri.getErrorMessage()); // Check the error code; an INTERNAL_ERROR can simply mean // network congestion or MySpace server instability if(opensocial.ResponseItem.Error.INTERNAL_ERROR === ri.getErrorCode()){ // Retry the request a certain number of times; make // sure you don't create an infinite loop here! if(retries > 0){ retries--; window.setTimeout(getInitialData, 1000); } } } return true; } return false; }
First, we check if the response is null or undefined, and then check if the global error flag was set in the DataResponse object by calling the object’s hadError() function. This flag is set if any of the requests had an error. Each ResponseItem has an error flag as well, so we’ll need to check each ResponseItem in order to find out which request had the error, if the global flag was set. We do this by calling the ResponseItem’s hadError() function. In this case we had only one request, so there is only one ResponseItem, but in the event of multiple ResponseItem objects we would check each one in turn to determine its error state. Similar to an opensocial.Enum object that has two types of data, one for display and one for logical comparisons, each opensocial.ResponseItem that encounters an error also has two types of data.The error message, accessed by getErrorMessage(), is an arbitrary string that should describe the error and help you debug what happened.The error code, accessed by getErrorCode(), matches up to one of the codes found in the opensocial.ResponseItem.Error enum. Common causes for each type of error code may be found in Table 2.4. You’ll notice that we also attempted to retry the request in the event of an INTERNAL_ERROR.This is because an INTERNAL_ERROR can sometimes occur because of network congestion or other temporary problems.Your request might succeed if you wait a second or so and try again.
25
26
Chapter 2 Getting Basic MySpace Data
Table 2.4
Common Error Code Causes
Common Error Codes
Causes
opensocial.ResponseItem.Error. BAD_REQUEST
The most common cause of this error is that some part of your request didn’t make sense to the container. For example, you passed in a negative value that should be positive, you didn’t supply a required field, or you passed in an invalid ID for a particular request.
opensocial.ResponseItem.Error. FORBIDDEN
This error is returned only when the server responds with a status code of 403 and is not commonly seen.
opensocial.ResponseItem.Error. INTERNAL_ERROR
This is a catchall error that typically occurs when something unknown happens on the MySpace side of the request. As such, it should be intermittent. It can also occur in opensocial.requestPermission (a topic we’ll cover in the section on requesting permissions in Chapter 3) if a permission was requested but no new permission was granted, or if the server returned an unknown status code.
opensocial.ResponseItem.Error. NOT_IMPLEMENTED
This error is returned if you’ve requested some OpenSocial functionality that MySpace doesn’t support. If you receive this error, either the entire function you’re calling isn’t available, or some parameter in a request isn’t supported. An example of the latter would be that you requested an opensocial.Person field that isn’t supported.
opensocial.ResponseItem.Error. UNAUTHORIZED
This error most commonly happens when you’ve requested data to which you don’t have access. It may be possible to use opensocial.requestPermission to ask the user to grant your app access to the data. Again, see Chapter 3 for details.
Handling errors offers two benefits: n
n
There are no embarrassing JavaScript errors when you try to retrieve objects that don’t exist. There’s a possibility that you can recover from your error by retrying the request.
Summary
Warning When retrying your request after an error, be careful you don’t code yourself into an infinite loop! It’s best to keep a retry counter as we do in the code shown previously.
Summary In this chapter we covered the basic flow of an OpenSocial application, focusing on the process of accessing data on the API and OpenSocial’s request/response pattern.We also defined an opensocial.Person object—what it is, how to get it, and what to do with it. This request/response pattern is the foundation of any OpenSocial app.You will use it every time you request data from the server, so we hope you were paying attention. Note Code listings and/or code examples for this chapter and every other chapter can be found on our Google Code page under http://opensocialtictactoe.googlecode.com.
27
This page intentionally left blank
3 Getting Additional MySpace Data I
n the preceding chapter we covered how to fetch and make use of the Person object on the MySpace platform. But there’s a lot more data out there—such as the user’s friend list and the photos the user has uploaded. It’s the use of all this data that will propel your app from a lifeless Web game to a dynamic and social application with viral potential. Most application developers use the friend list in order to spread and share their applications (see Chapter 5, Communication and Viral Features, to learn how) and other endpoints like albums and videos to make their apps more personal and engaging for the user. In this chapter we’ll do just that by covering how to access and use your users’ data to make your app more dynamic and appealing.We’ll show how to retrieve your users’ friends; use paging to sort, sift, and display the data; as well as explore viewing albums, photos, and videos.We’ll also discuss some extras, such as how to check whether a user already has an app installed and how to check for and request permissions from a user. As we start to request more and more sensitive data, such as a user’s photos, permissions will start to come into play more and more. From now on, our Tic-Tac-Toe game is no longer a boring JavaScript applet. It’s a feature-packed app that actually uses the power of the MySpace social network.
How to Fetch a Friend List and Make Use of the Data This section will show you how to fetch a list of friends and then page through that list. You’ll also learn the various ways you can sort and filter a friend list, such as restricting it to only friends who have the app installed.
30
Chapter 3 Getting Additional MySpace Data
In our example Tic-Tac-Toe app, we’ll use the list of friends in two ways: n
n
Provide a list of all the Viewer’s friends so the Viewer can send invitations to those friends to add the app. Provide a list of friends who already have the app installed so the Viewer can challenge those friends to a game of Tic-Tac-Toe.
The actual invitation sending and game play challenges will be covered in Chapter 5, Communication and Viral Features.This section will lay the foundation for those functions by showing you how to pull out the friend list and filter it accordingly.
A Quick JavaScript Lesson Up until now, we’ve kept the code simple by sticking to OpenSocial-specific JavaScript. We did this by separating the Tic-Tac-Toe game logic from the OpenSocial code and keeping everything globally namespaced (meaning the code can be called from anywhere; this is its “scope” in programming terms). Moving forward, our app will become more and more complex. This added complexity requires us to start introducing more advanced JavaScript techniques such as object notation and object-oriented programming. This is not a book on JavaScript programming but on how to build dynamic OpenSocial apps on MySpace. You’ll find, however, that you’ll learn a lot about JavaScript along the way! To this end you’ll start to notice the use of the TTT namespace. This is where we’ll keep all the code for game-specific functionality. For example, in Chapter 2, Getting Basic MySpace Data, we provided a key of "v" when we requested the Viewer data. In this chapter we move all of those keys into an enum: TTT.RequestKeys. So instead of using key "v", we use key TTT.RequestKeys.VIEWER. We concentrate on the OpenSocial code in this book, but feel free to dig into the full code listings found at our Google Code site: http://opensocialtictactoe.googlecode.com.
Getting the Friend List This is the same request/response idea from Chapter 2, Getting Basic MySpace Data, that helps define OpenSocial. In fact, the friend list is just an opensocial.Collection of opensocial.Person objects, but instead of dealing with just one Person as in Chapter 2, we’re dealing with a list of them. There are two major differences between fetching a single Person and fetching a friend list: n n
Call newFetchPeopleRequest instead of newFetchPersonRequest. Instead of passing in an ID (such as Viewer or Owner), pass in an opensocial.IdSpec object.
An IdSpec object is a different way to specify a group of IDs. It requires you to define two parameters: the user ID and the network distance.The user ID specifies the
How to Fetch a Friend List and Make Use of the Data
context for the IdSpec—basically letting MySpace know if you’re asking for a group involving the Owner or the Viewer. MySpace supports the contexts of only Viewer and Owner. In other words, you can’t get a friend list for a random user; it has to be for either the Viewer or the Owner. The network distance defines how far out the connections go. For example, specifying a network distance of 0 would denote just the Owner or the Viewer, and a network distance of 1 would denote the friends of the Owner or the Viewer. At this time MySpace supports a network distance only to a maximum of 1, so you can’t fetch the friends of your friends. Let’s create a fetchFriendList()function to get a basic friend list.This function wraps the newFetchPeopleRequest. Our example is heavily simplified, though, so it isn’t our final version of the function. We’ll expand on it as we see what else you can do with a friend list. We’ll be inserting this function into the code of our Tic-Tac-Toe app found in Chapter 2. For those of you following along at home, place this function anywhere in the script tag in that code: function fetchFriendList(callback){ // Create the IdSpec object var params = {}; params[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER; params[opensocial.IdSpec.Field.NETWORK_DISTANCE] = 1; var idspec = opensocial.newIdSpec(params); // Create the DataRequest object var request = opensocial.newDataRequest(); // Add the request to the queue request.add(request.newFetchPeopleRequest(idspec), TTT.RequestKeys.VIEWER_FRIENDS); // Send it off request.send(callback); }
First, the IdSpec object is created using a user ID of VIEWER and a network distance of 1 to specify that we want the friends of the Viewer. Next, an opensocial.DataRequest object is instantiated and the request is added to the queue with a custom key of TTT.RequestKeys.VIEWER_FRIENDS. Finally, the request is sent off (and the response will find its way back to the specified callback).
Filters and Sorts The first example we showed you just fetches an arbitrary subset of friends of the Viewer. This list is typically returned in the order in which the friendships were made and is not
31
32
Chapter 3 Getting Additional MySpace Data
terribly complex. OpenSocial, however, defines a number of ways to sort and filter fetched lists, making them a lot more useful. Before getting to the code, let’s look at the available options.The MySpace-supported filter types and sorts are shown in Table 3.1. Let’s revisit the fetchFriendList() function, but this time with support for sorts and filters. Again, we’re not quite at the final version of our function, but this code brings us a step closer: function fetchFriendList(callback, filter, sort){ // Create the IdSpec object var params = {}; params[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER; params[opensocial.IdSpec.Field.NETWORK_DISTANCE] = 1; var idspec = opensocial.newIdSpec(params); params = {}; // Add any filters if(filter && filter.length > 0){ params[opensocial.DataRequest.PeopleRequestFields.FILTER] = filter; } // Add any sorts if(sort && sort.length > 0){ params[opensocial.DataRequest.PeopleRequestFields.SORT_ORDER] = sort; } // Create the DataRequest object var request = opensocial.newDataRequest(); // Add the request to the queue request.add(request.newFetchPeopleRequest(idspec), TTT.RequestKeys.VIEWER_FRIENDS); // Send it off request.send(callback); }
You can see that the filters and sorts are specified by adding opensocial. DataRequest.PeopleRequestFields.FILTER and opensocial.DataRequest. PeopleRequestFields.SORT_ORDER to the parameters.
Paging According to the OpenSocial spec, the default number of items returned for a request is 20. That means our fetchFriendList() function returns the first 20 friends in the list. But what if a user has more than 20 friends? Enter paging.
How to Fetch a Friend List and Make Use of the Data
Table 3.1
Supported Friend List Filters and Sorts on MySpace
Filter Type
Details
opensocial.DataRequest.FilterType. ALL
The default filter, essentially no filter
opensocial.DataRequest.FilterType. HAS_APP
Fetches only those friends who have added the app
opensocial.DataRequest.FilterType. TOP_FRIENDS
MySpace allows users to define top or favorite friends; these are the friends who appear at the top of the user’s Friend Space, and this filter fetches only those friends.
Not a filter; allows you to sort a friend list by ID (ascending). Note that this sort is applied after the page is fetched, so it doesn’t take the user’s entire friend list, sort it, and return a particular page. What happens is that a page of friends is taken and that list is sorted. This is not particularly useful.
opensocial.DataRequest.SortOrder.NAME
Another sort; sorts the returned list by nickname (ascending). Like the preceding sort, this one isn’t the most useful as the entire friend list isn’t sorted up front, only the returned page.
Paging is a way to get a large collection of data one chunk at a time.That means that once we have fetched friends 1 to 20, we can then fetch friends 21 to 40, then 41 to 60, and so on. OpenSocial also provides a way to specify which chunk of friends you want for a given request. To demonstrate paging, let’s expand one final time on our fetchFriendList() function: function fetchFriendList(first, max, callback, filter, sort){ // Create the IdSpec object var params = {}; params[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER;
33
34
Chapter 3 Getting Additional MySpace Data
params[opensocial.IdSpec.Field.NETWORK_DISTANCE] = 1; var idspec = opensocial.newIdSpec(params); params = {}; // Set the paging parameters if(first){ params[opensocial.DataRequest.PeopleRequestFields.FIRST] = first; } if(max){ params[opensocial.DataRequest.PeopleRequestFields.MAX] = max; }
// Add any filters if(filter && filter.length > 0){ params[opensocial.DataRequest.PeopleRequestFields.FILTER] = filter; } // Add any sorts if(sort && sort.length > 0){ params[opensocial.DataRequest.PeopleRequestFields.SORT_ORDER] = sort; } // Create the DataRequest object var request = opensocial.newDataRequest(); // Add the request to the queue request.add(request.newFetchPeopleRequest(idspec), TTT.RequestKeys.VIEWER_FRIENDS); // Send it off request.send(callback); }
You’ll notice that opensocial.DataRequest.PeopleRequestFields.FIRST and opensocial.DataRequest.PeopleRequestFields.MAX have been added to the parameters. MAX defines the maximum number of friends to fetch at a time, so if you specified a MAX of 100 (the maximum allowed by MySpace), you’d get the friend list in chunks of 100. FIRST specifies which friend to start fetching from, so if you specified that FIRST is 201 and MAX is 100, you’d get friends 201 to 300. That means you’ll want to keep MAX at a constant value and increment FIRST by that value for each request. Assume we define MAX to be 50; we’d start out with a value of FIRST as 1. If the user requested the next page, we’d change FIRST to 51, and then 101, and so forth.The API provides you with the total number of friends in the list (in addition to the number fetched with the current request) to aid you in paging. In our Tic-Tac-Toe app we fetch 20 friends at a time. You can see a successful friend list request in Figure 3.1.
How to Fetch a Friend List and Make Use of the Data
Figure 3.1
Screen shot of a successful friend list request.
Let’s take a look at the paging object we used: TTT.Lists.Pager = { pager_wrap:"
{0}
", pager_prev:"<span onclick='TTT.Lists.Pager.prev();'><< prev ", pager_next:" <span onclick=’TTT.Lists.Pager.next();’>next >>", page_size:20, getMarkUp:function(){ var current_list = TTT.Lists.getCurrentList(); if(current_list.list.length < this.page_size){ // If the length is less than the amount requested // there is only one page, no need for a pager return ""; } var markup = ""; if(current_list.first > this.page_size){ // If the first index is greater than the size of // a page then there will be a previous page markup += this.pager_prev; } if(current_list.total > current_list.first + current_list.list.length - 1){ // If the total number of records is greater than
35
36
Chapter 3 Getting Additional MySpace Data
// the current index, plus the amount in the current // result set, minus 1, then there's another page. // e.g., first = 1, current set = 20, total = 55 // 1 + 20 - 1 = 20 records fetched, < 55 // e.g., first = 21, current set = 20, total = 55 // 21 + 20 - 1 = 40 records fetched, < 55 // e.g., first = 41, current set = 10, total = 55 // 41 + 15 - 1 = 55 records fetched, == 55 markup += this.pager_next; } if(markup.length > 0){ markup = this.pager_wrap.replace("{0}", markup); } return markup; }, prev:function(){ var current_list = TTT.Lists.getCurrentList(); current_list.first -= this.page_size; fetchFriends(current_list.list_type); }, next:function(){ var current_list = TTT.Lists.getCurrentList(); current_list.first += this.page_size; fetchFriends(current_list.list_type); } };
The Pager object in our Tic-Tac-Toe app does three things: 1. Provides HTML markup for Next and Prev buttons that we can use to allow the user to actually navigate between pages. 2. Stores the value of FIRST that is the current value. 3. Handles what happens when the Next and Prev buttons are clicked. Meanwhile, the getMarkUp() function handles the actual paging algorithm. With this function there are three possible outcomes: 1. The number of records fetched is less than the number requested (MAX). This means there is only one page of data, so no paging is required. 2. The value of FIRST is greater than the size of a page.Therefore, there is at least one previous page, so we need the Prev link. 3. The total number of friends is greater than all the friends fetched so far. Therefore, there are more friends who can be fetched, so a Next link must be added.
How to Fetch a Friend List and Make Use of the Data
Tip: Advanced Technique You’ll see as we progress through this chapter that our Tic-Tac-Toe app presents the user with three lists. One is the entire friend list, one is the list of friends who have the app installed, and the third is the list of photos the user has uploaded to his or her MySpace Profile. The lists all appear fairly similar—each item is in a box that can be clicked, each has a picture, and each has some text underneath. If all these lists are so similar, why not abstract the code so you can use the same functions to generate all three lists? This is exactly what we did. We created a TTT.List object to represent a list, a TTT.Lists object that keeps track of all the current lists that are on the page (note the s at the end), and a TTT.ListTypes enum to differentiate the types of lists. The TTT.List object contains the status of a particular list, including which type it is. It also has references to the actual Document Object Model (DOM) elements that get displayed, the OpenSocial objects that were fetched, paging parameters, what to do when it’s clicked, and the keys used to access the stored OpenSocial data. It’s essentially an interface for what a list should look like. The TTT.Lists object stores the collection of lists and which list is currently selected. We also do something similar for the set of tabs along the top of the app. Check out the TTT.Tab and TTT.Tabs objects for more abstraction fun!
Using the Data We’ve now defined a function that wraps the OpenSocial function newFetchPeopleRequest(). In our Tic-Tac-Toe app we call that function like so: function fetchFriends(list_type){ // Make sure the correct list is set TTT.Lists.setCurrentList(list_type); // Get the current list var current_list = TTT.Lists.getCurrentList(); // Do we need to add any filters? var filter = ""; if(TTT.ListTypes.HASAPP_FRIENDS === list_type){ // Add the HAS_APP filter filter = opensocial.DataRequest.FilterType.HAS_APP; } // Call the fetch friends wrapper fetchFriendList(current_list.first, current_list.max, TTT.Lists.callback, filter); }
First we get a reference to the currently selected list. If it’s the list of friends who have the app installed, we add that filter (TTT.ListTypes.HASAPP_FRIENDS) and send it into the wrapper.
37
38
Chapter 3 Getting Additional MySpace Data
The callback function we passed in, TTT.Lists.callback, will be executed once the friend request has been made and the API responds with some data. Let’s take a look at the callback function’s code and walk through it in detail. You’ll recognize a lot of the OpenSocial aspects from when we discussed callbacks and data parsing in Chapter 2, Getting Basic MySpace Data: TTT.Lists.callback = function(response){ var current_list = TTT.Lists.getCurrentList(); var retryRequest = function(){ current_list.retries--; window.setTimeout(fetchFriends, 250); }; // Check for an error if(!response.hadError()){ var data = null; // Loop through the available keys to // find the actual data in the response for(var key in TTT.RequestKeys){ if(response.get(TTT.RequestKeys[key])){ data = response.get(TTT.RequestKeys[key]).getData(); break; } } // Something bad happened to it if(null === data){ retryRequest(); return; } else{ // Save the actual data to the TTT.List object current_list.list = data.asArray(); // Save the total number of items in the list current_list.total = data.getTotalSize(); // Draw the list TTT.Lists.draw(); } } else{ // If there was a permission issue if(data.getErrorCode() === opensocial.ResponseItem.Error.UNAUTHORIZED){ var reason = "To set a custom background for the game board!"; opensocial.requestPermission(
Fetching Media
[MyOpenSpace.Permission.VIEWER_ACCESS_TO_PUBLIC_VIDEOS_PHOTOS], reason, TTT.Lists.permCallback); } // If there was an error and there are retries left, go for it else if(current_list.retries > 0){ retryRequest(); return; } // Out of retries ... else{ log("Oops, there was an error! Try refreshing the page."); } } };
The callback checks for errors in the request and handles any retry attempts. Each list maintains its own retry count, and a failed request is retried the specified number of times. If there was no error, the response is looped through in an attempt to find out what data it contains.This allows the callback to handle any kind of response. It can currently handle the friend list responses, and as you’ll see later on, it can also handle the response for the photo list. If everything works (and it should), the data is saved to the TTT.List object, along with the total size of the collection for paging purposes, and the filtered list is drawn. Since each friend is represented by an opensocial.Person object, actually parsing out the data for each friend is similar to how it was done in Chapter 2. For details, refer to that chapter, or for a complete code reference for the function TTT.Lists.draw(), check the Chapter 3 sample code found here: http://code.google.com/p/opensocialtictactoe/source/ browse/#svn/trunk/chapter3. Note You can get only the basic person fields for friends; you can’t get extended data. Basic person fields are the friend’s ID, display name, thumbnail URL, and Profile URL.
Fetching Media Media includes a user’s albums, photos, and videos. Dealing with media items is very similar to the other requests we’ve covered so far, including how they’re fetched, how you page through a list, and how the response is parsed.
Photos You can fetch all of the photos your user has uploaded to his or her MySpace Profile, provided the user grants your app proper permissions. Users can limit permissions, but we’ll explore how to deal with that later in the permissions section of this chapter. By default, though, you should have access to all of a user’s public photos. Photos are
39
40
Chapter 3 Getting Additional MySpace Data
defined by the MyOpenSpace.Photo object, which contains the fields shown in Table 3.2.These fields are accessed in the same way as for a Person, for example, photo.getField(MyOpenSpace.Photo.Field.CAPTION). Let’s take a look at the function signature that’s used to fetch photos.You’ll notice that it’s slightly different from how we fetch people: MyOpenSpace.DataRequest.newFetchPhotosRequest = function(id, opt_params)
id is a string used to identify who owns the photos.The value can be either opensocial.IdSpec.PersonId.VIEWER or opensocial.IdSpec.PersonId.OWNER. opt_params is an object that specifies optional parameters. In this case only the paging parameters opensocial.DataRequest.PeopleRequestFields.FIRST and opensocial.DataRequest.PeopleRequestFields.MAX are allowed.
The reason the function signature is in a different format from the other request functions is that it is a MySpace-specific extension.The OpenSocial spec defines ways for containers to extend the core functionality of the OpenSocial spec. In this case MySpace has added several new endpoints. Because it’s an extension, the function exists in the MyOpenSpace namespace and not the opensocial namespace. In our Tic-Tac-Toe app we fetch the list of photos for the user and allow the user to select one. That photo then becomes the background for the Tic-Tac-Toe game board. Let’s take a look at a function that wraps MyOpenSpace.DataRequest.newFetchPhotosRequest: function fetchPhotosList(first, max, callback){ // Add some paging parameters var params = {}; params[opensocial.DataRequest.PeopleRequestFields.FIRST] = first; params[opensocial.DataRequest.PeopleRequestFields.MAX] = max; // Create the DataRequest object var request = opensocial.newDataRequest(); // Add the request request.add(MyOpenSpace.DataRequest.newFetchPhotosRequest( opensocial.IdSpec.PersonId.VIEWER, params), TTT.RequestKeys.VIEWER_PHOTOS); // Send it off request.send(callback); }
Table 3.2
Fields for the MyOpenSpace.Photo Object
Field
Description
PHOTO_ID
The ID number—a photo’s unique identifier
PHOTO_URI
The RESTful API URI that corresponds to this photo
IMAGE_URI
The URL of the photo, which can be used to output the photo to the UI
CAPTION
The photo’s caption
Fetching Media
Figure 3.2
Screen shot of a successful photo request.
We set some paging parameters, create the opensocial.DataRequest object, and send off the request.You’ll notice the other big difference here, besides the namespace change, of executing one of the MySpace extensions.Typically the fetch request, such as newFetchPeopleRequest, is called via an instance of an opensocial.DataRequest object. However, when using this MySpace extension, we call it statically from the MyOpenSpace.DataRequest namespace.You can compare and contrast this function with the fetchFriendsList function to see the slight differences.This code can be seen in action in Figure 3.2.
Albums and Videos The pattern for fetching albums and videos is actually the same as it is for fetching photos. So, if you know how to request photos, you know how to fetch albums and videos. Obviously, the fields and objects differ depending on what you’re fetching, but the basic structure remains the same.That said, we’ve included the fields and function signatures here as a reference for you. Albums Albums are defined by the MyOpenSpace.Album object, which contains the fields outlined in Table 3.3.
41
42
Chapter 3 Getting Additional MySpace Data
Table 3.3
Fields for the MyOpenSpace.Album Object
Field
Description
ALBUM_ID
The ID number—an album’s unique identifier
ALBUM_URI
The value (RESTful URI) that the API recognizes
TITLE
The album’s title
LOCATION
The geographic location where the album’s photographs were taken
DEFAULT_IMAGE
A URL that points to the album’s default image
PRIVACY
A string indicating the album’s privacy settings; set to either “Private” or “Public”
PHOTO_COUNT
An integer indicating the number of photos in an album
PHOTOS_URI
The value (RESTful URI) for accessing the photos within the album
The function signature is as follows: MyOpenSpace.DataRequest.newFetchAlbumsRequest = function(id, opt_params)
id is a string used to identify who owns the albums.The value can be either opensocial.IdSpec.PersonId.VIEWER or opensocial.IdSpec.PersonId.OWNER. opt_params is an object map that specifies optional parameters. In this case only the paging parameters are allowed: opensocial.DataRequest.PeopleRequestFields. FIRST and opensocial.DataRequest.PeopleRequestFields.MAX.
Videos Videos are defined by the MyOpenSpace.Video object, which contains the fields outlined in Table 3.4. The function signature is as follows: MyOpenSpace.DataRequest.newFetchVideosRequest = function(id, opt_params)
id is a string used to identify who owns the videos.The value can be either opensocial.IdSpec.PersonId.VIEWER or opensocial.IdSpec.PersonId.OWNER. opt_params is an object map that specifies optional parameters. In this case, only the paging parameters are allowed: opensocial.DataRequest.PeopleRequestFields. FIRST and opensocial.DataRequest.PeopleRequestFields.MAX.
Since the implementation of both the video and album endpoints is so similar to that of photos, which we’ve already discussed, we leave it to you to experiment with these as you wish.
Using opensocial.requestPermission and opensocial.hasPermission
Table 3.4
Fields for the MyOpenSpace.Video Object
Field
Description
VIDEO_ID
The ID number—a video’s unique identifier
VIDEO_URI
The value (RESTful URI) that the API recognizes
TITLE
The video’s title
DATE_CREATED
The date when the video was created
LAST_UPDATE
The date when the video was last updated
MEDIA_TYPE
An integer indicating the media type of the video
DESCRIPTION
A description of the video
THUMB_URL
A URL pointing to the thumbnail image associated with the video
MEDIA_STATUS
The current status of the video
RUN_TIME
The run time, or length, of the video
TOTAL_VIEWS
The total number of views a video has received
TOTAL_COMMENTS
An integer indicating the number of comments the video has received
TOTAL_RATING
An integer representing the video’s overall rating
TOTAL_VOTES
An integer representing the total number of votes the video has received
COUNTRY
A string indicating the video’s country of origin; typically used to identify culture, not geographic location
LANGUAGE
A string indicating the video’s language
Using opensocial.requestPermission and opensocial.hasPermission to Check a User’s Permission Settings Permission settings are applied to both data and actions. A user can specify permissions on a per-app basis, but there are a couple of global permission settings that you’ll need to either work with or work around (see Chapter 2, Getting Basic MySpace Data, for details). In our Tic-Tac-Toe app we display a list of the user’s photos to the user. But public and private photos each have their own permission setting, so we should first check to see if we have permission before we send out the request. In our app we check whether we at least have access to the user’s public photos: function setBackgroundClicked(){ // Create a permission object var perm = MyOpenSpace.Permission.VIEWER_ACCESS_TO_PUBLIC_VIDEOS_PHOTOS; // Can we get the photos? if(opensocial.hasPermission(perm)){ // Yep fetchPhotos(TTT.ListTypes.SET_BACKGROUND); }
43
44
Chapter 3 Getting Additional MySpace Data
else{ // We can't; let's request the permission to do so var reason = "To set a custom background for the game board!"; opensocial.requestPermission([perm], reason, photosPermCallback); } } function photosPermCallback(response){ // If no new permissions were granted, the // response will have an error if(!response.hadError()){ // No error, so a permission was granted, // try again setBackgroundClicked(); } }
setBackgroundClicked() is called when the appropriate tab is clicked at the top of the app.We’re looking for permission to access public photos, so we call opensocial. hasPermission() to see if we can make this call. If we can, we fetch the photos. If we can’t, we request permission.To do this, we first must provide a reason.The reason is just a string that is displayed to the user.Then we call opensocial.requestPermission(), passing in a callback function. Note that MySpace supports only one permission at a time here, but you’ll need to put the Permission object into an array anyway. Finally, once the user closes the permission window, the callback function is executed. The main piece of information the response provides is whether or not a permission was granted. If no new permissions were granted, hadError()will be true. If a new permission was granted, hadError()will be false. If there was no error, we know the permission was granted and we can continue on to fetch the photo list.Table 3.5 presents all of the supported Person fields along with their return types.
Table 3.5
Permissions You Can Request on MySpace
MySpace-Supported Permission
Description
MyOpenSpace.Permission. DISPLAY_ON_PROFILE
Posts the app on the user’s MySpace Profile
MyOpenSpace.Permission. DISPLAY_ON_HOME
Posts the app on the user’s Home surface
MyOpenSpace.Permission. SEND_UPDATES_TO_FRIENDS
Sends activities updates to the user’s friends; for example, “Susan just won a game of Tic-Tac-Toe”
MyOpenSpace.Permission. SHOW_UPDATES_FROM_FRIENDS
Shows friends’ activities updates about the app; for example, “Your friend Bob just won a game of Tic-Tac-Toe”
Grants access to the app for all public photos and videos; enabled by default
MyOpenSpace.Permission. ACCESS_TO_IDENTITY_ INFORMATION
Grants access to the user’s identity information
MyOpenSpace.Permission. ADD_PHOTOS_TO_ALBUMS
Allows the app to upload photos directly to a user’s photo albums
MyOpenSpace.Permission. UPDATE_MOOD_STATUS
Allows the app to update a user’s status and mood
MyOpenSpace.Permission. CONTACT_INFO
Allows the app to view and use the user’s full contact info (full name, age, sex, location, and birthday)
MyOpenSpace.Permission. FULL_PROFILE_INFO
Allows the app to view the user’s full Profile information, such as favorite books, television shows, etc.
MyOpenSpace.Permission. UPDATE_PROFILE
Allows the app to make updates to the user’s Profile data; no endpoint currently makes use of this, however
MyOpenSpace.Permission. SEND_NOTIFICATIONS
Allows the app to send notifications to other users while the user is interacting with the app; see Chapter 5 for details on notifications
MyOpenSpace.Permission. RECEIVE_NOTIFICATIONS
If set, the user receives notifications from the app; see Chapter 5 for details on notifications
Summary One of the nice things about OpenSocial is that the basic techniques for most tasks are fairly similar. For example, requesting a friend list follows a similar request/response pattern to requesting a photo. Once you have that basic technique down, you can extrapolate and use it again and again. Just be careful and watch out for permissions—they’re one of the biggest causes of unexpected behavior in apps, and they will trip you up if your code doesn’t work to accommodate them. If you’re running into problems with any requests, always check your permissions. Note Code listings and/or code examples for this chapter can be found on our Google Code page under http://opensocialtictactoe.googlecode.com.
45
This page intentionally left blank
4 Persisting Information A s your ambitions for what you want to do with OpenSocial grow, so will your needs. One of the first needs you’ll have is persisting information between sessions.Without the help of an external server, you have two options for storing persistent data: the app data store and cookies. In this chapter we’ll compare different methods of persistent storage and show you how to use them. We’ll grow our sample application and show you how to use persistent storage to allow users to set a custom name for themselves in the game and keep track of their high scores from session to session.
App Data Store The app data store is your premier option for serverless storage.This is the only storage mechanism that is officially supported in OpenSocial.The app data store works as a simple key/value storage mechanism. If you’re not familiar with a key/value store, it looks something like what is shown in Table 4.1. The app data store allows you to get away with not building or setting up an infrastructure for your app, provided your needs are simple.You may store only text and are limited to 20K of total data and 1024 bytes (characters) per key (at the time of this writing). App data is fairly flexible.Your app is allowed only read/write access to the Viewer’s data, but it may read app data from any of the following people/groups: n
n n n
Viewer Owner Viewer friends Owner friends
48
Chapter 4 Persisting Information
Table 4.1
Basic Key/Value Stores
Key
Value
Name
Joe
FavoriteColor
Green
According to the spec, anything placed in the app data store is supposed to be able to evaluate to a valid JSON object. Depending on the version of OpenSocial being used, the data stored will behave slightly differently. If your app is under version 0.7, it can store just about any string value—JSON or not. With the 0.8 and later versions of the OpenSocial spec implementation, the data has to be properly parsable JSON data. There is currently discussion among those who participate in the development of the OpenSocial spec about whether the app data store should be only JSON objects, or JSON-parsable data—like simple strings.Your safest bet is to store data as JSON objects. If you do choose to store JSON data, you need to make use of the gadget JSON parser to evaluate it.The call is simple enough: gadgets.json.parse(your_json_string). In practice, however, the gadget JSON parser is a little temperamental, so you need to get used to formatting your JSON “just so.” Firebug is an invaluable tool in sorting this out (Firebug, along with how to get it, is mentioned in the Introduction). Now, let’s write some code.
Saving and Retrieving Data Saving data involves creating an opensocial.newUpdatePersonAppDataRequest request object and sending it to the server in a data request. Each edit or add of any key in the Viewer’s app data store requires a new request, so the following code is a handy utility method for doing this (the callback parameter is optional, but it’s good practice to include it for error processing): function setAppDataKey(key, value, callback) { var req = opensocial.newDataRequest(); req.add(req.newUpdatePersonAppDataRequest( opensocial.IdSpec.PersonId.VIEWER, key, value), "set_" + key); req.send(callback); };
Data retrieval is a little more involved.You can’t just ignore the callback since this is how the data actually arrives. On the retrieval you also have the option of specifying one, some, or all of the data in the app data store for the current user. For our Tic-Tac-Toe app we’re going to add a little customization by making use of the app data store.We’re going to allow the user to specify a game name that is different from his or her standard MySpace display name.We’re also going to add a placeholder key to the app data store for tracking the user’s high score.
App Data Store
First we’re going to add some markup to the app.This markup includes the following: n
A text box for entering data
n
A button to invoke our action handler A div element for displaying our custom name with ID myinfo
n
Here is the code for the markup; add it as the first element inside the div with an ID of right_column:
Next, we’re going to add some methods for storage and retrieval of the app data keys we’re going to define. In our Tic-Tac-Toe code, which you can find in our Google Code listing (http://opensocialtictactoe.googlecode.com), we’ve defined the two keys as constants: TTT.Keys.NAME and TTT.Keys.SCORE. Here is the code for adding the storage and retrieval methods.We’re using a slight variation on the generalized setAppDataKey function defined earlier. Add it within the first script block on the Canvas surface. function setNameData(){ var elem = document.getElementById("txtName"); if(elem){ setAppDataKey(TTT.Keys.NAME, elem.value, getInitialData); } } function setAppDataKey(key, value, callback) { var req = opensocial.newDataRequest(); req.add(req.newUpdatePersonAppDataRequest( opensocial.IdSpec.PersonId.VIEWER, key, value), "set_" + key); req.send(callback); };
/** * Get the discrete app data and initialize the view */ function getInitialData(){ var req = opensocial.newDataRequest();
49
50
Chapter 4 Persisting Information
var fields = [ TTT.Keys.NAME, TTT.Keys.SCORE ]; var idparams = {}; idparams[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER; idparams[opensocial.IdSpec.Field.NETWORK_DISTANCE] = 0; var id = opensocial.newIdSpec(idparams); req.add(req.newFetchPersonAppDataRequest( id, fields), TTT.RequestKeys.VIEWERDATA); if(viewer == null){ req.add(req.newFetchPersonRequest( opensocial.IdSpec.PersonId.VIEWER), TTT.RequestKeys.VIEWER); } req.send(getDataCallback); } /** * Callback handler for getting app data and Viewer * @param {Object} data */ function getDataCallback(data){ logDataError(data); var mydata = data.get(TTT.RequestKeys.VIEWERDATA).getData(); if(!viewer){ viewer = data.get(TTT.RequestKeys.VIEWER).getData(); } //This happens if app not installed if(!viewer){ log("APP NOT INSTALLED!") return; } else{ var key; var realData = null; for(key in mydata){ realData = mydata[key]; break; } writeNameAndScore( realData[TTT.Keys.NAME], realData[TTT.Keys.SCORE]); } }
App Data Store
/** * Write the name value and score value to the myinfo div * @param {Object} name * @param {Object} score */ function writeNameAndScore(name, score){ var elem = document.getElementById("myinfo"); if(elem){ var str = "
To make it all work, we also plugged in a call to our new function getInitialData() in the initializeGame() function previously defined, and we’re good to go.
Refactoring to Build a Local App Data Store Our code can be significantly cleaned up and made more efficient, however. In most scenarios and for most Ajax-like Web applications, your biggest performance cost is going to be your wire time.This means that the bulk of the latency that users notice comes in the form of waiting for an XMLHttpRequest (XHR) to a remote server to complete. In our case, this means getting all our app data to the client in a single request instead of making a separate request for each data key.
The Readers-Writers Problem, Ajax-Style In computer science there is a well-known threading problem known as the “readerswriters problem.” The crux of the problem is this: Writing isn’t instantaneous. If one thread reads data from a common store while another thread is in the middle of a write operation, the first thread may get corrupt or invalid data.
51
52
Chapter 4 Persisting Information
JavaScript is a single-threaded environment, so we don’t normally consider threading problems when designing client code. The inclusion of asynchronous calls and wire time for reading and writing the data essentially creates a pseudo-multithreaded environment. To solve this problem, we simply make use of a flag value (aka semaphore). This “write” flag indicates to the code that a write operation is occurring. The callback from the write operation clears the flag. The read function in turn must honor that flag and reinvoke itself with a setTimeout call until the write flag has been cleared or the maximum number of retries has been reached. While our sample code does not do this, it is something your production code should do.
The following code shows some convenience methods that can be used for app data retrieval. It provides a method for loading all data into a variable called myLocalAppData. It can be invoked to gather only specified fields or to gather all data fields if the fields parameter is null. We will invoke it with the call loadAppData(null).The parameter for the callback method is optional. It does, however, allow your code to be notified when the data has loaded. var myLocalAppData = {}; //Empty object hash for local app data var appdataKey = "mydata"; //Key to use in DataRequest for retrieval
/** * Loads some or all of the app data into a local store variable * myLocalAppData * @param {Array} fields Array of fields to retrieve, * or null for all fields * @param {Function} loadedCallback Callback handler * to fire when the app data has loaded. */ function loadAppData(fields, loadedCallback){ var req = opensocial.newDataRequest(); if(fields == null){ fields = "*"; //Default to all data } var idparams = {}; idparams[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER; idparams[opensocial.IdSpec.Field.NETWORK_DISTANCE] = 0; var id = opensocial.newIdSpec(idparams); req.add(req.newFetchPersonAppDataRequest (id, fields), appdataKey);
App Data Store
req.send(function(data){ loadAppDataCallback(data, loadedCallback) }); } /** * Callback wrapper method for loadAppData. * This loads all the data into myLocalAppData * and triggers the loadedCallback function, if specified * @param {Object} data * @param {Function} loadedCallback */ function loadAppDataCallback(data, loadedCallback){ logDataError(data); if (data.hadError()) { return; //Exit if nothing found } var mydata = data.get(appdataKey).getData(); //App data has an additional layer of indirection. //We reassign mydata object to the data map object //Circumvent viewer lookup by getting key var ok = false; var vkey; for( vkey in mydata){ mydata = mydata[vkey]; ok = true; break; } if(ok){ for(vkey in mydata){ myLocalAppData[vkey] = mydata[vkey]; } } if(loadedCallback && typeof(loadedCallback) == "function"){ loadedCallback(myLocalAppData); } }
The big “gotcha” in using this technique is when your app is performing multiple reads and writes of data. Then you have to make sure the local store stays synchronized with the remote store or provide some mechanism for publishing unsaved changes in a reliable manner.This is a classic “readers-writers problem” and is easily solved with a semaphore and some retry time-out calls on the read operation (see the “ReaderWriters Problem, Ajax-Style” sidebar for more information).
53
54
Chapter 4 Persisting Information
Now that we have this nifty method, we’ll refactor the storage methods to be a little more generic and configurable.They may be set to use direct app data accessors (V1) or local store accessors (V2). Here is the example code to refactor the storage methods: //App data store code version - V1 (direct) //or V2 (local store) var getAppDataV1 = false; /** * Data initializer to load stored information */ function getInitialData(){ if(getAppDataV1){ getInitialAppDataV1(); } else{ getInitialDataV2(); } } /** * Get the discrete app data and initialize the view */ function getInitialAppDataV1(){ var req = opensocial.newDataRequest(); var fields = [ TTT.Keys.NAME, TTT.Keys.SCORE ]; var idparams = {}; idparams[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER; idparams[opensocial.IdSpec.Field.NETWORK_DISTANCE] = 0; var id = opensocial.newIdSpec(idparams); req.add(req.newFetchPersonAppDataRequest (id, fields), TTT.RequestKeys.VIEWERDATA); if(viewer == null){ req.add(req.newFetchPersonRequest (opensocial.IdSpec.PersonId.VIEWER), TTT.RequestKeys.VIEWER); } req.send(getDataCallbackV1); }
App Data Store
/** * Refactored load of initial data. * This utilizes the local store */ function getInitialDataV2(){ if(viewer == null){ getViewerData(); } loadAppData(null, getDataCallbackV2); } /** * Callback handler for getting app data and Viewer * @param {Object} data */ function getDataCallbackV1(data){ logDataError(data); var mydata = data.get(TTT.RequestKeys.VIEWERDATA).getData(); if(!viewer){ viewer = data.get(TTT.RequestKeys.VIEWER).getData(); } //This happens if app not installed if(!viewer){ log("APP NOT INSTALLED!") return; } else{ var key; var realData = null; for(key in mydata){ realData = mydata[key]; break; } writeNameAndScore( realData[TTT.Keys.NAME], realData[TTT.Keys.SCORE]); } }
/** * Loads the data from a hash object passed in * @param {Object} localAppData */
55
56
Chapter 4 Persisting Information
function getDataCallbackV2(localAppData){ writeNameAndScore(localAppData[TTT.Keys.NAME], localAppData[TTT.Keys.SCORE]); } /** * Get just the Viewer data from a personRequest */ function getViewerData(){ ... }
/** * Callback handler to initialize the Viewer object from a DataRequest * @param {Object} data */ function getViewerCallback(data){ logDataError(data); viewer = data.get(TTT.RequestKeys.VIEWER).getData(); if(!viewer){ log("APP NOT INSTALLED!") return; } }
Cookies Cookies are one of the most convenient quasi-persistent storage mechanisms known in the Web today.There is no other mechanism so convenient to read and write on both the server and the client. Cookies are also a no-no when it comes to apps. Back in the old days of the Web, circa 1997, everyone used to believe that cookies were the path to ultimate damnation and loss of privacy. Criminals were going to steal your credit card number, or the G-men were going to track your every move on the Web, loading your FBI file with a list of whoopee cushion suppliers and midget porn sites you had visited. Well, it turns out the criminals did steal your credit card number, but by hacking the database, not your cookies. And someone is tracking your every move, but it’s not the government. And your Web usage is only going into your ad-targeting profile. None of this has anything to do with why you shouldn’t use cookies.
Cookies
Why You Shouldn’t Use Cookies The single biggest issue with cookies is this: Your app is sharing a domain, and hence a cookie collection, with every other app in the MySpace universe. Why does this matter? Here are a couple of reasons: n
n
Reason #1: Your cookie is prone to being purged or overwritten by cookies from other apps. Reason #2: Your cookies may be read by any other app the user has installed or viewed.
Most modern browsers have a per-domain cookie limit of 20. It’s not uncommon for a user to have more than 20 apps installed. It’s also not uncommon for developers to be greedy or wasteful in their usage of cookies, should they go down that path. A single app could push your otherwise persistent data out of the browser’s cookie collection. It should also go without saying that you should never store sensitive data in a cookie. But let’s say it anyway: Never store sensitive data in a cookie. Especially on MySpace. Every single app has access to every single app-created cookie, so there is no privacy.The app data store, on the other hand, is nominally locked to the app. It’s also not secure enough to store any sensitive data, however, since data for any user is readable by your app. To illustrate all this, we’ll modify our Tic-Tac-Toe app to use a cookie instead of the app data store to store the player’s game name.Then we’ll write another app that hijacks our cookies.We have to modify our app so it can switch between app data store and cookie storage using a flag value.Then we’ll modify both the initialize and the save methods to check that flag to determine whether they should use app data storage or cookie storage. Here is how we add cookie storage to the app: var useCookieStorage = true; var getAppDataV1 = true; /** * Data initializer to load stored information */ function getInitialData(){ if(useCookieStorage){ writeNameAndScore(readCookie(TTT.Keys.NAME), readCookie(TTT.Keys.SCORE)); } else if(getAppDataV1){ getInitialAppDataV1(); }
57
58
Chapter 4 Persisting Information
else{ getInitialDataV2(); } }
/** * Set a new persistent cookie (or overwrite existing) * @param {Object} key * @param {Object} value */ function setCookie(key, value){ var cook = key + '=' + value + '; expires=1 Jan 2050; path=/'; document.cookie=cook; } /** * Read the given cookie * @param {Object} key */ function readCookie(key){ var cookies = document.cookie; var keyName = key + '='; var pos = cookies.indexOf(keyName); if(pos > -1){ pos += keyName.length; var endPos = cookies.indexOf(';', pos); if(-1 == endPos){ endPos = cookies.length; } return cookies.substring(pos, endPos); } return null; } /** * Sets the persistent value for the game in the * chosen data store * @param {String} key * @param {String} value */ function setGameData(key, value){ if(useCookieStorage){ setCookie(key, value); getInitialData(); }
Cookies
else{ setAppDataKey(key, value); } }
As you can see, the cookie mechanism is much more direct. It feels comfortable, easy, and as if it’s the right solution. It’s not.To show you why, we’ll build our Cookie Jacker app.
Building the Cookie Jacker App The Cookie Jacker is a fairly simple app for manipulating cookies. It has the facilities to set a specific cookie, display all current cookies for the site, set a bunch of cookies, and delete all your site-specific cookies. And all in just 170 lines of code, most of that being display logic. If you don’t want to type it all in, you can download it from our Google Code site (http://opensocialtictactoe.googlecode.com). If you just want to use it, you can install the app at www.myspace.com/cookiejacker. We’ll list just the methods of interest here.The following code should be fairly straightforward—there are set and read methods for specific cookies.There is one method (writeAllCookies) that steps through all cookies currently available and displays them in a table.There is a method to loop and add 200 cookies to your browser (setLotsOfCookies), and there is a method to clear all the cookies (deleteAllCookies). Here are the Cookie Jacker’s mere 170 lines of code:
Cookie Jacker
<script type="text/javascript"> function setNewCookie(){ var key = document.getElementById('txtCookieName').value; var val = document.getElementById('txtCookieValue').value; if(key.length == 0) return; setCookie(key, val); writeAllCookies(); }
function setLotsOfCookies(){ var i; for(i=0; i < 200; i++){ setCookie("testCookie_" + i, i); } writeAllCookies(); }
59
60
Chapter 4 Persisting Information
function setCookie(key, value){ var cook = key + '=' + value + '; expires=1 Jan 2050; path=/'; document.cookie=cook; } function readCookie(key){ var cookies = document.cookie; var keyName = key + '='; var pos = cookies.indexOf(keyName); if(pos > -1){ pos += keyName.length; var endPos = cookies.indexOf(';', pos); if(-1 == endPos){ endPos = cookies.length; } return cookies.substring(pos, endPos); } return null; } /** * First round design to load app data and Viewer */ function getInitialData(){ writeAllCookies(); }
function deleteAllCookies(){ var cookies = document.cookie.split(';'); var expires = ";expires=1 Jan 1950;path=/"; var kparts; for(var i=0; i < cookies.length; i++){ kparts = cookies[i].split('='); document.cookie = kparts[0] + '=' + expires; } writeAllCookies(); }
function writeAllCookies(){ var tbl = document.getElementById('cookieTable'); var body = tbl.getElementsByTagName("tbody")[0];
Cookies
while(body.childNodes.length > 0){ body.removeChild(body.firstChild); } var cookies = document.cookie.split(';'); if(cookies.length == 1 && cookies[0] == "") return; var kparts; for(var i=0; i < cookies.length; i++){ kparts = cookies[i].split('='); addTableRow(body, kparts[0], kparts[1]); } var ht = Math.min((25 * (1 + body.childNodes.length)), 400); body.style.height=ht+"px"; }
function addTableRow(table, cell1, cell2){ var row = document.createElement("tr"); row.appendChild(document.createElement("td")); row.appendChild(document.createElement("td")); row.childNodes[0].innerHTML = cell1; row.childNodes[1].innerHTML = cell2; table.appendChild(row); }
function log(message){ var errFeed = document.getElementById("messages"); if(errFeed){ errFeed.innerHTML += message + " "; } }
//Register to look for initial data gadgets.util.registerOnLoadHandler(getInitialData);
Add this code as a new app on the developer site (the full listing is on our Chapter 4 Google Code listing, http://code.google.com/p/opensocialtictactoe/source/browse/ #svn/trunk/chapter4). Now go back to the Tic-Tac-Toe app and change the flag to use cookie storage: var useCookieStorage = true;
Save and run this app now. Go ahead and set a name for yourself (we used “Melba”). Now go back to the Cookie Jacker app and view (or reload).What do you see? There it is—my name is “Melba,” as shown in Figure 4.1. Look closely and you’ll see a couple of other surprises—some other app (or apps) is placing what looks to be a tracking cookie on the browser as well. Take some time to play around with the Cookie Jacker app. If you push the Set Lots of Cookies button, you will see all your current cookies go away and be replaced with new cookies.This is actually an interesting way to see how many cookies your browser will store for a domain (Firefox stores 50).
Figure 4.1
Cookie Jacker app exposing cookies.
63
64
Chapter 4 Persisting Information
I hope the point has been made: Cookies are not a viable persistent storage mechanism for apps (though they can be used for transitory storage across surface navigation or page reloads). Note Apps (and any Web pages, for that matter) store their cookies under the context of the domain from whence they originated. All our examples show MySpace-hosted apps. Iframe apps that are hosted on a third-party server do not suffer from the same cookie vulnerabilities as are outlined in this chapter.
Third-Party Database Storage In recent years there has been a blossoming of third-party storage and server services available on the Internet (collectively referred to as “cloud services”).We’re not going to implement the use of third-party storage for this app, but it warrants mention. In ancient times (circa 1996), one could get a persistent IP address with a cable modem and set up a private server with a database.Then came dynamic IP addresses, which could be handled by a dynamic DNS service. Over time, database-hosting costs came down from the stratosphere to where using an ISP for your data and app services made more sense than personal hosting. Costs came down because of many factors: the rise of open-source databases like MySQL, the crash in per-megabyte storage costs, and a general increase in the knowledge and ease of hosting databases. A database is now a standard add-on for all but the most vanilla, Kmart-blue-light-special hosting packages. An ISP-provided dedicated database solution has many advantages. Most of these advantages involve the level of control you have over the underlying data structures. However, they have one fatal flaw that can be summed up in a single word: scale.We cover scaling in more depth in Chapter 13, Performance, Scaling, and Security, but we mention it here because it can have a dramatic effect on your app’s design. A single database server can easily handle tens of thousands of records.The heavier-weight databases might manage one to two million records on a dedicated server without too much trouble, provided the data architecture is reasonably well designed.The problem is that social network scale goes into the hundreds of millions. As a solution, a handful of cloud-computing (which used to be called “gridcomputing”) solutions are coming online.The most attractive aspects of these types of cloud services are that you pay only a sliding usage cost, and they handle both the infrastructure and the 3:00 a.m. calls. For service providers, you have a number of options. Amazon seems to have the most complete set of cloud storage products, from key/object storage to queryable data storage, and at a cost much lower than that of buying a rack system,T3, and full-time network admin. Google has a hosting service known as App Engine that has become very popular among app developers. Microsoft also has cloud services in beta for both data (SSDS, or SQL Server Data Services) and computing (Azure Platform). Aptana
Summary
(maker of the IDE being used in the course of this book) is also pushing a service (Aptana Cloud), and the word is that Sun Microsystems also has plans in the works. Expect this space to become even more crowded and competitive in the very near term. So to sum up, MySpace has over 150 million users as of this writing.The most popular apps have over ten million users. Don’t let database scaling limit your success.
Summary There are two persistence mechanisms available to apps for which you do not want to use an external storage mechanism: the app data store and cookies. Both provide simple key/value storage. Cookies, however, are vulnerable to being manipulated by other apps and are suited only for very transitory storage. Meanwhile, app data provides a private store on a per-app and per-user basis but should not be considered secure. External cloud services may be used for more complex and data-intensive storage needs. Ideally, you should pick a service that can scale to multiple servers, as single-server storage will be inadequate should your app gain widespread adoption. Note Code listings and/or code examples for this chapter can be found on our Google Code page under http://opensocialtictactoe.googlecode.com.
65
This page intentionally left blank
5 Communication and Viral Features M
ost successful apps are spread through a few users; these users install the app, and then they tell their friends about it, and they tell their friends, and so on and so on. MySpace and OpenSocial have four built-in viral features designed to speed up and improve that process: 1. 2. 3. 4.
These four features make up the communication components that will help you spread your app virally. If you’d like to learn more about actually marketing and growing an app, see Chapter 14, Marketing and Monetizing. In this chapter we’ll cover each of these four viral features in detail, using examples built into our Tic-Tac-Toe application.
Using opensocial.requestShareApp to Spread Your App to Other Users Let’s start by looking at how opensocial.requestShareApp works.The requestShareApp function is essentially a request to install the app that is sent from one MySpace user to another. For example: 1. Susan has the Tic-Tac-Toe app installed. 2. The app provides the requestShareApp functionality via a button on the Canvas surface. 3. Susan invokes requestShareApp (see Figure 5.1) to invite Matt to also install the app and clicks Send.
68
Chapter 5 Communication and Viral Features
Figure 5.1
Figure 5.2
Screen shot of the requestShareApp function prompting a user to send an add request to friends.
Screen shot of the requestShareApp function inviting another user to add the app.
4. Matt logs in and sees a New App Invite in the Updates module on his Home page and clicks it, which takes him to the app invite. 5. Matt sees each app invite under Notifications S App Invites in his Mail Center (see Figure 5.2).
Using opensocial.requestShareApp to Spread Your App to Other Users
6. Matt clicks the Add Tic-Tac-Toe button and the Add App install modal dialog pops up, as shown in Figure 5.3. 7. Matt clicks Add and is taken to the app’s Canvas page, as shown in Figure 5.4.
Figure 5.3
Screen shot of the requestShareApp function prompting the user to install the app.
Figure 5.4
The user is transported to the app’s Canvas page.
69
70
Chapter 5 Communication and Viral Features
Communication Policies and Rules MySpace has several policies concerning communications or messaging in apps. The first policy is that most message types (requestShareApp, requestCreateActivity, requestSendMessage, etc.) require the user to agree to send the message. This is accomplished through the use of a modal pop-up dialog. The second policy is that the modal dialog can be shown only while on the app’s Canvas surface. Because of this requirement, all messaging happens on the Canvas surface. There is one exception to this rule, and that is app notifications. App notifications do not require the pop-up modal dialog and are not restricted to the Canvas. We’ll go into notifications in detail later in the chapter. The third policy is that there can be no reward for a user sending out a message. This means you can’t give players ten free carrots to feed their pet rabbit if they send out a bulletin. You’ll have to be a little slyer to get that bulletin sent!
Defining requestShareApp opensocial.requestShareApp has the following signature: opensocial.requestShareApp = function(recipients, reason, opt_callback,
opt_params)
The recipients parameter can be either an array of strings or a single string.This means that you can send an invite to multiple recipients, something that isn’t possible with requestSendMessage on the MySpace platform. MySpace actually allows integer IDs like 6221, integer IDs as strings, like "6221", or IDs in the form "myspace.com:6221". Any combination of these IDs can be placed inside an array or passed individually. The maximum number of recipients, as of this writing, is 20. Once the modal pop-up loads, the user also has another chance to manipulate the recipient list.The To: box in the pop-up actually has autocomplete functionality; after clicking inside the box, the user can type the name (display name, user name, or real name) of another recipient and select that user from the drop-down (see Figure 5.5).The user can also delete any current recipient. The reason object is an object of type opensocial.Message and is defined by OpenSocial as “the reason why the user wants the gadget to share itself.”The reason contains the content of the message that appears in the recipient’s Mail Center page.The intention is to make the app sound fun or interesting so the recipient will install it.The sender also sees a preview of the message before it’s sent, so it should be something the sender approves. The reason parameter does not allow HTML markup, but it does provide a simple templating system.The message can be up to 150 characters long, but that limit doesn’t include the following template variables: n
[sender]: Turns into a link to the sender’s Profile; the text is the sender’s display name.
n
[app]: Turns into a link to the app’s Profile; the text is the app’s name.
n
[recipient]: Turns into a link to the recipient’s Profile; the text is the recipient’s
display name.
Using opensocial.requestShareApp to Spread Your App to Other Users
Figure 5.5
As the user starts typing, a drop-down list of friends appears.
For example, your reason might read “[sender] wants you, [recipient], to install [app]” or “Hey, you, install this: [app].”The first example might render to something like this: “Susan wants you, Matt, to install Tic-Tac-Toe.” The last two parameters, opt_callback and opt_params, are optional. opt_ callback specifies the function to call once the request has been processed and the user has closed the Add App pop-up. opt_params is used to provide the function with additional parameters; however, MySpace doesn’t support any of the additional parameters, so opt_params can be safely ignored.
Writing the requestShareApp Code Now that we’ve defined our requestShareApp function, let’s take it for a test drive. The following code wraps the requestShareApp functionality so that you can use it throughout your application: function requestShareAppWrapper(id, body, callback){ // Create the message object var reason = opensocial.newMessage(body); // Initiate the share app modal opensocial.requestShareApp(id, reason, callback); }
This simple code starts out by creating an opensocial.Message object to use as our reason parameter. Only the body field of the message object is used, though; all other fields are ignored. Next, the recipient IDs, along with the callback function, are used to initiate the opensocial.requestShareApp function.
71
72
Chapter 5 Communication and Viral Features
Calling requestShareApp But how will we use this function throughout our Tic-Tac-Toe application? How will it be called? Typically, the IDs of the recipients are found by running a newFetchPeopleRequest API request and presenting your user with a list of the friends whom they can then invite to the app. Once the user selects some friends, their IDs will be passed into the requestShareApp function. We showed how to fetch a list of friends in Chapter 3, Getting Additional MySpace Data, so let’s build on that. Assume that each div tag containing a friend’s information has an onclick event handler.When invoked, that handler takes the ID of the clicked friend and adds it to an array. In the app we have a button with the following code: function rsaInviteButtonClicked(){ // TTT.Lists.itemClicked contains the list of friends // that were clicked if(TTT.Lists.itemClicked.length > 0){ // Create some catchy body text var body = "Hi [recipient]... [sender] wants you to come"; body += "play Tic-Tac-Toe, add [app] now!"; // Then pass the values into our wrapper requestShareAppWrapper(TTT.Lists.itemClicked, body, rsaWrapperCallback); } }
First, we check to see that at least one friend has been picked.We then define the actual reason text and pass it, along with the array of IDs, into the requestShareAppWrapper function we created earlier. But what about the callback? What’s it for?
The requestShareApp Callback The callback function is invoked either after the user has closed the modal pop-up prompt or if an error occurred before the pop-up prompt could be displayed. Once invoked, the callback is passed an opensocial.ResponseItem as a parameter.This ResponseItem behaves similarly to the way it did in API callbacks (see Chapter 2, Getting Basic MySpace Data, to learn how basic callbacks work).The difference is that the API callbacks were passed an opensocial.DataResponse object that contained opensocial.ResponseItem objects. Here we just have the opensocial.ResponseItem object as the response, so you can directly access things like hadError(), getErrorCode(), or getData(). Once the pop-up is shown, there are four possible outcomes: 1. The user accepts the request to share the app. In this case the value of getData() is the number 1. 2. The user cancels the request; in this case the response is 0.
Using opensocial.requestShareApp to Spread Your App to Other Users
3. Some error occurred with the page that loads inside the pop-up; in this case the response is the number ⫺1. 4. There was an error with your request syntax.This causes the hadError() function to return true. Details of this error are found using getErrorCode() and getErrorMessage(). One thing to remember is that once the pop-up has been shown, the user can adjust the recipients. So you may think you know who the recipients are going to be, but the user can completely alter that list.To get around that problem, MySpace pushes a couple of additional bits of data down into the callback function. In the following function we make use of this data; let’s take a look at how it works: function rsaWrapperCallback(response){ // Get the first clicked friend var current_list = TTT.Lists.getCurrentList(); var clicked; // Check for some kind of bad response, and show an error message if(!response || response.hadError() || -1 === response.getData()){ log("Oops, there was an error, try refreshing the page!"); } else{ // Revert all the friend boxes to a white background for(var i = 0; i < TTT.Lists.itemClicked.length; i++){ clicked = current_list.list_dom[TTT.Lists.itemClicked[i]]; if(clicked) clicked.style.backgroundColor = "white"; } // Reset the list TTT.Lists.itemClicked = []; // Was the pop-up not canceled? // i.e., were the invites sent? if(0 !== response.getData()){ // Array of IDs of recipients who were sent invites, // and those for whom the invite failed var success = response.responseValues.success; var failure = response.responseValues.failure; // Update the UI for the successes for(var i = 0; i < success.length; i++){ clicked = current_list.list_dom["myspace.com:" + success[i]]; if(clicked) clicked.style.backgroundColor = "green"; }
73
74
Chapter 5 Communication and Viral Features
// Update the UI for the failures for(var i = 0; i < failure.length; i++){ clicked = current_list.list_dom["myspace.com:" + failure[i]]; if(clicked) clicked.style.backgroundColor = "red"; } } } }
So, there is a lot going on here.The callback for requestShareApp is especially important.The first thing that really happens is that the various types of errors are trapped, and an error message is displayed if one is found. If there was no error, we cycle through the list of currently selected users, revert the background of the corresponding div tags to white, and reset the list.There are now two possibilities: the user either canceled the pop-up or hit Send. If the pop-up was canceled, there’s no more work to do. If the pop-up wasn’t canceled, the user must have hit the Send button, so we need to figure out who the actual recipients were.We do this by accessing the responseValues object in the ResponseItem that’s passed back to us. responseValues contains two arrays: One, success, contains the list of users who were successfully sent an invite.The other, failure, contains the list of users who had an error occur when the invite was sent. Both arrays contain the integer IDs of the specified users. There are a couple of things to note when accessing the success and failure arrays. First, users who already have the app installed won’t get the invite, but neither will they show up in either array. Second, the IDs in the array are integers, so they don’t exactly match the IDs that are sent by the API (in the format "myspace.com:6221"). Back to the function at hand.We extract the success and failure arrays and loop through them.We turn the background color of all the successful recipients to green, and that of all the failures to red.You’ll notice that we have to append "myspace.com:" to the beginning of each ID so it matches up with the IDs we fetched from the API. It may be useful to add a More button to your app, something like “Select Random 20” that will randomly select 20 of the user’s friends.This will help encourage users to share the app with a larger group of friends; turning 20 clicks into one click makes it much easier for your users.
Using opensocial.requestSendMessage to Send Messages and Communications Let’s examine how opensocial.requestSendMessage works.The requestSendMessage function provides a number of ways for apps to communicate with users. MySpace supports the following message types: n
n
Send Message: a private one-to-one message that will arrive in the user’s Mail Center in-box Comment: a public one-to-one message that will appear on the user’s Profile page in the Comments section
Using opensocial.requestSendMessage to Send Messages and Communications
n
n n
Bulletin: a one-to-many message that will appear in the Bulletins module of each of the user’s friends’ Home pages Blog: a one-to-many message that will appear on a MySpace user’s Blog Profile: not a message per se; allows an app to edit/update the user’s Profile.The following Profile sections can be edited and are chosen by the user in a dropdown menu: About Me I’d Like to Meet Interests Edit Artist Profile Movies Television Books Heroes
Defining requestSendMessage opensocial.requestSendMessage has the following signature: opensocial.requestSendMessage = function(recipients, message, opt_callback, opt_params)
You probably noticed that the signature for opensocial.requestSendMessage is very similar to that of opensocial.requestShareApp.The big difference is that you’re allowed to define only one recipient ID at a time; the container will reject an array of IDs. Similar to requestShareApp, however, are opt_callback, which is the function that’s invoked when the pop-up modal has closed, and opt_params, which is unused. Note Some targets don’t require a recipient; these are Bulletin, Profile, and Blog. Any recipient value passed in for these targets will be ignored.
The message parameter is an object of type opensocial.Message.The supported content of the message depends on the message type; here’s a quick breakdown: n
Types that support a message title are Send Message, Comment, Bulletin, and Blog. The title doesn’t support any HTML markup.
n
All of the message types support a message body. All support some HTML markup in the body.Tags like
Since bulletins don’t use any template messaging, we have to generate all the links ourselves.To do this, we make use of the instance of a MyOpenSpace.Application object for this app.When an app is rendered, the MySpace platform pushes down a script-accessible representation of the app’s information.This object may be accessed from the environment with the call opensocial.getEnvironment().currentApplication
Specific data that may be found in this object includes: 1. App ID, accessed via the field enum MyOpenSpace.Application.Field.ID 2. Name, accessed via the field enum MyOpenSpace.Application.Field.Name 3. Profile URL, accessed via the field enum MyOpenSpace.Application.Field. PROFILE_URL
4. Install URL, which for right now is the same as the Profile URL (which is where the app is installed from), accessed via the field enum MyOpenSpace. Application.Field.INSTALL_URL
5. Canvas URL, accessed via the field enum MyOpenSpace.Application. Field.CANVAS_URL
6. The 64⫻64 icon URL, accessed via the field enum MyOpenSpace.Application. Field.ICON_LARGE
7. The 16⫻16 icon URL, accessed via the field enum MyOpenSpace.Application. Field.ICON_SMALL
The Application object behaves just like the Person object—data is accessed through the getField function. For example, to access the app’s name in your script code, you would make the following call: opensocial.getEnvironment().currentApplication.getField( MyOpenSpace.Application.Field.NAME);
Here we use the app’s Profile URL and the large icon to generate the bulletin message. Some of the supported HTML tags are also used for the body.Then the wrapper function is called and voilà! We’ve sent our first message! Bulletins are great for getting the word out to a large audience, but they lack a personal touch. In our Tic-Tac-Toe app, one user can challenge another user to a game.
77
78
Chapter 5 Communication and Viral Features
When this challenge is initiated, we send out a message from the challenger to the challenged.The code for that looks like this: function rsmMessage(){ var current_list = TTT.Lists.getCurrentList(); var id = this.list_index; TTT.Lists.itemClicked = id; var this_app = opensocial.getEnvironment().currentApplication; var profile_link = this_app.getField(MyOpenSpace.Application.Field.PROFILE_URL); var image_link = this_app.getField(MyOpenSpace.Application.Field.ICON_LARGE); var subject = "I challenge you to a TTT duel!"; var name = ""; for(var i = 0; i < current_list.list.length; i++){ if(current_list.list[i].getId() == id){ name = current_list.list[i].getDisplayName(); break; } } // If the name isn't empty, add a space before the name name = ("" === name) ? name : " " + name; var body = "Hey" + name + "! You've been challenged to "; body += "a game of Tic-Tac-Toe, click "; body += "here to accept the challenge!
This code is fairly similar to the bulletins code.We use the MyOpenSpace. Application object to generate a message and then invoke the wrapper.There is also some app-specific code in this function that determines the ID and name of the challenged user; this ID is sent into the wrapper while the name is used to personalize the message. Adding a person’s name to a message is an old trick that helps the message seem a little less like a form letter.
Callback in requestSendMessage The callback for requestSendMessage works the same as requestShareApp. An error could be generated before the modal dialog is shown; otherwise the value 1, 0, or ⫺1 is returned.
Getting Your App Listed on the Friend Updates
Let’s take a look at one quick example: function rsmMessageCallback(response){ var div = TTT.Tabs.getCurrentContainer().firstChild; if(response && !response.hadError()){ if(0 === response.getData()){ div.innerHTML += "challenge cancelled..."; } else if(1 === response.getData()){ div.innerHTML = "challenge sent!"; } } else{ log("Oops, there was an error, try refreshing the page!"); } }
First we check for an error. If one was found, we display an error message asking the user to refresh the page. Most errors that occur with the messaging system are intermittent and can be fixed with a refresh. If there was no error, a simple message is displayed reaffirming the user’s action.
Getting Your App Listed on the Friend Updates with opensocial.requestCreateActivity Basics On every user’s MySpace Home page there is a module that displays the user’s Friend Updates.These updates are a feed and might include information like “John added a new photo,”“Mary and John are now friends,” or “Susan installed the Tic-Tac-Toe application.”These updates are ordered by date, and the newest information is always displayed on top. When an app creates an activity, the activity appears in this feed.That makes any app activity a one-to-many message that will appear in the Friend Updates feed for each of the user’s friends.With activities, applications can define numerous custom messages that will appear in a user’s Friend Updates. Activities can be created only from the app’s Canvas surface, but they’re a great way to embed your app into a user’s MySpace experience and promote your application at the same time.
Defining opensocial.requestCreateActivity opensocial.requestCreateActivity has the following signature: opensocial.requestCreateActivity = function(activity, priority, opt_callback)
This means it’s a function that must include an activity (opensocial.Activity), a priority (opensocial.CreateActivityPriority) setting for the request, and opt_callback as the function to call once the request has been processed.
79
80
Chapter 5 Communication and Viral Features
Note At the time of this writing, opensocial.CreateActivityPriority is not being checked or used by MySpace. You’ll notice in the code that follows that we always pass in a value of high (opensocial.CreateActivityPriority.HIGH) as opposed to low (opensocial.CreateActivityPriority.LOW). If you have opensocial.CreateActivityPriority in your code and MySpace starts making use of it, the behavior of your app may be affected, so it’s good to keep this in mind should your app suddenly start doing something.
Let’s take a quick look at how OpenSocial defines the priorities: High: If the activity is of high importance, it is created even if this requires asking the user for permission.This might cause the container to open a user flow that navigates away from your gadget. Low: If the activity is of low importance, it is not created if the user has not given permission for the current app to create activities.With this priority, the requestCreateActivity call never opens a user flow.
Using the Template System to Create Activities One of the most important elements of creating and raising activities is the template system. In fact, it’s so important that it needs to be explained before we start looking at code. Unlike messages, which are completely defined and passed into the function as static text, activities make use of a custom template system. Basically, you create a template for your activities’ messages and the variables are resolved at runtime.Templates must be used for activities. Every template must have a title containing up to a maximum of 160 visible characters. Each template may also optionally specify a body having up to 260 visible characters. Your message template is based on a text string with some optional variables (which you may occasionally hear referred to as “tokens”) thrown in.These variables are replaced by real data once the activity is raised. A basic activity’s message might look something like this: "Susan installed Tic-Tac-Toe on: ${date}."
When the template is run, you’ll specify that ${date} will be replaced with a string containing the current date and time. So, if you give the variable ${date} a value like “September 28, 2010,” the resulting message would read “Susan installed Tic-Tac-Toe on: September 28, 2010.”
Data Types In our first example, the ${date} variable was a string. However, you can also specify the data type of a variable.The two currently available data types are Literal and Person.
Getting Your App Listed on the Friend Updates
A Literal data type, used in our first date example, is any string.Your variable is then simply replaced by that straightforward string. A Person data type is slightly more complex. If a variable is defined as a Person data type, the variable is then replaced by a person’s display name and a link to the person’s Profile when it appears in the feed. For example, let’s say we have a template where the variable ${opponent} is of type Person. It might look like this: "You've been bested at Tic-Tac-Toe by ${opponent}."
To get this template message to display correctly, you need to pass in the opponent’s user ID. For example, if we pass in the string "6221" (Tom’s ID) for our ${opponent} variable, our message would read “You’ve been bested at Tic-Tac-Toe by Tom.” The word Tom would then link to Tom’s Profile.
Reserved Variable Names There are a number of variables that are reserved (see Table 5.2), but the most interesting one is ${sender}. This is because ${sender} is actually a required variable in your template title, meaning you’ll be using it a lot. The variable ${sender} is a Person-type variable, which means it’s replaced by the Viewer’s name and Profile link. For example, let’s say we changed our template to read "${sender} raised this event, making ${sender} the Viewer!"
If Susan were to raise an event with that variable, the resulting message would read “Susan raised this event, making Susan the Viewer!” Table 5.2
Reserved Variable Names
Reserved Variable Name
Use
${subject}
Replaced with a link to the Viewer’s Profile and the Viewer’s display name as the corresponding text
${subject.DisplayName}
Replaced by the Viewer’s display name (no link)
${subject.Id}
Replaced by the Viewer’s ID
${subject.ProfileUrl}
Replaced by the Viewer’s Profile URL
${canvasUrl}
Replaced by the app’s Canvas URL
${variable_name.Count}
Used when variables are aggregated and where variable_name is the name of a variable used in the template
81
82
Chapter 5 Communication and Viral Features
But if her arch-nemesis,Tom, raised the event, the message would read “Tom raised this event, making Tom the Viewer!” In each instance the Viewer’s information is used for the ${sender} variable.
Aggregation When a user has a large number of applications, and friends who like applications, the user’s feeds can get crowded with information and updates. Because of this, activity feeds are aggregated. If a user has five friends all playing Tic-Tac-Toe and raising activities for the app, all of those activities are aggregated into a single feed entry for Tic-Tac-Toe. For example, let’s create a new message template and see what would happen when it’s raised multiple times. Let’s start with a new template: "${sender} is playing Tic-Tac-Toe with ${opponent}."
In this example we’d want to aggregate the ${opponent} variable (you can learn how to specify aggregated variables by skipping ahead to the section on using the Template Editor). The first time I raise the event, my opponent’s ID is "6221" (Tom, again!).The resulting message reads “Susan is playing Tic-Tac-Toe with Tom.” If Susan raises the event again, this time challenging her friend Matt to a game, the resulting message reads “Susan is playing Tic-Tac-Toe with Tom and Matt.” And if Susan raises the event a third time, but this time challenging Tila Tequila to a battle of Tic-Tac-Toe, the resulting message would read “Susan is playing Tic-Tac-Toe with Tom, Matt, and Tila!” Now that’s a game I would like to see. Notice that not only does the templating system keep aggregating your message feeds, it also makes them grammatically correct by adding commas and the word and.
Body and Media Items We have variables and data types, but there’s more to a message template than that. What about pictures and the message itself? That’s where body and media items come into play. The body is similar to a template’s title, but it’s optional and can hold more characters. If you include a body, it displays on the second line under your specified title.
Getting Your App Listed on the Friend Updates
Media items can be any picture on MySpace that’s accessible to the Viewer. In Chapter 3, Getting Additional MySpace Data, we fetched a list of the Viewer’s photos, displayed them on the page, and allowed the player to select one for a custom board background. If a custom background is selected, it is used as a media item in any raised activity. You’re allowed to include a maximum of three media items in a message template. All will be rendered on the second line of the message.
Using the Template Editor to Create Templates Now that you know how templates work, you’re ready to create some new templates for your app.To do this, you’ll use the Template Editor tool found on your My Apps page. Under each application there is a tool entitled Templates (Figure 5.6); click it to be taken to your Templates page (shown in Figure 5.7). Click on Create Template to create new templates for your app.This will take you to the template creation screen. From this screen (Figure 5.8) you can edit existing templates or create a new one. Let’s look at an existing template for our Tic-Tac-Toe application.
Figure 5.6
Screen shot of My Apps S Templates link
Figure 5.7
Screen shot of an existing Templates list
83
84
Chapter 5 Communication and Viral Features
Figure 5.8
Screen shot of the Templates screen
On the individual Template screen, you can see all of the pertinent template information. Under Content you’ll find the template’s name and unique identifier along with the title and the body.There are also separate tabs for Single Form instances (when just one activity is raised) and Aggregate Form instances (multiple activities are raised). Under Variables, you can add and specify data types and even test values for each of your variables. Below this section are sample media items that you can use to test how they’ll appear when you preview or run your template. To test your template, click the Preview Template button.This creates a preview of your template in the bottom portion of the screen using the test data you indicated under Variables and Media Items. You’ll also be provided with sample JavaScript code that can be used to raise the event.You can actually just cut and paste this code into your app to begin raising activities, but you’ll most likely want to customize it (see “Raising the Event” in the following section). If you’re satisfied with your template, click the Save Template button to save your template for later use. Once your template is saved, you can switch it from development status to live status. To do this: 1. Go back to your Templates page. 2. Click the Publish button next to the template you want to make live. 3. Click OK when prompted. From your Templates page you can also delete or edit an existing template.
Getting Your App Listed on the Friend Updates
Using opensocial.requestCreateActivity Now that we understand what opensocial.requestCreateActivity looks like and how to construct a template, let’s actually use the function. Raising the Event The template we’re using for our activity is the following: ${subject} has started a game of Tic-Tac-Toe against ${opponent}
We’re doing something a bit tricky with the href attribute in the anchor tag.This actually allows us to send custom parameters into the Canvas surface of the app if the link is clicked.This can be especially useful for tracking purposes.You can try different messages, each with a different ${params} value, and see which one is clicked more often. The actual code to raise the event looks like the following: function raiseActivity(){ // Create the parameters var param = {}; // Required template name param[opensocial.Activity.Field.TITLE_ID] = "x_and_y_started_game"; // The actual parameter values param[opensocial.Activity.Field.TEMPLATE_PARAMS] = { "opponent" : currentGame.opponentId, "params" : "{\"from\":\"act\"}" }; // Check if a custom background is set to use for // a media item if(currentGame.customBG.usingCustomBG()){ // Get the photo object var photo = currentGame.customBG.photo; // Parse out the URI var uri = photo.getField(MyOpenSpace.Photo.Field.PHOTO_URI); // Create the opensocial.MediaItem object var media_item = opensocial.newMediaItem("image/jpeg", uri) // Stick it in an array var media_item_array = [ media_item ]; // Insert the array into the parameters param[opensocial.Activity.Field.MEDIA_ITEMS] = media_item_array; }
85
86
Chapter 5 Communication and Viral Features
// Create the opensocial.Activity object var activity = opensocial.newActivity(param); // Raise the activity! opensocial.requestCreateActivity(activity, opensocial.CreateActivityPriority.HIGH, raiseActivityCallback); }
First, the activity template is defined as "x_and_y_started_game".This is the unique identifier for the desired template and can be found in the Template Editor under Template Name. The template variables are then given actual values; the ${opponent} variable is assigned the value of the ID of the opponent, and the ${params} variable is assigned a JSON object.This is an example of how you might use different template messages for tracking purposes, since here we’re saying the user got to the Canvas page from an activity. Meanwhile, the two variables correspond to the list of variables in the Template Editor under Variables. The next block of code attaches a media item to the activity. A couple of things to note here: First, the media item URI must conform to an API-style URI and not a regular old URL, such as www.example.com/my_picture.jpg.The best way to get these API URIs is through the API itself, as we do here in the app. In our code we fetch the list of photos for a user and save those values into a list. When a photo is selected, we match it to the correct entry in the list and parse out the URI.This URI is accessed from the MyOpenSpace.Photo object using the MyOpenSpace.Photo.Field.PHOTO_URI. The second thing to note is that an activity requires an array of media items. So, even if you have only one media item, make sure to stick it in an array. Once all the parameters are set up, the opensocial.Activity object is created and it’s passed into requestCreateActivity. Note What are we doing there with the custom parameters in the template? Well, custom values can be passed into an app’s Canvas surface by appending an "appParams" key to the query string. So, for example, by appending &appParams={"hello"%3A"goodbye"%2C"from"%3A"act"} to the end of the Canvas URL, we pass the JSON object { "hello" : "goodbye", "from" : "act" } into the app. These values can be picked up inside the Canvas by doing the following: gadgets.views.getParams().hello; gadgets.views.getParams().from;
In this case, the first line resolves to "goodbye" and the second to "act".
Getting Your App Listed on the Friend Updates
Using Activity Callbacks to Combat Permission-Denied Errors Uh-oh! Permission denied? It turns out that a separate permission is required to raise an activity for a user. If this permission isn’t set, the container blocks the request and returns an error. So that’s that, I guess. Oh well, better luck next time… Not necessarily; opensocial.requestPermission to the rescue again! In a method similar to one used in Chapter 3, Getting Additional MySpace Data, where we requested permission to fetch photos, we can request permission here to raise an activity: function raiseActivityCallback(response) { // Check for an error if(response.hadError()){ // Was the error a permission issue? if(response.getErrorCode() === opensocial.ResponseItem.Error.UNAUTHORIZED){ // Beg for permission var reason = "To inform your friends of your upcoming match!"; // Pick which permission // STICK IT IN AN ARRAY!! var perm = [MyOpenSpace.Permission.VIEWER_SEND_UPDATES_TO_FRIENDS]; // Pretty please ... opensocial.requestPermission(perm, reason, actPermCallback); } else{ // Some other error log("Oops, there was an error, try refreshing!"); } } }
If there was an error, we first check whether it was a permissions error. If so, we specify why we want the permission and which permission we want. Again, don’t forget to stick the MyOpenSpace.Permission object into an array or it will be rejected. Once requestPermission is closed, it will invoke the specified callback function. Let’s take a look at it: function actPermCallback(response){ // Was permission granted? if(!response.hadError()){ // Yay! raiseActivity(); } }
87
88
Chapter 5 Communication and Viral Features
Very simple; if no new permissions were granted, the response has an error. No error means the permission has been granted, so retry the activity.This is a great way to ensure that the activity is sent while also providing a nice flow for the user.
Sending Notifications The fourth and final way to communicate with your users is the app notification. Notifications are great for turn-based games as they are a quick and easy way to let players know when it’s their turn in the game.This is exactly how we use them in our Tic-Tac-Toe app. It’s also highly regarded because it’s the only message type that doesn’t require the user’s confirmation. When using notifications, you have to watch that you’re not spamming your users.You could be shut down, or worse, you’ll annoy your user base and lose installs. But, if you keep it reasonable, notifications are a great way to increase user engagement. When a user gets a notification, he or she gets an indicator on the Home page saying a new notification has arrived. Clicking on the indicator takes the user to the Mail Center, where he or she will see the message in the notification folder. Each notification can have zero to two buttons to allow the user to take some action. In our Tic-Tac-Toe app, we tell users it’s their turn and provide one button to take them back to the Canvas page to play the game. Notifications are a MySpace-specific extension, but they are patterned after requestCreateActivity, so the code should be somewhat familiar. Let’s take a look at what that means: // Wrap requestCreateNotification function rsmNotification(recipient, game_id){ // Create the body text var body = "Hi ${recipient}, it's now your turn"; body += "in Tic-Tac-Toe!"; // Create the button that links to the Canvas var url1 = MyOpenSpace.NotificationButton.UrlTypes.CANVAS; var params1 = { "game_id" : game_id, "recipient" : recipient }; var text1 = "Take Your Turn!"; var button1 = MyOpenSpace.newNotificationButton(url1, text1, params1); // Create the button that links to the app's Profile var url2 = MyOpenSpace.NotificationButton.UrlTypes.APP_PROFILE; var text2 = "Check out Tic-Tac-Toe"; var button2 = MyOpenSpace.newNotificationButton(url2, text2); var param = {};
Notifications use a built-in template, similar to requestShareApp. However, only the variables ${recipient} and ${canvasUrl} are available, and you’ll need to use the activities-style variable format ${variable_name}, as opposed to the requestShareApp style of [variable_name]. The button is defined by a MyOpenSpace.NotificationButton object.This object has three fields: n
MyOpenSpace.NotificationButton.URL
n
MyOpenSpace.NotificationButton.PARAMS
n
MyOpenSpace.NotificationButton.TEXT
The URL can be one of two values: MyOpenSpace.NotificationButton. UrlTypes.CANVAS or MyOpenSpace.NotificationButton.UrlTypes. APP_PROFILE. You probably won’t want to take your users back to your app Profile, especially considering that only users who have installed the app receive notifications. Here, we want to take the user back to the Canvas instead. The PARAMS field allows us to define custom parameters that are sent into the Canvas page. In our Tic-Tac-Toe app, we append three custom parameters—essentially creating a tracking parameter.This parameter is used to let us know that a user came to the Canvas from a notification, what action the user wants to take, and which game the user wants to play. Once the button is created, we’re ready to create the MyOpenSpace.Notification object itself.We assign the body text first, using MyOpenSpace.Notification.Field.BODY, and then add the button using MyOpenSpace.Notification.Field.BUTTONS.The button has to be placed in an array, so if you have more than one button, you’d add each one to the array. Omitting the BUTTONS parameter or passing in an empty array would just attach no buttons to the message.
89
90
Chapter 5 Communication and Viral Features
Figure 5.9
The notification in the recipient’s Mail Center page
The notification is then sent off using a recipient ID, the notification object, and a callback.The recipient can be a single ID or an array of IDs, up to a maximum of ten. The callback behaves exactly like the other callbacks mentioned earlier, so we won’t go into detail. A sent notification will appear in the recipient’s Mail Center page; see Figure 5.9 for exactly how it’ll look. Tip Notifications use the permission MyOpenSpace.Permission.VIEWER_SEND_ NOTIFICATIONS. You may want to use opensocial.hasPermission (followed possibly by opensocial.requestPermission) to check that permission before attempting to send the message. See Chapter 3 to learn how to check and request permissions.
Summary The primary source of growth for most apps is friend-to-friend advertising. A user installs the app and then invites a few friends, or a posting appears in the Friend Updates feed with a link to the app. Either way, the app developer has written various activities, messages, and invitations as a way of advertising and seeding the application. It’s important to remember that while these tools must be used, they should never be abused. An app that’s spammy will quickly annoy users, leaving you with few installs and a declining user base. Note Code listings and/or code examples for this chapter can be found on our Google Code page under http://opensocialtictactoe.googlecode.com.
6 Mashups and External Server Communications Tandheclever Web is a big place. A very big place.With lots of smart people doing lots of smart things. Sometimes other people get to use their smart ideas. Sometimes your needs are so specific that you have to write your own services.Whatever your needs, you will likely reach a point where the “out-of-the-box” offerings from OpenSocial on MySpace are just not enough to satisfy your needs.Thankfully, OpenSocial recognizes these needs and provides you with a mechanism or two for communicating with external servers. In this chapter we’ll add two different features to our app that are dependent on external server communications. Each feature will use a different technique.We will make use of existing services on the Web to create a “mashup”-style app. Communication with your own servers, and securing those channels, will be covered when we discuss other styles of applications.
Communicating with External Servers What Is an External Server? An external server is any server whose DNS address does not resolve to the same top-level domain as the main hosting page. The Web browser imposes multiple security constraints when dealing with an external server, particularly with regard to cookies and XMLHttpRequest (XHR) calls. In the case of apps, an external server can also be considered any server that is not under the control of MySpace. The two notions are close enough to be interchangeable for most discussions in this book. The main MySpace top-level domain is myspace.com, but all apps are hosted in the domain msappspace.com, also referred to as a “jail domain” since it restricts apps’ access to the parent page via the same-origin policy regarding source domains.
92
Chapter 6 Mashups and External Server Communications
OpenSocial recognizes that it can’t be all things to all people. In fact, it’s designed to be just some things to all people—namely, an API for exposing social information.To that end it really is designed for use in external applications and mashups. There are a multitude of techniques for communicating with external servers. If all you are doing is referencing static content, there are almost no constraints. If you want to do something a little more dynamic, like invoke a Web service, things are a little more complicated. As we said before, static content on external servers has very little in the way of constraints. So long as you use a fully qualified address, the image file will resolve from any server.This is a common technique for large Web sites.They make use of a content delivery network, or CDN. A CDN may be simple or complex. A simple CDN just offloads the bandwidth that would be used to serve static content from the dynamic app servers, leaving more bandwidth and processing power for handling dynamic requests. Most browsers also throttle the number of concurrent connections back to a single domain, so this technique allows the browser to download more files at once; therefore the page loads faster. Dynamic content is another story.There are a handful of well-established techniques for creating dynamic content, some legitimate, some nefarious.We’ll cover the wellestablished techniques here and touch on the nefarious ones in Chapter 13, Performance, Scaling, and Security.
Mashups Origin of Mashups A mashup is a Web site or Web application that seamlessly combines content from more than one source into an integrated experience. The first mashups were simply hackers reverse-engineering the map APIs from companies like Yahoo! and MapQuest to make interesting overlays, such as laundromats near you. Over time, the mashup was recognized as not cannibalizing business but being a new business and application model in the Web 2.0 universe. While direct information is cloudy, the term mashup itself is likely derived from a practice in music where two different-source music tracks are combined, often through digital manipulation, to create something entirely new. An example of this would be mixing Madonna’s “Like a Virgin” vocals over heavy drum and bass instrumentals.
Mashups are a combination of one or more services in a new way to make a new service or product that is different from the original. As companies have recognized the value of allowing external parties to use their services and infrastructure, the number of open APIs catering to mashup-style applications has proliferated widely. Several new business models have emerged, allowing larger
Adding a Feed Reader to Our App
providers to leverage their services and infrastructure for new revenue streams by providing so-called cloud services to third parties.This is in sharp contrast to the reaction to what we’ll call “proto-mashups” from the early days of the Web. Back then it was common practice for preportal aggregation sites to either deep-link to buried services within another site (for example, linking to buying concert tickets via TicketMaster from a band review site) or completely hijack a competitor’s content by using framesets to make their site look like the competitor’s.
Pitfalls of Deep Linking A number of court cases have resulted in deep linking being declared illegal. The decisions were largely based on the fact that the deep linker/framer (utilizing site) used the content for commercial gain without the consent of the content generator (service site). The practical reasoning is that the utilizing site was using the service site as a nonconsenting content provider and bypassing the service site’s advertising to show the utilizing site’s own ads. This was a contentious issue for some time—so contentious, in fact, that the W3C felt the need to publish a paper arguing that deep linking should be legal since that is one of the major ways information interacts on the Web. In practice, deep linking has been difficult to prosecute. A cease-and-desist letter has been the action of choice for any company attempting to protect its content, and it has usually been successful. Almost every major Internet player has some sort of open API to cater to mashup applications. Content feeds continue to be a major component. As we transition to “Web 2.5/3.0,” consisting of cloud services and user-generated content, a number of new APIs are also emerging. More obvious and pedestrian applications come in the form of hosted databases (like Amazon’s SimpleDB service) and various app-hosting services. Among the newer services are those that help spread the reach of user-generated content across the Web, like Google’s Blogger Data API, Digg’s user ratings, and MySpace’s own PostTo endpoints.
Adding a Feed Reader to Our App We’ll now extend our app by adding a feed reader.This feature allows your users to never have to leave their game to keep up with the latest happenings on the Web. In the interest of theatrics, we’ll do this as a contrasting implementation in three parts.We’ll add a standard set of feeds for the user to select from.Then we will demonstrate three different implementations using makeRequest: n n n
Manual display using the FEED content type Manual display using the DOM content type Raw text response of the feed with TEXT content type
93
94
Chapter 6 Mashups and External Server Communications
XMLHttpRequest The technique of using an XMLHttpRequest (XHR) object to communicate with a backend server without completely rewriting the page is often alternately referred to as “Ajax” (Asynchronous JavaScript and XML). The term Ajax continues in common usage, though most sites employing this technique have now dropped XML as the transport mechanism in favor of the JSON (JavaScript Object Notation) format for a number of reasons: its ease of use from within JavaScript code, poor cross-browser compatibility and coding overhead associated with client-side XML processing, and JSON’s typically smaller packet size. All the OpenSocial REST endpoints support both XML and JSON return formats, with JSON being the default and the format used within the JavaScript client library. The term REST stands for Representational State Transfer. The REST endpoints and how to use them are discussed at length in Chapter 9, External Iframe Apps. XHR may be used synchronously or asynchronously, but most implementations are asynchronous since asynchronous implementations don’t cause the page to stop functioning during communication and generally deliver a better user experience.
Overview of gadgets.io.makeRequest The call gadgets.io.makeRequest is the standard way for apps to call out to an external server.This is the only officially supported technique for making external server calls in the OpenSocial specification. It is also the only technique that is allowed on all three surfaces. A makeRequest call is a powerful and flexible wrapper on top of the XMLHttpRequest (XHR) object built into all modern browsers.The makeRequest call wraps a batching mechanism on top of the XHR request and hides the ugly implementation details. If you have a special, well-known data type, it also provides several convenience formatters that will preprocess the response.You as the developer only need to supply a callback function that receives the response and does something useful with it. As you may have gleaned, makeRequest uses the same underlying communication mechanism as all the built-in OpenSocial endpoints.The difference is that the makeRequest call is designed to handle external server communication. In order to get around the same-origin policy applied to all XHR calls, the call to an external server is bounced off a proxy server that lives within the msappspace.com jail domain. The proxy is a resource shared by all apps on MySpace. As such, MySpace imposes some throttling restrictions on the number of requests an app may make within a certain time period. Particularly bad or sloppy apps have been known to take the proxy down entirely in the early days of the MySpace Open Platform. Now these apps are more likely to be suspended if they start adversely affecting the proxy servers’ performance. Even with this policy, the proxy is subject to slowed responses during peak load hours.
Adding a Feed Reader to Our App
The makeRequest call may be invoked as follows: gadgets.io.makeRequest(url, callback, opt_params)
where url indicates the URL of the service being invoked, callback is a function that is invoked when the data is received, and opt_params is an optional object hash of parameters to the request. By default, this call issues a GET request to url and invokes the function callback with the unprocessed raw text in the data response. As we alluded to before, makeRequest is more than just a wrapper on top of an XHR request. It provides many optional parameters that, depending on what kind of data service you are invoking, can prove extremely useful.These parameters can specify things like content preprocessing, security authorization, raw headers, and a specific HTTP method. For reference, optional parameters to gadgets.io.makeRequest are specified in Table 6.1. All optional keys are enum values from the enum gadgets.io. RequestParameters. In general, the string specified in the “Key” column will work, but it is good programming practice to use the enum value. For example, the key AUTHORIZATION is specified as gadgets.io.RequestParameters.AUTHORIZATION.
Table 6.1
Option Parameters to gadgets.io.makeRequest*
Key
Description
AUTHORIZATION
The type of authentication to use in the request; can be one of gadgets.io.AuthorizationType.NONE, gadgets.io.AuthorizationType.SIGNED, or
CONTENT_TYPE
The type of content you want to treat the response as; can be one of gadgets.io.ContentType.DOM, gadgets.io.ContentType.FEED, gadgets.io.ContentType.JSON, or
GET_SUMMARIES
If the content is a feed, whether to retrieve the summary fields for the feed, can be either true or false (default).
HEADERS
Headers to pass along in the request; should be an object containing key/value pairs corresponding to the headers to be sent to the server; for example,
Chapter 6 Mashups and External Server Communications
Table 6.1
Continued
Key
Description
METHOD
The HTTP method of the request; can be one of gadgets.io.MethodType.DELETE, gadgets.io.MethodType.GET, gadgets.io.MethodType.HEAD, gadgets.io.MethodType.POST, or gadgets.io.MethodType.PUT
NUM_ENTRIES
If the content is a feed, the number of entries from the feed to retrieve; should be specified as a number (the default at the time of this writing is 3).
POST_DATA
If the method is a POST, the data to pass to the other server. Data can be passed to the MySpace container either as an object containing key/value pairs or as an encoded string. The OpenSocial specification currently states that data should be only a string, but the MySpace OpenSocial container still honors both the object and the string format. Example as an object: { data1 : "test", data2 : 123456 }
Example as a string: data1=test&data2=123456
In order to convert simple objects to a string, you may make use of either the utility method gadgets.io.encodeValues or gadgets.json.stringify. REFRESH_INTERVAL
How long in seconds the response from this request should be kept in the container’s cache; should be specified as a number. The default interval is one hour, but this is subject to the browser and container’s cache implementation.
*Reprinted from Google (http://code.google.com/apis/opensocial/articles/makerequest-0.8.html) and used according to terms described in the Creative Commons 2.5 Attribution License (http://creativecommons.org/licenses/by/2.5/). Find the latest specification at www.opensocial.org or http://wiki.opensocial.org/index.php?title=Gadgets.io_(v0.9).
Response Structure The response that is returned as a parameter to the callback method is a JSON object and follows a predictable structure.This structure includes error messages, raw response, security information, and processed data.Table 6.2 identifies the properties of the makeRequest response object.
Parsed/processed data if a CONTENT_TYPE other than gadgets.io.ContentType.TEXT is specified
errors
Array of any errors that occurred when making the request, typically request errors, for example, 404 or 500
text
Raw text of the response; useful if you are performing partial content rendering, as is common when using Ruby on Rails
The actual content of the data field varies depending on the CONTENT_TYPE specified in the initial request. In the case of a TEXT content type request, the value of data is the raw text response and matches the value of the text field. For all other content types, it is some sort of JSON object representation of the data.
Handling JSON Content MySpace disallows the use of an eval statement, so the only two ways to process JSON data are by calling gadgets.json.parse or by using makeRequest.The CONTENT_TYPE parameter must be set to gadgets.io.ContentType.JSON for the response text to be processed as JSON. When a makeRequest call is made with the JSON content type, the parsed JSON object is placed in the response data property.The content of the text property is the raw, unevaluated JSON text.This method works on any app surface (Home, Profile, or Canvas). On the Canvas surface, only you can use a JSONP request to get JSON data (discussed later in this chapter in the section “Overview of JSONP”).
Handling Partial HTML Content When making a request for partial HTML content to be rendered directly into the page, your best bet is to use the default TEXT format (gadgets.io.ContentType.TEXT). Since the raw HTML is written directly into the innerHTML of some element on the page, no processing is needed.This technique is analogous to Ruby on Rails’s partials or using the Ajax.Updater from the Prototype JavaScript library.
Handling RSS Feed Content RSS feeds can be directly processed in the makeRequest call by specifying a CONTENT_TYPE of gadgets.io.ContentType.FEED.When this is done, the makeRequest response contains a processed JSON object that represents the feed.This can also be used in conjunction with the GET_SUMMARIES parameter to include summaries in the feed request.
97
98
Chapter 6 Mashups and External Server Communications
RSS feeds are widely available services on the Internet. Many sites make use of this fact to act as content aggregators. Later in this chapter, under “Setup and Design of the Feed Reader,” we will use a feed to add our own news reader to our Tic-Tac-Toe game.
Handling XML Content Raw XML content may be handled in one of two ways: by using the default TEXT format and managing your own XML processor in the browser, or by specifying a CONTENT_TYPE of gadgets.io.ContentType.DOM and having the XML document loaded into an actual DOMImplementation.This Document object is placed in the data property, and the raw response XML text is placed in the text property. Once you have the content loaded into the data DOM object, you are able to use the full DOM API in all its glory. Statements like the following may be used to select nodes: response.data.getElementsByTagName("item");
This can prove to be very useful when dealing with proprietary APIs. Even on occasion a well-known data type must be handled this way if the provider is not well conforming. This allows you as the developer to write more robust code, if you have a need. Another way in which this is useful is when applying XSLT transforms to the XML for display. If you wish to look further into this technique, it involves using the following methods (depending on your browser): n
DOMDocument.transformNode() (Internet Explorer)
n
XSLTProcessor.importStylesheet() and XSLTProcessor.transformToFragment() functions (Firefox and Safari)
Creating the XSLT object can be a little cumbersome because of browser compatibility issues. Maintaining the transforms can be problematic as in-browser debugging tools are poor at best and there are some minor implementation differences between browsers. It is not impossible, but we leave it to the reader to explore this technique.
“User’s Pick” Feed Reader Our mashup example is a user-driven feed reader.The user can pick a feed from our market-tested and carefully peer-reviewed (read: random) drop-down list of available feeds.The user can then choose to do a one-time read or have the list refresh periodically, à la PointCast Network.
Setup and Design of the Feed Reader This is a simple reader.The user can pick one of a preselected list of RSS source feeds from a drop-down.The user can also choose to have the feed continuously updated by selecting a check box. The first step is building the UI for our feed reader. It will consist of a drop-down list, a format (content type) radio button selector, an action button, and a display surface.
Adding a Feed Reader to Our App
For our feed reader we’ve selected the following RSS feeds: n n n n n n
Digg CNET Lolcats Mashable Slashdot Hulu
Following is the HTML that constructs the user interface. Add this code to our app on the Canvas surface code immediately below the "myinfo" div element.
Feed Items
<select name="feedSource" id="selFeedSource" >
Feed XML Text
99
100
Chapter 6 Mashups and External Server Communications
The basic elements we just added are
n
Select list of feeds Request format radio buttons (for testing—will be removed later) Action button to load the feed
n
Empty element "rssFeed" that will display the feed
n n
Now that the UI is built (as shown in Figure 6.1), we need to add the loadFeed() function, which is called from the button click event.This function controls the makeRequest call for the feed. Add the following code in our JavaScript source block below the TTT object: function loadFeed(){ var feedUrl = "http://www.digg.com/rss/index.xml"; //Default to Digg var params = {}; var format = gadgets.io.ContentType.FEED; params[gadgets.io.RequestParameters.CONTENT_TYPE] = format; params[gadgets.io.RequestParameters.NUM_ENTRIES] = 5; gadgets.io.makeRequest(feedUrl, feedCallback, params); }
As listed, this call is hard-coded to pull the Digg feed and take the result in the FEED format. It also specifies to send down the first five entries. By default only the top three entries are loaded when the FEED content type is specified. In this method is a callback reference to a function called feedCallback.This function processes the response and formats it for display.The simple implementation of the callback looks like this: function feedCallback(response, format){ if(response.errors && response.errors.length > 0){ showFeedError(); } else{ showFeedResults(response.data); } }
You will notice that the error handling from a makeRequest call is somewhat different from that of some of the other OpenSocial data calls.The makeRequest call holds its errors in a simple array, whereas the other OpenSocial calls use getter functions to test for errors. In that regard, the error handling from a makeRequest call is a little more straightforward.We just have to check the error’s property and make sure it is empty. Our simple example must be expanded upon to satisfy the behavior specified earlier in this section. The first order of business is some display housecleaning and
Adding a Feed Reader to Our App
Figure 6.1
Feed reader user interface in our application.
the addition of support for the user-selected values. The DOM elements are first cleaned up on each request. Because we are supporting multiple possible sources and multiple possible content type formats, the code must also read which items the user has selected. The expanded listing of our code follows: function loadFeed(){ var errElem = document.getElementById(“feedItemErrors"); errElem.innerHTML = ""; var elem = document.getElementById("rssFeed"); elem.innerHTML = "...Loading feed..."; var picked = document.forms[0].elements['feedSource']; var feedUrl = getSelectValue(picked) || "http://www.digg.com/rss/index.xml"; //Default to Digg;
101
102
Chapter 6 Mashups and External Server Communications
var typeRadio = document.forms[0].elements['feedformat']; var formatVal = getRadioValue(typeRadio); var format; switch(formatVal){ case "FEED": format = gadgets.io.ContentType.FEED; break; case "DOM": format = gadgets.io.ContentType.DOM; break; default: //text format = gadgets.io.ContentType.TEXT; break; } // Initialize the request to treat it as a feed var params = {}; params[gadgets.io.RequestParameters.CONTENT_TYPE] = format; params[gadgets.io.RequestParameters.NUM_ENTRIES] = 5; var callback = function(response){ feedCallback(response, format); } gadgets.io.makeRequest(feedUrl, callback, params); } function feedCallback(response, format){ var errElem = document.getElementById("feedItemErrors"); if(response.errors && response.errors.length > 0){ errElem.innerHTML = "Error:" + response.errors[0]; showFeedResults(response.text, gadgets.io.ContentType.TEXT); } else{ showFeedResults(response.data, format); } }
/** * Obtains the selected radio from radio input control(s) * @param {Object} radioInput */
Adding a Feed Reader to Our App
function getRadioValue(radioInput){ var checkedId=0, i; if(radioInput){ for(i=0; i < radioInput.length;i++){ if(radioInput[i].checked){ checkedId=i; break; } } return radioInput[checkedId].value; } return null; } /** * Obtains the selected option from a single item select list * @param {Object} selectInput */ function getSelectValue(selectInput){ if(!selectInput) return null; if(selectInput.selectedIndex == -1){ return null; } return selectInput[selectInput.selectedIndex].value; }
In order to carry the specified format value forward for the purpose of understanding how the response data is to be displayed, we have wrapped our callback in an anonymous function that passes down the response as well as the data format in a closure variable named format. Added to the listing are two convenience methods for getting correct values from the form fields. Now we will move on to actual data display.The display is handled by a function showFeedResults.This function determines what format is being consumed and takes proper action.You can see the skeletal listing here: /** * Display feed results. Parsing depends on format specified. * @param {Object} feed * @param {Object} format */ function showFeedResults(feed, format){ var elem = document.getElementById("rssFeed"); elem.innerHTML = ""; var titleElem = document.createElement("h3"); titleElem.style.marginTop="0"; titleElem.style.paddingTop="0";
103
104
Chapter 6 Mashups and External Server Communications
var title, copyright, items, desc; var i, entries; var line, link, body; var item; copyright=null; var tmpElem, tmp; var feedBody = document.createElement("div"); if(format==gadgets.io.ContentType.FEED){ // Do JSON FEED formatting } else if(format==gadgets.io.ContentType.DOM){ // Do XML parsing } else{ // Show raw text } titleElem.innerHTML = title; elem.appendChild(titleElem); //Honor the copyright if(copyright != null){ var cp = document.createElement("div"); cp.style.fontSize="7pt;" cp.style.color="#FFA"; cp.style.fontWeight="bold"; cp.style.fontFamily="Arial"; cp.innerHTML = "Copyright " + copyright; elem.appendChild(cp); } elem.appendChild(feedBody); }
FEED Content Type The FEED content type is the most obvious choice to use when processing an RSS feed.The makeRequest object automatically processes the raw XML feed data and parses it into a JSON object to be consumed.The structure of the JSON object mirrors the structure of the RSS feed. Constructing the display is a simple matter of iterating over the item array and building up our listings. Since all items are in JSON format, the code can directly access the values as properties. In the case of our code, we build up DOM elements for each entry (item) in the feed. Each line consists of a hyperlinked title and a truncated description.
XML Content Type with Parsing Why, you might ask yourself, would I want to manually parse an RSS feed as an XML document when there is a perfectly good feed reader built into makeRequest? The answer partially lies in the false premise of the question. Specifically, the feed reader built into makeRequest is not “perfectly good.” It is good, true, but it also has many flaws. Most noticeably, at the time of this writing, when it encounters a feed that doesn’t parse cleanly or is missing some required elements, it just dies. No error you can trap. No notification. It just dies, and the exception is swallowed.The irony of this situation is that this error occurs most often on feeds from FeedBurner, which was recently acquired by Google (Google also provides the underlying implementation code for gadgets.io.makeRequest). The second half of why you, as a developer, might want to do things the hard way is a simple and unfortunate truism of developing against an external API:You can never fully trust the stability or robustness of someone else’s API. This is a general
105
106
Chapter 6 Mashups and External Server Communications
coding truth learned by the authors over many years and through many scars. To that end, we will be equipping you with some tools and skills to overcome life’s little challenges. In the case of our code, it is almost identical in structure to process this as XML or as a JSON object.The only difference is in the use of the DOM API methods. Since the data coming back is a DOMImplementation, you are able to use all the familiar DOM API methods, such as getElementsByTagName.You are used to using the DOM API directly and aren’t one of those lazy developers who’s hooked on some fancy-schmancy JavaScript framework like jQuery or Prototype, aren’t you? Since the elements in this DOM object do not have explicit ID values for selection, they must be selected by traversing the node tree.To that end, we have created a convenience method for safely extracting the value of an element from a selected DOMElement list, getFirstDomNodeContents, which is at the bottom of this code listing: else if(format==gadgets.io.ContentType.DOM){ /* * Structure of an RSS XML document: * /rss/channel/[title,link,description,...] | /[copyright]|item/** */ var allChannels = feed.getElementsByTagName("channel"); var channel; if(allChannels && allChannels.length > 0){ channel = allChannels[0]; if(channel){ title = getFirstDomNodeContents(channel.getElementsByTagName("title")); copyright = getFirstDomNodeContents( channel.getElementsByTagName("copyright")); items = channel.getElementsByTagName("item"); if(items && items.length){ entries = Math.min(5, items.length); for(i=0; i < entries; i++){ item = items[i]; line = document.createElement("div"); body = document.createElement("div"); link = document.createElement("a"); line.appendChild(link); link.href = getFirstDomNodeContents( item.getElementsByTagName("link")); link.target = "_blank"; link.innerHTML = getFirstDomNodeContents( item.getElementsByTagName("title")); desc = getFirstDomNodeContents( item.getElementsByTagName("description")); if(desc){ body.innerHTML = desc.substr(0, Math.min(120, desc.length)); }
/** * Convenience method to get first items from a nodelist * @param {Object} nodelist */ function getFirstDomNodeContents(nodelist){ if(nodelist && nodelist.length > 0){ return nodelist[0].textContent; } else{ return null; } }
Because the use of the DOM (aka XML) content type is slightly more robust than the use of the FEED content type, your code can be designed to accommodate some error conditions. In the case of a FEED parse failure, the code simply dies.The underlying cause is often malformed or invalid XML in the feed.When the DOM content type is used, the response continues processing and allows your code to handle the error. A parsing error is reported, but the original response text is still placed in the text property of the response, so you have an opportunity to recover.This is where using the TEXT content type can come into play.
TEXT Content Type If our feed encounters an error, we’ll simply display the raw response as text, along with an error message.We’ll also make available the TEXT content type for raw debugging and demonstration purposes. In the case of an error in XML processing, the code changes the response format reported to showFeedResults as being TEXT. At this point we can just dump the results with a little escaping. Almost. function feedCallback(response, format){ var errElem = document.getElementById("feedItemErrors"); errElem.innerHTML = ""; if(response.errors && response.errors.length > 0){ errElem.innerHTML = "Error: " + response.errors[0]; showFeedResults(response.text, gadgets.io.ContentType.TEXT); }
107
108
Chapter 6 Mashups and External Server Communications
else{ showFeedResults(response.data, format); } }
function showFeedResults(feed, format){ if(){ //... (Code not shown for brevity)
else{ //Text title = "[RAW RESPONSE]" var pre = document.createElement("pre"); if(feed && typeof(feed) === "string"){ // App source is incorrectly escaped, // so you need to construct the escape sequence with two strings var es = "&"; var gt = "gt;"; var lt = "lt;"; tmp = feed.replace("<", es + lt, "g"); tmp = tmp.replace(">", es + gt, "g"); pre.innerHTML = tmp; feedBody.appendChild(pre); } else{ feedBody.innerHTML = "Huh?"; } } ... }
There are three curious lines in this code snippet. Instead of simply using a standard angle bracket escape sequence (> and <) when formatting the response text for display, our code needs to build them up.This is because of an oddity of the MySpace platform and how it saves app source. As part of the saving process, the code is run through a minimal security filter.This filter resolves any escape sequences in the app code to ensure that nothing nefarious slips past the reviewers.The usefulness and wisdom of running this sort of a processor over app code may be a subject that is open to debate.The reality is that this is a subtle yet significant “gotcha” that you must be aware of. As an alternative, you can make use of the utility method gadgets.util.escapeString instead of performing manual string manipulation.This accomplishes encoding the markup without the headache of confusing the MySpace security filters.
Adding a Feed Reader to Our App
Adding a Feed Refresh Option Contrary to what you might think, the makeRequest parameter REFRESH_INTERVAL does not control an autorefresh.This is a directive to aid the container in understanding whether it can local-cache the results of a feed request. Instead we will use a simple setTimeout directive. The first step is to create a new wrapper function for the load event that safely loads the Word of the Day feed and initializes the game. Each function call is wrapped in a try/catch to ensure that if one fails, it doesn’t kill the app entirely.We also have to change the gadget registerOnLoadHandler call to point to this new method. function loadInitializer(){ try{ loadWordOfTheDay(); } catch(ex){} try{ initializeGame(); } catch(ex){} }
//Register to look for initial data gadgets.util.registerOnLoadHandler(loadInitializer);
Note that the call to gadgets.util.registerOnLoadHandler must be placed after the loadInitializer function is declared. Otherwise the function pointer will be undefined and the code won’t work.We’ve also placed every initialization call into a try/catch block.This is so that if one fails, it will not affect the other. The next step is to tie the autorefresh check box (the code is not shown—the ID of "chkRefreshFeed") to a parameter and refactor out the actual makeRequest call into an independent function that can be invoked via a setTimeout call.The refactored function takes all the values we trapped as variables.This way, the setTimeout call can carry forward the calculated parameter values within closure variables and not lose the information. Any interval larger than zero is recognized as a call to autorefresh via a setTimeout call. function loadFeed(){ //... (Not showing code section for clarity) var refreshFeed = document.getElementById("chkRefreshFeed").checked; var interval = 0; if(refreshFeed){ interval = 3 * 60 * 1000; // Every three minutes } ...
109
110
Chapter 6 Mashups and External Server Communications
loadFeedWithOptions(feedUrl, format, interval); } /** * MakeRequest call with resolved parameters. * @param {Object} url * @param {Object} format */ function loadFeedWithOptions(url, format, refreshInterval){ // Initialize the request to treat it as a feed var params = {}; params[gadgets.io.RequestParameters.CONTENT_TYPE] = format; params[gadgets.io.RequestParameters.NUM_ENTRIES] = 5; var callback = function(response){ feedCallback(response, format); } gadgets.io.makeRequest(url, callback, params); if(refreshInterval && refreshInterval > 0){ window.setTimeout(function(){ loadFeedWithOptions(url, format, refreshInterval); }, refreshInterval); } }
In the code we hard-coded the interval to three minutes (3 min ⴱ 60 sec/min ⴱ 1000 ms/sec). If you make this interval too short, your app will just start thrashing the proxy. If MySpace determines that your app is adversely affecting the proxy through too many calls within too short a time, your app could be subject to throttling (just disregarding or denying extra requests) or, in the worst case, suspension.
Feed Automation Candy No proper push app (now commonly known as a feed reader) is complete without taking the user action out of the user experience.To that end, we’ll close this section out by adding a “Word of the Day” feature to our app. After all, nothing says Tic-Tac-Toe like the Word of the Day. We need a few divs for data display ("wordoftheday", "wodDesc", and "wodcopyright").Then we need to modify the loader function to also load the Word of the Day:
Word of the day is: <span id="wordoftheday">
Wordsmith.org word of the day
Adding a Feed Reader to Our App
The implementation of getting the Word of the Day and displaying it should be fairly familiar by now. It entails a function to encapsulate the makeRequest call and a callback handler to display the results. /** * Word of the Day */ function loadWordOfTheDay(){ var feedUrl = "http://wordsmith.org/awad/rss1.xml"; // Initialize the request to treat it as a feed var params = {}; params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.FEED; params[gadgets.io.RequestParameters.NUM_ENTRIES] = 1; gadgets.io.makeRequest(feedUrl, loadWODCallback, params); } function loadWODCallback(response){ var feed = response.data; var elem = document.getElementById("wordoftheday"); var desc = document.getElementById("wodDesc"); var copy = document.getElementById("wodcopyright"); elem.innerHTML = ""; copy.innerHTML = feed.copyright; if(response.errors && response.errors.length > 0){ elem.innerHTML = "error" } else{ var wod = feed.items[0]; elem.innerHTML = "" + wod.title + ""; desc.innerHTML = wod.description; } }
Secure Communication with Signed makeRequest Secure communications are extremely important when calling home to your own servers. OpenSocial makes use of OAuth to make secure calls back to an external server. This technique involves the use of a shared secret in order to verify the source of the request and that the request hasn’t been altered.This topic is large and complex enough to warrant its own chapter so will not be covered here. See Chapter 8, OAuth and Phoning Home, for more information on secure server communications.
111
112
Chapter 6 Mashups and External Server Communications
Adding an Image Search The second feature we’ll add is an image search. In case your user is in the middle of an intense Tic-Tac-Toe session but suddenly receives a tweet on his or her phone about Angelina Jolie’s new beehive hairdo and needs to search for what that looks like, the user will be able to perform that search without ever leaving the game. For this feature we’ll make use of the Yahoo! image search API. Many of the public APIs on the Web allow themselves to be called using a technique called JSONP. JSONP is a technique that allows JavaScript-only calls to an external server to be made in a way that removes the need for a proxy to make the call.
Overview of JSONP You may or may not be familiar with JSONP. Do not confuse this with JSON (JavaScript Object Notation). JSONP stands for “JSON with padding.”While JSON and JSONP share some similar characteristics, JSONP is more akin to an XSS (cross-site scripting) attack used for good. Most of the major client script libraries now have JSONP support, but we’re still going to describe the mechanism since it’s really not that complex. A JSONP call requires the current page’s code to manipulate the host page’s DOM to dynamically add a script tag.The source of this script tag is set to the remote server endpoint (REST/Web service). Since there is no cross-domain scripting policy in the browser for static content, the browser happily downloads the new script code. However, unlike an XSS attack, for JSONP to work the server must cooperate.The magic pixie dust involves adding a callback method name to the query string of the JSONP request.The server then invokes the named JavaScript method by appending the method call to the body of the response.The actual response payload from the API call is wrapped as a parameter to the method call; hence the “padding.” Warning There are a few “gotchas” for using JSONP. Firefox blocks until the JSONP call returns. This gives you slower page execution, but you can reliably call methods in the returned script block without waiting for the callback. Since it works that way only in Firefox, you are ill advised to rely on this behavior and execute code prior to getting the callback. Internet Explorer and Safari begin full-page rendering and execution prior to handling any dynamically generated DOM elements, so they do not block on JSONP calls. The long and short is that you must make use of the callback.
So why bother using JSONP instead of makeRequest? Your JSONP calls will be both faster and more reliable. In essence, you are cutting out the middleman. A makeRequest call has to be proxied off to another server, then the proxy has to wait for the response and write it back if and when the answer comes. It creates more network hops and multiple points of failure. If the MySpace proxy goes down, all your makeRequest calls will fail.Your JSONP calls won’t even notice.
Adding an Image Search
JSONP is much more restrictive in what can be done than makeRequest. As stated before, the MySpace terms of service disallow JSONP calls on any surface other than Canvas, so you are quite limited in when you can use it.The request may be only a GET call, so anything requiring form POST data is off the table. It is also restricted to JSON data, since the packet will be JavaScript. If your needs are simply for data on the Canvas, though, JSONP is a superior option.
Implementing the Image Search For the user interface, we’ll add a text field and a button to invoke the search in our code. Results will be displayed in the same area as the RSS feed results.
The basic mechanism used in most instances is to manipulate the DOM to add a new script tag with the source pointed to the target URL. Many JavaScript libraries provide this mechanism, but it is easy enough to write your own.The de facto standard for services providing a JSONP mechanism is to specify the callback function name in a query string parameter named callback.This function creates a script tag and appends it to the head element. /** * Make a JSONP request to any URL serving JSON data by using * the Google Ajax feed service * @param {Object} url * @param {Object} callback */ function makeJsonpRequest(url, callback){ var ajaxurl =url + "&callback=" + callback; var headElem = document.getElementsByTagName("head")[0]; var scriptElem = document.createElement("script"); scriptElem.src = ajaxurl; headElem.appendChild(scriptElem); }
The next step is to specify the entry point function and the callback to display results. The entry function pulls the search term from the input field imageTerm, constructs the search URL, and invokes the makeJsonpRequest function.The callback constructs DOM elements to display the search results. function searchImage(){ var searchUrl = "http://search.yahooapis.com/ImageSearchService/V1/imageSearch?appid=YahooDemo&query= ➥##term##&output=json";
113
114
Chapter 6 Mashups and External Server Communications
var term = document.getElementById("imageTerm").value; if(!term || term == "") term = "world"; var searchUrl = searchUrl.replace("##term##", term); makeJsonpRequest(searchUrl, "loadSearchCallback"); } function loadSearchCallback(data){ // data.ResultSet.Result[@Title, @Url] var elem = document.getElementById("rssFeed"); elem.innerHTML = ""; var line, img; var result; for(var i=0;i < data.ResultSet.Result.length; i++){ result = data.ResultSet.Result[i]; line = document.createElement("div"); img = document.createElement("img"); line.appendChild(img); img.src = result.Url; img.setAttribute("height", "60"); elem.appendChild(line); } }
Posting Data with a Form Good old-fashioned forms remain an effective way to communicate with external servers. Using a form to send data involves either placing the form in an iframe inside your app or passing the current app’s URL along as a parameter to the form. In this way, the form can issue a redirect back to your app after processing. Many third-party payment gateways make use of this technique.We leave it to the reader to explore using forms as a mechanism to communicate with payment gateways. From a MySpace-specific perspective, the terms of service allow form posts to external sites only from the Canvas view.This restriction can be circumvented by using a makeRequest call and specifying a method of gadgets.io.MethodType.POST (or gadgets.io.MethodType.GET for a query-string-encoded form) in the parameters to the makeRequest call.
Summary There are pros and cons to the methods of external communication discussed in this chapter. gadgets.io.makeRequest is a shared resource and can become a single point of failure if the service is having issues. However, it offers many useful features such as
Summary
built-in RSS feed support, along with being well defined by the OpenSocial specification. This is also the only mechanism that allows for signed and secure communications with an external server under your control.You should refer to Chapter 8, OAuth and Phoning Home, for a complete discussion of secure communications. JSONP, on the other hand, is a bit of a hack. Browsers and the HTML specification weren’t really intended to do some of the things that JSONP does, and it’s limited in what it can do. Only GET requests are permitted; POST, DELETE, and other verbs aren’t possible. It’s also hard to debug JSONP requests; depending on where an error occurred, your callback function may never be executed. In that case, how do you know what went wrong? But what JSONP does, it does well. It’s a quick and easy way to make cross-domain requests from JavaScript. HTML forms aren’t as exciting, but they are reliable. Form posts continue to be a common mechanism used to access third-party payment gateways. In the end, it’s up to you to figure out which method you use to talk to external servers, given the specific needs of your app. Note Code listings and/or code examples for this chapter can be found on our Google Code page under http://opensocialtictactoe.googlecode.com.
115
This page intentionally left blank
7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play A
t this point in development, there are a number of options for expanding our Tic-Tac-Toe game. In this chapter we’re going to delve into person-to-person game play but continue to focus for now on what can be done within the confines of the MySpace walls. Many games available on MySpace currently use external servers for their actual person-to-person game play.We’re going to explore an option for person-to-person play without external servers, and in the course of fleshing out our game, we’ll introduce the MySpace FriendPicker widget.
Turn-Based Games Turn-based gaming is a very broad genre. At its simplest, this is the kind of gaming performed in most card games or board games.Think Go Fish, Checkers, or even Chess. In computer gaming, turn-based gaming was often typified by war-and-conquest-style games where the player faced off against one or more human or computer opponents. As computers became more powerful, turn-based games were eclipsed in popularity by real-time games.Think Doom or Castle Wolfenstein or any number of other first-person shooters. But, luckily for us, turn-based games have resurfaced as so-called casual gaming has regained popularity. We’re going to show how to make a turn-based Tic-Tac-Toe game, but this same technique could easily be adapted to card games, board games, or something else of similar complexity.We’ll leverage the ability of the app data store to be read by friends in order to trade off turns of game play. Our user interface will be modified to include some new elements on the Play tab. It will also include a FriendPicker to select the appropriate friend to play against. It will
118
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
also display current games in progress and allow the user to activate an in-progress game in order to make a move.
Design Overview Most turn-based computer games make use of a shared database. In our case we have to make do with a different kind of structure.App data can be considered a shared-read, private-write design. Getting this to work takes a lot of cooperation and a little sprinkling of pixie dust. The best way to understand these complex workflows is to draw a picture of our logic. The first part we need to understand is the flow of starting a game between two opponents. Figure 7.1 outlines the basic logic flow of picking an opponent and starting a game.
Viewer (Opponent A) picks Opponent B to play
Get Viewer and Opponent B game AppData
Yes
Game in progress?
Identify most recent data and synchronize Viewer data
No
Create Viewer game data and save
Yes
Is Viewer’s turn?
No
Viewer takes turn and saves game data
Figure 7.1
Display “Not your turn” message
Logic flow of starting an app data game.
Turn-Based Games
When Opponent A selects Opponent B, a delicate dance between the two shared app data stores ensues.The code has to determine first if there is a game in progress. An in-progress game is identified by a matching object containing the game info being discovered in both the Viewer’s app data store and the opponent’s app data store.The next step is to reconcile the two versions of the game to determine whose is the more recent and contains the latest snapshot of the game with all moves.The final step is to determine whose move it is and allow or disallow a new move to be made, saving the latest results to the common store.
Connecting Two Nodes for Transferring Data The “three-way handshake” is a technique used to establish a connection between two nodes for transferring data. This technique is most widely used in the transmission control protocol (TCP) that helps make the Internet function. A rough example is this conversation: First person: “I’m telling you something.” Second person: “I hear that you told me something.” First person: “I hear that you heard that I told you something.” After this conversation, both parties have the information, and both parties know that the other party has the information.
Things get a little more interesting when we try to reconcile the winner and clear out the game.This requires mutual agreement between both players’ data stores that the game is indeed over. We could use a technique called a “three-way handshake” to accomplish this. A secondary variable is used to move the game from a “playing” state to a “pending removal” state. In this way we would clear out old games and allow new games to be played. Figure 7.2 outlines the logic required to complete a game in this manner. A three-way handshake ends up having a fairly complex logic flow.The upside is that games are cleared out after they are finished.The downside is that the user gets only one chance to see the result of the game. What if your players are true students of the art and science of tic-tac-toe? They may wish to review the winning strategy for days or even weeks. It might be something they obsess over for a lifetime. Given all that, the simpler solution ends up being the better path.When a game is finished, the user has the option of pushing a button to delete the game.Then our logic only has to be able to start new games from one side. Enough talk. Let’s code.
Adding FriendPicker The first thing we’ll do is build up a UI to facilitate the game play.To that end, we’ll make use of the FriendPicker widget.
119
120
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
Player A makes final move and winner is declared
Game placed in “Finished Queue” for Player A
(B to A) is game over?
Yes
B displays results, writes game to “Finished Queue”, and deletes game
(A to B) B has game in FQ?
No No
Allow B to make move
Figure 7.2
Do nothing
Yes
Delete game, FQ and ready for next game
Logic flow of finishing and clearing an app data game.
The FriendPicker is a powerful and evolving widget provided by MySpace for quickly adding friend selection to your app. It supports quick scrolling of people, autocomplete search, headless (search box only) operation, and dynamic interaction with your page. Figure 7.3 shows an open FriendPicker pop-up with default settings. The FriendPicker is loaded by making use of the MySpace dynamic loader and the Bootstrapper.While powerful, it is a fairly heavy piece of code, weighing in at almost 2000 lines.The Bootstrapper is a general-purpose object loader for handling dynamically included widgets. It has the following signature: MyOpenSpace.Widgets.Bootstrapper.createWidget( <widget_object>, loadFinishedCallback, initializationObject);
Turn-Based Games
Figure 7.3
FriendPicker.
Through the use of this Bootstrapper, your code can safely create one or more FriendPickers in your app.Table 7.1 shows the different operation modes. The FriendPicker has numerous other property settings to control its display. Some can be set with the intializationObject; some must be set in the loadFinishedCallback function.All are poorly documented. In order to figure out the subtleties, check the forums on the MySpace developer site or simply download the source code.At the time of this writing, the location of the latest code is http://api.msappspace.com/OpenSocial/MyOpenSpace004.Widgets.js.You can also use Firebug to determine where the latest script source is being downloaded from when you view our app.Table 7.2 shows a short list of some of the most useful properties of the FriendPicker widget. Using the FriendPicker The FriendPicker allows the user to select a friend easily. It is one component in the standard MySpace widgets library. Using the FriendPicker is a snap with a few easy steps: 1. Include the MYSPACE_WIDGETS library at the beginning of your Canvas surface. This is done by using the MyOpenSpace.ClientLibraries.includeScript call to dynamically include one of several available add-on libraries. At the top of the
121
122
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
Table 7.1
FriendPicker Operation Modes
Mode
Initialization Property Setting
Notes
Full FriendPicker interface
displayMode = 1
This is the default mode. The full interface includes both paging of friends with thumbnail images as well as autocomplete search.
Selected friend shown
buildSelectedUI
By default, the selected friend is shown after being picked. To disable this feature, set buildSelectedUI to false.
Text-only mode
displayMode=2
This mode shows only the autocomplete search box. When the user starts typing, a list of possible matches is shown. The user may use keystrokes or the mouse to select a friend. Keystrokes used for navigation and selection are standard arrow keys and Enter. This mode is useful on Home and Profile surfaces where space is limited.
Canvas surface code, within the first script block, add the following statements to dynamically include the latest widgets script: var scriptLibraries = MyOpenSpace.ClientLibraries.Scripts; var includeLib = MyOpenSpace.ClientLibraries.includeScript; includeLib(scriptLibraries.MYSPACE_WIDGETS);
2. Find the opponentinfo div in the source for Canvas view. Immediately below this div, add two new div elements identified as opponentPicker and availableGames:
3. In the script block, immediately after the function initializeGame, add the following opponentPickedAction function. For the time being, this will display
Turn-Based Games
Table 7.2
FriendPicker Useful Properties
Property
Values
Notes
pageSize
Numeric
This property identifies how many friends to display on each page when scrolling through the friends list. This applies only when in full interface mode. Default value is 4.
friendClickAction
Function reference
A callback function to trigger when the user selects a friend. This can be used for updating the UI or for using the FriendPicker to create a multiselect interface. The opensocial.Person
object for the selected friend is passed as a parameter to the callback function. displayMode
1 or 2
Sets the UI as full UI (1) or text-only (2). This value must be set in the initializationObject
to have any effect. selectedFriend
opensocial.Person
or null (read-only)
friendsCatalog
This property can be used to retrieve the currently selected person object at any time. If no person is selected, this value is null. An internally used catalog of all downloaded friends. This object can be used as a shortcut to reference the list of friends without setting up your own data requests and callbacks. The most useful internal property is friendMap. This contains a map of all the downloaded opensocial.Person
objects used internally by the FriendPicker.
123
124
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
only the picked opponent in the availableGames div. Later on we’ll make it do something more interesting. function opponentPickedAction(person){ var elem = document.getElementById("availableGames"); elem.innerHTML = person.getDisplayName(); }
4. Immediately after the opponentPicked function, add the following loadFriendPicker function.This function makes use of the widget Bootstrapper object to initialize the FriendPicker widget.The Bootstrapper allows us to safely create a widget from a dynamically included script library.The call to createWidget takes three parameters: the object name of the widget being created, an initializer function to fire when the widget has been created, and an object of parameters to pass into the widget at initialization. In this case, we’re passing in the ID of our target div element, a callback function to execute whenever a friend is picked, and a flag to build the “Selected User” user interface element. function loadFriendPicker(){ MyOpenSpace.Widgets.Bootstrapper.createWidget( "MyOpenSpace.Widgets.FriendPicker", function(p){ window.friendPicker = p; }, { element: "opponentPicker", buildSelectedUI: true, friendClickAction: opponentPicked }); }
5. In our loadInitializer function, add a call to loadFriendPicker: function loadInitializer(){ try{ loadWordOfTheDay(); } catch(ex){} try{ initializeGame(); } catch(ex){} loadFriendPicker(); }
Turn-Based Games
Figure 7.4
FriendPicker below player info.
Warning The call to initialize a FriendPicker must be made from a gadgets.util. registerOnLoadHandler-triggered function. If we try to initialize from an inline call, the initializer will fail in some browsers. Not all browsers handle dynamic script loading in the same manner, so we must use the least common denominator behavior.
If we save and view our app, we’ll see the FriendPicker initialized below the current viewer UI. Clicking on the [Click for Recipient] item shown in Figure 7.4 brings up the FriendPicker UI and allows the user to pick a friend.
App Data Game Store Our game store in app data will consist of some JSON objects in an array.We’ll build on some of the code shown previously in the book for using the app data store.This time we’ll clean things up a bit by placing all our code under the TTT namespace, instead of leaving a bunch of dangling functions hanging off the global window object. Storage JSON Objects The first step is to create the GameInfo storage object. In your script code, add the following code: TTT.GameInfo = function(opponentId){ this.opponentId = opponentId; this.dataSourceId = window.viewer.getId(); this.boardLayout = null; this.moveNumber = 0; this.currentPlayer = 0; this.winner=0; }
With this object, our game can track the opponent, whose move it is, if there is a winner, and all the moves on the board.The active games are stored as an array of these objects. AppDataPlay Game Object The next step is to set up our basic game data manager.This will contain the logic described earlier in the chapter. It will also encapsulate some utility methods for managing the data store.
125
126
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
Logging to Debug Apps The gadgets spec provides for a logging mechanism similar to the one popularized by log4j under the gadgets.log call. Logging is an incredibly useful way to debug an app. It’s not always convenient to set breakpoints and single steps. A log lets you dissect the execution sequence after the fact. At the time of this writing, gadgets.log isn’t implemented in the MySpace container. It’s simple to add, though. In our case, we just want to write out to a scrolling div. After development we can hide the div and leave the log calls in place. Here’s how: <style type="text/css"> #log{ float:left; margin:5px; padding:5px; border:5px solid #888; width:350px; height:350px; overflow:auto; background:white; }
... if(!gadgets.log){ gadgets.log = function(msg){ var el = document.getElementById("log"); if(!el) return; var m = document.createElement("div"); m.innerHTML = msg; el.appendChild(m); } }
In earlier chapters we’ve shown some techniques by making use of global functions.While convenient for small projects, it is good practice to namespace your script code.We’re going to encapsulate all of this in a static object namespace of TTT.AppDataPlay.
Turn-Based Games
1. Create the basic object skeleton and add it into your script source.The code is shown here: TTT.AppDataPlay = { }
2. Add a Keys object to this namespace that will encapsulate all the app data keys used for game play: TTT.AppDataPlay = { /** * Keys into the AppData store */ Keys: { activeGames: "activeGames", AppDataGetKey: "getData", finishedGames: "finishedGames" } }
3. Now we will add some empty object key definitions to hold our data. Even though JavaScript allows for dynamic creation of variables, it is good practice to explicitly declare and comment any variables that are used in multiple places. Add this code inside our TTT.AppDataPlay object: TTT.AppDataPlay = { . . . /** * Local cache of game app data for Viewer. */ myGameAppData: { "activeGames":null }, /** * Local cache of opponent game app data */ myOpponentAppData: {}, /** * Local cache of opponent in current game */ myOpponentCurrentGame: {},
127
128
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
/** * Current opponent being played against */ currentOpponentId: null, . . . }
4. Add method stubs for game play support.These functions are set up as placeholders so that our code won’t break as we build it out.We will fill in the functions with real logic later. TTT.AppDataPlay = { . . .
/** * Retrieves the gameInfo object for the specified opponent. * If one doesn't exist, a new gameInfo object is created. */ getCurrentGameObject: function(opponentId){}, /** * Convenience method to save the data currently in * this.myGameAppData to the backing app data store. */ updateStoredGameObject: function(){}, /** * Loads the specified gameInfo object into * window.currentGame and updates the board */ loadGame: function(gameInfo){}, /** * Displays a message regarding game play */ updateGameStatusMessage: function(message){}, /** * Serializes the board information into a string for storage */
Turn-Based Games
getGameMovesString: function(game){}, . . . }
5. Add the methods for retrieval and saving of the Viewer’s app data.These methods are adaptations of what we wrote in Chapter 4, Persisting Information.The only real difference is that we include them in our TTT.AppDataPlay namespace. TTT.AppDataPlay = { . . . /** * Retrieve the current Viewer's app data. * This is an upgrade of the method introduced in * Chapter 4, Persisting Information. */ getMyAppData: function (){ var idparams = {}; idparams[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER; idparams[opensocial.IdSpec.Field.NETWORK_DISTANCE] = 0; idparams[opensocial.IdSpec.Field.GROUP_ID] = opensocial.IdSpec.GroupId.SELF; var id = opensocial.newIdSpec(idparams); var req = opensocial.newDataRequest(); req.add( req.newFetchPersonAppDataRequest(id, "*"), TTT.AppDataPlay.Keys.AppDataGetKey); req.send(function(data){ TTT.AppDataPlay.loadAppDataCallback(data, TTT.AppDataPlay.myGameAppData); }); }, /** * Sets an AppData key for the current Viewer * @param {String} key * @param {JSON object} value */ setAppDataKey: function (key, value) { var req = opensocial.newDataRequest();
129
130
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
6. Referenced in the methods just shown is a general-purpose app data callback handler adapted from Chapter 4, Persisting Information. Add the function loadAppDataCallback in as well.This function places all the AppData keys and values in the response into the specified object hash. In that way we make only one call for all the data. TTT.AppDataPlay = { . . . /** * Callback wrapper method for loadAppData. * This loads all the data into myLocalAppData * and triggers the loadedCallback function, if specified * @param {Object} data * @param {Function} loadedCallback */ loadAppDataCallback: function (data, targetCollection, loadedCallback){ logDataError(data); if (data.hadError()) { return; //Exit if nothing found } var mydata = data.get(TTT.AppDataPlay.Keys.AppDataGetKey).getData(); //App data has an additional layer of indirection. //We reassign mydata object to the data map object //Circumvent Viewer lookup by getting key var ok = false; for(var vkey in mydata){ mydata = mydata[vkey]; ok = true; break; } if(ok){ if(targetCollection == null){ gadgets.log("Bad collection in load callback");
7. Finally, add in the function that retrieves a friend’s app data.We touched on this briefly in Chapter 4, Persisting Information, but did not write the code for retrieving other people’s app data.You can read, but cannot write, your friend’s data, provided the friend has the app installed as well.The main differences between this and Viewer are the idSpec used and the callback function. TTT.AppDataPlay = { . . . /** * Retrieve app data for an opponent. */ getFriendGameData: function (friendId){ var idparams = {}; idparams[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER; idparams[opensocial.IdSpec.Field.NETWORK_DISTANCE] = 1; if(!friendId){ friendId = window.friendPicker.selectedFriend.getId(); } idparams[opensocial.IdSpec.Field.GROUP_ID] = friendId; //If MySpace fixes the idSpec bug, use this /* if(!friendId){ friendId = window.friendPicker.selectedFriend.getId(); } idparams[opensocial.IdSpec.Field.USER_ID] = friendId; idparams[opensocial.IdSpec.Field.GROUP_ID] = opensocial.IdSpec.GroupId.SELF; */
131
132
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
Warning You may notice a commented-out block of code within this function. At the time of this writing, MySpace had an inconsistency in its interpretation of the idSpec object from how other OpenSocial implementations interpret the same object. MySpace requires the user ID to always be VIEWER and the target friend ID to be placed in the GROUP_ID field. Other implementations put the target friend ID in the user ID field and use the reserved enum value opensocial.IdSpec.GroupId.SELF for the GROUP_ID value.
8. Stub out the callback function TTT.AppDataPlay. selectedOpponentDataCallback.This function holds much of the logic for game management. For now, we’re just going to stub it out and add a log statement so our code will run. TTT.AppDataPlay = { . . . selectedOpponentDataCallback: function(data){ gadgets.log("Got opponent data: " + data); } . . . }
Supporting Person-to-Person Game Play
At this point, we have the skeleton of the AppDataPlay object built and the utility methods required for communicating with the app data store of both players.
Supporting Person-to-Person Game Play Thus far, our actual game engine (TTT.Game) has supported only human-versus-computer game play.We need to make a few tweaks to allow for dual-mode play (person-to-person, or P2P). In our game engine, we’ll always consider our opponent to be “Computer,” even when the opponent is another person.This saves us from a broader rewrite of the display logic and game engine. A simple flag value will give us enough information to differentiate computer from human opponents.
Adding P2P Game Play Support in the Game Engine The first step is to make a few minor modifications to the original Tic-Tac-Toe game engine in order to support a human opponent in a turn-based system. We must change the “made a move” trigger to store data and wait instead of triggering a computer move. We also now need to handle cases when the game ends in a draw. 1. Add a property in the TTT.Game object to flag the game as being against a human opponent.This will be used in the game logic to trigger a computer move or trigger an app data store update. var TTT = { . . . Game: function(){ . . . /** * Flag to set if this is a vs. computer * or vs. human game */ this.isHumanOpponent = false; . . . } }
2. Add a new method to the TTT.Game prototype object to test for a draw. In human-versus-computer play we didn’t need to test for a draw. When no squares were available, there were no moves to make.With a human opponent we need to be able to identify the end of a game when there is no winner. Search for the lookForWin function and add the isADraw function immediately below.
133
134
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
/** * Checks to see if the current game is a draw (all cells filled) * */ isADraw : function(){ var openCell = false; for(var i=0; i < 9; i++){ if(!this.moves[i]){ openCell = true; break; } } return (!openCell); },
3. Edit the game play event handler makePlayerMove to support P2P game play. Previously, this function would trigger a computer move if a winner was not discovered.We need to update this if statement with an else and include a check for isHumanOpponent. function makePlayerMove(e){ . . . if(!currentGame.hasWinner && !currentGame.isHumanOpponent){ window.setTimeout(function(){ currentGame.makeComputerMove(); }, 500); } else{ . . . } }
4. Add the game logic for taking a turn within the else statement in the makePlayerMove function from the preceding step.You will notice some new methods on the TTT.AppDataPlay object.We’ll fill in those methods in the next section. else{ // USER FEEDBACK MESSAGES HERE var adGame = TTT.AppDataPlay.getCurrentGameObject( TTT.AppDataPlay.currentOpponentId); adGame.moveNumber = adGame.moveNumber+1; if(currentGame.hasWinner){ msg = "You Win!";
Supporting Person-to-Person Game Play
adGame.winner = window.viewer.getId(); } else{ msg += " Waiting for your opponent"; } adGame.boardLayout = TTT.AppDataPlay.getGameMovesString(currentGame); adGame.currentPlayer = TTT.AppDataPlay.currentOpponentId; TTT.AppDataPlay.updateStoredGameObject(adGame); if(currentGame.isADraw()){ msg += " IT'S A DRAW!"; } else if(!currentGame.hasWinner && !currentGame.isADraw()){ //Wait for next move } TTT.AppDataPlay.updateGameStatusMessage(msg); }
Adding User Feedback Feedback to the user is an important part of the game play. In turn-based game play, sometimes it’s simply just not the Viewer’s turn.Without feedback the Viewer has the impression that the game is broken. When our Tic-Tac-Toe game is in P2P game play mode instead of P2C (personto-computer), the computer takes on the role of commentator/narrator instead of opponent. Since this is a game, we like to add some sass to the commentary. Feel free to customize the messages to your liking. 1. Above the table with an ID of gameboard, add a div element with an ID of gameStatus.This is where the feedback messages are displayed.
2. In the makePlayerMove function, add a check and associated message to short-circuit processing if it is not the Viewer’s turn to make a move. Place this code immediately after the line where the var cell = ... statement is located. if(currentGame.isHumanOpponent && (currentGame.currentPlayer == TTT.Players.Computer)){ TTT.AppDataPlay.updateGameStatusMessage( "YO! I said you have to wait your turn."); return; }
135
136
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
3. In the makePlayerMove function, add a random message generator within the else block that is executed during P2P game play. In our case we’ll create three messages and use a modulo operation coupled with a timestamp to pick a message in a quasi-random fashion.To the end user this will appear as a running commentary by the computer. var quotes = ["Nice Move!", "...interesting strategy", "What were you thinking?"]; var d = new Date(); var ndx = d.getMilliseconds() % 3; var msg = quotes[ndx]; //Random quote
Modulo Magic Modulo operations can be one of the most useful bits of programming pixie dust to put in your bag of tricks. A modulo operation calculates the remainder after a division operation between two numbers. To illustrate, if we divide the number 5 by 2, it divides evenly into two parts with 1 left over. The modulo (or “mod”) operation returns the 1. 5/2 = 2 with remainder of 1, or 5 mod 2 = 1
In JavaScript the statement would be written as 5 % 2. By far the most common usage is for displaying rows with alternating colors. This is accomplished with the following if statement: for(var i=0; i
There are many more uses for modulo operations. Modulo is great any time you want a quasi-random distribution of data. All it takes is an incrementing counter (like a timestamp) and the number of buckets to test for. True random numbers are much more cumbersome and computationally expensive than a modulo operation.
The following code is the final code listing for makePlayerMove: function makePlayerMove(e){ if(!e) var e = window.event; var cell = (e.target) ?
Supporting Person-to-Person Game Play
e.target.getAttribute("gamecell") : e.srcElement.getAttribute("gamecell"); if(currentGame.isHumanOpponent && (currentGame.currentPlayer == TTT.Players.Computer)){ TTT.AppDataPlay.updateGameStatusMessage( "YO! I said you have to wait your turn."); return; }
currentGame.makeMove(TTT.Players.Human, cell); if(!currentGame.hasWinner && !currentGame.isHumanOpponent){ window.setTimeout(function(){ currentGame.makeComputerMove(); }, 500); } else{ var quotes = [ "Nice Move!", "...interesting strategy", "What were you thinking?"]; var d = new Date(); var ndx = d.getMilliseconds() % 3; var msg = quotes[ndx]; //Random quote var adGame = TTT.AppDataPlay.getCurrentGameObject( TTT.AppDataPlay.currentOpponentId); adGame.moveNumber = adGame.moveNumber+1; if(currentGame.hasWinner){ msg = "You Win!"; adGame.winner = window.viewer.getId(); } else{ msg += " Waiting for your opponent"; } adGame.boardLayout = TTT.AppDataPlay.getGameMovesString(currentGame); adGame.currentPlayer = TTT.AppDataPlay.currentOpponentId; if(currentGame.isADraw()){ msg += " IT'S A DRAW!"; adGame.winner = -1; } else if(!currentGame.hasWinner && !currentGame.isADraw()){
137
138
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
Fleshing Out P2P Game Logic We have now created all the underpinnings of the game play. Now it’s time to glue it all together and implement our logic flows from the beginning of this chapter. Logic for Selecting an Opponent and Loading the Game Selecting an opponent and activating a game constitute the bulk of the workflow outlined at the beginning of the chapter.The app has to get both the Viewer’s and the opponent’s game data, reconcile which version is the latest, and determine whose move it is. If there is no active game, a new one is created. Find the function selectedOpponentDataCallback in the TTT.AppDataPlay object namespace that we stubbed out earlier.This is a big function, so we will discuss each section of it in turn. The first section of this function retrieves the GameInfo object for the two associated players by examining all the games stored in the selected opponent’s app data store until it finds one where the current Viewer’s ID matches the opponentId value in the GameInfo object.This object is then assigned to the variable TTT.AppDataPlay. myOpponentCurrentGame. At the very end of this section the code block similarly retrieves the GameInfo object associated with this opponent from the Viewer’s local copy of the app data store by calling getCurrentGameObject. if(!data){ return; } //Since we're in a callback, "this" has lost scope var thisObj = TTT.AppDataPlay; var myId = window.viewer.getId(); thisObj.myOpponentCurrentGame = null; var opponentStoredGames = data[thisObj.Keys.activeGames]; if(opponentStoredGames != undefined && opponentStoredGames != null){ opponentStoredGames = gadgets.json.parse(opponentStoredGames); if(opponentStoredGames.length){ var curGame; for(var i=0; i < opponentStoredGames.length; i++){
The second section of the selectedOpponentDataCallback function reconciles the two GameInfo objects. It does this by identifying which object has the latest move. The boardLayout is then copied into the Viewer’s GameInfo object to ensure that the Viewer’s copy is the latest copy. var latestGameInfo; if(thisObj.myOpponentCurrentGame != null){ //Reconcile to latest game version if(myGameInfo.moveNumber <= thisObj.myOpponentCurrentGame.moveNumber){ latestGameInfo = thisObj.myOpponentCurrentGame; //Sync the player's game board with most current board myGameInfo.boardLayout = latestGameInfo.boardLayout; myGameInfo.moveNumber = latestGameInfo.moveNumber; myGameInfo.currentPlayer = latestGameInfo.currentPlayer; } else{ latestGameInfo = myGameInfo; }
Once the latestGameInfo has been determined, the code tests to see if a winner has been declared.When a winner has been declared, the ID of the winning player is placed in this field. If there is no winner, this value is 0 (equivalent to false). If there is a winner, the board is loaded, a message is displayed to state who won, and the game is marked as finished. If there isn’t a winner, the game is loaded into view via a call to the loadGame function (described later in this section). User feedback is generated to indicate whose turn it is. if(latestGameInfo.winner != 0){ if(latestGameInfo.winner == myId){ thisObj.updateGameStatusMessage("YOU WON!!!"); }
139
140
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
else if(latestGameInfo.winner == -1){ thisObj.updateGameStatusMessage("ITS A DRAW!!!"); } else{ thisObj.updateGameStatusMessage("YOUR OPPONENT WON!!!"); } //debugger; thisObj.loadGame(latestGameInfo); thisObj.markGameFinished(latestGameInfo.gameId); thisObj.cancelPolling(); return; } thisObj.loadGame(latestGameInfo); if(latestGameInfo.currentPlayer==myId) { //My turn msg += " It is your turn"; } else{ msg += " Still waiting on opponent"; thisObj.startGameplayPolling(); }
The preceding code describes what occurs when a game is discovered to be in progress.The second branch of this is what occurs when this is a new game or the opponent has not yet accepted the game. In that instance the Viewer’s game is loaded and appropriate user feedback is displayed. } else{ latestGameInfo = myGameInfo thisObj.loadGame(latestGameInfo); //Save the new game back if(latestGameInfo.moveNumber==0){ msg += " New game started - make your move"; } else{ msg += " Waiting for your opponent to move"; } } thisObj.updateGameStatusMessage(msg);
That finishes the selectedOpponentDataCallback function.There were two other functions touched on in the preceding description: getCurrentGameObject and loadGame. Both methods are fairly simple.
Supporting Person-to-Person Game Play
The function getCurrentGameObject retrieves the GameInfo object from the Viewer’s app data store for the associated opponent. If there is no game in progress, a new GameInfo object is initialized for this opponent and returned. /** * Retrieves the gameInfo object for the specified opponent. * If one doesn't exist, a new gameInfo object is created. * */ getCurrentGameObject: function(opponentId){ var result = null; if(this.myGameAppData && this.myGameAppData.activeGames != null){ var items = this.myGameAppData.activeGames; var i; if(items.length){ for(i=0; i < items.length; i++){ if(items[i].opponentId == opponentId){ result = items[i]; break; } } } else{ //Is object var x; for(x in items){ if(items[x].opponentId == opponentId){ result = items[x]; break; } } } } if(result == null){ result = new TTT.GameInfo(opponentId); if(!this.myGameAppData.activeGames){ this.myGameAppData.activeGames = []; } this.myGameAppData.activeGames.push(result); } return result; }
The function loadGame loads the GameInfo into the game board and initializes the window.currentGame object.The bulk of this method is display logic. In our
141
142
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
implementation, the Viewer’s moves are always represented by an X (TTT.Players. Human) and the opponent’s moves are always represented by an O (TTT.Players. Computer). The board layout is represented by a pipe-delimited list of moves.This represents the nine squares of the Tic-Tac-Toe board. Each move is either an X, an O, or nothing. Depending on the originating source of the GameInfo object (Viewer’s object or opponent’s object), as determined by matching the Viewer’s ID against gameInfo.dataSourceId, these moves are transcribed to be appropriate for the Viewer. For example, if the latest GameInfo object originated from the Viewer’s app data store, the X’s and O’s remain the same. If the latest GameInfo object was from the opponent’s app data store, the X’s and O’s have to be flipped. /** * Loads the specified gameInfo object into * window.currentGame and updates the board */ loadGame: function(gameInfo){ if(currentGame){ currentGame.clear(); } else{ currentGame = new TTT.Game(); } currentGame.isHumanOpponent = true; //Flag to not make computer moves var myId = window.viewer.getId(); var fromMe = (gameInfo.dataSourceId == myId); if(gameInfo.boardLayout){ var moves = gameInfo.boardLayout.split("|"); var nOneMark, nZeroMark; if(fromMe){ nOneMark = TTT.Players.Human; nZeroMark = TTT.Players.Computer; } else{ nOneMark = TTT.Players.Computer; nZeroMark = TTT.Players.Human; } //1 == game owner, 0 == viewer for(var i=0; i < moves.length; i++){ if(moves[i]==""){ continue; } else{ if(moves[i]=="1"){
To wrap up this section, we’ll look at how the board is serialized into a string for storage in the app data store.The function getGameMovesString takes care of those details by interpreting the game board from currentGame and turning it into a pipedelimited string. Remember that the opponent is always considered the computer. /** * Serializes the board information into a string for storage */ getGameMovesString: function(game){ var i=0; var result = ""; for(i=0; i < 9; i++){ if(game.moves[i] === undefined || game.moves[i] == "" || game.moves[i] == null){ result += "|"; } else{ if(game.moves[i] == TTT.Players.Computer){ result += "0|"; } else{ result += "1|"; } }
143
144
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
} return result; }
Finishing and Clearing a Game Finishing a game ends up being a complex problem. If you remember the earlier discussion, we had two options to handle finishing and clearing a game.The first was through a complex three-way handshake.The second was to allow for manual clearing. We’re going to implement the latter. 1. The first step is to add in some methods for removal of a game.This deletes the current client snapshot of the app data store games, saves an updated app data store key back to the server, and clears currentGame. Add the three methods removeGame, clearGame, and clearAllGames to the TTT.AppDataPlay object: removeGame: function(opponentId){ if(this.myGameAppData && this.myGameAppData.activeGames != null){ var items = this.myGameAppData.activeGames; var newItems = []; var i; if(items.length){ for(i=0; i < items.length; i++){ if(items[i].opponentId != opponentId){ newItems.push(items[i]); } } } else{ //Is object var x; for(x in items){ if(items[x].opponentId != opponentId){ newItems.push(items[x]); } } } } this.myGameAppData.activeGames = newItems; this.updateStoredGameObject(); }, clearGame: function(opponentId){ //debugger; this.cancelPolling();
2. Create two buttons to trigger both the clearGame and clearAllGames methods:
3. Now we have to modify the selectedOpponentDataCallback function to recognize when one person has started a new game.The following code listing demonstrates identifying a winner. It is the first thing checked after identifying that the opponent’s current game is not null with the call if(thisObj. myOpponentCurrentGame != null). Download the full app code for Chapter 7 from our Google Code listing (http://code.google.com/p/opensocialtictactoe/ source/browse/#svn/trunk/chapter7) if you wish to see it in place. var isNewGame = false; if(myGameInfo.winner != 0 && thisObj.myOpponentCurrentGame.moves < 3){ thisObj.removeGame(thisObj.currentOpponentId); myGameInfo = thisObj.getCurrentGameObject(thisObj.currentOpponentId); isNewGame = true; latestGameInfo = myGameInfo; } else if(thisObj.myOpponentCurrentGame.winner != 0){ isNewGame = true; latestGameInfo = myGameInfo; }
145
146
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
“Real-Time” Play We use quotes to signify the air quotes we’d use if this conversation was happening face to face. It’s not actually real-time play.That would require an event-routing system, which we don’t have at our disposal in this design. Instead we’ll set up a polling system so that the game can periodically update without reloading the page. A simple polling design sets a time-out after the player makes a move.This time-out continues to retrigger at intervals until the code detects that the other player has made a move and control has returned to the Viewer. In practice, however, a polling design requires numerous safety checks and throttling mechanisms. Throughout this section we’ll talk about each part of the code in turn.You may also have noticed calls to the polling triggers in the code we’ve introduced already.We won’t be reviewing those calls in detail but will leave it to the reader to explore how they all interact in depth. Before we start, there are two constraints to put in place.The first is the polling interval.The second is the maximum number of times to retrigger the poll test. If a user walks away from the machine for half a day, we don’t want the game to continue polling every ten seconds, so we set a maximum count it can trigger before getting a reply. /** * Time (in seconds) that the system will wait * between polling to see * if the opponent has made a move */ OPPONENT_POLLING_TIME: 10, /** * Maximum number of times the poll will fire * without a response before * polling stops */ MAX_POLLING_COUNT: 100,
The first function to explore is startGameplayPolling. It’s the initial trigger to begin the polling system.The following is the first design we worked on: startGameplayPolling: function(){ gadgets.log("starting Poll"); if(!this.currentOpponentId){ return; } this.gamePollTimer = window.setTimeout(function(){ TTT.AppDataPlay.gamePollTimer = 0; TTT.AppDataPlay.pollForOpponentUpdatedMove();
This code sets the initial time-out trigger, which in turn calls the function pollForOpponentUpdatedMove.The pollForOpponentUpdatedMove function makes
a call to re-retrieve the opponent’s app data and reinitialize the game. pollForOpponentUpdatedMove: function(){ var thisObj = TTT.AppDataPlay; thisObj.gamePollTimer = null; thisObj.isPollingForActiveGame = false; thisObj.getFriendGameData(thisObj.currentOpponentId); if(thisObj.isPollingForActiveGame && thisObj.currentGamePollCount++ < thisObj.MAX_POLLING_COUNT){ thisObj.startGameplayPolling(); } gadgets.log("Polling fired: " + thisObj.currentGamePollCount); }
Within the code path of getFriendGameData is a trigger to cancel the polling.This involves clearing the time-out handle we saved in startGameplayPolling and resetting all the flags and counters. cancelPolling: function(){ window.clearTimeout(TTT.AppDataPlay.gamePollTimer); TTT.AppDataPlay.isPollingForActiveGame = false; TTT.AppDataPlay.currentGamePollCount = 0; }
In practice, this design worked. But we noticed that the updates were happening quite fast, much faster than the ten seconds that was set earlier. As it turned out, there were several user interaction code paths that would trigger multiple timers to be active. And the timers seemed to stack up and grow in number.This called for adding an if block to simply kill timers if a previous timer was already doing the job.The following code was added to startGameplayPolling: if(this.gamePollTimer && this.gamePollTimer > 0){ return; }
Unfortunately, this was too heavy-handed, as the timers would simply die out in some situations.To combat this, we added an additional counter to allow the trigger to sleep a few times before triggering the die code path: if(this.gamePollTimer && this.gamePollTimer > 0){ if(TTT.AppDataPlay.pollSleepCount++ > 5){
147
148
Chapter 7 Flushing and Fleshing: Expanding Your App and Person-to-Person Game Play
TTT.AppDataPlay.pollSleepCount = 0; return; } }
That seemed to work.We may have been able to get there more cleanly by using window.setInterval instead of a retriggering time-out.We’ll leave it to the reader to
explore different permutations of this design. Asynchronous processing, as many of you may know, can be a complicated problem.
Advantages and Disadvantages of App Data P2P Play There are a few advantages and a few disadvantages to this kind of architecture for P2P game play. First, let’s go with the advantages: n n
There’s no infrastructure required for your game to operate. There are no explicit scaling considerations—the problem of scaling the game is pushed onto MySpace.
Now let’s consider the downsides of this technique: n n
n
Logic can be much more complex than in a shared read/write database. The app data store can be read and written to by anyone with a little knowledge of Firebug, essentially allowing a malicious user to cheat. If your app takes off, it could flatten the MySpace app data infrastructure.
Oh, we didn’t mention that before? App data in its current state is not explicitly designed for high-volume read/write operations. Its initial conception was more for storage of settings, such as background colors and default fonts.There is much discussion within the spec group (those involved with the development of the OpenSocial spec) about the shortcomings of the app data store. Currently the main discussions center around both a private (not friend-readable) store and a real-time messaging system. As always, you can download the source code for this chapter from the Google Code site, http://code.google.com/p/opensocialtictactoe, or directly from the source repository at http://code.google.com/p/opensocialtictactoe/source/browse/#svn/trunk/chapter7. We looked at a lot of the code in small pieces, so it may be helpful to see it all together.
Summary P2P game play is one of the most attractive aspects of social gaming. If a user wants to play a game on his or her own, a multitude of casual and not-so-casual gaming sites are available online. Most of them have games of much greater polish and budget than are found on social networks. It is the social interaction aspect that makes building P2P games on MySpace interesting.
Summary
The technique we’ve shown in this chapter allows you to explore P2P apps, even beyond games, without an infrastructure investment. However, Caveat emptor—let the buyer beware.You will be limited in what can be done, and if the MySpace infrastructure hiccups, your app will be left out in the cold. In the authors’ opinion, you will be time and money ahead if you explore using an external database, such as those discussed in the chapters on external iframe apps (Chapter 9) and persisting information (Chapter 4). Making a system do something it wasn’t designed to do, while technically interesting, is not always the most prudent path. Note Code listings and/or code examples for this chapter can be found on our Google Code page under http://opensocialtictactoe.googlecode.com.
149
This page intentionally left blank
II Other Ways to Build Apps
8
OAuth and Phoning Home
9
External Iframe Apps
10
OSML, Gadgets, and the Data Pipeline
11
Advanced OSML: Templates, Internationalization, and View Navigation
This page intentionally left blank
8 OAuth and Phoning Home U
p until this point our app has lived in, and been hosted on, MySpace’s servers. MySpace provides a wide range of services that allow quite a few apps to function perfectly well completely in that environment.You can provide markup that will render out to the page, save data in the app data store, send various types of messages, and fetch information on your users—all within the confines of MySpace—and possibly most important, all for free. However, there are some limits to what you can accomplish with this. For example, the app data store has some severe limitations.There are inherent security concerns (see Chapter 13 for details) and a 1kB limit imposed on each item saved. And what about larger apps that may have gigabytes of data that needs to be properly indexed and stored in a relational database? Obviously the app data store can’t meet all of these needs. Also, there are the submission and approval headaches brought on each time you want to update your app, and the lack of complete control over the rendering surface, as MySpace wraps a header and footer around your app’s code. This chapter and the following one will show you how to get around these limitations by hosting some or all of your app on external servers.There are many hosting services out there with a wide range of price and performance, but for this chapter we chose to use one of the “cloud-computing” services: Google App Engine. Using OAuth, we will demonstrate how to validate calls made from your app to your servers using the gadgets.io.makeRequest functionality.This allows you to host a database on an external server but still be able to safely communicate between your app hosted on MySpace and your servers hosted on Google App Engine.
What Is OAuth? OAuth is the authentication mechanism used by the MySpace API servers.The oauth.net site summarizes OAuth as “an open protocol to allow secure API authorization in a simple and standard method from desktop and web applications.” MySpace gives each app a consumer key and a secret key; only MySpace and the app know the secret key.With knowledge of the secret key you can use the protocols defined
154
Chapter 8 OAuth and Phoning Home
by OAuth to either construct or validate requests. An app uses the secret key to construct a request to MySpace. MySpace uses the secret key to validate that the request actually came from the app. Because a user can deny or grant various permissions to an app, it is very important that MySpace be able to confirm that a request actually came from a specific app. The typical flow is something along the lines of the following: 1. A user installs an app, thereby granting it certain permissions. 2. The user navigates to the app’s Canvas page to use it. 3. The app builds an OAuth request to fetch data from MySpace, such as the user’s friend list. 4. MySpace receives the request and uses OAuth to validate that the request came from the app. 5. MySpace then looks up what permissions that user has granted to that app. If the user has granted access to the requested data, it is returned to the app. Otherwise a 401 permission-denied error is returned. 6. The app receives either the data or an error.
OAuth Libraries You can dig into the fairly complicated guts of OAuth if you’d like to know more; a good starting point is http://oauth.net. Because OAuth is an open protocol, anyone is free to implement it and release it for everybody else to use. That means there are libraries out there for use, written in almost every language you can think of. One of the great tenets of software development is code reuse, so to us it doesn’t make sense to reinvent the OAuth wheel for the purpose of this book.We will be using an OAuth library written in Python to communicate securely with MySpace, and we suggest you do the same. Not only is it a huge time-saver, but it abstracts all the complexity of OAuth and MySpace’s APIs. MySpace provides several software development kits (SDKs) for accessing its APIs. From the following link you can find libraries written in PHP, Python, Ruby, C#, and Java: http://code.google.com/p/myspaceid-sdk/. We will be using the Python SDK found through that site.
Setting Up Your Environment Your mileage may vary depending on what language you are writing in, but the process is essentially the same for all languages, so we hope this Python example will help you along even if you choose a different SDK. First, create a working folder somewhere in your file system, say, c:\working.We’ll refer to this as your root folder from here on out, or .
What Is OAuth?
Next, you’ll need to do some setup work.We suggest the following: 1. Download and install TortoiseSVN, which is a Subversion client. Subversion is an open-source version control system and is used by Google Code, where the MySpace SDKs are hosted, to provide convenient access to source code. Installation instructions and downloads can be found at http://tortoisesvn.tigris.org/. 2. Sign up for Google App Engine (GAE) and download the GAE SDK. GAE is a “cloud-computing” service provided by Google.You upload your source code to their servers and they do all the rest. As of this writing, small amounts of usage of the service are free, but this could change. The GAE SDK provides a useful test Web server that you run on your local computer in order to simulate the GAE production environment.The signup link for GAE can be found at http://code.google.com/appengine. 3. Get a good Python editor, such as Aptana with the Pydev plug-in. A good Python editor will give you syntax highlighting and some other niceties.The links to Pydev and Aptana are www.aptana.com/python and www.aptana.com/ respectively. 4. Downloand and install Python, which is required for GAE.You can download Python at www.python.org. Okay, you’re now ready to write a GAE app! Using either Subversion (http://myspaceid-python-sdk.googlecode.com/svn /trunk/) or the Google Code UI, download the file ckeynsecret.py (found in http://myspaceid-python-sdk.googlecode.com/svn/trunk/myspaceid-python-sdk/samples/google-app-engine/oauth/) and the myspace and oauthlib folders (and their contents) into your working root.You should now have a directory structure that looks something like the following: \ckeynsecret.py \myspace\ \oauthlib\
with various Python .py files in the two myspace and oauthlib folders.The file ckeynsecret.py holds your consumer key and secret in a centralized place so that it can be reused throughout your code.To find out what your keys are, navigate to http://developer.myspace.com, click the My Apps tab, find the relevant app in the list, and click Edit Details.You’ll be taken to the Edit App Information screen; scroll down to the OAuth Settings section as shown in Figure 8.1. Your consumer key is clearly listed because it’s not a secret. It’s actually just the URL for your app’s Profile page by default.To view your secret, check over both shoulders to make sure nobody’s watching (optional) and click the Show OAuth Consumer Secret button to reveal your secret (as shown in Figure 8.2).
155
156
Chapter 8 OAuth and Phoning Home
Figure 8.1
OAuth settings on the Edit App Information screen
Figure 8.2
Revealed OAuth consumer secret
That reveals your secret for the world to see. If someone were to steal your secret, he or she could make requests to the MySpace servers posing as your app and also make seemingly authentic requests back to your servers. It’s best to keep this secret a secret; don’t stick it in a publicly accessible text file on your servers or anything like that. Copy and paste those values into the CONSUMER_KEY and CONSUMER_SECRET variables in the file ckeynsecret.py. One last change needs to be made before you’re all set up.The MySpace SDK expects you to download the simplejson folder as well, so the library can use it.You can go that route if you’d like, and if you did, skip this next part. But since we’re using GAE, which comes with the Python framework Django, which in turn comes with simplejson, it seemed redundant to us to download it and link it again. 1. Open the file \myspace\myspaceapi.py. 2. Change the line import simplejson to from django.utils import simplejson. Note Google App Engine (GAE) provides some convenient tools to help you when developing your app, but its uses could probably fill another book, and this is an OpenSocial book. To learn more about GAE and what it can do for you, go right to the source: http://code.google.com/appengine/docs/python/gettingstarted/.
Secure Phone Home
The Getting Started guide will help you get over that initial learning curve, and it’s what we used to familiarize ourselves with the subject. If we can do it, so can you. It’s really quite simple and will get you right up to speed. The guide will get your development Web server running, show you how to create an app, give you some tips on what GAE provides, and walk you through how to publish the app. If you’d like to follow along, we recommend you create a new app (rooted in your working root folder) and add a new app.yaml file into it. Your directory structure will now include \app.yaml. See the Getting Started guide for details, but the app.yaml file essentially maps incoming HTTP requests to the correct Python script file.
When Is OAuth Not OAuth? Now, that may seem like a lot to get up and running, and it is, but unfortunately there’s some more to do.You see, there are some quirks with MySpace’s implementation of OAuth. By quirks we mean incompatibilities with the OAuth spec. The result of this is that you can’t really use the OAuth library that’s bundled with the MySpace Python SDK, because those little differences will shine through.We found that the best way to work around this was to use a slightly modified version written by Eric Van Dewoestine. His Python library has two nice features: First, the OAuth library has been modified to accommodate MySpace’s quirks, and second, the function to verify incoming signed requests has been greatly simplified. Here’s how we did it: 1. Navigate to the library home: http://code.google.com/p/myspace-python/. 2. Download Dewoestine’s oauth.py file and replace the one from MySpace. So http://myspace-python.googlecode.com/svn/trunk/myspace/oauth/oauth.py replaces \oauthlib\oauth.py. 3. Download a simplified verify function from http://myspace-python. googlecode.com/svn/trunk/myspace/app.py and save it to \myspace\app.py. That’s it.We know it’s a lot of steps, information, downloads, and miscellaneous stuff from all over the place, but once you get correctly set up, it all flows very easily.Take solace in the fact that if you’ve come this far, the hard part is over.
Secure Phone Home In Chapter 6 we discussed using gadgets.io.makeRequest to make requests to external servers. In that chapter we made requests only to public servers and public APIs that we didn’t own. Using other people’s services has its advantages, but sometimes you need to store your own data or write your own Web service. So, in this section we’ll use gadgets.io.makeRequest to make a request back to our GAE server, first with a simple GET request, and then with a more complex POST request using OAuth for verification.
157
158
Chapter 8 OAuth and Phoning Home
Unsigned GET Request Ah, the GET request. So simple, so elegant. GETs are intended to be concise requests for data from a server, and in our Tic-Tac-Toe app we’re going to start saving the win/loss record for players.We’ll use the POST request described later to update the win/loss record at the end of each match and a GET request to fetch the user’s record to display on screen. When storing data into the GAE data store (not to be confused with MySpace’s app data), you must define a model for the data.You’re essentially defining a class and describing its members in a way that the GAE database can understand. In our case we’re saving the win/loss record for a user, so we need to define a model that accommodates that. Since we don’t expect our app’s code base to get too large, and a win/loss record is a fairly simple data model, we’re throwing all shared entities into a single file in the working root: \entities.py. Right now it just defines what a WinLoss looks like: from google.appengine.ext import db class WinLoss(db.Model): wins = db.IntegerProperty() losses = db.IntegerProperty()
The first line imports GAE’s data store API.We then create a WinLoss class whose ancestor is the db.Model class. All models in GAE need to have db.Model as an ancestor.The next two lines define two properties, both integers, one called wins and the other, losses. See Table 8.1 for a list of supported GAE data store properties. Once the model was defined, we created a folder in our working root and added a script file to it: \ws\ws.py. Inside your app.yaml file, add a handler for this new script; don’t forget to change your application name as necessary: application: opensocial--tic-tac-toe version: 1 runtime: python api_version: 1 handlers: - url: /ws script: ws/ws.py
Let’s look at the code for ws.py, which right now handles an incoming GET request: import myspace.app from django.utils import simplejson from google.appengine.ext import webapp
Secure Phone Home
Table 8.1
Types and Property Classes*
Property Class
Value Type
Sort Order
StringProperty
str
Unicode (str is treated as ASCII)
Unicode ByteStringProperty
ByteString
Byte order
BooleanProperty
bool
False < True
IntegerProperty
int long
Numeric
FloatProperty
float
Numeric
DateTimeProperty DateProperty TimeProperty
datetime.datetime
Chronological
ListProperty ringListProperty
list of a supported type
If ascending, by least element; if descending, by greatest element
ReferenceProperty SelfReferenceProperty
db.Key
By path elements (kind, ID or name, kind, ID or name ...)
UserProperty
users.User
By e-mail address (Unicode)
BlobProperty
db.Blob
(not orderable)
TextProperty
db.Text
(not orderable)
CategoryProperty
db.Category
Unicode
LinkProperty
db.Link
Unicode
EmailProperty
db.Email
Unicode
GeoPtProperty
db.GeoPt
By latitude, then longitude
IMProperty
db.IM
Unicode
PhoneNumberProperty
db.PhoneNumber
Unicode
PostalAddressProperty
db.PostalAddress
Unicode
RatingProperty
db.Rating
Numeric
*This chart is taken from work created and shared by Google (http://code.google.com/appengine/ docs/python/datastore/typesandpropertyclasses.html) and used according to terms described in the Creative Commons 2.5 Attribution License (http://creativecommons.org/licenses/by/2.5/).
These lines pull in all the external functions and data structures that we’ll need. For the most part these are standard GAE modules, along with simplejson, which is used frequently throughout to manipulate JSON strings.To include the modules you’ve downloaded, you use the relative path to the .py file from your working root. Because we have a folder called oauthlib in our root, and in that folder is oauth.py, we can import that module using oauthlib.oauth. Now let’s jump to the end of that file: def main(): application = webapp.WSGIApplication([
Secure Phone Home
('/ws', WebService), ], debug=True) util.run_wsgi_app(application) if __name__ == ‘__main__': main()
Again, this is pretty standard stuff; you’ll have something similar to this in all your GAE Python scripts.This defines the request handler, WebService, and maps it to the URL /ws.You’ll want to remove the debug=True when you publish your app. Now, the meat: class WebService(webapp.RequestHandler): def get(self): id = self.request.get('id') try: int(id) except: error = {'errorCode':'invalidInput','errorMessage':'Invalid ID'} self.response.out.write(simplejson.dumps(error)) return key = db.Key.from_path('WinLoss', 'id_' + id) wl = db.get(key) if wl is None: error = {'errorCode':'inputNotFound','errorMessage':'ID not found'} self.response.out.write(simplejson.dumps(error)) return else: success = {'wins':wl.wins,'losses':wl.losses} self.response.out.write(simplejson.dumps(success))
Here we define the Web service that will handle all GET requests that are received at the URL /ws.We’re expecting a user ID to be passed in, so we first pull that from the query string. As always, it’s important to validate inputs, so we then try to parse the incoming ID to an integer. If it doesn’t parse, it was either bad data or a malicious attack. In that case we simply return an error. Note that this is a JSON Web service, so we build a JSON object and then output that using the simplejson library. Once the ID has been vetted, we start our database work. GAE keys database entries by both a model and a key.That means that we could use the key "id_6221" for both the model WinLoss and the model GameState. In this case the model is WinLoss, and our key is the string "id_" plus the passed-in ID. We create a Key object by calling db.Key.from_path with the model name and the key.The function db.get then goes to the data store and fetches our data.The variable
161
162
Chapter 8 OAuth and Phoning Home
wl is now an object of either type WinLoss if the key was found in the data store or None if it wasn’t.
If it wasn’t found, we return an error object. Otherwise we return the data. Note The example GET JSON Web service is a fairly simple one. The GAE data store is deep and complex and we barely scratched the surface; in fact, this whole chapter could be devoted solely to that one topic. But we have other things we’d like to discuss, OpenSocial things. As your app becomes more and more complex, you’ll probably want to take advantage of the more advanced features that the data store provides, such as GQL. GQL is a query language that looks an awful lot like SQL. To give you a starting point, if we wanted to fetch a list of the top 100 Tic-Tac-Toe players, we could do something like this: winners = db.GqlQuery("SELECT * FROM WinLoss ORDER BY wins DESC LIMIT 100") Again, see the GAE Getting Started guide for details.
Signed POST Request A POST is typically used when you need to save some data. In our Tic-Tac-Toe app, we’re using it to update the win/loss record for a player. In this case we’re going to sign the request using OAuth so that we can verify which user sent the request.We need to do this because a smart hacker could sniff your app’s network traffic using a program like Fiddler, see what parameters you pass to what URL, and then try to spoof requests. In this manner our increasingly nefarious hacker could send a bunch of losses to his or her arch-nemesis. So how does OAuth help prevent this? Here’s the typical flow for a signed makeRequest: 1. A call to gadgets.io.makeRequest in an app generates an Ajax-style request to the MySpace proxy, passing up a custom MySpace token. 2. The proxy takes this token and decrypts it, obtaining the Viewer’s ID, the Owner’s ID, as well as the app’s ID.The proxy can now be sure the request was initiated in the context of that app, with that particular Owner and Viewer, as that token is impossible (or cryptographically “hard”) to spoof. 3. An OAuth signature is then constructed using the following values: n n n
Consumer key Consumer secret Nonce, or a “number used once,” which is just a random number used to prevent replay attacks
Secure Phone Home
n n n n n n n n
Timestamp, used to invalidate a request after a certain period of time Signature method, always the HMAC-SHA1 algorithm Version, always 1.0 at the time of this writing Request method, like GET or POST Owner ID Viewer ID Resource URL, essentially the URL you are requesting in your makeRequest Any other parameters you may have passed in, such as "id=6221"
4. The signature generated using the OAuth protocol is then passed to your server, along with all the values used to generate it. 5. Using the OAuth protocol, your server creates a signature using the passed-in values. If the signature you build is equal to the signature that was passed to you, you know all the values you received are legit. You can now be confident that the Owner ID and Viewer ID that were passed to you are the actual users who generated the request. So back to our malicious hacker. He (or she) has installed our Tic-Tac-Toe app, is sniffing our network traffic, and knows all of our URLs and the parameters we use. If he used Firebug to generate a gadgets.io.makeRequest request to our servers, he’d still be passing in his own token. So when the proxy decrypted the token, it would find our hacker’s ID, and not the ID of his worst enemy. Let’s say the hacker got smarter, and instead of spoofing the request from the app to the MySpace proxy, he spoofs the request from the proxy to our servers.To do this, he signed up to be an app developer on MySpace, set up a Web service, created an app, and sent a signed makeRequest to it. He can then investigate all the parameters that were sent to his Web server from the proxy so he can send the exact same parameters, with slightly altered values, in a request to our Tic-Tac-Toe servers. He’d see something like this (imagine it all on one line): ?oauth_consumer_key=http%3a%2f%2fwww.myspace.com%2f452451983& oauth_nonce=633709335062612050& oauth_signature=HsFBfrkYqfVC1Vbg%2bPoDilQ%2bxwY%3d& oauth_signature_method=HMAC-SHA1& oauth_timestamp=1235336706& oauth_version=1.0& opensocial_owner_id=183399670& opensocial_viewer_id=183399670
Well, most of those values he can spoof. He knows the consumer key is just the URL of the app’s Profile, which is publicly available. He can make up a nonce and a timestamp.The signature method and the version are static. And he definitely knows the user ID of his worst enemy. But the one thing he’s missing is the consumer secret.Without
163
164
Chapter 8 OAuth and Phoning Home
our secret he can’t generate the signature. But he can use some other secret to generate a signature. Let’s say he does so.The preceding OAuth parameters would arrive at our servers along with a signature built using the hacker’s secret.We’d then generate a signature using our own secret. And because the secrets are different, the signature we build won’t be the same as the one he passed in. So, we’d reject the request as a fake. And since our hacker is a smart one, he’ll know he’s been beaten. Tip The recommendations for when to use a GET request and when to use a POST request are nice in theory, but they do have real-world implications. Most GET requests can be rewritten as POST requests, in the OpenSocial apps world anyway. There are some limitations to what GET can do, such as the hard limit on the number of characters you can use to construct a URL that a POST would overcome. But for most requests they can be functionally equivalent. We have actually observed that large apps that are makeRequest-heavy are able to save a significant amount of bandwidth by switching all POST requests to GET requests. We’ve even heard reports of up to a one-third savings in bandwidth. The overhead of a POST request, sent across the wire millions of times, adds up. Switching to GET actually can make a difference where it matters most: the pocketbook. It’s something to keep in mind when choosing verbs.
With the theory out of the way, let’s look at some code.This POST handler would go into our ws.py script under the previous GET handler (and before the definition for “main”): def post(self): debug = False if debug is True: output = '' params = self.request.GET for p in params: output += p + '=' + params[p] + '&'
self.response.out.write(simplejson.dumps(error)) return qs = self.request.GET secret = '349782a9...' # If we told, it wouldn't be a secret! method = 'POST' url = 'http://opensocial—tic-tac-toe.appspot.com/ws' key = 'http://www.myspace.com/459809216' try: valid = myspace.app.verify_request(key, secret, method, url, qs) except OAuthError, mse:
self.response.out.write(simplejson.dumps({'error':mse.message})) return if valid is False: error = {'errorCode':'invalidInput','errorMessage':'Invalid sig.'} self.response.out.write(simplejson.dumps(error)) return key = db.Key.from_path('WinLoss', 'id_' + id) wl = db.get(key) if wl is None: wl = WinLoss(wins=0,losses=0,key_name='id_'+id) if win: wl.wins += 1 else: wl.losses += 1 wl.key_name = id wl.put() success = {'success':'Record updated','wins':wl.wins,'losses':wl.losses} self.response.out.write(simplejson.dumps(success))
This function starts off by checking a debug flag. If the flag is set to True, the endpoint simply loops through and outputs each parameter on the query string.The proxy appends the OAuth parameters to the query string, so by setting debug to True you can see exactly what is being sent to your servers. After the passed-in ID is checked, we start building the signature. self.request.GET returns a dictionary of all the query string parameters.The consumer secret, key, method, and resource URL are all hard-coded in, as they won’t change. Then the fun begins.We pass the key, secret, method, URL, and all the query string parameters into the verify_request function referenced from the myspace.app
165
166
Chapter 8 OAuth and Phoning Home
module.That function abstracts all the OAuth magic and either throws an exception if the signature couldn’t be generated, returns True if the signatures matched, or returns False if they didn’t. Once the request is verified, we can update our data store.We either fetch the existing WinLoss model for the user, or, if it’s not found, we create a new model with no wins or losses. After the win/loss record is updated, we set the key_name property of this WinLoss instance to the ID of the user.Then, to save the data to the data store, we simply call wl.put(). Note By hooking up OAuth to our POST request, have we completely prevented malicious users from spoofing requests? Well, no, we haven’t. A user could send a fake request to our servers that would give him or her a win without having earned one. Because the request came from the correct user, the request would look legitimate. In a case like this, the only way to get around the problem would be to maintain the state of the game on the server. When the game starts, you send a request back to your servers saying so. Each time a player makes a move you send it back and validate that it’s a legal move to make, given the rules of the game. Instead of sending a “This player won” request back to your servers, you would send only moves the players made. The server would then respond with “This player won” if the situation warrants it. In general, don’t perform any important game logic on the client, because it can all be hacked; do it on the server instead. For example, don’t roll dice on the client; send a request to your server to roll dice and respond with the result.
Testing Your OAuth Implementation Locally Testing your OAuth implementation locally may seem somewhat easy, but it’s actually very tricky.The OAuth protocol is quite picky; you can’t miss a single parameter, or even the capitalization of a single parameter. So to work our way through this, we set up a test page. First add the following handler, which should later be removed, to your app.yaml file: - url: /test static_files: index.html upload: index.html
Now when you navigate to the URL /test on your local GAE Web server (by default http://localhost:8080/test), the static file index.html will load. Let’s take a look at that index.html file: <script type="text/javascript" src= "http://ajax.googleapis.com/ajax/libs/prototype/1.6.0.3/prototype.js"> <script>
function cbgood(response){ document.getElementById("output").innerHTML = response.responseText; } function cbbad(response){ document.getElementById("output").innerHTML = response; }
The file provides two functions that exercise both the GET and the POST handlers of ws.py using some dummy data. We pull in the prototype.js library to make it easier to make Ajax calls.The GET request is straightforward. A request is made and the correct value should be returned and output onto the screen. The POST, of course, is a bit trickier.You’ll see that we create a string variable called post_data and load a bunch of dummy data into it. All the data was once sent by the MySpace proxy to an external server; we captured the data and now use it as test data.You see, in the wild the parameters are constantly changing, so we use static dummy data to first get OAuth verification working locally. Once it’s working, it’s time for the real world. You probably noticed that there are two sets of data.The difference between the two is that the first has no additional query string parameters, whereas the second does ("a=b" in this case).You need to be able to check both scenarios; the first is simpler because you’re sending only OAuth parameters on the query string. It’s a good idea to get this one working first. In the second scenario we’re sending up our own custom parameters.These could be anything. In our Tic-Tac-Toe app we use the query string to send up whether the player won or lost a game, for example. But since the OAuth signature changes based on your custom parameters, you need to check to see if you can build the correct signature in this case, too. You’ll need to make a couple of changes to the ws.py file.We hard-code in four parameters: the secret, key, URL, and method.You should change those to the following: 1. 2. 3. 4.
Secret: 8d804c4ffe6c4f46bf5c040c3e9c0e21 Key: www.myspace.com/452451983 URL: http://mstari.org/Default.aspx Method: GET
Secure Phone Home
Then comment out (using the # character) the lines where we respond with an error object and return from the function if the ID doesn’t parse correctly. Then this: try: int(id) except: error = {'errorCode':'invalidInput','errorMessage':'Invalid ID'} self.response.out.write(simplejson.dumps(error)) return
And finally, since this data was generated a while ago, comment out the timestamp check in the myspace\app.py file; it can be found on line 218: #_check_timestamp(timestamp)
Once you confirm that everything is working locally, revert the changes made previously and update your live GAE app.You’re ready for the big time now.
Making Real MySpace Requests Getting everything running locally is great, but what really matters is getting everything running on MySpace. In our Tic-Tac-Toe app we added a TTT.Record.Getter object to handle the fetching of records and a TTT.Record.Poster object to handle the updating of records. The Getter object fires off a makeRequest when the app’s body has finished loading. When the response comes back, the UI is updated with the player’s win/loss record. gadgets.util.registerOnLoadHandler(TTT.Record.Getter.initialize);
That will hook the following function to the body’s onload event: // Entry point for fetching records initialize:function(){ // Get the URL params var params = gadgets.views.getParams();
169
170
Chapter 8 OAuth and Phoning Home
// Get the ID of the current Owner. There is a bug // here in MySpace; it's ownerId on the Home view // but ownerid (note lowercase) on Profile var id = params.ownerId || params.ownerid; // Build the URL using var url = "http://opensocial—tic-tac-toe.appspot.com/ws?id=" + id; // Make the request; opt_params isn't required as the // default parameters, such as GET, will be fine gadgets.io.makeRequest(url, TTT.Record.Getter.got_init); }
First we access the app’s current parameters to get the Owner’s ID. Note the bug where the Owner ID can be stored in either ownerId or ownerid (the second version has a lowercase i). Another thing to note, for those who are building apps for multiple social networks, is that providing the Owner and Viewer IDs like this is MySpacespecific and not in the OpenSocial spec. Then the URL is built using the discovered ID and is passed into gadgets.io. makeRequest.That makes a request back to our servers, which is handled by ws.py.The response is created and passed back to our app. // The response from the initial makeRequest // is handled here got_init:function(response){ // First check if there was an error back from the proxy if(!response || (response.errors && response.errors.length > 0) || response.errorCode){ TTT.Record.Getter.retryRequest(); } else{ // Response came back successfully; it should be // a JSON string, so parse it into an object response = gadgets.json.parse(response.text); // If the response was parsed correctly, do some work on it if(response){ // Check if the server returned an error if(response.errorCode){ // Probably means the server couldn't parse // the owner's ID that was passed in; maybe
Secure Phone Home
// a hack attempt? if("invalidInput" === response.errorCode){ TTT.Record.Getter.retryRequest(); } // If the ID wasn't found in the data store, that // means this player hasn't played any games yet, // so just set everything to 0 else if("inputNotFound" === response.errorCode){ // Display the record TTT.Record.Getter.showRecord({"wins":0,"losses":0}); } // Something unknown happened ... else{ TTT.Record.Getter.retryRequest(); } } // Check if the server returned a successful response else if(response.wins && response.losses){ // Display the record TTT.Record.Getter.showRecord(response); } // Otherwise, the server returned an unknown response else{ TTT.Record.Getter.retryRequest(); } } // The server returned some unknown response that couldn't be // parsed into a JSON object else{ TTT.Record.Getter.retryRequest(); } } }
We first check the response for errors generated by MySpace and, if they’re found, attempt to retry the request up to three times. Assuming no errors, we know our Web service was accessed and a response was generated. Since our Web service returns JSON, we specify the CONTENT_TYPE of gadgets.io.ContentType.JSON in the makeRequest call to get the response into an object representation. Once that’s done,
171
172
Chapter 8 OAuth and Phoning Home
we check for errors again. This time any errors would come from our Web service itself, and not MySpace. If the response was an “input not found” error, we know this user doesn’t yet exist in our data store, so the user must have zero wins and losses. If no error was found, we have legitimate data to display, and the actual wins and losses for the user are displayed. Now let’s take a look at the POST request: // Post the result of the game post:function(win){ if(win) this.win = win; // Get the URL params var params = gadgets.views.getParams(); // Get the ID of the current Owner; there is a bug // here in MySpace—it's ownerId on the Home view // but ownerid (note lowercase) on Profile var id = params.ownerId || params.ownerid; // Build the URL using var url = "http://opensocial—tic-tac-toe.appspot.com/ws?id=" + id; if(win){ url += "&win=1"; } // Set the makeRequest to use signed authentication and // the POST method var opt_params = {}; opt_params[gadgets.io.RequestParameters.METHOD] = gadgets.io.MethodType.POST; opt_params[gadgets.io.RequestParameters.AUTHORIZATION] = gadgets.io.AuthorizationType.SIGNED; opt_params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON; // Make the request gadgets.io.makeRequest(url, TTT.Record.Poster.got_post, opt_params); }
This is similar stuff to what we did before.The big difference here is that the default parameters for the makeRequest won’t suffice.We create the opt_params object to store our nondefault parameters, which are the method POST and the authorization type
Spicing Up the Home and Profile Surfaces Using makeRequest
SIGNED.This tells the MySpace proxy to sign the request, append the OAuth parameters,
and send out the request as a POST. When the call comes back, we do the usual things, such as check for errors and retry the request if necessary and able. If no errors were found, we update the UI with the new win/loss record. // Callback handler for the POST request got_post:function(response){ if(response){ var obj = response.data; // If it parsed, and has a success field, // the POST succeeded; update the record if(obj && obj.success){ TTT.Record.Getter.showRecord(obj); } // Otherwise, retry else{ TTT.Record.Poster.retryRequest(); } } else{ TTT.Record.Poster.retryRequest(); } }
Spicing Up the Home and Profile Surfaces Using makeRequest Up until now we haven’t paid too much attention to the Home and Profile surfaces. But at this point we can start to include some interesting things on those surfaces using makeRequest. On our Home and Profile we’re going to add the Owner’s win/loss record in a table. On the Home page we’ll simply provide a link to take users to the Canvas surface so they can start playing. On the Profile we’ll display a challenge message to anyone viewing the Profile and provide a button to take users to the Canvas page where they can install it (shown in Figure 8.3). The code is very similar to what was listed previously for the Getter, so we won’t repeat it here. As always, check out http://code.google.com/p/opensocialtictactoe for full code listings.
173
174
Chapter 8 OAuth and Phoning Home
Figure 8.3
Example of Canvas surface link on a user’s Profile
Summary Being able to communicate securely with an external server has some huge advantages. You can write custom Web services and host large databases.The possibilities are nearly limitless. There are many options out there for hosting providers.We chose Google App Engine mainly because it is free for sites with small amounts of traffic, and free is nice. Amazon Web Service (AWS) is a competing “cloud-computing” service that behaves differently from GAE.With GAE you’re forced to use their Web servers and database back end, write your code in Python or Java, and upload it all to their servers.With AWS you can pick and choose which of their services to use and in what language you write your code. There’s also Microsoft’s Azure, not to mention traditional Web hosting companies from which you can rent physical servers sitting in physical racks.There’s also that spare
Summary
computer you have sitting in your closet, but that probably can’t handle too many concurrent users (see Chapter 13, Performance, Scaling, and Security, to learn how to scale your app). Before deciding on a service, it’s best to shop around and do some research. See what’s easiest, and possibly cheapest, for your new app.Whatever you choose, use the basic principles outlined in this chapter to safely communicate between MySpace and your hosting service. Note Code listings and/or code examples for this chapter can be found on our Google Code page under http://opensocialtictactoe.googlecode.com.
175
This page intentionally left blank
9 External Iframe Apps Tseveral his chapter is essentially an extension of the preceding one. In Chapter 8 we discussed elements of OAuth that are relevant to this chapter and to writing external iframe apps on the MySpace platform. We recommend that if you skipped Chapter 8, go back now and read the sections that describe OAuth. Moving forward in this chapter, we’ll assume you’re familiar with OAuth and with setting up and publishing Web services using Python on Google App Engine (GAE). In this chapter we’ll take our existing on-site app and port it over to an external iframe, or off-site, app. Be prepared, because there’s a pretty large paradigm shift from an on-site JavaScript app to an off-site app. With on-site apps, you supply some markup and possibly a few Web services, and the MySpace platform takes care of the rest.Your markup is wrapped in some headers and footers (mostly script tags that pull in the OpenSocial container) so it can be correctly rendered. When a user accesses the page, the MySpace infrastructure takes care of the actual rendering of the page. When the app starts running, it has very little knowledge of the user; it really knows only the IDs of both the Viewer and the Owner.That means that the app has to send out requests for data when it loads—maybe it requires a friend list, or some details from your servers.These asynchronous requests are sent out while the app idles, perhaps with a loading message and one of those “spinner” graphics twirling away to give the illusion of activity.When the requests come back, the app leaps into action, as it should now have all the data it needs to fully load. That’s the typical flow of an on-site app, but off-site apps have a very different pattern. For an off-site app, MySpace simply renders an iframe onto the page with the src attribute set to the URL of your choice. Since you control the contents of the URL, you have full control over the app. But this control comes at a cost; you are now responsible for the rendering of the app. Off-site apps behave much more like traditional Web pages. A request comes in, then it’s picked up by your Web server and passed off for processing to your scripting language of choice, such as PHP, ASP.NET, or Python. The request is then processed.
178
Chapter 9 External Iframe Apps
Let’s say we send off a REST request to the MySpace API for a friend list and also pull some data out of our local database. The friend list and the data are then pushed down onto the page used as the iframe app. In this case, the app has all the data it needs to fully load at render time. The trade-off here is that on-site apps render immediately, then idle while waiting for data. Off-site apps, by contrast, do their waiting for data up front and then completely load. The other big difference is that with on on-site apps you really have only one page. In our Tic-Tac-Toe app we hide and show various HTML elements to give the illusion of multiple pages. Off-site apps can have multiple actual pages, so markup and JavaScript code can be separated a little more cleanly.Well-known techniques like clicking a button that posts back a form can be used. As you can see, there are some pros and cons to using an off-site app.The biggest reason for using one would be that you have an existing app somewhere on the Web that you could wrap an iframe around and there you go—you now have an instant MySpace app. So, let’s dig a little deeper into what it would take to accomplish that.
REST APIs What exactly is a “REST API”? Well, REST stands for REpresentational State Transfer, which isn’t overly helpful as an actual definition. It essentially comes down to how resources are accessed. The underlying assumption with REST is that objects and records exist in one place, their universal resource identifier (URI), on the Internet and stay there.The address is used to state the “who” of the object we wish to work with. This is coupled with an action verb (for example, GET or POST) in the request to state the “what” of what we wish to do.
How a REST Web Service Is Addressed A REST Web service is addressed via URIs. Say we have a REST Web service that returns data on fruit.To get a list of all the fruit available, we might access a URI like this with a GET request: http://fakefruitsite.com/fruit
Let’s say that Web service returns a list of available fruit, and one of the fruits listed is apple. We might make a call like this to get a list of the types of apples available: http://fakefruitsite.com/fruit/apple
Specifically, we might want to know which local producers grow Granny Smith apples: http://fakefruitsite.com/fruit/apple/grannysmith/producers
And even more specifically, there’s an orchard down the street that we want data on. Each producer would have a unique identifier; this one’s is 12345. We might access the price list for that producer’s Granny Smiths via the following URI: http://fakefruitsite.com/fruit/apple/grannysmith/producers/12345/pricelist
REST APIs
This is an example of a REST API; we access the actual data we want by addressing the URI in a specific way and use a globally unique identifier (GUID) to drill down to specific entities. MySpace’s APIs follow a similar pattern but replace fruit with MySpace users, GUIDs with MySpace IDs, and price lists with friend lists. If you are not used to using REST, this may seem a little strange and low-level, but MySpace provides several SDKs to help you out.The beauty of having an SDK is that just about all of this is abstracted away. You’ll see shortly how easy it is to make requests to the MySpace APIs using an SDK.
Setting Up an External Iframe App Now that we understand what the REST APIs are, let’s take a look at how to set up an app on MySpace that will make use of them. Up until now, our Tic-Tac-Toe app has used MySpace’s OpenSocial container to make requests to the API, has lived on MySpace servers, and has been written entirely in JavaScript. What if we had an app already written and running on a Web site and wanted to make it a MySpace app? Would we have to completely rewrite the app in JavaScript? And convert all of our server communication to makeRequest? That’s one possibility, but another is to simply “iframe your app.” What that means is that MySpace provides the ability to load in an external iframe on the Canvas surface instead of loading in the usual MySpace-hosted iframe. Let’s walk through what it might take to convert parts of our Tic-Tac-Toe app to an iframe app. First we’ll need to create a new app by following the instructions found in Chapter 1, Your First MySpace App. Once it’s created, navigate to Edit App Source and click on the Canvas Surface tab (it’s the default tab). Instead of selecting HTML/Javascript Source from the radio buttons at the top, we’ll select External Iframe. Then, inside the External IFrame URL field, enter the starting point for your app. In our case, and as shown in Figure 9.1, it’s http://opensocial—tic-tac-toe.appspot.com/play. When the Canvas surface for this app loads, it will load the page you entered into the External Iframe URL field instead of loading a MySpace-hosted page. In addition, it will append several parameters to the source of the iframe in order to pass values to your app. An example of this would be the following: http://opensocial—tic-tac-toe.appspot.com/play? appid=131583& country=us& installState=1& lang=en& oauth_consumer_key=http%3A%2F%2Fwww.myspace.com%2F461572677& oauth_nonce=633738729982017828& oauth_signature=11LDA8y%2FTsE36vg%2BKXKfSxJrkok%3D& oauth_signature_method=HMAC-SHA1& oauth_timestamp=1238276198& oauth_version=1.0&
179
180
Chapter 9 External Iframe Apps
Figure 9.1
Screen shot of the external iframe URL setup process.
As you can see, our base URL, http://opensocial—tic-tac-toe.appspot.com/play, has several query string parameters appended to it. There are some session-specific values, such as the language and country of the current user, the user ID, and whether the user has the app installed.The app’s ID and OAuth parameters are also passed down.This allows you to verify the request in the same way we verified a signed makeRequest in Chapter 8, Oauth and Phoning Home; essentially the iframe URL becomes an authenticated OAuth request. We’ll discuss the opensocial_token, perm, and ptoString parameters later in the chapter when we discuss cross-domain communication and sending messages. When our initial page loads, we’ll be able to determine the user’s ID, use the MySpace SDK to fetch any data we might need from the MySpace APIs, and push the data onto the page when it renders. Let’s implement the Play and Invite tabs of our current Tic-Tac-Toe app.The Play tab requires the user’s Profile data, and the Invite tab requires the user’s friend list.
REST APIs
The Server Code Let’s go back to our code from Chapter 8 and update our app.yaml again. For reference, you can find the Chapter 8 code in this folder: http://opensocialtictactoe.googlecode.com/svn/trunk/chapter8/opensocial—tic-tac-toe/. application: opensocial—tic-tac-toe version: 1 runtime: python api_version: 1 handlers: - url: /ws script: ws/ws.py - url: /play script: play/play.py - url: /invite script: invite/invite.py
As you can see, we’re going to use two separate pages for the two tabs. The handlers for each (play.py and invite.py) are very similar. Let’s take a look at invite.py in detail and then take a quick look at play.py. import os import ckeynsecret from django.utils import simplejson from google.appengine.ext import webapp from google.appengine.ext.webapp.util import run_wsgi_app from google.appengine.ext.webapp import template from myspace.myspaceapi import MySpace, MySpaceError from oauthlib import oauth from entities import WinLoss class Invite(webapp.RequestHandler): def get(self): id = self.request.get(‘opensocial_viewer_id’) appid = self.request.get(‘appid’) opensocial_token = self.request.get(‘opensocial_token’) error = False
181
182
Chapter 9 External Iframe Apps
try: int(id) int(appid) except: error = True ms = MySpace(ckeynsecret.CONSUMER_KEY, ckeynsecret.CONSUMER_SECRET) ms.token = oauth.OAuthToken(‘’, ‘’) try: friends = ms.get_friends(id) except MySpaceError, mse: error = True if error: msg = ‘Oops, there was an error, try refreshing the page!’ self.response.out.write(msg) return json = simplejson.dumps(friends) if json is None: json = ‘{}’ template_values = { ‘friends_obj’: json, ‘appid’: appid, ‘ownerId’: id, ‘opensocial_token’: opensocial_token } path = os.path.join(os.path.dirname(__file__), ‘invite.html’) self.response.out.write(template.render(path, template_values)) application = webapp.WSGIApplication( [(‘/invite’, Invite)], debug=True) def main(): run_wsgi_app(application) if __name__ == “__main__”: main()
import ckeynsecret includes our consumer key and secret from an external file, and we import the official MySpace SDK using from myspace.myspaceapi import MySpace, MySpaceError. For the request handler itself, we define only a GET handler. Just as in our Chapter 8 code, it starts by checking the incoming user ID and app ID to make sure they’re integers.
REST APIs
To make use of the MySpace SDK (you can download it at http://code.google.com/p/myspaceid-sdk/ and pick the language of your choice), we need to instantiate a MySpace object. All of the API functionality will be done through this object. To initialize the MySpace object, you need to always do the following two things: ms = MySpace(ckeynsecret.CONSUMER_KEY, ckeynsecret.CONSUMER_SECRET) ms.token = oauth.OAuthToken('', '')
The first line creates the MySpace object given the consumer key and secret.The second creates an OAuth token; the two parameters are always empty strings when accessing the MySpace API with an OpenSocial app. Fetching a friend list is as simple as this: friends = ms.get_friends(id)
That’s all there is to it; the SDK should take care of the rest. Assuming no exception was thrown, we now have the user’s friend list.The circumstances for throwing an exception are mostly permission-related—the user might not have the app installed, might be blocking the app, and so on. We then get the friend list ready to be pushed down onto the page by using simplejson.dumps(friends). We pass a total of four items down to the page: the friend list, the app ID, the owner ID, and the OpenSocial token.
REST API List Now that we’ve seen one of the APIs in action, let’s take a look at what other functionality the SDKs support. Table 9.1 describes all of the endpoints supported by the MySpace SDK as of this writing. Some of the data has been truncated for readability, but these are mostly just long URLs. The most important part, the schema of the data, is intact. MySpace also maintains documentation for the APIs (not the SDKs themselves, just the bare endpoints) at http://wiki.developer.myspace.com/index. php?title=MySpace_REST_Resources.
Table 9.1
Overview of Endpoints Supported by the MySpace SDK
user_id—the ID of the user page—the page number page_size—the number of records to fetch <string>list—defines how to filter the friend list; one of the following values can be provided: top—only the top friends online—only friends who are currently online app—only friends who have the app installed <string>show—what extra data to provide for each friend; any
combination of the following values is allowed: mood—each friend’s mood
185
186
Chapter 9 External Iframe Apps
Table 9.1
Continued status—each friend’s status online—whether or not each friend is online
The "next" URI is to aid in paging; it returns the URI to use to retrieve the next page of friends. But since we’re using the SDK and not manually sending out these requests, we’ll have to maintain the
REST APIs
Table 9.1
Continued paging state the same way we did for the on-site app. An example URI is http://api.myspace.com/v1/users/183399670/friends.json?
Function name
page=3&page_size=50. get_friendship
Description
Given a user ID and a list of friend IDs, returns true for each ID in the list that is a friend of the user, false otherwise
Either the MySpace SDK or the API has a bug with this endpoint. A semicolon-delimited list of friend IDs is required for the friend_ids parameter. The endpoint correctly returns data if you use just one ID, such as 6221. However, if you supply more than one ID, such as 6221;123456;876543, the API returns a 401 error. The error provided is “Invalid digital signature for base string.” Luckily, this endpoint isn’t the most useful.
Later in the chapter we’ll discuss the differences among the three levels of detail. The sample response here is for a basic Profile; we’ll make use of an extended Profile a little later.
Fetches the list of app activities that the user has raised
Parameters
user_id—the ID of the user
Sample response
" Recent activities from Chad Russell at MySpace <\/title> <subtitle type=\"text\"> This feed contains all of the activities for a ➥single MySpace user <\/subtitle> tag:myspace.com,2009:user\/183399670\ ➥/activities<\/id> Copyright (c) 2003-2009, ➥MySpace.com<\/rights> 2009-06-27T04:32:55Z<\/updated> Chad Russell<\/name> http:\/\/www.myspace.com\/cdsrussell<\/uri> <\/author> PersonalActivitiesSyndicationFeed ➥Factory.1.1<\/generator> <entry> tag:myspace.com,2009:\/...<\/id> Chad Russell is ➥thing\n<\/title> 2009-06-23T12:44:35Z<\/published> 2009-06-23T12:44:35Z<\/updated> Chad Russell<\/name> http:\/\/www.myspace.com\/cdsrussell<\/uri> <\/author>
Fetches the list of app activities that the user’s friends have raised
Parameters
user_id—the ID of the user
Sample response
Very similar to the schema outlined for the response of the get_activities_atom endpoint
Function name
set_status
Description
Sets the status for the particular user ID
Parameters
user_id—the ID of the user
Sample response
<string>status—the new status, as an arbitrary string ""
Function name
set_mood
Description
Sets the mood for the particular user ID
Parameters
user_id—the ID of the user
Sample response
mood—the new mood; must correspond to one of the valid mood IDs, which can be fetched using the get_moods API ""
Function name
create_album
Description
Creates a new media album
195
196
Chapter 9 External Iframe Apps
Table 9.1
Continued
Parameters
user_id—the ID of the user <string>title—the new title for the album, as an arbitrary string <string>location—the geographic location where the album
media takes place, as an arbitrary string
Sample response
<string>privacy—the album’s privacy setting; valid values are Me (a private album), FriendsOnly (just the user’s friends can see it), and Everyone (a public album) {"id": 2098891, "title": "family photos"}
Function name
get_indicators
Description
Fetches the indicators that are currently raised; can indicate a new friend request, message, app invite, or comment approval
In the sample response, the user has one new mail message pending and one IM message that has yet to be read.
Function name
send_notification
Description
Sends a notification from the app to the specified user; see Chapter 5, Communication and Viral Features, for details
Parameters
app_id—the app ID that will raise the notification <string>recipients—a comma-delimited list of integer recipient IDs, such as "6221,12345" <string>content—the main body of the message <string>btn0_label—optional; the label for the first button <string>btn0_surface—optional; which surface the first button will navigate to; can be canvas or appProfile
REST APIs
Table 9.1
Continued <string>btn1_label—optional; the label for the second button <string>btn1_surface—optional; which surface the second button will navigate to; can be canvas or appProfile <string>mediaitems—optional; must be in the format http://api.myspace.com/v1/users/6221, where 6221 is a
user ID; posts that user’s thumbnail image along with the notification
Sample response
""
Note In the course of researching all of the available endpoints, we created a test page. If you’re having trouble accessing any of the APIs, this test page might come in handy. The test page comes in two parts. The first is a client-side HTML page, which can be downloaded at http://opensocialtictactoe.googlecode.com/svn/trunk/chapter8/opensocial—tic-tactoe/index.html. This page has a series of buttons on it, each corresponding to an endpoint (there are some additional buttons that were used in Chapter 8). Clicking a button causes a Web service request to some Python code, which can be downloaded at http://opensocialtictactoe.googlecode.com/svn/trunk/chapter8/opensocial— tic-tac-toe/test.py. This file determines which endpoint you want to hit and uses the MySpace SDK to make the request. The response is then pushed down onto the client. If you’re having trouble, try it out for yourself. You may need to change the user ID found in the getter function in index.html to one of your own, along with some of the parameters passed into the getter function. For example, the album ID hard-coded into the file corresponds to the user ID. If you change the user ID, the album will be different. We hope you’ll find this page useful!
The Client Code To create the markup and JavaScript for our off-site app, we modified the Canvas surface code from our on-site app. Because it’s mostly the same, we won’t repeat it here.The main difference is that instead of Ajax requests being sent to the API when the app loads, the data is already there.The template object we push down from the Python script has four members: friends_obj, appid, ownerId, and opensocial_token. In our client-side markup, we can use these four template parameters by enclosing the member names in double braces: {{ some_name }}.The space between the braces, and including the braces, will be replaced by actual data at render time. So the typical flow becomes the following: 1. A user loads our Canvas surface. 2. MySpace creates an iframe with the src attribute pointing to our URL.
197
198
Chapter 9 External Iframe Apps
3. 4. 5. 6. 7.
The iframe loads and the request hits our servers. We interrogate the query string of the request to determine the Viewer’s ID. Using that ID, we make requests against the MySpace API for data. The data comes back (or it doesn’t) and we put that data into a template object. When the page renders, it looks for the double braces; if any are found, and the label matches the name of one of the template members, the template is replaced by actual data.
Our friends_obj parameter contained a JSON object of our user’s friend list. We can access this on the client by doing the following: var friends = {{ friends_obj }};
The friend list has now made its way from the MySpace API into our server scripts and down to the client in the form of a JavaScript object. It’s now ready to be used on the page. We also want to pass along pertinent data between pages. Each page in our app is expecting certain data to exist on the query string.These query string parameters are initially set by MySpace. But if a user clicks a link to go to another page, those query string parameters are lost. So when we construct the link that will take the user to the Play page, we add the other template values manually.The Play page has a similar link pointing back to the Invite page: Play
When a user clicks this link, we pass the three parameters ownerId, appid, and opensocial_token to the receiving page.This is just one of many ways to maintain session state in your app. Now that we have all of our data pushed down onto the page, we need to make a few more changes in order to get everything working correctly.These modifications are necessary because the friend data isn’t in the same format as it was for an on-site app. We will no longer need to use the getField function to access data; the fields are instead accessed directly from the object.Table 9.1 can be a great help with figuring out where all the data should be.Tools like Firebug and Fiddler (see the Introduction for what these tools are and where to get them) are also invaluable for understanding the data responses. We first make the body execute a function after it has loaded:
The runOnLoad function then initializes a number of things, including saving the friend data: function runOnLoad(){ TTT.Lists.setCurrentList(TTT.ListTypes.ALL_FRIENDS); TTT.Lists.getCurrentList().list = friends.Friends;
You’ll see that we save the Friends array into the current list, as well as the number of friends returned, so we can correctly page through the results.The draw function is then called, which is where the friend data is used to display the grid of images and names.This function is almost exactly the same as it was in Chapter 2, Getting Basic MySpace Data, so let’s just take a look at the differences: for(var i = 0; i < current_list.list.length; i++){ id = current_list.list[i].userId; name = current_list.list[i].name; picture = current_list.list[i].image; // Create the item's div element div = document.createElement("div"); div.className = current_list.class_name; div.onclick = current_list.item_onclick; div.list_index = id; var friend = this.markup_format.replace("{0}", picture); friend = friend.replace("{1}", name); div.innerHTML = friend; // Append it to the container all_items.appendChild(div); // Save a reference to the element current_list.list_dom[id] = div; }
Here we’re looping through the array of friends, which is held in the object current_list.list.The actual data can be accessed directly from the friend object using the schema found under get_friends in Table 9.1.You can see in the code just
shown that we access the user ID, name, and image of each friend to build the markup for the page.
Friends Web Service and Paging There are a couple more twists and turns before we can fully port our Invite page into an off-site app.The first is how we actually send app invites, bulletins, and activities from an off-site app.This is fairly tricky to accomplish, but we’ll discuss it in detail a little later in the chapter. The other source of complexity is paging. When a user wants to see the next page of friends, what do we do? Well, we could post back each time the user pages, using essentially the same rendering flow we’re using now and just pushing down the next page of friends.
199
200
Chapter 9 External Iframe Apps
But what is this? 1996? We can do better than that. Ajax to the rescue again.To accomplish this we’ll set up another Web service, similar to the ones we created in Chapter 8, OAuth and Phoning Home, but this will just handle paging our friend list. When the user asks for another page, we’ll send a Web request back to our servers, which in turn will request the data from the MySpace APIs. (See Chapter 13, Performance, Scaling, and Security, for ideas on how to better scale this solution. Hint:You’ll probably want to cache the pages of friends instead of requesting them anew each time.) Let’s implement our client-side pager.We don’t want to reinvent the Ajax wheel, so let’s include the Prototype library to make use of its Ajax object. <script type="text/javascript" src= "http://ajax.googleapis.com/ajax/libs/prototype/1.6.0.3/prototype.js">
To implement our client pager, we maintain the paging state the same way we did in Chapter 3, Getting Additional MySpace Data.The following function takes in three parameters—first and max are the standard paging parameters passed in as integers, and callback is the function that’s executed once the request has completed. // Fetch a list of friends, given the various // parameters function fetchFriendList(first, max, callback){ // Build the URL var url = "/friends?id=" + friends.user.userId; url += "&first=" + first + "&max=" + max; // Use Prototype's Ajax object new Ajax.Request(url, { method: 'get', onSuccess: callback, onFailure: callback }); }
The URL is built and the request is made using the Ajax object of the Prototype library. Let’s take a look at the callback function before we peek at the friends Web service. The main difference here between an on-site and an off-site app is the error handling. In an on-site app we would use hadError or getErrorCode to determine what happened. But our response is no longer mapped into an opensocial.ResponseItem object, so we’re left to return our own errors as we see fit. TTT.Lists.callback = function(response){ var current_list = TTT.Lists.getCurrentList(); var retryRequest = function(){ current_list.retries––; window.setTimeout(fetchFriends, 250); };
REST APIs
// Check for errors if(!response || !response.responseJSON || response.responseJSON.errorCode || !response.responseJSON.Friends){ if(current_list.retries > 0){ retryRequest(); } // Out of retries ... else{ log("Oops, there was an error! Try refreshing the page."); } return; } var data = response.responseJSON.Friends; // Something bad happened to it if(null === data || "undefined" === typeof(data)){ retryRequest(); return; } else{ // Save the actual data to the TTT.List object current_list.list = data; // Save the total number of items in the list current_list.total = response.responseJSON.count; // Draw the list TTT.Lists.draw(); } };
First, as our Web service returns JSON, the meat of the response can be accessed in the responseJSON property.To look for errors we check four things: 1. Is the response null or undefined? 2. Is the responseJSON property null or undefined? 3. Our Web service will return caught errors in an errorCode property; has this property been set? 4. Is the Friends property null or undefined? If all of those criteria are satisfied, we can be fairly certain we have a valid response. Our request retry code is exactly the same code we’ve used throughout this book.The Friends array is then pulled out of the responseJSON, and the new list is redrawn.
201
202
Chapter 9 External Iframe Apps
Now that we’ve seen the client code, let’s take a look at how the Web service itself looks.The first thing to do, as always, is to update our app.yaml file: application: opensocial––tic-tac-toe version: 2 runtime: python api_version: 1 handlers: - url: /ws script: ws/ws.py - url: /play script: play/play.py - url: /invite script: invite/invite.py - url: /friends script: friends/friends.py
And now, our friends.py script.You’ll notice that it’s fairly similar to the Web services we defined in Chapter 8. import os import ckeynsecret from django.utils import simplejson from google.appengine.ext import webapp from google.appengine.ext.webapp import util from myspace.myspaceapi import MySpace, MySpaceError from oauthlib import oauth class Friends(webapp.RequestHandler): def get(self): id = self.request.get('id') first = self.request.get('first') max = self.request.get('max')
Let’s take a closer look at the GET handler. We first try to parse all of our parameters into integers to make sure they’re legitimate values. If they’re not, we return an error object that will be picked up by our JavaScript callback. A MySpace object is then created and we attempt to fetch the friend list for the given user and paging values. If this request fails (usually a permission error; make sure the recipient has the app installed, for example), we again return an error object describing what happened.
The Profile Endpoint We’ve now mostly converted our Invite tab.The last piece of the puzzle is the ability to actually send messages to our users, which we’ll cover later in the chapter. First, we’d like to port the Play tab to an off-site app, as there’s not much fun in an app that just invites people to it. Adding a game would be a nice touch. In the Tic-Tac-Toe app we pull down every Profile field (opensocial.Person.Field in OpenSocial speak) that MySpace supports.These fields are mapped loosely to the MySpace API using the detailtype parameter.There are three levels of detail: basic, full, and extended. Let’s take a look at exactly what that means in handy tabular format as shown in Table 9.2.
203
204
Chapter 9 External Iframe Apps
Table 9.2
Basic, Full, and Extended Profile Data
Basic
Full
Extended
name
city
interests
largeImage
maritalstatus
television
image
country
mood
userId
aboutme
status
uri
hometown
movies
webUri
culture
books
lastUpdatedDate
gender
desiretomeet
userType
postalcode
music
region
heroes
age
zodiacsign occupation
A full Profile also returns basic data, and an extended Profile returns full and basic data. So if you request extended, you should get back everything. MySpace provides some good documentation on this functionality, which can be found at http://wiki.developer.myspace.com/index.php?title=GET_v1_users_userId_ profile_ basic_full_extended. With this information in hand, we can go on to porting over our Play tab to an external iframe app.The Python script for the Play tab is almost identical to the script for our Invite tab, so we’ll skip listing all the code again.The only main difference is what data we request. We need the user’s extended Profile data instead of the friend list. profile = ms.get_profile(id, 'extended')
This data is pushed down onto the page exactly as it was with our friend list from earlier. On the client side we also do some now familiar things.The Profile data is saved into a JavaScript object: var viewer = {{ viewer_obj }};
And the link back to the Invite page is created: Invite
The big difference comes when we want to access the Profile data. In the Tic-TacToe app, we have a printPerson function that takes all the details for the Viewer, builds some markup, and displays it onto the page. Let’s convert this function to use our extended Profile data that was fetched from the MySpace REST API.
REST APIs
/** * Output the Viewer data onto the surface */ function printPerson(img_div, name_div, location_div, gender_div){ if(!viewer || !viewer.fullprofile){ log("Oops, there was an error, try refreshing the page!"); } else{ // You can set the src attribute of an // tag directly with THUMBNAIL_URL img_div.src = viewer.fullprofile.basicprofile.image; // getDisplayName is a shortcut for // getField(opensocial.Person.Field.NICKNAME) name_div.innerHTML = viewer.fullprofile.basicprofile.name; // Get the Viewer's status var status = viewer.status; if(status && status.length > 0){ // If the status has been set, append it after the name name_div.innerHTML += " \"" + status + "\""; } // Get the Viewer's mood var mood = viewer.mood; if(mood && mood.length > 0){ // If the mood has been set, append it after the status name_div.innerHTML += " \"" + mood + "\""; } // The Address object is used similarly to the Person object; // both pass a Field into a getField function location_div.innerHTML = viewer.fullprofile.region; // getDisplayValue is defined by the specific container and can and // will differ between containers and is designed for displaying only gender_div.innerHTML = viewer.fullprofile.gender; // The response of getKey is defined by OpenSocial and therefore // you can compare the result to some known value if(viewer.fullprofile.gender.toLowerCase() === "female"){ div.style.backgroundColor = "#fcf"; }
205
206
Chapter 9 External Iframe Apps
else{ div.style.backgroundColor = "#09f"; } // MySpace provides a built-in StringBuilder class // which can take an initial string in the constructor // and has append and toString functions; this can be // easier to use in some cases than simple concatenation var sb = new MyOpenSpace.StringBuilder("
You’ll notice that the data is contained in the object in a strange manner. All of the extended fields (see Table 9.2 for which data is which) are accessed directly from the object, such as viewer.heroes.The viewer object also contains a fullprofile object, which in turn contains all of the full fields, such as viewer.fullprofile.age. The fullprofile object also contains a basicprofile object.This object contains all of the basic fields, such as viewer.fullprofile.basicprofile.image. Just to clarify a bit, let’s take a look at a sample JSON response: { "interests": "General", "fullprofile": { "city": "SEATTLE", "maritalstatus": "Married", "country": "US", "aboutme": "very interesting...", "hometown": "Winnipeg", "culture": "en-US", "basicprofile": { "name": "Chad Russell", "largeImage": "http:\/\/path_to_img.jpg", "image": "http:\/\/path_to_img.jpg", "userId": 183399670, "uri": "http:\/\/api.myspace.com\/v1\/users\/183399670", "webUri": "http:\/\/www.myspace.com\/cdsrussell", "lastUpdatedDate": "4\/8\/2009 7:11:00 PM" }, "gender": "Male", "postalcode": "98109", "region": "Washington", "age": 28 }, "television": "Television", "mood": "", "headline": "", "status": "", "movies": "Movies", "books": "Books", "desiretomeet": "Who I'd Like to Meet", "music": "Music", "heroes": "Heroes",
That should give you some idea of how the data is laid out. As you can see in the printPerson function, all of this data is accessed fairly simply from the viewer object.
That’s all there is to porting the Play tab over to an off-site app; the rest of the code can be used as is. However, as promised earlier, there is still one thing left to do for the Invite tab. Let’s take a look.
Sending Messages Using IFPC IFPC, or inter-frame procedure call, was explained in Chapter 2, Getting Basic MySpace Data, but briefly. It’s a library that facilitates cross-domain JavaScript calls.Typically, you can’t access the DOM of a page that isn’t living on the same domain as the DOM on which you are currently living.This is a security feature of browsers, created to prevent various malicious attacks. MySpace uses IFPC for a number of purposes, such as resizing the app, navigating between pages, and sending messages; see Chapter 2, Getting Basic MySpace Data, for a list of the features that are exposed. For on-site apps, this functionality works with little or no work on your part. MySpace has set up IFPC to work between the msappspace.com domain where on-site apps live and myspace.com. However, the same can’t be said about communication between myspace.com and whatever domain an off-site app happens to be running on. So, in order to expose the IFPC functionality to off-site apps, we’ll have to set it up to work with a different domain. Luckily, MySpace provides some libraries to help us do this. Setting it up can be pretty simple, but it’s a very picky process. Keep in mind that this whole IFPC cross-domain thing is just a big hack; browsers were not meant to do this. But if we’re careful with our settings, it’s a snap. The library MySpace provides does a couple of things for us. First, it pushes down all the necessary IFPC JavaScript code onto the page.This essentially gives us the container for use on our external domain. Second, if you provide the correct details, it sets up all the required IFPC parameters. The most basic way to include the library is as follows: <script type="text/javascript" src="http://api.msappspace.com/JSLoader/JSLoader.aspx?v=0.7&f=rpc"
The library is actually a dynamic ASP.NET page that spews out JavaScript in its response.The two required parameters are "v" for OpenSocial container version and "f" for feature. Here we’re asking for version 0.7 and the "rpc" feature; RPC is just another name for IFPC. So this allows us to make IFPC calls using the 0.7 container. Why the 0.7 container and not something a little newer, you may ask? We’ll get to that a little later, but there’s a good reason for it.
Sending Messages Using IFPC
That should actually be all that’s required to make IFPC calls, as long as you don’t need a callback; the callback is always the tricky part. So at this point you should be able to call gadgets.window.adjustHeight() and it should actually resize your app. Magic! So what about that callback? The other parameters that the JSLoader accepts are appid, ownerId, and localRelay.The first two are fairly self-explanatory, but the third not so much. IFPC works by using two relays, one on each domain.These relays are just simple HTML files that don’t do much, but they must live on the two domains between which you want to communicate.That means one has to live on myspace.com, which is taken care of by MySpace, and the other has to live on the external domain; in our case, it’s opensocial-tic-tac-toe.appspot.com. We’ll need to do two things to get this working. First, download the relay HTML file from MySpace’s servers: http://x.myspacecdn.com/modules/applications/static/js/ifpc_ relay_external001.html. Save that file and upload it to your server. In our case, we put it at http://opensocial-tic-tac-toe.appspot.com/relay/ifpc_relay_external001.html. Note that there are no UI elements on these HTML pages, so don’t worry when you load them and it looks as if there’s nothing there. Now add this entry to your app.yaml: - url: /relay/ifpc_relay_external001.html static_files: relay/ifpc_relay_external001.html upload: relay/ifpc_relay_external001.html
The next step is to tell the MySpace library where this relay file lives.That’s done by setting the localRelay parameter. So putting it all together, we get a script tag that pulls in all the JavaScript we’ll need and correctly sets up IFPC: <script type="text/javascript" src="http://api.msappspace.com/JSLoader/JSLoader.aspx? v=0.7& f=rpc& appid={{ appid }}& ownerId={{ ownerId }}& localRelay=http://opensocial—tic-tactoe.appspot.com/relay/ifpc_relay_external001.html">
Warning Make sure your relay file has the same domain as your app.
Just remember to keep it all on one line in your code; it’s broken up here for readability only.This final script tag should allow IFPC calls along with callbacks.The one “gotcha” to look out for is to make sure the relay file has the same domain as your app. It’s a common mistake to host the relay on something like http://static.example.com/ifpc_relay_external001.html but host the actual app somewhere like http://app.example.com/app.html. Because the subdomains are different, IFPC won’t be able to function.
209
210
Chapter 9 External Iframe Apps
If you can’t host all your files on the same subdomain, or prefer not to, there is one thing you can do. JavaScript allows you to modify the domain of the current page, but only if you make the domain more generic.That means you can remove the subdomain— to go from static.example.com to example.com, for example—but not the other way around. Once you set it to example.com, you can’t set it back to static.example.com. At the top of the relay file, and any other page in your app, you would have to generalize your domain, for example: document.domain = "example.com"
Putting that at the top of each page should allow IFPC to function correctly.
Using the 0.7 Container for postTo Now, why were we including the 0.7 container? An early quirk of the MySpace platform was to implement a function called postTo.This function behaved exactly like opensocial.requestSendMessage (see Chapter 5, Communication and Viral Features, for details), except it didn’t take IDs for the recipient parameter; it took opensocial.Person objects.The result of this was that the back-end infrastructure expected not only the recipient’s ID but also the recipient’s name and image.To work around this limitation, when you use opensocial.requestSendMessage and pass in an ID, MySpace sends a request to the API to retrieve the full opensocial.Person object for that user ID. The callback then initiates the postTo function. So requestSendMessage just wraps postTo. The problem is that you can’t use the MySpace OpenSocial API from an external iframe; only IFPC calls are possible. So when you call requestSendMessage from an external iframe, it attempts to make a request to the API, which fails because it would be a cross-domain Ajax request.This is where the 0.7 container comes in handy, because it contains the postTo functionality. Fortunately, the opensocial.requestShareApp function doesn’t have the same limitation as opensocial.requestSendMessage. Passing in just an ID doesn’t cause a request to the API, so it just works as normal. Let’s take a look at how to modify our JavaScript to use the 0.7 postTo functionality: // Wrap 0.7's postTo functionality function postToWrapper(person, subject, body, type, callback){ // Create the opensocial.Message as normal var param = {}; param[opensocial.Message.Field.TYPE] = type; param[opensocial.Message.Field.TITLE] = subject; var message = opensocial.newMessage(body, param); // Create an opensocial.Name object param = {}; param[opensocial.Name.Field.UNSTRUCTURED] = person.name; var name = new opensocial.Name(param);
Sending Messages Using IFPC
// Then the opensocial.Person object param = {}; param[opensocial.Person.Field.ID] = person.userId; param[opensocial.Person.Field.NAME] = name; param[opensocial.Person.Field.PROFILE_URL] = person.webUri; param[opensocial.Person.Field.THUMBNAIL_URL] = person.image; var recipient = new opensocial.Person(param); // Set the token MyOpenSpace.MySpaceContainer.OSToken = "{{ opensocial_token }}"; // Call postTo opensocial.Container.get().postTo("", message, recipient, callback); }
This function wraps the postTo functionality.The person parameter comes from data retrieved by the REST API, and we’ll take a look at how that’s created in a second. First, the opensocial.Message object is created.We then need to map the friend data from the REST API into opensocial.Person objects. From there, an opensocial.Name object is created, and then the opensocial.Person itself. Once all of our little OpenSocial objects are created, the token needs to be set.The postTo function looks for the token at MyOpenSpace.MySpaceContainer.OSToken, so we set that to the token we’ve pushed down from our Python script. The first parameter for postTo is no longer used, so just pass in an empty string. message is the opensocial.Message object we created, recipient is the opensocial.Person object we created, and callback is the function that will execute once postTo has completed. The callback response is an integer, specifically 0 if the user canceled the pop-up dialog, 1 if the user sent the message, or –1 if an error occurred.
The Friends Response from the REST API Let’s take a look at one last function.The friends object is the response from the REST API when we requested a friend list.The get_friends section of Table 9.1 has the details of exactly how this data object looks. // Finds the correct friend from the REST API response function requestSendMessageWrapper(id, subject, body, type, callback){ var person = null; // Loop through the list of friends for(var i = 0; i < friends.Friends.length; i++){ // If the ID is found in the list if(friends.Friends[i].userId === id){
211
212
Chapter 9 External Iframe Apps
// Save the person person = friends.Friends[i]; break; } } if(null !== person){ postToWrapper(person, subject, body, type, callback); } }
We loop through the list of friends looking for a particular ID. If the ID is found in the list, the person is saved and passed into the postToWrapper function described previously.That function then converts the data into an opensocial.Person object for use in postTo.
Summary A lot of complexity is generated when using external iframe apps compared to on-site apps.Trying to figure out the vagaries of OAuth and IFPC is a daunting task and one we recommend only for the academic pursuit of knowledge. MySpace provides some fairly capable libraries that abstract a lot of the complexity away, and we highly recommend using them. Off-site apps also greatly lower the bar for entry onto the MySpace platform. Many existing Web apps can be ported directly onto MySpace without much work at all, as long as they don’t require any social features. Adding those social features is then fairly simple, given the libraries described throughout this chapter. Before you know it, your simple Web app will become a fully integrated MySpace app, with an audience of tens of millions. Note Code listings and/or code examples for this chapter can be found on our Google Code page under http://opensocialtictactoe.googlecode.com.
10 OSML, Gadgets, and the Data Pipeline W
hen OpenSocial first came into being, it was simply some haphazard REST endpoints and a JavaScript API. Over the course of a year of hard hammering and app building, the spec group, working with the community, began to smooth out the lumps and clean up the rough edges. Consistency was brought to the REST API.The JavaScript and REST APIs were aligned with each other.The envelope was pushed by app developers, and many of the best ideas were pushed into the spec. Something was missing through this process, though. Common tasks, like retrieving a list of friends, were cumbersome and repetitive. In order to do even simple things, app developers had to possess a good working knowledge of JavaScript. App initialization was often slow and ugly as data was being asynchronously transferred after the initial rendering of the app. As a result, a markup language along the lines of JSP, ASP, or ColdFusion was conceived and added to the 0.9 version of the OpenSocial specification.This is the OpenSocial Markup Language (OSML). In this chapter we’ll cover gadget XML, how to access data from the social graph using Data Pipeline tags, and how to then cycle and display that data without JavaScript. We’ll also delve into using client-side DataContext to show pipelined data with JavaScript and even rewrite our Tic-Tac-Toe app as an OSML gadget. OSML is a very late addition to the OpenSocial spec. It is supported only beginning in the 0.9 version of the OpenSocial specification—previous versions do not recognize OSML or Data Pipelining tags. Using it requires fully adopting the gadget XML format for writing apps on MySpace and moving away from the Surface Editor interface that many longtime MySpace app developers have become familiar with.The benefits of this transition, we believe, will be worth the time it takes to learn the gadget XML format.
The Big Picture There are some new terms in this chapter.The big three are gadgets, OSML, and Data Pipelining.These three work in concert to enable the functionality collectively referred to as the OpenSocial Markup Language.
214
Chapter 10 OSML, Gadgets, and the Data Pipeline
Gadget XML Gadget XML is a format that predates OpenSocial.There are actually two “gadget” specifications in common parlance today: the Google Gadgets specification and the Windows Vista Gadgets specification. OpenSocial uses the former, not the latter. Google Gadgets started prior to the OpenSocial specification. Later, around version 0.6 of the OpenSocial spec, the OpenSocial spec group realized that apps were actually a type of gadget, essentially a gadget communicating with a social network. As a result, the OpenSocial JavaScript specification and the numerous hosted apps written on various networks have become more closely aligned with the gadget specification. OpenSocial apps became a specialized gadget.This certainly doesn’t apply to off-site and iframe apps that make use of the OpenSocial REST API, but it has been a steady trend for the types of apps that MySpace refers to as “on-site.” Today, the gadget XML format is the format of choice for portably packaging apps for distribution between different social networking sites.The multiple options and opportunities for code reuse allowed by OSML are not supported with the app Surface Editor interface; you must author with the gadget XML format.
Data Pipeline The Data Pipeline is a way to declaratively define data and associate it with keys.This allows our app to have data prepopulated and available at app load time instead of after load and after making a series of XMLHttpRequest (XHR) calls.
OSML OSML is a markup language that allows app developers to more easily create user interfaces, handle basic display control flow, and create reusable components. Gluing OSML together with the Data Pipeline is a simple expression language, loosely based on JSP EL (JavaServer Pages Expression Language).
Writing a Gadget The MySpace OSML implementation requires you to use the gadget XML format for authoring your apps. Up to this point in the book we have worked almost exclusively with the app Surface Editor interface (except in Chapter 9, External Iframe Apps).The Surface Editor interface effectively abstracts out the gadget XML file by allowing the app developer to access the contents of only a single surface at once.The full gadget XML allows the developer to manage the code for all three surfaces, along with metadata and external references, in a single file.We’ll start with some simple examples, then move on to updating our app.
“Hello World” Gadget Our discussion about gadgets will start with a simple example: “Hello World.”To get this example running quickly, we’ll make use of the OpenSocial Sandbox tool.This tool
Writing a Gadget
allows you to play around with some OSML gadget code and immediately display the results without the overhead of creating an app.The app makes use of canned sample data to give the user a feel for how the app will act when fully installed. Entering and Executing the Code in the Sandbox The following steps introduce you to the OpenSocial Sandbox Editor.The Sandbox Editor allows you to enter full gadget apps—including OSML and Data Pipeline tags— and test the behavior against a set of sample data. It also provides a number of samples that can be tweaked to play with new tags. At the time of this writing this feature was still in beta, so some buttons and links may have changed by the time you read this book. 1. Log in to the MySpace developer site. 2. Navigate to the Sandbox by clicking Tools → OSTool/Harness and clicking the Try Beta OpenSocial Sandbox Editor link. In the editor box you will see a blank sample gadget. 3. Replace the source in the editor box with the following code, which defines the gadget XML and adds one display surface: <Module> <script type="text/os-template"> Hello World!
4. Click View.The results will show on the Render tab. As you can see, gadget XML is a fairly simple structure.The main root of the gadget is defined by the Module node. Display sections are defined with a Content element. In this case, the content node corresponds to the Canvas surface.
Basic Gadget Structure A gadget is composed of two major sections: the metadata and the display content. In the “Hello World” example we did not make use of the metadata section, only the display section. The metadata is defined in a section called ModulePrefs. This section allows the app author to declare things like the title of the gadget, issue directives to the container, and even define user-specific input fields. Content elements, or Content blocks as they are also referred to, are the meat of an app’s gadget XML. These nodes identify the data and content that are shown on the various surfaces.
215
216
Chapter 10 OSML, Gadgets, and the Data Pipeline
Defining Basic App Meta-Information Basic app information may be embedded within the gadget XML, or it may be inputted directly into the Edit App Information screen. It’s good practice to keep these values synchronized between the App Information screen and the gadget XML.The App Information screen, however, is the only reliable editing interface for meta-information. Beyond the initial load, this information is not updated from the gadget XML. Changes to category, title, or description must be made in the App Information screen as we have previously discussed. The basic settings in ModulePrefs are defined as attributes of the ModulePrefs tag. Table 10.1 lists the ModulePrefs attributes supported by MySpace. There are also a number of elements that may appear in the ModulePrefs section of the gadget XML. Table 10.2 lists some of the elements currently supported by MySpace Warning Make sure to check the MySpace developer site regularly for the latest information on MySpace-specific settings that can go into your app’s gadget source. MySpace regularly releases new features and extensions to the OpenSocial specification and app behavior on short notice.
Let’s add some basic information to our gadget for consistency. In the real world you should plan on using the App Information screen as well. <Module> <ModulePrefs title="My OSML Hello World" description="This is the best gadget in the world!" > http://example.com/images/myIcon.png <script type="text/os-template"> Hello World!
The display title of this app; should corre spond to the title defined for the app at creation time
Description
Gallery description of this app
Thumbnail
The large icon image used in the app gallery at MySpace; must be a 64⫻64 pixel image in GIF, JPEG, or PNG format
Writing a Gadget
Table 10.2 Supported ModulePrefs Elements Element
Description
Icon
The small image used by an app in Home view; must be a 16⫻16 pixel image in GIF, JPEG, or PNG format
Optional feature="MySpace-Settings"
A MySpace-specific item using the standard Optional element to define all MySpacespecific app settings. Check the documentation on the MySpace developer site for specific parameters and values.
Optional feature="MySpace-Views"
A MySpace-specific item using the standard Optional element to define surface view sizes for Home, Profile, and Canvas. Check the documentation on the MySpace developer site for specific parameters and values.
Our app is now identified and tagged. Should anyone open it up in one thousand years with whatever text editor is being used at that point, it will be clear that this is another variant of the prodigious Helloius Worldius programs that flooded the electronic earth in ancient times.
Adding a Second Surface to the Gadget Gadgets make use of one or more Content sections for each view (aka surface). Creating app content for a new view is as simple as copying a new Content block and editing the view attribute. The following steps define content for the Home view: 1. Navigate to the Sandbox Editor, if you’re not already on that page. If you are on the Rendered Output tab, click the Editor tab. 2. Add the following Content block code immediately below the current Content block for Canvas: <script type="text/os-template"> Home is where the heart is!
217
218
Chapter 10 OSML, Gadgets, and the Data Pipeline
3. Select Home from the Surface to Render drop-down above the editor box and click View.The Home surface result will show on the Render tab. In this way you can build up your surfaces gradually and keep the code organized.
Declaring and Using Basic Data One of the most fundamental things we do with apps is access data about the Viewer. For our short sample, we’ll declare our need for the Viewer data and use it to personalize the app.The first step is to add a Data Pipeline section and declare a Viewer request.This data must be declared in the gadget XML prior to its being referenced. 1. Go back to the Editor tab of the Sandbox and enter the following Content block above all the previous Content blocks: <script type="text/os-data">
2. Edit the display contents of the "canvas" Content block, replacing the current content with the following.This code personalizes the “Hello World” message by accessing the Viewer’s name. <script type="text/os-template"> Hello World, ${vwr.displayName}!
3. Select Canvas from the Views drop-down and click the View button.The rendered result will be a personalized “Hello World” message using the Sandbox mock data: “Hello World, John Doe.” The entire code listing for the introductory “Hello World” app follows: <Module> <ModulePrefs title="My OSML Hello World" description="This is the best gadget in the world!" > http://example.com/images/myIcon.png <script type="text/os-data">
Data Pipelining
<script type="text/os-template"> Hello World, ${vwr.displayName}! <script type="text/os-template"> Home is where the heart is!
This Data Pipeline script is embedded within a Content block. Declaring the Viewer request in this way allows our code to access the Viewer data through an expression language. In this case the Viewer’s data (a JSON person object) is accessed via the key vwr.To access a specific property, our code uses a dot notation common in most object-oriented languages.
Data Pipelining Data Pipelining is a feature that allows us to declare data items instead of writing a bunch of client JavaScript code.We just used it in our test app, but now it’s time to go a little deeper and contrast it to what we’ve learned so far. Let’s compare writing the Viewer’s name in a div using the client-side JavaScript OpenSocial library versus using OSML. First, the JavaScript version: ... ... var viewer = null; function getViewerData(){ var req = opensocial.newDataRequest(); req.add(req.newFetchPersonRequest( opensocial.DataRequest.PersonId.VIEWER), "vwr"); req.send(getViewerCallback); } function getViewerCallback(data){ if(!data.hadError()){ viewer = data.get("vwr").getData();
219
220
Chapter 10 OSML, Gadgets, and the Data Pipeline
if(!viewer){ // APP NOT INSTALLED! return; } else{ var elem = document.getElementById("foo"); elem.innerHTML = viewer.getDisplayName(); } } }
Now, the OSML version: <script type="text/os-data"> . . .
${vwr.DisplayName}
Which looks better to you? It appears to be roughly 30 lines of code versus four lines of code.Why use the JavaScript API at all? you might ask.The JavaScript API is slightly more mature than OSML and Data Pipelining, so your mileage may vary between the two. At the time of this writing, OSML and OpenSocial 0.9 were still in a prerelease state.
DataContext The DataContext is a key concept in Data Pipelining. It is the container into which all data is put for future use. At the simplest level you can think of it as one big hash table. Data tags identify both what data to get and how to get it, as well as a key under which to place the results in the DataContext. The DataContext is an unusual object. It exists both on the server during initial rendering and on the client once the app arrives at the browser.This means your app can access data declared with Data Pipelining tags both in OSML tags and within client-side JavaScript code. Initial processing of server-side data tags causes them to be defined in the DataContext.This data is then available for server-processed display code, like repeaters and expression language statements.The DataContext is also pushed down to the client and is accessible via a JavaScript API.This allows your app to dynamically modify the contents and make use of data already fetched on the server—like the Viewer or an activity stream.
Data Tags Data is requested via data tags.These tags identify the type of data being requested, parameters used in making the request, and the key under which the result will be
Data Pipelining
Table 10.3
Data Pipeline Tags
Tag
Purpose
os:ViewerRequest
Gets a reference to the current Viewer. The result is an opensocial.Person object in JSON format.
os:OwnerRequest
Gets a reference to the current Owner. The result is an opensocial.Person object in JSON format.
os:PeopleRequest
Gets a list of people. This list may consist of a special, built-in group of specific user IDs. The result is an array of opensocial.Person objects in JSON format.
os:ActivitiesRequest
Gets a list of activities. The result is an array of opensocial.Activity objects in JSON format.
os:DataRequest
General-purpose tag to get data from an OpenSocial REST endpoint. This can be used to get equivalent data for any of the preceding tags or any of the available REST endpoints.
os:HttpRequest
General-purpose tag to get data from an arbitrary URL. This is used to request data from a third-party server and is roughly equivalent to gadgets.io.makeRequest. This tag is not processed until after the app is rendered on the client.
stored within the DataContext.There are four special-purpose data tags and two general-purpose data tags.Table 10.3 identifies all the data tags currently available in Data Pipelining.
In-Network versus Out-of-Network Data Data Pipeline tags may be used to pull in data both from MySpace and from external servers. Data being declared from MySpace servers is processed on the server prior to rendering the app (server-processed). Data being requested from external servers is resolved using a makeRequest call (client-processed).The net result is that data calls to the built-in OpenSocial endpoints execute faster and provide more flexibility than calls to external servers. It should be noted that the distinction between server-processed and client-processed tags is a MySpace-specific implementation detail of the OpenSocial Data Pipelining specification. Other networks and implementations perform all server-based resolution. Some others may implement only client-side resolution of data tags with makeRequest or some other mechanism.This information is provided to help you as an app developer to understand more deeply how your app will behave on MySpace. The following tags are all server-side-processed.This means their data can be used more efficiently by the app for display purposes, and the user experience will appear smoother.
221
222
Chapter 10 OSML, Gadgets, and the Data Pipeline
n
os:PeopleRequest
n
os:ViewerRequest
n
os:OwnerRequest
n
os:ActivitiesRequest
n
os:DataRequest
The following tag is the only Data Pipelining tag that is always processed on the client. All the other server-processed tags may be reprocessed on the client by using the client-side opensocial.data JavaScript API introduced in version 0.9 of the OpenSocial specification. ■
os:HttpRequest
Server-processed data requests offer significant performance advantages over clientprocessed requests.There is no visible stutter while your app waits for the result of a data request—it’s already evaluated.The trade-off is that server-processed calls are much less flexible than data evaluated on the client.The code can call to external servers only from the client. Server-processed tags also can’t reevaluate for dynamic parameters. In the next chapter on advanced OSML we’ll show you how to reevaluate data tags on the client with different parameters.
Data Tags os:ViewerRequest and os:OwnerRequest As stated previously, the os:ViewerRequest and os:OwnerRequest tags place the Viewer or Owner information in the DataContext under the specified key. The data placed in the DataContext is an opensocial.Person JSON object (as shown in Table 10.4).
Data Tag os:PeopleRequest A PeopleRequest tag returns an array of OpenSocial.Person objects corresponding to the parameters used in the tag. A PeopleRequest may be used to retrieve a single person, a specified group, or a paged range within a group. In the case where a single record is requested, the result is an array with a single element.Table 10.5 shows the PeopleRequest attributes. Table 10.4
os:ViewerRequest and os:OwnerRequest Attributes
Attribute
Description
Key
Required. A string value used to identify this data within the DataContext. The key value must be unique within the app across all data tags.
Fields
Optional. A comma-delimited list of OpenSocial person fields to return.
Data Pipelining
Table 10.5
os:PeopleRequest Attributes
Attribute
Description
Key
Required. A string value used to identify this data within the DataContext. The key value must be unique with in the app across all data tags.
userId
Required. A comma-delimited list of IDs to use in conjunction with the groupId attribute. Valid values are @viewer @owner
a specific user ID groupId
Optional. The group of users to get, relative to the value defined in the userId attribute. If this is not specified, it defaults to @self, which means the person object(s) corresponding to the value of the userId attribute. Valid values are @self @friends
The value @self means the records specified in the userId attribute. The value @friends means friends of the user(s) specified in the userId attribute. Fields
Optional. A comma-delimited list of OpenSocial person fields to return with each record.
startIndex
Optional. In a paged list of records, this specifies the starting index for results.
Count
Optional. An integer value specifying the maximum number of records to return.
sortOrder
Optional. The order in which the sortBy parameter is applied. Can be either “ascending” or “descending”; defaults to ascending.
filterBy filterOp filterValue
Optional. Filter to apply over users to retrieve. These values match the REST specification.
Data Tag os:ActivitiesRequest This is a request to get activities for the specified person.Table 10.6 shows its attributes.
Data Tag os:DataRequest The DataRequest tag retrieves data from any defined OpenSocial REST endpoint. In addition to the attributes identified in Table 10.7, any other attribute may be added. Other attributes are passed through to the request so that the final method target has enough information to complete.
223
224
Chapter 10 OSML, Gadgets, and the Data Pipeline
Table 10.6
os:ActivitiesRequest Attributes
Attribute
Description
Key
Required. A string value used to identify this data within the DataContext. The key value must be unique within the app across all data tags.
userId
Required. A comma-delimited list of IDs to use in conjunction with the groupId attribute. Valid values are @viewer @owner
a specific user ID Optional. The group of users to get, relative to the value defined in the userId attribute. If this is not specified, it defaults to @self, which means the person object(s) corresponding to the value of the userId attribute. Valid values are
groupId
@self @friends
The value @self means the records specified in the userId attribute. The value @friends means friends of the user(s) specified in the userId attribute. Fields
Optional. A comma-delimited list of OpenSocial activity fields to return with each record.
startIndex
Optional. In a paged list of records, this specifies the starting index for results.
Count
Optional. An integer value specifying the maximum number of records to return.
Table 10.7
os:DataRequest Attributes
Attribute
Description
Key
Required. A string value used to identify this data within the DataContext. The key value must be unique within the app across all data tags.
method
The REST endpoint method used for this request. Currently, only GET requests are allowed. Valid requests are people.get (equivalent to os:PeopleRequest) activities.get (equivalent to os:ActivitiesRequest)
More endpoints are being added all the time, so check the MySpace developer site for the latest information.
OpenSocial Markup Language
JavaScript Blocks in OSML Apps Warning Gadget XML and OSML must be a well-formed XML document. To prevent surprise parsing errors stemming from your JavaScript code, always make use of CDATA tags in your JavaScript blocks.
All apps that make use of OSML or Data Pipelining must be in gadget XML format and consist of well-formed XHTML content only. If your Content blocks wrap their innards in CDATA tags as is suggested in some of the older Gadget XML documentation, none of the OSML or Data Pipeline tags will be evaluated. As a result, your app code must consist of well-formed XHTML content only. Any JavaScript code containing a “less-than” or “greater-than” test will violate this requirement. Here is an example: if(i > 0){ ...
The solution is to wrap all your JavaScript blocks in a CDATA section: <script type="text/javascript"> //
Wrapping the client script block contents with CDATA sections instructs the OSML parser to ignore things that might otherwise look like tags inside. Even though tags are not evaluated, expression language statements will be.Therefore, the following statement will still work: var greeting = "Welcome to my app, ${vwr.displayName}";
OpenSocial Markup Language The OpenSocial Markup Language, or OSML, is a tag-based markup language for adding and defining reusable user interface components, easily displaying content from an external server, and managing simple control flow.When OSML is coupled with Data Pipelining, it becomes a formidable new way of writing apps. In this section we introduce some of the basic tags available in the initial version of OSML.
225
226
Chapter 10 OSML, Gadgets, and the Data Pipeline
Table 10.8
OSML Markup Tags
Tag
Purpose
os:Name
The specified person’s display name, hyperlinked to the person’s Profile
os:Badge
The formatted Profile thumbnail image and name, hyperlinked to the person’s Profile
os:PeopleSelector
Displays a FriendPicker, scoped to the identified group
Table 10.9
Remote Content Display Tags
Tag
Purpose
os:Get
Fetches the markup from an arbitrary URL via an HTTP GET request and displays it inline at the tag location
myspace:RenderRequest
Fetches the markup using any HTTP method supported by gadgets.io.makeRequest. Contents may be displayed inline or in a specified element.
Basic Display Tags There are a limited number of convenience tags defined within OSML.These tags allow simple and common UI elements to be easily rendered.The tag set is currently small, but look for more complex UI elements to be added over time.Table 10.8 identifies the current built-in OSML markup tags.
Remote Content Display Tags HTML partial fragment rendering has become a common Ajax technique. OSML provides a simple tag for displaying inline content from remote servers via a GET request. MySpace also provides a more general-purpose display extension tag.Table 10.9 identifies these two tags.
Control Flow Tags There are two basic mechanisms provided for display control flow: os:If and os:Repeat. Additionally, MySpace provides an extension to os:If to give it an “else” syntax as well.Table 10.10 identifies the control flow tags available in OSML
Putting It Together: OSML Tic-Tac-Toe Our previous “Hello World” gadget app gave a light introduction to using Data Pipeline tags along with the general gadget structure. Now we’re going to take what we’ve seen so far and apply it to our existing Tic-Tac-Toe app to see what happens.
Putting It Together: OSML Tic-Tac-Toe
Table 10.10
OSML Control Flow Tags
Tag
Purpose
os:If
A conditional block that is shown if the expression statement evaluates to true
osx:Else
Inverse conditional support for os:If. This is a MySpace extension to the OSML specification. The osx:Else tag must appear immediately adjacent to an os:If tag.
os:Repeat
Iterates over an array of data items and displays the contents of the repeat tag, evaluated for each data item
Setting Up the Gadget Our first step is to move the existing code as is into the gadget XML format. For this step we’ll make use of our desktop code editor to paste all three of the existing app surfaces into an empty gadget XML file.The OpenSocial Sandbox tool will be our best friend for validating our work in this initial stage. Creating the Initial Gadget File from Existing Code Here we will construct the initial app gadget XML source file from the code blocks we have already developed using the app Surface Editor. At the end of these steps we will have our same app in the gadget XML format. 1. Navigate to the Sandbox by clicking Tools → OSTool/Harness from the developer site and clicking the Try the Beta OpenSocial Sandbox Editor link to open the Sandbox Editor (shown in Figure 10.1). 2. Copy the contents of the Blank Sample Gadget and paste it into a new code file in your code editor (Aptana for us). 3. Change the value of the “title” attribute of the <ModulePrefs> element from “Hello World” to “OpenSocial Tic-Tac-Toe.” 4. Save your file as OpenSocialTTTGadget.xml. 5. Open the source for the Home Tic-Tac-Toe surface in your code editor. 6. Copy the entire contents of the Profile code file and paste it into your new gadget in the Profile view template script.The surrounding code will look like this: <script type="text/os-template"> // PASTE THE CODE FROM Tic-Tac-Toe HOME SURFACE HERE
7. Copy the contents of your new gadget file and paste them into the Sandbox Editor.
227
228
Chapter 10 OSML, Gadgets, and the Data Pipeline
Figure 10.1
OpenSocial Sandbox Editor.
8. Select the Home view from the Views drop-down and click Render.You will see an error similar to the following: An error occurred while parsing EntityName. Line 210, position 43. // First check if there was an error back from the proxy if(!response || (response.errors && response.errors.length > 0) || response.errorCode){ retryRequest(); }
We need to fix these parsing errors before we can get any further. Fix Parsing Errors The error we encountered was caused by a greater-than comparison occurring within a JavaScript block.To get around this, we’ll wrap all client JavaScript script tag contents in CDATA sections: 1. Go back to the gadget code in your source editor. 2. Search for all the <script> tags that do not have a type attribute of text/os-template or text/os-data.
Putting It Together: OSML Tic-Tac-Toe
Figure 10.2
Sandbox rendering of Home.
3. Add an opening CDATA tag immediately after the script tag and a closing CDATA tag immediately before the closing tag.These tags should be preceded by JavaScript inline comment characters. Here is an example of what the updated script block will look like: <script type="text/javascript"> //
//JavaScript code excluded for brevity
//]]>
4. Save the results and re-render using the Sandbox Editor as before.When it is working correctly, you should see the Home view, rendered as in the live app (as shown in Figure 10.2). Adding Other Surfaces The other two surfaces must also be added to the gadget file. Paste the Profile and Canvas code into the templates in the appropriate Content blocks of your gadget file. Make sure you add CDATA sections to all JavaScript blocks until the Sandbox Editor can render all three surfaces without any parsing errors. There is one more issue that will surface when reconciling the existing JavaScript app with the gadget/OSML format. Activity templates make use of an expression marker in their templates that’s similar to one found in the Data Pipelining expression language. Both use the format ${SOME_VAR} to define an expression.The OSML renderer, finding the embedded activity templates, will attempt to resolve the contained tokens as
229
230
Chapter 10 OSML, Gadgets, and the Data Pipeline
expressions and hiccup.The resolution is to build up the activity template strings in a way that does not look like an expression to the server-side OSML processor. Search the gadget code for "${" and make the following changes: // Version using the default template function rsmNotification(recipient, game_id){ // Set up all the data we'll need var body = "$" body += "{sender} has finished their move "; body += "in $" body += "{app}, it's your turn!";
This splits the activity template expression up so the server doesn’t recognize it, but the template still looks the same to the client code.
Reusing Common Content We have now created a single-source-file behemoth for our app, weighing in at almost 2500 lines of code. It’s nice to have a single source file for storage and tracking purposes, but it can be a bit ungainly to manage from a code maintenance standpoint.The Aptana IDE does a nice job of allowing you to expand and collapse nodes for easier visibility, but that’s no substitute for cleaning up our code.We need to DRY things out a bit.
The DRY Principle The acronym DRY stands for Don’t Repeat Yourself. It’s a clever catchphrase to remind developers to reuse code as much as possible. One of the failings of using the Surface Editor for writing apps is that you will find yourself repeating the same code over and over again. With an OSML gadget an app can reuse parts of the code through the use of shared Content blocks.
If we look at our code, we’ll see a lot of repeated lines across the three surfaces. In fact, putting the original Home and Profile source files in a diff tool (we used WinMerge), we find that there are only five differences, consisting of an additional style on Profile, some slightly different text on the button between the two, and a trailing div on Profile.Through the use of shared Content blocks, we can almost immediately remove 98% of the code associated with one of these surfaces. Merging Home and Profile with Shared Content Blocks Gadgets and, by extension, OSML define their different surfaces with blocks. The view attribute of a Content block identifies the surface for which the enclosed content is valid. During rendering of the OSML gadget, any and all Content blocks that
Putting It Together: OSML Tic-Tac-Toe
have a view value matching the current surface are rendered in the order in which they are encountered in the gadget XML file. If a particular block of content is valid on more than one surface, it may be identified as such by using a comma-delimited list of surface names for which it is to be rendered.This is in contrast to the app Surface Editor, which allows one and only one discrete view per surface. Our diff of the Home and Profile sources showed that there are relatively few differences between the two. Since Profile seems to be a superset of the Home view, with some small textual changes, we’ll use that as the basis for both surfaces. Creating Shared Style Content Blocks One of the powers of the gadget XML format is the ability to stream shared code blocks across multiple surface views. In this section we use the notion of specifying multiple valid view values in a single Content block in order to share common markup, styles, and JavaScript between the Home and Profile surfaces. 1. Edit your app’s gadget XML file to completely delete the home Content block. 2. Change the profile Content block to also include the Home view, like this:
3. Save your changes and view them using the Sandbox tool.The Home and Profile surfaces should now match. 4. Add three new Content blocks with template script tags above the existing combined Home and Profile block.The first block will be used to hold the common styles.The second two will hold view-specific styles.The code should look like this: <script type="text/os-template"> <script type="text/os-template"> <script type="text/os-template">
5. Add the complete <style> block to the first shared Content block.
231
232
Chapter 10 OSML, Gadgets, and the Data Pipeline
6. Create new style elements in both the subsequent blocks and cut and paste the subtitle-style class definition out of the shared style area and into each private style block. Modify the Home surface’s version to be display:none; the two private style blocks should appear as shown here: <script type="text/os-template"> <style> .subtitle { margin:5px 0; } <script type="text/os-template"> <style> .subtitle { display:none; }
7. Save and view the results.The Profile surface should still show the subtitle, but the Home surface should be free of the subtitle. There is still one visible issue.The button text is incorrect on the Home view.We’ll fix that by adding some client-side JavaScript to modify the button’s text for the Home view. Customizing the Button Text Between Views There are supposed to be some minor differences between the Home and Profile views of our app. In this section we add some subtle changes in a Content block specific to the Home view that fixes the button to show appropriate text whether on the Home view or the Profile view. 1. Find the button element in the shared Profile/Home content template. It should look like this:
2. Modify the element to add an attribute of id="playButton" to the button element.
Putting It Together: OSML Tic-Tac-Toe
3. Add a new Content block after the shared Profile/Home Content block that is specific to the Home view. Include the template script tags and client-side JavaScript script tags. <script type="text/os-template"> <script type="text/javascript"> //
4. Add an inline JavaScript statement to change the text of the button by manipulating the DOM: document.getElementById("playButton").innerHTML = "Click here to play now!";
5. Save and view the results.The Home view should now show the proper button text. Here is an abbreviated code listing of the updated Home/Profile content blocks: <script type="text/os-template"> <style> body { background-color:#1E4C9F; color:#fff; margin:0; padding:0; } img { border:0; } ... <script type="text/os-template">
Working with Data Having shared display code among the surfaces is great. Having shared data is even more awesome.The next step is to start migrating some of our data calls and our display to make use of the Data Pipeline. We’ll start by changing the Viewer request to an os:ViewerRequest and use the expression language to display the Viewer info. Getting Viewer Information with os:ViewerRequest 1. Edit the canvas Content block to add a new os-data section above the os-template section.The modified code section will look like this: <script type="text/os-data"> <script type="text/os-template">
2. Add a new os:ViewerRequest tag to this os-data section with the key viewer. This tag will retrieve the basic Viewer fields by default. Since our code uses several extended fields, we will specify the full field set by adding the value @all to the fields attribute.
3. Edit the contents of the myinfo div to pull the Viewer image and name from our new pipelined data stored under the viewer key in the DataContext. An expression language statement is used to set the name and image URL:
... ...
${viewer.DisplayName}
4. Search for the call to printPerson in the JavaScript. It is in the getDataCallback function. Comment out this call: // printPerson(document.getElementById("myinfo"));
235
236
Chapter 10 OSML, Gadgets, and the Data Pipeline
5. Save and view your results in the Sandbox Editor.The app will now render with the Viewer information prepopulated on the rendered surface. That’s pretty exciting.We got access to all of that information without a single line of JavaScript. Now, let’s fix the More button to show the player bio again instead of allowing the app to get stuck in lightbox mode. Updating the Player Bio Lightbox to Use Client-Side DataContext In this section we make some minor refactors to the existing JavaScript function that controls the display of the opponent box. At this point we only swap out some of the data calls with calls to the DataContext and leave most of the code intact. Moving to using a template will be introduced in the next chapter: Chapter 11, Advanced OSML. 1. Add a new div below the myinfo div with the ID of playerBioWrapper and set the style equal to display:none. 2. Search for the printPerson function. Notice all of the HTML being built later in the function. In the original version of this function the div element for the lightbox is created dynamically.We’re going to code the lightbox content container directly on the page. 3. Copy out the wrapping divs and close button divs. Add these to the div created in step 1.Your markup should appear like this:
4. Add a new JavaScript function called showViewerBio.This holds the logic for triggering the display of the Viewer’s bio in the lightbox. 5. Add a line to this function to get the Viewer from the client DataContext: var vwr = opensocial.data.getDataContext().getDataSet("viewer");
6. Build out the contents of the lightbox using the vwr object. Client DataContext objects are in JSON format and have their properties directly accessible as properties. var str str str
str = ""; += "
" += "
" += "
\n"; var tryAppendLine = function(val){ if(val && val != ""){ return val + " \n"; } } str += tryAppendLine (vwr.DisplayName); str += tryAppendLine (vwr.DateOfBirth);
7. Assign the contents of your string to the innerHTML of the element internal to the player bio div and assign the entire bio contents to the lightbox: var bioElem = document.getElementById("playerBioWrapper"); var bioContents = document.getElementById("bio_contents"); bioContents.innerHTML = str; //Add to lightbox TTT.LightBox.setContent(bioElem.innerHTML); //Now show TTT.LightBox.show();
8. Update the More link to fire the new showViewerBio function instead of directly invoking the lightbox.Your code should properly show the lightbox again.
Displaying Data Lists The next operation is to display a list of friends.This is done by combining the results of an os:PeopleRequest tag with an os:Repeat control.We’re going to update the Invite tab to use OSML and the Data Pipeline instead of client XHR requests. Displaying Friends with a Repeater A repeater allows your app to apply the same display content to multiple items in a list. The most common use case is to display a list of friends. In this section we’ll use a repeater to display the friends in the Invite tab of our app instead of the previously designed JavaScript function for manually outputting this code. 1. Add an os:PeopleRequest tag to the Data Pipeline script (text/os-data) for our Canvas view.We’ll place it under the key "friends" and get the Viewer’s friends with it.The following tag states that “I’m going to get a group of friends”—denoted by the special value "@friends" in the groupId attribute—”and these friends will be friends of the Viewer”—denoted by the special value "@viewer" in the userId attribute:
237
238
Chapter 10 OSML, Gadgets, and the Data Pipeline
2. Find the invite_container div.We’re going to modify this div to add a repeat tag to iterate over friends. Ahead of the last closing div on this element, add the following:
${Cur.DisplayName}
3. Save and view.The Invite tab should now show all of the Viewer’s friends prepopulated.We leave it to the reader to rewire the client script call to rsaInviteClicked.
Summary OSML is a powerful, convenient, and simple way to build apps. Operations that are tedious and repetitive with JavaScript are a snap with OSML and Data Pipelining.We’ve only just introduced some common elements, though. In the next chapter we’ll discuss some more advanced topics, such as tag templates and internationalization. The astute reader will also notice that as we ported over our Tic-Tac-Toe app to use OSML, we were not cleaning out the retired JavaScript.With the exception of commenting out some clashing client JavaScript calls, the now unused script code is still in place. A partial listing of the retired JavaScript functions includes n
getInitialData
n
getDataCallback
n
printPerson
Since proper refactoring and cleanup of old code can be an arduous process, we will leave the code in place for now.We leave it to the reader to fully identify superseded code blocks and clean them out of the app. What we’ve seen so far are some of the most common operations in OSML. Remember, OSML is a late addition to the OpenSocial spec and supported starting only in version 0.9. At the time of this writing, OSML and Data Pipelining are not even fully released and aren’t recognized by previous versions, whereas the OpenSocial JavaScript API has more than a year under its belt and is fairly stable.We encourage you to push the limits with OSML, though, and give your feedback both to MySpace and to the OpenSocial community so that we may continue to improve things. Note Code listings and/or code examples for this chapter can be found on our Google Code page under http://opensocialtictactoe.googlecode.com.
11 Advanced OSML: Templates, Internationalization, and View Navigation Tyouhiswhen chapter will introduce you to some of the more advanced techniques available to using OSML. After reading it, you should be able to define your own custom tags in your apps and use them on both the client and the server.We’ll explore directly rendering HTML fragment content from external servers and interacting with data through data listeners. And to wrap it all up, we’ll internationalize our app and localize it into different languages. OSML provides significant features beyond simple markup reuse and tag-based data declaration. In this chapter we’re going to explore some of the more advanced features of OSML.These features include defining custom tags, translating your app to different cultures and languages, and direct rendering from your external server. In the preceding chapter we covered some different ways of solving the same problems we’ve previously solved, but using OSML. In this chapter we’ll do a little more of that, plus introduce some new features that open even more doors to our app. Remember, though, OSML is a very late addition to the OpenSocial spec, and it is supported beginning only with version 0.9 of the OpenSocial specification; previous versions do not recognize OSML.
Inline Tag Templates Inline tag templates are reusable components that are declared directly inside the main gadget XML.They look very similar to standard inline templates with one exception: the presence of a tag attribute in the declaring template script tag.Templates defined in this manner are not rendered in place but are instead registered as custom tags for use later in the app.
240
Chapter 11 Advanced OSML: Templates, Internationalization, and View Navigation
Figure 11.1
Player Info box.
Defining and Using a Tag Template Tag templates are an easy and convenient way to create reusable display elements. For the Tic-Tac-Toe app, we’re going to convert the Player Info box (shown in Figure 11.1) to a custom tag.This element is used for the current Viewer as well as an opponent. Our custom tag will display based on the person object passed in as a parameter. Creating the Custom Tag Template Definition The first step is to create our custom tag definition. In the following steps we’ll convert the existing Player Info box into a custom tag template for use in our app.We’ll generalize it so that it can be used for both the Viewer and the opponent. 1. Create a new template script element in the Content block of your Canvas code. This element should appear at the top of the block, before any other template scripts. In addition to the normal template script tags, it has an additional attribute of tag="my:PlayerInfo". <script type="text/os-template" tag="my:PlayerInfo">
2. Find the Player Info box markup in our existing app gadget. It can be found by searching for the div with an ID of myinfo. Copy and paste the contents of the myinfo div into our new tag: <script type="text/os-template" tag="my:PlayerInfo">
3. Now we’re going to clean out the hard-coded data and change the tag template to use parameters. Parameters are local values passed in from a tag instance.They are accessible through the reserved variable ${My}. Our tag requires that a person object be passed into the tag instance in the Player element. Replace all the ${viewer.X} statements with ${My.Player.X} statements: <script type="text/os-template" tag="my:PlayerInfo">
4. Add an additional borderColor parameter to specify the border color.This parameter value will be placed in an inline style in the containing div.
5. Remove the id="myinfo" attribute from the main div. Each instance tag has an individual ID, so this should not be in the template. ID values from the tag instance are carried through to the resolved tag. 6. Modify the styles to accommodate the blue/pink (male/female) background colors on this element. It would be a little tedious to do an "if" test within this markup for male/female on the My.Player object, so we’ll accommodate this feature by using style class names instead.The player_info style definition will have a blue background (default), and an additional player_info_FEMALE style will be defined to override the background to pink: .player_info { border:solid 2px black; padding:5px; background-color:#09f; } .player_info_FEMALE { background-color:#fcf; }
241
242
Chapter 11 Advanced OSML: Templates, Internationalization, and View Navigation
7. Add an expression statement as a final CSS class name in the main div so that the gender is included in the style class name. In this way the style class name evaluates to "player_info_MALE" or "player_info_FEMALE", removing the need for any "if" checks.
Using the Custom Tag Now that we’ve defined our template, the next step is to use it.We’re going to add instances of the custom tag for both the current Viewer and for the opponent, if present. 1. Search again in the code for the div with an ID of myinfo. Delete this entire div and all contained elements. 2. Add in place of the myinfo div an instance of our my:PlayerInfo tag with the id="myinfo": <my:PlayerInfo id="myinfo" >
3. Add in parameter elements for Player and borderColor.The Player value should be ${viewer} and the borderColor can be any valid CSS color string or color hex.We chose the color green. <my:PlayerInfo id="myinfo" > ${viewer}green
4. Save and view your updated app in the Sandbox Editor.The Player Info box should display as before. You can download this code and a full code listing for this chapter from our Google Code repository at http://code.google.com/p/opensocialtictactoe/source/browse/#svn/ trunk/chapter11.
Using Client-Side Templates Custom templates are also available for use in client-side code. All your custom templates are available for use on the client side via the new opensocial.template namespace. This allows your app to create and render new template instances on the client.The call sequence looks something like this: var templateInst = opensocial.template.getTemplate("my:CustomTag"); var tdata = {} tdata["background"] = "red";
This code creates a new instance of the tag template defined for my:CustomTag. It then builds some data to pass in to the template as local parameters and renders the instance to a target div element target_element_id.
Warning Client-side templates are processed by the browser’s DOM engine. As such, they are sometimes subject to the various “features” of each DOM implementation. Some browsers attempt to perform tag balancing or add in new elements to get the markup to conform to their idea of what a proper DOM structure should look like. The net result is that templates that render perfectly on the server may not render correctly on the client. To avoid client-side template-rendering issues, use only complete DOM element blocks as a template—for example, div elements, fully enclosed tables, and the like. Avoid starting your templates with elements that are typically nested in other elements, such as list items (LI) and table rows (TR). Tables in particular are problematic because many browsers introduce a multitude of new elements, such as TBODY, THEAD, and TFOOT, without telling you. Favor styled div blocks in these instances. The same problem also exists with client-side repeaters and tables. If you wish to repeat a table row (TR) element, you must make use of attribute-based repeaters, not the os:Repeat element in your template.
We’re going to use client templates to render the opponent’s display block using our my:PlayerInfo custom tag template.
Client-Side Rendering with Custom Tags Because apps are dynamic, the information that is needed doesn’t always exist at server render time. User interactions affect the app. In our case, the opponent is selected after the initial app load.To address this, we use the templating JavaScript methods to dynamically render our custom tag after the opponent has been selected. In the following steps we will convert the opponentinfo div element from markup that required tedious editing in JavaScript code to a second implementation of our custom my:PlayerInfo template. 1. Edit the div element with an ID of opponentinfo and delete all the contents. Also remove the style class attribute so that we are left with an empty div element.
2. Add an os:PeopleSelector tag element immediately above the opponentinfo div defined in step 1. It should specify the group as the contents of our friends data item and a var="selectedOpponent" to have the selected friend placed in
243
244
Chapter 11 Advanced OSML: Templates, Internationalization, and View Navigation
the DataContext under that key.We’ll also tie its select behavior into our existing selectOpponent method.This replaces the client-side FriendPicker we added in Chapter 7, Flushing and Fleshing.
3. Remove the div with an ID of opponentPicker and comment out the call to loadFriendPicker since this is now being handled by the os:PeopleSelector. You may skip this step if you did not implement app data person-to-person play (discussed and outlined in Chapter 7). 4. Define a new function to trigger the client-side rendering of a new template instance in the opponentinfo div named updateOpponentTemplate.This function gets a template instance using the opensocial.template.getTemplate function and then calls renderInto to render the template into our opponentinfo div.The function looks like this: function updateOpponentTemplate(player){ var elem = document.getElementById("opponentinfo"); if(!player){ elem.innerHTML = ""; return; } //Get a new instance of the template var tplate = opensocial.template.getTemplate("my:PlayerInfo"); //Construct the data to pass into the template var data = {}; data["Player"] = player; data["borderColor"] = "red"; //Render the template into our target div tplate.renderInto(elem, data); }
5. Update the function selectOpponent to invoke this new method: function selectOpponent(player){ updateOpponentTemplate(player); ... }
Now, in addition to setting up a game challenge, the display will update to show the opponent’s information using our template.
Working with Subviews
Working with Subviews Subviews are a new concept introduced with OSML. So, yes, that means they work only starting with version 0.9 of the OpenSocial spec. If you’re writing or editing an app for a lower version, you won’t be able to use subviews. In addition to the main surface views (Home, Profile, and Canvas), subviews allow for multiple views or view parts to be defined on a particular surface.The app can then “navigate” to these subviews without forcing a page reload. Using subviews is quite simple. All you have to do is name the subview in a Content block’s view attribute, then use the following call: gadgets.views.requestNavigateTo();
This call causes the target subview to show, and all other subviews are hidden. Subviews are useful, but they do have some design and behavior characteristics you should be aware of: n n
n n
Subview Content blocks must consist of coherent, well-formed XHTML. Adjacent non-subview Content blocks must also be well formed (this means no partial nesting of elements). By default, activating one subview turns off all other subviews. On initial load, all subviews are hidden.
Converting Tabs to Subviews We’re going to reimplement our tab interface (shown in Figure 11.2) by making use of subviews. Changing the interface in this way allows us to make use of the built-in view navigation call gadgets.views.requestNavigateTo instead of manually manipulating CSS classes or styles on the elements. It gives us the added benefit of not having to write loops to manage turning off other elements that should be hidden; it’s all handled by the subview processing.
Figure 11.2
Tab interface in app.
245
246
Chapter 11 Advanced OSML: Templates, Internationalization, and View Navigation
Editing the Markup to Use Subviews for Tab Content Tabs are currently defined as div blocks.We need to modify the markup so that the header is a Content block, the footer is a Content block, and each subview is a Content block.This allows our app to control the display of the tabs as subviews but still display the header and footer in a consistent manner. 1. Find the location of the first tab content container by searching for the div with an ID of play_container. 2. Close out the Content block above this by adding the closing template script and Content block tags.The code will look like this:
3. Add a new Content block and template script tag around the play_container div. For the view attribute, set the value to canvas.play. This defines the play subview on the Canvas. <script type="text/os-template">
...
4. Add similar wrappers around the other tab content div elements. Name the new subviews as defined in Table 11.1 (the subview name goes into the view attribute of the Content block element). 5. Edit each tab content element’s CSS class attribute to remove the hide class:
...
Table 11.1
New Subview Names
Tab Div
Subview Name
invite_container
canvas.invite
challenge_container
canvas.challenge
set_background_container
canvas.pickBackground
Working with Subviews
6. Enclose the markup after the tab content container elements in a standard Content block with the view attribute set to canvas. Modifying the Script to Use Subviews The code currently has a TTT.Tabs object.We will modify this object to use subviews instead of direct DOM manipulation to do tab management. 1. Edit the TTT.Tab object to simplify its representation. It will now hold the subview name instead of a reference to the tab container DOM element. TTT.Tab = function(tab_dom, view, action){ this.tab_dom = tab_dom; this.view = view; this.action = action; };
2. Modify the initTabs function in TTT.Tabs to pass in the view name instead of the DOM reference when creating each TTT.Tab object: _initTabs:function(){ this._tabs[0] = new TTT.Tab(document.getElementById("tab0"), "canvas.play", playClicked); this._tabs[1] = new TTT.Tab(document.getElementById("tab1"), "canvas.invite", inviteClicked); this._tabs[2] = new TTT.Tab(document.getElementById("tab2"), "canvas.challenge", challengeClicked); this._tabs[3] = new TTT.Tab(document.getElementById("tab3"), "canvas.setBackground", setBackgroundClicked); }
3. Modify the selectTab function in TTT.Tabs to use the requestNavigateTo call. As part of this modification, you must also remove the calls to container.show(); and container.hide();. selectTab:function(index){ if(0 === this._tabs.length){ this._initTabs(); } var tab = this._tabs[index]; if(tab){ gadgets.views.requestNavigateTo(tab.view); } else{ return; } ... }
247
248
Chapter 11 Advanced OSML: Templates, Internationalization, and View Navigation
4. Add a call in the loadInitializer to activate the canvas.play subview: function loadInitializer(){ ... gadgets.views.requestNavigateTo("canvas.play"); ... }
We haven’t actually saved much code by making this change. Primarily, this is because the code still has to modify the display of the tab. The OpenSocial spec group is considering proposals to add activate/deactivate event handlers to subviews, but at the time of this writing this functionality has not been settled in the OSML specification. The well-formedness constraint can be a problem in some interfaces with tag nesting. As a result, subviews tend to lend themselves best to absolutely positioned elements (for example,
xxx
) or root children DOM elements of the view. In this way the subviews do not interfere with layout flow when being enabled or disabled.
HTML Fragment Rendering One of the most common paradigms we have found with OpenSocial apps is that of content rewriting. Many apps perform an initial load of a Bootstrap page, then call back to their external servers for the actual app content and perform a wholesale innerHTML rewrite of the app.While this is very flexible, it is also a very slow user experience. As we previously mentioned, OSML brings a number of new options to the table for finegrained content rewriting. In our app we’re going to add a tab to view one of our favorite sites and then render a banner ad from a pretend ad server URL (actually, a historic Web site).
Adding Content with os:Get In this section we’ll create a new tab that displays content from an external site.We’ll use the os:Get tag to pull content directly from the external site and display it inline on a subview. 1. Add a new tab to our series of tabs. Search for the div with an ID of tab3. Create a copy of this immediately after as tab4 and adjust the text to be “Laugh.” Also adjust the CSS class definitions as shown here:
Set Background
Laugh
HTML Fragment Rendering
2. Create a new content subview for the tab surface and identify it as canvas.laugh: <script type="text/os-template">
Am I a comedian? Do I make you laugh?
3. Modify the _initTabs function in the TTT.Tabs object to include this new subview tab: this._tabs[4] = new TTT.Tab( document.getElementById("tab4"), "canvas.laugh");
4. Add a div container and an os:Get tag within this Content block.The div container constrains the display size and the os:Get retrieves the content.The finished tab contents subview will look like this: <script type="text/os-template">
Am I a comedian? Do I make you laugh?
Adding Targeted Content with myspace:RenderRequest The os:Get tag is useful for simple fragment rendering. Sometimes you need more power, though.The myspace:RenderRequest allows for any HTTP verbs supported by makeRequest in addition to just GET. It also supports targeting output to a specific element instead of just inline rendering. We’re going to add a target element, presized for the ad banner, and a render request to fetch the ad. 1. Above the title heading for the app, add a div for the banner ad with an ID of bannerAd. Style it to be 80 pixels high.
OpenSocial Tic-Tac-Toe
249
250
Chapter 11 Advanced OSML: Templates, Internationalization, and View Navigation
2. Below the ad, add a myspace:RenderRequest tag. For our fictitious ad we’ll use something from a bit of Internet history: an article from Suck Daily. <myspace:RenderRequest target="bannerAd" href="http://www.suck.com/daily/2000/12/26/" />
A Note on Internet History For those of you too young to remember, Suck was one of the first, best satire sites where all of us dot-commies got to “nudge-nudge, wink-wink” each other about how ridiculous the notion was that we’d all somehow become overnight millionaires. Then we’d get back to our 60-hour-a-week jobs and labor under the ridiculous notion that we’d all somehow become overnight millionaires.
Data Listeners In the traditional JavaScript OpenSocial API, there is no need for a data listener. All data requests are processed as XHR requests. Any actions your app may need to execute are placed in the callback function.With Data Pipelining it’s a different case. Since the data is simply declared via a tag language, there isn’t a traditional callback function.To make up for this, any number of JavaScript functions may subscribe to a data key, or “listen” for when the data becomes available. A data listener is registered by calling the registerListener function on the clientside DataContext and passing in the data key and the function to trigger.The function receives the key(s), causing it to trigger when the data is ready for processing. Any handler has to then retrieve the data from the DataContext using the key.The benefit of this design is that generalized handlers can be written to handle multiple data keys. var dataContext = opensocial.data.getDataContext(); dataContext.registerListener("key", handler_function);
Exactly when and how data listeners trigger depend a little on the underlying data to which they’re being attached.The trigger is supposed to fire as soon as the data is available for use.This might be as soon as the app renders in the browser, at some arbitrary point after the initial rendering, or never. You’ll recall that any calls to the built-in OpenSocial data endpoints (for example, people,Viewer, activities, etc.) are resolved server-side prior to sending the processed app down to the browser client.This means the listener function triggers on initial load of the app. Server-processed data keys are added at the very bottom of the processed app markup, so listeners trigger as part of the load sequence.They may, however, fire before any functions registered with the gadgets.util.registerOnLoadHandler call. Data keys that are resolved by calling external servers do not fire until the callback trigger is fired and succeeds in loading the data.This means these listeners behave in a
Data Listeners
manner more akin to what we’re used to with the JavaScript OpenSocial API data calls coupled with callback functions.
Displaying JSON Results with a Data Listener For our app, we’re going to add a new tab that shows image search results for our favorite game,Tic-Tac-Toe.We’ll make use of the Yahoo! image search API to get the search results in JSON format, then render them using a listener and a custom template. The results will appear as in Figure 11.3. Warning Remember to use proper escaping of ampersands and other characters when placing URLs in attributes.
Set Up the Data Pipeline Request and the UI Tab The first step in handling display via data listeners is to create the initial data request. In this case we’re going to use an os:HttpRequest tag that will search for tic-tac-toe image results.
Figure 11.3
Rendered search results from the data listener.
251
252
Chapter 11 Advanced OSML: Templates, Internationalization, and View Navigation
1. Create a new os:HttpRequest in the Data Pipeline script to invoke the Yahoo! image search API and search for “tic-tac-toe.” Key: tttImages URL: http://search.yahooapis.com/ImageSearchService/V1/ imageSearch?appid=YahooDemo&query=tic+tac+toe&output=json <script type="text/os-data"> ... ...
2. Add a new tab to the interface to hold the image results. Modify the CSS classes so the display remains consistent.
Laugh
Tic-Tac-Toe Images
3. Create a new content subview for the image results and identify it as canvas.tttImages. On the subview, add a div to hold the results and use the ID tttImageDisplay. Within this div we can place any content to be displayed while waiting for the results. In this manner your app can display loading messages or images until the data arrives with almost no additional coding. <script type="text/os-template">
Look at Tic-Tac-Toe
Still Loading Search Results
4. Modify the _initTabs function in the TTT.Tabs object to include this new subview tab: this._tabs[5] = new TTT.Tab( document.getElementById("tab5"), "canvas.tttImages");
Data Listeners
Creating the Display Template for the Results You didn’t think we’d be manually parsing the search results, did you? Given all the powerful new templating tools at our disposal, we’d be crazy to do that. Instead, we’re going to build a template to display these results. First, we need to understand the format of the JSON search results.The following is a much-truncated result from this search: {"ResultSet": {"totalResultsAvailable":"38979", "totalResultsReturned":10, "firstResultPosition":1, "Result":[{"Title":"tic tac toe jpg", "Summary":"Tic Tac Toe - A game that never goes out of fashion Play", "Url":"http:\/\/www.something.com\/blog\/tic_tac_toe.jpg", "ClickUrl":"http:\/\/www.something.com\/blog\/tic_tac_toe.jpg", "RefererUrl":"http:\/\/www.something.com\/classic-tic-tac-toe", "FileSize":6041, "FileFormat":"jpeg", "Height":"182", "Width":"200", "Thumbnail":{ "Url":"http:\/\/thm-a01.yimg.com\/image\/0c7837e21c1b1598", "Height":"113", "Width":"125" } } , ... ] } }
The results include some aggregate information about the results, then an array of results as objects. Most of this information we don’t care about, so we’ll just show the total result count and the first page of thumbnails with the summary text. Our template makes use of the expression language tokens to pull values from the data passed into the template.We’ll pass the entire result set into the parameter searchResults.Then we can reference any property with the expression language like this: ${My.searchResults.totalResultsAvailable}
Create the template shown below and add it to the Content block containing all the other custom template definitions.This defines the tag my:YImageSearchResults. <script type="text/os-template" tag="my:YImageSearchResults">
Search Returned ${My.searchResults.totalResultsAvailable} Results
253
254
Chapter 11 Advanced OSML: Templates, Internationalization, and View Navigation
${Cur.Summary}
Attaching the Data Listener to Render the Results The final step is to create and attach the listener function that triggers the template processing. Add the following function to your JavaScript code. It turns the tab red when the data returns and performs a client-side rendering of the results using the my:YImageSearchResults template we just created. function tttImagesLoaded(key){ var tab = document.getElementById("tab5"); tab.style.background="red"; var tplate = opensocial.template.getTemplate( "my:YImageSearchResults"); var dataContext = opensocial.data.getDataContext(); var d = dataContext.getDataSet(key[0]); var tdata = {} tdata["searchResults"] = d.ResultSet; tplate.renderInto("tttImageDisplay", tdata); }
The actual registerListener call should be added in the loadInitializer function.This notifies the DataContext to trigger the tttImagesLoaded function as soon as the data key tttImages is loaded. function loadInitializer(){ ... var dataContext = opensocial.data.getDataContext(); dataContext.registerListener("tttImages", tttImagesLoaded); ... }
Internationalization and Message Bundles
Internationalization and Message Bundles The gadget XML structure recognizes the need to support users from around the world.To that end, localization is a key component of the gadget design.We’re going to translate our Tic-Tac-Toe app into three different languages: English, German, and Japanese. To those readers unfamiliar with the concept of internationalization, it’s the process of designing software so that it can easily be adapted to other cultures and languages without in-depth code changes. Localization is generally referred to as the actual translation work of adapting a program to a particular language or locale. The Gadgets specification on message bundles is designed to work in a manner similar to Windows resource files or Java internationalization support.The application must be stripped of all text, which is replaced with message keys (resource keys). Message bundles are composed for each locale (language and possibly culture) that will be supported, and the original message is translated and saved under the corresponding message key. The app makes use of fallback logic in the process of resolving message keys to get their translated text.The translation starts with the most specific language and culture for the current Viewer and attempts to find the message bundle and value. If a bundle for the specific culture is not found, the translation moves up to the general culture for the current language. If this is not found, the translation falls back to the invariant culture (global) message bundle. To illustrate, let’s look at the example of French Canada. The culture code for French Canada is fr-CA.That’s pretty specific. If an app does not have a specific translation for French Canada, it attempts to resolve the message by looking at the global French translation. If that fails, the invariant message bundle is used. If that fails, the message is probably not defined. Table 11.2 shows the different culture codes that are examined.
Table 11.2
Culture Code Processing Order
Culture
Culture Code
Notes
French Canada
fr-CA
This is the most specific culture in the table. It is examined first for viewers in French Canada.
French global
fr-FR
Also may be interpreted as “French in France.” In the Locale tag, the country code is omitted for the global language translation. When external MessageBundle files are used, the file name convention is fr_ALL.xml.
Invariant
The invariant (global) culture is the final fallback. It is specified by omitting both language and country in the Locale tag. If external MessageBundle files are used, the file name would be ALL_ALL.xml.
255
256
Chapter 11 Advanced OSML: Templates, Internationalization, and View Navigation
This provision allows you to truly tailor your app to the audience. An example of when to use specific cultures in English illustrates this point. In the United States (code: en-US), one would call a semi-truck simply a “truck.” In the UK (code: en-GB), the same thing would be called a “lorry.” Culture and Country Codes Culture codes are four-letter codes with a dash in the middle. They follow this format: -
Most conventions use lowercase for the language code and uppercase for the country code. For global translations, most internationalization implementations use the recognized originating country of the language. The Gadgets spec breaks from this convention by instead using the text ALL in the country code position. The Gadgets specification recommends using the name ALL_ALL for the invariant message bundle instead of leaving the culture code off entirely, which is what many conventions do.
In our Tic-Tac-Toe app, we will start by creating the invariant culture (global) message bundle and tokenizing all strings.Then we’ll translate this file into our target cultures. Finally, we’ll inline the translations in our main gadget file.
Creating Our First Message Bundle In this section we’ll create our first external message bundle file to hold translations.The same message bundle XML format will be used for each of the languages we translate. 1. Create an XML file named ticStrings.xml.This contains the invariant translations. In our case, the invariant fallback is to English, since that is the language in which the app is being written. 2. Open the file in an editor and add the following structure: <messageBundle>
3. Open our Tic-Tac-Toe gadget source file in an editor as well. 4. Find the first string on the Profile surface, which should be the app title: Tic-Tac-Toe. 5. Copy this string and add the following line in our message bundle strings file between the <messagebundle> open and close tags: <msg name="apptitle">Tic-Tac-Toe
6. Go back to the app gadget source and replace the title “Tic-Tac-Toe” with the expression ${msg.apptitle}.The finished line of code should look like this:
<span>${msg.apptitle}
Internationalization and Message Bundles
7. Copy the next visible string, starting with the text “So...You think you can beat me?” into our strings file in a <msg> element with the name challengetaunt. 8. Replace the text in the gadget source with the expression ${msg.challengetaunt}. 9. Continue until you’ve extracted all visible strings and replaced them with message expressions. You have now internationalized your app. Even though the only translation we have is to the original strings, our app is ready for translation and set to become a global phenomenon. We should probably test things before going much further.To test in the Sandbox Editor, do the following: 1. Edit your app gadget source to add a tag in the ModulePrefs section: <ModulePrefs title="Tic-Tac-Toe"> ...
2. Copy the markup from your message bundle file and paste it inside the tag, excluding the starting tag: <ModulePrefs title="Tic-Tac-Toe"> ... <msg name="apptitle">Tic-Tac-Toe <msg name="challengetaunt">So... you think you can beat me!? Check out my stats below...
3. Copy your newly modified app code into the Sandbox Editor and view it. If we’ve done everything correctly, you should see no difference between this version and the previous version. Now it’s time to translate our message bundle into the other target languages: German and Japanese.
Creating Translations of the Message Bundle Now that we have created a baseline message bundle in the preceding section, “Creating Our First Message Bundle,” we will duplicate and translate it into our two target language localizations: German (de) and Japanese (ja).
257
258
Chapter 11 Advanced OSML: Templates, Internationalization, and View Navigation
1. Create two copies of our message bundle XML file and name them ticStrings_de_ALL.xml and ticStrings_ja_ALL.xml. 2. Open your Web browser and go to a translation site.Two sites are Yahoo! Babelfish (http://babelfish.yahoo.com) and Google Translate (http://translate.google.com/ translate), though there are many others. For our translations we used the iGoogle translation gadget. 3. Open the German translation file (ticStrings_de_ALL.xml) and set the translation tool to be English to German. 4. Copy and translate each message in turn using the translation tool. Replace each original message with the translated result, keeping the name attribute the same. 5. Repeat steps 3 and 4 for the Japanese file (ticStrings_ja_ALL.xml), using English to Japanese as the translation. If the pasted Japanese characters come out as a series of question marks, it means your editor cannot handle this character set. You may be able to change the file encoding to UTF-8, or you may need to find a different code editor. Aptana (http://aptana.com), a Web development environment, handles Unicode characters well. Warning App gadgets (and OpenSocial apps by extension) always use UTF-8 encoding. Make sure you save your app gadget source with UTF-8 encoding. If it is submitted using a different encoding (Unicode or ANSI, for example), you may experience unusual and unpredictable results. The app review process may even flag your app as being an attempt to hack MySpace and summarily reject it.
Unfortunately, this technique does not end up with the best-quality translations. Sometimes the context is lost in translation and the text can come out like gobbledygook—think something along the lines of “I can has cheeseburger.” The only way to get good-quality translations is to have a native speaker translate the results in the context of your app or hire a professional translator. However, the translation technique we just described is adequate for our purposes (Tic-Tac-Toe is a pretty basic game). Our app is now ready to have the translations linked with the main app.
Including Translations in an App and Testing The final step is to include all our new translations in the app’s gadget code. In the following steps we will define appropriate Locale sections for each language translation and save our translated messages. 1. Define a new tag for each of our translations—Japanese (language code: ja) and German (language code: de), plus the invariant locale:
Internationalization and Message Bundles
2. Copy the associated message bundle contents, excluding the tag, and paste them inside the associated Locale element: <messagebundle> <msg name="apptitle">Tic-Tac-Toe <msg name="challengetaunt"> So... you think you can beat me!? Check out my stats below... <messagebundle> <msg name="apptitle"> <msg name="challangetaunt"> <messagebundle> <msg name="apptitle">Tic-Tac-Toe <msg name="challangetaunt">So ... Sie denken, Sie können mich!? Check out my stats unter ...
3. Open the Sandbox Editor and paste our gadget code into it. Render the results, which should be in English. 4. Now go back to the Editor tab and select German from the Culture drop-down (de-DE). Render the results. Our German translations should appear. 5. Repeat step 4, selecting Japanese (ja-JP) from the Culture drop-down.The rendered results should show in Japanese. 6. Test the invariant culture by going to the Editor tab and selecting a culture not translated, such as Spanish (es-ES).The results should appear in our invariant language, English.
259
260
Chapter 11 Advanced OSML: Templates, Internationalization, and View Navigation
The technique described in this chapter inlines the translations with the main app gadget source.This is convenient for testing with the Sandbox Editor but can be difficult from a code maintenance standpoint.The specification allows for external message bundle files to be used as well.This is accomplished by specifying a message attribute in the tag that points to an external URL where the translated message bundle is located:
External message bundles can significantly reduce the amount of code you have to look at since all the strings are externalized. Using external message bundles has some downsides as well: n n
n
Your translations must be hosted on a public Web server. It’s more difficult to test your translations since you will be unable to use the Sandbox Editor; your app must be saved and have the message strings fetched.This takes time. MySpace does not currently update translations separately from app source code. They are fetched and saved on the MySpace servers when you update your gadget source. So, updating your translations requires you to republish your app.
It’s currently the recommendation of the authors that you manage your message bundles in separate XML files but use a script to inline the translations prior to saving your app source to the MySpace developer site.
Future Directions The Google Gadgets specification is only one of many gadget or widget specifications out in the wild today.Three of the most commonly known widget formats are the Google Gadgets specification, the Windows Sidebar gadget format, and the Apple Dashboard widget format. On top of that are a multitude of other formats with a much smaller reach.The underlying commonality of all these specifications is the use of HTML as a primary programming language. The W3C has now waded into the mix as well with a Widgets specification.They have even rolled the W3C Widgets specification into the general HTML 5 specification.While the only implementations today are on mobile phones and the Opera Web browser, look to see this format grow in importance. It may even be that OSML, or even the whole OpenSocial specification, will be merged with W3C Widgets at some future date.
Summary We have explored some of the more advanced techniques available when using OSML. Templates, data listeners, and fragment rendering can save you a tremendous amount of
Summary
work when building your app. Localization support takes what used to be a hideous hack everyone had to reinvent and makes everyone able to give apps a global reach. Even with these techniques, we’re only scratching the surface. Nested templates, proxied content, and data reprocessing have only been hinted at. New techniques are sure to evolve as developers become familiar with what OSML can do for their apps. Maybe you’ll be the one to invent the next great set of techniques. Note Code listings and/or code examples for this chapter can be found on our Google Code page under http://opensocialtictactoe.googlecode.com.
261
This page intentionally left blank
III Growth and How to Deal with It
12
App Life Cycle
13
Performance, Scaling, and Security
14
Marketing and Monetizing
15
Porting Your App to OpenSocial 0.9
This page intentionally left blank
12 App Life Cycle Tpublishing his chapter is about advanced app management and the sometimes difficult app process.To learn how to create a developer account and application from scratch, please review Chapter 1,Your First MySpace App.There you’ll find everything you need to know about applying for developer status and creating your first app. Of course, once your first app is created, then what? How do you take it live? How do you make changes to it? How do you add other developers or alter your app’s public Profile? This chapter will show you.
Publishing Your App Getting your app published involves more than simply hitting the Publish button on your My Applications screen (as shown in Figure 12.1).
Figure 12.1
My Applications screen on the developer platform.
266
Chapter 12 App Life Cycle
When you hit Publish, your app won’t immediately be published.The app’s status changes to “Pending” while it’s sent to a team of reviewers who check the app to make sure it actually works and abides by the MySpace Terms of Use and Developer Addendum and the MySpace Agreement. When your app is Pending, you can opt to “Unpublish” it. Unpublishing an app simply removes it from the approval queue; it does not delete the app.
What’s Allowed, or Why So Many Apps Get Rejected There are two major reasons why apps are not approved for publication: n n
The app doesn’t work. The app breaks the rules.
If an app doesn’t function correctly or it violates the Terms of Use, the MySpace Agreement, or the subsequent Developer Addendum to the Terms of Use, it will be rejected. Our own Tic-Tac-Toe app was rejected numerous times for various reasons, which we’ll detail a little later.You only need to visit the App Denial and Status Clarification section of the MySpace Developers’ Forum (http://developer.myspace.com/Community/forums/44.aspx) to quickly realize that app approval can often be a tricky, frustrating, and very human process. First and foremost, though, your app has to work—that means no glaring bugs—and it has to actually do something.Then it has to follow the rules.To read the Terms of Use, the MySpace Agreement, and the Developer Addendum, visit the Application Guidelines page at http://wiki.developer.myspace.com/index.php?title=MySpace_Apps_Developer_ Addendum_to_MySpace.com_Terms_of_Use_Agreement. Here are some of the most common reasons for rejection (culled from the App Denial and Status Clarification forum’s frustrated posts): n
n
n n
n
n
n
Requiring the user to install something (the only exception to this is installing Flash or Silverlight) Anything that hinders the functionality of the site or browsers for the user; for example, an app that doesn’t allow a user to navigate away from the app Canvas surface by clicking the browser’s Back button JavaScript errors (this falls under the “The app has to work” rule) Improperly labeling an application; for example, describing an app as a Chess app when it’s actually an app designed to advertise and sell your Widgets Including an “incentive” to get people to invite friends to the app (points, rewards, etc.) Being incorrectly labeled as “for all ages”; for example, designating a drink-making app (must be 21+) or a dating app (must be 18+) as “all ages” Using either the MySpace name or brand or the name Tom (don’t call your app “MySpace Tic-Tac-Toe” or “Tom’s Favorite Games,” for instance)
Publishing Your App
Dealing with Rejection If your app is rejected, you will be given a reason, as shown in Figure 12.2.You can then acknowledge the feedback (if you’re the app’s creator) by clicking the Acknowledge Feedback button, fix the problem, and attempt to publish the app again. But what if you feel your app was incorrectly denied? What if you don’t understand the feedback? There is recourse:You can contest the rejection.
Contesting a Rejection If you don’t understand the feedback or you think the feedback is incorrect, you can contest the rejection by going to the App Denial and Status Clarification forum (http://developer.myspace.com/Community/forums/44.aspx) and stating your case.To contest the rejection, simply make a post. Note To make sure your post is answered as quickly as possible, follow these posting guidelines: Post Subject Heading [app name], [app ID] - [brief description of the issue/question] Body [app name] [app ID] [denial reason] [your question about the denial]
Figure 12.2
Example of feedback on a rejected app.
267
268
Chapter 12 App Life Cycle
The folks at MySpace are generally pretty fast at replying to posts on the App Denial and Status Clarification forum. Going through the process may take a while, but in the end things usually work out, as they did for the developer in the following case study. A Case Study in Successful Rejection Negotiation Sometimes the app reviewers make mistakes, but they can be appealed and rectified. We show you how to do just that in the following steps and accompanying figures. By following up, you can get those mistakes reversed. Remember that the app reviewers don’t reject apps out of malice or spite, but they do make mistakes. When negotiating app rejections, as in life, you can catch more flies with honey than with vinegar, so try to be nice! 1. The developer makes a post arguing that his app was incorrectly rejected (see Figure 12.3). 2. The review team representative responds, acknowledging the mistake and requesting that the developer attempt to publish the app again (Figure 12.4). 3. The developer resubmits the app, and it is rejected again for the same reasons.The developer posts again. (See Figure 12.5.) 4. Here’s where staying with it and showing perseverance pays off—the review team representative flags the app for approval (Figure 12.6). Yes, it must have been frustrating and yes, the process took four days, but the app was eventually approved. So, stay with it, and if your app is incorrectly rejected, it should get fixed.
Figure 12.3
The original poster contests the app’s rejection and states his case.
Publishing Your App
Figure 12.4
The MySpace review team member responds.
Figure 12.5
The original poster explains that the app was rejected again.
269
270
Chapter 12 App Life Cycle
Figure 12.6
The MySpace review team member resolves the issue and approves the app.
Even We Aren’t Immune Just to show what a black art getting apps approved can be, here are some examples of the rejection e-mails we received while writing this book.We have been intimately involved with the MySpace OpenSocial platform since its inception, yet it still took four tries to get an app live. Our intention was to create one master Tic-Tac-Toe app that showed the final result, along with apps for each chapter of the book, to show the progress of the app up to that point. Take 1: Multiple Submissions From: Developer <developer@myspace.com> Date: Mon, Jan 26, 2009 at 12:43 PM Subject: MDP Submission Denied: Multiple Submissions To: opensocialatmyspace@gmail.com Thank you for your submission to the MySpace Developer Platform. We review all applications to ensure that they are compliant with our Terms of Use and Application Guidelines. Unfortunately, your current applications do not meet our criteria for the following reason(s): 119328 120983 125557 124920
TTT TTT TTT TTT
Chapter Chapter Chapter Chapter
1 2 5 3
1. Templated Apps. If an application is "templated," it must (i) contain substantial differences between each instance of the application and (ii) allow
Publishing Your App
for a User to switch between the various templates within the application itself. Please combine all your templated applications into a single app. If your application is still going through development, please complete the application before submission as this area is not intended for testing. Please update your app and republish. Note, fixing the issues above will not guarantee approval as we were unable to perform a full review and may uncover additional problems. If you have questions, please refer to our forums or documentation. MDP Team
Our first rejection happened because we submitted the apps for Chapters 1, 2, 3, and 5 all at once.The reviewers felt we were trying to submit “templated” apps, because there were only slight differences between them.This was, of course, true. Each chapter built slightly on the one before it.To get around this, we decided to publish just one at a time. Take 2: User Experience/Functionality and Loading Issues From: Developer <developer@myspace.com> Date: Tue, Jan 27, 2009 at 5:20 AM Subject: MDP Submission Denied: [Tic-Tac-Toe Chapter 5, app ID: 125557] To: opensocialatmyspace@gmail.com Thank you for your submission to the MySpace Developer Platform. We review all applications to ensure that they are compliant with our Terms of Use and Application Guidelines. Unfortunately, your current application does not meet our criteria for the following reason(s): Issue/Request: 1. User Experience / Functionality: The app is not functioning properly. While playing the game, user is allowed to make multiple moves without having to wait for the opponent's turn. 2. User Experience / Functionality: Loading Issues. On Canvas Surface, the "Challenge" tab fails to load and appears blank. Please update your application and republish. Note, fixing the issues above will not guarantee approval as we were unable to perform a full review and may uncover additional problems. If you have questions, please refer to our forums or documentation. MDP Team
271
272
Chapter 12 App Life Cycle
Next we published just one app, and it was rejected for two reasons, both valid. First, you were initially able to cheat the computer player in the Tic-Tac-Toe game by making all three moves before the computer made any.This was a legitimate bug that we fixed. Second, when the reviewers clicked the Challenge tab, they just got a blank screen.This is because we were showing only friends who had the app installed, and none of the reviewers’ friends had installed the app.To fix this, we simply checked for a result of 0 friends and displayed a message to the user. As you can see, the reviewers actually dive pretty deep into the app’s functionality; it’s not just a cursory glance. Once those issues were fixed, we republished and the app went live! We then thought, Well, we’re on our way! We’ll submit the apps one at a time and they’ll all be approved. Hmm, not so fast ... Take 3: Duplicate Applications From: Developer <developer@myspace.com> Date: Thu, Jan 29, 2009 at 6:53 AM Subject: MDP Submission Denied: [Tic-Tac-Toe Chapter 3, app ID: 124920] To: opensocialatmyspace@gmail.com Thank you for your submission to the MySpace Developer Platform. We review all applications to ensure that they are compliant with our Terms of Use and Application Guidelines. Unfortunately, your current application does not meet our criteria for the following reason(s): Issue/Request: Duplicate applications (124920, 125557). If you have any questions please refer to the developer forums (http://developer.myspace.com/Community/forums/44.aspx). Please update your application and republish. Note, fixing the issues above will not guarantee approval as we were unable to perform a full review and may uncover additional problems. If you have questions, please refer to our forums or documentation. MDP Team
We submitted the next app, and it got rejected for being a duplicate of our previously submitted chapters.This is where things got a little hairy.The reviewer could see our other live app along with the app we had just published.The reviewer then loaded the live app, compared it to the app we were trying to publish, and felt they were similar enough to be considered duplicates. So how did we work around this? At first it seemed as though we’d need to 1. Create a bunch of e-mail accounts. 2. Make a spreadsheet with all the e-mail account logins and passwords.
Publishing Your App
3. 4. 5. 6. 7.
Sign each one up for MySpace. Apply for developer status on each account. Have each account create one app. Publish each app. Once all the apps were live, add our master account as a developer to each app.
And, well, that’s essentially what we had to do. But we did use one small trick that you may find useful. Our master account e-mail address is opensocialatmyspace@gmail.com. Gmail allows you to create throwaway e-mail addresses by using the plus (+) character in the address. Because of this, we didn’t actually have to create e-mail addresses for each app or developer account. For our Chapter 1 developer account, we signed up for a MySpace account with the e-mail address opensocialatmyspace+chapter1@gmail.com. At the end, we had a bunch of MySpace accounts with developer status, and all the e-mails sent to any of those accounts actually got delivered to our master account.This allowed us to sign in to a single e-mail account and administer several developer accounts at once. So, that took care of creating the developer accounts, but each app needs its own e-mail address, too.Well, we thought we could just follow the same trick and create apps using an e-mail address like opensocialatmyspace+chapter1app@gmail.com. But no, that didn’t work. The Create App page doesn’t accept e-mail addresses with the plus character. So, we could just create a bunch of e-mail addresses for each app, right? You’re wrong again, dear reader. Each app gets its own MySpace Profile like any other MySpace account, so you can log in as an app.This means you can create an app with a dummy e-mail address, log in to MySpace as the app with that e-mail address, and then change your e-mail address.When you first log in, you get a warning box at the top middle of your Home page saying your e-mail address isn’t yet verified; at the bottom of this box is a “Wrong e-mail address?” link. Clicking this link allows you to change your e-mail address, and there it will accept pluses in the address. The final result was that we had a bunch of e-mail addresses like this: opensocialatmyspace+chapter1@gmail.com opensocialatmyspace+chapter1app@gmail.com opensocialatmyspace+chapter2@gmail.com opensocialatmyspace+chapter2app@gmail.com opensocialatmyspace+chapter3@gmail.com opensocialatmyspace+chapter3app@gmail.com These all went to the same master e-mail address, opensocialatmyspace@gmail.com, and allowed us to manage everything from one central location. Phew! Of course, there are no guarantees that this bug or work-around feature won’t be fixed by the time you read this, but it’s worth a shot.
273
274
Chapter 12 App Life Cycle
Managing Your App Once your app is live, you can hide it, publish changes to the code, or delete it.
Hiding and Deleting an App When you hide an app, you change the status of the application to “Hidden.”This only stops your app from showing up in search results in the apps gallery (http://apps.myspace.com). It does not hide the app from users who already have the app installed. Warning The only way to hide an app from users who already have the app installed or force an uninstall is to delete the app. Hiding the app does not hide it from existing users.
If you want to delete an app, click Delete.You will be prompted with a confirmation. Click Yes and the app will be deleted.
Making Changes to a Live App (Multiple Versions) Once your app has gone live, there are two versions of it: n n
Development version Live version
When you click Edit App Source on a live app, you see two links: Dev App and Live App at the bottom of the screen (as shown in Figure 12.7).
Figure 12.7
Dev App and Live App selection options.
Managing Your App
Figure 12.8
Note the &appvers=dev at the end of the URL in the address bar.
Your Live version can’t be edited directly.You must edit your source code on the Development version. Any changes are immediately reflected in the Development version and don’t appear in the Live version until the app is republished.When you’re testing or viewing your Development version, you should see &appvers=dev at the end of the app’s URL, as shown in Figure 12.8. So, if you ever want to see the Development version of your app, just append &appvers=dev to the end of the query string for the surface you’re currently viewing.
Republishing a Live App You’ve changed your code, tested it, made some improvements, and you’re ready to release version 1.1; now what? Click Publish Changes to start the review process over again.
Changing the App Profile/Landing Page When users discover your app in the app gallery or through a friend’s page, they’ll click on your app link and be taken to your app Profile page.This is like a landing page, and it contains that magical button that you want everyone to click: Add This App. To see what your app’s Profile looks like, go to the Canvas page and click More Info at the top of the page. Now, do you see a boring default surface with little more than the basic details? Or something that looks more like the landing page shown in Figure 12.9? This is one area that developers often overlook, but it’s very important.The app’s Profile is the first introduction of a potential user to your app, so you should try to sell your app here in order to turn a potential user into a user.You’ll notice that the large professional app development companies have polished-looking app Profile pages.
275
276
Chapter 12 App Life Cycle
Figure 12.9
App Profile page for iLike (http:/myspace.com/ilike), a popular application.
Common tweaks include making the Add This App button larger, adding branding, putting up screen shots of the app in action, and just generally making the page more appealing to prospective installs. Editing your app Profile page is similar to the way users “bling out” their MySpace Profile pages, and you’re under the same restrictions.That means no JavaScript, but you can modify CSS and add image, anchor, and various style tags, like bold. To edit the app Profile page, you must first log in to MySpace as the app (not as the developer), click Edit Profile, then click the CSS tab. Inside the CSS box you can put any valid CSS.You can hook your CSS styles into existing elements on the page using their IDs or element types. For example, the Add This App button has an ID of profile_appinstall_button, so you can style it using the # CSS selector. Some other useful element IDs are found in Table 12.1, and you can use Firebug or view the source of the page to see all the markup on the page. Another option is to add custom HTML elements to the About section. Inside the Edit Profile page click the About tab; inside the box you can add HTML elements including and
tags. If any of the tags have a class attribute, you can add the class definition into the CSS markup under the CSS tab. Here’s an example of how we spiffed up the Tic-Tac-Toe app Profile. First, let’s look at the About section:
Managing Your App
Table 12.1
CSS Selectors in MySpace
CSS Selector
HTML Element
#profile_appinstall_button{}
Add This App button
#mainLayout{}
Main container for the contents of the Profile; doesn’t include the header or footer of the page or the gray (by default) background
#profile_appdetail{}
Container for the top left portion of the Profile, including the app’s icon, developers, number of installs, and categories
#profile_appinstall{}
Container for the top right portion of the Profile; includes the Add This App button
#profile_aboutMe{}
Container for the bottom portion of the Profile, from “About [app name]” to above the forums
#profile_forums{}
Container for the forum links
body{}
The body element for the page, useful for modifying the background color and image
The game that’s sweeping the world!
Features:
Custom backgrounds!
Challenge friends!
Play the computer!
Click the Add this App button above to play now!
Then the CSS section: body{ background-image:url(http://c1.acimages.myspacecdn.com/images02/31/l_e98f74e73ace46f2939c2de7322fcce4.png); } #profile_appinstall_button{ background-color:#1E4C9F; border:solid 5px #0cf; color:White; font-family:Verdana; font-size:28px;
The final product is shown in Figure 12.10. Tip You’ll notice that in both our CSS and About sections we include images. To save on hosting and bandwidth costs, both images are hosted by MySpace. While logged in as the app to edit the Profile, we uploaded the photos through the Upload link on the Home page, just as normal users would upload their photos. Once the photos are uploaded, you can view the source of the image by right-clicking it and selecting Properties. Voilà! Free image hosting for MySpace apps!
Figure 12.10
Updated Tic-Tac-Toe app Profile page.
Managing Developers
Managing Developers We already know how to edit the source code, details, and templates, but you can also use the developer platform to manage multiple developers. By clicking the Developers & Testers option under an app on the My Apps page, you can add and delete codevelopers. Once you click on Developers & Testers, you will be shown your friend list.To be added to your app, the developer must first be your MySpace friend. He or she must also be an approved developer; all of the approved developers in your friend list will have a gear icon under their pictures.To add a friend as a developer, click and drag his or her picture box into the Developers column.To remove the friend as a developer, simply click on the red circular icon in the Developers column next to the friend’s name. Before you grant developer status to anyone, it’s recommended that you back up your source code with every version of the app. Note You can’t set specific permissions for each codeveloper. Once someone is added as a developer to an app, the person can install the app in development status, make changes to the code, and even submit the app for publishing. However, the person can’t delete the app, add other developers, or acknowledge feedback. For example, if your app is denied publication because of an error, only you, the original developer, may acknowledge feedback and revert it to a ready-to-publish status.
There’s one last kick in the pants. As long as you’re someone’s friend and an approved developer, that person can add you as a developer to his or her app and you can’t say no or remove yourself as a developer.The app creator must remove you. So, if someone adds you as a developer on a questionable app, such as a Battlestar Galactica Sucks app, your name will be attached to it publicly unless you ask the original creator very nicely to please remove you as a developer.
Managing Testers You may want to have a few people test your app before you make it live, or test it yourself on different accounts.That’s smart. So, how do you do it? From the My Apps section of the developer platform, click the Developers & Testers link.You’ll be shown a list of your friends alongside a Developer column and a Test Users column. Simply drag and drop your friends over to the Test Users column to add them as testers. Once added, they’ll have instant access to your app in progress.
Event Handling—Installs and Uninstalls The concept of signed data calls was covered extensively in Chapter 8, OAuth and Phoning Home, and it really comes into play here with an app’s Install Callback URL and Uninstall Callback URL.These two settings fields can be found under the Edit Details tab of your app and are shown in Figure 12.11. Basically, whenever a user installs or uninstalls your app, MySpace makes a signed request to these URLs and passes the user’s ID. So, other than the OAuth signature, it
279
280
Chapter 12 App Life Cycle
Figure 12.11
Callback URL settings.
sends only one piece of information, the user’s ID.What you then choose to do with that piece of information is up to you. Some apps use the user IDs to simply track who has the app installed. Others may use them to create profiles for all the users and add them to an off-site database. Again, how you use the ID that’s passed in to your callback URLs is your decision.
Suspension and Deletion of Your App Suspension is pretty rare in the world of MySpace apps. If an app violates the Terms of Use and gets past the reviewers, the app developer usually receives a warning before the app is suspended or shut down.The developer then has 24 hours (or however long is specified in the violation notice) to fix the problem.This is not set in stone, and if an app grossly violates the Terms of Use, or the law, it can be suspended immediately. Any suspension or violation notices are sent to the e-mail of the app’s creator (not the app’s e-mail address or the e-mail address of any codevelopers), so make sure you monitor your app developer e-mail address. Warning If your MySpace account is deleted for violating the MySpace Terms of Use, you will not be able to log in and manage your application. Even getting your now-orphaned application deleted can be a major headache. There is a work-around for this, though—don’t violate the Terms of Use.
If your app is suspended, you need to fix the problem before the app is reinstated. So, while you can appeal on the forums, there is no quick fix if your app breaks the rules.
Summary
Summary If your app works and doesn’t violate the Terms of Use, you should have no problem getting it published and up and running. However, in the rare instance that an app is incorrectly flagged, we hope this chapter will help you navigate the ins and outs of app publishing. Remember, though, that it’s easier to catch flies with honey, so be courteous when dealing with the developer relations team. Note Code listings and/or code examples for this chapter can be found on our Google Code page under http://opensocialtictactoe.googlecode.com.
281
This page intentionally left blank
13 Performance, Scaling, and Security W
e’re going to delve into some of the more esoteric aspects of building your app empire. It’s one thing to build a “Hello World” app, or even put together a small mashup. It’s something else entirely to build an app that becomes an everyday part of life for over a million people. Consistent performance and user trust become some of the most important aspects of your app. This chapter is not an exhaustive study of Web app performance. If you want to study Web app performance, there are many resources online.Yahoo! Developer Network has done some excellent research into performance and publishes the information online. You could also pick up any number of books on performance, such as High Performance Web Sites by Steve Souders. It is a large topic worthy of several books.We are, however, going to cover some of the basics, such as the difference between performance and scaling of an application and some design hints to improve your app’s ability to grow.
Performance and Responsiveness To start this discussion we’re going to explore different aspects of performance. Performance is an overloaded term. Cars are often said to have good performance when they can reach top speed quickly and handle tight turns without sliding off the road. Another less common but equally correct statement is that a car has good performance if it gets good gas mileage and can travel long distances on a limited amount of fuel. People are often said to perform well on the job if they are able to handle multiple competing priorities simultaneously. It’s not that anything is getting done fast, but that lots of things are getting done.
What Is Responsive Performance and What Is Scale Performance? Performance is generally discussed in terms of how responsive a system is: “When I click the button, does something happen immediately?”To borrow from the auto industry, responsive performance is your “zero-to-60” measure. If your app is like my boss’s
284
Chapter 13 Performance, Scaling, and Security
Ferrari, it’s doing the equivalent of zero to 60 in 3.5 seconds. If your app is more like my jalopy (a 1985 Toyota Tercel, engine factory-rated at 65 HP when it rolled off the assembly line), it’s doing zero to 60 in 35.0 seconds. A secondary, and less common, measure of performance is in terms of scale: “If everyone in the world pushes the button at the same time, does it still work?”This is equivalent to a hot summer day when everyone in the city turns on the air conditioning. If the power grid scales, everyone’s air conditioner continues to function and homes and apartments cool off. If the power grid doesn’t scale, as in California, everyone’s lights flicker or brown out.This behavior is not to be confused with the situation in the mid-nineties when a rat chewed through a main feeder line, which in turn blew some circuits and took down the Western power grid (a failure of robustness). Nor is it to be confused with events of the late nineties into the early twenty-first century when Enron was running its “Death Star” and “Fat Boy” projects to create artificial scarcity and brownouts so it could jack up the price and “relieve the congestion” (a failure of public policy).
Design for Responsiveness Responsive performance is generally well understood. For years the common wisdom in many computing circles, particularly with Internet applications, was to ignore responsive performance and focus on feature delivery.The underlying assumption was that because computing power and network connection speeds were constantly improving, by the time a given product had reached market penetration, the underlying hardware and network speeds would have improved enough to account for poor performance designs in the early stages of development. An object example of this idea that hardware would fix all issues with software responsiveness can be witnessed in the early growth and attitudes of the Java community. Programming in Java was very powerful because of the rich and clean abstraction layer it provided.The promise of a cross-operating-system language that one could “write once, run anywhere” was very compelling. Unfortunately for Sun, the hardware performance envelope failed to progress quickly enough to account for the initial nonperforming virtual machine design. Over the course of a decade the JVM ( Java Virtual Machine) did improve and hardware did catch up, but the damage was done.Today, because of this and a number of other reasons, Java is almost never found in user-facing applications and user interfaces, for which it was originally designed. Instead, it is mostly relegated to back-end servers and some embedded applications. While CPU power continues its relentless Moore’s march, overall computer performance has hit a bit of a plateau. Much, if not most, of the excess computing power is being used for visual effects and to support more background processes and higherlevel programming tools. Another major driver in this performance plateau, in our opinion, is the rise of Web applications and the underlying constraints of network latencies and Web browser rendering engines and JavaScript interpretation.
Performance and Responsiveness
Broadband connection speeds, at least in the United States, have been increasing more linearly (actually, probably more like cubically, but we’re not math majors), not exponentially. Back in 1993, when NCSA Mosaic first launched and brought the Web out of the doldrums of Telnet and IRC, most people who had private Internet access were connecting via 9600Bps or 14.4kBps modems. For a short while, connection speeds almost doubled every two years (following a path similar to Moore’s law) but have moved more slowly in recent years as broadband providers seek to wring profits from their networks by creating artificial product differentiation.Two recent studies put the average connection speed in the United States somewhere between 1.9 and 4.8MBps.A U.S. Department of Agriculture census found that in 2007 only 33% of all farms had broadband connections.That means the rest were still on 56K modems or had no connectivity at all.This is in contrast to Japan, which has an average broadband speed of 61MBps (about ten times what would be considered the upper half of U.S. connection speeds). Responsive performance has seen a bit of a renaissance in the last few years. New research into user interaction and how great the effect of response times is on user behavior has highlighted the importance of responsive design.A responsive product is now seen as a strategic differentiator in a world of me-too products.That and new research into what leads to a responsive design have given rise to a number of rules for writing Web apps. Basic Rules Here are some basic rules that every developer concerned with performance and responsiveness should know: n n n
Place CSS styling at the top. Place JavaScript, particularly includes, at the bottom. Simplify your markup.
At this point in time these rules are bordering on common wisdom, much as “Size your images” and “Make pages smaller” were in the late dot-com days.
Responsive OpenSocial App Performance Guidelines There are also some less obvious guidelines that are specific to writing OpenSocial apps. These rules will help your app be more responsive when dealing with fetching and working with OpenSocial data. Following these guidelines will also help your app perform consistently and keep your users coming back rather than being annoyed by quirks and stutters when using your app. 1. 2. 3. 4.
Don’t re-request the same piece of data. Prefetch record lists for paging. Batch requests. Handle your errors.
We’ll cover each rule in greater detail in the following sections.
285
286
Chapter 13 Performance, Scaling, and Security
Rule 1: Don’t Re-Request the Same Piece of Data Many times when we review code, we see this mistake being made. Recently we were reviewing some code written in jQuery that was reselecting the same node each time it wanted to change a property: $("#navhead").addClass("enabled"); $("#navhead").text = "Welcome";
This business went on for another five lines. Because it was so convenient to reselect the node, the author mistook it for being free. In actuality each call went through a seven-layer call stack to select the node and add the jQuery extensions.The simple tune-up was to assign the result to a variable and reuse it: var navHead = $("#navhead"); navHead.addClass("enabled"); navHead.text = "Welcome";
This is fairly basic stuff.We know you jQuery people out there are saying, “Well, the dev should have just chained things.” True, but that is a jQuery-specific feature. Stick with us here for the example. The same rules apply for any piece of data being requested via the OpenSocial framework.The difference is that the performance ramifications are much more noticeable.Take the case of requesting the Viewer: function getViewer(myCallback){ var req = opensocial.newDataRequest(); var id = opensocial.IdSpec.PersonId.VIEWER; req.add(req.newFetchPersonRequest(id), "viewer"); req.send(function(data){ myCallback(data.get("viewer").getData()); }); }
This call is fine—once.What if this code were to be called every single time the Viewer data was needed? Not only is that execution clunky with extra callbacks, but it’s unnecessarily slow. A better solution is to execute this call once, place the result in a variable, such as myAppStuff.Viewer, and read it from this variable every time it is needed. In the early versions of the MySpace OpenSocial container, the container code would actually make an XHR request to the REST endpoint to refetch the Viewer data every time this code was called. Over time, MySpace has made some good optimizations on the container side. Now, many data call results are either cached and re-served client-side or prefetched. Even with those optimizations, the code still executes a 20-function code call each time the data is re-requested. The MySpace optimizations for fetched data do not extend to the results of makeRequest calls. If your code is calling out to your servers or some other external server, only re-execute the call if you think the data might have changed.
Performance and Responsiveness
Rule 2: Prefetch Record Lists for Paging This performance enhancer is about both efficiency and responsiveness. Paging through a list of friends is a very common operation.There are two ways to do this: our way and the wrong way. ( Just kidding, but only a little.) We’ll start with a simple example.Your app wants to page through the Viewer’s list of friends, four at a time.The simple and obvious solution is to place code handlers under the Prev and Next links to create a newFetchPeopleRequest, passing in the start record and number of records to retrieve.The guts of the code appear something like this: var pageSize = 4; var curPage = 0; function loadFriendsPage(){ var dataReqObj = opensocial.newDataRequest(); var viewerReq; var viewerFriendsReq; var startNum = (curPage * pageSize) + 1; var idspec = opensocial.newIdSpec( {userId : 'VIEWER', groupId : 'FRIENDS'}); var requestOpts = {}; var fldEnum = opensocial.DataRequest.PeopleRequestFields; requestOpts[fldEnum.MAX] = pageSize; requestOpts[fldEnum.FIRST] = startNum; viewerFriendsReq = dataReqObj.newFetchPeopleRequest( idspec, requestOpts); dataReqObj.add(viewerFriendsReq, "viewerFriendsReq"); dataReqObj.send(loadFriendsPageCallback); var prev = document.getElementById("prevLink"); if(curPage == 0){ prev.className = "pageaction disabled"; } else{ prev.className = "pageaction enabled"; } } function loadFriendsPageCallback(data){ var elem = document.getElementById("currentFriends"); var label = document.getElementById("pageHeader"); var fData = data.get("viewerFriendsReq").getData();
287
288
Chapter 13 Performance, Scaling, and Security
if(data.hadError()){ var err = "error: " + fData.getErrorMessage(); label.innerHTML = "error: "; return; } var startNum = (curPage * pageSize) + 1; var endNum = startNum + pageSize; label.innerHTML = "Friends " + startNum + " to " + endNum; var items = ""; fData.each(function(person){ var src = person.getField(opensocial.Person.Field.THUMBNAIL_URL); items += ""; items += person.getField(opensocial.Person.Field.NICKNAME); items += " "; }) elem.innerHTML = items; } function getPreviousFriends(){ if(curPage < 1){ return; } curPage--; loadFriendsPage(); } function getNextFriends(){ curPage++; loadFriendsPage(); }
The code works fine. If you test it in a simple scenario, the performance is probably okay as well, provided it’s not during peak hours. Looking at the network traffic (see Figure 13.1) tells a more interesting story. Each request shown in the figure is approximately 150ms.That’s not much time in and of itself, but that time adds up. If the user goes through six pages, a full second of time is used waiting for the network.What’s more, if the user pages backward, the same data your app just had is re-requested.This violates Rule 1: Don’t re-request the same piece of data. If your app is simultaneously attempting to retrieve album information or communicate with another server, this design would add noticeable and unnecessary overhead. A better design, and one used by the FriendPicker widget, is to ask to preload all the data in the background, then page by moving through the local data list. In order to make this happen, we use most of the same code with two additional code elements: an
Performance and Responsiveness
Figure 13.1
Network traffic on a wasteful pager.
array to hold all the friend information and a new display function. Here is a look at the modified method of retrieving the friend list: var pageSize = 4; var curPage = 0; var allMyFriends = []; function loadFriendsInfo(){ var dataReqObj = opensocial.newDataRequest(); var viewerReq; var viewerFriendsReq; var idspec = opensocial.newIdSpec( {userId : 'VIEWER', groupId : 'FRIENDS'}); var requestOpts = {}; var fldEnum = opensocial.DataRequest.PeopleRequestFields; requestOpts[fldEnum.MAX] = 100; requestOpts[fldEnum.FIRST] = 1; viewerFriendsReq = dataReqObj.newFetchPeopleRequest( idspec, requestOpts); dataReqObj.add(viewerFriendsReq, "viewerFriendsReq"); dataReqObj.send(loadFriendsPageCallback); } function loadFriendsPageCallback(data){ var fData = data.get("viewerFriendsReq").getData();
289
290
Chapter 13 Performance, Scaling, and Security
if(data.hadError()){ var err = "error: " + fData.getErrorMessage() label.innerHTML = "error: "; return; } fData.each(function(person){ var src = person.getField(opensocial.Person.Field.THUMBNAIL_URL); var item = ""; item += person.getField(opensocial.Person.Field.NICKNAME); allMyFriends.push(item); }); showCurrentPage(); }
You may notice that the code for these two methods is actually smaller. It’s also cleaner because the two methods simply deal with retrieving and managing the data.The basic information of each friend (name and image) is pushed into our new data array, allMyFriends. Display of the data is now external to the data retrieval methods and in the showCurrentPage method.This separation of concerns leads to more efficient and maintainable code. function showCurrentPage(){ var elem = document.getElementById("currentFriends") var label = document.getElementById("pageHeader") var startNum = (curPage * pageSize) + 1; var endNum = startNum + pageSize; label.innerHTML = "Friends " + startNum + " to " + endNum; var items = ""; if(startNum < allMyFriends.length){ for(var i=0; i < pageSize; i++){ if((startNum + i) >= allMyFriends.length ){ break; } items += allMyFriends[startNum+i] + " "; } } elem.innerHTML = items; var prev = document.getElementById("prevLink"); if(curPage == 0){ prev.className = "pageaction disabled"; }
Performance and Responsiveness
else{ prev.className = "pageaction enabled"; } }
function getPreviousFriends(){ if(curPage < 1){ return; } curPage--; showCurrentPage(); } function getNextFriends(){ curPage++; showCurrentPage(); }
Now each time the user clicks the Prev/Next link, the page is repainted with data coming from a local array variable, not across the network.The network traffic sniffer results shown in Figure 13.2 also tell the story. The initial request now takes 400ms. It happens only once, though.This image also displays all the traffic after scrolling back and forth through the list several times.These measurements were taken on an account with 52 friends.The step-by-step pager would have taken 1950ms to retrieve the same number of records—almost five times as long! Not only that, but it would have continued to waste network requests each time the user scrolled.To really see the difference, the user can click the Next link in quick succession in both implementations.The second displays smoothly and quickly.The first will occasionally stutter, then catch up. Sometimes it will skip pages and can even get out sync if the XHR responses are returned out of order.
Figure 13.2
Network traffic on a good pager.
291
292
Chapter 13 Performance, Scaling, and Security
Rule 3: Batch Requests The OpenSocial spec has a built-in mechanism for batching requests. Use it. At the time of this writing, MySpace sends these requests individually, but if you write your code to batch things, when the batching performance optimization is made on the underlying MySpace container, your code will be able to take advantage of it immediately. For servers under your control, design your endpoints to deliver all the data your app needs to execute a given task in one shot.This may feel as if it’s violating the integrity of your code design, where each type of data gets its own endpoint, but it is a worthwhile optimization. As an example, say your app allows virtual flowers to be sent to a friend. The user has an allotment of credits for buying flowers. One design is to have one call to purchase the flowers, then a second call to request the user’s remaining credits and update the “available credit” display. A better design is to make a single request that purchases the flowers and returns the remaining available credits in the response.You just cut your server request load in half! Rule 4: Handle Errors Stability and robustness will be covered in more depth later in this chapter, but they’re worth touching on now. Often when an unhandled error occurs in program execution, the program is left in an indeterminate state—it doesn’t understand where it is. As it attempts to continue execution, it may make calls with insufficient or invalid inputs, generating even more errors. More often than one might think, a program descends into a state of thrash— alternately requesting one piece of data incorrectly, getting a bad response, requesting a different piece of data incorrectly, getting a bad response, and ultimately descending into an infinite loop, thrashing back and forth between requests until a stack overflow happens or the browser pegs the CPU and has to be forcefully killed. If this sort of error happens often enough with your app, it may come to the attention of MySpace.When that occurs, you are typically given a few hours to rectify things. If the errors are bad enough that your app is adversely affecting other apps by tying up connections or causing user Home pages to crash, it may even be summarily suspended. Handle your errors.
Design for Scale An application is said to “scale” if it maintains its performance characteristics while the number of users increases dramatically. MySpace scales. At the time of this writing, MySpace has in excess of 150 million unique users. Friendster did not scale. It predated MySpace and had more users initially, but once it hit some level of critical mass, the system froze and became nonresponsive. As a result, everyone left and moved to MySpace.
Design for Scale
Application Scale Definitions Applications are typically designed to perform and scale within one of these definitions: 1. Small/Workgroup Typified by a user base of fewer than 100 users/month. Mom-and-pop Web sites, niche e-commerce sites, and internal IT projects designed to address the needs of a single workgroup fall into this range. Deployments are usually single-server or partial-server (a server hosting several applications). 2. Enterprise Enterprise-scale applications are designed to satisfy the needs of an entire organization or a large part of an organization, servicing anywhere from a few hundred to tens of thousands of users. They tend to be industry-specific, like a medical records management system or a payroll system. Deployments may be to a single dedicated server or a small cluster (three to five computers). Classic relational databases and reporting systems are very common. Enterprise applications are almost never consumer-oriented. 3. Internet Internet-scale applications may service from several hundred thousand to several hundred million users. Internet-scale applications are almost always consumeroriented, although some also provide business-oriented features. These applications have unique designs to reach their performance needs. The architecture appears foreign to anyone trained solely in classic computer science and data modeling and seems to break many core rules. Deployments are across tens, hundreds, or even thousands of servers and must be able to handle individual server failures.
There are several applicable guidelines to keep in mind when designing for scale.The sidebar “Application Scale Definitions” identifies different scaling terms to consider. At MySpace, the developers think only in Internet scale. As an app developer who could reach millions of users, you should also be thinking in Internet scale.
App Guidelines for Internet-Scale Performance Here are some simple guidelines to consider when designing for scale. Follow these rules and you won’t get tripped up by some of the most common scaling mistakes. 1. 2. 3. 4. 5.
Know your scaling point. Identify scaling bottlenecks. Remember what you know. Scale horizontally. Move beyond relational databases.
293
294
Chapter 13 Performance, Scaling, and Security
6. Push work to the nodes. 7. Consider utility computing. 8. Load-test your system. Your app must also scale.What works for a few hundred or even a few thousand users may not work for a few hundred thousand users. If you do hit the magic tipping point and your app takes off, there will be little time to redesign things. Take the example of Own Your Friends (OYF), the first big-hit app on MySpace. When Own Your Friends launched, the install rate was just a few hundred a day. Something happened at around 30 days and the app reached the magic tipping point (somewhere between 20,000 and 60,000 installs), and the app install rate jumped by an order of magnitude. At 60 days OYF was growing by over 100,000 installs a day.The developers (moonlighting at the time) were staying up all night and working weekends to try to bring more servers online and eliminate bottlenecks.Yes, it may have been exciting and frantic, but it was also a time that very nearly cost them the business. Users are fickle, and if you fail to execute when your app becomes the big hit, you rarely get a second chance to make a first impression. Now, let’s delve just a little deeper into our scaling rules. Rule 1: Know Your Scaling Point The “scaling point” is the point in a system’s utilization where it is no longer able to accommodate additional usage. If this scaling point is outside the system’s expected usage parameters, the current architecture should be adequate. If this scaling point is inside the system’s usage parameters, you need to adjust your architecture to accommodate a larger scaling point. In terms of pure numbers, we can calculate how much data will overflow the available RAM with some simple math. Let’s say you have a user table with the structure shown in Table 13.1. Every row of data is 304 bytes. If 2MB are available to hold this database table, your table could hold 6578 records and still fit completely in memory.That’s not very many records. All this begs the question of how much data is too much for a single server. That’s not an easy question to answer because it depends so much on how your app is using the data and the overall load of the system. Why don’t more database-backed software applications cause their servers to tip over? It has to do with partial usage, the principle of locality, and load. Irrespective of the number of users of a system, not all of them are actively pushing buttons at the same
Table 13.1
Sample User Table
Column
Field Data Type
Size
ID
32-bit integer
4 bytes
Name
100 Unicode character
200 bytes
FavoriteColor
50 Unicode character
100 bytes
Design for Scale
time. Also, only some of the data is used at a given time, and within a time period it tends to be the same data, like a nurse updating the same ten medical records. An enterprise-class decision support system might have five or six data tables with a few hundred thousand records in them, plus a multitude of other tables having 20 to 100 records in them.The application is quite often required to go to hard disk in order to look up the data to perform some operation, such as running a report. Only a fraction of the people who have access to a system use it at one time, though. In a company of 10,000 people, only 10% may ever access the system. Of that number, perhaps only 10% use it at a given time.That works out to 100 concurrent users. If the average time between clicks is 5 seconds, the application must be able to process 20 requests/second for this system to operate. Even with an architecture not specifically designed to scale and the database reading from disk on every request, the system should continue to perform handily (the hard disk latency allows for over 100 requests/second).To reach the scaling tipping point of this system, usage would have to increase by a factor of 5 (go from 10,000 employees to 50,000 employees or increase usage from 10% to 50%). Rule 2: Identify Scaling Bottlenecks The first step in designing for scale is to identify potential bottlenecks. Bottlenecks occur wherever there is a limited resource that many or all of the clients need to access.This could be a REST endpoint on a single server, a routing method, a shared database table, or even a single shared log file. The first scaling bottleneck most applications hit is the database.This tends to be the classic point of scaling failure. If you can design your system to not use a database, do it. If your needs are simple enough, the app data store may be enough.Then you will be building on the battle-hardened MySpace infrastructure. Unfortunately, apps of any complexity can rarely get around using a database. Other sorts of back-end data stores have been experimented with, such as simply writing to a flat file, but a database almost always outperforms them under load.
Dissecting How Hardware Affects the Database There are several reasons why databases tend to be the primary scaling bottleneck: the physical limitations of reading from a hard drive, CPU saturation from processing queries, and buffer cache running into the wall of available physical memory. If a system is using a misconfigured database, it may also run out of available concurrent database connections. To illustrate, let’s examine the read performance of various components in a computer, from the CPU down to the magnetic hard disk drive. Performance tends to change by orders of magnitude. In-CPU registers and L1 cache read at a speed of 2 CPU cycles. The L3 cache, if present, has a latency of 24 CPU cycles. This is incredibly fast. Hertz is a measure of cycles per second. A 1GHz CPU could conceivably find over 1 billion pieces of data from its cache in 1 second.
295
296
Chapter 13 Performance, Scaling, and Security
Not much data can live in these caches, however. The L1 cache in modern CPUs is around 128kB, depending on the processor. The L2 cache is commonly 256kB or 512kB. Higher-end chips have added an L3 cache as large as 8MB. This is still a fairly small amount of data in the grand scheme of things. A small database with 5000 data rows could easily fill that cache. And the reality is that CPU cache is going to be filled with numbers and instructions and bits the computer needs to execute its immediate responsibility. There’s no room for your data there, other than what is being actively worked on. Moving from cache memory that is directly on the CPU to RAM memory, the performance jumps by another order of magnitude. The L3 cache access of 24 CPU cycles is now closer to 200 CPU cycles on 6ns RAM. That translates to theoretically finding 5 million pieces of data in a second using a 1GHz CPU. This is still nothing to scoff at, but it’s quite a difference from data on the CPU cache. A hard disk, where most of your data lives, is not one, not two, not three, but several orders of magnitude slower. This calculation is not completely accurate since it depends on CPU frequency and bus speed, but it is about 1 million times slower to read data off a hard drive than it is to read it out of memory. A 7200RPM drive has a latency of approximately 4ms and a seek time of 9ms. This correlates to finding somewhere between 100 and 250 distinct pieces of data from hard disk in 1 second. 1ms = 1,000,000 ns On average, in the time it takes to find one piece of data on hard disk, a computer could find 20,000 pieces of data in RAM. Of course, these numbers are somewhat deceptive since they are calculated based solely on latency and seek times. In reality, the data should be stored contiguously and read from the seek point. Then the constraints become hard disk platter rotation speed and bus transfer rate. The numbers we’ve given are enough to illustrate the point, however. This performance spread is why most databases attempt to load all available data into RAM memory. There was a tremendous performance boost to SQL Server once it started loading entire tables of data off hard disk and completely into memory. In this manner an entire database can be loaded into RAM and processed at a rate several orders of magnitude faster than if the physical hard drive had to be accessed continuously.
Modern databases use several tricks to speed up performance. One of those tricks is to load the entire database into RAM for processing.This leads to a tremendous improvement in access speeds—of several orders of magnitude. So if the database is in RAM, why is this a scaling performance issue instead of a responsiveness performance issue? It has to do with database size and how much RAM is available. If the database is small enough to fit entirely in memory, the performance will be off the charts. As soon as the database outgrows the size of physical memory available (about 4GB on a 32-bit machine, minus the space needed by the operating system), it must store part of its information on the hard disk. If the database has to read from the hard drive into memory too many times, the computer starts “thrashing.”Therefore, as soon as your database
Design for Scale
crosses that line in terms of the amount of data stored, the performance of your app will plummet. Code that previously could execute immediately will become blocked waiting for the database to respond.This in turn ties up HTTP and database connections.The connections will begin to queue up until the queue is full. Any new connections get turned away, and the Web server returns 503 “Server too busy” errors. At MySpace, we refer to a server exhibiting this behavior as “tipping over.” Rule 3: Remember What You Know This rule is a variation of “Don’t re-request the same piece of data” from the responsive performance section. Let’s say we are going to see the kind of growth outlined previously and expect usage to jump from 10% to 80%—well above our parameters.The first step is to move often accessed and seldom changing data into a location where it is guaranteed to be in RAM. For example, let’s assume the app in question allows users to pick their favorite color.The 20 or so available colors and their hex definitions should be loaded from the database once and placed into a static hash table object.This avoids any question of whether the data will be read from memory or disk.The same holds true for all the domain reference tables—lookup data defined in the system that rarely or never changes. Much of the data being accessed is read-only or limited to a certain session. Often this can be placed in a caching layer, like a MySQL MEMORY table or a memcached system distributed across a cluster of computers.These systems start to get fairly complicated and are a little beyond the scope of what this book covers, but be aware that they exist. In reality, a reasonably intelligent database, like SQL Server, does a fairly good job of automatically keeping the most frequently accessed records in memory, provided the number of “most recently used” records can fit in a single system’s memory. Rule 4: Scale Horizontally Once an app hits a point where it can no longer live on a single computer, it must be split to scale across multiple computers.There are two ways to do this: vertically and horizontally.Vertical scaling is scaling by specialty: One computer is the Web server and another is the database. Another might serve static files, like a content delivery network (CDN). Horizontal scaling is distributing the same work across many computers.The URL www.example.com could resolve to a Web farm of hundreds of machines.The database can be federated across many, many computers. Each computer has the same kind of data but is responsible for only a small segment of the total data, say, 500,000 records. Vertical scaling is typically used to improve responsive performance. Long-running or expensive processes, like running reports, could easily bog down a Web server while servicing only a few users. Unfortunately, there is a limit to how much specialized processing can be segmented out, so vertically scaled systems often top out at distributing the load across two to five machines (Web server, CDN, transactional database server, reporting database server, data warehouse). Some things simply can’t be vertically scaled,
297
298
Chapter 13 Performance, Scaling, and Security
such as a system containing 100 million user records. Horizontal scaling can distribute load across a much larger set of machines. Hundreds of machines are sometimes used to scale out different feature areas of MySpace. Designing for horizontal scale means not requiring all of your data to exist on the same machine or to come from any other single machine. Classic ASP Session handling was notorious for causing performance problems and hampering horizontal scaling. The problems were actually twofold: It dropped the Web server into single-threaded execution per user, and it couldn’t be distributed across different computers. In using classic ASP Session, an application essentially condemned itself to not using a Web farm and not being able to scale. Rule 5: Move Beyond Relational Databases Relational databases are hard to beat within their domain space—small to enterpriseclass transactional systems. Joining data tables starts to lose its efficiency once a system has to scale across multiple servers. Large federated database systems often perform better when laid out in a dimensional model, like a data warehouse.There are data redundancy issues, but the performance gains of avoiding multiple joins can be worth it. Rule 6: Push Work Out to the Nodes In the same way that using a Web farm distributes request load across many computers, some work can be pushed all the way down to the app running in a client browser. By using the JavaScript engine in client browsers, your app can create what amounts to a giant grid computer.Whenever practical, perform calculations in the client script prior to sending the information back to the server. Consider an app that needs to draw a pie chart for each user based on the user’s data. It could be done efficiently for a few users on the server, then pushed to the client as an image.Thousands of requests would make this operation a bottleneck.This work would scale better if the data points were pushed to the client and the browser drew the pie chart using Canvas, Flash, or any of a number of JavaScript graphing solutions, such as PlotKit. Rule 7: Consider Utility Computing Utility computing may have sounded like crazy talk a few years ago, but today it makes sense. Amazon Web Services, Google App Engine, and Microsoft Azure are the three big players in this space. Rather than paying your own network engineers and leasing rack space, it can be much more cost-effective to build on one of these services and leverage the expertise of a multibillion-dollar corporation. Utility computing, by its very nature, is designed for Internet-scale businesses. One of the reasons utility computing sounded like snake oil a few years ago was that few applications needed Internet scale; most were only enterprise scale. Existing applications were also not designed to take advantage of a utility computing architecture, and an existing application is unlikely to port easily to a utility computing model.
Stability and Fault Tolerance
New applications, such as your “Next Big Thing” app, can be built from the ground up on a utility model. Rule 8: Load-Test Your System You can never fully predict how your system will behave in the wild based on class diagrams or whiteboard architectures.You can, however, make a reasonable simulation to find the most egregious bottlenecks and cross-server interaction issues.This is done by load-testing your system. There are numerous load-testing tools available, including numerous free ones. A load-test tool for a Web application starts with one basic premise: It must simulate numerous simultaneous connections to the Web application. From there, the complexity grows; the tools are able to record and play back requests, remember cookies, or perform full activities with your app. A fairly competent tool is the Microsoft Web Application Stress Tool, available from this URL or by searching the Web: http://support.microsoft.com/kb/231282.You can also search for “free load-testing tool” to find numerous results or sites that review available tools, like OpenSource testing (www.opensourcetesting.org/performance.php). If you desire to fully simulate a user experience, tools like Watir or WatiN allow programmatically driving a browser. Sometimes a combination of multiple tools is required to satisfy your needs.
Stability and Fault Tolerance While stability and fault tolerance do not look integral to security and performance at first blush, they are actually one of the most integral aspects of both items. If your app is so unstable that it falls flat on its face twice an hour, it won’t have very good responsive performance, will it? Sometimes these failures even puke up error messages detailing some internal aspect of your system, like database table structure, that can later be used to mount an attack. These rules will help you avoid simply “forgetting” to handle stability issues until it’s too late. Ingrain these simple rules in how you code instead of making them a checklist to run after the fact, and you will avoid many firefights as your app grows. 1. Validate inputs. 2. Test OpenSocial
objects for errors. 3. Provide time-outs and error flow. 4. Don’t assume that weird error was an anomaly. DataResponse
Rule 1: Validate Inputs One of the first things you should do is validate all the inputs to your system. Even user-entered data that is coming from a constrained drop-down should be validated. If you don’t, it may be possible for a nefarious user to inject string data into an integer parameter and bring your carefully built house of cards tumbling down.Validating inputs winds up being both a stability and a security concern.
299
300
Chapter 13 Performance, Scaling, and Security
Rule 2: Test OpenSocial DataResponse Objects for Errors Too often developers consider a single pass of success a sign that they are finished coding. Just because you finally got your app to read the Viewer’s photos does not mean you are finished. Far from it.There are many, many ways an OpenSocial endpoint may fail. Here are a few examples: n n n n n n
App not installed Viewer not defined Network congestion causing a time-out User has no valid data defined for given endpoint User has not granted app needed permissions Underlying server endpoint has a failure
In any of these circumstances your app may go into an indeterminate state. Sometimes the user flow can break because of insufficient data, forcing the user to reload the Web page (or uninstall your app). A little care and planning can keep things humming or get the user through these situations gracefully. At the very least you need to code general error paths. For an extra bonus, try to code recovery paths so the user doesn’t have to refresh the page.
Rule 3: Provide Time-Outs and Error Flow In addition to errors with OpenSocial endpoints, you may have errors with your servers or third-party servers.Try not to just fail. In the course of writing this book, we discovered a silent failure in the main makeRequest method.This was very bad form, because the developer could never write code to compensate for the issue. Databases may time out when overloaded. Rather than doing nothing and allowing all your impatient users to click refresh 20 more times in frustration, further overloading your database, you should display a friendly error with a helpful message, like “Don’t panic.” Not only will it help your scaling and recovery, it’s a common courtesy.
Rule 4: Don’t Assume That Weird Error Was an Anomaly If you saw it once, it will happen again. One of the unexpected rules of working at Internet scale is that there are no edge cases. An error that occurs in 0.5% of cases will happen 5000 times if you have one million users.You just have to decide if those 5000 occurrences justify the cost of tracking down the issue and fixing it.
User and Application Security Security has become a much more prominent topic recently. Identity theft and credit fraud are simply some of the best-known issues with security, and compromised account logins are often how identity theft and credit fraud happen. Add to that malware
User and Application Security
installation, having a compromised machine become part of a botnet for spammers, and phishing.Then there’s simple in-game cheating to think about, too. If you ignore security, your app might be the gateway to compromise hundreds or thousands of people’s personal information. The two basic types of security we’ll discuss are user data security and application security.
User Data Security There is only one main rule in user data security: Respect the rights of your users. Follow it and you’ll already have the answer to most of the other questions that may arise. Often this becomes a matter of policy, so you need to be willing to read (or have someone read for you) the various terms of service.You will even need to consider your own terms of service. More than a few companies have gotten themselves into hot water by thinking that as soon as someone used their services, they owned the right to resell every bit of data that user had provided. At best they ended up with some upset users. Many ended up in court, the target of class-action lawsuits.The first step in avoiding problems is to settle on, write down, and publish your privacy policy. If your app will be pulling user data off of MySpace and using it on your servers, you must be aware of MySpace policies regarding data availability and off-site caching of data. Read the MySpaceID Off-Site Service Developer Addendum if you plan to hold data on your servers.The basic rules are that you can only temporarily cache data off of MySpace.Your app must periodically refresh that data. Feeds, like the friend feed, must be refreshed every 4 hours. User data may be kept for only 24 hours; then it must be purged. It may be refreshed at that point, if required. About the only thing from MySpace your app can store indefinitely is the user’s ID. Information your users supply directly to your app is a different story, though.That is typically governed by your terms of service, applicable laws, and your own good sense. In general, don’t store more data than you have to. Don’t, for instance, store credit card numbers without good reason. And for goodness’ sake, don’t store them in an unencrypted, publicly accessible database. Don’t store independent logins without need—you can rely on the MySpace platform to provide accurate user ID information. An exception to this would be any sort of e-commerce operation. In this case have your security team evaluate the best path. A third-party payment gateway, like PayPal, is a good starting point. Unless there is good reason, don’t provide access points to bulk-download user data. In all likelihood, someone will hack you.
Application Security An application is only as secure as its weakest link. In the case of most apps, this is the interface between your server and the MySpace servers.The best way to address this issue is by specifying all requests as signed. Specific instructions on using signed makeRequest calls are found in Chapter 8, OAuth and Phoning Home.
301
302
Chapter 13 Performance, Scaling, and Security
In addition to locking down the MySpace-to-your-server access points, you need to confirm that other access points to your app are locked down as well. Consider the case that you are writing a space battle MMOG.Your users have to play scenarios and use the game to earn credits. Credits are then used to buy better ships. Users can also earn credits by watching advertisements or registering to donate plasma.To facilitate plasma donation, you set up a REST endpoint so that the blood bank can call back to your app with a user’s ID after that user finishes making an appointment to donate. Because the blood bank is not technically astute, you opt to leave this unsecured. Now Hannah Hacker just loves your game. She stays up nights playing it, hoping to get enough credits to buy that fleet of dreadnoughts. One night she gets so excited she decides to go ahead and donate plasma. Being a good hacker, she’s got Fiddler fired up and is watching all the network traffic while she’s playing.What’s this? It’s an unencrypted redirect to your server from the blood bank with her user ID and a confirmation number? So that’s how plasma donation credits are counted.Well, after about 15 minutes of script work Hannah just signed up to donate plasma 359 times. Don’t roll your eyes.This scenario has already played out more than a few times.
Hacking and Cracking While this does not seek to be a security book, it is worth describing a few of the more common exploits used on the Web.We hope you have at least a passing awareness of these attacks and that this will serve only as a short refresher. If not, you’d better get cracking (no pun intended) and do some real research. The scenario in the previous section describes a simple method to discover and hack an open endpoint. If you are not using signed requests, you might as well add a bunch of buttons titled “Hack This” with a text field to facilitate easier data entry. Earlier, in the chapter about data persistence (Chapter 4, Persisting Information), we wrote a Cookie Jacker application. Everyone can read your cookies (including your competitors), so only the most transitory data, like temporary session data, should be placed there. And really, not even that. The app data store is not a secure store. Data is stored on a per-app/per-user basis, but your app can read the Viewer’s friends’ app data. Even if your app does not explicitly read other users’ data, it is quite easy to hack. Here’s how:You are very foolishly storing users’ private login information to your e-commerce site in the app data store. I, being a curious and slightly evil person, know this. I fire up your app with my credentials. Next I re-edit the HTML of your app using Firebug to include my hacking script, which fires a newFetchPersonAppData request. Once I confirm that my script works, I start sending out friend requests to everyone I see who has your app installed so I can read their app data stores.Violà! You have allowed your app to be used to hack yourself. Unvalidated input fields can be a real problem, as mentioned in “Stability and Fault Tolerance.” SQL injection remains a very dangerous attack. As an example, let’s say that code in your app takes a name field and constructs the following SQL statement: INSERT INTO MyAppUsers(id, name) VALUES(nnn, '$UserName');
Summary
The user enters Bob');
DELETE * FROM MyAppUsers; --
Your SQL statement then becomes INSERT INTO MyAppUsers(id, name) VALUES(nnn, 'Bob'); DELETE * FROM MyAppUsers –-');
Many frameworks have built-in testing for SQL injection, but you should do your own testing. Cross-site scripting (XSS) attacks remain a very popular attack vector.The MySpace message channels (requestSendMessage, notifications) do a fairly good job of stopping XSS attacks. If your app has an independent user-to-user communication channel, you will have to write your own security mechanism to stop these. There are many other attack vectors to which Web apps are susceptible, and new issues are being discovered all the time. Neglect security at your peril.
Summary Performance and security are major factors in app design.With a little care and forethought, you can save yourself some messy rewriting under fire. Responsive performance in apps is most commonly improved by optimizing the front-end client code. Minor responsiveness improvements on the server pale in comparison to the overall wire time to push information back and forth across the Internet. Designing for scale is one of the most common and difficult problems in Internet computing. Scaling performance often involves limiting the number of requests from the client and designing the database to scale horizontally.These and the other guidelines presented in this chapter will help you avoid some of the most common errors. Load testing will help as well. At the end of the day, though, you cannot completely predict how an app will behave in the wild.
303
This page intentionally left blank
14 Marketing and Monetizing Tdo here are two types of app developers: those who write apps for profit, and those who it for fun. At first glance this chapter may seem to be directed toward the former group, but we think everyone can get something out of reading it. For those looking for a secondary revenue stream from writing apps, we don’t need to convince you of the need to monetize. In this chapter we’ll look at the different monetization techniques, including ads and micropayments, and see what’s involved in setting them up.There are a slew of services out there, each promising the best CPM, or cost per 1000-page impressions. We’ll compare and contrast some of the more popular services in order to help you determine which best suits you and your app’s needs. For those who are just writing apps for a bit of fun, there are a few things here for you to consider. You may just be curious about OpenSocial and the MySpace platform and want to tinker around. Or perhaps you want to learn some new coding techniques, and writing a Web service and JavaScript-based social Web app appeals to you. Or maybe you just want an excuse to try out Google App Engine. If that’s the case, and you don’t plan to publish your apps, this chapter may not be for you. On the other hand, maybe you’re not in it for the money, but you want to make and share some of your creative ideas for apps.Well, who wants to spend hours making a great app, only to have 14 people install it? The sections on the app gallery and app Profile may help you get a few extra installs, at which point the viral features of your app can further spread the word. But who can’t use a few extra bucks from time to time? Maybe you could use that money to beef up your app’s infrastructure to give your users a better experience, or maybe just to buy yourself something nice. You deserve it! It’s a really great app! It can be quick and easy to hook up an ad network to your app, and it’s always rewarding to get paid for your hard work. And finally, why should you take our word for it? Well, in this chapter you won’t have to. We’ve tracked down the developers of some of the more popular apps on MySpace so they can share their success stories. How did they get so many users? How are they monetizing? What suggestions do they have for up-and-coming app developers? Look for their insights and tidbits at the end of the chapter.
306
Chapter 14 Marketing and Monetizing
Using MySpace to Promote Your App MySpace provides a few ways to showcase your app, along with a fairly reasonable advertising platform. Let’s take a look at some of the methods MySpace provides that you can employ to attract new users.
The App Gallery The main gateway that connects users to apps is the app gallery. It’s a simple list of all the apps on MySpace. The list can be sorted, filtered, and searched.This is where the link in the My Apps module on every user’s Home page will lead, as well as any other apprelated links provided by MySpace. Know it and learn to love it, for it is the nexus of the MySpace app universe. Take a look for yourself at http://apps.myspace.com/. This is where the usual “How do I get to the top of the list?” search engine gaming starts. Similar to the way a higher search engine listing can dramatically increase traffic to a Web site, making it easier for users to find your app in the app gallery can quickly increase your number of installs. There are three main ways for the app list to be filtered. Along the left-hand side of the page are the various categories, such as Communicating, Fashion, Games, and Sports, as well as a language filter. The filters along the top of the page are fairly selfexplanatory; they are Alphabetical, Most Users, Newest, and Recently Popular. All of these filters can be active at the same time; so, for example, a user can view the newest apps, in Japanese, under the Events category, as we have shown in Figure 14.1.
Figure 14.1 The app gallery filtered to show the newest Events-based apps in Japanese.
Using MySpace to Promote Your App
The page defaults to the categories All Applications, All Languages, and Recently Popular, so it should be your goal to get your app as high as possible on this list for as long as possible. Landing on this list can give your app a nice jump start; we’ve seen anywhere from several hundred to several thousand installs generated within a 24-hour period. At that point the awesomeness of your app, plus various viral channels, could push your app over the tipping point where you start to see exponential growth. So how do you get on the list? There are some key points to look at, and we’ll address them each in turn: 1. Icon, name, and description 2. The Try App link 3. Categories It can help to think of your app as a book.When browsing in the local bookstore, what do you look for? What makes you buy one book instead of another? Icon, Name, and Description The first thing users see is your app’s icon and name. If they like what they see, they’ll read the description.This would be like picking up a book and looking at the cover. You look at the graphics on the front, the title of the book, and then maybe read the one or two sentences that are always there—you know, like “From the bestselling author of …” or “ ‘Mesmerizing!’ —Joe Fake, New York Times Book Review.” The same idea applies here; you need to catch the potential user’s attention. All of the apps with the highest installs have colorful, polished icons with interesting graphics. It may be easy to overlook, but a little Photoshop wizardry can go a long way. If the icon looks cheap, well, the app probably has similar qualities. Also, by having a name, description, and icon that give the user a good idea of what the app actually does, you can better find your target audience. If users like what they see, they can do one of three things. First, they can click Add App to install it on the spot, and your work is done, plus one install. Second, they can click More Info, which takes them to the app’s Profile page. (More on that a little later.) Third, they can click Try App. Try App To further our book analogy, clicking Try App is like reading the inner flap of the dust jacket. It gives the reader a quick and easy sample of what the book is like. When users click Try App, they are taken to the Canvas page of your app. In this context, very few, if any, permissions are granted to an app when a user has yet to install it. That means that the data to which the app has access is limited. You won’t be able to fetch friend lists, but under some circumstances you may be able to get the Viewer’s basic information (see Chapter 2, Getting Basic MySpace Data, for details on permissions). For apps that rely heavily on social features, this can be severely limiting. For this reason a lot of developers simply decide to blank out the Canvas and show an “Add App!” graphic with a big arrow that points to the Add App button.
307
308
Chapter 14 Marketing and Monetizing
Well, that’s not the greatest user experience because you can’t exactly try the app in that scenario. But if your app is crippled without a friend list, it may be your only recourse. However, if you can enable some functionality, but still strongly persuade the (potential) user to install, it may be the best of both worlds. Categories To stretch our book analogy even further, app categories are similar to book genres. A book might be one of a small number in a section at a bookstore. Let’s say, for example, an OpenSocial book is in the Computer section. It’s not the most popular topic in the world, so the masses won’t be clamoring for it, but the few who do seek it out won’t have endless choices. Conversely, a book in a very popular genre, like a mystery, has a very large target audience, but there are shelves upon shelves of choices. In MySpace app land, the Games and Fun Stuff categories are the most popular; the others lag behind. So, depending on your app and how you categorize it, you could be a big fish in a little pond or a little fish in a big pond. One other thing to keep in mind is that you can apply up to two categories to an app. Adding that second category, even if it is a bit of a stretch, is yet another way for a user to find your app, so it really can’t hurt.
App Profile, or Bringing Out the Bling The app Profile is like the back cover of a book; it should have a further description of the app along with some graphics or screen shots to show more about the app. A user who navigates to the app Profile from the gallery is obviously interested in the app but is probably looking for more information. You need to convince that user to take the plunge by selling your app. We discussed how to bling out our app Profile in Chapter 12, App Life Cycle, so check out that chapter for the technical details.
MySpace’s Own MyAds MySpace offers an ad platform called MyAds that can be found at http://advertise. myspace.com. MyAds allows you to create a banner ad or upload an existing ad. You can then pinpoint your target audience by specifying criteria such as gender, education, and interests. You then bid on the per-click cost of the ad, how much per day to spend (with a $5 minimum), and set the end date for the ad campaign. See Figure 14.2 for a screen shot of MyAds in action. The nice part about MyAds is that you know the target audience is already using MySpace, so half the battle is already won.This can be a pretty good way to drive traffic to your Canvas or app Profile pages to help maintain or increase growth. The app advertisement at the top of Figure 14.3 illustrates how one app is taking advantage of the leaderboard banner space to advertise.
User Base and Viral Spreading
Figure 14.2 Setting up a MyAds campaign.
Figure 14.3 One of the more popular apps making use of the leaderboard-style ad on a MySpace Home page.
User Base and Viral Spreading Ultimately, the spread of apps will live and die with the users themselves. Unless you’re sitting on a pile of money for advertisements, your app will have to grow organically. That means a user will have to install your app, use it, and either enjoy it enough to want to share it, or get some other benefit from installing it. Making an app great
309
Chapter 14 Marketing and Monetizing
enough that people want to share it is easier said than done, but that’s your job, dear reader—you provide the great idea and we’ll show you how to execute it. On the other hand, you may want your users to gain something from installing your app. Now, this is a fine line to walk, as the MySpace Terms of Service forbid incentivized app installs. For example, you couldn’t say, “Invite a friend to receive 100 free points.” That incentive would break the terms you agree to when creating your app. Many of the popular games on the platform have found interesting ways around this. The most common way is to add the number of friends you’ve invited into the game rules. In many games you perform various missions or quests or whatnot and the early missions are available to everyone. As you progress through the game, the harder and more valuable missions are available only if you’ve invited a certain number of users.This is rationalized in the game’s world by the idea that you need to build up a team to complete the harder missions, and the team consists of the friends you’ve successfully invited to the game. This technique has proved very successful for many of the top apps, and the viral nature can lead to exponential growth patterns. Once you hit a critical mass of users, the growth seems to take care of itself. Figure 14.4 shows the installation rate of the Own Your Friends app, the first MySpace app to hit the one-million-install mark. 160000
140000
120000 Number of Installs
310
100000
80000
60000
40000
20000
0 Mar. 22, 2008
Apr. 19, 2008
May 20, 2008
Timeline for the Own Your Friends app
Figure 14.4 The early growth pattern for Own Your Friends, the first big MySpace app.
Ads
There seems to be a tipping point at around several thousand users; once that mark is hit, the viral nature of the app increases the installs exponentially. On April 19, 2008, Own Your Friends had just 4920 installs; just 11 days later, on April 30, 2008, it had 65,594. Fifteen days after that it nearly doubled to 127,768.
Listen to Your Customers “The customer is always right” isn’t just for the mall. An easy way to retain users is to listen to them. Users send messages to the developers of an app, they post messages to the app’s forum (found on the app’s Profile), they report bugs, and they try to hack you. It’s important that you listen to your users and try to respond to them. It’s even more important that you fix bugs and especially any security vulnerabilities.This is doubly true if your app is a game; the integrity of the game is very important, and if it’s compromised, your users won’t find too much enjoyment in it. In return, you’ll find that you grow a base of “power users.” These users will spend hours using your app, finding and reporting bugs, and even letting you know about security exploits.They will also spread the game far and wide, inviting hundreds of friends. So, in return, you get a marketing department for free!
Ads You’ve now got a stylish icon and an interesting description for your app in the gallery.Your app Profile is fully blinged out with custom style and graphics. Maybe you’re even driving traffic to your app through the use of MyAds, and your users are organically growing the app as well. Now what? Let’s make some money! The most obvious way for you to cash in is to add advertisements to your app’s Canvas page. Let’s take a look at the various ad services available out there, as there is a huge number of choices. Note According to the MySpace Terms of Use, ads are allowed only on the Canvas view; they can’t be placed on the Home or Profile views.
Google AdSense Google AdSense (https://www.google.com/adsense) is the granddaddy of modern advertising on the Web. The AdSense engine attempts to contextualize the contents of the page and serve ads from its roster that are relevant to that content.This can be especially useful if your app involves a particular theme that can translate into real-life products. For example, if your app involves golf, ads for actual golfing products are a logical choice. You’ll need a Google account as well as a Web site URL in addition to all the other usual biographical information. Once you’ve signed up, your application will be submitted for approval, and apparently someone or something will “review” it.The site states
311
312
Chapter 14 Marketing and Monetizing
that this process will take one or two days, and the application page says “within a week.”We received our confirmation in a few days. Make sure the Web site URL you specify is an actual Web site with some content on it; the reviewers will reject sites that don’t exist or are under construction. Once your account is set up, it’s time to create some ads. AdSense offers various types of ads geared toward various types of Web sites; we chose to use plain old AdSense for Content. These ads try to guess the content of your page and ideally display something relevant. There are a few more customizations you can make to the ads that will be displayed, such as size, shape, color, and what to display if there are no relevant ads available. Once all the customizations are complete, and the ad looks and behaves just so, AdSense provides you with the necessary JavaScript to place inside your app. Here’s the code we were provided; yours will be slightly different depending on the options you chose (all personal information and identifiers have, of course, been changed): <script type="text/javascript"> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
We pasted this script into the top of our Tic-Tac-Toe app and got the result shown in Figure 14.5.
Figure 14.5 AdSense ads inside Tic-Tac-Toe.
Ads
We’re not too sure how relevant the ads are in this case (lemon laws?), but they’re there, ready to make money nonetheless.
Cubics Cubics (http://cubics.com) is a little different from AdSense in that it’s geared toward generating revenue with apps on social networks.This is nice, because, well, that’s exactly what we’re trying to do. Cubics started out serving ads for Facebook apps but has since branched out to other social networks, including MySpace, as those networks created app platforms. Signing up for Cubics is a little easier than for AdSense in that your application doesn’t need to be “reviewed,” so you don’t have to wait around for a couple of days to start placing ads in your app. Once you’ve provided your personal biography, you’ll need to select a social network.The rest of the flow is essentially this: 1. Add an application to administer by providing a few details of the app. 2. Select the type of ad, such as a 468⫻60 banner ad or a 120⫻600 skyscraper ad. 3. Pick some style elements of the ad, such as background color. 4. Copy and paste the provided code into your app’s code. For example, here’s the code we were provided, which we stuck at the top of our app: <script type="text/javascript"> var pid = 338280; var appId = 70194; var plid = 17263; var adSize = "468x60"; var linkColor = "%230033ff"; var textColor = "%23000000"; var bgColor = "%23ffffff"; var channel = ""; var frameSize = "468x60"; <script language="javascript" type="text/javascript" src="http://social.bidsystem.com/displayAd.js">
Figure 14.6 shows a sample ad being rendered into the Tic-Tac-Toe app and placed above the “OpenSocial Tic-Tac-Toe” title.
313
314
Chapter 14 Marketing and Monetizing
Figure 14.6 A sample ad rendered in the Tic-Tac-Toe app.
Warning Several times with Cubics we got a “Server too busy” error displayed where the ad should have been, which is a little disconcerting! It may have been an intermittent issue, and we couldn’t re-create it the next day, but it’s something to keep in mind when choosing an ad network. You can’t make money with ads you don’t show, so always test a network before you jump in. Also from time to time the ads were slow to load, say, on the order of 5 to 15 seconds. Because of this, the onload event of our app was delayed by the same amount of time, so everything essentially was put on hold until the ad rendered. This can lead to a pretty bad user experience; a user may even think the app is broken and never come back. But again, the next day we tried again and everything seemed fine.
RockYou! Ads RockYou! (https://www.rockyouads.com) is actually primarily an app company. It started way back when by providing Flash widgets for MySpace Profiles and branched out to social apps when the networks started opening up their APIs. Over time they developed an ad network geared toward social networking apps. Once you provide your details and verify your e-mail address, you can start placing ads in your apps.You’ll need to fill out some details of your app in addition to the size and position of the ad. For some reason it wants the URL of the app’s Profile page, even though you’re not allowed to show ads there, so it’s a little strange.To determine this
Ads
URL, click the More Info link on your app’s Canvas page; this takes you to your app’s Profile page, but the URL will be something like www.myspace.com/446431217.That last number, 446431217 in this case, is the user ID of your app’s Profile.Take that number and paste it into the field for Application URL on the RockYou! Ads page. It’ll look something like this: http://profile.myspace.com/index.cfm?fuseaction=user. viewprofile&friendid⫽446431217. Once that’s done, you’ll be taken to your list of apps. Click Embed Code to see the JavaScript and HTML markup you’ll need to paste into your app. Here’s an example of what ours looked like: <script type='text/javascript'> <script type='text/javascript' src='http://cdn.rockyou.com/apps/ams/tag_os.js'>
Pasting that into the top of our app gave us what you see in Figure 14.7. The RockYou! ads seem to be a very different style from both Google AdSense and Cubics ads.The previous ads were text-based, but RockYou! ads, for the most part, seem to be graphics-based.
Figure 14.7 RockYou! ad in the Tic-Tac-Toe app.
315
316
Chapter 14 Marketing and Monetizing
Warning We ran into a couple of problems when using RockYou! Ads. From time to time we would get no ads rendered to the page at all; there would just be a cryptic error message displayed. We got in contact with the engineers at RockYou! and they were quick to fix the problem. However, it would have resulted in a loss of revenue for a time.
Micropayments Micropayments are a very different way for an app to generate revenue compared to showing ads.The way it works is that you provide some kind of premium content for your users at the cost of a dollar or two. A few dollars doesn’t seem like a lot to the bottom line, but it’s a low enough price that a lot of users will be willing to pay it.Those few dollars can add up quickly to a nice flow of money. For a real-life example, let’s take a look at Mobsters, the app that currently sits atop all MySpace apps in terms of number of installs: http://profile.myspace.com/Modules/ Applications/Pages/Canvas.aspx?appId=104283. You’ll notice that there isn’t a single ad anywhere in the app; it generates revenue solely from micropayments. By paying real money, users of the app can gain certain advantages in the game, whether it’s beefing up certain stats or acquiring rare items. Let’s take a look at how to take advantage of this potential revenue stream.
PayPal PayPal (https://www.paypal.com/IntegrationCenter/ic_micropayments.html) is the leader in online payments, and it now offers micropayment support. As of this writing, the rate for micropayments, which means charges under $12, is 5% + $0.05 per transaction.To get started you’ll need to follow the link we’ve given and sign up for a Business Account. This signup process is a drawn out-affair; you’ll need to supply your own personal details, details of your company, such as a customer service line, and then finally bank account details (which will take several days to process). As always, be careful when providing bank or credit card details to anyone, especially over the Internet. If you’re not comfortable doing this, definitely don’t do it; we’ve all seen news reports of credit card numbers being stolen from online vendors. Assuming you’re willing to provide a bank account to PayPal, once it’s set up, click on the Merchant Services tab to add a payment button to your app. Fill out the details of your payment button, such as text, button type, price, and tax rate. Once you’re happy with everything, click Create Button.That takes you to a page that supplies the markup that you’ll need to copy and paste into your app. Here’s the markup that was generated to insert into our Tic-Tac-Toe app:
Boku Boku (www.boku.com) is a micropayment platform that uses cell phones to make payments. You may know it as Mobillcash, which was the company that Boku recently bought out.To make payments, users select their cell phone carrier, enter their phone number, and are sent a text message.The users then confirm purchases by responding to the text message. The signup process is fairly straightforward, but you need to submit a request to become an ad publisher.That submission is then sent for a manual review (which can take a few days). Once you’ve successfully opened an account, you can fine-tune your settings before obtaining your code.We don’t include the markup sample here because at the time of this writing it hadn’t been updated to reflect the switch to Boku. However, like the other ad networks, it’s a relatively straightforward signup process and copy-and-paste job. Signing up for Boku and installing the code gives you the little Pay by Mobile button shown in Figure 14.8.
Figure 14.8 The Boku Pay by Mobile button.
317
318
Chapter 14 Marketing and Monetizing
Others A huge number of companies offer micropayments, and there are more on the way every day. We’ve touched on only a few, but the ones we’ve listed seem to work well and, even more important, work well with MySpace. Some other micropayment platforms of note are n n n n n
It can be in your interest, and the interest of your users, to implement multiple micropayment platforms in your app. Nothing says you need to choose just one, and giving your users a choice is always nice. Also, if they’re familiar with one of the platforms you’ve implemented, they may be more likely to make use of it.You’ll notice that the aforementioned Mobsters app uses most of the platforms described here.
Interviews with Successful App Developers We’re developers, and though we may know a lot about the MySpace platform, we’ve never built a viral app that attracted millions of users. So, when it comes to marketing and monetizing, why should you listen to us? We thought the same thing, which is why we sat down with some successful app developers whom we admire, big and small, to talk to them about what they did that worked and didn’t work.We hope these interviews will provide some insight into how an app hits it “big.”
Dave Westwood: BuddyPoke (www.myspace.com/buddypoke) 1. Who are you? My name is Dave Westwood. My primary skills were initially in 3D renderers, and I created a few Java-based 3D engines at various companies, later moving on to Mobile renderers and now Flash. My partner, Randall Ho, is a 3D character artist/animator and scripter.We’ve worked together at five different companies over the past 12 years or so and decided a couple of years ago to strike it out on our own after seeing the advent of social networks and, in particular, social network widgets. 2. What OpenSocial apps have you created or helped to create? BuddyPoke, as the name of our company suggests, is our only OpenSocial application. Because of the complexity of our particular application as well as the room for growth in our design, we decided to focus on a single, rich
Interviews with Successful App Developers
application rather than a portfolio of simpler applications. Considering the sheer volume of applications that are available to users, it seemed like the best approach to stand out from the crowd. 3. Is there anything you wish you had known before you started developing your first app? We always wish to learn more about the users of each social networking site. Each site is unique in its own way, and having a better understanding of the demographics and interests of the users can have a huge influence on the direction and content that goes into an app. 4. How do you find the MySpace platform, in terms of both technology and growth opportunities? I find myself amazed at the success we’ve been able to achieve thus far considering the size of our company (tiny) and the amount of capital we’ve put in (none). It’s truly a testament to the openness of the MySpace development platform and the OpenSocial community that’s given us access to a market that really sprang from nothing and now gives us access to millions of users around the world. 5. And how do you find the MySpace platform in comparison to other platforms like Facebook? The primary reason that we find MySpace (and OpenSocial) to be a “better” platform is that our particular application is better suited to it. BuddyPoke’s primary strength relies on customizing a 3D avatar and seeing it animated. Having a Flash application viewable (and animated) from the Profile page is key to the attractiveness and virality of BuddyPoke. That’s not to say that one platform is better than the other—simply that MySpace is a perfect match for animated expressive content. 6. What has been your most successful app in terms of number of installs? How many installs does it have? As of April 2009, there are approximately 40 million installs of BuddyPoke worldwide across ten different containers. 7. What did you try that did and didn’t work to promote your app and increase installs? Honestly, we haven’t done a whole lot to promote the application aside from trying to make the best application that we could. Quite literally, we spent about a year on the technology and assets behind BuddyPoke, the minority of which was really OpenSocial per se. The virality of BuddyPoke is based on two things: Firstly, people like to play “dress-up.”They like to express themselves, and BuddyPoke seems to fit the bill insofar as the customization aspects as well as the universality of “cute” characters such as Peanuts, Mario, Mickey Mouse, etc. Secondly, people like to communicate
319
320
Chapter 14 Marketing and Monetizing
8.
9.
10.
11.
with each other, so they are more than happy to spread the word with their friends to find rich ways of interacting with each other. I think a lot of app developers do one (or both) of two things: Either they have “spammy” incentivized features to achieve large install numbers, or they heavily invest in advertising. As far as the spam is concerned, we’ve decided from Day 1 to take the “high road” in this regard and rely on the app’s strength rather than, shall we say, tricking users into installing the application. Our users must be passionate about the application for us to really be a success. Dormant installs, while great on paper, do us nothing. As far as advertising is concerned, we’d gladly have bought advertising, except that we didn’t have the money(!)—so that was an easy decision for us. How did you scale your app once growth took off? Were you prepared, or was it a surprise? We were very fortunate that Google released the preview of App Engine around the same time that we went live on MySpace. Its ease of use and fluid scaling is game changing and allows a two-person start-up like ours to support such a large user base without buying servers, worrying about scaling a database, etc. Have you tried to monetize your app? If so, how? Our primary form of revenue has been through ads, and we’re exploring branded pokes, which is the app equivalent of product placement. This has been very challenging for us, and even though there has been considerable interest from a variety of ad agencies, many of these things have not come to fruition. We just recently released our first branded poke for X-Men Origins: Wolverine. We hope to have many more products like this that really appeal to our demographic. Finally, we’ve just recently started to implement a virtual currency and offer add-on features for printing and “VIP” pokes. Have you taken advantage of the OpenSocial platform and imported your apps to other social networks like Orkut or Hi5? We’re on ten different containers, including Orkut, Hi5, Hyves, Friendster, NetLog, and others, though, of course, MySpace has a special place in our hearts since we’re based in the U.S. Every container is slightly different, but it’s really quite easy to port from one to the next.The main challenge is dealing with the localization, but this is a relatively minor task in the overall development of BuddyPoke. What advice, if any, would you offer a first-time app developer? Go ahead and get your feet wet! Maybe start out with a simpler application and take advantage of the many code examples available to you.Then move on to something more complex. Spend time thinking about your viral loop, iterate your ideas, closely track user actions and feedback, and localize your content.
Interviews with Successful App Developers
Eugene Park: Flixster (www.myspace.com/flixstermovies) 1. Who are you? Eugene Park, Director of Development, Flixster, Inc. 2. What OpenSocial apps have you created or helped to create? Flixster Movies, currently on Myspace, Orkut,Yahoo!, and soon iGoogle. 3. Is there anything you wish you had known before you started developing your first app? OpenSocial containers run our apps inside of iframes under a different domain than the host, so tools like Firebug are disabled by default for these unrecognized domains.This can make debugging JavaScript a bit of a pain.The work-around is simple: Load the iframe’s URL into a new browser window and enable this domain on Firebug.This should help you get the most out of Firebug. 4. How do you find the MySpace platform, in terms of both technology and growth opportunities? MySpace platform (and OpenSocial in general) requires only an understanding of HTML and JavaScript, so it’s easy to learn and get started.The platform also provides a clean REST API, one of the best implementations of REST on the Web. This gives us a pretty complete set of tools for building deep applications for our movie fans. MySpace also provides access to one of the world’s largest and most active social networks, so the growth opportunity is huge. 5. And how do you find the MySpace platform in comparison to other platforms like Facebook? MySpace and Facebook both provide complete platforms for application development, and each has its unique strengths.The thing that I love the most about the MySpace platform is that it gives us full access to our favorite JavaScript libraries (such as Prototype and Scriptaculous), so it’s a lot easier to develop rich, engaging user interfaces. 6. What has been your most successful app in terms of number of installs? How many installs does it have? Our OpenSocial app on MySpace (Movies) now has over 5 million installs—and growing! 7. What did you try that did and didn’t work to promote your app and increase installs? According to our product manager, Josh Gould, the most effective thing has been to create fun features that people want to share with their friends and provide easy ways to let them do so. For example, after taking a movie quiz (one of our most popular features), we allow people to easily share the quiz with their friends by sending a bulletin or comment message.This has helped introduce many new people to the app without much effort.
321
322
Chapter 14 Marketing and Monetizing
8. Do you think your app has been successful? Why or why not? Flixster Movies has been a huge success, thanks in large part to a vibrant social community and a platform that supports (rather than constrains) active interaction and sharing. 9. How did you scale your app once growth took off? Were you prepared, or was it a surprise? We have an amazing team of engineers, and they’ve built a solid infrastructure that supports all of our different product platforms—so we were well prepared for growth. One thing worth noting on the MySpace platform is that the Home surface gets exponentially more traffic than the other surfaces, so we spend a lot of time optimizing our content for caching there. Also, as an application dedicated to movies, we serve a lot of rich media (images and Flash), so we use content delivery networks to help us scale our bandwidth. 10. Have you tried to monetize your app? If so, how? Like all of our other products, we monetize primarily through advertisements. 11. Have you ever spent money advertising your app? If so, where and was it worth it? When we launched our application, we invested some advertising dollars on featured app placement in the MySpace apps directory—this helped us jump-start our first installations.We also exchanged ads with other popular OpenSocial applications; this is a pretty cost-effective way of reaching new users. 12. Have you taken advantage of the OpenSocial platform and imported your apps to other social networks like Orkut or Hi5? We’ve successfully imported our app into Orkut, and it was pretty easy. Most containers are pretty committed to OpenSocial standards, so the cost of porting an app from one OpenSocial container to the next has been pretty low. 13. What advice, if any, would you offer a first-time app developer? Take the time to learn JavaScript properly, and take advantage of any of the great JavaScript frameworks out there (Prototype, Dojo, etc.).
Tom Kincaid: TK’s Apps (www.myspace.com/tomsapps) 1. Who are you? My name is Tom Kincaid and I create TK’s Apps; on other sites they’re called Tom’s Apps, but this turned out to be an issue on MySpace. [Authors’ note: Apps are not allowed to use the name Tom or Tom references in their titles or descriptions because of Tom Anderson, the founder of MySpace and every member’s first friend.] I work in interactive media, but most of my apps are my own side projects.
Interviews with Successful App Developers
2. What OpenSocial apps have you created or helped to create? My apps include Ultimate Fan for Football, Baseball, Basketball, and Hockey teams, Astrology Badge and Match, Profile Poster Gifts, and TV Show Badges and Chat. 3. Is there anything you wish you had known before you started developing your first app? I regret making an app for every sports team. It’s too difficult to maintain so many. 4. How do you find the MySpace platform, in terms of both technology and growth opportunities? For whatever reason, MySpace users are more prone to click on ads, so per user it’s more profitable. On the technology side, it’s very controlled (for reasons I understand), so to update an app, it takes more work than other platforms and there are continual little bug fixes and improvements that need to be done. 5. And how do you find the MySpace platform in comparison to other platforms like Facebook? One thing that benefited the MySpace platform launching after Facebook was they understood that developers would abuse it and they have better technical restrictions in place to combat this. For example, every communication sent by a user must be actively confirmed, whereas on Facebook, notifications can be sent automatically and they try to address abuse through policy, which their staff cannot possibly keep up with across tens of thousands of apps. 6. What has been your most successful app in terms of number of installs? How many installs does it have? I’m not that big a developer. Posters and Astrology have over 15K each. 7. What did you try that did and didn’t work to promote your app and increase installs? I just try to cross-promote. I don’t use ads or anything. 8. Do you think your app(s) has been successful? Why or why not? For something one guy did alone, not bad, but it’s not enough to make a full-time living doing. 9. How did you scale your app once growth took off? Were you prepared, or was it a surprise? Didn’t have this problem yet. 10. Have you tried to monetize your app? If so, how? Mostly just banner ads. Get about $0.50 CPM. 11. Have you ever spent money advertising your app? If so, where and was it worth it? Have not done any advertising, just cross-app promotions.
323
324
Chapter 14 Marketing and Monetizing
12. Have you taken advantage of the OpenSocial platform and imported your apps to other social networks like Orkut or Hi5? I did at the start, but it became overwhelming to support multiple apps across multiple sites that are not 100% compatible both in technology and policies, so now I just focus on MySpace and Facebook. 13. What advice, if any, would you offer a first-time app developer? Don’t expect your “big idea” will make you rich overnight.
Dan Yue: Playdom (www.myspace.com/playdom) 1. Who are you? My name is Dan Yue and I’m the cofounder and CEO of Playdom, the largest game developer on MySpace. Prior to founding Playdom, I was employee number 1 at Adify, an ad network platform acquired by Cox Enterprises in 2008; founded several wildly unsuccessful technology start-ups; and served as technical consultant to Wynn Design and Development … As a lifelong gamer, I’ve played thousands of hours of Final Fantasy 7 and Baldur’s Gate 2 and spent 34 long days as a semiprofessional Blackjack player. My commitment to the player experience runs so deep that I wake up several times a night to check the performance of Playdom’s games. 2. What OpenSocial apps have you created or helped to create? Playdom has developed and successfully launched 12 apps on MySpace, including seven RPGs. Playdom has also launched Poker Palace and Bumper Stickers on Hi5. 3. Is there anything you wish you had known before you started developing your first app? I wish we realized that games are much more successful on social platforms than other types of social apps.We first launched Kiss Me on MySpace, which was (and is) a success, but it may have been wiser to launch a game first, then follow with a social app. In the perfect scenario, you’d attract a high volume of installs with a highly engaging game; then you can use the first game’s popularity to cross-promote new apps. 4. How do you find the MySpace platform, in terms of both technology and growth opportunities? MySpace is a great platform that provides a lot of possibilities for developers. The average length of visit is longer on MySpace, compared to other social networks, and the emphasis is on self-expression and fun. Social games— and Playdom’s games in particular—work really well in MySpace’s environment.
Interviews with Successful App Developers
5. And how do you find the MySpace platform in comparison to other platforms like Facebook? MySpace is very developer-focused, and we value our relationship with them. As one of many examples, their platform team recently conducted a survey to pinpoint developer pain points and solicit recommendations. (That said, we have a positive relationship with Facebook and find their team and policies by and large developer-friendly.) 6. What has been your most successful app in terms of number of installs? How many installs does it have? Mobsters is the number-1 app on MySpace with over 13.6 million installs. Playdom also has three of the top four apps on MySpace, including Own Your Friends, Kiss Me, and Bumper Stickers. 7. What did you try that did and didn’t work to promote your app and increase installs? We have generally enjoyed great success promoting our apps. Our two biggest strengths: cross-promotions (marketing a new game via a popular established game) and deeply integrating viral channels into our games (the best virals are a natural part of the game play). 8. Do you think your app(s) has been successful? Why or why not? A number of our apps have been successful. The most obvious indicator of success is in the number of installs: Mobsters is the obvious success, our largest MySpace app with over 13.6 installs; and Bumper Stickers is in second place with over 11 million installs to date. However, we really focus on the level of player engagement, and we’re very pleased with our DAU [Daily Active Users] and length-of-session metrics. 9. How did you scale your app once growth took off? Were you prepared, or was it a surprise? Playdom’s first app to hit it big was Own Your Friends, which rocketed to 5.7 million installs in the third month, only to grow to more than 7.5 [million] installs a month later.To be honest, we weren’t fully prepared:We didn’t have the server infrastructure to support the app’s massive growth. As a stopgap solution, the founding team got up several times a night to make sure the game was online. And we quickly focused on scaling the operation. Thanks to an angel investor, this was possible early in our history. 10. Have you tried to monetize your app? If so, how? Playdom has monetized various apps—and all our RPGs. Roughly 60% of our revenue comes from direct payments for virtual goods, including in-game advancement and limited edition items. Five percent of the people playing our games purchase virtual goods and the percentage goes up as players spend more time on our game.The other half of our revenue is generated via incentivized offers.
325
326
Chapter 14 Marketing and Monetizing
11. Have you ever spent money advertising your app? If so, where and was it worth it? We have purchased both sponsorships and cost-per-click banner ads on MySpace. Both have been worth the energy and money because of the return-per-user metrics they provide.With each user that clicks on the ad, that user brings in additional players through the app’s viral channels.These advertisements help seed virality—a critical driver of growth. 12. Have you taken advantage of the OpenSocial platform and imported your apps to other social networks like Orkut or Hi5? Poker Palace and Bumper Stickers have both been successfully launched on Hi5. 13. What advice, if any, would you offer a first-time app developer? New developers need to understand that in today’s market, it’s difficult to go massively viral by relying on old-school “spammy” methods.Viral channels need to be authentic to the game, and the game play needs to be fundamentally engaging. We’ve created a formula that speaks to a joint need to drive growth and reduce churn: G ⫺ cD ⬎ 0 or [Growth ⫺ % of daily churn ⫻ number of daily active users must be greater than 0] We’ve fueled growth by identifying the right in-game channels to deploy the virals.We’ve reduced churn by introducing in-game comments, compelling storylines, and mini-games.
Summary In this chapter we looked at ways to spread your app, and then how to cash in once the installs start coming in. One thing to take away is that there are individuals and companies out there that are making money by making apps. We heard from a few of them in the interviews section and got some tips on how to achieve some success. When it comes to generating income, there is a wide variety of options; this is an expanding market and one that is interesting for advertisers. In terms of getting the biggest bang for your buck with ads, there are a lot of variables to consider. Using one of the bigger and more established companies, like Google, is a superior technical solution. You won’t see a lot of downtime with AdSense, for example. But AdSense isn’t built to target social networking apps. On the other hand, RockYou! Ads and Cubics are designed to specifically target the social networking user. But we encountered technical problems with both ad networks, so there’s a give-and-take you’ll need to consider. This is one of the most important decisions when it comes to developing and growing your app; ultimately you can’t make successful apps if you can’t pay for the
Summary
bandwidth. So it may take time for you to try different solutions, or a combination of solutions. Maybe your app is suited for micropayments and you needn’t bug your users with ads. Maybe your golfing app pulls up some great targeted ads via Google AdSense and you get lots of click-throughs. As we’ve seen, it’s easier than ever before to simply drop an ad into your app and start making money; the sooner you start thinking in terms of profit margins, the better.
327
This page intentionally left blank
15 Porting Your App to OpenSocial 0.9 W e have some good news and some bad news. Let’s go with the bad news first.The bad news is that the OpenSocial spec is currently in constant flux while the spec group seeks the Holy Grail of version 1.0.You may think that this isn’t so bad. After all, you logically go from version 0.8 to 0.9, then finally 1.0, right? Well, there is currently some talk of going from 0.8 to 0.9 to 0.10.That’s right: zero dot eight, zero dot nine, zero dot ten. We hope that won’t be the case and that the 0.9 spec will be stable enough that it can be called version 1.0 without too many changes. If that’s what happens, the spec should stop churning for a while, excluding a few bug fixes here and there. The other piece of bad news is that MySpace will probably continue to support the latest and greatest versions of the OpenSocial spec as they are released.That means version 0.9 will have the full support of MySpace. Sounds great, right? Well, if your app is currently written in version 0.7, that means you’ll now be two spec versions behind.The more versions that are released and supported by MySpace, the more likely it is that the older versions will receive less and less support.We’re already seeing this phenomenon to some extent. Bug fixes and feature releases for 0.7 are fewer and farther between compared to those for 0.8. Fortunately, the good news more than makes up for the bad. For the foreseeable future, MySpace will continue to support older versions of the OpenSocial spec. It’s a lot of effort to deprecate older versions of the spec, and too many apps are running on old versions. It will happen eventually, but it will probably be a long and drawn-out process. A little-known fact is that the 0.7 container actually also supports OpenSocial 0.6, an early and buggy release of the spec. By the time you read this page, dear readers, version 0.6 will be about two years old (or more)—an eternity in Internet time. If version 0.6 can last that long, and probably longer, then version 0.8 will be around for a long time as well. Even better news is the fact that when it comes down to it, not a whole lot is changing from version 0.8 to version 0.9.The biggest change will be the inclusion of OSML to the spec. Since we covered OSML extensively in Chapters 10 and 11, we won’t
330
Chapter 15 Porting Your App to OpenSocial 0.9
rehash that topic here.The other big change is what is called “OS Lite,” or the “Lightweight JS APIs.” OS Lite is, for the most part, a rewrite of the OpenSocial JavaScript APIs and has two main goals: to unify the JavaScript and REST APIs, and to use JSON for both inputs and outputs.These are both worthy goals, but the truth of the matter is that the original APIs will work just fine in version 0.9. Moving forward, OS Lite will probably become the standard, but it’s probably best to wait for version 1.0, when it becomes a bit more baked-in. Other than that, there are a few bug fixes, a feature or two, and a bit of a cleanup. Now, let’s take a look at the big changes.
Media Item Support A slew of functions to support media items were added in 0.9; this includes fetches, updates, deletes, and (finally!) uploads. However, some of this functionality already existed on the MySpace platform as MySpace-specific extensions in 0.8. In the sections that follow, we’ll take a look at the new APIs and, if applicable, contrast them with the 0.8 APIs.Then we’ll show some sample code so you can see how to use the new features.
opensocial.Album The first thing we’ll look at is the new opensocial.Album object.The album object behaves exactly like the other OpenSocial objects, such as opensocial.Person; it has fields, and those fields are fetched with getField. Let’s take a look at the available fields that albums provide. OpenSocial 0.9* opensocial.Album.Field = { /** * String, unique identifier for the album. * May be used interchangeably with the string 'id'. * @member opensocial.Album.Field */ ID: 'id', /** * String, URL to a thumbnail cover of the album. * May be used interchangeably with the string 'thumbnailUrl'. * @member opensocial.Album.Field */ THUMBNAIL_URL: 'thumbnailUrl',
*Code
courtesy of OpenSocial.org: http://sites.google.com/site/opensocialdraft/Home/opensocialjavascript-api-reference/albums-js.
Media Item Support
/** * String, the title of the album. * May be used interchangeably with the string 'title'. * @member opensocial.Album.Field */ TITLE: 'title', /** * opensocial.Address, location corresponding to the album. * May be used interchangeably with the string 'location'. * @member opensocial.Album.Field */ LOCATION: 'location', /** * String, ID of the owner of the album. * May be used interchangeably with the string 'ownerId'. * @member opensocial.Album.Field */ OWNER_ID: 'ownerId', /** * Array of MediaItem.TYPE, types of MediaItems in the album. * May be used interchangeably with the string 'mediaType'. * @member opensocial.Album.Field */ MEDIA_TYPE: 'mediaType', /** * Array of strings identifying the mime-types of media items in the album. * May be used interchangeably with the string 'mediaMimeType'. * @member opensocial.Album.Field */ MEDIA_MIME_TYPE:'mediaMimeType', /** * Integer, number of items in the album. * May be used interchangeably with the string 'mediaItemCount'. * @member opensocial.Album.Field */ MEDIA_ITEM_COUNT:'mediaItemCount' };
OpenSocial 0.8 on MySpace MyOpenSpace.Album.Field = { /** * A number representing an album's unique identifier.
331
332
Chapter 15 Porting Your App to OpenSocial 0.9
* @memberOf MyOpenSpace.Album.Field */ ALBUM_ID:"ALBUM_ID", /** * The RESTFUL URI with which to access the album on the API. * @memberOf MyOpenSpace.Album.Field */ ALBUM_URI:"ALBUM_URI", /** * The album's title. * @memberOf MyOpenSpace.Album.Field */ TITLE:"TITLE", /** * The geographic location where the album's pictures were taken. * @memberOf MyOpenSpace.Album.Field */ LOCATION:"LOCATION", /** * A URL for the album's default image. * @memberOf MyOpenSpace.Album.Field */ DEFAULT_IMAGE:"DEFAULT_IMAGE", /** * A string representing the album's privacy setting, * such as "Public" or "Private" * @memberOf MyOpenSpace.Album.Field */ PRIVACY:"PRIVACY", /** * An integer representing the total number of photos in the album * (not the number of photos actually contained * within the current object). * @memberOf MyOpenSpace.Album.Field */ PHOTO_COUNT:"PHOTO_COUNT", /** * A RESTFUL URI with which to access the photos contained in the album.
The entities are basically the same—all the important stuff is there in both cases, such as IDs, album cover URL, and the number of media items in the album.
Fetching Albums Here’s the functionality for fetching an album. First, let’s look at OpenSocial 0.9. OpenSocial 0.9† /** * The newFetchAlbumsRequest() creates an object for * DataRequest to request albums. * * @param {opensocial.IdSpec} An IdSpec used to specify which * people/groups to fetch albums from. * * @param {Map.<string, string>} opt_params * opt_params can specify the following: * opensocial.Album.Field.ID - an array of album IDs to fetch * (fetch all albums if empty, subject to pagination) * opensocial.Album.Field.MEDIA_TYPE - an array of MediaItem.TYPE * values to specify the kind of albums to fetch. * opensocial.DataRequest.AlbumRequestFields.FIRST * The first item to fetch. * opensocial.DataRequest.AlbumRequestFields.MAX * The maximum number of items to fetch. * @return {Object} A request object */ opensocial.DataRequest.prototype.newFetchAlbumsRequest = function(idSpec, opt_params) {};
And now, the MySpace 0.8 extension. OpenSocial 0.8 on MySpace /** * Creates an object to be used when sending to the server * @param {String} id The ID (VIEWER or OWNER) * of the person who owns the albums
†Code
courtesy of OpenSocial.org: http://sites.google.com/site/opensocialdraft/Home/opensocialjavascript-api-reference/datarequest.
333
334
Chapter 15 Porting Your App to OpenSocial 0.9
* @param {Map} * opt_params Optional parameters specified when creating the albums. * @return {Object} A request object * @static * @memberOf MyOpenSpace.DataRequest */ MyOpenSpace.DataRequest.newFetchAlbumsRequest = function(id, opt_params) {};
There are no real functional differences between versions.The new version uses an IdSpec object instead of a plain old string ID. It can specify a particular album, whereas 0.8
had a separate endpoint for that, and the paging parameters are in a different namespace. Here’s an example function that takes in optional paging parameters, album ID, and callback function and makes a request for the Viewer’s photo albums: // Fetches the Viewer's photo albums function fetchViewerPhotoAlbums(first, max, album_id, callback){ // Create the IdSpec object var params = {}; params[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER; params[opensocial.IdSpec.Field.NETWORK_DISTANCE] = 0; var idspec = opensocial.newIdSpec(params); // Create the DataRequest object var request = opensocial.newDataRequest(); params = {}; // Set the paging parameters if(first){ params[opensocial.DataRequest.AlbumRequestFields.FIRST] = first; } if(max){ params[opensocial.DataRequest.AlbumRequestFields.MAX] = max; } // Pick an album; album_id is an array of album IDs if(album_id){ params[opensocial.Album.Field.ID] = album_id; } // Force it to images params[opensocial.Album.Field.MEDIA_TYPE] = opensocial.MediaItem.Type.IMAGE;
Media Item Support
// Add the request to the queue request.add(request.newFetchAlbumsRequest(idspec, params), "albums"); // Send it off request.send(callback); }
In the function we specified opensocial.MediaItem.Type.IMAGE.The other possibility is opensocial.MediaItem.Type.VIDEO.
Fetching Media Items Let’s look at the OpenSocial 0.9 method for fetching media items. OpenSocial 0.9‡ /** * The newFetchAlbumsRequest() creates an object for * DataRequest to request albums. * * @param {opensocial.IdSpec} An IdSpec used to specify which * people/groups to fetch media items from. * * @param {string} albumId * The ID of the album to fetch MediaItems from. * * @param {Map.<string, string>} opt_params * opt_params can specify the following: * opensocial.MediaItem.Field.ID - an array of media item IDs to * selectively fetch (fetch all items if empty, subject to pagination) * opensocial.MediaItem.Field.MEDIA_TYPE - an array of MediaItem.TYPE * values to specify the types of MediaItems to fetch * opensocial.DataRequest.MediaItemRequestFields.FIRST * The first item to fetch. * opensocial.DataRequest.MediaItemRequestFields.MAX * The maximum number of items to fetch. * * @return {Object} A request object */ opensocial.DataRequest.prototype.newFetchMediaItemsRequest = function(idSpec, albumId, opt_params){};
And now for version 0.8. ‡Code
courtesy of OpenSocial.org: http://sites.google.com/site/opensocialdraft/Home/opensocialjavascript-api-reference/datarequest.
335
336
Chapter 15 Porting Your App to OpenSocial 0.9
OpenSocial 0.8 on MySpace /** * Creates an object to be used when sending to the server * @param {String} id The ID (VIEWER or OWNER) of * the person who owns the albums * @param {Map} * opt_params Optional parameters specified when creating the videos. * @return {Object} A request object * @static * @memberOf MyOpenSpace.DataRequest */ MyOpenSpace.DataRequest.newFetchVideosRequest = function(id, opt_params) {}; /** * Creates an object to be used when sending to the server * @param {String} id The ID (VIEWER or OWNER) * of the person who owns the photos * @param {Map} * opt_params Optional parameters specified when creating the photos. * @return {Object} A request object * @static * @memberOf MyOpenSpace.DataRequest */ MyOpenSpace.DataRequest.newFetchPhotosRequest = function(id, opt_params) {};
The big difference here is that the 0.9 API combines two MySpace 0.8 APIs into one, and it again uses an IdSpec object and modifies the paging parameter namespaces. In our Tic-Tac-Toe app, we had a function that fetched the Viewer’s photos; we’ll reproduce it here so that you can see the differences between versions. Version 0.8 Function for Fetching Viewer’s Photos // Fetch the Viewer's photos function fetchPhotosList(first, max, callback){ // Set the paging parameters var params = {}; params[opensocial.DataRequest.PeopleRequestFields.FIRST] = first; params[opensocial.DataRequest.PeopleRequestFields.MAX] = max; // Send the request var request = opensocial.newDataRequest();
Media Item Support
var id = opensocial.IdSpec.PersonId.VIEWER; var req = MyOpenSpace.DataRequest.newFetchPhotosRequest(id, params); request.add(req, TTT.RequestKeys.VIEWER_PHOTOS); request.send(callback); }
Let’s now convert that code into 0.9. Version 0.9 Function for Fetching Viewer’s Photos // Fetches the Viewer's photos function fetchPhotosList(first, max, callback, media_id){ // Create the IdSpec object var params = {}; params[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER; params[opensocial.IdSpec.Field.NETWORK_DISTANCE] = 0; var idspec = opensocial.newIdSpec(params); // Create the DataRequest object var request = opensocial.newDataRequest(); params = {}; // Set the paging parameters if(first){ params[opensocial.DataRequest.MediaItemRequestFields.FIRST] = first; } if(max){ params[opensocial.DataRequest.MediaItemRequestFields.MAX] = max; } // Pick a media item; media_id is an array of media IDs if(media_id){ params[opensocial.MediaItem.Field.ID] = media_id; } // Force it to images params[opensocial.MediaItem.Field.MEDIA_TYPE] = opensocial.MediaItem.Type.IMAGE; // Add the request to the queue request.add(request.newFetchAlbumsRequest(idspec, params), TTT.RequestKeys.VIEWER_PHOTOS);
337
338
Chapter 15 Porting Your App to OpenSocial 0.9
// Send it off request.send(callback); }
Again we specified opensocial.MediaItem.Type.IMAGE, but opensocial. MediaItem.Type.VIDEO is supported as well.
Updating Albums and Media Items These are APIs that allow you to update the metadata of albums and media items, not actually upload new ones.There are no 0.8 substitutes for these endpoints:§ /** * Updates the fields specified in the params. The following * fields cannot be set: MEDIA_ITEM_COUNT, * OWNER_ID, ID. Containers implement restrictions. * * @param {opensocial.IdSpec} An IdSpec used to specify which * people/groups to own the album. * * @param {string} albumId * The album to update. * * @param {Map} fields * The Album Fields to update.The following fields * cannot be set: MEDIA_ITEM_COUNT, * OWNER_ID, ID. Containers implement restrictions. * * @return {Object} A request object */ opensocial.DataRequest.prototype.newUpdateAlbumRequest = function(idSpec, albumId, fields){}; /** * Updates the fields specified in the params. The following * fields cannot be set: * ID, CREATED, ALBUM_ID, FILE_SIZE, NUM_COMMENTS. * Containers implement restrictions. * * @param {opensocial.IdSpec} An IdSpec used to specify which * people/groups own the album/media item. * * @param {string} albumId * The album containing the media item to update. * §Code
courtesy of OpenSocial.org: http://sites.google.com/site/opensocialdraft/Home/opensocialjavascript-api-reference/datarequest.
Media Item Support
* @param {string} mediaItemId * The media item to update. * * @param {Map} fields * The Album Fields to update. The following fields cannot be set: * ID, CREATED, ALBUM_ID, FILE_SIZE, NUM_COMMENTS. * Containers implement restrictions. * * @return {Object} A request object */ opensocial.DataRequest.prototype.newUpdateMediaItemRequest = function(idSpec, albumId, mediaItemId, fields){};
Let’s take a look at a quick example.The following function can be used to update the title, thumbnail URL, and description of a particular media item in a particular album. Since the functionality to update an album is very similar, we’ll leave it as an exercise for the reader to create a function to do so. // Updates a media item for the Viewer function updateMediaItem(album_id, media_id, newTitle, newPic, newDesc, callback){ // Create the IdSpec object var params = {}; params[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER; params[opensocial.IdSpec.Field.NETWORK_DISTANCE] = 0; var idspec = opensocial.newIdSpec(params); // Create the DataRequest object var request = opensocial.newDataRequest(); var fields = {}; // Set the new fields fields[opensocial.MediaItem.Field.TITLE] = newTitle; fields[opensocial.MediaItem.Field.THUMBNAIL_URL] = newPic; fields[opensocial.MediaItem.Field.DESCRIPTION] = newDesc; // Add the request to the queue request.add(request.newUpdateMediaItemRequest(idspec, album_id, media_id, fields), "update_mi"); // Send it off request.send(callback); }
339
340
Chapter 15 Porting Your App to OpenSocial 0.9
Uploading Media Items One of the big new features added to the spec is the ability to upload media. Sending the actual contents of an image isn’t very easy in JavaScript, so because of this the upload functionality works similarly to opensocial.requestSendMessage and opensocial. requestShareApp.You invoke opensocial.requestUploadMediaItem and a pop-up modal appears.The pop-up itself takes care of the actual uploading process. On MySpace either a Flash or a Java-based widget is used, depending on the end user’s system. Initially, only images are available for upload. Once an image has been uploaded and the user closes the pop-up window, a callback function is executed.The one parameter passed to this callback is an opensocial. ResponseItem object.This object contains information on any error that may have occurred, or an array of opensocial.MediaItem objects if there were any successful uploads. An example is in order: // Uploads an image to the specified album function uploadMediaItem(album_id){ opensocial.requestUploadMediaItem(album_id, uploadMediaItemCallback); } // Handles the response function uploadMediaItemCallback(response){ if(!response || response.hadError()){ // Error code var error_code = response.getErrorCode(); // Comma-delimited list of failed files var error_message = response.getErrorMessage(); // Array of failed files var failed_files = error_message.split(","); // Retry request? Update UI? } else{ // Array of MediaItem objects var mi_array = response.getData(); var div = document.getElementById("messages"); var url = opensocial.MediaItem.Field.THUMBNAIL_URL; for(var i = 0; i < mi_array.length; i++){ div.innerHTML += " "; } } }
There are a couple of functions in the code.The first just wraps opensocial. requestUploadMediaItem.The second is specified as the callback function and handles the result of the attempted image upload.The response is checked for an error, and if one is found, we parse the error field for data.The error code is set as normal and should give you a good idea of what went wrong.The error message is a comma-delimited list of the files that had an error. If there was no error, either the user canceled the pop-up window or some files were uploaded. If the upload was successful, the data portion of the response contains an array of opensocial.MediaItem objects, one for each successfully uploaded image. If the array has a length of zero, you can assume the user canceled the action; otherwise you’re free to parse the array as you see fit. In our case, we simply iterate through the list of images and output them to the UI.
Simplification of App Data The functionality of app data has remained constant between versions, but the APIs used to interact with it have been significantly simplified.The original API was deemed to be too complex for what was actually allowed. For example, it was possible to update the app data only for the Viewer, yet the function accepted an ID as a parameter even though its only valid value was VIEWER. To that end, the signatures for updating and deleting app data were modified.The following is what they used to look like. Version 0.8 (Updating and Deleting App Data) opensocial.DataRequest.prototype.newUpdatePersonAppDataRequest = function(id, key, value) {}; opensocial.DataRequest.prototype.newRemovePersonAppDataRequest = function(id, keys) {};
They have now been changed for 0.9. Version 0.9 (Updating and Deleting App Data) opensocial.DataRequest.prototype.newUpdatePersonAppDataRequest = function(key, value) {}; opensocial.DataRequest.prototype.newRemovePersonAppDataRequest = function(keys) {};
341
342
Chapter 15 Porting Your App to OpenSocial 0.9
It’s a very simple change; you’ll literally just have to go through and remove the ID you were passing in. Updating and deleting app data weren’t the only APIs to be simplified; fetching app data also got a once-over.The entire opensocial.DataRequest. newFetchPersonAppDataRequest function was deprecated. Instead, app data was attached to the opensocial.Person object. Let’s take a look at an example: // Fetches the app data for Owner or Viewer function fetchPersonAppData(id, callback){ // Create an empty object to use for passing in the parameters var params = {}; var keys = ["key1", "key2", "key3"]; // Add the list of fields to the parameters params[opensocial.DataRequest.PeopleRequestFields.APP_DATA] = keys; // ID will be either: // opensocial.IdSpec.PersonId.VIEWER // or: // opensocial.IdSpec.PersonId.OWNER var appdata_req = req.newFetchPersonRequest(id, params); // Add the request to the queue and give it a key req.add(appdata_req, "app_data"); // Send it off req.send(callback); }
This function fetches the app data for either the Viewer or the Owner. First an array of strings is created with all the app data keys we’d like to fetch—in this case we’d like to fetch three keys.Those keys are added to the parameter object via the opensocial. DataRequest.PeopleRequestFields.APP_DATA enum.The id variable can be either opensocial.IdSpec.PersonId.OWNER or opensocial.IdSpec.PersonId.VIEWER. The request is then sent as normal. Some small modifications are required to fetch the app data for the Owner’s or Viewer’s friends.The basic idea is to do everything as we did in the previous code sample but use opensocial.DataRequest.newFetchPeopleRequest instead. Note that when fetching friends, you need to provide an opensocial.IdSpec object instead of a string ID; see Chapter 3, Getting Additional MySpace Data, for details on that. To retrieve the app data in the callback, you must use a function that has been added to the opensocial.Person object:** **Code
courtesy of OpenSocial.org: http://sites.google.com/site/opensocialdraft/Home/opensocialjavascript-api-reference/person.
REST APIs
/** * Gets the app data for this person that * key. * * @param {String} key The key to get app * @return {String} The corresponding app */ opensocial.Person.prototype.getAppData =
is associated with the specified
data for. data. function(key) {};
You simply pass in the key of the app data you’re looking for, and the value is returned. Let’s look at an example: // Parse the app data function getAppData(response){ // First check for an error if(!response.hadError()){ // Person is now an opensocial.Person object var person = response.get("app_data").getData(); var val1 = person.getAppData("key1"); var val2 = person.getAppData("key2"); var val3 = person.getAppData("key3"); } else{ // Retry the request? } }
The values of val1, val2, and val3 are the JSON objects that were stored in app data for key1, key2, and key3 respectively or undefined if nothing was found for that key.
REST APIs For those of you using iframe apps and the REST APIs, there’s not too much change for you either.The SDKs will be updated to point to the new APIs, so the change should be invisible to you. Simply get the updated SDKs from the usual place: http://code.google. com/p/myspaceid-sdk/. Replace your existing source files with the new ones. If any of this sounds mysterious to you, check out Chapters 8 and 9 (where we cover OAuth and external iframe apps) for details.That should set you up with hitting the updated APIs. Unfortunately, it’s not quite that easy, since the responses of some of the APIs have been slightly modified.This difference in the response between versions exists because the MySpace 0.8 APIs weren’t quite to spec, but now in 0.9 they are. So it’s not that the spec changed; it’s just that MySpace is now compliant. Let’s take a look at the two most important APIs, person and friends, and take a look at how they’ve changed.
343
344
Chapter 15 Porting Your App to OpenSocial 0.9
This is a sample response from the 0.9 REST API for a friend list: { "isFiltered":"false", "itemsPerPage":2, "startIndex":1, "totalResults":123 "entry": [ {"person": { "displayName":"Tom", "hasApp":"false", "id":"myspace.com.person.6221", "profileUrl":"http:\/\/www.myspace.com\/tom", "thumbnailUrl":"http:\/\/path_to_img.png" } }, {"person": { "displayName":"Chad Russell", "hasApp":"false", "id":"myspace.com.person.123965750", "profileUrl":"http:\/\/www.myspace.com\/chad_at_myspace", "thumbnailUrl":"http:\/\/path_to_img.png" } } ] }
Meanwhile, a sample response from the 0.8 REST API looks like this: { "count": 2, "Friends": [ {"name": "Tom", "largeImage": "http:\/\/path_to_img.jpg", "image": "http:\/\/path_to_img.jpg", "userId": 6221, "uri": "http:\/\/api.myspace.com\/v1\/users\/6221", "webUri": "http:\/\/www.myspace.com\/tom", "userType": "RegularUser"}, {"name": "Chris Cole", "largeImage": "http:\/\/path_to_img.png", "image": "http:\/\/path_to_img.png",
So, there are some small differences there; you’ll have to make some modifications to your mapping code to compensate. For example, in Chapter 9 we pushed the results of the friend list call down onto the client in a JavaScript object called friends.We then used friends like this: TTT.Lists.getCurrentList().list = friends.Friends; TTT.Lists.getCurrentList().total = friends.count;
In 0.9 that code would become TTT.Lists.getCurrentList().list = friends.entry; TTT.Lists.getCurrentList().total = friends.totalResults;
Essentially Friends becomes entry and count becomes totalResults.You can see this difference in the two sample responses shown earlier. Also, some other code accessed the individual fields for each friend: id = friends[i].userId; name = friends[i].name; picture = friends[i].image;
That code would need to be converted to the following for 0.9: id = friends[i].person.id; name = friends[i].person.displayName; picture = friends[i].person.thumbnailUrl;
Notice that a user’s ID changed from just an integer, like 6221, to something a little more complex; it now looks like myspace.com.person.6221.
345
346
Chapter 15 Porting Your App to OpenSocial 0.9
As for the person endpoint, let’s take a look at a sample response from the 0.9 REST API when all the person fields are requested: { "itemsPerPage":1, "startIndex":0, "totalResults":1, "person":{ "aboutMe":"very interesting...", "age":"29", "bodyType":{"build":"Slim \/ Slender"}, "books":["Books"], "children":["Someday"], "currentLocation":{ "country":"US", "formatted":"SEATTLE, Washington, US", "latitude":"47.6344", "locality":"SEATTLE", "longitude":"-122.3422", "postalCode":"98109", "region":"Washington" }, "displayName":"Chad Russell", "drinker":{"displayValue":"Yes","value":"YES"}, "ethnicity":"White \/ Caucasian", "gender":"male", "hasApp":"true", "heroes":["Heroes"], "id":"myspace.com.person.183399670", "interests":["General"], "lookingFor":[ {"displayValue":"Friends","value":"FRIENDS"}, {"displayValue":"Networking","value":"NETWORKING"} ], "movies":["Movies"], "msLargeImage":"http:\/\/path_to_img.jpg", "msMediumImage":"http:\/\/path_to_img.jpg", "msMood":"(none)", "msMoodLastUpdated":"2009-05-20T16:24:19", "msUserType":"RegularUser", "msZodiacSign":"Gemini", "music":["Music"], "name":{"familyName":"Russell","givenName":"Chad"}, "networkPresence":{"displayValue":"Online","value":"ONLINE"}, "organizations":[{ "address":{ "country":"US",
Again, there are quite a few differences. Fortunately most of the same data is present in 0.9, so it’s basically the same data presented in a different way. For the friends endpoint earlier, we showed the client-side code modifications necessary to make use of the new JSON object that will be generated. Here we’ll leave it as an exercise for the reader. Well, okay … one example. In Chapter 9 we requested the Viewer’s data and output all the fields to the UI.To parse the Viewer’s display name from the response from the REST API, we did this: sb.append(viewer.fullprofile.basicprofile.name + " ");
In the updated 0.9 response, the display name would be accessed like so: sb.append(viewer.person.displayName + " ");
Summary There are a few more small tweaks between OpenSocial versions 0.8 and 0.9 coming down the OpenSocial pipeline, but the bulk of the changes that will directly affect porting your apps can be found in this chapter. However, this chapter has a couple of “sister” chapters that deal with the biggest change in 0.9: OSML.To learn more about OSML, see Chapter 10, OSML, Gadgets, and the Data Pipeline, and Chapter 11, Advanced OSML.
Summary
Overall, we think you’ll find that MySpace’s implementation of version 0.9 is cleaner and more spec-compliant; after all, it’s the third iteration, and we all (we hope) learn from our mistakes, even MySpace developers. Deciding when and why to port your app is a question you’ll have to answer for yourself.You’ll have to weigh the benefits and the costs since porting your apps isn’t free. There will be extra development and testing time, not to mention the research time that came from reading this chapter. One of the ways MySpace tries to get app developers to upgrade their apps is to release new features only on the latest and greatest version. So you may want to wait until some killer new feature is released before you port your app. Or you may just want to play around with the newest version of the spec.The choice is yours. Note Code listings and/or code examples for this chapter can be found on our Google Code page under http://opensocialtictactoe.googlecode.com.
349
This page intentionally left blank
References
This appendix is a list of references used in writing this book. References appear in alphabetical order. In addition to the cited references here, refer to the OpenSocial Foundation Web site, www.opensocial.org/, for the latest information, documentation, and announcements. Amazon Web Services LLC. “Amazon Web Services.” Accessed October 2008. http://aws.amazon.com/. Aptana, Inc. “Aptana Cloud Services.” Accessed November 2008. http://aptana.com/cloud. Bishop, Bill, and Tim Murphy. “Broadband Connection Highs and Lows Across Rural America.” Daily Yonder, February 11, 2009. Accessed April 2009. www.dailyyonder.com/broadband-connection-highs-and-lows-across-rural-america/ 2009/02/11/1921. Brain, Marshall. “How the Radio Spectrum Works.” HowStuff Works.com, April 1, 2000. Accessed May 2009. www.howstuffworks.com/radio-spectrum.htm. Caceres, Marcos. “Widgets 1.0: Packaging and Configuration.” W3C Working Draft, December 2008. Accessed May 2009. www.w3.org/TR/widgets/. ———. “Widgets 1.0: The Widget Landscape (Q1 2008),” April 14, 2008. Accessed May 2009. www.w3.org/TR/widgets-land/. Cheng, Jacqui. “CWA Survey: Average Broadband Speed in the US Is 1.9 Mbps.” ARS Technica, May 29, 2007. Accessed April 2009. http://arstechnica.com/tech-policy/ news/2007/05/survey-average-broadband-speed-in-us-is-1-9mbps.ars. Correa, Daniel K. “Accessing Broadband in America: OECD and IEIF Broadband Rankings,” April 24, 2007.The Information Technology and Innovation Foundation. Accessed April 2009. www.itif.org/index.php?id=57. Google Inc. “Gadgets and Internationalization (i18n).” Accessed May 2009. http://code.google.com/apis/gadgets/docs/i18n.html. ———. “Getting Started: gadgets.* API,” 2009. Accessed May 2009. http://code.google.com/apis/gadgets/docs/gs.html. ———. “Google App Engine.” Accessed November 2008. http://code.google.com/appengine/. Intel Corporation. “Instruction Latencies in Assembly Code for 64-Bit Intel® Architecture,” July 13, 2007. Accessed April 2009. http://software.intel.com/ en-us/articles/instruction-latencies-in-assembly-code-for-64-bit-intel-architecture/.
352
References
Karbo, Michael B. “About RAM.” KarbosGuide.com, 2005. Accessed April 2009. www.karbosguide.com/hardware/module2e1.htm. Koch, Peter Paul. “Introduction to W3C Widgets.” Quirksmode, April 2009. Accessed April 2009. www.quirksmode.org/blog/archives/2009/04/introduction_to.html. Lawyer, David S. “Modem-HOWTO: Appendix G: Antique Modems.” ModemHOWTO, January 2007. Accessed April 2009. http://tldp.org/HOWTO/ Modem-HOWTO-29.html. Microsoft Corporation. “SQL Data Services Blog,” October 2008. Accessed November 2008. http://blogs.msdn.com/ssds/. “Mosaic—The First Global Web Browser.” Living Internet. Accessed April 2009. www.livinginternet.com/w/wi_mosaic.htm. MySpace.com. “MySpaceID Developer Addendum to MySpace.com Terms of Use Agreement,” March 16, 2008. Accessed April 2009. http://wiki.developer.myspace. com/index.php?title=MySpaceID_Developer_Addendum_to_MySpace.com_Terms_ of_Use_Agreement. “New Documents Show Enron Traders Manipulated California Energy Costs.” The Wall Street Journal, May 7, 2002. Accessed April 2009. http://online.wsj.com/article/ SB1020718637382274400.html. OpenSocial Foundation. “OpenSocial Data Pipelining Specification v0.9,” April 2009. Accessed May 2009. http://opensocial-resources.googlecode.com/svn/spec/0.9/ OpenSocial-Data-Pipelining.xml. ———. “OpenSocial Javascript API Reference,” September 28, 2008. Accessed December 2008. http://wiki.opensocial.org/index.php?title=JavaScript_API_Reference. ———. “OpenSocial Templating Specification v0.9,” April 15, 2009. Accessed May 2009. www.opensocial.org/Technical-Resources/opensocial-spec-v09/ OpenSocial-Templating.html. “TCP Connections:The Three-Way Handshake.” InetDaemon. Accessed June 2009. www.inetdaemon.com/tutorials/internet/tcp/3-way_handshake.shtml. U.S. Department of Commerce, National Telecommunications and Information Administration Office of Spectrum Management. “United States Frequency Allocations—The Radio Spectrum,” October 2003. Wikimedia Foundation Inc. “Death Star (Business),” February 2009. Accessed April 2009. http://en.wikipedia.org/wiki/Death_Star_(Business). ———. “Hertz,” April 2009. Accessed April 2009. http://en.wikipedia.org/wiki/Hertz. ———. “HTTP Cookie,” October 3, 2008. Accessed October 2008. http://en.wikipedia.org/wiki/Http_cookie.
References
———. “Internationalization and Localization.” Accessed April 2009. http://en.wikipedia.org/wiki/Internationalization_and_localization. ———. “Transmission Control Protocol,” June 26, 2009. Accessed June 2009. http://en.wikipedia.org/wiki/Transmission_Control_Protocol. Yahoo! Inc. “Exceptional Performance,” 2009. Accessed April 2009. http://developer.yahoo.com/performance/.
353
This page intentionally left blank
Index
Symbols and Numbers ${sender} reserved variable, 81–82 0.7 container, 208–211
A About section, Profile page, 276–278 access points, application security, 302 Acknowledge button, 267 activities
creating from app’s Canvas surface, 79 get_activities_atom, 194–195 get_friends_activities_atom, 195 getting app listed on Friend Updates. See opensocial.requestCreate Activity os:ActivitiesRequest tag, Data Pipeline, 223–224 Add App button, 69, 308 add function, 17 Add This App button, 7, 276 adjustheight function, cross-domain access, 23 AdSense, 311–313 advertisements
BuddyPoke app and, 320 creating with Cubics, 313–314 creating with Google AdSense, 311–313 creating with RockYou! ads, 314–316 monetizing Flixster Movies app through, 322 monetizing Playdom apps through, 326 TK’s apps, 323
356
aggreggation, of activity feeds
aggregation, of activity feeds, 82 Ajax (Asynchronous JavaScript and XML), 94, 200–203 albums
creating with create_album, 195–196 fetching, 41–42 fetching with get_album, 184–185 fetching with get_albums, 183 albums, porting to OpenSocial 0.9
fetching, 333–335 updating, 338–339 using opensocial.Album, 330–333 ALL, not filtering friend list, 33 ALL_ALL, invariant message bundles, 256 Amazon
cloud storage products, 64 Web Service, 174, 298–299
managing developers, 279–280 publishing. See publishing app republishing live app, 275 suspension and deletion of app, 280 app Profile, 275–278, 308 AppDataPlay game object, 125–133 Apple Dashboard widget format, 260 application security, 301–302 &appvers=dev, 275 app.yaml
getting started with Google App Engine, 157–158 sending messages using IFPC, 209 server code for REST API, 181 testing OAuth implementation locally, 166 updating for friends Web service, 202
app categories, 308
Aptana, and Unicode, 65
app data store, 47–56
arrays, responseValues, 74
app data P2P play downsides, 147 to avoid scaling bottlenecks, 298 hacking, 302 limitations of, 153 overview of, 47–48 refactoring to build local, 51–56 saving and retrieving data, 48–51 setting up AppDataPlay game, 127–133
Asynchronous JavaScript and XML (Ajax), 94, 200–203 asynchronous JavaScript requests, 15–17 authentication. See OAuth AUTHORIZATION parameter, gadgets.io.makeRequest, 95 automation candy, adding feed, 110–111 AWS (Amazon Web Service), 174 Azure, Microsoft, 174–175, 298–299
App Denial and Status Clarification, MySpace Developer’s Forum, 266–268
B
App Engine, Google, 64
BAD_REQUEST error, 26
app gallery, promoting app with, 306–308
bandwidth, saving, 164
app life cycle, 265–281
basic Profile, 204
changing app Profile/landing page, 275–278 hiding and deleting app, 274 making changes to live app, 274–275
batch requests, performance optimization, 292 blogs, 75–76 body items, 82–83
comments
body, message, 75 Boku (Mobilcash), 317 Bootstrapper
creating “Hello World” app on, 4–6 defined, 5 MySpace messaging policies, 70 permission model for accessing user data, 11 sending notifications, 89 spreading app to other users, 69
adding other surfaces to gadgets, 229–230 fixing parsing errors in gadget code, 228–229 JavaScript blocks using, 225 CDN (content delivery network), 92
C cache memory, scaling bottlenecks and, 295–296 callback function
accessing more than just default Profile data, 18 accessing Profile information, 15–17 checking user permissions to access media, 44 combating permission-denied errors in activities, 87–88 defined, 15 paging friend list, 200–202 in requestSendMessage, 78–79 in requestShareApp, 72–74 setting up AppDataPlay game, 130–132 using friends list data, 38–39 using user data, 20 Canvas page
accessing MySpace data on, 10 creating activities from, 79
Data Pipeline tags processed in, 221–222 implementing paging in, 200–203 REST APIs, 197–199 updating player bio lightbox, 236–237 using custom templates in, 242–244 clients, and scaling performance, 298 cloud services
Amazon Web Service, 174 Google App Engine, 155 overview of, 64 pitfalls of deep linking, 93 codeveloper permissions, 279 comments
adding to turn-based games, 135–138 as supported message type, 74–76
357
358
communication
communication
external server. See external server communications viral features and. See viral features and communication
external server security constraints for, 91 overview of, 56 reasons to not use, 57–59 uses for, 64
connection speeds, and performance, 285
country codes, 256
Console tab, Firebug, xxv
CPUs, scaling bottlenecks in databases and, 295–296
constraints
polling interval for real-time play, 146–147 static content on external servers, 92 consumer key, MySpace
defined, 153–154 OAuth settings, 155–156 server code for REST API, 183 container-generator keys, obtaining, 19 Content block, gadget XML
adding surfaces to gadgets, 217–218, 229–230 custom tag template definition, 240–242 customizing button text between views, 232–235 defined, 215 getting information with os:ViewerRequest tag, 235–236 merging Home and Profile with shared, 230–231 reusing common content, 230–231 shared style, 231–232 subviews, 245–248 using basic data, 218–219 content delivery network (CDN), 92 CONTENT_TYPE parameter, gadgets.io.makeRequest, 95, 97 control flow tags, OSML, 226–227 Cookie Jacker, 59–64 cookies, 56–64
D data, getting additional. See MySpace, getting additional data data, getting basic. See MySpace, getting basic data data listeners, OSML, 250–254 Data Pipeline, 219–225
coupling OSML with. See OSML (OpenSocial Markup Language) data tag os:ActivitiesRequest, 223–224 data tag os:DataRequest, 223–224 data tag os:PeopleRequest, 222–223 data tags, 220–221 data tags os:ViewerRequest and os:OwnerRequest, 222 DataContext, 220 defined, 214
Dewoestine, Eric Van
Data Pipeline, contd.
displaying JSON results with data listener, 251–252 in-network vs. out-of-network data, 221–222 JavaScript blocks in OSML apps, 225 overview of, 219–220 working with, 235–237 data security, 301 data tags, Data Pipeline
in-network vs. out-of-network data, 221–222 os:ActivitiesRequest, 223–224 os:DataRequest, 223–224 os:PeopleRequest, 222–223 os:ViewerRequest and os:OwnerRequest, 222 overview of, 220–221 data types, creating activities, 80–81 data warehouses, for Internet-scale apps, 298 databases
data warehouses vs., 298 as primary scaling bottleneck, 295–297 DataContext, Data Pipelining, 220, 236–237 DataRequest object
accessing more than just default Profile data, 18–19 accessing Profile information, 15–17 app data store, saving and retrieving data, 48–51 asynchronous JavaScript requests and, 15 fetching albums, 333–335 fetching friend list, 31 fetching media, 39–43 fetching media items, 335–336 fetching Viewer’s photos, 336–338 friend list filters and sorts, 32–33
os:DataRequest tag, Data Pipeline, 223–225 paging friend list, 32–37 updating albums and media items, 338–339 DataResponse object
error handling, 25 testing for errors, 300 using MySpace user data, 19–24 “Death Star” project, Enron, 284 debug flag, POST requests, 165 debugging, using Script tab of Firebug, xxv–xxvi deep linking, 93 deleting app data, OpenSocial 0.8 and 0.9, 341–343 deleting apps, 274, 280 description, app, 307 design
managing, 279–281 signing up for MySpace account, 3–4 Developers & Testers option, My Apps page, 279–281 developers, interviews with successful, 318–326
Dan Yue (Playdom), 324–326 Dave Westwood (BuddyPoke app), 318–319 Eugene Park (Flixster Movies), 321–322 Tom Kincaid (TK’s apps), 322–324 Development version, changing live app, 274–275 Dewoestine, Eric Van, 157
359
360
display content, gadgets
display content, gadgets, 215
Enron, 284
display, designing feed reader, 103–104
Enterprise-scale applications, 293
display value, opensocial.Enum, 22
enums
displayMode property, FriendPicker, 123 DOM (Document Object Model)
adding feed reader to app, 93, 95 customizing button text between views, 233 handling raw XML content in, 98 JSONP calls and, 112–113 modifying script to use subviews and, 247–248 processing client-side templates, issues with, 243 processing RSS feed with FEED content type, 104, 106–107 setting up feed reader, 101–102, 104 TTT.List object references to, 37 domains, cross-domain access, 23–24 DRY (Don’t Repeat Yourself) acronym, 230 duplicate applications, and app rejection, 271–272 dynamic content, creating, 92
E e-mail accounts
creating “Hello World” app, 4 dealing with duplicate applications, 273 signing up for MySpace developer account, 3–4 Edit App Information screen, 155, 216–217 Edit App Source, 274–275 Edit Profile, 276 endpoints
Profile, 203–204 supported by MySpace SDK. See REST API list testing for errors with OpenSocial, 300
for fault tolerance and stability, 300 fixing parsing in gadget code, 228–229 from makeRequest call, 100 in on-site vs. off-site app, 200–202 OpenSocial DataResponse objects, 300 OpenSocial functions for, 24–27 for performance optimization, 292 event handling, installs and uninstalls, 279–280 extended Profile, 204 external Iframe apps, 177–212
cookie vulnerabilities not applicable to, 64 pros and cons of, 177–178 REST APIs. See REST (REpresentational State Transfer) APIs sending messages using IFPC, 208–212 talking to parent page, 23 external server communications
adding feed reader. See feed reader, adding to app adding image search, 111–114 mashups, 92–93 overview of, 91–92 pitfalls of deep linking, 93 posting data with form, 114
FriendPicker
external servers
defined, 91 using Data Pipeline tags to pull in data from, 221 using OAuth. See OAuth
calling requestShareApp, 72 fetching, 30–31 using data, 37–39 using filters and sorts, 31–32 using paging, 32–37 Friend Updates, getting app listed on. See opensocial.requestCreate Activity
feedCallback function, 100
friendClickAction property, FriendPicker, 123
fetchFriendList( ) function
FriendPicker
fetching friend list, 31 paging, 33–34
adding, 119–121 operation modes, 122
361
362
friends
FriendPicker, contd.
using in turn-based games, 121–125 friends
adding as developers, 279 displaying with repeater, 237–238 get_friends function, 185–187 get_friends_activities_atom function, 195 get_friendship function, 187–188 interacting with on MySpace, xxii–xxiii prefetching record lists for paging, 287–291 Web service and paging, 200–203 friends object, 211–212 friendsCatalog property, FriendPicker, 123 friends_obj parameter, 198 friends.py script, 202–203 full Profile, 204 function signatures
adding other surfaces, 229–230 adding second surface, 217–218 basic structure, 215 creating “Hello World”, 214–217 creating initial file from existing code, 227–228 declaring and using basic data, 218–219 defined, 214 defining basic app meta-information, 216–217
including translations in app and testing, 259–260 internationalization and message bundles, 255–260 using UTF-8 encoding, 259 gadgets.io.makeRequest
application security and, 301 feed reader, adding to app, 93 feed reader, setting up and designing, 100, 102 feed refresh option, adding, 109–110 Google providing implementation code for, 105 making real MySpace requests, 170–173 making requests back to GAE server, 157 making signed POST request, 162–166 myspace:RenderRequest tag and, 226 option parameters to, 95–96 os:HttpRequest tag equivalent to, 221 overview of, 94–96 performance ramifications of, 286 requesting data with Data Pipeline tags using, 221 spicing up Home and Profile surfaces, 173–174 gadgets.log, 126 gadgets.util.escapeString, 108 gadgets.views.requestNavigateTo(view) function, 23–24, 245–248 GAE (Google App Engine)
Amazon Web Service vs., 174 getting started with, 157–158 making signed POST request using OAuth to, 162–166 making simple GET request to, 158–162 OAuth settings, 155–157
hardware
GAE (Google App Engine), contd.
supported data store properties, 158–159 testing OAuth implementation locally, 166–169 game engine, supporting P2P game play, 133–135 game play. See P2P (person-to-person) game play GameInfo storage object, 125, 138–139
creating app, 3–4 entering app source code, 4–6 for gadgets, 214–217 installing and running app, 7 signing up for developer account, 3–4 Hi5, importing apps to, 326 hiding apps, 274 high priority, defining in OpenSocial, 80 Home page
creating shared style Content blocks, 231–232 customizing button text between views, 232–235 defined, 5, xxii–xxiii getting app listed on Friend Updates, see opensocial.requestCreateActivity indicating new notification on, 88 merging with Profile page, 230–231 not accessing MySpace data on, 10 spicing up surface, 173–174 spreading app to other users, 68–69 horizontal scaling, 297–298 href attribute, opensocial.requestShareApp, 85 HTML
adding custom elements to About section of Profile page, 276–278 adding feed reader to app, 97 building feed reader UI, 99–101 fragment rendering in OSML using, 248–250 Inspect feature of Firebug using, xxv learning in order to use MySpace, 321 widget formats using, 260
internationalization, and message bundles, 255–260
ISPs, database storage using, 64
creating first message bundle, 256–257 creating translations of message bundle, 257–258 culture code processing order, 255 including translations in app and testing, 258–260 limitations of message bundles, 260 overview of, 255 Internet history, 250 Internet-scale applications, defined, 293 Internet-scale applications, performance guidelines
data warehouses vs. relational databases, 298 identifying scaling bottlenecks, 295–297 knowing scaling point, 294–295 load-testing system, 299 overview of, 293–294 pushing work out to nodes, 298 remembering what you know, 297 scaling horizontally, 297–298 utility computing, 298–299 interviews with developers. See developers, interviews with successful invariant (global) culture, 255, 260 Invite page
client code for off-site app, 198–199 creating link to, 204 Invite tab
implementing, 180–182 sending messages to users, 203–208 updating to use OSML and Data Pipeline, 237–238
blocks in OSML apps, 225 error handling in, 24–27 information on using, 30 learning in order to use MySpace, 321 OSML vs., 219–220 responsive performance rules for, 285 sending messages using IFPC, 208–212 TTT namespace, 30 understanding asynchronous requests in, 15 JSLoader, 208–209 JSON (JavaScript Object Notation)
adding feed reader to app, 97 Ajax using, 94 app data game store using, 125 displaying results with data listener, 251–254 error handling in off-site app, 201 evaluating data in app data store, 48 handling content for feed reader, 97 makeRequest response object properties and, 96–97 processing RSS feed with FEED content type, 104–105 using simplejson file to manipulate strings, 160 JSP EL (JavaServer Pages Expression Language), 214 JVM (Java Virtual Machine), 284
365
366
keys
K keys
MySpace secret and consumer, 153–154 opensocial.Enum object, 22 setting up AppDataPlay game, 127 Kincaid, Tom, 322–324
L
logic flows
designing turn-based games, 118 for P2P game play, 138–144, 147 setting up AppDataPlay game object, 125–133 three-way handshakes as, 119 lookForWin function, P2P game play, 133–135 low priority, defining OpenSocial, 80
M
landing page, changing app, 275–276 latestGameInfo, 139 legal issues, deep linking, 93 libraries, OAuth, 154 lightbox, updating player bio, 236–237
Mail Center, notification folder, 88 makePlayerMove function, 134, 135–138
Lightweight JS APIs, 330
makeRequest calls. See gadgets.io.makeRequest
literal data type, 81
marketing and monetizing, 305–327
literals
defined, 19 parsing out, 21–22 live app, making changes to, 274–275 Live version, making changes to, 274–275 load-testing, 299 loadAppData function, 52–53 loadAppDataCallback function, 130–131 loadFeed function, 100 loadFriendPicker function, 124 loadGame function, 139–142 loading issues, example of app rejection, 271–272 localization
developer interviews. See developers, interviews with successful generating revenue with micropayments, 316–318 overview of, 305 promoting app on MySpace, 306–309 user base and viral spreading. See viral spreading markup
fetching albums, 41–42 fetching photos, 39–41 fetching videos, 42–43 including on message template, 82–83 using opensocial.requestShareApp, 86
MySpace, getting basic data
media items, OpenSocial 0.9
fetching albums, 333–335 fetching in 0.8 and, 335–336 fetching media items, 335–336 fetching Viewer’s photos, 336–338 opensocial.Album, 330–333 updating albums and media items, 338–339 uploading media items, 340–341 memory, scaling bottlenecks, 295–297
Agreement, 266 creating “Hello World” app, 3–6 Developer’s Forum, 266 installing and running app, 7 Open Platform, xxiv promoting app on, 306–309 Terms of Service, 310 Terms of Use. See Terms of Use understanding, xxii
reserved variable, 81–82 sorting friend list by nicknames, 33 namespaces
MyOpenSpace, 40–41 TTT. See TTT namespace navigation
away from current page to specified view, 23–24 keystrokes used for, 122 to subviews, 245–248 using cookies for storage across surface, 64 using Pager object, 36
external server communication security with, 111 libraries, 154 MySpace incompatibilities with, 157 overview of, 153 secure phone home. See phoning home setting up environment, 154–157 spicing up Home and Profile surfaces, 173–174 testing implementation locally, 166–169 understanding, 153–154 objects
defined, 19 parsing out, 21–22
network distance, fetching friend list and, 31
off-site apps. See external Iframe apps
New App Invite, 68–69
online references
newFetchAlbumsRequest function, 333–335 newFetchMediaItemsRequest function, 335 newFetchPeopleRequest function
calling requestShareApp, 72 fetching friend list, 30–31 using data, 37–39 newFetchPersonRequest, 16 new_height function, for cross-domain access, 23 Next button, handling with paging, 36
Aptana, 259 basic, full and extended Profile data, 204 code examples for developing first app, 7 Cookie Jacker, 59 Cubics, 313–314 Fiddler, xxvi Firebug, xxv FriendPicker properties, 121
app data store. See app data store basic request and response pattern for, 17 container, 40, xxiii porting app to 0.9, porting app to OpenSocial 0.9 Sandbox tool, 214–217, 227–228 understanding, xxiii–xxiv OpenSocial Markup Language. See OSML (OpenSocial Markup Language) opensocial.Activity, 221 opensocial.Album, 330–333 opensocial.DataResponse. See DataResponse object opensocial.hasPermission, 43–45 opensocial.IdSpec. See IdSpec object opensocial.Message
accessing more than just default Profile data, 18–19 accessing MySpace user data, 11–12 accessing Profile information, 15–17 Data Pipeline tags resulting in, 221 fetching friend list vs. fetching single, 30 request/response pattern, 19–24 opensocial.requestCreateActivity, 79–88
aggregation, 82 body and media items, 82–83 data types, 80–81 defining, 79–80 getting app listed on friend updates, 79–88 notifications patterned after, 88–89 overview of, 79 raising the event, 85–86 reserved variable names, 81–82 using activity callbacks to combat permission-denied errors, 86–87 using Template Editor to create templates, 83–94 using template system to create activities, 80 opensocial.requestCreateActivityPriority, 80 opensocial.requestPermission, 43–45, 87–88 opensocial.requestSendMessage, 74–79
data listeners, 250–254 future directions, 260 HTML fragment rendering, 248–250 inline tag templates, 239–244 internationalization and message bundles, 255–260 working with subviews, 245–248 OSML (OpenSocial Markup Language), applying to Tic-Tac-Toe app, 226–238
displaying data lists, 237–238 reusing common content, 230–235 setting up gadget, 227–230 working with data, 235–237 os:OwnerRequest tag, Data Pipeline, 221–222 os:PeopleRequest tag, Data Pipeline
defined, 221 displaying friends list with repeater, 237–238 overview of, 222–223 processed on server, 221–222
permissions
os:ViewerRequest tag, Data Pipeline
defined, 221 getting Viewer information with, 235–236 overview of, 222 processed on server, 221–222 osx:Else control flow tag, OSML, 227
paging
friend list, 199–203 prefetching record lists for performance optimization, 287–291 using, 32–37 parameters
custom tag template definitions, 241 gadgets.io.makeRequest, 95–96
out-of-network data, Data Pipelining, 221–222
Park, Eugene, 321–322
Own Your Friends app, 325
parsing
Owner
app data store. See app data store concept of, 9–10 fetching app data for, 342 fetching friend list for, 30–31 getting ID of current, 169–170, 172 getting more than just default profile data, 19 os:OwnerRequest tag, 221–222 permission model for accessing user data, 10–11 reading app data from, 47–48 signed POST requests for authenticating, 162–163
P P2P (person-to-person) game play, 117–149
adding user feedback, 135–138 advantages/disadvantages of, 148 finishing and clearing game, 144–145 fleshing out game logic, 138–144 “real-time” play, 146–148 supporting in game engine, 133–135 turn-based games. See turn-based gaming pageSize property, FriendPicker, 123
fixing errors in gadget code, 228–229 XML content type with, 105–106 Pay by Mobile button, Boku, 317 PayPal, 316–317 Pending status, publishing app, 266 performance, responsive
designing for, 284–285 designing for scale. See scaling performance OpenSocial app guidelines, 285–292 overview of, 283 stability and fault tolerance, 299–300 understanding, 283–284 understanding scale, 284 user and application security, 300–303 permission-denied errors, combating, 87–88 permissions
accessing more than just default Profile data, 18–19 checking user settings, 43–45 codeveloper, 279 error code causes, 26 fetching photos uploaded to Profile, 39–41 “Hello World” app, 6 MySpace model for, 9–11 MySpace supported, 44–45 notifications, 90
371
372
persisting information (between sessions)
persisting information (between sessions)
setting up AppDataPlay game, 129–132 using app data store. See app data store using cookies. See cookies using third-party database storage, 64–65 Person data type, 81 Person objects, see opensocial.Person object person-to-person game play. See P2P (person-to-person) game play phoning home, 157–173
making real MySpace requests, 169–173 overview of, 157 signed POST request, 162–166 testing OAuth implementation locally, 166–169 unsigned GET request, 158–162 photos
adding to Profile page, 276–278 checking user permissions to access, 43–45 fetching, 39–41 fetching for Viewer in OpenSocial 0.9 and 0.8, 336–338 fetching with get_photo function, 190 fetching with get_photos function, 188–190 Play page, 198 Play tab
porting to off-site app, 203–208 requiring user’s Profile data, 180 Playdom apps, 324–326 playerBioWrapper ID, 236–237 policies, MySpace communications or messaging, 70 pollForOpponentUpdatedMove function, 147 polling design, for “real time” play, 146–148
porting app to OpenSocial 0.9, 329–349
fetching albums, 333–335 fetching media items, 335–336 fetching Viewer’s photos, 336–338 opensocial.Album, 330–333 overview of, 329–330 REST APIs, 343–348 simplification of app data, 341–343 updating albums and media items, 338–339 uploading media items, 340–341 POST requests
making real MySpace requests, 172–173 making to GAE server using OAuth, 162–166 real-world implications of, 164 testing OAuth implementation locally, 166–168 POST_DATA parameter, gadgets.io.makeRequest, 96 Poster object, real MySpace requests, 169, 172–173 postTo function, 0.7, 210–211 power users, growing base of, 311 Prev button, 36 Preview Template button, Template Editor, 84 printPerson function
getting information with os:Viewer-Request tag, 235–236 updating player bio lightbox, 236–237 using MySpace user data, 21 priorities
defining in OpenSocial, 80 defining opensocial.requestCreateActivity, 79 privacy, cookies and, 57
relay files, using IFPC
Profile page
accessing information using opensocial.Person, 15–17 accessing more than just default data, 18–19 accessing user data, permission model for, 11 bulletins, 77 changing, 275–276 comments on, 74 creating shared style Content blocks, 231–232 customizing button text between views, 232–235 defined, 5, xxii defining for app, 4 defining requestShareApp on, 70–71 editing/updating user’s, 75 endpoint, 203–204 fetching photos uploaded to, 39–41 get_profile function, 190–191 installing and running app, 7 linking to, 7 merging with Home page using shared Content blocks, 230–231 not accessing MySpace data on, 10 reserved variable names and, 81 spicing up surface using makeRequest, 173–174 as supported message type, 75–76 profileDetail property, 19 promoting app. See marketing and monetizing properties
FriendPicker, 121–123 Google App Engine dat store, 158–159 makeRequest response object, 97 proto-mashups, 93
case study in successful rejection negotiation, 268–273 contesting rejection, 267–268 dealing with rejection, 267 overview of, 265–266 republishing live app, 275 why apps are not approved, 266 pushing work out to nodes, for Internet-scale apps, 298 Python editor, OAuth settings, 155
client code, 197–199 friends response from, 211–212 friends Web service and paging, 199–203 how Web service is addressed, 178–179 os:DataRequest tag resulting in endpoints, 221 overview of, 178 porting app to OpenSocial 0.9, 343–348 Profile endpoint, 203–208 server code, 181–183 setting up external Iframe app, 179–180 supported endpoints. See endpoints, supported by MySpace SDK supporting XML and JSON formats, 94 retrieving, friend’s app data, 131–132 RockYou! ads, 314–316 RPC. See IFPC (inter-frame procedure call) RSS feeds
adding content to feed reader app, 97–98 building feed reader UI, 98–101 processing with FEED content type, 104–105 runOnLoad function, 198–199
source code
S Sandbox Editor
declaring and using basic data, 218–219 entering and executing code, 215–216 using os:ViewerRequest tag, 236 Sandbox tool, 214–217, 227–228 Save Template button, Template Editor, 84 scaling
advantages of app data P2P play, 147 BuddyPoke app, 320 Flixster Movies app, 322 using external cloud servers for storage, 64–65 scaling performance
application scale definitions, 293 defined, 284 Internet-scale app guidelines. See Internet-scale applications, performance guidelines scaling point, 294–295 Script tab, Firebug, xxv–xxvi scripts, 247–248 SDKs (software development kits)
app data store limitations, 153 application, 301–302 external server communications, 111 fixing vulnerabilities, 311 hacking and cracking, 302–303 user data, 301 selectedFriend property, FriendPicker, 123 selectedOpponentDataCallback function
app data game store, 131–132
recognizing when new game has started, 145 selecting opponent, 138–140 semaphore (flag value), readers-writers problem, 52–53 send( ) function, 17 Send Message, 74–76 send_notification function, 196–197 servers
code for REST API, 181–183 Data Pipeline tags processed on, 221–222 external. See external server communications; external servers Set Lots of Cookies option, Cookie Jacker, 63 setBackgroundClicked function, 43–44 set_mood function, 195 set_status function, 195 setTimeout directive, 52–53, 109–110 shared style Content blocks, 231–232 showCurrentPage method, 290–291 showFeedResults function, 103–104 showViewerBio function, 236–237 signatures
function. See function signatures OAuth, 162–164 simplejson folder, 156–157, 160 Small/workgroup scale applications, 293 software development kits (SDKs)
adding to gadget file, 229–230 creating “Hello World” app, 4–6 MySpace Open Social, 5 suspension of app, 280
T
creating with Template Editor, 83–84 defining requestShareApp using, 70–71 inline tag. See inline tag templates, OSML notification built-in, 89 using opensocial.requestShareApp, 85–86 Terms of Use
app suspension from violating, 280 example of app rejection, 270–273 overview of, 266 test pages, REST APIs, 197 testing
albums and media items in OpenSocial 0.9, 338–339 app data in Open Social 0.8 and 0.9, 341–343 translations, 260 uploading media items, 340–341 URIs (universal resource identifiers), REST, 178–179 URLs
making real MySpace requests, 170–173 warning when placing in attributes, 251 user data
accessing, 11–14 accessing more than just default Profile data, 18–19 accessing Profile information, 15–17 using, 19–24 user interface. See UI (user interface) users
adding feedback to P2P game play, 135–138 data security for, 301 experience/functionality of app, 271–272, 285 implementing logic for selecting opponent, 140 retaining by listening to them, 311 viral spreading and. See viral spreading “user’s pick” feed reader, 98
UNAUTHORIZED error, cause of, 26
UTF-8 encoding, for app gadgets, 259
Unicode encoding, and app gadgets, 258
utility computing, 298–299
377
378
validating inputs
V validating inputs, 299 variables, setting up AppDataPlay game, 127–128 variables, template
creating templates with Template Editor, 84 data types, 80–81 notifications, 89 reserved names, 81 using opensocial.requestShareApp, 86 verify function, OAuth, 157 verify_request function, OAuth, 165–166 vertical scaling, 297–298 videos
fetching, 42–43 fetching with get_video function, 193 fetching with get_videos function, 192–193
viral features and communication, 67–90
getting app listed on friend updates, 79–88 sending messages and communications, 74–79 sending notifications, 88–90 spreading app to other users, 67–74 viral spreading, 309–316
of BuddyPoke app, 319–320 Cubics, 313–314 Google AdSense, 311–313 listening to customers, 311 overview of, 309–311 Playdom apps using, 325–326 RockYou! ads, 314–316 Visit Profile link, 7
W W3C Widgets specification, 260
View Development Version link, “Hello World” app, 6
Watch window, Script tab of Firebug, xxv–xxvi
Viewer, 236–237
Web applications, driving performance plateau, 284
accessing more than just default Profile data, 18–19 accessing Profile information, 15–17 app data store. See app data store concept of, 9–10 creating custom tag template definition for, 240–242 declaring data in gadget XML for, 218–219 fetching app data for, 342 fetching friend list for, 30–31 getting information with os:ViewerRequest tag, 235–236 os:ViewerRequest tag, 221–222 permission model for accessing user data, 10–11
Web references. See online references Web service, 202–203 Westwood, Dave, 318–319 widget formats, commonly known, 260 Widgets specification, W3C, 260 win/loss record
making MySpace requests, 169–173 making simple GET request to save, 158–162 testing OAuth locally, 166–169 updating with signed POST request, 162–166 Windows Sidebar gadget format, 260 Winloss class, 158–162, 164–166
Yue, Dan
“Word of the Day”, feed reader apps, 110–111 ws.py script, 158, 164–166
X XHR (XMLHtppRequest) object
asynchronous JavaScript requests, 15 external server security constraints for, 91 makeRequest as wrapper on top of, 94–95 overview of, 94
refactoring to build local app data store, 51–52 XML. See also gadget XML
adding feed reader to app, 98 content type with parsing, 105–107 handling content for feed reader, 98 XSS (cross-site scripting) attacks, 303
Y Yahoo! Babelfish, 258 Yue, Dan, 324–326
379
informIT.com
THE TRUSTED TECHNOLOGY LEARNING SOURCE
InformIT is a brand of Pearson and the online presence for the world’s leading technology publishers. It’s your source for reliable and qualified content and knowledge, providing access to the top brands, authors, and contributors from the tech community.
LearnIT at InformIT Looking for a book, eBook, or training video on a new technology? Seeking timely and relevant information and tutorials? Looking for expert opinions, advice, and tips? InformIT has the solution. • Learn about new releases and special promotions by subscribing to a wide variety of newsletters. Visit informit.com /newsletters. • Access FREE podcasts from experts at informit.com /podcasts. • Read the latest author articles and sample chapters at informit.com /articles. • Access thousands of books and videos in the Safari Books Online digital library at safari.informit.com. • Get tips from expert blogs at informit.com /blogs. Visit informit.com /learn to discover all the ways you can access the hottest technology content.
Are You Part of the IT Crowd? Connect with Pearson authors and editors via RSS feeds, Facebook, Twitter, YouTube, and more! Visit informit.com /socialconnect.
informIT.com
THE TRUSTED TECHNOLOGY LEARNING SOURCE
Developer’s Library Series
Visit developers-library.com for a complete list of available products
T
he Developer’s Library Series from Addison-Wesley provides practicing programmers with unique, high-quality references and
tutorials on the latest programming languages and technologies they use in their daily work. All books in the Developer’s Library are written by expert technology practitioners who are exceptionally skilled at organizing and presenting information in a way that’s useful for other programmers. Developer’s Library books cover a wide range of topics, from opensource programming languages and databases, Linux programming, Microsoft, and Java, to Web development, social networking platforms, Mac/iPhone programming, and Android programming.
Try Safari Books Online FREE Get online access to 5,000+ Books and Videos
FREE TRIAL—GET STARTED TODAY! www.informit.com/safaritrial Find trusted answers, fast Only Safari lets you search across thousands of best-selling books from the top technology publishers, including Addison-Wesley Professional, Cisco Press, O’Reilly, Prentice Hall, Que, and Sams.
Master the latest tools and techniques In addition to gaining access to an incredible inventory of technical books, Safari’s extensive collection of video tutorials lets you learn from the leading video training experts.
WAIT, THERE’S MORE! Keep your competitive edge With Rough Cuts, get access to the developing manuscript and be among the first to learn the newest technologies.
Stay current with emerging technologies Short Cuts and Quick Reference Sheets are short, concise, focused content created to get you up-to-speed quickly on new and cutting-edge technologies.
FREE Online Edition
Your purchase of Building OpenSocial Apps includes access to a free online edition for 45 days through the Safari Books Online subscription service. Nearly every AddisonWesley Professional book is available online through Safari Books Online, along with more than 5,000 other technical books and videos from publishers such as Cisco Press, Exam Cram, IBM Press, O’Reilly, Prentice Hall, Que, and Sams.
SAFARI BOOKS ONLINE allows you to search for a specific answer, cut and paste code, download chapters, and stay current with emerging technologies.
Activate your FREE Online Edition at www.informit.com/safarifree STEP 1: Enter the coupon code: SGBUPVH. STEP 2: New Safari users, complete the brief registration form. Safari subscribers, just log in.
If you have difficulty registering on Safari or accessing the online edition, please e-mail customer-service@safaribooksonline.com