@app-connect/core 0.0.3 → 1.5.8
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/README.md +13 -4
- package/handlers/log.js +4 -3
- package/index.js +5 -3
- package/lib/callLogComposer.js +98 -64
- package/lib/constants.js +9 -0
- package/lib/oauth.js +60 -23
- package/package.json +1 -1
- package/releaseNotes.json +28 -0
package/README.md
CHANGED
|
@@ -24,9 +24,14 @@ npm install @app-connect/core
|
|
|
24
24
|
|
|
25
25
|
```javascript
|
|
26
26
|
const { createCoreApp, adapterRegistry } = require('@app-connect/core');
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const myCRMAdapter = require('./adapters/myCRM');
|
|
28
|
+
const manifest = require('./adapters/manifest.json');
|
|
29
|
+
// Set the default manifest for the adapter registry. This ensures that all adapters
|
|
30
|
+
// have access to the necessary configuration and metadata before registration.
|
|
31
|
+
adapterRegistry.setDefaultManifest(manifest);
|
|
32
|
+
// Register your CRM adapters. The default manifest must be set before registration
|
|
33
|
+
// to ensure proper initialization of the adapter with the required settings.
|
|
34
|
+
adapterRegistry.registerAdapter('myCRM', myCRMAdapter, manifest);
|
|
30
35
|
|
|
31
36
|
// Create Express app with all core functionality
|
|
32
37
|
const app = createCoreApp();
|
|
@@ -113,8 +118,12 @@ const {
|
|
|
113
118
|
adapterRegistry
|
|
114
119
|
} = require('@app-connect/core');
|
|
115
120
|
|
|
121
|
+
const myCRMAdapter = require('./adapters/myCRM');
|
|
122
|
+
const manifest = require('./adapters/manifest.json');
|
|
123
|
+
// Set manifest
|
|
124
|
+
adapterRegistry.setDefaultManifest(manifest);
|
|
116
125
|
// Register adapters
|
|
117
|
-
adapterRegistry.registerAdapter('myCRM', myCRMAdapter);
|
|
126
|
+
adapterRegistry.registerAdapter('myCRM', myCRMAdapter, manifest);
|
|
118
127
|
|
|
119
128
|
// Initialize core services
|
|
120
129
|
initializeCore();
|
package/handlers/log.js
CHANGED
|
@@ -4,8 +4,9 @@ const { MessageLogModel } = require('../models/messageLogModel');
|
|
|
4
4
|
const { UserModel } = require('../models/userModel');
|
|
5
5
|
const oauth = require('../lib/oauth');
|
|
6
6
|
const errorMessage = require('../lib/generalErrorMessage');
|
|
7
|
-
const { composeCallLog, getLogFormatType
|
|
7
|
+
const { composeCallLog, getLogFormatType } = require('../lib/callLogComposer');
|
|
8
8
|
const adapterRegistry = require('../adapter/registry');
|
|
9
|
+
const { LOG_DETAILS_FORMAT_TYPE } = require('../lib/constants');
|
|
9
10
|
|
|
10
11
|
async function createCallLog({ platform, userId, incomingData }) {
|
|
11
12
|
try {
|
|
@@ -76,7 +77,7 @@ async function createCallLog({ platform, userId, incomingData }) {
|
|
|
76
77
|
// Compose call log details centrally
|
|
77
78
|
const logFormat = getLogFormatType(platform);
|
|
78
79
|
let composedLogDetails = '';
|
|
79
|
-
if (logFormat ===
|
|
80
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT || logFormat === LOG_DETAILS_FORMAT_TYPE.HTML || logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
80
81
|
composedLogDetails = await composeCallLog({
|
|
81
82
|
logFormat,
|
|
82
83
|
callLog,
|
|
@@ -309,7 +310,7 @@ async function updateCallLog({ platform, userId, incomingData }) {
|
|
|
309
310
|
let existingCallLogDetails = null; // Compose updated call log details centrally
|
|
310
311
|
const logFormat = getLogFormatType(platform);
|
|
311
312
|
let composedLogDetails = '';
|
|
312
|
-
if (logFormat ===
|
|
313
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT || logFormat === LOG_DETAILS_FORMAT_TYPE.HTML || logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
313
314
|
let existingBody = '';
|
|
314
315
|
try {
|
|
315
316
|
const getLogResult = await platformModule.getCallLog({
|
package/index.js
CHANGED
|
@@ -76,7 +76,7 @@ function createCoreRouter() {
|
|
|
76
76
|
const versions = Object.keys(adapterReleaseNotes);
|
|
77
77
|
for (const version of versions) {
|
|
78
78
|
mergedReleaseNotes[version] = {
|
|
79
|
-
global: globalReleaseNotes[version]
|
|
79
|
+
global: globalReleaseNotes[version]?.global ?? {},
|
|
80
80
|
...adapterReleaseNotes[version] ?? {}
|
|
81
81
|
};
|
|
82
82
|
}
|
|
@@ -229,7 +229,8 @@ function createCoreRouter() {
|
|
|
229
229
|
platformName = unAuthData?.platform ?? 'Unknown';
|
|
230
230
|
const user = await UserModel.findByPk(unAuthData?.id);
|
|
231
231
|
if (!user) {
|
|
232
|
-
res.status(400).send();
|
|
232
|
+
res.status(400).send('User not found');
|
|
233
|
+
return;
|
|
233
234
|
}
|
|
234
235
|
const { isValidated, rcAccountId } = await adminCore.validateAdminRole({ rcAccessToken: req.query.rcAccessToken });
|
|
235
236
|
const hashedRcAccountId = util.getHashValue(rcAccountId, process.env.HASH_KEY);
|
|
@@ -402,7 +403,8 @@ function createCoreRouter() {
|
|
|
402
403
|
platformName = unAuthData?.platform ?? 'Unknown';
|
|
403
404
|
const user = await UserModel.findByPk(unAuthData?.id);
|
|
404
405
|
if (!user) {
|
|
405
|
-
res.status(400).send();
|
|
406
|
+
res.status(400).send('User not found');
|
|
407
|
+
return;
|
|
406
408
|
}
|
|
407
409
|
else {
|
|
408
410
|
const rcAccessToken = req.query.rcAccessToken;
|
package/lib/callLogComposer.js
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
const moment = require('moment-timezone');
|
|
2
2
|
const { secondsToHoursMinutesSeconds } = require('./util');
|
|
3
3
|
const adapterRegistry = require('../adapter/registry');
|
|
4
|
+
const { LOG_DETAILS_FORMAT_TYPE } = require('./constants');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Centralized call log composition module
|
|
7
8
|
* Supports both plain text and HTML formats used across different CRM adapters
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
// Format types
|
|
11
|
-
const FORMAT_TYPES = {
|
|
12
|
-
PLAIN_TEXT: 'plainText',
|
|
13
|
-
HTML: 'html'
|
|
14
|
-
};
|
|
15
|
-
|
|
16
11
|
/**
|
|
17
12
|
* Compose call log details based on user settings and format type
|
|
18
13
|
* @param {Object} params - Composition parameters
|
|
@@ -33,7 +28,7 @@ const FORMAT_TYPES = {
|
|
|
33
28
|
*/
|
|
34
29
|
async function composeCallLog(params) {
|
|
35
30
|
const {
|
|
36
|
-
logFormat =
|
|
31
|
+
logFormat = LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT,
|
|
37
32
|
existingBody = '',
|
|
38
33
|
callLog,
|
|
39
34
|
contactInfo,
|
|
@@ -117,67 +112,85 @@ async function composeCallLog(params) {
|
|
|
117
112
|
|
|
118
113
|
function upsertCallAgentNote({ body, note, logFormat }) {
|
|
119
114
|
if (!note) return body;
|
|
120
|
-
|
|
115
|
+
|
|
116
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
121
117
|
// HTML logFormat with proper Agent notes section handling
|
|
122
118
|
const noteRegex = RegExp('<b>Agent notes</b>([\\s\\S]+?)Call details</b>');
|
|
123
119
|
if (noteRegex.test(body)) {
|
|
124
120
|
return body.replace(noteRegex, `<b>Agent notes</b><br>${note}<br><br><b>Call details</b>`);
|
|
125
121
|
}
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
return `<b>Agent notes</b><br>${note}<br><br><b>Call details</b><br>` + body;
|
|
123
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
124
|
+
// Markdown logFormat with proper Agent notes section handling
|
|
125
|
+
const noteRegex = /## Agent notes\n([\s\S]*?)\n## Call details/;
|
|
126
|
+
if (noteRegex.test(body)) {
|
|
127
|
+
return body.replace(noteRegex, `## Agent notes\n${note}\n\n## Call details`);
|
|
128
|
+
}
|
|
129
|
+
if (body.startsWith('## Call details')) {
|
|
130
|
+
return `## Agent notes\n${note}\n\n` + body;
|
|
128
131
|
}
|
|
132
|
+
return `## Agent notes\n${note}\n\n## Call details\n` + body;
|
|
129
133
|
} else {
|
|
130
134
|
// Plain text logFormat - FIXED REGEX for multi-line notes with blank lines
|
|
131
135
|
const noteRegex = /- (?:Note|Agent notes): ([\s\S]*?)(?=\n- [A-Z][a-zA-Z\s/]*:|\n$|$)/;
|
|
132
136
|
if (noteRegex.test(body)) {
|
|
133
137
|
return body.replace(noteRegex, `- Note: ${note}`);
|
|
134
|
-
} else {
|
|
135
|
-
return `- Note: ${note}\n` + body;
|
|
136
138
|
}
|
|
139
|
+
return `- Note: ${note}\n` + body;
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
function upsertCallSessionId({ body, id, logFormat }) {
|
|
141
144
|
if (!id) return body;
|
|
142
145
|
|
|
143
|
-
if (logFormat ===
|
|
146
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
144
147
|
// More flexible regex that handles both <li> wrapped and unwrapped content
|
|
145
148
|
const idRegex = /(?:<li>)?<b>Session Id<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
|
|
146
149
|
if (idRegex.test(body)) {
|
|
147
150
|
return body.replace(idRegex, `<li><b>Session Id</b>: ${id}</li>`);
|
|
148
|
-
} else {
|
|
149
|
-
return body + `<li><b>Session Id</b>: ${id}</li>`;
|
|
150
151
|
}
|
|
152
|
+
return body + `<li><b>Session Id</b>: ${id}</li>`;
|
|
153
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
154
|
+
// Markdown format: **Session Id**: value
|
|
155
|
+
const sessionIdRegex = /\*\*Session Id\*\*: [^\n]*\n*/;
|
|
156
|
+
if (sessionIdRegex.test(body)) {
|
|
157
|
+
return body.replace(sessionIdRegex, `**Session Id**: ${id}\n`);
|
|
158
|
+
}
|
|
159
|
+
return body + `**Session Id**: ${id}\n`;
|
|
151
160
|
} else {
|
|
152
161
|
// Match Session Id field and any trailing newlines, replace with single newline
|
|
153
162
|
const sessionIdRegex = /- Session Id: [^\n]*\n*/;
|
|
154
163
|
if (sessionIdRegex.test(body)) {
|
|
155
164
|
return body.replace(sessionIdRegex, `- Session Id: ${id}\n`);
|
|
156
|
-
} else {
|
|
157
|
-
return body + `- Session Id: ${id}\n`;
|
|
158
165
|
}
|
|
166
|
+
return body + `- Session Id: ${id}\n`;
|
|
159
167
|
}
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
function upsertCallSubject({ body, subject, logFormat }) {
|
|
163
171
|
if (!subject) return body;
|
|
164
172
|
|
|
165
|
-
if (logFormat ===
|
|
173
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
166
174
|
// More flexible regex that handles both <li> wrapped and unwrapped content
|
|
167
175
|
const subjectRegex = /(?:<li>)?<b>Summary<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
|
|
168
176
|
if (subjectRegex.test(body)) {
|
|
169
177
|
return body.replace(subjectRegex, `<li><b>Summary</b>: ${subject}</li>`);
|
|
170
|
-
} else {
|
|
171
|
-
return body + `<li><b>Summary</b>: ${subject}</li>`;
|
|
172
178
|
}
|
|
179
|
+
return body + `<li><b>Summary</b>: ${subject}</li>`;
|
|
180
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
181
|
+
// Markdown format: **Summary**: value
|
|
182
|
+
const subjectRegex = /\*\*Summary\*\*: [^\n]*\n*/;
|
|
183
|
+
if (subjectRegex.test(body)) {
|
|
184
|
+
return body.replace(subjectRegex, `**Summary**: ${subject}\n`);
|
|
185
|
+
}
|
|
186
|
+
return body + `**Summary**: ${subject}\n`;
|
|
173
187
|
} else {
|
|
174
188
|
// Match Summary field and any trailing newlines, replace with single newline
|
|
175
189
|
const subjectRegex = /- Summary: [^\n]*\n*/;
|
|
176
190
|
if (subjectRegex.test(body)) {
|
|
177
191
|
return body.replace(subjectRegex, `- Summary: ${subject}\n`);
|
|
178
|
-
} else {
|
|
179
|
-
return body + `- Summary: ${subject}\n`;
|
|
180
192
|
}
|
|
193
|
+
return body + `- Summary: ${subject}\n`;
|
|
181
194
|
}
|
|
182
195
|
}
|
|
183
196
|
|
|
@@ -187,7 +200,7 @@ function upsertContactPhoneNumber({ body, phoneNumber, direction, logFormat }) {
|
|
|
187
200
|
const label = direction === 'Outbound' ? 'Recipient' : 'Caller';
|
|
188
201
|
let result = body;
|
|
189
202
|
|
|
190
|
-
if (logFormat ===
|
|
203
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
191
204
|
// More flexible regex that handles both <li> wrapped and unwrapped content
|
|
192
205
|
const phoneNumberRegex = new RegExp(`(?:<li>)?<b>${label} phone number</b>:\\s*([^<\\n]+)(?:</li>|(?=<|$))`, 'i');
|
|
193
206
|
if (phoneNumberRegex.test(result)) {
|
|
@@ -195,6 +208,14 @@ function upsertContactPhoneNumber({ body, phoneNumber, direction, logFormat }) {
|
|
|
195
208
|
} else {
|
|
196
209
|
result += `<li><b>${label} phone number</b>: ${phoneNumber}</li>`;
|
|
197
210
|
}
|
|
211
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
212
|
+
// Markdown format: **Contact Number**: value
|
|
213
|
+
const phoneNumberRegex = /\*\*Contact Number\*\*: [^\n]*\n*/;
|
|
214
|
+
if (phoneNumberRegex.test(result)) {
|
|
215
|
+
result = result.replace(phoneNumberRegex, `**Contact Number**: ${phoneNumber}\n`);
|
|
216
|
+
} else {
|
|
217
|
+
result += `**Contact Number**: ${phoneNumber}\n`;
|
|
218
|
+
}
|
|
198
219
|
} else {
|
|
199
220
|
// More flexible regex that handles both with and without newlines
|
|
200
221
|
const phoneNumberRegex = /- Contact Number: ([^\n-]+)(?=\n-|\n|$)/;
|
|
@@ -225,7 +246,7 @@ function upsertCallDateTime({ body, startTime, timezoneOffset, logFormat }) {
|
|
|
225
246
|
const formattedDateTime = momentTime.format('YYYY-MM-DD hh:mm:ss A');
|
|
226
247
|
let result = body;
|
|
227
248
|
|
|
228
|
-
if (logFormat ===
|
|
249
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
229
250
|
// More flexible regex that handles both <li> wrapped and unwrapped content
|
|
230
251
|
const dateTimeRegex = /(?:<li>)?<b>Date\/time<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
|
|
231
252
|
if (dateTimeRegex.test(result)) {
|
|
@@ -233,6 +254,14 @@ function upsertCallDateTime({ body, startTime, timezoneOffset, logFormat }) {
|
|
|
233
254
|
} else {
|
|
234
255
|
result += `<li><b>Date/time</b>: ${formattedDateTime}</li>`;
|
|
235
256
|
}
|
|
257
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
258
|
+
// Markdown format: **Date/Time**: value
|
|
259
|
+
const dateTimeRegex = /\*\*Date\/Time\*\*: [^\n]*\n*/;
|
|
260
|
+
if (dateTimeRegex.test(result)) {
|
|
261
|
+
result = result.replace(dateTimeRegex, `**Date/Time**: ${formattedDateTime}\n`);
|
|
262
|
+
} else {
|
|
263
|
+
result += `**Date/Time**: ${formattedDateTime}\n`;
|
|
264
|
+
}
|
|
236
265
|
} else {
|
|
237
266
|
// Handle duplicated Date/Time entries and match complete date/time values
|
|
238
267
|
const dateTimeRegex = /(?:- Date\/Time: [^-]*(?:-[^-]*)*)+/;
|
|
@@ -251,7 +280,7 @@ function upsertCallDuration({ body, duration, logFormat }) {
|
|
|
251
280
|
const formattedDuration = secondsToHoursMinutesSeconds(duration);
|
|
252
281
|
let result = body;
|
|
253
282
|
|
|
254
|
-
if (logFormat ===
|
|
283
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
255
284
|
// More flexible regex that handles both <li> wrapped and unwrapped content
|
|
256
285
|
const durationRegex = /(?:<li>)?<b>Duration<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
|
|
257
286
|
if (durationRegex.test(result)) {
|
|
@@ -259,6 +288,14 @@ function upsertCallDuration({ body, duration, logFormat }) {
|
|
|
259
288
|
} else {
|
|
260
289
|
result += `<li><b>Duration</b>: ${formattedDuration}</li>`;
|
|
261
290
|
}
|
|
291
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
292
|
+
// Markdown format: **Duration**: value
|
|
293
|
+
const durationRegex = /\*\*Duration\*\*: [^\n]*\n*/;
|
|
294
|
+
if (durationRegex.test(result)) {
|
|
295
|
+
result = result.replace(durationRegex, `**Duration**: ${formattedDuration}\n`);
|
|
296
|
+
} else {
|
|
297
|
+
result += `**Duration**: ${formattedDuration}\n`;
|
|
298
|
+
}
|
|
262
299
|
} else {
|
|
263
300
|
// More flexible regex that handles both with and without newlines
|
|
264
301
|
const durationRegex = /- Duration: ([^\n-]+)(?=\n-|\n|$)/;
|
|
@@ -276,7 +313,7 @@ function upsertCallResult({ body, result, logFormat }) {
|
|
|
276
313
|
|
|
277
314
|
let bodyResult = body;
|
|
278
315
|
|
|
279
|
-
if (logFormat ===
|
|
316
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
280
317
|
// More flexible regex that handles both <li> wrapped and unwrapped content
|
|
281
318
|
const resultRegex = /(?:<li>)?<b>Result<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
|
|
282
319
|
if (resultRegex.test(bodyResult)) {
|
|
@@ -284,6 +321,14 @@ function upsertCallResult({ body, result, logFormat }) {
|
|
|
284
321
|
} else {
|
|
285
322
|
bodyResult += `<li><b>Result</b>: ${result}</li>`;
|
|
286
323
|
}
|
|
324
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
325
|
+
// Markdown format: **Result**: value
|
|
326
|
+
const resultRegex = /\*\*Result\*\*: [^\n]*\n*/;
|
|
327
|
+
if (resultRegex.test(bodyResult)) {
|
|
328
|
+
bodyResult = bodyResult.replace(resultRegex, `**Result**: ${result}\n`);
|
|
329
|
+
} else {
|
|
330
|
+
bodyResult += `**Result**: ${result}\n`;
|
|
331
|
+
}
|
|
287
332
|
} else {
|
|
288
333
|
// More flexible regex that handles both with and without newlines
|
|
289
334
|
const resultRegex = /- Result: ([^\n-]+)(?=\n-|\n|$)/;
|
|
@@ -297,12 +342,11 @@ function upsertCallResult({ body, result, logFormat }) {
|
|
|
297
342
|
}
|
|
298
343
|
|
|
299
344
|
function upsertCallRecording({ body, recordingLink, logFormat }) {
|
|
300
|
-
// console.log({ m: "upsertCallRecording", recordingLink, hasBody: !!body, logFormat, bodyLength: body?.length });
|
|
301
345
|
if (!recordingLink) return body;
|
|
302
346
|
|
|
303
347
|
let result = body;
|
|
304
348
|
|
|
305
|
-
if (logFormat ===
|
|
349
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
306
350
|
// More flexible regex that handles both <li> wrapped and unwrapped content
|
|
307
351
|
const recordingLinkRegex = /(?:<li>)?<b>Call recording link<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
|
|
308
352
|
if (recordingLink) {
|
|
@@ -326,6 +370,14 @@ function upsertCallRecording({ body, recordingLink, logFormat }) {
|
|
|
326
370
|
}
|
|
327
371
|
}
|
|
328
372
|
}
|
|
373
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
374
|
+
// Markdown format: **Call recording link**: value
|
|
375
|
+
const recordingLinkRegex = /\*\*Call recording link\*\*: [^\n]*\n*/;
|
|
376
|
+
if (recordingLinkRegex.test(result)) {
|
|
377
|
+
result = result.replace(recordingLinkRegex, `**Call recording link**: ${recordingLink}\n`);
|
|
378
|
+
} else {
|
|
379
|
+
result += `**Call recording link**: ${recordingLink}\n`;
|
|
380
|
+
}
|
|
329
381
|
} else {
|
|
330
382
|
// Match recording link field and any trailing content, replace with single newline
|
|
331
383
|
const recordingLinkRegex = /- Call recording link: [^\n]*\n*/;
|
|
@@ -347,7 +399,7 @@ function upsertAiNote({ body, aiNote, logFormat }) {
|
|
|
347
399
|
const clearedAiNote = aiNote.replace(/\n+$/, '');
|
|
348
400
|
let result = body;
|
|
349
401
|
|
|
350
|
-
if (logFormat ===
|
|
402
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
351
403
|
const formattedAiNote = clearedAiNote.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
|
352
404
|
const aiNoteRegex = /<div><b>AI Note<\/b><br>(.+?)<\/div>/;
|
|
353
405
|
if (aiNoteRegex.test(result)) {
|
|
@@ -355,6 +407,14 @@ function upsertAiNote({ body, aiNote, logFormat }) {
|
|
|
355
407
|
} else {
|
|
356
408
|
result += `<div><b>AI Note</b><br>${formattedAiNote}</div><br>`;
|
|
357
409
|
}
|
|
410
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
411
|
+
// Markdown format: ### AI Note
|
|
412
|
+
const aiNoteRegex = /### AI Note\n([\s\S]*?)(?=\n### |\n$|$)/;
|
|
413
|
+
if (aiNoteRegex.test(result)) {
|
|
414
|
+
result = result.replace(aiNoteRegex, `### AI Note\n${clearedAiNote}\n`);
|
|
415
|
+
} else {
|
|
416
|
+
result += `### AI Note\n${clearedAiNote}\n`;
|
|
417
|
+
}
|
|
358
418
|
} else {
|
|
359
419
|
const aiNoteRegex = /- AI Note:([\s\S]*?)--- END/;
|
|
360
420
|
if (aiNoteRegex.test(result)) {
|
|
@@ -371,7 +431,7 @@ function upsertTranscript({ body, transcript, logFormat }) {
|
|
|
371
431
|
|
|
372
432
|
let result = body;
|
|
373
433
|
|
|
374
|
-
if (logFormat ===
|
|
434
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
375
435
|
const formattedTranscript = transcript.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
|
376
436
|
const transcriptRegex = /<div><b>Transcript<\/b><br>(.+?)<\/div>/;
|
|
377
437
|
if (transcriptRegex.test(result)) {
|
|
@@ -379,6 +439,14 @@ function upsertTranscript({ body, transcript, logFormat }) {
|
|
|
379
439
|
} else {
|
|
380
440
|
result += `<div><b>Transcript</b><br>${formattedTranscript}</div><br>`;
|
|
381
441
|
}
|
|
442
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
443
|
+
// Markdown format: ### Transcript
|
|
444
|
+
const transcriptRegex = /### Transcript\n([\s\S]*?)(?=\n### |\n$|$)/;
|
|
445
|
+
if (transcriptRegex.test(result)) {
|
|
446
|
+
result = result.replace(transcriptRegex, `### Transcript\n${transcript}\n`);
|
|
447
|
+
} else {
|
|
448
|
+
result += `### Transcript\n${transcript}\n`;
|
|
449
|
+
}
|
|
382
450
|
} else {
|
|
383
451
|
const transcriptRegex = /- Transcript:([\s\S]*?)--- END/;
|
|
384
452
|
if (transcriptRegex.test(result)) {
|
|
@@ -401,43 +469,9 @@ function getLogFormatType(platform) {
|
|
|
401
469
|
return platformConfig?.logFormat;
|
|
402
470
|
}
|
|
403
471
|
|
|
404
|
-
/**
|
|
405
|
-
* Create a specialized composition function for specific CRM requirements
|
|
406
|
-
* @param {string} platform - CRM platform name
|
|
407
|
-
* @returns {Function} Customized composition function
|
|
408
|
-
*/
|
|
409
|
-
function createComposer(platform) {
|
|
410
|
-
const logFormat = getLogFormatType(platform);
|
|
411
|
-
|
|
412
|
-
return async function (params) {
|
|
413
|
-
// Add platform-specific formatting
|
|
414
|
-
if (logFormat === FORMAT_TYPES.HTML && platform === 'pipedrive') {
|
|
415
|
-
// Pipedrive wraps call details in <ul> tags
|
|
416
|
-
const composed = await composeCallLog({ ...params, logFormat });
|
|
417
|
-
if (composed && !composed.includes('<ul>')) {
|
|
418
|
-
return `<b>Call details</b><ul>${composed}</ul>`;
|
|
419
|
-
}
|
|
420
|
-
return composed;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (logFormat === FORMAT_TYPES.HTML && platform === 'bullhorn') {
|
|
424
|
-
// Bullhorn also wraps call details in <ul> tags
|
|
425
|
-
const composed = await composeCallLog({ ...params, logFormat });
|
|
426
|
-
if (composed && !composed.includes('<ul>')) {
|
|
427
|
-
return `<b>Call details</b><ul>${composed}</ul>`;
|
|
428
|
-
}
|
|
429
|
-
return composed;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
return composeCallLog({ ...params, logFormat });
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
|
|
436
472
|
module.exports = {
|
|
437
473
|
composeCallLog,
|
|
438
|
-
createComposer,
|
|
439
474
|
getLogFormatType,
|
|
440
|
-
FORMAT_TYPES,
|
|
441
475
|
// Export individual upsert functions for backward compatibility
|
|
442
476
|
upsertCallAgentNote,
|
|
443
477
|
upsertCallSessionId,
|
package/lib/constants.js
ADDED
package/lib/oauth.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* eslint-disable no-param-reassign */
|
|
2
2
|
const ClientOAuth2 = require('client-oauth2');
|
|
3
|
-
const { Lock } = require('../models/dynamo/lockSchema');
|
|
4
3
|
const { UserModel } = require('../models/userModel');
|
|
5
4
|
const adapterRegistry = require('../adapter/registry');
|
|
5
|
+
const dynamoose = require('dynamoose');
|
|
6
6
|
|
|
7
7
|
// oauthApp strategy is default to 'code' which use credentials to get accessCode, then exchange for accessToken and refreshToken.
|
|
8
8
|
// To change to other strategies, please refer to: https://github.com/mulesoft-labs/js-client-oauth2
|
|
@@ -31,33 +31,67 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 10)
|
|
|
31
31
|
}
|
|
32
32
|
// Other CRMs
|
|
33
33
|
if (user && user.accessToken && user.refreshToken && tokenExpiry.getTime() < (dateNow.getTime() + expiryBuffer)) {
|
|
34
|
+
let newLock;
|
|
34
35
|
// case: use dynamoDB to manage token refresh lock
|
|
35
36
|
if (process.env.USE_TOKEN_REFRESH_LOCK === 'true') {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
await
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
37
|
+
const { Lock } = require('../models/dynamo/lockSchema');
|
|
38
|
+
// Try to atomically create lock only if it doesn't exist
|
|
39
|
+
try {
|
|
40
|
+
newLock = await Lock.create(
|
|
41
|
+
{
|
|
42
|
+
userId: user.id,
|
|
43
|
+
ttl: dateNow.getTime() + 1000 * 30
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
condition: new dynamoose.Condition().where('userId').not().exists()
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
console.log('lock created')
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// If creation failed due to condition, a lock exists
|
|
52
|
+
if (e.name === 'ConditionalCheckFailedException' || e.__type === 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException') {
|
|
53
|
+
let lock = await Lock.get({ userId: user.id });
|
|
54
|
+
if (!!lock?.ttl && lock.ttl < dateNow.getTime()) {
|
|
55
|
+
// Try to delete expired lock and create a new one atomically
|
|
56
|
+
try {
|
|
57
|
+
await lock.delete();
|
|
58
|
+
newLock = await Lock.create(
|
|
59
|
+
{ userId: user.id },
|
|
60
|
+
{
|
|
61
|
+
condition: new dynamoose.Condition().where('userId').not().exists()
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
} catch (e2) {
|
|
65
|
+
if (e2.name === 'ConditionalCheckFailedException' || e2.__type === 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException') {
|
|
66
|
+
// Another process created a lock between our delete and create
|
|
67
|
+
lock = await Lock.get({ userId: user.id });
|
|
68
|
+
} else {
|
|
69
|
+
throw e2;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (lock && !newLock) {
|
|
75
|
+
let processTime = 0;
|
|
76
|
+
while (!!lock && processTime < tokenLockTimeout) {
|
|
77
|
+
await new Promise(resolve => setTimeout(resolve, 2000)); // wait for 2 seconds
|
|
78
|
+
processTime += 2;
|
|
79
|
+
lock = await Lock.get({ userId: user.id });
|
|
80
|
+
}
|
|
81
|
+
// Timeout -> let users try another time
|
|
82
|
+
if (processTime >= tokenLockTimeout) {
|
|
83
|
+
throw new Error('Token lock timeout');
|
|
84
|
+
}
|
|
85
|
+
user = await UserModel.findByPk(user.id);
|
|
86
|
+
console.log('locked. bypass')
|
|
87
|
+
return user;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
throw e;
|
|
52
91
|
}
|
|
53
|
-
user = await UserModel.findByPk(user.id);
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
newLock = await Lock.create({
|
|
57
|
-
userId: user.id
|
|
58
|
-
});
|
|
59
92
|
}
|
|
60
93
|
const token = oauthApp.createToken(user.accessToken, user.refreshToken);
|
|
94
|
+
console.log('token refreshing...')
|
|
61
95
|
const { accessToken, refreshToken, expires } = await token.refresh();
|
|
62
96
|
user.accessToken = accessToken;
|
|
63
97
|
user.refreshToken = refreshToken;
|
|
@@ -66,15 +100,18 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 10)
|
|
|
66
100
|
if (newLock) {
|
|
67
101
|
await newLock.delete();
|
|
68
102
|
}
|
|
103
|
+
console.log('token refreshing finished')
|
|
69
104
|
}
|
|
70
105
|
// case: run withou token refresh lock
|
|
71
106
|
else {
|
|
107
|
+
console.log('token refreshing...')
|
|
72
108
|
const token = oauthApp.createToken(user.accessToken, user.refreshToken);
|
|
73
109
|
const { accessToken, refreshToken, expires } = await token.refresh();
|
|
74
110
|
user.accessToken = accessToken;
|
|
75
111
|
user.refreshToken = refreshToken;
|
|
76
112
|
user.tokenExpiry = expires;
|
|
77
113
|
await user.save();
|
|
114
|
+
console.log('token refreshing finished')
|
|
78
115
|
}
|
|
79
116
|
|
|
80
117
|
}
|
package/package.json
CHANGED
package/releaseNotes.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.5.8": {
|
|
3
|
+
"global": [
|
|
4
|
+
{
|
|
5
|
+
"type": "Fix",
|
|
6
|
+
"description": "- Error on showing a window-size warning message"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"type": "Fix",
|
|
10
|
+
"description": "- Disconnected users shown as connected"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"type": "Better",
|
|
14
|
+
"description": "- Every 5min, retroatively check recording links at pending state and update them to existing call logs"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"type": "Better",
|
|
18
|
+
"description": "- More stable user session"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "Better",
|
|
22
|
+
"description": "- User settings sync message suppressed"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"type": "Better",
|
|
26
|
+
"description": "- Webpage embed (Click-to-dial and Quick-access-button) urls setting is renamed to 'Enabled domains' under General -> Appearance"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
"1.5.7": {
|
|
3
31
|
"global": [
|
|
4
32
|
{
|