@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 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
  }
@@ -61,10 +61,10 @@ async function composeCallLog(params) {
61
61
  body = upsertCallSessionId({ body, id: callLog.sessionId, logFormat });
62
62
  }
63
63
 
64
- const ringcentralUsername = callLog.direction === 'Inbound' ? callLog?.to?.name : callLog?.from?.name;
65
- if (ringcentralUsername && (userSettings?.addRingCentralUserName?.value ?? false)) {
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
- if (userNameRegex.test(body)) {
188
- return body.replace(userNameRegex, `<li><b>RingCentral user name</b>: ${userName}</li>`);
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]*\n*/i;
194
- if (userNameRegex.test(body)) {
195
- return body.replace(userNameRegex, `**RingCentral user name**: ${userName}\n`);
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]*\n*/;
201
- if (userNameRegex.test(body)) {
202
- return body.replace(userNameRegex, `- RingCentral user name: ${userName}\n`);
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 = /(?:- Date\/Time: [^-]*(?:-[^-]*)*)+/;
349
+ const dateTimeRegex = /^(- Date\/Time:).*$/m;
331
350
  if (dateTimeRegex.test(result)) {
332
- result = result.replace(dateTimeRegex, `- Date/Time: ${formattedDateTime}\n`);
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
  });
@@ -7,6 +7,9 @@ exports.UserModel = sequelize.define('users', {
7
7
  type: Sequelize.STRING,
8
8
  primaryKey: true,
9
9
  },
10
+ rcAccountId: {
11
+ type: Sequelize.STRING,
12
+ },
10
13
  hostname: {
11
14
  type: Sequelize.STRING,
12
15
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-connect/core",
3
- "version": "1.6.4",
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.1.2",
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.1.2",
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
  {