ring-client-api 13.2.1-beta.1 → 14.0.0-beta.0
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/lib/api.d.ts +9 -7
- package/lib/api.js +56 -63
- package/lib/device-data.js +6 -9
- package/lib/ffmpeg.js +2 -6
- package/lib/index.d.ts +7 -7
- package/lib/index.js +7 -23
- package/lib/location.d.ts +22 -21
- package/lib/location.js +61 -65
- package/lib/refresh-token.js +6 -10
- package/lib/rest-client.d.ts +3 -3
- package/lib/rest-client.js +39 -50
- package/lib/ring-auth-cli.js +2 -4
- package/lib/ring-camera.d.ts +21 -18
- package/lib/ring-camera.js +57 -56
- package/lib/ring-chime.d.ts +5 -5
- package/lib/ring-chime.js +10 -14
- package/lib/ring-device-data-cli.js +2 -4
- package/lib/ring-device.d.ts +4 -4
- package/lib/ring-device.js +12 -16
- package/lib/ring-intercom.d.ts +6 -6
- package/lib/ring-intercom.js +19 -23
- package/lib/ring-types.d.ts +2 -0
- package/lib/ring-types.js +24 -24
- package/lib/streaming/peer-connection.d.ts +5 -3
- package/lib/streaming/peer-connection.js +25 -30
- package/lib/streaming/simple-webrtc-session.d.ts +2 -2
- package/lib/streaming/simple-webrtc-session.js +3 -7
- package/lib/streaming/streaming-messages.js +1 -2
- package/lib/streaming/streaming-session.d.ts +3 -3
- package/lib/streaming/streaming-session.js +34 -38
- package/lib/streaming/webrtc-connection.d.ts +4 -4
- package/lib/streaming/webrtc-connection.js +38 -42
- package/lib/subscribed.d.ts +1 -1
- package/lib/subscribed.js +1 -5
- package/lib/util.js +33 -54
- package/package.json +14 -16
- package/lib/test/rest-client.spec.d.ts +0 -1
- package/lib/test/rest-client.spec.js +0 -242
- package/lib/test/ring-camera.spec.d.ts +0 -1
- package/lib/test/ring-camera.spec.js +0 -37
package/lib/location.js
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const ring_camera_1 = require("./ring-camera");
|
|
11
|
-
const ring_device_1 = require("./ring-device");
|
|
12
|
-
const subscribed_1 = require("./subscribed");
|
|
1
|
+
import { connect as connectSocketIo } from 'socket.io-client';
|
|
2
|
+
import { BehaviorSubject, firstValueFrom, merge, ReplaySubject, Subject, } from 'rxjs';
|
|
3
|
+
import { concatMap, debounceTime, distinctUntilChanged, filter, map, scan, shareReplay, skip, } from 'rxjs/operators';
|
|
4
|
+
import { delay, generateUuid, logDebug, logError, logInfo } from "./util.js";
|
|
5
|
+
import { DispatchSignalType, RingDeviceType, disabledLocationModes, isWebSocketSupportedAsset, } from "./ring-types.js";
|
|
6
|
+
import { appApi, clientApi } from "./rest-client.js";
|
|
7
|
+
import { getSearchQueryString } from "./ring-camera.js";
|
|
8
|
+
import { RingDevice } from "./ring-device.js";
|
|
9
|
+
import { Subscribed } from "./subscribed.js";
|
|
13
10
|
const deviceListMessageType = 'DeviceInfoDocGetList';
|
|
14
11
|
function flattenDeviceData(data) {
|
|
15
12
|
return Object.assign({}, data.general && data.general.v2, data.device && data.device.v1);
|
|
16
13
|
}
|
|
17
|
-
class Location extends
|
|
14
|
+
export class Location extends Subscribed {
|
|
18
15
|
locationDetails;
|
|
19
16
|
cameras;
|
|
20
17
|
chimes;
|
|
@@ -22,13 +19,13 @@ class Location extends subscribed_1.Subscribed {
|
|
|
22
19
|
options;
|
|
23
20
|
restClient;
|
|
24
21
|
seq = 1;
|
|
25
|
-
onMessage = new
|
|
26
|
-
onDataUpdate = new
|
|
27
|
-
onDeviceDataUpdate = this.onDataUpdate.pipe(
|
|
22
|
+
onMessage = new Subject();
|
|
23
|
+
onDataUpdate = new Subject();
|
|
24
|
+
onDeviceDataUpdate = this.onDataUpdate.pipe(filter((message) => {
|
|
28
25
|
return message.datatype === 'DeviceInfoDocType' && Boolean(message.body);
|
|
29
|
-
}),
|
|
30
|
-
onDeviceList = this.onMessage.pipe(
|
|
31
|
-
onDevices = this.onDeviceList.pipe(
|
|
26
|
+
}), concatMap((message) => message.body), map(flattenDeviceData));
|
|
27
|
+
onDeviceList = this.onMessage.pipe(filter((m) => m.msg === deviceListMessageType));
|
|
28
|
+
onDevices = this.onDeviceList.pipe(scan((devices, { body: deviceList, src }) => {
|
|
32
29
|
if (!deviceList) {
|
|
33
30
|
return devices;
|
|
34
31
|
}
|
|
@@ -41,16 +38,16 @@ class Location extends subscribed_1.Subscribed {
|
|
|
41
38
|
existingDevice.updateData(flatData);
|
|
42
39
|
return updatedDevices;
|
|
43
40
|
}
|
|
44
|
-
return [...updatedDevices, new
|
|
41
|
+
return [...updatedDevices, new RingDevice(flatData, this, src)];
|
|
45
42
|
}, devices);
|
|
46
|
-
}, []),
|
|
43
|
+
}, []), distinctUntilChanged((a, b) => a.length === b.length), filter(() => {
|
|
47
44
|
return Boolean(this.assets &&
|
|
48
45
|
this.assets.every((asset) => this.receivedAssetDeviceLists.includes(asset.uuid)));
|
|
49
|
-
}),
|
|
50
|
-
onSessionInfo = this.onDataUpdate.pipe(
|
|
51
|
-
onConnected = new
|
|
52
|
-
onLocationMode = new
|
|
53
|
-
onLocationModeRequested = new
|
|
46
|
+
}), shareReplay(1));
|
|
47
|
+
onSessionInfo = this.onDataUpdate.pipe(filter((m) => m.msg === 'SessionInfo'), map((m) => m.body));
|
|
48
|
+
onConnected = new BehaviorSubject(false);
|
|
49
|
+
onLocationMode = new ReplaySubject(1);
|
|
50
|
+
onLocationModeRequested = new Subject();
|
|
54
51
|
reconnecting = false;
|
|
55
52
|
disconnected = false;
|
|
56
53
|
connectionPromise;
|
|
@@ -86,20 +83,20 @@ class Location extends subscribed_1.Subscribed {
|
|
|
86
83
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
87
84
|
this.requestList(deviceListMessageType, assetUuid).catch(() => { });
|
|
88
85
|
this.offlineAssets = this.offlineAssets.filter((id) => id !== assetUuid);
|
|
89
|
-
|
|
86
|
+
logInfo(`Ring ${asset.kind} ${assetUuid} has come back online`);
|
|
90
87
|
}
|
|
91
88
|
}
|
|
92
89
|
else if (!assetWasOffline) {
|
|
93
|
-
|
|
90
|
+
logError(`Ring ${asset.kind} ${assetUuid} is offline or on cellular backup. Waiting for status to change`);
|
|
94
91
|
this.offlineAssets.push(assetUuid);
|
|
95
92
|
}
|
|
96
93
|
});
|
|
97
94
|
}));
|
|
98
95
|
if (!options.hasAlarmBaseStation && options.locationModePollingSeconds) {
|
|
99
|
-
this.addSubscriptions(
|
|
100
|
-
.pipe(
|
|
96
|
+
this.addSubscriptions(merge(this.onLocationModeRequested, this.onLocationMode)
|
|
97
|
+
.pipe(debounceTime(options.locationModePollingSeconds * 1000))
|
|
101
98
|
.subscribe(() => this.getLocationMode()));
|
|
102
|
-
this.getLocationMode().catch(
|
|
99
|
+
this.getLocationMode().catch(logError);
|
|
103
100
|
}
|
|
104
101
|
}
|
|
105
102
|
get id() {
|
|
@@ -115,39 +112,39 @@ class Location extends subscribed_1.Subscribed {
|
|
|
115
112
|
if (this.disconnected) {
|
|
116
113
|
return Promise.resolve({ disconnected: true });
|
|
117
114
|
}
|
|
118
|
-
|
|
115
|
+
logInfo('Creating location socket.io connection - ' + this.name);
|
|
119
116
|
if (process.version.startsWith('v15.')) {
|
|
120
|
-
|
|
117
|
+
logError('Node 15 is not currently supported by the Ring client. Please install the latest Node 14 instead. May not be able to fetch devices from Ring Alarm and Smart Lighting Hubs on this version of node.');
|
|
121
118
|
}
|
|
122
119
|
const { assets, ticket, host } = await this.restClient.request({
|
|
123
|
-
url:
|
|
124
|
-
}), supportedAssets = assets.filter(
|
|
120
|
+
url: appApi('clap/tickets?locationID=' + this.id),
|
|
121
|
+
}), supportedAssets = assets.filter(isWebSocketSupportedAsset);
|
|
125
122
|
this.assets = supportedAssets;
|
|
126
123
|
this.receivedAssetDeviceLists.length = 0;
|
|
127
124
|
this.offlineAssets.length = 0;
|
|
128
125
|
if (!supportedAssets.length) {
|
|
129
126
|
const errorMessage = `No assets (alarm hubs or beam bridges) found for location ${this.name} - ${this.id}`;
|
|
130
|
-
|
|
127
|
+
logError(errorMessage);
|
|
131
128
|
throw new Error(errorMessage);
|
|
132
129
|
}
|
|
133
|
-
const connection = (
|
|
130
|
+
const connection = connectSocketIo(`wss://${host}/?authcode=${ticket}&ack=false&EIO=3`, { transports: ['websocket'] }), reconnect = () => {
|
|
134
131
|
if (this.reconnecting && this.connectionPromise) {
|
|
135
132
|
return this.connectionPromise;
|
|
136
133
|
}
|
|
137
134
|
this.onConnected.next(false);
|
|
138
135
|
if (!this.disconnected) {
|
|
139
|
-
|
|
136
|
+
logInfo('Reconnecting location socket.io connection');
|
|
140
137
|
}
|
|
141
138
|
this.reconnecting = true;
|
|
142
139
|
connection.close();
|
|
143
|
-
return (this.connectionPromise =
|
|
140
|
+
return (this.connectionPromise = delay(1000).then(() => {
|
|
144
141
|
return this.createConnection();
|
|
145
142
|
}));
|
|
146
143
|
};
|
|
147
144
|
this.reconnecting = false;
|
|
148
145
|
connection.on('DataUpdate', (message) => {
|
|
149
146
|
if (message.datatype === 'HubDisconnectionEventType') {
|
|
150
|
-
|
|
147
|
+
logInfo('Location connection told to reconnect');
|
|
151
148
|
return reconnect();
|
|
152
149
|
}
|
|
153
150
|
this.onDataUpdate.next(message);
|
|
@@ -159,7 +156,7 @@ class Location extends subscribed_1.Subscribed {
|
|
|
159
156
|
connection.once('connect', () => {
|
|
160
157
|
resolve(connection);
|
|
161
158
|
this.onConnected.next(true);
|
|
162
|
-
|
|
159
|
+
logInfo('Ring connected to socket.io server');
|
|
163
160
|
assets.forEach((asset) => this.requestList(deviceListMessageType, asset.uuid));
|
|
164
161
|
});
|
|
165
162
|
connection.once('error', reject);
|
|
@@ -184,7 +181,7 @@ class Location extends subscribed_1.Subscribed {
|
|
|
184
181
|
securityPanel.sendCommand(commandType, data);
|
|
185
182
|
}
|
|
186
183
|
async setAlarmMode(alarmMode, bypassSensorZids) {
|
|
187
|
-
const securityPanel = await this.getSecurityPanel(), updatedDataPromise =
|
|
184
|
+
const securityPanel = await this.getSecurityPanel(), updatedDataPromise = firstValueFrom(securityPanel.onData.pipe(skip(1)));
|
|
188
185
|
await this.sendCommandToSecurityPanel('security-panel.switch-mode', {
|
|
189
186
|
mode: alarmMode,
|
|
190
187
|
bypass: bypassSensorZids,
|
|
@@ -217,7 +214,7 @@ class Location extends subscribed_1.Subscribed {
|
|
|
217
214
|
});
|
|
218
215
|
}
|
|
219
216
|
getNextMessageOfType(type, src) {
|
|
220
|
-
return
|
|
217
|
+
return firstValueFrom(this.onMessage.pipe(filter((m) => m.msg === type && m.src === src), map((m) => m.body)));
|
|
221
218
|
}
|
|
222
219
|
requestList(listType, assetId) {
|
|
223
220
|
return this.sendMessage({ msg: listType, dst: assetId });
|
|
@@ -233,7 +230,7 @@ class Location extends subscribed_1.Subscribed {
|
|
|
233
230
|
if (!this.connectionPromise) {
|
|
234
231
|
await this.getConnection();
|
|
235
232
|
}
|
|
236
|
-
return
|
|
233
|
+
return firstValueFrom(this.onDevices);
|
|
237
234
|
}
|
|
238
235
|
getRoomList(assetId) {
|
|
239
236
|
return this.getList('RoomGetList', assetId);
|
|
@@ -243,7 +240,7 @@ class Location extends subscribed_1.Subscribed {
|
|
|
243
240
|
return this.securityPanel;
|
|
244
241
|
}
|
|
245
242
|
const devices = await this.getDevices(), securityPanel = devices.find((device) => {
|
|
246
|
-
return device.data.deviceType ===
|
|
243
|
+
return device.data.deviceType === RingDeviceType.SecurityPanel;
|
|
247
244
|
});
|
|
248
245
|
if (!securityPanel) {
|
|
249
246
|
throw new Error(`Could not find a security panel for location ${this.name} - ${this.id}`);
|
|
@@ -262,7 +259,7 @@ class Location extends subscribed_1.Subscribed {
|
|
|
262
259
|
getHistory(options = {}) {
|
|
263
260
|
options.maxLevel = options.maxLevel || 50;
|
|
264
261
|
return this.restClient.request({
|
|
265
|
-
url:
|
|
262
|
+
url: appApi(`rs/history${getSearchQueryString({
|
|
266
263
|
accountId: this.id,
|
|
267
264
|
...options,
|
|
268
265
|
})}`),
|
|
@@ -270,22 +267,22 @@ class Location extends subscribed_1.Subscribed {
|
|
|
270
267
|
}
|
|
271
268
|
getCameraEvents(options = {}) {
|
|
272
269
|
return this.restClient.request({
|
|
273
|
-
url:
|
|
270
|
+
url: clientApi(`locations/${this.id}/events${getSearchQueryString(options)}`),
|
|
274
271
|
});
|
|
275
272
|
}
|
|
276
273
|
getAccountMonitoringStatus() {
|
|
277
274
|
return this.restClient.request({
|
|
278
|
-
url:
|
|
275
|
+
url: appApi('rs/monitoring/accounts/' + this.id),
|
|
279
276
|
});
|
|
280
277
|
}
|
|
281
278
|
triggerAlarm(signalType) {
|
|
282
|
-
const now = Date.now(), alarmSessionUuid =
|
|
279
|
+
const now = Date.now(), alarmSessionUuid = generateUuid(), baseStationAsset = this.assets && this.assets.find((x) => x.kind === 'base_station_v1');
|
|
283
280
|
if (!baseStationAsset) {
|
|
284
281
|
throw new Error('Cannot dispatch panic events without an alarm base station');
|
|
285
282
|
}
|
|
286
283
|
return this.restClient.request({
|
|
287
284
|
method: 'POST',
|
|
288
|
-
url:
|
|
285
|
+
url: appApi(`rs/monitoring/accounts/${this.id}/assets/${baseStationAsset.uuid}/userAlarm`),
|
|
289
286
|
json: {
|
|
290
287
|
alarmSessionUuid,
|
|
291
288
|
currentTsMs: now,
|
|
@@ -295,16 +292,16 @@ class Location extends subscribed_1.Subscribed {
|
|
|
295
292
|
});
|
|
296
293
|
}
|
|
297
294
|
triggerBurglarAlarm() {
|
|
298
|
-
return this.triggerAlarm(
|
|
295
|
+
return this.triggerAlarm(DispatchSignalType.Burglar);
|
|
299
296
|
}
|
|
300
297
|
triggerFireAlarm() {
|
|
301
|
-
return this.triggerAlarm(
|
|
298
|
+
return this.triggerAlarm(DispatchSignalType.Fire);
|
|
302
299
|
}
|
|
303
300
|
async getLocationMode() {
|
|
304
301
|
this.onLocationModeRequested.next(null);
|
|
305
302
|
const response = await this.restClient.request({
|
|
306
303
|
method: 'GET',
|
|
307
|
-
url:
|
|
304
|
+
url: appApi(`mode/location/${this.id}`),
|
|
308
305
|
});
|
|
309
306
|
this.onLocationMode.next(response.mode);
|
|
310
307
|
return response;
|
|
@@ -312,7 +309,7 @@ class Location extends subscribed_1.Subscribed {
|
|
|
312
309
|
async setLocationMode(mode) {
|
|
313
310
|
const response = await this.restClient.request({
|
|
314
311
|
method: 'POST',
|
|
315
|
-
url:
|
|
312
|
+
url: appApi(`mode/location/${this.id}`),
|
|
316
313
|
json: { mode },
|
|
317
314
|
});
|
|
318
315
|
this.onLocationMode.next(response.mode);
|
|
@@ -321,14 +318,14 @@ class Location extends subscribed_1.Subscribed {
|
|
|
321
318
|
async disableLocationModes() {
|
|
322
319
|
await this.restClient.request({
|
|
323
320
|
method: 'DELETE',
|
|
324
|
-
url:
|
|
321
|
+
url: appApi(`mode/location/${this.id}/settings`),
|
|
325
322
|
});
|
|
326
323
|
this.onLocationMode.next('disabled');
|
|
327
324
|
}
|
|
328
325
|
async enableLocationModes() {
|
|
329
326
|
const response = await this.restClient.request({
|
|
330
327
|
method: 'POST',
|
|
331
|
-
url:
|
|
328
|
+
url: appApi(`mode/location/${this.id}/settings/setup`),
|
|
332
329
|
});
|
|
333
330
|
await this.getLocationMode();
|
|
334
331
|
return response;
|
|
@@ -336,26 +333,26 @@ class Location extends subscribed_1.Subscribed {
|
|
|
336
333
|
getLocationModeSettings() {
|
|
337
334
|
return this.restClient.request({
|
|
338
335
|
method: 'GET',
|
|
339
|
-
url:
|
|
336
|
+
url: appApi(`mode/location/${this.id}/settings`),
|
|
340
337
|
});
|
|
341
338
|
}
|
|
342
339
|
setLocationModeSettings(settings) {
|
|
343
340
|
return this.restClient.request({
|
|
344
341
|
method: 'POST',
|
|
345
|
-
url:
|
|
342
|
+
url: appApi(`mode/location/${this.id}/settings`),
|
|
346
343
|
json: settings,
|
|
347
344
|
});
|
|
348
345
|
}
|
|
349
346
|
getLocationModeSharing() {
|
|
350
347
|
return this.restClient.request({
|
|
351
348
|
method: 'GET',
|
|
352
|
-
url:
|
|
349
|
+
url: appApi(`mode/location/${this.id}/sharing`),
|
|
353
350
|
});
|
|
354
351
|
}
|
|
355
352
|
setLocationModeSharing(sharedUsersEnabled) {
|
|
356
353
|
return this.restClient.request({
|
|
357
354
|
method: 'POST',
|
|
358
|
-
url:
|
|
355
|
+
url: appApi(`mode/location/${this.id}/sharing`),
|
|
359
356
|
json: { sharedUsersEnabled },
|
|
360
357
|
});
|
|
361
358
|
}
|
|
@@ -364,8 +361,8 @@ class Location extends subscribed_1.Subscribed {
|
|
|
364
361
|
return false;
|
|
365
362
|
}
|
|
366
363
|
const modeResponse = await this.getLocationMode(), { mode, readOnly } = modeResponse;
|
|
367
|
-
|
|
368
|
-
return !readOnly && !
|
|
364
|
+
logDebug('Location Mode: ' + JSON.stringify(modeResponse));
|
|
365
|
+
return !readOnly && !disabledLocationModes.includes(mode);
|
|
369
366
|
}
|
|
370
367
|
disconnect() {
|
|
371
368
|
this.disconnected = true;
|
|
@@ -375,12 +372,11 @@ class Location extends subscribed_1.Subscribed {
|
|
|
375
372
|
.then((devices) => {
|
|
376
373
|
devices.forEach((device) => device.disconnect());
|
|
377
374
|
})
|
|
378
|
-
.catch(
|
|
375
|
+
.catch(logError);
|
|
379
376
|
if (this.connectionPromise) {
|
|
380
377
|
this.connectionPromise
|
|
381
378
|
.then((connection) => connection.close())
|
|
382
|
-
.catch(
|
|
379
|
+
.catch(logError);
|
|
383
380
|
}
|
|
384
381
|
}
|
|
385
382
|
}
|
|
386
|
-
exports.Location = Location;
|
package/lib/refresh-token.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.acquireRefreshToken = acquireRefreshToken;
|
|
4
|
-
exports.logRefreshToken = logRefreshToken;
|
|
5
1
|
/* eslint-disable no-console */
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
async function acquireRefreshToken() {
|
|
9
|
-
const email = await
|
|
10
|
-
const code = await
|
|
2
|
+
import { RingRestClient } from "./rest-client.js";
|
|
3
|
+
import { requestInput } from "./util.js";
|
|
4
|
+
export async function acquireRefreshToken() {
|
|
5
|
+
const email = await requestInput('Email: '), password = await requestInput('Password: '), restClient = new RingRestClient({ email, password }), getAuthWith2fa = async () => {
|
|
6
|
+
const code = await requestInput('2fa Code: ');
|
|
11
7
|
try {
|
|
12
8
|
return await restClient.getAuth(code);
|
|
13
9
|
}
|
|
@@ -25,7 +21,7 @@ async function acquireRefreshToken() {
|
|
|
25
21
|
});
|
|
26
22
|
return auth.refresh_token;
|
|
27
23
|
}
|
|
28
|
-
async function logRefreshToken() {
|
|
24
|
+
export async function logRefreshToken() {
|
|
29
25
|
console.log('This CLI will provide you with a refresh token which you can use to configure ring-client-api and homebridge-ring.');
|
|
30
26
|
const refreshToken = await acquireRefreshToken();
|
|
31
27
|
console.log('\nSuccessfully logged in to Ring. Please add the following to your config:\n');
|
package/lib/rest-client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AuthTokenResponse, SessionResponse } from './ring-types';
|
|
1
|
+
import type { AuthTokenResponse, SessionResponse } from './ring-types.ts';
|
|
2
2
|
import { ReplaySubject } from 'rxjs';
|
|
3
|
-
import type { Credentials } from '@eneris/push-receiver/dist/types';
|
|
3
|
+
import type { Credentials } from '@eneris/push-receiver/dist/types.d.js';
|
|
4
4
|
import { Agent } from 'undici';
|
|
5
5
|
interface RequestOptions extends RequestInit {
|
|
6
6
|
responseType?: 'json' | 'buffer';
|
|
@@ -44,7 +44,7 @@ export declare class RingRestClient {
|
|
|
44
44
|
oldRefreshToken?: string;
|
|
45
45
|
newRefreshToken: string;
|
|
46
46
|
}>;
|
|
47
|
-
onSession: ReplaySubject<import("./ring-types").ProfileResponse>;
|
|
47
|
+
onSession: ReplaySubject<import("./ring-types.ts").ProfileResponse>;
|
|
48
48
|
readonly baseSessionMetadata: {
|
|
49
49
|
api_version: number;
|
|
50
50
|
device_model: string;
|
package/lib/rest-client.js
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
exports.RingRestClient = void 0;
|
|
7
|
-
exports.clientApi = clientApi;
|
|
8
|
-
exports.deviceApi = deviceApi;
|
|
9
|
-
exports.commandsApi = commandsApi;
|
|
10
|
-
exports.appApi = appApi;
|
|
11
|
-
const util_1 = require("./util");
|
|
12
|
-
const rxjs_1 = require("rxjs");
|
|
13
|
-
const assert_1 = __importDefault(require("assert"));
|
|
14
|
-
const undici_1 = require("undici");
|
|
15
|
-
const fetchAgent = new undici_1.Agent({
|
|
1
|
+
import { delay, fromBase64, getHardwareId, logDebug, logError, logInfo, stringify, toBase64, } from "./util.js";
|
|
2
|
+
import { ReplaySubject } from 'rxjs';
|
|
3
|
+
import assert from 'assert';
|
|
4
|
+
import { Agent } from 'undici';
|
|
5
|
+
const fetchAgent = new Agent({
|
|
16
6
|
connections: 6,
|
|
17
7
|
pipelining: 1,
|
|
18
8
|
keepAliveTimeout: 115000,
|
|
@@ -27,16 +17,16 @@ const fetchAgent = new undici_1.Agent({
|
|
|
27
17
|
7062: 'UPDATING',
|
|
28
18
|
7063: 'MAINTENANCE',
|
|
29
19
|
}, clientApiBaseUrl = 'https://api.ring.com/clients_api/', deviceApiBaseUrl = 'https://api.ring.com/devices/v1/', commandsApiBaseUrl = 'https://api.ring.com/commands/v1/', appApiBaseUrl = 'https://prd-api-us.prd.rings.solutions/api/v1/', apiVersion = 11;
|
|
30
|
-
function clientApi(path) {
|
|
20
|
+
export function clientApi(path) {
|
|
31
21
|
return clientApiBaseUrl + path;
|
|
32
22
|
}
|
|
33
|
-
function deviceApi(path) {
|
|
23
|
+
export function deviceApi(path) {
|
|
34
24
|
return deviceApiBaseUrl + path;
|
|
35
25
|
}
|
|
36
|
-
function commandsApi(path) {
|
|
26
|
+
export function commandsApi(path) {
|
|
37
27
|
return commandsApiBaseUrl + path;
|
|
38
28
|
}
|
|
39
|
-
function appApi(path) {
|
|
29
|
+
export function appApi(path) {
|
|
40
30
|
return appApiBaseUrl + path;
|
|
41
31
|
}
|
|
42
32
|
async function responseToError(response) {
|
|
@@ -123,13 +113,13 @@ async function requestWithRetry(requestOptions, retryCount = 0) {
|
|
|
123
113
|
let detailedError = `Error: ${e.message}`;
|
|
124
114
|
detailedError += e.cause?.message ? `, Cause: ${e.cause.message}` : '';
|
|
125
115
|
detailedError += e.cause?.code ? `, Code: ${e.cause.code}` : '';
|
|
126
|
-
|
|
116
|
+
logError(`Retry #${retryCount} failed to reach Ring server at ${requestOptions.url}. ${detailedError}. Trying again in 5 seconds...`);
|
|
127
117
|
if (e.message.includes('NGHTTP2_ENHANCE_YOUR_CALM')) {
|
|
128
|
-
|
|
118
|
+
logError(`There is a known issue with your current NodeJS version (${process.version}). Please see https://github.com/dgreif/ring/wiki/NGHTTP2_ENHANCE_YOUR_CALM-Error for details`);
|
|
129
119
|
}
|
|
130
|
-
|
|
120
|
+
logDebug(e);
|
|
131
121
|
}
|
|
132
|
-
await
|
|
122
|
+
await delay(5000);
|
|
133
123
|
return requestWithRetry(requestOptions, retryCount + 1);
|
|
134
124
|
}
|
|
135
125
|
throw e;
|
|
@@ -140,9 +130,9 @@ function parseAuthConfig(rawRefreshToken) {
|
|
|
140
130
|
return;
|
|
141
131
|
}
|
|
142
132
|
try {
|
|
143
|
-
const config = JSON.parse(
|
|
144
|
-
(
|
|
145
|
-
(
|
|
133
|
+
const config = JSON.parse(fromBase64(rawRefreshToken));
|
|
134
|
+
assert(config);
|
|
135
|
+
assert(config.rt);
|
|
146
136
|
return config;
|
|
147
137
|
}
|
|
148
138
|
catch {
|
|
@@ -151,7 +141,7 @@ function parseAuthConfig(rawRefreshToken) {
|
|
|
151
141
|
};
|
|
152
142
|
}
|
|
153
143
|
}
|
|
154
|
-
class RingRestClient {
|
|
144
|
+
export class RingRestClient {
|
|
155
145
|
authOptions;
|
|
156
146
|
refreshToken;
|
|
157
147
|
authConfig;
|
|
@@ -184,8 +174,8 @@ class RingRestClient {
|
|
|
184
174
|
sessionPromise = undefined;
|
|
185
175
|
using2fa = false;
|
|
186
176
|
promptFor2fa;
|
|
187
|
-
onRefreshTokenUpdated = new
|
|
188
|
-
onSession = new
|
|
177
|
+
onRefreshTokenUpdated = new ReplaySubject(1);
|
|
178
|
+
onSession = new ReplaySubject(1);
|
|
189
179
|
baseSessionMetadata;
|
|
190
180
|
constructor(authOptions) {
|
|
191
181
|
this.authOptions = authOptions;
|
|
@@ -193,7 +183,7 @@ class RingRestClient {
|
|
|
193
183
|
'refreshToken' in authOptions ? authOptions.refreshToken : undefined;
|
|
194
184
|
this.authConfig = parseAuthConfig(this.refreshToken);
|
|
195
185
|
this.hardwareIdPromise =
|
|
196
|
-
this.authConfig?.hid ||
|
|
186
|
+
this.authConfig?.hid || getHardwareId(authOptions.systemId);
|
|
197
187
|
this.baseSessionMetadata = {
|
|
198
188
|
api_version: apiVersion,
|
|
199
189
|
device_model: authOptions.controlCenterDisplayName ?? 'ring-client-api',
|
|
@@ -240,7 +230,7 @@ class RingRestClient {
|
|
|
240
230
|
rt: response.refresh_token,
|
|
241
231
|
hid: hardwareId,
|
|
242
232
|
};
|
|
243
|
-
this.refreshToken =
|
|
233
|
+
this.refreshToken = toBase64(JSON.stringify(this.authConfig));
|
|
244
234
|
// Emit an event with the new token
|
|
245
235
|
this.onRefreshTokenUpdated.next({
|
|
246
236
|
oldRefreshToken,
|
|
@@ -257,7 +247,7 @@ class RingRestClient {
|
|
|
257
247
|
// failed request with refresh token
|
|
258
248
|
this.refreshToken = undefined;
|
|
259
249
|
this.authConfig = undefined;
|
|
260
|
-
|
|
250
|
+
logError(requestError);
|
|
261
251
|
return this.getAuth();
|
|
262
252
|
}
|
|
263
253
|
const response = requestError.response || {}, responseData = response.body || {}, responseError = 'error' in responseData && typeof responseData.error === 'string'
|
|
@@ -292,8 +282,8 @@ class RingRestClient {
|
|
|
292
282
|
? 'You have requested too many 2fa codes. Ring limits 2fa to 10 codes within 10 minutes. Please try again in 10 minutes.'
|
|
293
283
|
: `Verify that your ${authTypeMessage} correct.`) +
|
|
294
284
|
` (error: ${responseError})`;
|
|
295
|
-
|
|
296
|
-
|
|
285
|
+
logError(requestError.response || requestError);
|
|
286
|
+
logError(errorMessage);
|
|
297
287
|
throw new Error(errorMessage);
|
|
298
288
|
}
|
|
299
289
|
}
|
|
@@ -330,9 +320,9 @@ class RingRestClient {
|
|
|
330
320
|
const retryAfter = e.response.headers.get('retry-after'), waitSeconds = isNaN(retryAfter)
|
|
331
321
|
? 200
|
|
332
322
|
: Number.parseInt(retryAfter, 10);
|
|
333
|
-
|
|
334
|
-
await
|
|
335
|
-
|
|
323
|
+
logError(`Session response rate limited. Waiting to retry after ${waitSeconds} seconds`);
|
|
324
|
+
await delay((waitSeconds + 1) * 1000);
|
|
325
|
+
logInfo('Retrying session request');
|
|
336
326
|
return this.getSession();
|
|
337
327
|
}
|
|
338
328
|
throw e;
|
|
@@ -355,7 +345,7 @@ class RingRestClient {
|
|
|
355
345
|
}, 12 * 60 * 60 * 1000); // 12 hours
|
|
356
346
|
this.timeouts.push(timeout);
|
|
357
347
|
})
|
|
358
|
-
.catch((e) =>
|
|
348
|
+
.catch((e) => logError(e));
|
|
359
349
|
}
|
|
360
350
|
async request(options) {
|
|
361
351
|
const hardwareId = await this.hardwareIdPromise, url = options.url, initialSessionPromise = this.sessionPromise;
|
|
@@ -380,7 +370,7 @@ class RingRestClient {
|
|
|
380
370
|
}
|
|
381
371
|
if (response.status === 504) {
|
|
382
372
|
// Gateway Timeout. These should be recoverable, but wait a few seconds just to be on the safe side
|
|
383
|
-
await
|
|
373
|
+
await delay(5000);
|
|
384
374
|
return this.request(options);
|
|
385
375
|
}
|
|
386
376
|
if (response.status === 404 &&
|
|
@@ -391,29 +381,29 @@ class RingRestClient {
|
|
|
391
381
|
.filter((x) => x)
|
|
392
382
|
.join(', ');
|
|
393
383
|
if (errorText) {
|
|
394
|
-
|
|
395
|
-
await
|
|
384
|
+
logError(`http request failed. ${url} returned errors: (${errorText}). Trying again in 20 seconds`);
|
|
385
|
+
await delay(20000);
|
|
396
386
|
return this.request(options);
|
|
397
387
|
}
|
|
398
|
-
|
|
388
|
+
logError(`http request failed. ${url} returned unknown errors: (${stringify(errors)}).`);
|
|
399
389
|
}
|
|
400
390
|
if (response.status === 404 && url.startsWith(clientApiBaseUrl)) {
|
|
401
|
-
|
|
391
|
+
logError('404 from endpoint ' + url);
|
|
402
392
|
if (response.body?.error?.includes(hardwareId)) {
|
|
403
|
-
|
|
393
|
+
logError('Session hardware_id not found. Creating a new session and trying again.');
|
|
404
394
|
if (this.sessionPromise === initialSessionPromise) {
|
|
405
395
|
this.refreshSession();
|
|
406
396
|
}
|
|
407
397
|
return this.request(options);
|
|
408
398
|
}
|
|
409
|
-
throw new Error('Not found with response: ' +
|
|
399
|
+
throw new Error('Not found with response: ' + stringify(response.body));
|
|
410
400
|
}
|
|
411
401
|
if (response.status) {
|
|
412
|
-
|
|
402
|
+
logError(`Request to ${url} failed with status ${response.status}. Response body: ${stringify(response.body)}`);
|
|
413
403
|
}
|
|
414
404
|
else if (!options.allowNoResponse) {
|
|
415
|
-
|
|
416
|
-
|
|
405
|
+
logError(`Request to ${url} failed:`);
|
|
406
|
+
logError(e);
|
|
417
407
|
}
|
|
418
408
|
throw e;
|
|
419
409
|
}
|
|
@@ -437,7 +427,7 @@ class RingRestClient {
|
|
|
437
427
|
pnc: credentials,
|
|
438
428
|
};
|
|
439
429
|
// SOMEDAY: refactor the conversion from auth config to refresh token - DRY from above
|
|
440
|
-
const newRefreshToken =
|
|
430
|
+
const newRefreshToken = toBase64(JSON.stringify(this.authConfig));
|
|
441
431
|
if (newRefreshToken === oldRefreshToken) {
|
|
442
432
|
// No change, so we don't need to emit an updated refresh token
|
|
443
433
|
return;
|
|
@@ -450,4 +440,3 @@ class RingRestClient {
|
|
|
450
440
|
});
|
|
451
441
|
}
|
|
452
442
|
}
|
|
453
|
-
exports.RingRestClient = RingRestClient;
|
package/lib/ring-auth-cli.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const refresh_token_1 = require("./refresh-token");
|
|
5
|
-
(0, refresh_token_1.logRefreshToken)().catch((e) => {
|
|
2
|
+
import { logRefreshToken } from "./refresh-token.js";
|
|
3
|
+
logRefreshToken().catch((e) => {
|
|
6
4
|
// eslint-disable-next-line no-console
|
|
7
5
|
console.error(e);
|
|
8
6
|
process.exit(1);
|