Tech Blog

Integrate Chatter & Twitter on Google App Engine using OAuth

Jeff Douglas

At Appirio we’ve been excited about Salesforce Chatter for quite a while. We firmly believe that Chatter has the potential to bridge the gap between enterprise applications and the way people work. We were luckily enough to receive special prerelease access to Chatter to develop our Social PS Enterprise for the Dreamforce ’09 Chatter Keynote and if you missed the demo at Dreamforce ’09 you can find it here.

Chatter is now in private beta for 100 companies and it is enabled in our production org. We’ve been using it for couple of weeks now and I find myself logging into our org more and more to check the status of other employees, projects and opportunities. As a developer I really wanted to get my hands on the code and test drive Chatter’s functionality. Luckily Quinton Wall has a great Intro to Chatter on developer.force.com to get me started. Sure, I could have developed an Apex and Visualforce application for Chatter but I naturally wanted to integrate Chatter with Twitter. So what I came up with is a Chatter/Twitter app running on Google App Engine using OAuth for Twitter authentication.

Understanding Chatter

Initially I was under the assumption that Salesforce.com would release some sort of API for Chatter. However, they’ve done something even better. Instead of a new API to learn, Salesforce.com exposed Chatter as a series of sObjects allowing you to query for records using the same SOQL that you know and love and manipulate records using DML. Once you get a grip on the Chatter object model and where data lives, developing applications for Chatter is essentially the same as using the Sales or Service Could.

The Chatter model is based upon familiar social networking “Feed Posts”. These posts are made up of a series of Feed Items and Feed Types. The FeedPost stores most of the information that you are concerned about such as the body, title and any content related data. The FeedPost object also contains the information for all posts for the User object including profile statuses, news feeds and entity updates (accounts, contacts or custom objects). The Feed Types are dependent on what actions you are performing:

  • UserStatus – this is the user status update (e.g., “What are you working on?”)
  • TextPost – a post you make from a record
  • LinkPost – a post that contains a URL link (when you click on the link icon)
  • ContentPost – a post that contains some type of uploaded content such as a document or graphic
  • TrackedChanges – whenever a field on a record (set up during Chatter Feed Tracking configuration) is updated

One thing to understand from the beginning is that you do not query for Feed Posts directly. You must query via the Feed Item which contains a reference to the details of the post. So to get the last status update for the current user, you would issue the following SOQL:

SELECT Id, FeedPost.Body FROM UserFeed WHERE ParentId = :Userinfo.getUserId() And Type = ‘UserStatus’ ORDER BY CreatedDate DESC LIMIT 1

For more sample Chatter code, check out the Chatter Code Recipes.

Functional Design

From a high-level overview, the application is fairly simple. When it initially loads the user is prompted to log into Twitter using OAuth.

Twitter asks you to grant the App Engine application the ability to access and update your Twitter account. I’m currently working on OAuth for Salesforce.com and hope to have both sides of the application using OAuth soon. Currently my Salesforce.com sandbox credentials are hard-coded in the application.

Once you authorize access you are redirected back to the application on Google App Engine and presented the following options:

  1. Send your latest tweet to Chatter – fetches your last tweet from your timeline and sends it to Chatter as a status update.
  2. Tweet your latest Chatter status update – queries for you last Chatter update and tweets it. Since Chatter is designed to be private within your org this option isn’t recommended for production and I only implemented it for academic purposes.
  3. Send a status update to both Chatter and Twitter – presents you with a simple form to enter your status update. Once the form is submitted, your status is sent to both Chatter and Twitter.

Technical Design

The application is developed on Google App Engine using the Force.com Web Service Connector (WSC), Salesfore.com Partner library, and the Twitter4j Java library. Since we are using Google App Engine, download the wsc-gae-16_0.jar and partner-library.jar Jars from the WSC project. I used Chatter on one of our sandboxes so I had to do a little tweaking to get the Partner jar running. Now create a new Web Application Project for App Engine and then drop your two jars and the twitter4j jar into the lib directory. You’ll also need to add them to your project’s build path in Eclipse.

Next you’ll have to register your app with Twitter. This will give you the consumer key, consumer secret and URLs you’ll need to authenticate and make requests to Twitter. I’m storing these credentials along with the Salesforce.com sandbox credentials and user id as static variables in a simple credentials class for ease of use.

