Integrations, ML/AI and Design
A brand is no longer what we tell consumer it is. It is what consumers tell each other it is - Scott Cook

And so platforms like twitter are not just an option but a necessity for brands. It therefore naturally becomes a channel where companies can better engage with their customers. From marketing campaigns to customer support, from backing a noble cause to issuing mass apology for damage control, twitter - the platform that made us acquainted to the world of #hashtags and @mentions, is a must integration with any CRM platform.

In this article we demonstrate one such use case by integrating Salesforce service cloud with twitter. Lets take a quick look at the demo and also understand the flow of the integration.

Twitter Salesforce Integration Demo

Now that we have seen the demo and flow, let's take a look at the configuration steps required for each component of the flow along with the associated code.

Configuration Steps

  1. Create a Twitter app - the connected app that will generate client_id and client_secret for integration
  2. Create an Apex REST Service - to process the tweet information that reaches Salesforce via webhook and create a Case record. Click here to understand why a webhook is required and how is different from achieving it through simple API.
  3. Register the above service to a webhook - to receive the tweet information in Salesforce when a user mentions your organizations twitter handler
  4. Setup Einstein Language API - to analyze the sentiment of tweet and set the priority of case based on the results.
  5. Create a case comment trigger - to send a reply to the user when a service agent adds/updates comment on the Case.

Now follow the below detailed steps to complete each of the above configuration steps for the integration.

Create a Twitter app

  • Create a Twitter app with an approved developer account from the developer portal. To apply for a developer account, click here.
  • Enable Read, Write and Access direct messages on the permissions tab of your app page.
  • On the Keys and Access Tokens tab, take note of your app's Consumer Key (API Key) and Consumer Token (API Secret).
  • On the same tab, generate your app's Access Token and Access Token Secret. You will need these Access Tokens to register your webhook URL, which is where Twitter will send tweet information.
  • Take note of your app's numeric ID, as seen in the Apps page of the developer portal. When you apply for Account Activity API access, you'll need this app ID.

Once we have a developer account, then we are ready to set up our access to the Premium Account Activity API. We will need below steps for the same

  • Login and navigate to the Dev Environments page.
  • Click Set up dev environment, name your environment, and specify a Twitter app ID for the environment. The environment name you chose will replace the :env_name token in our example premium endpoint URLs. For example, if we use the Development environment name, our URL pattern would be

https://api.twitter.com/1.1/account_activity/all/Development/webhooks

Once you have received Account Activity API access, you need to develop, deploy and host a web app that will receive tweet information via webhook events. In our case the web app is the Apex REST service, the second configuration step which involves some complex coding for establishing an endpoint capable of CRC - Challenge - Response Checks. For more details on what a CRC is, how it works and why is it needed - check my previous article.

Create an Apex REST Service

This is the Apex REST Service registered as webhook

@RestResource(urlMapping='/SFTwitter')
global class TwitterSF {    
    public class myres{
        String response_token;
    }
    
    @HttpGet
    global static void getResponse()
    {
        RestRequest restReq = RestContext.request; 
        String crc_token = restReq.params.get('crc_token'); 
        RestContext.response.addHeader('Content-Type','application/json'); 
        
        system.debug('crc_token'+crc_token);
        String secretKey = String.valueOf('{Obtained API secret key}');//API secret key of App
        String key = 'key';
        String crctoken;
        
        Blob data = crypto.generateMac('HmacSHA256',
                                       Blob.valueOf(crc_token),Blob.valueOf(secretKey));
        String response = '{'+
            'response_token: sha256=' +EncodingUtil.base64Encode(data)+
            '}';
        myres r = new myres();
        r.response_token = 'sha256='+EncodingUtil.base64Encode(data);
        RestContext.response.responseBody = Blob.valueOf(JSON.serialize(r));
        
    }
    
    @HttpPost
    global static void getEvents()
    {
        RestRequest req = RestContext.request;
        Blob body = req.requestBody;
        String bodyString = body.toString();
        system.debug('bodyString'+bodyString);
        TwitterWebhookResponseWrapper.tweetResponseWrapper responseBodyString = (TwitterWebhookResponseWrapper.tweetResponseWrapper)JSON.deserialize(bodyString, TwitterWebhookResponseWrapper.tweetResponseWrapper.class);
        
        List<Case> cases =  new List<Case>();
        Case cas = new Case();
        for (Integer i = 0;i< responseBodyString.tweet_create_events.size();i++)
        {
            String sentimentAnalysisResponse =EinsteinAPI.findSentiment(responseBodyString.tweet_create_events[i].text);
            EinsteinCalloutWrapper.EinsteinCalloutResponseWrapper eioo = (EinsteinCalloutWrapper.EinsteinCalloutResponseWrapper)
                        JSON.deserialize(sentimentAnalysisResponse.replace('"object":', '"object_x":'), 
                                         EinsteinCalloutWrapper.EinsteinCalloutResponseWrapper.class);
            for(EinsteinCalloutWrapper.EinsteinCalloutProbablityWrapper prob:eioo.probabilities){
                system.debug(prob);
                cas.put(prob.label+'__c',prob.probability);
            }
            
            cas.Subject = responseBodyString.tweet_create_events[i].text;
            cas.Status = 'New';
            cas.Origin = 'Web';
            cas.Twitter_Screen_Name__c = responseBodyString.tweet_create_events[0].user.screen_name;
            cas.Twitter_User_id_str__c = responseBodyString.tweet_create_events[0].user.id_str;
            if(Math.max(cas.Positive__c , cas.Negative__c ) == cas.Positive__c &&  Math.max(cas.Positive__c , cas.Neutral__c ) == cas.Positive__c)
            {
                cas.Priority = 'Low';
            }
            if(Math.max(cas.Negative__c , cas.Positive__c ) == cas.Negative__c &&  Math.max(cas.Negative__c , cas.Neutral__c ) == cas.Negative__c)
            {
                cas.Priority = 'High';
            }
            if(Math.max(cas.Neutral__c , cas.Positive__c ) == cas.Neutral__c &&  Math.max(cas.Neutral__c , cas.Negative__c ) == cas.Neutral__c)
            {
                cas.Priority = 'Medium';
            }
            cases.add(cas);
        }
        Database.insert(cases, false);
        
    }
}

