Retrieve Voice transcripts programmatically.
Tags: salesforce, servicecloudvoice, scv, jwt
Published: 2022-04-28 05:04:52

## Usecase

You have a functioning service cloud voice product configured in your org and you would like to retrieve the transcripts programmatically to further use them with Einstein API (or) Providing Recommendations via NBA . (here , I am consuming this in a LWC component) 


###  Details

While there are many options on how to use the transcripts fetched, We will focus on the "fetching" part of transcriptions. i.e. We will focus on how to retrieve these transcriptions programmatically and what is needed for that. 
The retrieved data then can be consumed by LWC / Aura or Visualforce page as per your requirements.
 

### Pre-requisites 

- Service Cloud Voice enabled org. 
(Here I am using Service Cloud voice with Amazon Telephony model where                 Amazon connect comes bundled with SCV product)

- Rest API (Enabled by default) end point for Transcription
- Documentation for reference [ [ConnectAPI's ConversationEntries](https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/connect_resources_conversation_entries.htm)]
- Apex Programming (Basics,Making callouts, Understanding of a JWT token)


### Steps Outline

1. JWT Token
2. Callout to ConversationEntry endpoint by passing ConversationIdentifier (VendorCallKey)


### Let's get Started !

**Why JWT Token ?** 

Since we cannot query the Entries directly via SOQL, we are using REST API to retrieve those. To do this, we would need to authenticate ourselves to access the data. 
We have to use one of the auth flows to get access token for that. And, JWT is the opted approach. 
 
Wait, I am making a callout from Apex (Used by a Lightning Web Component) to the REST API endpoint of the same org. Do I still need to authenticate again? 
Answer is Yes, Also, We cannot use Lightning sessions to access REST APIs. You would see an INVALID_SESSION error. 

Now there is a Hack , I have tried creating a visualforce page and adding Api Global Variable to get the session Id like this

```
<apex:page>
##{!$Api.Session_ID}##
</apex:page>
``` 
and then, and then call getContent(thatvfpage) from Apex to get a page that contains an API-capable session ID, which we should be able to use (parsing out ##..##).
 
However, this doesn't seem to work. So, my next best option was to perform JWT authentication , retrieve the token and use it as this doesn't need user interaction to authenticate the user. 

> **Step-1    JWT Token**

**Step-1A : Create a Signed Certificate and Key**

The OAuth 2.0 JWTbearer authorization flow requires a digital certificate and the private key used to sign the certificate.
This is pretty straight forward and can be done following this doc [[Create a Private Key and Self-Signed Digital Certificate](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_key_and_cert.htm)]
--> We only need the certificate and key file 


**Step-1B : Upload this certificate in Files**

Go to App Launcher > Files > Upload files  > (upload the certificate generated). Assuming the name of file is Server


**Step-1C : Create a Connected App and upload the certificate create in Step-1B**

This is required as you would be using the certificate for authenticating via this connected app.  Provide proper scopes and call back URL can be anything (eg: http://localhost:1717/OauthRedirect) 


**Step-1D: Write an apex class to handle JWT authentication.**

[Refer the Documentation here](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&type=5) if you want to try this out.

If you are not aware of the entire logic, No worries ! - You can refer [**this git repo code**](https://github.com/kashyapkbandi/oauthflows/blob/master/JWTUtils.apxc)
which I have written for a visualforce page.  The Logic will be still the same.

**Quick Explanation of the code:**

- retrievePKContent -> Is responsible to fetch the content of the certificate we uploaded in Step-1B and stripping off the -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY-----  strings. 
- createHeader -> Just creates JWT header with algorithm name
- base64URLencode  -> This is to encoding blob input to base64
- generateJsonBody -> This generates JSON body for the claimsset.
- combineJWTHeader_Claimset ->Combines the header and claimsset.
- signPkey-> Signs the Private key and returns the assertion
- fetchToken-> basically fetches the token with all the above info passed as JWT assertion.
- doInit-> calls all of the methods and performs callout by calling fetchToken. 


**Step-1E: Test the apex code.**

You can try calling doInit() method from execute anonymous to check and verify if the token is being obtained(or not)
Once you are seeing the token properly, You can quickly use postman by making a simple call to any salesforce REST API endpoint of that org to cross check if that token's working.


**Step-2: . Callout to ConversationEntry endpoint by passing ConversationIdentifier (VendorCallKey).**


**Step-2A:  The LWC Component**

I am trying to fetch the conversations on a button click. So this is the UI part of my LWC. 

```<template>
    <article class="slds-card">
        <div class="slds-card__body slds-card__body_inner">                
            <lightning-button variant="brand-outline" label="Fetch Conversations" title="Fetch Conversations" onclick={onclickhandler} class="slds-m-left_x-small"></lightning-button>
        </div>
        <footer class="slds-card__footer">
                 <template for:each={conversationEntryList} for:item="conversationEntryObject">
                    <p key={conversationEntryObject.serverReceivedTimestamp}>{conversationEntryObject.sender.role} said ---> "{conversationEntryObject.messageText}"</p>
                </template>
         </footer>
        </article>
</template>
``` 

Exposing this to Record Page, home page  and App  page (this can be modified as per the requirement)

```
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>54.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Connect API Utility Component</masterLabel>
    <description>This is a demo component used to get conversation payload.</description>
    <targets>
        <target>lightning__RecordPage</target>
        <target>lightning__AppPage</target>
        <target>lightning__HomePage</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordPage">
            <objects>
                <object>VoiceCall</object>
                <object>Case</object>
            </objects>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
```

**Step-2B:  Javascript part of the LWC. **

We are fetching the VendorCallKey field value from the current VoiceCall record page

```
import { LightningElement,api,wire } from 'lwc';
import { getRecord, getFieldValue } from "lightning/uiRecordApi";
import CALLSTATUS_FIELD from "@salesforce/schema/VoiceCall.CallDisposition";
import VENDORCALLKEY_FIELD from "@salesforce/schema/VoiceCall.VendorCallKey";
import fetchConversations from "@salesforce/apex/JWTUtils.fetchConversations";
import MailingPostalCode from '@salesforce/schema/Contact.MailingPostalCode';

const fields = [CALLSTATUS_FIELD,VENDORCALLKEY_FIELD];

export default class EinsteinConversationSentiment extends LightningElement {
    
    isCallCompleted = false;   
    conversationEntriesResponse={};
    conversationEntryList=[];
    currentCE;
    conversationEntryObjectList=[{
        role:"NA",
        message:"NA",
        servertimestamp:"NA"
    }];
    error;
    conversationMap;

    @api
  recordId;

    @wire(getRecord, {
        recordId: "$recordId",
        fields
      })
      voicecall;



      onclickhandler(e)
      {
            console.log("Vendor Call Key for this record - "+this.voicecall.data.fields.VendorCallKey.value );
        fetchConversations({ vcKey: this.voicecall.data.fields.VendorCallKey.value })
            .then(result => {
                this.conversationEntriesResponse = JSON.parse(result);
                console.log(this.conversationEntriesResponse.conversationEntries);
                this.conversationEntryList = this.conversationEntriesResponse.conversationEntries;
            })
            .catch(error => {
                this.error = error;
            });
      }
    
}

```

**Step-2C:  Calling the Connect Conversation Entries Endpoint. **

Now, the Logic to fetch the entries. 
> **NOTE- I have added this in the same class where I am fetching JWT token. 
> Refer Step-1D git repo class.**

```
@AuraEnabled
    public static String fetchConversations(String cid){
        // this method is responsible to fetch the JWT token. 
// stores the access token in access_token variable
        JWTUtils.doInit();
// replace your mydomain here.
        String url = String url = 'https://<mydomain>.my.salesforce.com/services/data/v53.0/connect/conversation/'+cid+'/entries';

        try {
            // Instantiate a new http object
            Http h = new Http();
            // Instantiate a new HTTP request, specify the method (GET) as well as the endpoint
            HttpRequest req = new HttpRequest();
            req.setEndpoint(url);
            req.setMethod('GET');
            req.setHeader('Authorization','Bearer '+access_token);
            // Send the request, and return a response
            HttpResponse res = h.send(req);
            System.debug(res.getBody());
            return res.getBody();
        } catch (Exception e) {
            throw new AuraHandledException(e.getMessage());
        }
    }
``` 

You would receive the list of ConversationEntries as a response from the conversation entries endpoint.  The below object shows one of the ConversationEntry from the List returned.

In Step-2B you are parsing the response, and storing the list of conversationentries in "conversationEntryList" and if you compare this with Step-2A , you can see we are iterating over "conversationEntryList" 

```
this.conversationEntryList = this.conversationEntriesResponse.conversationEntries;
``` 
```
<template for:each={conversationEntryList} for:item="conversationEntryObject">
 <p key={conversationEntryObject.serverReceivedTimestamp}>{conversationEntryObject.sender.role} said ---> "{conversationEntryObject.messageText}"</p>
 </template>
```

Each conversation Entry is stored in "conversationEntryObject"  variable and that object consists of below information. 

```
{
    "clientDuration": 4695,
    "clientTimestamp": 1649938471000,
    "identifier": "f7e30ac2-6666-3333-oooo-0c0811wsfb304",
    "messageText": "12, okay?",
    "relatedRecords": [
        "0LQ5j000000PuK1"
    ],
    "sender": {
        "appType": "telephony_integration",
        "role": "Agent",
        "subject": "VIRTUAL_AGENT"
    },
    "serverReceivedTimestamp": 1649938501255
}
```

Now, (for this project) what we are looking here is for 2 things: 

-> Who said it ? (Is the conversation message from Agent Or End user?)-> "sender.role"
-> What did they say ?  (The actual Message) -->"messageText"

We are obtaining those in below way

{conversationEntryObject.sender.role}
{conversationEntryObject.messageText}


Once All of these pieces are in place, You should see something like this 

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1651120845145/kb3SYhEP6.png)


Now we are able to see the transcripts (the conversation of the voice call) obtained programmatically.