@app-connect/core 1.7.5 → 1.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/handlers/admin.js +29 -17
- package/handlers/auth.js +10 -4
- package/handlers/calldown.js +33 -0
- package/handlers/contact.js +54 -3
- package/handlers/log.js +4 -4
- package/index.js +377 -163
- package/lib/debugTracer.js +159 -0
- package/lib/oauth.js +26 -18
- package/lib/ringcentral.js +2 -2
- package/lib/s3ErrorLogReport.js +66 -0
- package/models/accountDataModel.js +34 -0
- package/package.json +70 -67
- package/releaseNotes.json +68 -0
- package/test/connector/registry.test.js +145 -0
- package/test/handlers/admin.test.js +583 -0
- package/test/handlers/auth.test.js +355 -0
- package/test/handlers/contact.test.js +852 -0
- package/test/handlers/log.test.js +868 -0
- package/test/lib/callLogComposer.test.js +1231 -0
- package/test/lib/debugTracer.test.js +328 -0
- package/test/lib/oauth.test.js +359 -0
- package/test/lib/ringcentral.test.js +473 -0
- package/test/lib/util.test.js +282 -0
- package/test/models/accountDataModel.test.js +98 -0
- package/test/models/dynamo/connectorSchema.test.js +189 -0
- package/test/models/models.test.js +539 -0
- package/test/setup.js +176 -176
package/handlers/admin.js
CHANGED
|
@@ -5,6 +5,8 @@ const oauth = require('../lib/oauth');
|
|
|
5
5
|
const { RingCentral } = require('../lib/ringcentral');
|
|
6
6
|
const { Connector } = require('../models/dynamo/connectorSchema');
|
|
7
7
|
|
|
8
|
+
const CALL_AGGREGATION_GROUPS = ["Company", "CompanyNumbers", "Users", "Queues", "IVRs", "IVAs", "SharedLines", "UserGroups", "Sites", "Departments"]
|
|
9
|
+
|
|
8
10
|
async function validateAdminRole({ rcAccessToken }) {
|
|
9
11
|
const rcExtensionResponse = await axios.get(
|
|
10
12
|
'https://platform.ringcentral.com/restapi/v1.0/account/~/extension/~',
|
|
@@ -72,7 +74,7 @@ async function updateServerLoggingSettings({ user, additionalFieldValues }) {
|
|
|
72
74
|
return {};
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
async function getAdminReport({ rcAccountId, timezone, timeFrom, timeTo }) {
|
|
77
|
+
async function getAdminReport({ rcAccountId, timezone, timeFrom, timeTo, groupBy }) {
|
|
76
78
|
try {
|
|
77
79
|
if (!process.env.RINGCENTRAL_SERVER || !process.env.RINGCENTRAL_CLIENT_ID || !process.env.RINGCENTRAL_CLIENT_SECRET) {
|
|
78
80
|
return {
|
|
@@ -99,29 +101,39 @@ async function getAdminReport({ rcAccountId, timezone, timeFrom, timeTo }) {
|
|
|
99
101
|
token: { access_token: adminConfig.adminAccessToken, token_type: 'Bearer' },
|
|
100
102
|
timezone,
|
|
101
103
|
timeFrom,
|
|
102
|
-
timeTo
|
|
104
|
+
timeTo,
|
|
105
|
+
groupBy: groupBy == 'undefined' ? CALL_AGGREGATION_GROUPS[0] : groupBy
|
|
103
106
|
});
|
|
104
|
-
var
|
|
105
|
-
var
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
107
|
+
var callLogStats = [];
|
|
108
|
+
var itemKeys = [];
|
|
109
|
+
for (const record of callsAggregationData.data.records) {
|
|
110
|
+
if(!record?.info?.name){
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
itemKeys.push(record.info.name);
|
|
114
|
+
var dataCounter = record.counters;
|
|
115
|
+
var inboundCallCount = dataCounter.callsByDirection.values.inbound;
|
|
116
|
+
var outboundCallCount = dataCounter.callsByDirection.values.outbound;
|
|
117
|
+
var answeredCallCount = dataCounter.callsByResponse.values.answered;
|
|
118
|
+
// keep 2 decimal places
|
|
119
|
+
var answeredCallPercentage = inboundCallCount === 0 ? '0%' : `${((answeredCallCount / inboundCallCount) * 100).toFixed(2)}%`;
|
|
120
|
+
var totalTalkTime = Number(record.timers.allCalls.values) === 0 ? 0 : Number(record.timers.allCalls.values).toFixed(2);
|
|
121
|
+
var averageTalkTime = Number(totalTalkTime) === 0 ? 0 : (Number(totalTalkTime) / (inboundCallCount + outboundCallCount)).toFixed(2);
|
|
122
|
+
callLogStats.push({
|
|
123
|
+
name: record.info.name,
|
|
118
124
|
inboundCallCount,
|
|
119
125
|
outboundCallCount,
|
|
120
126
|
answeredCallCount,
|
|
121
127
|
answeredCallPercentage,
|
|
122
128
|
totalTalkTime,
|
|
123
129
|
averageTalkTime
|
|
124
|
-
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
callLogStats,
|
|
134
|
+
itemKeys,
|
|
135
|
+
groupedBy: callsAggregationData.data.groupedBy,
|
|
136
|
+
groupKeys: CALL_AGGREGATION_GROUPS
|
|
125
137
|
};
|
|
126
138
|
} catch (error) {
|
|
127
139
|
console.error(error);
|
package/handlers/auth.js
CHANGED
|
@@ -6,13 +6,18 @@ const { RingCentral } = require('../lib/ringcentral');
|
|
|
6
6
|
const adminCore = require('./admin');
|
|
7
7
|
const { Connector } = require('../models/dynamo/connectorSchema');
|
|
8
8
|
|
|
9
|
-
async function onOAuthCallback({ platform, hostname, tokenUrl,
|
|
9
|
+
async function onOAuthCallback({ platform, hostname, tokenUrl, query }) {
|
|
10
|
+
const callbackUri = query.callbackUri;
|
|
11
|
+
const apiUrl = query.apiUrl;
|
|
12
|
+
const username = query.username;
|
|
13
|
+
const proxyId = query.proxyId;
|
|
14
|
+
const userEmail = query.userEmail;
|
|
10
15
|
const platformModule = connectorRegistry.getConnector(platform);
|
|
11
16
|
let proxyConfig = null;
|
|
12
17
|
if (proxyId) {
|
|
13
18
|
proxyConfig = await Connector.getProxyConfig(proxyId);
|
|
14
19
|
}
|
|
15
|
-
const oauthInfo = await platformModule.getOauthInfo({ tokenUrl, hostname, rcAccountId: query.rcAccountId, proxyId, proxyConfig });
|
|
20
|
+
const oauthInfo = await platformModule.getOauthInfo({ tokenUrl, hostname, rcAccountId: query.rcAccountId, proxyId, proxyConfig, userEmail });
|
|
16
21
|
|
|
17
22
|
if (oauthInfo.failMessage) {
|
|
18
23
|
return {
|
|
@@ -27,12 +32,13 @@ async function onOAuthCallback({ platform, hostname, tokenUrl, callbackUri, apiU
|
|
|
27
32
|
// Some platforms require different oauth queries, this won't affect normal OAuth process unless CRM module implements getOverridingOAuthOption() method
|
|
28
33
|
let overridingOAuthOption = null;
|
|
29
34
|
if (platformModule.getOverridingOAuthOption != null) {
|
|
30
|
-
|
|
35
|
+
const code = new URL(callbackUri).searchParams.get('code');
|
|
36
|
+
overridingOAuthOption = platformModule.getOverridingOAuthOption({ code });
|
|
31
37
|
}
|
|
32
38
|
const oauthApp = oauth.getOAuthApp(oauthInfo);
|
|
33
39
|
const { accessToken, refreshToken, expires } = await oauthApp.code.getToken(callbackUri, overridingOAuthOption);
|
|
34
40
|
const authHeader = `Bearer ${accessToken}`;
|
|
35
|
-
const { successful, platformUserInfo, returnMessage } = await platformModule.getUserInfo({ authHeader, tokenUrl, apiUrl, hostname, platform, username, callbackUri, query, proxyId, proxyConfig });
|
|
41
|
+
const { successful, platformUserInfo, returnMessage } = await platformModule.getUserInfo({ authHeader, tokenUrl, apiUrl, hostname, platform, username, callbackUri, query, proxyId, proxyConfig, userEmail });
|
|
36
42
|
|
|
37
43
|
if (successful) {
|
|
38
44
|
let userInfo = await saveUserInfo({
|
package/handlers/calldown.js
CHANGED
|
@@ -53,8 +53,41 @@ async function markCalled({ jwtToken, id, lastCallAt }) {
|
|
|
53
53
|
return { successful: true };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
async function update({ jwtToken, id, updateData }) {
|
|
57
|
+
const unAuthData = jwt.decodeJwt(jwtToken);
|
|
58
|
+
if (!unAuthData?.id) throw new Error('Unauthorized');
|
|
59
|
+
|
|
60
|
+
// Prepare the update object with only valid fields
|
|
61
|
+
const allowedFields = ['contactId', 'contactType', 'contactName', 'phoneNumber', 'status', 'scheduledAt', 'lastCallAt', 'note'];
|
|
62
|
+
const updateObject = {};
|
|
63
|
+
|
|
64
|
+
// Filter and prepare update data
|
|
65
|
+
Object.keys(updateData).forEach(key => {
|
|
66
|
+
if (allowedFields.includes(key)) {
|
|
67
|
+
let value = updateData[key];
|
|
68
|
+
|
|
69
|
+
// Handle date fields
|
|
70
|
+
if ((key === 'scheduledAt' || key === 'lastCallAt') && value) {
|
|
71
|
+
value = new Date(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
updateObject[key] = value;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// If no valid fields to update, throw error
|
|
79
|
+
if (Object.keys(updateObject).length === 0) {
|
|
80
|
+
throw new Error('No valid fields to update');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const [affected] = await CallDownListModel.update(updateObject, { where: { id, userId: unAuthData.id } });
|
|
84
|
+
if (!affected) throw new Error('Not found');
|
|
85
|
+
return { successful: true };
|
|
86
|
+
}
|
|
87
|
+
|
|
56
88
|
exports.schedule = schedule;
|
|
57
89
|
exports.list = list;
|
|
58
90
|
exports.remove = remove;
|
|
59
91
|
exports.markCalled = markCalled;
|
|
92
|
+
exports.update = update;
|
|
60
93
|
|
package/handlers/contact.js
CHANGED
|
@@ -3,8 +3,11 @@ const { UserModel } = require('../models/userModel');
|
|
|
3
3
|
const errorMessage = require('../lib/generalErrorMessage');
|
|
4
4
|
const connectorRegistry = require('../connector/registry');
|
|
5
5
|
const { Connector } = require('../models/dynamo/connectorSchema');
|
|
6
|
+
const { DebugTracer } = require('../lib/debugTracer');
|
|
7
|
+
const { AccountDataModel } = require('../models/accountDataModel');
|
|
6
8
|
|
|
7
|
-
async function findContact({ platform, userId, phoneNumber, overridingFormat, isExtension }) {
|
|
9
|
+
async function findContact({ platform, userId, phoneNumber, overridingFormat, isExtension, tracer, isForceRefreshAccountData = false }) {
|
|
10
|
+
tracer?.trace('handler.findContact:entered', { platform, userId, phoneNumber });
|
|
8
11
|
try {
|
|
9
12
|
let user = await UserModel.findOne({
|
|
10
13
|
where: {
|
|
@@ -12,7 +15,10 @@ async function findContact({ platform, userId, phoneNumber, overridingFormat, is
|
|
|
12
15
|
platform
|
|
13
16
|
}
|
|
14
17
|
});
|
|
18
|
+
tracer?.trace('handler.findContact:userFound', { user });
|
|
19
|
+
|
|
15
20
|
if (!user || !user.accessToken) {
|
|
21
|
+
tracer?.trace('handler.findContact:noUser', { userId });
|
|
16
22
|
return {
|
|
17
23
|
successful: false,
|
|
18
24
|
returnMessage: {
|
|
@@ -22,30 +28,73 @@ async function findContact({ platform, userId, phoneNumber, overridingFormat, is
|
|
|
22
28
|
}
|
|
23
29
|
};
|
|
24
30
|
}
|
|
31
|
+
// find cached contact by composite key; findByPk expects raw PK values, so use where clause
|
|
32
|
+
const existingMatchedContactInfo = await AccountDataModel.findOne({
|
|
33
|
+
where: {
|
|
34
|
+
rcAccountId: user.rcAccountId,
|
|
35
|
+
platformName: platform,
|
|
36
|
+
dataKey: `contact-${phoneNumber}`
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
if (!isForceRefreshAccountData) {
|
|
40
|
+
if (existingMatchedContactInfo) {
|
|
41
|
+
console.log('found existing matched contact info in account data');
|
|
42
|
+
return { successful: true, returnMessage: null, contact: existingMatchedContactInfo.data, extraDataTracking: null };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
25
45
|
const proxyId = user.platformAdditionalInfo?.proxyId;
|
|
26
46
|
let proxyConfig = null;
|
|
27
47
|
if (proxyId) {
|
|
28
48
|
proxyConfig = await Connector.getProxyConfig(proxyId);
|
|
49
|
+
tracer?.trace('handler.findContact:proxyConfig', { proxyConfig });
|
|
29
50
|
}
|
|
30
51
|
const platformModule = connectorRegistry.getConnector(platform);
|
|
31
52
|
const authType = await platformModule.getAuthType({ proxyId, proxyConfig });
|
|
53
|
+
tracer?.trace('handler.findContact:authType', { authType });
|
|
54
|
+
|
|
32
55
|
let authHeader = '';
|
|
33
56
|
switch (authType) {
|
|
34
57
|
case 'oauth':
|
|
35
58
|
const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
|
|
36
59
|
user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
|
|
37
60
|
authHeader = `Bearer ${user.accessToken}`;
|
|
61
|
+
tracer?.trace('handler.findContact:oauthAuth', { authHeader });
|
|
38
62
|
break;
|
|
39
63
|
case 'apiKey':
|
|
40
64
|
const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
|
|
41
65
|
authHeader = `Basic ${basicAuth}`;
|
|
66
|
+
tracer?.trace('handler.findContact:apiKeyAuth', {});
|
|
42
67
|
break;
|
|
43
68
|
}
|
|
44
|
-
|
|
69
|
+
|
|
70
|
+
const { successful, matchedContactInfo, returnMessage, extraDataTracking } = await platformModule.findContact({ user, authHeader, phoneNumber, overridingFormat, isExtension, proxyConfig, tracer, isForceRefreshAccountData });
|
|
71
|
+
tracer?.trace('handler.findContact:platformFindResult', { successful, matchedContactInfo });
|
|
72
|
+
|
|
45
73
|
if (matchedContactInfo != null && matchedContactInfo?.filter(c => !c.isNewContact)?.length > 0) {
|
|
74
|
+
tracer?.trace('handler.findContact:contactsFound', { count: matchedContactInfo.length });
|
|
75
|
+
// save in org data
|
|
76
|
+
// Danger: it does NOT support one RC account mapping to multiple CRM platforms, because contacts will be shared
|
|
77
|
+
if (user.rcAccountId) {
|
|
78
|
+
if(existingMatchedContactInfo)
|
|
79
|
+
{
|
|
80
|
+
await existingMatchedContactInfo.update({
|
|
81
|
+
data: matchedContactInfo
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else{
|
|
85
|
+
await AccountDataModel.create({
|
|
86
|
+
rcAccountId: user.rcAccountId,
|
|
87
|
+
platformName: platform,
|
|
88
|
+
dataKey: `contact-${phoneNumber}`,
|
|
89
|
+
data: matchedContactInfo
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
console.log('store new matched contact info in account data');
|
|
93
|
+
}
|
|
46
94
|
return { successful, returnMessage, contact: matchedContactInfo, extraDataTracking };
|
|
47
95
|
}
|
|
48
96
|
else {
|
|
97
|
+
tracer?.trace('handler.findContact:noContactsMatched', { matchedContactInfo });
|
|
49
98
|
if (returnMessage) {
|
|
50
99
|
return {
|
|
51
100
|
successful,
|
|
@@ -78,6 +127,8 @@ async function findContact({ platform, userId, phoneNumber, overridingFormat, is
|
|
|
78
127
|
}
|
|
79
128
|
} catch (e) {
|
|
80
129
|
console.error(`platform: ${platform} \n${e.stack} \n${JSON.stringify(e.response?.data)}`);
|
|
130
|
+
tracer?.traceError('handler.findContact:error', e, { platform, statusCode: e.response?.status });
|
|
131
|
+
|
|
81
132
|
if (e.response?.status === 429) {
|
|
82
133
|
return {
|
|
83
134
|
successful: false,
|
|
@@ -161,7 +212,7 @@ async function createContact({ platform, userId, phoneNumber, newContactName, ne
|
|
|
161
212
|
return { successful: false, returnMessage };
|
|
162
213
|
}
|
|
163
214
|
} catch (e) {
|
|
164
|
-
console.
|
|
215
|
+
console.error(`platform: ${platform} \n${e.stack}`);
|
|
165
216
|
if (e.response?.status === 429) {
|
|
166
217
|
return {
|
|
167
218
|
successful: false,
|
package/handlers/log.js
CHANGED
|
@@ -194,7 +194,7 @@ async function getCallLog({ userId, sessionIds, platform, requireDetails }) {
|
|
|
194
194
|
}
|
|
195
195
|
let logs = [];
|
|
196
196
|
let returnMessage = null;
|
|
197
|
-
let extraDataTracking = {}
|
|
197
|
+
let extraDataTracking = {};
|
|
198
198
|
|
|
199
199
|
// Handle undefined or null sessionIds
|
|
200
200
|
if (!sessionIds) {
|
|
@@ -303,7 +303,7 @@ async function getCallLog({ userId, sessionIds, platform, requireDetails }) {
|
|
|
303
303
|
{
|
|
304
304
|
id: '1',
|
|
305
305
|
type: 'text',
|
|
306
|
-
text: `Please check if your account has permission to
|
|
306
|
+
text: `Please check if your account has permission to READ logs.`
|
|
307
307
|
}
|
|
308
308
|
]
|
|
309
309
|
}
|
|
@@ -483,7 +483,7 @@ async function updateCallLog({ platform, userId, incomingData, hashedAccountId,
|
|
|
483
483
|
async function createMessageLog({ platform, userId, incomingData }) {
|
|
484
484
|
try {
|
|
485
485
|
let returnMessage = null;
|
|
486
|
-
let extraDataTracking = {}
|
|
486
|
+
let extraDataTracking = {};
|
|
487
487
|
if (incomingData.logInfo.messages.length === 0) {
|
|
488
488
|
return {
|
|
489
489
|
successful: false,
|
|
@@ -625,7 +625,7 @@ async function createMessageLog({ platform, userId, incomingData }) {
|
|
|
625
625
|
return { successful: true, logIds, returnMessage, extraDataTracking };
|
|
626
626
|
}
|
|
627
627
|
catch (e) {
|
|
628
|
-
console.
|
|
628
|
+
console.error(`platform: ${platform} \n${e.stack}`);
|
|
629
629
|
if (e.response?.status === 429) {
|
|
630
630
|
return {
|
|
631
631
|
successful: false,
|