Rails.application.config.middleware.use OmniAuth::Builder do provider :google_oauth2, ENV["GAM_OAUTH_KEY"], ENV["GAM_OAUTH_SECRET"] end
# Subclass the GoogleOauth2 Omniauth strategy for # Google Apps Marketplace V2 SSO. module OmniAuth module Strategies class GoogleAppsMarketplace < OmniAuth::Strategies::GoogleOauth2 option :name, 'google_apps_marketplace' end end end
Rails.application.config.middleware.use OmniAuth::Builder do provider :google_oauth2, ENV["OAUTH_KEY"], ENV["OAUTH_SECRET"], {:scope => ENV["OAUTH_SCOPE"]} provider :google_apps_marketplace, ENV["GAM_OAUTH_KEY"], ENV["GAM_OAUTH_SECRET"], { :scope => ENV["GAM_OAUTH_SCOPE"], :access_type => 'online' }end
Gmail.connect!(:xoauth, 'ben@example.com', { token: authentication.token, secret: authentication.secret, consumer_key: google.key, consumer_secret: google.secret, read_only: true })
key = Google::APIClient::PKCS12.load_key( google_apps.service.p12path, # this is a constant value Google uses # to password protect the key. 'notasecret' )service_account = Google::APIClient::JWTAsserter.new( google_apps.service.email, 'https://mail.google.com/', key )client = Google::APIClient.new( :application_name => APPLICATION_NAME, :version => APPLICATION_VERSION ).tap do |client| client.authorization = service_account.authorize('ben@example.com')end Google.connect!(:xoauth2, 'ben@example.com', { :oauth2_token => client.authorization.access_token, })
# Subclass the GoogleOauth2 Omniauth strategy for # Google Apps Marketplace V2 SSO. module OmniAuth module Strategies class GoogleAppsMarketplace < OmniAuth::Strategies::GoogleOauth2 option :name, 'google_apps_marketplace' def request_phase # Store the opensocial_viewer_id in the session. # this allows us to bind the Google Apps contextual # gadget to a user account. if request.params['opensocial_viewer_id'] session[:opensocial_viewer_id] = request.params['opensocial_viewer_id'] end super end end end end
public void changeOwner(String user, String fileId, String newOwner) { // Find what is the current permission of the new owner on the file Permission newOwnerPermission = null; PermissionList permissionList = RetriableTask.execute(new DrivePermissionListTask(drive.permissions().list(fileId))); newOwnerPermission = findPermission(permissionList, newOwner); if (newOwnerPermission == null) { // New owner is not in the list, we need to insert it newOwnerPermission = new Permission(); newOwnerPermission.setValue(newOwner); newOwnerPermission.setType("user"); newOwnerPermission.setRole("owner"); Drive.Permissions.Insert insert = drive.permissions().insert(fileId, newOwnerPermission); RetriableTask.execute(new DrivePermissionInsertTask(insert)); } else { // New owner is already in the list, update the existing permission newOwnerPermission.setRole("owner"); Drive.Permissions.Update update = drive.permissions().update(fileId, newOwnerPermission.getId(), newOwnerPermission); update.setTransferOwnership(true); RetriableTask.execute(new DrivePermissionUpdateTask(update)); } }
public class RetriableTask implements Callable { [...] private final Callable task; [...] @Override public T call() { T result = null; try { startTime = System.currentTimeMillis(); result = task.call(); } catch (NonFatalErrorException e) { if (numberOfTriesLeft > 0) { // Wait some time, using exponential back-off in case of multiple attempts Thread.sleep(getWaitTime()); // Try again result = call(); } else { // Too many failed attempts: now this is a fatal error throw new RetryException(); } } catch (FatalErrorException e) { // This one should not be retried Throwables.propagate(e); } return result; }
Editor’s note: This is a guest post by Cameron Henneke. Cameron is the founder and principal engineer of GQueues, a task management app on the Google Apps Marketplace. Cameron tells the story of his application and provides some tips for developers considering integrating with Google Apps and launching on the Marketplace -- Ryan Boyd
Google recently announced that over 4 million businesses now run on Google Apps, continuing its growth as enterprise software that focuses on collaboration. This of course is great news for Google Apps developers, since this means there are 4 million potential customers on the Google Apps Marketplace looking for complimentary tools to enhance their productivity. As you know, listing an app requires just a few quick steps, and the Marketplace targets a growing audience of customers ready to purchase additional solutions.
So what kind of success might you see on the Marketplace and how can you maximize revenue? As the founder of GQueues, an online task manager, I have listed the app on the Marketplace since its launch in March 2010. Over the past year and half, I have found the Marketplace to be my most successful channel, and have discovered a few tips along the way that proved key to this success.
Though this seems obvious, this first point is critical: make sure your app solves a real problem. This means you’ve identified actual people and businesses that have this problem and are actively looking for a solution. Perhaps they have already tried other tools or cobbled something together on their own. For example, I’ve verified Google Apps users are looking for an intuitive way to organize their work and manage tasks with others. GQueues fills this need as a full-featured task manager that allows users to assign tasks, share lists, set reminders, create tasks from email and tag work for easy filtering. Google Apps users come to the Marketplace with a variety of needs, make sure your app addresses at least one of them.
As you solve a customer’s problem, make sure you integrate with their existing tools. For Marketplace customers, this means adding as many integration points with Google products as possible. This is important for several reasons.
First, it’s great for the user and facilitates adoption. If your service works seamlessly with other products they are already familiar with, they don’t have to spend time learning something new. For instance, GQueues has two-way syncing with Google Calendar. Since users already know how to drag events to new dates in Calendar, dragging GQueues tasks around the calendar is quite intuitive.
Secondly, more integration directly helps your app’s listing in the Marketplace. Each listing has a set of icons representing product integrations. GQueues integrates with Calendar, Mail, Contacts and Google Talk, which indicates to a customer that using this tool will allow their users to work more efficiently. Plus, customers can search based on integration points, so the more you have, the broader your presence in the Marketplace.
Lastly, integrating with existing products speeds development. Utilizing Google APIs allows you to innovate faster and respond to your customers growing needs. GQueues uses the XMPP service of Google App Engine, which eliminated the need to build a separate chat backend and makes it easy for users to add tasks by chatting a message from anywhere.
Once you’ve listed your deeply integrated app that solves a real problem on the Marketplace, be sure to engage with your customers. The Marketplace allows users to rate your app and leave verified reviews, which not only impact the app’s listing position, but greatly influence potential customers’ willingness to test it out. I manage the GQueues Marketplace listing with a two-fold approach:
These actions are quite simple, but immensely effect your app’s presence in the Marketplace.
Though each app is unique, I’ve found that following the tips mentioned above have helped the Google Apps Marketplace become GQueues’ top revenue channel.
GQueues is based on a freemium model, and the average conversion rate for a typical freemium product is 3-5%. Looking at all the regular channels, GQueues has a 6% average conversion rate from free users to paid subscribers - slightly higher than expected. However, the GQueues users from the Marketplace convert at an astonishing rate of 30%.
The Marketplace claims to target an audience ready to buy, and the data really backs this up.
Not only does the Marketplace have a substantially higher conversion rate, but it also drives a considerable amount of traffic. Looking at the data over the same period, 27% of all new GQueues users were acquired via the Marketplace.
Combining the acquisition rate with the conversion rate shows that the Marketplace is actually responsible for 63% of all paid GQueues users.
As Google Apps continues to grow worldwide, the need for deeply integrated, complimentary business tools will also expand. Based on my experience with GQueues, I strongly recommend the Google Apps Marketplace as a rewarding channel for apps that integrate with Google Apps.
Editors note: This is a guest post by Alex Steshenko. Alex is a core software engineer for Solve360, a highly-rated CRM in the Google Apps Marketplace which was recently chosen as a Staff Pick. Solve360 offers many points of useful integration with Google Apps. Today, in contrast to the conventional Data API integrations, Alex will showcase how he extended Solve360 using Google Apps Script. --- Ryan Boyd
Solve360 CRM integrates with Google services to provide a two-way contact & calendar sync, email sync and a comprehensive Gmail contextual gadget. We use the standard Google Data APIs. However, some of our use cases required us to use Google documents and spreadsheets. Enter Apps Script!. What brought our attention to Google Apps Script was that it allows you to run your application code right within the Google Apps platform itself, where documents can be manipulated using a wide range of native Google Apps Script functions, changing the perspective.
Our first experience with Google Apps Script was writing a "Contact Us" form. We decided to use the power and flexibility of Apps Script again to generate different kinds of reports.
Google Spreadsheets can produce rich reports leveraging features such as filters, pivot tables, built-in functions and charts. But where’s the data to report on? Using Google Apps Script, users can integrate Google Spreadsheets with a valuable source of data - the Solve360 CRM - completing the solution.
Solve360 Google Apps Reporting script lets users configure the reporting criteria while pulling reports into a Google Spreadsheet.
Here's a video demonstrating a real use case for Solve360 Reporting:
For this script, we realized, simply providing spreadsheet functions would not be good enough. We needed a user interface to let users configure their account details and define what kind of data to fetch from the Solve360 CRM. Google Apps Script’s Ui Services came in handy. For instance, here is the function responsible for showing the “Solve360 account info” dialog:
/* * Creates new UI application and opens setting window */ function settingsUi() { var app = UiApp.createApplication(); app.setTitle('Solve360 account info') .setWidth(260) .setHeight(205); var absolutePanel = app.createAbsolutePanel(); absolutePanel.add(authenticationPanel_(app)); app.add(absolutePanel); SpreadsheetApp.getActiveSpreadsheet().show(app); }
Solve360 CRM has an external API available so the system can be integrated with custom business applications and processes. Reporting script use case is a good example of what it can be used for.
One of the first tricks learned was creating our own Google Apps-like “service” to encapsulate all those functions responsible for interacting with Solve360 CRM’s API. What is the most interesting is that this service’s code isn’t a part of the distributed Google Apps script. Instead the library is loaded from within the script itself directly from our servers. Why? Let’s say we found a bug or added new functions - if we had many copies of the service we would need to update them all, somehow notifying our clients, and so on. With one source, there’s no such problem. You may think of it as a way to distribute a Google Apps Script solution, or, in our case, a part of the solution. The service is called Solve360Service and its usage looks like this:
Solve360Service
var contact = Solve360Service.addContact(contactData);
There were two problems with getting such an external service to work: Google Apps Script permissions restrictions and actually including it in the script.
The issue with permissions is that the Google Apps Script environment can’t see which Google Apps Script services are used inside the external service - that’s why it doesn’t ask you to grant special permissions for them. To force the authorization request for those permissions we added this to the onInstall function (called once when script is added to the spreadsheet):
onInstall
function onInstall() { // to get parseJS permissions Xml.parseJS(['solve360', '1']); // to get base64Encode permissions Utilities.base64Encode('solve360'); // ... }
Here is the solution we used to load our centralized code into the script:
eval(UrlFetchApp.fetch("https://secure.solve360.com/gadgets/resources/js/Solve360Service.js").getContentText());
The Solve360Service is loaded from a single source - no copy-paste. All the functions for accessing the Solve360 API aka “the plumbing” are abstracted and hidden in inside this service, while the essentials of the reports script itself can be modified and tweaked to a particular client’s case. Inside of Solve360Service we use UrlFetchApp:
UrlFetchApp
/** * Request to the Solve360 API server * data should be an Array in Short Hand notation */ request : function(uri, restVerb, data) { if (this._credentials == null) { throw new Error('Solve360 credentials are not set'); } if (typeof(data) != 'undefined') { if (restVerb.toLowerCase() == 'get') { var parameters = []; for each(var parameter in data) { parameters.push(encodeURIComponent(parameter[0]) + '=' + encodeURIComponent(parameter[1])); } uri += '?' + parameters.join('&'); data = ''; } else { data.unshift('request'); data = Xml.parseJS(data).toXmlString(); } } else { data = ''; } var options = { "contentType" : "application/xml", "method" : restVerb.toLowerCase(), "payload" : data, "headers" : {"Authorization" : "Basic " + this._credentials} }; return Xml.parse(UrlFetchApp.fetch(this._url + uri, options).getContentText()).getElement(); }
As the result is always XML, in order to remove any extra work we call Xml.parse() right inside the request function and always return a XmlElement so you can iterate through it, access nodes and attributes. Here is a simplified version of how we load some items when building a report:
Xml.parse()
XmlElement
/* * Builds a search config from user preferences and loads a slice of data from Solve360 * To configure how many items should be loaded at a time, change ITEMS_LOAD_REQUEST_LIMIT constant */ function retrieveItems_(parameter, offset) { initSolve360Service_(); // ... var searchParameters = [ ['layout', '1'], ['sortdir', 'ASC'], ['sortfield', 'name'], ['start', '' + offset], ['limit', '' + ITEMS_LOAD_REQUEST_LIMIT], ['filtermode', filtermode], ['filtervalue', filtervalue], ['searchmode', searchmode], ['searchvalue', searchvalue], ['special', special], ['categories', '1'] ]; if (parameter.showAllFieldsCheckbox != 'true' && fields.length > 0) { searchParameters.push(['fieldslist', fields.join(',')]); } // ... var items = Solve360Service.searchProjectBlogs(searchParameters); // ... return items; }
To simplify the usage of the service we added another function which initializes the service object, named Solve360Service:
/* * Loads external Solve360Service * For the service functions available refer to the source code here: * https://secure.solve360.com/gadgets/resources/js/Solve360Service.js */ var Solve360Service = null; function initSolve360Service_() { if (Solve360Service == null) { eval(UrlFetchApp.fetch("https://secure.solve360.com/gadgets/resources/js/Solve360Service.js").getContentText()); var user = UserProperties.getProperty(USERPROPERTY_USER); var token = UserProperties.getProperty(USERPROPERTY_TOKEN); if (user == null || user.length == 0 || token == null || token.length == 0) { throw new Error('Use Solve360 spreadsheet menu to set email and token first'); } Solve360Service.setCredentials(user, token); } }
As you can see, it uses the email/token pair previously saved in the “Solve360 Account Info” dialog or signals an error if the credentials were not yet saved.
There are many use cases where you can apply the Google Apps Script. The fact that you can work and implement solutions right from “inside” one of the greatest and most universal web applications available is amazing.
You can integrate your own software with Google Docs or even learn from us and build a reporting script for any other system accessible online. Try to look at solving business tasks from a different perspective, from the Google Apps point of view. We encourage it!
The code of the new script is available for use and study here: https://secure.solve360.com/docs/google-apps-reports.js.
Alex Steshenko is a core software engineer for Solve360, a CRM application on the Google Apps Marketplace.
def u2open_appengine(self, u2request):tm = self.options.timeouturl = self.u2opener()# socket.setdefaulttimeout(tm) can't call this on app engineif self.u2ver() < 2.6: return url.open(u2request)else: return url.open(u2request, timeout=tm)transport.http.HttpTransport.u2open = u2open_appengine
class MemCache(cache.Cache): def __init__(self, duration=3600): self.duration = duration self.client = memcache.Client() def get(self, id): return self.client.get(str(id)) def getf(self, id): return self.get(id) def put(self, id, object): self.client.set(str(id), object, self.duration) def putf(self, id, fp): self.put(id, fp) def purge(self, id): self.client.delete(str(id)) def clear(self): self.client.flush_all()
docmail_client = client.Client(USERNAME, PASSWORD, SOURCE)
mailing = client.Mailing(name='test mailing')mailing = docmail_client.create_mailing(mailing)
mailing = docmail_client.get_mailing('enter-your-mailing-guid')
docs_client = gdata.docs.client.DocsClient()# authenticate client using oauth (see google docs documentation for example code)
file_content = docs_client.GetFileContent(uri=doc_url + '&exportFormat=rtf')
docmail_client.add_template_file(mailing.guid, file_content)
docs_client = gdata.docs.client.DocsClient()file_content = docs_client.GetFileContent(uri=doc_url + '&exportFormat=csv')docmail_client.add_mailing_list_file(mailing.guid, file_content)
docmail_client.process_mailing(mailing.guid, False, True)
docmail_client.process_mailing(mailing.guid, True, False)
Posted by Stuart Keeble & Gwyn Howell, Appogee
Want to weigh in on this topic? Discuss on Buzz