Location-Aware Collaborative Applications on Force.com with Foursquare and Salesforce Chatter
Building Location-Aware Collaborative Applications on Force.com with Foursquare and Salesforce Chatter
Abstract
Location aware services are currently one of the most innovative areas of the social web. An interesting vendor in this space is Foursquare, which lets users broadcast their current physical location by “checking-in” to places via their mobile phone apps (the Foursquare app uses the phone's built-in location services to determine the user's location). With each check-in, members can be awarded points and even get access to special promotions.
Although Foursquare and other services of its ilk (Loopt, Gowalla etc.) are mostly used in a social/personal context (e.g. telling your friends which restaurant/bar you’re currently at), it is not hard to imagine a business use case where knowing someone’s or something's physical location could significantly enhance work productivity and/or collaboration.
This article demonstrates one such example: it shows how Foursquare could be integrated with Salesforce Chatter to achieve such collaboration.
Use Cases
Imagine a Field Service Engineer (FSE) who is required to be out in the field resolving customer cases onsite. If the engineer has the Foursquare application installed on his mobile device, he could check-in at a customer location using the application. The check-in could be automatically posted to his Chatter feed.
Taking it to the next level, his check-in could also be posted to the entity feed for all Accounts (and/or Cases) that are within 5 miles of his current location. Knowing that the FSE is currently in the vicinity of the Account/Case, other users that are following those records could then collaborate with the FSE for some real-time follow-up with the other customers in the neighborhood.
Alternatively, imagine a Sales Rep who’s on the road, visiting one of his Accounts/Leads. If he check-ins using his mobile Foursquare application, he could be emailed a list of nearby Accounts and Leads that he may not have even known were close by. He could then add some of those customers/prospects to his itinerary.
High level solution design
Since Foursquare does not allow a ‘push’ integration pattern whereby each check-in by a user would result in a notification to an external application (i.e. Force.com), we have to implement a ‘pull’ design.
A Scheduled Apex class will be used to poll Foursquare every hour for recent check-ins by the user. As stated before, Foursquare is typically used in a personal context. Users therefore wouldn’t want all their check-ins to be posted to their Chatter Feed (an even better reason - who amongst us really wants to know that Doug from IT likes karaoke bars!).
A simple solution to that dilemma is to check for a ‘filter’ tag in the text associated with each Foursquare check-in. If, for example, the user includes ‘#sfdc’ in their check-in post, that (and only that) check-in would be posted to their Chatter feed. The next piece of the synchronization logic would optionally search for Account/Case records within a configurable mile radius of the check-in and post the check-in to the entity feeds of any matching records that the user was subscribed to. Finally, the application would optionally email the user a list of the nearby Accounts and Leads (with a Google Map image for the same).
Installation and configuration
In the first part of this article, you'll learn how to install the code package that implements the above scenario. Along the way you'll have to register at Foursquare, setup authentication, and flip a few switches. It's pretty easy, and at the end you'll have an awesome Foursquare/Chatter mashup. The second part of this article will walk through some of the code.
Step 1 – Install the unmanaged package
The Foursquare synchronization application is available as an unmanaged package here - [available at this install link https://login.salesforce.com/?startURL=%2Fpackaging%2FinstallPackage.apexp%3Fp0%3D04t50000000Lh1Q]. First, install the package in your Developer or Sandbox environment (replace the ‘login.salesforce.com’ portion of the URL with ‘test.salesforce.com’ if installing in a Sandbox Org). As a pre-requisite, Chatter should have been enabled in the destination environment. Make sure you select the ‘Ignore Apex test failures that may cause the installed application not to function properly.’ option when installing the package. All the Apex classes, Visualforce pages and other artifacts referenced in the article are included in this package.
Step 2 – Create a Foursquare account and install the mobile app
The next order of business is to create a Foursquare account if you don’t already have one. You can do so by signing up here - http://foursquare.com/signup/. Next, download and setup the Foursquare application to your mobile device of choice – Blackberry, iPhone, Android etc.
Step 3 - Authentication
One of the first issues to address when integrating two platforms such as Foursquare and Force.com is obviously security – specifically authentication. (Note that while some Foursquare APIs don’t require authentication, the check-in history API call does). The good news is that Foursquare supports the one security protocol that is ideally suited for this kind of use case – OAuth.
Note that Foursquare also supports username-password basic authentication. However, using OAuth is the better option since it does not require storing the user’s Foursquare credentials on Force.com.
Force.com supports OAuth authentication where Force.com is the service/resource provider. However, in this case Force.com would be the OAuth consumer and that is not currently supported out-of-the-box. But despair not – the Force.com developer community is here to the rescue! This wonderful open source project on Code Share includes all the code and configuration needed for Force.com to act as an OAuth consumer using Apex and Visualforce. The unmanaged package that you installed in Step 1 includes a slightly modified version of that code base (therefore no need to install the managed package referenced in the OAuth project).
Step 3.1 – Next, in order to authenticate with the Foursquare API using OAuth you need to register a new application and take note of your consumer key and secret. You can do so here - http://playfoursquare.com/oauth (you’ll need to login). The Application Name, Web Site and Callback URL that you enter are irrelevant to the integration and you can enter any values for them. Make note of the ‘Key’ and ‘Secret’ as you’ll need them in the next step.
Step 3.2 – Back to Force.com. Click on the ‘OAuth Services’ tab that is part of package that you installed in Step 1 (use the '+' link to view all available tabs). Add a new OAuth Service record to represent Foursquare API as follows:
Note: Make sure to select ‘FourSquare’ as the Service Name since the underlying Apex code depends on it.
Step 3.3 – In the next step, you’ll authenticate yourself with Foursquare. Once authenticated, an OAuth token will be issued by Foursquare. We’ll store this token on Force.com in the ‘OAuth Token’ custom object and will use it for all subsequent calls to the Foursquare API.
Click on the ‘Foursquare Settings’ tab. The first time that you access that tab you should see the following :
Click on the link and you’ll be challenged to login to your Foursquare account. Once you login, you’ll need to authorize the application that you created in Step 3.1. Note that these steps are part of the intricate dance that is part of the OAuth protocol and at no point are we storing the Foursquare credentials on the platform.
For a deeper dive into the OAuth protocol, you can read the excellent article Using OAuth to Authorize External Applications. Remember though that the use case described in that article is the exact reverse of ours – i.e. in that article, Force.com is the service provider while an external application is the consumer.
Step 4 – Configuring your Foursquare sync settings
In the final step, we’ll use a simple Visualforce page to control the settings that govern the synchronization of check-in data from Foursquare to Force.com. The page that you’re redirected to after authenticating with Foursquare should look like this:
A quick word on the settings controlled by this page
- Your current Foursquare sync Status: This determines if the Apex Scheduled job that syncs Foursquare check-ins with Force.com every hour is currently enabled. The user can start or stop the Foursquare synchronization process by pressing the power icon. When the status is ‘Off’, no Apex Scheduled job will be active and check-ins made to your Foursquare account will not be synchronized with Salesforce.
- Filter tag: Enter the text that will need to appear in a Foursquare ‘shout’ (the small snippet of text that you can optionally enter with each check-in) in order for the check-in to be synchronized with your Chatter feed. For example you could set this to ‘#sfdc’. As discussed earlier, one of the challenges of using a service like Foursquare is that its predominant use is in a social/personal context. You therefore want to selectively filter which check-ins are posted to your Chatter feed for all your co-workers to see. The filter tag allows you to do that.
- Search Radius (in miles): The search radius for finding nearby Accounts/Cases/Leads.
- Post Check-in to my Chatter Feed: If enabled, Foursquare check-ins will posted to the user’s Chatter feed in Salesforce.
- Enable ‘Nearby Chatter’: If enabled, a post will be added to the entity feed of all Account and Case records that the user is following and which happen to be within the selected search radius of the check-in.
- Email me nearby Accounts and Leads: If enabled, an email will be sent to the user with a list of Accounts and Leads that are near each check-in (‘near’ is again defined by the selected search radius).
- Only include Accounts and Leads that I own (only visible if the previous setting is enabled): If you’ve enabled the ’Email me nearby Accounts and Leads’ option, this setting controls which Accounts and Leads are included in the email.
Note: Of the three synchronization options – Post Check-in to my Chatter Feed, Enable ‘Nearby Chatter’ and Email me nearby Accounts and Leads – at least one has to be enabled.
Update the synchronization settings and hit Save. Saving the settings will automatically turn on the synchronization process by creating an Apex Scheduled job that pulls check-in data from Foursquare on an hourly basis.
Try it out
With the Foursquare synchronization settings configured, let’s take it for a spin! Log into your Foursquare mobile application and check-in. Be sure to include the Filter tag in the Shout.
Now go back to Salesforce and navigate to the ‘Foursquare Settings’ tab. Reset the synchronization by turning it on and off (using the power button). This will force an immediate pull of check-in data from Foursquare (instead of waiting for the next hourly scheduled sync). You should now see something similar to this in your Chatter feed:
Note that you would only see something equivalent to the image above if
- The ‘Post Check-in to my Chatter Feed’ setting was enabled
- The ‘Enable Nearby Chatter’ setting was enabled
- All Account records in Salesforce are geo-coded (see below for details)
- You were following a Case or Account record that happens to be within the selected search radius of the check-in.
Any other user who is following that record would then see this post and collaboration ensues!
Clicking on the link next to your status update should bring up a Google Map of the check-in location.
Finally, if the 'Email me nearby Accounts and Leads' setting is enabled, all Account and Leads records are geocoded and there are some Account/Lead records that are within the selected search radius of the check-in, you should receive an email that looks something like this:
Code walkthrough
If you followed the tutorial, you will have all the source code for the application already installed in your environment. Here's a quick run down of all the important classes.
FourSqSyncController.cls
This class in the custom controller for the FourSqSyncSettings Visualforce page that the user uses to control the Foursquare sync settings. The settings themselves are stored in a simple custom object (Foursquare_Settings__c).
SyncFromFourSq.cls
This class implements the Schedulable interface and is invoked every hour by Force.com as per the schedule setup in the FourSqSyncController.cls. This class in turn calls the GetFourSqCheckinHistory.cls class to pull the latest check-in history for the user.
GetFourSqCheckinHistory.cls
This class is the heart of the application and it invokes the Foursquare API to pull the latest check-in history for the user.
Let's walk through step-by-step how the getCheckinHistory method retrieves the Foursquare checkin history for a given user:
1) We start by querying the Foursquare settings for the respective user. We'll use these settings to determine which Foursquare checkins should be posted to the user's Chatter Feed.
settings = [SELECT User__c, Job_ID__c, Last_Checkin_Id__c, Filter_tag__c,
Enable_Nearby_Chatter__c, Nearby_Accounts_and_Leads__c, Post_Checkin__c,
Search_Radius__c, Own_Accounts_and_Leads__c
FROM FourSquare_Settings__c
WHERE User__c = :UserInfo.getUserId() limit 1];
2) Next, we make a callout to the Foursquare API to retrieve the check-in history for the user. One of the GET parameters (sinceid) of that API call is the check-in id to start returning results from. Here, we pass in the ID of the last check-in that was synchronized with Force.com in a previous run of the scheduler. That way we won’t keep reposting the same Foursquare check-in to the user’s Chatter feed with each run of the scheduler.
Note also the use of the ‘OAuth’ class to sign the API request. That class is part of the OAuth Playground Code Share project that allows Apex to act as an OAuth consumer.
Http h = new Http();
HttpRequest req = new HttpRequest();
req.setMethod('GET');
String endpointURL = 'http://api.foursquare.com/v1/history';
if (settings.Last_Checkin_Id__c != null && settings.Last_Checkin_Id__c != '')
{
endpointURL += '?sinceid='+settings.Last_Checkin_Id__c;
}
req.setEndpoint(endpointURL);
req.setTimeout(60000);
OAuth oa = new OAuth();
if (!isTest)
{
if(!oa.setService('FourSquare')) {
System.debug(oa.message);
return;
}
oa.sign(req);
}
System.debug('Sending request...');
Dom.Document doc;
if (!isTest)
{
HttpResponse res = h.send(req);
doc = res.getBodyDocument();
}
else
{
String testHistory = '<checkins><checkin><id>1234</id><venue><geolat>123</geolat><geolong>123</geolong></venue></checkin></checkins>';
doc = new Dom.Document();
doc.load(testHistory);
}
3) Next, we iterate through all the check-ins returned by the Foursquare API. Note that since we passed a ‘sinceid’ parameter to the check-in history API, the response will be a delta between now and the last time that the Apex scheduler ran – i.e. all Foursquare check-ins by the user in the last hour.
For each check-in, we'll extract the location details (address as well as the latitude and longitude) and use those to construct a Google Static Map URL for the check-in location. Finally, if the 'Post Check-in to my Chatter Feed' sync setting is enabled, we'll create a new FeedPost record for the user to add an entry in the user's Chatter Feed.
Integer lastCheckinId = 0;
List<FeedPost> posts = new List<FeedPost> ();
boolean accountChatter = Account.sObjectType.getDescribe().isFeedEnabled();
boolean caseChatter = Case.sObjectType.getDescribe().isFeedEnabled();
Set<Id> recsUserIsSubscribedTo;
for (Dom.XMLNode checkin : doc.getRootElement().getChildElements())
{
Integer checkinId =
Integer.valueof(checkin.getChildElement('id', null).getText());
if (checkinId > lastCheckinId)
lastCheckinId = checkinId;
//The venue node contains the details about the check-in.
Dom.XMLNode venue = checkin.getChildElement('venue', null);
if (venue == null)
continue;
//If the Shout does not contain the ‘Privacy Tag’ text that the user
//setup in Force.com, discard it.
String shout = (checkin.getChildElement('shout', null) != null)?
checkin.getChildElement('shout', null).getText(): '';
if (settings.Filter_tag__c != null)
{
if (!shout.contains(settings.Filter_tag__c))
continue;
}
String name = (venue.getChildElement('name', null) != null)?
venue.getChildElement('name', null).getText(): '';
String address = (venue.getChildElement('address', null) != null)?
venue.getChildElement('address', null).getText(): '';
String city = (venue.getChildElement('city', null) != null)?
venue.getChildElement('city', null).getText(): '';
String state = (venue.getChildElement('state', null) != null)?
venue.getChildElement('state', null).getText(): '';
String zip = (venue.getChildElement('zip', null) != null)?
venue.getChildElement('zip', null).getText(): '';
Double lat = (venue.getChildElement('geolat', null) != null)?
Double.valueOf(venue.getChildElement('geolat', null).getText()): 0;
Double lon = (venue.getChildElement('geolong', null) != null)?
Double.valueOf(venue.getChildElement('geolong', null).getText()): 0;
String googleMapsUrl;
if (settings.Post_Checkin__c)
{
String msg = 'Checked in at ';
//Create a link to a Google Map that displays the check-in location
googleMapsUrl = 'http://maps.google.com/maps?q=' +
EncodingUtil.urlEncode( address + ',' + city + ','
+ state + ',' + zip + '+(' + name + ')', 'UTF-8');
System.debug('The URL is:'+googleMapsUrl);
//Add a 'Link' Feed Post to the user's Chatter feed with the check-in
//details (including the Google Maps link)
FeedPost post = new FeedPost(ParentId = UserInfo.getUserId(),
body = msg,
LinkUrl = googleMapsUrl,
Title = name,
Type = 'LinkPost');
System.debug('Post is:'+ post);
posts.add(post);
}
.....
}
4) The next step is to find Account and Case records that are within the selected search radius miles of the user's check-in location. The FindNearbyUtil.cls utility class encapsulates the logic for computing the search grid around the latitude and longitude coordinates of the user's check-in. Once we have the search grid, we query Account records that fall within those coordinates (this assumes that Account records have already been geo-coded). Since we want to be a little more discriminating in our Chatter posts, we also query the EntitySubscription object to determine which of those Account records the user is explicitly following. For each such matching Account record, we create a new Entity Post record with the user's check-in location.
if ( (settings.Enable_Nearby_Chatter__c || settings.Nearby_Accounts_and_Leads__c) &&
settings.Search_Radius__c != null && settings.Search_Radius__c != '')
{
System.debug('Checking lat is:'+lat);
System.debug('Checking long is:'+lon);
FindNearbyUtil con = new FindNearbyUtil();
//Calculate the search radius around the check-in coordinates
con.calculateSearchRadius(lon, lat, Double.valueOf(settings.Search_Radius__c));
//Query for all Accounts (and associated Case children records) that fall
//within the search grid.
Account[] nearbyAccts = [select id, name, BillingStreet, lat__c, lon__c, ownerid,
(select id from Cases)
from Account where lat__c <= :con.latMax
and lat__c >=:con.latMin
and lon__c <= :con.lonMax
and lon__c >=:con.lonMin];
if (settings.Enable_Nearby_Chatter__c && nearbyAccts.size() > 0)
{
if (recsUserIsSubscribedTo == null)
{
recsUserIsSubscribedTo = new Set<Id> ();
//Find out all the records that the use is currently following.
//We want to be a little more discriminating by only posting to
//those records that the user is explicity following.
for (EntitySubscription sub : [select parentid, subscriberid
from EntitySubscription
where subscriberid =
:UserInfo.getUserId()])
{
recsUserIsSubscribedTo.add(sub.ParentId);
}
}
String postMsg = 'Just checked in near this ';
for (Account a : nearbyAccts)
{
if (accountChatter && recsUserIsSubscribedTo.contains(a.Id))
{
FeedPost entityPost = new FeedPost(ParentId = a.Id,
body = postMsg + 'Account',
Type = 'TextPost');
posts.add(entityPost);
recsUserIsSubscribedTo.remove(a.Id);
}
if (caseChatter)
{
for (Case c : a.Cases)
{
if (recsUserIsSubscribedTo.contains(c.Id))
{
FeedPost entityPost = new FeedPost(ParentId = c.Id,
body = postMsg + 'Case',
Type = 'TextPost');
posts.add(entityPost);
recsUserIsSubscribedTo.remove(c.Id);
}
}
}
}
}
5) Finally, if the 'Email me nearby Accounts and Leads' sync setting is also enabled, we query for any Lead records that fall within the search radius. An email then gets generated and sent out using the SendNearbyEmailUtil.cls utility class.
if (settings.Nearby_Accounts_and_Leads__c)
{
Lead[] nearbyLeads = [select id, FirstName, LastName, Street, Company,
lat__c, lon__c, ownerid
from Lead where lat__c <= :con.latMax
and lat__c >=:con.latMin
and lon__c <= :con.lonMax
and lon__c >=:con.lonMin limit 5];
SendNearbyEmailUtil.sendNearbyEmail(nearbyAccts, nearbyLeads, name, lat, lon,
settings.Own_Accounts_and_Leads__c);
}
SendNearbyEmailUtil.cls
This utility class generates the email (in HTML format) that lists all the nearby Account and Lead records along with a Google Map image showing them next to the check-in location.
FindNearbyUtil.cls
The logic for this class is shamelessly copied from the free ‘Find Nearby Accounts’ AppExchange package. While it is not necessary to install that package in your Developer/Sandbox Org for the Foursquare application to function, it is necessary to geo-code all Account records (i.e. determine the Latitude and Longitude based on the billing or shipping address) in order for the ‘Enable Nearby Chatter’ functionality to work. The ‘Find Nearby Accounts’ package includes one such geo-coding function (using Google geo-coding APIs), though you can choose to use any other geo-coding service.
References
- Visit Foursquare.com for all things Foursquare, including their API documentation
- The code in this project uses code from two other projects:
- See OAuth Consumer Project for the OAuth library that's used
- See the Find Nearby Accounts AppExchange package for the "nearby" logic
- Another great mashup of location aware services (Glimpse in this case) and Chatter - was posted in the Chatter Dev Challenge http://developer.force.com/chatterdevchallenge/entry?id=087300000002lHSAAY
- Learn more about OAuth in Using OAuth to Authorize External Applications
About the Author
Sandeep Bhanot is a Developer Evangelist with Saleforce.com and has never actually been to a karaoke bar.






