@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.
- package/.env.test +5 -5
- package/README.md +441 -441
- package/connector/developerPortal.js +31 -31
- package/connector/mock.js +84 -77
- package/connector/proxy/engine.js +164 -164
- package/connector/proxy/index.js +500 -500
- package/connector/registry.js +252 -252
- package/docs/README.md +50 -50
- package/docs/architecture.md +93 -93
- package/docs/connectors.md +116 -116
- package/docs/handlers.md +125 -125
- package/docs/libraries.md +101 -101
- package/docs/models.md +144 -144
- package/docs/routes.md +115 -115
- package/docs/tests.md +73 -73
- package/handlers/admin.js +523 -523
- package/handlers/appointment.js +193 -0
- package/handlers/auth.js +296 -296
- package/handlers/calldown.js +99 -99
- package/handlers/contact.js +280 -280
- package/handlers/disposition.js +82 -80
- package/handlers/log.js +984 -973
- package/handlers/managedAuth.js +446 -446
- package/handlers/plugin.js +208 -208
- package/handlers/user.js +142 -142
- package/index.js +3140 -2652
- package/jest.config.js +56 -56
- package/lib/analytics.js +54 -54
- package/lib/authSession.js +109 -109
- package/lib/cacheCleanup.js +21 -0
- package/lib/callLogComposer.js +898 -898
- package/lib/callLogLookup.js +34 -0
- package/lib/constants.js +8 -8
- package/lib/debugTracer.js +177 -177
- package/lib/encode.js +30 -30
- package/lib/errorHandler.js +218 -206
- package/lib/generalErrorMessage.js +41 -41
- package/lib/jwt.js +18 -18
- package/lib/logger.js +190 -190
- package/lib/migrateCallLogsSchema.js +116 -0
- package/lib/ringcentral.js +266 -266
- package/lib/s3ErrorLogReport.js +65 -65
- package/lib/sharedSMSComposer.js +471 -471
- package/lib/util.js +67 -67
- package/mcp/README.md +412 -395
- package/mcp/lib/validator.js +91 -91
- package/mcp/mcpHandler.js +425 -425
- package/mcp/tools/cancelAppointment.js +101 -0
- package/mcp/tools/checkAuthStatus.js +105 -105
- package/mcp/tools/confirmAppointment.js +101 -0
- package/mcp/tools/createAppointment.js +157 -0
- package/mcp/tools/createCallLog.js +327 -316
- package/mcp/tools/createContact.js +117 -117
- package/mcp/tools/createMessageLog.js +287 -287
- package/mcp/tools/doAuth.js +60 -60
- package/mcp/tools/findContactByName.js +93 -93
- package/mcp/tools/findContactByPhone.js +101 -101
- package/mcp/tools/getCallLog.js +111 -102
- package/mcp/tools/getGoogleFilePicker.js +99 -99
- package/mcp/tools/getHelp.js +43 -43
- package/mcp/tools/getPublicConnectors.js +94 -94
- package/mcp/tools/getSessionInfo.js +90 -90
- package/mcp/tools/index.js +51 -41
- package/mcp/tools/listAppointments.js +163 -0
- package/mcp/tools/logout.js +96 -96
- package/mcp/tools/rcGetCallLogs.js +65 -65
- package/mcp/tools/updateAppointment.js +154 -0
- package/mcp/tools/updateCallLog.js +130 -126
- package/mcp/ui/App/App.tsx +358 -358
- package/mcp/ui/App/components/AuthInfoForm.tsx +113 -113
- package/mcp/ui/App/components/AuthSuccess.tsx +22 -22
- package/mcp/ui/App/components/ConnectorList.tsx +82 -82
- package/mcp/ui/App/components/DebugPanel.tsx +43 -43
- package/mcp/ui/App/components/OAuthConnect.tsx +270 -270
- package/mcp/ui/App/lib/callTool.ts +130 -130
- package/mcp/ui/App/lib/debugLog.ts +41 -41
- package/mcp/ui/App/lib/developerPortal.ts +111 -111
- package/mcp/ui/App/main.css +5 -5
- package/mcp/ui/App/root.tsx +13 -13
- package/mcp/ui/index.html +13 -13
- package/mcp/ui/package-lock.json +6356 -6356
- package/mcp/ui/package.json +25 -25
- package/mcp/ui/tsconfig.json +26 -26
- package/mcp/ui/vite.config.ts +16 -16
- package/models/accountDataModel.js +33 -33
- package/models/adminConfigModel.js +35 -35
- package/models/cacheModel.js +30 -26
- package/models/callDownListModel.js +34 -34
- package/models/callLogModel.js +33 -27
- package/models/dynamo/connectorSchema.js +146 -146
- package/models/dynamo/lockSchema.js +24 -24
- package/models/dynamo/noteCacheSchema.js +29 -29
- package/models/llmSessionModel.js +17 -17
- package/models/messageLogModel.js +25 -25
- package/models/sequelize.js +16 -16
- package/models/userModel.js +45 -45
- package/package.json +1 -1
- package/releaseNotes.json +1093 -1081
- package/test/connector/proxy/engine.test.js +126 -126
- package/test/connector/proxy/index.test.js +279 -279
- package/test/connector/proxy/sample.json +161 -161
- package/test/connector/registry.test.js +415 -415
- package/test/handlers/admin.test.js +616 -616
- package/test/handlers/auth.test.js +1018 -1018
- package/test/handlers/contact.test.js +1014 -1014
- package/test/handlers/log.test.js +1298 -1160
- package/test/handlers/managedAuth.test.js +457 -457
- package/test/handlers/plugin.test.js +380 -380
- package/test/index.test.js +105 -105
- package/test/lib/cacheCleanup.test.js +42 -0
- package/test/lib/callLogComposer.test.js +1231 -1231
- package/test/lib/debugTracer.test.js +328 -328
- package/test/lib/jwt.test.js +176 -176
- package/test/lib/logger.test.js +206 -206
- package/test/lib/oauth.test.js +359 -359
- package/test/lib/ringcentral.test.js +467 -467
- package/test/lib/sharedSMSComposer.test.js +1084 -1084
- package/test/lib/util.test.js +329 -329
- package/test/mcp/tools/checkAuthStatus.test.js +83 -83
- package/test/mcp/tools/createCallLog.test.js +436 -436
- package/test/mcp/tools/createContact.test.js +58 -58
- package/test/mcp/tools/createMessageLog.test.js +595 -595
- package/test/mcp/tools/doAuth.test.js +113 -113
- package/test/mcp/tools/findContactByName.test.js +275 -275
- package/test/mcp/tools/findContactByPhone.test.js +296 -296
- package/test/mcp/tools/getCallLog.test.js +298 -298
- package/test/mcp/tools/getGoogleFilePicker.test.js +281 -281
- package/test/mcp/tools/getPublicConnectors.test.js +107 -107
- package/test/mcp/tools/getSessionInfo.test.js +127 -127
- package/test/mcp/tools/logout.test.js +233 -233
- package/test/mcp/tools/rcGetCallLogs.test.js +56 -56
- package/test/mcp/tools/updateCallLog.test.js +360 -360
- package/test/models/accountDataModel.test.js +98 -98
- package/test/models/dynamo/connectorSchema.test.js +189 -189
- package/test/models/models.test.js +568 -539
- package/test/routes/managedAuthRoutes.test.js +104 -104
- package/test/setup.js +178 -178
package/handlers/admin.js
CHANGED
|
@@ -1,523 +1,523 @@
|
|
|
1
|
-
const axios = require('axios');
|
|
2
|
-
const { AdminConfigModel } = require('../models/adminConfigModel');
|
|
3
|
-
const connectorRegistry = require('../connector/registry');
|
|
4
|
-
const oauth = require('../lib/oauth');
|
|
5
|
-
const { RingCentral } = require('../lib/ringcentral');
|
|
6
|
-
const { Connector } = require('../models/dynamo/connectorSchema');
|
|
7
|
-
const logger = require('../lib/logger');
|
|
8
|
-
const { handleDatabaseError } = require('../lib/errorHandler');
|
|
9
|
-
const { getHashValue } = require('../lib/util');
|
|
10
|
-
|
|
11
|
-
const CALL_AGGREGATION_GROUPS = ["Company", "CompanyNumbers", "Users", "Queues", "IVRs", "IVAs", "SharedLines", "UserGroups", "Sites", "Departments"]
|
|
12
|
-
const RC_EXTENSION_ENDPOINT = 'https://platform.ringcentral.com/restapi/v1.0/account/~/extension/~';
|
|
13
|
-
|
|
14
|
-
async function validateRcUserToken({ rcAccessToken }) {
|
|
15
|
-
if (!rcAccessToken) {
|
|
16
|
-
throw new Error('rcAccessToken is required');
|
|
17
|
-
}
|
|
18
|
-
const rcExtensionResponse = await axios.get(
|
|
19
|
-
RC_EXTENSION_ENDPOINT,
|
|
20
|
-
{
|
|
21
|
-
headers: {
|
|
22
|
-
Authorization: `Bearer ${rcAccessToken}`,
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
const extensionData = rcExtensionResponse.data ?? {};
|
|
26
|
-
return {
|
|
27
|
-
rcAccountId: extensionData?.account?.id?.toString() ?? '',
|
|
28
|
-
rcExtensionId: extensionData?.id?.toString() ?? ''
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function validateAdminRole({ rcAccessToken }) {
|
|
33
|
-
const rcExtensionResponse = await axios.get(
|
|
34
|
-
RC_EXTENSION_ENDPOINT,
|
|
35
|
-
{
|
|
36
|
-
headers: {
|
|
37
|
-
Authorization: `Bearer ${rcAccessToken}`,
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
return {
|
|
41
|
-
isValidated: !!rcExtensionResponse.data?.permissions?.admin?.enabled || (!!process.env.ADMIN_EXTENSION_ID_DEV_PASS_LIST && process.env.ADMIN_EXTENSION_ID_DEV_PASS_LIST.split(',').includes(rcExtensionResponse.data.id.toString())),
|
|
42
|
-
rcAccountId: rcExtensionResponse.data.account.id
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function upsertAdminSettings({ hashedRcAccountId, adminSettings }) {
|
|
47
|
-
let existingAdminConfig = await AdminConfigModel.findByPk(hashedRcAccountId);
|
|
48
|
-
if (existingAdminConfig) {
|
|
49
|
-
await existingAdminConfig.update({
|
|
50
|
-
...adminSettings
|
|
51
|
-
});
|
|
52
|
-
} else {
|
|
53
|
-
await AdminConfigModel.create({
|
|
54
|
-
id: hashedRcAccountId,
|
|
55
|
-
...adminSettings
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function getAdminSettings({ hashedRcAccountId }) {
|
|
61
|
-
const existingAdminConfig = await AdminConfigModel.findByPk(hashedRcAccountId);
|
|
62
|
-
return existingAdminConfig;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function updateAdminRcTokens({ hashedRcAccountId, adminAccessToken, adminRefreshToken, adminTokenExpiry }) {
|
|
66
|
-
const existingAdminConfig = await AdminConfigModel.findByPk(hashedRcAccountId);
|
|
67
|
-
if (existingAdminConfig) {
|
|
68
|
-
await existingAdminConfig.update({ adminAccessToken, adminRefreshToken, adminTokenExpiry });
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
await AdminConfigModel.create({
|
|
72
|
-
id: hashedRcAccountId,
|
|
73
|
-
adminAccessToken,
|
|
74
|
-
adminRefreshToken,
|
|
75
|
-
adminTokenExpiry
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function getServerLoggingSettings({ user }) {
|
|
81
|
-
const platformModule = connectorRegistry.getConnector(user.platform);
|
|
82
|
-
if (platformModule.getServerLoggingSettings) {
|
|
83
|
-
const serverLoggingSettings = await platformModule.getServerLoggingSettings({ user });
|
|
84
|
-
return serverLoggingSettings;
|
|
85
|
-
}
|
|
86
|
-
return {};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function updateServerLoggingSettings({ user, additionalFieldValues }) {
|
|
90
|
-
const platformModule = connectorRegistry.getConnector(user.platform);
|
|
91
|
-
const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname })));
|
|
92
|
-
if (platformModule.updateServerLoggingSettings) {
|
|
93
|
-
const { successful, returnMessage } = await platformModule.updateServerLoggingSettings({ user, additionalFieldValues, oauthApp });
|
|
94
|
-
return { successful, returnMessage };
|
|
95
|
-
}
|
|
96
|
-
return {};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function getAdminReport({ rcAccountId, timezone, timeFrom, timeTo, groupBy }) {
|
|
100
|
-
try {
|
|
101
|
-
if (!process.env.RINGCENTRAL_SERVER || !process.env.RINGCENTRAL_CLIENT_ID || !process.env.RINGCENTRAL_CLIENT_SECRET) {
|
|
102
|
-
return {
|
|
103
|
-
callLogStats: {}
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
const rcSDK = new RingCentral({
|
|
107
|
-
server: process.env.RINGCENTRAL_SERVER,
|
|
108
|
-
clientId: process.env.RINGCENTRAL_CLIENT_ID,
|
|
109
|
-
clientSecret: process.env.RINGCENTRAL_CLIENT_SECRET,
|
|
110
|
-
redirectUri: `${process.env.APP_SERVER}/ringcentral/oauth/callback`
|
|
111
|
-
});
|
|
112
|
-
const hashedRcAccountId = getHashValue(rcAccountId, process.env.HASH_KEY);
|
|
113
|
-
let adminConfig = await AdminConfigModel.findByPk(hashedRcAccountId);
|
|
114
|
-
const isTokenExpired = adminConfig.adminTokenExpiry < new Date();
|
|
115
|
-
if (isTokenExpired) {
|
|
116
|
-
const { access_token, refresh_token, expire_time } = await rcSDK.refreshToken({
|
|
117
|
-
refresh_token: adminConfig.adminRefreshToken,
|
|
118
|
-
expires_in: adminConfig.adminTokenExpiry,
|
|
119
|
-
refresh_token_expires_in: adminConfig.adminTokenExpiry
|
|
120
|
-
});
|
|
121
|
-
adminConfig = await AdminConfigModel.update({ adminAccessToken: access_token, adminRefreshToken: refresh_token, adminTokenExpiry: expire_time }, { where: { id: hashedRcAccountId } });
|
|
122
|
-
}
|
|
123
|
-
const callsAggregationData = await rcSDK.getCallsAggregationData({
|
|
124
|
-
token: { access_token: adminConfig.adminAccessToken, token_type: 'Bearer' },
|
|
125
|
-
timezone,
|
|
126
|
-
timeFrom,
|
|
127
|
-
timeTo,
|
|
128
|
-
groupBy: groupBy == 'undefined' ? CALL_AGGREGATION_GROUPS[0] : groupBy
|
|
129
|
-
});
|
|
130
|
-
var callLogStats = [];
|
|
131
|
-
var itemKeys = [];
|
|
132
|
-
for (const record of callsAggregationData.data.records) {
|
|
133
|
-
if (!record?.info?.name) {
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
itemKeys.push(record.info.name);
|
|
137
|
-
var dataCounter = record.counters;
|
|
138
|
-
var inboundCallCount = dataCounter.callsByDirection.values.inbound;
|
|
139
|
-
var outboundCallCount = dataCounter.callsByDirection.values.outbound;
|
|
140
|
-
var answeredCallCount = dataCounter.callsByResponse.values.answered;
|
|
141
|
-
// keep 2 decimal places
|
|
142
|
-
var answeredCallPercentage = inboundCallCount === 0 ? '0%' : `${((answeredCallCount / inboundCallCount) * 100).toFixed(2)}%`;
|
|
143
|
-
var totalTalkTime = Number(record.timers.allCalls.values) === 0 ? 0 : Number(record.timers.allCalls.values).toFixed(2);
|
|
144
|
-
var averageTalkTime = Number(totalTalkTime) === 0 ? 0 : (Number(totalTalkTime) / (inboundCallCount + outboundCallCount)).toFixed(2);
|
|
145
|
-
callLogStats.push({
|
|
146
|
-
name: record.info.name,
|
|
147
|
-
inboundCallCount,
|
|
148
|
-
outboundCallCount,
|
|
149
|
-
answeredCallCount,
|
|
150
|
-
answeredCallPercentage,
|
|
151
|
-
totalTalkTime,
|
|
152
|
-
averageTalkTime
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
return {
|
|
156
|
-
callLogStats,
|
|
157
|
-
itemKeys,
|
|
158
|
-
groupedBy: callsAggregationData.data.groupedBy,
|
|
159
|
-
groupKeys: CALL_AGGREGATION_GROUPS
|
|
160
|
-
};
|
|
161
|
-
} catch (error) {
|
|
162
|
-
logger.error('Error getting admin report', { error });
|
|
163
|
-
return {
|
|
164
|
-
callLogStats: {}
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async function getUserReport({ rcAccountId, rcExtensionId, timezone, timeFrom, timeTo }) {
|
|
170
|
-
try {
|
|
171
|
-
if (!process.env.RINGCENTRAL_SERVER || !process.env.RINGCENTRAL_CLIENT_ID || !process.env.RINGCENTRAL_CLIENT_SECRET) {
|
|
172
|
-
return {
|
|
173
|
-
callLogStats: {}
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
const rcSDK = new RingCentral({
|
|
177
|
-
server: process.env.RINGCENTRAL_SERVER,
|
|
178
|
-
clientId: process.env.RINGCENTRAL_CLIENT_ID,
|
|
179
|
-
clientSecret: process.env.RINGCENTRAL_CLIENT_SECRET,
|
|
180
|
-
redirectUri: `${process.env.APP_SERVER}/ringcentral/oauth/callback`
|
|
181
|
-
});
|
|
182
|
-
const hashedRcAccountId = getHashValue(rcAccountId, process.env.HASH_KEY);
|
|
183
|
-
let adminConfig = await AdminConfigModel.findByPk(hashedRcAccountId);
|
|
184
|
-
const isTokenExpired = adminConfig.adminTokenExpiry < new Date();
|
|
185
|
-
if (isTokenExpired) {
|
|
186
|
-
const { access_token, refresh_token, expire_time } = await rcSDK.refreshToken({
|
|
187
|
-
refresh_token: adminConfig.adminRefreshToken,
|
|
188
|
-
expires_in: adminConfig.adminTokenExpiry,
|
|
189
|
-
refresh_token_expires_in: adminConfig.adminTokenExpiry
|
|
190
|
-
});
|
|
191
|
-
adminConfig = await AdminConfigModel.update({ adminAccessToken: access_token, adminRefreshToken: refresh_token, adminTokenExpiry: expire_time }, { where: { id: hashedRcAccountId } });
|
|
192
|
-
}
|
|
193
|
-
const callLogData = await rcSDK.getCallLogData({
|
|
194
|
-
extensionId: rcExtensionId,
|
|
195
|
-
token: { access_token: adminConfig.adminAccessToken, token_type: 'Bearer' },
|
|
196
|
-
timezone,
|
|
197
|
-
timeFrom,
|
|
198
|
-
timeTo
|
|
199
|
-
});
|
|
200
|
-
// phone activity
|
|
201
|
-
const inboundCallCount = callLogData.records.filter(call => call.direction === 'Inbound').length;
|
|
202
|
-
const outboundCallCount = callLogData.records.filter(call => call.direction === 'Outbound').length;
|
|
203
|
-
const answeredCallCount = callLogData.records.filter(call => call.direction === 'Inbound' && (call.result === 'Call connected' || call.result === 'Accepted' || call.result === 'Answered Not Accepted')).length;
|
|
204
|
-
const answeredCallPercentage = answeredCallCount === 0 ? '0%' : `${((answeredCallCount / (inboundCallCount || 1)) * 100).toFixed(2)}%`;
|
|
205
|
-
// phone engagement
|
|
206
|
-
const totalTalkTime = Math.round(callLogData.records.reduce((acc, call) => acc + (call.duration || 0), 0) / 60) || 0;
|
|
207
|
-
const averageTalkTime = Math.round(totalTalkTime / (inboundCallCount + outboundCallCount)) || 0;
|
|
208
|
-
const smsLogData = await rcSDK.getSMSData({
|
|
209
|
-
extensionId: rcExtensionId,
|
|
210
|
-
token: { access_token: adminConfig.adminAccessToken, token_type: 'Bearer' },
|
|
211
|
-
timezone,
|
|
212
|
-
timeFrom,
|
|
213
|
-
timeTo
|
|
214
|
-
});
|
|
215
|
-
const smsSentCount = smsLogData.records.filter(sms => sms.direction === 'Outbound').length;
|
|
216
|
-
const smsReceivedCount = smsLogData.records.filter(sms => sms.direction === 'Inbound').length;
|
|
217
|
-
const reportStats = {
|
|
218
|
-
callLogStats: {
|
|
219
|
-
inboundCallCount,
|
|
220
|
-
outboundCallCount,
|
|
221
|
-
answeredCallCount,
|
|
222
|
-
answeredCallPercentage,
|
|
223
|
-
totalTalkTime,
|
|
224
|
-
averageTalkTime
|
|
225
|
-
},
|
|
226
|
-
smsLogStats: {
|
|
227
|
-
smsSentCount,
|
|
228
|
-
smsReceivedCount
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
return reportStats;
|
|
232
|
-
} catch (error) {
|
|
233
|
-
logger.error('Error getting user report', { error });
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async function getUserMapping({ user, hashedRcAccountId, rcExtensionList }) {
|
|
239
|
-
let adminConfig = null;
|
|
240
|
-
try {
|
|
241
|
-
adminConfig = await getAdminSettings({ hashedRcAccountId });
|
|
242
|
-
}
|
|
243
|
-
catch (error) {
|
|
244
|
-
return handleDatabaseError(error, 'Error getting user mapping');
|
|
245
|
-
}
|
|
246
|
-
const platformModule = connectorRegistry.getConnector(user.platform);
|
|
247
|
-
if (platformModule.getUserList) {
|
|
248
|
-
const proxyId = user.platformAdditionalInfo?.proxyId;
|
|
249
|
-
let proxyConfig = null;
|
|
250
|
-
if (proxyId) {
|
|
251
|
-
proxyConfig = await Connector.getProxyConfig(proxyId);
|
|
252
|
-
if (!proxyConfig?.operations?.getUserList) {
|
|
253
|
-
return [];
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
const authType = await platformModule.getAuthType({ proxyId, proxyConfig });
|
|
257
|
-
let authHeader = '';
|
|
258
|
-
switch (authType) {
|
|
259
|
-
case 'oauth':
|
|
260
|
-
const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
|
|
261
|
-
// eslint-disable-next-line no-param-reassign
|
|
262
|
-
user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
|
|
263
|
-
if (!user) {
|
|
264
|
-
return {
|
|
265
|
-
successful: false,
|
|
266
|
-
returnMessage: {
|
|
267
|
-
message: `User session expired. Please connect again.`,
|
|
268
|
-
messageType: 'warning',
|
|
269
|
-
ttl: 5000
|
|
270
|
-
},
|
|
271
|
-
isRevokeUserSession: true
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
authHeader = `Bearer ${user.accessToken}`;
|
|
275
|
-
break;
|
|
276
|
-
case 'apiKey':
|
|
277
|
-
const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
|
|
278
|
-
authHeader = `Basic ${basicAuth}`;
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
281
|
-
const crmUserList = await platformModule.getUserList({ user, authHeader, proxyConfig });
|
|
282
|
-
const userMappingResult = [];
|
|
283
|
-
const newUserMappings = [];
|
|
284
|
-
for (const crmUser of crmUserList) {
|
|
285
|
-
const existingMapping = adminConfig?.userMappings?.find(u => u.crmUserId == crmUser.id);
|
|
286
|
-
let existingMappingRcExtensionIds = [];
|
|
287
|
-
// TEMP: backward compatibility for string value
|
|
288
|
-
if (existingMapping?.rcExtensionId) {
|
|
289
|
-
if (typeof (existingMapping.rcExtensionId) === 'string') {
|
|
290
|
-
existingMappingRcExtensionIds = [existingMapping.rcExtensionId];
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
existingMappingRcExtensionIds = existingMapping.rcExtensionId;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
const rcExtension = rcExtensionList.filter(e => existingMappingRcExtensionIds.includes(e.id));
|
|
297
|
-
// Case: existing mapping
|
|
298
|
-
if (existingMapping) {
|
|
299
|
-
userMappingResult.push({
|
|
300
|
-
crmUser: {
|
|
301
|
-
id: crmUser.id,
|
|
302
|
-
name: crmUser.name ?? '',
|
|
303
|
-
email: crmUser.email ?? '',
|
|
304
|
-
},
|
|
305
|
-
rcUser: rcExtension.map(e => ({
|
|
306
|
-
extensionId: e.id,
|
|
307
|
-
name: e?.name || `${e.firstName} ${e.lastName}`,
|
|
308
|
-
extensionNumber: e?.extensionNumber ?? '',
|
|
309
|
-
email: e?.email ?? ''
|
|
310
|
-
}))
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
// Case: new mapping
|
|
314
|
-
else {
|
|
315
|
-
const rcExtensionForNewMapping = rcExtensionList.find(u =>
|
|
316
|
-
u.email === crmUser.email ||
|
|
317
|
-
u.name === crmUser.name ||
|
|
318
|
-
(`${u.firstName} ${u.lastName}` === crmUser.name)
|
|
319
|
-
);
|
|
320
|
-
if (rcExtensionForNewMapping) {
|
|
321
|
-
userMappingResult.push({
|
|
322
|
-
crmUser: {
|
|
323
|
-
id: crmUser.id,
|
|
324
|
-
name: crmUser.name ?? '',
|
|
325
|
-
email: crmUser.email ?? '',
|
|
326
|
-
},
|
|
327
|
-
rcUser: [{
|
|
328
|
-
extensionId: rcExtensionForNewMapping.id,
|
|
329
|
-
name: rcExtensionForNewMapping.name || `${rcExtensionForNewMapping.firstName} ${rcExtensionForNewMapping.lastName}`,
|
|
330
|
-
extensionNumber: rcExtensionForNewMapping?.extensionNumber ?? '',
|
|
331
|
-
email: rcExtensionForNewMapping?.email ?? ''
|
|
332
|
-
}]
|
|
333
|
-
});
|
|
334
|
-
newUserMappings.push({
|
|
335
|
-
crmUserId: crmUser.id.toString(),
|
|
336
|
-
rcExtensionId: [rcExtensionForNewMapping.id.toString()]
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
userMappingResult.push({
|
|
341
|
-
crmUser: {
|
|
342
|
-
id: crmUser.id,
|
|
343
|
-
name: crmUser.name ?? '',
|
|
344
|
-
email: crmUser.email ?? '',
|
|
345
|
-
},
|
|
346
|
-
rcUser: []
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
// One-time init
|
|
352
|
-
if (!adminConfig?.userMappings) {
|
|
353
|
-
const initialUserMappings = [];
|
|
354
|
-
for (const userMapping of userMappingResult) {
|
|
355
|
-
if (userMapping.rcUser?.extensionId) {
|
|
356
|
-
initialUserMappings.push({
|
|
357
|
-
crmUserId: userMapping.crmUser.id.toString(),
|
|
358
|
-
rcExtensionId: [userMapping.rcUser.extensionId.toString()]
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
try {
|
|
363
|
-
await upsertAdminSettings({
|
|
364
|
-
hashedRcAccountId,
|
|
365
|
-
adminSettings: {
|
|
366
|
-
userMappings: initialUserMappings
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
catch (error) {
|
|
371
|
-
return handleDatabaseError(error, 'Error initializing user mapping');
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
// Incremental update
|
|
375
|
-
if (newUserMappings.length > 0) {
|
|
376
|
-
// TEMP: convert string to array
|
|
377
|
-
if (adminConfig?.userMappings) {
|
|
378
|
-
adminConfig.userMappings = adminConfig.userMappings.map(u => ({
|
|
379
|
-
...u,
|
|
380
|
-
rcExtensionId: [u.rcExtensionId]
|
|
381
|
-
}));
|
|
382
|
-
try {
|
|
383
|
-
await upsertAdminSettings({
|
|
384
|
-
hashedRcAccountId,
|
|
385
|
-
adminSettings: {
|
|
386
|
-
userMappings: [...adminConfig.userMappings, ...newUserMappings]
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
catch (error) {
|
|
391
|
-
return handleDatabaseError(error, 'Error updating user mapping');
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
try {
|
|
396
|
-
await upsertAdminSettings({
|
|
397
|
-
hashedRcAccountId,
|
|
398
|
-
adminSettings: {
|
|
399
|
-
userMappings: [...newUserMappings]
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
catch (error) {
|
|
404
|
-
return handleDatabaseError(error, 'Error updating user mapping');
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
return userMappingResult;
|
|
409
|
-
}
|
|
410
|
-
return [];
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
async function reinitializeUserMapping({ user, hashedRcAccountId, rcExtensionList }) {
|
|
414
|
-
const platformModule = connectorRegistry.getConnector(user.platform);
|
|
415
|
-
if (!platformModule.getUserList) {
|
|
416
|
-
return [];
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const proxyId = user.platformAdditionalInfo?.proxyId;
|
|
420
|
-
let proxyConfig = null;
|
|
421
|
-
if (proxyId) {
|
|
422
|
-
proxyConfig = await Connector.getProxyConfig(proxyId);
|
|
423
|
-
if (!proxyConfig?.operations?.getUserList) {
|
|
424
|
-
return [];
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const authType = await platformModule.getAuthType({ proxyId, proxyConfig });
|
|
429
|
-
let authHeader = '';
|
|
430
|
-
switch (authType) {
|
|
431
|
-
case 'oauth':
|
|
432
|
-
const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
|
|
433
|
-
// eslint-disable-next-line no-param-reassign
|
|
434
|
-
user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
|
|
435
|
-
if (!user) {
|
|
436
|
-
return {
|
|
437
|
-
successful: false,
|
|
438
|
-
returnMessage: {
|
|
439
|
-
message: `User session expired. Please connect again.`,
|
|
440
|
-
messageType: 'warning',
|
|
441
|
-
ttl: 5000
|
|
442
|
-
},
|
|
443
|
-
isRevokeUserSession: true
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
authHeader = `Bearer ${user.accessToken}`;
|
|
447
|
-
break;
|
|
448
|
-
case 'apiKey':
|
|
449
|
-
const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
|
|
450
|
-
authHeader = `Basic ${basicAuth}`;
|
|
451
|
-
break;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const crmUserList = await platformModule.getUserList({ user, authHeader, proxyConfig });
|
|
455
|
-
const userMappingResult = [];
|
|
456
|
-
const initialUserMappings = [];
|
|
457
|
-
|
|
458
|
-
// Auto-match CRM users with RC extensions by email or name
|
|
459
|
-
for (const crmUser of crmUserList) {
|
|
460
|
-
const rcExtensionForMapping = rcExtensionList.find(u =>
|
|
461
|
-
u.email === crmUser.email ||
|
|
462
|
-
u.name === crmUser.name ||
|
|
463
|
-
(`${u.firstName} ${u.lastName}` === crmUser.name)
|
|
464
|
-
);
|
|
465
|
-
|
|
466
|
-
if (rcExtensionForMapping) {
|
|
467
|
-
userMappingResult.push({
|
|
468
|
-
crmUser: {
|
|
469
|
-
id: crmUser.id,
|
|
470
|
-
name: crmUser.name ?? '',
|
|
471
|
-
email: crmUser.email ?? '',
|
|
472
|
-
},
|
|
473
|
-
rcUser: [{
|
|
474
|
-
extensionId: rcExtensionForMapping.id,
|
|
475
|
-
name: rcExtensionForMapping.name || `${rcExtensionForMapping.firstName} ${rcExtensionForMapping.lastName}`,
|
|
476
|
-
extensionNumber: rcExtensionForMapping?.extensionNumber ?? '',
|
|
477
|
-
email: rcExtensionForMapping?.email ?? ''
|
|
478
|
-
}]
|
|
479
|
-
});
|
|
480
|
-
initialUserMappings.push({
|
|
481
|
-
crmUserId: crmUser.id.toString(),
|
|
482
|
-
rcExtensionId: [rcExtensionForMapping.id.toString()]
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
else {
|
|
486
|
-
userMappingResult.push({
|
|
487
|
-
crmUser: {
|
|
488
|
-
id: crmUser.id,
|
|
489
|
-
name: crmUser.name ?? '',
|
|
490
|
-
email: crmUser.email ?? '',
|
|
491
|
-
},
|
|
492
|
-
rcUser: []
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Overwrite existing mappings with fresh auto-matched mappings
|
|
498
|
-
try {
|
|
499
|
-
await upsertAdminSettings({
|
|
500
|
-
hashedRcAccountId,
|
|
501
|
-
adminSettings: {
|
|
502
|
-
userMappings: initialUserMappings
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
catch (error) {
|
|
507
|
-
return handleDatabaseError(error, 'Error reinitializing user mapping');
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
return userMappingResult;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
exports.validateAdminRole = validateAdminRole;
|
|
514
|
-
exports.validateRcUserToken = validateRcUserToken;
|
|
515
|
-
exports.upsertAdminSettings = upsertAdminSettings;
|
|
516
|
-
exports.getAdminSettings = getAdminSettings;
|
|
517
|
-
exports.updateAdminRcTokens = updateAdminRcTokens;
|
|
518
|
-
exports.getServerLoggingSettings = getServerLoggingSettings;
|
|
519
|
-
exports.updateServerLoggingSettings = updateServerLoggingSettings;
|
|
520
|
-
exports.getAdminReport = getAdminReport;
|
|
521
|
-
exports.getUserReport = getUserReport;
|
|
522
|
-
exports.getUserMapping = getUserMapping;
|
|
523
|
-
exports.reinitializeUserMapping = reinitializeUserMapping;
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const { AdminConfigModel } = require('../models/adminConfigModel');
|
|
3
|
+
const connectorRegistry = require('../connector/registry');
|
|
4
|
+
const oauth = require('../lib/oauth');
|
|
5
|
+
const { RingCentral } = require('../lib/ringcentral');
|
|
6
|
+
const { Connector } = require('../models/dynamo/connectorSchema');
|
|
7
|
+
const logger = require('../lib/logger');
|
|
8
|
+
const { handleDatabaseError } = require('../lib/errorHandler');
|
|
9
|
+
const { getHashValue } = require('../lib/util');
|
|
10
|
+
|
|
11
|
+
const CALL_AGGREGATION_GROUPS = ["Company", "CompanyNumbers", "Users", "Queues", "IVRs", "IVAs", "SharedLines", "UserGroups", "Sites", "Departments"]
|
|
12
|
+
const RC_EXTENSION_ENDPOINT = 'https://platform.ringcentral.com/restapi/v1.0/account/~/extension/~';
|
|
13
|
+
|
|
14
|
+
async function validateRcUserToken({ rcAccessToken }) {
|
|
15
|
+
if (!rcAccessToken) {
|
|
16
|
+
throw new Error('rcAccessToken is required');
|
|
17
|
+
}
|
|
18
|
+
const rcExtensionResponse = await axios.get(
|
|
19
|
+
RC_EXTENSION_ENDPOINT,
|
|
20
|
+
{
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${rcAccessToken}`,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const extensionData = rcExtensionResponse.data ?? {};
|
|
26
|
+
return {
|
|
27
|
+
rcAccountId: extensionData?.account?.id?.toString() ?? '',
|
|
28
|
+
rcExtensionId: extensionData?.id?.toString() ?? ''
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function validateAdminRole({ rcAccessToken }) {
|
|
33
|
+
const rcExtensionResponse = await axios.get(
|
|
34
|
+
RC_EXTENSION_ENDPOINT,
|
|
35
|
+
{
|
|
36
|
+
headers: {
|
|
37
|
+
Authorization: `Bearer ${rcAccessToken}`,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
isValidated: !!rcExtensionResponse.data?.permissions?.admin?.enabled || (!!process.env.ADMIN_EXTENSION_ID_DEV_PASS_LIST && process.env.ADMIN_EXTENSION_ID_DEV_PASS_LIST.split(',').includes(rcExtensionResponse.data.id.toString())),
|
|
42
|
+
rcAccountId: rcExtensionResponse.data.account.id
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function upsertAdminSettings({ hashedRcAccountId, adminSettings }) {
|
|
47
|
+
let existingAdminConfig = await AdminConfigModel.findByPk(hashedRcAccountId);
|
|
48
|
+
if (existingAdminConfig) {
|
|
49
|
+
await existingAdminConfig.update({
|
|
50
|
+
...adminSettings
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
await AdminConfigModel.create({
|
|
54
|
+
id: hashedRcAccountId,
|
|
55
|
+
...adminSettings
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function getAdminSettings({ hashedRcAccountId }) {
|
|
61
|
+
const existingAdminConfig = await AdminConfigModel.findByPk(hashedRcAccountId);
|
|
62
|
+
return existingAdminConfig;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function updateAdminRcTokens({ hashedRcAccountId, adminAccessToken, adminRefreshToken, adminTokenExpiry }) {
|
|
66
|
+
const existingAdminConfig = await AdminConfigModel.findByPk(hashedRcAccountId);
|
|
67
|
+
if (existingAdminConfig) {
|
|
68
|
+
await existingAdminConfig.update({ adminAccessToken, adminRefreshToken, adminTokenExpiry });
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
await AdminConfigModel.create({
|
|
72
|
+
id: hashedRcAccountId,
|
|
73
|
+
adminAccessToken,
|
|
74
|
+
adminRefreshToken,
|
|
75
|
+
adminTokenExpiry
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function getServerLoggingSettings({ user }) {
|
|
81
|
+
const platformModule = connectorRegistry.getConnector(user.platform);
|
|
82
|
+
if (platformModule.getServerLoggingSettings) {
|
|
83
|
+
const serverLoggingSettings = await platformModule.getServerLoggingSettings({ user });
|
|
84
|
+
return serverLoggingSettings;
|
|
85
|
+
}
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function updateServerLoggingSettings({ user, additionalFieldValues }) {
|
|
90
|
+
const platformModule = connectorRegistry.getConnector(user.platform);
|
|
91
|
+
const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname })));
|
|
92
|
+
if (platformModule.updateServerLoggingSettings) {
|
|
93
|
+
const { successful, returnMessage } = await platformModule.updateServerLoggingSettings({ user, additionalFieldValues, oauthApp });
|
|
94
|
+
return { successful, returnMessage };
|
|
95
|
+
}
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function getAdminReport({ rcAccountId, timezone, timeFrom, timeTo, groupBy }) {
|
|
100
|
+
try {
|
|
101
|
+
if (!process.env.RINGCENTRAL_SERVER || !process.env.RINGCENTRAL_CLIENT_ID || !process.env.RINGCENTRAL_CLIENT_SECRET) {
|
|
102
|
+
return {
|
|
103
|
+
callLogStats: {}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const rcSDK = new RingCentral({
|
|
107
|
+
server: process.env.RINGCENTRAL_SERVER,
|
|
108
|
+
clientId: process.env.RINGCENTRAL_CLIENT_ID,
|
|
109
|
+
clientSecret: process.env.RINGCENTRAL_CLIENT_SECRET,
|
|
110
|
+
redirectUri: `${process.env.APP_SERVER}/ringcentral/oauth/callback`
|
|
111
|
+
});
|
|
112
|
+
const hashedRcAccountId = getHashValue(rcAccountId, process.env.HASH_KEY);
|
|
113
|
+
let adminConfig = await AdminConfigModel.findByPk(hashedRcAccountId);
|
|
114
|
+
const isTokenExpired = adminConfig.adminTokenExpiry < new Date();
|
|
115
|
+
if (isTokenExpired) {
|
|
116
|
+
const { access_token, refresh_token, expire_time } = await rcSDK.refreshToken({
|
|
117
|
+
refresh_token: adminConfig.adminRefreshToken,
|
|
118
|
+
expires_in: adminConfig.adminTokenExpiry,
|
|
119
|
+
refresh_token_expires_in: adminConfig.adminTokenExpiry
|
|
120
|
+
});
|
|
121
|
+
adminConfig = await AdminConfigModel.update({ adminAccessToken: access_token, adminRefreshToken: refresh_token, adminTokenExpiry: expire_time }, { where: { id: hashedRcAccountId } });
|
|
122
|
+
}
|
|
123
|
+
const callsAggregationData = await rcSDK.getCallsAggregationData({
|
|
124
|
+
token: { access_token: adminConfig.adminAccessToken, token_type: 'Bearer' },
|
|
125
|
+
timezone,
|
|
126
|
+
timeFrom,
|
|
127
|
+
timeTo,
|
|
128
|
+
groupBy: groupBy == 'undefined' ? CALL_AGGREGATION_GROUPS[0] : groupBy
|
|
129
|
+
});
|
|
130
|
+
var callLogStats = [];
|
|
131
|
+
var itemKeys = [];
|
|
132
|
+
for (const record of callsAggregationData.data.records) {
|
|
133
|
+
if (!record?.info?.name) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
itemKeys.push(record.info.name);
|
|
137
|
+
var dataCounter = record.counters;
|
|
138
|
+
var inboundCallCount = dataCounter.callsByDirection.values.inbound;
|
|
139
|
+
var outboundCallCount = dataCounter.callsByDirection.values.outbound;
|
|
140
|
+
var answeredCallCount = dataCounter.callsByResponse.values.answered;
|
|
141
|
+
// keep 2 decimal places
|
|
142
|
+
var answeredCallPercentage = inboundCallCount === 0 ? '0%' : `${((answeredCallCount / inboundCallCount) * 100).toFixed(2)}%`;
|
|
143
|
+
var totalTalkTime = Number(record.timers.allCalls.values) === 0 ? 0 : Number(record.timers.allCalls.values).toFixed(2);
|
|
144
|
+
var averageTalkTime = Number(totalTalkTime) === 0 ? 0 : (Number(totalTalkTime) / (inboundCallCount + outboundCallCount)).toFixed(2);
|
|
145
|
+
callLogStats.push({
|
|
146
|
+
name: record.info.name,
|
|
147
|
+
inboundCallCount,
|
|
148
|
+
outboundCallCount,
|
|
149
|
+
answeredCallCount,
|
|
150
|
+
answeredCallPercentage,
|
|
151
|
+
totalTalkTime,
|
|
152
|
+
averageTalkTime
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
callLogStats,
|
|
157
|
+
itemKeys,
|
|
158
|
+
groupedBy: callsAggregationData.data.groupedBy,
|
|
159
|
+
groupKeys: CALL_AGGREGATION_GROUPS
|
|
160
|
+
};
|
|
161
|
+
} catch (error) {
|
|
162
|
+
logger.error('Error getting admin report', { error });
|
|
163
|
+
return {
|
|
164
|
+
callLogStats: {}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function getUserReport({ rcAccountId, rcExtensionId, timezone, timeFrom, timeTo }) {
|
|
170
|
+
try {
|
|
171
|
+
if (!process.env.RINGCENTRAL_SERVER || !process.env.RINGCENTRAL_CLIENT_ID || !process.env.RINGCENTRAL_CLIENT_SECRET) {
|
|
172
|
+
return {
|
|
173
|
+
callLogStats: {}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const rcSDK = new RingCentral({
|
|
177
|
+
server: process.env.RINGCENTRAL_SERVER,
|
|
178
|
+
clientId: process.env.RINGCENTRAL_CLIENT_ID,
|
|
179
|
+
clientSecret: process.env.RINGCENTRAL_CLIENT_SECRET,
|
|
180
|
+
redirectUri: `${process.env.APP_SERVER}/ringcentral/oauth/callback`
|
|
181
|
+
});
|
|
182
|
+
const hashedRcAccountId = getHashValue(rcAccountId, process.env.HASH_KEY);
|
|
183
|
+
let adminConfig = await AdminConfigModel.findByPk(hashedRcAccountId);
|
|
184
|
+
const isTokenExpired = adminConfig.adminTokenExpiry < new Date();
|
|
185
|
+
if (isTokenExpired) {
|
|
186
|
+
const { access_token, refresh_token, expire_time } = await rcSDK.refreshToken({
|
|
187
|
+
refresh_token: adminConfig.adminRefreshToken,
|
|
188
|
+
expires_in: adminConfig.adminTokenExpiry,
|
|
189
|
+
refresh_token_expires_in: adminConfig.adminTokenExpiry
|
|
190
|
+
});
|
|
191
|
+
adminConfig = await AdminConfigModel.update({ adminAccessToken: access_token, adminRefreshToken: refresh_token, adminTokenExpiry: expire_time }, { where: { id: hashedRcAccountId } });
|
|
192
|
+
}
|
|
193
|
+
const callLogData = await rcSDK.getCallLogData({
|
|
194
|
+
extensionId: rcExtensionId,
|
|
195
|
+
token: { access_token: adminConfig.adminAccessToken, token_type: 'Bearer' },
|
|
196
|
+
timezone,
|
|
197
|
+
timeFrom,
|
|
198
|
+
timeTo
|
|
199
|
+
});
|
|
200
|
+
// phone activity
|
|
201
|
+
const inboundCallCount = callLogData.records.filter(call => call.direction === 'Inbound').length;
|
|
202
|
+
const outboundCallCount = callLogData.records.filter(call => call.direction === 'Outbound').length;
|
|
203
|
+
const answeredCallCount = callLogData.records.filter(call => call.direction === 'Inbound' && (call.result === 'Call connected' || call.result === 'Accepted' || call.result === 'Answered Not Accepted')).length;
|
|
204
|
+
const answeredCallPercentage = answeredCallCount === 0 ? '0%' : `${((answeredCallCount / (inboundCallCount || 1)) * 100).toFixed(2)}%`;
|
|
205
|
+
// phone engagement
|
|
206
|
+
const totalTalkTime = Math.round(callLogData.records.reduce((acc, call) => acc + (call.duration || 0), 0) / 60) || 0;
|
|
207
|
+
const averageTalkTime = Math.round(totalTalkTime / (inboundCallCount + outboundCallCount)) || 0;
|
|
208
|
+
const smsLogData = await rcSDK.getSMSData({
|
|
209
|
+
extensionId: rcExtensionId,
|
|
210
|
+
token: { access_token: adminConfig.adminAccessToken, token_type: 'Bearer' },
|
|
211
|
+
timezone,
|
|
212
|
+
timeFrom,
|
|
213
|
+
timeTo
|
|
214
|
+
});
|
|
215
|
+
const smsSentCount = smsLogData.records.filter(sms => sms.direction === 'Outbound').length;
|
|
216
|
+
const smsReceivedCount = smsLogData.records.filter(sms => sms.direction === 'Inbound').length;
|
|
217
|
+
const reportStats = {
|
|
218
|
+
callLogStats: {
|
|
219
|
+
inboundCallCount,
|
|
220
|
+
outboundCallCount,
|
|
221
|
+
answeredCallCount,
|
|
222
|
+
answeredCallPercentage,
|
|
223
|
+
totalTalkTime,
|
|
224
|
+
averageTalkTime
|
|
225
|
+
},
|
|
226
|
+
smsLogStats: {
|
|
227
|
+
smsSentCount,
|
|
228
|
+
smsReceivedCount
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
return reportStats;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
logger.error('Error getting user report', { error });
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function getUserMapping({ user, hashedRcAccountId, rcExtensionList }) {
|
|
239
|
+
let adminConfig = null;
|
|
240
|
+
try {
|
|
241
|
+
adminConfig = await getAdminSettings({ hashedRcAccountId });
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
return handleDatabaseError(error, 'Error getting user mapping');
|
|
245
|
+
}
|
|
246
|
+
const platformModule = connectorRegistry.getConnector(user.platform);
|
|
247
|
+
if (platformModule.getUserList) {
|
|
248
|
+
const proxyId = user.platformAdditionalInfo?.proxyId;
|
|
249
|
+
let proxyConfig = null;
|
|
250
|
+
if (proxyId) {
|
|
251
|
+
proxyConfig = await Connector.getProxyConfig(proxyId);
|
|
252
|
+
if (!proxyConfig?.operations?.getUserList) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const authType = await platformModule.getAuthType({ proxyId, proxyConfig });
|
|
257
|
+
let authHeader = '';
|
|
258
|
+
switch (authType) {
|
|
259
|
+
case 'oauth':
|
|
260
|
+
const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
|
|
261
|
+
// eslint-disable-next-line no-param-reassign
|
|
262
|
+
user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
|
|
263
|
+
if (!user) {
|
|
264
|
+
return {
|
|
265
|
+
successful: false,
|
|
266
|
+
returnMessage: {
|
|
267
|
+
message: `User session expired. Please connect again.`,
|
|
268
|
+
messageType: 'warning',
|
|
269
|
+
ttl: 5000
|
|
270
|
+
},
|
|
271
|
+
isRevokeUserSession: true
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
authHeader = `Bearer ${user.accessToken}`;
|
|
275
|
+
break;
|
|
276
|
+
case 'apiKey':
|
|
277
|
+
const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
|
|
278
|
+
authHeader = `Basic ${basicAuth}`;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
const crmUserList = await platformModule.getUserList({ user, authHeader, proxyConfig });
|
|
282
|
+
const userMappingResult = [];
|
|
283
|
+
const newUserMappings = [];
|
|
284
|
+
for (const crmUser of crmUserList) {
|
|
285
|
+
const existingMapping = adminConfig?.userMappings?.find(u => u.crmUserId == crmUser.id);
|
|
286
|
+
let existingMappingRcExtensionIds = [];
|
|
287
|
+
// TEMP: backward compatibility for string value
|
|
288
|
+
if (existingMapping?.rcExtensionId) {
|
|
289
|
+
if (typeof (existingMapping.rcExtensionId) === 'string') {
|
|
290
|
+
existingMappingRcExtensionIds = [existingMapping.rcExtensionId];
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
existingMappingRcExtensionIds = existingMapping.rcExtensionId;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const rcExtension = rcExtensionList.filter(e => existingMappingRcExtensionIds.includes(e.id));
|
|
297
|
+
// Case: existing mapping
|
|
298
|
+
if (existingMapping) {
|
|
299
|
+
userMappingResult.push({
|
|
300
|
+
crmUser: {
|
|
301
|
+
id: crmUser.id,
|
|
302
|
+
name: crmUser.name ?? '',
|
|
303
|
+
email: crmUser.email ?? '',
|
|
304
|
+
},
|
|
305
|
+
rcUser: rcExtension.map(e => ({
|
|
306
|
+
extensionId: e.id,
|
|
307
|
+
name: e?.name || `${e.firstName} ${e.lastName}`,
|
|
308
|
+
extensionNumber: e?.extensionNumber ?? '',
|
|
309
|
+
email: e?.email ?? ''
|
|
310
|
+
}))
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
// Case: new mapping
|
|
314
|
+
else {
|
|
315
|
+
const rcExtensionForNewMapping = rcExtensionList.find(u =>
|
|
316
|
+
u.email === crmUser.email ||
|
|
317
|
+
u.name === crmUser.name ||
|
|
318
|
+
(`${u.firstName} ${u.lastName}` === crmUser.name)
|
|
319
|
+
);
|
|
320
|
+
if (rcExtensionForNewMapping) {
|
|
321
|
+
userMappingResult.push({
|
|
322
|
+
crmUser: {
|
|
323
|
+
id: crmUser.id,
|
|
324
|
+
name: crmUser.name ?? '',
|
|
325
|
+
email: crmUser.email ?? '',
|
|
326
|
+
},
|
|
327
|
+
rcUser: [{
|
|
328
|
+
extensionId: rcExtensionForNewMapping.id,
|
|
329
|
+
name: rcExtensionForNewMapping.name || `${rcExtensionForNewMapping.firstName} ${rcExtensionForNewMapping.lastName}`,
|
|
330
|
+
extensionNumber: rcExtensionForNewMapping?.extensionNumber ?? '',
|
|
331
|
+
email: rcExtensionForNewMapping?.email ?? ''
|
|
332
|
+
}]
|
|
333
|
+
});
|
|
334
|
+
newUserMappings.push({
|
|
335
|
+
crmUserId: crmUser.id.toString(),
|
|
336
|
+
rcExtensionId: [rcExtensionForNewMapping.id.toString()]
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
userMappingResult.push({
|
|
341
|
+
crmUser: {
|
|
342
|
+
id: crmUser.id,
|
|
343
|
+
name: crmUser.name ?? '',
|
|
344
|
+
email: crmUser.email ?? '',
|
|
345
|
+
},
|
|
346
|
+
rcUser: []
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// One-time init
|
|
352
|
+
if (!adminConfig?.userMappings) {
|
|
353
|
+
const initialUserMappings = [];
|
|
354
|
+
for (const userMapping of userMappingResult) {
|
|
355
|
+
if (userMapping.rcUser?.extensionId) {
|
|
356
|
+
initialUserMappings.push({
|
|
357
|
+
crmUserId: userMapping.crmUser.id.toString(),
|
|
358
|
+
rcExtensionId: [userMapping.rcUser.extensionId.toString()]
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
await upsertAdminSettings({
|
|
364
|
+
hashedRcAccountId,
|
|
365
|
+
adminSettings: {
|
|
366
|
+
userMappings: initialUserMappings
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
return handleDatabaseError(error, 'Error initializing user mapping');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Incremental update
|
|
375
|
+
if (newUserMappings.length > 0) {
|
|
376
|
+
// TEMP: convert string to array
|
|
377
|
+
if (adminConfig?.userMappings) {
|
|
378
|
+
adminConfig.userMappings = adminConfig.userMappings.map(u => ({
|
|
379
|
+
...u,
|
|
380
|
+
rcExtensionId: [u.rcExtensionId]
|
|
381
|
+
}));
|
|
382
|
+
try {
|
|
383
|
+
await upsertAdminSettings({
|
|
384
|
+
hashedRcAccountId,
|
|
385
|
+
adminSettings: {
|
|
386
|
+
userMappings: [...adminConfig.userMappings, ...newUserMappings]
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
return handleDatabaseError(error, 'Error updating user mapping');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
try {
|
|
396
|
+
await upsertAdminSettings({
|
|
397
|
+
hashedRcAccountId,
|
|
398
|
+
adminSettings: {
|
|
399
|
+
userMappings: [...newUserMappings]
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
return handleDatabaseError(error, 'Error updating user mapping');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return userMappingResult;
|
|
409
|
+
}
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function reinitializeUserMapping({ user, hashedRcAccountId, rcExtensionList }) {
|
|
414
|
+
const platformModule = connectorRegistry.getConnector(user.platform);
|
|
415
|
+
if (!platformModule.getUserList) {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const proxyId = user.platformAdditionalInfo?.proxyId;
|
|
420
|
+
let proxyConfig = null;
|
|
421
|
+
if (proxyId) {
|
|
422
|
+
proxyConfig = await Connector.getProxyConfig(proxyId);
|
|
423
|
+
if (!proxyConfig?.operations?.getUserList) {
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const authType = await platformModule.getAuthType({ proxyId, proxyConfig });
|
|
429
|
+
let authHeader = '';
|
|
430
|
+
switch (authType) {
|
|
431
|
+
case 'oauth':
|
|
432
|
+
const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
|
|
433
|
+
// eslint-disable-next-line no-param-reassign
|
|
434
|
+
user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
|
|
435
|
+
if (!user) {
|
|
436
|
+
return {
|
|
437
|
+
successful: false,
|
|
438
|
+
returnMessage: {
|
|
439
|
+
message: `User session expired. Please connect again.`,
|
|
440
|
+
messageType: 'warning',
|
|
441
|
+
ttl: 5000
|
|
442
|
+
},
|
|
443
|
+
isRevokeUserSession: true
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
authHeader = `Bearer ${user.accessToken}`;
|
|
447
|
+
break;
|
|
448
|
+
case 'apiKey':
|
|
449
|
+
const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
|
|
450
|
+
authHeader = `Basic ${basicAuth}`;
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const crmUserList = await platformModule.getUserList({ user, authHeader, proxyConfig });
|
|
455
|
+
const userMappingResult = [];
|
|
456
|
+
const initialUserMappings = [];
|
|
457
|
+
|
|
458
|
+
// Auto-match CRM users with RC extensions by email or name
|
|
459
|
+
for (const crmUser of crmUserList) {
|
|
460
|
+
const rcExtensionForMapping = rcExtensionList.find(u =>
|
|
461
|
+
u.email === crmUser.email ||
|
|
462
|
+
u.name === crmUser.name ||
|
|
463
|
+
(`${u.firstName} ${u.lastName}` === crmUser.name)
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
if (rcExtensionForMapping) {
|
|
467
|
+
userMappingResult.push({
|
|
468
|
+
crmUser: {
|
|
469
|
+
id: crmUser.id,
|
|
470
|
+
name: crmUser.name ?? '',
|
|
471
|
+
email: crmUser.email ?? '',
|
|
472
|
+
},
|
|
473
|
+
rcUser: [{
|
|
474
|
+
extensionId: rcExtensionForMapping.id,
|
|
475
|
+
name: rcExtensionForMapping.name || `${rcExtensionForMapping.firstName} ${rcExtensionForMapping.lastName}`,
|
|
476
|
+
extensionNumber: rcExtensionForMapping?.extensionNumber ?? '',
|
|
477
|
+
email: rcExtensionForMapping?.email ?? ''
|
|
478
|
+
}]
|
|
479
|
+
});
|
|
480
|
+
initialUserMappings.push({
|
|
481
|
+
crmUserId: crmUser.id.toString(),
|
|
482
|
+
rcExtensionId: [rcExtensionForMapping.id.toString()]
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
userMappingResult.push({
|
|
487
|
+
crmUser: {
|
|
488
|
+
id: crmUser.id,
|
|
489
|
+
name: crmUser.name ?? '',
|
|
490
|
+
email: crmUser.email ?? '',
|
|
491
|
+
},
|
|
492
|
+
rcUser: []
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Overwrite existing mappings with fresh auto-matched mappings
|
|
498
|
+
try {
|
|
499
|
+
await upsertAdminSettings({
|
|
500
|
+
hashedRcAccountId,
|
|
501
|
+
adminSettings: {
|
|
502
|
+
userMappings: initialUserMappings
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
return handleDatabaseError(error, 'Error reinitializing user mapping');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return userMappingResult;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
exports.validateAdminRole = validateAdminRole;
|
|
514
|
+
exports.validateRcUserToken = validateRcUserToken;
|
|
515
|
+
exports.upsertAdminSettings = upsertAdminSettings;
|
|
516
|
+
exports.getAdminSettings = getAdminSettings;
|
|
517
|
+
exports.updateAdminRcTokens = updateAdminRcTokens;
|
|
518
|
+
exports.getServerLoggingSettings = getServerLoggingSettings;
|
|
519
|
+
exports.updateServerLoggingSettings = updateServerLoggingSettings;
|
|
520
|
+
exports.getAdminReport = getAdminReport;
|
|
521
|
+
exports.getUserReport = getUserReport;
|
|
522
|
+
exports.getUserMapping = getUserMapping;
|
|
523
|
+
exports.reinitializeUserMapping = reinitializeUserMapping;
|