The application is a series of JSPs and Servlets and if you’d like the code for the entire project, send me a message. The interesting parts of the application are described below and hopefully you can extrapolate the rest.

LoginServlet

This is the initial request for the application. The code uses the Twitter credentials and gets the authorization URL for the app and presents it to the users in the JSP page. The user clicks this link and is taken to Twitter to authorize the application.

package com.jeffdouglas;

import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import org.apache.log4j.Logger;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.http.RequestToken;

public class LoginServlet extends HttpServlet {

private static final Logger log = Logger.getLogger(LoginServlet.class);

public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {

HttpSession session = req.getSession();
Twitter twitter = new TwitterFactory().getInstance();
twitter.setOAuthConsumer(Credentials.TWITTER_CONSUMERKEY,Credentials.TWITTER_CONSUMERSECRET);
RequestToken requestToken = null;

try {
requestToken = twitter.getOAuthRequestToken();
} catch (TwitterException e) {
log.error(e.toString());
}

// get the token and tokenSecret
String token = (String)requestToken.getToken();
String tokenSecret = (String)requestToken.getTokenSecret();
// store the token and tokenSecret in the session
session.setAttribute("token", token);
session.setAttribute("tokenSecret", tokenSecret);

// get the url that the user must click to authenticate w/OAuth
String authUrl = requestToken.getAuthorizationURL();
req.setAttribute("authUrl", authUrl);
RequestDispatcher rd = req.getRequestDispatcher("login.jsp");

try {
rd.forward(req, resp);
} catch (ServletException e) {
log.error(e.toString());
}

}
}

SendChatterServlet

This Servlet runs when the user clicks the Twitter -> Chatter link. The code grabs the user’s last tweet and the uses the Partner Web Services API to submit the sObject with the new Chatter status to Salesforce.com.

package com.jeffdouglas;

import java.io.IOException;
import java.util.List;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.log4j.Logger;
import twitter4j.Status;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.http.AccessToken;

import com.sforce.ws.*;
import com.sforce.soap.partner.*;
import com.sforce.soap.partner.sobject.SObject;

public class SendChatterServlet extends HttpServlet {

private static final Logger log = Logger.getLogger(SendTweetServlet.class);

public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {

PartnerConnection connection = null;
// get the user's last tweet
String tweet = getLastTweet(req,resp);

if (tweet != null) {

try {
if (connection == null) {
ConnectorConfig config = new ConnectorConfig();
config.setUsername(Credentials.SFDC_USERNAME);
config.setPassword(Credentials.SFDC_PASSWORD);
connection = Connector.newConnection(config);
}

// create the sobject to hold the post
SObject post = new SObject();
post.setType("FeedPost");
post.setField("ParentId", Credentials.SFDC_USERID);
post.setField("Body", tweet);
// submit the update to Salesforce.com
connection.create(new SObject[]{post});

} catch (ConnectionException ce) {
log.error(ce.toString());
}

resp.getWriter().println("Tweet sent to Chatter: "+tweet);
} else {
resp.getWriter().println("Could not fetch the lastes update from Twitter. Nothing sent to Chatter.");
}

}

// gets the user's last tweet from their timeline
private String getLastTweet(HttpServletRequest req, HttpServletResponse resp) {

String tweet = null;
HttpSession session = req.getSession();
Twitter twitter = new TwitterFactory().getInstance();

twitter.setOAuthConsumer(Credentials.TWITTER_CONSUMERKEY,
Credentials.TWITTER_CONSUMERSECRET);

// if the access token is present in the session
if (session.getAttribute("accessToken") == null){
// get the request token from the session
String token = (String) session.getAttribute("token");
String tokenSecret = (String)session.getAttribute("tokenSecret");

// get the access token from twitter
AccessToken accessToken = null;
try {
accessToken = twitter.getOAuthAccessToken(token, tokenSecret);
} catch (TwitterException e) {
log.error(e.toString());
}
twitter.setOAuthAccessToken(accessToken);

// save the access token, that are different from request token
session.setAttribute("accessToken", accessToken.getToken());
session.setAttribute("accessTokenSecret", accessToken.getTokenSecret());

} else {
// use the access token from the session
twitter.setOAuthAccessToken((String)session.getAttribute("accessToken"),
(String)session.getAttribute("accessTokenSecret"));
}

List statuses = null;
try {
// get the user's timeline
statuses = twitter.getUserTimeline();
// set their last tweet to return
tweet = statuses.get(0).getText();
} catch (TwitterException e) {
log.error(e.toString());
}

return tweet;

}

}

