@carbon/ai-chat 0.1.1-alpha5 → 0.1.1-react17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.js +933 -55468
- package/dist/Carousel.js +1 -5705
- package/dist/Chat.js +1 -8339
- package/dist/GenesysMessengerServiceDesk.js +1 -579
- package/dist/HumanAgentServiceImpl.js +1 -1122
- package/dist/NiceDFOServiceDesk.js +2 -2097
- package/dist/PDFViewerContainer.js +2 -27439
- package/dist/SFServiceDesk.js +1 -1012
- package/dist/ServiceDeskImpl.js +1 -72
- package/dist/ZendeskServiceDesk.js +1 -649
- package/dist/_commonjsHelpers.js +1 -33
- package/dist/_node-resolve_empty.js +1 -25
- package/dist/agentActions.js +1 -187
- package/dist/aiChatEntry.js +1 -28
- package/dist/aiChatEntry2.js +4 -7022
- package/dist/anonymousUserIDStorage.js +2 -250
- package/dist/ar-dz.js +1 -55
- package/dist/ar-kw.js +1 -55
- package/dist/ar-ly.js +1 -55
- package/dist/ar-ma.js +1 -55
- package/dist/ar-sa.js +1 -55
- package/dist/ar-tn.js +1 -55
- package/dist/ar.js +1 -55
- package/dist/ar2.js +1 -470
- package/dist/cs.js +1 -55
- package/dist/cs2.js +1 -470
- package/dist/de-at.js +1 -55
- package/dist/de-ch.js +1 -55
- package/dist/de.js +1 -55
- package/dist/de2.js +1 -470
- package/dist/domUtils.js +2 -820
- package/dist/en-au.js +1 -55
- package/dist/en-ca.js +1 -55
- package/dist/en-gb.js +1 -55
- package/dist/en-ie.js +1 -55
- package/dist/en-il.js +1 -55
- package/dist/en-nz.js +1 -55
- package/dist/es-do.js +1 -55
- package/dist/es-us.js +1 -55
- package/dist/es.js +1 -55
- package/dist/es2.js +1 -470
- package/dist/export.js +1 -25
- package/dist/export.legacy.js +1 -25
- package/dist/fontUtils.js +1 -1036
- package/dist/fr-ca.js +1 -55
- package/dist/fr-ch.js +1 -55
- package/dist/fr.js +1 -55
- package/dist/fr2.js +1 -470
- package/dist/humanAgentUtils.js +1 -1393
- package/dist/it-ch.js +1 -55
- package/dist/it.js +1 -55
- package/dist/it2.js +1 -470
- package/dist/ja.js +1 -55
- package/dist/ja2.js +1 -470
- package/dist/jstz.min.js +1 -41
- package/dist/ko.js +1 -55
- package/dist/ko2.js +1 -470
- package/dist/messageUtils.js +1 -1338
- package/dist/mockServiceDesk.js +1 -851
- package/dist/moduleFederationPluginUtils.js +2 -5852
- package/dist/nl.js +1 -55
- package/dist/nl2.js +1 -470
- package/dist/pt-br.js +1 -55
- package/dist/pt-br2.js +1 -470
- package/dist/pt.js +1 -55
- package/dist/render.js +1 -88
- package/dist/web-components/cds-aichat-container/index.js +3 -3
- package/dist/web-components/cds-aichat-container/index.js.map +1 -1
- package/dist/web-components/cds-aichat-custom-element/index.js +2 -2
- package/dist/web-components/cds-aichat-custom-element/index.js.map +1 -1
- package/dist/zh-cn.js +1 -55
- package/dist/zh-tw.js +1 -55
- package/dist/zh-tw2.js +1 -470
- package/dist/zh.js +1 -470
- package/package.json +1 -1
package/dist/SFServiceDesk.js
CHANGED
|
@@ -1,1012 +1 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
(C) Copyright IBM Corp. 2017, 2024. All Rights Reserved.
|
|
5
|
-
|
|
6
|
-
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
|
7
|
-
in compliance with the License. You may obtain a copy of the License at
|
|
8
|
-
|
|
9
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
|
|
11
|
-
Unless required by applicable law or agreed to in writing, software distributed under the License
|
|
12
|
-
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
13
|
-
or implied. See the License for the specific language governing permissions and limitations under
|
|
14
|
-
the License.
|
|
15
|
-
|
|
16
|
-
@carbon/ai-chat 0.1.1-alpha5
|
|
17
|
-
|
|
18
|
-
Built: Oct 24 2024 10:59 am -04:00
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*/
|
|
23
|
-
import { ak as ErrorType, t as isConnectToAgent, o as asyncForEach, av as pushAll, j as createMessageResponseForText, X as AgentMessageType, f as createMessageRequestForText } from './messageUtils.js';
|
|
24
|
-
import { h as debugLog, l as cloneDeep, t as consoleWarn, j as isEnableDebugLog, by as mergeNoArrays, c as consoleError, s as safeFetchText, a5 as StatusCodes, n as sleep } from './aiChatEntry2.js';
|
|
25
|
-
import { aL as getSummaryMessages } from './humanAgentUtils.js';
|
|
26
|
-
import { S as ServiceDeskImpl } from './ServiceDeskImpl.js';
|
|
27
|
-
import '@lit/react';
|
|
28
|
-
import 'react';
|
|
29
|
-
import 'lit';
|
|
30
|
-
import 'lit/decorators.js';
|
|
31
|
-
import 'react-dom';
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
*
|
|
35
|
-
* IBM Confidential
|
|
36
|
-
*
|
|
37
|
-
* (C) Copyright IBM Corp. 2019, 2023
|
|
38
|
-
*
|
|
39
|
-
* The source code for this program is not published or otherwise
|
|
40
|
-
* divested of its trade secrets, irrespective of what has been
|
|
41
|
-
* deposited with the U. S. Copyright Office
|
|
42
|
-
*
|
|
43
|
-
* US Government Users Restricted Rights - Use, duplication or
|
|
44
|
-
* disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
|
|
45
|
-
*
|
|
46
|
-
*/
|
|
47
|
-
/* eslint-disable no-await-in-loop */
|
|
48
|
-
const PREFIX = '[SFServiceDesk]';
|
|
49
|
-
// Fields required for routing to the correct Salesforce queue.
|
|
50
|
-
const BUTTON_ID = 'buttonId';
|
|
51
|
-
const DOMAIN = 'sfDomain';
|
|
52
|
-
const ORGANIZATION_ID = 'organizationId';
|
|
53
|
-
// Retry parameters for reattempting calls to Salesforce.
|
|
54
|
-
const MAX_POLLING_RETRIES = 3;
|
|
55
|
-
const MAX_POST_RETRIES = 3;
|
|
56
|
-
const RETRY_DELAY = 100;
|
|
57
|
-
// Parameter needed to introduce delay between two Salesforce API calls
|
|
58
|
-
const DELAY_BETWEEN_SF_API_CALLS = 15;
|
|
59
|
-
// The version of Live Agent REST API.
|
|
60
|
-
const SF_API_VERSION = '47';
|
|
61
|
-
// Salesforce specific labels for information that needs to be sent on chat initiation
|
|
62
|
-
const SFChatTranscriptLabels = {
|
|
63
|
-
IP_ADDRESS: 'Visitor IP Address',
|
|
64
|
-
NETWORK: 'Network',
|
|
65
|
-
LOCATION: 'Location',
|
|
66
|
-
AGENT_APP_SESSION: 'x_watson_assistant_session',
|
|
67
|
-
AGENT_APP_TOKEN: 'x_watson_assistant_token',
|
|
68
|
-
AGENT_APP_KEY: 'x_watson_assistant_key',
|
|
69
|
-
};
|
|
70
|
-
// Salesforce automatically adds this suffix to the user defined custom field names
|
|
71
|
-
const SFCustomFieldNameSuffix = '__c';
|
|
72
|
-
// Salesforce specific transcript field names for the labels referenced above
|
|
73
|
-
const SFChatTranscriptFieldNames = {
|
|
74
|
-
IP_ADDRESS: 'IpAddress',
|
|
75
|
-
NETWORK: 'VisitorNetwork',
|
|
76
|
-
LOCATION: 'Location',
|
|
77
|
-
AGENT_APP_SESSION: `x_watson_assistant_session${SFCustomFieldNameSuffix}`,
|
|
78
|
-
AGENT_APP_TOKEN: `x_watson_assistant_token${SFCustomFieldNameSuffix}`,
|
|
79
|
-
AGENT_APP_KEY: `x_watson_assistant_key${SFCustomFieldNameSuffix}`,
|
|
80
|
-
};
|
|
81
|
-
// Salesforce specific entity names for entities that are created at the time of chat initiation by the user
|
|
82
|
-
var SFEntityName;
|
|
83
|
-
(function (SFEntityName) {
|
|
84
|
-
SFEntityName["CASE"] = "Case";
|
|
85
|
-
SFEntityName["CHAT_TRANSCRIPT"] = "LiveChatTranscript";
|
|
86
|
-
})(SFEntityName || (SFEntityName = {}));
|
|
87
|
-
// Salesforce specific entity types for entities that are created at the time of chat initiation by the user
|
|
88
|
-
var SFEntityType;
|
|
89
|
-
(function (SFEntityType) {
|
|
90
|
-
SFEntityType["CASE_ID"] = "CaseId";
|
|
91
|
-
})(SFEntityType || (SFEntityType = {}));
|
|
92
|
-
class SFServiceDesk extends ServiceDeskImpl {
|
|
93
|
-
constructor(callback, serviceDeskConfig, regionHostname, serviceManager) {
|
|
94
|
-
super(callback, serviceDeskConfig, serviceManager);
|
|
95
|
-
/**
|
|
96
|
-
* The set of routing infos to use when attempting to connect the user to an agent. This array is built when agent
|
|
97
|
-
* availability is checked. Each info is checked one by one and if an info is found to have no available agents,
|
|
98
|
-
* it is removed from the queue. The first info in this queue (if any) will indicate agents are available for that
|
|
99
|
-
* info which can then be used to start chat. If all of those agents decline the chat, that info will be removed
|
|
100
|
-
* from the queue and the next one can be checked.
|
|
101
|
-
*/
|
|
102
|
-
this.routingInfoQueue = [];
|
|
103
|
-
this.serviceManager = serviceManager;
|
|
104
|
-
const { subscription } = serviceDeskConfig;
|
|
105
|
-
if (!subscription?.data) {
|
|
106
|
-
this.callback.setErrorStatus({
|
|
107
|
-
type: ErrorType.CONNECTING,
|
|
108
|
-
logInfo: 'The integration needs to be subscribed first to the service desk',
|
|
109
|
-
});
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
if (!subscription.account?.id ||
|
|
113
|
-
!subscription.data[BUTTON_ID] ||
|
|
114
|
-
!subscription.data[DOMAIN] ||
|
|
115
|
-
!subscription.data[ORGANIZATION_ID]) {
|
|
116
|
-
this.callback.setErrorStatus({
|
|
117
|
-
type: ErrorType.CONNECTING,
|
|
118
|
-
logInfo: 'Mandatory service desk subscription values missing',
|
|
119
|
-
});
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (!regionHostname) {
|
|
123
|
-
this.callback.setErrorStatus({
|
|
124
|
-
type: ErrorType.CONNECTING,
|
|
125
|
-
logInfo: 'Unable to determine URL to call Salesforce',
|
|
126
|
-
});
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
this.baseUrl = `https://${regionHostname}/chat/rest`;
|
|
130
|
-
this.callback.updateCapabilities({ allowMultipleFileUploads: false });
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Function builds a Salesforce specific JSON object for the pre-chat information that was provided by the chat
|
|
134
|
-
* visitor. This JSON object is sent to Salesforce when a user initiates a chat. For more details, please refer to
|
|
135
|
-
* https://developer.salesforce.com/docs/atlas.en-us.live_agent_rest.meta/live_agent_rest/live_agent_rest_request_bodies.htm
|
|
136
|
-
*/
|
|
137
|
-
buildPrechatDetails(label, value, fieldName, displayToAgent) {
|
|
138
|
-
const obj = {
|
|
139
|
-
label,
|
|
140
|
-
value,
|
|
141
|
-
entityFieldMaps: [
|
|
142
|
-
{
|
|
143
|
-
entityName: SFEntityName.CHAT_TRANSCRIPT,
|
|
144
|
-
fieldName: typeof fieldName === 'string' ? fieldName : fieldName[0],
|
|
145
|
-
},
|
|
146
|
-
],
|
|
147
|
-
transcriptFields: typeof fieldName === 'string' ? [fieldName] : fieldName,
|
|
148
|
-
displayToAgent,
|
|
149
|
-
};
|
|
150
|
-
return obj;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Function builds a Salesforce specific JSON object for the entities that are to be created when a chat visitor
|
|
154
|
-
* begins a chat. This JSON object is sent to Salesforce when a user initiates a chat. For more details, please refer
|
|
155
|
-
* to
|
|
156
|
-
* https://developer.salesforce.com/docs/atlas.en-us.live_agent_rest.meta/live_agent_rest/live_agent_rest_request_bodies.htm
|
|
157
|
-
*/
|
|
158
|
-
buildPrechatEntities(label, fieldName) {
|
|
159
|
-
return {
|
|
160
|
-
entityName: SFEntityName.CHAT_TRANSCRIPT,
|
|
161
|
-
showOnCreate: true,
|
|
162
|
-
linkToEntityName: SFEntityName.CASE,
|
|
163
|
-
linkToEntityField: SFEntityType.CASE_ID,
|
|
164
|
-
saveToTranscript: SFEntityType.CASE_ID,
|
|
165
|
-
entityFieldsMaps: [
|
|
166
|
-
{
|
|
167
|
-
fieldName,
|
|
168
|
-
label,
|
|
169
|
-
},
|
|
170
|
-
],
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Function gathers all the visitor details that are to be sent to Salesforce at the time of chat initiation
|
|
175
|
-
*
|
|
176
|
-
* @param sessionId Session ID for the current session
|
|
177
|
-
* @param authJWT Auth JWT is needed for verification purposes
|
|
178
|
-
* @param sessionHistoryKey Config for agent app based on web chat
|
|
179
|
-
* @param clientIpAddress Client's public IP address
|
|
180
|
-
* @param location Client's location
|
|
181
|
-
* @param network Client's network provider
|
|
182
|
-
* @param preChat Custom Metadata coming from dialog. Note that the current structure for metadata uses Prechat interface.
|
|
183
|
-
* We need Record<string, string> for backward compatibility.
|
|
184
|
-
*/
|
|
185
|
-
getAllPrechatDetails(sessionId, authJWT, sessionHistoryKey, clientIpAddress, location, network, preChat) {
|
|
186
|
-
const allPrechatDetails = [];
|
|
187
|
-
const agentAppSessionDetails = this.buildPrechatDetails(SFChatTranscriptLabels.AGENT_APP_SESSION, sessionId, SFChatTranscriptFieldNames.AGENT_APP_SESSION, false);
|
|
188
|
-
allPrechatDetails.push(agentAppSessionDetails);
|
|
189
|
-
const agentAppTokenDetails = this.buildPrechatDetails(SFChatTranscriptLabels.AGENT_APP_TOKEN, authJWT, SFChatTranscriptFieldNames.AGENT_APP_TOKEN, false);
|
|
190
|
-
allPrechatDetails.push(agentAppTokenDetails);
|
|
191
|
-
const agentMetaData = this.buildPrechatDetails(SFChatTranscriptLabels.AGENT_APP_KEY, sessionHistoryKey, SFChatTranscriptFieldNames.AGENT_APP_KEY, false);
|
|
192
|
-
allPrechatDetails.push(agentMetaData);
|
|
193
|
-
const ipAddressDetails = this.buildPrechatDetails(SFChatTranscriptLabels.IP_ADDRESS, clientIpAddress || '', SFChatTranscriptFieldNames.IP_ADDRESS, false);
|
|
194
|
-
allPrechatDetails.push(ipAddressDetails);
|
|
195
|
-
const networkDetails = this.buildPrechatDetails(SFChatTranscriptLabels.NETWORK, network || '', SFChatTranscriptFieldNames.NETWORK, false);
|
|
196
|
-
allPrechatDetails.push(networkDetails);
|
|
197
|
-
const locationDetails = this.buildPrechatDetails(SFChatTranscriptLabels.LOCATION, location || '', SFChatTranscriptFieldNames.LOCATION, false);
|
|
198
|
-
allPrechatDetails.push(locationDetails);
|
|
199
|
-
if (preChat) {
|
|
200
|
-
// If pre_chat object in dialog consists of details object, then we use this object to build all the prechat details.
|
|
201
|
-
// If details object is not present, we assume that the pre_chat object is referring to the old structure and
|
|
202
|
-
// we use key/value pairs to build details
|
|
203
|
-
if (Array.isArray(preChat.details)) {
|
|
204
|
-
preChat?.details.forEach((preChatDetail) => {
|
|
205
|
-
allPrechatDetails.push(this.buildPrechatDetails(preChatDetail.label, preChatDetail.value || '', preChatDetail.transcriptFields || preChatDetail.label + SFCustomFieldNameSuffix, preChatDetail.displayToAgent || false));
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
// Backward compatibility
|
|
210
|
-
Object.entries(preChat).forEach(([key, value]) => allPrechatDetails.push(this.buildPrechatDetails(key, value, key + SFCustomFieldNameSuffix, false)));
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return allPrechatDetails;
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Function gathers all the entities that are to be created in Salesforce at the time of chat initiation
|
|
217
|
-
*
|
|
218
|
-
* @param preChat Custom Metadata coming from dialog. Note that the current structure for metadata uses Prechat interface. We need Record<string, string> for backward compatibility.
|
|
219
|
-
*/
|
|
220
|
-
getAllPrechatEntities(preChat) {
|
|
221
|
-
const allPrechatEntities = [];
|
|
222
|
-
const agentAppSessionEntity = this.buildPrechatEntities(SFChatTranscriptLabels.AGENT_APP_SESSION, SFChatTranscriptFieldNames.AGENT_APP_SESSION);
|
|
223
|
-
allPrechatEntities.push(agentAppSessionEntity);
|
|
224
|
-
const agentAppTokenEntity = this.buildPrechatEntities(SFChatTranscriptLabels.AGENT_APP_TOKEN, SFChatTranscriptFieldNames.AGENT_APP_TOKEN);
|
|
225
|
-
allPrechatEntities.push(agentAppTokenEntity);
|
|
226
|
-
const agentMetaDataEntity = this.buildPrechatEntities(SFChatTranscriptLabels.AGENT_APP_KEY, SFChatTranscriptFieldNames.AGENT_APP_KEY);
|
|
227
|
-
allPrechatEntities.push(agentMetaDataEntity);
|
|
228
|
-
const ipAddressEntity = this.buildPrechatEntities(SFChatTranscriptLabels.IP_ADDRESS, SFChatTranscriptFieldNames.IP_ADDRESS);
|
|
229
|
-
allPrechatEntities.push(ipAddressEntity);
|
|
230
|
-
const networkEntity = this.buildPrechatEntities(SFChatTranscriptLabels.NETWORK, SFChatTranscriptFieldNames.NETWORK);
|
|
231
|
-
allPrechatEntities.push(networkEntity);
|
|
232
|
-
const locationEntity = this.buildPrechatEntities(SFChatTranscriptLabels.LOCATION, SFChatTranscriptFieldNames.LOCATION);
|
|
233
|
-
allPrechatEntities.push(locationEntity);
|
|
234
|
-
if (preChat) {
|
|
235
|
-
// If pre_chat object in dialog consists of entities object, then we use the object as is and send to Salesforce.
|
|
236
|
-
// If entities object is not present but we detect a details object, then we try to build entities using the details object.
|
|
237
|
-
// If neither entities and details are present, we assume that the pre_chat object is referring to the old structure and
|
|
238
|
-
// we use key/value pairs to build entities
|
|
239
|
-
if (Array.isArray(preChat.entities)) {
|
|
240
|
-
preChat.entities.forEach((preChatEntity) => {
|
|
241
|
-
allPrechatEntities.push(preChatEntity);
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
else if (Array.isArray(preChat.details)) {
|
|
245
|
-
preChat.details.forEach((preChatDetail) => {
|
|
246
|
-
allPrechatEntities.push(this.buildPrechatEntities(preChatDetail.label, preChatDetail.transcriptFields?.length > 0
|
|
247
|
-
? preChatDetail.transcriptFields[0]
|
|
248
|
-
: preChatDetail.label + SFCustomFieldNameSuffix));
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
// Backward compatibility
|
|
253
|
-
Object.keys(preChat).forEach(key => allPrechatEntities.push(this.buildPrechatEntities(key, key + SFCustomFieldNameSuffix)));
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return allPrechatEntities;
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Helper function that initializes all the variables needed to start a chat
|
|
260
|
-
*/
|
|
261
|
-
async startChat(connectMessage, startChatOptions) {
|
|
262
|
-
debugLog(`${PREFIX} Called startChat`, connectMessage, startChatOptions);
|
|
263
|
-
// The following values are initialized/reset every time user starts a new chat.
|
|
264
|
-
this.connectMessage = connectMessage;
|
|
265
|
-
this.startChatOptions = startChatOptions;
|
|
266
|
-
this.hasAgentJoined = false;
|
|
267
|
-
this.updatePersistedState({ fileUploadRequest: null });
|
|
268
|
-
this.callback.updateCapabilities({ allowFileUploads: false });
|
|
269
|
-
// Rebuild the routing info queues so we can start over and check the whole list again in case the agent
|
|
270
|
-
// availability has changed.
|
|
271
|
-
const connectItem = connectMessage.output.generic.find(isConnectToAgent);
|
|
272
|
-
this.buildRoutingInfos(connectItem);
|
|
273
|
-
await this.startChatInternal();
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Builds the list of possible routing infos that should be checked to determine if an agent is available.
|
|
277
|
-
*/
|
|
278
|
-
buildRoutingInfos(connectItem) {
|
|
279
|
-
const { subscription } = this.config;
|
|
280
|
-
const additionalRoutingInfo = connectItem?.transfer_info?.target?.salesforce
|
|
281
|
-
?.additional_routing_info;
|
|
282
|
-
debugLog(`${PREFIX} Building routing infos`, connectItem, subscription);
|
|
283
|
-
let allRoutingInfo;
|
|
284
|
-
if (additionalRoutingInfo) {
|
|
285
|
-
// The connect item has specified all the routing info we need. This will override the default web chat
|
|
286
|
-
// configuration settings.
|
|
287
|
-
allRoutingInfo = cloneDeep(additionalRoutingInfo);
|
|
288
|
-
allRoutingInfo.forEach(routingInfo => {
|
|
289
|
-
// Fill in some default values if the routing info is not complete.
|
|
290
|
-
routingInfo.org_id = routingInfo.org_id || subscription.data.organizationId;
|
|
291
|
-
routingInfo.deployment_id = routingInfo.deployment_id || subscription.account.id;
|
|
292
|
-
routingInfo.deployment_url = routingInfo.deployment_url || subscription.data.sfDomain;
|
|
293
|
-
// Backwards compatibility for the deprecated property.
|
|
294
|
-
if (routingInfo.button_ids) {
|
|
295
|
-
routingInfo.button_overrides = routingInfo.button_ids;
|
|
296
|
-
consoleWarn(`${PREFIX} The additionalRoutingInfo.button_ids property is deprecated. Use button_overrides instead.`);
|
|
297
|
-
}
|
|
298
|
-
// The button ID can either be provided in the routing info, it can be overridden by the connect to agent item
|
|
299
|
-
// (a field available in the tooling UI), or it can come from the web chat live agent settings.
|
|
300
|
-
routingInfo.button_id =
|
|
301
|
-
routingInfo.button_id ||
|
|
302
|
-
connectItem.transfer_info?.target?.salesforce?.button_id ||
|
|
303
|
-
subscription.data.buttonId;
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
// Create a single routing info object.
|
|
308
|
-
allRoutingInfo = [
|
|
309
|
-
{
|
|
310
|
-
org_id: subscription.data.organizationId,
|
|
311
|
-
deployment_id: subscription.account.id,
|
|
312
|
-
deployment_url: subscription.data.sfDomain,
|
|
313
|
-
button_id: connectItem?.transfer_info?.target?.salesforce?.button_id || subscription.data.buttonId,
|
|
314
|
-
},
|
|
315
|
-
];
|
|
316
|
-
}
|
|
317
|
-
this.routingInfoQueue = allRoutingInfo;
|
|
318
|
-
if (isEnableDebugLog()) {
|
|
319
|
-
debugLog(`${PREFIX} Built routing infos`, cloneDeep(allRoutingInfo));
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Sets up a connection to Salesforce. This initiates the connection, gets the user into the queue for an agent, and
|
|
324
|
-
* polls for actions from an agent if an agent accepts the chat. It is the main function for returning data in the
|
|
325
|
-
* Salesforce to browser direction.
|
|
326
|
-
*/
|
|
327
|
-
async startChatInternal() {
|
|
328
|
-
debugLog(`${PREFIX} Starting chat`, this.startChatOptions);
|
|
329
|
-
// Check the routing queue to determine which currently has any agents available. This also has the effect of
|
|
330
|
-
// rechecking the info at the top of the queue to determine if agents that were previously available have since
|
|
331
|
-
// become unavailable.
|
|
332
|
-
await this.checkRoutingInfoQueue();
|
|
333
|
-
// Attempt to start a chat using the first routing info (the one previously determined to be available).
|
|
334
|
-
const routingInfo = this.routingInfoQueue[0];
|
|
335
|
-
if (!routingInfo) {
|
|
336
|
-
this.callback.setErrorStatus({
|
|
337
|
-
type: ErrorType.CONNECTING,
|
|
338
|
-
messageToUser: this.getIntlText('errors_noAgentsAvailable'),
|
|
339
|
-
});
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
// First message we send to SF always starts with 1, start it at 0 as we always increment before sending.
|
|
343
|
-
this.updatePersistedState({ toAgentSequence: 0 });
|
|
344
|
-
// The reconnecting state starts as false.
|
|
345
|
-
this.isReconnecting = false;
|
|
346
|
-
this.connectItem = this.connectMessage.output.generic.find(isConnectToAgent);
|
|
347
|
-
// Extracts the browser information from context.integrations.chat.browser_info object.
|
|
348
|
-
const browserInfo = this.connectMessage.context?.integrations?.chat?.browser_info;
|
|
349
|
-
// Pre-chat data can be set inside the dialog. This data is later sent to salesforce. It can also be provided in
|
|
350
|
-
// the pre:agent:startChat event.
|
|
351
|
-
const preChatEvent = this.startChatOptions.preStartChatPayload?.preChat;
|
|
352
|
-
const preChatContext = this.connectMessage.context?.integrations?.salesforce?.pre_chat;
|
|
353
|
-
// The Record<string, string> piece is for undocumented legacy code.
|
|
354
|
-
const preChat = mergeNoArrays({}, preChatContext, preChatEvent);
|
|
355
|
-
if (!this.connectItem?.transfer_info?.additional_data?.jwt) {
|
|
356
|
-
// This isn't actually used anymore but if the back-end removes it, existing web chats will break because of
|
|
357
|
-
// this check. If we remove this check from new web chats, it'll make it easier to forget about the old ones
|
|
358
|
-
// so we'll just leave this here for now.
|
|
359
|
-
this.callback.setErrorStatus({
|
|
360
|
-
type: ErrorType.CONNECTING,
|
|
361
|
-
logInfo: 'Unable to connect due to missing security tokens',
|
|
362
|
-
});
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
const authJWT = this.connectItem.transfer_info.additional_data.jwt;
|
|
366
|
-
// Open a session with Salesforce, all calls will need this.
|
|
367
|
-
const sessionUrl = `${this.baseUrl}/System/SessionId`;
|
|
368
|
-
let sessionIdResponse;
|
|
369
|
-
try {
|
|
370
|
-
const headers = {
|
|
371
|
-
'X-LIVEAGENT-AFFINITY': null,
|
|
372
|
-
'X-LIVEAGENT-API-VERSION': SF_API_VERSION,
|
|
373
|
-
'X-WATSON-TARGET-DOMAIN': routingInfo.deployment_url,
|
|
374
|
-
};
|
|
375
|
-
sessionIdResponse = await this.sendToSalesforce('GET', sessionUrl, null, headers);
|
|
376
|
-
}
|
|
377
|
-
catch (error) {
|
|
378
|
-
this.callback.setErrorStatus({ type: ErrorType.CONNECTING, logInfo: error });
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
if (!sessionIdResponse || !sessionIdResponse.ok) {
|
|
382
|
-
this.callback.setErrorStatus({
|
|
383
|
-
type: ErrorType.CONNECTING,
|
|
384
|
-
logInfo: `Unable to start a live agent session: ${sessionIdResponse.status}`,
|
|
385
|
-
});
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
try {
|
|
389
|
-
const sessionData = await sessionIdResponse.json();
|
|
390
|
-
this.updatePersistedState({ sessionData });
|
|
391
|
-
debugLog(`${PREFIX} Starting with chat session data`, sessionData);
|
|
392
|
-
}
|
|
393
|
-
catch (error) {
|
|
394
|
-
this.callback.setErrorStatus({ type: ErrorType.CONNECTING, logInfo: error });
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
// Programmatically mimic an end user requesting an agent.
|
|
398
|
-
const buttonPressUrl = `${this.baseUrl}/Chasitor/ChasitorInit`;
|
|
399
|
-
// Gets all the visitor details that we need to send over to salesforce
|
|
400
|
-
// Currently, we are sending session id, auth JWT, client's IP address, location and network to Salesforce
|
|
401
|
-
// Please note that prechatDetails and prechatEntities go hand-in-hand i.e. if you decide to add additional prechatDetails (or update getAllPrechatDetails),
|
|
402
|
-
// then you must add corresponding preChatEntities (or update getAllPrechatEntities) as well
|
|
403
|
-
// TODO : Note that last two parameters (location and network) are "undefined" for the timebeing.
|
|
404
|
-
// We need to update these two parameters with correct location and network information.
|
|
405
|
-
const prechatDetails = this.getAllPrechatDetails(this.state.sessionID, authJWT, this.startChatOptions.agentAppInfo.sessionHistoryKey, browserInfo?.client_ip_address, undefined, undefined, preChat);
|
|
406
|
-
// Gets all the entities that are to be created in Salesforce at the time of chat intiation
|
|
407
|
-
// Please note that prechatDetails and prechatEntities go hand-in-hand i.e. if you decide to add additional prechatDetails (or update getAllPrechatDetails),
|
|
408
|
-
// then you must add corresponding preChatEntities (or update getAllPrechatEntities) as well
|
|
409
|
-
const prechatEntities = this.getAllPrechatEntities(preChat);
|
|
410
|
-
const body = JSON.stringify({
|
|
411
|
-
organizationId: routingInfo.org_id,
|
|
412
|
-
deploymentId: routingInfo.deployment_id,
|
|
413
|
-
buttonId: routingInfo.button_id,
|
|
414
|
-
buttonOverrides: routingInfo.button_overrides,
|
|
415
|
-
userAgent: navigator?.userAgent,
|
|
416
|
-
language: this.state.locale ? this.state.locale : navigator?.language,
|
|
417
|
-
screenResolution: `${window?.screen?.height}X${window?.screen?.width}`,
|
|
418
|
-
prechatDetails,
|
|
419
|
-
receiveQueueUpdates: true,
|
|
420
|
-
isPost: true,
|
|
421
|
-
prechatEntities,
|
|
422
|
-
});
|
|
423
|
-
let chasitorResponse;
|
|
424
|
-
try {
|
|
425
|
-
chasitorResponse = await this.sendToSalesforce('POST', buttonPressUrl, body, null);
|
|
426
|
-
}
|
|
427
|
-
catch (error) {
|
|
428
|
-
this.callback.setErrorStatus({ type: ErrorType.CONNECTING, logInfo: error });
|
|
429
|
-
}
|
|
430
|
-
if (!chasitorResponse || !chasitorResponse.ok) {
|
|
431
|
-
this.callback.setErrorStatus({
|
|
432
|
-
type: ErrorType.CONNECTING,
|
|
433
|
-
logInfo: `Unable to contact an agent: ${chasitorResponse.status}`,
|
|
434
|
-
});
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
this.updatePersistedState({ fromAgentSequence: -1 });
|
|
438
|
-
this.startPolling();
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Handles a ChatEstablished message. This occurs when an agent joins and the chat is ready for messages.
|
|
442
|
-
*/
|
|
443
|
-
async handleChatEstablished(chatEstablished) {
|
|
444
|
-
const newAgent = {
|
|
445
|
-
nickname: chatEstablished.name,
|
|
446
|
-
id: chatEstablished.userId,
|
|
447
|
-
};
|
|
448
|
-
this.callback.agentJoined(newAgent);
|
|
449
|
-
this.hasAgentJoined = true;
|
|
450
|
-
// Send the initial summary messages to the agent.
|
|
451
|
-
const messages = getSummaryMessages(this.connectItem, 'Begin conversation');
|
|
452
|
-
await asyncForEach(messages, async (text) => {
|
|
453
|
-
// If calls to ChatMessage API are made too quickly, Salesforce tends to not process some calls on their end
|
|
454
|
-
// (even though they return a 200 for those calls). Introducing a small delay ensures that calls to ChatMessage
|
|
455
|
-
// API are processed correctly.
|
|
456
|
-
await sleep(DELAY_BETWEEN_SF_API_CALLS);
|
|
457
|
-
await this.sendMessageToAgent(createMessageRequestForText(text), '', {});
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
async endChat() {
|
|
461
|
-
// Stop polling as we don't want to keep doing it even if we fail to tell Salesforce the chat is over. We'll
|
|
462
|
-
// stop the current poller and clear this so we can get a new poller the next time we start polling.
|
|
463
|
-
if (this.currentPoller) {
|
|
464
|
-
this.currentPoller.stop = true;
|
|
465
|
-
this.currentPoller = null;
|
|
466
|
-
// We only want to tell Salesforce to end the chat if we are not in a reconnecting state, if we are, ignore this call
|
|
467
|
-
// as it isn't worth telling the user we can't tell the agent the chat is over. It will time out shortly after as
|
|
468
|
-
// the polling is off.
|
|
469
|
-
if (!this.isReconnecting) {
|
|
470
|
-
const endChatUrl = `${this.baseUrl}/Chasitor/ChatEnd`;
|
|
471
|
-
const body = JSON.stringify({
|
|
472
|
-
reason: 'client',
|
|
473
|
-
});
|
|
474
|
-
try {
|
|
475
|
-
const response = await this.sendToSalesforce('POST', endChatUrl, body, null);
|
|
476
|
-
if (!response || !response.ok) {
|
|
477
|
-
consoleError('Failed to end chat', response);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
catch (error) {
|
|
481
|
-
consoleError('Unable to close chat with Salesforce agent.', error);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
async sendMessageToAgent(message, _, additionalData) {
|
|
487
|
-
// If in 503 state we need to lock down sending messages to the agent. Indicate to the user that there is a problem.
|
|
488
|
-
if (this.isReconnecting) {
|
|
489
|
-
throw new Error('[SFServiceDesk] Message failed to send due to a reconnection in progress');
|
|
490
|
-
}
|
|
491
|
-
else {
|
|
492
|
-
if (message.input.text) {
|
|
493
|
-
const sendMessageUrl = `${this.baseUrl}/Chasitor/ChatMessage`;
|
|
494
|
-
const body = JSON.stringify({
|
|
495
|
-
text: message.input.text,
|
|
496
|
-
});
|
|
497
|
-
const response = await this.sendToSalesforce('POST', sendMessageUrl, body, null);
|
|
498
|
-
if (!response?.ok) {
|
|
499
|
-
throw new Error('[SFServiceDesk] Message failed to send');
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
if (additionalData.filesToUpload?.length) {
|
|
503
|
-
await this.doFileUpload(additionalData.filesToUpload[0]);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
async doFileUpload(fileUpload) {
|
|
508
|
-
const { fileUploadRequest } = this.persistedState();
|
|
509
|
-
if (!fileUploadRequest) {
|
|
510
|
-
this.callback.setFileUploadStatus(fileUpload.id, true);
|
|
511
|
-
}
|
|
512
|
-
else {
|
|
513
|
-
const formData = new FormData();
|
|
514
|
-
formData.append('file', fileUpload.file);
|
|
515
|
-
const requestMessage = fileUploadRequest;
|
|
516
|
-
const { sessionData } = this.persistedState();
|
|
517
|
-
const url = `${requestMessage.uploadServletUrl}?orgId=${this.routingInfoQueue[0].org_id}&chatKey=${sessionData.id}&fileToken=${requestMessage.fileToken}&encoding=UTF-8`;
|
|
518
|
-
const response = await this.serviceManager.fetch(url, {
|
|
519
|
-
method: 'POST',
|
|
520
|
-
body: formData,
|
|
521
|
-
});
|
|
522
|
-
debugLog(`${PREFIX} Got response in doFileUpload`, await safeFetchText(response));
|
|
523
|
-
this.callback.setFileUploadStatus(fileUpload.id, !response.ok);
|
|
524
|
-
}
|
|
525
|
-
this.callback.updateCapabilities({ allowFileUploads: false });
|
|
526
|
-
this.updatePersistedState({ fileUploadRequest: null });
|
|
527
|
-
}
|
|
528
|
-
userReadMessages() {
|
|
529
|
-
return Promise.resolve();
|
|
530
|
-
}
|
|
531
|
-
async userTyping(isTyping) {
|
|
532
|
-
// We only want to update Salesforce if we are not in a reconnecting state, if we are, ignore this call
|
|
533
|
-
// as it isn't worth telling the user we can't update the agent about his typing status.
|
|
534
|
-
if (!this.isReconnecting) {
|
|
535
|
-
let response;
|
|
536
|
-
let url;
|
|
537
|
-
if (isTyping) {
|
|
538
|
-
url = `${this.baseUrl}/Chasitor/ChasitorTyping`;
|
|
539
|
-
}
|
|
540
|
-
else {
|
|
541
|
-
url = `${this.baseUrl}/Chasitor/ChasitorNotTyping`;
|
|
542
|
-
}
|
|
543
|
-
try {
|
|
544
|
-
response = await this.sendToSalesforce('POST', url, '{}');
|
|
545
|
-
}
|
|
546
|
-
catch (error) {
|
|
547
|
-
consoleError(`Failed calling ${url}`, error);
|
|
548
|
-
}
|
|
549
|
-
if (!response?.ok) {
|
|
550
|
-
consoleError(`Failed calling ${url}, response code: ${response.status}`);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
/**
|
|
555
|
-
* This will determine if any agents are available to connect to the user. If the message contains
|
|
556
|
-
* "additional_routing_info", it will check each set of routing info to determine if any agent is available for
|
|
557
|
-
* that routing info. Each info will be checked in the order provided in the message.
|
|
558
|
-
*
|
|
559
|
-
* After this method is complete, the "routingInfoQueue" property will be populated with the routing infos that
|
|
560
|
-
* can later be used when a chat is actually started. The first info in the queue will always be info where an
|
|
561
|
-
* agent is available. Additional infos in the queue represent infos that were not checked. If a chat is started
|
|
562
|
-
* and all the available agents decline, the chat may attempt to start the chat with the subsequent infos.
|
|
563
|
-
*
|
|
564
|
-
* If no agents are available, "routingInfoQueue" will be left an empty array.
|
|
565
|
-
*/
|
|
566
|
-
async areAnyAgentsOnline(connectMessage) {
|
|
567
|
-
debugLog(`${PREFIX} Called areAnyAgentsOnline`, connectMessage);
|
|
568
|
-
const connectItem = connectMessage.output.generic.find(isConnectToAgent);
|
|
569
|
-
this.buildRoutingInfos(connectItem);
|
|
570
|
-
await this.checkRoutingInfoQueue();
|
|
571
|
-
return this.routingInfoQueue.length !== 0;
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Checks the current values in the routing info queue to determine if any agents are available for any of them.
|
|
575
|
-
* Any infos found to not be available will be removed from the queue. If no infos have any agents available, this
|
|
576
|
-
* queue will end up as an empty array.
|
|
577
|
-
*
|
|
578
|
-
* SF appears to follow the following rules when connecting a user to an agent.
|
|
579
|
-
*
|
|
580
|
-
* 1. The /Availability check works for both agentId and buttonId but not agentId_buttonId.
|
|
581
|
-
* 2. When you pass buttonOverrides to /ChasitorInit, SF will check the availability of each item in the array one
|
|
582
|
-
* by one. When it finds an item that has some availability, it will assign that item to the agent or button
|
|
583
|
-
* (queue).
|
|
584
|
-
* 3. Once an item is assigned, if that assignment then fails (because all the agents decline or go offline), the
|
|
585
|
-
* chat fails. SF does not go back to buttonOverrides and try the next item.
|
|
586
|
-
* 4. If a given item has multiple agents (like a button), SF will pass the item from one agent to another agent
|
|
587
|
-
* if the first agent declines. It will only fail the chat at this point if there is no one else to pass the item to.
|
|
588
|
-
*/
|
|
589
|
-
async checkRoutingInfoQueue() {
|
|
590
|
-
// Try each of the routing configurations until we find one that has something available.
|
|
591
|
-
while (this.routingInfoQueue.length) {
|
|
592
|
-
const anyAvailable = await this.callAgentAvailabilityAPI(this.routingInfoQueue[0]);
|
|
593
|
-
if (!anyAvailable) {
|
|
594
|
-
// Remove the first item from the queue and try again.
|
|
595
|
-
this.routingInfoQueue.splice(0, 1);
|
|
596
|
-
}
|
|
597
|
-
else {
|
|
598
|
-
// Found one, so return and leave it in the queue.
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
/**
|
|
604
|
-
* Makes a call to Salesforce's agent availability API to determine if the given routing info contains any button
|
|
605
|
-
* or agent that's available.
|
|
606
|
-
*
|
|
607
|
-
* @see https://developer.salesforce.com/docs/atlas.en-us.live_agent_rest.meta/live_agent_rest/live_agent_rest_Availability.htm
|
|
608
|
-
*/
|
|
609
|
-
async callAgentAvailabilityAPI(routingInfo) {
|
|
610
|
-
const { org_id, deployment_id, button_overrides, button_id } = routingInfo;
|
|
611
|
-
// The list of things we want to test for are all the button IDs in the routing info, plus all the button IDs in
|
|
612
|
-
// the button_overrides field.
|
|
613
|
-
const idsToCheck = [];
|
|
614
|
-
if (button_overrides) {
|
|
615
|
-
button_overrides.forEach(id => {
|
|
616
|
-
// Pull out the IDs to check. This may either be an agent ID or a button ID but it may also be a
|
|
617
|
-
// "agentID_buttonID" in which case we need both.
|
|
618
|
-
pushAll(idsToCheck, id.split('_'));
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
else if (button_id) {
|
|
622
|
-
idsToCheck.push(button_id);
|
|
623
|
-
}
|
|
624
|
-
const queryParams = `?org_id=${org_id}&deployment_id=${deployment_id}&Availability.ids=[${idsToCheck}]`;
|
|
625
|
-
const agentAvailabilityUrl = `${this.baseUrl}/Visitor/Availability${queryParams}`;
|
|
626
|
-
try {
|
|
627
|
-
const headers = {
|
|
628
|
-
'X-LIVEAGENT-API-VERSION': SF_API_VERSION,
|
|
629
|
-
'X-WATSON-TARGET-DOMAIN': routingInfo.deployment_url,
|
|
630
|
-
};
|
|
631
|
-
const response = await this.sendToSalesforce('GET', agentAvailabilityUrl, null, headers);
|
|
632
|
-
const responseJSON = await response.json();
|
|
633
|
-
// Look for a message of type "Availability" and then see if it has any results in it that indicate any of the
|
|
634
|
-
// thing we asked about are available.
|
|
635
|
-
const results = responseJSON?.messages?.find((message) => message.type === 'Availability')?.message?.results;
|
|
636
|
-
return Boolean(results?.find((availability) => availability.isAvailable));
|
|
637
|
-
}
|
|
638
|
-
catch (error) {
|
|
639
|
-
// In case of an error, we want to return false so that agent unavailability message is rendered.
|
|
640
|
-
consoleError('Error in callAgentAvailabilityAPI', error);
|
|
641
|
-
return false;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
/**
|
|
645
|
-
* This function is in charge of calling the /Messages endpoint, examining the returned type(s), and taking correct
|
|
646
|
-
* action based on those types (for example, sending text to the browser if the response contains a ChatMessage type).
|
|
647
|
-
*
|
|
648
|
-
* @param poller The object that is controlling this polling.
|
|
649
|
-
* @returns Returns the sequence number from the last call to use in subsequence /Messages acks.
|
|
650
|
-
*/
|
|
651
|
-
async getMessagesFromAgent(poller) {
|
|
652
|
-
const getUpdatesUrl = `${this.baseUrl}/System/Messages?ack=${this.persistedState().fromAgentSequence}`;
|
|
653
|
-
// Call the messages endpoint to get information about what the agent is doing, including possibly getting back a
|
|
654
|
-
// message for the user. This returns immediately if the agent is doing something, even just typing.
|
|
655
|
-
// In cases where data is returned a 200 and a JSON body is provided. If the agent is doing nothing, this call
|
|
656
|
-
// will hang, in a pending state, for up to 30 seconds waiting for data to appear. This timeout may be configurable
|
|
657
|
-
// in Salesforce but so far it appears not to be. After 30 seconds, a 204 will be returned with an empty body.
|
|
658
|
-
const response = await this.serviceManager.fetch(getUpdatesUrl, {
|
|
659
|
-
method: 'GET',
|
|
660
|
-
credentials: 'include',
|
|
661
|
-
headers: this.getHeaders(false),
|
|
662
|
-
});
|
|
663
|
-
debugLog(`${PREFIX} Got response in getMessagesFromAgent`, await safeFetchText(response));
|
|
664
|
-
if (response.status === 200) {
|
|
665
|
-
const parsedResponse = await response.json();
|
|
666
|
-
debugLog(`${PREFIX} Parsed response`, parsedResponse);
|
|
667
|
-
// Update sequence to use in future calls.
|
|
668
|
-
this.updatePersistedState({ fromAgentSequence: parsedResponse.sequence });
|
|
669
|
-
// If offset is provided, it is only provided for messages that have data, not most connection messages, store it
|
|
670
|
-
// in case we need to reconnect due to failures.
|
|
671
|
-
if (parsedResponse.offset) {
|
|
672
|
-
this.updatePersistedState({ lastOffset: parsedResponse.offset });
|
|
673
|
-
}
|
|
674
|
-
// Process all messages that came back in a call to /Messages. Multiple can be returned in each response.
|
|
675
|
-
// Full list of possible types are here: https://developer.salesforce.com/docs/atlas.en-us.live_agent_rest.meta/live_agent_rest/live_agent_rest_Messages_responses_overview.htm
|
|
676
|
-
let gotAgentNotTyping = false;
|
|
677
|
-
for (let index = 0; index < parsedResponse.messages.length; index++) {
|
|
678
|
-
const element = parsedResponse.messages[index];
|
|
679
|
-
switch (element.type) {
|
|
680
|
-
case 'AgentDisconnect': {
|
|
681
|
-
await this.callback.agentLeftChat();
|
|
682
|
-
break;
|
|
683
|
-
}
|
|
684
|
-
case 'AgentTyping': {
|
|
685
|
-
// If this response included both AgentTyping and AgentNotTyping, it might exemplify a bug in SF where it
|
|
686
|
-
// sends them in the wrong order (this in particular can happen during reconnect if the agent sent messages
|
|
687
|
-
// while the user was away). This can result in the typing indicator being stuck. So if we see both, ignore
|
|
688
|
-
// AgentTyping.
|
|
689
|
-
if (!gotAgentNotTyping) {
|
|
690
|
-
await this.callback.agentTyping(true);
|
|
691
|
-
}
|
|
692
|
-
break;
|
|
693
|
-
}
|
|
694
|
-
case 'AgentNotTyping': {
|
|
695
|
-
gotAgentNotTyping = true;
|
|
696
|
-
await this.callback.agentTyping(false);
|
|
697
|
-
break;
|
|
698
|
-
}
|
|
699
|
-
case 'ChasitorSessionData': {
|
|
700
|
-
// Not tested as we can't get Salesforce to send this, but this should attempt to resync state.
|
|
701
|
-
await this.resyncState();
|
|
702
|
-
break;
|
|
703
|
-
}
|
|
704
|
-
case 'ChatEnded': {
|
|
705
|
-
poller.stop = true;
|
|
706
|
-
await this.callback.agentEndedChat();
|
|
707
|
-
break;
|
|
708
|
-
}
|
|
709
|
-
case 'ChatEstablished': {
|
|
710
|
-
// This will send messages to the agent to start the chat. We don't want to wait for that process to finish
|
|
711
|
-
// so no await here.
|
|
712
|
-
this.handleChatEstablished(element.message).catch(error => {
|
|
713
|
-
consoleError('Error establishing chat', error);
|
|
714
|
-
});
|
|
715
|
-
break;
|
|
716
|
-
}
|
|
717
|
-
case 'ChatMessage': {
|
|
718
|
-
// Right now we only handle text from the agent. This seems to be the only type of data they can send based
|
|
719
|
-
// on our exploration of their chat console and the fact that the API calls the field data appears in text.
|
|
720
|
-
const chatMessageElement = element.message;
|
|
721
|
-
// As soon as the agent sends a message, make sure to clear the "isTyping" event for the agent.
|
|
722
|
-
await this.callback.agentTyping(false);
|
|
723
|
-
await this.callback.sendMessageToUser(chatMessageElement.text);
|
|
724
|
-
break;
|
|
725
|
-
}
|
|
726
|
-
case 'ChatRequestFail': {
|
|
727
|
-
// This can happen when trying to initiate a chat and it fails (maybe an agent isn't available) or
|
|
728
|
-
// during the chat (if the only agent goes offline).
|
|
729
|
-
const chatRequestFailElement = element.message;
|
|
730
|
-
poller.stop = true;
|
|
731
|
-
if (chatRequestFailElement.reason === 'Unavailable') {
|
|
732
|
-
if (!this.hasAgentJoined) {
|
|
733
|
-
// This occurs while waiting for an agent to join and all available agents have declined the call. To
|
|
734
|
-
// handle this, we remove the current routing info from the queue and start the process over using
|
|
735
|
-
// whatever routing infos may still be in the queue (if any).
|
|
736
|
-
this.routingInfoQueue.splice(0, 1);
|
|
737
|
-
this.startChatInternal().catch(error => consoleError('Error restarting chat', error));
|
|
738
|
-
}
|
|
739
|
-
else {
|
|
740
|
-
// If the user is chatting with an agent, however the agent suddenly goes offline,
|
|
741
|
-
// then we want to send an "agent ended the chat" event to the user. We don't want to
|
|
742
|
-
// throw an error in this case because Salesforce shows "Chat session ended by the agent"
|
|
743
|
-
// on their end when an agent goes offline so we'd want to emulate the same here.
|
|
744
|
-
await this.callback.agentEndedChat();
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
break;
|
|
748
|
-
}
|
|
749
|
-
case 'ChatRequestSuccess': {
|
|
750
|
-
// Indicates that a user has successfully queued for an agent, does not indicate an agent has accepted
|
|
751
|
-
// the chat, ChatEstablished does. Does indicate agents are online to theoretically answer.
|
|
752
|
-
// If we desire we can get wait time instead of queue position.
|
|
753
|
-
// Salesforce has queue routing that does not support position and will return 0 always so ignore that.
|
|
754
|
-
const chatRequestSuccessElement = element.message;
|
|
755
|
-
if (chatRequestSuccessElement.queuePosition !== 0) {
|
|
756
|
-
const availability = {
|
|
757
|
-
position_in_queue: chatRequestSuccessElement.queuePosition + 1,
|
|
758
|
-
};
|
|
759
|
-
await this.callback.updateAgentAvailability(availability);
|
|
760
|
-
}
|
|
761
|
-
break;
|
|
762
|
-
}
|
|
763
|
-
case 'ChatTransferred': {
|
|
764
|
-
const chatTransferredElement = element.message;
|
|
765
|
-
await this.callback.beginTransferToAnotherAgent();
|
|
766
|
-
const newAgent = {
|
|
767
|
-
nickname: chatTransferredElement.name,
|
|
768
|
-
id: chatTransferredElement.userId,
|
|
769
|
-
};
|
|
770
|
-
await this.callback.agentJoined(newAgent);
|
|
771
|
-
break;
|
|
772
|
-
}
|
|
773
|
-
case 'CustomEvent': {
|
|
774
|
-
// Not implemented but perhaps defining custom events would help some use cases.
|
|
775
|
-
break;
|
|
776
|
-
}
|
|
777
|
-
case 'NewVisitorBreadcrumb': {
|
|
778
|
-
// We don't plan to make use of this case right now as there isn't really value to telling the user the
|
|
779
|
-
// page they are on but we don't want it to fall into the default bucket and trigger an unexpected error.
|
|
780
|
-
break;
|
|
781
|
-
}
|
|
782
|
-
case 'QueueUpdate': {
|
|
783
|
-
// If receiveQueueUpdates is true in the ChasitorInit call then updates will be sent as the user moves
|
|
784
|
-
// through the queue.
|
|
785
|
-
const queueUpdateElement = element.message;
|
|
786
|
-
const availability = {
|
|
787
|
-
position_in_queue: queueUpdateElement.position + 1,
|
|
788
|
-
};
|
|
789
|
-
await this.callback.updateAgentAvailability(availability);
|
|
790
|
-
break;
|
|
791
|
-
}
|
|
792
|
-
case 'FileTransfer': {
|
|
793
|
-
const request = element.message;
|
|
794
|
-
if (request.type === 'Requested') {
|
|
795
|
-
this.callback.updateCapabilities({ allowFileUploads: true });
|
|
796
|
-
this.updatePersistedState({ fileUploadRequest: element.message });
|
|
797
|
-
const messageText = this.getIntlText('fileSharing_request');
|
|
798
|
-
const message = createMessageResponseForText(messageText);
|
|
799
|
-
message.output.generic[0].agent_message_type = AgentMessageType.SYSTEM;
|
|
800
|
-
await this.callback.sendMessageToUser(message);
|
|
801
|
-
}
|
|
802
|
-
else if (request.type === 'Canceled') {
|
|
803
|
-
this.callback.updateCapabilities({ allowFileUploads: false });
|
|
804
|
-
this.callback.updatePersistedState({ fileUploadRequest: null });
|
|
805
|
-
}
|
|
806
|
-
break;
|
|
807
|
-
}
|
|
808
|
-
default: {
|
|
809
|
-
consoleError(`unhandled Salesforce message: ${element.type}`);
|
|
810
|
-
break;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
else if (response.status === StatusCodes.SERVICE_UNAVAILABLE) {
|
|
816
|
-
await this.issueReconnect();
|
|
817
|
-
}
|
|
818
|
-
else if (!response.ok) {
|
|
819
|
-
throw new Error(`Error polling for messages: ${response.status}`);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
/**
|
|
823
|
-
* If any response from Salesforce is a 503 we need to call this to re-establish the connection. This
|
|
824
|
-
* is the first step in reacting to a 503.
|
|
825
|
-
* Step 1: After getting a 503 issue a call to issueReconnect (this function) to call the ReconnectSession endpoint.
|
|
826
|
-
* Step 2: The next call to /messages will return with a ChasitorSessionData type, we can optionally process that.
|
|
827
|
-
* Step 3: Call resyncState to call the ChasitorResyncState endpoint to inform Salesforce we are
|
|
828
|
-
* ready to start chatting again.
|
|
829
|
-
*/
|
|
830
|
-
async issueReconnect() {
|
|
831
|
-
// Untested, SF rejects this call with a 405 in testing, presumably because it never sent a 503.
|
|
832
|
-
this.isReconnecting = true;
|
|
833
|
-
// Make a ReconnectSession call
|
|
834
|
-
const { lastOffset } = this.persistedState();
|
|
835
|
-
const reconnectSessionUrl = `${this.baseUrl}/System/ReconnectSession?ReconnectSession.offset=${lastOffset}`;
|
|
836
|
-
const response = await this.serviceManager.fetch(reconnectSessionUrl, {
|
|
837
|
-
method: 'GET',
|
|
838
|
-
credentials: 'include',
|
|
839
|
-
headers: this.getHeaders(false),
|
|
840
|
-
});
|
|
841
|
-
debugLog(`${PREFIX} Got response in issueReconnect`, await safeFetchText(response));
|
|
842
|
-
// Check that the call is okay. After this call the next call to /Messages should return a ChasitorSessionData
|
|
843
|
-
// response type and we'll continue trying to reconnect there.
|
|
844
|
-
if (!response || !response.ok) {
|
|
845
|
-
throw new Error(`Error reconnecting after 503 Salesforce error: ${response.status}`);
|
|
846
|
-
}
|
|
847
|
-
const parsedResponse = await response.json();
|
|
848
|
-
if (!parsedResponse.messages || parsedResponse.messages.length < 1) {
|
|
849
|
-
throw new Error(`Unexpected response when trying to reconnect: ${parsedResponse}`);
|
|
850
|
-
}
|
|
851
|
-
// Update the affinity as we are switching to a new Salesforce server.
|
|
852
|
-
this.updatePersistedState({ sessionData: { affinityToken: parsedResponse.messages[0].message.affinityToken } });
|
|
853
|
-
// Untested, doc indicates to reset the sequence number of the next request but does not say if we start from
|
|
854
|
-
// scratch or if we go back in time a number or so.
|
|
855
|
-
this.updatePersistedState({ fromAgentSequence: -1 });
|
|
856
|
-
}
|
|
857
|
-
/**
|
|
858
|
-
* Makes a REST call to Salesforce
|
|
859
|
-
*
|
|
860
|
-
* @param type The kind of REST call to make, POST or GET
|
|
861
|
-
* @param url The URL to send the data to.
|
|
862
|
-
* @param body The stringified data to send.
|
|
863
|
-
* @param headers Optional set of headers. If not provided it will be constructed using the most common set of
|
|
864
|
-
* headers.
|
|
865
|
-
* @returns Returns the fetch response object from a call to the url.
|
|
866
|
-
*/
|
|
867
|
-
async sendToSalesforce(type, url, body, headers) {
|
|
868
|
-
let retry = 0;
|
|
869
|
-
let response;
|
|
870
|
-
do {
|
|
871
|
-
try {
|
|
872
|
-
/* eslint-disable-next-line no-await-in-loop */
|
|
873
|
-
response = await this.serviceManager.fetch(url, {
|
|
874
|
-
method: type,
|
|
875
|
-
credentials: 'include',
|
|
876
|
-
body,
|
|
877
|
-
headers: headers || this.getHeaders(true),
|
|
878
|
-
});
|
|
879
|
-
debugLog(`${PREFIX} Got response in sendToSalesforce`, await safeFetchText(response));
|
|
880
|
-
if (response.status === StatusCodes.CONFLICT) {
|
|
881
|
-
// There is an error in the sequence number. Salesforce will respond with something like:
|
|
882
|
-
// "Out of sync Ack. Expected 498. Actual 4"
|
|
883
|
-
// This is unfortunate as the expected is wrong, it is just an echo of the sequence number we sent. The actual is
|
|
884
|
-
// the last successful ack that Salesforce received. So retry with 1 plus the reported actual number.
|
|
885
|
-
/* eslint-disable-next-line no-await-in-loop */
|
|
886
|
-
const errorMessage = await response.text();
|
|
887
|
-
const toAgentSequence = Number(errorMessage.match(/\d+$/)[0]) + 1;
|
|
888
|
-
this.updatePersistedState({ toAgentSequence });
|
|
889
|
-
}
|
|
890
|
-
// Retry again if there are tries left and the status was not in the 200's.
|
|
891
|
-
if (!response.ok) {
|
|
892
|
-
retry++;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
catch (error) {
|
|
896
|
-
console.error('Error occurred sending message to Salesforce', error);
|
|
897
|
-
retry++;
|
|
898
|
-
if (retry === MAX_POST_RETRIES) {
|
|
899
|
-
// If all retries have failed throw an error and let the caller decide how they want to handle things. This
|
|
900
|
-
// method will not set any error conditions.
|
|
901
|
-
throw error;
|
|
902
|
-
}
|
|
903
|
-
else {
|
|
904
|
-
/* eslint-disable-next-line no-await-in-loop */
|
|
905
|
-
await sleep(RETRY_DELAY);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
} while (retry !== MAX_POST_RETRIES && !response.ok);
|
|
909
|
-
return response;
|
|
910
|
-
}
|
|
911
|
-
/**
|
|
912
|
-
* Indicates we have reacted to the ChasitorSessionData message response type and are ready to resume the chat. This
|
|
913
|
-
* is the last step in reacting to a 503.
|
|
914
|
-
* Step 1: After getting a 503 issue a call to issueReconnect to call the ReconnectSession endpoint.
|
|
915
|
-
* Step 2: The next call to /messages will return with a ChasitorSessionData type, we can optionally process that.
|
|
916
|
-
* Step 3: Call resyncState (this function) to call the ChasitorResyncState endpoint to inform Salesforce we are
|
|
917
|
-
* ready to start chatting again.
|
|
918
|
-
*
|
|
919
|
-
* @returns Nothing to return if the call is successful, otherwise an error is thrown to start a
|
|
920
|
-
* potential retry.
|
|
921
|
-
*/
|
|
922
|
-
async resyncState() {
|
|
923
|
-
const resyncUrl = `${this.baseUrl}/Chasitor/ChasitorResyncState`;
|
|
924
|
-
// Do not send sequence header for this call.
|
|
925
|
-
const response = await this.serviceManager.fetch(resyncUrl, {
|
|
926
|
-
method: 'POST',
|
|
927
|
-
credentials: 'include',
|
|
928
|
-
body: JSON.stringify({ organizationId: this.routingInfoQueue[0].org_id }),
|
|
929
|
-
headers: this.getHeaders(false),
|
|
930
|
-
});
|
|
931
|
-
debugLog(`${PREFIX} Got response in resyncState`, await safeFetchText(response));
|
|
932
|
-
if (!response || !response.ok) {
|
|
933
|
-
throw new Error(`Failed to resync state: ${response.status}`);
|
|
934
|
-
}
|
|
935
|
-
this.isReconnecting = false;
|
|
936
|
-
}
|
|
937
|
-
/**
|
|
938
|
-
* Poll for messages to get an agent response. Polling will continue until the chat is ended by either party,
|
|
939
|
-
* a chatRequestFailure occurs, or 3 consecutive fetch failures force it to stop.
|
|
940
|
-
* Polling is stopped when an event of some sort changes the isPolling flag (either side ends the chat, errors in the
|
|
941
|
-
* REST API occur that are not fixed by the retry mechanisms, etc.).
|
|
942
|
-
*/
|
|
943
|
-
async startPolling() {
|
|
944
|
-
// Create a new poller object that can be used to stop this instance of polling.
|
|
945
|
-
const poller = { stop: false };
|
|
946
|
-
this.currentPoller = poller;
|
|
947
|
-
let numFailures = 0;
|
|
948
|
-
do {
|
|
949
|
-
try {
|
|
950
|
-
// We have until clientPollTimeout returned by the sessionId call to make a call to /Messages or a timeout will
|
|
951
|
-
// occur and we'll have to reconnect. Right now we call /Messages immediately after we either get an agent response
|
|
952
|
-
// or the call returns with a 204, which happens after 30 seconds of no agent activity.
|
|
953
|
-
// Note that this call will continue to run after a chat has been ended until the long polling is terminated
|
|
954
|
-
// by Salesforce.
|
|
955
|
-
/* eslint-disable-next-line no-await-in-loop */
|
|
956
|
-
await this.getMessagesFromAgent(poller);
|
|
957
|
-
// Reset numFailures if we successfully got a response
|
|
958
|
-
numFailures = 0;
|
|
959
|
-
}
|
|
960
|
-
catch (error) {
|
|
961
|
-
if (++numFailures === MAX_POLLING_RETRIES) {
|
|
962
|
-
if (!poller.stop) {
|
|
963
|
-
this.callback.setErrorStatus({ type: ErrorType.DISCONNECTED, isDisconnected: true });
|
|
964
|
-
}
|
|
965
|
-
else {
|
|
966
|
-
this.callback.setErrorStatus({ type: ErrorType.CONNECTING });
|
|
967
|
-
}
|
|
968
|
-
poller.stop = true;
|
|
969
|
-
}
|
|
970
|
-
else {
|
|
971
|
-
/* eslint-disable-next-line no-await-in-loop */
|
|
972
|
-
await sleep(RETRY_DELAY);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
} while (!poller.stop);
|
|
976
|
-
}
|
|
977
|
-
/**
|
|
978
|
-
* Helper method to construct headers for a Salesforce. Most useful for POSTs as GETs sometimes require specific
|
|
979
|
-
* headers.
|
|
980
|
-
*
|
|
981
|
-
* @param includeSequence True to include the sequence header. This will also increment the sequence before making
|
|
982
|
-
* the call.
|
|
983
|
-
* @returns A JSON object that can be used as the headers to a fetch call.
|
|
984
|
-
*/
|
|
985
|
-
getHeaders(includeSequence) {
|
|
986
|
-
const { sessionData, toAgentSequence } = this.persistedState();
|
|
987
|
-
const headers = {
|
|
988
|
-
'X-LIVEAGENT-AFFINITY': `${sessionData.affinityToken}`,
|
|
989
|
-
'X-LIVEAGENT-API-VERSION': SF_API_VERSION,
|
|
990
|
-
'X-LIVEAGENT-SESSION-KEY': `${sessionData.key}`,
|
|
991
|
-
'X-WATSON-TARGET-DOMAIN': this.routingInfoQueue[0].deployment_url,
|
|
992
|
-
};
|
|
993
|
-
if (includeSequence) {
|
|
994
|
-
this.updatePersistedState({ toAgentSequence: toAgentSequence + 1 });
|
|
995
|
-
headers['X-LIVEAGENT-SEQUENCE'] = `${toAgentSequence + 1}`;
|
|
996
|
-
}
|
|
997
|
-
return headers;
|
|
998
|
-
}
|
|
999
|
-
async reconnect() {
|
|
1000
|
-
const persistedState = this.persistedState();
|
|
1001
|
-
if (persistedState.fromAgentSequence && persistedState.fromAgentSequence !== -1) {
|
|
1002
|
-
if (this.persistedState().fileUploadRequest) {
|
|
1003
|
-
this.callback.updateCapabilities({ allowFileUploads: true });
|
|
1004
|
-
}
|
|
1005
|
-
this.startPolling();
|
|
1006
|
-
return true;
|
|
1007
|
-
}
|
|
1008
|
-
return false;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
export { SFServiceDesk };
|
|
1
|
+
import{ak as t,t as e,o as s,av as a,j as i,X as n,f as o}from"./messageUtils.js";import{h as r,l,t as c,j as h,by as d,c as u,s as g,a5 as p,n as f}from"./aiChatEntry2.js";import{aL as b}from"./humanAgentUtils.js";import{S as E}from"./ServiceDeskImpl.js";import"@lit/react";import"react";import"lit";import"lit/decorators.js";import"react-dom";const S="[SFServiceDesk]",A="47",y="Visitor IP Address",T="Network",I="Location",_="x_watson_assistant_session",N="x_watson_assistant_token",m="x_watson_assistant_key",C="__c",w={IP_ADDRESS:"IpAddress",NETWORK:"VisitorNetwork",LOCATION:"Location",AGENT_APP_SESSION:`x_watson_assistant_session${C}`,AGENT_APP_TOKEN:`x_watson_assistant_token${C}`,AGENT_APP_KEY:`x_watson_assistant_key${C}`};var k,P;!function(t){t.CASE="Case",t.CHAT_TRANSCRIPT="LiveChatTranscript"}(k||(k={})),function(t){t.CASE_ID="CaseId"}(P||(P={}));class O extends E{constructor(e,s,a,i){super(e,s,i),this.routingInfoQueue=[],this.serviceManager=i;const{subscription:n}=s;n?.data?n.account?.id&&n.data.buttonId&&n.data.sfDomain&&n.data.organizationId?a?(this.baseUrl=`https://${a}/chat/rest`,this.callback.updateCapabilities({allowMultipleFileUploads:!1})):this.callback.setErrorStatus({type:t.CONNECTING,logInfo:"Unable to determine URL to call Salesforce"}):this.callback.setErrorStatus({type:t.CONNECTING,logInfo:"Mandatory service desk subscription values missing"}):this.callback.setErrorStatus({type:t.CONNECTING,logInfo:"The integration needs to be subscribed first to the service desk"})}buildPrechatDetails(t,e,s,a){return{label:t,value:e,entityFieldMaps:[{entityName:k.CHAT_TRANSCRIPT,fieldName:"string"==typeof s?s:s[0]}],transcriptFields:"string"==typeof s?[s]:s,displayToAgent:a}}buildPrechatEntities(t,e){return{entityName:k.CHAT_TRANSCRIPT,showOnCreate:!0,linkToEntityName:k.CASE,linkToEntityField:P.CASE_ID,saveToTranscript:P.CASE_ID,entityFieldsMaps:[{fieldName:e,label:t}]}}getAllPrechatDetails(t,e,s,a,i,n,o){const r=[],l=this.buildPrechatDetails(_,t,w.AGENT_APP_SESSION,!1);r.push(l);const c=this.buildPrechatDetails(N,e,w.AGENT_APP_TOKEN,!1);r.push(c);const h=this.buildPrechatDetails(m,s,w.AGENT_APP_KEY,!1);r.push(h);const d=this.buildPrechatDetails(y,a||"",w.IP_ADDRESS,!1);r.push(d);const u=this.buildPrechatDetails(T,n||"",w.NETWORK,!1);r.push(u);const g=this.buildPrechatDetails(I,i||"",w.LOCATION,!1);return r.push(g),o&&(Array.isArray(o.details)?o?.details.forEach((t=>{r.push(this.buildPrechatDetails(t.label,t.value||"",t.transcriptFields||t.label+C,t.displayToAgent||!1))})):Object.entries(o).forEach((([t,e])=>r.push(this.buildPrechatDetails(t,e,t+C,!1))))),r}getAllPrechatEntities(t){const e=[],s=this.buildPrechatEntities(_,w.AGENT_APP_SESSION);e.push(s);const a=this.buildPrechatEntities(N,w.AGENT_APP_TOKEN);e.push(a);const i=this.buildPrechatEntities(m,w.AGENT_APP_KEY);e.push(i);const n=this.buildPrechatEntities(y,w.IP_ADDRESS);e.push(n);const o=this.buildPrechatEntities(T,w.NETWORK);e.push(o);const r=this.buildPrechatEntities(I,w.LOCATION);return e.push(r),t&&(Array.isArray(t.entities)?t.entities.forEach((t=>{e.push(t)})):Array.isArray(t.details)?t.details.forEach((t=>{e.push(this.buildPrechatEntities(t.label,t.transcriptFields?.length>0?t.transcriptFields[0]:t.label+C))})):Object.keys(t).forEach((t=>e.push(this.buildPrechatEntities(t,t+C))))),e}async startChat(t,s){r(`${S} Called startChat`,t,s),this.connectMessage=t,this.startChatOptions=s,this.hasAgentJoined=!1,this.updatePersistedState({fileUploadRequest:null}),this.callback.updateCapabilities({allowFileUploads:!1});const a=t.output.generic.find(e);this.buildRoutingInfos(a),await this.startChatInternal()}buildRoutingInfos(t){const{subscription:e}=this.config,s=t?.transfer_info?.target?.salesforce?.additional_routing_info;let a;r(`${S} Building routing infos`,t,e),s?(a=l(s),a.forEach((s=>{s.org_id=s.org_id||e.data.organizationId,s.deployment_id=s.deployment_id||e.account.id,s.deployment_url=s.deployment_url||e.data.sfDomain,s.button_ids&&(s.button_overrides=s.button_ids,c(`${S} The additionalRoutingInfo.button_ids property is deprecated. Use button_overrides instead.`)),s.button_id=s.button_id||t.transfer_info?.target?.salesforce?.button_id||e.data.buttonId}))):a=[{org_id:e.data.organizationId,deployment_id:e.account.id,deployment_url:e.data.sfDomain,button_id:t?.transfer_info?.target?.salesforce?.button_id||e.data.buttonId}],this.routingInfoQueue=a,h()&&r(`${S} Built routing infos`,l(a))}async startChatInternal(){r(`${S} Starting chat`,this.startChatOptions),await this.checkRoutingInfoQueue();const s=this.routingInfoQueue[0];if(!s)return void this.callback.setErrorStatus({type:t.CONNECTING,messageToUser:this.getIntlText("errors_noAgentsAvailable")});this.updatePersistedState({toAgentSequence:0}),this.isReconnecting=!1,this.connectItem=this.connectMessage.output.generic.find(e);const a=this.connectMessage.context?.integrations?.chat?.browser_info,i=this.startChatOptions.preStartChatPayload?.preChat,n=this.connectMessage.context?.integrations?.salesforce?.pre_chat,o=d({},n,i);if(!this.connectItem?.transfer_info?.additional_data?.jwt)return void this.callback.setErrorStatus({type:t.CONNECTING,logInfo:"Unable to connect due to missing security tokens"});const l=this.connectItem.transfer_info.additional_data.jwt,c=`${this.baseUrl}/System/SessionId`;let h;try{const t={"X-LIVEAGENT-AFFINITY":null,"X-LIVEAGENT-API-VERSION":A,"X-WATSON-TARGET-DOMAIN":s.deployment_url};h=await this.sendToSalesforce("GET",c,null,t)}catch(e){return void this.callback.setErrorStatus({type:t.CONNECTING,logInfo:e})}if(!h||!h.ok)return void this.callback.setErrorStatus({type:t.CONNECTING,logInfo:`Unable to start a live agent session: ${h.status}`});try{const t=await h.json();this.updatePersistedState({sessionData:t}),r(`${S} Starting with chat session data`,t)}catch(e){return void this.callback.setErrorStatus({type:t.CONNECTING,logInfo:e})}const u=`${this.baseUrl}/Chasitor/ChasitorInit`,g=this.getAllPrechatDetails(this.state.sessionID,l,this.startChatOptions.agentAppInfo.sessionHistoryKey,a?.client_ip_address,void 0,void 0,o),p=this.getAllPrechatEntities(o),f=JSON.stringify({organizationId:s.org_id,deploymentId:s.deployment_id,buttonId:s.button_id,buttonOverrides:s.button_overrides,userAgent:navigator?.userAgent,language:this.state.locale?this.state.locale:navigator?.language,screenResolution:`${window?.screen?.height}X${window?.screen?.width}`,prechatDetails:g,receiveQueueUpdates:!0,isPost:!0,prechatEntities:p});let b;try{b=await this.sendToSalesforce("POST",u,f,null)}catch(e){this.callback.setErrorStatus({type:t.CONNECTING,logInfo:e})}b&&b.ok?(this.updatePersistedState({fromAgentSequence:-1}),this.startPolling()):this.callback.setErrorStatus({type:t.CONNECTING,logInfo:`Unable to contact an agent: ${b.status}`})}async handleChatEstablished(t){const e={nickname:t.name,id:t.userId};this.callback.agentJoined(e),this.hasAgentJoined=!0;const a=b(this.connectItem,"Begin conversation");await s(a,(async t=>{await f(15),await this.sendMessageToAgent(o(t),"",{})}))}async endChat(){if(this.currentPoller&&(this.currentPoller.stop=!0,this.currentPoller=null,!this.isReconnecting)){const t=`${this.baseUrl}/Chasitor/ChatEnd`,e=JSON.stringify({reason:"client"});try{const s=await this.sendToSalesforce("POST",t,e,null);s&&s.ok||u("Failed to end chat",s)}catch(t){u("Unable to close chat with Salesforce agent.",t)}}}async sendMessageToAgent(t,e,s){if(this.isReconnecting)throw new Error("[SFServiceDesk] Message failed to send due to a reconnection in progress");if(t.input.text){const e=`${this.baseUrl}/Chasitor/ChatMessage`,s=JSON.stringify({text:t.input.text}),a=await this.sendToSalesforce("POST",e,s,null);if(!a?.ok)throw new Error("[SFServiceDesk] Message failed to send")}s.filesToUpload?.length&&await this.doFileUpload(s.filesToUpload[0])}async doFileUpload(t){const{fileUploadRequest:e}=this.persistedState();if(e){const s=new FormData;s.append("file",t.file);const a=e,{sessionData:i}=this.persistedState(),n=`${a.uploadServletUrl}?orgId=${this.routingInfoQueue[0].org_id}&chatKey=${i.id}&fileToken=${a.fileToken}&encoding=UTF-8`,o=await this.serviceManager.fetch(n,{method:"POST",body:s});r(`${S} Got response in doFileUpload`,await g(o)),this.callback.setFileUploadStatus(t.id,!o.ok)}else this.callback.setFileUploadStatus(t.id,!0);this.callback.updateCapabilities({allowFileUploads:!1}),this.updatePersistedState({fileUploadRequest:null})}userReadMessages(){return Promise.resolve()}async userTyping(t){if(!this.isReconnecting){let e,s;s=t?`${this.baseUrl}/Chasitor/ChasitorTyping`:`${this.baseUrl}/Chasitor/ChasitorNotTyping`;try{e=await this.sendToSalesforce("POST",s,"{}")}catch(t){u(`Failed calling ${s}`,t)}e?.ok||u(`Failed calling ${s}, response code: ${e.status}`)}}async areAnyAgentsOnline(t){r(`${S} Called areAnyAgentsOnline`,t);const s=t.output.generic.find(e);return this.buildRoutingInfos(s),await this.checkRoutingInfoQueue(),0!==this.routingInfoQueue.length}async checkRoutingInfoQueue(){for(;this.routingInfoQueue.length;){if(await this.callAgentAvailabilityAPI(this.routingInfoQueue[0]))return;this.routingInfoQueue.splice(0,1)}}async callAgentAvailabilityAPI(t){const{org_id:e,deployment_id:s,button_overrides:i,button_id:n}=t,o=[];i?i.forEach((t=>{a(o,t.split("_"))})):n&&o.push(n);const r=`?org_id=${e}&deployment_id=${s}&Availability.ids=[${o}]`,l=`${this.baseUrl}/Visitor/Availability${r}`;try{const e={"X-LIVEAGENT-API-VERSION":A,"X-WATSON-TARGET-DOMAIN":t.deployment_url},s=await this.sendToSalesforce("GET",l,null,e),a=await s.json(),i=a?.messages?.find((t=>"Availability"===t.type))?.message?.results;return Boolean(i?.find((t=>t.isAvailable)))}catch(t){return u("Error in callAgentAvailabilityAPI",t),!1}}async getMessagesFromAgent(t){const e=`${this.baseUrl}/System/Messages?ack=${this.persistedState().fromAgentSequence}`,s=await this.serviceManager.fetch(e,{method:"GET",credentials:"include",headers:this.getHeaders(!1)});if(r(`${S} Got response in getMessagesFromAgent`,await g(s)),200===s.status){const e=await s.json();r(`${S} Parsed response`,e),this.updatePersistedState({fromAgentSequence:e.sequence}),e.offset&&this.updatePersistedState({lastOffset:e.offset});let a=!1;for(let s=0;s<e.messages.length;s++){const o=e.messages[s];switch(o.type){case"AgentDisconnect":await this.callback.agentLeftChat();break;case"AgentTyping":a||await this.callback.agentTyping(!0);break;case"AgentNotTyping":a=!0,await this.callback.agentTyping(!1);break;case"ChasitorSessionData":await this.resyncState();break;case"ChatEnded":t.stop=!0,await this.callback.agentEndedChat();break;case"ChatEstablished":this.handleChatEstablished(o.message).catch((t=>{u("Error establishing chat",t)}));break;case"ChatMessage":{const t=o.message;await this.callback.agentTyping(!1),await this.callback.sendMessageToUser(t.text);break}case"ChatRequestFail":{const e=o.message;t.stop=!0,"Unavailable"===e.reason&&(this.hasAgentJoined?await this.callback.agentEndedChat():(this.routingInfoQueue.splice(0,1),this.startChatInternal().catch((t=>u("Error restarting chat",t)))));break}case"ChatRequestSuccess":{const t=o.message;if(0!==t.queuePosition){const e={position_in_queue:t.queuePosition+1};await this.callback.updateAgentAvailability(e)}break}case"ChatTransferred":{const t=o.message;await this.callback.beginTransferToAnotherAgent();const e={nickname:t.name,id:t.userId};await this.callback.agentJoined(e);break}case"CustomEvent":case"NewVisitorBreadcrumb":break;case"QueueUpdate":{const t={position_in_queue:o.message.position+1};await this.callback.updateAgentAvailability(t);break}case"FileTransfer":{const t=o.message;if("Requested"===t.type){this.callback.updateCapabilities({allowFileUploads:!0}),this.updatePersistedState({fileUploadRequest:o.message});const t=this.getIntlText("fileSharing_request"),e=i(t);e.output.generic[0].agent_message_type=n.SYSTEM,await this.callback.sendMessageToUser(e)}else"Canceled"===t.type&&(this.callback.updateCapabilities({allowFileUploads:!1}),this.callback.updatePersistedState({fileUploadRequest:null}));break}default:u(`unhandled Salesforce message: ${o.type}`)}}}else if(s.status===p.SERVICE_UNAVAILABLE)await this.issueReconnect();else if(!s.ok)throw new Error(`Error polling for messages: ${s.status}`)}async issueReconnect(){this.isReconnecting=!0;const{lastOffset:t}=this.persistedState(),e=`${this.baseUrl}/System/ReconnectSession?ReconnectSession.offset=${t}`,s=await this.serviceManager.fetch(e,{method:"GET",credentials:"include",headers:this.getHeaders(!1)});if(r(`${S} Got response in issueReconnect`,await g(s)),!s||!s.ok)throw new Error(`Error reconnecting after 503 Salesforce error: ${s.status}`);const a=await s.json();if(!a.messages||a.messages.length<1)throw new Error(`Unexpected response when trying to reconnect: ${a}`);this.updatePersistedState({sessionData:{affinityToken:a.messages[0].message.affinityToken}}),this.updatePersistedState({fromAgentSequence:-1})}async sendToSalesforce(t,e,s,a){let i,n=0;do{try{if(i=await this.serviceManager.fetch(e,{method:t,credentials:"include",body:s,headers:a||this.getHeaders(!0)}),r(`${S} Got response in sendToSalesforce`,await g(i)),i.status===p.CONFLICT){const t=await i.text(),e=Number(t.match(/\d+$/)[0])+1;this.updatePersistedState({toAgentSequence:e})}i.ok||n++}catch(t){if(console.error("Error occurred sending message to Salesforce",t),n++,3===n)throw t;await f(100)}}while(3!==n&&!i.ok);return i}async resyncState(){const t=`${this.baseUrl}/Chasitor/ChasitorResyncState`,e=await this.serviceManager.fetch(t,{method:"POST",credentials:"include",body:JSON.stringify({organizationId:this.routingInfoQueue[0].org_id}),headers:this.getHeaders(!1)});if(r(`${S} Got response in resyncState`,await g(e)),!e||!e.ok)throw new Error(`Failed to resync state: ${e.status}`);this.isReconnecting=!1}async startPolling(){const e={stop:!1};this.currentPoller=e;let s=0;do{try{await this.getMessagesFromAgent(e),s=0}catch(a){3==++s?(e.stop?this.callback.setErrorStatus({type:t.CONNECTING}):this.callback.setErrorStatus({type:t.DISCONNECTED,isDisconnected:!0}),e.stop=!0):await f(100)}}while(!e.stop)}getHeaders(t){const{sessionData:e,toAgentSequence:s}=this.persistedState(),a={"X-LIVEAGENT-AFFINITY":`${e.affinityToken}`,"X-LIVEAGENT-API-VERSION":A,"X-LIVEAGENT-SESSION-KEY":`${e.key}`,"X-WATSON-TARGET-DOMAIN":this.routingInfoQueue[0].deployment_url};return t&&(this.updatePersistedState({toAgentSequence:s+1}),a["X-LIVEAGENT-SEQUENCE"]=`${s+1}`),a}async reconnect(){const t=this.persistedState();return!(!t.fromAgentSequence||-1===t.fromAgentSequence)&&(this.persistedState().fileUploadRequest&&this.callback.updateCapabilities({allowFileUploads:!0}),this.startPolling(),!0)}}export{O as SFServiceDesk};
|