@app-connect/core 1.7.25 → 1.7.26

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