@app-connect/core 1.6.9 → 1.7.0-beta.2

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 CHANGED
@@ -2,6 +2,7 @@ const axios = require('axios');
2
2
  const { AdminConfigModel } = require('../models/adminConfigModel');
3
3
  const adapterRegistry = require('../adapter/registry');
4
4
  const oauth = require('../lib/oauth');
5
+ const { RingCentral } = require('../lib/ringcentral');
5
6
 
6
7
  async function validateAdminRole({ rcAccessToken }) {
7
8
  const rcExtensionResponse = await axios.get(
@@ -36,6 +37,21 @@ async function getAdminSettings({ hashedRcAccountId }) {
36
37
  return existingAdminConfig;
37
38
  }
38
39
 
40
+ async function updateAdminRcTokens({ hashedRcAccountId, adminAccessToken, adminRefreshToken, adminTokenExpiry }) {
41
+ const existingAdminConfig = await AdminConfigModel.findByPk(hashedRcAccountId);
42
+ if (existingAdminConfig) {
43
+ await existingAdminConfig.update({ adminAccessToken, adminRefreshToken, adminTokenExpiry });
44
+ }
45
+ else {
46
+ await AdminConfigModel.create({
47
+ id: hashedRcAccountId,
48
+ adminAccessToken,
49
+ adminRefreshToken,
50
+ adminTokenExpiry
51
+ });
52
+ }
53
+ }
54
+
39
55
  async function getServerLoggingSettings({ user }) {
40
56
  const platformModule = adapterRegistry.getAdapter(user.platform);
41
57
  if (platformModule.getServerLoggingSettings) {
@@ -55,6 +71,123 @@ async function updateServerLoggingSettings({ user, additionalFieldValues }) {
55
71
  return {};
56
72
  }
57
73
 
74
+ async function getAdminReport({ rcAccountId, timezone, timeFrom, timeTo }) {
75
+ try {
76
+ const rcSDK = new RingCentral({
77
+ server: process.env.RINGCENTRAL_SERVER,
78
+ clientId: process.env.RINGCENTRAL_CLIENT_ID,
79
+ clientSecret: process.env.RINGCENTRAL_CLIENT_SECRET,
80
+ redirectUri: `${process.env.APP_SERVER}/ringcentral/oauth/callback`
81
+ });
82
+ let adminConfig = await AdminConfigModel.findByPk(rcAccountId);
83
+ const isTokenExpired = adminConfig.adminTokenExpiry < new Date();
84
+ if (isTokenExpired) {
85
+ const { access_token, refresh_token, expire_time } = await rcSDK.refreshToken({
86
+ refresh_token: adminConfig.adminRefreshToken,
87
+ expires_in: adminConfig.adminTokenExpiry,
88
+ refresh_token_expires_in: adminConfig.adminTokenExpiry
89
+ });
90
+ adminConfig = await AdminConfigModel.update({ adminAccessToken: access_token, adminRefreshToken: refresh_token, adminTokenExpiry: expire_time }, { where: { id: rcAccountId } });
91
+ }
92
+ const callsAggregationData = await rcSDK.getCallsAggregationData({
93
+ token: { access_token: adminConfig.adminAccessToken, token_type: 'Bearer' },
94
+ timezone,
95
+ timeFrom,
96
+ timeTo
97
+ });
98
+ var dataCounter = callsAggregationData.data.records[0].counters;
99
+ var inboundCallCount = dataCounter.callsByDirection.values.inbound;
100
+ var outboundCallCount = dataCounter.callsByDirection.values.outbound;
101
+ var answeredCallCount = dataCounter.callsByResponse.values.answered;
102
+ // keep 2 decimal places
103
+ var answeredCallPercentage = inboundCallCount === 0 ? '0%' : `${((answeredCallCount / inboundCallCount) * 100).toFixed(2)}%`;
104
+
105
+ var dataTimer = callsAggregationData.data.records[0].timers;
106
+ // keep 2 decimal places
107
+ var totalTalkTime = (dataTimer.allCalls.values / 60).toFixed(2);
108
+ // keep 2 decimal places
109
+ var averageTalkTime = (totalTalkTime / (inboundCallCount + outboundCallCount)).toFixed(2);
110
+ return {
111
+ callLogStats: {
112
+ inboundCallCount,
113
+ outboundCallCount,
114
+ answeredCallCount,
115
+ answeredCallPercentage,
116
+ totalTalkTime,
117
+ averageTalkTime
118
+ }
119
+ };
120
+ } catch (error) {
121
+ console.error(error);
122
+ return {
123
+ callLogStats: {}
124
+ };
125
+ }
126
+ }
127
+
128
+ async function getUserReport({ rcAccountId, rcExtensionId, timezone, timeFrom, timeTo }) {
129
+ try {
130
+ const rcSDK = new RingCentral({
131
+ server: process.env.RINGCENTRAL_SERVER,
132
+ clientId: process.env.RINGCENTRAL_CLIENT_ID,
133
+ clientSecret: process.env.RINGCENTRAL_CLIENT_SECRET,
134
+ redirectUri: `${process.env.APP_SERVER}/ringcentral/oauth/callback`
135
+ });
136
+ let adminConfig = await AdminConfigModel.findByPk(rcAccountId);
137
+ const isTokenExpired = adminConfig.adminTokenExpiry < new Date();
138
+ if (isTokenExpired) {
139
+ const { access_token, refresh_token, expire_time } = await rcSDK.refreshToken({
140
+ refresh_token: adminConfig.adminRefreshToken,
141
+ expires_in: adminConfig.adminTokenExpiry,
142
+ refresh_token_expires_in: adminConfig.adminTokenExpiry
143
+ });
144
+ adminConfig = await AdminConfigModel.update({ adminAccessToken: access_token, adminRefreshToken: refresh_token, adminTokenExpiry: expire_time }, { where: { id: rcAccountId } });
145
+ }
146
+ const callLogData = await rcSDK.getCallLogData({
147
+ extensionId: rcExtensionId,
148
+ token: { access_token: adminConfig.adminAccessToken, token_type: 'Bearer' },
149
+ timezone,
150
+ timeFrom,
151
+ timeTo
152
+ });
153
+ // phone activity
154
+ const inboundCallCount = callLogData.records.filter(call => call.direction === 'Inbound').length;
155
+ const outboundCallCount = callLogData.records.filter(call => call.direction === 'Outbound').length;
156
+ const answeredCallCount = callLogData.records.filter(call => call.direction === 'Inbound' && (call.result === 'Call connected' || call.result === 'Accepted' || call.result === 'Answered Not Accepted')).length;
157
+ const answeredCallPercentage = answeredCallCount === 0 ? '0%' : `${((answeredCallCount / (inboundCallCount || 1)) * 100).toFixed(2)}%`;
158
+ // phone engagement
159
+ const totalTalkTime = Math.round(callLogData.records.reduce((acc, call) => acc + (call.duration || 0), 0) / 60) || 0;
160
+ const averageTalkTime = Math.round(totalTalkTime / (inboundCallCount + outboundCallCount)) || 0;
161
+ const smsLogData = await rcSDK.getSMSData({
162
+ extensionId: rcExtensionId,
163
+ token: { access_token: adminConfig.adminAccessToken, token_type: 'Bearer' },
164
+ timezone,
165
+ timeFrom,
166
+ timeTo
167
+ });
168
+ const smsSentCount = smsLogData.records.filter(sms => sms.direction === 'Outbound').length;
169
+ const smsReceivedCount = smsLogData.records.filter(sms => sms.direction === 'Inbound').length;
170
+ const reportStats = {
171
+ callLogStats: {
172
+ inboundCallCount,
173
+ outboundCallCount,
174
+ answeredCallCount,
175
+ answeredCallPercentage,
176
+ totalTalkTime,
177
+ averageTalkTime
178
+ },
179
+ smsLogStats: {
180
+ smsSentCount,
181
+ smsReceivedCount
182
+ }
183
+ };
184
+ return reportStats;
185
+ } catch (error) {
186
+ console.error(error);
187
+ return null;
188
+ }
189
+ }
190
+
58
191
  async function getUserMapping({ user, hashedRcAccountId, rcExtensionList }) {
59
192
  const adminConfig = await getAdminSettings({ hashedRcAccountId });
60
193
  const platformModule = adapterRegistry.getAdapter(user.platform);
@@ -188,6 +321,9 @@ async function getUserMapping({ user, hashedRcAccountId, rcExtensionList }) {
188
321
  exports.validateAdminRole = validateAdminRole;
189
322
  exports.upsertAdminSettings = upsertAdminSettings;
190
323
  exports.getAdminSettings = getAdminSettings;
324
+ exports.updateAdminRcTokens = updateAdminRcTokens;
191
325
  exports.getServerLoggingSettings = getServerLoggingSettings;
192
326
  exports.updateServerLoggingSettings = updateServerLoggingSettings;
327
+ exports.getAdminReport = getAdminReport;
328
+ exports.getUserReport = getUserReport;
193
329
  exports.getUserMapping = getUserMapping;
package/handlers/auth.js CHANGED
@@ -2,6 +2,8 @@ const oauth = require('../lib/oauth');
2
2
  const { UserModel } = require('../models/userModel');
3
3
  const adapterRegistry = require('../adapter/registry');
4
4
  const Op = require('sequelize').Op;
5
+ const { RingCentral } = require('../lib/ringcentral');
6
+ const adminCore = require('./admin');
5
7
 
6
8
  async function onOAuthCallback({ platform, hostname, tokenUrl, callbackUri, apiUrl, username, query }) {
7
9
  const platformModule = adapterRegistry.getAdapter(platform);
@@ -92,6 +94,7 @@ async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken,
92
94
  if (existingUser) {
93
95
  await existingUser.update(
94
96
  {
97
+ platform,
95
98
  hostname,
96
99
  timezoneName,
97
100
  timezoneOffset,
@@ -204,7 +207,25 @@ async function authValidation({ platform, userId }) {
204
207
  }
205
208
  }
206
209
 
210
+ // Ringcentral
211
+ async function onRingcentralOAuthCallback({ code, rcAccountId }) {
212
+ const rcSDK = new RingCentral({
213
+ server: process.env.RINGCENTRAL_SERVER,
214
+ clientId: process.env.RINGCENTRAL_CLIENT_ID,
215
+ clientSecret: process.env.RINGCENTRAL_CLIENT_SECRET,
216
+ redirectUri: `${process.env.APP_SERVER}/ringcentral/oauth/callback`
217
+ });
218
+ const { access_token, refresh_token, expire_time } = await rcSDK.generateToken({ code });
219
+ await adminCore.updateAdminRcTokens({
220
+ hashedRcAccountId: rcAccountId,
221
+ adminAccessToken: access_token,
222
+ adminRefreshToken: refresh_token,
223
+ adminTokenExpiry: expire_time
224
+ });
225
+ }
226
+
207
227
  exports.onOAuthCallback = onOAuthCallback;
208
228
  exports.onApiKeyLogin = onApiKeyLogin;
209
229
  exports.authValidation = authValidation;
210
- exports.getLicenseStatus = getLicenseStatus;
230
+ exports.getLicenseStatus = getLicenseStatus;
231
+ exports.onRingcentralOAuthCallback = onRingcentralOAuthCallback;
@@ -0,0 +1,60 @@
1
+ const { UserModel } = require('../models/userModel');
2
+ const { CallDownListModel } = require('../models/callDownListModel');
3
+ const { Op } = require('sequelize');
4
+ const jwt = require('../lib/jwt');
5
+
6
+ async function schedule({ jwtToken, rcAccessToken, body }) {
7
+ const unAuthData = jwt.decodeJwt(jwtToken);
8
+ if (!unAuthData?.id) throw new Error('Unauthorized');
9
+ const user = await UserModel.findByPk(unAuthData.id);
10
+ if (!user) throw new Error('User not found');
11
+ const crypto = require('crypto');
12
+ const recordId = crypto.randomBytes(16).toString('hex');
13
+ const payload = {
14
+ id: recordId,
15
+ userId: user.id,
16
+ contactId: body.contactId?.toString?.() ?? body.contactId,
17
+ contactType: body.contactType ?? 'contact',
18
+ contactName: body.contactName ?? '',
19
+ phoneNumber: body.phoneNumber ?? '',
20
+ status: 'scheduled',
21
+ scheduledAt: body.scheduledAt ? new Date(body.scheduledAt) : null,
22
+ lastCallAt: null
23
+ };
24
+ await CallDownListModel.create(payload);
25
+ return { id: recordId };
26
+ }
27
+
28
+ async function list({ jwtToken, status }) {
29
+ const unAuthData = jwt.decodeJwt(jwtToken);
30
+ if (!unAuthData?.id) throw new Error('Unauthorized');
31
+ const statusParam = (status || 'All').toString().toLowerCase();
32
+ const whereClause = { userId: unAuthData.id };
33
+ if (statusParam === 'called') whereClause.status = 'called';
34
+ else if (['not called', 'not_called', 'notcalled'].includes(statusParam)) whereClause.status = { [Op.ne]: 'called' };
35
+ const items = await CallDownListModel.findAll({ where: whereClause, order: [["scheduledAt", "ASC"]] });
36
+ return { items };
37
+ }
38
+
39
+ async function remove({ jwtToken, id }) {
40
+ const unAuthData = jwt.decodeJwt(jwtToken);
41
+ if (!unAuthData?.id) throw new Error('Unauthorized');
42
+ const deleted = await CallDownListModel.destroy({ where: { id, userId: unAuthData.id } });
43
+ if (!deleted) throw new Error('Not found');
44
+ return { successful: true };
45
+ }
46
+
47
+ async function markCalled({ jwtToken, id, lastCallAt }) {
48
+ const unAuthData = jwt.decodeJwt(jwtToken);
49
+ if (!unAuthData?.id) throw new Error('Unauthorized');
50
+ const when = lastCallAt ? new Date(lastCallAt) : new Date();
51
+ const [affected] = await CallDownListModel.update({ status: 'called', lastCallAt: when }, { where: { id, userId: unAuthData.id } });
52
+ if (!affected) throw new Error('Not found');
53
+ return { successful: true };
54
+ }
55
+
56
+ exports.schedule = schedule;
57
+ exports.list = list;
58
+ exports.remove = remove;
59
+ exports.markCalled = markCalled;
60
+
package/index.js CHANGED
@@ -4,6 +4,8 @@ const bodyParser = require('body-parser');
4
4
  const dynamoose = require('dynamoose');
5
5
  const axios = require('axios');
6
6
  const { UserModel } = require('./models/userModel');
7
+ const { CallDownListModel } = require('./models/callDownListModel');
8
+ const { Op } = require('sequelize');
7
9
  const { CallLogModel } = require('./models/callLogModel');
8
10
  const { MessageLogModel } = require('./models/messageLogModel');
9
11
  const { AdminConfigModel } = require('./models/adminConfigModel');
@@ -20,6 +22,7 @@ const releaseNotes = require('./releaseNotes.json');
20
22
  const analytics = require('./lib/analytics');
21
23
  const util = require('./lib/util');
22
24
  const adapterRegistry = require('./adapter/registry');
25
+ const calldown = require('./handlers/calldown');
23
26
 
24
27
  let packageJson = null;
25
28
  try {
@@ -44,6 +47,7 @@ async function initDB() {
44
47
  await MessageLogModel.sync();
45
48
  await AdminConfigModel.sync();
46
49
  await CacheModel.sync();
50
+ await CallDownListModel.sync();
47
51
  }
48
52
  }
49
53
 
@@ -1000,7 +1004,7 @@ function createCoreRouter() {
1000
1004
  }
1001
1005
  const { id: userId, platform } = decodedToken;
1002
1006
  platformName = platform;
1003
- const { successful, logId, returnMessage, extraDataTracking } = await logCore.createCallLog({ platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.logInfo?.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL'});
1007
+ const { successful, logId, returnMessage, extraDataTracking } = await logCore.createCallLog({ platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.logInfo?.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
1004
1008
  if (extraDataTracking) {
1005
1009
  extraData = extraDataTracking;
1006
1010
  }
@@ -1199,6 +1203,174 @@ function createCoreRouter() {
1199
1203
  });
1200
1204
  });
1201
1205
 
1206
+ router.post('/calldown', async function (req, res) {
1207
+ const requestStartTime = new Date().getTime();
1208
+ let platformName = null;
1209
+ let success = false;
1210
+ let statusCode = 200;
1211
+ const { hashedExtensionId, hashedAccountId, userAgent, ip, author, eventAddedVia } = getAnalyticsVariablesInReqHeaders({ headers: req.headers })
1212
+ try {
1213
+ const jwtToken = req.query.jwtToken;
1214
+ if (!jwtToken) {
1215
+ res.status(400).send('Please go to Settings and authorize CRM platform');
1216
+ return;
1217
+ }
1218
+ const { id } = await calldown.schedule({ jwtToken, rcAccessToken: req.query.rcAccessToken, body: req.body });
1219
+ success = true;
1220
+ res.status(200).send({ successful: true, id });
1221
+ } catch (e) {
1222
+ console.log(`platform: ${platformName} \n${e.stack}`);
1223
+ statusCode = e.response?.status ?? 'unknown';
1224
+ res.status(400).send(e);
1225
+ success = false;
1226
+ }
1227
+ const requestEndTime = new Date().getTime();
1228
+ analytics.track({
1229
+ eventName: 'Schedule call down',
1230
+ interfaceName: 'scheduleCallDown',
1231
+ adapterName: platformName,
1232
+ accountId: hashedAccountId,
1233
+ extensionId: hashedExtensionId,
1234
+ success,
1235
+ requestDuration: (requestEndTime - requestStartTime) / 1000,
1236
+ userAgent,
1237
+ ip,
1238
+ author,
1239
+ extras: {
1240
+ statusCode
1241
+ },
1242
+ eventAddedVia
1243
+ });
1244
+ });
1245
+
1246
+
1247
+ router.get('/calldown', async function (req, res) {
1248
+ const requestStartTime = new Date().getTime();
1249
+ let platformName = null;
1250
+ let success = false;
1251
+ let statusCode = 200;
1252
+ const { hashedExtensionId, hashedAccountId, userAgent, ip, author, eventAddedVia } = getAnalyticsVariablesInReqHeaders({ headers: req.headers })
1253
+ try {
1254
+ const jwtToken = req.query.jwtToken;
1255
+ if (!jwtToken) {
1256
+ res.status(400).send('Please go to Settings and authorize CRM platform');
1257
+ return;
1258
+ }
1259
+ const { items } = await calldown.list({ jwtToken, status: req.query.status });
1260
+ success = true;
1261
+ res.status(200).send({ successful: true, items });
1262
+ } catch (e) {
1263
+ console.log(`platform: ${platformName} \n${e.stack}`);
1264
+ statusCode = e.response?.status ?? 'unknown';
1265
+ res.status(400).send(e);
1266
+ success = false;
1267
+ }
1268
+ const requestEndTime = new Date().getTime();
1269
+ analytics.track({
1270
+ eventName: 'Get call down list',
1271
+ interfaceName: 'getCallDownList',
1272
+ adapterName: platformName,
1273
+ accountId: hashedAccountId,
1274
+ extensionId: hashedExtensionId,
1275
+ success,
1276
+ requestDuration: (requestEndTime - requestStartTime) / 1000,
1277
+ userAgent,
1278
+ ip,
1279
+ author,
1280
+ extras: { statusCode },
1281
+ eventAddedVia
1282
+ });
1283
+ });
1284
+
1285
+
1286
+ router.delete('/calldown/:id', async function (req, res) {
1287
+ const requestStartTime = new Date().getTime();
1288
+ let platformName = null;
1289
+ let success = false;
1290
+ let statusCode = 200;
1291
+ const { hashedExtensionId, hashedAccountId, userAgent, ip, author, eventAddedVia } = getAnalyticsVariablesInReqHeaders({ headers: req.headers })
1292
+ try {
1293
+ const jwtToken = req.query.jwtToken;
1294
+ const id = req.query.id;
1295
+ if (!jwtToken) {
1296
+ res.status(400).send('Please go to Settings and authorize CRM platform');
1297
+ return;
1298
+ }
1299
+ const rid = req.params.id || id;
1300
+ if (!rid) {
1301
+ res.status(400).send('Missing id');
1302
+ return;
1303
+ }
1304
+ await calldown.remove({ jwtToken, id: rid });
1305
+ success = true;
1306
+ res.status(200).send({ successful: true });
1307
+ } catch (e) {
1308
+ console.log(`platform: ${platformName} \n${e.stack}`);
1309
+ statusCode = e.response?.status ?? 'unknown';
1310
+ res.status(400).send(e);
1311
+ success = false;
1312
+ }
1313
+ const requestEndTime = new Date().getTime();
1314
+ analytics.track({
1315
+ eventName: 'Delete call down item',
1316
+ interfaceName: 'deleteCallDownItem',
1317
+ adapterName: platformName,
1318
+ accountId: hashedAccountId,
1319
+ extensionId: hashedExtensionId,
1320
+ success,
1321
+ requestDuration: (requestEndTime - requestStartTime) / 1000,
1322
+ userAgent,
1323
+ ip,
1324
+ author,
1325
+ extras: { statusCode },
1326
+ eventAddedVia
1327
+ });
1328
+ });
1329
+
1330
+
1331
+ router.patch('/calldown/:id', async function (req, res) {
1332
+ const requestStartTime = new Date().getTime();
1333
+ let platformName = null;
1334
+ let success = false;
1335
+ let statusCode = 200;
1336
+ const { hashedExtensionId, hashedAccountId, userAgent, ip, author, eventAddedVia } = getAnalyticsVariablesInReqHeaders({ headers: req.headers })
1337
+ try {
1338
+ const jwtToken = req.query.jwtToken;
1339
+ if (!jwtToken) {
1340
+ res.status(400).send('Please go to Settings and authorize CRM platform');
1341
+ return;
1342
+ }
1343
+ const id = req.params.id || req.body?.id;
1344
+ if (!id) {
1345
+ res.status(400).send('Missing id');
1346
+ return;
1347
+ }
1348
+ await calldown.markCalled({ jwtToken, id, lastCallAt: req.body?.lastCallAt });
1349
+ success = true;
1350
+ res.status(200).send({ successful: true });
1351
+ } catch (e) {
1352
+ console.log(`platform: ${platformName} \n${e.stack}`);
1353
+ statusCode = e.response?.status ?? 'unknown';
1354
+ res.status(400).send(e);
1355
+ success = false;
1356
+ }
1357
+ const requestEndTime = new Date().getTime();
1358
+ analytics.track({
1359
+ eventName: 'Mark call down called',
1360
+ interfaceName: 'markCallDownCalled',
1361
+ adapterName: platformName,
1362
+ accountId: hashedAccountId,
1363
+ extensionId: hashedExtensionId,
1364
+ success,
1365
+ requestDuration: (requestEndTime - requestStartTime) / 1000,
1366
+ userAgent,
1367
+ ip,
1368
+ author,
1369
+ extras: { statusCode },
1370
+ eventAddedVia
1371
+ });
1372
+ });
1373
+
1202
1374
  router.get('/custom/contact/search', async function (req, res) {
1203
1375
  const requestStartTime = new Date().getTime();
1204
1376
  let platformName = null;
@@ -1243,8 +1415,108 @@ function createCoreRouter() {
1243
1415
  statusCode
1244
1416
  }
1245
1417
  });
1418
+ });
1419
+
1420
+ router.get('/ringcentral/admin/report', async function (req, res) {
1421
+ const requestStartTime = new Date().getTime();
1422
+ let platformName = null;
1423
+ let success = false;
1424
+ const { hashedExtensionId, hashedAccountId, userAgent, ip, author, eventAddedVia } = getAnalyticsVariablesInReqHeaders({ headers: req.headers })
1425
+ const jwtToken = req.query.jwtToken;
1426
+ try {
1427
+ if (jwtToken) {
1428
+ const unAuthData = jwt.decodeJwt(jwtToken);
1429
+ const user = await UserModel.findByPk(unAuthData?.id);
1430
+ if (!user) {
1431
+ res.status(400).send('User not found');
1432
+ return;
1433
+ }
1434
+ const report = await adminCore.getAdminReport({ rcAccountId: user.rcAccountId, timezone: req.query.timezone, timeFrom: req.query.timeFrom, timeTo: req.query.timeTo });
1435
+ res.status(200).send(report);
1436
+ success = true;
1437
+ return;
1438
+ }
1439
+ res.status(400).send('Invalid request');
1440
+ success = false;
1441
+ }
1442
+ catch (e) {
1443
+ console.log(`${e.stack}`);
1444
+ res.status(400).send(e);
1445
+ }
1446
+ const requestEndTime = new Date().getTime();
1447
+ analytics.track({
1448
+ eventName: 'Get admin report',
1449
+ interfaceName: 'getAdminReport',
1450
+ adapterName: platformName,
1451
+ accountId: hashedAccountId,
1452
+ extensionId: hashedExtensionId,
1453
+ success,
1454
+ requestDuration: (requestEndTime - requestStartTime) / 1000,
1455
+ userAgent,
1456
+ ip,
1457
+ author,
1458
+ eventAddedVia
1459
+ });
1460
+ });
1461
+
1462
+ router.get('/ringcentral/admin/userReport', async function (req, res) {
1463
+ const requestStartTime = new Date().getTime();
1464
+ let platformName = null;
1465
+ let success = false;
1466
+ const { hashedExtensionId, hashedAccountId, userAgent, ip, author, eventAddedVia } = getAnalyticsVariablesInReqHeaders({ headers: req.headers })
1467
+ const jwtToken = req.query.jwtToken;
1468
+ try {
1469
+ if (jwtToken) {
1470
+ const unAuthData = jwt.decodeJwt(jwtToken);
1471
+ const user = await UserModel.findByPk(unAuthData?.id);
1472
+ if (!user) {
1473
+ res.status(400).send('User not found');
1474
+ return;
1475
+ }
1476
+ const report = await adminCore.getUserReport({ rcAccountId: user.rcAccountId, rcExtensionId: req.query.rcExtensionId, timezone: req.query.timezone, timeFrom: req.query.timeFrom, timeTo: req.query.timeTo });
1477
+ res.status(200).send(report);
1478
+ return;
1479
+ }
1480
+ res.status(400).send('Invalid request');
1481
+ success = false;
1482
+ }
1483
+ catch (e) {
1484
+ console.log(`${e.stack}`);
1485
+ res.status(400).send(e);
1486
+ }
1487
+ const requestEndTime = new Date().getTime();
1488
+ analytics.track({
1489
+ eventName: 'Get user report',
1490
+ interfaceName: 'getUserReport',
1491
+ adapterName: platformName,
1492
+ accountId: hashedAccountId,
1493
+ extensionId: hashedExtensionId,
1494
+ success,
1495
+ requestDuration: (requestEndTime - requestStartTime) / 1000,
1496
+ userAgent,
1497
+ ip,
1498
+ author,
1499
+ eventAddedVia
1500
+ });
1501
+ });
1246
1502
 
1503
+ router.get('/ringcentral/oauth/callback', async function (req, res) {
1504
+ const jwtToken = req.query.jwtToken;
1505
+ if (jwtToken) {
1506
+ const unAuthData = jwt.decodeJwt(jwtToken);
1507
+ const { code } = req.query;
1508
+ const user = await UserModel.findByPk(unAuthData?.id);
1509
+ if (!user) {
1510
+ res.status(400).send('User not found');
1511
+ return;
1512
+ }
1513
+ await authCore.onRingcentralOAuthCallback({ code, rcAccountId: user.rcAccountId });
1514
+ res.status(200).send('OK');
1515
+ return;
1516
+ }
1517
+ res.status(400).send('Invalid request');
1247
1518
  });
1519
+
1248
1520
  if (process.env.IS_PROD === 'false') {
1249
1521
  router.post('/registerMockUser', async function (req, res) {
1250
1522
  const secretKey = req.query.secretKey;
@@ -1306,7 +1578,7 @@ function createCoreMiddleware() {
1306
1578
  return [
1307
1579
  bodyParser.json(),
1308
1580
  cors({
1309
- methods: ['GET', 'POST', 'PATCH', 'PUT']
1581
+ methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE']
1310
1582
  })
1311
1583
  ];
1312
1584
  }
@@ -0,0 +1,275 @@
1
+
2
+ const fetch = require('node-fetch');
3
+
4
+ const DEFAULT_RENEW_HANDICAP_MS = 60 * 1000; // 1 minute
5
+
6
+ function stringifyQuery(query) {
7
+ const queryParams = new URLSearchParams(query);
8
+ return queryParams.toString();
9
+ }
10
+
11
+ const REFRESH_RENEW_HANDICAP_MS = 10 * 1000; // 10s
12
+ function isRefreshTokenValid(token, handicap = REFRESH_RENEW_HANDICAP_MS) {
13
+ const expireTime = token.refresh_token_expire_time;
14
+ return expireTime - handicap > Date.now();
15
+ }
16
+
17
+ function isAccessTokenValid(token, handicap = DEFAULT_RENEW_HANDICAP_MS) {
18
+ const expireTime = token.expire_time;
19
+ return expireTime - handicap > Date.now();
20
+ }
21
+
22
+ class RingCentral {
23
+ constructor(options) {
24
+ this._options = options;
25
+ }
26
+
27
+ loginUrl({
28
+ state,
29
+ }) {
30
+ const query = {
31
+ response_type: 'code',
32
+ redirect_uri: this._options.redirectUri,
33
+ client_id: this._options.clientId,
34
+ response_hint: 'brand_id contracted_country_code',
35
+ };
36
+ if (state) {
37
+ query.state = state;
38
+ }
39
+ return `${this._options.server}/restapi/oauth/authorize?${stringifyQuery(query)}`;
40
+ }
41
+
42
+ async generateToken({ code }) {
43
+ const body = {
44
+ code,
45
+ grant_type: 'authorization_code',
46
+ redirect_uri: this._options.redirectUri,
47
+ };
48
+ const response = await this._tokenRequest('/restapi/oauth/token', body);
49
+ if (Number.parseInt(response.status, 10) >= 400) {
50
+ throw new Error('Generate Token error', response.status);
51
+ }
52
+ const {
53
+ expires_in,
54
+ refresh_token_expires_in,
55
+ scope,
56
+ endpoint_id, // do no save this field into db to reduce db size
57
+ ...token
58
+ } = await response.json();
59
+ return {
60
+ ...token,
61
+ expire_time: Date.now() + parseInt(expires_in, 10) * 1000,
62
+ refresh_token_expire_time: Date.now() + parseInt(refresh_token_expires_in, 10) * 1000,
63
+ };
64
+ }
65
+
66
+ async refreshToken(token) {
67
+ const body = {
68
+ grant_type: 'refresh_token',
69
+ refresh_token: token.refresh_token,
70
+ access_token_ttl: token.expires_in,
71
+ refresh_token_ttl: token.refresh_token_expires_in,
72
+ };
73
+ const response = await this._tokenRequest('/restapi/oauth/token', body);
74
+ if (Number.parseInt(response.status, 10) >= 400) {
75
+ const error = new Error('Refresh Token error', response.status);
76
+ error.response = response;
77
+ throw error;
78
+ }
79
+ const {
80
+ expires_in,
81
+ refresh_token_expires_in,
82
+ scope,
83
+ endpoint_id, // do no save this field into db to reduce db size
84
+ ...newToken
85
+ } = await response.json();
86
+ return {
87
+ ...newToken,
88
+ expire_time: Date.now() + parseInt(expires_in, 10) * 1000,
89
+ refresh_token_expire_time: Date.now() + parseInt(refresh_token_expires_in, 10) * 1000,
90
+ }
91
+ }
92
+
93
+ async revokeToken(token) {
94
+ const body = {
95
+ token: token.access_token,
96
+ };
97
+ const response = await this._tokenRequest('/restapi/oauth/revoke', body);
98
+ if (Number.parseInt(response.status, 10) >= 400) {
99
+ throw new Error('Revoke Token error', response.status);
100
+ }
101
+ }
102
+
103
+ async _tokenRequest(path, body) {
104
+ const authorization = `${this._options.clientId}:${this._options.clientSecret}`;
105
+ const response = await fetch(
106
+ `${this._options.server}${path}`, {
107
+ method: 'POST',
108
+ body: stringifyQuery(body),
109
+ headers: {
110
+ 'Accept': 'application/json',
111
+ 'Content-Type': 'application/x-www-form-urlencoded',
112
+ 'Authorization': `Basic ${Buffer.from(authorization).toString('base64')}`
113
+ },
114
+ }
115
+ );
116
+ return response;
117
+ }
118
+
119
+ async request({
120
+ server = this._options.server,
121
+ path,
122
+ query,
123
+ body,
124
+ method,
125
+ accept = 'application/json',
126
+ }, token) {
127
+ let uri = `${server}${path}`;
128
+ if (query) {
129
+ uri = uri + (uri.includes('?') ? '&' : '?') + stringifyQuery(query);
130
+ }
131
+ const response = await fetch(uri, {
132
+ method,
133
+ body: body ? JSON.stringify(body) : body,
134
+ headers: {
135
+ 'Accept': accept,
136
+ 'Content-Type': 'application/json',
137
+ 'Authorization': `${token.token_type} ${token.access_token}`,
138
+ },
139
+ });
140
+ if (Number.parseInt(response.status, 10) >= 400) {
141
+ const error = new Error(`request data error ${response.status}`);
142
+ const errorText = await response.text();
143
+ error.message = errorText;
144
+ error.response = response;
145
+ throw error;
146
+ }
147
+ return response;
148
+ }
149
+
150
+ async createSubscription({
151
+ eventFilters,
152
+ webhookUri,
153
+ }, token) {
154
+ const response = await this.request({
155
+ method: 'POST',
156
+ path: '/restapi/v1.0/subscription',
157
+ body: {
158
+ eventFilters,
159
+ deliveryMode: {
160
+ transportType: 'WebHook',
161
+ address: webhookUri,
162
+ },
163
+ expiresIn: 7 * 24 * 3600, // 7 days
164
+ },
165
+ }, token);
166
+ const {
167
+ uri,
168
+ creationTime,
169
+ deliveryMode,
170
+ status, // do no save those field into db to reduce db size
171
+ ...subscription
172
+ } = await response.json();
173
+ return subscription;
174
+ }
175
+
176
+ async getExtensionInfo(extensionId, token) {
177
+ const response = await this.request({
178
+ method: 'GET',
179
+ path: `/restapi/v1.0/account/~/extension/${extensionId}`,
180
+ }, token);
181
+ return response.json();
182
+ }
183
+
184
+ async getAccountInfo(token) {
185
+ const response = await this.request({
186
+ method: 'GET',
187
+ path: `/restapi/v1.0/account/~`,
188
+ }, token);
189
+ return response.json();
190
+ }
191
+
192
+ async getCallsAggregationData({ token, timezone, timeFrom, timeTo }) {
193
+ const body = {
194
+ grouping: {
195
+ groupBy: "Company"
196
+ },
197
+ timeSettings: {
198
+ timeZone: timezone,
199
+ timeRange: {
200
+ timeFrom: timeFrom,
201
+ timeTo: timeTo
202
+ }
203
+ },
204
+ responseOptions: {
205
+ counters: {
206
+ callsByDirection: {
207
+ aggregationType: "Sum"
208
+ },
209
+ callsByResponse: {
210
+ aggregationType: "Sum"
211
+ }
212
+ },
213
+ timers: {
214
+ allCallsDuration: {
215
+ aggregationType: "Sum"
216
+ }
217
+ }
218
+ }
219
+ }
220
+ const response = await this.request({
221
+ method: 'POST',
222
+ path: `/analytics/calls/v1/accounts/~/aggregation/fetch`,
223
+ body,
224
+ accept: 'application/json'
225
+ }, token);
226
+ return response.json();
227
+ }
228
+
229
+ async getCallLogData({ extensionId = '~', token, timezone, timeFrom, timeTo }) {
230
+ let pageStart = 1;
231
+ let isFinalPage = false;
232
+ let callLogResponse = null;
233
+ let result = { records: [] };
234
+ while (!isFinalPage) {
235
+ callLogResponse = await this.request({
236
+ method: 'GET',
237
+ path: `/restapi/v1.0/account/~/extension/${extensionId}/call-log?dateFrom=${timeFrom}&dateTo=${timeTo}&page=${pageStart}&view=Simple&perPage=1000`,
238
+ }, token);
239
+ const resultJson = await callLogResponse.json();
240
+ result.records.push(...resultJson.records);
241
+ if (resultJson.navigation?.nextPage) {
242
+ pageStart++;
243
+ }
244
+ else {
245
+ isFinalPage = true;
246
+ }
247
+ }
248
+ return result;
249
+ }
250
+ async getSMSData({ extensionId = '~', token, timezone, timeFrom, timeTo }) {
251
+ let pageStart = 1;
252
+ let isFinalPage = false;
253
+ let smsLogResponse = null;
254
+ let result = { records: [] };
255
+ while (!isFinalPage) {
256
+ smsLogResponse = await this.request({
257
+ method: 'GET',
258
+ path: `/restapi/v1.0/account/~/extension/${extensionId}/message-store?dateFrom=${timeFrom}&dateTo=${timeTo}&page=${pageStart}&perPage=100`,
259
+ }, token);
260
+ const resultJson = await smsLogResponse.json();
261
+ result.records.push(...resultJson.records);
262
+ if (resultJson.navigation?.nextPage) {
263
+ pageStart++;
264
+ }
265
+ else {
266
+ isFinalPage = true;
267
+ }
268
+ }
269
+ return result;
270
+ }
271
+ }
272
+
273
+ exports.RingCentral = RingCentral;
274
+ exports.isRefreshTokenValid = isRefreshTokenValid;
275
+ exports.isAccessTokenValid = isAccessTokenValid;
package/lib/util.js CHANGED
@@ -37,6 +37,20 @@ function secondsToHoursMinutesSeconds(seconds) {
37
37
  return resultString;
38
38
  }
39
39
 
40
+ function getMostRecentDate({ allDateValues }) {
41
+ var result = 0;
42
+ for (const date of allDateValues) {
43
+ if(!date)
44
+ {
45
+ continue;
46
+ }
47
+ if (date > result) {
48
+ result = date;
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+
40
54
  // media reader link: https://ringcentral.github.io/ringcentral-media-reader/?media=https://media.ringcentral.com/restapi/v1.0/account/{accountId}/extension/{extensionId}/message-store/{messageId}/content/{contentId}
41
55
  // platform media link: https://media.ringcentral.com/restapi/v1.0/account/{accountId}/extension/{extensionId}/message-store/{messageId}/content/{contentId}
42
56
  function getMediaReaderLinkByPlatformMediaLink(platformMediaLink){
@@ -50,4 +64,5 @@ function getMediaReaderLinkByPlatformMediaLink(platformMediaLink){
50
64
  exports.getTimeZone = getTimeZone;
51
65
  exports.getHashValue = getHashValue;
52
66
  exports.secondsToHoursMinutesSeconds = secondsToHoursMinutesSeconds;
67
+ exports.getMostRecentDate = getMostRecentDate;
53
68
  exports.getMediaReaderLinkByPlatformMediaLink = getMediaReaderLinkByPlatformMediaLink;
@@ -1,7 +1,7 @@
1
1
  const Sequelize = require('sequelize');
2
2
  const { sequelize } = require('./sequelize');
3
3
 
4
- // Model for User data
4
+ // Model for Admin data
5
5
  exports.AdminConfigModel = sequelize.define('adminConfigs', {
6
6
  // hashed rc account ID
7
7
  id: {
@@ -14,7 +14,16 @@ exports.AdminConfigModel = sequelize.define('adminConfigs', {
14
14
  customAdapter: {
15
15
  type: Sequelize.JSON
16
16
  },
17
- // Array of:
17
+ adminAccessToken: {
18
+ type: Sequelize.STRING(512),
19
+ },
20
+ adminRefreshToken: {
21
+ type: Sequelize.STRING(512),
22
+ },
23
+ adminTokenExpiry: {
24
+ type: Sequelize.DATE
25
+ },
26
+ // Array of:
18
27
  // {
19
28
  // crmUserId: string,
20
29
  // rcExtensionId: array of strings
@@ -0,0 +1,35 @@
1
+ const Sequelize = require('sequelize');
2
+ const { sequelize } = require('./sequelize');
3
+
4
+ exports.CallDownListModel = sequelize.define('callDownLists', {
5
+ id: {
6
+ type: Sequelize.STRING,
7
+ primaryKey: true,
8
+ },
9
+ userId: {
10
+ type: Sequelize.STRING,
11
+ },
12
+ contactId: {
13
+ type: Sequelize.STRING,
14
+ },
15
+ contactType: {
16
+ type: Sequelize.STRING,
17
+ },
18
+ status: {
19
+ type: Sequelize.STRING,
20
+ },
21
+ scheduledAt: {
22
+ type: Sequelize.DATE,
23
+ },
24
+ lastCallAt: {
25
+ type: Sequelize.DATE,
26
+ }
27
+ }, {
28
+ timestamps: true,
29
+ indexes: [
30
+ { fields: ['userId'] },
31
+ { fields: ['status'] },
32
+ { fields: ['scheduledAt'] },
33
+ { fields: ['userId', 'status'] }
34
+ ]
35
+ });
@@ -3,6 +3,7 @@ const { sequelize } = require('./sequelize');
3
3
 
4
4
  // Model for User data
5
5
  exports.UserModel = sequelize.define('users', {
6
+ // id = {crmName}-{crmUserId}
6
7
  id: {
7
8
  type: Sequelize.STRING,
8
9
  primaryKey: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-connect/core",
3
- "version": "1.6.9",
3
+ "version": "1.7.0-beta.2",
4
4
  "description": "RingCentral App Connect Core",
5
5
  "main": "index.js",
6
6
  "repository": {
package/releaseNotes.json CHANGED
@@ -1,28 +1,4 @@
1
1
  {
2
- "1.6.9": {
3
- "global": [
4
- {
5
- "type": "New",
6
- "description": "- Server-side logging now supports multiple RingCentral users to be mapped under one CRM user"
7
- },
8
- {
9
- "type": "Fix",
10
- "description": "- Server-side logging user mapping not working properly when running under multi-site scenarios"
11
- }
12
- ]
13
- },
14
- "1.6.8": {
15
- "global": [
16
- {
17
- "type": "Fix",
18
- "description": "- Server-side logging, if it's created by one admin, the other admin will be able to see its status"
19
- },
20
- {
21
- "type": "Fix",
22
- "description": "- Conference calls show wrong warning message"
23
- }
24
- ]
25
- },
26
2
  "1.6.7": {
27
3
  "global": [
28
4
  {