@app-connect/core 1.6.4 → 1.6.9
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/handlers/admin.js +131 -0
- package/handlers/auth.js +7 -2
- package/handlers/log.js +28 -6
- package/index.js +54 -2
- package/lib/callLogComposer.js +110 -15
- package/lib/util.js +11 -1
- package/models/adminConfigModel.js +8 -0
- package/models/userModel.js +3 -0
- package/package.json +3 -3
- package/releaseNotes.json +68 -0
package/handlers/admin.js
CHANGED
|
@@ -55,8 +55,139 @@ async function updateServerLoggingSettings({ user, additionalFieldValues }) {
|
|
|
55
55
|
return {};
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
async function getUserMapping({ user, hashedRcAccountId, rcExtensionList }) {
|
|
59
|
+
const adminConfig = await getAdminSettings({ hashedRcAccountId });
|
|
60
|
+
const platformModule = adapterRegistry.getAdapter(user.platform);
|
|
61
|
+
if (platformModule.getUserList) {
|
|
62
|
+
const authType = platformModule.getAuthType();
|
|
63
|
+
let authHeader = '';
|
|
64
|
+
switch (authType) {
|
|
65
|
+
case 'oauth':
|
|
66
|
+
const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname })));
|
|
67
|
+
// eslint-disable-next-line no-param-reassign
|
|
68
|
+
user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
|
|
69
|
+
authHeader = `Bearer ${user.accessToken}`;
|
|
70
|
+
break;
|
|
71
|
+
case 'apiKey':
|
|
72
|
+
const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
|
|
73
|
+
authHeader = `Basic ${basicAuth}`;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
const crmUserList = await platformModule.getUserList({ user, authHeader });
|
|
77
|
+
const userMappingResult = [];
|
|
78
|
+
const newUserMappings = [];
|
|
79
|
+
for (const crmUser of crmUserList) {
|
|
80
|
+
const existingMapping = adminConfig?.userMappings?.find(u => u.crmUserId == crmUser.id);
|
|
81
|
+
let existingMappingRcExtensionIds = [];
|
|
82
|
+
// TEMP: backward compatibility for string value
|
|
83
|
+
if (existingMapping?.rcExtensionId) {
|
|
84
|
+
if (typeof (existingMapping.rcExtensionId) === 'string') {
|
|
85
|
+
existingMappingRcExtensionIds = [existingMapping.rcExtensionId];
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
existingMappingRcExtensionIds = existingMapping.rcExtensionId;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const rcExtension = rcExtensionList.filter(e => existingMappingRcExtensionIds.includes(e.id));
|
|
92
|
+
// Case: existing mapping
|
|
93
|
+
if (existingMapping) {
|
|
94
|
+
userMappingResult.push({
|
|
95
|
+
crmUser: {
|
|
96
|
+
id: crmUser.id,
|
|
97
|
+
name: crmUser.name ?? '',
|
|
98
|
+
email: crmUser.email ?? '',
|
|
99
|
+
},
|
|
100
|
+
rcUser: rcExtension.map(e => ({
|
|
101
|
+
extensionId: e.id,
|
|
102
|
+
name: e?.name || `${e.firstName} ${e.lastName}`,
|
|
103
|
+
extensionNumber: e?.extensionNumber ?? '',
|
|
104
|
+
email: e?.email ?? ''
|
|
105
|
+
}))
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// Case: new mapping
|
|
109
|
+
else {
|
|
110
|
+
const rcExtensionForNewMapping = rcExtensionList.find(u =>
|
|
111
|
+
u.email === crmUser.email ||
|
|
112
|
+
u.name === crmUser.name ||
|
|
113
|
+
(`${u.firstName} ${u.lastName}` === crmUser.name)
|
|
114
|
+
);
|
|
115
|
+
if (rcExtensionForNewMapping) {
|
|
116
|
+
userMappingResult.push({
|
|
117
|
+
crmUser: {
|
|
118
|
+
id: crmUser.id,
|
|
119
|
+
name: crmUser.name ?? '',
|
|
120
|
+
email: crmUser.email ?? '',
|
|
121
|
+
},
|
|
122
|
+
rcUser: [{
|
|
123
|
+
extensionId: rcExtensionForNewMapping.id,
|
|
124
|
+
name: rcExtensionForNewMapping.name || `${rcExtensionForNewMapping.firstName} ${rcExtensionForNewMapping.lastName}`,
|
|
125
|
+
extensionNumber: rcExtensionForNewMapping?.extensionNumber ?? '',
|
|
126
|
+
email: rcExtensionForNewMapping?.email ?? ''
|
|
127
|
+
}]
|
|
128
|
+
});
|
|
129
|
+
newUserMappings.push({
|
|
130
|
+
crmUserId: crmUser.id.toString(),
|
|
131
|
+
rcExtensionId: [rcExtensionForNewMapping.id.toString()]
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
userMappingResult.push({
|
|
136
|
+
crmUser: {
|
|
137
|
+
id: crmUser.id,
|
|
138
|
+
name: crmUser.name ?? '',
|
|
139
|
+
email: crmUser.email ?? '',
|
|
140
|
+
},
|
|
141
|
+
rcUser: []
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// One-time init
|
|
147
|
+
if (!adminConfig?.userMappings) {
|
|
148
|
+
const initialUserMappings = [];
|
|
149
|
+
for (const userMapping of userMappingResult) {
|
|
150
|
+
if (userMapping.rcUser?.extensionId) {
|
|
151
|
+
initialUserMappings.push({
|
|
152
|
+
crmUserId: userMapping.crmUser.id.toString(),
|
|
153
|
+
rcExtensionId: [userMapping.rcUser.extensionId.toString()]
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
await upsertAdminSettings({
|
|
158
|
+
hashedRcAccountId,
|
|
159
|
+
adminSettings: {
|
|
160
|
+
userMappings: initialUserMappings
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// Incremental update
|
|
165
|
+
if (newUserMappings.length > 0) {
|
|
166
|
+
// TEMP: convert string to array
|
|
167
|
+
if (adminConfig?.userMappings) {
|
|
168
|
+
adminConfig.userMappings = adminConfig.userMappings.map(u => ({
|
|
169
|
+
...u,
|
|
170
|
+
rcExtensionId: [u.rcExtensionId]
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
adminConfig.userMappings = [];
|
|
175
|
+
}
|
|
176
|
+
await upsertAdminSettings({
|
|
177
|
+
hashedRcAccountId,
|
|
178
|
+
adminSettings: {
|
|
179
|
+
userMappings: [...adminConfig.userMappings, ...newUserMappings]
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return userMappingResult;
|
|
184
|
+
}
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
|
|
58
188
|
exports.validateAdminRole = validateAdminRole;
|
|
59
189
|
exports.upsertAdminSettings = upsertAdminSettings;
|
|
60
190
|
exports.getAdminSettings = getAdminSettings;
|
|
61
191
|
exports.getServerLoggingSettings = getServerLoggingSettings;
|
|
62
192
|
exports.updateServerLoggingSettings = updateServerLoggingSettings;
|
|
193
|
+
exports.getUserMapping = getUserMapping;
|
package/handlers/auth.js
CHANGED
|
@@ -36,7 +36,8 @@ async function onOAuthCallback({ platform, hostname, tokenUrl, callbackUri, apiU
|
|
|
36
36
|
hostname: platformUserInfo?.overridingHostname ? platformUserInfo.overridingHostname : hostname,
|
|
37
37
|
accessToken,
|
|
38
38
|
refreshToken,
|
|
39
|
-
tokenExpiry: expires
|
|
39
|
+
tokenExpiry: expires,
|
|
40
|
+
rcAccountId: query.rcAccountId
|
|
40
41
|
});
|
|
41
42
|
if (platformModule.postSaveUserInfo) {
|
|
42
43
|
userInfo = await platformModule.postSaveUserInfo({ userInfo, oauthApp });
|
|
@@ -81,7 +82,7 @@ async function onApiKeyLogin({ platform, hostname, apiKey, additionalInfo }) {
|
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken, refreshToken, tokenExpiry }) {
|
|
85
|
+
async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken, refreshToken, tokenExpiry, rcAccountId }) {
|
|
85
86
|
const id = platformUserInfo.id;
|
|
86
87
|
const name = platformUserInfo.name;
|
|
87
88
|
const existingUser = await UserModel.findByPk(id);
|
|
@@ -97,6 +98,7 @@ async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken,
|
|
|
97
98
|
accessToken,
|
|
98
99
|
refreshToken,
|
|
99
100
|
tokenExpiry,
|
|
101
|
+
rcAccountId,
|
|
100
102
|
platformAdditionalInfo: {
|
|
101
103
|
...existingUser.platformAdditionalInfo, // keep existing platformAdditionalInfo
|
|
102
104
|
...platformAdditionalInfo,
|
|
@@ -119,6 +121,7 @@ async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken,
|
|
|
119
121
|
accessToken,
|
|
120
122
|
refreshToken,
|
|
121
123
|
tokenExpiry,
|
|
124
|
+
rcAccountId,
|
|
122
125
|
platformAdditionalInfo,
|
|
123
126
|
userSettings: userWithOldID.userSettings
|
|
124
127
|
});
|
|
@@ -134,6 +137,7 @@ async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken,
|
|
|
134
137
|
accessToken,
|
|
135
138
|
refreshToken,
|
|
136
139
|
tokenExpiry,
|
|
140
|
+
rcAccountId,
|
|
137
141
|
platformAdditionalInfo,
|
|
138
142
|
userSettings: {}
|
|
139
143
|
});
|
|
@@ -149,6 +153,7 @@ async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken,
|
|
|
149
153
|
accessToken,
|
|
150
154
|
refreshToken,
|
|
151
155
|
tokenExpiry,
|
|
156
|
+
rcAccountId,
|
|
152
157
|
platformAdditionalInfo,
|
|
153
158
|
userSettings: {}
|
|
154
159
|
});
|
package/handlers/log.js
CHANGED
|
@@ -9,8 +9,9 @@ const adapterRegistry = require('../adapter/registry');
|
|
|
9
9
|
const { LOG_DETAILS_FORMAT_TYPE } = require('../lib/constants');
|
|
10
10
|
const { NoteCache } = require('../models/dynamo/noteCacheSchema');
|
|
11
11
|
const moment = require('moment');
|
|
12
|
+
const { getMediaReaderLinkByPlatformMediaLink } = require('../lib/util');
|
|
12
13
|
|
|
13
|
-
async function createCallLog({ platform, userId, incomingData, isFromSSCL }) {
|
|
14
|
+
async function createCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
|
|
14
15
|
try {
|
|
15
16
|
const existingCallLog = await CallLogModel.findOne({
|
|
16
17
|
where: {
|
|
@@ -42,7 +43,7 @@ async function createCallLog({ platform, userId, incomingData, isFromSSCL }) {
|
|
|
42
43
|
const callLog = incomingData.logInfo;
|
|
43
44
|
const additionalSubmission = incomingData.additionalSubmission;
|
|
44
45
|
let note = incomingData.note;
|
|
45
|
-
if (isFromSSCL) {
|
|
46
|
+
if (process.env.USE_CACHE && isFromSSCL) {
|
|
46
47
|
const noteCache = await NoteCache.get({ sessionId: incomingData.logInfo.sessionId });
|
|
47
48
|
if (noteCache) {
|
|
48
49
|
note = noteCache.note;
|
|
@@ -113,6 +114,7 @@ async function createCallLog({ platform, userId, incomingData, isFromSSCL }) {
|
|
|
113
114
|
aiNote,
|
|
114
115
|
transcript,
|
|
115
116
|
composedLogDetails,
|
|
117
|
+
hashedAccountId,
|
|
116
118
|
isFromSSCL
|
|
117
119
|
});
|
|
118
120
|
if (logId) {
|
|
@@ -209,6 +211,11 @@ async function getCallLog({ userId, sessionIds, platform, requireDetails }) {
|
|
|
209
211
|
}
|
|
210
212
|
});
|
|
211
213
|
for (const sId of sessionIdsArray) {
|
|
214
|
+
if(sId == 0)
|
|
215
|
+
{
|
|
216
|
+
logs.push({ sessionId: sId, matched: false });
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
212
219
|
const callLog = callLogs.find(c => c.sessionId === sId);
|
|
213
220
|
if (!callLog) {
|
|
214
221
|
logs.push({ sessionId: sId, matched: false });
|
|
@@ -288,7 +295,7 @@ async function getCallLog({ userId, sessionIds, platform, requireDetails }) {
|
|
|
288
295
|
}
|
|
289
296
|
}
|
|
290
297
|
|
|
291
|
-
async function updateCallLog({ platform, userId, incomingData, isFromSSCL }) {
|
|
298
|
+
async function updateCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
|
|
292
299
|
try {
|
|
293
300
|
const existingCallLog = await CallLogModel.findOne({
|
|
294
301
|
where: {
|
|
@@ -347,7 +354,8 @@ async function updateCallLog({ platform, userId, incomingData, isFromSSCL }) {
|
|
|
347
354
|
result: incomingData.result,
|
|
348
355
|
direction: incomingData.direction,
|
|
349
356
|
from: incomingData.from,
|
|
350
|
-
to: incomingData.to
|
|
357
|
+
to: incomingData.to,
|
|
358
|
+
legs: incomingData.legs || [],
|
|
351
359
|
},
|
|
352
360
|
contactInfo: null, // Not needed for updates
|
|
353
361
|
user,
|
|
@@ -375,9 +383,11 @@ async function updateCallLog({ platform, userId, incomingData, isFromSSCL }) {
|
|
|
375
383
|
result: incomingData.result,
|
|
376
384
|
aiNote: incomingData.aiNote,
|
|
377
385
|
transcript: incomingData.transcript,
|
|
386
|
+
legs: incomingData.legs || [],
|
|
378
387
|
additionalSubmission: incomingData.additionalSubmission,
|
|
379
388
|
composedLogDetails,
|
|
380
389
|
existingCallLogDetails, // Pass the fetched details to avoid duplicate API calls
|
|
390
|
+
hashedAccountId,
|
|
381
391
|
isFromSSCL
|
|
382
392
|
});
|
|
383
393
|
return { successful: true, logId: existingCallLog.thirdPartyLogId, updatedNote, returnMessage, extraDataTracking };
|
|
@@ -515,6 +525,18 @@ async function createMessageLog({ platform, userId, incomingData }) {
|
|
|
515
525
|
faxDocLink = message.attachments.find(a => a.type === 'RenderedDocument').link;
|
|
516
526
|
faxDownloadLink = message.attachments.find(a => a.type === 'RenderedDocument').uri + `?access_token=${incomingData.logInfo.rcAccessToken}`
|
|
517
527
|
}
|
|
528
|
+
let imageLink = null;
|
|
529
|
+
let videoLink = null;
|
|
530
|
+
if (message.attachments && message.attachments.some(a => a.type === 'MmsAttachment')) {
|
|
531
|
+
const imageAttachment = message.attachments.find(a => a.type === 'MmsAttachment' && a.contentType.startsWith('image/'));
|
|
532
|
+
if (imageAttachment) {
|
|
533
|
+
imageLink = getMediaReaderLinkByPlatformMediaLink(imageAttachment?.uri);
|
|
534
|
+
}
|
|
535
|
+
const videoAttachment = message.attachments.find(a => a.type === 'MmsAttachment' && a.contentType.startsWith('video/'));
|
|
536
|
+
if (videoAttachment) {
|
|
537
|
+
videoLink = getMediaReaderLinkByPlatformMediaLink(videoAttachment?.uri);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
518
540
|
const existingSameDateMessageLog = await MessageLogModel.findOne({
|
|
519
541
|
where: {
|
|
520
542
|
conversationLogId: incomingData.logInfo.conversationLogId
|
|
@@ -522,12 +544,12 @@ async function createMessageLog({ platform, userId, incomingData }) {
|
|
|
522
544
|
});
|
|
523
545
|
let crmLogId = ''
|
|
524
546
|
if (existingSameDateMessageLog) {
|
|
525
|
-
const updateMessageResult = await platformModule.updateMessageLog({ user, contactInfo, existingMessageLog: existingSameDateMessageLog, message, authHeader, additionalSubmission });
|
|
547
|
+
const updateMessageResult = await platformModule.updateMessageLog({ user, contactInfo, existingMessageLog: existingSameDateMessageLog, message, authHeader, additionalSubmission, imageLink, videoLink });
|
|
526
548
|
crmLogId = existingSameDateMessageLog.thirdPartyLogId;
|
|
527
549
|
returnMessage = updateMessageResult?.returnMessage;
|
|
528
550
|
}
|
|
529
551
|
else {
|
|
530
|
-
const createMessageLogResult = await platformModule.createMessageLog({ user, contactInfo, authHeader, message, additionalSubmission, recordingLink, faxDocLink, faxDownloadLink });
|
|
552
|
+
const createMessageLogResult = await platformModule.createMessageLog({ user, contactInfo, authHeader, message, additionalSubmission, recordingLink, faxDocLink, faxDownloadLink, imageLink, videoLink });
|
|
531
553
|
crmLogId = createMessageLogResult.logId;
|
|
532
554
|
returnMessage = createMessageLogResult?.returnMessage;
|
|
533
555
|
extraDataTracking = createMessageLogResult.extraDataTracking;
|
package/index.js
CHANGED
|
@@ -331,6 +331,58 @@ function createCoreRouter() {
|
|
|
331
331
|
});
|
|
332
332
|
});
|
|
333
333
|
|
|
334
|
+
router.post('/admin/userMapping', async function (req, res) {
|
|
335
|
+
const requestStartTime = new Date().getTime();
|
|
336
|
+
let platformName = null;
|
|
337
|
+
let success = false;
|
|
338
|
+
const { hashedExtensionId, hashedAccountId, userAgent, ip, author, eventAddedVia } = getAnalyticsVariablesInReqHeaders({ headers: req.headers })
|
|
339
|
+
try {
|
|
340
|
+
const jwtToken = req.query.jwtToken;
|
|
341
|
+
if (jwtToken) {
|
|
342
|
+
const unAuthData = jwt.decodeJwt(jwtToken);
|
|
343
|
+
platformName = unAuthData?.platform ?? 'Unknown';
|
|
344
|
+
const user = await UserModel.findByPk(unAuthData?.id);
|
|
345
|
+
if (!user) {
|
|
346
|
+
res.status(400).send('User not found');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const { isValidated, rcAccountId } = await adminCore.validateAdminRole({ rcAccessToken: req.query.rcAccessToken });
|
|
350
|
+
const hashedRcAccountId = util.getHashValue(rcAccountId, process.env.HASH_KEY);
|
|
351
|
+
if (isValidated) {
|
|
352
|
+
const userMapping = await adminCore.getUserMapping({ user, hashedRcAccountId, rcExtensionList: req.body.rcExtensionList });
|
|
353
|
+
res.status(200).send(userMapping);
|
|
354
|
+
success = true;
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
res.status(401).send('Admin validation failed');
|
|
358
|
+
success = true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
res.status(400).send('Please go to Settings and authorize CRM platform');
|
|
363
|
+
success = false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch (e) {
|
|
367
|
+
console.log(`${e.stack}`);
|
|
368
|
+
res.status(400).send(e);
|
|
369
|
+
}
|
|
370
|
+
const requestEndTime = new Date().getTime();
|
|
371
|
+
analytics.track({
|
|
372
|
+
eventName: 'Get user mapping',
|
|
373
|
+
interfaceName: 'getUserMapping',
|
|
374
|
+
adapterName: platformName,
|
|
375
|
+
accountId: hashedAccountId,
|
|
376
|
+
extensionId: hashedExtensionId,
|
|
377
|
+
success,
|
|
378
|
+
requestDuration: (requestEndTime - requestStartTime) / 1000,
|
|
379
|
+
userAgent,
|
|
380
|
+
ip,
|
|
381
|
+
author,
|
|
382
|
+
eventAddedVia
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
334
386
|
router.get('/admin/serverLoggingSettings', async function (req, res) {
|
|
335
387
|
const requestStartTime = new Date().getTime();
|
|
336
388
|
let platformName = null;
|
|
@@ -948,7 +1000,7 @@ function createCoreRouter() {
|
|
|
948
1000
|
}
|
|
949
1001
|
const { id: userId, platform } = decodedToken;
|
|
950
1002
|
platformName = platform;
|
|
951
|
-
const { successful, logId, returnMessage, extraDataTracking } = await logCore.createCallLog({ platform, userId, incomingData: req.body, isFromSSCL: userAgent === 'SSCL'
|
|
1003
|
+
const { successful, logId, returnMessage, extraDataTracking } = await logCore.createCallLog({ platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.logInfo?.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL'});
|
|
952
1004
|
if (extraDataTracking) {
|
|
953
1005
|
extraData = extraDataTracking;
|
|
954
1006
|
}
|
|
@@ -1000,7 +1052,7 @@ function createCoreRouter() {
|
|
|
1000
1052
|
}
|
|
1001
1053
|
const { id: userId, platform } = decodedToken;
|
|
1002
1054
|
platformName = platform;
|
|
1003
|
-
const { successful, logId, updatedNote, returnMessage, extraDataTracking } = await logCore.updateCallLog({ platform, userId, incomingData: req.body, isFromSSCL: userAgent === 'SSCL' });
|
|
1055
|
+
const { successful, logId, updatedNote, returnMessage, extraDataTracking } = await logCore.updateCallLog({ platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
|
|
1004
1056
|
if (extraDataTracking) {
|
|
1005
1057
|
extraData = extraDataTracking;
|
|
1006
1058
|
}
|
package/lib/callLogComposer.js
CHANGED
|
@@ -61,10 +61,10 @@ async function composeCallLog(params) {
|
|
|
61
61
|
body = upsertCallSessionId({ body, id: callLog.sessionId, logFormat });
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
if (userSettings?.addRingCentralUserName?.value) {
|
|
65
|
+
const ringcentralUsername = (callLog.direction === 'Inbound' ? callLog?.to?.name : callLog?.from?.name) ?? '(pending...)';
|
|
66
66
|
body = upsertRingCentralUserName({ body, userName: ringcentralUsername, logFormat });
|
|
67
|
-
}
|
|
67
|
+
}
|
|
68
68
|
|
|
69
69
|
const ringcentralNumber = callLog.direction === 'Inbound' ? callLog?.to?.phoneNumber : callLog?.from?.phoneNumber;
|
|
70
70
|
if (ringcentralNumber && (userSettings?.addRingCentralNumber?.value ?? false)) {
|
|
@@ -115,6 +115,10 @@ async function composeCallLog(params) {
|
|
|
115
115
|
body = upsertTranscript({ body, transcript, logFormat });
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
if (callLog?.legs && (userSettings?.addCallLogLegs?.value ?? true)) {
|
|
119
|
+
body = upsertLegs({ body, legs: callLog.legs, logFormat });
|
|
120
|
+
}
|
|
121
|
+
|
|
118
122
|
return body;
|
|
119
123
|
}
|
|
120
124
|
|
|
@@ -184,22 +188,37 @@ function upsertRingCentralUserName({ body, userName, logFormat }) {
|
|
|
184
188
|
|
|
185
189
|
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
186
190
|
const userNameRegex = /(?:<li>)?<b>RingCentral user name<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
|
|
187
|
-
|
|
188
|
-
|
|
191
|
+
const match = body.match(userNameRegex);
|
|
192
|
+
if (match) {
|
|
193
|
+
// Only replace if existing value is (pending...)
|
|
194
|
+
if (match[1].trim() === '(pending...)') {
|
|
195
|
+
return body.replace(userNameRegex, `<li><b>RingCentral user name</b>: ${userName}</li>`);
|
|
196
|
+
}
|
|
197
|
+
return body;
|
|
189
198
|
} else {
|
|
190
199
|
return body + `<li><b>RingCentral user name</b>: ${userName}</li>`;
|
|
191
200
|
}
|
|
192
201
|
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
193
|
-
const userNameRegex = /\*\*RingCentral user name\*\*: [^\n]
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
const userNameRegex = /\*\*RingCentral user name\*\*: ([^\n]*)\n*/i;
|
|
203
|
+
const match = body.match(userNameRegex);
|
|
204
|
+
if (match) {
|
|
205
|
+
// Only replace if existing value is (pending...)
|
|
206
|
+
if (match[1].trim() === '(pending...)') {
|
|
207
|
+
return body.replace(userNameRegex, `**RingCentral user name**: ${userName}\n`);
|
|
208
|
+
}
|
|
209
|
+
return body;
|
|
196
210
|
} else {
|
|
197
211
|
return body + `**RingCentral user name**: ${userName}\n`;
|
|
198
212
|
}
|
|
199
213
|
} else {
|
|
200
|
-
const userNameRegex = /- RingCentral user name: [^\n]
|
|
201
|
-
|
|
202
|
-
|
|
214
|
+
const userNameRegex = /- RingCentral user name: ([^\n]*)\n*/;
|
|
215
|
+
const match = body.match(userNameRegex);
|
|
216
|
+
if (match) {
|
|
217
|
+
// Only replace if existing value is (pending...)
|
|
218
|
+
if (match[1].trim() === '(pending...)') {
|
|
219
|
+
return body.replace(userNameRegex, `- RingCentral user name: ${userName}\n`);
|
|
220
|
+
}
|
|
221
|
+
return body;
|
|
203
222
|
} else {
|
|
204
223
|
return body + `- RingCentral user name: ${userName}\n`;
|
|
205
224
|
}
|
|
@@ -327,9 +346,9 @@ function upsertCallDateTime({ body, startTime, timezoneOffset, logFormat, logDat
|
|
|
327
346
|
}
|
|
328
347
|
} else {
|
|
329
348
|
// Handle duplicated Date/Time entries and match complete date/time values
|
|
330
|
-
const dateTimeRegex =
|
|
349
|
+
const dateTimeRegex = /^(- Date\/Time:).*$/m;
|
|
331
350
|
if (dateTimeRegex.test(result)) {
|
|
332
|
-
result = result.replace(dateTimeRegex, `- Date/Time: ${formattedDateTime}
|
|
351
|
+
result = result.replace(dateTimeRegex, `- Date/Time: ${formattedDateTime}`);
|
|
333
352
|
} else {
|
|
334
353
|
result += `- Date/Time: ${formattedDateTime}\n`;
|
|
335
354
|
}
|
|
@@ -521,6 +540,81 @@ function upsertTranscript({ body, transcript, logFormat }) {
|
|
|
521
540
|
return result;
|
|
522
541
|
}
|
|
523
542
|
|
|
543
|
+
function getLegPartyInfo(info) {
|
|
544
|
+
let phoneNumber = info.phoneNumber;
|
|
545
|
+
let extensionNumber = info.extensionNumber;
|
|
546
|
+
let numberInfo = phoneNumber;
|
|
547
|
+
if (!phoneNumber && !extensionNumber) {
|
|
548
|
+
return '';
|
|
549
|
+
}
|
|
550
|
+
if (extensionNumber && phoneNumber) {
|
|
551
|
+
numberInfo = `${phoneNumber}, ext ${extensionNumber}`;
|
|
552
|
+
}
|
|
553
|
+
if (phoneNumber && !extensionNumber) {
|
|
554
|
+
numberInfo = phoneNumber;
|
|
555
|
+
}
|
|
556
|
+
if (!phoneNumber && extensionNumber) {
|
|
557
|
+
numberInfo = `ext ${extensionNumber}`;
|
|
558
|
+
}
|
|
559
|
+
if (info.name) {
|
|
560
|
+
return `${info.name}, ${numberInfo}`;
|
|
561
|
+
}
|
|
562
|
+
return numberInfo;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function getLegsJourney(legs) {
|
|
566
|
+
return legs.map((leg, index) => {
|
|
567
|
+
if (index === 0) {
|
|
568
|
+
if (leg.direction === 'Outbound') {
|
|
569
|
+
return `Made call from ${getLegPartyInfo(leg.from)}`;
|
|
570
|
+
} else {
|
|
571
|
+
return `Received call at ${getLegPartyInfo(leg.to)}`;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (leg.direction === 'Outbound') {
|
|
575
|
+
let party = leg.from;
|
|
576
|
+
if (leg.legType === 'PstnToSip') {
|
|
577
|
+
party = leg.to;
|
|
578
|
+
}
|
|
579
|
+
return `Transferred to ${getLegPartyInfo(party)}, duration: ${leg.duration} second${leg.duration > 1 ? 's' : ''}`;
|
|
580
|
+
} else {
|
|
581
|
+
return `Transferred to ${getLegPartyInfo(leg.to)}, duration: ${leg.duration} second${leg.duration > 1 ? 's' : ''}`;
|
|
582
|
+
}
|
|
583
|
+
}).join('\n');
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function upsertLegs({ body, legs, logFormat }) {
|
|
587
|
+
if (!legs || legs.length === 0) return body;
|
|
588
|
+
|
|
589
|
+
let result = body;
|
|
590
|
+
let legsJourney = getLegsJourney(legs);
|
|
591
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
592
|
+
legsJourney = legsJourney.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
|
593
|
+
const legsRegex = /<div><b>Call journey<\/b><br>(.+?)<\/div>/;
|
|
594
|
+
if (legsRegex.test(result)) {
|
|
595
|
+
result = result.replace(legsRegex, `<div><b>Call journey</b><br>${legsJourney}</div>`);
|
|
596
|
+
} else {
|
|
597
|
+
result += `<div><b>Call journey</b><br>${legsJourney}</div>`;
|
|
598
|
+
}
|
|
599
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
600
|
+
const legsRegex = /### Call journey\n([\s\S]*?)(?=\n### |\n$|$)/;
|
|
601
|
+
if (legsRegex.test(result)) {
|
|
602
|
+
result = result.replace(legsRegex, `### Call journey\n${legsJourney}\n`);
|
|
603
|
+
} else {
|
|
604
|
+
result += `### Call journey\n${legsJourney}\n`;
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
const legsRegex = /- Call journey:([\s\S]*?)--- JOURNEY END/;
|
|
608
|
+
if (legsRegex.test(result)) {
|
|
609
|
+
result = result.replace(legsRegex, `- Call journey:\n${legsJourney}\n--- JOURNEY END`);
|
|
610
|
+
} else {
|
|
611
|
+
result += `- Call journey:\n${legsJourney}\n--- JOURNEY END\n`;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
|
|
524
618
|
/**
|
|
525
619
|
* Helper function to determine format type for a CRM platform
|
|
526
620
|
* @param {string} platform - CRM platform name
|
|
@@ -547,5 +641,6 @@ module.exports = {
|
|
|
547
641
|
upsertCallResult,
|
|
548
642
|
upsertCallRecording,
|
|
549
643
|
upsertAiNote,
|
|
550
|
-
upsertTranscript
|
|
551
|
-
|
|
644
|
+
upsertTranscript,
|
|
645
|
+
upsertLegs,
|
|
646
|
+
};
|
package/lib/util.js
CHANGED
|
@@ -37,7 +37,17 @@ function secondsToHoursMinutesSeconds(seconds) {
|
|
|
37
37
|
return resultString;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// media reader link: https://ringcentral.github.io/ringcentral-media-reader/?media=https://media.ringcentral.com/restapi/v1.0/account/{accountId}/extension/{extensionId}/message-store/{messageId}/content/{contentId}
|
|
41
|
+
// platform media link: https://media.ringcentral.com/restapi/v1.0/account/{accountId}/extension/{extensionId}/message-store/{messageId}/content/{contentId}
|
|
42
|
+
function getMediaReaderLinkByPlatformMediaLink(platformMediaLink){
|
|
43
|
+
if(!platformMediaLink){
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const encodedPlatformMediaLink = encodeURIComponent(platformMediaLink);
|
|
47
|
+
return `https://ringcentral.github.io/ringcentral-media-reader/?media=${encodedPlatformMediaLink}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
exports.getTimeZone = getTimeZone;
|
|
41
51
|
exports.getHashValue = getHashValue;
|
|
42
52
|
exports.secondsToHoursMinutesSeconds = secondsToHoursMinutesSeconds;
|
|
43
|
-
|
|
53
|
+
exports.getMediaReaderLinkByPlatformMediaLink = getMediaReaderLinkByPlatformMediaLink;
|
|
@@ -13,5 +13,13 @@ exports.AdminConfigModel = sequelize.define('adminConfigs', {
|
|
|
13
13
|
},
|
|
14
14
|
customAdapter: {
|
|
15
15
|
type: Sequelize.JSON
|
|
16
|
+
},
|
|
17
|
+
// Array of:
|
|
18
|
+
// {
|
|
19
|
+
// crmUserId: string,
|
|
20
|
+
// rcExtensionId: array of strings
|
|
21
|
+
// }
|
|
22
|
+
userMappings: {
|
|
23
|
+
type: Sequelize.JSON
|
|
16
24
|
}
|
|
17
25
|
});
|
package/models/userModel.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@app-connect/core",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.9",
|
|
4
4
|
"description": "RingCentral App Connect Core",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"author": "RingCentral Labs",
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"axios": "^1.
|
|
17
|
+
"axios": "^1.12.2",
|
|
18
18
|
"express": "^4.21.2",
|
|
19
19
|
"pg": "^8.8.0",
|
|
20
20
|
"sequelize": "^6.29.0",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@eslint/js": "^9.22.0",
|
|
46
46
|
"@octokit/rest": "^19.0.5",
|
|
47
|
-
"axios": "^1.
|
|
47
|
+
"axios": "^1.12.2",
|
|
48
48
|
"express": "^4.21.2",
|
|
49
49
|
"eslint": "^9.22.0",
|
|
50
50
|
"globals": "^16.0.0",
|
package/releaseNotes.json
CHANGED
|
@@ -1,4 +1,72 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.6.9": {
|
|
3
|
+
"global": [
|
|
4
|
+
{
|
|
5
|
+
"type": "New",
|
|
6
|
+
"description": "- Server-side logging now supports multiple RingCentral users to be mapped under one CRM user"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"type": "Fix",
|
|
10
|
+
"description": "- Server-side logging user mapping not working properly when running under multi-site scenarios"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"1.6.8": {
|
|
15
|
+
"global": [
|
|
16
|
+
{
|
|
17
|
+
"type": "Fix",
|
|
18
|
+
"description": "- Server-side logging, if it's created by one admin, the other admin will be able to see its status"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "Fix",
|
|
22
|
+
"description": "- Conference calls show wrong warning message"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"1.6.7": {
|
|
27
|
+
"global": [
|
|
28
|
+
{
|
|
29
|
+
"type": "New",
|
|
30
|
+
"description": "- Clio now supports image/video media link in message logs"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"1.6.6": {
|
|
35
|
+
"global": [
|
|
36
|
+
{
|
|
37
|
+
"type": "New",
|
|
38
|
+
"description": "- Server-side call logging now supports user mapping configuration in the admin tab, allowing admin users to log calls on behalf of other users."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"type": "New",
|
|
42
|
+
"description": "- Separate enable domains for click-to-dial and quick access button"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"type": "Fix",
|
|
46
|
+
"description": "- Server-side call logging now displays RingCentral user names in the correct order within log details."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"type": "Fix",
|
|
50
|
+
"description": "- Server-side call logging now shows the correct RingCentral user name instead of displaying the Caller ID"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"1.6.5": {
|
|
55
|
+
"global": [
|
|
56
|
+
{
|
|
57
|
+
"type": "New",
|
|
58
|
+
"description": "- Support call journey in call logging details with server side logging [Details](https://appconnect.labs.ringcentral.com/users/logging/#controlling-what-information-gets-logged)"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "Better",
|
|
62
|
+
"description": "- Tabs orders updated"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"type": "Fix",
|
|
66
|
+
"description": "- Date/Time display issue"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
},
|
|
2
70
|
"1.6.4": {
|
|
3
71
|
"global": [
|
|
4
72
|
{
|