Register the above service to a webhook

In order to register our service to webhook, we need to make a POST request to

https://api.twitter.com/1.1/account_activity/all/:ENV_NAME/webhooks.json?url=<our apex REST service URL>

When we make this request twitter will issue a CRC request to our service. When a webhook is successfully registered, the response will include a webhook id as shown below. This webhook id is needed later when making some requests to the Account Activity API.

{
    "id": "1234567890",
    "url": "https:///services/apexrest/{Prefix of your Apex REST Service}",
    "valid": true,
    "createdat": "2016-06-02T23:54:02Z"
}

Below is sample cURL request for registration. You can try the same in clients like Postman, Insomnia etc.

$ curl --request POST 
 --url 'https://api.twitter.com/1.1/account_activity/all/:ENV_NAME/webhooks.json?url=<our apex REST service URL>' 
 --header 'authorization: OAuth oauth_consumer_key="CONSUMER_KEY", oauth_nonce="GENERATED", oauth_signature="GENERATED", oauth_signature_method="HMAC-SHA1", oauth_timestamp="GENERATED", oauth_token="ACCESS_TOKEN", oauth_version="1.0"'

If you happen to receive a status code other than 200 then the response code description in this link can help you identify what could have been wrong

Once the webhook is registered successfully, the events will start hitting the Apex REST service that we created and any business logic added to the POST method will be executed. In this demo we are creating a Case - the Subject set to the tweeted text, Priority set to High, Medium or Low based on the detected sentiment Negative, Neutral or Positive respectively by using the Einstein Language API. For more details refer the article on Tweet sentiment analysis using Salesforce Einstein Language

Along with the above fields two more custom fields are used for storing Twitter_Screen_Name and Twitter_User_id of the user who made a tweet that is used to send reply tweet when a case comment is added/updated as shown in the following class.

Wrapper class used for deserializing incoming webhook request

