Oct 3, 2011

Google+ to Salesforce Chatter Integration

This article describes my entry to the cloudspokes.com Google+ to Salesforce Chatter integration challenge. It covers using OAuth2 to connect to Google+ from Salesforce Apex, making call to the Google+ API to retrieve the Google+ posts to be posted to Chatter and then posting them to Chatter. 

Requirements

Though the title says Google+ to Salesforce Chatter integration, the integration is initiated in reverse. From Salesforce end, we should be able to pull the user's Google+ posts targeted to be sent to the user's Chatter account using a specific hashtag (#ch). 

The Moving Parts
  1. Google OAuth2 - Authentication and Authorization to Google+ APIs. 
  2. Google+ API - Provides access to the user's Google+ posts. 
  3. JSON Parser by Ron Hess - Is used to parse the JSON returned by the Google OAuth2 for server-side web applications and the Google+ APIs. 
  4. Visualforce page and it's controller - Implements the integration logic and allows the user to fetch his Google+ posts and insert the same to Salesforce Chatter. 
Setting Up Google APIs Access
  • Navigate to Goole APIs Console. Create a new project, e.g. API Project. 
  • Navigate to Services tab of Google APIs Console and turn on the Google+ API service. 
  • Navigate to API Access tab of Google APIs Console. Click on Create an OAuth 2.0 client ID
  • Enter 'G+ Chatter Integration' in product name field. This information will be shown to users when your application requests access to their private data using your new client ID. Click Next. 
  • Select Web Application on the next screen. Click on more options link next to Authorized Redirect URIs and enter https:///apex/GooglePlusToChatter in Authorized Redirect URIs text box and https:// in Authorized JavaScript Origins text box. Replace  with your salesforce instance url. Click on Create client ID. Note the client ID and Client Secret. These will be required to setup the Apex connectivity to Google+ in later steps. 
  • See the pictures below for visuals of the above steps. 

Picture 1 - Initiate creation of client ID

    Picture 2 - Create Client ID Step 1
    Picture 3 - Create Client ID Step 2
Picture 4 - View of newly created Client ID
Google+ APIs
Google+ APIs were made available in Sep 2011. The initial set provides read access only. 


The three APIs provided in the initial set are: 

  • People:get - Get a person's profile - https://www.googleapis.com/plus/v1/people/{userId}
  • Activities:list - List all of the activities in the specified collection for a particular user. We will be using this API for our purpose - https://www.googleapis.com/plus/v1/people/{userId}/activities/{collection}
  • Activities: get - Get an activity - https://www.googleapis.com/plus/v1/activities/{activityId}

The userId referred to in the first two bullets above is not the google id used to login. It is an id which uniquely identifies each resource in Google+. You can find your userId by going to your google+ profile. The numeric text following plus.google.com/ is your userId. The picture below shows my Google+ profile with my userId enclosed in the blue rectangle. 
Picture 5 - Getting userId from Google+ profile
The Process
The overall integration works in the following manner: 
  1. The application passes the client id, redirect URI and requested scope of access to Google OAuth2 URL. 
  2. If the user is not logged into Google, user is presented with the authentication screen. Once the user logs in, the user is presented with a screen to authorize your application to access your data. If the user were already logged into Google, only the authorization screen is presented. 
  3. If the user declines to authorize the access, Google returns an access denied message. 
  4. If the user authorizes the access, Google returns and authorization code to the application. 
  5. The application then passes the code, client id, client secret and redirect URI to the Google OAuth2 token URL. 
  6. Google responds with an access token and a refresh token. 
  7. The application uses the access token and the Google userId to send an HTTP get request to Google+ activities list URL. 
  8. Google+ responds with the last 20 Google+ posts in JSON format. 
  9. Application uses JSONObject.cls to parse the response and find all Google+ posts with hashtag #ch which have not been posted to Chatter. 
  10. Application posts selected Google+ posts to Chatter as user's current status. 

Implementation Details


Integration Settings: The following data elements need to be stored in force.com Custom Settings: Google_Plus_to_Chatter_Settings__c under name 'MySettings':
  • Client ID
  • Client Secret
  • Google Plus User
  • Last Published
Picture 6 - Storing integration settings in Custom Settings
Constants used in the integration  
INTEGRATION_SETTINGS_KEY = 'MySettings';  
GOOGLE_OAUTH_URL_CODE = 'https://accounts.google.com/o/oauth2/auth';  
GOOGLE_OAUTH_URL_TOKEN = 'https://accounts.google.com/o/oauth2/token';  
GOOGLE_API_ACCESS_SCOPE = 'https://www.googleapis.com/auth/plus.me';  
GOOGLE_PLUS_REDIRECT_URI_SUFFIX = '/apex/GooglePlusToChatter';  
GOOGLE_PLUS_POSTINGS_URL = 'https://www.googleapis.com/plus/v1/people/';  

URL to Retrieve Authorization Code
 String url = GOOGLE_OAUTH_URL_CODE  
  + '?client_id=' + integrationSettings.Client_Id__c  
  + '&redirect_uri=' + getInstance() + GOOGLE_PLUS_REDIRECT_URI_SUFFIX   
  + '&scope=' + GOOGLE_API_ACCESS_SCOPE  
  + '&response_type=code'; //Retrieving code  


Code to Retrieve Access Token
 //Using the code, client_id and client_secret, get the access token.   
 HttpRequest req = new HttpRequest();  
 req.setEndpoint(GOOGLE_OAUTH_URL_TOKEN);  
 String postData = 'code=' + EncodingUtil.urlEncode(code, 'UTF-8');  
 postData = postData + '&client_id=' + EncodingUtil.urlEncode(integrationSettings.Client_Id__c, 'UTF-8');  
 postData = postData + '&client_secret=' + EncodingUtil.urlEncode(integrationSettings.Client_Secret__c, 'UTF-8');  
 postData = postData + '&redirect_uri=' + EncodingUtil.urlEncode(getInstance() + GOOGLE_PLUS_REDIRECT_URI_SUFFIX, 'UTF-8');  
 postData = postData + '&grant_type=' + EncodingUtil.urlEncode('authorization_code', 'UTF-8');  

Code to Invoke Google+ API to get Activities
 HttpRequest req = new HttpRequest();  
 req.setEndpoint(GOOGLE_PLUS_POSTINGS_URL   
  + integrationSettings.Google_Plus_User__c   
  + '/activities/public'   
  + '?access_token=' + EncodingUtil.urlEncode(accessToken, 'UTF-8'));  
        req.setMethod('GET');  

Code to parse JSON returned by Google
 String json = res.getBody().replace('\n', '');  
 JSONObject j = new JSONObject( json );  
 totalPostsRetrieved = j.getValue('items').values.size();  
 //Iterate through the post and select the chatter posts  
 for (Integer i = 0; i < totalPostsRetrieved; i++) {  
      //Use this post only if the post timestamp is after the last published timestamp and  
      //if it contains the #ch hashtag  
      datetime googlePlusPostTimestamp = convertGoogleTimestampToDatetime(j.getValue('items').values[i].obj.getString('published'));  
      String googlePlusPost = j.getValue('items').values[i].obj.getValue('object').obj.getString('content');  
      if (googlePlusPostTimestamp > integrationSettings.Last_Published__c && googlePlusPost.contains('#ch')) {  
           postsForChatter.add(googlePlusPost);  
      }  
 }  
 chatterPosts = postsForChatter.size();  
 //Update last published timestamp  
 integrationSettings.Last_Published__c = publishedDatetime;  
 try {  
   upsert(integrationSettings);  
 } catch(System.DMLException e) {  
   ApexPages.Message pageMsg = new ApexPages.Message(ApexPages.Severity.ERROR, 'Error updating last published timestamp: ' + e);  
   ApexPages.addMessage(pageMsg);  
 }  


Code to Post to Chatter
 //On first time page load, 'code' parameter is not set.   
 //'code' parameter is set by Google OAuth 2 on redirection back to this page.    
 if (parms.containsKey('code')) {  
   String accessToken = getAccessToken(parms.get('code'));  
   if (accessToken != null) {       
     List<String> postsForChatter = getGooglePlusPostsForChatter(accessToken);     
     for (String post : postsForChatter) {  
         User user = [select id, CurrentStatus from User where id = :UserInfo.getUserId()];  
         user.CurrentStatus = post;  
         update user;  
     }     
   }  
 }  


Application In Action
Picture 7 - Google+ Before posting new entry
Picture 8 - Chatter Before posting new entry
Picture 9 - Page to invoke Chatter integration

Picture 10 - Added New Post on Google+
Picture 11 - Google Authorization Challenge
Picture 12 - Shows 1 post to Chatter and updated Last Published Date

Picture 13 - Shows Google+ post made to Chatter
Improvement Areas
The following enhancements to this integration are possible:

  1. JSON Parser gives too many scripts exception if the number of posts retrieved is high. This needs to be addressed. 
  2. The retrieval of posts can be automated via a scheduled job. 
  3. The code currently requests new access tokens each time. It does not reuse existing token and on failure retrieve a new access token using refresh token. This can be implemented to make the code more in line with the Google OAuth2 design. 

References

I would appreciate feedback from the readers. I will be posting a short video of the application in action in a few days. For additional information and actual code, please get in touch with me.

      Post a Comment