@app-connect/core 1.7.25 → 1.7.27

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.
Files changed (138) hide show
  1. package/.env.test +5 -5
  2. package/README.md +441 -441
  3. package/connector/developerPortal.js +31 -31
  4. package/connector/mock.js +84 -77
  5. package/connector/proxy/engine.js +164 -164
  6. package/connector/proxy/index.js +500 -500
  7. package/connector/registry.js +252 -252
  8. package/docs/README.md +50 -50
  9. package/docs/architecture.md +93 -93
  10. package/docs/connectors.md +116 -116
  11. package/docs/handlers.md +125 -125
  12. package/docs/libraries.md +101 -101
  13. package/docs/models.md +144 -144
  14. package/docs/routes.md +115 -115
  15. package/docs/tests.md +73 -73
  16. package/handlers/admin.js +523 -523
  17. package/handlers/appointment.js +193 -0
  18. package/handlers/auth.js +320 -296
  19. package/handlers/calldown.js +99 -99
  20. package/handlers/contact.js +280 -280
  21. package/handlers/disposition.js +82 -80
  22. package/handlers/log.js +990 -973
  23. package/handlers/managedAuth.js +446 -446
  24. package/handlers/managedOAuth.js +247 -0
  25. package/handlers/plugin.js +208 -208
  26. package/handlers/user.js +142 -142
  27. package/index.js +3162 -2652
  28. package/jest.config.js +56 -56
  29. package/lib/analytics.js +54 -54
  30. package/lib/authSession.js +109 -109
  31. package/lib/cacheCleanup.js +21 -0
  32. package/lib/callLogComposer.js +898 -898
  33. package/lib/callLogLookup.js +34 -0
  34. package/lib/constants.js +8 -8
  35. package/lib/debugTracer.js +177 -177
  36. package/lib/encode.js +30 -30
  37. package/lib/errorHandler.js +218 -206
  38. package/lib/generalErrorMessage.js +41 -41
  39. package/lib/jwt.js +18 -18
  40. package/lib/logger.js +190 -190
  41. package/lib/migrateCallLogsSchema.js +116 -0
  42. package/lib/ringcentral.js +266 -266
  43. package/lib/s3ErrorLogReport.js +65 -65
  44. package/lib/sharedSMSComposer.js +471 -471
  45. package/lib/util.js +67 -67
  46. package/mcp/README.md +412 -395
  47. package/mcp/lib/validator.js +91 -91
  48. package/mcp/mcpHandler.js +425 -425
  49. package/mcp/tools/cancelAppointment.js +101 -0
  50. package/mcp/tools/checkAuthStatus.js +105 -105
  51. package/mcp/tools/confirmAppointment.js +101 -0
  52. package/mcp/tools/createAppointment.js +157 -0
  53. package/mcp/tools/createCallLog.js +327 -316
  54. package/mcp/tools/createContact.js +117 -117
  55. package/mcp/tools/createMessageLog.js +287 -287
  56. package/mcp/tools/doAuth.js +60 -60
  57. package/mcp/tools/findContactByName.js +93 -93
  58. package/mcp/tools/findContactByPhone.js +101 -101
  59. package/mcp/tools/getCallLog.js +111 -102
  60. package/mcp/tools/getGoogleFilePicker.js +99 -99
  61. package/mcp/tools/getHelp.js +43 -43
  62. package/mcp/tools/getPublicConnectors.js +94 -94
  63. package/mcp/tools/getSessionInfo.js +90 -90
  64. package/mcp/tools/index.js +51 -41
  65. package/mcp/tools/listAppointments.js +163 -0
  66. package/mcp/tools/logout.js +99 -96
  67. package/mcp/tools/rcGetCallLogs.js +65 -65
  68. package/mcp/tools/updateAppointment.js +154 -0
  69. package/mcp/tools/updateCallLog.js +130 -126
  70. package/mcp/ui/App/App.tsx +358 -358
  71. package/mcp/ui/App/components/AuthInfoForm.tsx +113 -113
  72. package/mcp/ui/App/components/AuthSuccess.tsx +22 -22
  73. package/mcp/ui/App/components/ConnectorList.tsx +82 -82
  74. package/mcp/ui/App/components/DebugPanel.tsx +43 -43
  75. package/mcp/ui/App/components/OAuthConnect.tsx +270 -270
  76. package/mcp/ui/App/lib/callTool.ts +130 -130
  77. package/mcp/ui/App/lib/debugLog.ts +41 -41
  78. package/mcp/ui/App/lib/developerPortal.ts +111 -111
  79. package/mcp/ui/App/main.css +5 -5
  80. package/mcp/ui/App/root.tsx +13 -13
  81. package/mcp/ui/index.html +13 -13
  82. package/mcp/ui/package-lock.json +6356 -6356
  83. package/mcp/ui/package.json +25 -25
  84. package/mcp/ui/tsconfig.json +26 -26
  85. package/mcp/ui/vite.config.ts +16 -16
  86. package/models/accountDataModel.js +33 -33
  87. package/models/adminConfigModel.js +35 -35
  88. package/models/cacheModel.js +30 -26
  89. package/models/callDownListModel.js +34 -34
  90. package/models/callLogModel.js +33 -27
  91. package/models/dynamo/connectorSchema.js +146 -146
  92. package/models/dynamo/lockSchema.js +24 -24
  93. package/models/dynamo/noteCacheSchema.js +29 -29
  94. package/models/llmSessionModel.js +17 -17
  95. package/models/messageLogModel.js +25 -25
  96. package/models/sequelize.js +16 -16
  97. package/models/userModel.js +45 -45
  98. package/package.json +1 -1
  99. package/releaseNotes.json +1101 -1081
  100. package/test/connector/proxy/engine.test.js +126 -126
  101. package/test/connector/proxy/index.test.js +279 -279
  102. package/test/connector/proxy/sample.json +161 -161
  103. package/test/connector/registry.test.js +415 -415
  104. package/test/handlers/admin.test.js +616 -616
  105. package/test/handlers/auth.test.js +1092 -1018
  106. package/test/handlers/contact.test.js +1014 -1014
  107. package/test/handlers/log.test.js +1298 -1160
  108. package/test/handlers/managedAuth.test.js +457 -457
  109. package/test/handlers/plugin.test.js +380 -380
  110. package/test/index.test.js +105 -105
  111. package/test/lib/cacheCleanup.test.js +42 -0
  112. package/test/lib/callLogComposer.test.js +1231 -1231
  113. package/test/lib/debugTracer.test.js +328 -328
  114. package/test/lib/jwt.test.js +176 -176
  115. package/test/lib/logger.test.js +206 -206
  116. package/test/lib/oauth.test.js +359 -359
  117. package/test/lib/ringcentral.test.js +467 -467
  118. package/test/lib/sharedSMSComposer.test.js +1084 -1084
  119. package/test/lib/util.test.js +329 -329
  120. package/test/mcp/tools/checkAuthStatus.test.js +83 -83
  121. package/test/mcp/tools/createCallLog.test.js +436 -436
  122. package/test/mcp/tools/createContact.test.js +58 -58
  123. package/test/mcp/tools/createMessageLog.test.js +595 -595
  124. package/test/mcp/tools/doAuth.test.js +113 -113
  125. package/test/mcp/tools/findContactByName.test.js +275 -275
  126. package/test/mcp/tools/findContactByPhone.test.js +296 -296
  127. package/test/mcp/tools/getCallLog.test.js +298 -298
  128. package/test/mcp/tools/getGoogleFilePicker.test.js +281 -281
  129. package/test/mcp/tools/getPublicConnectors.test.js +107 -107
  130. package/test/mcp/tools/getSessionInfo.test.js +127 -127
  131. package/test/mcp/tools/logout.test.js +233 -233
  132. package/test/mcp/tools/rcGetCallLogs.test.js +56 -56
  133. package/test/mcp/tools/updateCallLog.test.js +360 -360
  134. package/test/models/accountDataModel.test.js +98 -98
  135. package/test/models/dynamo/connectorSchema.test.js +189 -189
  136. package/test/models/models.test.js +568 -539
  137. package/test/routes/managedAuthRoutes.test.js +235 -104
  138. package/test/setup.js +178 -178