SendTweetServlet

When the user clicks the Chatter -> Twitter link, this Servlet queries Salesforce.com for the user’s most recent status update, finds the status in the returned XML results and then sends the status out as a tweet.

package com.jeffdouglas;

import java.io.IOException;
import java.util.Iterator;
import javax.servlet.http.*;
import org.apache.log4j.Logger;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.http.AccessToken;

import com.sforce.ws.*;
import com.sforce.ws.bind.XmlObject;
import com.sforce.soap.partner.*;
import com.sforce.soap.partner.sobject.SObject;

public class SendTweetServlet extends HttpServlet {

private static final Logger log = Logger.getLogger(SendTweetServlet.class);

public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {

PartnerConnection connection = null;
String feedPost = null;

try {
if (connection == null) {
ConnectorConfig config = new ConnectorConfig();
config.setUsername(Credentials.SFDC_USERNAME);
config.setPassword(Credentials.SFDC_PASSWORD);
connection = Connector.newConnection(config);
}

QueryResult results = connection
.query("SELECT Id, FeedPost.Body FROM UserFeed WHERE "
+ "ParentId = '" + Credentials.SFDC_USERID + "'"
+ " And Type = 'UserStatus' ORDER BY CreatedDate DESC LIMIT 1");

// in this case there will only be 1 record returned, but for future....
for (int i = 0; i < results.getRecords().length; i++) {
SObject feed = results.getRecords()[i];
feedPost = getFeedBody(feed);
}

} catch (ConnectionException ce) {
log.error(ce.toString());
}

if (feedPost != null) {
sendTweet(feedPost, req, resp);
resp.getWriter().println("Chatter message sent to Twitter: " + feedPost);
} else {
resp.getWriter().println("Nothing sent to Twitter");
}

}

// send the tweet to Twitter
private void sendTweet(String tweet, HttpServletRequest req, HttpServletResponse resp) {

HttpSession session = req.getSession();
Twitter twitter = new TwitterFactory().getInstance();

twitter.setOAuthConsumer(Credentials.TWITTER_CONSUMERKEY,
Credentials.TWITTER_CONSUMERSECRET);

// if the access token is present in the session
if (session.getAttribute("accessToken") == null){
// get the request token from the session
String token = (String) session.getAttribute("token");
String tokenSecret = (String)session.getAttribute("tokenSecret");

// get the access token from twitter
AccessToken accessToken = null;
try {
accessToken = twitter.getOAuthAccessToken(token, tokenSecret);
} catch (TwitterException e) {
log.error(e.toString());
}
twitter.setOAuthAccessToken(accessToken);

// save the access token, that are different from request token
session.setAttribute("accessToken", accessToken.getToken());
session.setAttribute("accessTokenSecret", accessToken.getTokenSecret());

} else {
// use the access token from the session
twitter.setOAuthAccessToken((String)session.getAttribute("accessToken"),
(String)session.getAttribute("accessTokenSecret"));
}

try {
// update the user's twitter status
twitter.updateStatus(tweet);
} catch (TwitterException e) {
log.error(e.toString());
}

}

// parse the xml sObject result and find the user's status
private String getFeedBody(SObject feed) {
String feedBody = "";
Iterator feedPost = feed.getChildren();
while (feedPost.hasNext()) {
XmlObject post = feedPost.next();
if (post.getValue() == null) {
Iterator body = post.getChildren();
while (body.hasNext()) {
XmlObject child = body.next();
if (child.getName().toString().equals(
"{urn:sobject.partner.soap.sforce.com}Body")) {
feedBody = child.getValue().toString();
break;
}
}
}
}
return feedBody;
}

}

SendBothServlet

This Servlet loads the HTML form presenting the user with a textbox to enter their new status. When the form is posted, the status is sent out to both Chatter and Twitter.

