ring-client-api 13.2.1 → 14.0.0-beta.1
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 +10 -8
- package/lib/api.js +57 -64
- 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 +32 -31
- package/lib/location.js +67 -72
- package/lib/refresh-token.js +6 -11
- package/lib/rest-client.d.ts +4 -4
- package/lib/rest-client.js +40 -51
- package/lib/ring-auth-cli.js +2 -4
- package/lib/ring-camera.d.ts +26 -23
- package/lib/ring-camera.js +64 -61
- package/lib/ring-chime.d.ts +7 -7
- package/lib/ring-chime.js +14 -18
- package/lib/ring-device-data-cli.js +2 -4
- package/lib/ring-device.d.ts +7 -7
- package/lib/ring-device.js +16 -20
- package/lib/ring-intercom.d.ts +8 -8
- package/lib/ring-intercom.js +23 -27
- package/lib/ring-types.d.ts +142 -133
- package/lib/ring-types.js +137 -153
- 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 +3 -3
- package/lib/streaming/simple-webrtc-session.js +3 -7
- package/lib/streaming/streaming-messages.d.ts +6 -5
- package/lib/streaming/streaming-messages.js +6 -9
- package/lib/streaming/streaming-session.d.ts +5 -5
- package/lib/streaming/streaming-session.js +34 -38
- package/lib/streaming/webrtc-connection.d.ts +5 -5
- package/lib/streaming/webrtc-connection.js +39 -43
- package/lib/subscribed.d.ts +1 -1
- package/lib/subscribed.js +1 -5
- package/lib/test/rest-client.spec.js +44 -45
- package/lib/test/ring-camera.spec.js +12 -13
- package/lib/util.js +33 -54
- package/package.json +17 -19
package/lib/location.js
CHANGED
|
@@ -1,34 +1,25 @@
|
|
|
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
|
|
18
|
-
locationDetails;
|
|
19
|
-
cameras;
|
|
20
|
-
chimes;
|
|
21
|
-
intercoms;
|
|
22
|
-
options;
|
|
23
|
-
restClient;
|
|
14
|
+
export class Location extends Subscribed {
|
|
24
15
|
seq = 1;
|
|
25
|
-
onMessage = new
|
|
26
|
-
onDataUpdate = new
|
|
27
|
-
onDeviceDataUpdate = this.onDataUpdate.pipe(
|
|
16
|
+
onMessage = new Subject();
|
|
17
|
+
onDataUpdate = new Subject();
|
|
18
|
+
onDeviceDataUpdate = this.onDataUpdate.pipe(filter((message) => {
|
|
28
19
|
return message.datatype === 'DeviceInfoDocType' && Boolean(message.body);
|
|
29
|
-
}),
|
|
30
|
-
onDeviceList = this.onMessage.pipe(
|
|
31
|
-
onDevices = this.onDeviceList.pipe(
|
|
20
|
+
}), concatMap((message) => message.body), map(flattenDeviceData));
|
|
21
|
+
onDeviceList = this.onMessage.pipe(filter((m) => m.msg === deviceListMessageType));
|
|
22
|
+
onDevices = this.onDeviceList.pipe(scan((devices, { body: deviceList, src }) => {
|
|
32
23
|
if (!deviceList) {
|
|
33
24
|
return devices;
|
|
34
25
|
}
|
|
@@ -41,16 +32,16 @@ class Location extends subscribed_1.Subscribed {
|
|
|
41
32
|
existingDevice.updateData(flatData);
|
|
42
33
|
return updatedDevices;
|
|
43
34
|
}
|
|
44
|
-
return [...updatedDevices, new
|
|
35
|
+
return [...updatedDevices, new RingDevice(flatData, this, src)];
|
|
45
36
|
}, devices);
|
|
46
|
-
}, []),
|
|
37
|
+
}, []), distinctUntilChanged((a, b) => a.length === b.length), filter(() => {
|
|
47
38
|
return Boolean(this.assets &&
|
|
48
39
|
this.assets.every((asset) => this.receivedAssetDeviceLists.includes(asset.uuid)));
|
|
49
|
-
}),
|
|
50
|
-
onSessionInfo = this.onDataUpdate.pipe(
|
|
51
|
-
onConnected = new
|
|
52
|
-
onLocationMode = new
|
|
53
|
-
onLocationModeRequested = new
|
|
40
|
+
}), shareReplay(1));
|
|
41
|
+
onSessionInfo = this.onDataUpdate.pipe(filter((m) => m.msg === 'SessionInfo'), map((m) => m.body));
|
|
42
|
+
onConnected = new BehaviorSubject(false);
|
|
43
|
+
onLocationMode = new ReplaySubject(1);
|
|
44
|
+
onLocationModeRequested = new Subject();
|
|
54
45
|
reconnecting = false;
|
|
55
46
|
disconnected = false;
|
|
56
47
|
connectionPromise;
|
|
@@ -60,6 +51,12 @@ class Location extends subscribed_1.Subscribed {
|
|
|
60
51
|
offlineAssets = [];
|
|
61
52
|
hasHubs;
|
|
62
53
|
hasAlarmBaseStation;
|
|
54
|
+
locationDetails;
|
|
55
|
+
cameras;
|
|
56
|
+
chimes;
|
|
57
|
+
intercoms;
|
|
58
|
+
options;
|
|
59
|
+
restClient;
|
|
63
60
|
constructor(locationDetails, cameras, chimes, intercoms, options, restClient) {
|
|
64
61
|
super();
|
|
65
62
|
this.locationDetails = locationDetails;
|
|
@@ -83,23 +80,22 @@ class Location extends subscribed_1.Subscribed {
|
|
|
83
80
|
}
|
|
84
81
|
if (connectionStatus === 'online') {
|
|
85
82
|
if (assetWasOffline) {
|
|
86
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
87
83
|
this.requestList(deviceListMessageType, assetUuid).catch(() => { });
|
|
88
84
|
this.offlineAssets = this.offlineAssets.filter((id) => id !== assetUuid);
|
|
89
|
-
|
|
85
|
+
logInfo(`Ring ${asset.kind} ${assetUuid} has come back online`);
|
|
90
86
|
}
|
|
91
87
|
}
|
|
92
88
|
else if (!assetWasOffline) {
|
|
93
|
-
|
|
89
|
+
logError(`Ring ${asset.kind} ${assetUuid} is offline or on cellular backup. Waiting for status to change`);
|
|
94
90
|
this.offlineAssets.push(assetUuid);
|
|
95
91
|
}
|
|
96
92
|
});
|
|
97
93
|
}));
|
|
98
94
|
if (!options.hasAlarmBaseStation && options.locationModePollingSeconds) {
|
|
99
|
-
this.addSubscriptions(
|
|
100
|
-
.pipe(
|
|
95
|
+
this.addSubscriptions(merge(this.onLocationModeRequested, this.onLocationMode)
|
|
96
|
+
.pipe(debounceTime(options.locationModePollingSeconds * 1000))
|
|
101
97
|
.subscribe(() => this.getLocationMode()));
|
|
102
|
-
this.getLocationMode().catch(
|
|
98
|
+
this.getLocationMode().catch(logError);
|
|
103
99
|
}
|
|
104
100
|
}
|
|
105
101
|
get id() {
|
|
@@ -115,39 +111,39 @@ class Location extends subscribed_1.Subscribed {
|
|
|
115
111
|
if (this.disconnected) {
|
|
116
112
|
return Promise.resolve({ disconnected: true });
|
|
117
113
|
}
|
|
118
|
-
|
|
114
|
+
logInfo('Creating location socket.io connection - ' + this.name);
|
|
119
115
|
if (process.version.startsWith('v15.')) {
|
|
120
|
-
|
|
116
|
+
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
117
|
}
|
|
122
118
|
const { assets, ticket, host } = await this.restClient.request({
|
|
123
|
-
url:
|
|
124
|
-
}), supportedAssets = assets.filter(
|
|
119
|
+
url: appApi('clap/tickets?locationID=' + this.id),
|
|
120
|
+
}), supportedAssets = assets.filter(isWebSocketSupportedAsset);
|
|
125
121
|
this.assets = supportedAssets;
|
|
126
122
|
this.receivedAssetDeviceLists.length = 0;
|
|
127
123
|
this.offlineAssets.length = 0;
|
|
128
124
|
if (!supportedAssets.length) {
|
|
129
125
|
const errorMessage = `No assets (alarm hubs or beam bridges) found for location ${this.name} - ${this.id}`;
|
|
130
|
-
|
|
126
|
+
logError(errorMessage);
|
|
131
127
|
throw new Error(errorMessage);
|
|
132
128
|
}
|
|
133
|
-
const connection = (
|
|
129
|
+
const connection = connectSocketIo(`wss://${host}/?authcode=${ticket}&ack=false&EIO=3`, { transports: ['websocket'] }), reconnect = () => {
|
|
134
130
|
if (this.reconnecting && this.connectionPromise) {
|
|
135
131
|
return this.connectionPromise;
|
|
136
132
|
}
|
|
137
133
|
this.onConnected.next(false);
|
|
138
134
|
if (!this.disconnected) {
|
|
139
|
-
|
|
135
|
+
logInfo('Reconnecting location socket.io connection');
|
|
140
136
|
}
|
|
141
137
|
this.reconnecting = true;
|
|
142
138
|
connection.close();
|
|
143
|
-
return (this.connectionPromise =
|
|
139
|
+
return (this.connectionPromise = delay(1000).then(() => {
|
|
144
140
|
return this.createConnection();
|
|
145
141
|
}));
|
|
146
142
|
};
|
|
147
143
|
this.reconnecting = false;
|
|
148
144
|
connection.on('DataUpdate', (message) => {
|
|
149
145
|
if (message.datatype === 'HubDisconnectionEventType') {
|
|
150
|
-
|
|
146
|
+
logInfo('Location connection told to reconnect');
|
|
151
147
|
return reconnect();
|
|
152
148
|
}
|
|
153
149
|
this.onDataUpdate.next(message);
|
|
@@ -159,7 +155,7 @@ class Location extends subscribed_1.Subscribed {
|
|
|
159
155
|
connection.once('connect', () => {
|
|
160
156
|
resolve(connection);
|
|
161
157
|
this.onConnected.next(true);
|
|
162
|
-
|
|
158
|
+
logInfo('Ring connected to socket.io server');
|
|
163
159
|
assets.forEach((asset) => this.requestList(deviceListMessageType, asset.uuid));
|
|
164
160
|
});
|
|
165
161
|
connection.once('error', reject);
|
|
@@ -184,7 +180,7 @@ class Location extends subscribed_1.Subscribed {
|
|
|
184
180
|
securityPanel.sendCommand(commandType, data);
|
|
185
181
|
}
|
|
186
182
|
async setAlarmMode(alarmMode, bypassSensorZids) {
|
|
187
|
-
const securityPanel = await this.getSecurityPanel(), updatedDataPromise =
|
|
183
|
+
const securityPanel = await this.getSecurityPanel(), updatedDataPromise = firstValueFrom(securityPanel.onData.pipe(skip(1)));
|
|
188
184
|
await this.sendCommandToSecurityPanel('security-panel.switch-mode', {
|
|
189
185
|
mode: alarmMode,
|
|
190
186
|
bypass: bypassSensorZids,
|
|
@@ -217,7 +213,7 @@ class Location extends subscribed_1.Subscribed {
|
|
|
217
213
|
});
|
|
218
214
|
}
|
|
219
215
|
getNextMessageOfType(type, src) {
|
|
220
|
-
return
|
|
216
|
+
return firstValueFrom(this.onMessage.pipe(filter((m) => m.msg === type && m.src === src), map((m) => m.body)));
|
|
221
217
|
}
|
|
222
218
|
requestList(listType, assetId) {
|
|
223
219
|
return this.sendMessage({ msg: listType, dst: assetId });
|
|
@@ -233,7 +229,7 @@ class Location extends subscribed_1.Subscribed {
|
|
|
233
229
|
if (!this.connectionPromise) {
|
|
234
230
|
await this.getConnection();
|
|
235
231
|
}
|
|
236
|
-
return
|
|
232
|
+
return firstValueFrom(this.onDevices);
|
|
237
233
|
}
|
|
238
234
|
getRoomList(assetId) {
|
|
239
235
|
return this.getList('RoomGetList', assetId);
|
|
@@ -243,7 +239,7 @@ class Location extends subscribed_1.Subscribed {
|
|
|
243
239
|
return this.securityPanel;
|
|
244
240
|
}
|
|
245
241
|
const devices = await this.getDevices(), securityPanel = devices.find((device) => {
|
|
246
|
-
return device.data.deviceType ===
|
|
242
|
+
return device.data.deviceType === RingDeviceType.SecurityPanel;
|
|
247
243
|
});
|
|
248
244
|
if (!securityPanel) {
|
|
249
245
|
throw new Error(`Could not find a security panel for location ${this.name} - ${this.id}`);
|
|
@@ -262,7 +258,7 @@ class Location extends subscribed_1.Subscribed {
|
|
|
262
258
|
getHistory(options = {}) {
|
|
263
259
|
options.maxLevel = options.maxLevel || 50;
|
|
264
260
|
return this.restClient.request({
|
|
265
|
-
url:
|
|
261
|
+
url: appApi(`rs/history${getSearchQueryString({
|
|
266
262
|
accountId: this.id,
|
|
267
263
|
...options,
|
|
268
264
|
})}`),
|
|
@@ -270,22 +266,22 @@ class Location extends subscribed_1.Subscribed {
|
|
|
270
266
|
}
|
|
271
267
|
getCameraEvents(options = {}) {
|
|
272
268
|
return this.restClient.request({
|
|
273
|
-
url:
|
|
269
|
+
url: clientApi(`locations/${this.id}/events${getSearchQueryString(options)}`),
|
|
274
270
|
});
|
|
275
271
|
}
|
|
276
272
|
getAccountMonitoringStatus() {
|
|
277
273
|
return this.restClient.request({
|
|
278
|
-
url:
|
|
274
|
+
url: appApi('rs/monitoring/accounts/' + this.id),
|
|
279
275
|
});
|
|
280
276
|
}
|
|
281
277
|
triggerAlarm(signalType) {
|
|
282
|
-
const now = Date.now(), alarmSessionUuid =
|
|
278
|
+
const now = Date.now(), alarmSessionUuid = generateUuid(), baseStationAsset = this.assets && this.assets.find((x) => x.kind === 'base_station_v1');
|
|
283
279
|
if (!baseStationAsset) {
|
|
284
280
|
throw new Error('Cannot dispatch panic events without an alarm base station');
|
|
285
281
|
}
|
|
286
282
|
return this.restClient.request({
|
|
287
283
|
method: 'POST',
|
|
288
|
-
url:
|
|
284
|
+
url: appApi(`rs/monitoring/accounts/${this.id}/assets/${baseStationAsset.uuid}/userAlarm`),
|
|
289
285
|
json: {
|
|
290
286
|
alarmSessionUuid,
|
|
291
287
|
currentTsMs: now,
|
|
@@ -295,16 +291,16 @@ class Location extends subscribed_1.Subscribed {
|
|
|
295
291
|
});
|
|
296
292
|
}
|
|
297
293
|
triggerBurglarAlarm() {
|
|
298
|
-
return this.triggerAlarm(
|
|
294
|
+
return this.triggerAlarm(DispatchSignalType.Burglar);
|
|
299
295
|
}
|
|
300
296
|
triggerFireAlarm() {
|
|
301
|
-
return this.triggerAlarm(
|
|
297
|
+
return this.triggerAlarm(DispatchSignalType.Fire);
|
|
302
298
|
}
|
|
303
299
|
async getLocationMode() {
|
|
304
300
|
this.onLocationModeRequested.next(null);
|
|
305
301
|
const response = await this.restClient.request({
|
|
306
302
|
method: 'GET',
|
|
307
|
-
url:
|
|
303
|
+
url: appApi(`mode/location/${this.id}`),
|
|
308
304
|
});
|
|
309
305
|
this.onLocationMode.next(response.mode);
|
|
310
306
|
return response;
|
|
@@ -312,7 +308,7 @@ class Location extends subscribed_1.Subscribed {
|
|
|
312
308
|
async setLocationMode(mode) {
|
|
313
309
|
const response = await this.restClient.request({
|
|
314
310
|
method: 'POST',
|
|
315
|
-
url:
|
|
311
|
+
url: appApi(`mode/location/${this.id}`),
|
|
316
312
|
json: { mode },
|
|
317
313
|
});
|
|
318
314
|
this.onLocationMode.next(response.mode);
|
|
@@ -321,14 +317,14 @@ class Location extends subscribed_1.Subscribed {
|
|
|
321
317
|
async disableLocationModes() {
|
|
322
318
|
await this.restClient.request({
|
|
323
319
|
method: 'DELETE',
|
|
324
|
-
url:
|
|
320
|
+
url: appApi(`mode/location/${this.id}/settings`),
|
|
325
321
|
});
|
|
326
322
|
this.onLocationMode.next('disabled');
|
|
327
323
|
}
|
|
328
324
|
async enableLocationModes() {
|
|
329
325
|
const response = await this.restClient.request({
|
|
330
326
|
method: 'POST',
|
|
331
|
-
url:
|
|
327
|
+
url: appApi(`mode/location/${this.id}/settings/setup`),
|
|
332
328
|
});
|
|
333
329
|
await this.getLocationMode();
|
|
334
330
|
return response;
|
|
@@ -336,26 +332,26 @@ class Location extends subscribed_1.Subscribed {
|
|
|
336
332
|
getLocationModeSettings() {
|
|
337
333
|
return this.restClient.request({
|
|
338
334
|
method: 'GET',
|
|
339
|
-
url:
|
|
335
|
+
url: appApi(`mode/location/${this.id}/settings`),
|
|
340
336
|
});
|
|
341
337
|
}
|
|
342
338
|
setLocationModeSettings(settings) {
|
|
343
339
|
return this.restClient.request({
|
|
344
340
|
method: 'POST',
|
|
345
|
-
url:
|
|
341
|
+
url: appApi(`mode/location/${this.id}/settings`),
|
|
346
342
|
json: settings,
|
|
347
343
|
});
|
|
348
344
|
}
|
|
349
345
|
getLocationModeSharing() {
|
|
350
346
|
return this.restClient.request({
|
|
351
347
|
method: 'GET',
|
|
352
|
-
url:
|
|
348
|
+
url: appApi(`mode/location/${this.id}/sharing`),
|
|
353
349
|
});
|
|
354
350
|
}
|
|
355
351
|
setLocationModeSharing(sharedUsersEnabled) {
|
|
356
352
|
return this.restClient.request({
|
|
357
353
|
method: 'POST',
|
|
358
|
-
url:
|
|
354
|
+
url: appApi(`mode/location/${this.id}/sharing`),
|
|
359
355
|
json: { sharedUsersEnabled },
|
|
360
356
|
});
|
|
361
357
|
}
|
|
@@ -364,8 +360,8 @@ class Location extends subscribed_1.Subscribed {
|
|
|
364
360
|
return false;
|
|
365
361
|
}
|
|
366
362
|
const modeResponse = await this.getLocationMode(), { mode, readOnly } = modeResponse;
|
|
367
|
-
|
|
368
|
-
return !readOnly && !
|
|
363
|
+
logDebug('Location Mode: ' + JSON.stringify(modeResponse));
|
|
364
|
+
return !readOnly && !disabledLocationModes.includes(mode);
|
|
369
365
|
}
|
|
370
366
|
disconnect() {
|
|
371
367
|
this.disconnected = true;
|
|
@@ -375,12 +371,11 @@ class Location extends subscribed_1.Subscribed {
|
|
|
375
371
|
.then((devices) => {
|
|
376
372
|
devices.forEach((device) => device.disconnect());
|
|
377
373
|
})
|
|
378
|
-
.catch(
|
|
374
|
+
.catch(logError);
|
|
379
375
|
if (this.connectionPromise) {
|
|
380
376
|
this.connectionPromise
|
|
381
377
|
.then((connection) => connection.close())
|
|
382
|
-
.catch(
|
|
378
|
+
.catch(logError);
|
|
383
379
|
}
|
|
384
380
|
}
|
|
385
381
|
}
|
|
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,11 +21,10 @@ 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');
|
|
32
28
|
console.log(`"refreshToken": "${refreshToken}"`);
|
|
33
29
|
}
|
|
34
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
35
30
|
process.on('unhandledRejection', () => { });
|
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';
|
|
@@ -29,7 +29,6 @@ export interface SessionOptions {
|
|
|
29
29
|
controlCenterDisplayName?: string;
|
|
30
30
|
}
|
|
31
31
|
export declare class RingRestClient {
|
|
32
|
-
private authOptions;
|
|
33
32
|
refreshToken: string | undefined;
|
|
34
33
|
private authConfig;
|
|
35
34
|
private hardwareIdPromise;
|
|
@@ -44,11 +43,12 @@ export declare class RingRestClient {
|
|
|
44
43
|
oldRefreshToken?: string;
|
|
45
44
|
newRefreshToken: string;
|
|
46
45
|
}>;
|
|
47
|
-
onSession: ReplaySubject<import("./ring-types").ProfileResponse>;
|
|
46
|
+
onSession: ReplaySubject<import("./ring-types.ts").ProfileResponse>;
|
|
48
47
|
readonly baseSessionMetadata: {
|
|
49
48
|
api_version: number;
|
|
50
49
|
device_model: string;
|
|
51
50
|
};
|
|
51
|
+
private authOptions;
|
|
52
52
|
constructor(authOptions: (EmailAuth | RefreshTokenAuth) & SessionOptions);
|
|
53
53
|
private getGrantData;
|
|
54
54
|
getAuth(twoFactorAuthCode?: string): Promise<AuthTokenResponse>;
|
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,8 +141,7 @@ function parseAuthConfig(rawRefreshToken) {
|
|
|
151
141
|
};
|
|
152
142
|
}
|
|
153
143
|
}
|
|
154
|
-
class RingRestClient {
|
|
155
|
-
authOptions;
|
|
144
|
+
export class RingRestClient {
|
|
156
145
|
refreshToken;
|
|
157
146
|
authConfig;
|
|
158
147
|
hardwareIdPromise;
|
|
@@ -184,16 +173,17 @@ class RingRestClient {
|
|
|
184
173
|
sessionPromise = undefined;
|
|
185
174
|
using2fa = false;
|
|
186
175
|
promptFor2fa;
|
|
187
|
-
onRefreshTokenUpdated = new
|
|
188
|
-
onSession = new
|
|
176
|
+
onRefreshTokenUpdated = new ReplaySubject(1);
|
|
177
|
+
onSession = new ReplaySubject(1);
|
|
189
178
|
baseSessionMetadata;
|
|
179
|
+
authOptions;
|
|
190
180
|
constructor(authOptions) {
|
|
191
181
|
this.authOptions = authOptions;
|
|
192
182
|
this.refreshToken =
|
|
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);
|