@app-connect/core 1.7.24 → 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 -42
- package/connector/mock.js +84 -77
- package/connector/proxy/engine.js +164 -163
- 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 -117
- 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 +72 -72
- package/releaseNotes.json +1093 -1073
- package/test/connector/proxy/engine.test.js +126 -93
- 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 -1015
- package/test/handlers/contact.test.js +1014 -1014
- package/test/handlers/log.test.js +1298 -1160
- package/test/handlers/managedAuth.test.js +458 -458
- 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 -82
- 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 -129
- package/test/setup.js +178 -178
package/lib/ringcentral.js
CHANGED
|
@@ -1,267 +1,267 @@
|
|
|
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
|
-
...token
|
|
56
|
-
} = await response.json();
|
|
57
|
-
return {
|
|
58
|
-
...token,
|
|
59
|
-
expire_time: Date.now() + parseInt(expires_in, 10) * 1000,
|
|
60
|
-
refresh_token_expire_time: Date.now() + parseInt(refresh_token_expires_in, 10) * 1000,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async refreshToken(token) {
|
|
65
|
-
const body = {
|
|
66
|
-
grant_type: 'refresh_token',
|
|
67
|
-
refresh_token: token.refresh_token,
|
|
68
|
-
access_token_ttl: token.expires_in,
|
|
69
|
-
refresh_token_ttl: token.refresh_token_expires_in,
|
|
70
|
-
};
|
|
71
|
-
const response = await this._tokenRequest('/restapi/oauth/token', body);
|
|
72
|
-
if (Number.parseInt(response.status, 10) >= 400) {
|
|
73
|
-
const error = new Error('Refresh Token error', response.status);
|
|
74
|
-
error.response = response;
|
|
75
|
-
throw error;
|
|
76
|
-
}
|
|
77
|
-
const {
|
|
78
|
-
expires_in,
|
|
79
|
-
refresh_token_expires_in,
|
|
80
|
-
...newToken
|
|
81
|
-
} = await response.json();
|
|
82
|
-
return {
|
|
83
|
-
...newToken,
|
|
84
|
-
expire_time: Date.now() + parseInt(expires_in, 10) * 1000,
|
|
85
|
-
refresh_token_expire_time: Date.now() + parseInt(refresh_token_expires_in, 10) * 1000,
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async revokeToken(token) {
|
|
90
|
-
const body = {
|
|
91
|
-
token: token.access_token,
|
|
92
|
-
};
|
|
93
|
-
const response = await this._tokenRequest('/restapi/oauth/revoke', body);
|
|
94
|
-
if (Number.parseInt(response.status, 10) >= 400) {
|
|
95
|
-
throw new Error('Revoke Token error', response.status);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async _tokenRequest(path, body) {
|
|
100
|
-
const authorization = `${this._options.clientId}:${this._options.clientSecret}`;
|
|
101
|
-
const response = await fetch(
|
|
102
|
-
`${this._options.server}${path}`, {
|
|
103
|
-
method: 'POST',
|
|
104
|
-
body: stringifyQuery(body),
|
|
105
|
-
headers: {
|
|
106
|
-
'Accept': 'application/json',
|
|
107
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
108
|
-
'Authorization': `Basic ${Buffer.from(authorization).toString('base64')}`
|
|
109
|
-
},
|
|
110
|
-
}
|
|
111
|
-
);
|
|
112
|
-
return response;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async request({
|
|
116
|
-
server = this._options.server,
|
|
117
|
-
path,
|
|
118
|
-
query,
|
|
119
|
-
body,
|
|
120
|
-
method,
|
|
121
|
-
accept = 'application/json',
|
|
122
|
-
}, token) {
|
|
123
|
-
let uri = `${server}${path}`;
|
|
124
|
-
if (query) {
|
|
125
|
-
uri = uri + (uri.includes('?') ? '&' : '?') + stringifyQuery(query);
|
|
126
|
-
}
|
|
127
|
-
const response = await fetch(uri, {
|
|
128
|
-
method,
|
|
129
|
-
body: body ? JSON.stringify(body) : body,
|
|
130
|
-
headers: {
|
|
131
|
-
'Accept': accept,
|
|
132
|
-
'Content-Type': 'application/json',
|
|
133
|
-
'Authorization': `${token.token_type} ${token.access_token}`,
|
|
134
|
-
},
|
|
135
|
-
});
|
|
136
|
-
if (Number.parseInt(response.status, 10) >= 400) {
|
|
137
|
-
const error = new Error(`request data error ${response.status}`);
|
|
138
|
-
const errorText = await response.text();
|
|
139
|
-
error.message = errorText;
|
|
140
|
-
error.response = response;
|
|
141
|
-
throw error;
|
|
142
|
-
}
|
|
143
|
-
return response;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async createSubscription({
|
|
147
|
-
eventFilters,
|
|
148
|
-
webhookUri,
|
|
149
|
-
}, token) {
|
|
150
|
-
const response = await this.request({
|
|
151
|
-
method: 'POST',
|
|
152
|
-
path: '/restapi/v1.0/subscription',
|
|
153
|
-
body: {
|
|
154
|
-
eventFilters,
|
|
155
|
-
deliveryMode: {
|
|
156
|
-
transportType: 'WebHook',
|
|
157
|
-
address: webhookUri,
|
|
158
|
-
},
|
|
159
|
-
expiresIn: 7 * 24 * 3600, // 7 days
|
|
160
|
-
},
|
|
161
|
-
}, token);
|
|
162
|
-
const {
|
|
163
|
-
...subscription
|
|
164
|
-
} = await response.json();
|
|
165
|
-
return subscription;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async getExtensionInfo(extensionId, token) {
|
|
169
|
-
const response = await this.request({
|
|
170
|
-
method: 'GET',
|
|
171
|
-
path: `/restapi/v1.0/account/~/extension/${extensionId}`,
|
|
172
|
-
}, token);
|
|
173
|
-
return response.json();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async getAccountInfo(token) {
|
|
177
|
-
const response = await this.request({
|
|
178
|
-
method: 'GET',
|
|
179
|
-
path: `/restapi/v1.0/account/~`,
|
|
180
|
-
}, token);
|
|
181
|
-
return response.json();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async getCallsAggregationData({ token, timezone, timeFrom, timeTo, groupBy }) {
|
|
185
|
-
const body = {
|
|
186
|
-
grouping: {
|
|
187
|
-
groupBy
|
|
188
|
-
},
|
|
189
|
-
timeSettings: {
|
|
190
|
-
timeZone: timezone,
|
|
191
|
-
timeRange: {
|
|
192
|
-
timeFrom: timeFrom,
|
|
193
|
-
timeTo: timeTo
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
responseOptions: {
|
|
197
|
-
counters: {
|
|
198
|
-
callsByDirection: {
|
|
199
|
-
aggregationType: "Sum"
|
|
200
|
-
},
|
|
201
|
-
callsByResponse: {
|
|
202
|
-
aggregationType: "Sum"
|
|
203
|
-
}
|
|
204
|
-
},
|
|
205
|
-
timers: {
|
|
206
|
-
allCallsDuration: {
|
|
207
|
-
aggregationType: "Sum"
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const response = await this.request({
|
|
213
|
-
method: 'POST',
|
|
214
|
-
path: `/analytics/calls/v1/accounts/~/aggregation/fetch`,
|
|
215
|
-
body,
|
|
216
|
-
accept: 'application/json'
|
|
217
|
-
}, token);
|
|
218
|
-
return response.json();
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async getCallLogData({ extensionId = '~', token, timeFrom, timeTo }) {
|
|
222
|
-
let pageStart = 1;
|
|
223
|
-
let isFinalPage = false;
|
|
224
|
-
let callLogResponse = null;
|
|
225
|
-
let result = { records: [] };
|
|
226
|
-
while (!isFinalPage) {
|
|
227
|
-
callLogResponse = await this.request({
|
|
228
|
-
method: 'GET',
|
|
229
|
-
path: `/restapi/v1.0/account/~/extension/${extensionId}/call-log?dateFrom=${timeFrom}&dateTo=${timeTo}&page=${pageStart}&view=Simple&perPage=1000`,
|
|
230
|
-
}, token);
|
|
231
|
-
const resultJson = await callLogResponse.json();
|
|
232
|
-
result.records.push(...resultJson.records);
|
|
233
|
-
if (resultJson.navigation?.nextPage) {
|
|
234
|
-
pageStart++;
|
|
235
|
-
}
|
|
236
|
-
else {
|
|
237
|
-
isFinalPage = true;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return result;
|
|
241
|
-
}
|
|
242
|
-
async getSMSData({ extensionId = '~', token, timeFrom, timeTo }) {
|
|
243
|
-
let pageStart = 1;
|
|
244
|
-
let isFinalPage = false;
|
|
245
|
-
let smsLogResponse = null;
|
|
246
|
-
let result = { records: [] };
|
|
247
|
-
while (!isFinalPage) {
|
|
248
|
-
smsLogResponse = await this.request({
|
|
249
|
-
method: 'GET',
|
|
250
|
-
path: `/restapi/v1.0/account/~/extension/${extensionId}/message-store?dateFrom=${timeFrom}&dateTo=${timeTo}&page=${pageStart}&perPage=100`,
|
|
251
|
-
}, token);
|
|
252
|
-
const resultJson = await smsLogResponse.json();
|
|
253
|
-
result.records.push(...resultJson.records);
|
|
254
|
-
if (resultJson.navigation?.nextPage) {
|
|
255
|
-
pageStart++;
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
isFinalPage = true;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return result;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
exports.RingCentral = RingCentral;
|
|
266
|
-
exports.isRefreshTokenValid = isRefreshTokenValid;
|
|
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
|
+
...token
|
|
56
|
+
} = await response.json();
|
|
57
|
+
return {
|
|
58
|
+
...token,
|
|
59
|
+
expire_time: Date.now() + parseInt(expires_in, 10) * 1000,
|
|
60
|
+
refresh_token_expire_time: Date.now() + parseInt(refresh_token_expires_in, 10) * 1000,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async refreshToken(token) {
|
|
65
|
+
const body = {
|
|
66
|
+
grant_type: 'refresh_token',
|
|
67
|
+
refresh_token: token.refresh_token,
|
|
68
|
+
access_token_ttl: token.expires_in,
|
|
69
|
+
refresh_token_ttl: token.refresh_token_expires_in,
|
|
70
|
+
};
|
|
71
|
+
const response = await this._tokenRequest('/restapi/oauth/token', body);
|
|
72
|
+
if (Number.parseInt(response.status, 10) >= 400) {
|
|
73
|
+
const error = new Error('Refresh Token error', response.status);
|
|
74
|
+
error.response = response;
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
const {
|
|
78
|
+
expires_in,
|
|
79
|
+
refresh_token_expires_in,
|
|
80
|
+
...newToken
|
|
81
|
+
} = await response.json();
|
|
82
|
+
return {
|
|
83
|
+
...newToken,
|
|
84
|
+
expire_time: Date.now() + parseInt(expires_in, 10) * 1000,
|
|
85
|
+
refresh_token_expire_time: Date.now() + parseInt(refresh_token_expires_in, 10) * 1000,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async revokeToken(token) {
|
|
90
|
+
const body = {
|
|
91
|
+
token: token.access_token,
|
|
92
|
+
};
|
|
93
|
+
const response = await this._tokenRequest('/restapi/oauth/revoke', body);
|
|
94
|
+
if (Number.parseInt(response.status, 10) >= 400) {
|
|
95
|
+
throw new Error('Revoke Token error', response.status);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async _tokenRequest(path, body) {
|
|
100
|
+
const authorization = `${this._options.clientId}:${this._options.clientSecret}`;
|
|
101
|
+
const response = await fetch(
|
|
102
|
+
`${this._options.server}${path}`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
body: stringifyQuery(body),
|
|
105
|
+
headers: {
|
|
106
|
+
'Accept': 'application/json',
|
|
107
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
108
|
+
'Authorization': `Basic ${Buffer.from(authorization).toString('base64')}`
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
return response;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async request({
|
|
116
|
+
server = this._options.server,
|
|
117
|
+
path,
|
|
118
|
+
query,
|
|
119
|
+
body,
|
|
120
|
+
method,
|
|
121
|
+
accept = 'application/json',
|
|
122
|
+
}, token) {
|
|
123
|
+
let uri = `${server}${path}`;
|
|
124
|
+
if (query) {
|
|
125
|
+
uri = uri + (uri.includes('?') ? '&' : '?') + stringifyQuery(query);
|
|
126
|
+
}
|
|
127
|
+
const response = await fetch(uri, {
|
|
128
|
+
method,
|
|
129
|
+
body: body ? JSON.stringify(body) : body,
|
|
130
|
+
headers: {
|
|
131
|
+
'Accept': accept,
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
'Authorization': `${token.token_type} ${token.access_token}`,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
if (Number.parseInt(response.status, 10) >= 400) {
|
|
137
|
+
const error = new Error(`request data error ${response.status}`);
|
|
138
|
+
const errorText = await response.text();
|
|
139
|
+
error.message = errorText;
|
|
140
|
+
error.response = response;
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
return response;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async createSubscription({
|
|
147
|
+
eventFilters,
|
|
148
|
+
webhookUri,
|
|
149
|
+
}, token) {
|
|
150
|
+
const response = await this.request({
|
|
151
|
+
method: 'POST',
|
|
152
|
+
path: '/restapi/v1.0/subscription',
|
|
153
|
+
body: {
|
|
154
|
+
eventFilters,
|
|
155
|
+
deliveryMode: {
|
|
156
|
+
transportType: 'WebHook',
|
|
157
|
+
address: webhookUri,
|
|
158
|
+
},
|
|
159
|
+
expiresIn: 7 * 24 * 3600, // 7 days
|
|
160
|
+
},
|
|
161
|
+
}, token);
|
|
162
|
+
const {
|
|
163
|
+
...subscription
|
|
164
|
+
} = await response.json();
|
|
165
|
+
return subscription;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async getExtensionInfo(extensionId, token) {
|
|
169
|
+
const response = await this.request({
|
|
170
|
+
method: 'GET',
|
|
171
|
+
path: `/restapi/v1.0/account/~/extension/${extensionId}`,
|
|
172
|
+
}, token);
|
|
173
|
+
return response.json();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async getAccountInfo(token) {
|
|
177
|
+
const response = await this.request({
|
|
178
|
+
method: 'GET',
|
|
179
|
+
path: `/restapi/v1.0/account/~`,
|
|
180
|
+
}, token);
|
|
181
|
+
return response.json();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async getCallsAggregationData({ token, timezone, timeFrom, timeTo, groupBy }) {
|
|
185
|
+
const body = {
|
|
186
|
+
grouping: {
|
|
187
|
+
groupBy
|
|
188
|
+
},
|
|
189
|
+
timeSettings: {
|
|
190
|
+
timeZone: timezone,
|
|
191
|
+
timeRange: {
|
|
192
|
+
timeFrom: timeFrom,
|
|
193
|
+
timeTo: timeTo
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
responseOptions: {
|
|
197
|
+
counters: {
|
|
198
|
+
callsByDirection: {
|
|
199
|
+
aggregationType: "Sum"
|
|
200
|
+
},
|
|
201
|
+
callsByResponse: {
|
|
202
|
+
aggregationType: "Sum"
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
timers: {
|
|
206
|
+
allCallsDuration: {
|
|
207
|
+
aggregationType: "Sum"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const response = await this.request({
|
|
213
|
+
method: 'POST',
|
|
214
|
+
path: `/analytics/calls/v1/accounts/~/aggregation/fetch`,
|
|
215
|
+
body,
|
|
216
|
+
accept: 'application/json'
|
|
217
|
+
}, token);
|
|
218
|
+
return response.json();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async getCallLogData({ extensionId = '~', token, timeFrom, timeTo }) {
|
|
222
|
+
let pageStart = 1;
|
|
223
|
+
let isFinalPage = false;
|
|
224
|
+
let callLogResponse = null;
|
|
225
|
+
let result = { records: [] };
|
|
226
|
+
while (!isFinalPage) {
|
|
227
|
+
callLogResponse = await this.request({
|
|
228
|
+
method: 'GET',
|
|
229
|
+
path: `/restapi/v1.0/account/~/extension/${extensionId}/call-log?dateFrom=${timeFrom}&dateTo=${timeTo}&page=${pageStart}&view=Simple&perPage=1000`,
|
|
230
|
+
}, token);
|
|
231
|
+
const resultJson = await callLogResponse.json();
|
|
232
|
+
result.records.push(...resultJson.records);
|
|
233
|
+
if (resultJson.navigation?.nextPage) {
|
|
234
|
+
pageStart++;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
isFinalPage = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
async getSMSData({ extensionId = '~', token, timeFrom, timeTo }) {
|
|
243
|
+
let pageStart = 1;
|
|
244
|
+
let isFinalPage = false;
|
|
245
|
+
let smsLogResponse = null;
|
|
246
|
+
let result = { records: [] };
|
|
247
|
+
while (!isFinalPage) {
|
|
248
|
+
smsLogResponse = await this.request({
|
|
249
|
+
method: 'GET',
|
|
250
|
+
path: `/restapi/v1.0/account/~/extension/${extensionId}/message-store?dateFrom=${timeFrom}&dateTo=${timeTo}&page=${pageStart}&perPage=100`,
|
|
251
|
+
}, token);
|
|
252
|
+
const resultJson = await smsLogResponse.json();
|
|
253
|
+
result.records.push(...resultJson.records);
|
|
254
|
+
if (resultJson.navigation?.nextPage) {
|
|
255
|
+
pageStart++;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
isFinalPage = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
exports.RingCentral = RingCentral;
|
|
266
|
+
exports.isRefreshTokenValid = isRefreshTokenValid;
|
|
267
267
|
exports.isAccessTokenValid = isAccessTokenValid;
|
package/lib/s3ErrorLogReport.js
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
// packages/core/lib/s3ErrorReport.js
|
|
2
|
-
const { S3Client, PutObjectCommand, GetObjectCommand, CreateBucketCommand, HeadBucketCommand } = require('@aws-sdk/client-s3');
|
|
3
|
-
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
|
4
|
-
const shortid = require('shortid');
|
|
5
|
-
|
|
6
|
-
const BUCKET_NAME = process.env.ERROR_REPORT_S3_BUCKET || 'error-reports-bucket';
|
|
7
|
-
const PRESIGN_EXPIRY = 300; // 5 minutes
|
|
8
|
-
const IS_LOCAL = !process.env.ERROR_REPORT_S3_BUCKET;
|
|
9
|
-
const LOCALSTACK_ENDPOINT = process.env.LOCALSTACK_ENDPOINT || 'http://localhost:9001';
|
|
10
|
-
|
|
11
|
-
const s3Client = new S3Client({
|
|
12
|
-
region: process.env.AWS_REGION || 'us-east-1',
|
|
13
|
-
...(IS_LOCAL && {
|
|
14
|
-
endpoint: LOCALSTACK_ENDPOINT,
|
|
15
|
-
forcePathStyle: true, // Required for LocalStack/MinIO
|
|
16
|
-
credentials: {
|
|
17
|
-
accessKeyId: 'minioadmin',
|
|
18
|
-
secretAccessKey: 'minioadmin'
|
|
19
|
-
}
|
|
20
|
-
})
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Ensure bucket exists (useful for local testing)
|
|
25
|
-
*/
|
|
26
|
-
async function ensureBucketExists() {
|
|
27
|
-
if (!IS_LOCAL) return;
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
await s3Client.send(new HeadBucketCommand({ Bucket: BUCKET_NAME }));
|
|
31
|
-
} catch (err) {
|
|
32
|
-
if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) {
|
|
33
|
-
await s3Client.send(new CreateBucketCommand({ Bucket: BUCKET_NAME }));
|
|
34
|
-
console.log(`[LocalStack] Created bucket: ${BUCKET_NAME}`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Generate presigned URL for upload
|
|
41
|
-
*/
|
|
42
|
-
async function getUploadUrl({ userId, platform, metadata }) {
|
|
43
|
-
await ensureBucketExists();
|
|
44
|
-
|
|
45
|
-
const reportId = shortid.generate();
|
|
46
|
-
const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
47
|
-
const key = `error-reports/${timestamp}/${userId}-${reportId}.json`;
|
|
48
|
-
|
|
49
|
-
const command = new PutObjectCommand({
|
|
50
|
-
Bucket: BUCKET_NAME,
|
|
51
|
-
Key: key,
|
|
52
|
-
ContentType: 'application/json',
|
|
53
|
-
Metadata: {
|
|
54
|
-
'user-id': userId,
|
|
55
|
-
'platform': platform,
|
|
56
|
-
...metadata
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: PRESIGN_EXPIRY });
|
|
61
|
-
|
|
62
|
-
return uploadUrl;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
exports.getUploadUrl = getUploadUrl;
|
|
1
|
+
// packages/core/lib/s3ErrorReport.js
|
|
2
|
+
const { S3Client, PutObjectCommand, GetObjectCommand, CreateBucketCommand, HeadBucketCommand } = require('@aws-sdk/client-s3');
|
|
3
|
+
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
|
4
|
+
const shortid = require('shortid');
|
|
5
|
+
|
|
6
|
+
const BUCKET_NAME = process.env.ERROR_REPORT_S3_BUCKET || 'error-reports-bucket';
|
|
7
|
+
const PRESIGN_EXPIRY = 300; // 5 minutes
|
|
8
|
+
const IS_LOCAL = !process.env.ERROR_REPORT_S3_BUCKET;
|
|
9
|
+
const LOCALSTACK_ENDPOINT = process.env.LOCALSTACK_ENDPOINT || 'http://localhost:9001';
|
|
10
|
+
|
|
11
|
+
const s3Client = new S3Client({
|
|
12
|
+
region: process.env.AWS_REGION || 'us-east-1',
|
|
13
|
+
...(IS_LOCAL && {
|
|
14
|
+
endpoint: LOCALSTACK_ENDPOINT,
|
|
15
|
+
forcePathStyle: true, // Required for LocalStack/MinIO
|
|
16
|
+
credentials: {
|
|
17
|
+
accessKeyId: 'minioadmin',
|
|
18
|
+
secretAccessKey: 'minioadmin'
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ensure bucket exists (useful for local testing)
|
|
25
|
+
*/
|
|
26
|
+
async function ensureBucketExists() {
|
|
27
|
+
if (!IS_LOCAL) return;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await s3Client.send(new HeadBucketCommand({ Bucket: BUCKET_NAME }));
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) {
|
|
33
|
+
await s3Client.send(new CreateBucketCommand({ Bucket: BUCKET_NAME }));
|
|
34
|
+
console.log(`[LocalStack] Created bucket: ${BUCKET_NAME}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate presigned URL for upload
|
|
41
|
+
*/
|
|
42
|
+
async function getUploadUrl({ userId, platform, metadata }) {
|
|
43
|
+
await ensureBucketExists();
|
|
44
|
+
|
|
45
|
+
const reportId = shortid.generate();
|
|
46
|
+
const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
47
|
+
const key = `error-reports/${timestamp}/${userId}-${reportId}.json`;
|
|
48
|
+
|
|
49
|
+
const command = new PutObjectCommand({
|
|
50
|
+
Bucket: BUCKET_NAME,
|
|
51
|
+
Key: key,
|
|
52
|
+
ContentType: 'application/json',
|
|
53
|
+
Metadata: {
|
|
54
|
+
'user-id': userId,
|
|
55
|
+
'platform': platform,
|
|
56
|
+
...metadata
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: PRESIGN_EXPIRY });
|
|
61
|
+
|
|
62
|
+
return uploadUrl;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
exports.getUploadUrl = getUploadUrl;
|
|
66
66
|
exports.ensureBucketExists = ensureBucketExists;
|