package com.jeffdouglas;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.log4j.Logger;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.http.AccessToken;

import com.sforce.soap.partner.Connector;
import com.sforce.soap.partner.PartnerConnection;
import com.sforce.soap.partner.sobject.SObject;
import com.sforce.ws.ConnectionException;
import com.sforce.ws.ConnectorConfig;

public class SendBothServlet extends HttpServlet {

private static final Logger log = Logger.getLogger(SendTweetServlet.class);

// sends the status out to Chatter
private void sendToChatter(String status) {

PartnerConnection connection = null;

try {
if (connection == null) {
ConnectorConfig config = new ConnectorConfig();
config.setUsername(Credentials.SFDC_USERNAME);
config.setPassword(Credentials.SFDC_PASSWORD);
connection = Connector.newConnection(config);
}

// create the sobject to hold the post
SObject post = new SObject();
post.setType("FeedPost");
post.setField("ParentId", Credentials.SFDC_USERID);
post.setField("Body", status);
// submit the update to Salesforce.com
connection.create(new SObject[]{post});

} catch (ConnectionException ce) {
log.error(ce.toString());
}

}

// sends the status out to Twitter
private void sendToTwitter(String status, HttpServletRequest req) {

HttpSession session = req.getSession();
Twitter twitter = new TwitterFactory().getInstance();

twitter.setOAuthConsumer(Credentials.TWITTER_CONSUMERKEY,
Credentials.TWITTER_CONSUMERSECRET);

// if the access token is present in the session
if (session.getAttribute("accessToken") == null){
// get the request token from the session
String token = (String) session.getAttribute("token");
String tokenSecret = (String)session.getAttribute("tokenSecret");

// get the access token from twitter
AccessToken accessToken = null;
try {
accessToken = twitter.getOAuthAccessToken(token, tokenSecret);
} catch (TwitterException e) {
log.error(e.toString());
}
twitter.setOAuthAccessToken(accessToken);

// save the access token, that are different from request token
session.setAttribute("accessToken", accessToken.getToken());
session.setAttribute("accessTokenSecret", accessToken.getTokenSecret());

} else {
// use the access token from the session
twitter.setOAuthAccessToken((String)session.getAttribute("accessToken"),
(String)session.getAttribute("accessTokenSecret"));
}

try {
// update the user's twitter status
twitter.updateStatus(status);
} catch (TwitterException e) {
log.error(e.toString());
}

}

public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {

sendToChatter(req.getParameter("status"));
sendToTwitter(req.getParameter("status"),req);

resp.getWriter().println("Sent the following to both Chatter and Twitter: "+req.getParameter("status"));

}

public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {

try {
RequestDispatcher rd = req.getRequestDispatcher("post.jsp");
rd.forward(req, resp);
} catch (ServletException e) {
log.error(e.toString());
}

}

}

About Jeff Douglas

Jeff Douglas

Jeff Douglas is a senior technical consultant at Appirio and the platform architect for the CloudSpokes community. He is one of the first Force.com MVPs and holds multiple Salesforce.com certifications. Jeff has contributed in various ways to many Force.com projects including the Force.com Web Services Connector, Force.com Toolkit for Facebook and the Force.com for Google App Engine Java Toolkit. He is also the co-author of two books, the Salesforce Handbook and Beginning Google App Engine for Java.

3 Responses to Integrate Chatter & Twitter on Google App Engine using OAuth

cmort says:

Great Post. You can use OAuth with Salesforce.com as well to finish things off….

Dave says:

Dude, really?

This seems senselessly overcomplicated. Have you seen Twitterforce? That’s the toolkit that enables you to use Twitter from Force.com. Not to mention that it seems that you pulled the trigger before implementing the salesforce.com OAuth side.

I really hope the spirit of this post is meant as an academic exercise because I certainly would not recommend injecting another potential point of failure in what should be a point to point (as opposed to a point to point, point to other point) solution.

You have around 400 lines of java code displayed here! This is not simplifying connecting clouds but rather complicating the connections needlessly.

I have a ton of respect for your previous work but this, this is not something that I would promote. There are plenty of examples of doing OAuth to twitter from GAE. Likewise there are plenty of examples of doing WSC from GAE.

I would argue that this is not the right application of GAE or Force.com.

Leave a Reply