public with sharing class TwitterWebhookResponseWrapper {
    
    public class tweetResponseWrapper { 
        public String for_user_id;
        Public String user_has_blocked;
        public cls_tweet_create_events[] tweet_create_events;
        public cls_favorite_events[] favorite_events;
        public cls_user user;
        public cls_in_reply_to_user_id user_id;
        
    }
    
    /*tweet_create Events*/
    class cls_tweet_create_events {
        public String created_at;
        public String id;
        public String id_str;
        public String text;
        public String source;
        public boolean truncated;
        public cls_in_reply_to_status_id in_reply_to_status_id;
        public cls_in_reply_to_status_id_str in_reply_to_status_id_str;
        public String in_reply_to_user_id;
        public String in_reply_to_user_id_str;
        public String in_reply_to_screen_name;
        public cls_user user;
        //public String user;
        public cls_geo geo;
        public cls_coordinates coordinates;
        public cls_place place;
        public cls_contributors contributors;
        public boolean is_quote_status;
        public Integer quote_count;
        public Integer reply_count;
        public Integer retweet_count;
        public Integer favorite_count;
        public cls_entities entities;
        public boolean favorited;
        public boolean retweeted;
        public String filter_level;	
        public String lang;	
        public String timestamp_ms;	
    }
    class cls_in_reply_to_status_id {
    }
    class cls_in_reply_to_status_id_str {
    }
    public class cls_in_reply_to_user_id {
    }
    class cls_in_reply_to_user_id_str {
    }
    class cls_in_reply_to_screen_name {
    }
    public class cls_user {
        public String id;
        public String id_str;
        public String name;
        public String screen_name;
        public cls_location location;
        public cls_url url;
        public cls_description description;
        public String translator_type;
        public boolean protected1;
        public boolean verified;
        public Integer followers_count;
        public Integer friends_count;
        public Integer listed_count;
        public Integer favourites_count;
        public Integer statuses_count;
        public String created_at;
        public cls_utc_offset utc_offset;
        public cls_time_zone time_zone;
        public boolean geo_enabled;
        public cls_lang lang;
        public boolean contributors_enabled;
        public boolean is_translator;
        public String profile_background_color;
        public String profile_background_image_url;
        public String profile_background_image_url_https;
        public boolean profile_background_tile;
        public String profile_link_color;
        public String profile_sidebar_border_color;
        public String profile_sidebar_fill_color;
        public String profile_text_color;
        public boolean profile_use_background_image;
        public String profile_image_url;
        public String profile_image_url_https;
        public boolean default_profile;
        public boolean default_profile_image;
        public cls_following following;
        public cls_follow_request_sent follow_request_sent;
        public cls_notifications notifications;
    }
    class cls_location {
    }
    class cls_url {
    }
    class cls_description {
    }
    class cls_utc_offset {
    }
    class cls_time_zone {
    }
    class cls_lang {
    }
    class cls_following {
    }
    class cls_follow_request_sent {
    }
    class cls_notifications {
    }
    class cls_geo {
    }
    class cls_coordinates {
    }
    class cls_place {
    }
    class cls_contributors {
    }
    class cls_entities {
        public cls_hashtags[] hashtags;
        public cls_urls[] urls;
        public cls_user_mentions[] user_mentions;
        public cls_symbols[] symbols;
    }
    class cls_hashtags {
    }
    class cls_urls {
    }
    class cls_user_mentions {
    }
    class cls_symbols {
    }
    
    
    /*Favorite Events*/
    class cls_favorite_events {
        public String id;
        public String created_at;
        public String timestamp_ms;
        public cls_favorited_status favorited_status;
        public cls_user user;
    }
    class cls_favorited_status {
        public String created_at;
        public String id;
        public String id_str;
        public String text;
        public String source;
        public boolean truncated;
        public cls_in_reply_to_status_id in_reply_to_status_id;
        public cls_in_reply_to_status_id_str in_reply_to_status_id_str;
        public cls_in_reply_to_user_id in_reply_to_user_id;
        public cls_in_reply_to_user_id_str in_reply_to_user_id_str;
        public cls_in_reply_to_screen_name in_reply_to_screen_name;
        public cls_user user;
        public cls_geo geo;
        public cls_coordinates coordinates;
        public cls_place place;
        public cls_contributors contributors;
        public boolean is_quote_status;
        public Integer quote_count;
        public Integer reply_count;
        public Integer retweet_count;
        public Integer favorite_count;
        public cls_entities entities;
        public boolean favorited;
        public boolean retweeted;
        public String filter_level;
        public String lang;
    }
    
    
}

Create a case comment trigger

In this demo we created a trigger on CaseComment such that whenever a service agent adds a comment it will be send as a reply tweet to the twitter user who initiated the conversation. Below is the handler class for CaseComment trigger to send reply tweet

public class CaseCommentToTweetHelper {
    