package/handlers/log.js CHANGED
@@ -1,973 +1,990 @@
1
- const Op = require('sequelize').Op;
2
- const { CallLogModel } = require('../models/callLogModel');
3
- const { MessageLogModel } = require('../models/messageLogModel');
4
- const { UserModel } = require('../models/userModel');
5
- const { CacheModel } = require('../models/cacheModel');
6
- const oauth = require('../lib/oauth');
7
- const { composeCallLog } = require('../lib/callLogComposer');
8
- const { composeSharedSMSLog } = require('../lib/sharedSMSComposer');
9
- const connectorRegistry = require('../connector/registry');
10
- const { LOG_DETAILS_FORMAT_TYPE } = require('../lib/constants');
11
- const { NoteCache } = require('../models/dynamo/noteCacheSchema');
12
- const { Connector } = require('../models/dynamo/connectorSchema');
13
- const moment = require('moment');
14
- const { getMediaReaderLinkByPlatformMediaLink } = require('../lib/util');
15
- const axios = require('axios');
16
- const { getPluginsFromUserSettings } = require('../lib/util');
17
- const logger = require('../lib/logger');
18
- const { handleApiError, handleDatabaseError } = require('../lib/errorHandler');
19
- const { v4: uuidv4 } = require('uuid');
20
- const { AccountDataModel } = require('../models/accountDataModel');
21
- const pluginCore = require('./plugin');
22
-
23
- function mergePluginWarnings({ returnMessage, warningMessages }) {
24
- if (!warningMessages.length) {
25
- return returnMessage;
26
- }
27
- const warningMessage = warningMessages.join(' ');
28
- if (!returnMessage) {
29
- return {
30
- message: warningMessage,
31
- messageType: 'warning',
32
- ttl: 5000,
33
- };
34
- }
35
- return {
36
- ...returnMessage,
37
- message: `${returnMessage.message || ''} ${warningMessage}`.trim(),
38
- messageType: returnMessage.messageType === 'error' ? 'error' : 'warning',
39
- };
40
- }
41
-
42
- function getPluginWarningMessage({ pluginId }) {
43
- return `Plugin ${pluginId} skipped: missing account-level plugin jwtToken. Reinstall or re-register plugin.`;
44
- }
45
-
46
- async function createCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
47
- try {
48
- let existingCallLog = null;
49
- try {
50
- existingCallLog = await CallLogModel.findOne({
51
- where: {
52
- sessionId: incomingData.logInfo.sessionId
53
- }
54
- });
55
- }
56
- catch (error) {
57
- return handleDatabaseError(error, 'Error finding existing call log');
58
- }
59
- if (existingCallLog) {
60
- return {
61
- successful: false,
62
- returnMessage: {
63
- message: `Existing log for session ${incomingData.logInfo.sessionId}`,
64
- messageType: 'warning',
65
- ttl: 3000
66
- }
67
- }
68
- }
69
- let user = null;
70
- try {
71
- user = await UserModel.findByPk(userId);
72
- }
73
- catch (error) {
74
- return handleDatabaseError(error, 'Error finding user');
75
- }
76
- if (!user || !user.accessToken) {
77
- return {
78
- successful: false,
79
- returnMessage: {
80
- message: `User not found`,
81
- messageType: 'warning',
82
- ttl: 5000
83
- }
84
- };
85
- }
86
-
87
- const platformModule = connectorRegistry.getConnector(platform);
88
- const callLog = incomingData.logInfo;
89
- const additionalSubmission = incomingData.additionalSubmission;
90
- let note = incomingData.note;
91
- if (process.env.USE_CACHE && isFromSSCL) {
92
- const noteCache = await NoteCache.get({ sessionId: incomingData.logInfo.sessionId });
93
- if (noteCache) {
94
- note = noteCache.note;
95
- }
96
- }
97
- const aiNote = incomingData.aiNote;
98
- const transcript = incomingData.transcript;
99
- let proxyConfig;
100
- const proxyId = user.platformAdditionalInfo?.proxyId;
101
- if (proxyId) {
102
- proxyConfig = await Connector.getProxyConfig(proxyId);
103
- }
104
- const authType = await platformModule.getAuthType({ proxyId, proxyConfig });
105
- let authHeader = '';
106
- switch (authType) {
107
- case 'oauth':
108
- const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
109
- user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
110
- if (!user) {
111
- return {
112
- successful: false,
113
- returnMessage: {
114
- message: `User session expired. Please connect again.`,
115
- messageType: 'warning',
116
- ttl: 5000
117
- },
118
- isRevokeUserSession: true
119
- }
120
- }
121
- authHeader = `Bearer ${user.accessToken}`;
122
- break;
123
- case 'apiKey':
124
- const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
125
- authHeader = `Basic ${basicAuth}`;
126
- break;
127
- }
128
- const contactNumber = callLog.direction === 'Inbound' ? callLog.from.phoneNumber : callLog.to.phoneNumber;
129
- const contactId = incomingData.contactId;
130
- if (!contactId || contactId === 0) {
131
- return {
132
- successful: false,
133
- returnMessage: {
134
- message: `Contact not found for number ${contactNumber}`,
135
- messageType: 'warning',
136
- ttl: 5000
137
- }
138
- };
139
- }
140
- const contactInfo = {
141
- id: contactId,
142
- phoneNumber: contactNumber,
143
- type: incomingData.contactType ?? "",
144
- name: incomingData.contactName ?? ""
145
- };
146
-
147
-
148
- const pluginAsyncTaskIds = [];
149
- const pluginWarnings = [];
150
- // Plugins
151
- const accountPlugins = await pluginCore.getPluginsFromRcAccountId({ rcAccountId: user.rcAccountId });
152
- const callPlugins = accountPlugins.filter(plugin => plugin.data.supportedLogTypes.includes('call'));
153
- for (const plugin of callPlugins) {
154
- const pluginId = plugin.id;
155
- const pluginJwtToken = plugin.data.jwtToken;
156
- const pluginManifest = plugin.data;
157
- const pluginEndpointUrl = pluginManifest.endpointUrl;
158
- if (!pluginEndpointUrl) {
159
- throw new Error('Plugin URL is not set');
160
- }
161
- const userConfig = pluginCore.getPluginConfigFromUserSettings({ userSettings: user.userSettings, pluginId });
162
- if (plugin.data.isAsync) {
163
- const asyncTaskId = `${userId}-${uuidv4()}`;
164
- pluginAsyncTaskIds.push(asyncTaskId);
165
- await CacheModel.create({
166
- id: asyncTaskId,
167
- status: 'initialized',
168
- userId,
169
- cacheKey: `pluginTask-${plugin.data.name}`,
170
- expiry: moment().add(1, 'hour').toDate()
171
- });
172
- try {
173
- const syncPluginTokenResponse = await axios.post(plugin.data.tokenSyncUrl, {},
174
- {
175
- headers: {
176
- Authorization: `Bearer ${pluginJwtToken}`,
177
- }
178
- }
179
- )
180
- const syncedPluginJwtToken = syncPluginTokenResponse?.data?.jwtToken ?? pluginJwtToken;
181
- axios.post(pluginEndpointUrl, {
182
- data: incomingData,
183
- config: userConfig,
184
- asyncTaskId
185
- }, {
186
- headers: {
187
- Authorization: `Bearer ${syncedPluginJwtToken ?? pluginJwtToken}`,
188
- },
189
- });
190
- if (syncedPluginJwtToken) {
191
- pluginCore.persistPluginData({
192
- rcAccountId: user.rcAccountId,
193
- platformName: platform,
194
- pluginId,
195
- jwtToken: syncedPluginJwtToken,
196
- });
197
- }
198
- }
199
- catch (error) {
200
- logger.error('Error syncing plugin JWT token', { stack: error.stack });
201
- }
202
- }
203
- else {
204
- const processedResultResponse = await axios.post(pluginEndpointUrl, {
205
- data: incomingData,
206
- config: userConfig,
207
- }, {
208
- headers: {
209
- Authorization: `Bearer ${pluginJwtToken}`,
210
- },
211
- });
212
- const refreshedPluginJwtToken = pluginCore.getRefreshedJwtTokenFromHeaders({ headers: processedResultResponse.headers });
213
- if (refreshedPluginJwtToken) {
214
- pluginCore.persistPluginData({
215
- rcAccountId: user.rcAccountId,
216
- platformName: platform,
217
- pluginId,
218
- jwtToken: refreshedPluginJwtToken,
219
- });
220
- }
221
- // eslint-disable-next-line no-param-reassign
222
- incomingData = processedResultResponse.data;
223
- note = incomingData.note;
224
- }
225
- }
226
-
227
- // Compose call log details centrally
228
- const logFormat = platformModule.getLogFormatType ? platformModule.getLogFormatType(platform, proxyConfig) : LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT;
229
- let composedLogDetails = '';
230
- if (logFormat === LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT || logFormat === LOG_DETAILS_FORMAT_TYPE.HTML || logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
231
- composedLogDetails = composeCallLog({
232
- logFormat,
233
- callLog,
234
- contactInfo,
235
- user,
236
- note,
237
- aiNote,
238
- transcript,
239
- recordingLink: callLog.recording?.link,
240
- subject: callLog.customSubject,
241
- startTime: callLog.startTime,
242
- duration: callLog.duration,
243
- result: callLog.result,
244
- platform,
245
- ringSenseTranscript: incomingData.ringSenseTranscript,
246
- ringSenseSummary: incomingData.ringSenseSummary,
247
- ringSenseAIScore: incomingData.ringSenseAIScore,
248
- ringSenseBulletedSummary: incomingData.ringSenseBulletedSummary,
249
- ringSenseLink: incomingData.ringSenseLink,
250
- });
251
- }
252
-
253
- let { logId, returnMessage, extraDataTracking } = await platformModule.createCallLog({
254
- user,
255
- contactInfo,
256
- authHeader,
257
- callLog,
258
- note,
259
- additionalSubmission,
260
- aiNote,
261
- transcript,
262
- ringSenseTranscript: incomingData.ringSenseTranscript,
263
- ringSenseSummary: incomingData.ringSenseSummary,
264
- ringSenseAIScore: incomingData.ringSenseAIScore,
265
- ringSenseBulletedSummary: incomingData.ringSenseBulletedSummary,
266
- ringSenseLink: incomingData.ringSenseLink,
267
- composedLogDetails,
268
- hashedAccountId,
269
- isFromSSCL,
270
- proxyConfig,
271
- });
272
- if (!extraDataTracking) {
273
- extraDataTracking = {};
274
- }
275
- extraDataTracking.withSmartNoteLog = !!aiNote;
276
- extraDataTracking.withTranscript = !!transcript;
277
- if (logId) {
278
- try {
279
- await CallLogModel.create({
280
- id: incomingData.logInfo.telephonySessionId || incomingData.logInfo.id,
281
- sessionId: incomingData.logInfo.sessionId,
282
- platform,
283
- thirdPartyLogId: logId,
284
- userId,
285
- contactId
286
- });
287
- }
288
- catch (error) {
289
- return handleDatabaseError(error, 'Error creating call log');
290
- }
291
- return {
292
- successful: !!logId,
293
- logId,
294
- returnMessage: mergePluginWarnings({ returnMessage, warningMessages: pluginWarnings }),
295
- extraDataTracking,
296
- pluginAsyncTaskIds
297
- };
298
- }
299
- } catch (e) {
300
- return handleApiError(e, platform, 'createCallLog', { userId });
301
- }
302
- }
303
-
304
- async function getCallLog({ userId, sessionIds, platform, requireDetails }) {
305
- try {
306
- let user = await UserModel.findByPk(userId);
307
- if (!user || !user.accessToken) {
308
- return { successful: false, message: `Contact not found` };
309
- }
310
- let logs = [];
311
- let returnMessage = null;
312
- let extraDataTracking = {};
313
-
314
- // Handle undefined or null sessionIds
315
- if (!sessionIds) {
316
- return { successful: false, message: `No session IDs provided` };
317
- }
318
-
319
- let sessionIdsArray = sessionIds.split(',');
320
- if (sessionIdsArray.length > 5) {
321
- sessionIdsArray = sessionIdsArray.slice(0, 5);
322
- }
323
- if (requireDetails) {
324
- const proxyId = user.platformAdditionalInfo?.proxyId;
325
- let proxyConfig = null;
326
- if (proxyId) {
327
- proxyConfig = await Connector.getProxyConfig(proxyId);
328
- }
329
- const platformModule = connectorRegistry.getConnector(platform);
330
- const authType = await platformModule.getAuthType({ proxyId, proxyConfig });
331
- let authHeader = '';
332
- switch (authType) {
333
- case 'oauth':
334
- const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
335
- user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
336
- if (!user) {
337
- return {
338
- successful: false,
339
- returnMessage: {
340
- message: `User session expired. Please connect again.`,
341
- messageType: 'warning',
342
- ttl: 5000
343
- },
344
- isRevokeUserSession: true
345
- }
346
- }
347
- authHeader = `Bearer ${user.accessToken}`;
348
- break;
349
- case 'apiKey':
350
- const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
351
- authHeader = `Basic ${basicAuth}`;
352
- break;
353
- }
354
- const callLogs = await CallLogModel.findAll({
355
- where: {
356
- sessionId: {
357
- [Op.in]: sessionIdsArray
358
- }
359
- }
360
- });
361
- for (const sId of sessionIdsArray) {
362
- if (sId == 0) {
363
- logs.push({ sessionId: sId, matched: false });
364
- continue;
365
- }
366
- const callLog = callLogs.find(c => c.sessionId === sId);
367
- if (!callLog) {
368
- logs.push({ sessionId: sId, matched: false });
369
- }
370
- else {
371
- const getCallLogResult = await platformModule.getCallLog({ user, telephonySessionId: callLog.id, callLogId: callLog.thirdPartyLogId, contactId: callLog.contactId, authHeader, proxyConfig });
372
- returnMessage = getCallLogResult.returnMessage;
373
- extraDataTracking = getCallLogResult.extraDataTracking;
374
- logs.push({ sessionId: callLog.sessionId, matched: true, logId: callLog.thirdPartyLogId, logData: getCallLogResult.callLogInfo });
375
- }
376
- }
377
- }
378
- else {
379
- const callLogs = await CallLogModel.findAll({
380
- where: {
381
- sessionId: {
382
- [Op.in]: sessionIdsArray
383
- }
384
- }
385
- });
386
- for (const sId of sessionIdsArray) {
387
- const callLog = callLogs.find(c => c.sessionId === sId);
388
- if (!callLog) {
389
- logs.push({ sessionId: sId, matched: false });
390
- }
391
- else {
392
- logs.push({ sessionId: callLog.sessionId, matched: true, logId: callLog.thirdPartyLogId });
393
- }
394
- }
395
- }
396
- return { successful: true, logs, returnMessage, extraDataTracking };
397
- }
398
- catch (e) {
399
- return handleApiError(e, platform, 'getCallLog', { userId, sessionIds, requireDetails });
400
- }
401
- }
402
-
403
- async function updateCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
404
- try {
405
- let existingCallLog = null;
406
- try {
407
- existingCallLog = await CallLogModel.findOne({
408
- where: {
409
- sessionId: incomingData.sessionId
410
- }
411
- });
412
- }
413
- catch (error) {
414
- return handleDatabaseError(error, 'Error finding existing call log');
415
- }
416
- if (existingCallLog) {
417
- let user = await UserModel.findByPk(userId);
418
- if (!user || !user.accessToken) {
419
- return { successful: false, message: `Contact not found` };
420
- }
421
- const platformModule = connectorRegistry.getConnector(platform);
422
- const proxyId = user.platformAdditionalInfo?.proxyId;
423
- let proxyConfig = null;
424
- if (proxyId) {
425
- proxyConfig = await Connector.getProxyConfig(proxyId);
426
- }
427
- const authType = await platformModule.getAuthType({ proxyId, proxyConfig });
428
- let authHeader = '';
429
- switch (authType) {
430
- case 'oauth':
431
- const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
432
- user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
433
- if (!user) {
434
- return {
435
- successful: false,
436
- returnMessage: {
437
- message: `User session expired. Please connect again.`,
438
- messageType: 'warning',
439
- ttl: 5000
440
- },
441
- isRevokeUserSession: true
442
- }
443
- }
444
- authHeader = `Bearer ${user.accessToken}`;
445
- break;
446
- case 'apiKey':
447
- const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
448
- authHeader = `Basic ${basicAuth}`;
449
- break;
450
- }
451
-
452
- const pluginAsyncTaskIds = [];
453
- const pluginWarnings = [];
454
- // Plugins
455
- const accountPlugins = await pluginCore.getPluginsFromRcAccountId({ rcAccountId: user.rcAccountId });
456
- const callPlugins = accountPlugins.filter(plugin => plugin.data.supportedLogTypes.includes('call'));
457
- for (const plugin of callPlugins) {
458
- const pluginId = plugin.id;
459
- const pluginJwtToken = plugin.data.jwtToken;
460
- const pluginManifest = plugin.data;
461
- const pluginEndpointUrl = pluginManifest.endpointUrl;
462
- if (!pluginEndpointUrl) {
463
- throw new Error('Plugin URL is not set');
464
- }
465
- const userConfig = pluginCore.getPluginConfigFromUserSettings({ userSettings: user.userSettings, pluginId });
466
- if (plugin.data.isAsync) {
467
- const asyncTaskId = `${userId}-${uuidv4()}`;
468
- pluginAsyncTaskIds.push(asyncTaskId);
469
- await CacheModel.create({
470
- id: asyncTaskId,
471
- status: 'initialized',
472
- userId,
473
- cacheKey: `pluginTask-${plugin.data.name}`,
474
- expiry: moment().add(1, 'hour').toDate()
475
- });
476
- try {
477
- const syncPluginTokenResponse = await axios.post(plugin.data.tokenSyncUrl, {},
478
- {
479
- headers: {
480
- Authorization: `Bearer ${pluginJwtToken}`,
481
- },
482
- }
483
- );
484
- const syncedPluginJwtToken = pluginCore.getRefreshedJwtTokenFromHeaders({ headers: syncPluginTokenResponse.headers });
485
- axios.post(pluginEndpointUrl, {
486
- data: { logInfo: incomingData },
487
- config: userConfig,
488
- asyncTaskId
489
- }, {
490
- headers: {
491
- Authorization: `Bearer ${syncedPluginJwtToken ?? pluginJwtToken}`,
492
- },
493
- });
494
- if (syncedPluginJwtToken) {
495
- pluginCore.persistPluginData({
496
- rcAccountId: user.rcAccountId,
497
- platformName: platform,
498
- pluginId,
499
- jwtToken: syncedPluginJwtToken,
500
- });
501
- }
502
- }
503
- catch (error) {
504
- logger.error('Error syncing plugin JWT token', { stack: error.stack });
505
- }
506
- }
507
- else {
508
- const processedResultResponse = await axios.post(pluginEndpointUrl, {
509
- data: incomingData,
510
- config: userConfig
511
- }, {
512
- headers: {
513
- Authorization: `Bearer ${pluginJwtToken}`,
514
- },
515
- });
516
- const refreshedPluginJwtToken = pluginCore.getRefreshedJwtTokenFromHeaders({ headers: processedResultResponse.headers });
517
- if (refreshedPluginJwtToken) {
518
- pluginCore.persistPluginData({
519
- rcAccountId: user.rcAccountId,
520
- platformName: platform,
521
- pluginId,
522
- jwtToken: refreshedPluginJwtToken,
523
- });
524
- }
525
- // eslint-disable-next-line no-param-reassign
526
- incomingData = processedResultResponse.data;
527
- }
528
- }
529
-
530
- // Fetch existing call log details once to avoid duplicate API calls
531
- let existingCallLogDetails = null; // Compose updated call log details centrally
532
- const logFormat = platformModule.getLogFormatType ? platformModule.getLogFormatType(platform, proxyConfig) : LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT;
533
- let composedLogDetails = '';
534
- if (logFormat === LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT || logFormat === LOG_DETAILS_FORMAT_TYPE.HTML || logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
535
- let existingBody = '';
536
- try {
537
- const getLogResult = await platformModule.getCallLog({
538
- user,
539
- telephonySessionId: existingCallLog.id,
540
- callLogId: existingCallLog.thirdPartyLogId,
541
- contactId: existingCallLog.contactId,
542
- authHeader,
543
- proxyConfig,
544
- });
545
- existingCallLogDetails = getLogResult?.callLogInfo?.fullLogResponse;
546
- // Extract existing body from the platform-specific response
547
- if (getLogResult.callLogInfo?.fullBody) {
548
- existingBody = getLogResult.callLogInfo.fullBody;
549
- } else if (getLogResult.callLogInfo?.note) {
550
- existingBody = getLogResult.callLogInfo.note;
551
- }
552
- } catch (error) {
553
- logger.error('Error getting existing log details, proceeding with empty body', { stack: error.stack });
554
- }
555
- composedLogDetails = composeCallLog({
556
- logFormat,
557
- existingBody,
558
- callLog: {
559
- sessionId: existingCallLog.sessionId,
560
- startTime: incomingData.startTime,
561
- duration: incomingData.duration,
562
- result: incomingData.result,
563
- direction: incomingData.direction,
564
- from: incomingData.from,
565
- to: incomingData.to,
566
- legs: incomingData.legs || [],
567
- },
568
- contactInfo: null, // Not needed for updates
569
- user,
570
- note: incomingData.note,
571
- aiNote: incomingData.aiNote,
572
- transcript: incomingData.transcript,
573
- recordingLink: incomingData.recordingLink,
574
- subject: incomingData.subject,
575
- startTime: incomingData.startTime,
576
- duration: incomingData.duration,
577
- result: incomingData.result,
578
- ringSenseTranscript: incomingData.ringSenseTranscript,
579
- ringSenseSummary: incomingData.ringSenseSummary,
580
- ringSenseAIScore: incomingData.ringSenseAIScore,
581
- ringSenseBulletedSummary: incomingData.ringSenseBulletedSummary,
582
- ringSenseLink: incomingData.ringSenseLink,
583
- });
584
- }
585
-
586
- let { updatedNote, returnMessage, extraDataTracking } = await platformModule.updateCallLog({
587
- user,
588
- existingCallLog,
589
- authHeader,
590
- recordingLink: incomingData.recordingLink,
591
- recordingDownloadLink: incomingData.recordingDownloadLink,
592
- subject: incomingData.subject,
593
- note: incomingData.note,
594
- startTime: incomingData.startTime,
595
- duration: incomingData.duration,
596
- result: incomingData.result,
597
- aiNote: incomingData.aiNote,
598
- transcript: incomingData.transcript,
599
- legs: incomingData.legs || [],
600
- ringSenseTranscript: incomingData.ringSenseTranscript,
601
- ringSenseSummary: incomingData.ringSenseSummary,
602
- ringSenseAIScore: incomingData.ringSenseAIScore,
603
- ringSenseBulletedSummary: incomingData.ringSenseBulletedSummary,
604
- ringSenseLink: incomingData.ringSenseLink,
605
- additionalSubmission: incomingData.additionalSubmission,
606
- composedLogDetails,
607
- existingCallLogDetails, // Pass the fetched details to avoid duplicate API calls
608
- hashedAccountId,
609
- isFromSSCL,
610
- proxyConfig,
611
- });
612
- return {
613
- successful: true,
614
- logId: existingCallLog.thirdPartyLogId,
615
- updatedNote,
616
- returnMessage: mergePluginWarnings({ returnMessage, warningMessages: pluginWarnings }),
617
- extraDataTracking,
618
- pluginAsyncTaskIds
619
- };
620
- }
621
- return { successful: false };
622
- } catch (e) {
623
- return handleApiError(e, platform, 'updateCallLog', { userId });
624
- }
625
- }
626
-
627
- async function createMessageLog({ platform, userId, incomingData }) {
628
- try {
629
- let returnMessage = null;
630
- let extraDataTracking = {};
631
- if (incomingData.logInfo.messages.length === 0) {
632
- return {
633
- successful: false,
634
- returnMessage:
635
- {
636
- message: 'No message to log.',
637
- messageType: 'warning',
638
- ttl: 3000
639
- }
640
- }
641
- }
642
- const platformModule = connectorRegistry.getConnector(platform);
643
- const contactNumber = incomingData.logInfo.correspondents[0].phoneNumber;
644
- const additionalSubmission = incomingData.additionalSubmission;
645
- let user = null;
646
- try {
647
- user = await UserModel.findByPk(userId);
648
- }
649
- catch (error) {
650
- return handleDatabaseError(error, 'Error finding user');
651
- }
652
- if (!user || !user.accessToken) {
653
- return {
654
- successful: false,
655
- returnMessage:
656
- {
657
- message: `Contact not found`,
658
- messageType: 'warning',
659
- ttl: 5000
660
- }
661
- };
662
- }
663
- const proxyId = user.platformAdditionalInfo?.proxyId;
664
- let proxyConfig = null;
665
- if (proxyId) {
666
- proxyConfig = await Connector.getProxyConfig(proxyId);
667
- }
668
- const authType = await platformModule.getAuthType({ proxyId, proxyConfig });
669
- let authHeader = '';
670
- switch (authType) {
671
- case 'oauth':
672
- const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
673
- user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
674
- if (!user) {
675
- return {
676
- successful: false,
677
- returnMessage: {
678
- message: `User session expired. Please connect again.`,
679
- messageType: 'warning',
680
- ttl: 5000
681
- },
682
- isRevokeUserSession: true
683
- }
684
- }
685
- authHeader = `Bearer ${user.accessToken}`;
686
- break;
687
- case 'apiKey':
688
- const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
689
- authHeader = `Basic ${basicAuth}`;
690
- break;
691
- }
692
- const contactId = incomingData.contactId;
693
- if (!contactId) {
694
- return {
695
- successful: false,
696
- returnMessage:
697
- {
698
- message: `Contact not found for number ${contactNumber}`,
699
- messageType: 'warning',
700
- ttl: 5000
701
- }
702
- };
703
- }
704
- const contactInfo = {
705
- id: contactId,
706
- phoneNumber: contactNumber,
707
- type: incomingData.contactType ?? "",
708
- name: incomingData.contactName ?? ""
709
- };
710
- const isGroupSMS = incomingData.logInfo.correspondents.length > 1;
711
- // For shared SMS
712
- const assigneeName = incomingData.logInfo.assignee?.name;
713
- const ownerName = incomingData.logInfo.owner?.name;
714
- const isSharedSMS = !!ownerName;
715
-
716
- const pluginAsyncTaskIds = [];
717
- const pluginWarnings = [];
718
- // Plugins
719
- const isSMS = incomingData.logInfo.messages.some(m => m.type === 'SMS');
720
- const isFax = incomingData.logInfo.messages.some(m => m.type === 'Fax');
721
- const accountPlugins = await pluginCore.getPluginsFromRcAccountId({ rcAccountId: user.rcAccountId });
722
- const smsPlugins = isSMS ? accountPlugins.filter(plugin => plugin.data.supportedLogTypes.includes('sms')) : [];
723
- const faxPlugins = isFax ? accountPlugins.filter(plugin => plugin.data.supportedLogTypes.includes('fax')) : [];
724
- const plugins = [...smsPlugins, ...faxPlugins];
725
- for (const plugin of plugins) {
726
- const pluginId = plugin.id;
727
- const pluginJwtToken = plugin.data.jwtToken;
728
- const pluginManifest = plugin.data;
729
- const pluginEndpointUrl = pluginManifest.endpointUrl;
730
- if (!pluginEndpointUrl) {
731
- throw new Error('Plugin URL is not set');
732
- }
733
- const userConfig = pluginCore.getPluginConfigFromUserSettings({ userSettings: user.userSettings, pluginId });
734
- if (plugin.data.isAsync) {
735
- const asyncTaskId = `${userId}-${uuidv4()}`;
736
- pluginAsyncTaskIds.push(asyncTaskId);
737
- await CacheModel.create({
738
- id: asyncTaskId,
739
- status: 'initialized',
740
- userId,
741
- cacheKey: `pluginTask-${plugin.data.name}`,
742
- expiry: moment().add(1, 'hour').toDate()
743
- });
744
- try {
745
- const syncPluginTokenResponse = await axios.post(plugin.data.tokenSyncUrl, {},
746
- {
747
- headers: {
748
- Authorization: `Bearer ${pluginJwtToken}`,
749
- },
750
- }
751
- );
752
- const syncedPluginJwtToken = pluginCore.getRefreshedJwtTokenFromHeaders({ headers: syncPluginTokenResponse.headers });
753
- axios.post(pluginEndpointUrl, {
754
- data: { logInfo: incomingData },
755
- config: userConfig,
756
- asyncTaskId
757
- }, {
758
- headers: {
759
- Authorization: `Bearer ${syncedPluginJwtToken ?? pluginJwtToken}`,
760
- },
761
- });
762
- if (syncedPluginJwtToken) {
763
- pluginCore.persistPluginData({
764
- rcAccountId: user.rcAccountId,
765
- platformName: platform,
766
- pluginId,
767
- jwtToken: syncedPluginJwtToken,
768
- });
769
- }
770
- }
771
- catch (error) {
772
- logger.error('Error syncing plugin JWT token', { stack: error.stack });
773
- }
774
- }
775
- else {
776
- const processedResultResponse = await axios.post(pluginEndpointUrl, {
777
- data: incomingData,
778
- config: userConfig,
779
- }, {
780
- headers: {
781
- Authorization: `Bearer ${pluginJwtToken}`,
782
- },
783
- });
784
- const refreshedPluginJwtToken = pluginCore.getRefreshedJwtTokenFromHeaders({ headers: processedResultResponse.headers });
785
- if (refreshedPluginJwtToken) {
786
- pluginCore.persistPluginData({
787
- rcAccountId: user.rcAccountId,
788
- platformName: platform,
789
- pluginId,
790
- jwtToken: refreshedPluginJwtToken,
791
- });
792
- }
793
- // eslint-disable-next-line no-param-reassign
794
- incomingData = processedResultResponse.data;
795
- }
796
- }
797
-
798
- let messageIds = [];
799
- const correspondents = [];
800
- if (isGroupSMS) {
801
- messageIds = incomingData.logInfo.messages.map(m => { return { id: m.id.toString() + `-${incomingData.contactId}` }; });
802
- for (var i = 0; i < incomingData.logInfo.correspondents.length; i++) {
803
- // find cached contact by composite key; findByPk expects raw PK values, so use where clause
804
- const correspondentContactInfo = await AccountDataModel.findOne({
805
- where: {
806
- rcAccountId: user.rcAccountId,
807
- platformName: platform,
808
- dataKey: `contact-${incomingData.logInfo.correspondents[i].phoneNumber}`
809
- }
810
- })
811
- if (correspondentContactInfo && correspondentContactInfo.data[0]?.name != incomingData.contactName) {
812
- correspondents.push(correspondentContactInfo.data);
813
- }
814
- }
815
- }
816
- else {
817
- messageIds = incomingData.logInfo.messages.map(m => { return { id: m.id.toString() }; });
818
- }
819
- let existingMessages = null;
820
- try {
821
- existingMessages = await MessageLogModel.findAll({
822
- where: {
823
- [Op.or]: messageIds
824
- }
825
- });
826
- }
827
- catch (error) {
828
- return handleDatabaseError(error, 'Error finding existing messages');
829
- }
830
- const existingIds = existingMessages.map(m => m.id);
831
- const logIds = [];
832
- // Case: Shared SMS
833
- if (isSharedSMS) {
834
- const existingMessageLog = await MessageLogModel.findOne({
835
- where: {
836
- conversationLogId: incomingData.logInfo.conversationLogId
837
- }
838
- });
839
- const sharedSMSLogContent = composeSharedSMSLog({ logFormat: platformModule.getLogFormatType(platform, proxyConfig), conversation: incomingData.logInfo, contactName: contactInfo.name, timezoneOffset: user.timezoneOffset });
840
- if (existingMessageLog) {
841
- const updateMessageResult = await platformModule.updateMessageLog({ user, contactInfo, sharedSMSLogContent, existingMessageLog: existingMessageLog, authHeader, additionalSubmission, proxyConfig });
842
- returnMessage = updateMessageResult?.returnMessage;
843
- }
844
- else {
845
- const createMessageLogResult = await platformModule.createMessageLog({ user, contactInfo, sharedSMSLogContent, authHeader, additionalSubmission, proxyConfig });
846
- returnMessage = createMessageLogResult?.returnMessage;
847
- extraDataTracking = createMessageLogResult.extraDataTracking;
848
- if (createMessageLogResult.logId) {
849
- const createdMessageLog =
850
- await MessageLogModel.create({
851
- id: incomingData.logInfo.conversationLogId,
852
- platform,
853
- conversationId: incomingData.logInfo.conversationId,
854
- thirdPartyLogId: createMessageLogResult.logId,
855
- userId,
856
- conversationLogId: incomingData.logInfo.conversationLogId
857
- });
858
- logIds.push(createdMessageLog.id);
859
- }
860
- }
861
- }
862
- // Case: normal SMS
863
- else {
864
- if (isGroupSMS) {
865
- // eslint-disable-next-line no-param-reassign
866
- incomingData.logInfo.conversationLogId = incomingData.logInfo.conversationLogId + `-${incomingData.contactId}`;
867
- // eslint-disable-next-line no-param-reassign
868
- incomingData.logInfo.conversationId = incomingData.logInfo.conversationId + `-${incomingData.contactId}`;
869
- }
870
- // reverse the order of messages to log the oldest message first
871
- const reversedMessages = incomingData.logInfo.messages.reverse();
872
- for (const message of reversedMessages) {
873
- if (isGroupSMS) {
874
- message.id = message.id.toString() + `-${incomingData.contactId}`;
875
- }
876
- if (existingIds.includes(message.id.toString())) {
877
- continue;
878
- }
879
- let recordingLink = null;
880
- if (message.attachments && message.attachments.some(a => a.type === 'AudioRecording')) {
881
- recordingLink = message.attachments.find(a => a.type === 'AudioRecording').link;
882
- }
883
- let faxDocLink = null;
884
- let faxDownloadLink = null;
885
- if (message.attachments && message.attachments.some(a => a.type === 'RenderedDocument') && incomingData.logInfo.rcAccessToken) {
886
- faxDocLink = message.attachments.find(a => a.type === 'RenderedDocument').link;
887
- faxDownloadLink = message.attachments.find(a => a.type === 'RenderedDocument').uri + `?access_token=${incomingData.logInfo.rcAccessToken}`
888
- }
889
- let imageLink = null;
890
- let imageDownloadLink = null;
891
- let imageContentType = null;
892
- if (message.attachments && message.attachments.some(a => a.type === 'MmsAttachment' && a.contentType.startsWith('image/')) && incomingData.logInfo.rcAccessToken) {
893
- const imageAttachment = message.attachments.find(a => a.type === 'MmsAttachment' && a.contentType.startsWith('image/'));
894
- if (imageAttachment) {
895
- imageLink = getMediaReaderLinkByPlatformMediaLink(imageAttachment?.uri);
896
- imageDownloadLink = imageAttachment?.uri + `?access_token=${incomingData.logInfo.rcAccessToken}`;
897
- imageContentType = imageAttachment?.contentType;
898
- }
899
- }
900
- let videoLink = null;
901
- if (message.attachments && message.attachments.some(a => a.type === 'MmsAttachment')) {
902
- const imageAttachment = message.attachments.find(a => a.type === 'MmsAttachment' && a.contentType.startsWith('image/'));
903
- if (imageAttachment) {
904
- imageLink = getMediaReaderLinkByPlatformMediaLink(imageAttachment?.uri);
905
- }
906
- const videoAttachment = message.attachments.find(a => a.type === 'MmsAttachment' && a.contentType.startsWith('video/'));
907
- if (videoAttachment) {
908
- videoLink = getMediaReaderLinkByPlatformMediaLink(videoAttachment?.uri);
909
- }
910
- }
911
- const existingSameDateMessageLog = await MessageLogModel.findOne({
912
- where: {
913
- conversationLogId: incomingData.logInfo.conversationLogId
914
- }
915
- });
916
- let crmLogId = ''
917
- if (existingSameDateMessageLog) {
918
- const updateMessageResult = await platformModule.updateMessageLog({ user, contactInfo, assigneeName, ownerName, existingMessageLog: existingSameDateMessageLog, message, authHeader, additionalSubmission, imageLink, imageDownloadLink, imageContentType, videoLink, proxyConfig });
919
- crmLogId = existingSameDateMessageLog.thirdPartyLogId;
920
- returnMessage = updateMessageResult?.returnMessage;
921
- extraDataTracking = updateMessageResult.extraDataTracking;
922
- }
923
- else {
924
- const createMessageLogResult = await platformModule.createMessageLog({ user, contactInfo, correspondents, assigneeName, ownerName, authHeader, message, additionalSubmission, recordingLink, faxDocLink, faxDownloadLink, imageLink, imageDownloadLink, imageContentType, videoLink, proxyConfig });
925
- crmLogId = createMessageLogResult.logId;
926
- returnMessage = createMessageLogResult?.returnMessage;
927
- extraDataTracking = createMessageLogResult.extraDataTracking;
928
- }
929
- if (crmLogId) {
930
- try {
931
- const createdMessageLog =
932
- await MessageLogModel.create({
933
- id: message.id.toString(),
934
- platform,
935
- conversationId: incomingData.logInfo.conversationId,
936
- thirdPartyLogId: crmLogId,
937
- userId,
938
- conversationLogId: incomingData.logInfo.conversationLogId
939
- });
940
- logIds.push(createdMessageLog.id);
941
- } catch (error) {
942
- return handleDatabaseError(error, 'Error creating message log');
943
- }
944
- }
945
- }
946
- }
947
- return {
948
- successful: true,
949
- logIds,
950
- returnMessage: mergePluginWarnings({ returnMessage, warningMessages: pluginWarnings }),
951
- extraDataTracking
952
- };
953
- }
954
- catch (e) {
955
- return handleApiError(e, platform, 'createMessageLog', { userId });
956
- }
957
- }
958
-
959
- async function saveNoteCache({ platform, userId, sessionId, note }) {
960
- try {
961
- const now = moment();
962
- await NoteCache.create({ sessionId, note, ttl: now.unix() + 3600 });
963
- return { successful: true, returnMessage: 'Note cache saved' };
964
- } catch (e) {
965
- return handleApiError(e, platform, 'saveNoteCache', { userId, sessionId, note });
966
- }
967
- }
968
-
969
- exports.createCallLog = createCallLog;
970
- exports.updateCallLog = updateCallLog;
971
- exports.createMessageLog = createMessageLog;
972
- exports.getCallLog = getCallLog;
973
- exports.saveNoteCache = saveNoteCache;
1
+ const Op = require('sequelize').Op;
2
+ const { CallLogModel } = require('../models/callLogModel');
3
+ const { MessageLogModel } = require('../models/messageLogModel');
4
+ const { UserModel } = require('../models/userModel');
5
+ const { CacheModel } = require('../models/cacheModel');
6
+ const oauth = require('../lib/oauth');
7
+ const { composeCallLog } = require('../lib/callLogComposer');
8
+ const { composeSharedSMSLog } = require('../lib/sharedSMSComposer');
9
+ const connectorRegistry = require('../connector/registry');
10
+ const { LOG_DETAILS_FORMAT_TYPE } = require('../lib/constants');
11
+ const { NoteCache } = require('../models/dynamo/noteCacheSchema');
12
+ const { Connector } = require('../models/dynamo/connectorSchema');
13
+ const moment = require('moment');
14
+ const { getMediaReaderLinkByPlatformMediaLink } = require('../lib/util');
15
+ const axios = require('axios');
16
+ const { getPluginsFromUserSettings } = require('../lib/util');
17
+ const logger = require('../lib/logger');
18
+ const { handleApiError, handleDatabaseError } = require('../lib/errorHandler');
19
+ const { v4: uuidv4 } = require('uuid');
20
+ const { AccountDataModel } = require('../models/accountDataModel');
21
+ const pluginCore = require('./plugin');
22
+ const {
23
+ getCallLogExtensionNumber,
24
+ buildCallLogSessionWhere,
25
+ findMatchingCallLog,
26
+ } = require('../lib/callLogLookup');
27
+
28
+ function mergePluginWarnings({ returnMessage, warningMessages }) {
29
+ if (!warningMessages.length) {
30
+ return returnMessage;
31
+ }
32
+ const warningMessage = warningMessages.join(' ');
33
+ if (!returnMessage) {
34
+ return {
35
+ message: warningMessage,
36
+ messageType: 'warning',
37
+ ttl: 5000,
38
+ };
39
+ }
40
+ return {
41
+ ...returnMessage,
42
+ message: `${returnMessage.message || ''} ${warningMessage}`.trim(),
43
+ messageType: returnMessage.messageType === 'error' ? 'error' : 'warning',
44
+ };
45
+ }
46
+
47
+ function getPluginWarningMessage({ pluginId }) {
48
+ return `Plugin ${pluginId} skipped: missing account-level plugin jwtToken. Reinstall or re-register plugin.`;
49
+ }
50
+
51
+ async function createCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
52
+ try {
53
+ const extensionNumber = getCallLogExtensionNumber(incomingData);
54
+ let existingCallLog = null;
55
+ try {
56
+ existingCallLog = await CallLogModel.findOne({
57
+ where: buildCallLogSessionWhere({
58
+ sessionId: incomingData.logInfo.sessionId,
59
+ extensionNumber,
60
+ })
61
+ });
62
+ }
63
+ catch (error) {
64
+ return handleDatabaseError(error, 'Error finding existing call log');
65
+ }
66
+ if (existingCallLog) {
67
+ return {
68
+ successful: false,
69
+ returnMessage: {
70
+ message: `Existing log for session ${incomingData.logInfo.sessionId}`,
71
+ messageType: 'warning',
72
+ ttl: 3000
73
+ }
74
+ }
75
+ }
76
+ let user = null;
77
+ try {
78
+ user = await UserModel.findByPk(userId);
79
+ }
80
+ catch (error) {
81
+ return handleDatabaseError(error, 'Error finding user');
82
+ }
83
+ if (!user || !user.accessToken) {
84
+ return {
85
+ successful: false,
86
+ returnMessage: {
87
+ message: `User not found`,
88
+ messageType: 'warning',
89
+ ttl: 5000
90
+ }
91
+ };
92
+ }
93
+
94
+ const platformModule = connectorRegistry.getConnector(platform);
95
+ const callLog = incomingData.logInfo;
96
+ const additionalSubmission = incomingData.additionalSubmission;
97
+ let note = incomingData.note;
98
+ if (process.env.USE_CACHE && isFromSSCL) {
99
+ const noteCache = await NoteCache.get({ sessionId: incomingData.logInfo.sessionId });
100
+ if (noteCache) {
101
+ note = noteCache.note;
102
+ }
103
+ }
104
+ const aiNote = incomingData.aiNote;
105
+ const transcript = incomingData.transcript;
106
+ let proxyConfig;
107
+ const proxyId = user.platformAdditionalInfo?.proxyId;
108
+ if (proxyId) {
109
+ proxyConfig = await Connector.getProxyConfig(proxyId);
110
+ }
111
+ const authType = await platformModule.getAuthType({ proxyId, proxyConfig });
112
+ let authHeader = '';
113
+ switch (authType) {
114
+ case 'oauth':
115
+ const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
116
+ user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
117
+ if (!user) {
118
+ return {
119
+ successful: false,
120
+ returnMessage: {
121
+ message: `User session expired. Please connect again.`,
122
+ messageType: 'warning',
123
+ ttl: 5000
124
+ },
125
+ isRevokeUserSession: true
126
+ }
127
+ }
128
+ authHeader = `Bearer ${user.accessToken}`;
129
+ break;
130
+ case 'apiKey':
131
+ const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
132
+ authHeader = `Basic ${basicAuth}`;
133
+ break;
134
+ }
135
+ const contactNumber = callLog.direction === 'Inbound' ? callLog.from.phoneNumber : callLog.to.phoneNumber;
136
+ const contactId = incomingData.contactId;
137
+ if (!contactId || contactId === 0) {
138
+ return {
139
+ successful: false,
140
+ returnMessage: {
141
+ message: `Contact not found for number ${contactNumber}`,
142
+ messageType: 'warning',
143
+ ttl: 5000
144
+ }
145
+ };
146
+ }
147
+ const contactInfo = {
148
+ id: contactId,
149
+ phoneNumber: contactNumber,
150
+ type: incomingData.contactType ?? "",
151
+ name: incomingData.contactName ?? ""
152
+ };
153
+
154
+
155
+ const pluginAsyncTaskIds = [];
156
+ const pluginWarnings = [];
157
+ // Plugins
158
+ const accountPlugins = await pluginCore.getPluginsFromRcAccountId({ rcAccountId: user.rcAccountId });
159
+ const callPlugins = accountPlugins.filter(plugin => plugin.data.supportedLogTypes.includes('call'));
160
+ for (const plugin of callPlugins) {
161
+ const pluginId = plugin.id;
162
+ const pluginJwtToken = plugin.data.jwtToken;
163
+ const pluginManifest = plugin.data;
164
+ const pluginEndpointUrl = pluginManifest.endpointUrl;
165
+ if (!pluginEndpointUrl) {
166
+ throw new Error('Plugin URL is not set');
167
+ }
168
+ const userConfig = pluginCore.getPluginConfigFromUserSettings({ userSettings: user.userSettings, pluginId });
169
+ if (plugin.data.isAsync) {
170
+ const asyncTaskId = `${userId}-${uuidv4()}`;
171
+ pluginAsyncTaskIds.push(asyncTaskId);
172
+ await CacheModel.create({
173
+ id: asyncTaskId,
174
+ status: 'initialized',
175
+ userId,
176
+ cacheKey: `pluginTask-${plugin.data.name}`,
177
+ expiry: moment().add(1, 'hour').toDate()
178
+ });
179
+ try {
180
+ const syncPluginTokenResponse = await axios.post(plugin.data.tokenSyncUrl, {},
181
+ {
182
+ headers: {
183
+ Authorization: `Bearer ${pluginJwtToken}`,
184
+ }
185
+ }
186
+ )
187
+ const syncedPluginJwtToken = pluginCore.getRefreshedJwtTokenFromHeaders({ headers: syncPluginTokenResponse.headers });
188
+ axios.post(pluginEndpointUrl, {
189
+ data: incomingData,
190
+ config: userConfig,
191
+ asyncTaskId
192
+ }, {
193
+ headers: {
194
+ Authorization: `Bearer ${syncedPluginJwtToken ?? pluginJwtToken}`,
195
+ },
196
+ });
197
+ if (syncedPluginJwtToken) {
198
+ pluginCore.persistPluginData({
199
+ rcAccountId: user.rcAccountId,
200
+ platformName: platform,
201
+ pluginId,
202
+ jwtToken: syncedPluginJwtToken,
203
+ });
204
+ }
205
+ }
206
+ catch (error) {
207
+ logger.error('Error syncing plugin JWT token', { stack: error.stack });
208
+ }
209
+ }
210
+ else {
211
+ const processedResultResponse = await axios.post(pluginEndpointUrl, {
212
+ data: incomingData,
213
+ config: userConfig,
214
+ }, {
215
+ headers: {
216
+ Authorization: `Bearer ${pluginJwtToken}`,
217
+ },
218
+ });
219
+ const refreshedPluginJwtToken = pluginCore.getRefreshedJwtTokenFromHeaders({ headers: processedResultResponse.headers });
220
+ if (refreshedPluginJwtToken) {
221
+ pluginCore.persistPluginData({
222
+ rcAccountId: user.rcAccountId,
223
+ platformName: platform,
224
+ pluginId,
225
+ jwtToken: refreshedPluginJwtToken,
226
+ });
227
+ }
228
+ // eslint-disable-next-line no-param-reassign
229
+ incomingData = processedResultResponse.data;
230
+ note = incomingData.note;
231
+ }
232
+ }
233
+
234
+ // Compose call log details centrally
235
+ const logFormat = platformModule.getLogFormatType ? platformModule.getLogFormatType(platform, proxyConfig) : LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT;
236
+ let composedLogDetails = '';
237
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT || logFormat === LOG_DETAILS_FORMAT_TYPE.HTML || logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
238
+ composedLogDetails = composeCallLog({
239
+ logFormat,
240
+ callLog,
241
+ contactInfo,
242
+ user,
243
+ note,
244
+ aiNote,
245
+ transcript,
246
+ recordingLink: callLog.recording?.link,
247
+ subject: callLog.customSubject,
248
+ startTime: callLog.startTime,
249
+ duration: callLog.duration,
250
+ result: callLog.result,
251
+ platform,
252
+ ringSenseTranscript: incomingData.ringSenseTranscript,
253
+ ringSenseSummary: incomingData.ringSenseSummary,
254
+ ringSenseAIScore: incomingData.ringSenseAIScore,
255
+ ringSenseBulletedSummary: incomingData.ringSenseBulletedSummary,
256
+ ringSenseLink: incomingData.ringSenseLink,
257
+ });
258
+ }
259
+
260
+ let { logId, returnMessage, extraDataTracking } = await platformModule.createCallLog({
261
+ user,
262
+ contactInfo,
263
+ authHeader,
264
+ callLog,
265
+ note,
266
+ additionalSubmission,
267
+ aiNote,
268
+ transcript,
269
+ ringSenseTranscript: incomingData.ringSenseTranscript,
270
+ ringSenseSummary: incomingData.ringSenseSummary,
271
+ ringSenseAIScore: incomingData.ringSenseAIScore,
272
+ ringSenseBulletedSummary: incomingData.ringSenseBulletedSummary,
273
+ ringSenseLink: incomingData.ringSenseLink,
274
+ composedLogDetails,
275
+ hashedAccountId,
276
+ isFromSSCL,
277
+ proxyConfig,
278
+ });
279
+ if (!extraDataTracking) {
280
+ extraDataTracking = {};
281
+ }
282
+ extraDataTracking.withSmartNoteLog = !!aiNote;
283
+ extraDataTracking.withTranscript = !!transcript;
284
+ if (logId) {
285
+ try {
286
+ await CallLogModel.create({
287
+ id: incomingData.logInfo.telephonySessionId || incomingData.logInfo.id,
288
+ sessionId: incomingData.logInfo.sessionId,
289
+ extensionNumber,
290
+ platform,
291
+ thirdPartyLogId: logId,
292
+ userId,
293
+ contactId
294
+ });
295
+ }
296
+ catch (error) {
297
+ return handleDatabaseError(error, 'Error creating call log');
298
+ }
299
+ return {
300
+ successful: !!logId,
301
+ logId,
302
+ returnMessage: mergePluginWarnings({ returnMessage, warningMessages: pluginWarnings }),
303
+ extraDataTracking,
304
+ pluginAsyncTaskIds
305
+ };
306
+ }
307
+ else {
308
+ return {
309
+ successful: false,
310
+ returnMessage
311
+ };
312
+ }
313
+ } catch (e) {
314
+ return handleApiError(e, platform, 'createCallLog', { userId });
315
+ }
316
+ }
317
+
318
+ async function getCallLog({ userId, sessionIds, extensionNumber, platform, requireDetails }) {
319
+ try {
320
+ let user = await UserModel.findByPk(userId);
321
+ if (!user || !user.accessToken) {
322
+ return { successful: false, message: `Contact not found` };
323
+ }
324
+ let logs = [];
325
+ let returnMessage = null;
326
+ let extraDataTracking = {};
327
+
328
+ // Handle undefined or null sessionIds
329
+ if (!sessionIds) {
330
+ return { successful: false, message: `No session IDs provided` };
331
+ }
332
+
333
+ let sessionIdsArray = sessionIds.split(',');
334
+ if (sessionIdsArray.length > 5) {
335
+ sessionIdsArray = sessionIdsArray.slice(0, 5);
336
+ }
337
+
338
+ if (requireDetails) {
339
+ const proxyId = user.platformAdditionalInfo?.proxyId;
340
+ let proxyConfig = null;
341
+ if (proxyId) {
342
+ proxyConfig = await Connector.getProxyConfig(proxyId);
343
+ }
344
+ const platformModule = connectorRegistry.getConnector(platform);
345
+ const authType = await platformModule.getAuthType({ proxyId, proxyConfig });
346
+ let authHeader = '';
347
+ switch (authType) {
348
+ case 'oauth':
349
+ const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
350
+ user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
351
+ if (!user) {
352
+ return {
353
+ successful: false,
354
+ returnMessage: {
355
+ message: `User session expired. Please connect again.`,
356
+ messageType: 'warning',
357
+ ttl: 5000
358
+ },
359
+ isRevokeUserSession: true
360
+ }
361
+ }
362
+ authHeader = `Bearer ${user.accessToken}`;
363
+ break;
364
+ case 'apiKey':
365
+ const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
366
+ authHeader = `Basic ${basicAuth}`;
367
+ break;
368
+ }
369
+ const callLogs = await CallLogModel.findAll({
370
+ where: buildCallLogSessionWhere({
371
+ sessionIds: sessionIdsArray,
372
+ extensionNumber,
373
+ }),
374
+ order: [['extensionNumber', 'ASC']]
375
+ });
376
+ for (const sId of sessionIdsArray) {
377
+ if (sId == 0) {
378
+ logs.push({ sessionId: sId, matched: false });
379
+ continue;
380
+ }
381
+ const callLog = findMatchingCallLog(callLogs, sId, extensionNumber);
382
+ if (!callLog) {
383
+ logs.push({ sessionId: sId, matched: false });
384
+ }
385
+ else {
386
+ const getCallLogResult = await platformModule.getCallLog({ user, telephonySessionId: callLog.id, callLogId: callLog.thirdPartyLogId, contactId: callLog.contactId, authHeader, proxyConfig });
387
+ returnMessage = getCallLogResult.returnMessage;
388
+ extraDataTracking = getCallLogResult.extraDataTracking;
389
+ logs.push({ sessionId: callLog.sessionId, matched: true, logId: callLog.thirdPartyLogId, logData: getCallLogResult.callLogInfo });
390
+ }
391
+ }
392
+ }
393
+ else {
394
+ const callLogs = await CallLogModel.findAll({
395
+ where: buildCallLogSessionWhere({
396
+ sessionIds: sessionIdsArray,
397
+ extensionNumber,
398
+ }),
399
+ order: [['extensionNumber', 'ASC']]
400
+ });
401
+ for (const sId of sessionIdsArray) {
402
+ const callLog = findMatchingCallLog(callLogs, sId, extensionNumber);
403
+ if (!callLog) {
404
+ logs.push({ sessionId: sId, matched: false });
405
+ }
406
+ else {
407
+ logs.push({ sessionId: callLog.sessionId, matched: true, logId: callLog.thirdPartyLogId });
408
+ }
409
+ }
410
+ }
411
+ return { successful: true, logs, returnMessage, extraDataTracking };
412
+ }
413
+ catch (e) {
414
+ return handleApiError(e, platform, 'getCallLog', { userId, sessionIds, requireDetails, extensionNumber });
415
+ }
416
+ }
417
+
418
+ async function updateCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
419
+ try {
420
+ const extensionNumber = getCallLogExtensionNumber(incomingData);
421
+ let existingCallLog = null;
422
+ try {
423
+ existingCallLog = await CallLogModel.findOne({
424
+ where: buildCallLogSessionWhere({
425
+ sessionId: incomingData.sessionId,
426
+ extensionNumber,
427
+ })
428
+ });
429
+ }
430
+ catch (error) {
431
+ return handleDatabaseError(error, 'Error finding existing call log');
432
+ }
433
+ if (existingCallLog) {
434
+ let user = await UserModel.findByPk(userId);
435
+ if (!user || !user.accessToken) {
436
+ return { successful: false, message: `Contact not found` };
437
+ }
438
+ const platformModule = connectorRegistry.getConnector(platform);
439
+ const proxyId = user.platformAdditionalInfo?.proxyId;
440
+ let proxyConfig = null;
441
+ if (proxyId) {
442
+ proxyConfig = await Connector.getProxyConfig(proxyId);
443
+ }
444
+ const authType = await platformModule.getAuthType({ proxyId, proxyConfig });
445
+ let authHeader = '';
446
+ switch (authType) {
447
+ case 'oauth':
448
+ const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
449
+ user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
450
+ if (!user) {
451
+ return {
452
+ successful: false,
453
+ returnMessage: {
454
+ message: `User session expired. Please connect again.`,
455
+ messageType: 'warning',
456
+ ttl: 5000
457
+ },
458
+ isRevokeUserSession: true
459
+ }
460
+ }
461
+ authHeader = `Bearer ${user.accessToken}`;
462
+ break;
463
+ case 'apiKey':
464
+ const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
465
+ authHeader = `Basic ${basicAuth}`;
466
+ break;
467
+ }
468
+
469
+ const pluginAsyncTaskIds = [];
470
+ const pluginWarnings = [];
471
+ // Plugins
472
+ const accountPlugins = await pluginCore.getPluginsFromRcAccountId({ rcAccountId: user.rcAccountId });
473
+ const callPlugins = accountPlugins.filter(plugin => plugin.data.supportedLogTypes.includes('call'));
474
+ for (const plugin of callPlugins) {
475
+ const pluginId = plugin.id;
476
+ const pluginJwtToken = plugin.data.jwtToken;
477
+ const pluginManifest = plugin.data;
478
+ const pluginEndpointUrl = pluginManifest.endpointUrl;
479
+ if (!pluginEndpointUrl) {
480
+ throw new Error('Plugin URL is not set');
481
+ }
482
+ const userConfig = pluginCore.getPluginConfigFromUserSettings({ userSettings: user.userSettings, pluginId });
483
+ if (plugin.data.isAsync) {
484
+ const asyncTaskId = `${userId}-${uuidv4()}`;
485
+ pluginAsyncTaskIds.push(asyncTaskId);
486
+ await CacheModel.create({
487
+ id: asyncTaskId,
488
+ status: 'initialized',
489
+ userId,
490
+ cacheKey: `pluginTask-${plugin.data.name}`,
491
+ expiry: moment().add(1, 'hour').toDate()
492
+ });
493
+ try {
494
+ const syncPluginTokenResponse = await axios.post(plugin.data.tokenSyncUrl, {},
495
+ {
496
+ headers: {
497
+ Authorization: `Bearer ${pluginJwtToken}`,
498
+ },
499
+ }
500
+ );
501
+ const syncedPluginJwtToken = pluginCore.getRefreshedJwtTokenFromHeaders({ headers: syncPluginTokenResponse.headers });
502
+ axios.post(pluginEndpointUrl, {
503
+ data: { logInfo: incomingData },
504
+ config: userConfig,
505
+ asyncTaskId
506
+ }, {
507
+ headers: {
508
+ Authorization: `Bearer ${syncedPluginJwtToken ?? pluginJwtToken}`,
509
+ },
510
+ });
511
+ if (syncedPluginJwtToken) {
512
+ pluginCore.persistPluginData({
513
+ rcAccountId: user.rcAccountId,
514
+ platformName: platform,
515
+ pluginId,
516
+ jwtToken: syncedPluginJwtToken,
517
+ });
518
+ }
519
+ }
520
+ catch (error) {
521
+ logger.error('Error syncing plugin JWT token', { stack: error.stack });
522
+ }
523
+ }
524
+ else {
525
+ const processedResultResponse = await axios.post(pluginEndpointUrl, {
526
+ data: incomingData,
527
+ config: userConfig
528
+ }, {
529
+ headers: {
530
+ Authorization: `Bearer ${pluginJwtToken}`,
531
+ },
532
+ });
533
+ const refreshedPluginJwtToken = pluginCore.getRefreshedJwtTokenFromHeaders({ headers: processedResultResponse.headers });
534
+ if (refreshedPluginJwtToken) {
535
+ pluginCore.persistPluginData({
536
+ rcAccountId: user.rcAccountId,
537
+ platformName: platform,
538
+ pluginId,
539
+ jwtToken: refreshedPluginJwtToken,
540
+ });
541
+ }
542
+ // eslint-disable-next-line no-param-reassign
543
+ incomingData = processedResultResponse.data;
544
+ }
545
+ }
546
+
547
+ // Fetch existing call log details once to avoid duplicate API calls
548
+ let existingCallLogDetails = null; // Compose updated call log details centrally
549
+ const logFormat = platformModule.getLogFormatType ? platformModule.getLogFormatType(platform, proxyConfig) : LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT;
550
+ let composedLogDetails = '';
551
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT || logFormat === LOG_DETAILS_FORMAT_TYPE.HTML || logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
552
+ let existingBody = '';
553
+ try {
554
+ const getLogResult = await platformModule.getCallLog({
555
+ user,
556
+ telephonySessionId: existingCallLog.id,
557
+ callLogId: existingCallLog.thirdPartyLogId,
558
+ contactId: existingCallLog.contactId,
559
+ authHeader,
560
+ proxyConfig,
561
+ });
562
+ existingCallLogDetails = getLogResult?.callLogInfo?.fullLogResponse;
563
+ // Extract existing body from the platform-specific response
564
+ if (getLogResult.callLogInfo?.fullBody) {
565
+ existingBody = getLogResult.callLogInfo.fullBody;
566
+ } else if (getLogResult.callLogInfo?.note) {
567
+ existingBody = getLogResult.callLogInfo.note;
568
+ }
569
+ } catch (error) {
570
+ logger.error('Error getting existing log details, proceeding with empty body', { stack: error.stack });
571
+ }
572
+ composedLogDetails = composeCallLog({
573
+ logFormat,
574
+ existingBody,
575
+ callLog: {
576
+ sessionId: existingCallLog.sessionId,
577
+ startTime: incomingData.startTime,
578
+ duration: incomingData.duration,
579
+ result: incomingData.result,
580
+ direction: incomingData.direction,
581
+ from: incomingData.from,
582
+ to: incomingData.to,
583
+ legs: incomingData.legs || [],
584
+ },
585
+ contactInfo: null, // Not needed for updates
586
+ user,
587
+ note: incomingData.note,
588
+ aiNote: incomingData.aiNote,
589
+ transcript: incomingData.transcript,
590
+ recordingLink: incomingData.recordingLink,
591
+ subject: incomingData.subject,
592
+ startTime: incomingData.startTime,
593
+ duration: incomingData.duration,
594
+ result: incomingData.result,
595
+ ringSenseTranscript: incomingData.ringSenseTranscript,
596
+ ringSenseSummary: incomingData.ringSenseSummary,
597
+ ringSenseAIScore: incomingData.ringSenseAIScore,
598
+ ringSenseBulletedSummary: incomingData.ringSenseBulletedSummary,
599
+ ringSenseLink: incomingData.ringSenseLink,
600
+ });
601
+ }
602
+
603
+ let { updatedNote, returnMessage, extraDataTracking } = await platformModule.updateCallLog({
604
+ user,
605
+ existingCallLog,
606
+ authHeader,
607
+ recordingLink: incomingData.recordingLink,
608
+ recordingDownloadLink: incomingData.recordingDownloadLink,
609
+ subject: incomingData.subject,
610
+ note: incomingData.note,
611
+ startTime: incomingData.startTime,
612
+ duration: incomingData.duration,
613
+ result: incomingData.result,
614
+ aiNote: incomingData.aiNote,
615
+ transcript: incomingData.transcript,
616
+ legs: incomingData.legs || [],
617
+ ringSenseTranscript: incomingData.ringSenseTranscript,
618
+ ringSenseSummary: incomingData.ringSenseSummary,
619
+ ringSenseAIScore: incomingData.ringSenseAIScore,
620
+ ringSenseBulletedSummary: incomingData.ringSenseBulletedSummary,
621
+ ringSenseLink: incomingData.ringSenseLink,
622
+ additionalSubmission: incomingData.additionalSubmission,
623
+ composedLogDetails,
624
+ existingCallLogDetails, // Pass the fetched details to avoid duplicate API calls
625
+ hashedAccountId,
626
+ isFromSSCL,
627
+ proxyConfig,
628
+ });
629
+ return {
630
+ successful: true,
631
+ logId: existingCallLog.thirdPartyLogId,
632
+ updatedNote,
633
+ returnMessage: mergePluginWarnings({ returnMessage, warningMessages: pluginWarnings }),
634
+ extraDataTracking,
635
+ pluginAsyncTaskIds
636
+ };
637
+ }
638
+ return { successful: false };
639
+ } catch (e) {
640
+ return handleApiError(e, platform, 'updateCallLog', { userId });
641
+ }
642
+ }
643
+
644
+ async function createMessageLog({ platform, userId, incomingData }) {
645
+ try {
646
+ let returnMessage = null;
647
+ let extraDataTracking = {};
648
+ if (incomingData.logInfo.messages.length === 0) {
649
+ return {
650
+ successful: false,
651
+ returnMessage:
652
+ {
653
+ message: 'No message to log.',
654
+ messageType: 'warning',
655
+ ttl: 3000
656
+ }
657
+ }
658
+ }
659
+ const platformModule = connectorRegistry.getConnector(platform);
660
+ const contactNumber = incomingData.logInfo.correspondents[0].phoneNumber;
661
+ const additionalSubmission = incomingData.additionalSubmission;
662
+ let user = null;
663
+ try {
664
+ user = await UserModel.findByPk(userId);
665
+ }
666
+ catch (error) {
667
+ return handleDatabaseError(error, 'Error finding user');
668
+ }
669
+ if (!user || !user.accessToken) {
670
+ return {
671
+ successful: false,
672
+ returnMessage:
673
+ {
674
+ message: `Contact not found`,
675
+ messageType: 'warning',
676
+ ttl: 5000
677
+ }
678
+ };
679
+ }
680
+ const proxyId = user.platformAdditionalInfo?.proxyId;
681
+ let proxyConfig = null;
682
+ if (proxyId) {
683
+ proxyConfig = await Connector.getProxyConfig(proxyId);
684
+ }
685
+ const authType = await platformModule.getAuthType({ proxyId, proxyConfig });
686
+ let authHeader = '';
687
+ switch (authType) {
688
+ case 'oauth':
689
+ const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
690
+ user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
691
+ if (!user) {
692
+ return {
693
+ successful: false,
694
+ returnMessage: {
695
+ message: `User session expired. Please connect again.`,
696
+ messageType: 'warning',
697
+ ttl: 5000
698
+ },
699
+ isRevokeUserSession: true
700
+ }
701
+ }
702
+ authHeader = `Bearer ${user.accessToken}`;
703
+ break;
704
+ case 'apiKey':
705
+ const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
706
+ authHeader = `Basic ${basicAuth}`;
707
+ break;
708
+ }
709
+ const contactId = incomingData.contactId;
710
+ if (!contactId) {
711
+ return {
712
+ successful: false,
713
+ returnMessage:
714
+ {
715
+ message: `Contact not found for number ${contactNumber}`,
716
+ messageType: 'warning',
717
+ ttl: 5000
718
+ }
719
+ };
720
+ }
721
+ const contactInfo = {
722
+ id: contactId,
723
+ phoneNumber: contactNumber,
724
+ type: incomingData.contactType ?? "",
725
+ name: incomingData.contactName ?? ""
726
+ };
727
+ const isGroupSMS = incomingData.logInfo.correspondents.length > 1;
728
+ // For shared SMS
729
+ const assigneeName = incomingData.logInfo.assignee?.name;
730
+ const ownerName = incomingData.logInfo.owner?.name;
731
+ const isSharedSMS = !!ownerName;
732
+
733
+ const pluginAsyncTaskIds = [];
734
+ const pluginWarnings = [];
735
+ // Plugins
736
+ const isSMS = incomingData.logInfo.messages.some(m => m.type === 'SMS');
737
+ const isFax = incomingData.logInfo.messages.some(m => m.type === 'Fax');
738
+ const accountPlugins = await pluginCore.getPluginsFromRcAccountId({ rcAccountId: user.rcAccountId });
739
+ const smsPlugins = isSMS ? accountPlugins.filter(plugin => plugin.data.supportedLogTypes.includes('sms')) : [];
740
+ const faxPlugins = isFax ? accountPlugins.filter(plugin => plugin.data.supportedLogTypes.includes('fax')) : [];
741
+ const plugins = [...smsPlugins, ...faxPlugins];
742
+ for (const plugin of plugins) {
743
+ const pluginId = plugin.id;
744
+ const pluginJwtToken = plugin.data.jwtToken;
745
+ const pluginManifest = plugin.data;
746
+ const pluginEndpointUrl = pluginManifest.endpointUrl;
747
+ if (!pluginEndpointUrl) {
748
+ throw new Error('Plugin URL is not set');
749
+ }
750
+ const userConfig = pluginCore.getPluginConfigFromUserSettings({ userSettings: user.userSettings, pluginId });
751
+ if (plugin.data.isAsync) {
752
+ const asyncTaskId = `${userId}-${uuidv4()}`;
753
+ pluginAsyncTaskIds.push(asyncTaskId);
754
+ await CacheModel.create({
755
+ id: asyncTaskId,
756
+ status: 'initialized',
757
+ userId,
758
+ cacheKey: `pluginTask-${plugin.data.name}`,
759
+ expiry: moment().add(1, 'hour').toDate()
760
+ });
761
+ try {
762
+ const syncPluginTokenResponse = await axios.post(plugin.data.tokenSyncUrl, {},
763
+ {
764
+ headers: {
765
+ Authorization: `Bearer ${pluginJwtToken}`,
766
+ },
767
+ }
768
+ );
769
+ const syncedPluginJwtToken = pluginCore.getRefreshedJwtTokenFromHeaders({ headers: syncPluginTokenResponse.headers });
770
+ axios.post(pluginEndpointUrl, {
771
+ data: { logInfo: incomingData },
772
+ config: userConfig,
773
+ asyncTaskId
774
+ }, {
775
+ headers: {
776
+ Authorization: `Bearer ${syncedPluginJwtToken ?? pluginJwtToken}`,
777
+ },
778
+ });
779
+ if (syncedPluginJwtToken) {
780
+ pluginCore.persistPluginData({
781
+ rcAccountId: user.rcAccountId,
782
+ platformName: platform,
783
+ pluginId,
784
+ jwtToken: syncedPluginJwtToken,
785
+ });
786
+ }
787
+ }
788
+ catch (error) {
789
+ logger.error('Error syncing plugin JWT token', { stack: error.stack });
790
+ }
791
+ }
792
+ else {
793
+ const processedResultResponse = await axios.post(pluginEndpointUrl, {
794
+ data: incomingData,
795
+ config: userConfig,
796
+ }, {
797
+ headers: {
798
+ Authorization: `Bearer ${pluginJwtToken}`,
799
+ },
800
+ });
801
+ const refreshedPluginJwtToken = pluginCore.getRefreshedJwtTokenFromHeaders({ headers: processedResultResponse.headers });
802
+ if (refreshedPluginJwtToken) {
803
+ pluginCore.persistPluginData({
804
+ rcAccountId: user.rcAccountId,
805
+ platformName: platform,
806
+ pluginId,
807
+ jwtToken: refreshedPluginJwtToken,
808
+ });
809
+ }
810
+ // eslint-disable-next-line no-param-reassign
811
+ incomingData = processedResultResponse.data;
812
+ }
813
+ }
814
+
815
+ let messageIds = [];
816
+ const correspondents = [];
817
+ if (isGroupSMS) {
818
+ messageIds = incomingData.logInfo.messages.map(m => { return { id: m.id.toString() + `-${incomingData.contactId}` }; });
819
+ for (var i = 0; i < incomingData.logInfo.correspondents.length; i++) {
820
+ // find cached contact by composite key; findByPk expects raw PK values, so use where clause
821
+ const correspondentContactInfo = await AccountDataModel.findOne({
822
+ where: {
823
+ rcAccountId: user.rcAccountId,
824
+ platformName: platform,
825
+ dataKey: `contact-${incomingData.logInfo.correspondents[i].phoneNumber}`
826
+ }
827
+ })
828
+ if (correspondentContactInfo && correspondentContactInfo.data[0]?.name != incomingData.contactName) {
829
+ correspondents.push(correspondentContactInfo.data);
830
+ }
831
+ }
832
+ }
833
+ else {
834
+ messageIds = incomingData.logInfo.messages.map(m => { return { id: m.id.toString() }; });
835
+ }
836
+ let existingMessages = null;
837
+ try {
838
+ existingMessages = await MessageLogModel.findAll({
839
+ where: {
840
+ [Op.or]: messageIds
841
+ }
842
+ });
843
+ }
844
+ catch (error) {
845
+ return handleDatabaseError(error, 'Error finding existing messages');
846
+ }
847
+ const existingIds = existingMessages.map(m => m.id);
848
+ const logIds = [];
849
+ // Case: Shared SMS
850
+ if (isSharedSMS) {
851
+ const existingMessageLog = await MessageLogModel.findOne({
852
+ where: {
853
+ conversationLogId: incomingData.logInfo.conversationLogId
854
+ }
855
+ });
856
+ const sharedSMSLogContent = composeSharedSMSLog({ logFormat: platformModule.getLogFormatType(platform, proxyConfig), conversation: incomingData.logInfo, contactName: contactInfo.name, timezoneOffset: user.timezoneOffset });
857
+ if (existingMessageLog) {
858
+ const updateMessageResult = await platformModule.updateMessageLog({ user, contactInfo, sharedSMSLogContent, existingMessageLog: existingMessageLog, authHeader, additionalSubmission, proxyConfig });
859
+ returnMessage = updateMessageResult?.returnMessage;
860
+ }
861
+ else {
862
+ const createMessageLogResult = await platformModule.createMessageLog({ user, contactInfo, sharedSMSLogContent, authHeader, additionalSubmission, proxyConfig });
863
+ returnMessage = createMessageLogResult?.returnMessage;
864
+ extraDataTracking = createMessageLogResult.extraDataTracking;
865
+ if (createMessageLogResult.logId) {
866
+ const createdMessageLog =
867
+ await MessageLogModel.create({
868
+ id: incomingData.logInfo.conversationLogId,
869
+ platform,
870
+ conversationId: incomingData.logInfo.conversationId,
871
+ thirdPartyLogId: createMessageLogResult.logId,
872
+ userId,
873
+ conversationLogId: incomingData.logInfo.conversationLogId
874
+ });
875
+ logIds.push(createdMessageLog.id);
876
+ }
877
+ }
878
+ }
879
+ // Case: normal SMS
880
+ else {
881
+ if (isGroupSMS) {
882
+ // eslint-disable-next-line no-param-reassign
883
+ incomingData.logInfo.conversationLogId = incomingData.logInfo.conversationLogId + `-${incomingData.contactId}`;
884
+ // eslint-disable-next-line no-param-reassign
885
+ incomingData.logInfo.conversationId = incomingData.logInfo.conversationId + `-${incomingData.contactId}`;
886
+ }
887
+ // reverse the order of messages to log the oldest message first
888
+ const reversedMessages = incomingData.logInfo.messages.reverse();
889
+ for (const message of reversedMessages) {
890
+ if (isGroupSMS) {
891
+ message.id = message.id.toString() + `-${incomingData.contactId}`;
892
+ }
893
+ if (existingIds.includes(message.id.toString())) {
894
+ continue;
895
+ }
896
+ let recordingLink = null;
897
+ if (message.attachments && message.attachments.some(a => a.type === 'AudioRecording')) {
898
+ recordingLink = message.attachments.find(a => a.type === 'AudioRecording').link;
899
+ }
900
+ let faxDocLink = null;
901
+ let faxDownloadLink = null;
902
+ if (message.attachments && message.attachments.some(a => a.type === 'RenderedDocument') && incomingData.logInfo.rcAccessToken) {
903
+ faxDocLink = message.attachments.find(a => a.type === 'RenderedDocument').link;
904
+ faxDownloadLink = message.attachments.find(a => a.type === 'RenderedDocument').uri + `?access_token=${incomingData.logInfo.rcAccessToken}`
905
+ }
906
+ let imageLink = null;
907
+ let imageDownloadLink = null;
908
+ let imageContentType = null;
909
+ if (message.attachments && message.attachments.some(a => a.type === 'MmsAttachment' && a.contentType.startsWith('image/')) && incomingData.logInfo.rcAccessToken) {
910
+ const imageAttachment = message.attachments.find(a => a.type === 'MmsAttachment' && a.contentType.startsWith('image/'));
911
+ if (imageAttachment) {
912
+ imageLink = getMediaReaderLinkByPlatformMediaLink(imageAttachment?.uri);
913
+ imageDownloadLink = imageAttachment?.uri + `?access_token=${incomingData.logInfo.rcAccessToken}`;
914
+ imageContentType = imageAttachment?.contentType;
915
+ }
916
+ }
917
+ let videoLink = null;
918
+ if (message.attachments && message.attachments.some(a => a.type === 'MmsAttachment')) {
919
+ const imageAttachment = message.attachments.find(a => a.type === 'MmsAttachment' && a.contentType.startsWith('image/'));
920
+ if (imageAttachment) {
921
+ imageLink = getMediaReaderLinkByPlatformMediaLink(imageAttachment?.uri);
922
+ }
923
+ const videoAttachment = message.attachments.find(a => a.type === 'MmsAttachment' && a.contentType.startsWith('video/'));
924
+ if (videoAttachment) {
925
+ videoLink = getMediaReaderLinkByPlatformMediaLink(videoAttachment?.uri);
926
+ }
927
+ }
928
+ const existingSameDateMessageLog = await MessageLogModel.findOne({
929
+ where: {
930
+ conversationLogId: incomingData.logInfo.conversationLogId
931
+ }
932
+ });
933
+ let crmLogId = ''
934
+ if (existingSameDateMessageLog) {
935
+ const updateMessageResult = await platformModule.updateMessageLog({ user, contactInfo, assigneeName, ownerName, existingMessageLog: existingSameDateMessageLog, message, authHeader, additionalSubmission, imageLink, imageDownloadLink, imageContentType, videoLink, proxyConfig });
936
+ crmLogId = existingSameDateMessageLog.thirdPartyLogId;
937
+ returnMessage = updateMessageResult?.returnMessage;
938
+ extraDataTracking = updateMessageResult.extraDataTracking;
939
+ }
940
+ else {
941
+ const createMessageLogResult = await platformModule.createMessageLog({ user, contactInfo, correspondents, assigneeName, ownerName, authHeader, message, additionalSubmission, recordingLink, faxDocLink, faxDownloadLink, imageLink, imageDownloadLink, imageContentType, videoLink, proxyConfig });
942
+ crmLogId = createMessageLogResult.logId;
943
+ returnMessage = createMessageLogResult?.returnMessage;
944
+ extraDataTracking = createMessageLogResult.extraDataTracking;
945
+ }
946
+ if (crmLogId) {
947
+ try {
948
+ const createdMessageLog =
949
+ await MessageLogModel.create({
950
+ id: message.id.toString(),
951
+ platform,
952
+ conversationId: incomingData.logInfo.conversationId,
953
+ thirdPartyLogId: crmLogId,
954
+ userId,
955
+ conversationLogId: incomingData.logInfo.conversationLogId
956
+ });
957
+ logIds.push(createdMessageLog.id);
958
+ } catch (error) {
959
+ return handleDatabaseError(error, 'Error creating message log');
960
+ }
961
+ }
962
+ }
963
+ }
964
+ return {
965
+ successful: true,
966
+ logIds,
967
+ returnMessage: mergePluginWarnings({ returnMessage, warningMessages: pluginWarnings }),
968
+ extraDataTracking
969
+ };
970
+ }
971
+ catch (e) {
972
+ return handleApiError(e, platform, 'createMessageLog', { userId });
973
+ }
974
+ }
975
+
976
+ async function saveNoteCache({ platform, userId, sessionId, note }) {
977
+ try {
978
+ const now = moment();
979
+ await NoteCache.create({ sessionId, note, ttl: now.unix() + 3600 });
980
+ return { successful: true, returnMessage: 'Note cache saved' };
981
+ } catch (e) {
982
+ return handleApiError(e, platform, 'saveNoteCache', { userId, sessionId, note });
983
+ }
984
+ }
985
+
986
+ exports.createCallLog = createCallLog;
987
+ exports.updateCallLog = updateCallLog;
988
+ exports.createMessageLog = createMessageLog;
989
+ exports.getCallLog = getCallLog;
990
+ exports.saveNoteCache = saveNoteCache;