    public static String generateOathNonce()
    {
        Blob blobKey = crypto.generateAesKey(256);
        String key = EncodingUtil.convertToHex(blobKey);
        String oathNonce = EncodingUtil.base64Encode(Blob.valueOf(key));
        oathNonce = oathNonce.replaceAll('[^a-zA-Z0-9]+',''+integer.valueOf(math.random() * 10));
        oathNonce = oathNonce.substring(0,42);
        return oathNonce;
    }
    public static long generateTimeStamp()
    {
        long timestamp = datetime.now().getTime(); 
        timestamp = timestamp / 1000;
        return timestamp;
    }
    @Future(callout = true)
    public static void giveReplytoTweet(Set<Id> newCaseCommentIds){
        
        List<Case> cas = new List<Case>();
        List<Id> parentIds = new List<Id>();
        List<CaseComment> caseComments = new List<CaseComment>();
        system.debug('newCaseCommentIds.size' + newCaseCommentIds.size());
        caseComments = [SELECT Id, ParentId, CommentBody FROM CaseComment WHERE Id IN :newCaseCommentIds];
        for(Integer i=0; i< caseComments.size();i++){
            parentIds.add(caseComments[i].ParentId);
        }
        cas = [SELECT Id,Subject,Status,Origin,Priority,Twitter_Screen_Name__c,Twitter_User_id_str__c
               FROM Case where Id IN : parentIds];
        for(Integer i=0; i< caseComments.size();i++){
            
            String oathNonce = generateOathNonce();
            Long timestamp = generateTimeStamp();
            String signature = '';
            String message = Encodingutil.urlEncode('@'+cas[i].Twitter_Screen_Name__c+' '+caseComments[i].CommentBody, 'UTF-8');
            system.debug('message 37'+message);
            String othSecret = Encodingutil.urlEncode('{Access_token_secret}', 'UTF-8');
            String consumSecret = Encodingutil.urlEncode('{API_secret_key}', 'UTF-8');
            String othToken = Encodingutil.urlEncode('{Access_token}', 'UTF-8');
            String othNonce = Encodingutil.urlEncode(oathNonce, 'UTF-8');
            String consumKey = Encodingutil.urlEncode('{API_key}', 'UTF-8');
            message = message.replaceAll('\\+','%20');
            system.debug('message 44'+message);
            String paramString = 'include_entities=true&'+
                'oauth_consumer_key='+consumKey+'&'+
                'oauth_nonce='+othNonce+'&'+
                'oauth_signature_method=HMAC-SHA1&'+
                'oauth_timestamp='+timestamp+'&'+
                'oauth_token='+othToken+'&'+
                'oauth_version=1.0&'+
                'status='+message;
            string baseString = 'POST&'+EncodingUtil.urlEncode('https://api.twitter.com/1.1/statuses/update.json', 'UTF-8')+'&'+ 
                EncodingUtil.urlEncode(paramString, 'UTF-8');
            system.debug('### baseString :'+baseString);
            string signString = consumSecret+'&'+othSecret;
            system.debug('### signString :'+signString);
            blob blobBaseString = Blob.ValueOf(baseString);
            blob blobSignString = Blob.ValueOf(signString);
            blob signBlob = crypto.generateMac('hmacSHA1',blobBaseString ,blobSignString );        
            signature =  EncodingUtil.base64Encode(signBlob);
            system.debug('### Sign :'+signature);
            
            if(String.isNotBlank(signature))
            {
                signature = Encodingutil.urlEncode(signature, 'UTF-8');
                HttpRequest req = new HttpRequest();
                req.setEndpoint('https://api.twitter.com/1.1/statuses/update.json?include_entities=true');
                req.setMethod('POST');
                string reqstring = 'OAuth oauth_consumer_key="'+consumKey+'",'+
                    'oauth_nonce="'+othNonce+'",'+
                    'oauth_signature="'+signature +'",'+
                    'oauth_signature_method="HMAC-SHA1",'+
                    'oauth_timestamp="'+timestamp+'",'+
                    'oauth_token="'+othToken+'",'+
                    'oauth_version="1.0"';
                system.debug('### req String :'+reqstring);
                req.setHeader('Authorization',reqstring);
                req.setHeader('Content-Type','application/x-www-form-urlencoded');
                req.setBody('status='+message);
                if(!Test.isRunningTest())
                {
                    Http http = new Http();
                    HTTPResponse res = http.send(req);
                    system.debug('response body: '+res.getBody());
                }
            }
            
        }
    }
}

Why webhook, why not a simple API?

The answer to this question should become obvious if we understand the difference in how each of this approach would make the tweet information reach Salesforce and what is the impact from network calls point of view. As is evident from the below image, the API approach would prove to be costly in terms of network call since it has to keep polling to check if any event did occur or not (in our case did anyone tweet to our twitter handler) Whereas, with webhook the event trigger automatically notifies the service in pub-sub fashion

Sample Client - Server Network interactions to receive event updates using API Polling
Sample Client - Server Network interactions to receive event updates using Webhook
You've successfully subscribed to inteygrate
Welcome back! You've successfully signed in.
Great! You've successfully signed up.
Success! Your account is fully activated, you now have access to all content.